atlogin #
This fork builds on anirudh.fi/atlogin, which builds on apenwarr/atlogin. See CHANGES.md for authentication changes and compatibility notes.
OIDC identity provider for AT Protocol handles. Lets users log in to any OIDC-compatible app using their AT Protocol identity.
how it works #
Users can enter a plain AT Protocol handle on the authorization page. Existing email-style login hints are also accepted and map to a handle as follows:
| Login | Handle |
|---|---|
alice@example.com |
@alice.example.com |
alice@alice.bsky.social |
@alice.bsky.social (prefix match) |
alice.bsky.social@your-instance.example.com |
@alice.bsky.social (passthrough) |
The server's own hostname is a passthrough domain — the username part is used directly as the AT Protocol handle.
Profile suggestions use the public Bluesky AppView; lookup requests include the entered query and recent handles. Recent handles are stored in the browser.
deployment #
files #
atlogin reads config.json and signing-key.json from its state directory
(default: ./state). Initialize it once:
atlogin -init -state-dir ./state
Edit ./state/config.json:
{
"addr": "127.0.0.1:9411",
"issuer": "https://your-instance.example.com",
"client_name": "your instance name",
"master_key": "<hex string, generate with: openssl rand -hex 32>",
"secrets": {
"your-client-id": "your-client-secret"
},
"redirect_uris": {
"your-client-id": ["https://yourapp.example.com/callback"]
}
}
Replace the client secret with a strong random value (at least 32 bytes), then start the server:
atlogin -state-dir ./state
Keep the directory private and retain the signing key across restarts. Put an HTTPS reverse proxy in front of the loopback listener.
env vars #
For deployments that inject secrets through environment variables:
ATLOGIN_CONFIG: base64-encoded contents ofconfig.jsonATLOGIN_SIGNING_KEY: base64-encoded contents ofsigning-key.json
export ATLOGIN_CONFIG="$(base64 < ./state/config.json | tr -d '\n')"
export ATLOGIN_SIGNING_KEY="$(base64 < ./state/signing-key.json | tr -d '\n')"
atlogin -state-dir ./state
Each variable is optional. When set, it overwrites its corresponding state file at startup; unset it to use the file directly. Base64 is encoding, not encryption.
client credentials #
To provision a client (e.g. Tailscale), generate the client ID and secret
using gen-client.sh:
./gen-client.sh <login-email> <app-name> <master-key>
# e.g.:
./gen-client.sh alice@example.com Tailscale abc123...
# Client ID: alice-at-example-com-Tailscale-v1
# Client Secret: <deterministic base64>
Add the output to the secrets (and mandatory redirect_uris) in your
config.json (or its ATLOGIN_CONFIG copy).
The client secret is deterministic: base64(HMAC-SHA256(client_id, master_key)).
Running the script again with the same inputs produces the same secret.
flags #
-state-dir <path> State directory for signing key and config (default: ./state)
-init Initialize state directory and exit
-new-client <id> Add a new client to config.json and print the secret
webfinger #
atlogin serves WebFinger for its own hostname automatically. For users to log
in as user@otherdomain.com, otherdomain.com needs to proxy
/.well-known/webfinger to your atlogin instance's /helpers/webfinger:
location /.well-known/webfinger {
proxy_pass https://your-instance.example.com/helpers/webfinger;
}
This also works with a Cloudflare Worker or any static redirect.
NixOS #
A NixOS module is included in the flake:
{
inputs.atlogin.url = "git+https://tangled.org/kira.ws/atlogin";
# in your NixOS config:
imports = [ atlogin.nixosModules.default ];
services.atlogin = {
enable = true;
settings = null; # use /var/lib/atlogin/config.json directly
# Or inject the base64 variables from a private file:
# environmentFile = "/etc/secrets/atlogin.env";
};
}
The service user must be able to read the state files. Avoid putting client
secrets in services.atlogin.settings: they would enter the Nix store.
OIDC endpoints #
| Endpoint | Description |
|---|---|
/.well-known/openid-configuration |
OIDC discovery |
/.well-known/jwks.json |
Public keys |
/.well-known/webfinger |
WebFinger |
/helpers/webfinger |
WebFinger helper for reverse proxying |
/authorize |
Authorization |
/token |
Token exchange |
/userinfo |
User info |
remembered sign-in #
Remember me on this device creates a Secure, HttpOnly cookie backed by a
private remembered-sessions.json file. Sessions survive restarts and expire
without sliding renewal after 30 days. Set remember_days to 0 to disable, or
1–90 to choose a lifetime. The file stores token hashes, not PDS credentials.
switch_account=1, prompt=login and max_age can force a new login flow.
prompt=select_account normally shows the picker; set remember_select_account: true when your clients send that prompt routinely and you want the saved-account
countdown instead. Silent prompt=none requests do not display a countdown.
custom CSS #
Set custom_css_file to a local stylesheet path. It is loaded at startup and
served after the default styles on the login and remembered-account screens:
{"custom_css_file": "/var/lib/atlogin/theme.css"}
The service user must be able to read it. Missing or oversized files fail startup; the limit is 8 MiB. Same-origin font files and data-URL fonts are supported. Restart the service to load stylesheet changes. No theme is required by default.
relying-party setup #
Register an exact HTTPS callback URL, a fixed HTTPS issuer origin, and a client
secret of at least 32 bytes. Use the openid profile scopes; S256 PKCE is supported.
sub is the DID. name and preferred_username are the handle.
Signing keys and remembered sessions persist across restarts. In-flight logins and access tokens do not.