Add sign-in to a web app
This guide adds sign-in to a web app that runs on a server, so your users sign in with Goiabada and your app knows who they are.
Here’s the whole trip. Your app sends the browser to the auth server, the user signs in there, and the browser comes back to your app with an authorization code: a one-time code your server trades for tokens. That’s the authorization code flow, and PKCE ties the code to your app so nobody who intercepts it can use it. Your server can keep a secret, so your app is a confidential client.
Most OpenID Connect libraries do every step below once you give them the discovery URL, https://auth.example.com/.well-known/openid-configuration, your client identifier and your client secret. The steps show what they send, so you know what to configure and what to look for when something goes wrong.
Sign users in
Section titled “Sign users in”-
Register your app. In the admin console, open Admin, Clients and click New client. Enter a Client identifier, such as
my-web-app, leave Authorization code with PKCE on, and click Create client. -
Click your new client’s identifier. On Authentication, click Show and copy the Client secret to your server’s configuration. On Redirect URIs, enter the address your app receives the code at, such as
https://app.example.com/callback, click Add, then Save. -
When a user clicks your app’s sign-in button, make three random values: a
state, anonceand a PKCE code verifier, with the challenge made from it. Keep all three in the user’s session on your server, then send the browser to the auth server:GET /auth/authorize?client_id=my-web-app&redirect_uri=https%3A%2F%2Fapp.example.com%2Fcallback&response_type=code&scope=openid%20email&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&code_challenge_method=S256&state=af0ifjsldkj&nonce=n-0S6_WzA2Mj HTTP/1.1Host: auth.example.com -
The user signs in, and the browser comes back to your redirect URI:
GET /callback?code=SplxlOBeZQQYbYS6WxSbIA&state=af0ifjsldkj&iss=https%3A%2F%2Fauth.example.com HTTP/1.1Host: app.example.comCheck that
stateis the one you kept. If it isn’t, stop: the request didn’t come from your app. Check too thatissis the auth server’s issuer, theissuerin its discovery document, and stop if it isn’t. If the browser bringserroranderror_descriptioninstead ofcode, the sign-in was refused, and the description says why. -
From your server, redeem the code at the token endpoint within 60 seconds, with your client secret and the code verifier:
Terminal window curl -X POST https://auth.example.com/auth/token \-u my-web-app:CLIENT_SECRET \-d grant_type=authorization_code \-d code=SplxlOBeZQQYbYS6WxSbIA \-d redirect_uri=https://app.example.com/callback \-d code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXkThe answer carries an
id_token, anaccess_tokenand arefresh_token. -
Check the ID token, as Tokens describes: its signature against the keys at
/certs,iss,exp, anaudequal to your client identifier, and anonceequal to the one you kept. -
Sign the user in to your app as the user its
subnames, and keep the tokens on your server. When the access token expires, trade the refresh token for new ones, as Refresh tokens shows.
That’s sign-in done. The next thing your users will want is a way to sign out: see Sign users out.
What the ID token says
Section titled “What the ID token says”The ID token is a signed JWT, a set of claims about the sign-in. Decoded, the one from the sign-in above looks like this:
{ "iss": "https://auth.example.com", "sub": "c5b9b6a2-4b39-4b8e-9a8e-6f7f2b3a1d10", "aud": "my-web-app", "iat": 1760000000, "nbf": 1760000000, "exp": 1760000300, "jti": "9a6f1c1e-2d0b-4f3e-8a55-0c1b7a3e2f44", "auth_time": 1759999950, "acr": "urn:goiabada:level2_optional", "amr": ["pwd"], "sid": "0b7e6a31-5f2c-4d8e-9c1a-3e4f5a6b7c8d", "nonce": "n-0S6_WzA2Mj", "email_verified": true}Identify the user by sub, the user’s subject. It never changes, where an email address can. email and email_verified are there because the request asked for the email scope. Each scope brings its own claims, and they’re in the ID token while Include OpenID Connect claims in the ID token is on, as it is in a new install. acr and amr say how the user signed in: see ACR and AMR. Keep the ID token itself: signing the user out sends it back.
state, nonce and PKCE
Section titled “state, nonce and PKCE”Each of the three values you made in step 3 guards against something different, so send all three, new for every sign-in:
statecomes back unchanged with the code or the error. Checking it tells your app the answer is to a request it made, not one an attacker started in the user’s browser.noncecomes back inside the ID token. Checking it tells your app the ID token was issued for this sign-in, not replayed from an earlier one.- PKCE: the auth server redeems the code only with the verifier the challenge was made from, and only your server has it.
The auth server requires PKCE from a confidential client while Require PKCE is on, under Admin, General, as it is in a new install. Send it anyway: PKCE explains why, and how to make the challenge.
The client secret
Section titled “The client secret”Your server proves who it is when it redeems the code. Step 5 sends the secret in an Authorization: Basic header, which curl’s -u builds. Sending client_id and client_secret in the body works too, but not both ways in one request. See client authentication.
Staying signed in
Section titled “Staying signed in”Your app’s session and the auth server’s are two things. Once a user has signed in, the auth server keeps a session of its own. While it lasts, the next time your app, or another app using Goiabada, sends them to sign in, they don’t enter their password again. That’s single sign-on.
Access tokens last 5 minutes in a new install. The refresh token gets your app new ones without the user, while the user’s session lasts. Ask for the offline_access scope as well when your app needs to work while the user is away. See Refresh tokens.
When something goes wrong
Section titled “When something goes wrong”- The sign-in page says “Invalid redirect_uri parameter.”: see Invalid redirect_uri.
- The browser never comes back with an error: see The app gets no error back.
- The browser comes back with
error=invalid_scope: see invalid_scope. - A refresh answers “This refresh token has been revoked.”: see This refresh token has been revoked.
The authorize and token endpoint pages list every parameter and every refusal.