SOB: Designing Alby OAuth Connector - Part 1
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
-
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/tokenHere is how we handled authorization in below code snippet:
-
initialize alby client with appropriate scopes
-
generate appropriate URL
-
launch OAuth flow
-
capture tokens
-
store and use tokens
-
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