# 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

```json
{
  "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.
