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 Create new.
-
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 flow with PKCE starts on:
- Authorization code flow with PKCE if users sign in to your app.
- Client credentials flow if your app is a service that calls an API on its own behalf.
-
Click Create. You’re back on the list of clients. Click Manage next to your new client.
-
If your app runs in a browser or on a phone, open Authentication, choose Public client, click Save, and confirm with Yes. 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.
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 Consent required 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, Web origins, User sessions and Permissions.
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 Reveal. Opening the tab reads the secret, and each read leaves a viewed_client_secret entry in the audit log, whether or not you click Reveal. Generate 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 two 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.
Switching a client from confidential to public deletes its secret, turns off the client credentials flow, 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 requirement for this client? for a confidential client: Required, Optional (not recommended), or inherit PKCE required for confidential clients using the authorization code flow 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.
The scheme, host and fragment rules are checked again on every authorization request, so a stored redirect URI that breaks them gets an error page instead of a redirect. The http rule applies only when you add a redirect URI. One the client already has keeps working, but once you remove it, you can’t add it back.
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.
Web origins
Section titled “Web origins”Register a web origin when JavaScript in the browser calls /auth/token, /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 Consent required 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, until they’ve approved it for that client. 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.
Whether a sign-in shows the consent screen depends on the setting, the request, and what the user approved before. In the approval column, Yes means stored consent covers every requested scope when Consent required is on. When it’s off, Yes means it covers authserver:manage-account if the client asks for that scope:
| Consent required | offline_access asked for |
prompt=consent |
authserver:manage-account asked for |
Stored consent covers what’s required | Consent screen |
|---|---|---|---|---|---|
| Off | No | No | No | Either | Skipped |
| Off | No | No | Yes | No | Shown |
| Off | No | No | Yes | Yes | Skipped |
| On | No | No | Either | No | Shown |
| On | No | No | Either | Yes | Skipped |
| Either | Yes | Either | Either | Either | Shown |
| Either | Either | Yes | Either | Either | Shown |
authserver:manage-account counts here only when a client other than the admin console’s asks for it. With Consent required off, a prior approval for that scope skips the screen even if the user hasn’t approved the client’s other requested scopes.
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, so the next sign-in skips the screen while it covers every scope, and a user can withdraw it under Account, Manage consents. A user who unticks authserver:manage-account is asked for it again at the client’s next sign-in that asks for it.
Choosing Cancel consent 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 |
ACR level 1 - password only | A password |
urn:goiabada:level2_optional |
ACR level 2 - password + OTP (if enabled by the user) | A password, plus a one-time code if they’ve set up two-factor authentication |
urn:goiabada:level2_mandatory |
ACR level 3 - password + mandatory OTP | 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 Yes, or call the Admin API. Only an administrator with
authserver:managecan switch it, on or off. Any other administrator can still save the tab’s other settings: the switch is saved only when it changed, and otherwise the page says the settings were saved and shows the refusal under the switch.
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.
Tokens
Section titled “Tokens”The Tokens tab overrides the global token settings for this client:
- Token expiration in seconds, for its access and ID tokens.
- Offline refresh token - idle timeout in seconds and max lifetime in seconds, for refresh tokens from the
offline_accessscope. - Include OpenID Connect claims in the access token? and 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.
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 Show display name on. 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 is on: Show display name, Show description and Show website URL.
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. Show logo on the Settings tab puts it on the screens.
The logo is public, at GET /client/logo/{clientIdentifier}, with no sign-in, even while Show logo 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. Access tokens it already has stay valid until they expire.
Delete on the list of clients removes the client with its redirect URIs, web origins, permissions, logo, unredeemed codes, refresh tokens and users’ consents to it. Access tokens it already has stay valid 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 enabled 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"] }'The auth server reads four fields and ignores the rest:
redirect_uris, required when the client uses the authorization code flow.token_endpoint_auth_method:nonefor a public client, orclient_secret_basic(the default) orclient_secret_postfor a confidential one.grant_types:authorization_code(the default),refresh_tokenandclient_credentials. A public client can’t ask forclient_credentials, and the legacy flows can’t be asked for at all: both are off on the client, whatever the global settings, until an administrator turns one on for it.client_name: the name the consent screen shows, up to 100 characters, which becomes the client’s Description.
It answers 201 with the new client_id and, for a confidential client, its client_secret. Metadata it refuses gets a 400 with invalid_client_metadata, or invalid_redirect_uri for a redirect URI. Every field, answer and error is 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 Show display name to replace both.
- 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.
Once you’ve reviewed a self-registered client, you can change its settings like any other, Consent required 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.
| 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.