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
Section titled “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
Section titled “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, ornull.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
Section titled “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:
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/callbackand 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
Section titled “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:
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.jsonNo 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:
issandsub: the client_id;aud: Prosopia’stoken_endpointfrom 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
Section titled “Configure snapgate”Snapgate signs in through any one OpenID Connect issuer. Set:
OIDC_ISSUER=https://prosopia.databio.orgOIDC_CLIENT_ID=pigeonOIDC_CLIENT_SECRET=$(pass webdev/pigeon/oidc_client_secret)OIDC_REDIRECT_URI=https://pigeon.databio.org/auth/login/callbackOIDC_LABEL=ProsopiaOIDC_ADOPT_CLAIM=google_subOIDC_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
Section titled “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):
<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
Section titled “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_idorgithub_logindrop 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.