Signing in with a dasmaffin.com account
This site is an OpenID Connect provider. A program somewhere else sends somebody here, they sign in the way they already do, and the program is handed a token saying who they are.
It is a plain, standard provider. Any OpenID Connect library will work against it, and you should use one rather than writing the flow by hand.
What to configure
| Issuer | https://www.dasmaffin.com/ |
| Discovery | https://www.dasmaffin.com/.well-known/openid-configuration |
| Key set | https://www.dasmaffin.com/.well-known/jwks |
| Flow | Authorization code, PKCE with S256, public client, no client secret |
| Signing | RS256 |
Point your library at the issuer and let it read the rest from the discovery document. Do not copy the endpoint addresses into your configuration: the key set in particular is meant to be fetched, because the keys in it change.
Fetch the discovery document from the issuer's own address - the www one. The endpoints it
advertises are built from the address you asked at, so fetching it from anywhere else gives you a
document whose endpoints and whose issuer disagree, and a strict client is right to refuse that.
Getting registered
Send these. They go in src/Host/oidc-clients.json in the site's repository, which deploys with
the site:
- A client id. Short, lowercase, and yours forever - it is never reused for anything else.
- A display name. What the person is asked about, in the words they would recognise.
- Every redirect address, in full. Matched exactly, character for character. A custom scheme
(
com.example.yourapp:/oauth2redirect) is fine and is the normal answer for an app on a phone. - Which scopes you need, out of the list below.
- Whether you need to stay signed in between sessions.
There is no self-service registration page, and no client secret to collect: everything above is public by construction, since a client id and a redirect address both travel in the address bar during any sign-in. Registering you is a commit and a deploy.
Exact matching is the point, so send the addresses as your app will actually send them. A trailing slash is a different address. If you need another one later, that is another commit - it is not something the app can add for itself, deliberately.
Redirect addresses, and the one that catches native apps out
Matching is exact. Only the form that was registered is accepted, and near-misses are refused with
a 400 rather than being tidied up. Measured against the live site for the registered
com.dasmaffin.cairn:/oauth2redirect:
| Sent | |
|---|---|
com.dasmaffin.cairn:/oauth2redirect |
accepted |
com.dasmaffin.cairn://oauth2redirect |
refused |
com.dasmaffin.cairn://oauth2redirect/ |
refused |
com.dasmaffin.cairn:/oauth2redirect/ |
refused |
com.dasmaffin.cairn://oauth2redirect/extra |
refused |
But the address you are sent back to is the normalised one, with two slashes and a trailing slash:
com.dasmaffin.cairn://oauth2redirect/?code=...&state=...
So an app sends one form and must catch another. On Android that means an intent filter matching
host oauth2redirect and path / - the scheme-only filter AppAuth sets up by default is fine, and
one that pins android:path="/oauth2redirect" will never fire. It does not present as an error: the
site redirects, nothing catches it, and the sign-in simply hangs.
A reverse-DNS scheme is a convention for not colliding with somebody else, not a claim of ownership, so it does not have to match your application id.
Scopes
| Scope | What it is |
|---|---|
openid |
Required. Asks who they are, and gets you sub and nothing else. |
profile |
The display name on their profile, as name and preferred_username. |
email |
Their email address and whether it has been confirmed. |
offline_access |
A refresh token, so they stay signed in between sessions. |
Ask for what you need. Every scope is a line on the consent screen the person reads before they press Allow, and a shorter list is one people say yes to.
What is in the identity token
{
"iss": "https://www.dasmaffin.com/",
"aud": "your-client-id",
"sub": "3f2b7c10-9a4e-4c53-8f1d-0b6e2a5d7c88",
"name": "Maffin",
"preferred_username": "Maffin",
"email": "someone@example.com",
"email_verified": true,
"exp": 1788888888,
"iat": 1788885288
}
name, preferred_username, email and email_verified appear only if you asked for the scope
that carries them, and only if there is something to say - a profile with no display name set sends
no name at all rather than an empty one. Everything else is always there.
The access token carries sub and nothing else about the person. If you want the current answer
rather than the one frozen into the token, call /connect/userinfo with it.
About sub
sub is the account, not the login, and it never changes.
Somebody here can sign in with Steam or with an email address, and can add the second one later.
Both are ways into the same profile, and all of them produce the same sub. It is a GUID generated
once for the account and never reassigned - not their email address, not their SteamID, not a row
id. File everything you know about a person under it.
The corollary: sub is the only identifier you should key on. An email address can be changed and a
display name can be changed to somebody else's. Neither is an identity.
Lifetimes
| Authorization code | 5 minutes, single use |
| Access token | 1 hour |
| Identity token | 1 hour |
| Refresh token | 90 days |
Refresh tokens roll. Redeeming one issues a new one and retires the one you used, so keep the
newest and throw the old one away. Presenting a refresh token that has already been redeemed gets
you invalid_grant, and it is meant to: a second use is what a stolen token looks like. Do not
work around it by retrying with the old one. A repeat within a few seconds is forgiven, so a
request that timed out on the wire can safely be sent again.
An account that has been deleted stops producing tokens immediately, refresh token or not.
Consent, and taking it back
The person is asked once, per program, and their answer is remembered. They can withdraw it at any time from the Privacy tab of their account page. Withdrawing it revokes every token issued under it straight away, so plan for an access token that stops working before it expires - which you have to anyway.
Signing keys
Published at the key set address, each with a kid. Tokens name the key they were signed with in
their header. Fetch the key set, cache it, and refetch when you see a kid you do not know: keys
are rotated, and during a rotation the old key stays published for as long as tokens signed with it
could still be alive.
Do not pin a key. Do not copy one into your configuration.
Errors
Standard OAuth errors, sent back to your redirect address as query parameters:
error |
What happened |
|---|---|
access_denied |
They pressed Not now. |
login_required |
You sent prompt=none and nobody was signed in here. |
consent_required |
You sent prompt=none and they have not agreed to this yet. |
invalid_grant |
The code or refresh token is spent, expired, or was not yours. |
Running it
The provider is off unless a database is configured, because all of it - codes, consent, refresh
tokens - is stored. Its signing keys live in Oidc-Keys/ beside the site and are generated on first
run; the deploy is told not to delete that folder, in the same way and for the same reason as
DataProtection-Keys/. Losing it does not lose accounts, but it does invalidate every token in the
wild at once.
To rotate the signing key, call OidcKeys.Rotate() and restart. The new key signs from then on, the
old one stays published and keeps verifying what it signed, and it is retired by deleting its file
once nothing signed with it can still be alive - which is the refresh token lifetime above.