Clients
This page helps you register your app with Goiabada as a client and pick the right settings for it.
A client is any application that asks the auth server for tokens: a web app your users sign in to, a mobile app, a backend service calling an API, or the admin console itself. Each client has a client identifier, the client_id it sends on every request.
Create a client
Section titled “Create a client”-
In the admin console, open Admin, Clients and click New client.
-
Enter a Client identifier. It’s the
client_idyour app will send, such asmy-web-app. -
Turn on the flows your app uses, and turn off the ones it doesn’t. Authorization code with PKCE starts on:
- Authorization code with PKCE if users sign in to your app.
- Client credentials if your app is a service that calls an API on its own behalf.
-
Click Create client. You’re back on the list of clients. Click your new client’s identifier.
-
If your app runs in a browser or on a phone, open Authentication, choose Public, click Save, and confirm with Save. Otherwise, copy the Client secret from there and keep it on your server.
-
If users sign in to your app, open Redirect URIs, enter the address your app receives the authorization code at, such as
https://app.example.com/callback, click Add, then click Save. -
If your app sends users back to a page of its own after they sign out, open Sign-out redirect URIs, enter that page’s address, such as
https://app.example.com/signed-out, click Add, then click Save.
That’s enough to sign users in. The rest of this page explains each setting, so come back when you need one.
Public or confidential
Section titled “Public or confidential”Pick this by where your app’s code runs.
- A confidential client runs on a server you control, so it can keep a client secret. Web apps with a backend and backend services are confidential.
- A public client runs where anyone can read its code, such as a single-page app in the browser, a mobile app or a desktop app. A secret there isn’t secret, so a public client has none.
New clients are confidential. Switch to public on the Authentication tab.
Client identifier
Section titled “Client identifier”A client identifier you enter is 3 to 38 characters long. (A self-registered client’s, dcr_ and a UUID, is 40.) It starts with a letter, ends with a letter or digit, and uses only letters, digits, - and _, with no -- or __.
It’s case-sensitive: my-app and My-App are two different clients, and a request whose client_id differs only in case finds no client.
It can’t be a resource’s identifier either.
Settings a new client starts with
Section titled “Settings a new client starts with”A client you create in the admin console starts:
- confidential, with a generated client secret;
- enabled;
- with Ask users for consent off;
- with the default ACR level
urn:goiabada:level2_optional; - not allowed to request the administrative scopes.
The client’s page has these tabs: Settings, Logo, Tokens, Authentication, OAuth2 flows, Redirect URIs, Sign-out redirect URIs, Back-channel logout, Web origins, Sessions, Permissions, Introspection for a confidential client, and Legal documents.
Public and confidential clients
Section titled “Public and confidential clients”A confidential client proves who it is at the token endpoint with its client secret. The Authentication tab shows it behind Show. Opening the tab reads the secret, and each read leaves a viewed_client_secret entry in the audit log, whether or not you click Show. Generate a new secret makes a new one, which replaces the old one when you save, so your app needs it straight away.
A public client has no secret, so three rules make up for it:
- It always uses PKCE. PKCE ties an authorization code to the app that asked for it, so a stolen code is useless. You can’t turn it off for a public client. See PKCE.
- It can’t use the client credentials flow, since that flow is nothing but the client’s own secret.
- A user’s earlier approval doesn’t skip the consent screen, unless you turn on Remember consent for it.
Switching a client from confidential to public deletes its secret, turns off the client credentials flow and Remember consent, empties its introspection resources, and revokes the authorization codes and refresh tokens it already holds, because they were issued to a client that had to authenticate. Switching the other way revokes nothing.
The OAuth2 flows tab turns each flow on or off for the client.
- Authorization code with PKCE signs users in. Your app sends the browser to
/auth/authorize, the user signs in, and the auth server sends back an authorization code, which your app exchanges at/auth/tokenfor tokens. See Add sign-in to a web app and Add sign-in to a SPA or mobile app. - Client credentials lets a confidential client get an access token for itself, with no user involved. See Protect an API.
- Implicit flow and Resource Owner Password Credentials (ROPC) are legacy flows, deprecated in OAuth 2.1. Each one is on for a client or off, or inherits the global setting under Admin, General, which is off in a new install. Leave them off unless an old app needs one. See Implicit and ROPC.
The same tab sets PKCE for a confidential client: Required, Optional (not recommended), or inherit Require PKCE under Admin, General, which is on in a new install. New clients inherit it.
Redirect URIs
Section titled “Redirect URIs”A redirect URI is the address the auth server sends the browser back to after sign-in, with the authorization code. Your app names one in its authorization request, and it must match one registered on the client exactly: no wildcards, and scheme, host, path and query are compared byte for byte.
Every redirect URI must:
- be absolute, with a scheme.
/callbackand//example.com/callbackare refused. - name a host, when it’s
httporhttps.https:///example.com/callbackis refused. A custom scheme such ascom.example.app:/oauth2redirectneeds no host. - not use plain
http, except on loopback.http://app.example.com/callbackis refused, since the code would travel unencrypted.httpon127.0.0.1,[::1]orlocalhost, on any port, is fine. - have no fragment.
https://example.com/callback#doneis refused. An encoded%23is fine. - have well-formed percent-escapes.
https://example.com/a%zzis refused. - fit the limits: at most 2048 bytes each, at most 60 per client, and no duplicates.
A query is allowed and comes back as you registered it. The auth server adds its response to it rather than rewriting it. The one exception is a parameter the response owns: code, state, error, error_description and iss are removed from the registered query in the default query response mode, even when the response doesn’t set them. So a state in the callback is always the one your app sent, and no state comes back if your app sent none. With response_mode=fragment or form_post, the response doesn’t travel in the query, and the registered query is left exactly as it is.
The authorization code is bound to the redirect URI it was issued for. Your app must send that same redirect_uri when it exchanges the code at /auth/token.
Removing a redirect URI takes effect on sign-ins already under way: Invalid redirect_uri says what the user sees.
Loopback redirect URIs
Section titled “Loopback redirect URIs”A desktop or command-line app often receives its callback on a temporary local server, on a port the operating system picks at startup. So for an http redirect URI on a loopback host, 127.0.0.1, [::1] or localhost, the port is ignored when comparing: register http://127.0.0.1/callback and http://127.0.0.1:54321/callback matches. RFC 8252 section 7.3 asks this for the two IP addresses. Goiabada allows it for localhost too, as a convenience, though section 8.3 advises against localhost, so prefer 127.0.0.1.
The port is the only part that may differ. It applies to http only, never https, and only to response_type=code, never the implicit flow.
Sign-out redirect URIs
Section titled “Sign-out redirect URIs”A sign-out redirect URI is an address the auth server may send the browser back to after a sign-out, such as your app’s “You’re signed out” page. Your app names one as post_logout_redirect_uri. The auth server sends the user there only when it matches one on the client’s Sign-out redirect URIs tab exactly, port included. Redirect URIs aren’t used for sign-out, and an address on this list doesn’t become a sign-in callback: authorization codes go only to addresses on Redirect URIs. So a plain landing page can go here alone, and an address that serves both purposes goes on both lists.
The redirect URI rules apply to each one, with these differences:
- The list is its own. A client can have up to 60 sign-out redirect URIs, besides its redirect URIs.
- The port of a loopback address counts. Register the exact port your app listens on:
http://127.0.0.1/signed-outdoesn’t matchhttp://127.0.0.1:54321/signed-out. An app that picks its port at startup needs a fixed port to be sent back.
Through the Admin API, replace the list whole with PUT /api/v1/admin/clients/{id}/post-logout-redirect-uris, sending the list as you read it in expectedPostLogoutRedirectURIs. Each save leaves an updated_post_logout_redirect_uris entry in the audit log.
Back-channel logout
Section titled “Back-channel logout”A back-channel logout URI is an address on your app’s server where the auth server tells your app that a user’s session ended. It POSTs a signed logout token there, as OpenID Connect Back-Channel Logout 1.0 describes. A client without one is sent nothing. Get told when a session ends shows how your app receives it.
- It uses
https. A confidential client may use plainhttpon any host, such ashttp://app:3000/backchannel-logouton a private network. A public client can’t, and neither can a self-registered client, whoever sets its URI. - It’s an absolute URI that names a host, at most 2048 bytes, with no user name or password and no fragment. A query is allowed, and kept exactly as you enter it.
- The client needs a flow that signs users in, the authorization code flow or the implicit flow. A client with neither has no session to end.
- A self-registered client’s URI reaches only public addresses. See Self-registered clients.
A client can also require the session identifier, sid, in every logout token it’s sent. Turn that on when your app finds its sessions by sid alone. Telling the apps that knew the user says which tokens it’s sent then.
In the admin console, open the client’s Back-channel logout tab, enter the URI, such as https://app.example.com/backchannel-logout, tick Require the session identifier if your app needs sid, then click Save. Empty the field and save to send nothing.
Through the Admin API, set both with PUT /api/v1/admin/clients/{id}/backchannel-logout: backchannelLogoutUri, empty for none, and backchannelLogoutSessionRequired. Setting or changing the URI takes authserver:manage, because the auth server sends requests to whatever address it names, inside your network included. authserver:manage-clients can still clear it and change backchannelLogoutSessionRequired. Each save leaves an updated_client_backchannel_logout entry in the audit log.
Web origins
Section titled “Web origins”Register a web origin when JavaScript in the browser calls /auth/token, /auth/revoke, /auth/logout or /userinfo directly, such as a single-page app. Without it, the browser blocks the call (CORS).
An origin is a scheme, a host and an optional port, with nothing after the host: https://app.example.com or https://app.example.com:8443. The auth server stores it lowercased, without a default port, and with any path, query or fragment dropped. It refuses what a browser would send differently, such as a scheme other than http or https, a non-ASCII host, an IPv6 literal or user information.
Web origins are permitted server-wide. A browser’s CORS check doesn’t say which client is calling, so an origin registered on any client is accepted for every client. The Web origins tab lists every origin permitted server-wide, and the client each was registered for.
The discovery document, at both its addresses, and /certs accept every origin, with nothing to register.
Consent required
Section titled “Consent required”Turn Ask users for consent on when users should approve what a client can access before it gets a token.
You usually don’t need it for your own clients. Turn it on for third-party ones, so users can see who’s asking for their data. Users see the consent screen anyway when a client asks for offline_access or sends prompt=consent.
They also see it when a client asks for authserver:manage-account, unless they’ve approved that scope for the client before and it has Remember consent on. That scope lets a client call the Account API for the user: change their profile, end their sessions and withdraw their consents. Every user holds it, so without the screen any client could get it for whoever is signed in. The admin console’s own client is the one exception: it signs users in without the screen, so their Account pages work as they always do.
Clients you create start with consent off. Clients that register themselves start with it on, since nobody has reviewed them yet.
Remember consent decides whether a user’s earlier approval can skip the screen. It’s always on for a confidential client. A public client starts with it off, because it has no secret: nothing proves a request comes from your app rather than another app that uses its client ID and receives its redirects. With it off, users see the screen on every sign-in that asks for consent, even when they’ve approved the app before, and a prompt=none request that needs consent is refused.
To turn it on, tick it under Ask users for consent on a public client’s Settings tab, click Save, and confirm with Save, or call the Admin API with rememberConsent.
Whether a sign-in shows the consent screen depends on the two settings, the request, and what the user approved before. In the approval column, Yes means stored consent covers every requested scope when Ask users for consent is on. When it’s off, Yes means it covers authserver:manage-account if the client asks for that scope:
| Ask users for consent | Remember consent | offline_access asked for |
prompt=consent |
authserver:manage-account asked for |
Stored consent covers what’s required | Consent screen |
|---|---|---|---|---|---|---|
| Off | Either | No | No | No | Either | Skipped |
| Off | Off | No | No | Yes | Either | Shown |
| Off | On | No | No | Yes | No | Shown |
| Off | On | No | No | Yes | Yes | Skipped |
| On | Off | No | No | Either | Either | Shown |
| On | On | No | No | Either | No | Shown |
| On | On | No | No | Either | Yes | Skipped |
| Either | Either | Yes | Either | Either | Either | Shown |
| Either | Either | Either | Yes | Either | Either | Shown |
authserver:manage-account counts here only when a client other than the admin console’s asks for it.
The scopes are the ones left once the auth server has dropped those the user doesn’t hold, as Tokens describes. On the screen the user can untick scopes, and the tokens carry only those they approved. Their answer is kept, and the table above says when it skips the screen. A user can withdraw it under Account, Consents, which also revokes the refresh tokens they hold with that client. A user who unticks authserver:manage-account is asked for it again at the client’s next sign-in that asks for it.
Choosing Don’t allow ends the sign-in. A client you created gets access_denied, “The user did not provide consent”, at its redirect URI. A client that registered itself gets nothing: the user sees a page naming the app and the address it asked to send them to, and the request stops there. So does a client whose redirect URI has been removed since the request began.
Default ACR level
Section titled “Default ACR level”The ACR level is how strongly a user must sign in to use the client:
| Level | In the admin console | The user signs in with |
|---|---|---|
urn:goiabada:level1 |
Level 1: password only | A password |
urn:goiabada:level2_optional |
Level 2: password, and a one-time code from users who set up two-factor authentication | A password, plus a one-time code if they’ve set up two-factor authentication |
urn:goiabada:level2_mandatory |
Level 3: password and a one-time code, always | A password and a one-time code, setting up two-factor authentication first if they haven’t |
New clients start at urn:goiabada:level2_optional. The field shows only while Authorization code with PKCE is on. Change it on the client’s Settings tab and click Save.
The default is a floor. An authorization request can ask for a stronger level with acr_values, but never a weaker one: the auth server uses the stronger of the two. A change applies to sign-ins that start after it. See ACR and AMR.
Administrative scopes
Section titled “Administrative scopes”Six permissions on the authserver resource let whoever holds them administer Goiabada: authserver:manage, authserver:admin-read, authserver:manage-users, authserver:manage-clients, authserver:manage-settings and authserver:browser-sessions. A token carrying one of them on a user’s behalf acts at the Admin API with that user’s authority. So only a client allowed to request the administrative scopes may get one:
- The admin console’s own client, always. Its allowance can’t be switched off.
- Any other client you allow. Turn on May request administrative scopes on the client’s Settings tab, click Save, and confirm with Save, or call the Admin API. Only an administrator with
authserver:managecan switch it, on or off.
Every other client starts not allowed, including one that registers itself, which has no way to ask.
A client that isn’t allowed and asks for an administrative scope is refused, never given the rest of what it asked for. It gets invalid_scope from the authorization endpoint and the password grant, and invalid_grant when it redeems a code or uses a refresh token. That holds for every flow where a client acts for a user, prompt=none included. The allowance is read again when a code or implicit tokens are issued, when a code is redeemed and at every refresh, so switching it off takes effect straight away. Access tokens already issued stay valid until they expire.
The client credentials flow isn’t affected. There a client gets only the permissions granted to the client itself, and granting it an administrative one already takes authserver:manage.
Allowing a client changes nothing else. A token still carries only the scopes the signed-in user holds, and the consent screen is shown as Consent required describes: an allowed client asking for authserver:manage-account is asked for it like any other client.
Each switch leaves an updated_client_administrative_scopes event in the audit log, and each refusal reaching a signed-in user or an authenticated client leaves an administrative_scope_refused event.
Permissions
Section titled “Permissions”The Permissions tab grants permissions to the client itself, for the client credentials flow, and is available only while Client credentials is on: a token the client gets for itself can carry only these permissions. It has no effect on tokens a client gets for a user, which carry the user’s permissions. See Resources and permissions.
Introspection resources
Section titled “Introspection resources”Introspection is an API asking the auth server whether a token is still active, at the introspection endpoint. The API calls as a confidential client, since a resource holds no credentials of its own. A client’s introspection resources are the resources whose access tokens it may introspect: an access token is active to it only when its aud names one of them. The list starts empty. One client can stand for several resources, and several clients for one resource. A client needs no list to introspect its own refresh tokens.
Set the list on the client’s Introspection tab: choose a resource, click Add resource, and click Save. Through the Admin API, read it with GET /api/v1/admin/clients/{id}/introspection-resources, and replace it whole with PUT on the same path, sending the list as you read it in expectedResourceIds. Each save leaves an updated_client_introspection_resources entry in the audit log.
- A public client has none. It can’t authenticate, so it can’t introspect, and its page has no Introspection tab.
- Only
authserver:manageadds theauthserverresource. A client listed for it can introspect every access token issued with an OpenID Connect scope oroffline_access, from every app.authserver:manage-clientsedits the rest of the list, and can removeauthserverfrom it. While the client is listed for it, onlyauthserver:managereads or sets its secret.
Tokens
Section titled “Tokens”The Tokens tab overrides the global token settings for this client:
- Token lifetime, in seconds, for its access and ID tokens.
- Offline idle timeout, in seconds and Offline maximum lifetime, in seconds, for refresh tokens from the
offline_accessscope. - OpenID Connect claims in the access token and OpenID Connect claims in the ID token, for claims such as
emailorname. Each can be on, off, or inherit the global setting, which leaves them out of access tokens and puts them in ID tokens.
A value of 0 uses the global setting. See Tokens.
Require DPoP for token requests
Section titled “Require DPoP for token requests”Turn this on when every token a client gets must be bound to its key with DPoP, so none can be had without a proof. It’s a switch on the client’s Tokens tab, saved with the tab.
- Every token request needs a
DPoPproof, on every grant. One without is refused with400invalid_dpop_proof, “This client requires a DPoP proof with every token request.” - The client can’t get access tokens through the implicit flow. They’d travel in the redirect, with no proof.
response_type=tokenandid_token tokenare refused withunauthorized_client, even for a sign-in already under way when you turn it on.id_tokenalone, and the authorization code flow, work as before. - It’s off until you turn it on. A client that sends a proof gets bound tokens either way: this setting stops it getting tokens without one.
- The admin console’s client can’t have it, since the console calls the auth server with Bearer tokens. Its Tokens tab doesn’t show the switch.
Through the Admin API, set it with PUT /api/v1/admin/clients/{id}/dpop and dpopBoundAccessTokens, which every client the API answers carries. A self-registered client asks for it with dpop_bound_access_tokens. Each save leaves an updated_client_dpop entry in the audit log.
Display name, description and logo
Section titled “Display name, description and logo”Users see a client on the password, one-time code and consent screens. By default it’s shown by its client identifier, or by the display name you entered when you created it, which turns Display name on under Sign-in and consent pages. On the Settings tab you can give it a Display name and a Description, up to 100 characters each, and a Website URL, which must be http or https. Each one shows only when its switch under Sign-in and consent pages is on: Display name, Description and Website URL.
A client can also have its own Privacy policy URL and Terms of service URL, on the Settings tab. Each must be http or https, up to 256 characters. The consent screen links each one whenever it’s set, with no switch: users get the app’s own documents the moment they decide what it gets. Goiabada doesn’t host these pages or ask users to accept them. For documents you write and users accept, see Publish legal documents.
The Logo tab uploads a logo: a JPEG, PNG, GIF or WebP file of up to 3 MiB, which you crop and which is uploaded scaled to at most 512 pixels on its longest side, a GIF as PNG. See Logo and picture for what the Admin API takes. Logo under Sign-in and consent pages, on the Settings tab, puts it on the screens.
The logo is public, at GET /client/logo/{clientIdentifier}, with no sign-in, even while that switch is off. It answers 404 when the client has no logo, and carries an ETag and Cache-Control: public, max-age=300, must-revalidate.
Disabling and deleting a client
Section titled “Disabling and deleting a client”Untick Enabled on the Settings tab to stop a client while keeping its settings. The authorization endpoint then shows an error page instead of signing users in, and the token endpoint answers 401 with invalid_client, so its refresh tokens stop working too. The auth server’s own /userinfo and its Admin and Account APIs refuse the client’s access tokens while it stays disabled, and accept them again if you turn it back on before they expire. An API that introspects them gets {"active": false} for as long. An API that checks tokens by their signature alone accepts them until they expire.
Delete on the list of clients removes the client with its redirect URIs, sign-out redirect URIs, web origins, permissions, introspection resources, logo, unredeemed codes, refresh tokens and users’ consents to it. The auth server’s own endpoints refuse its access tokens from then on, even after a new client takes its identifier; an API that checks tokens by their signature alone accepts them until they expire.
The admin console’s client
Section titled “The admin console’s client”The admin console signs administrators in through its own client, admin-console-client. It’s a system-level client, so its identifier can’t be changed, it can’t be deleted, and it’s always allowed to request the administrative scopes. Its other settings can be changed, by an administrator with authserver:manage.
Self-registered clients
Section titled “Self-registered clients”With dynamic client registration (DCR) turned on, an app can register itself as a client by calling POST /connect/register, as RFC 7591 describes, with nobody creating it in the admin console. It’s how tools like MCP clients get a client of their own.
DCR is off until you turn on Dynamic client registration under Admin, General. Until then, /connect/register answers 403 with access_denied.
When it’s on, anyone who can reach the endpoint can register a client. Limit /connect/register at your reverse proxy, or turn on Goiabada’s rate limiter with GOIABADA_AUTHSERVER_RATELIMITER_ENABLED, which then allows 10 registrations a minute from each IP address. See rate limits.
A registration looks like this:
curl -X POST https://auth.example.com/connect/register \ -H 'Content-Type: application/json' \ -d '{ "client_name": "My CLI tool", "redirect_uris": ["http://127.0.0.1/callback"], "token_endpoint_auth_method": "none", "grant_types": ["authorization_code"] }'It answers 201 with the new client_id and, for a confidential client, its client_secret. Every field it reads, its answer and its errors are on Dynamic client registration.
Because nobody has reviewed them, self-registered clients are treated more carefully:
- They’re marked. The list of clients shows a Self-registered badge on each one.
- Their identifier can’t be changed. It’s generated,
dcr_followed by a UUID. - They start with consent on. The consent screen shows the name the client gave itself, with a note that it registered itself and hasn’t been verified. Give it a display name and turn on Display name under Sign-in and consent pages to replace both. A public one also starts with Remember consent off, so its users see the screen on every sign-in.
- They’re never sent an error. An error response is a redirect to the client, and anyone can point a self-registered client anywhere. So when the user declines or the auth server refuses the request, the user sees a page naming the app and the address it asked for, and the request stops there.
prompt=noneincluded. - Their back-channel logout URI reaches only public addresses. Otherwise anyone could register a client that has the auth server send requests into your network, to a cloud metadata service for one. This holds whoever set the URI, and after you’ve reviewed the client too. For an app on your own network, create the client in the admin console instead. See Where the auth server connects.
- A sign-out without a hint doesn’t send the user back to them. Their sign-out redirect URIs prove nothing about where the user should go, so a sign-out with no
id_token_hintends on the auth server’s “Signed out” page. One with a hint the auth server confirms still goes back to one of the client’s sign-out redirect URIs.
Once you’ve reviewed a self-registered client, you can change its settings like any other, Ask users for consent included.
Redirect URIs for self-registered clients
Section titled “Redirect URIs for self-registered clients”The redirect URI rules apply, plus stricter ones, since the caller is anonymous. A client is public when it registers with token_endpoint_auth_method set to none, and confidential otherwise. The same rules apply to the sign-out redirect URIs it registers.
| Client | Allowed | Refused |
|---|---|---|
| Public | http on 127.0.0.1, [::1] or localhost; custom schemes such as myapp://callback |
https; http on any other host |
| Confidential | https on any host; http on 127.0.0.1, [::1] or localhost |
custom schemes; http on any other host |
On top of that:
- The loopback host must match exactly, ignoring case.
localhost.example.comisn’t loopback. - Characters that can’t appear in a URI are refused:
<,>,",{,},|,\,^, backtick and space. - Some schemes are always refused, because a browser can’t deliver a response to them or they run script:
javascript,data,vbscript,file,blob,about,chrome,chrome-extension,moz-extension,view-source,filesystem,resource,ftp,ftps,ws,wss,gopherandtelnet.
A single-page app can’t register itself, since it needs an https redirect URI as a public client. Create it in the admin console instead.