(WIP) atlogin port to elixir
README.md

Atlogin #

A Plug-based OpenID Connect provider backed by AT Protocol identities. Phoenix is not required.

Status: experimental. Atlogin is an early port of apenwarr/atlogin. It has not had an independent security review or OIDC conformance test.

Features #

  • OIDC discovery, JWKS, WebFinger, authorization code, PKCE, and UserInfo
  • AT Protocol login through Atex
  • RS256 ID tokens and opaque access tokens
  • Exact redirect URI matching
  • Single-use, expiring callback state and authorization codes
  • ETS storage for single-node deployments

Client provisioning, refresh tokens, logout, durable storage, and a browser UI are not included yet.

Installation #

def deps do
  [
    {:atlogin, "~> 0.1.0"}
  ]
end

Atlogin requires Elixir 1.18 or later.

Setup #

Start a store under your application's supervisor:

children = [
  {Atlogin.Store.ETS,
   name: MyApp.AtloginStore,
   sweep_interval: 30_000,
   max_entries: 100_000}
]

Create the provider configuration at runtime. Keep both signing keys stable across restarts.

defmodule MyApp.IdentityProvider do
  alias Atlogin.{Client, ClientRegistry, Store}

  def config do
    Atlogin.new!(
      issuer: "https://id.example.com",
      store: {Store.ETS, MyApp.AtloginStore},
      clients: {
        ClientRegistry.Static,
        %{
          "my-client" => %Client{
            id: "my-client",
            secret: System.fetch_env!("OIDC_CLIENT_SECRET"),
            redirect_uris: ["https://app.example.com/oidc/callback"]
          }
        }
      },
      signing_key: {__MODULE__, :oidc_signing_key, []},
      signing_key_id: "oidc-2026-01",
      atproto: {
        Atlogin.ATProto.Atex,
        [
          base_url: "https://id.example.com",
          private_key: {__MODULE__, :atproto_private_key, []},
          key_id: "atproto-2026-01"
        ]
      }
    )
  end

  def oidc_signing_key do
    System.fetch_env!("OIDC_PRIVATE_JWK")
    |> Jason.decode!()
    |> JOSE.JWK.from_map()
  end

  def atproto_private_key do
    System.fetch_env!("ATPROTO_PRIVATE_JWK")
    |> Jason.decode!()
    |> JOSE.JWK.from_map()
  end
end

Mount it in any Plug router:

defmodule MyApp.Router do
  use Plug.Router

  plug :match
  plug :dispatch

  forward "/",
    to: Atlogin.Plug,
    init_opts: [config: {MyApp.IdentityProvider, :config, []}]
end

For local development, allow_insecure_http: true permits an HTTP issuer on localhost, 127.0.0.1, or ::1. HTTP callback URIs are allowed only for the exact host 127.0.0.1. Use HTTPS in public deployments.

Routes #

  • GET /.well-known/openid-configuration
  • GET /.well-known/jwks.json
  • GET /.well-known/webfinger
  • GET /authorize
  • GET /client-metadata.json
  • GET /atproto/callback
  • POST /token
  • GET or POST /userinfo

Storage and security #

Atlogin.Store.ETS is for tests and single-node deployments. It is in-memory; active logins are lost on restart. Multi-node deployments need a shared Atlogin.Store implementation.

Atlogin fixes the issuer in configuration, verifies AT Protocol DIDs, requires exact registered redirect URIs, and consumes callback state and authorization codes once. The email claim is a login alias, not proof of mailbox ownership; email_verified is always false.

Development #

mix test