Skip to content

Instantly share code, notes, and snippets.

@PSingletary
Forked from immber/oAuthLoginwithBsky.md
Created March 23, 2025 23:20
Show Gist options
  • Save PSingletary/111ed8d2451a47810ac55b25fb273ab5 to your computer and use it in GitHub Desktop.
Save PSingletary/111ed8d2451a47810ac55b25fb273ab5 to your computer and use it in GitHub Desktop.

Tales of Adding oAuth "Login with Bluesky" to an OS Comments Tool

An Atmosphere Conf 2025 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.

Stage 1: Idea

Open Source Background

Coral by Vox Media Favicon Logo

Coral by Vox Media 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

coral's login form showing email, facebook and google 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.

Stage 2: Enthusiastic new branch

spiderman this will be easy meme

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

picard i blame myself meme

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:

atproto logo

  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

πŸ”΄ 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 βœ…
  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. βœ…

raccoon can not stop me now meme

Stage 6: A sample app working locally

I needed to get a better understanding of how Atproto's oauth-client-node flow was going to actually work. So I:

  1. Cloned 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.

Stage 7: More Coral Development & Troubleshooting

πŸ’ Incorporate new integration tests, cherry pick to new branch

πŸ’¬ 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. πŸ’

β›” Errors along the way β›”

  • 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

😍 Authorization Steps When it works 😍

  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
    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
    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

plan vs reality meme

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

  • ❓ 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 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 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

πŸ’™ Use the API & Syntax npm packages for convenience

  • Use @atproto/syntax to validate handles before passing to client.authorize()
  • Use the API Agent @atproto/api to getProfile(), and interact with the atproto authenticated user once you have a session
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment