Skip to content

Conversation

@avdb13
Copy link

@avdb13 avdb13 commented Jul 19, 2025

Implements the first step towards #2796. This PR introduces foundational support for single sign-on in the PDS, allowing users to authenticate using identity providers. Tested successfully with Github, Discord (OAuth) and Gitlab (OIDC) so far.

Summary of changes

  • Adds a new SSOManager class responsible for verifying external identity tokens and mapping them to PDS accounts.
  • Extends session and account creation logic to support identity-based authentication without a password.
  • Supports automatic account provisioning for new users logging in with a valid external identity, if account creation is enabled.
  • Integrates external identity verification into existing authentication flows in a backward-compatible way.

Key behavior

  • When a valid external identity token is presented:
    • If a user with that identity exists, a session is created.
    • If not, and account creation is allowed, a new account is provisioned and a session is created.
  • External identities are treated as secure, unique identifiers bound to PDS accounts.

Notes

  • Current logging is for debug purposes only.
  • HTTP status code of the response is changed for com.atproto.sso.getRedirect and com.atproto.sso.getCallback, this might not be desired behavior.
  • This implementation is provider-agnostic and designed to support multiple identity providers through the SSOManager abstraction.
  • Traditional password-based authentication remains fully supported and unchanged.

Test it yourself

  • Clone my forked repository or add it as a new upstream, switch to this branch.
  • Execute make deps && pnpm codegen && make build and run the PDS behind a reverse proxy (OIDC requires usage of TLS for the redirect URI usually).
  • Run curl -X POST -H "Content-Type: application/json" -d @github.json https://<hostname>/xrpc/com.atproto.admin.createIdentityProvider (see below).
  • Run curl --verbose "https://<hostname>/xrpc/com.atproto.sso.getRedirect?idpId=gitlab&redirectUri=https%3A%2F%2F<hostname>%2Fxrpc%2Fcom.atproto.sso.getCallback".
  • Find the Location header and open the link.
  • You should get a successful session as a JSON response.

Example of github.json

{
  "id": "github",
  "name": "Github",
  "issuer": "https://github.com",
  "clientId": "xxx",
  "clientSecret": "xxx",
  "scope": "read:user user:email",
  "usePkce": false,
  "discoverable": false,
  "metadata": {
    "endpoints": {
      "authorization": "https://github.com/login/oauth/authorize",
      "token": "https://github.com/login/oauth/access_token",
      "userinfo": "https://api.github.com/user"
    },
    "mappings": {
      "sub": "id",
      "username": "login"
    },
    "authMethods": ["client_secret_post"]
  }
}

On assignment from @muni-town collective.

@avdb13 avdb13 changed the title Single-sign-on Single sign-on Jul 19, 2025
@bnewbold
Copy link
Collaborator

As a heads up, many of the relevant Bluesky staff are traveling for a couple weeks, and it may take some time to comment or give focused feedback.

@avdb13
Copy link
Author

avdb13 commented Aug 21, 2025

All we need now is to think of now is what rate limits to apply if I'm correct.

Copy link

@Minion3665 Minion3665 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A comment about a build failure here - as well as you seem not to have run pnpm codegen which causes further failures

I think your code probably(?) all works, but it definitely doesn't build in any mode where full type checking is happening. I'll comment more once I've got this up and running and tested this a bit...

Comment on lines 69 to 82
if (typeof req.headers.cookie === 'string') {
try {
const cookies: Record<string, string | undefined>
= cookie.parse(req.headers.cookie);

callbackId = cookies["atproto-callback"];
} catch (err) {
throw new InvalidRequestError(`Invalid cookie header: ${err}`);
}
}

if (!callbackId) {
throw new InvalidRequestError("Missing callback cookie");
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You don't really need the cookie here, since you have that from the state parameter.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I rewrote the cookie logic to do encryption, so that the state inside the cookie can provide CSRF protection.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants