Skip to content

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.

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:

ClaimWhat it is
subThe person’s Prosopia id. Random, never changes, never an ORCID. Key your users on issuer#sub.
nameTheir name (with the profile scope).
email, email_verifiedAn address a login provider vouched for (with the email scope). Present only when there is one.
orcidTheir ORCID iD, when they have linked one.
google_subTheir Google id, when they have linked Google. Use it once to find users you stored under Google before.
github_idTheir 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_loginTheir GitHub username as of their last GitHub sign-in or link. Can change.
owned_ridsThe rid of the profile that is about them (with the profiles:own scope): [self_rid], or empty when they have none.
self_ridThe rid of the profile about this person (with the profiles:own scope). null when they have none.
idpHow they signed in this time: orcid, google or github.
auth_time, nonceWhen 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, 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.

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

Terminal window
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.

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:

Terminal window
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).

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

Terminal window
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).

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:

FileWhat it is
/brand/sign-in-button.htmlThe button
/brand/sign-in-button.cssIts style, light and dark
/brand/prosopia-mark.svgThe Prosopia mark
/brand/prosopia-lockup.svgThe 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.

  • 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.