# How to sign people in to your app with Prosopia

Our own web apps (Pigeon, Sponsus, Echelon) can let Prosopia handle sign-in
instead of each one holding its own Google login. Prosopia is a standard
**OpenID Connect** provider: your app sends the person to Prosopia, the person
signs in there with ORCID, Google or GitHub, and Prosopia sends them back with a signed
answer to one question: who is this?

A person already signed in to Prosopia goes straight through, so signing in to
a second app takes one click or none.

## What your app gets

An **id_token**, a signed JWT (RS256) that lives five minutes. Check it
against the keys at `https://prosopia.databio.org/oauth/jwks.json`. It holds:

| Claim | What it is |
|-------|------------|
| `sub` | The person's Prosopia id. Random, never changes, never an ORCID. Key your users on `issuer#sub`. |
| `name` | Their name (with the `profile` scope). |
| `email`, `email_verified` | An address a login provider vouched for (with the `email` scope). Present only when there is one. |
| `orcid` | Their ORCID iD, when they have linked one. |
| `google_sub` | Their Google id, when they have linked Google. Use it once to find users you stored under Google before. |
| `github_id` | Their GitHub user id (a number, sent as a string), when they have linked GitHub. Never changes, even if they rename their GitHub account. Key on this, not on `github_login`. |
| `github_login` | Their GitHub username as of their last GitHub sign-in or link. Can change. |
| `owned_rids` | The rid of the profile that is about them (with the `profiles:own` scope): `[self_rid]`, or empty when they have none. |
| `self_rid` | The rid of the profile about this person (with the `profiles:own` scope). `null` when they have none. |
| `idp` | How they signed in this time: `orcid`, `google` or `github`. |
| `auth_time`, `nonce` | When they signed in to Prosopia; your nonce, echoed. |

You also get a five-minute access token that works only at
`/oauth/userinfo`, which returns the same claims. There is no refresh token:
your app keeps its own session, as it does now.

A GitHub login never provides `email`. If your app needs an email, keep
`requires_email` on (the default): a person with only a GitHub login is asked
to link Google first. If your app does not need one, register it with
`--no-require-email` (see below), or GitHub-only people cannot sign in.

### The person's own profile, and the profiles they own

Ask for the `profiles:own` scope (for example `openid email profile
profiles:own`) and the id_token and userinfo carry two claims:

- `self_rid`: the rid of the one profile that is about this person, or `null`.
- `owned_rids`: the rid of the profile that is about this person, as a list:
  `[self_rid]`, or `[]` when there is none. A profile they hold on someone
  else's behalf is not listed, and neither is one they co-own or edit.

Without the scope neither claim is sent. Both are worked out fresh each time
the person signs in, so a profile they gained or lost shows up at their next
sign-in.

To show the person their own public profile, read
`GET /api/v1/profiles/{self_rid}` without credentials. A 404 means they have
no public profile. Do not search the profile list for their ORCID: their
profile may have a `local:` rid, and people who sign in with Google or GitHub
may have no ORCID at all.

Only sign-in apps (the ones an operator adds) may ask for `profiles:own`. A
chat app or the rp CLI that asks for it is refused with `invalid_scope`.

What Prosopia does **not** decide: whether this person may use your app. Keep
your own approval step and admin lists.

## Get a client

Ask a Prosopia operator. They add it on the **Admin -> Sign-in apps** tab
(`/admin/sign-in-apps`), or with the CLI:

```bash
prosopia oauth add-client pigeon --name "Pigeon" \
  --redirect-uri https://pigeon.databio.org/auth/login/callback \
  --redirect-uri http://127.0.0.1:8133/auth/login/callback
```

and give you the secret it shows (it is shown once; the tab also gives a
copy-paste block of the settings below). By default Prosopia
refuses to sign in an account with no verified email, since most apps need
one: such a person is asked to link a Google or university login first. Pass
`--no-require-email` if your app can do without.

### Sign in with a key instead of a secret

An app can prove who it is with a key instead of a secret
(`private_key_jwt`). The app makes a key pair, keeps the private key, and
publishes the public key as a JWKS at an https address on its own site. Add it
with `--jwks-uri`:

```bash
prosopia oauth add-client notes --name "Notes" \
  --redirect-uri https://notes.databio.org/auth/login/callback \
  --jwks-uri https://notes.databio.org/.well-known/jwks.json
```

No secret is made or shown. The JWKS URI must be https, and every redirect
URI must be https on the same host, so only whoever runs the app's site can
publish a key Prosopia accepts. A local `http://localhost` callback cannot be
used with a key, since Prosopia cannot fetch a key from your laptop.

At the token call the app sends, in the body, its `client_id`,
`client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer`,
and `client_assertion`: a JWT signed with RS256, with `kid` in the header and
these claims:

- `iss` and `sub`: the client_id;
- `aud`: Prosopia's `token_endpoint` from the discovery document (the issuer
  is accepted too);
- `exp`: at most 5 minutes after now; `iat`: now;
- `jti`: a fresh random value. Each assertion works once.

Prosopia fetches the JWKS (public addresses only, no redirects, 16 KB at
most), caches it as its `Cache-Control` says but never longer than 5
minutes, and fetches it again when an assertion names a `kid` it does not
have, at most once every 30 seconds. So an app may make a new key each time it
starts. PKCE and the exact redirect URI are still required. An app with a key
cannot send a secret, and an app with a secret cannot send an assertion.

To move an existing app from a secret to a key, deploy the app so that it
publishes its JWKS and signs assertions, then run
`prosopia oauth set-jwks-uri <client_id> <jwks-uri>`. That deletes the app's
secret at once, and there is no way back to a secret, so do it only when the
app is ready. Snapgate does both with `OIDC_CLIENT_AUTH=private_key_jwt`
(snapgate 10.2.0 or later).

## Configure snapgate

Snapgate signs in through any one OpenID Connect issuer. Set:

```bash
OIDC_ISSUER=https://prosopia.databio.org
OIDC_CLIENT_ID=pigeon
OIDC_CLIENT_SECRET=$(pass webdev/pigeon/oidc_client_secret)
OIDC_REDIRECT_URI=https://pigeon.databio.org/auth/login/callback
OIDC_LABEL=Prosopia
OIDC_ADOPT_CLAIM=google_sub
```

`OIDC_ADOPT_CLAIM=google_sub` lets people who used your app with Google
before keep their account: on their first Prosopia sign-in, snapgate finds
their old row by Google id and moves it over. Keep it set.

Any other OpenID Connect library works the same way: point it at
`https://prosopia.databio.org/.well-known/openid-configuration`, use the
authorization code flow with PKCE (S256), and send the secret with
`client_secret_basic` or `client_secret_post` (or, for an app added with
`--jwks-uri`, a `private_key_jwt` client assertion, as above).

## Make your sign-in page

Your app's own login page is the first thing a person sees, and Prosopia's
sign-in page is the second. Make the first look like the second, so the step
between them feels like one flow. Prosopia serves a small kit for this at
`https://prosopia.databio.org/brand/`.

Your login page should show:

- your app's name and a short line, such as "Prosopia signs you in with
  ORCID, Google, GitHub or your university";
- one **Sign in with Prosopia** button, and nothing else to sign in with.

The button is a plain link to **your app's own** `/auth/login` (snapgate's
login route). That route starts the OpenID Connect flow and sends the person
to Prosopia. Never link the button straight to a Prosopia URL: your app must
create the `state`, `nonce` and PKCE values itself.

Copy the files into your app instead of linking to them, so your login page
still works when Prosopia is slow:

| File | What it is |
|------|------------|
| `/brand/sign-in-button.html` | The button |
| `/brand/sign-in-button.css` | Its style, light and dark |
| `/brand/prosopia-mark.svg` | The Prosopia mark |
| `/brand/prosopia-lockup.svg` | The mark with the word Prosopia |

The button looks like this (the mark's SVG path is cut short here; take the
whole thing from `/brand/sign-in-button.html`):

```html
<a class="prosopia-signin" href="/auth/login">
  <span class="prosopia-signin__mark"><svg aria-hidden="true" width="24" height="24" viewBox="0 0 44.257 42.78">...</svg></span>
  <span class="prosopia-signin__text">
    <span class="prosopia-signin__label">Sign in with Prosopia</span>
    <span class="prosopia-signin__hint">Prosopia signs you in with ORCID, Google, GitHub or your university.</span>
  </span>
</a>
```

The CSS uses only `.prosopia-signin` classes and its own `--prosopia-*`
colors, so it does not clash with your page's style. It uses the same font,
colors, rounded box and hover effect as the buttons on Prosopia's sign-in
page, and switches to dark colors when the person's device is in dark mode.

## Good to know

- **Suspending** someone in Prosopia stops new sign-ins to every app. A
  session your app already gave them lasts until your app ends it.
- **Unlinking** a login changes only later sign-ins: `email`, `google_sub`,
  `github_id` or `github_login` drop out of the next id_token.
- **Signing out** of Prosopia does not sign the person out of your app.
- If Prosopia is down, nobody can start a new sign-in to your app.
