-
-
Save PSingletary/111ed8d2451a47810ac55b25fb273ab5 to your computer and use it in GitHub Desktop.
Revisions
-
immber revised this gist
Mar 19, 2025 . 1 changed file with 1 addition and 1 deletion.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -135,7 +135,7 @@ Some errors encountered were: 2. Get the user's handle ie `"immber.bsky.social"` 3. Try to pass the handle to `client.authorize()` 1. The `client` sets `state` in the `StateStore` 2. The `client.authorize()` returns the `redirect_url` ie: https://bsky.social/oauth/authorize?client_id=http%3A%2F%2Flocalhost%3Fredirect_uri%3Dhttp%253A%252F%252F127.0.0.1%253A8080%252Foauth%252Fcallback%26scope%3Datproto%2520transition%253Ageneric&request_uri=urn%3Aietf%3Aparams%3Aoauth%3Arequest_uri%3Areq-eb89d03dc33c4de8b86d12a60778fb00 4. User completes authorization and is sent back to the `/callback` route with `URLSearchParams: { iss, state, code )` 5. Pass the URLSearchParams to `client.callback()` 1. The `client` gets `state` from the `StateStore` to compare with `state` that came back in params -
immber revised this gist
Mar 19, 2025 . 1 changed file with 66 additions and 51 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -8,34 +8,37 @@ In this talk I will give a brief overview of how that went, and what you should - [Tales of Adding oAuth "Login with Bluesky" to an OS Comments Tool](#tales-of-adding-oauth-login-with-bluesky-to-an-os-comments-tool) - [An Atmosphere Conf 2025 Lightning Talk](#an-atmosphere-conf-2025-lightning-talk) - [Stage 1: Idea](#stage-1-idea) - [Open Source Background](#open-source-background) - [Existing Auth Strategies](#existing-auth-strategies) - [The "Before" Login Form: Coral w/ Social Logins Enabled](#the-before-login-form-coral-w-social-logins-enabled) - [Why not OIDC?](#why-not-oidc) - [Stage 2: Enthusiastic new branch](#stage-2-enthusiastic-new-branch) - [Initial Assumptions](#initial-assumptions) - [Making Buttons](#making-buttons) - [Stage 3: Panic](#stage-3-panic) - [Uh-oh](#uh-oh) - [Faulty Assumptions](#faulty-assumptions) - [Important differences between Atproto oauth clients vs common oAuth2 clients:](#important-differences-between-atproto-oauth-clients-vs-common-oauth2-clients) - [Stage 4: External validation](#stage-4-external-validation) - [I did two things to validate the idea:](#i-did-two-things-to-validate-the-idea) - [Stage 6: A sample app working locally](#stage-6-a-sample-app-working-locally) - [Stage 7: More Coral Development \& Troubleshooting](#stage-7-more-coral-development--troubleshooting) - [:cherries: Incorporate new integration tests, cherry pick to new branch](#cherries-incorporate-new-integration-tests-cherry-pick-to-new-branch) - [:no\_entry: Errors along the way :no\_entry:](#no_entry-errors-along-the-way-no_entry) - [:heart\_eyes: Authorization Steps When it works :heart\_eyes:](#heart_eyes-authorization-steps-when-it-works-heart_eyes) - [Stage 8: Test and Deploy](#stage-8-test-and-deploy) - [Stage 9: Celebrate \& Reflect](#stage-9-celebrate--reflect) - [Questions that I can answer now](#questions-that-i-can-answer-now) - [What you should know before starting your own version of this project:](#what-you-should-know-before-starting-your-own-version-of-this-project) - [:blue\_heart: Bluesky === Atproto](#blue_heart-bluesky--atproto) - [:blue\_heart: Issue your own ClientID \& Secrets](#blue_heart-issue-your-own-clientid--secrets) - [:blue\_heart: Use atproto's node package](#blue_heart-use-atprotos-node-package) - [:blue\_heart: Encode your URIs](#blue_heart-encode-your-uris) - [:blue\_heart: Use the API \& Syntax npm packages for convenience](#blue_heart-use-the-api--syntax-npm-packages-for-convenience) ## Stage 1: Idea ### Open Source Background @@ -59,34 +62,39 @@ Because of this quote taken directly from `https://atproto.com/specs/oauth` >OAuth 2.0 is traditionally an authorization (authz) system, not an authentication (authn) system, meaning that it is not always a solution for pure account authentication use cases, such as "Signup/Login with XYZ" identity integrations. OpenID Connect (OIDC), which builds on top of OAuth 2.0, is usually the recommended standard for identity authentication. Unfortunately, the current version of OIDC does not enable authentication of atproto identities in a secure and generic way. The atproto profile of OAuth includes a (mandatory) mechanism for account authentication during the authorization flow and can be used for atproto identity authentication use cases. ## Stage 2: Enthusiastic new branch  ### Initial Assumptions Initially I assumed that adding Bluesky would be as simple as reusing Coral's existing `oauth2` client. I thought that I would copy either the FB or Google authenticator class and just make a new Bluesky one. TLDR - that was an incorrect assumption. ### Making Buttons After spending the better part of a week building out `login-button-containers`, and adding the settings interface and updating models to allow users to enable *"Login with Bluesky"* in the multi-tenant `Admin/Config/` form routes, I start to dig into the oauth clients, and realize that... _"none of this is how this is going to work"_ ## Stage 3: Panic ### Uh-oh As I start cloning & copy pasta-ing Coral's FB Authenticator Class, (an extension of an internal `oauth2` abstract class) I realize that I'm not going to be able to use Coral's existing built in `oauth2` client with Atproto's [`oauth-client-node`](https://www.npmjs.com/package/@atproto/oauth-client-node)  ### Faulty Assumptions Already, this is way past the quick and easy copy/pasta job that I initially embarked on. I had thought that creating all of the `login-button-containers` etc was going to be the hard part, and that the Oauth part would be pretty simple. ### Important differences between Atproto oauth clients vs common oAuth2 clients:  1. Because an atproto profile can live anywhere on the internet, not just at `bsky.social` the `redirect-url` (where you send the user) isn't static, it can be any pds host 2. There is no central authority to issue client secrets, so you create and issue your own client ID & secret keys at a `/client-metadata.json` route. ## Stage 4: External validation :red_circle: It was at this point that I decided it was time to externally validate my idea. I needed to even confirm if anyone else in the world besides myself even wanted this integration to exist. @@ -96,7 +104,7 @@ TLDR - that was an incorrect assumption.  ## Stage 6: A sample app working locally I needed to get a better understanding of how Atproto's [`oauth-client-node`](https://www.npmjs.com/package/@atproto/oauth-client-node) flow was going to actually work. So I: @@ -105,20 +113,25 @@ I needed to get a better understanding of how Atproto's [`oauth-client-node`](ht Once I could use my own Bluesky profile to authorize my sample app locally, I was ready to go back to Coral. ## Stage 7: More Coral Development & Troubleshooting ### :cherries: Incorporate new integration tests, cherry pick to new branch :speech_balloon: Meanwhile, Coral's developers had published some new integration tests around auth strategies, and it was suggested that I branch off of those tests to ensure that my changes wouldn't break anything. So I cherry picked my changes to a new branch. :cherries: ### :no_entry: Errors along the way :no_entry: - Can not resolve handle - 401 not authorized Some errors encountered were: 1. not correctly passing `{ signal, ...options }` to `client.authorize()` resulted in `Can not resolve handle` since `scope: "atproto transition:generic"` was missing 2. not correctly connecting the `StateStore` to the `client` resulted in a `401 not authorized` due to the `client` failing to match the `state` on the callback ### :heart_eyes: Authorization Steps When it works :heart_eyes: 1. Step 1, the atproto oauth `client` is instantiated && `/client-metadata.json` exists 2. Get the user's handle ie `"immber.bsky.social"` 3. Try to pass the handle to `client.authorize()` 1. The `client` sets `state` in the `StateStore` @@ -127,40 +140,42 @@ Once I could use my own Bluesky profile to authorize my sample app locally, I wa 5. Pass the URLSearchParams to `client.callback()` 1. The `client` gets `state` from the `StateStore` to compare with `state` that came back in params 2. If matched, `client` deletes `state` and sets `session` 3. The `session` obj is returned by `client.callback()`, and the user is _authorized_ 6. Use the `session` to instantiate an `Agent` 7. Use the `Agent` to make authorized API calls, `Agent` will call get `session` on the `client` as needed 8. Handle `session` termination and refresh ## Stage 8: Test and Deploy  At this point, I am working with Coral to complete the integration, and take it through their QA process for release. I was hoping to have it deployed by the time of this talk, but that is still a work in progress. ## Stage 9: Celebrate & Reflect ### Questions that I can answer now - :question: Would I have attempted this if I'd known how long it would take? - Definitely no - :question: Did I learn a lot? - Tons - Ultimately, it was worth it - :question: Would I do it again? - Probably yes ### What you should know before starting your own version of this project: #### :blue_heart: Bluesky === Atproto - You're not just adding "Bluesky" oauth, it's any atproto pds host, so there isn't a single redirect URL to send the user to #### :blue_heart: Issue your own ClientID & Secrets - The process is different from other socials, instead of registering your client with a central authority like FB or Google, have to issue your own ClientID and secrets #### :blue_heart: Use atproto's node package - I used the [`oauth-client-node`](https://www.npmjs.com/package/@atproto/oauth-client-node) npm package which handles all of the "token hockey" for you. - If you've written your own `oauth2` clients from scratch in the past you don't have to do that this time, but also, you probably can't easily reuse your existing ones #### :blue_heart: Encode your URIs - I struggled a bit with the localhost overrides for dev testing, this is called out in the docs so pay extra attention and don't forget to encode your uri #### :blue_heart: Use the API & Syntax npm packages for convenience - Use [`@atproto/syntax`](https://www.npmjs.com/package/@atproto/syntax) to validate handles before passing to `client.authorize()` - Use the API Agent [`@atproto/api`](https://www.npmjs.com/package/@atproto/api) to `getProfile()`, and interact with the atproto authenticated user once you have a session -
immber revised this gist
Mar 15, 2025 . 1 changed file with 1 addition and 1 deletion.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -24,7 +24,7 @@ In this talk I will give a brief overview of how that went, and what you should - [Development \& troubleshooting](#development--troubleshooting) - [:no\_entry: Errors along the way](#no_entry-errors-along-the-way) - [:heart\_eyes: Authorization Steps When it works](#heart_eyes-authorization-steps-when-it-works) - [Test and Deploy](#test-and-deploy) - [Celebrate \& Reflect](#celebrate--reflect) - [Questions that I can now answer](#questions-that-i-can-now-answer) - [What you should know before starting your own version of this project:](#what-you-should-know-before-starting-your-own-version-of-this-project) -
immber revised this gist
Mar 15, 2025 . 1 changed file with 2 additions and 3 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -24,7 +24,7 @@ In this talk I will give a brief overview of how that went, and what you should - [Development \& troubleshooting](#development--troubleshooting) - [:no\_entry: Errors along the way](#no_entry-errors-along-the-way) - [:heart\_eyes: Authorization Steps When it works](#heart_eyes-authorization-steps-when-it-works) - [Test and Deploy](#deploy-and-test) - [Celebrate \& Reflect](#celebrate--reflect) - [Questions that I can now answer](#questions-that-i-can-now-answer) - [What you should know before starting your own version of this project:](#what-you-should-know-before-starting-your-own-version-of-this-project) @@ -132,8 +132,7 @@ Once I could use my own Bluesky profile to authorize my sample app locally, I wa 7. Use the `Agent` to make authorized API calls, `Agent` will call get `session` on the `client` as needed 8. Handle `session` termination and refresh ## Test and Deploy  ## Celebrate & Reflect -
immber revised this gist
Mar 15, 2025 . 1 changed file with 1 addition and 1 deletion.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -51,7 +51,7 @@ Coral is a multi-tenant node app depending on Mongo & redis databases. Existing #### The "Before" Login Form: Coral w/ Social Logins Enabled  #### Why not OIDC? Because of this quote taken directly from `https://atproto.com/specs/oauth` -
immber revised this gist
Mar 15, 2025 . 1 changed file with 8 additions and 8 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -39,19 +39,19 @@ In this talk I will give a brief overview of how that went, and what you should ### Open Source Background  [Coral by Vox Media](https://coralproject.net/) was originally founded by The New York Times, Washington Post and Mozilla Foundation to bring journalists and the communities they serve closer together. Today this open source app powers the comments sections of sites all over the world. Having previously worked as an SRE on Coral, I recently embarked on the task of adding Bluesky Oauth Login to Coral as an open source contribution. ### Existing Auth Strategies Coral is a multi-tenant node app depending on Mongo & redis databases. Existing auth strategies include: em/pwd, SSO, OIDC, as well as oAuth2 clients for FaceBook and Google. #### The "Before" Login Form: Coral w/ Social Logins Enabled  #### Why not OIDC? Because of this quote taken directly from `https://atproto.com/specs/oauth` @@ -61,7 +61,7 @@ Because of this quote taken directly from `https://atproto.com/specs/oauth` ## Enthusiastic new branch  Initially I assumed that adding Bluesky would be as simple as reusing Coral's existing `oauth2` client and just copying either the FB or Google authenticator class and make a new one. @@ -71,7 +71,7 @@ As I start cloning & copy pasta-ing Coral's FB Authenticator, I realize that I'm ## Panic  ### Faulty Assumptions @@ -81,7 +81,7 @@ TLDR - that was an incorrect assumption. ### Important differences between common oAuth2 clients and Atproto oauth clients:  1. Because an atproto profile can live anywhere on the internet, not just at `bsky.social` the `redirect-url` (where you send the user) isn't static, it can be any pds host 2. There is no central authority to issue client secrets, so you create and publish your own client ID & secret keys at a `/client-metadata.json` route. @@ -94,7 +94,7 @@ TLDR - that was an incorrect assumption. 1. I opened an issue on Coral to make sure that they actually wanted this contribution :white_check_mark: 2. I submitted this very lightning talk to the CFP, assuming that if it was selected that meant that this was something the atproto community saw value in as well. :white_check_mark:  ## Local dev sample app @@ -134,7 +134,7 @@ Once I could use my own Bluesky profile to authorize my sample app locally, I wa ## Deploy and test  ## Celebrate & Reflect -
immber created this gist
Mar 15, 2025 .There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,167 @@ # Tales of Adding oAuth "Login with Bluesky" to an OS Comments Tool # An [Atmosphere Conf 2025](https://atprotocol.dev/atmosphereconf/) Lightning Talk I recently embarked on the task of adding Bluesky Oauth Login to Coral by Vox Media (a popular comments tool) as an open source contribution. In this talk I will give a brief overview of how that went, and what you should know before attempting to create your own atprotocol oauth clients. - [Tales of Adding oAuth "Login with Bluesky" to an OS Comments Tool](#tales-of-adding-oauth-login-with-bluesky-to-an-os-comments-tool) - [An Atmosphere Conf 2025 Lightning Talk](#an-atmosphere-conf-2025-lightning-talk) - [Idea](#idea) - [Open Source Background](#open-source-background) - [Existing Auth Strategies](#existing-auth-strategies) - [The "Before" Login Form: Coral w/ Social Logins Enabled](#the-before-login-form-coral-w-social-logins-enabled) - [Why not OIDC?](#why-not-oidc) - [Enthusiastic new branch](#enthusiastic-new-branch) - [Panic](#panic) - [Faulty Assumptions](#faulty-assumptions) - [Important differences between common oAuth2 clients and Atproto oauth clients:](#important-differences-between-common-oauth2-clients-and-atproto-oauth-clients) - [External validation](#external-validation) - [I did two things to validate the idea:](#i-did-two-things-to-validate-the-idea) - [Local dev sample app](#local-dev-sample-app) - [Incorporate new integration tests, cherry pick to new branch](#incorporate-new-integration-tests-cherry-pick-to-new-branch) - [Development \& troubleshooting](#development--troubleshooting) - [:no\_entry: Errors along the way](#no_entry-errors-along-the-way) - [:heart\_eyes: Authorization Steps When it works](#heart_eyes-authorization-steps-when-it-works) - [Deploy and test](#deploy-and-test) - [Celebrate \& Reflect](#celebrate--reflect) - [Questions that I can now answer](#questions-that-i-can-now-answer) - [What you should know before starting your own version of this project:](#what-you-should-know-before-starting-your-own-version-of-this-project) - [Bluesky === Atproto](#bluesky--atproto) - [Issue your own ClientID \& Secrets](#issue-your-own-clientid--secrets) - [Use atproto's node package](#use-atprotos-node-package) - [Encode your URIs](#encode-your-uris) - [Use the API \& Syntax npm packages for convenience](#use-the-api--syntax-npm-packages-for-convenience) ## Idea ### Open Source Background  [Coral by Vox Media](https://coralproject.net/) was originally founded by The New York Times, Washington Post and Mozilla Foundation to bring journalists and the communities they serve closer together. Today this open source app powers the comments sections of sites all over the world. Having previously worked as an SRE on Coral, I recently embarked on the task of adding Bluesky Oauth Login to Coral as an open source contribution. ### Existing Auth Strategies Coral is a multi-tenant node app depending on Mongo & redis databases. Existing auth strategies inlcude: em/pwd, SSO, OIDC, as well as oAuth2 clients for FaceBook and Google. #### The "Before" Login Form: Coral w/ Social Logins Enabled  #### Why not OIDC? Because of this quote taken directly from `https://atproto.com/specs/oauth` >OAuth 2.0 is traditionally an authorization (authz) system, not an authentication (authn) system, meaning that it is not always a solution for pure account authentication use cases, such as "Signup/Login with XYZ" identity integrations. OpenID Connect (OIDC), which builds on top of OAuth 2.0, is usually the recommended standard for identity authentication. Unfortunately, the current version of OIDC does not enable authentication of atproto identities in a secure and generic way. The atproto profile of OAuth includes a (mandatory) mechanism for account authentication during the authorization flow and can be used for atproto identity authentication use cases. ## Enthusiastic new branch  Initially I assumed that adding Bluesky would be as simple as reusing Coral's existing `oauth2` client and just copying either the FB or Google authenticator class and make a new one. After spending the better part of a week building out `login-button-containers`, and adding the settings interface and models to allow users to enable *"Login with Bluesky"* in the multi-tenant `Admin/Config/` form routes, I start to dig into the oauth clients, and realize that... "none of this is how this is going to work" As I start cloning & copy pasta-ing Coral's FB Authenticator, I realize that I'm not going to be able to use Coral's existing built in `oauth2` client with Atproto's [`oauth-client-node`](https://www.npmjs.com/package/@atproto/oauth-client-node) ## Panic  ### Faulty Assumptions Already, this is way past the quick and easy copy/pasta job that I initially embarked on. I had thought that creating all of the `login-button-containers` etc was going to be the hard part, and that the Oauth part would be pretty simple. TLDR - that was an incorrect assumption. ### Important differences between common oAuth2 clients and Atproto oauth clients:  1. Because an atproto profile can live anywhere on the internet, not just at `bsky.social` the `redirect-url` (where you send the user) isn't static, it can be any pds host 2. There is no central authority to issue client secrets, so you create and publish your own client ID & secret keys at a `/client-metadata.json` route. ## External validation :red_circle: It was at this point that I decided it was time to externally validate my idea. I needed to even confirm if anyone else in the world besides myself even wanted this integration to exist. ### I did two things to validate the idea: 1. I opened an issue on Coral to make sure that they actually wanted this contribution :white_check_mark: 2. I submitted this very lightning talk to the CFP, assuming that if it was selected that meant that this was something the atproto community saw value in as well. :white_check_mark:  ## Local dev sample app I needed to get a better understanding of how Atproto's [`oauth-client-node`](https://www.npmjs.com/package/@atproto/oauth-client-node) flow was going to actually work. So I: 1. Cloned [statusphere-example-app](https://github.com/bluesky-social/statusphere-example-app/) 2. Used it as a template to make my own even simpler example app that ONLY does oauth Once I could use my own Bluesky profile to authorize my sample app locally, I was ready to go back to Coral. ## Incorporate new integration tests, cherry pick to new branch :speech_balloon: Meanwhile, Coral's developers had published some new integration tests around auth strategies, and it was suggested that I branch off of those tests to ensure that my changes wouldn't break anything. :cherries: So I cherry picked my changes to a new branch. ## Development & troubleshooting ### :no_entry: Errors along the way - Can not resolve handle - 401 not authorized ### :heart_eyes: Authorization Steps When it works 1. Step 1, the atproto oauth `client` is instantiated && `/client-metadata.json` route must exist 2. Get the user's handle ie `"immber.bsky.social"` 3. Try to pass the handle to `client.authorize()` 1. The `client` sets `state` in the `StateStore` 2. The `client` returns the `redirect_url` ie: https://bsky.social/oauth/authorize?client_id=http%3A%2F%2Flocalhost%3Fredirect_uri%3Dhttp%253A%252F%252F127.0.0.1%253A8080%252Foauth%252Fcallback%26scope%3Datproto%2520transition%253Ageneric&request_uri=urn%3Aietf%3Aparams%3Aoauth%3Arequest_uri%3Areq-eb89d03dc33c4de8b86d12a60778fb00 4. User completes authorization and is sent back to the `/callback` route with `URLSearchParams: { iss, state, code )` 5. Pass the URLSearchParams to `client.callback()` 1. The `client` gets `state` from the `StateStore` to compare with `state` that came back in params 2. If matched, `client` deletes `state` and sets `session` 3. The `session` obj is returned by `client.callback()`, user is _authorized_ 6. Use the `session` to instantiate an `Agent` 7. Use the `Agent` to make authorized API calls, `Agent` will call get `session` on the `client` as needed 8. Handle `session` termination and refresh ## Deploy and test  ## Celebrate & Reflect ### Questions that I can now answer - Would I have attempted this if I'd known how long it would take? - Definitely no - Did I learn a lot? - Tons - Ultimately, it was worth it - Would I do it again? - Probably yes ### What you should know before starting your own version of this project: #### Bluesky === Atproto - You're not just adding "Bluesky" oauth, it's any atproto pds host, so there isn't a single redirect URL to send the user to #### Issue your own ClientID & Secrets - The process is different from other socials, instead of registering your client with a central authority like FB or Google, have to issue your own ClientID and secrets #### Use atproto's node package - I used the [`oauth-client-node`](https://www.npmjs.com/package/@atproto/oauth-client-node) npm package which handles all of the "token hockey" for you. - If you've written your own `oauth2` clients from scratch in the past you don't have to do that this time, but also, you probably can't easily reuse your existing ones #### Encode your URIs - I really struggled with the localhost overrides for dev testing, this is called out in the docs so pay extra attention and don't forget to encode your uri #### Use the API & Syntax npm packages for convenience - Use [`@atproto/syntax`](https://www.npmjs.com/package/@atproto/syntax) to validate handles before passing to `client.authorize()` - Use the API Agent [`@atproto/api`](https://www.npmjs.com/package/@atproto/api) to `getProfile()`, and interact with the atproto authenticated user once you have a session