Quickstart: any OIDC library
Usher is a standard OpenID Connect provider. If your language has a certified OIDC client, you already have everything you need: point it at your environment’s discovery document and use the authorisation code flow with PKCE.
The settings
| Setting | Value |
|---|---|
| Issuer | https://<handle>.id.hallelo.net |
| Discovery URL | https://<handle>.id.hallelo.net/.well-known/openid-configuration |
| Flow | Authorisation code (response_type=code) |
| PKCE | Required, code_challenge_method=S256 |
| Scopes | openid profile email offline_access |
| Client authentication | As listed in token_endpoint_auth_methods_supported |
Your environment’s handle is in Usher Console, beside its issuer. Development environments get a random one, such as wise-otter-42; production environments have the handle you chose. A production environment can live at your own domain instead, such as login.acme.com: then that is its issuer, once you have pointed the domain at Usher with a CNAME record.
Read the discovery document
Every endpoint and key your client needs is listed here. Let your library read it rather than copying URLs into your code.
curl -s https://acme.id.hallelo.net/.well-known/openid-configuration | jq{
"issuer": "https://acme.id.hallelo.net",
"authorization_endpoint": "https://acme.id.hallelo.net/api/auth/oauth2/authorize",
"token_endpoint": "https://acme.id.hallelo.net/api/auth/oauth2/token",
"userinfo_endpoint": "https://acme.id.hallelo.net/api/auth/oauth2/userinfo",
"jwks_uri": "https://acme.id.hallelo.net/api/auth/jwks",
"introspection_endpoint": "https://acme.id.hallelo.net/api/auth/oauth2/introspect",
"revocation_endpoint": "https://acme.id.hallelo.net/api/auth/oauth2/revoke",
"end_session_endpoint": "https://acme.id.hallelo.net/api/auth/oauth2/end-session",
"scopes_supported": ["openid", "profile", "email", "offline_access"],
"response_types_supported": ["code"],
"grant_types_supported": ["authorization_code", "refresh_token", "client_credentials"],
"code_challenge_methods_supported": ["S256"]
}The flow
Send the person to the authorisation endpoint
Make a random PKCE verifier and keep it, with a random state, somewhere only this browser’s session can reach (a short-lived, httpOnly cookie works well). Then redirect with response_type=code, your client_id and redirect_uri, the scopes above, the state, and the S256 code_challenge of your verifier.
Let them sign in
The Lobby takes it from here: it names your app, offers the sign-in methods your environment allows, and asks for consent the first time.
Check the callback
Usher redirects to your redirect_uri with code and state. Refuse the response unless state matches the one you stored. The code is single-use and lasts five minutes.
Swap the code for tokens
From your server, post the code and the PKCE verifier to the token endpoint:
curl -s -X POST https://acme.id.hallelo.net/api/auth/oauth2/token \
-u "$CLIENT_ID:$CLIENT_SECRET" \
-d grant_type=authorization_code \
-d code="$CODE" \
-d redirect_uri=https://app.acme.example/callback \
-d code_verifier="$CODE_VERIFIER"Validate the ID token
Check its signature against the keys at jwks_uri, and that iss is your issuer, aud is your client ID and exp is in the future. Your library does this for you; make sure it isn’t turned off.
In Node.js
openid-client does all of the above, including validation:
import * as client from 'openid-client'
// discovery: every endpoint and key comes from the issuer
const config = await client.discovery(
new URL('https://acme.id.hallelo.net'),
process.env.USHER_CLIENT_ID!,
process.env.USHER_CLIENT_SECRET!,
)
// 1. send them to the Lobby, with PKCE (S256) and a state to check later
const codeVerifier = client.randomPKCECodeVerifier()
const state = client.randomState()
const url = client.buildAuthorizationUrl(config, {
redirect_uri: 'https://app.acme.example/callback',
scope: 'openid profile email offline_access',
code_challenge: await client.calculatePKCECodeChallenge(codeVerifier),
code_challenge_method: 'S256',
state,
})
// keep codeVerifier and state in a short-lived httpOnly cookie, then redirect to url
// 2. back at /callback: swap the code for tokens
const tokens = await client.authorizationCodeGrant(config, new URL(request.url), {
pkceCodeVerifier: codeVerifier,
expectedState: state,
})
const { sub, email, name } = tokens.claims()!In other languages
These widely used libraries all speak standard OpenID Connect and work with Usher without anything Usher-specific:
| Language | Library |
|---|---|
| Python | Authlib |
| Go | coreos/go-oidc with golang.org/x/oauth2 |
| Java | Spring Security OAuth 2.0 Client |
| .NET | Microsoft.AspNetCore.Authentication.OpenIdConnect |
| Ruby | omniauth_openid_connect |
| PHP | jumbojett/openid-connect-php |
Staying signed in
With the offline_access scope, the token response includes a refresh token. Use it with grant_type=refresh_token to get a new access token when the old one expires. Refresh tokens rotate: each use returns a new one, and the old one stops working.
Signing out
Clear your own session first, then redirect to the end_session_endpoint with post_logout_redirect_uri (and id_token_hint if you have it), so the Lobby forgets the person too.