SOB: Designing Alby OAuth Connector - Part 1

0 sats
0 comments

During the first two weeks of the Summer of Bitcoin, we had a great start in taking steps towards developing tools and standards to attach metadata to Lightning transactions. ๐Ÿš€

We pushed different specifications, proposed changes to lnurl repositories, and received valuable feedback from Fiatjaf. ๐Ÿ’ก Additionally, we developed practical examples, such as implementing metadata in the Alby Boost Button. ๐ŸŒŸ

Building upon this progress, I dedicated most of my time during these two weeks to developing the Alby OAuth Connector! ๐Ÿ”๐Ÿ’ป

What is Connector?

A connector is a crucial component that enables communication and interaction with a Lightning node, facilitating various Lightning operations. Here are some common operations that a connector can perform:

  • ๐Ÿ’ก getInfo: Retrieve information about the Lightning node, including its identity, network addresses, and supported features.

  • ๐Ÿ’ธ keysend: Perform a spontaneous Lightning payment without generating an invoice, allowing for quick and seamless transactions.

  • ๐Ÿงพ makeInvoice: Request the Lightning node to generate a Bolt11 invoice, which includes the payment details and cryptographic information necessary for receiving funds.

  • ๐Ÿ’ณ sendPayment: Initiate Lightning payments by providing the necessary payment details, such as the recipient's Lightning address and the amount to be transferred.

  • ๐Ÿ” signMessage: Utilize the Lightning node's signing capabilities to sign a message, providing cryptographic proof of authenticity or ownership.

These operations represent some of the essential functionalities that a Lightning connector can provide, enabling developers and users to interact with the Lightning network efficiently and securely. โšก

There are various types of connectors depending on the lightning node you use :

  • โšก LND: Lightning Network Daemon by Lightning Labs

  • ๐Ÿ”Œ LNDHub: Extension of LND with multi-user support

  • ๐Ÿ› ๏ธ Kollider: Simple and easy-to-use Lightning node implementation

  • ๐Ÿ’ผ lnbits: Open-source Lightning wallet and server

  • ๐Ÿš€ LNC: Lightning Network Client written in Rust

  • ๐Ÿ”’ Galoy: Lightning node for Bitcoin financial services

  • โšก Eclair: Lightning node implementation by ACINQ

  • ๐Ÿฐ Citadel: Lightning node built on Bitcoin Core

Each implementation has its own features and characteristics. Choose one that suits your needs and preferences.

Why Alby Connector?

Having an Alby connector specifically designed for Alby Accounts, instead of relying on other connector APIs like LNDHub API, can provide greater flexibility and customization.

๐Ÿงฉ๐Ÿ’ก By leveraging Alby's own wallet API, you can optimize the integration, include metadata, and make Lightning transactions smarter for both incoming and outgoing transactions.

๐ŸŒŸโšก Developing a dedicated connector ensures a cohesive and streamlined experience, aligned with Alby's vision. ๐Ÿค๐Ÿ”’ It empowers you to prioritize enhancements and build a tailored solution for Alby Accounts. ๐Ÿ’ช๐Ÿ’ผ

๐Ÿ“… Week 3 Highlights:

Getting Started with Alby Connector

  1. Getting Started with OAuth2 Authorization

    โœจ Every connector needs to authorize users before they can start performing any lightning operations.

    Alby API endpoints are secured with OAuth2 for doing lightning operations so firstly we need to implement Oauth2.0 flow in Chrome extension, we replaced the entire Alby Signup and Login Mechanism with the Alby OAuth2 authorization mechanism. ๐Ÿ”’๐Ÿ”‘

    Alby API provides two different URI for authorization and Token Access

    Authorization URI https://getalby.com/oauth 
    Token URI https://api.getalby.com/oauth/token
    

    Here is how we handled authorization in below code snippet:

    1. initialize alby client with appropriate scopes

    2. generate appropriate URL

    3. launch OAuth flow

    4. capture tokens

    5. store and use tokens

    6. handle edge cases such as access token expiry and refresh token expiry

    private async authorize(): Promise<auth.OAuth2User> { // console.info("this.config.oAuthToken", this.config.oAuthToken); try { const clientId = process.env.ALBY_OAUTH_CLIENT_ID; const clientSecret = process.env.ALBY_OAUTH_CLIENT_SECRET; if (!clientId || !clientSecret) { throw new Error("OAuth client credentials missing"); }

      const redirectURL = browser.identity.getRedirectURL();
      const authClient = new auth.OAuth2User({
        request_options: this._getRequestOptions(),
        client_id: clientId,
        client_secret: clientSecret,
        callback: redirectURL,
        scopes: [
          "invoices:read",
          "account:read",
          "balance:read",
          "invoices:create",
          "invoices:read",
          "payments:send",
          "transactions:read", // for outgoing invoice
        ],
        token: this.config.oAuthToken, // initialize with existing token
      });
    
      if (this.config.oAuthToken) {
        console.info("Requesting new oAuth token");
        try {
          if (authClient.isAccessTokenExpired()) {
            const token = await authClient.refreshAccessToken();
            await this._updateOAuthToken(token.token);
          }
          return authClient;
        } catch (error) {
          // if auth token refresh fails, the refresh token has probably expired or is invalid
          // the user will be asked to re-login
          console.error("Failed to request new auth token", error);
        }
      }
    
      let authUrl = authClient.generateAuthURL({
        code_challenge_method: "S256",
      });
      // TODO: make authorize URL in alby-js-sdk customizable
      if (process.env.ALBY_OAUTH_AUTHORIZE_URL) {
        authUrl = authUrl.replace(
          "https://getalby.com/oauth",
          process.env.ALBY_OAUTH_AUTHORIZE_URL
        );
      }
      authUrl += "&webln=false"; // stop getalby.com login modal launching lnurl auth
      const authResult = await this.launchWebAuthFlow(authUrl);
      const code = new URL(authResult).searchParams.get("code");
      if (!code) {
        throw new Error("Authentication failed: missing authResult");
      }
    
      const token = await authClient.requestAccessToken(code);
      await this._updateOAuthToken(token.token);
      return authClient;
    } catch (error) {
      console.error(error);
      throw error;
    }
    

    }

    async launchWebAuthFlow(authUrl: string) { const authResult = await browser.identity.launchWebAuthFlow({ interactive: true, url: authUrl, });

    return authResult;
    

    }

Case 1: User Login for the first time

The provided code in Alby handles OAuth2 authorization using the alby-js-sdk library. ๐Ÿš€๐Ÿ’ก

It begins by checking OAuth client credentials and launches a web-based authentication flow using the browser identity API. ๐Ÿ”‘๐ŸŒ

The code generates an authorization URL and initiates the authentication flow, enabling users to authenticate and obtain an authorization code. โœจ๐Ÿ”

After acquiring the authorization code, the code requests an access token, updates the OAuth token, and returns the authorized user object for lightning operations. ๐ŸŽฏ๐Ÿ”Œ๐Ÿ”’

In summary, this code simplifies the OAuth2 authorization process in Alby, providing a seamless and secure way for users to authenticate and access lightning functionalities. ๐ŸŒŸ

Case 2: If the token already Exist But Expired

If the access token is expired, we relaunch the webAuth flow to update it, preventing frequent re-login. โ™ป๏ธ๐Ÿ”‘

private async _updateOAuthToken(newToken: Token) {
    console.info("Updating account oauth token", newToken);
    const access_token = newToken.access_token;
    const refresh_token = newToken.refresh_token;
    const expires_at = newToken.expires_at;

    if (access_token && refresh_token && expires_at) {
      this.config.oAuthToken = { access_token, refresh_token, expires_at };
      if (this.account.id) {
        const accounts = state.getState().accounts;
        const password = (await state.getState().password()) as string;

        const configData = decryptData(
          accounts[this.account.id].config,
          password
        );
        configData.oAuthToken = this.config.oAuthToken;
        accounts[this.account.id].config = encryptData(configData, password);
        state.setState({ accounts });
        // make sure we immediately persist the updated accounts
        await state.getState().saveToStorage();
      }
      console.info(
        "Updated account oauth token",
        this.config.oAuthToken,
        "Account ID: " + this.account.id
      );
    } else {
      console.error("Invalid token", newToken);
      throw new Error("Invalid token");
    }
  }
}

By doing so, users can reauthenticate and obtain a fresh access token without interruptions. ๐ŸŒŸโšก๏ธ๐Ÿ”

This approach improves usability and ensures a seamless experience with the connector. ๐Ÿ’ช๐Ÿ”’

Case3: If the refresh Token Expired

we Just relaunch the web auth flow and tell the user to log in again

Once the authorization is done tokens are captured from the callback account is validated, added, and initialized by updating the account list and Alby Account Context State

  async function connectAlby() {
    setLoading(true);
    const name = "Alby";
    const initialAccount = {
      name,
      config: {},
      connector: "alby",
    };

    try {
      const validation = await msg.request("validateAccount", initialAccount);
      if (validation.valid) {
        if (!validation.oAuthToken) {
          throw new Error("No oAuthToken returned");
        }

        const account = {
          ...initialAccount,
          name: (validation.info as { data: WebLNNode }).data.alias,
          config: {
            ...initialAccount.config,
            oAuthToken: validation.oAuthToken,
          },
        };

        const addResult = await msg.request("addAccount", account);
        if (addResult.accountId) {
          await msg.request("selectAccount", {
            id: addResult.accountId,
          });
          if (fromWelcome) {
            navigate("/pin-extension");
          } else {
          
            navigate("/test-connection");
          }
        } else {
          console.error("Failed to add account", addResult);
          throw new Error(addResult.error as string);
        }
      } else {
        console.error("Failed to validate account", validation);
        throw new Error(validation.error as string);
      }
    } catch (e) {
      console.error(e);
      if (e instanceof Error) {
        toast.error(`${tCommon("errors.connection_failed")} (${e.message})`);
      }
    } finally {
      setLoading(false);
    }
  }

Result

<iframe class="remirror-iframe remirror-iframe-youtube" src="https://www.youtube-nocookie.com/embed/4pqmRX392uM?" data-embed-type="youtube" allowfullscreen="true" frameborder="0"></iframe>

B Tech CE || GSoC2021 @CircuitVerse || GSOC'2022 Mentor @CircuitVerse || SOB'2022 @getAlby || Core Team Member @CircuitVerse || Open Source Enthusiast || Building spec to make lightning transactions smart