# Introduction Source: https://goiabada.dev/get-started/introduction/ Goiabada is an open-source server that signs your users in, so your apps don’t have to. You run it yourself. Your apps send users to Goiabada to sign in, and get back tokens that say who the user is and what they’re allowed to do. Goiabada speaks OAuth2 and OpenID Connect, the standards almost every language and framework already has a library for. ## What it does - **Sign-in with passwords and two-factor authentication.** Users sign in with a password, and optionally a code from an authenticator app. Each app chooses whether a code is required. See [Require two-factor authentication](https://goiabada.dev/guides/require-two-factor-authentication/). - **Single sign-on.** Users sign in once and can use all your apps without signing in again. See [Single sign-on across clients](https://goiabada.dev/guides/single-sign-on-across-clients/). - **Permissions.** You decide who can do what in your APIs, with resources, permissions and groups. See [Resources and permissions](https://goiabada.dev/concepts/resources-and-permissions/). - **Custom claims.** You add groups and your own user and group attributes to ID tokens, access tokens or both. See [Attributes](https://goiabada.dev/concepts/users-and-groups/#attributes). - **API protection.** Apps and services get tokens for calling your APIs, with or without a user. See [Protect an API](https://goiabada.dev/guides/protect-an-api/). - **Self-service accounts.** Users update their own profile, picture, email, phone, address, password and two-factor authentication, end their sessions and revoke the consents they gave. - **Self-registration and password recovery.** People create their own accounts, with or without email verification, and reset a forgotten password by email. See [Self-registration](https://goiabada.dev/concepts/self-registration/) and [Password recovery](https://goiabada.dev/concepts/password-recovery/). - **Dynamic client registration.** Apps such as MCP clients can register themselves, when you turn it on. See [Let clients register themselves (DCR)](https://goiabada.dev/guides/let-clients-register-themselves-dcr/). - **An admin API.** Your scripts and tools can do everything the admin console does, with an OpenAPI reference and permissions as narrow as read-only. See [API authentication](https://goiabada.dev/reference/api/authentication/). - **An audit log.** Sign-ins, failed passwords and every change an administrator makes are recorded. See [Audit log](https://goiabada.dev/concepts/audit-log/). - **A setup wizard.** It asks a few questions and writes a ready-to-run configuration for Docker Compose, Kubernetes or the native binaries, keys and passwords included. See [Setup wizard](https://goiabada.dev/deploy/setup-wizard/). - **Ready for Kubernetes.** Generated manifests with probes, graceful shutdown and disruption budgets, Prometheus metrics, and servers that scale out to several replicas. See [Kubernetes](https://goiabada.dev/deploy/kubernetes/overview/) and [Monitoring](https://goiabada.dev/deploy/monitoring/). ## How the parts fit together Goiabada has three parts: - The **auth server** signs users in and issues tokens. It serves the OAuth2 and OpenID Connect endpoints, the sign-in pages, and an API for managing everything. - The **admin console** is the web app where you manage clients, users, groups, permissions and settings, and where users manage their own account. It’s a client of the auth server and talks to it over HTTP. - The **database** holds everything: MySQL, PostgreSQL, SQL Server or SQLite. Only the auth server connects to it. Both servers are written in Go, and each ships as a Docker image and a native binary for Linux, macOS and Windows. If OAuth2 and OpenID Connect are new to you, the [glossary](https://goiabada.dev/concepts/glossary/) explains each term in plain English. ## Docs for AI agents These docs are also published as plain text, for AI agents and tools that read documentation: - [`/llms.txt`](https://goiabada.dev/llms.txt) lists every page, with its title, URL and a one-line description. - [`/llms-full.txt`](https://goiabada.dev/llms-full.txt) holds every page in full, as Markdown, in one file. ## Next steps [Quickstart](https://goiabada.dev/get-started/quickstart/): Run Goiabada on your machine in a few minutes. [Setup wizard](https://goiabada.dev/deploy/setup-wizard/): Generate the configuration for a real deployment. # Quickstart Source: https://goiabada.dev/get-started/quickstart/ This page gets Goiabada running on your machine in a few minutes, so you can try it out. All you need is Docker with Docker Compose. > **Caution** > > This test setup serves plain HTTP on `localhost`. **Never** use it for a deployment other people reach: for that, see [Choose a method](https://goiabada.dev/deploy/choose-a-method/). 1. Download the [setup wizard](https://goiabada.dev/deploy/setup-wizard/) for your platform. **Linux (x86_64)** ```bash curl -LO https://github.com/leodip/goiabada/releases/latest/download/goiabada-setup-linux-amd64 chmod +x goiabada-setup-linux-amd64 ``` **Linux (ARM64)** ```bash curl -LO https://github.com/leodip/goiabada/releases/latest/download/goiabada-setup-linux-arm64 chmod +x goiabada-setup-linux-arm64 ``` **macOS (Apple Silicon)** ```bash curl -LO https://github.com/leodip/goiabada/releases/latest/download/goiabada-setup-darwin-arm64 chmod +x goiabada-setup-darwin-arm64 ``` **macOS (Intel)** ```bash curl -LO https://github.com/leodip/goiabada/releases/latest/download/goiabada-setup-darwin-amd64 chmod +x goiabada-setup-darwin-amd64 ``` **Windows** ```bash curl -LO https://github.com/leodip/goiabada/releases/latest/download/goiabada-setup-windows-amd64.exe ``` 2. In an empty directory, run it for local testing with SQLite. Use the name of the file you downloaded. ```bash ./goiabada-setup-linux-amd64 --type=local --db=sqlite ``` It writes `docker-compose.yml` and, beside it, `docker-compose.override.yml`, which holds the secrets. 3. Start Goiabada. ```bash docker compose up -d ``` 4. Check that both servers answer. Each one replies `healthy`. ```bash curl http://localhost:9090/health curl http://localhost:9091/health ``` The first start takes a little longer, while the auth server sets up its database. 5. Get the admin password, which the wizard generated. ```bash grep GOIABADA_ADMIN_PASSWORD docker-compose.override.yml ``` 6. Open the admin console at `http://localhost:9091` and [sign in](https://goiabada.dev/get-started/first-sign-in/) as `admin@example.com` with that password. ## What’s running The wizard writes a Docker Compose file with two services, both from the images of the wizard’s own release: | Service | Address | What it does | | - | - | - | | `goiabada-authserver` | `http://localhost:9090` | The auth server: signs users in and issues tokens. | | `goiabada-adminconsole` | `http://localhost:9091` | The admin console, where you manage clients, users and settings. | `docker compose` commands take these names, as in `docker compose logs goiabada-authserver`. Docker names the containers themselves after the directory holding the file, as in `-goiabada-authserver-1`. The admin console starts once the auth server is healthy. On its first start, the auth server finds an empty database and fills it: the first administrator, the admin console’s client, its resource and permissions, and two signing keys. The SQLite database lives in a Docker volume, so it survives `docker compose down`. To start over from an empty database, remove the volume too: ```bash docker compose down -v ``` Prefer MySQL, PostgreSQL or SQL Server? Pass `--db=mysql`, `--db=postgres` or `--db=mssql` instead, and the Compose file runs that database in a third container. Or leave out every flag and the wizard asks you each question. ## Next steps [First sign-in](https://goiabada.dev/get-started/first-sign-in/): Sign in to the admin console and secure your account. [Setup wizard](https://goiabada.dev/deploy/setup-wizard/): Every question and flag of the wizard. # First sign-in Source: https://goiabada.dev/get-started/first-sign-in/ This page walks you through your first sign-in to the admin console, and the three settings worth checking straight after. 1. Find the first administrator’s email and password. The wizard’s last message names the email, and the password is here: | Deployment | Where the password is | | - | - | | Docker Compose | `GOIABADA_ADMIN_PASSWORD` in `docker-compose.override.yml` | | Native binaries | `GOIABADA_ADMIN_PASSWORD` in `goiabada.env` | | Kubernetes | In the cluster. The wizard’s last message prints the command that reads it, like this one for the namespace `goiabada`. | ```bash kubectl get secret goiabada-secrets -n goiabada -o jsonpath='{.data.admin-password}' | base64 -d ``` Didn’t use the wizard? They’re the `GOIABADA_ADMIN_EMAIL` and `GOIABADA_ADMIN_PASSWORD` you set. 2. Open the admin console: `http://localhost:9091` after the [Quickstart](https://goiabada.dev/get-started/quickstart/), or the admin console URL you gave the wizard. 3. Click **Admin**. The admin console sends you to the auth server’s sign-in page. 4. Enter the email and password, and click **Sign in**. You’re back in the admin console, on the list of clients. ## Right after you sign in ### Turn on two-factor authentication Your account can manage everything, so give it a second factor. Under **Account**, open **Two-factor authentication**, scan the QR code with an authenticator app, and enter your password and the app’s code. From then on, signing in to the admin console asks for a code from that app as well as your password. ### Decide who can register A new installation lets nobody register. Only you create users until you turn on **User self registration enabled** under **Admin**, **General**. Registering then needs email set up, since a new installation also asks people to verify their address. Accounts people create can manage their own account, including their profile, sessions and consents, but can’t administer Goiabada. See [Self-registration](https://goiabada.dev/concepts/self-registration/). ### Set up email Goiabada sends email to verify addresses and to reset passwords. Add your SMTP server under **Admin**, **Email - SMTP**, and send yourself a test message from there. Turning **SMTP enabled** off later empties every field on that page, the password included, so note them first if you’ll turn it back on. ## How the sign-in works The admin console is a client of the auth server, like the apps you’ll connect later. Clicking **Admin** starts a standard OpenID Connect sign-in: the browser goes to the auth server, you sign in there, and the auth server sends you back to the admin console with an authorization code. To open the **Admin** pages, a user needs the `authserver:manage` permission. The first administrator has it, along with `authserver:manage-account`, which opens a user’s own **Account** pages and which every new user gets. A user without `authserver:manage` who clicks **Admin** sees an “Unauthorized (403)” page. The admin console’s client asks for a second factor only from users who have one set up. That’s why your first sign-in needs only a password, and why signing in needs a code too once you turn on two-factor authentication. The auth server reads `GOIABADA_ADMIN_EMAIL` and `GOIABADA_ADMIN_PASSWORD` only on its first start, when it creates the first administrator in an empty database. Changing them later does nothing, so change your password under **Account**, **Change password**. Changed them and can’t sign in? See [Locked out of the admin console](https://goiabada.dev/troubleshooting/locked-out-of-the-admin-console/). The admin console’s home page also links to the auth server’s discovery document, at `/.well-known/openid-configuration` on the auth server, such as `http://localhost:9090/.well-known/openid-configuration`. It lists the auth server’s endpoints, scopes and claims, and every OpenID Connect library can configure itself from it. ## Next steps [Clients](https://goiabada.dev/concepts/clients/): Register your app with Goiabada. [Users and groups](https://goiabada.dev/concepts/users-and-groups/): Create users and organize them. # Add sign-in to a web app Source: https://goiabada.dev/guides/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](https://goiabada.dev/concepts/glossary/#authorization-code): a one-time code your server trades for tokens. That’s the authorization code flow, and [PKCE](https://goiabada.dev/concepts/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](https://goiabada.dev/concepts/clients/#public-or-confidential). 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 1. Register your app. In the admin console, open **Admin**, **Clients** and click **Create new**. Enter a **Client identifier**, such as `my-web-app`, leave **Authorization code flow with PKCE** on, and click **Create**. 2. Click **Manage** beside your new client. On **Authentication**, click **Reveal** and copy the **Client secret** to your server’s configuration. On **Redirect URIs**, add the address your app receives the code at, such as `https://app.example.com/callback`, and click **Save**. 3. When a user clicks your app’s sign-in button, make three random values: a `state`, a `nonce` and 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: ```http 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.1 Host: auth.example.com ``` 4. The user signs in, and the browser comes back to your redirect URI: ```http GET /callback?code=SplxlOBeZQQYbYS6WxSbIA&state=af0ifjsldkj HTTP/1.1 Host: app.example.com ``` Check that `state` is the one you kept. If it isn’t, stop: the request didn’t come from your app. If the browser brings `error` and `error_description` instead of `code`, the sign-in was refused, and the description says why. 5. From your server, redeem the code at the token endpoint within 60 seconds, with your client secret and the code verifier: ```bash 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_wW1gFWFOEjXk ``` The answer carries an `id_token`, an `access_token` and a `refresh_token`. 6. Check the ID token, as [Tokens](https://goiabada.dev/concepts/tokens/#check-a-token) describes: its signature against the keys at `/certs`, `iss`, `exp`, an `aud` equal to your client identifier, and a `nonce` equal to the one you kept. 7. Sign the user in to your app as the user its `sub` names, and keep the tokens on your server. When the access token expires, trade the refresh token for new ones, as [Refresh tokens](https://goiabada.dev/concepts/refresh-tokens/#refresh-a-token) shows. That’s sign-in done. The next thing your users will want is a way to sign out: see [Sign users out](https://goiabada.dev/guides/sign-users-out/). ## 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: ```json { "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": "alice@example.com", "email_verified": true } ``` Identify the user by `sub`, the user’s [subject](https://goiabada.dev/concepts/glossary/#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](https://goiabada.dev/concepts/scopes/) 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](https://goiabada.dev/concepts/acr-and-amr/). Keep the ID token itself: signing the user out sends it back. ## 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: - **`state`** comes 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. - **`nonce`** comes 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 **PKCE required for confidential clients using the authorization code flow** is on, under **Admin**, **General**, as it is in a new install. Send it anyway: [PKCE](https://goiabada.dev/concepts/pkce/) explains why, and how to make the challenge. ## 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](https://goiabada.dev/reference/endpoints/token/#client-authentication). > **Danger** > > **Never** send the client secret to the browser, or put it in your app’s front-end code. Anyone who has it can redeem codes as your app. ## 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](https://goiabada.dev/concepts/sessions/) 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](https://goiabada.dev/concepts/glossary/#single-sign-on-sso). 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](https://goiabada.dev/concepts/refresh-tokens/). ## When something goes wrong - The sign-in page says “Invalid redirect_uri parameter.”: see [Invalid redirect_uri](https://goiabada.dev/troubleshooting/invalid-redirect-uri/). - The browser never comes back with an error: see [The app gets no error back](https://goiabada.dev/troubleshooting/the-app-gets-no-error-back/). - The browser comes back with `error=invalid_scope`: see [invalid_scope](https://goiabada.dev/troubleshooting/invalid-scope/). - A refresh answers “This refresh token has been revoked.”: see [This refresh token has been revoked](https://goiabada.dev/troubleshooting/this-refresh-token-has-been-revoked/). The [authorize](https://goiabada.dev/reference/endpoints/authorize/) and [token](https://goiabada.dev/reference/endpoints/token/) endpoint pages list every parameter and every refusal. ## Next steps [Sign users out](https://goiabada.dev/guides/sign-users-out/): End the user's session from your app, and bring them back. [Protect an API](https://goiabada.dev/guides/protect-an-api/): Let your app call your API with the user's access token. [Tokens](https://goiabada.dev/concepts/tokens/): What each token carries, and how long it lasts. # Add sign-in to a SPA or mobile app Source: https://goiabada.dev/guides/add-sign-in-to-a-spa-or-mobile-app/ This guide adds sign-in to an app whose code runs on the user’s device: a single-page app (SPA) in the browser, a mobile app, or a desktop app. It’s the same trip as [a web app’s](https://goiabada.dev/guides/add-sign-in-to-a-web-app/): the browser goes to the auth server, the user signs in, and your app gets an authorization code it trades for tokens. The difference is that anyone can read your app’s code, so it can’t keep a client secret. It’s registered as a [public client](https://goiabada.dev/concepts/clients/#public-or-confidential), with no secret at all, and [PKCE](https://goiabada.dev/concepts/pkce/) is what ties each code to the app that asked for it. ## Sign users in 1. Register your app. In the admin console, open **Admin**, **Clients** and click **Create new**. Enter a **Client identifier**, such as `my-spa`, leave **Authorization code flow with PKCE** on, and click **Create**. 2. Click **Manage** beside your new client. On **Authentication**, choose **Public client**, click **Save**, and confirm with **Yes**. 3. On **Redirect URIs**, add the address your app receives the code at, and click **Save**: | Your app | Redirect URI | | - | - | | A SPA | A page of your app, such as `https://app.example.com/callback` | | A mobile app | A URI with your app’s own scheme, such as `com.example.app:/oauth2redirect` | | A desktop app | A loopback address, such as `http://127.0.0.1/callback`. Any port matches. | 4. A SPA only: on **Web origins**, add your app’s origin, such as `https://app.example.com`, and click **Save**. Without it, the browser blocks your app’s call to the token endpoint. 5. When a user signs in, make a `state`, a `nonce` and a PKCE code verifier, with the challenge made from it, and keep them until the browser comes back. Then open the authorization request in the browser. A mobile or desktop app opens it in the system browser: ```http GET /auth/authorize?client_id=my-spa&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.1 Host: auth.example.com ``` 6. The user signs in, and the browser comes back to your redirect URI with `code` and `state`. Check that `state` is the one you kept, then redeem the code within 60 seconds. A public client sends its `client_id` and no secret: ```bash curl -X POST https://auth.example.com/auth/token \ -d grant_type=authorization_code \ -d client_id=my-spa \ -d code=SplxlOBeZQQYbYS6WxSbIA \ -d redirect_uri=https://app.example.com/callback \ -d code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk ``` 7. Check the ID token as [a web app does](https://goiabada.dev/guides/add-sign-in-to-a-web-app/#what-the-id-token-says), `nonce` included, and the user is signed in as its `sub`. When the access token expires, refresh it with your `client_id` and no secret: ```bash curl -X POST https://auth.example.com/auth/token \ -d grant_type=refresh_token \ -d client_id=my-spa \ -d refresh_token=eyJhbGciOiJSUzI1NiIs... ``` ## No secret, always PKCE A public client has no secret, so nothing it sends to the token endpoint proves who it is. PKCE makes up for that: your app sends a hash of a fresh secret with each authorization request, and the code is redeemed only with the secret itself. A code someone intercepts on its way back is no use without it. So a public client **always** uses PKCE, and the auth server refuses a public client’s authorization request without a `code_challenge`. There’s no setting to turn it off. A public client also can’t use the client credentials flow, and a request that sends it a `client_secret` is refused with `invalid_request`. See [Clients](https://goiabada.dev/concepts/clients/#public-and-confidential-clients). ## In the browser Your SPA calls the token endpoint from JavaScript, from another origin than the auth server’s, so the browser lets it read the answer only when the auth server allows that origin (CORS). The auth server allows every origin registered as a [web origin](https://goiabada.dev/concepts/clients/#web-origins), at `/userinfo` and `/auth/logout` too. A web origin is permitted server-wide, for every client, whichever client you register it on. Keep the tokens where other sites can’t reach them. Keeping them in memory is the safest: the user signs in again when the page reloads, which single sign-on usually makes a redirect and back, with no password. > **Caution** > > Refresh from **one** place. When two tabs refresh with the same refresh token, the second can look like a stolen token being replayed, and the auth server then revokes every refresh token of the sign-in, so the user has to sign in again. See [Rotation](https://goiabada.dev/concepts/refresh-tokens/#rotation). ## On a phone or a desktop Open the authorization request in the system browser, never in a web view inside your app. [RFC 8252](https://www.rfc-editor.org/rfc/rfc8252.html#section-8.12) says a native app must not use one, since your app could read the password the user types into it. The system browser also shares the auth server’s session with every other app, so a user who’s already signed in isn’t asked for their password again. A mobile app’s redirect URI uses a scheme of its own, such as `com.example.app:/oauth2redirect`, which the phone hands to your app. Use a reverse domain name you control, so no other app claims it. A custom scheme needs no host. A desktop app can receive the code on a local server it starts on a free port. For an `http` redirect URI on `127.0.0.1`, `[::1]` or `localhost`, the auth server ignores the port when it compares. RFC 8252 asks this for the two IP addresses, and Goiabada allows it for `localhost` too, as a convenience: register `http://127.0.0.1/callback`, and `http://127.0.0.1:54321/callback` matches. Prefer `127.0.0.1` to `localhost`. See [loopback redirect URIs](https://goiabada.dev/concepts/clients/#loopback-redirect-uris). ## Refresh tokens A public client gets a refresh token like any other, and it works the same way: every refresh hands your app a new refresh token and retires the one it sent, so store each new one before you use the rest of the answer. See [Refresh tokens](https://goiabada.dev/concepts/refresh-tokens/). ## Next steps [Sign users out](https://goiabada.dev/guides/sign-users-out/): End the user's session from your app, and bring them back. [Protect an API](https://goiabada.dev/guides/protect-an-api/): Let your app call your API with the user's access token. [PKCE](https://goiabada.dev/concepts/pkce/): How the challenge is made, and what the auth server checks. # Sign users out Source: https://goiabada.dev/guides/sign-users-out/ This guide adds a sign-out button that signs the user out of the auth server too, and brings them back to your app. Ending your app’s own session isn’t enough. The auth server keeps a [session](https://goiabada.dev/concepts/sessions/) of its own, so the next time your app sent the user to sign in, they’d be signed straight back in. Your app sends the browser to the logout endpoint, `/auth/logout`, with the ID token the user signed in with, and the auth server signs them out and sends them back. ## Sign users out 1. When the user signs in, keep the ID token from the token endpoint’s answer with the rest of their session in your app. It tells the auth server which sign-in to end. 2. Decide where the user lands afterwards, such as `https://app.example.com/signed-out`, and add it to your client’s **Redirect URIs** in the admin console, if it isn’t there already. The auth server sends the browser only to an address registered there. 3. When the user clicks your app’s sign-out button, read the ID token from their session, end your app’s own session, and delete every token your app kept for them. 4. Answer with a redirect that sends the browser to `/auth/logout`, with the ID token as `id_token_hint`, the address from step 2 as `post_logout_redirect_uri`, and a new random `state`: ```http HTTP/1.1 302 Found Location: https://auth.example.com/auth/logout?id_token_hint=eyJhbGciOiJSUzI1NiIsImtpZCI6Ii4uLiJ9...&post_logout_redirect_uri=https%3A%2F%2Fapp.example.com%2Fsigned-out&state=af0ifjsldkj ``` The browser follows it with a GET, which is what most OpenID Connect libraries send. To keep the ID token out of the browser’s history, post the parameters instead: see [Hiding the hint](https://goiabada.dev/guides/sign-users-out/#hiding-the-hint). 5. The auth server signs the user out at once, without asking, and sends the browser back with your `state`: ```http HTTP/1.1 302 Found Location: https://app.example.com/signed-out?state=af0ifjsldkj ``` Check that `state` is the one you sent, and show the user they’re signed out. ## What a sign-out ends With the ID token as its hint, a sign-out ends your app’s part of the user’s session on the auth server. If other apps signed in with the same session, the user stays signed in to them, and the session ends when the last one signs out. Either way the browser’s session cookie is cleared, so the next sign-in in that browser starts with the password. A sign-out with no hint, or with one the auth server can’t confirm, asks the user “Are you sure you want to sign out?” first, and when they click **Yes**, it ends the whole session, every app’s part of it. RP-Initiated Logout, the OpenID Connect standard the endpoint follows, asks for that, since anyone can send a browser to `/auth/logout`. [Ending sessions](https://goiabada.dev/concepts/ending-sessions/#signing-out) has the precise rules. An ID token that has expired still works as a hint while the session it came from is there, so a user who leaves your app open all day can still sign out without the question. ## Your app’s tokens Signing out revokes no token. That’s why step 3 deletes them: - **A refresh token** keeps working while the session it’s tied to does, which, when other apps share that session, is after your app has signed out. An offline refresh token, from the `offline_access` scope, isn’t tied to a session at all. - **An access token** is checked by its signature, so your APIs accept it until it expires. To cut off everything a session handed out, end the session instead. A user can, under **Account**, **Sessions** in the admin console, and an administrator can, on the user’s **Sessions** tab. See [Ending sessions](https://goiabada.dev/concepts/ending-sessions/#end-a-session). ## Where the user lands The browser goes to your `post_logout_redirect_uri` only when it’s one of the redirect URIs registered on the client the hint was issued to. Otherwise the user stays on the auth server’s “Signed out” page. The sign-out has happened either way. [Logout](https://goiabada.dev/reference/endpoints/logout/#where-the-user-lands) gives every case. The auth server adds `state` exactly as you sent it, and nothing else. ## Hiding the hint The hint is the user’s ID token, and anyone who decodes it can read the claims about the user in it. In the query string of a GET, it’s kept in the browser’s history, and in the access log of any proxy in front of the auth server. The auth server’s own request log leaves it out. To keep it out of both, post the parameters from a form your page submits on its own: ```http POST /auth/logout HTTP/1.1 Host: auth.example.com Content-Type: application/x-www-form-urlencoded id_token_hint=eyJhbGciOiJSUzI1NiIsImtpZCI6Ii4uLiJ9...&post_logout_redirect_uri=https%3A%2F%2Fapp.example.com%2Fsigned-out&state=af0ifjsldkj ``` A confidential client can go further and encrypt the hint with a key made from its client secret, so it’s unreadable wherever it’s seen or logged on the way. Send `client_id` beside an encrypted hint, so the auth server knows whose secret decrypts it. [Encrypting the hint](https://goiabada.dev/reference/endpoints/logout/#encrypting-the-hint) gives the scheme. ## When it’s refused A POST from another origin with no hint, whether another site or another host of the same domain, is refused with `403 Forbidden`, to stop a page you don’t control from signing your users out. See [Sign-out answers 403](https://goiabada.dev/troubleshooting/sign-out-answers-403/). ## Next steps [Logout](https://goiabada.dev/reference/endpoints/logout/): Every parameter of /auth/logout, and every answer. [Ending sessions](https://goiabada.dev/concepts/ending-sessions/): What a sign-out ends, and what it leaves alone. [Sessions](https://goiabada.dev/concepts/sessions/): How long a session lasts, and single sign-on. # Protect an API Source: https://goiabada.dev/guides/protect-an-api/ This guide protects your API with Goiabada, so it answers only callers holding the right permission, whether an app calls it for a user or a service calls it on its own. Callers show what they may do with an [access token](https://goiabada.dev/concepts/tokens/): a signed JWT the auth server issues, which carries the [permission scopes](https://goiabada.dev/concepts/scopes/#permission-scopes) its holder was granted. Your API checks the token on every request, on its own, with the auth server’s public keys. It never has to call the auth server to do that. ## Let users call your API 1. Describe your API. In the admin console, open **Admin**, **Resources and permissions** and click **Create new**. Enter a **Resource identifier**, such as `product-api`, and a **Description**, and click **Create**. 2. Click **Manage** beside it and open **Permissions**. For each thing your API lets callers do, enter a **Permission identifier**, such as `read` or `write`, and click **Create permission**. Click **Save** when the list is done. Each one is a scope your app can ask for, `product-api:read` and `product-api:write`. See [Resources and permissions](https://goiabada.dev/concepts/resources-and-permissions/). 3. Give the permissions to the users who need them, on each user’s **Permissions** tab, or to a group, on the group’s **Permissions** tab, so every member gets them. Choose the **Resource** and the **Permission**, click **Grant permission**, and click **Save**: until you save, nothing is granted. 4. Have your app ask for the scope when it signs the user in, beside the OpenID Connect scopes it already asks for, as in [Add sign-in to a web app](https://goiabada.dev/guides/add-sign-in-to-a-web-app/): ```http GET /auth/authorize?client_id=my-web-app&redirect_uri=https%3A%2F%2Fapp.example.com%2Fcallback&response_type=code&scope=openid%20email%20product-api%3Aread&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&code_challenge_method=S256&state=af0ifjsldkj&nonce=n-0S6_WzA2Mj HTTP/1.1 Host: auth.example.com ``` 5. Your app sends the access token it gets to your API, in the `Authorization` header: ```bash curl https://api.example.com/products \ -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6Ii4uLiJ9..." ``` 6. Your API checks the token before it answers, as [Tokens](https://goiabada.dev/concepts/tokens/#check-a-token) describes: its signature against the keys at `https://auth.example.com/certs`, an `iss` equal to the auth server’s issuer, an `exp` that hasn’t passed, `product-api` among its `aud`, and the permission the operation needs in its `scope`. A token that fails a check gets `401 Unauthorized`, and one that’s valid but lacks the permission gets `403 Forbidden`, as [RFC 6750 section 3.1](https://www.rfc-editor.org/rfc/rfc6750.html#section-3.1) describes for an API answering bearer tokens. ## Let a service call your API A service that calls your API on its own, with no user, such as a nightly job, gets its own token with the client credentials flow. 1. Register the service. In the admin console, open **Admin**, **Clients** and click **Create new**. Enter a **Client identifier**, such as `billing-service`, turn on **Client credentials flow**, turn off **Authorization code flow with PKCE**, and click **Create**. 2. Click **Manage** beside it. On **Permissions**, choose the **Resource** and the **Permission** the service needs, click **Grant permission**, and click **Save**. On **Authentication**, click **Reveal** and copy the **Client secret** to the service’s configuration. 3. From the service, ask the token endpoint for a token: ```bash curl -X POST https://auth.example.com/auth/token \ -u billing-service:CLIENT_SECRET \ -d grant_type=client_credentials \ -d scope=product-api:read ``` 4. Read the access token from the answer: ```json { "access_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6Ii4uLiJ9...", "token_type": "Bearer", "expires_in": 300, "scope": "product-api:read" } ``` 5. Call your API with it, as step 5 above does. When it expires, ask for a new one: there’s no refresh token. ## What a user’s token carries An access token from a sign-in names the user in `sub`, and carries in `scope` the permission scopes your app asked for that the user holds, directly or through a group. A permission the user doesn’t hold is left out without an error, so check `scope` rather than assuming your app got what it asked for. Its `aud` names the resource of each permission scope, and `authserver` too when the token carries an OpenID Connect scope, since the auth server’s own `/userinfo` accepts it. [Tokens](https://goiabada.dev/concepts/tokens/#the-access-token) lists every claim. ## What a service’s token carries A token from the client credentials flow names no user. Decoded, the one from the steps above looks like this: ```json { "iss": "https://auth.example.com", "sub": "billing-service", "aud": "product-api", "scope": "product-api:read", "typ": "Bearer", "iat": 1760000000, "nbf": 1760000000, "exp": 1760000300, "jti": "9a6f1c1e-2d0b-4f3e-8a55-0c1b7a3e2f44" } ``` `sub` is the client identifier, and there’s no `auth_time`, `acr`, `amr` or `sid`, since nobody signed in. Your API checks it exactly as it checks a user’s token. The scope is the client’s own permissions, never a user’s. Leave `scope` out of the request to get every permission the client holds, as long as it holds one. A scope the client doesn’t hold is refused with `invalid_scope`, and so is an OpenID Connect scope such as `openid`, since there’s no user for it to describe. [Token](https://goiabada.dev/reference/endpoints/token/#client-credentials-grant) lists every refusal. Only a confidential client can use the flow, since its client secret is all it proves itself with. Keep the secret in the service’s secret store or environment, never in code, and give each service a client of its own, so you can replace one’s secret without touching the others. ## The keys Fetch the key set from `/certs` once and cache it, then pick the key whose `kid` matches the one in the token’s header. When a token names a `kid` your cache doesn’t have, fetch the set again: the auth server publishes the next signing key before it signs anything with it, so a fresh copy has it. See [the key set](https://goiabada.dev/reference/endpoints/discovery-and-jwks/#the-key-set). ## What your API can’t see Your API trusts a valid token until it expires. Ending the user’s session, disabling them or changing their password stops the next refresh, not a token already issued. That’s why access tokens are short, 5 minutes in a new install. > **Caution** > > If an operation must stop the moment a user is disabled or their session is ended, check the signature and also call [`/userinfo`](https://goiabada.dev/reference/endpoints/userinfo/) with the token: the auth server refuses it from then on. That works for a token carrying `openid`, except one from an `offline_access` grant: it names no session, so `/userinfo` goes on accepting it after the session ends, and refuses it only once the user is disabled or their credentials change. See [what ending a session doesn’t reach](https://goiabada.dev/concepts/ending-sessions/#what-it-doesnt-reach). ## When something goes wrong - The app gets `error=invalid_scope`, or a token without the permission: see [invalid_scope](https://goiabada.dev/troubleshooting/invalid-scope/). - The service gets `unauthorized_client`: **Client credentials flow** is off for its client, or the client is public. - The service gets `invalid_client`: the client identifier or secret is wrong, or the client is disabled. ## Next steps [Resources and permissions](https://goiabada.dev/concepts/resources-and-permissions/): Who holds a permission, and the authserver resource. [Tokens](https://goiabada.dev/concepts/tokens/): Every claim an access token carries, and how long it lasts. [Token](https://goiabada.dev/reference/endpoints/token/): The token endpoint's grants, answers and errors. # Require two-factor authentication Source: https://goiabada.dev/guides/require-two-factor-authentication/ This guide makes everyone who signs in to your app enter a one-time code as well as their password. That’s [two-factor authentication](https://goiabada.dev/concepts/glossary/#two-factor-authentication): the code is six digits from an authenticator app on the user’s phone, such as Google Authenticator, Microsoft Authenticator or FreeOTP, and it changes every 30 seconds. How strongly a user must sign in to a client is the client’s [ACR level](https://goiabada.dev/concepts/acr-and-amr/), and the strongest one asks for a code at every sign-in that starts a new session. ## Require a code for your app 1. In the admin console, open your client under **Admin**, **Clients**. On the **Settings** tab, set **Default ACR level** to **ACR level 3 - password + mandatory OTP**, which is `urn:goiabada:level2_mandatory`, and click **Save**. Sign-ins that start after the change use it. 2. Sign in to your app as a user with no authenticator. After the password, the sign-in page asks you to set one up: scan the QR code with an authenticator app, then enter the code it shows. From then on, a sign-in that starts a new session asks for your password and then a code. While your session lasts, it’s reused: signing in to your app again, or to another app at the same level, asks for neither. A request that asks for a fresh sign-in, with `prompt=login`, asks for both, whatever the session already gave. See [Users who are already signed in](https://goiabada.dev/guides/require-two-factor-authentication/#users-who-are-already-signed-in). 3. In your app, check how the user signed in before you let them in. The ID token says it in two claims, `acr`, the level, and `amr`, the methods the user used: ```json { "acr": "urn:goiabada:level2_mandatory", "amr": ["pwd", "otp"] } ``` Accept the sign-in only when `amr` has `otp`. The client’s setting already makes the auth server ask for the code, so this check is what keeps your app safe if someone lowers that setting later. 4. If your app calls an API of yours, the access token carries the same `acr` and `amr`, so the API can make the same check. See [Protect an API](https://goiabada.dev/guides/protect-an-api/). To ask for a code only before one sensitive operation, such as a payment, leave the client’s level as it is and send `acr_values=urn:goiabada:level2_mandatory` on that one authorization request. [ACR and AMR](https://goiabada.dev/concepts/acr-and-amr/#require-a-level) shows how. It asks for a level, not for a new code: a user whose session already reached that level isn’t asked again. To require a recent sign-in as well, add `max_age`, as [Sessions](https://goiabada.dev/concepts/sessions/) explains. ## Require a code for administrators The admin console signs administrators in through its own client, `admin-console-client`, so the same setting makes every administrator use a code. 1. Set up an authenticator for yourself first, under **Account**, **Two-factor authentication**. That way you know it works before it’s required. 2. Open **Admin**, **Clients**, `admin-console-client`, and on the **Settings** tab set **Default ACR level** to **ACR level 3 - password + mandatory OTP**, then click **Save**. Only an administrator with `authserver:manage` can change this client. 3. The next time each administrator signs in to the admin console, they’re asked for a code, and one without an authenticator sets one up first. > **Danger** > > **There are no backup codes.** An administrator who loses their authenticator needs another administrator to turn it off for them, or a client of your own holding `authserver:manage`. Give `authserver:manage` to at least two people before you require a code: otherwise, once the only one loses their phone, such a client or editing the database by hand are the only ways back. See [Locked out of the admin console](https://goiabada.dev/troubleshooting/locked-out-of-the-admin-console/#the-last-administrator-cant-sign-in). ## Setting up an authenticator A user can set up an authenticator before any client requires one, in the admin console under **Account**, **Two-factor authentication**: they scan the QR code, enter their password and the code the app shows, and click **Enable OTP**. A client at level 2, the level new clients start with, then asks them for a code too. At level 3, a user with no authenticator sets one up during the sign-in, on the page that asks for the code. Nothing is stored if their two-factor settings changed while they were doing it, for example in another tab: the sign-in ends, and they sign in again. See [ACR and AMR](https://goiabada.dev/concepts/acr-and-amr/#setting-up-two-factor-authentication-during-a-sign-in). The authenticator app lists the account under the auth server’s **App name** and the user’s email address. ## The code The code is a time-based one-time password ([RFC 6238](https://www.rfc-editor.org/rfc/rfc6238)): six digits, a new one every 30 seconds. The auth server also accepts the code from the 30 seconds before and after, so a phone whose clock is a little off still works. Each code works once. Sent again, it’s refused, and the [audit log](https://goiabada.dev/concepts/audit-log/) records `otp_code_replay_detected`. Each user gets 5 wrong codes in 15 minutes, and then waits. See [Too many attempts or 429](https://goiabada.dev/troubleshooting/too-many-attempts-or-429/). ## Users who are already signed in A user whose session already reached the client’s level isn’t asked for anything: the session is reused, and the tokens say `urn:goiabada:level2_mandatory` and `["pwd", "otp"]` from the sign-in that earned it. `prompt=login` doesn’t reuse the session: the user signs in again with their password and a code, whatever their session already gave, and the tokens say `["pwd", "otp"]` and `auth_time` of this sign-in. A session that has idled out, reached its maximum lifetime or fallen outside the request’s `max_age` doesn’t count either, and the user is asked for both. A user who signed in to another app with a password alone isn’t asked for their password again. They’re asked only for the code, which is a [step-up](https://goiabada.dev/concepts/acr-and-amr/#a-session-thats-already-there), and their session keeps the higher level from then on. When a user sets up or removes an authenticator, each of their sessions is checked again for a second factor before it’s used for a level above `urn:goiabada:level1`: a user with an authenticator enters a code, and a user with none left is asked for nothing at level 2 and sets up a new one at level 3. ## A user who lost their authenticator An administrator opens **Admin**, **Users**, the user, and their **Authentication** tab, turns off **Two-factor authentication enabled**, and saves. At level 3, the user sets up a new authenticator at their next sign-in. Turning it off lowers each of the user’s sessions to what a password alone reaches, so no token issued from them afterwards claims a code. Nobody is signed out, though, and apps the user was already signed in to keep refreshing their tokens with the claims they had. If the device could be in someone else’s hands, also end the user’s sessions: on their **Sessions** tab, choose **End session** beside each one, and confirm. That signs every browser out and revokes the refresh tokens each session gave out. A session left open is enough to set up a new authenticator at a level 3 sign-in, since that asks only for what the session doesn’t already have. See [Ending sessions](https://goiabada.dev/concepts/ending-sessions/). A user who still has their authenticator can turn it off themselves, with their password, under **Account**, **Two-factor authentication** and **Disable OTP**. At level 3, they then set up a new one at their next sign-in to your app. ## What a code can’t protect - **A silent request** (`prompt=none`) can’t show the code page. When a code is due, it gets `interaction_required`. See [prompt](https://goiabada.dev/concepts/prompt/#the-silent-checks). - **The password grant** ([ROPC](https://goiabada.dev/legacy-flows/ropc/)) can’t ask for a code, so it refuses every user who has an authenticator. - **The client credentials flow** has no user, so nothing asks for a code. ## Next steps [ACR and AMR](https://goiabada.dev/concepts/acr-and-amr/): Every level, and what each sign-in ends with. [Single sign-on across clients](https://goiabada.dev/guides/single-sign-on-across-clients/): What a user signed in to another app is asked for. [Locked out of the admin console](https://goiabada.dev/troubleshooting/locked-out-of-the-admin-console/): What works when the only administrator loses their authenticator. # Single sign-on across clients Source: https://goiabada.dev/guides/single-sign-on-across-clients/ This guide lets a user who signed in to one of your apps go straight into your other apps, without entering their password again. That’s [single sign-on](https://goiabada.dev/concepts/glossary/#single-sign-on-sso), and there’s nothing to turn on. When a user signs in, the auth server starts a [session](https://goiabada.dev/concepts/sessions/) for their browser. Any client that sends that browser to sign in afterwards finds the session, and the user goes straight through. ## Set up single sign-on 1. Register each app as a client of its own, as [Add sign-in to a web app](https://goiabada.dev/guides/add-sign-in-to-a-web-app/) or [Add sign-in to a SPA or mobile app](https://goiabada.dev/guides/add-sign-in-to-a-spa-or-mobile-app/) shows. Each gets its own client identifier, redirect URIs, consent setting, ACR level and tokens. 2. Send every app’s users to the same auth server address, such as `https://auth.example.com`. The session cookie belongs to that host name. Your apps can run on any domains, but an auth server reached under two names holds two separate sessions. 3. Sign in to the first app. The auth server asks for the password and starts a session. 4. In the same browser, open the second app and start its sign-in, as an ordinary authorization request. The auth server finds the session and sends the browser straight back with a code, without showing a page. The second app redeems the code for tokens of its own. 5. To sign the user in to the second app without even a click, send a silent request from it when its first page loads. Add `prompt=none` and redirect the whole page, rather than loading it in a hidden iframe: the session cookie is `SameSite=Lax`, so a request from a frame on another site never carries it, and the auth server’s pages refuse to be framed: ```http GET /auth/authorize?client_id=second-app&redirect_uri=https%3A%2F%2Fsecond.example.com%2Fcallback&response_type=code&scope=openid%20profile&prompt=none&code_challenge=...&code_challenge_method=S256&state=... HTTP/1.1 Host: auth.example.com ``` A code means the user is signed in. `login_required` means there’s no session to use, so show your sign-in button. `interaction_required` and `consent_required` mean the user has a page to see: send them through the same request without `prompt=none`. `access_denied` means the user holds none of the scopes asked for, or has been disabled, which a sign-in doesn’t change. See [prompt](https://goiabada.dev/concepts/prompt/#check-for-a-session-silently). ## When the user is still asked something A session saves the user their password, not every step. A sign-in through it still shows a page when: - **The second app needs a stronger sign-in.** When its [ACR level](https://goiabada.dev/concepts/acr-and-amr/) is higher than the session reached, the user enters only what the new level adds, usually a one-time code. That’s a [step-up](https://goiabada.dev/concepts/acr-and-amr/#a-session-thats-already-there), and the session keeps the higher level afterwards. - **The second app asks for consent.** With **Consent required** on and no earlier approval from the user covering every scope, or whenever it asks for `offline_access`, the user sees the consent screen. Consent is given to each client on its own, so approving the first app doesn’t approve the second. See [Clients](https://goiabada.dev/concepts/clients/#consent-required). - **The request asks for a password.** `prompt=login`, a `max_age` shorter than the time since the user last entered a credential (`auth_time`), or an `id_token_hint` naming another user each ask for the password whatever the session, and for a one-time code whenever the level needs one. See [prompt](https://goiabada.dev/concepts/prompt/#promptlogin) and [Sessions](https://goiabada.dev/concepts/sessions/#ask-for-a-recent-sign-in). - **The session can’t be used.** It has been idle too long or reached its maximum lifetime, so the user signs in again. See [How long a session lasts](https://goiabada.dev/concepts/sessions/#how-long-a-session-lasts). A user who has been disabled can’t use a session: disabling them ends their sessions, so their next sign-in answers “Your user account is disabled.” once the right password is entered, and a silent request gets `login_required`. ## How long it lasts One session serves every client, so its lifetimes apply to all of them. They’re under **Admin**, **Sessions** in the admin console: by default a session ends after 2 hours idle, and after 24 hours whatever happens. A sign-in to any app through the session counts as activity, and so does a refresh with a normal [refresh token](https://goiabada.dev/concepts/refresh-tokens/) from it. Each app’s tokens keep their own lifetimes, set under **Admin**, **Tokens**, or on the client’s **Tokens** tab. See [How long tokens last](https://goiabada.dev/concepts/tokens/#how-long-tokens-last). ## Signing out When the user signs out of one app with its ID token as the hint, the auth server removes only that app from the session. The other apps the user signed in to stay signed in, and their normal refresh tokens keep working. The browser’s session cookie is cleared, though, so the next sign-in in that browser asks for the password, for every app. See [Sign users out](https://goiabada.dev/guides/sign-users-out/). The auth server doesn’t tell your other apps about a sign-out. Each one keeps its own session until it ends it, or until it next asks the auth server for something the ended session no longer gives, such as a refresh. To end the session for every app at once, the user signs out with no hint and confirms on the sign-out page. Every app’s normal refresh tokens from the session stop working; offline ones keep working. Ending the session under **Account**, **Sessions** in the admin console goes further and revokes them all, offline ones included. See [Ending sessions](https://goiabada.dev/concepts/ending-sessions/). > **Note** > > Every sign-in that reuses a session, silent requests included, leaves a `bumped_user_session` entry in the [audit log](https://goiabada.dev/concepts/audit-log/) naming the user and the client, so you can see which apps the user’s sessions reached. A refresh with a refresh token that isn’t offline leaves one too. ## Next steps [Sessions](https://goiabada.dev/concepts/sessions/): How long a session lasts, and what keeps it alive. [Sign users out](https://goiabada.dev/guides/sign-users-out/): End your app's part of the session. [Require two-factor authentication](https://goiabada.dev/guides/require-two-factor-authentication/): Ask for a code before a session reaches your app. # Let clients register themselves (DCR) Source: https://goiabada.dev/guides/let-clients-register-themselves-dcr/ This guide lets apps you don’t build register themselves as clients, and shows you how to keep an eye on what registered. Some apps can’t be set up by hand. A command-line tool or an AI assistant connecting through MCP (the Model Context Protocol) is installed by its users, each copy on its own machine, and expects to get a client of its own. Dynamic client registration (DCR) lets it: the app calls `POST /connect/register`, as [RFC 7591](https://www.rfc-editor.org/rfc/rfc7591) defines, and gets a client identifier back, with nobody creating it in the admin console. ## Let clients register themselves 1. In the admin console, open **Admin**, **General**, turn on **Dynamic client registration enabled**, and save. 2. Limit who can reach `/connect/register`. While DCR is on, anyone who can reach it can create a client, with no token and no sign-in. Turn on the rate limiter with `GOIABADA_AUTHSERVER_RATELIMITER_ENABLED`, which then allows 10 registrations a minute from each IP address, or limit the endpoint at your reverse proxy. See [rate limits](https://goiabada.dev/reference/environment-variables/#rate-limits). 3. Check that the auth server now advertises it. The [discovery document](https://goiabada.dev/reference/endpoints/discovery-and-jwks/) carries `registration_endpoint`, which is how an app finds out it may register: ```bash curl https://auth.example.com/.well-known/openid-configuration ``` ```json { "issuer": "https://auth.example.com", "registration_endpoint": "https://auth.example.com/connect/register" } ``` That’s two fields of many. While DCR is off, `registration_endpoint` isn’t there. > **Caution** > > A self-registered client has no permissions and can’t ask for the administrative scopes, so on its own it can’t do much. But it can show your users a sign-in page that names any app it likes. That’s why each one starts with consent on, and why it’s worth [reviewing what registered](https://goiabada.dev/guides/let-clients-register-themselves-dcr/#review-what-registered). ## Register your app If you’re writing the tool, this is what it does the first time it runs. 1. Read `registration_endpoint` from the discovery document. If it isn’t there, the auth server doesn’t take registrations: ask its administrator for a client instead. 2. Register, naming your app and where the browser comes back to. A tool on the user’s machine is a public client, with no secret, and receives its callback on a loopback address: ```http POST /connect/register HTTP/1.1 Host: auth.example.com Content-Type: application/json { "client_name": "Report builder", "redirect_uris": ["http://127.0.0.1/callback"], "token_endpoint_auth_method": "none", "grant_types": ["authorization_code", "refresh_token"] } ``` 3. Keep the `client_id` from the answer. It’s yours for good: there’s no endpoint to read or change a registration later, so register again only if you lose it. ```http HTTP/1.1 201 Created Content-Type: application/json Cache-Control: no-store { "client_id": "dcr_6f1c2a9e-3b7d-4e0a-9c55-2d8b1e4f7a10", "client_id_issued_at": 1759900000, "client_secret_expires_at": 0, "redirect_uris": ["http://127.0.0.1/callback"], "token_endpoint_auth_method": "none", "grant_types": ["authorization_code", "refresh_token"], "client_name": "Report builder" } ``` 4. Sign the user in with the authorization code flow and PKCE, as [Add sign-in to a SPA or mobile app](https://goiabada.dev/guides/add-sign-in-to-a-spa-or-mobile-app/) shows. Start a local server on any free port and send that port in `redirect_uri`, such as `http://127.0.0.1:53682/callback`: for a loopback address the port isn’t compared. > **Caution** > > A self-registered client is never sent an error. When a sign-in fails, the user sees a page on the auth server and nothing comes back to your app, so give up waiting for the callback after a while and let the user start over. See [The app gets no error back](https://goiabada.dev/troubleshooting/the-app-gets-no-error-back/). ## Review what registered 1. Open **Admin**, **Clients**. Each client that registered itself carries a **Self-registered** badge. 2. Open one you recognize and fix how your users see it. On the **Settings** tab, give it a **Display name** and turn on **Show display name**, which replaces the name it gave itself and the consent screen’s note that it hasn’t been verified. Once you trust it, you can turn **Consent required** off. 3. For one you don’t recognize, untick **Enabled** to stop it signing anyone in, or delete it from the list. Every registration leaves a `dynamic_client_registration` entry in the [audit log](https://goiabada.dev/concepts/audit-log/), with the new client, its grant types, whether it’s public, and the IP address it came from. ## What a registration may ask for The auth server reads four fields, `redirect_uris`, `token_endpoint_auth_method`, `grant_types` and `client_name`, and ignores the rest. A registration can’t ask for the [legacy flows](https://goiabada.dev/legacy-flows/implicit/), and a public client can’t ask for `client_credentials`. Redirect URIs follow stricter rules than for clients you create, since nobody has checked who’s asking: a public client gets loopback `http` and custom schemes such as `myapp://callback`, and a confidential one `https` and loopback `http`. So a single-page app can’t register itself: create it in the admin console. [Dynamic client registration](https://goiabada.dev/reference/endpoints/dynamic-client-registration/) has every field, rule and error. ## What a self-registered client gets It’s enabled straight away, with an identifier made up of `dcr_` and a UUID that can’t be changed later. Consent is on, its ACR level is `urn:goiabada:level2_optional`, and a public one always uses PKCE. A confidential one gets a secret, which the answer carries once, and which an administrator can read later on the client’s **Authentication** tab. The implicit flow and ROPC are off on the client itself, so turning either on globally doesn’t reach it. Its token lifetimes are copied from the server’s settings when it registers. Apart from its identifier, an administrator can change any of it like any other client’s. See [Self-registered clients](https://goiabada.dev/concepts/clients/#self-registered-clients). ## Turning it off Turning **Dynamic client registration enabled** off stops new registrations: `/connect/register` answers `403` with `access_denied`, and discovery stops carrying `registration_endpoint`. Clients that already registered keep working. To stop one, disable or delete it. ## Next steps [Dynamic client registration](https://goiabada.dev/reference/endpoints/dynamic-client-registration/): Every field, answer and error of /connect/register. [Self-registered clients](https://goiabada.dev/concepts/clients/#self-registered-clients): How a self-registered client is treated, and why. [Add sign-in to a SPA or mobile app](https://goiabada.dev/guides/add-sign-in-to-a-spa-or-mobile-app/): Sign users in with a public client. # Customize and translate the pages Source: https://goiabada.dev/guides/customize-and-translate-the-pages/ This guide makes the pages your users see look and read like your product, in their language. There are three levels, from a setting in the admin console to your own copy of the pages. Start with the first: most deployments need nothing more. ## Brand the pages 1. In the admin console, open **Admin**, **General** and set **App name**, up to 30 characters. It’s the heading of the sign-in pages and the start of their titles, it signs every email, and it’s the name authenticator apps list your users’ accounts under. 2. Open **Admin**, **UI theme** and choose a color theme under **Theme selection**. It applies to the sign-in pages and the admin console alike. **Default** is the dark theme; the others are [daisyUI](https://daisyui.com/docs/themes/)’s built-in themes, such as `light`, `corporate` and `emerald`. 3. Give each client a display name and a logo, so users see which app they’re signing in to. See [Clients](https://goiabada.dev/concepts/clients/#display-name-description-and-logo). ## Add a language The pages come in English and Portuguese (Brazil). To add another language, you translate one file of text and point both servers at it, with no rebuild. 1. Get the English text of the release you run: `src/core/i18n/catalogs/active.en.toml` in [the source](https://github.com/leodip/goiabada) at that release’s tag, such as `v1.7.0`. The release is in the image tag and in the `build information` line each server logs when it starts. 2. Copy it into a folder named `catalogs`, as `active..toml`, named with the language’s [BCP 47](https://www.rfc-editor.org/info/bcp47) tag: `active.es.toml` for Spanish. Translate the text on the right of each `=` and leave the keys on the left as they are: *catalogs/active.es.toml* ```toml "auth.pwd.title" = "Iniciar sesión" "auth.pwd.email_label" = "Correo electrónico" "auth.pwd.password_label" = "Contraseña" "auth.pwd.button" = "Entrar" ``` Keep a placeholder such as `{{.appName}}` exactly as it is: the server writes the value there. A key you leave out, or leave empty, shows in English. 3. Make the folder that holds `catalogs` readable by both servers, and set `GOIABADA_I18N_OVERRIDES_DIR` to it on the auth server and on the admin console. In a container, mount it read-only, as the [templates are mounted](https://goiabada.dev/guides/customize-and-translate-the-pages/#change-the-templates) below. 4. Restart both servers. Each reads the catalogs once, at start, and refuses to start when one doesn’t parse, naming the file. 5. Users choose the language under **Account**, **Profile** in the admin console, and your app can ask for it on the sign-in pages with `ui_locales`. See [how the language is chosen](https://goiabada.dev/guides/customize-and-translate-the-pages/#how-the-language-is-chosen). ## Change a few words To reword a page in a language Goiabada already has, override only the keys you change, in the same folder: *catalogs/active.en.toml* ```toml "auth.pwd.title" = "Sign in to Acme" ``` Every key you don’t name keeps the built-in text. Set `GOIABADA_I18N_OVERRIDES_DIR` and restart, as in [Add a language](https://goiabada.dev/guides/customize-and-translate-the-pages/#add-a-language). ## Change the templates To change the layout of a page, or an email’s body, you give a server its own copy of the templates. The templates and static files are compiled into each server’s binary, and the images hold the binary alone, so there’s no `web` folder inside a container to copy out. 1. Copy the web files from the source of the release you run, since a template from another release may use data the server no longer passes to it: ```bash RELEASE=v1.7.0 # replace with the release you run git clone --depth 1 --branch "$RELEASE" https://github.com/leodip/goiabada.git goiabada-source mkdir authserver-web adminconsole-web cp -r goiabada-source/src/authserver/web/{template,static,tailwindcss} authserver-web/ cp -r goiabada-source/src/adminconsole/web/{template,static,tailwindcss} adminconsole-web/ ``` `tailwindcss` isn’t served: it’s what [regenerating the CSS](https://goiabada.dev/guides/customize-and-translate-the-pages/#regenerate-the-css) reads. 2. Edit the templates. They’re Go [`html/template`](https://pkg.go.dev/html/template) files. Keep the element IDs and the scripts the pages use, or the forms stop working. 3. Make the files readable by the containers. Both images run as uid 10001 and gid 10001 (see [the user the images run as](https://goiabada.dev/deploy/docker-compose/#the-user-the-images-run-as)), and a page whose template the server can’t read fails with a 500: ```bash chmod -R a+rX authserver-web adminconsole-web ``` 4. Mount each directory read-only and point its server at it. In the generated `docker-compose.yml`, which already lists the four variables empty: ```yaml goiabada-authserver: volumes: - sqlite-data:/data # keep the volumes the service already has - ./authserver-web/template:/app/web/template:ro - ./authserver-web/static:/app/web/static:ro environment: - "GOIABADA_AUTHSERVER_STATICDIR=/app/web/static" - "GOIABADA_AUTHSERVER_TEMPLATEDIR=/app/web/template" goiabada-adminconsole: volumes: - ./adminconsole-web/template:/app/web/template:ro - ./adminconsole-web/static:/app/web/static:ro environment: - "GOIABADA_ADMINCONSOLE_STATICDIR=/app/web/static" - "GOIABADA_ADMINCONSOLE_TEMPLATEDIR=/app/web/template" ``` Leave a server’s pair empty if you haven’t changed its files. Then run `docker compose up -d`. With the native binaries, set the same variables in the env file, naming directories the binaries’ user can read. 5. Open every page you changed. A mistake in a template shows only when its page is rendered. > **Caution** > > A directory replaces the built-in set **whole**, so it must hold every file of that set, not only the ones you change. When you upgrade Goiabada, copy the new release’s files again and redo your changes on them. ## Regenerate the CSS The pages are styled with [Tailwind CSS](https://tailwindcss.com/), and `main.css` holds only the classes the templates used when it was generated. A class you add to a template does nothing until you regenerate it. 1. Download the standalone Tailwind CLI for your platform from [Tailwind’s releases](https://github.com/tailwindlabs/tailwindcss/releases), at the version the release you copied pins: `tools.tailwind` in `goiabada-source/src/authserver/versions.yaml`. Rename it to `tailwindcss` and make it executable. 2. Run it in each web directory whose templates you changed. Add `--minify` for a smaller file: ```bash cd authserver-web tailwindcss -i ./tailwindcss/input.css -o ./static/main.css ``` `input.css` looks for class names in `../template` and in the JavaScript in `../static`, so keep the three folders side by side. It builds daisyUI’s components in from `daisyui.mjs`, the bundle beside it, so the pages load nothing from another site. 3. The new `main.css` is in the static directory you mounted, so the server serves it at once. Browsers may keep the previous one for up to five minutes: reload without the cache to check. ## How the language is chosen On the auth server’s pages, the sign-in, one-time code, consent, sign-out, registration and password reset pages, the language is the first of these that names one: 1. The `ui_locales` parameter of the authorization request, or of the [sign-out](https://goiabada.dev/reference/endpoints/logout/): one or more language tags in your order of preference, separated by single spaces, such as `ui_locales=es pt-BR`. It’s kept for the rest of that sign-in, and applies to the error page the auth server shows when it refuses the request outright. 2. The browser’s `Accept-Language` header. 3. English. In the admin console, the user’s own choice comes first: the **Locale** they picked under **Account**, **Profile**, which reaches the console as the `locale` claim of their ID token. The console picks a change up the next time it refreshes the user’s tokens, within five minutes with the default token lifetime. Without one, the browser’s `Accept-Language` decides, then English. Whichever of these decides, it gets the first of its languages Goiabada has text for, or English when it has none of them: `ui_locales=fr es` shows Spanish when you’ve added Spanish but not French, and `ui_locales=fr` alone shows English. An email is written in its recipient’s language: the **Locale** on their profile, or English when they haven’t picked one. The activation link and the welcome email a user gets when they register are written in the language of the page they registered on, since their account has no language yet. ## Catalogs The text of every page, message and email subject is in two built-in catalogs, English, which is complete, and Portuguese (Brazil). A key with no text in the chosen language shows in English. `GOIABADA_I18N_OVERRIDES_DIR` names a folder whose `catalogs` subfolder holds `active..toml` files. Nothing else in the folder is read: files put in the folder itself are ignored, and the server logs `override directory has no catalogs subdirectory, skipping it` when it starts. Each file is merged over the built-in catalog of its language, key by key, and the file wins. A value left empty removes the built-in text, so the key shows in English. Every value is a plain string: a `[table]` section stops the server at start. The names of countries and phone country codes, and the country in each time zone’s label, come from Unicode’s CLDR data, in the page’s language, and each language on the **Locale** list is named in itself and in English, so none of them needs translating. ## Email bodies An email’s subject is in the catalogs, and its body is a template under `emails/` in the auth server’s templates. To translate a body, add a copy beside the English one named with the language, such as `emails/email_forgot_password.es.html`, in your own [templates directory](https://goiabada.dev/guides/customize-and-translate-the-pages/#change-the-templates). The auth server uses it for a recipient whose language is exactly that tag, `es` here, and the English template for everyone else. ## Templates and static files A server reads a template each time it renders the page, so an edited template shows on the next request with no restart. Static files are served with `Cache-Control: public, max-age=300`. The four directory variables are on [Environment variables](https://goiabada.dev/reference/environment-variables/#customization). ## Next steps [Clients](https://goiabada.dev/concepts/clients/#display-name-description-and-logo): Each client's display name, description and logo. [Environment variables](https://goiabada.dev/reference/environment-variables/#customization): The template, static and catalog directories. [Docker Compose](https://goiabada.dev/deploy/docker-compose/): The user the images run as, and what a mounted file needs. # Clients Source: https://goiabada.dev/concepts/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 1. In the admin console, open **Admin**, **Clients** and click **Create new**. 2. Enter a **Client identifier**. It’s the `client_id` your app will send, such as `my-web-app`. 3. 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. 4. Click **Create**. You’re back on the list of clients. Click **Manage** next to your new client. 5. 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. 6. 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 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 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. ## 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 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](https://goiabada.dev/concepts/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. ## Flows 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/token` for tokens. See [Add sign-in to a web app](https://goiabada.dev/guides/add-sign-in-to-a-web-app/) and [Add sign-in to a SPA or mobile app](https://goiabada.dev/guides/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](https://goiabada.dev/guides/protect-an-api/#let-a-service-call-your-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](https://goiabada.dev/legacy-flows/implicit/) and [ROPC](https://goiabada.dev/legacy-flows/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 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. `/callback` and `//example.com/callback` are refused. - **name a host,** when it’s `http` or `https`. `https:///example.com/callback` is refused. A custom scheme such as `com.example.app:/oauth2redirect` needs no host. - **have no fragment.** `https://example.com/callback#done` is refused. An encoded `%23` is fine. - **have well-formed percent-escapes.** `https://example.com/a%zz` is 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. **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` and `error_description` 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](https://goiabada.dev/troubleshooting/invalid-redirect-uri/#how-redirect-uris-are-checked) says what the user sees. ### 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](https://datatracker.ietf.org/doc/html/rfc8252#section-7.3) asks this for the two IP addresses. Goiabada allows it for `localhost` too, as a convenience, though [section 8.3](https://datatracker.ietf.org/doc/html/rfc8252#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. > **Caution** > > Prefer `127.0.0.1` or `[::1]` to `localhost`. Resolving `localhost` isn’t guaranteed to reach the loopback interface, which is why [RFC 8252](https://datatracker.ietf.org/doc/html/rfc8252#section-8.3) recommends against it. ## 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. `/.well-known/openid-configuration` and `/certs` accept every origin, with nothing to register. ## 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](https://goiabada.dev/reference/api/scopes/#the-account-api-scope) 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](https://goiabada.dev/concepts/tokens/#which-scopes-a-token-gets) 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](https://goiabada.dev/concepts/clients/#self-registered-clients) 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 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](https://goiabada.dev/concepts/acr-and-amr/). ## 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](https://goiabada.dev/reference/api/admin/operations/updateclientadministrativescopes/). Only an administrator with `authserver:manage` can 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`](https://goiabada.dev/concepts/prompt/) 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](https://goiabada.dev/concepts/clients/#consent-required) describes: an allowed client asking for `authserver:manage-account` is asked for it like any other client. > **Danger** > > **An allowed client is an administrator client.** It can get a signed-in administrator’s whole authority, so changing it, reading its secret or deleting it takes `authserver:manage`. Allow a client as carefully as you’d grant `authserver:manage` itself. Each switch leaves an `updated_client_administrative_scopes` event in the [audit log](https://goiabada.dev/concepts/audit-log/#events-to-alert-on), and each refusal reaching a signed-in user or an authenticated client leaves an `administrative_scope_refused` event. ## 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](https://goiabada.dev/concepts/resources-and-permissions/). ## 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_access` scope. - **Include OpenID Connect claims in the access token?** and **in the ID token?**, for claims such as `email` or `name`. 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](https://goiabada.dev/concepts/tokens/). ## 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](https://goiabada.dev/reference/endpoints/logo-and-picture/#uploading) 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 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 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 With dynamic client registration (DCR) turned on, an app can register itself as a client by calling `POST /connect/register`, as [RFC 7591](https://datatracker.ietf.org/doc/html/rfc7591) 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](https://goiabada.dev/reference/environment-variables/#rate-limits). A registration looks like this: ```bash 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`: `none` for a public client, or `client_secret_basic` (the default) or `client_secret_post` for a confidential one. - `grant_types`: `authorization_code` (the default), `refresh_token` and `client_credentials`. A public client can’t ask for `client_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](https://goiabada.dev/reference/endpoints/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=none` included. Once you’ve reviewed a self-registered client, you can change its settings like any other, **Consent required** included. > **Caution** > > **Building a self-registered client?** You get no error response when a sign-in fails: it just never comes back. See [The app gets no error back](https://goiabada.dev/troubleshooting/the-app-gets-no-error-back/#the-user-sees-you-have-not-been-sent-anywhere). ### Redirect URIs for self-registered clients The [redirect URI rules](https://goiabada.dev/concepts/clients/#redirect-uris) 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.com` isn’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`, `gopher` and `telnet`. 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. ## Next steps [Add sign-in to a web app](https://goiabada.dev/guides/add-sign-in-to-a-web-app/): Sign users in to your app. [Resources and permissions](https://goiabada.dev/concepts/resources-and-permissions/): Decide what each client and user can do. # Users and groups Source: https://goiabada.dev/concepts/users-and-groups/ This page helps you create users, put them in groups, and decide what a client learns about them. A user is a person who signs in. A group is a set of users: the permissions and attributes you give a group apply to every user in it, so you set them once instead of on each user. ## Create a user 1. In the admin console, open **Admin**, **Users** and click **Create new**. 2. Enter the user’s **Email**, and their **Given name**, **Middle name** and **Family name** if you like. Turn on **Email verified** if you know the address is theirs. 3. Choose how the user gets a password: - **Set password now**, and type it in. Tell the user what it is. - **Email the user a link to set up their password.** The user chooses the password themselves, within 24 hours. This choice appears only when email is set up, under **Email - SMTP**. 4. Click **Create**. The user’s page opens on its **Details** tab. Users can also create their own accounts from the sign-in page: see [Self-registration](https://goiabada.dev/concepts/self-registration/). ## Create a group 1. Open **Admin**, **Groups** and click **Create new**. 2. Enter a **Group identifier**, such as `editors`, and a **Description**. 3. Leave **Include group in access token if requested** and **Include group in id token if requested** on if clients should see that a user is in this group. Click **Create**. 4. Open the group’s **Members** tab, click **Add member**, search for a user, click **Add to group** and confirm with **Yes**. Now give the group what its members need: permissions on its **Permissions** tab, and attributes on its **Attributes** tab. ## Users Each user has a **subject**, a random identifier the auth server gives them when they’re created. It never changes, and it’s the `sub` claim in every token about them, so key your app’s records on it rather than on the email address, which can change. The email address is the user’s sign-in name. Two users can’t share one, it’s stored in lowercase, and it’s at most 60 characters long. Every new user gets the `authserver:manage-account` permission, which lets them manage their own account in the admin console’s **Account** pages. Nothing else: a new user holds no other permission and is in no group. The user’s page has these tabs: **Details**, **Profile**, **Picture**, **Email**, **Phone**, **Address**, **Authentication**, **Consents**, **Sessions**, **Attributes**, **Permissions** and **Groups**. The profile, email, phone and address are what a client reads through [scopes](https://goiabada.dev/concepts/scopes/). ## Enabled and disabled A disabled user can’t sign in. Turning off **Enabled** on the **Details** tab also ends every session the user has and revokes their refresh tokens, so no client can get a new token for them. Turning it back on lets them sign in again, and gives back nothing that was revoked. Deleting a user deletes everything attached to them: their permissions, their group memberships and their attributes. ## Passwords **Set password** on the **Authentication** tab gives the user a new password. It signs them out of every session and revokes their refresh tokens. A password the user changes themselves does the same, except for the session they change it from. See [credential changes](https://goiabada.dev/concepts/ending-sessions/#credential-changes). A user who forgot their password can ask for a reset link from the sign-in page: see [Password recovery](https://goiabada.dev/concepts/password-recovery/). The link you email a new user to set up their password is the same kind of link. The **Authentication** tab also shows whether the user has two-factor authentication, and lets you turn it off for a user who lost their authenticator. They can set it up again under **Account**. If the device could be in someone else’s hands, end their sessions too: see [A user who lost their authenticator](https://goiabada.dev/guides/require-two-factor-authentication/#a-user-who-lost-their-authenticator). ## Groups A group identifier is 3 to 38 characters long. It starts with a letter, ends with a letter or digit, and uses only letters, digits, `-` and `_`, with no `--` or `__`. A description is at most 100 characters. A user can be in any number of groups. They hold every permission each of their groups holds, as well as their own. Deleting a group takes nobody’s account with it: its members just lose what the group gave them. A group is listed in the `groups` claim when the client asks for the `groups` scope. **Include group in access token if requested** puts it in the access token, and **Include group in id token if requested** puts it in the ID token and in the `/userinfo` answer. Turn either off for a group a client has no business seeing. ## Attributes An attribute is a key and a value you attach to a user or a group, such as `department` and `sales`. A client gets them in the `attributes` claim when it asks for the `attributes` scope. - A **key** is 1 to 38 characters long, with the same characters as a group identifier. - A **value** is at most 250 characters, and can’t contain `<` or `>`. - **Include attribute in access token if requested** and **Include attribute in id token if requested** work as they do for groups. A user’s claim holds their own attributes and those of every group they’re in. When a user and one of their groups both have an attribute with the same key, the group’s value wins. ## Administrators A user who holds one of the administrative permissions on the `authserver` resource, directly or through a group, is an administrator. So a group holding one makes every member an administrator, and adding a user to it, removing one, or deleting it needs `authserver:manage`, just as granting the permission directly does. Goiabada also refuses any change that would leave no enabled user holding `authserver:manage`. [Administrators](https://goiabada.dev/reference/api/administrators/) has the whole rule, including [the last administrator](https://goiabada.dev/reference/api/administrators/#the-last-administrator). > **Tip** > > Keep `authserver:manage` on at least two people you trust, so one can always let the other back in. ## Next steps [Resources and permissions](https://goiabada.dev/concepts/resources-and-permissions/): Decide what each user and group can do. [Scopes](https://goiabada.dev/concepts/scopes/): Which of a user's details a client gets. # Self-registration Source: https://goiabada.dev/concepts/self-registration/ This page helps you decide whether people can create their own accounts, and how they prove their email address when they do. Self-registration is a user creating their own account, from the **Register** link on the sign-in page, instead of an administrator creating it. It’s off in a new installation, so only your administrators create users until you turn it on. ## Turn on self-registration Email verification is on in a new installation, so a new account’s address is proven before the account exists: the person who registers gets a link by email, and chooses their password only after following it. That needs email, so set it up first. 1. Set up email under **Admin**, **Email - SMTP**, and send yourself a test message from the **Send test email** tab. 2. Open **Admin**, **General**. 3. Turn on **User self registration enabled**, leave **User self registration requires email verification** on, and click **Save**. The sign-in page now shows a **Register** link. If it doesn’t, email isn’t set up yet, and **Admin**, **General** shows a warning under **User self registration requires email verification** saying registration is unavailable. To turn self-registration off again, turn off **User self registration enabled** on the same page. Only administrators can then create users. **User self registration requires email verification** keeps its value meanwhile, greyed out, so turning self-registration back on brings back the choice you made. ## Who can register | Setting | Default | | - | - | | **User self registration enabled** | Off | | **User self registration requires email verification** | On | While **User self registration enabled** is off, the sign-in page shows no **Register** link and the registration pages lead to a not-found page. Turning it on opens registration when email is set up or email verification is off. With **User self registration requires email verification** on, people prove their address first, as [With email verification](https://goiabada.dev/concepts/self-registration/#with-email-verification) describes. With it off, they don’t, as [Without email verification](https://goiabada.dev/concepts/self-registration/#without-email-verification) describes. Email verification needs email. While it’s on and email isn’t set up, nobody can register: the sign-in page shows no **Register** link and the registration form leads to a not-found page, just as the forgot-password page does without email. Meanwhile **Admin**, **General** shows a warning saying so, under **User self registration requires email verification**. Set up email, or turn email verification off, to open registration. ## Without email verification The registration form asks for an email address and a password, twice. The password must meet the **Password policy** on **Admin**, **General**. When the form is sent, the account is created at once, with its address not verified, and the page says “Your account has been created.” When email is set up, the user also gets a “Welcome!” email. An address that already has an account is refused with “Apologies, but this email address is already registered.” This form appears only while email verification is off. It works with or without email set up. > **Caution** > > Without email verification, anyone can find out whether an address has an account, by trying to register it. The answer can’t be hidden, because the account the form creates is usable at once: signing in with the chosen password would fail for an address that was already taken. Leave email verification on, and set up email, if the list of addresses with an account shouldn’t be discoverable. ## With email verification The registration form asks for the email address alone, and every well-formed address gets the same “Check your email” page, whether or not it has an account. What happens next travels by email: - **A new address** gets an “Activate your account” email with a link, valid for 5 minutes. Following it opens a “Choose your password” form, which can be sent for 5 minutes more. Sending it creates the account, with its address verified. - **An address with an account** gets a “You already have an account” email instead, pointing at the forgot-password page, when the account is enabled and its address verified, the same rule [password recovery](https://goiabada.dev/concepts/password-recovery/) uses. Any other account gets nothing. - **An address with a registration still pending** gets nothing. Its link is still the one that works. Once both 5-minute windows have passed, registering again sends a new link. Following the link creates nothing by itself. Only sending the form does, so a mail scanner or link previewer that opens the link can’t complete a registration, and someone who registers an address that isn’t theirs never gets to choose its password. If the address gains an account before the form is sent, because an administrator created one, the registration is refused and discarded. While self-registration is off, the registration page and every link already sent lead to a not-found page. Turning email off doesn’t stop a link already sent: following it needs no email, so it still creates the account. An expired or used link shows “Unable to activate the account. The verification code appears to be expired.” The person registers again to get a new one. While registration is unavailable because email isn’t set up, the page leaves out its link to the registration form. ## What a new account has A user who registers gets what every new user gets: the `authserver:manage-account` permission, so they can manage their own account in the admin console’s **Account** pages, and nothing else. See [Users and groups](https://goiabada.dev/concepts/users-and-groups/#users). ## Limits and the audit log An address can register 5 times every 5 minutes, whether or not the rate limiter is on. With it on, an IP address can register 20 times, and following links and sending the password form is limited per IP address. Past that, the page shows “Too many attempts”: see [Too many attempts or 429](https://goiabada.dev/troubleshooting/too-many-attempts-or-429/). Every registration with email verification leaves a `requested_registration` entry in the [audit log](https://goiabada.dev/concepts/audit-log/), whose `outcome` says what it led to, such as `link_issued` or `notice_issued`. The address is recorded only as a digest. An account created leaves `created_user`, an activated one `activated_account`, and a refused link `failed_account_activation_code`. > **Tip** > > Self-registration is for people. [Dynamic client registration](https://goiabada.dev/concepts/clients/#self-registered-clients) is different: it lets applications register themselves as clients. ## Next steps [Password recovery](https://goiabada.dev/concepts/password-recovery/): How users who forgot their password set a new one. [Users and groups](https://goiabada.dev/concepts/users-and-groups/): Manage the accounts people create. # Password recovery Source: https://goiabada.dev/concepts/password-recovery/ This page helps you let users set a new password when they forget theirs, without an administrator’s help. Password recovery works by email: the user asks for a link, and the link opens a form where they choose a new password. It needs email set up, and an email address the auth server knows is theirs. With email off, the sign-in page shows no **Forgot password?** link, and `/forgot-password` answers `404`. ## Let users recover their password 1. Set up email under **Admin**, **Email - SMTP**, and send yourself a test message from the **Send test email** tab. The sign-in page now shows **Forgot password?**. 2. Make sure your users’ email addresses are verified. A user’s **Email** tab shows it as **Email verified**, and users can verify their own under **Account**, **Email verification**. That’s all. A user who clicks **Forgot password?** enters their email address and gets a link to set a new password. When something goes wrong, see [A user cannot reset their password](https://goiabada.dev/troubleshooting/a-user-cannot-reset-their-password/). ## Who gets a link The link is sent only when the address belongs to a user who is **enabled** and whose address is **verified**. For any other address, one with no account, an unverified one, or a disabled user’s, nothing is sent and no link is made. Whatever happened, the page says the same thing: “If you’re a registered user, a password reset link has been sent to your email address.” That way the form can’t be used to find out which addresses have an account. The reason stays in the [audit log](https://goiabada.dev/concepts/audit-log/), as the `outcome` of the `requested_password_reset` entry, which records the address only as a digest. > **Caution** > > Users who registered without email verification, and users an administrator created without turning on **Email verified**, start with an unverified address, so they can’t recover a password on their own until it’s verified. They can verify it under **Account**, **Email verification**, which appears whenever email is set up, or an administrator can mark it verified or set a new password for them. ## The link A reset link works once. It must be followed within 5 minutes of being sent, and the form it opens must then be sent within 5 more. It stops working when: - it’s been used; - the user asked for another one, which replaces it; - the user was disabled; - the user’s email address was changed, by the user or by an administrator, since the link belongs to the address it was sent to. A dead link shows “Unable to set the password. The verification code appears to be invalid or expired.” and the user asks for a new one. Each refusal leaves a `failed_reset_password_code` entry in the audit log, whose `reason` narrows it down: `code_expired` for a link past its lifetime, `marker_expired` for a form sent more than 5 minutes after the link was followed, `account_disabled` for a disabled user, and `unknown_code` for a link that was used, replaced by a newer one, or retired by an address change, which it can’t tell apart. The new password must meet the **Password policy** under **Admin**, **General**. ## What a reset does Setting a new password from the link ends every session the user has and revokes their refresh tokens, so every client must sign them in again. Whoever reset the password isn’t necessarily whoever holds those sessions, which is the point when a laptop is stolen. The audit log records it as `revoked_user_auth_state`, with the reason `password_reset`. See [credential changes](https://goiabada.dev/concepts/ending-sessions/#credential-changes). ## The link an administrator sends When you create a user and choose **Email the user a link to set up their password**, the user gets this same kind of link, except that it works for 24 hours, since they read it when they read their mail. Any link to an account that has no password yet lasts 24 hours. It works even though the address isn’t verified yet, since an administrator chose to send it there. If it expires unused, the user can’t ask for another with **Forgot password?**, which sends nothing to an unverified address, and nothing sends the setup link again. Turn on **Email verified** on the user’s **Email** tab so they can, or set their password yourself on the **Authentication** tab. Setting the password through it also marks the address verified, since following the link proved it’s theirs, and leaves a `verified_email` entry in the audit log. If you typed the address wrong, correcting it retires the link that went to the wrong address. Open the user’s **Authentication** tab and use **Set password** instead, or have the user ask for a reset link once their address is verified. ## Limits An address can ask for 5 links every 5 minutes, whether or not the rate limiter is on. With it on, an IP address can ask for 20, and following links and sending the form is limited to 30 every 5 minutes per IP address. Past that, the page shows “Too many attempts”: see [Too many attempts or 429](https://goiabada.dev/troubleshooting/too-many-attempts-or-429/). ## Next steps [A user cannot reset their password](https://goiabada.dev/troubleshooting/a-user-cannot-reset-their-password/): What stops a reset, and how to fix it. [Self-registration](https://goiabada.dev/concepts/self-registration/): Let people create their own accounts. # Resources and permissions Source: https://goiabada.dev/concepts/resources-and-permissions/ This page helps you describe what your APIs protect, and give users, groups and clients permission to use it. A **resource** is something you protect, usually an API, such as `product-api`. A **permission** is something a resource lets you do, such as `read` or `delete-product`. Together they make a scope, written `resource:permission`: `product-api:read` is the permission `read` on the resource `product-api`. That’s what your app asks for, and what the token it gets back carries. ## Create a resource and its permissions 1. In the admin console, open **Admin**, **Resources and permissions** and click **Create new**. 2. Enter a **Resource identifier**, such as `product-api`, and a **Description**. Click **Create**. 3. Click **Manage** beside it and open **Permissions**. For each thing your API lets callers do, enter a **Permission identifier**, such as `read`, and a **Permission description**, and click **Create permission**. Click **Save** when the list is done. 4. Give each permission to whoever needs it: - a **user**, on the user’s **Permissions** tab, or on the resource’s **Users with permission** tab; - a **group**, on the group’s **Permissions** tab, or on the resource’s **Groups with permission** tab, and every member gets it; - a **client**, on the client’s **Permissions** tab, for a service that calls your API on its own behalf. The tab is available while **Client credentials** is on for the client. On a **Permissions** tab, choose the **Resource** and the **Permission**, click **Grant permission**, and click **Save**: until you save, nothing is granted. Your app then asks for `product-api:read` in its `scope`, beside any OpenID Connect scopes, and your API checks the access token’s `scope` before it answers. See [Scopes](https://goiabada.dev/concepts/scopes/). ## Identifiers A resource identifier and a permission identifier are each 3 to 38 characters long. They start with a letter, end with a letter or digit, and use only letters, digits, `-` and `_`, with no `--` or `__`. They’re case-sensitive, so the scope is too: `product-api:read` isn’t `Product-API:read`. A description is at most 100 characters, and can’t contain `<` or `>`. Two resources can’t share an identifier, and neither can two permissions of one resource. ## A permission belongs to its resource A permission identifier only means something on its own resource. Two resources can each have a permission called `read`, and they’re two different permissions: someone holding `product-api:read` can’t use `reports-api:read`. Grant each one separately. So reusing short names like `read`, `write` and `delete` on every resource is normal, and it’s what makes the resource half of the scope matter. It goes for the `authserver` resource too: a `manage` permission on your own resource has nothing to do with `authserver:manage`. ## Who holds a permission A **user** holds the permissions given to them and those of every group they’re in. When your app signs a user in, the token carries only the permissions the user holds out of those it asked for. The others are left out without an error, unless none is left: then the request is refused with `access_denied`, as [invalid_scope](https://goiabada.dev/troubleshooting/invalid-scope/#the-user-doesnt-hold-it) explains. A **client’s** own permissions are for the client credentials flow alone, when the client gets a token for itself. A token a client gets for a user carries the user’s permissions, never the client’s. ## Renaming and deleting Renaming a resource or a permission changes its scope, so every app asking for the old one starts getting refused. The admin console asks you to confirm a renamed resource identifier. Deleting a permission takes it away from every user, group and client that held it. Deleting a resource deletes all its permissions too, with the same effect. ## The authserver resource `authserver` is Goiabada’s own resource. Its permissions decide who can manage their own account and who can administer Goiabada, so it’s protected: - its identifier can’t be changed, and it can’t be deleted; - its built-in permissions can’t be renamed or deleted; - its description can be changed, and you can add permissions of your own to it. These are its built-in permissions, with the description each starts with: | Permission identifier | Description | Administrative | | - | - | - | | `manage-account` | View and update user account data for the current user | No | | `manage` | Full administration, including administrators and administrative permissions | Yes | | `admin-read` | Read-only access to all admin API endpoints | Yes | | `manage-users` | Manage users and groups that are not administrators, and their non-administrative permissions | Yes | | `manage-clients` | Manage OAuth2 clients that are not administrators | Yes | | `manage-settings` | Manage system settings, except email and audit logging, and signing keys | Yes | | `browser-sessions` | Read and write admin console browser sessions | Yes | Every user gets `manage-account` when they’re created. A user, group or client holding any of the six administrative ones is an administrator, and only `authserver:manage` can grant one, revoke one, or change its description. [Administrators](https://goiabada.dev/reference/api/administrators/) has the whole rule. > **Caution** > > Granting an administrative permission is how you make someone an administrator. Grant one as carefully as you’d hand out the admin console’s password. Each grant and revocation leaves an `administrative_permission_changed` entry in the [audit log](https://goiabada.dev/concepts/audit-log/#events-to-alert-on) you can alert on. ## Next steps [Scopes](https://goiabada.dev/concepts/scopes/): How your app asks for a permission, and what else it can ask for. [Administrators](https://goiabada.dev/reference/api/administrators/): Who can administer Goiabada, and what each scope may do. # Scopes Source: https://goiabada.dev/concepts/scopes/ This page helps your app ask for the right scopes, and tells you which claims about the user each one gets you. A scope is something a client asks for when it requests a token. There are two kinds: - **OpenID Connect scopes**, such as `openid`, `profile` and `email`, which give your app information about the user, as claims. - **Permission scopes**, written `resource:permission`, such as `product-api:read`, which let your app call an API. See [Resources and permissions](https://goiabada.dev/concepts/resources-and-permissions/). ## Ask for scopes 1. Decide what your app needs. To know who the user is, ask for `openid`. Add `profile`, `email` or another scope below for each piece of information you’ll use, and a permission scope for each API call. 2. Put them in the `scope` parameter of the authorization request, separated by single spaces: ```http GET /auth/authorize?client_id=my-app&redirect_uri=https%3A%2F%2Fapp.example.com%2Fcallback&response_type=code&scope=openid%20profile%20email%20product-api%3Aread&code_challenge=...&code_challenge_method=S256&state=... HTTP/1.1 Host: auth.example.com ``` 3. Read the `scope` in the token response. It’s what was granted, which can be less than you asked for. Ask only for what you use: the user sees the list on the consent screen, when the client asks for consent. ## OpenID Connect scopes and their claims | Scope | Claims | | - | - | | `openid` | `sub`, the user’s subject. It also gets your app an ID token, and lets it call `/userinfo`. | | `profile` | `name`, `given_name`, `middle_name`, `family_name`, `nickname`, `preferred_username`, `profile`, `picture`, `website`, `gender`, `birthdate`, `zoneinfo`, `locale` and `updated_at` | | `email` | `email` and `email_verified` | | `address` | `address`, an object with `formatted`, `street_address`, `locality`, `region`, `postal_code` and `country` | | `phone` | `phone_number` and `phone_number_verified` | | `groups` | `groups`, the identifiers of the user’s groups | | `attributes` | `attributes`, the user’s attributes and those of their groups, as keys and values | | `offline_access` | No claim. It gets your app an offline refresh token, which keeps working after the user’s session ends. | A claim with no value is left out rather than sent empty: a user with no nickname has no `nickname`. `email_verified` and `phone_number_verified` are always sent with their scope, `address` only when some part of the address is set, and `picture` only when the user has a profile picture. `profile` is the auth server’s base URL followed by `/account/profile`, where the auth server has no page: a user’s own profile is in the admin console, under **Account**, **Profile**, so don’t send users to the claim’s URL. `locale` is the language the user chose. `groups` lists only the groups set to be included, and `attributes` only the attributes set to be included: see [Users and groups](https://goiabada.dev/concepts/users-and-groups/#groups). `groups` and `attributes` are Goiabada’s own. `offline_access` is defined by [OpenID Connect Core, section 11](https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess), and the rest by [section 5.4](https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims). ## Where the claims go Your app can read the claims in three places. - **`/userinfo`** answers with the claims of every OpenID Connect scope in the access token you call it with. The token must carry `openid`. - **The ID token** carries them when **Include OpenID Connect claims in the ID token** is on, under **Admin**, **Tokens**. It’s on in a new installation. - **The access token** carries them when **Include OpenID Connect claims in the access token** is on, on the same page, and the token carries `openid`. It’s off in a new installation. A client can override either setting on its own **Tokens** tab. Turn ID token claims off for a client that expects them only from `/userinfo`, as OpenID Connect describes when an access token is issued, or that needs a small ID token. Turn access token claims on when your API should read the user’s details from the token without calling `/userinfo`. Neither setting decides `groups` and `attributes`. Each group and attribute has its own two settings instead: **Include in access token** for the access token, and **Include in id token** for the ID token and `/userinfo`. The claims about the sign-in itself, such as `sub`, `iss`, `aud`, `exp`, `auth_time` and `acr`, are always in the ID token. See [Tokens](https://goiabada.dev/concepts/tokens/). ## Permission scopes A permission scope names a permission: `product-api:read` is the permission `read` on the resource `product-api`. - When your app signs a user in, the token carries the permissions the user holds, directly or through a group, out of those it asked for. The rest are left out without an error, unless nothing is left: then the request is refused with `access_denied`. - With the client credentials flow, the token carries the permissions the client itself holds. Leave `scope` out to get all of them. - The administrative scopes, such as `authserver:manage`, need the client to be allowed to request them. See [administrative scopes](https://goiabada.dev/concepts/clients/#administrative-scopes). ## Rules Scopes are case-sensitive and separated by exactly one space, with none at either end. Asking for `offline_access` alone is refused, since it grants nothing by itself. A scope the auth server doesn’t know, or a permission the client may not have, is refused with `invalid_scope`: see [invalid_scope](https://goiabada.dev/troubleshooting/invalid-scope/). ## Next steps [Resources and permissions](https://goiabada.dev/concepts/resources-and-permissions/): Create the permissions your permission scopes name. [Tokens](https://goiabada.dev/concepts/tokens/): What each token carries, and how long it lasts. # Tokens Source: https://goiabada.dev/concepts/tokens/ This page helps you understand the tokens your app gets, and check one before you trust it. Your app gets up to three tokens from the [token endpoint](https://goiabada.dev/reference/endpoints/token/): - **An access token**, which your app sends to an API in an `Authorization: Bearer` header. It says what your app may do there. - **An ID token**, which tells your app who signed in and how. Your app gets one only when it asks for the `openid` scope. - **A refresh token**, which your app trades for new tokens without asking the user to sign in again. See [Refresh tokens](https://goiabada.dev/concepts/refresh-tokens/). Each one is a JWT (JSON Web Token): a set of claims, statements like “the user is `sub`”, signed by the auth server. The access token and the ID token are for your app and your APIs to read. The refresh token is for the auth server alone, so treat it as an opaque string. ## Check a token Your API checks every access token it’s sent, and your app checks the ID token before it trusts who signed in. Most JWT and OpenID Connect libraries do all of this for you once they know the auth server’s discovery URL. 1. Check the signature. Take the key whose `kid` matches the one in the token’s header from the key set at `/certs`, and verify the token with it. Tokens are signed with RS256. See [Discovery and JWKS](https://goiabada.dev/reference/endpoints/discovery-and-jwks/). 2. Check `iss` is the `issuer` from the discovery document, and that `exp` hasn’t passed. 3. Check `aud`. An ID token’s is your client identifier. An access token’s names the resource of each permission it carries, such as `product-api`, so your API checks its own resource identifier is there. 4. Check what the token is for: - An ID token’s `nonce` is the one your app sent in the authorization request. - An access token’s `scope` holds the permission the operation needs, such as `product-api:read`. A decoded access token looks like this: ```json { "iss": "https://auth.example.com", "sub": "c5b9b6a2-4b39-4b8e-9a8e-6f7f2b3a1d10", "aud": ["authserver", "product-api"], "scope": "openid profile product-api:read", "typ": "Bearer", "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", "auth_state_generation": 1 } ``` ## How long tokens last These settings are under **Admin**, **Tokens** in the admin console, and apply to every client: | Setting | Default | | - | - | | **Token expiration in seconds** | 300 (5 minutes) | | **Include OpenID Connect claims in the access token** | Off | | **Include OpenID Connect claims in the ID token** | On | The first sets how long both the access token and the ID token last. The other two decide whether user claims such as `email` or `name` go in each token, as [Scopes](https://goiabada.dev/concepts/scopes/#where-the-claims-go) describes. A client can override all three on its own **Tokens** tab: a lifetime of 0 there uses this setting, and each claims switch can be on, off, or left to this setting. See [Clients](https://goiabada.dev/concepts/clients/#tokens). Keep the lifetime short. An access token can’t be taken back once it’s issued: an API checking it by signature alone accepts it until `exp`, whatever happens to the user’s session meanwhile. The lifetime is the longest that can last. See [what ending a session doesn’t reach](https://goiabada.dev/concepts/ending-sessions/#what-it-doesnt-reach). An authorization code works once, for 60 seconds. [Refresh tokens](https://goiabada.dev/concepts/refresh-tokens/#how-long-a-refresh-token-lasts) have lifetimes of their own. ## The access token | Claim | | | - | - | | `iss` | The issuer | | `sub` | The user’s [subject](https://goiabada.dev/concepts/glossary/#subject). In a token from the client credentials flow, the client identifier | | `aud` | The resource of each permission scope, and `authserver` when the scope has an OpenID Connect scope, since `/userinfo` belongs to it. A string when there’s one, an array when there are more | | `scope` | The scopes the token carries | | `typ` | `Bearer` | | `iat`, `nbf`, `exp` | When it was issued, when it becomes valid, which is the same moment, and when it expires | | `jti` | The token’s own identifier | | `auth_time`, `acr`, `amr` | When and how the user signed in. See [ACR and AMR](https://goiabada.dev/concepts/acr-and-amr/) | | `sid` | The session the token came from, when there is one. See below | | `auth_state_generation` | Reserved. See below | | `nonce` | The `nonce` of the authorization request, when it had one | User claims, `groups` and `attributes` come on top, as [Scopes](https://goiabada.dev/concepts/scopes/#where-the-claims-go) describes. A token from the client credentials flow names no user, so it carries none of `auth_time`, `acr`, `amr`, `sid`, `auth_state_generation` or `nonce`. **`sid` isn’t always there.** It’s in an access token only when the grant it came from is tied to a session. A token from an `offline_access` grant has none, nor does one from the [password grant](https://goiabada.dev/legacy-flows/ropc/): those grants are meant to outlive the browser session that started them. Don’t treat a missing `sid` as an invalid token. **`auth_state_generation` is reserved.** It’s how the auth server tells a token issued before the user’s credentials last changed from one issued after. Don’t parse it, compare it between tokens or build anything on it: its meaning can change. ## The ID token | Claim | | | - | - | | `iss` | The issuer | | `sub` | The user’s subject | | `aud` | Your client identifier | | `iat`, `nbf`, `exp` | When it was issued, when it becomes valid, and when it expires | | `jti` | The token’s own identifier | | `auth_time` | When the user last entered a credential, a password or a one-time code | | `acr`, `amr` | How the user signed in. See [ACR and AMR](https://goiabada.dev/concepts/acr-and-amr/) | | `sid` | The session the user signed in with, when there is one. [`/auth/logout`](https://goiabada.dev/reference/endpoints/logout/) matches on it | | `nonce` | The `nonce` of the authorization request, when it had one | | `at_hash` | A hash of the access token issued beside it, in the [implicit flow](https://goiabada.dev/legacy-flows/implicit/) only | User claims, `groups` and `attributes` come on top, as [Scopes](https://goiabada.dev/concepts/scopes/#where-the-claims-go) describes. `auth_time` is never simply when the token was issued: a sign-in that reused a session, [`prompt=none`](https://goiabada.dev/concepts/prompt/) included, keeps the time the user last signed in, which is what `max_age` is measured against. ## Which scopes a token gets The scope your app asks for isn’t always the scope it gets. Once the user has signed in, the auth server drops every [permission scope](https://goiabada.dev/concepts/scopes/#permission-scopes) the user doesn’t hold, directly or through a group, without an error. OpenID Connect scopes and `offline_access` are always kept. If that leaves nothing, the request ends with `access_denied`, “The user is not authorized to access any of the requested scopes”. What’s left is what the [consent screen](https://goiabada.dev/concepts/clients/#consent-required) asks about, when it’s shown. The user can untick scopes there, and the tokens carry only the ones they approved. ## Checked again before a code is issued A user can sit on the consent screen for as long as they like, so just before it issues the code, or the implicit flow’s tokens, the auth server checks the request again, in this order: 1. The redirect URI is still registered on the client. If it isn’t, the user sees a page, and nothing reaches your app. 2. The client is still enabled, or the user sees an error page. The flow is still on for it, or your app gets `unauthorized_client`. It’s still [allowed to request](https://goiabada.dev/concepts/clients/#administrative-scopes) any administrative scope it asked for, or it gets `invalid_scope`. 3. The user who signed in is the one the [`id_token_hint`](https://goiabada.dev/concepts/id-token-hint/) names, when there was one, or your app gets `login_required`, “The authenticated user does not match the id_token_hint”. 4. The user’s session is still there, still valid, and still theirs, or the sign-in starts again from the password. 5. The user is still enabled, or your app gets `access_denied` with “The user account is disabled.” Their credentials haven’t changed since they signed in, or the sign-in starts again from the password. 6. The user still holds at least one of the scopes, or your app gets `access_denied`. A [silent request](https://goiabada.dev/concepts/glossary/#silent-request) can’t start a sign-in again, so it gets `login_required`, “User authentication is required”, instead. > **Note** > > Each error reaches your app the way [Authorize](https://goiabada.dev/reference/endpoints/authorize/#when-an-error-reaches-your-app) describes, and a client that registered itself is never sent one. ## Next steps [Refresh tokens](https://goiabada.dev/concepts/refresh-tokens/): Get new tokens without asking the user to sign in again. [Scopes](https://goiabada.dev/concepts/scopes/): What to ask for, and which claims each scope brings. [Token](https://goiabada.dev/reference/endpoints/token/): The token endpoint's parameters, answers and errors. # Refresh tokens Source: https://goiabada.dev/concepts/refresh-tokens/ This page helps you keep users signed in to your app with a refresh token, and keep that token working. A refresh token is what your app trades for a new access token when the old one expires, without sending the user back to sign in. Your app gets one every time it redeems an authorization code, and there are two kinds: - **A normal refresh token** is tied to the user’s [session](https://goiabada.dev/concepts/sessions/), and stops working when the session does. - **An offline refresh token** is what your app gets when it asks for the `offline_access` scope. It keeps working after the user’s session expires, so a background job can call an API for the user while they’re away. ## Refresh a token 1. If your app needs to work while the user is away, add `offline_access` to the scope of the authorization request. The user is then shown the [consent screen](https://goiabada.dev/concepts/clients/#consent-required), so they can see your app is asking for it: ```http GET /auth/authorize?client_id=my-app&redirect_uri=https%3A%2F%2Fapp.example.com%2Fcallback&response_type=code&scope=openid%20offline_access%20product-api%3Aread&code_challenge=...&code_challenge_method=S256&state=... HTTP/1.1 Host: auth.example.com ``` 2. When the access token expires, send the refresh token to the token endpoint: ```bash curl -X POST https://auth.example.com/auth/token \ -u my-app:CLIENT_SECRET \ -d grant_type=refresh_token \ -d refresh_token=eyJhbGciOiJSUzI1NiIs... ``` A public client sends `client_id` in the body instead of the `-u` line. 3. The answer carries new tokens, a new refresh token among them, and a new ID token when the scope has `openid`: ```json { "access_token": "eyJhbGciOiJSUzI1NiIs...", "token_type": "Bearer", "expires_in": 300, "id_token": "eyJhbGciOiJSUzI1NiIs...", "refresh_token": "eyJhbGciOiJSUzI1NiIs...", "refresh_expires_in": 2592000, "scope": "openid offline_access product-api:read" } ``` Store the new refresh token in place of the old one **before** you use anything else in the answer. The one you sent is spent. ## How long a refresh token lasts An offline refresh token’s lifetimes are under **Admin**, **Tokens** in the admin console: | Setting | Default | | - | - | | **Offline refresh token - idle timeout in seconds** | 2592000 (30 days) | | **Offline refresh token - max lifetime in seconds** | 31536000 (1 year) | The idle timeout is how long the token keeps working unused: each refresh hands your app a new token with the full idle timeout ahead of it. The maximum lifetime counts from the first token of the grant, when the user signed in, and no refresh goes past it. A client can override both on its own **Tokens** tab, where 0 uses these settings. A normal refresh token follows the session it’s tied to instead: it stops working unused after the session’s idle timeout, and at the latest when the session reaches its maximum lifetime. Both are under **Admin**, **Sessions**, and no client overrides them. See [how long a session lasts](https://goiabada.dev/concepts/sessions/#how-long-a-session-lasts). Each refresh with a normal refresh token counts as activity, so it keeps the session from idling out. `refresh_expires_in` in the answer is how many seconds the new refresh token has left if it isn’t used. | | Normal | Offline | | - | - | - | | Your app gets it | Without `offline_access` | With `offline_access`, and always from the [password grant](https://goiabada.dev/legacy-flows/ropc/), which has no session | | When the session expires | It stops working | It keeps working | | When someone [ends the session](https://goiabada.dev/concepts/ending-sessions/#end-a-session) | It’s revoked | It’s revoked | | When the user’s [credentials change](https://goiabada.dev/concepts/ending-sessions/#credential-changes) | It’s revoked, unless it belongs to the session the user changed their own password in | It’s revoked, with the same exception | ## What a refresh checks A refresh is refused with `invalid_grant` when the token can’t be used any more: - it expired, or the offline grant reached its maximum lifetime; - a normal refresh token’s session ended or expired; - the user was disabled, or their password changed or was reset, apart from the tokens of the session a user changed their own password in; - the user no longer holds a permission the token carries; - the user withdrew their consent to your app, when it has **Consent required** on or the token is an offline one from a sign-in; - the user hasn’t approved `authserver:manage-account` for your app, or withdrew it, when the refresh renews that scope from a sign-in, whatever **Consent required** says. The admin console’s own client is the one exception. A token issued to another client is refused with `invalid_grant`. The [token endpoint](https://goiabada.dev/reference/endpoints/token/#refresh-token-grant) lists every answer, and `scope` can narrow one refresh’s access token. [This refresh token has been revoked](https://goiabada.dev/troubleshooting/this-refresh-token-has-been-revoked/#other-reasons-a-refresh-fails) gives each `error_description` and why it happens. ## Rotation Every refresh rotates the token: the one your app sent is retired, and the answer carries its replacement. A refresh token is good for one refresh. That’s enforced atomically. When several requests present the same refresh token at once, at most one gets new tokens: if the token is live and issuance succeeds, one wins and the others are refused with `invalid_grant`. If it was already retired, or issuance fails, none does. > **Caution** > > Refresh through **one** path at a time, and store each new refresh token in place of the old one before you use the rest of the answer. > > A retired refresh token sent again revokes every live refresh token of its grant, the one your app holds now included, and the user has to sign in again. That happens whether an attacker sent it or your own app did. Two things send a retired token by accident: - **Parallel refreshes.** Two threads, tabs or instances notice the access token has expired, and each refreshes with the same refresh token. One wins. The other is either refused, or, when it reads the token after the winner retired it, treated as a replay. Share one refresh between them instead. - **Retrying after a lost answer.** The auth server rotated the token, but the answer never reached your app. Retrying sends the retired token. Treat a lost answer as “sign in again”, not “retry”. ## Replay A retired refresh token coming back is a replay. The auth server can’t tell whether an attacker copied it or your app sent it twice, so it assumes the worst, as [RFC 9700 section 4.14.2](https://www.rfc-editor.org/rfc/rfc9700.html#section-4.14.2) describes: it revokes every live token of the token’s rotation family, which is every refresh token descended from the same sign-in, and records the family as revoked. A refresh still under way when that happens yields nothing usable, so the family can’t keep rotating. Someone who stole a refresh token and lost the race to use it can’t wait and rotate later. Only that family is revoked. The user’s session, their sign-ins to other clients and their other grants keep working, a second grant from the same session included. Access tokens already issued aren’t revoked: they’re signed JWTs your APIs check on their own, so revoking the family stops the next refresh, not an access token already handed out. That’s one more reason to keep [access tokens short-lived](https://goiabada.dev/concepts/tokens/#how-long-tokens-last). A replay leaves a `refresh_token_replay_detected` entry in the [audit log](https://goiabada.dev/concepts/audit-log/) when it changes something: it revokes at least one live token, or records the family as revoked for the first time. A repeat that does neither leaves no entry, so one incident isn’t a row per attempt. The entry never holds the refresh token itself. It doesn’t prove an attack either: an app that refreshes in parallel causes one too. The auth server keeps a revoked refresh token until it would have expired, which is what lets it recognize a replay at all. See [refresh token storage](https://goiabada.dev/deploy/database/#refresh-token-storage). ## Next steps [This refresh token has been revoked](https://goiabada.dev/troubleshooting/this-refresh-token-has-been-revoked/): Why a refresh failed and took the newest token with it. [Sessions](https://goiabada.dev/concepts/sessions/): How long a session lasts, and what keeps it alive. [Ending sessions](https://goiabada.dev/concepts/ending-sessions/): What ending a session revokes, and what it leaves. # Sessions Source: https://goiabada.dev/concepts/sessions/ This page helps you understand when users are asked to sign in, and when they go straight through. When a user signs in, the auth server starts a session for their browser and names it in a cookie. The next time any client sends that browser to sign in, the session is enough: the user isn’t asked for their password again. That’s [single sign-on](https://goiabada.dev/concepts/glossary/#single-sign-on-sso). The session remembers who signed in, when, and how strongly, as its [ACR level](https://goiabada.dev/concepts/acr-and-amr/). Each user can see their sessions under **Account**, **Sessions** in the admin console, and an administrator sees them on the user’s **Sessions** tab and on a client’s **User sessions** tab. ## Ask for a recent sign-in A session can be hours old. When an operation needs the user to have signed in recently, such as changing their payment details, ask for it. 1. Add `max_age` to the authorization request, in seconds. This asks for a sign-in within the last five minutes: ```http GET /auth/authorize?client_id=my-app&redirect_uri=https%3A%2F%2Fapp.example.com%2Fcallback&response_type=code&scope=openid&max_age=300&code_challenge=...&code_challenge_method=S256&state=... HTTP/1.1 Host: auth.example.com ``` A user who signed in longer ago than that enters their password again. One who signed in within it goes straight through. 2. Check `auth_time` in the ID token before you allow the operation. It’s when the user last entered a credential, so it’s no older than `max_age` allows: ```json { "sub": "c5b9b6a2-4b39-4b8e-9a8e-6f7f2b3a1d10", "auth_time": 1760000000, "acr": "urn:goiabada:level1" } ``` To ask for a password every time, whatever the session, send [`prompt=login`](https://goiabada.dev/concepts/prompt/#promptlogin) instead. With both, `prompt=login` wins. ## How long a session lasts A session’s lifetimes are under **Admin**, **Sessions** in the admin console, and apply to every client: | Setting | Default | | - | - | | **User session - idle timeout in seconds** | 7200 (2 hours) | | **User session - max lifetime in seconds** | 86400 (24 hours) | A session can be used only while all of these hold: | Check | Measured from | Set by | | - | - | - | | Idle timeout | The session’s last activity | **User session - idle timeout in seconds** | | Maximum lifetime | When the session started, so activity never extends it | **User session - max lifetime in seconds** | | `max_age` | When the user last entered a credential, which is `auth_time`. When the user’s two-factor authentication is removed, it’s when they entered the password, as [ACR and AMR](https://goiabada.dev/concepts/acr-and-amr/#a-session-thats-already-there) explains | The authorization request, when it has one | Each bound includes its last second: a session idle for exactly the idle timeout still counts, and so does a sign-in exactly `max_age` seconds ago, as [OpenID Connect Core 1.0 section 3.1.2.1](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest) describes. The idle timeout can’t be longer than the maximum lifetime. A session that fails one of them isn’t used, and the user signs in again. It isn’t ended either: a session that has merely expired revokes nothing, and its offline refresh tokens keep working. A background job removes idle and expired sessions every 12 hours. ## What keeps a session alive A session’s last activity moves forward whenever it’s used: - a sign-in to any client completes through it, a [silent request](https://goiabada.dev/concepts/glossary/#silent-request) included; - a client refreshes with a [normal refresh token](https://goiabada.dev/concepts/refresh-tokens/) from it. Neither moves its maximum lifetime. ## Single sign-on When a browser arrives with a session that’s valid and belongs to the user signing in, the user isn’t asked for their password, and goes on with the session’s ACR level. They’re asked for a one-time code only when the client needs a higher level than the session reached, which is a [step-up](https://goiabada.dev/concepts/acr-and-amr/#a-session-thats-already-there). A request whose `id_token_hint` names someone else, or with `prompt=login`, asks for a password whatever the session, and for a one-time code whenever the level needs one. A user who has been disabled can’t use a session. Disabling them ends their sessions, so their next sign-in starts from the password, which answers “Your user account is disabled.” once the right password is entered, and a [silent request](https://goiabada.dev/concepts/glossary/#silent-request) gets `login_required`. A session of a disabled user that’s still found ends the request with `access_denied` and “The user account is disabled.” ## At the end of a sign-in Once the user has signed in, the auth server checks them again before it records anything: - **A disabled user** is refused with `access_denied`, and no session is written. - **A user whose credentials changed** while they were signing in, because their password was changed or reset, signs in again from the password, with nothing written. Then it records the sign-in in a session: - **The browser’s session is valid and the user’s own.** It’s reused: its last activity moves forward, and its ACR level goes up when this sign-in reached a higher one. Reusing it never lowers its level. When the user entered a credential in this sign-in, its `auth_time` is updated to then. A sign-in that asked for the password again, such as one with `prompt=login`, replaces the session’s level and methods with its own, as described in [A session that’s already there](https://goiabada.dev/concepts/acr-and-amr/#a-session-thats-already-there). - **Otherwise, a new session starts.** It replaces the user’s own session the browser had, if it had one that’s no longer valid, and the user’s other sessions from the same device, which means the same browser `User-Agent` and the same IP address. Nothing those sessions handed out is revoked, though their normal refresh tokens stop working with them. A session the browser had for **another** user is ended, as [Ending sessions](https://goiabada.dev/concepts/ending-sessions/#a-different-user-signs-in-on-the-same-browser) describes. A new session needs a password entered in this sign-in. A sign-in that was relying on a session that ended partway through goes back to the password step rather than starting one from nothing. The cookie’s identifier changes whenever a session starts and whenever its ACR level goes up, so an identifier copied before the sign-in names nothing afterwards. ## Closing the browser Closing the browser doesn’t end a session with the auth server. The cookie lasts as long as the session does, so reopening the browser within the idle timeout signs the user straight back in, and a restarted machine or a laptop shut for lunch costs nobody a password. The admin console does the opposite on purpose. Its own cookie lasts only until the browser closes, so reopening the browser asks an administrator to sign in to it again, even while the auth server still has their session. An administrator’s browser reaches every part of the deployment, so one more sign-in is worth it to keep a lost or forgotten machine from still holding that access. Some browsers restore what was open after a crash or an update, and that can bring an admin console session back with it. > **Note** > > To end a session rather than leave it to expire, sign out or end it from the sessions page. See [Ending sessions](https://goiabada.dev/concepts/ending-sessions/). ## Next steps [Ending sessions](https://goiabada.dev/concepts/ending-sessions/): What ends a session, and what that revokes. [prompt](https://goiabada.dev/concepts/prompt/): Check for a session silently, or always ask for a password. [ACR and AMR](https://goiabada.dev/concepts/acr-and-amr/): How strongly a session's user signed in. # Ending sessions Source: https://goiabada.dev/concepts/ending-sessions/ This page helps you end a user’s session, and know what that cuts off. A [session](https://goiabada.dev/concepts/sessions/) ends in one of four ways: someone ends it from the admin console or the API, the user signs out, the user’s credentials change, or a different user signs in on the same browser. And when an app redeems an authorization code twice, the auth server ends the session the code came from if it had refresh tokens to revoke: see `auth_code_reuse_detected` in the [audit log](https://goiabada.dev/concepts/audit-log/#signs-of-an-attack). Each is a security action rather than housekeeping, so each one decides what happens to the tokens the session handed out. A session that merely expires is different: it revokes nothing. ## End a session 1. Find the session. A user sees their own under **Account**, **Sessions** in the admin console. An administrator sees a user’s on the user’s **Sessions** tab, and every session signed in to a client on the client’s **User sessions** tab. 2. Choose **End session** beside it, and confirm. The same goes through the API: an administrator calls [`DELETE /api/v1/admin/user-sessions/{id}`](https://goiabada.dev/reference/api/admin/operations/deleteusersession/), and a user calls [`DELETE /api/v1/account/sessions/{id}`](https://goiabada.dev/reference/api/account/operations/deleteaccountsession/) for one of their own. 3. The browser that held the session has to sign in again, and the tokens the session handed out stop working, as below. ## What ending a session revokes | What | What happens | | - | - | | The session | It’s deleted. The browser that held it has to sign in again | | Refresh tokens from it | Revoked, offline ones included | | Authorization codes from it | Revoked, so a code that was issued but not redeemed yet can’t be exchanged | | The user’s other sessions | Untouched | That’s what sets ending a session apart from one expiring. An [offline refresh token](https://goiabada.dev/concepts/refresh-tokens/) is meant to outlive the session it came from, so it keeps working when that session times out. Ending the session revokes it on purpose. Each one leaves two entries in the [audit log](https://goiabada.dev/concepts/audit-log/): `terminated_user_session`, which records what was revoked, and `deleted_user_session`, the record that the session is gone. Count `terminated_user_session` to know how many sessions were ended. ## A sign-in under way A sign-in can’t slip past an ended session. If someone was partway through signing in to an app on the strength of the session you ended, the sign-in goes back to the password step rather than quietly starting a new session. That holds on the consent screen too: just before it issues the code, the auth server checks the session again, so a sign-in left waiting there goes back to the password step as well, and so does one whose session timed out while the screen was open. A [silent request](https://goiabada.dev/concepts/glossary/#silent-request) can’t show a page, so it gets `login_required` instead, and your app decides when to send the user to sign in. Two things withhold that error: a client that [registered itself](https://goiabada.dev/concepts/clients/#self-registered-clients) is never sent one, and neither is a redirect URI no longer registered on the client. A silent request that doesn’t come back is then the signal. See [The app gets no error back](https://goiabada.dev/troubleshooting/the-app-gets-no-error-back/). ## Signing out Your app signs a user out by sending them to [`/auth/logout`](https://goiabada.dev/reference/endpoints/logout/). Signing out revokes nothing: no refresh token and no authorization code is revoked. What it ends depends on what the request carries: - **With an `id_token_hint`**, the auth server removes your app from the session the hint names. The session itself ends only when no other client is using it, so the user stays signed in to the others. - **Without one**, once the user confirms on the sign-out page, the whole session ends. Either way the browser’s cookie is cleared, so its next sign-in starts from the password. A normal refresh token stops working once the session behind it has ended. Offline refresh tokens aren’t tied to a session and keep working. A sign-out with a confirmed hint leaves `deleted_user_session_client` in the audit log, for your app leaving the session, and also `logout` when yours was the session’s last client and the session ended. One without a hint leaves `logout`, even when there was no session, and `deleted_user_session` when there was one. ## Credential changes When a user’s credentials change, their sessions and refresh tokens stop being usable: | Action | What happens | `reason` | | - | - | - | | The user resets a forgotten password | Every session of theirs is ended and every refresh token revoked | `password_reset` | | The user changes their own password | The same, except the session they’re changing it from | `password_change` | | An administrator sets the user’s password | Every session of theirs is ended and every refresh token revoked | `admin_password_set` | | An administrator disables the user | The same. Enabling them again doesn’t bring the sessions back | `account_disabled` | That covers every grant, offline refresh tokens whose session has already expired and tokens from the [password grant](https://goiabada.dev/legacy-flows/ropc/) included, and an authorization code that was issued but not redeemed yet can’t be exchanged any more. Each change leaves a `revoked_user_auth_state` entry in the audit log, with the `reason` above and the sessions and refresh tokens it ended. A user changing their own password normally stays signed in to the app they did it from. If that app refreshes at the same moment as the change, it may be asked to sign in again. These end nothing: - **Changing an email address.** The change already asks for the current password, so signing other devices out would protect nothing that person couldn’t do again. Tokens issued before it carry the old `email` claim until they’re refreshed or expire. - **Setting up or removing two-factor authentication.** Each session is checked again for a second factor before it’s used for a level above `urn:goiabada:level1`, and removing it also lowers each session to what a password alone reaches. See [ACR and AMR](https://goiabada.dev/concepts/acr-and-amr/#a-session-thats-already-there). For a lost or stolen device, end the user’s sessions as well: see [A user who lost their authenticator](https://goiabada.dev/guides/require-two-factor-authentication/#a-user-who-lost-their-authenticator). ## A different user signs in on the same browser A shared or borrowed browser can reach the sign-in page still holding someone else’s session. That happens whenever a client sends [`prompt=login`](https://goiabada.dev/concepts/prompt/) or an [`id_token_hint`](https://goiabada.dev/concepts/id-token-hint/) naming another user, and whenever the session there has stopped being valid. When the person who signs in is a **different** user, the browser has changed hands: | What | What happens | | - | - | | The previous user’s session | Ended, as if someone had ended it: deleted, with its refresh tokens and unredeemed authorization codes revoked | | The new user | Gets a session of their own, never the one that was there | | The previous user’s other devices | Untouched. Only what this browser’s session handed out is revoked | The grants that session held were reachable from this browser, and the browser is now someone else’s. An offline refresh token from it would otherwise keep working in the background indefinitely. Nothing carries over. If the client asks for a one-time code, the new user is asked for one, however the previous user signed in, and the `acr` and `amr` in their tokens describe only what they did. Each handover leaves a `cross_user_session_replaced` entry in the audit log, naming both users and the session that was ended, beside the `terminated_user_session` and `deleted_user_session` entries for that session and the `started_new_user_session` entry for the new one. It’s the entry that tells a browser changing hands apart from an administrator ending a session. > **Note** > > When the **same** user signs in again, none of this applies. Their session is reused while it’s still valid. When it isn’t, they get a new session, and nothing is revoked. ## What it doesn’t reach An access token is checked by signature, and an API doesn’t ask the auth server about it, so nothing on this page can take back one already issued: - **The auth server’s own endpoints**, [`/userinfo`](https://goiabada.dev/reference/endpoints/userinfo/) and the Admin and Account APIs, refuse an access token whose session has ended or expired, or whose user was disabled or changed their credentials, on the very next request. - **An access token from an offline grant** carries no `sid`, so ending the session it came from doesn’t reach it, even at the auth server’s own endpoints. A credential change does. - **Your own APIs**, checking the signature alone, accept the token until it expires. That’s inherent to signed tokens, and it’s the main reason to keep [access tokens short-lived](https://goiabada.dev/concepts/tokens/#how-long-tokens-last): the token lifetime, 5 minutes in a new install, is the longest a token can outlive any of this. > **Caution** > > If an API must refuse access the moment a session ends or a user’s credentials change, it has to ask the auth server on every request rather than check the signature alone. A token carrying `openid` can be checked by calling [`/userinfo`](https://goiabada.dev/reference/endpoints/userinfo/) with it. ## Next steps [Sessions](https://goiabada.dev/concepts/sessions/): How long a session lasts, and what keeps it alive. [Refresh tokens](https://goiabada.dev/concepts/refresh-tokens/): The two kinds, and which one outlives a session. [Audit log](https://goiabada.dev/concepts/audit-log/): The entries each ending leaves. # ACR and AMR Source: https://goiabada.dev/concepts/acr-and-amr/ This page helps you choose how strongly users sign in to your app, and check how they did. Two claims in every ID token, and in every access token issued for a user, tell your app about the sign-in: - **`acr`** (Authentication Context Class Reference) is how strongly the user signed in. Goiabada has three levels, from a password alone to a password and a one-time code. - **`amr`** (Authentication Methods References) is what the user did: `pwd` for a password, and `otp` for a one-time code from an authenticator app, which is what [two-factor authentication](https://goiabada.dev/concepts/glossary/#two-factor-authentication) adds. ## Require a level 1. Set the least a sign-in to your app must reach. In the admin console, open your client, choose its **Default ACR level** on the **Settings** tab, and click **Save**. The field shows only while **Authorization code with PKCE** is on. See [Clients](https://goiabada.dev/concepts/clients/#default-acr-level). 2. To ask for more on one request, such as before a payment, add `acr_values` to the authorization request: ```http GET /auth/authorize?client_id=my-app&redirect_uri=https%3A%2F%2Fapp.example.com%2Fcallback&response_type=code&scope=openid&acr_values=urn%3Agoiabada%3Alevel2_mandatory&code_challenge=...&code_challenge_method=S256&state=... HTTP/1.1 Host: auth.example.com ``` 3. Check `acr` in the ID token before you allow the operation. It can be higher than you asked for, so compare it against what the operation needs rather than against your `acr_values`: ```json { "acr": "urn:goiabada:level2_mandatory", "amr": ["pwd", "otp"] } ``` ## Levels | Level | The user signs in with | | - | - | | `urn:goiabada:level1` | A password | | `urn:goiabada:level2_optional` | A password, plus a one-time code if they’ve set up two-factor authentication | | `urn:goiabada:level2_mandatory` | A password and a one-time code, setting up two-factor authentication first if they haven’t | They’re in that order, weakest first. A level satisfies itself and every level before it. Discovery lists the three in `acr_values_supported`. ## Methods `amr` lists every method the user used, in the order they used them: `["pwd"]`, or `["pwd", "otp"]`. Goiabada never sends `mfa`: look for `otp` in `amr`, or check `acr`. Tokens from a refresh token carry the `acr` and `amr` of the sign-in they came from. Tokens from the [password grant](https://goiabada.dev/legacy-flows/ropc/) carry `urn:goiabada:level1` and `["pwd"]`. ## Which level a request asks for The auth server takes the stronger of two levels: the client’s **Default ACR level**, and the first value in `acr_values` it recognizes. The client’s level is a floor, so `acr_values=urn:goiabada:level1` on a client set to `urn:goiabada:level2_mandatory` still asks for a code. A value is recognized only when it’s one of the three levels, exactly: `level1` on its own isn’t. Values it doesn’t recognize are skipped, and when none is recognized the client’s level applies, with no error. The values are separated by single spaces; a parameter with any other spacing is ignored whole. The level is fixed when the auth server accepts the request, so changing a client’s level doesn’t change sign-ins already under way. ## A sign-in with no session When the browser has no session with the auth server, the level and the user’s two-factor authentication decide what they’re asked for, and what your app gets: | Level | User has two-factor authentication | The user enters | `acr` | `amr` | | - | - | - | - | - | | `urn:goiabada:level1` | No | A password | `urn:goiabada:level1` | `["pwd"]` | | `urn:goiabada:level1` | Yes | A password | `urn:goiabada:level1` | `["pwd"]` | | `urn:goiabada:level2_optional` | No | A password | `urn:goiabada:level2_optional` | `["pwd"]` | | `urn:goiabada:level2_optional` | Yes | A password and a code | `urn:goiabada:level2_optional` | `["pwd", "otp"]` | | `urn:goiabada:level2_mandatory` | No | A password, then sets up two-factor authentication and enters a code | `urn:goiabada:level2_mandatory` | `["pwd", "otp"]` | | `urn:goiabada:level2_mandatory` | Yes | A password and a code | `urn:goiabada:level2_mandatory` | `["pwd", "otp"]` | `urn:goiabada:level2_optional` with only `pwd` is the level working as intended: the user hasn’t set up two-factor authentication. If your app wants to nudge them to, that’s the case to look for. ## A session that’s already there A user who already signed in has a [session](https://goiabada.dev/concepts/sessions/), and the session remembers the level it reached. Only a session of the user who’s signing in counts: a session the browser holds for someone else counts as none. - **The session’s level is at least the one asked for.** The user isn’t asked for anything, and your app gets the session’s level, which can be higher than the one asked for. - **It’s lower.** That’s a step-up. The user isn’t asked for their password again, only for what the new level adds, as in the table above: usually a one-time code. The session moves up to the new level, and reusing a session never lowers its level. When the user enters a code, `auth_time` becomes the time they entered it. A request with [`prompt=login`](https://goiabada.dev/concepts/prompt/#promptlogin) doesn’t count the session at all. The user signs in as if there were none, entering every factor the level needs, as in the table above, and the session then takes that sign-in’s level, methods and time. That can lower it: `prompt=login` at a level 1 client over a session at `urn:goiabada:level2_mandatory` leaves the session at `urn:goiabada:level1` with `["pwd"]`, so the next level 3 request asks for the code again. `acr`, `amr` and `auth_time` then always describe the same sign-in. When a user sets up or removes two-factor authentication, each of their sessions is checked again for a second factor before it’s used for a level above `urn:goiabada:level1`: a user with an authenticator enters a code, and a user with none left is asked for nothing at `urn:goiabada:level2_optional` and sets a new one up at `urn:goiabada:level2_mandatory`. That check stays owed until a sign-in completes it: a sign-in abandoned at the code asks again next time. A silent request ([`prompt=none`](https://goiabada.dev/concepts/prompt/)) gets `interaction_required` instead. Removing the authenticator also lowers each of the user’s sessions to what a password alone reaches: `urn:goiabada:level2_optional` at most, and `["pwd"]`. Its `auth_time` goes back to when the password was entered, because after a code `auth_time` is the code’s time, and `["pwd"]` beside it would say a password was entered then. Nobody is signed out. Every token issued from those sessions afterwards says so, at any level: a level 3 request is a step-up, and the user sets up a new authenticator. What was issued before the removal keeps its claims: tokens, and the refresh tokens that renew them, say what the sign-in they came from said. [Ending the user’s sessions](https://goiabada.dev/concepts/ending-sessions/) revokes those refresh tokens. A sign-in under way when the authenticator is removed starts again from the password, even if the user has set up another one meanwhile: its code came from the one removed. `acr` and `max_age` are separate checks. A session at a high enough level still asks for a sign-in when the user signed in longer ago than `max_age` allows. See [Sessions](https://goiabada.dev/concepts/sessions/). ## Setting up two-factor authentication during a sign-in At `urn:goiabada:level2_mandatory`, a user with no authenticator sets one up before they enter a code. The new authenticator is stored only while the account still has none. If it gains one meanwhile, because the same user finished setting one up in another tab or on their account page, nothing is stored, and the sign-in ends on a page saying “Your two-factor authentication settings changed”. That sign-in can’t go on: the user goes back to the app and signs in again, which starts from the account’s current settings. ## Next steps [Clients](https://goiabada.dev/concepts/clients/#default-acr-level): Set a client's default ACR level. [prompt](https://goiabada.dev/concepts/prompt/): What a silent request is refused with when a step-up is due. [Sessions](https://goiabada.dev/concepts/sessions/): How long a session keeps its level. # prompt Source: https://goiabada.dev/concepts/prompt/ This page helps you control what the user sees when your app sends them to sign in. `prompt` is an OpenID Connect parameter of the authorization request. It takes three values: | Value | What the auth server does | | - | - | | `none` | Shows the user nothing. It answers with a code if the browser’s session is enough, and with an error if it isn’t. This is a silent request. | | `login` | Asks the user to sign in, even when the browser has a session. | | `consent` | Shows the consent screen, even when the user has already approved what your app asks for. | Leave `prompt` out and the auth server decides: it uses the browser’s session when there’s a valid one, which is [single sign-on](https://goiabada.dev/concepts/glossary/#single-sign-on-sso), and asks the user to sign in when there isn’t. ## Check for a session silently 1. Send the authorization request with `prompt=none`. Add [`id_token_hint`](https://goiabada.dev/concepts/id-token-hint/) when you know which user to expect: ```http GET /auth/authorize?client_id=my-app&redirect_uri=https%3A%2F%2Fapp.example.com%2Fcallback&response_type=code&scope=openid%20profile&prompt=none&code_challenge=...&code_challenge_method=S256&state=... HTTP/1.1 Host: auth.example.com ``` 2. If the session is enough, the browser comes straight back to your redirect URI with a code, and the user sees no page: ```http HTTP/1.1 302 Found Location: https://app.example.com/callback?code=...&state=... ``` Redeem it at the token endpoint as usual. 3. If it isn’t, the browser comes back with an `error` instead, such as `login_required`. Send the user through the same request without `prompt=none`, so they can sign in. See [login_required](https://goiabada.dev/troubleshooting/login-required/). > **Caution** > > A client that [registered itself](https://goiabada.dev/concepts/clients/#self-registered-clients) gets no error back from a silent request: the auth server never redirects an error to one. Treat a silent request that doesn’t come back as “send the user through an ordinary sign-in”. See [The app gets no error back](https://goiabada.dev/troubleshooting/the-app-gets-no-error-back/). ## Values Separate values with a single space. `login` and `consent` go together: `prompt=login consent` asks the user to sign in and then shows the consent screen. `none` goes alone, and `none` with anything else is refused with `invalid_request`, as is a value Goiabada doesn’t know. `select_account` is defined by OpenID Connect, but the auth server can’t ask a user to choose an account, so it’s refused with `account_selection_required`. Discovery lists the three values it supports in `prompt_values_supported`. ## The silent checks With `prompt=none`, the auth server makes the checks it would otherwise show the user a page for, in this order, and answers with the first that fails: | Check | `error` | `error_description` | | - | - | - | | The browser has a session | `login_required` | “User authentication is required” | | The session is within its idle timeout and maximum lifetime | `login_required` | “User session has expired” | | The user signed in within the request’s `max_age`, when it has one | `login_required` | “Session age exceeds max_age” | | The user is enabled | `access_denied` | “The user account is disabled” | | The session’s user is the one the `id_token_hint` names, when there’s a hint | `login_required` | “The current session user does not match the id_token_hint” | | The session reached the request’s [ACR level](https://goiabada.dev/concepts/acr-and-amr/) | `interaction_required` | “Higher authentication level required” | | The user has two-factor authentication, when the level is `urn:goiabada:level2_mandatory` | `interaction_required` | “Additional authentication setup required” | | The user’s two-factor authentication hasn’t changed since the session was last checked for a second factor, when the level is above `urn:goiabada:level1` | `interaction_required` | “Authentication configuration has changed” | | The user holds at least one of the scopes asked for | `access_denied` | “The user is not authorized to access any of the requested scopes” | | The user has consented to the client, when it requires consent, the request asks for `offline_access`, or the scope holds `authserver:manage-account` and the client isn’t the admin console’s | `consent_required` | “User consent is required” | | That consent covers every scope the user would get, or only `authserver:manage-account` when that alone is why it’s checked | `consent_required` | “Additional consent is required” | When every check passes, the auth server issues the code and records the session’s activity, so it doesn’t idle out. Just before it does, it checks the user again: when the session’s methods include `otp` and the user no longer has the authenticator that code came from, the answer is `login_required` with “User authentication is required”. That’s an authenticator removed while the request was under way, whether or not the user set up another one since. The ID token’s `auth_time` is when the user last signed in, not the time of the silent request, and its `acr` is the session’s level when that’s higher than the one asked for. The auth server redirects each error to your redirect URI with your `state`, in the request’s response mode. A request with `prompt=none` that fails validation, such as one with a scope that doesn’t exist, is answered at once too, rather than after a sign-in. ## prompt=login The auth server doesn’t use the browser’s session to skip the sign-in, or to skip any part of it: whoever is at the browser enters a password, and a one-time code too whenever the request’s level needs one, whatever the session already gave. The ID token’s `acr`, `amr` and `auth_time` all describe this sign-in. What happens to the session that was in the browser depends on who signs in: - **The same user.** Their session is kept, as long as it’s still valid, and takes this sign-in’s level, methods and time, which can be lower than what it held. See [A session that’s already there](https://goiabada.dev/concepts/acr-and-amr/#a-session-thats-already-there). A session that has idled out, reached its maximum lifetime or fallen outside the request’s `max_age` is replaced by a new one, with nothing revoked. - **Someone else.** The other user’s session is ended, as described in [A different user signs in on the same browser](https://goiabada.dev/concepts/ending-sessions/#a-different-user-signs-in-on-the-same-browser). `max_age` can ask for a sign-in too, but only when the last one is older than the number of seconds it gives. Use `prompt=login` when you always want a fresh sign-in, and `max_age` when a recent one is enough. With both, `prompt=login` wins. ## prompt=consent The auth server shows the consent screen after the sign-in, even when the user has already approved every scope your app asks for. Use it when your app should hear the user’s answer again, for example after it starts asking for more. See [Consent required](https://goiabada.dev/concepts/clients/#consent-required). ## Next steps [login_required](https://goiabada.dev/troubleshooting/login-required/): Why a silent request came back with an error, and what to do. [id_token_hint](https://goiabada.dev/concepts/id-token-hint/): Make sure a sign-in comes back for the user you expect. [Sessions](https://goiabada.dev/concepts/sessions/): How long a session lasts, and what ends it. # id_token_hint Source: https://goiabada.dev/concepts/id-token-hint/ This page helps your app make sure a sign-in comes back for the user it expects. `id_token_hint` is an OpenID Connect parameter of the authorization request. Its value is an ID token the auth server gave your app earlier, and it names the user your app expects by that token’s subject, its `sub`. The auth server never gives your app a code for anyone else. ## Send a hint 1. Keep the ID token from the user’s last sign-in. 2. Send it, URL-encoded, as `id_token_hint`. It’s most useful with [`prompt=none`](https://goiabada.dev/concepts/prompt/), to renew the user’s tokens without showing them anything: ```http GET /auth/authorize?client_id=my-app&redirect_uri=https%3A%2F%2Fapp.example.com%2Fcallback&response_type=code&scope=openid%20profile&prompt=none&id_token_hint=eyJhbGciOiJSUzI1NiIsImtpZCI6Ii4uLiJ9...&code_challenge=...&code_challenge_method=S256&state=... HTTP/1.1 Host: auth.example.com ``` 3. If the browser’s session is the hinted user’s, a code comes back as usual. If it isn’t, `error=login_required` comes back: send the user to sign in again, without the hint if someone else may sign in. See [login_required](https://goiabada.dev/troubleshooting/login-required/). ## What the auth server checks The hint must be an ID token the auth server signed, with its issuer in `iss` and a `sub`. Access tokens and refresh tokens are refused, although the same key signs them. Two things aren’t checked. The token can be expired, as OpenID Connect recommends, since a hint usually comes from a sign-in some time ago. And it doesn’t have to have been issued to the client that sends it. A hint that fails a check is refused with `invalid_request`: | `error_description` | Why | | - | - | | “The id_token_hint is invalid.” | It isn’t a token the auth server signed, or it’s an access or refresh token. | | “The id_token_hint was not issued by this server.” | Its `iss` isn’t the auth server’s issuer. | | “The id_token_hint does not contain a valid sub claim.” | It has no `sub`. | That error reaches your app like any other error in the request: at once when the request has `prompt=none`, or when the browser has a valid session and the request doesn’t have `prompt=login`, and otherwise after the user signs in. See [The app gets no error back](https://goiabada.dev/troubleshooting/the-app-gets-no-error-back/). ## What the hint changes | The browser has | With `prompt=none` | Without it | | - | - | - | | A session of the hinted user | A code, silently | A code, with single sign-on | | A session of someone else | `login_required`: “The current session user does not match the id_token_hint” | The user is asked to sign in | | No session | `login_required`: “User authentication is required” | The user is asked to sign in | Without `prompt=none`, whoever then signs in has to be the hinted user. If someone else does, the request ends with `login_required`: “The authenticated user does not match the id_token_hint”. They’re still signed in to the auth server, so your app’s next request without the hint signs them in to your app; send it with the hint, or with `prompt=login`. Whoever signs in over another user’s session ends it, the hinted user or not, as [Ending sessions](https://goiabada.dev/concepts/ending-sessions/#a-different-user-signs-in-on-the-same-browser) describes. With [`prompt=login`](https://goiabada.dev/concepts/prompt/#promptlogin), the user signs in again even over their own session, and it still has to be the hinted user. Send the two together to confirm it’s still the same person before a sensitive operation. ## Signing out `/auth/logout` takes an `id_token_hint` too, with rules of its own, an encrypted hint among them. See [Logout](https://goiabada.dev/reference/endpoints/logout/#what-a-hint-must-pass). ## Next steps [prompt](https://goiabada.dev/concepts/prompt/): Silent requests, and asking for a fresh sign-in. [login_required](https://goiabada.dev/troubleshooting/login-required/): Why a request with a hint came back with an error. [Sessions](https://goiabada.dev/concepts/sessions/): What a session is, and what ends it. # PKCE Source: https://goiabada.dev/concepts/pkce/ This page helps your app use PKCE, and helps you decide whether a confidential client may go without it. PKCE (Proof Key for Code Exchange, said “pixy”) ties an authorization code to the app that asked for it. Your app makes up a secret for each sign-in, sends a hash of it with the authorization request, and sends the secret itself when it redeems the code. Whoever intercepts the code, on its way back through the browser, doesn’t have the secret, so the code is no use to them. It’s defined in [RFC 7636](https://www.rfc-editor.org/rfc/rfc7636.html). Most OAuth libraries do all of this for you: turn PKCE on and use `S256`. ## Use PKCE 1. Make a code verifier: a random string of 43 to 128 characters, using only `A`–`Z`, `a`–`z`, `0`–`9`, `-`, `.`, `_` and `~`. 32 random bytes, base64url-encoded without padding, give 43 of them. Make a new one for every authorization request, and keep it until your app redeems the code. 2. Make the code challenge: the SHA-256 hash of the verifier, base64url-encoded without padding. RFC 7636’s own example, which you can test your code against: | | | | - | - | | Code verifier | `dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk` | | Code challenge | `E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM` | 3. Send the challenge, and `code_challenge_method=S256`, with the authorization request: ```http GET /auth/authorize?client_id=my-app&redirect_uri=https%3A%2F%2Fapp.example.com%2Fcallback&response_type=code&scope=openid%20profile&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&code_challenge_method=S256&state=... HTTP/1.1 Host: auth.example.com ``` 4. Send the verifier when you redeem the code: ```bash curl -X POST https://auth.example.com/auth/token \ -d "grant_type=authorization_code" \ -d "client_id=my-app" \ -d "code=..." \ -d "redirect_uri=https://app.example.com/callback" \ -d "code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk" ``` A confidential client authenticates as well, with its client secret. ## When it’s required PKCE applies to the authorization code flow, the one flow where a code travels through the browser. - **A public client always uses it.** A public client has no secret, so the verifier is all that ties the code to it. You can’t turn it off: the admin console doesn’t offer the choice, and the Admin API stores it as required whatever it’s sent. See [public and confidential clients](https://goiabada.dev/concepts/clients/#public-and-confidential-clients). - **A confidential client uses it when the global setting says so,** **PKCE required for confidential clients using the authorization code flow**, under **Admin**, **General**. It’s on in a new installation. - **A confidential client can override the global setting** on its **OAuth2 flows** tab, under **PKCE requirement for this client?**: **Required**, **Optional (not recommended)**, or **Inherit from global setting**, which a new client starts with. > **Caution** > > Making PKCE optional takes away a defence against stolen codes. Do it only for a confidential client whose software can’t send PKCE, and turn it back on once it can. Optional doesn’t mean ignored: a client that sends PKCE when it’s optional is held to the same rules as one that has to. ## What the auth server checks At `/auth/authorize`, `code_challenge_method` must be `S256`, and `code_challenge` must be 43 to 128 characters of the set above. `plain` isn’t supported, and discovery lists `S256` alone in `code_challenge_methods_supported`. Each failure is refused with `invalid_request`. When the browser has no session, the user sees the sign-in page first and your app gets the error after it, as [The app gets no error back](https://goiabada.dev/troubleshooting/the-app-gets-no-error-back/) explains. At `/auth/token`, the verifier has to match the challenge the code was issued with: | What’s wrong | `error` | `error_description` | | - | - | - | | The code was issued with a challenge, and no `code_verifier` was sent | `invalid_request` | “Missing required code_verifier parameter.” | | The verifier isn’t 43 to 128 characters of the set above | `invalid_grant` | “The code_verifier parameter is incorrect. It should be 43 to 128 characters long and may only contain …” | | The verifier’s hash isn’t the challenge | `invalid_grant` | “Invalid code_verifier (PKCE).” | | The code was issued without a challenge, and a `code_verifier` was sent | `invalid_request` | “The code_verifier parameter was provided, but PKCE was not used during authorization.” | | The client is public, and the code was issued without a challenge | `invalid_grant` | “This code was issued without PKCE, and public clients are required to use PKCE. …” | The last one catches a code issued while the client was confidential and PKCE optional, then redeemed after it became public: start a new authorization request. ## Next steps [Clients](https://goiabada.dev/concepts/clients/): Public and confidential clients, and their flows. [Tokens](https://goiabada.dev/concepts/tokens/): What your app gets for the code. # Audit log Source: https://goiabada.dev/concepts/audit-log/ This page helps you find out what happened on your auth server, and alert on the events that matter. Goiabada records security events as they happen: sign-ins, refused passwords, tokens issued, and every change an administrator makes. Each one is an audit event, with a name such as `auth_failed_pwd` and details saying who and what. ## See what happened 1. In the admin console, open **Admin**, **Audit log viewer**. 2. Pick an event from the list, or paste a request id into **Filter by request id**, and click **Filter**. 3. Read the entries, newest first. Each one shows its **Timestamp**, its **Event**, its **Request id** and its **Details**. By default, events are kept for 180 days and then deleted. To change that, or where events go, open **Admin**, **Audit log settings**. ## Events to alert on Most events are routine. These are the ones worth an alert rule, or a saved filter in the viewer. Each is one event, so a single filter on its name sees every occurrence. ### Who is an administrator These record who can administer Goiabada and who tried to reach past that. See [Administrators](https://goiabada.dev/reference/api/administrators/) for the rule they’re about, and [administrative scopes](https://goiabada.dev/concepts/clients/#administrative-scopes) for which clients may ask for them on a user’s behalf. | Event | Written when | Details | | - | - | - | | `administrator_change_refused` | An Admin API request reached for something only `authserver:manage` may do, and was refused `403 MANAGE_SCOPE_REQUIRED`. That’s creating or changing an administrator, granting or revoking an administrative permission, changing what reaches an administrator, or changing the email or audit log settings. It also covers reading an administrator client’s secret, rewording an administrative permission’s description, switching a client’s **May request administrative scopes**, and deleting a group holding an administrative permission. One entry per refused request, so a client that retries leaves one per attempt. A token with none of a route’s scopes is refused `INSUFFICIENT_SCOPE` instead, and writes nothing. | `logged_in_user`, `method`, `route` (the route pattern, such as `/api/v1/admin/users/{id}/permissions`), `ceiling` (`grant`, `target` or `settings`), `target_kind` and `target_id` when the request has a target, `permission_ids` for a grant or a description change, and `group_ids` for a group membership change | | `administrative_permission_changed` | An administrative permission was granted to or revoked from a user, a group or a client, or a user joined or left a group holding one. It’s written after the change’s own entries, such as `added_user_permission` or `user_added_to_group`, never instead of them. Deleting an administrator or an administrative group writes only its own deletion entry. | `change` (`granted` or `revoked`), `target_kind` and `target_id`, `permission_identifiers` (such as `authserver:manage`), `group_id` for a membership change, and `logged_in_user` | | `updated_client_administrative_scopes` | A client’s **May request administrative scopes** switch was saved, on or off. An allowed client is an administrator client, so watch this beside `administrative_permission_changed`. The Admin API writes it for every call to that route, even one that keeps the value it had; the admin console’s **Save** calls it only when the switch changed. | `client_id`, `client_identifier`, `allowed` (the new value) and `logged_in_user` | | `administrative_scope_refused` | A client that isn’t allowed to ask for the administrative scopes asked for one on a signed-in user’s behalf, and was refused (see the answers on [Authorize](https://goiabada.dev/reference/endpoints/authorize/#administrative-scopes) and [Token](https://goiabada.dev/reference/endpoints/token/#administrative-scopes)). It’s written at the authorization endpoint when the browser holds a valid session and the request doesn’t ask for `prompt=login`, at the last step before a code or tokens are issued, and on the token endpoint’s authorization code, refresh token and password grants. A refusal at the authorization endpoint with no valid session, or held back until after the password because of `prompt=login`, writes a Warn log record instead, since anyone can reach that endpoint. | `client_id`, `client_identifier`, `scopes`, `checkpoint` (`"authorize"`, `"issue"`, `"authorization_code"`, `"refresh_token"` or `"password"`) and `user_id` | | `viewed_client_secret` | Someone read a client’s secret through `GET /api/v1/admin/clients/{id}/secret`. The admin console reads it there each time you open a confidential client’s **Authentication** tab. Reading a client with no secret writes nothing. | `client_id`, `client_identifier` and `logged_in_user` | The `updated_client_permissions` entry names what a client permission save changed, the ids of the permissions it granted and of those it revoked, so you can trace a grant to a client back to the permission it was. ### Signs of an attack Each of these means the server caught something and refused it. One entry is worth a look; a run of them is worth an alert. | Event | Why it matters | | - | - | | `refresh_token_replay_detected` | A refresh token that had already been rotated came back. Either it was stolen or a client sent it twice. The server can’t tell which, so it revoked the rest of that token’s family. | | `auth_code_reuse_detected` | A client redeemed an authorization code a second time, after authenticating and passing PKCE. The server revoked any refresh tokens issued through that code’s session and, if it revoked any, ended the session. When the entry’s list of revoked refresh tokens is empty, nothing was left to revoke. | | `otp_code_replay_detected` | An authenticator code that had already been used was sent again. That usually means a phishing proxy relaying a user’s sign-in in real time. | | `rate_limit_exceeded` | A rate limiter started refusing requests, often a password or code guessing run. Each auth server instance writes it at most once per limiter and key per window, not once per refused request. | Two more are worth watching. A run of `auth_ceremony_mismatch` against one client is a page trying to act on sign-ins its users never started. And `redemption_refused_redirect_uri` means someone who held a valid code, and passed client authentication and PKCE, tried to redeem it after you removed its redirect URI. ## Where events go Goiabada writes each event to two places, and **Audit log settings** has a switch for each. Both are on by default. - **Log audit events to console** writes one log record per event, with the message `audit event`, the event’s name in `event`, its details in `details`, and the `request_id`. Your container or host log pipeline picks it up. The record is at `INFO`, so it’s written only while the auth server’s log level is `info` or `debug`. - **Persist audit logs to database** stores the event in the `audit_logs` table. The viewer and the Admin API read it from there. Turn both off and nothing is recorded. Changing either switch, or the retention, needs `authserver:manage`. The change is recorded once it’s saved, under the settings it replaced, so turning logging off still leaves an entry saying who did it. Recording an event never fails the request that raised it. If an event can’t be written, the server logs an error and carries on. > **Caution** > > If the database is what you’re auditing, keep the console switch on too, so your events also reach a log collector outside it. ## The request id Every entry carries the id of the request that raised it. That’s the `X-Request-Id` the client or a proxy sent, or one Goiabada generated when none came. Use it to find the request’s own log lines next to the event. It’s not proof of who sent the request, since a client can send any id it likes. ## Retention | Setting | Default | Range | | - | - | - | | Audit log retention (days) | `180` | `0` to `3650`. `0` keeps events forever. | A background worker runs every 12 hours and deletes stored events older than the retention. It deletes at most 100,000 per run, 1,000 at a time, so a big backlog clears over several runs. It runs whatever the database switch says, so events you stored earlier still age out after you turn that switch off. ## The `audit_logs` table Each stored event is one row: | Column | Holds | | - | - | | `id` | The row’s id. | | `created_at` | When the event was recorded. | | `audit_event` | The event’s name. | | `details` | The details, as JSON text. | | `request_id` | The id of the request that raised it, or empty when there was none. | ## Details The details are a JSON object saying who and what. Which keys an entry has depends on its event, and the table below lists every key the auth server writes. Every key is snake_case, and a key means the same thing in every event that carries it. Where the auth server’s own log records name the same value, the key uses their name, so `client_id`, `client_identifier`, `user_id`, `group_id`, `session_identifier` and `ip` read the same in both. `client_id` is always the client’s row id, and `client_identifier` always the identifier the client is known by. Events are about people and clients, so details carry ids, not secrets. A refresh token appears only as its `jti`, and an authenticator code only as its time step. An email address typed into a form nobody has signed in to is recorded only as `email_digest`, the SHA-256 hex digest of the address as that form looks it up. That covers a failed sign-in, a refused password grant, a registration, a forgot-password request, and a rate limit on any of them. The digest lets you match attempts on one address, and check an address you know against them. Anyone holding a candidate address can do the same, so treat it as a pseudonym, not a secret. An address an account holds is recorded as it is, since administrators already see it in the users list, and so is the address an administrator sends a test email to. | Key | Meaning | | - | - | | `allowed` | Whether the client may now ask for the administrative scopes. | | `audit_log_retention_days` | The new audit log retention, in days. | | `audit_logs_in_console_enabled` | Whether events are now written to the console log. | | `audit_logs_in_database_enabled` | Whether events are now stored in the database. | | `auth_state` | The step the browser’s sign-in was on when a page from another sign-in, or from none, was refused. | | `ceiling` | Which rule refused an administrator change: `grant`, `target` or `settings`. | | `change` | `granted` or `revoked`. | | `checkpoint` | Where an administrative scope was refused: `authorize`, `issue`, or the token endpoint’s `authorization_code`, `refresh_token` or `password` grant. | | `client_id` | The client’s row id, the id in the Admin API’s client paths. | | `client_identifier` | The identifier the client is known by, sent as `client_id` in OAuth requests. On a refusal at the token endpoint, it’s the one the request named. | | `code_id` | The row id of an authorization code. | | `consent_id` | The row id of a consent a user gave a client. | | `email` | The address of an account: the one created, activated, or given an email verification code. | | `email_destination` | The address an email verification code was sent to. | | `email_digest` | The SHA-256 hex digest of an address typed into a form nobody has signed in to, or named by a password grant. | | `first_refresh_token_jti` | The `jti` of the first refresh token in a replayed token’s family. | | `flow` | How a refresh token’s family began: `auth_code` or `ropc`. | | `grant_type` | The `grant_type` of a token request. | | `grant_types` | The grant types a client asked for when it registered itself. | | `granted_permission_ids` | The row ids of the permissions a client permission save granted. | | `group_attribute_id` | The row id of a group’s custom attribute. | | `group_id` | The group’s row id. | | `group_identifier` | The identifier the group is known by. | | `group_ids` | The row ids of the administrative groups a refused membership change would have moved the user into or out of. | | `include_open_id_connect_claims_in_access_token` | Under `old` and `new`: whether OpenID Connect claims go in access tokens by default. | | `include_open_id_connect_claims_in_id_token` | Under `old` and `new`: whether OpenID Connect claims go in ID tokens by default. | | `ip` | The client’s IP address, as the auth server resolved it. On `rate_limit_exceeded`, the block the limiter counts: the address for IPv4, its /64 for IPv6. | | `is_public` | Whether a client that registered itself is public, with no secret. | | `issue_access_token` | Whether the implicit flow issued an access token. | | `issue_id_token` | Whether the implicit flow issued an ID token. | | `key_id` | The key id (`kid`) of a deleted signing key. | | `limiter` | The name of the rate limiter that refused. | | `logged_in_user` | The subject of the token that made an Admin API or Account API request: a user’s subject, or the client identifier for a token from the client credentials flow. Empty on the entries a sign-out or a sign-in writes, which no token makes. | | `method` | The HTTP method of a refused Admin API request. | | `new` | The default token settings after the change, keyed as below. | | `new_generation` | The user’s authentication generation after their sessions and tokens were revoked. | | `new_ui_theme` | The UI theme after the change. | | `old` | The default token settings before the change, keyed as below. | | `old_generation` | The user’s authentication generation before their sessions and tokens were revoked. | | `old_ui_theme` | The UI theme before the change. | | `outcome` | What a registration or forgot-password request led to, such as `link_issued`, `code_issued` or `unknown_address`. | | `permission_id` | The row id of a permission. | | `permission_identifiers` | The administrative permissions changed, such as `authserver:manage`. | | `permission_ids` | The row ids of the administrative permissions a refused request would have changed. | | `pre_registration_id` | The row id of a registration waiting for its activation link. | | `presented_refresh_token_jti` | The `jti` of the refresh token that came back after it was rotated. | | `preserved_session_identifier` | The session a password change kept, the one the user changed it from. Empty when none was kept. | | `previous_session_identifier` | The session of the other user that a sign-in on the same browser ended. | | `previous_user_id` | The user whose session a sign-in on the same browser ended. | | `reason` | Why a link was refused, such as `code_expired`, or why sessions and tokens were revoked, such as `account_disabled`. | | `refresh_token_jti` | The `jti` of the refresh token issued. | | `refresh_token_offline_idle_timeout_in_seconds` | Under `old` and `new`: the default idle timeout of an offline refresh token, in seconds. | | `refresh_token_offline_max_lifetime_in_seconds` | Under `old` and `new`: the default maximum lifetime of an offline refresh token, in seconds. | | `resource_id` | The resource’s row id. | | `resource_identifier` | The identifier the resource is known by. | | `response_type` | The `response_type` of an implicit flow request. | | `revoked_code_count` | How many authorization codes were revoked. | | `revoked_count` | How many refresh tokens of a replayed token’s family were revoked. | | `revoked_permission_ids` | The row ids of the permissions a client permission save revoked. | | `revoked_refresh_token_jtis` | The `jti` of each refresh token revoked. | | `route` | The route pattern of a refused Admin API request, such as `/api/v1/admin/users/{id}/permissions`. | | `scope` | The scope issued, or the scope a token request asked for and was refused. | | `scopes` | The administrative scopes a client asked for and was refused. | | `session_identifier` | The identifier of a user’s session. | | `step` | The time step of an authenticator code that was sent again. | | `target_id` | The row id of the user, group, client or resource a request acted on. | | `target_kind` | What `target_id` names: `user`, `group`, `client` or `resource`. | | `terminated_session_identifiers` | The identifier of each session ended. | | `to` | The address an administrator sent a test email to. | | `token_expiration_in_seconds` | Under `old` and `new`: the default access token lifetime, in seconds. | | `user_attribute_id` | The row id of a user’s custom attribute. | | `user_id` | The user’s row id. | | `user_session_id` | The row id of a user’s session. | ## The Admin API - [`GET /api/v1/admin/audit-logs`](https://goiabada.dev/reference/api/admin/operations/getauditlogs/) lists stored events, newest first, a page at a time: `page`, `size` (1 to 200; anything else gets the default, 20), and the `auditEvent` and `requestId` filters, each an exact match. - [`GET /api/v1/admin/audit-logs/event-types`](https://goiabada.dev/reference/api/admin/operations/getauditeventtypes/) returns every event name the server can write. The viewer’s event list comes from here, so it includes events that haven’t happened yet. - [`GET /api/v1/admin/settings/audit-logs`](https://goiabada.dev/reference/api/admin/operations/getsettingsauditlogs/) and [`PUT`](https://goiabada.dev/reference/api/admin/operations/updatesettingsauditlogs/) read and change the two switches and the retention. Reading any of these needs `authserver:admin-read`, `authserver:manage-settings` or `authserver:manage`. Changing the settings needs `authserver:manage`. ## Every event This is every event the auth server writes, grouped by what raises it. The viewer’s event list holds the same names. “An administrator” means whoever called the Admin API, which is what the admin console does for you. ### Sign-in | Event | Meaning | | - | - | | `auth_success_pwd` | A user’s password was accepted on the sign-in page. | | `auth_failed_pwd` | A password was refused on the sign-in page, because no user has that email or the password was wrong. | | `auth_success_otp` | A user’s authenticator code was accepted at sign-in, including a first code that set up their authenticator. | | `auth_failed_otp` | An authenticator code was refused at sign-in, because it was wrong or had already been used. | | `otp_code_replay_detected` | An authenticator code that had already been used was sent again, at sign-in or while turning on two-factor authentication, and was refused. | | `user_disabled` | A disabled user was refused: at sign-in, when reusing a session, before a code or tokens were issued, at the token endpoint or at `/userinfo`. | | `rate_limit_exceeded` | A rate limiter started refusing requests from an IP address block, for an email address or for a user. Each instance writes it at most once per key per window. | | `auth_ceremony_mismatch` | A sign-in step was refused because its page belonged to a sign-in the browser had since replaced, or named none. | | `started_new_user_session` | A user signed in and a new session was created for them. | | `bumped_user_session` | A user’s existing session was reused and kept alive, by single sign-on, a `prompt=none` request or a refresh token grant. | | `cross_user_session_replaced` | A user signed in on a browser that still held another user’s session, so that session was ended. | | `saved_consent` | A user approved a client’s consent screen, and the scopes they granted were saved. | | `created_auth_code` | An authorization code was issued to a client for a user. | | `issuance_refused_session_invalid` | The last step of a sign-in was refused because the user’s session had passed its idle timeout or maximum lifetime. | | `issuance_refused_scope_denied` | The last step of a sign-in was refused because the user no longer held any of the scopes the client asked for. | | `issuance_refused_redirect_uri` | The last step of a sign-in was refused because the client’s redirect URI was removed while the user was signing in. Nothing was sent to the client. | | `administrative_scope_refused` | A client that isn’t allowed to ask for the administrative scopes asked for one on a signed-in user’s behalf, and was refused. | ### Sign-out and sessions | Event | Meaning | | - | - | | `logout` | A user signed out and their session ended. A sign-out confirmed with no session to end writes it too. | | `deleted_user_session_client` | A client signed a user out, and was removed from the user’s session. The session ends only when no other client is left on it. | | `deleted_user_session` | A session was deleted: ended by an administrator or its user, replaced at sign-in, superseded by a new sign-in on the same device, or removed at sign-out. | | `terminated_user_session` | A session was ended by an administrator, by its own user, or by another user signing in on the same browser, and its codes and refresh tokens were revoked. | | `revoked_user_auth_state` | A user’s password was reset or changed, or the user was disabled, so their sessions and refresh tokens were revoked. A user changing their own password keeps the session they did it from. | ### Tokens | Event | Meaning | | - | - | | `token_issued_authorization_code_response` | The token endpoint exchanged an authorization code for tokens. | | `token_issued_refresh_token_response` | The token endpoint exchanged a refresh token for new tokens. | | `token_issued_client_credentials_response` | The token endpoint issued an access token to a client through the client credentials flow. | | `token_issued_ropc_response` | The token endpoint issued tokens for a username and password, through the password grant. | | `token_issued_implicit_response` | The authorization endpoint issued tokens straight to a client, through the implicit flow. | | `token_scope_denied` | The token endpoint refused a request for a scope the client or user may not have, or an OpenID Connect scope on the client credentials grant, or a client credentials request that left `scope` out from a client holding no permissions, recorded with an empty `scope`. | | `ropc_auth_failed` | The token endpoint refused a password grant, because the email or password was wrong, or the user was disabled or has two-factor authentication on. | | `auth_code_reuse_detected` | A client redeemed an authorization code a second time. The refresh tokens issued through that code’s session were revoked, and, if there were any, the session ended. | | `refresh_token_replay_detected` | A refresh token that had already been rotated came back, so the rest of its family was revoked. | | `redemption_refused_redirect_uri` | The token endpoint refused an authorization code because its redirect URI was removed from the client after the code was issued. | ### Registration and emailed links | Event | Meaning | | - | - | | `requested_registration` | Someone sent the registration form while email verification is required. `outcome` says whether an activation link, a notice that the account exists, or nothing was sent. | | `activated_account` | Someone followed their activation link and chose a password, which created their account. | | `failed_account_activation_code` | An activation link was refused, because it was unknown, expired or already used, or its address already has an account. | | `requested_password_reset` | Someone sent the forgot-password form. `outcome` says whether a reset link was sent, or why not. | | `failed_reset_password_code` | A password reset link was refused, because it was unknown, expired or already used, or no longer valid for its account. | ### A user’s own account These come from the Account API, which users reach through the admin console’s account pages. | Event | Meaning | | - | - | | `updated_own_profile` | A user saved their own profile: names, nickname, username, website, gender, birth date, time zone or locale. | | `updated_own_email` | A user changed their own email address, after confirming their password. The new address starts unverified. | | `sent_email_verification_message` | A user asked for an email verification code, and it was sent to their address. | | `verified_email` | A user verified their email address with the code they were sent, or by setting their password through a link sent to it, such as the one an administrator emails a new user. | | `failed_email_verification_code` | A user entered a wrong or expired email verification code. | | `updated_own_phone` | A user saved or cleared their own phone number. | | `updated_own_address` | A user saved their own address. | | `updated_own_profile_picture` | A user uploaded or replaced their own profile picture. | | `deleted_own_profile_picture` | A user removed their own profile picture. | | `changed_password` | A user changed their own password, after confirming the current one. | | `enabled_otp` | A user set up an authenticator for two-factor authentication, on their account page or when a sign-in required it. | | `disabled_otp` | A user’s authenticator was removed, by the user or by an administrator. | | `deleted_own_user_consent` | A user withdrew the consent they had given a client. | ### Users | Event | Meaning | | - | - | | `created_user` | A user was created: by an administrator, by someone registering, or by someone activating their registration. | | `updated_user_details` | An administrator enabled or disabled a user. | | `updated_user_profile` | An administrator changed a user’s profile: username, names, nickname, website, gender, birth date, time zone or locale. | | `updated_user_email` | An administrator changed a user’s email address, or whether it’s verified. | | `generated_email_verification_code` | An administrator generated an email verification code for a user. It was returned to the administrator, not emailed. | | `updated_user_phone` | An administrator changed or cleared a user’s phone number. | | `updated_user_address` | An administrator changed a user’s address. | | `updated_user_authentication` | An administrator set a new password for a user. | | `updated_user_profile_picture` | An administrator uploaded or replaced a user’s profile picture. | | `deleted_user_profile_picture` | An administrator removed a user’s profile picture. | | `added_user_attribute` | An administrator added a custom attribute to a user. | | `updated_user_attribute` | An administrator changed a user’s custom attribute. | | `deleted_user_attribute` | An administrator deleted a user’s custom attribute. | | `added_user_permission` | An administrator granted a permission to a user. One entry per permission. | | `deleted_user_permission` | An administrator revoked a permission from a user. One entry per permission. | | `deleted_user_consent` | An administrator deleted a consent a user had given a client. | | `deleted_user` | An administrator deleted a user. | ### Groups | Event | Meaning | | - | - | | `created_group` | An administrator created a group. | | `updated_group` | An administrator changed a group’s identifier, description, or whether it goes in tokens. | | `deleted_group` | An administrator deleted a group. | | `added_group_attribute` | An administrator added a custom attribute to a group. | | `updated_group_attribute` | An administrator changed a group’s custom attribute. | | `deleted_group_attribute` | An administrator deleted a group’s custom attribute. | | `added_group_permission` | An administrator granted a permission to a group. One entry per permission. | | `deleted_group_permission` | An administrator revoked a permission from a group. One entry per permission. | | `user_added_to_group` | An administrator added a user to a group. One entry per group. | | `user_removed_from_group` | An administrator removed a user from a group. One entry per group. | ### Administrators | Event | Meaning | | - | - | | `administrative_permission_changed` | Who is an administrator changed: an administrative permission was granted or revoked, or a user joined or left a group holding one. | | `administrator_change_refused` | An Admin API request was refused because only `authserver:manage` may make that change. | ### Clients | Event | Meaning | | - | - | | `created_client` | An administrator created a client. | | `dynamic_client_registration` | A client registered itself through dynamic client registration. | | `updated_client_settings` | An administrator saved a client’s settings: identifier, description, display options, enabled, consent required or default ACR level. | | `updated_client_authentication` | An administrator saved a client’s authentication: made it public, or made it confidential with a new or the same secret. | | `revoked_client_grants` | An administrator made a confidential client public, so its authorization codes and refresh tokens were revoked. | | `viewed_client_secret` | Someone read a client’s secret. | | `updated_client_oauth2_flows` | An administrator saved which flows a client may use, and whether it needs PKCE. | | `updated_redirect_uris` | An administrator saved a client’s redirect URIs. | | `updated_web_origins` | An administrator saved a client’s web origins. | | `updated_client_tokens` | An administrator saved a client’s token settings: lifetimes, and which claims go in its tokens. | | `updated_client_permissions` | An administrator changed the permissions granted to a client. | | `updated_client_administrative_scopes` | An administrator allowed a client to ask for the administrative scopes, or stopped it. | | `updated_client_logo` | An administrator uploaded or replaced a client’s logo. | | `deleted_client_logo` | An administrator removed a client’s logo. | | `deleted_client` | An administrator deleted a client. | ### Resources | Event | Meaning | | - | - | | `created_resource` | An administrator created a resource. | | `updated_resource` | An administrator changed a resource’s identifier or description. | | `updated_resource_permissions` | An administrator changed the permissions a resource defines: added, renamed, described or deleted them. Granting a permission writes a different event. | | `deleted_resource` | An administrator deleted a resource. | ### Settings and keys | Event | Meaning | | - | - | | `updated_general_settings` | An administrator saved the **General** settings. | | `updated_ui_theme_settings` | An administrator changed the **UI theme**. The entry names the old and the new theme. | | `updated_sessions_settings` | An administrator changed the session idle timeout or maximum lifetime. | | `updated_tokens_settings` | An administrator changed the default token settings. The entry holds the old and the new values. | | `updated_smtp_settings` | An administrator changed the **Email - SMTP** settings, including turning email on or off. | | `sent_test_email` | An administrator sent a test email, and the mail server accepted it. | | `updated_audit_logs_settings` | An administrator changed where audit events go or how long they’re kept. It holds the new values, and is recorded under the old ones, so turning logging off is recorded too. | | `rotated_keys` | An administrator rotated the signing keys: the next key became the current one, and a new next key was generated. | | `revoked_key` | An administrator deleted a previous signing key. | ## Next steps [Resources and permissions](https://goiabada.dev/concepts/resources-and-permissions/): Who can administer Goiabada, and what each permission allows. [Monitoring](https://goiabada.dev/deploy/monitoring/): Metrics and logs to watch beside the audit log. # Glossary Source: https://goiabada.dev/concepts/glossary/ This page tells you what each word in the docs means, so you can read any page without guessing. Each concept has one name, the one the admin console uses, and the docs never swap in a synonym. When a page uses a term for the first time, it explains it in a few words and links here or to the term’s concept page. ## Words we use **Sign in** and **sign out** are the verbs: “sign in to the admin console”, “the user signs out”. **Sign-in** is the noun: “the sign-in page”, “a sign-in that took too long”. The docs never say log in, log out, login or logout in prose. Two things keep their own spelling: - **Protocol names**, which are spelled as the protocol spells them: `prompt=login`, `login_hint`, `/auth/logout`. - **UI labels**, which are quoted exactly as the screen shows them. **Your app** is the software you’re building, the one you register in Goiabada as a client. The docs don’t use it for anything else. ## Terms ### Access token A token a client sends to an API to show what it’s allowed to do. Goiabada issues access tokens as signed JWTs, and an API checks the signature against the auth server’s public keys at `/certs`. See [Tokens](https://goiabada.dev/concepts/tokens/). ### ACR Authentication Context Class Reference: how strongly the user signed in. Goiabada has three levels, `urn:goiabada:level1` (password), `urn:goiabada:level2_optional` (password, plus a one-time code if the user has two-factor authentication) and `urn:goiabada:level2_mandatory` (password and a one-time code). See [ACR and AMR](https://goiabada.dev/concepts/acr-and-amr/). ### Admin console The web app where you manage clients, users, groups, resources, permissions and settings. It’s a client of the auth server and reaches the database only through the auth server. ### Administrator A user, group or client holding one of the administrative permissions on the `authserver` resource, such as `authserver:manage`, and also the admin console’s own client, and any client allowed to request the administrative scopes. Only `authserver:manage` can make someone an administrator or change one. See [Administrators](https://goiabada.dev/reference/api/administrators/). ### AMR Authentication Methods References: which methods the user signed in with. Goiabada uses `pwd` (password) and `otp` (one-time code). See [ACR and AMR](https://goiabada.dev/concepts/acr-and-amr/). ### Attribute A key and a value you add to a user or a group. A client gets a user’s attributes, and those of the user’s groups, in the user’s claims when it asks for the `attributes` scope and the attribute is set to be included. ### Audit log The record of security events: sign-ins, failed passwords, changes an administrator makes, and more. See [Audit log](https://goiabada.dev/concepts/audit-log/). ### Auth server The server that signs users in and issues tokens. It serves the OAuth2 and OpenID Connect endpoints, the sign-in pages and the API the admin console uses. ### Authorization code A short-lived, single-use code the auth server sends to a client after the user signs in. The client exchanges it at `/auth/token` for tokens. See [Add sign-in to a web app](https://goiabada.dev/guides/add-sign-in-to-a-web-app/). ### Claim One piece of information inside a token, such as `sub` (who the user is) or `email`. ### Client An application that asks the auth server for tokens: your app, an API calling another API, or the admin console itself. A client is identified by its client identifier, the `client_id` it sends. See [Clients](https://goiabada.dev/concepts/clients/). - A **confidential client** runs on a server and has a client secret. - A **public client** runs where it can’t keep a secret, such as a browser or a phone, so it has none and must always use PKCE. ### Consent A user’s approval for a client to receive what it asked for. The auth server asks for it when the client has “Consent required” turned on, when the client asks for `offline_access`, or when the request says `prompt=consent`. It also asks when a client other than the admin console’s asks for `authserver:manage-account` and the user hasn’t approved that scope for it yet. See [Consent required](https://goiabada.dev/concepts/clients/#consent-required) for the full rule. Users can revoke a consent under “Manage consents” in their account. ### Dynamic client registration (DCR) A way for a client to register itself, by calling `/connect/register`, instead of an administrator creating it in the admin console. It’s off until you turn it on. See [Let clients register themselves (DCR)](https://goiabada.dev/guides/let-clients-register-themselves-dcr/). ### Group A set of users. Permissions and attributes you give a group apply to every user in it. See [Users and groups](https://goiabada.dev/concepts/users-and-groups/). ### ID token A token that tells a client who the user is and how they signed in. Only clients that ask for the `openid` scope get one. See [Tokens](https://goiabada.dev/concepts/tokens/). ### Implicit flow A deprecated flow in which the browser comes back from sign-in with the tokens themselves rather than an authorization code. It’s off by default. See [Implicit flow](https://goiabada.dev/legacy-flows/implicit/). ### Permission Something a resource lets you do, such as `manage` on the `authserver` resource. You give permissions to users, groups and clients, and a client asks for one as a scope written `resource:permission`, like `authserver:manage`. See [Resources and permissions](https://goiabada.dev/concepts/resources-and-permissions/). ### PKCE Proof Key for Code Exchange: a check that ties an authorization code to the client that asked for it, so a stolen code is useless. Public clients must always use it. See [PKCE](https://goiabada.dev/concepts/pkce/). ### Redirect URI The address the auth server sends the browser back to after sign-in, carrying the authorization code. It must be one of the redirect URIs registered on the client. ### Refresh token A token a client exchanges for new tokens without asking the user to sign in again. A client that asks for the `offline_access` scope gets an offline refresh token, which keeps working after the user’s session expires. See [Refresh tokens](https://goiabada.dev/concepts/refresh-tokens/). ### Refresh-token rotation What happens to a refresh token when it’s used: the auth server retires it and hands back a new one, so each refresh token is good for one refresh. A retired one sent again is a replay, which revokes every live refresh token descended from the same grant, its rotation family. Not to be confused with [secret rotation](https://goiabada.dev/concepts/glossary/#secret-rotation) or [signing-key rotation](https://goiabada.dev/concepts/glossary/#signing-key-rotation). See [Refresh tokens](https://goiabada.dev/concepts/refresh-tokens/#rotation). ### Resource Something you protect, usually an API, and the permissions that go with it. Goiabada’s own resource is `authserver`. See [Resources and permissions](https://goiabada.dev/concepts/resources-and-permissions/). ### Resource Owner Password Credentials (ROPC) A deprecated flow, also called the password grant, in which a client sends a user’s email and password to the token endpoint for tokens. It’s off by default. See [ROPC](https://goiabada.dev/legacy-flows/ropc/). ### Scope What a client asks for when it requests a token. There are two kinds: OpenID Connect scopes, such as `openid`, `profile` and `email`, which give access to the user’s information, and permission scopes, written `resource:permission`. See [Scopes](https://goiabada.dev/concepts/scopes/). ### Secret rotation Replacing one of the secrets your deployment holds with a new one: a server’s session keys, the AES key, the database password or the admin console’s client secret, each without signing anybody out or with the shortest interruption that secret allows. See [Rotate secrets](https://goiabada.dev/deploy/rotate-secrets/). ### Self-registration A user creating their own account from the sign-in page’s **Register** link, instead of an administrator creating it. See [Self-registration](https://goiabada.dev/concepts/self-registration/). ### Session What lets a user sign in once and use several clients. After the user signs in, the auth server keeps a session for their browser, so the next client they use doesn’t ask for the password again. A session ends when it has been idle too long, when it reaches its maximum lifetime, or when it’s ended: by the user or an administrator, by signing out, or by a change to the user’s credentials. See [Sessions](https://goiabada.dev/concepts/sessions/) and [Ending sessions](https://goiabada.dev/concepts/ending-sessions/). ### Signing-key rotation What **Rotate key** under **Admin**, **Keys** does: the next signing key starts signing tokens, the current one becomes the previous one, and the old previous one is deleted, so a token it signed no longer verifies. See [The key set](https://goiabada.dev/reference/endpoints/discovery-and-jwks/#the-key-set). ### Silent request An authorization request with `prompt=none`, which checks for a session without showing the user anything. It comes back with a code when the session is enough, and with an error, such as `login_required`, when it isn’t. See [prompt](https://goiabada.dev/concepts/prompt/). ### Single sign-on (SSO) Signing in once and using several clients without signing in again, which the session makes possible. See [Single sign-on across clients](https://goiabada.dev/guides/single-sign-on-across-clients/). ### Step-up Asking a user who already has a session for more, usually a one-time code, because a client needs a stronger ACR level than the session reached. The session moves up to that level. See [ACR and AMR](https://goiabada.dev/concepts/acr-and-amr/#a-session-thats-already-there). ### Subject The identifier the auth server gives each user when they’re created. It never changes, and it’s the `sub` claim in every token about the user. See [Users and groups](https://goiabada.dev/concepts/users-and-groups/#users). ### Two-factor authentication Signing in with a password and a one-time code from an authenticator app. In the admin console, you set it up under **Account**, in **Two-factor authentication**. See [Require two-factor authentication](https://goiabada.dev/guides/require-two-factor-authentication/). ### User A person who signs in. A user has a profile, an email address, a password, and optionally two-factor authentication, and can belong to groups. See [Users and groups](https://goiabada.dev/concepts/users-and-groups/). ## Next steps [Clients](https://goiabada.dev/concepts/clients/): Register your app and choose its settings. [Resources and permissions](https://goiabada.dev/concepts/resources-and-permissions/): Decide who can do what. # Choose a method Source: https://goiabada.dev/deploy/choose-a-method/ This page helps you pick how to run Goiabada in production. Most people want **Cloudflare Tunnel**: no open port, no certificate to renew and no Nginx. Already run Nginx, or don’t use Cloudflare? Pick by what you have: | Method | TLS | Exposed to the internet | Setup wizard | Database | Suits | | - | - | - | - | - | - | | [Cloudflare Tunnel](https://goiabada.dev/deploy/cloudflare-tunnel/) Least effort | Cloudflare’s certificates | Nothing: `cloudflared` connects out | Production with reverse proxy | MySQL, PostgreSQL, SQL Server or SQLite, in the Compose file | One server, with your domain on Cloudflare | | [Cloudflare + Nginx](https://goiabada.dev/deploy/cloudflare-nginx/) Some effort | Cloudflare’s, and Let’s Encrypt’s on Nginx | Ports 80 and 443 | Production with reverse proxy | The same | A server already running Nginx, with your domain on Cloudflare | | [Reverse proxy](https://goiabada.dev/deploy/reverse-proxy/) Some effort | Let’s Encrypt’s on Nginx | Ports 80 and 443 | Production with reverse proxy | The same | One server, without Cloudflare | | [Kubernetes](https://goiabada.dev/deploy/kubernetes/overview/) Most effort | cert-manager’s, on the Gateway | The Gateway’s load balancer | Kubernetes cluster | MySQL, PostgreSQL or SQL Server, run by you | Several replicas, on a cluster you already run | | [Native binaries](https://goiabada.dev/deploy/native-binaries/) Some effort | Your reverse proxy’s, or each server’s own | Your proxy’s ports, or each server’s | Native binaries | MySQL, PostgreSQL or SQL Server run by you, or SQLite | A server without Docker | The first three run the [Docker Compose](https://goiabada.dev/deploy/docker-compose/) files the setup wizard writes, and differ only in what sits in front of them. ## What every method needs - **Two hostnames,** one for the auth server and one for the admin console, such as `auth.example.com` and `admin.example.com`. Each server answers on its own. - **One registrable domain for both.** `auth.example.com` and `admin.example.com` work; `auth.example.com` and `admin.example.net` don’t. The admin console’s sign-in ends with the auth server posting the answer back to the admin console, and a browser sends the admin console’s session cookie on that post only when both are on the same site. On two sites, every sign-in to the admin console stops at “This sign-in can’t be finished”. - **HTTPS on both public URLs.** Each server marks its cookies `Secure` only when its URL is `https://`. - **An empty database on the first start,** which the auth server seeds with the administrator and the admin console’s client, for the URLs you gave. Changing the URLs later means changing that client’s redirect URIs too: see [Invalid redirect_uri](https://goiabada.dev/troubleshooting/invalid-redirect-uri/). ## What changes between methods Every method runs the same two servers. The auth server answers sign-ins, tokens and the API on port 9090, and the admin console answers administrators on port 9091. Each serves plain HTTP by default, and something in front of it serves HTTPS: Cloudflare, Nginx, a Kubernetes Gateway, or the server itself with a certificate of its own. Two things change from method to method, and each has a page: - **How Goiabada learns the client’s IP address** through whatever is in front of it, which decides what the rate limiter counts: [Client IP and proxy trust](https://goiabada.dev/deploy/client-ip-and-proxy-trust/). - **How the admin console reaches the auth server.** It calls the auth server’s API at `GOIABADA_AUTHSERVER_INTERNALBASEURL`, and that hop carries its client secret and administrators’ tokens. The generated Compose files and Kubernetes manifests point it at the auth server’s container or Service over plain HTTP, inside the deployment’s own network; native binaries go through the public URL. [Docker Compose](https://goiabada.dev/deploy/docker-compose/#the-hop-to-the-auth-server), [Kubernetes](https://goiabada.dev/deploy/kubernetes/security/#encrypt-the-hop-to-the-auth-server) and the [production checklist](https://goiabada.dev/deploy/production-checklist/#the-hop-from-the-admin-console-to-the-auth-server) say when that’s enough and how to encrypt it. > **Caution** > > The wizard’s **Local testing** choice serves plain HTTP on `localhost`, for the [Quickstart](https://goiabada.dev/get-started/quickstart/). **Never** use it for a deployment other people reach. ## Next steps [Setup wizard](https://goiabada.dev/deploy/setup-wizard/): Generate the files for the method you picked. [Cloudflare Tunnel](https://goiabada.dev/deploy/cloudflare-tunnel/): The simplest way in: no open port, no certificate. [Production checklist](https://goiabada.dev/deploy/production-checklist/): What to check before going live. # Setup wizard Source: https://goiabada.dev/deploy/setup-wizard/ The setup wizard, `goiabada-setup`, asks you a few questions and writes the configuration for your deployment, with every key and password already generated. 1. Download the wizard for your platform. **Linux (x86_64)** ```bash curl -LO https://github.com/leodip/goiabada/releases/latest/download/goiabada-setup-linux-amd64 chmod +x goiabada-setup-linux-amd64 ``` **Linux (ARM64)** ```bash curl -LO https://github.com/leodip/goiabada/releases/latest/download/goiabada-setup-linux-arm64 chmod +x goiabada-setup-linux-arm64 ``` **macOS (Apple Silicon)** ```bash curl -LO https://github.com/leodip/goiabada/releases/latest/download/goiabada-setup-darwin-arm64 chmod +x goiabada-setup-darwin-arm64 ``` **macOS (Intel)** ```bash curl -LO https://github.com/leodip/goiabada/releases/latest/download/goiabada-setup-darwin-amd64 chmod +x goiabada-setup-darwin-amd64 ``` **Windows** ```bash curl -LO https://github.com/leodip/goiabada/releases/latest/download/goiabada-setup-windows-amd64.exe ``` Every release has the same five binaries on the [releases page](https://github.com/leodip/goiabada/releases). 2. Run it in the directory where you want the files, and answer the questions. Pressing Enter takes the default shown in brackets. ```bash ./goiabada-setup-linux-amd64 ``` 3. Check the summary and answer yes to “Generate configuration files?”. 4. Start Goiabada with the command the wizard prints at the end. It also prints both URLs and where to find the admin password. 5. [Sign in to the admin console](https://goiabada.dev/get-started/first-sign-in/). ## Deployment types The first question picks one of four deployments. Each writes its own files, in the current directory unless you pass `--output`. | Deployment | Use it for | Files it writes | | - | - | - | | 1. Local testing | Trying Goiabada on your machine. Plain HTTP on `localhost`. The [Quickstart](https://goiabada.dev/get-started/quickstart/) walks through it. | `docker-compose.yml` and `docker-compose.override.yml` | | 2. Production with reverse proxy | Docker Compose behind a proxy that serves HTTPS, such as Nginx or Cloudflare. | `docker-compose.yml` and `docker-compose.override.yml` | | 3. Kubernetes cluster | A cluster with Envoy Gateway and cert-manager. | `goiabada-k8s.yaml` and `goiabada-secrets.yaml` | | 4. Native binaries | Running the two binaries yourself, without containers. | `goiabada.env` | > **Caution** > > Local testing serves plain HTTP. **Never** use it for a deployment other people reach. ## The questions The wizard asks only the questions that apply to your deployment, in this order, each under its step’s heading. | Question | Asked for | Default | | - | - | - | | Deployment type | Every deployment. See [Deployment types](https://goiabada.dev/deploy/setup-wizard/#deployment-types). | 1, Local testing (`--type` has none) | | Database type | Every deployment. MySQL, PostgreSQL, SQL Server or SQLite. Kubernetes doesn’t offer SQLite. | 1, MySQL (`--db` has none) | | Domain names | The auth server’s and the admin console’s URLs, for every deployment but local testing, which uses `http://localhost:9090` and `http://localhost:9091`. On Kubernetes, each URL needs a host of its own. | `https://auth.example.com`, then the admin console’s on the same domain: `https://admin.example.com` | | Kubernetes namespace | Kubernetes | `goiabada` | | Gateway traffic policy | Kubernetes. Cluster works behind every load balancer, but Goiabada then sees a node’s address for every client. Local runs Envoy on every node and keeps each client’s own address, behind a load balancer that passes connections through rather than proxying them. | Cluster | | Network policy | Kubernetes. Yes admits only Envoy to both servers, and the admin console to the auth server. It needs a network plugin that enforces NetworkPolicies. | No | | Reverse proxy | Native binaries. Yes has both servers listen on `127.0.0.1` and trust the proxy’s forwarded headers. No has them listen on every interface and trust no forwarded header, and you set up their HTTPS yourself. | Yes | | Rate limiter | Every deployment but local testing, which leaves it off. See [why it’s off under Cluster](https://goiabada.dev/deploy/setup-wizard/#the-rate-limiter-under-the-cluster-traffic-policy). | On, but off on Kubernetes under the Cluster policy | | Metrics | Kubernetes. None, pod annotations, or a PodMonitor for the Prometheus Operator. See [Monitoring](https://goiabada.dev/deploy/monitoring/). | None | | Admin credentials | Every deployment: the admin’s email and password. | `admin@example.com` for local testing, otherwise `admin@` and the auth server’s domain. A generated password. | | Database connection | Kubernetes and native binaries, with any database but SQLite: host, port, name, user, password, [TLS mode](https://goiabada.dev/deploy/setup-wizard/#the-database-connections-tls) and, for `verify-ca` and `verify-full`, a CA file. Then an optional connection test, which connects as the auth server will. If it fails, you can enter them all again. | The database’s usual port and user, the name `goiabada`, the TLS mode `prefer`, and the system’s roots | | Database password | Local testing and production, with any database but SQLite. The wizard runs that database for you, and writes the TLS mode `prefer`. | A generated password | ## The database connection’s TLS The wizard writes `GOIABADA_DB_TLS_MODE`, which decides how the auth server protects its connection to MySQL, PostgreSQL or SQL Server. [Where the database sits](https://goiabada.dev/deploy/database/#where-the-database-sits) explains each mode. - **Local testing and production** write `prefer`. The database runs on the Compose file’s own network, so the connection never leaves the host. - **Kubernetes and native binaries** ask you to pick one of `disable`, `prefer`, `require`, `verify-ca` and `verify-full`. Only `verify-full` makes sure the auth server reached your database and nothing in between. For `verify-ca` and `verify-full`, the wizard then asks for a CA file: a PEM file of the authorities your database’s certificate chains to. Leave it blank to trust the system’s roots. The wizard refuses a file the auth server would refuse: one it can’t read, or one that holds no certificate. - **Native binaries** write the CA file’s absolute path as `GOIABADA_DB_TLS_CA_FILE`, so keep the file where it is. - **Kubernetes** copies the certificates into a `goiabada-db-ca` ConfigMap, mounts it read-only into the auth server’s pods at `/etc/goiabada/db-ca/ca.pem`, and names that path as `GOIABADA_DB_TLS_CA_FILE`. A private key in the same file isn’t copied. The wizard warns you whenever the mode it writes checks no certificate. ## The rate limiter under the Cluster traffic policy The auth server’s limits counted by a user or an email apply whether the rate limiter is on or off: wrong one-time codes, wrong passwords per account, and password-reset and registration mails per address among them. The rate limiter adds the limits counted by an IP address. Under the Cluster policy, Goiabada sees a node’s address for every client. The per-address limits would then count every user who arrives through one node together, and throttle sign-ins on a busy site. That’s why it’s off by default there. Turned on, it also limits wrong passwords per email from one network, and every client through a node counts as one network: 10 wrong passwords block an email’s password sign-ins for 15 minutes for every client through that node. See the [rate limits](https://goiabada.dev/reference/environment-variables/#rate-limits). ## Where the secrets go The wizard keeps every secret in one file, and the rest of the configuration in another, so you can commit the second one. - **Docker Compose:** the secrets are in `docker-compose.override.yml`. Docker Compose merges it into `docker-compose.yml` by itself, so `docker compose up -d` starts both. - **Kubernetes:** the secrets are in `goiabada-secrets.yaml`. Apply it before the manifest, so no pod starts with an older copy of the Secrets; both files create the namespace: `kubectl apply -f goiabada-secrets.yaml -f goiabada-k8s.yaml`. - **Native binaries:** `goiabada.env` is a single file that holds everything, secrets included. > **Danger** > > **Never** commit the secrets file. It holds the AES encryption key, which encrypts the client secrets, SMTP credentials, two-factor seeds and signing keys in the database. [Back the key up](https://goiabada.dev/deploy/secrets/#back-up-the-aes-key) somewhere other than the database’s backups: without it, that data can’t be recovered. Every file the wizard writes can be read by its owner only. When it writes into a Git working tree, it warns you and prints the line to add to `.gitignore`. It never edits `.gitignore` itself. Running the wizard again where its files already are writes new secrets over them, so it warns first and asks before going on; without prompts, it stops unless you pass `--overwrite`. A database seeded with the old secrets can’t be used with the new ones, and without another copy of the old AES key, the data it encrypts can never be read again. So back the files up first, then either start over with an empty database, or copy your existing secrets over the new ones, as [Secrets](https://goiabada.dev/deploy/secrets/#when-a-secret-changes) describes. Never apply the new secrets file to a deployment that already runs. With `--output`, the files go into the directory it names. When it names a file instead, the secrets file is named after it: `compose.yaml` gets `compose.override.yaml`, and `my-k8s.yaml` gets `my-k8s-secrets.yaml`. ## Passwords The wizard never prints a password. The summary says only whether each one was set or generated, and the last message tells you where the admin password is. When you type a password at a terminal, the wizard doesn’t show it. The admin password must be at least 15 characters and at most 72 bytes. The wizard refuses any other and asks again, because the auth server would refuse to start with it. A password without an uppercase letter, a lowercase letter, a digit and a symbol gets a warning, and you choose whether to keep it. ## Answering with flags Pass `--type` and the wizard asks nothing: it takes every answer from the flags and the defaults. That’s useful in scripts and CI. ```bash ./goiabada-setup-linux-amd64 --type=kubernetes --db=postgres \ --auth-url=https://auth.example.com \ --db-host=postgres.default.svc --db-password-file=/run/secrets/db-password ``` Without `--type`, the wizard asks every question and reads only `--output` and `--no-color`, and asks rather than reading `--overwrite`. Prefer a generated password, or a password file, to `--admin-password` and `--db-password`. A password on the command line lands in your shell history and, while the wizard runs, in the process list. A password file can be `-` to read standard input, and one trailing line break is dropped. ```bash printf '%s' "$DB_PASSWORD" | ./goiabada-setup-linux-amd64 --type=native --db=postgres \ --auth-url=https://auth.example.com --db-host=localhost --db-password-file=- ``` The wizard exits with 0 when it’s done or when you abort it, 1 when it can’t finish, and 2 when it can’t read a flag. It writes nothing until every answer is valid. ## Flags | Flag | What it does | | - | - | | `--type` | The deployment: `local`, `production`, `kubernetes` or `native`. Giving it turns off the questions. | | `--db` | The database: `mysql`, `postgres`, `mssql` or `sqlite`. Required with `--type`. | | `--auth-url` | The auth server’s URL. Required with `--type`, except for `local`. | | `--admin-url` | The admin console’s URL. Default: `https://admin.` and the auth server’s domain, so `https://admin.example.com` for `https://auth.example.com`. Required when the auth server’s host is an IP address or a single word. | | `--namespace` | Kubernetes: the namespace. Default: `goiabada`. | | `--admin-email` | The first administrator’s email. Default: `admin@example.com` for `local`, otherwise `admin@` and the auth server’s domain. Required when the auth server’s host is an IP address or a single word. | | `--admin-password` | The first administrator’s password. Generated when you leave it out. | | `--admin-password-file` | Reads the admin password from a file, or from standard input with `-`. | | `--db-host` | Kubernetes and native: the database’s host. Required unless the database is SQLite. | | `--db-port` | Kubernetes and native: the database’s port. Default: 3306, 5432 or 1433, for MySQL, PostgreSQL and SQL Server. | | `--db-name` | Kubernetes and native: the database’s name. Default: `goiabada`. | | `--db-user` | Kubernetes and native: the database user. Default: `root`, `postgres` or `sa`. | | `--db-password` | The database password. Generated when you leave it out. | | `--db-password-file` | Reads the database password from a file, or from standard input with `-`. | | `--db-tls-mode` | Kubernetes and native: how the auth server protects its database connection: `disable`, `prefer`, `require`, `verify-ca` or `verify-full`. Default: `prefer`. | | `--db-tls-ca-file` | Kubernetes and native, with `--db-tls-mode` `verify-ca` or `verify-full`: the PEM file of the authorities your database’s certificate is checked against. Default: the system’s roots. | | `--skip-db-test` | Skips the database connection test. Without it, a failed test warns and the files are still written. | | `--gateway-traffic-policy` | Kubernetes: `cluster` or `local`. Default: `cluster`. | | `--network-policy` | Kubernetes: admits only Envoy and the admin console to the servers, with NetworkPolicies. | | `--metrics` | Kubernetes: `none`, `annotations` or `podmonitor`. Default: `none`. | | `--podmonitor-labels` | Kubernetes, with `--metrics=podmonitor`: the labels your Prometheus selects PodMonitors by, such as `release=kube-prometheus-stack`. | | `--metrics-namespace` | Kubernetes, with metrics and `--network-policy`: the namespace your metrics scraper runs in. Default: `monitoring`. | | `--rate-limiter` | Production, Kubernetes and native: `true` or `false`. Default: `true`, except on Kubernetes under the `cluster` policy. | | `--local-proxy` | Native: `true` when a reverse proxy on the same machine forwards to Goiabada. Default: `true`. | | `--output`, `-o` | Where to write the files: a directory, or the name of the main file. Default: the current directory. | | `--no-color` | Turns off colored output. | | `--overwrite` | Without prompts: writes over output files that already exist, and the secrets in them. Without it, the wizard stops. | | `--version`, `-v` | Prints the wizard’s version. | ## Next steps [First sign-in](https://goiabada.dev/get-started/first-sign-in/): Sign in to the admin console and secure your account. [Docker Compose](https://goiabada.dev/deploy/docker-compose/): Run Goiabada in production with Docker Compose. [Kubernetes](https://goiabada.dev/deploy/kubernetes/overview/): Run Goiabada on a Kubernetes cluster. [Native binaries](https://goiabada.dev/deploy/native-binaries/): Run the two binaries yourself. # Docker Compose Source: https://goiabada.dev/deploy/docker-compose/ This page helps you run Goiabada in production from the Docker Compose files the setup wizard writes. The Compose files run the auth server, the admin console and the database on one host, and publish the two servers on `127.0.0.1` alone. A proxy on the same host puts them on the internet: [Cloudflare Tunnel](https://goiabada.dev/deploy/cloudflare-tunnel/), [Cloudflare + Nginx](https://goiabada.dev/deploy/cloudflare-nginx/) or [Reverse proxy](https://goiabada.dev/deploy/reverse-proxy/). Each of those pages runs this one first. ## Run the wizard’s files 1. **Run the [setup wizard](https://goiabada.dev/deploy/setup-wizard/)** in an empty directory, and choose **Production with reverse proxy**. Give it the two public URLs, such as `https://auth.example.com` and `https://admin.example.com`, and a database. Or answer with flags: ```bash ./goiabada-setup-linux-amd64 --type=production --db=postgres \ --auth-url=https://auth.example.com --admin-url=https://admin.example.com ``` It writes `docker-compose.yml`, which you can commit, and `docker-compose.override.yml` beside it, which holds every secret. **Never** commit the override. 2. **Back up the AES key** from the override before anything else: see [Back up the AES key](https://goiabada.dev/deploy/secrets/#back-up-the-aes-key). 3. **Start Goiabada** with the command the wizard printed. With both files in the current directory, under their own names, it’s: ```bash docker compose up -d ``` Docker Compose reads the override without being asked. The first start seeds the database, which can take a minute. 4. **Check both servers answer** on the host: ```bash curl http://127.0.0.1:9090/health # healthy curl http://127.0.0.1:9091/health # healthy ``` 5. **Put a proxy in front,** with one of the three pages above, and then [sign in](https://goiabada.dev/get-started/first-sign-in/). ## What the files run - **The database runs beside Goiabada,** in a container of its own for MySQL, PostgreSQL and SQL Server, with its data in a named volume. With SQLite, the auth server keeps the database file in the `sqlite-data` volume. [Database](https://goiabada.dev/deploy/database/) covers using a database server of your own instead. - **The auth server starts once the database answers,** and the admin console once the auth server does. A first start or an upgrade has up to five minutes to seed or migrate the database before Docker Compose counts the auth server unhealthy. - **Both servers publish their ports on `127.0.0.1` alone,** 9090 for the auth server and 9091 for the admin console. Nothing outside the host reaches them, so your proxy is the only way in. - **Both servers trust one proxy hop** to tell them the client’s IP address: see [Client IP and proxy trust](https://goiabada.dev/deploy/client-ip-and-proxy-trust/#reverse-proxy). - **Each server gets up to 60 seconds to stop** when you run `docker compose down` or `restart`, so requests in flight finish. - **The wizard’s rate limiter answer,** yes unless you said no, is written as `GOIABADA_AUTHSERVER_RATELIMITER_ENABLED`. See [Rate limits](https://goiabada.dev/reference/environment-variables/#rate-limits). ## The hop to the auth server The admin console calls the auth server at `GOIABADA_AUTHSERVER_INTERNALBASEURL`, which the wizard sets to `http://goiabada-authserver:9090`: straight to the auth server’s container, over the Compose network, without passing your proxy. That hop is plain HTTP, and it carries the admin console’s client secret, its refresh token and administrators’ tokens. A Compose network never leaves the host, so the hop is as safe as the host is. The generated file says so in a comment above the variable. When that isn’t enough, encrypt the hop with the auth server’s own HTTPS listener: 1. Make a certificate for the name the admin console calls, `goiabada-authserver`, from a certificate authority of your own, and mount it into the auth server’s container, readable by [the user the images run as](https://goiabada.dev/deploy/docker-compose/#the-user-the-images-run-as). 2. Turn on the auth server’s HTTPS listener, in its `environment`: ```yaml - "GOIABADA_AUTHSERVER_LISTEN_HOST_HTTPS=0.0.0.0" - "GOIABADA_AUTHSERVER_LISTEN_PORT_HTTPS=9443" - "GOIABADA_AUTHSERVER_CERTFILE=/certs/goiabada-authserver.pem" - "GOIABADA_AUTHSERVER_KEYFILE=/certs/goiabada-authserver-key.pem" ``` 3. Mount your certificate authority’s certificate into the admin console’s container, then point the admin console at the listener and have it trust that authority, in its `environment`: ```yaml - "GOIABADA_AUTHSERVER_INTERNALBASEURL=https://goiabada-authserver:9443" - "SSL_CERT_FILE=/certs/ca.pem" ``` `SSL_CERT_FILE`, or `SSL_CERT_DIR` for a directory of certificates, adds your authority to the ones the image already trusts. To trust yours alone, set both, `SSL_CERT_DIR` to a directory holding only your certificates. 4. Restart both servers with `docker compose up -d`. If the admin console then can’t reach the auth server, see [Unable to load the configuration from the auth server](https://goiabada.dev/troubleshooting/unable-to-load-the-configuration-from-the-auth-server/). ## The user the images run as Both images run as uid 10001 and gid 10001, a user and group named `goiabada`, and never as root. The image names them by number, so a platform that refuses a root container, such as Kubernetes with `runAsNonRoot`, can verify the user without reading the image’s `/etc/passwd`. The binary and the `/app` directory holding it belong to root, so the process can’t replace what its next start runs. A file you mount into a container must be readable by uid 10001 or gid 10001: the certificate and key named by `GOIABADA_AUTHSERVER_CERTFILE` and `GOIABADA_AUTHSERVER_KEYFILE` (and the admin console’s pair), and the templates and static files of a [customization](https://goiabada.dev/guides/customize-and-translate-the-pages/#change-the-templates). A key file left at mode `0600` and owned by root can’t be read; give it to the user, or to the group with mode `0640`: ```bash sudo chown 10001:10001 key.pem # or sudo chgrp 10001 key.pem && sudo chmod 0640 key.pem ``` The auth server writes to two directories, and its image creates both owned by 10001: - `/data`, where the generated Compose file mounts the SQLite volume. Docker copies a mount point’s ownership into an empty named volume, so a new volume mounted here is writable from the first start. A volume mounted at a path the image lacks gets a directory Docker creates owned by root, where the server can write nothing. - `/bootstrap`, where the legacy bootstrap writes `bootstrap.env` when `GOIABADA_AUTHSERVER_BOOTSTRAP_ENV_OUTFILE` names a file in it. Mount a named volume there, and read the file with `docker compose cp goiabada-authserver:/bootstrap/bootstrap.env .`. A bind mount of a host directory that doesn’t exist yet is created owned by root, and the write is refused. The generated file also drops every capability, keeps the root file system read-only and gives each server a `/tmp` in memory. > **Local testing** > > The wizard’s **Local testing** choice writes Compose files too, serving plain HTTP on `localhost`, published on every interface, with no proxy. It’s for the [Quickstart](https://goiabada.dev/get-started/quickstart/), never for a deployment other people reach. ## Next steps [Choose a method](https://goiabada.dev/deploy/choose-a-method/): Which proxy to put in front of these files. [Secrets](https://goiabada.dev/deploy/secrets/): What the override file protects, and backing up the AES key. [Upgrade Goiabada](https://goiabada.dev/deploy/upgrade-goiabada/): Move to a new release, and back again. # Cloudflare Tunnel Source: https://goiabada.dev/deploy/cloudflare-tunnel/ This page helps you put Goiabada on the internet through a Cloudflare Tunnel: no inbound port, no certificate and no Nginx. `cloudflared`, Cloudflare’s connector, runs on your server and connects out to Cloudflare. Cloudflare serves HTTPS for both hostnames and sends each request down that connection to the Docker Compose file the setup wizard writes: ```plaintext Browser ── HTTPS ──▶ Cloudflare ── tunnel ──▶ cloudflared on the host ── HTTP ──▶ 127.0.0.1:9090 and :9091 ``` You need a domain on Cloudflare, a server with Docker, two hostnames under that domain, such as `auth.example.com` and `admin.example.com`, and the [setup wizard](https://goiabada.dev/deploy/setup-wizard/). Both hostnames must share a registrable domain: see [What every method needs](https://goiabada.dev/deploy/choose-a-method/#what-every-method-needs). ## Set it up 1. **Generate and start Goiabada.** Run the setup wizard, choose **Production with reverse proxy**, enter `https://auth.example.com` and `https://admin.example.com`, then start the files it wrote, as [Docker Compose](https://goiabada.dev/deploy/docker-compose/) describes: ```bash docker compose up -d curl http://127.0.0.1:9090/health # healthy curl http://127.0.0.1:9091/health # healthy ``` 2. **Create a tunnel.** In the [Cloudflare Zero Trust dashboard](https://one.dash.cloudflare.com/), go to **Networks** → **Tunnels & Mesh**, choose **Create a tunnel**, pick **Cloudflared**, and name it, such as `goiabada`. 3. **Install `cloudflared` on the server,** with the commands the dashboard shows for your operating system. The tunnel shows as **Healthy** once it has connected; on the server, `sudo systemctl status cloudflared` shows it running. If you run `cloudflared` in Docker instead, add `--network host` to the dashboard’s `docker run`, so that `127.0.0.1` in the routes below is the server’s and not the container’s. 4. **Route both hostnames to Goiabada.** On the tunnel’s **Published application routes** tab, add two routes, each of type `HTTP`. Creating the tunnel ends on the form for the first one: | Hostname | URL | | - | - | | `auth.example.com` | `127.0.0.1:9090` | | `admin.example.com` | `127.0.0.1:9091` | Cloudflare creates a proxied `CNAME` record for each. If it says a record with that name already exists, delete the old record under **DNS** → **Records** and add the route again. 5. **Turn on Always Use HTTPS,** under **SSL/TLS** → **Edge Certificates** for your domain, so a browser that asks for `http://` is sent to `https://`. 6. **Sign in** at `https://admin.example.com`, as [First sign-in](https://goiabada.dev/get-started/first-sign-in/) describes. ## What this setup gives you - **Nothing on the server is published.** `cloudflared` connects out, so the firewall can refuse every inbound connection, and the server’s address never appears in DNS. The Compose file publishes 9090 and 9091 on `127.0.0.1` alone, where only `cloudflared` and other processes on the host reach them. - **Cloudflare holds the certificates** for both hostnames, and the tunnel encrypts the hop from Cloudflare to `cloudflared`. Goiabada serves plain HTTP on the host, and marks its cookies `Secure` because its URLs are `https://`. - **Each server sees the client’s address,** which Cloudflare appends to `X-Forwarded-For`: see [Client IP and proxy trust](https://goiabada.dev/deploy/client-ip-and-proxy-trust/#cloudflare-tunnel). - **The admin console reaches the auth server inside the Compose network,** not through the tunnel: see [The hop to the auth server](https://goiabada.dev/deploy/docker-compose/#the-hop-to-the-auth-server). ## Next steps [Client IP and proxy trust](https://goiabada.dev/deploy/client-ip-and-proxy-trust/): How Goiabada reads the client's address behind the tunnel. [Secrets](https://goiabada.dev/deploy/secrets/): Back up the AES key, and keep the override file safe. [Production checklist](https://goiabada.dev/deploy/production-checklist/): What to check before going live. # Cloudflare + Nginx Source: https://goiabada.dev/deploy/cloudflare-nginx/ This page helps you put Goiabada behind Cloudflare’s proxy on a server that already runs Nginx for other sites. Cloudflare terminates the browser’s HTTPS and connects to Nginx over HTTPS again, with a certificate Nginx holds. Nginx forwards each request to the Docker Compose file the setup wizard writes, which publishes the two servers on `127.0.0.1` alone: ```plaintext Browser ── HTTPS ──▶ Cloudflare ── HTTPS ──▶ Nginx on the host ── HTTP ──▶ 127.0.0.1:9090 and :9091 ``` You need a domain on Cloudflare, a server with Docker and Nginx, two hostnames under that domain, such as `auth.example.com` and `admin.example.com`, and the [setup wizard](https://goiabada.dev/deploy/setup-wizard/). Both hostnames must share a registrable domain: see [What every method needs](https://goiabada.dev/deploy/choose-a-method/#what-every-method-needs). ## Set it up 1. **Point both hostnames at the server, through Cloudflare.** In Cloudflare’s DNS, add an `A` record for each, with the server’s IP address, and leave them **DNS only** (grey cloud) until you have the certificates: ```plaintext auth A DNS only admin A DNS only ``` If the server has an IPv6 address too, add an `AAAA` record for each with it. 2. **Generate and start Goiabada.** Run the setup wizard, choose **Production with reverse proxy**, enter `https://auth.example.com` and `https://admin.example.com`, then start the files it wrote, as [Docker Compose](https://goiabada.dev/deploy/docker-compose/) describes: ```bash docker compose up -d curl http://127.0.0.1:9090/health # healthy ``` 3. **Get the certificates and configure Nginx,** as steps 3 and 4 of [Reverse proxy](https://goiabada.dev/deploy/reverse-proxy/#set-it-up) show. Let’s Encrypt reaches the server directly while the records are DNS only. 4. **Turn Cloudflare’s proxy on.** Switch both records to **Proxied** (orange cloud). Then, under **SSL/TLS**, set the encryption mode to **Full (strict)**, so Cloudflare checks Nginx’s certificate, and turn on **Always Use HTTPS**. 5. **Have Nginx resolve Cloudflare’s addresses,** so Goiabada sees each client’s address rather than Cloudflare’s. [Cloudflare + Nginx](https://goiabada.dev/deploy/client-ip-and-proxy-trust/#cloudflare--nginx) on Client IP and proxy trust has the file to write. 6. **Open only 80 and 443,** besides SSH. With ufw, allow your SSH port first, so turning the firewall on keeps your access. Set `SSH_PORT` to the port your SSH server listens on. In an SSH session, it’s the last field of `$SSH_CONNECTION`: ```bash SSH_PORT="${SSH_CONNECTION##* }"; echo "SSH port: ${SSH_PORT:-unknown}" ``` If that prints `unknown`, or you’re in a `sudo -i` or `su` shell, at a console, or in a tmux, screen or mosh session, which can keep an old value, look the port up and set it yourself, such as `SSH_PORT=2222`. `sudo ss -tlnp` shows SSH listening as `sshd`, or as `systemd` on a host that starts it through socket activation, as recent Ubuntu releases do, where `systemctl cat ssh.socket` shows its `ListenStream`. Then add the rules. Nothing changes unless `SSH_PORT` is a port from 1 to 65535 written without a leading zero, and each command runs only if the one before it succeeded. It runs the same in Bash and in `sh`: ```bash case "$SSH_PORT" in [1-9] | [1-9][0-9] | [1-9][0-9][0-9] | [1-9][0-9][0-9][0-9] | [1-9][0-9][0-9][0-9][0-9]) port_ok=yes ;; *) port_ok=no ;; esac if [ "$port_ok" = yes ] && [ "$SSH_PORT" -le 65535 ]; then sudo ufw allow "$SSH_PORT"/tcp && sudo ufw allow 80/tcp && sudo ufw allow 443/tcp && sudo ufw enable else echo "SSH_PORT isn't a port from 1 to 65535; nothing was changed." fi ``` Before you close this session, open a second SSH connection to check that you still get in. ufw comes with Ubuntu; on Debian, `sudo apt-get install ufw` first, or open the same ports in the firewall you use. **Never** open 9090 or 9091. 7. **Sign in** at `https://admin.example.com`, as [First sign-in](https://goiabada.dev/get-started/first-sign-in/) describes. Then read the auth server’s request log, `docker compose logs goiabada-authserver`: the `ip` of your request should be your own address, not Cloudflare’s. ## What this setup gives you - **Two certificates protect each request:** Cloudflare’s edge certificate on the browser’s side, and the Let’s Encrypt certificate Nginx serves to Cloudflare. **Full (strict)** makes Cloudflare refuse an Nginx certificate that isn’t valid for the hostname. - **Renewals work with the proxy on.** The Nginx configuration answers Let’s Encrypt’s challenge on both 80 and 443, where Cloudflare’s redirect to HTTPS sends it. `sudo certbot renew --dry-run` checks it. - **Each server sees the client’s address,** once Nginx resolves Cloudflare’s: see [Client IP and proxy trust](https://goiabada.dev/deploy/client-ip-and-proxy-trust/#cloudflare--nginx). - **The admin console reaches the auth server inside the Compose network,** not through Cloudflare: see [The hop to the auth server](https://goiabada.dev/deploy/docker-compose/#the-hop-to-the-auth-server). ## Next steps [Client IP and proxy trust](https://goiabada.dev/deploy/client-ip-and-proxy-trust/): How Goiabada reads the client's address behind Cloudflare and Nginx. [Secrets](https://goiabada.dev/deploy/secrets/): Back up the AES key, and keep the override file safe. [Production checklist](https://goiabada.dev/deploy/production-checklist/): What to check before going live. # Reverse proxy Source: https://goiabada.dev/deploy/reverse-proxy/ This page helps you put Goiabada on the internet behind Nginx, with certificates from Let’s Encrypt and no Cloudflare. Nginx answers HTTPS on ports 80 and 443 and forwards each request to the Docker Compose file the setup wizard writes, which publishes the two servers on `127.0.0.1` alone: ```plaintext Browser ── HTTPS ──▶ Nginx on the host ── HTTP ──▶ auth.example.com → 127.0.0.1:9090 admin.example.com → 127.0.0.1:9091 ``` You need a server with Docker and Nginx, two hostnames under one domain, such as `auth.example.com` and `admin.example.com`, and the [setup wizard](https://goiabada.dev/deploy/setup-wizard/). Both hostnames must share a registrable domain: see [What every method needs](https://goiabada.dev/deploy/choose-a-method/#what-every-method-needs). ## Set it up 1. **Point both hostnames at the server.** At your DNS provider, add an `A` record for each, with the server’s IP address: ```plaintext auth A admin A ``` If the server has an IPv6 address too, add an `AAAA` record for each with it. `dig +short auth.example.com` shows the address once the record has spread. 2. **Generate and start Goiabada.** Run the setup wizard, choose **Production with reverse proxy**, enter `https://auth.example.com` and `https://admin.example.com`, then start the files it wrote, as [Docker Compose](https://goiabada.dev/deploy/docker-compose/) describes: ```bash docker compose up -d curl http://127.0.0.1:9090/health # healthy ``` 3. **Get the certificates.** Install certbot and give Nginx a temporary site that answers Let’s Encrypt’s challenge for both hostnames: ```bash sudo apt-get update && sudo apt-get install certbot sudo mkdir -p /var/www/certbot ``` Write `/etc/nginx/sites-available/goiabada`, with your own hostnames: ```nginx server { listen 80; listen [::]:80; server_name auth.example.com admin.example.com; location /.well-known/acme-challenge/ { root /var/www/certbot; } } ``` Enable it, and ask for one certificate per hostname: ```bash sudo ln -s /etc/nginx/sites-available/goiabada /etc/nginx/sites-enabled/ sudo nginx -t && sudo nginx -s reload sudo certbot certonly --webroot -w /var/www/certbot -d auth.example.com sudo certbot certonly --webroot -w /var/www/certbot -d admin.example.com ``` 4. **Proxy both hostnames to Goiabada.** Replace `/etc/nginx/sites-available/goiabada` with the full site: ```nginx # Auth server server { listen 443 ssl; listen [::]:443 ssl; http2 on; server_name auth.example.com; ssl_certificate /etc/letsencrypt/live/auth.example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/auth.example.com/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; location / { proxy_pass http://127.0.0.1:9090; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } location /.well-known/acme-challenge/ { root /var/www/certbot; } } # Admin console server { listen 443 ssl; listen [::]:443 ssl; http2 on; server_name admin.example.com; ssl_certificate /etc/letsencrypt/live/admin.example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/admin.example.com/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; location / { proxy_pass http://127.0.0.1:9091; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } location /.well-known/acme-challenge/ { root /var/www/certbot; } } # HTTP: Let's Encrypt's challenge, and a redirect to HTTPS for everything else server { listen 80; listen [::]:80; server_name auth.example.com admin.example.com; location /.well-known/acme-challenge/ { root /var/www/certbot; } location / { return 301 https://$host$request_uri; } } ``` `http2 on;` needs Nginx 1.25.1 or later, and `nginx -v` shows yours. On an older one, delete it and write `listen 443 ssl http2;` and `listen [::]:443 ssl http2;` instead. ```bash sudo nginx -t && sudo systemctl reload nginx ``` 5. **Open only 80 and 443,** besides SSH. With ufw, allow your SSH port first, so turning the firewall on keeps your access. Set `SSH_PORT` to the port your SSH server listens on. In an SSH session, it’s the last field of `$SSH_CONNECTION`: ```bash SSH_PORT="${SSH_CONNECTION##* }"; echo "SSH port: ${SSH_PORT:-unknown}" ``` If that prints `unknown`, or you’re in a `sudo -i` or `su` shell, at a console, or in a tmux, screen or mosh session, which can keep an old value, look the port up and set it yourself, such as `SSH_PORT=2222`. `sudo ss -tlnp` shows SSH listening as `sshd`, or as `systemd` on a host that starts it through socket activation, as recent Ubuntu releases do, where `systemctl cat ssh.socket` shows its `ListenStream`. Then add the rules. Nothing changes unless `SSH_PORT` is a port from 1 to 65535 written without a leading zero, and each command runs only if the one before it succeeded. It runs the same in Bash and in `sh`: ```bash case "$SSH_PORT" in [1-9] | [1-9][0-9] | [1-9][0-9][0-9] | [1-9][0-9][0-9][0-9] | [1-9][0-9][0-9][0-9][0-9]) port_ok=yes ;; *) port_ok=no ;; esac if [ "$port_ok" = yes ] && [ "$SSH_PORT" -le 65535 ]; then sudo ufw allow "$SSH_PORT"/tcp && sudo ufw allow 80/tcp && sudo ufw allow 443/tcp && sudo ufw enable else echo "SSH_PORT isn't a port from 1 to 65535; nothing was changed." fi ``` Before you close this session, open a second SSH connection to check that you still get in. ufw comes with Ubuntu; on Debian, `sudo apt-get install ufw` first, or open the same ports in the firewall you use. **Never** open 9090 or 9091: a caller that reaches Goiabada around Nginx picks the IP address it’s rate limited and audited under. 6. **Check the renewals.** Let’s Encrypt certificates last 90 days, and the certbot package renews them on a timer: ```bash sudo certbot renew --dry-run systemctl list-timers | grep certbot ``` 7. **Sign in** at `https://admin.example.com`, as [First sign-in](https://goiabada.dev/get-started/first-sign-in/) describes. ## What this setup gives you - **Nginx terminates TLS.** Goiabada serves plain HTTP on `127.0.0.1`, which only processes on the host reach. Goiabada marks its cookies `Secure` because its URLs are `https://`, whatever Nginx speaks to it. - **Each server sees the client’s address,** from the `X-Forwarded-For` entry Nginx appends: see [Client IP and proxy trust](https://goiabada.dev/deploy/client-ip-and-proxy-trust/#reverse-proxy). - **The admin console reaches the auth server inside the Compose network,** not through Nginx: see [The hop to the auth server](https://goiabada.dev/deploy/docker-compose/#the-hop-to-the-auth-server). - **Goiabada sets its own security headers,** `Strict-Transport-Security` included once its URLs are `https://`, so Nginx adds none. A header Nginx added as well would reach the browser twice. ## Next steps [Client IP and proxy trust](https://goiabada.dev/deploy/client-ip-and-proxy-trust/): How Goiabada reads the client's address behind Nginx. [Secrets](https://goiabada.dev/deploy/secrets/): Back up the AES key, and keep the override file safe. [Production checklist](https://goiabada.dev/deploy/production-checklist/): What to check before going live. # Overview Source: https://goiabada.dev/deploy/kubernetes/overview/ This page helps you plan a Goiabada deployment on a Kubernetes cluster. ## Deploy it 1. **Check you have what Goiabada needs,** below: a cluster with `kubectl` access, a database server, and two host names on one domain. 2. **Set up a gateway and its certificates, then deploy,** as [Gateway and certificates](https://goiabada.dev/deploy/kubernetes/gateway-and-certificates/) walks through with Envoy Gateway and cert-manager. The [setup wizard](https://goiabada.dev/deploy/setup-wizard/) writes the manifest and the Secrets in that procedure. 3. **Sign in** at your admin console’s URL, as [First sign-in](https://goiabada.dev/get-started/first-sign-in/) describes, with the password you read back out of the cluster. 4. **Get it ready for production:** [High availability](https://goiabada.dev/deploy/kubernetes/high-availability/) to run more than one pod, [Security](https://goiabada.dev/deploy/kubernetes/security/) to decide who can reach the pods, and [Secrets](https://goiabada.dev/deploy/kubernetes/secrets/) to choose how the Secrets are created and who can read them. ## What Goiabada needs | Requirement | Why | | - | - | | **HTTPS on both public URLs** | Each server marks its cookies `Secure` only when its URL is `https://`. The gateway serves HTTPS and passes plain HTTP to the pods. | | **Two host names, on one registrable domain** | The gateway routes by host name, so the auth server and the admin console each need one of their own, such as `auth.example.com` and `admin.example.com`. Both must be on the same site: `admin.example.net` beside `auth.example.com` fails, because the admin console’s sign-in ends with the auth server posting back to it, and the browser leaves the admin console’s session cookie off a cross-site post. Every sign-in then stops at “This sign-in can’t be finished”. | | **A database every pod shares** | MySQL, PostgreSQL or SQL Server, in the cluster or outside it. SQLite isn’t offered: it’s a file on one host, used through one connection. See [Database](https://goiabada.dev/deploy/database/). | | **An empty database on the first start** | The auth server seeds it with the administrator and the admin console’s client, configured for the URLs you gave the wizard. A database seeded for other URLs refuses the admin console’s sign-in: see [Invalid redirect_uri](https://goiabada.dev/troubleshooting/invalid-redirect-uri/). | | **A Gateway API implementation and a certificate issuer** | The manifest creates a Gateway of class `eg` with a cert-manager annotation. [Gateway and certificates](https://goiabada.dev/deploy/kubernetes/gateway-and-certificates/) is the recipe the project tests; any implementation and issuer work if you adapt the Gateway to them. | You also need `kubectl` configured for the cluster, and the setup wizard on the machine you run it from. ## What runs where ```plaintext Internet → Gateway (HTTPS) → auth.example.com → goiabada-authserver Service (9090) → auth server pods → admin.example.com → goiabada-adminconsole Service (9091) → admin console pods admin console pods → goiabada-authserver Service (9090), plain HTTP inside the cluster auth server pods → your database ``` The admin console calls the auth server’s API at `GOIABADA_AUTHSERVER_INTERNALBASEURL`, which the wizard sets to `http://goiabada-authserver:9090`: straight to the Service, without passing the gateway. That hop is plain HTTP, and it carries the admin console’s client secret, its refresh token and administrators’ tokens. It’s sound on a pod network you trust; [Encrypt the hop to the auth server](https://goiabada.dev/deploy/kubernetes/security/#encrypt-the-hop-to-the-auth-server) has what to do when it isn’t. ## What the wizard generates The wizard writes two files. `goiabada-k8s.yaml` holds no secret, so you can commit it: - **A Namespace,** labelled to warn about any pod that breaks the restricted Pod Security Standard. See [Security](https://goiabada.dev/deploy/kubernetes/security/#pod-security). - **Two ConfigMaps,** `goiabada-authserver-config` and `goiabada-adminconsole-config`, each holding only what its server reads. The three URLs are in both, written from the same answers, so change them in both. - **A third ConfigMap, `goiabada-db-ca`,** if you gave the wizard a CA file for the database’s certificate. It’s mounted into the auth server’s pods. See [Check the database’s certificate](https://goiabada.dev/deploy/kubernetes/security/#check-the-databases-certificate). - **Two Deployments,** one per server, at one replica each, with a rolling update that never runs fewer pods than replicas and a spread over nodes. See [High availability](https://goiabada.dev/deploy/kubernetes/high-availability/). - **Probes, a stop pause and a grace period** on every container. See [Probes and shutdown](https://goiabada.dev/deploy/kubernetes/probes-and-shutdown/). - **Two PodDisruptionBudgets,** one per Deployment. - **Two ClusterIP Services,** `goiabada-authserver` on 9090 and `goiabada-adminconsole` on 9091. - **Two NetworkPolicies,** if you ask for them, admitting only Envoy and the admin console. See [Who can reach Goiabada](https://goiabada.dev/deploy/kubernetes/security/#who-can-reach-goiabada). - **A Gateway and three HTTPRoutes:** one route per host name, and one redirecting HTTP to HTTPS. - **A PodMonitor,** if you ask for one, or scrape annotations. See [Scrape on Kubernetes](https://goiabada.dev/deploy/monitoring/#scrape-on-kubernetes). `goiabada-secrets.yaml`, beside it, holds the two Secrets the Deployments read: `goiabada-encryption-key`, with the AES key alone, and `goiabada-secrets`, with every other secret. **Never** commit it. See [Secrets](https://goiabada.dev/deploy/kubernetes/secrets/). Both servers trust one proxy hop, the gateway, to tell them each client’s IP address, and log one record per request. See [Client IP and proxy trust](https://goiabada.dev/deploy/client-ip-and-proxy-trust/#kubernetes) and [Logs](https://goiabada.dev/deploy/logs/). > **Note** > > The wizard takes a lowercase domain name for each host, never an IP address, and refuses the same host for both servers: the Gateway routes by host name, and two listeners on one host and port are both rejected. ## Next steps [Gateway and certificates](https://goiabada.dev/deploy/kubernetes/gateway-and-certificates/): Install Envoy Gateway and cert-manager, then deploy Goiabada. [High availability](https://goiabada.dev/deploy/kubernetes/high-availability/): Run more than one pod, and size the database for it. [Secrets](https://goiabada.dev/deploy/kubernetes/secrets/): The Secrets the manifest reads, and other ways to create them. # Gateway and certificates Source: https://goiabada.dev/deploy/kubernetes/gateway-and-certificates/ This page helps you put Goiabada on a cluster behind Envoy Gateway, with certificates from Let’s Encrypt through cert-manager. It’s the recipe the project tests. Any Gateway API implementation and certificate issuer work too: the Goiabada side is the same, and [Overview](https://goiabada.dev/deploy/kubernetes/overview/#what-goiabada-needs) lists what it needs. > **For a cluster without a gateway or cert-manager** > > The steps install Envoy Gateway and cert-manager, and create cluster-wide resources. If your cluster already runs either, skip its installation, check that what it runs fits the steps, and **never** apply these files over it. Ask whoever runs the cluster when in doubt. ## Set it up 1. **Install Envoy Gateway and its GatewayClass.** Envoy Gateway’s install brings the Gateway API’s resources; wait for it: ```bash kubectl apply --server-side -f https://github.com/envoyproxy/gateway/releases/download/v1.9.1/install.yaml kubectl wait --timeout=5m -n envoy-gateway-system \ deployment/envoy-gateway --for=condition=Available ``` Check the [Envoy Gateway releases](https://github.com/envoyproxy/gateway/releases) for a newer version. Then create the `eg` GatewayClass the manifest names, with an EnvoyProxy that sets the traffic policy of the load balancer in front of Envoy. This is the `Cluster` policy, the wizard’s default, which works behind every load balancer; for `Local`, use the file in [The gateway’s traffic policy](https://goiabada.dev/deploy/kubernetes/gateway-and-certificates/#the-gateways-traffic-policy) instead. Save it as `gatewayclass.yaml`: ```yaml apiVersion: gateway.envoyproxy.io/v1alpha1 kind: EnvoyProxy metadata: name: goiabada-proxy namespace: envoy-gateway-system spec: provider: type: Kubernetes kubernetes: envoyService: externalTrafficPolicy: Cluster --- apiVersion: gateway.networking.k8s.io/v1 kind: GatewayClass metadata: name: eg spec: controllerName: gateway.envoyproxy.io/gatewayclass-controller parametersRef: group: gateway.envoyproxy.io kind: EnvoyProxy name: goiabada-proxy namespace: envoy-gateway-system ``` ```bash kubectl apply -f gatewayclass.yaml ``` 2. **Install cert-manager,** after Envoy Gateway, since it looks for the Gateway API’s resources when it starts. Turn on its Gateway API support, and wait for it: ```bash kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.21.2/cert-manager.yaml kubectl -n cert-manager patch deployment cert-manager --type=json \ -p='[{"op":"add","path":"/spec/template/spec/containers/0/args/-","value":"--enable-gateway-api"}]' kubectl -n cert-manager rollout status deployment/cert-manager --timeout=5m kubectl -n cert-manager rollout status deployment/cert-manager-webhook --timeout=5m kubectl -n cert-manager rollout status deployment/cert-manager-cainjector --timeout=5m ``` Check the [cert-manager releases](https://github.com/cert-manager/cert-manager/releases) for a newer version. 3. **Prepare the database,** MySQL, PostgreSQL or SQL Server, reachable from the cluster’s pods, as [Database](https://goiabada.dev/deploy/database/) describes. Only trying Goiabada out? [A PostgreSQL to try Goiabada on Kubernetes](https://goiabada.dev/deploy/database/#a-postgresql-to-try-goiabada-on-kubernetes) runs one in the cluster. 4. **Generate the files and deploy them.** Run the [setup wizard](https://goiabada.dev/deploy/setup-wizard/) and choose **Kubernetes cluster**. Then answer, in order: the database, the two public URLs (such as `https://auth.example.com` and `https://admin.example.com`), the namespace (`goiabada` unless you change it), the gateway’s traffic policy, whether [NetworkPolicies](https://goiabada.dev/deploy/kubernetes/security/#who-can-reach-goiabada) restrict who reaches the pods, the [rate limiter](https://goiabada.dev/deploy/kubernetes/high-availability/#rate-limiting-across-replicas), whether to expose [metrics](https://goiabada.dev/deploy/monitoring/#scrape-on-kubernetes), the administrator, and the database’s connection details. The wizard offers to test the database connection from your machine, which can’t reach a host only the cluster resolves, such as `postgres.db.svc.cluster.local`: for one, it says so and defaults to No. It warns if the database isn’t empty, and writes `goiabada-k8s.yaml` and `goiabada-secrets.yaml`. **Back up the AES key** from `goiabada-secrets.yaml` before anything else, as [Back up the AES key](https://goiabada.dev/deploy/secrets/#back-up-the-aes-key) says, and **never** commit that file; the wizard warns when it writes into a git working tree. Deploy both files, the Secrets first: ```bash kubectl apply -f goiabada-secrets.yaml -f goiabada-k8s.yaml ``` A container reads its Secrets once, when it starts. Applied after the manifest, Secrets that replace older ones of the same names, from an earlier attempt in the same namespace, would arrive after the new pods had started with the old ones, and the database would be seeded with secrets the cluster no longer holds. Both files create the namespace. For production, you can create the Secrets another way instead of from the file: see [Secrets](https://goiabada.dev/deploy/kubernetes/secrets/#create-the-secrets-another-way). 5. **Point DNS at the Gateway.** Read its address, which can take a minute to appear: ```bash kubectl get gateway goiabada -n goiabada -o jsonpath='{.status.addresses[0].value}' ``` Then create an `A` record for each host name, `auth.example.com` and `admin.example.com`, with that address, or a `CNAME` when the address is a host name, as on AWS. If your DNS provider can proxy traffic, as Cloudflare does, leave both records unproxied, **DNS only**: Let’s Encrypt has to reach the Gateway itself. Wait until both names resolve, for instance with `nslookup auth.example.com`. 6. **Create a ClusterIssuer for Let’s Encrypt,** which answers its challenge through Goiabada’s Gateway. Put your namespace in if it isn’t `goiabada`, then save it as `letsencrypt-issuer.yaml`: ```yaml apiVersion: cert-manager.io/v1 kind: ClusterIssuer metadata: name: letsencrypt-prod spec: acme: server: https://acme-v02.api.letsencrypt.org/directory privateKeySecretRef: name: letsencrypt-prod solvers: - http01: gatewayHTTPRoute: parentRefs: - name: goiabada namespace: goiabada kind: Gateway ``` ```bash kubectl apply -f letsencrypt-issuer.yaml ``` Create it only once both names resolve. Until it exists, cert-manager waits; from then on, it looks the names up. A name looked up before its record exists can stay missing for the cluster’s resolver for as long as your zone lets it remember a missing name, often 30 minutes. Let’s Encrypt needs no email address: it sends no expiry notices since June 2025. 7. **Check it’s up, and sign in.** ```bash kubectl get pods -n goiabada # both Running, 1/1 kubectl get gateway,httproute -n goiabada # the Gateway PROGRAMMED True, with an ADDRESS kubectl get certificates -n goiabada # both READY True, within a few minutes ``` Then sign in at `https://admin.example.com`, as [First sign-in](https://goiabada.dev/get-started/first-sign-in/) describes. The wizard prints no secret, so read the administrator’s password back out of the cluster: ```bash kubectl get secret goiabada-secrets -n goiabada -o jsonpath='{.data.admin-password}' | base64 -d ``` If something goes wrong: - The certificates stay not ready: [Certificates are not issued](https://goiabada.dev/troubleshooting/certificates-are-not-issued/). - The auth server’s pod is in `CrashLoopBackOff`, or can’t reach the database: [CrashLoopBackOff or unable to create the database connection](https://goiabada.dev/troubleshooting/crashloopbackoff-or-unable-to-create-the-database-connection/). - The admin console’s pod is in `CrashLoopBackOff`, or signing in to it fails: [Locked out of the admin console](https://goiabada.dev/troubleshooting/locked-out-of-the-admin-console/). - A start waits for the migration lock, or refuses a dirty schema: [Waiting for the migration lock, or marked dirty](https://goiabada.dev/troubleshooting/waiting-for-the-migration-lock-or-marked-dirty/). - Signing in fails with “Invalid redirect_uri”: [Invalid redirect_uri](https://goiabada.dev/troubleshooting/invalid-redirect-uri/). ## The route in The Gateway has three listeners: HTTP on port 80, and HTTPS on port 443 for each host name, each ending TLS with a certificate of its own, `goiabada-tls-auth` and `goiabada-tls-admin`. One HTTPRoute per host name sends its traffic to the server’s Service, and a third answers every plain HTTP request with a `301` to HTTPS. The Gateway carries the annotation `cert-manager.io/cluster-issuer: "letsencrypt-prod"`. From it, cert-manager creates one Certificate per HTTPS listener, and the ClusterIssuer proves you control each host name by answering Let’s Encrypt’s HTTP-01 challenge through a temporary route on the Gateway’s port 80 listener. cert-manager renews the certificates itself. ## The gateway’s traffic policy The traffic policy of the load balancer in front of Envoy decides which address Envoy sees for a client, and so what Goiabada’s rate limits, audit records and session records count. The wizard asks which one you use, writes its comments to match, and prints the GatewayClass file for it. **`Cluster`,** the wizard’s default, works behind every load balancer: the load balancer may send a connection to any node, and the node forwards it to an Envoy pod wherever one runs. The node replaces the client’s address with its own, so Goiabada sees a node’s address for every client. **`Local`, with Envoy on every node** (`--gateway-traffic-policy=local`), keeps the client’s address behind a load balancer that passes each connection through: a node delivers a connection only to an Envoy pod on that same node, so nothing rewrites its source. A load balancer that proxies connections instead, opening its own to the nodes, as some clouds’ do, shows Goiabada its own address under either policy, and only Proxy Protocol, below, carries the client’s. Check which you have once Goiabada runs: request any page, then read that request’s `ip=` in the auth server’s log with `kubectl logs -n goiabada deployment/goiabada-authserver`. Your own address means `Local` works; one address for every client is the load balancer’s. A node with no Envoy pod drops what the load balancer sends it, which is why Envoy runs as a DaemonSet here, at the cost of one Envoy pod per node. Save this as `gatewayclass.yaml` instead: ```yaml apiVersion: gateway.envoyproxy.io/v1alpha1 kind: EnvoyProxy metadata: name: goiabada-proxy namespace: envoy-gateway-system spec: provider: type: Kubernetes kubernetes: envoyDaemonSet: {} envoyService: externalTrafficPolicy: Local --- apiVersion: gateway.networking.k8s.io/v1 kind: GatewayClass metadata: name: eg spec: controllerName: gateway.envoyproxy.io/gatewayclass-controller parametersRef: group: gateway.envoyproxy.io kind: EnvoyProxy name: goiabada-proxy namespace: envoy-gateway-system ``` An empty `envoyDaemonSet` is a DaemonSet with every default; an EnvoyProxy takes it or `envoyDeployment`, not both. This variant follows Envoy Gateway v1.9.1’s API. On a managed cluster whose load balancer proxies connections, applying it over the `Cluster` file replaced Envoy’s Deployment with a DaemonSet, the load balancer kept its address and traffic flowed, and Goiabada saw the load balancer’s address for every client. Check that a pod runs on every node after applying it: `kubectl get daemonset -n envoy-gateway-system`. Changing the policy on a running cluster replaces Envoy rather than rolling it: switching back to `Cluster` there failed requests for about 15 seconds, so change it when an interruption is acceptable. A third way keeps the client’s address without an Envoy pod per node: Proxy Protocol, where the load balancer prepends the client’s address to each connection. It needs annotations on the load balancer’s Service that differ from cloud to cloud, so the wizard doesn’t offer it; Envoy Gateway’s [client traffic policy](https://gateway.envoyproxy.io/docs/tasks/traffic/client-traffic-policy/) documentation covers the Envoy side. > **The EnvoyProxy and GatewayClass are cluster-wide** > > Every Gateway of class `eg` shares them. On a cluster that already runs Envoy Gateway, the traffic policy belongs to whoever runs it: agree it with them rather than apply either file over theirs, and answer the wizard with the policy the cluster actually uses. [Client IP and proxy trust](https://goiabada.dev/deploy/client-ip-and-proxy-trust/#kubernetes) has how Goiabada reads the address Envoy forwards. ## Next steps [High availability](https://goiabada.dev/deploy/kubernetes/high-availability/): Run more than one pod, and size the database for it. [Security](https://goiabada.dev/deploy/kubernetes/security/): Who can reach the pods, and what they may do. [Secrets](https://goiabada.dev/deploy/kubernetes/secrets/): The Secrets the manifest reads, and other ways to create them. # High availability Source: https://goiabada.dev/deploy/kubernetes/high-availability/ This page helps you run more than one pod of each Goiabada server, so a node going away doesn’t take sign-ins with it. The manifest the setup wizard generates runs one replica of each server. At one replica, a node drain or a node failure stops that server until its replacement is ready. ## Scale out 1. **Check the database can take the connections.** Every auth server pod opens a pool of its own, of at most `GOIABADA_DB_MAX_OPEN_CONNS` connections, 20 unless you set it, against the one database they share. The pods must fit under the database’s connection limit: ```text (replicas + surge) × GOIABADA_DB_MAX_OPEN_CONNS + headroom < max_connections ``` [Connections](https://goiabada.dev/deploy/kubernetes/high-availability/#connections) explains each term. With the defaults, three replicas make 4 × 20 = 80 connections. A stock PostgreSQL allows 100, 3 of them reserved for superusers, which leaves 17 for the headroom; a stock MySQL allows 151. Until the sum holds, lower `GOIABADA_DB_MAX_OPEN_CONNS` in the auth server’s ConfigMap, or raise the database’s `max_connections`. 2. **Scale the Deployments:** ```bash kubectl scale deployment goiabada-authserver -n goiabada --replicas=3 kubectl scale deployment goiabada-adminconsole -n goiabada --replicas=2 ``` To keep the count across a reapply of the manifest, change `replicas` in `goiabada-k8s.yaml` too. 3. **Decide on the rate limiter,** whose per-pod limits multiply with the replicas: see [Rate limiting across replicas](https://goiabada.dev/deploy/kubernetes/high-availability/#rate-limiting-across-replicas). ## Connections - **Surge** is the extra pod a rolling update runs beside the old ones. The generated Deployments set `maxSurge: 1` and `maxUnavailable: 0`: a rollout starts one new pod, waits for it to be ready, and only then stops an old one, so three replicas briefly run four, and never fewer than three. A stopping pod holds its connections until it exits, up to a minute, and the next new pod can start meanwhile, so count one more pod’s connections in the headroom. - **Headroom** is every other connection the database must accept: the `migrate` subcommand, the maintenance connection a starting pod opens to create the database when `GOIABADA_DB_CREATE` is on, your own sessions, and any other application on the same server. The admin console opens none. - **With a HorizontalPodAutoscaler, replicas is its `maxReplicas`,** not today’s count, since it may scale out to it under load, at the worst moment. A capped pool trades one failure for another, on purpose. When every connection of a pod is busy, its next request waits for one to be released, until the client or the gateway gives up, rather than the database refusing a new connection to every pod at once. Raising replicas without raising `max_connections` adds no capacity at the database: it moves the bottleneck from the pods’ queues to the database’s refusals. The pool’s settings are in [Environment variables](https://goiabada.dev/reference/environment-variables/#database). ## Disruptions and spread Each Deployment has a PodDisruptionBudget with `maxUnavailable: 1`, so a voluntary disruption, such as a `kubectl drain`, a node pool upgrade or an autoscaler removing a node, evicts at most one of its pods at a time. At one replica that one is the only pod, and the server is down until its replacement is scheduled elsewhere and ready; the rolling update doesn’t help, because an eviction starts no surge pod first. Run at least two replicas to keep serving through a drain. The same budget then keeps all but one pod serving at any count, so it needs no change when you scale. Each pod template also spreads its pods over nodes, with a `topologySpreadConstraints` on `kubernetes.io/hostname`, `maxSkew: 1` and `whenUnsatisfiable: ScheduleAnyway`. At three replicas on three nodes it places one per node, so losing a node takes one pod rather than all of them. On fewer nodes than replicas it still schedules every pod rather than leave one pending. ## Autoscaling The manifest emits no HorizontalPodAutoscaler. You can add one; settle two things first: - **The database’s connections,** sized for `maxReplicas` plus the surge pod, as [Connections](https://goiabada.dev/deploy/kubernetes/high-availability/#connections) says. - **What to scale on.** CPU is the auth server’s bottleneck under a burst of sign-ins (see [Resource limits](https://goiabada.dev/deploy/kubernetes/high-availability/#resource-limits)), so a CPU target relative to its `100m` request is a reasonable signal. The admin console checks no passwords and rarely needs more than one or two replicas. ## Rate limiting across replicas The auth server’s limits counted by a user or an email always apply: wrong passwords for one account, 100 an hour, one-time codes, the Account pages’ password checks, email verification, and password-reset and registration mails to one address. The limits counted by an IP address are off unless you turn them on, with `GOIABADA_AUTHSERVER_RATELIMITER_ENABLED`. The wizard asks, and writes the answer into the auth server’s ConfigMap either way; `--rate-limiter=true` or `=false` answers without the prompt. Its default follows the [gateway’s traffic policy](https://goiabada.dev/deploy/kubernetes/gateway-and-certificates/#the-gateways-traffic-policy): - **Under `Cluster`, off.** Goiabada sees a node’s address for every client, so a per-IP limit counts together every user the load balancer sends through one node: 30 password posts a minute and 20 forgot-password requests per 5 minutes per node, for example, which throttles sign-ins on a busy site. On, it also limits wrong passwords for one email from one network, and counts every client through one node as one network, so anyone who knows an email can block its password sign-ins for 15 minutes with 10 wrong passwords. Off, the limit of 100 wrong passwords an hour for one account still bounds guessing. Turn it on if a tighter bound matters more to you than that. - **Under `Local`, on.** Goiabada sees each client’s address, so each per-IP limit counts one client, behind a load balancer that passes connections through. One that proxies them shows Goiabada its own address for every client, and the per-IP limits then count every client together, as under `Cluster`. [The gateway’s traffic policy](https://goiabada.dev/deploy/kubernetes/gateway-and-certificates/#the-gateways-traffic-policy) has how to tell. Not every limit holds across replicas: - **Shared:** the limits on guessing credentials, which count failed passwords (at sign-in and through the password grant), failed OTP codes, failed account password checks and failed email verification codes. They’re counted in the database every pod shares, so three replicas still allow one budget, and a rollout doesn’t reset it. If the database can’t answer such a check within 5 seconds, the request is answered with a server error rather than let through. - **Per pod:** every other limit, the per-IP limits and the limits on outgoing mail. Behind the Service, three replicas give a client up to three times each budget, and a rollout resets them. For a limit across the whole deployment, use the gateway’s own rate limiting. The budgets are in [Rate limits](https://goiabada.dev/reference/environment-variables/#rate-limits). With the limiter on, the auth server also logs a warning at every start that it trusts one proxy hop with no list; behind Envoy alone that’s expected, as [The startup warnings](https://goiabada.dev/deploy/client-ip-and-proxy-trust/#the-startup-warnings) explains. ## Resource limits Both containers request `100m` of CPU and `128Mi` of memory, and are limited to `500m` and `512Mi`: ```yaml resources: requests: memory: "128Mi" cpu: "100m" limits: memory: "512Mi" cpu: "500m" ``` For the auth server, the CPU numbers come down to one operation. A sign-in costs one bcrypt password check at cost 10, about 35 ms of CPU on a modern core, which no limit slows when it runs alone. Under the `500m` limit a pod checks about 14 passwords a second, and a burst queues behind that: measured, 16 sign-ins arriving at once took about 1.2 s to complete, and every other request on that pod waited with them. Cloud vCPUs are often slower than the core measured, and the `100m` request is all the scheduler guarantees on a busy node. If your sign-in peak is higher, raise the CPU limit or add replicas. > **Caution** > > Keep a CPU limit on the auth server. Anyone can make it spend CPU by sending passwords to check, and the limit bounds what one pod spends. The admin console checks no passwords and needs less; it carries the same numbers for simplicity. ## Next steps [Probes and shutdown](https://goiabada.dev/deploy/kubernetes/probes-and-shutdown/): How a pod starts, how long it may take, and how it stops. [Database](https://goiabada.dev/deploy/database/): Prepare the database every pod shares. [Monitoring](https://goiabada.dev/deploy/monitoring/): Watch the pool waits and rate-limit refusals. # Security Source: https://goiabada.dev/deploy/kubernetes/security/ This page helps you decide who in your cluster can reach Goiabada’s pods, and what those pods may do. ## Lock it down 1. **Restrict who reaches the pods** with the NetworkPolicies, by answering yes when the setup wizard asks, or passing `--network-policy`. Check the two things in [Who can reach Goiabada](https://goiabada.dev/deploy/kubernetes/security/#who-can-reach-goiabada) first. 2. **Enforce the restricted Pod Security Standard,** if you own the namespace outright and everything else running there meets it, cert-manager’s HTTP-01 solver pods included: ```bash kubectl label namespace goiabada pod-security.kubernetes.io/enforce=restricted ``` 3. **Decide whether the hop from the admin console to the auth server needs encrypting,** as [Encrypt the hop to the auth server](https://goiabada.dev/deploy/kubernetes/security/#encrypt-the-hop-to-the-auth-server) explains. 4. **Limit who can read the Secrets,** and the AES key above all: see [Who can read the AES key](https://goiabada.dev/deploy/kubernetes/secrets/#who-can-read-the-aes-key). ## Pod security Every generated container runs with this `securityContext`, which is everything the restricted [Pod Security Standard](https://kubernetes.io/docs/concepts/security/pod-security-standards/) requires, and a read-only root file system besides: ```yaml securityContext: runAsNonRoot: true runAsUser: 10001 runAsGroup: 10001 allowPrivilegeEscalation: false readOnlyRootFilesystem: true capabilities: drop: ["ALL"] seccompProfile: type: RuntimeDefault ``` uid 10001 is [the user the images run as](https://goiabada.dev/deploy/docker-compose/#the-user-the-images-run-as). No writable volume is mounted, because neither server writes to its file system here. The legacy bootstrap file isn’t used under Kubernetes, and the auth server’s one other write, Go spilling a picture upload over its size limit to `/tmp`, fails on the read-only root and is refused as `FILE_TOO_LARGE` rather than by the image check: a 400 either way. A certificate or customization you mount from a Secret or ConfigMap is readable by default, mode `0644`; with a stricter `defaultMode`, keep it readable by uid or gid 10001. Both pod specs also set: - **`automountServiceAccountToken: false`,** because neither server calls the Kubernetes API, so a compromised pod gets no API credential. - **`enableServiceLinks: false`,** because Kubernetes would otherwise give each container variables for every Service in the namespace, and two of the generated Services are named so that those begin with `GOIABADA_`, such as `GOIABADA_AUTHSERVER_PORT` and `GOIABADA_ADMINCONSOLE_SERVICE_HOST`, which neither server reads. The generated Namespace is labelled `pod-security.kubernetes.io/warn: restricted` and `pod-security.kubernetes.io/audit: restricted`. For any pod in it that breaks the standard, Goiabada’s own included, `kubectl apply` prints a warning and the API server writes an audit entry; nothing is refused. It doesn’t enforce, because the wizard accepts an existing namespace, whose other workloads’ pods would then be refused the next time they’re created. ## Who can reach Goiabada Without a NetworkPolicy, any pod in the cluster can call the `goiabada-authserver` and `goiabada-adminconsole` Services directly, around Envoy. Besides reaching the admin console from inside the cluster, such a pod chooses the address it’s rate limited and audited under, by sending its own `X-Forwarded-For` (see [Client IP and proxy trust](https://goiabada.dev/deploy/client-ip-and-proxy-trust/#kubernetes)). The wizard asks whether to close that; the answer is no unless you say yes, or pass `--network-policy`. On yes, the manifest carries one ingress-only NetworkPolicy per Deployment: | Pods | Admitted on | From | | - | - | - | | `goiabada-authserver` | TCP 9090 | Envoy’s namespace, `envoy-gateway-system`; the admin console’s pods | | `goiabada-adminconsole` | TCP 9091 | Envoy’s namespace, `envoy-gateway-system` | | `goiabada-authserver` | TCP 9190, with metrics exposed | The metrics scraper’s namespace, `monitoring` unless you name another | | `goiabada-adminconsole` | TCP 9191, with metrics exposed | The metrics scraper’s namespace, `monitoring` unless you name another | The metrics ports are a second rule of each policy, so the scraper’s namespace reaches the metrics port and nothing else, and Envoy and the admin console don’t reach it at all (see [Scrape on Kubernetes](https://goiabada.dev/deploy/monitoring/#scrape-on-kubernetes)). Envoy’s namespace is selected by its `kubernetes.io/metadata.name` label, which Kubernetes sets on every namespace. `envoy-gateway-system` is where Envoy Gateway runs its proxies by default; if yours runs them elsewhere, change the name in both policies. The kubelet’s probes come from the pod’s own node, which a NetworkPolicy can’t block, and cert-manager’s HTTP-01 solver pods carry other labels, so neither is affected. Before you answer yes, check two things: - **Your network plugin enforces NetworkPolicy.** Calico and Cilium do. Some clusters’ default plugin doesn’t, or does only once enforcement is turned on in the cluster’s settings; there the policies are accepted and do nothing. - **Nothing else in the cluster calls the auth server through its Service.** A resource server that fetches `/certs` or calls `/userinfo` at `http://goiabada-authserver.goiabada:9090` is blocked once the policy applies: its connection times out or is refused, depending on the plugin. Admit its namespace by adding a peer to the first rule of the auth server’s policy, as its comment shows: ```yaml ingress: - from: - namespaceSelector: matchLabels: kubernetes.io/metadata.name: envoy-gateway-system - podSelector: matchLabels: app: goiabada-adminconsole - namespaceSelector: matchLabels: kubernetes.io/metadata.name: my-resource-server ``` A workload that calls the auth server’s public URL goes through Envoy and needs nothing. The policies state no egress rules, so they restrict nothing the servers connect to. An egress rule would have to name every destination, and two can’t be named: the SMTP host and port are settings in the database, changed in the admin console, and the database host is often a DNS name, while a NetworkPolicy selects only pods, namespaces and IP blocks. A rule written for today’s addresses would cut email or the database when either moved. To see what the policies admit: ```bash kubectl describe networkpolicy -n goiabada ``` ## Encrypt the hop to the auth server The admin console calls the auth server at `GOIABADA_AUTHSERVER_INTERNALBASEURL`, which the wizard sets to `http://goiabada-authserver:9090`: plain HTTP to the Service, inside the cluster. That hop carries the admin console’s client secret, its refresh token and administrators’ tokens. The gateway’s hop to each pod is plain HTTP too, and carries what users send, passwords included. Both are sound on a pod network only your cluster uses. The comment the manifest carries above the variable says so. When the pod network isn’t one you trust, such as a cluster shared with workloads you don’t control, or nodes that talk across a network others can read, encrypt the hops. Any of these works, and none needs a change to Goiabada: - **A network plugin that encrypts pod traffic,** such as Calico’s or Cilium’s WireGuard mode. It encrypts every hop between nodes, the gateway’s included, and the manifest stays as it is. - **A service mesh with mutual TLS** between the pods, such as Istio or Linkerd. It covers both hops too. Check that its sidecars leave `X-Forwarded-For` as Envoy wrote it, or Goiabada reads the wrong client address: see [A second proxy hop](https://goiabada.dev/deploy/client-ip-and-proxy-trust/#a-second-proxy-hop). - **TLS at the auth server itself,** for the admin console’s hop: 1. Make a certificate for the name the admin console calls, `goiabada-authserver`, from a certificate authority of your own (cert-manager’s CA issuer can issue and renew it). Put it in a Secret, and mount it into the auth server’s container. 2. Turn on the auth server’s HTTPS listener, on port 9443, by setting `GOIABADA_AUTHSERVER_CERTFILE` and `GOIABADA_AUTHSERVER_KEYFILE` in its ConfigMap to the mounted files. Add port 9443 to its container and its Service, and, with the NetworkPolicies on, to the auth server’s policy. Its plain HTTP listener stays on for the gateway and the probes. 3. Mount your certificate authority’s certificate into the admin console’s container, and set, in the admin console’s ConfigMap: ```yaml GOIABADA_AUTHSERVER_INTERNALBASEURL: "https://goiabada-authserver:9443" SSL_CERT_FILE: "/certs/ca.pem" ``` `SSL_CERT_FILE`, or `SSL_CERT_DIR` for a directory, adds your authority to the ones the image already trusts. To trust yours alone, set both, `SSL_CERT_DIR` to a directory holding only your certificates. 4. Restart both Deployments. The auth server reads its certificate when it starts, so restart it after each renewal too. - **A [`BackendTLSPolicy`](https://gateway-api.sigs.k8s.io/api-types/backendtlspolicy/)** for the gateway’s hop, once the auth server serves HTTPS as above: it has the gateway connect to a Service port over TLS and check the certificate against your authority. Point the auth server’s HTTPRoute at port 9443, and name every host name the gateway uses in the certificate. If the admin console then can’t reach the auth server, see [Unable to load the configuration from the auth server](https://goiabada.dev/troubleshooting/unable-to-load-the-configuration-from-the-auth-server/). ## Check the database’s certificate The wizard asks how the auth server should protect its connection to the database, and writes your answer into the `goiabada-authserver-config` ConfigMap as `GOIABADA_DB_TLS_MODE`. [Where the database sits](https://goiabada.dev/deploy/database/#where-the-database-sits) explains each mode. For `verify-ca` and `verify-full`, the wizard also asks for a CA file: the authorities your database’s certificate chains to. Leave it empty to trust the system’s authorities, and the manifest gains nothing more. With a CA file, the manifest carries its certificates. They’re not secret: - **A ConfigMap, `goiabada-db-ca`,** holds them as PEM under the key `ca.pem`. If the file you named also held a private key, the wizard leaves it out. - **A read-only volume** mounts that ConfigMap at `/etc/goiabada/db-ca` in the auth server’s pods. - **`GOIABADA_DB_TLS_CA_FILE`** in `goiabada-authserver-config` names the mounted file, `/etc/goiabada/db-ca/ca.pem`. The auth server then trusts those authorities in place of the system’s. If a pod can’t verify the database’s certificate, it stops at start and crash-loops: see [Database TLS connection fails](https://goiabada.dev/troubleshooting/database-tls-connection-fails/). ### Change the authorities The auth server reads the file only when it starts, so change it like this. The commands use the wizard’s default namespace, `goiabada`; replace it with the namespace you chose: 1. Edit the `goiabada-db-ca` ConfigMap: ```bash kubectl edit configmap goiabada-db-ca -n goiabada ``` Or run the wizard again, and apply only the new `goiabada-k8s.yaml`. 2. Restart the auth server: ```bash kubectl rollout restart -n goiabada deployment/goiabada-authserver ``` Moving the database to a certificate from another authority? Put both authorities in the file and restart before the database switches. Remove the old one once it has. ## Next steps [Secrets](https://goiabada.dev/deploy/kubernetes/secrets/): Who can read the Secrets, and other ways to create them. [Client IP and proxy trust](https://goiabada.dev/deploy/client-ip-and-proxy-trust/#kubernetes): How Goiabada reads the address Envoy forwards. [Production checklist](https://goiabada.dev/deploy/production-checklist/): What to check before going live. # Secrets Source: https://goiabada.dev/deploy/kubernetes/secrets/ This page helps you create the Secrets Goiabada’s Kubernetes manifest reads, and keep them readable only by what needs them. What each secret protects, and backing up the AES key, are the same on every platform: see [Secrets](https://goiabada.dev/deploy/secrets/). ## Create the Secrets The setup wizard writes them into `goiabada-secrets.yaml`, beside the manifest. That suits a first deployment on a cluster you control: 1. **Back up the AES key** from the file, before anything else: see [Back up the AES key](https://goiabada.dev/deploy/secrets/#back-up-the-aes-key). 2. **Apply the Secrets, then the manifest,** so no pod starts with an older copy of the Secrets. Both files create the namespace: ```bash kubectl apply -f goiabada-secrets.yaml -f goiabada-k8s.yaml ``` Apply this file only for a new deployment. Every run of the wizard writes newly generated secrets into it, and applied over a deployment that runs, they replace the keys its database is under. Nothing shows at first: a pod reads its Secrets only when it starts, so a changed Secret reaches no running pod. At the next restart, the auth server refuses to start with the new AES key, and the admin console with the new client secret, and the rollout waits with the earlier pods still serving. For a deployment that already runs, apply only `goiabada-k8s.yaml`, and change a Secret as [Rotate secrets](https://goiabada.dev/deploy/rotate-secrets/) does, restart included. 3. **Keep the file out of version control,** or delete it once the Secrets are in the cluster. The wizard warns when it writes into a git working tree, and gives the line to add to its `.gitignore`. Applying the file again after a [rotation](https://goiabada.dev/deploy/rotate-secrets/) puts the old keys back. For production, keep the manifest and create the Secrets by [another way](https://goiabada.dev/deploy/kubernetes/secrets/#create-the-secrets-another-way) instead. The manifest needs only the [Secrets it reads](https://goiabada.dev/deploy/kubernetes/secrets/#the-secrets-the-manifest-reads) to exist, however they got there. ## Create the Secrets another way ### From your terminal `kubectl create secret` writes nothing to disk, and no value reaches the process list or your shell history: each one goes through a process substitution, a key straight from `openssl` and a password from a variable read without echo. In bash, once the manifest has created the namespace: ```bash read -rsp 'Database password: ' DB_PASSWORD; echo read -rsp 'Admin password: ' ADMIN_PASSWORD; echo kubectl create secret generic goiabada-secrets -n goiabada \ --from-file=db-password=<(printf %s "$DB_PASSWORD") \ --from-file=admin-password=<(printf %s "$ADMIN_PASSWORD") \ --from-file=auth-session-auth-key=<(openssl rand -hex 64 | tr -d '\n') \ --from-file=auth-session-enc-key=<(openssl rand -hex 32 | tr -d '\n') \ --from-file=admin-session-auth-key=<(openssl rand -hex 64 | tr -d '\n') \ --from-file=admin-session-enc-key=<(openssl rand -hex 32 | tr -d '\n') \ --from-file=oauth-client-secret=<(openssl rand -hex 32 | tr -d '\n') kubectl create secret generic goiabada-encryption-key -n goiabada \ --from-file=aes-encryption-key=<(openssl rand -hex 32 | tr -d '\n') unset DB_PASSWORD ADMIN_PASSWORD ``` The values then live in the cluster alone: there’s no file to leak or commit, and none to restore from. Losing the namespace or the cluster loses them, so [back up the AES key](https://goiabada.dev/deploy/secrets/#back-up-the-aes-key) and keep the passwords wherever you keep others. Nothing is declarative, so a GitOps tool can’t recreate the Secrets, and rebuilding the cluster means creating them again with the same values: new session keys sign everybody out, a new OAuth client secret no longer matches the one in the database, and a new AES key can’t read it. ### From a secret manager The [External Secrets Operator](https://external-secrets.io/) syncs Secrets from a secret manager, such as HashiCorp Vault or your cloud’s. You keep each value in the manager, and an `ExternalSecret` per Secret has the operator create it under the names and keys below, then refresh it: ```yaml apiVersion: external-secrets.io/v1 kind: ExternalSecret metadata: name: goiabada-encryption-key namespace: goiabada spec: refreshInterval: 1h secretStoreRef: kind: ClusterSecretStore name: my-secret-store # yours target: name: goiabada-encryption-key data: - secretKey: aes-encryption-key remoteRef: key: goiabada/aes-encryption-key # the entry in your secret manager ``` The second, `goiabada-secrets`, lists its seven keys the same way. The manager becomes the source of truth, with its own access control, audit log and backups; a manager kept apart from the database’s backups is a sound home for the AES key’s backup. The cost is an operator and its resources to run, a credential that lets it read the manager (workload identity where your cloud offers it), and a Kubernetes Secret that still exists, readable as [below](https://goiabada.dev/deploy/kubernetes/secrets/#who-can-read-the-aes-key). A value changed in the manager reaches the Secret at the next refresh, but a pod reads it only when it starts, so a pod restarted for any reason picks it up alone. The operator also writes the Secret back to what the manager holds, the keys its `data` lists and no other, so a `kubectl patch` of the Secret lasts until the next refresh. Change a session key or the AES key only as [Rotate secrets](https://goiabada.dev/deploy/rotate-secrets/) says, with the changes in the manager as [Rotating Secrets something else owns](https://goiabada.dev/deploy/kubernetes/secrets/#rotating-secrets-something-else-owns) describes. > **Danger** > > **Never** replace the AES key in the manager on its own. An auth server started with no previous key re-encrypts nothing, and can’t decrypt what the database holds. ### Sealed in your repository [SealedSecrets](https://github.com/bitnami-labs/sealed-secrets) encrypts a Secret you can commit. `kubeseal` encrypts it with the public key of the controller running in your cluster, and only that controller can decrypt it, which it does by creating the Secret: ```bash kubectl create secret generic goiabada-encryption-key -n goiabada --dry-run=client -o yaml \ --from-file=aes-encryption-key=<(openssl rand -hex 32 | tr -d '\n') \ | kubeseal --format yaml > goiabada-encryption-key.sealed.yaml ``` Seal `goiabada-secrets` the same way, with the keys of the first `kubectl create secret` above, and commit both files beside the manifest. A GitOps tool can then apply everything, and the repository holds no readable secret. The controller’s private key now decrypts all of them: back it up as the SealedSecrets documentation describes, since without it the sealed files are useless. That backup doesn’t replace the AES key’s own, which must be readable without the cluster. A sealed Secret is bound to its name and namespace, so a renamed namespace means sealing again, and once unsealed it’s an ordinary Secret, readable as [below](https://goiabada.dev/deploy/kubernetes/secrets/#who-can-read-the-aes-key). The controller writes the Secret from the sealed file, so a `kubectl patch` of the Secret doesn’t last, and a value changes by sealing it. Change a session key or the AES key only as [Rotate secrets](https://goiabada.dev/deploy/rotate-secrets/) says, sealing what each step says as [Rotating Secrets something else owns](https://goiabada.dev/deploy/kubernetes/secrets/#rotating-secrets-something-else-owns) describes. > **Danger** > > **Never** seal a new `aes-encryption-key` on its own, outside a rotation. Once it’s applied, the cluster no longer holds the key the database is under. ## What the generated Secrets are `goiabada-secrets.yaml` holds two Secrets: `goiabada-encryption-key`, holding the AES key alone, and `goiabada-secrets`, holding every other secret. The AES key has a Secret of its own so that a role can be allowed to read the others without it. What they aren’t: - **Encrypted.** A Secret’s `data` is base64, which `base64 -d` reverses with no key. On disk, the file’s mode, `0600`, is its only protection: anyone who reads the file, or a backup or a repository holding it, reads every secret. - **Encrypted in the cluster, unless you make them so.** The API server keeps Secrets in etcd, in the clear unless the cluster configures [encryption at rest](https://kubernetes.io/docs/tasks/administer-cluster/encrypt-data/); many managed clusters do, so check yours. Anyone allowed to `get`, `list` or `watch` Secrets in the namespace reads them, and so does anyone who can create a pod there or `kubectl exec` into Goiabada’s, since the values are in the containers’ environment. - **Rotated.** Nothing changes them after the wizard writes them; [Rotate secrets](https://goiabada.dev/deploy/rotate-secrets/) says how. - **Backed up.** Deleting the namespace deletes them, and the AES key needs a backup of its own. ## The Secrets the manifest reads The manifest reads two Secrets from its namespace, by these names and keys. Created by any route, the rows not marked optional are all it needs: | Secret | Key | Variable | Read by | Value | | - | - | - | - | - | | `goiabada-secrets` | `db-password` | `GOIABADA_DB_PASSWORD` | auth server | The database user’s password | | `goiabada-secrets` | `admin-password` | `GOIABADA_ADMIN_PASSWORD` | auth server | The first administrator’s password, read only by the first start, which creates the account. Change it later in the admin console, not here | | `goiabada-secrets` | `auth-session-auth-key` | `GOIABADA_AUTHSERVER_SESSION_AUTHENTICATION_KEY` | auth server | 64 random bytes as 128 hex characters, `openssl rand -hex 64` | | `goiabada-secrets` | `auth-session-enc-key` | `GOIABADA_AUTHSERVER_SESSION_ENCRYPTION_KEY` | auth server | 32 random bytes as 64 hex characters, `openssl rand -hex 32` | | `goiabada-secrets` | `admin-session-auth-key` | `GOIABADA_ADMINCONSOLE_SESSION_AUTHENTICATION_KEY` | admin console | 64 random bytes as 128 hex characters, `openssl rand -hex 64` | | `goiabada-secrets` | `admin-session-enc-key` | `GOIABADA_ADMINCONSOLE_SESSION_ENCRYPTION_KEY` | admin console | 32 random bytes as 64 hex characters, `openssl rand -hex 32` | | `goiabada-secrets` | `oauth-client-secret` | `GOIABADA_ADMINCONSOLE_OAUTH_CLIENT_SECRET` | both | The admin console’s OAuth client secret. The auth server reads it only at the first start, to create the admin console’s client with it; the admin console authenticates with it from then on, so it must match the client’s secret in the database, or the admin console refuses to start | | `goiabada-encryption-key` | `aes-encryption-key` | `GOIABADA_AES_ENCRYPTION_KEY` | auth server | 32 random bytes as 64 hex characters, `openssl rand -hex 32`. It encrypts the secrets the database holds | | `goiabada-secrets` | `auth-session-auth-key-previous` | `GOIABADA_AUTHSERVER_SESSION_AUTHENTICATION_KEY_PREVIOUS` | auth server | Optional, held only while the session keys rotate: with the next key, the pair the auth server opens sessions with beside its current one | | `goiabada-secrets` | `auth-session-enc-key-previous` | `GOIABADA_AUTHSERVER_SESSION_ENCRYPTION_KEY_PREVIOUS` | auth server | Optional, the other half of the auth server’s previous pair | | `goiabada-secrets` | `admin-session-auth-key-previous` | `GOIABADA_ADMINCONSOLE_SESSION_AUTHENTICATION_KEY_PREVIOUS` | admin console | Optional, held only while the session keys rotate: with the next key, the admin console’s previous pair | | `goiabada-secrets` | `admin-session-enc-key-previous` | `GOIABADA_ADMINCONSOLE_SESSION_ENCRYPTION_KEY_PREVIOUS` | admin console | Optional, the other half of the admin console’s previous pair | | `goiabada-encryption-key` | `aes-encryption-key-previous` | `GOIABADA_AES_ENCRYPTION_KEY_PREVIOUS` | auth server | Optional, held only while the AES key rotates: the key the database is under, which the auth server re-encrypts from when it starts | Each Secret must exist before the pods that read it start. A pod that starts first waits in `CreateContainerConfigError`, and starts once they exist. The manifest’s references to the five `-previous` keys are optional: a pod starts without them, and each server reads a missing one as no rotation in progress. Create them only as [Rotate secrets](https://goiabada.dev/deploy/rotate-secrets/) says. A previous session pair is read whole or not at all, since a server refuses to start with one half of it, so add and remove its two keys together. ## Who can read the AES key RBAC grants and never denies, so the AES key’s Secret of its own protects it only from roles that name what they may read. This Role reads `goiabada-secrets` and not the key: ```yaml apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: goiabada-secrets-reader namespace: goiabada rules: - apiGroups: [""] resources: ["secrets"] resourceNames: ["goiabada-secrets"] verbs: ["get"] ``` A role that may `list` or `watch` Secrets in the namespace, or `get` them with no `resourceNames`, reads the key too, since a list returns every Secret with its data; so does one that may create pods in the namespace, or exec into the auth server’s. Goiabada’s own pods need no role at all: the kubelet reads the Secrets to set their environment, and the pods mount no service account token. ## Rotating Secrets something else owns When the External Secrets Operator or the SealedSecrets controller owns a Secret, its source, the secret manager or the sealed file, is what the Secret holds, and a step’s change goes there. Three rules apply to every step of [Rotate secrets](https://goiabada.dev/deploy/rotate-secrets/): - **Put the whole state the step ends in into the source,** every key the step says the Secret holds, and only those. Both write the Secret from their source, its keys and no other, so a key left out of the source is removed from the Secret, a `-previous` key included. - **Wait until the Secret holds it before the step’s rollout or scale,** since a pod reads whatever the Secret holds when it starts. Compare a digest of each key the step changed with the same digest of the value in your source, which shows the value without printing it: ```bash digest() { kubectl get secret "$1" -n goiabada -o "jsonpath={.data.$2}" | base64 -d | sha256sum | cut -c1-16; } digest goiabada-encryption-key aes-encryption-key-previous ``` - **Leave the source holding what the Secret holds when the rotation is done,** so that a refresh, a reapply, a deleted Secret or a rebuilt cluster brings back the rotated keys and not the old ones. **With the External Secrets Operator,** never change the value of an entry the `ExternalSecret` reads. The operator reads each line of its `data` from the manager on its own, not as one snapshot, so a refresh that runs while you change several entries can write the Secret with some changed and the rest not: halfway through the session keys’ swap, the new authentication key beside the old encryption key, a current pair that opens nothing while the previous pair is already the new one. A pod a crash or an eviction restarts then can’t open any session sealed under the old pair. Instead, each new value goes into an entry of its own, named for the rotation it belongs to, such as `goiabada/auth-session-auth-key-2026-10`, and a step changes which entries the `ExternalSecret` maps, in one edit of it, applied once. A refresh reads every line under one version of the `ExternalSecret`, and the entries that version names don’t change while it reads them, so it writes the state before the edit or the state after it, each whole; a refresh already running when you apply the edit writes the state before it, and the next one the state after. Ask the operator to refresh at once rather than at its `refreshInterval`, then wait for the digests: ```bash kubectl annotate externalsecret goiabada-encryption-key -n goiabada force-sync="$(date +%s)" --overwrite ``` - **For the session keys,** adding the new pairs as the previous pairs creates four entries holding them, `openssl rand -hex 64` for each authentication key and `openssl rand -hex 32` for each encryption key, then adds the four lines mapping the `-previous` keys to them. The swap is one edit that maps the four current keys, `auth-session-auth-key` and the rest, to the new entries, and the four `-previous` keys to the entries the current keys mapped until then; no entry’s value changes. Step 3 removes the four `-previous` lines from `data`, restarts the pods once the Secret no longer holds those keys, then deletes the old pairs’ entries. - **For the AES key,** step 3 creates an entry holding a new key, `openssl rand -hex 32`, such as `goiabada/aes-encryption-key-2026-10`, then, in one edit, maps `aes-encryption-key` to it and adds a line mapping `aes-encryption-key-previous` to the entry `aes-encryption-key` mapped until then, the one holding the key the database is under. Once the Secret holds both, check that the digest of `aes-encryption-key-previous` is that of the key you backed up in step 1. Step 4 reads the new key out of the cluster. Step 6 removes the line from `data`, then restarts the auth server. The old key’s entry can stay as that key’s backup, for as long as you keep a database backup taken under it. Removing a line from `data` before its entry, and deleting the entry only once the Secret no longer holds that key, keeps the operator from failing on an entry it can no longer read, which would leave the Secret as it was. **With SealedSecrets,** each step’s state is sealed into the file and applied, by you or by your GitOps tool. `kubeseal --merge-into` adds or replaces keys in an existing sealed file, each key sealed on its own, and deleting a key’s line from the file’s `spec.encryptedData` removes that key. The values come from the Secret the controller unsealed, through process substitutions, so none reaches a file, an argument or the screen: ```bash key() { kubectl get secret goiabada-secrets -n goiabada -o "jsonpath={.data.$1}" | base64 -d; } seal() { kubectl create secret generic goiabada-secrets -n goiabada --dry-run=client -o json "$@" \ | kubeseal --format yaml --merge-into goiabada-secrets.sealed.yaml; } # Session keys: the new pairs as the previous pairs. seal --from-file=auth-session-auth-key-previous=<(openssl rand -hex 64 | tr -d '\n') \ --from-file=auth-session-enc-key-previous=<(openssl rand -hex 32 | tr -d '\n') \ --from-file=admin-session-auth-key-previous=<(openssl rand -hex 64 | tr -d '\n') \ --from-file=admin-session-enc-key-previous=<(openssl rand -hex 32 | tr -d '\n') # Session keys: the pairs swapped, once the first file has been applied and unsealed. seal --from-file=auth-session-auth-key=<(key auth-session-auth-key-previous) \ --from-file=auth-session-enc-key=<(key auth-session-enc-key-previous) \ --from-file=admin-session-auth-key=<(key admin-session-auth-key-previous) \ --from-file=admin-session-enc-key=<(key admin-session-enc-key-previous) \ --from-file=auth-session-auth-key-previous=<(key auth-session-auth-key) \ --from-file=auth-session-enc-key-previous=<(key auth-session-enc-key) \ --from-file=admin-session-auth-key-previous=<(key admin-session-auth-key) \ --from-file=admin-session-enc-key-previous=<(key admin-session-enc-key) ``` Step 3 deletes the four `-previous` lines from `spec.encryptedData`, and restarts the pods once the controller has removed those keys from the Secret. For the AES key, step 3 seals the key the cluster holds now as the previous key, beside a new one, in one file: ```bash kubectl create secret generic goiabada-encryption-key -n goiabada --dry-run=client -o json \ --from-file=aes-encryption-key-previous=<(kubectl get secret goiabada-encryption-key -n goiabada \ -o jsonpath='{.data.aes-encryption-key}' | base64 -d) \ --from-file=aes-encryption-key=<(openssl rand -hex 32 | tr -d '\n') \ | kubeseal --format yaml --merge-into goiabada-encryption-key.sealed.yaml ``` Before you apply it, check that the digest of `aes-encryption-key` in the cluster is that of the key you backed up in step 1: that’s the key the previous entry has to hold. Step 6 deletes the `aes-encryption-key-previous` line, then restarts the auth server. Commit each file as you apply it, so the repository’s file is always the one the controller last unsealed. **When a GitOps tool applies the manifest,** the only Deployment field a rotation changes is the auth server’s `replicas`, during the AES key’s: the tool puts back the manifest’s count, which would start auth server pods while step 2 keeps them stopped. Suspend its automated sync for the AES key’s rotation (in Argo CD, turn off the Application’s automated sync; in Flux, `flux suspend kustomization `) and resume it once step 5 has scaled back. The session keys’ rotation changes nothing the manifest sets: the `-previous` references are already in it, and `kubectl rollout restart` adds only an annotation to the pod template. ## Next steps [Rotate secrets](https://goiabada.dev/deploy/rotate-secrets/): Change each secret without signing everybody out or losing the database. [Back up the AES key](https://goiabada.dev/deploy/secrets/#back-up-the-aes-key): Without it, a restored database decrypts nothing. [Security](https://goiabada.dev/deploy/kubernetes/security/): Who can reach Goiabada's pods, and what they may do. # Probes and shutdown Source: https://goiabada.dev/deploy/kubernetes/probes-and-shutdown/ This page helps you understand how Kubernetes checks, starts and stops Goiabada’s pods, and when to change the manifest’s numbers. The manifest the setup wizard generates sets every probe, the stop pause and the grace period already. You change them in two cases: an upgrade whose migrations take longer than five minutes, and a different stop pause. ## Give a long migration more time 1. **Find out how long the migration takes.** The release notes announce a long one, and running the new release against a copy of your database shows it: the `database migrated` record carries its `duration`. 2. **Raise the auth server’s startup budget** until `failureThreshold × periodSeconds` covers it. In `goiabada-k8s.yaml`, on the auth server’s container, the default allows 60 × 5 = 300 seconds: ```yaml startupProbe: httpGet: path: /health port: 9090 periodSeconds: 5 timeoutSeconds: 1 failureThreshold: 120 # 10 minutes ``` Leave liveness and readiness as they are. 3. **Apply the manifest,** then upgrade as [Upgrade Goiabada](https://goiabada.dev/deploy/upgrade-goiabada/) describes. ## Health endpoints Both servers answer a health endpoint, and every probe in the manifest points at it: - Auth server: `http://goiabada-authserver:9090/health` - Admin console: `http://goiabada-adminconsole:9091/health` `/health` answers `200` with the body `healthy` whenever the process is up and listening. That’s all it tells you. It doesn’t read the database, the settings or the session, and the admin console’s doesn’t call the auth server, so it keeps answering `200` while the database or the auth server is down. Watch the servers’ error records, your database’s monitoring and the [metrics](https://goiabada.dev/deploy/monitoring/) for those. That’s on purpose. Every auth server pod shares one database, and every admin console pod one auth server, so a probe that checked either would fail on every pod at the same moment. A liveness probe would restart them all at once, and a readiness probe would take every pod out of the Service together, so clients would get the gateway’s “no healthy upstream” in place of Goiabada’s own error, and recovery would wait a probe period longer. ## First start and upgrades The auth server does its database work before it listens: it opens the database, runs every outstanding migration, and on a first start seeds the empty database. Until that’s done `/health` doesn’t answer, so a pod that can’t reach the database never answers it at all. Each container has a startup probe on `/health`, every 5 seconds with 60 failures allowed: up to 5 minutes to start. Liveness and readiness wait for the startup probe’s first success, so neither counts a slow start against the pod, and once it succeeds they take over at their own periods. The admin console gets the same probe, and starts in seconds. With several replicas, the first pod to start takes the migration lock and migrates; the others wait for the lock, then find the schema current and start. A first start on an empty database works the same way, and then the pods race to seed it: one seed commits, and every other pod finds the database seeded and starts, logging `another instance seeded the database while this one was seeding it`. Every pod’s wait counts against its own startup budget, so the budget must exceed the longest migration an upgrade runs. Otherwise Kubernetes restarts the container before the migration finishes, which is why a long one needs [more time](https://goiabada.dev/deploy/kubernetes/probes-and-shutdown/#give-a-long-migration-more-time). A pod stopped while it starts, because it was deleted or its startup budget ran out, stops cleanly: a wait for the database or the migration lock ends at once, a migration file already running runs to its end and the next doesn’t start, and the auth server exits 0 with the schema clean at the version it reached, which the next start carries on from. [A start that’s stopped](https://goiabada.dev/deploy/upgrade-goiabada/#a-start-thats-stopped) has the details and the records. The grace period below bounds that too: a single migration file running longer than 65 seconds is ended by SIGKILL mid-file, leaving the schema dirty. So it’s the startup budget, not the grace period, that has to cover the longest migration. ## Stopping and the grace period When a pod is deleted, by a rollout, a scale-down or a node drain, Kubernetes removes it from the Service and starts stopping its containers at the same moment. Both servers close their listening socket the moment the signal arrives, so a connection the gateway opens before it has learned the pod is gone would be refused, and its client answered 503. The manifest gives both containers a five-second pause before they’re signalled, through Kubernetes’ native `preStop` sleep action, which keeps the pod serving while the gateway catches up. The sleep action is on by default from Kubernetes 1.30, and generally available from 1.34. Once signalled, the auth server stops in three steps, each bounded: 1. up to 15 seconds for requests in flight to finish, 2. up to 15 seconds for the work handlers hand off after answering, such as a forgot-password request’s code, audit record and email, 3. up to 20 seconds for the background cleanup worker. The admin console only drains its requests, within 15 seconds. Both pods get the same grace period, `terminationGracePeriodSeconds: 65`, from this sum: ```plaintext terminationGracePeriodSeconds = preStop pause + auth server's stop + headroom 65 = 5 + (15 + 15 + 20) + 10 ``` The grace period is a ceiling, not a wait: a container that exits sooner isn’t held, so the admin console loses nothing by sharing the auth server’s value. Kubernetes counts the `preStop` pause against it, so if you change the pause, change the grace period by the same amount. > **Caution** > > **Never** set the grace period lower. Kubernetes’ default is 30 seconds, and whatever is left of the stop when the grace period runs out is ended by SIGKILL: the cleanup worker, the last step, is cut short first, then the handed-off work, then requests still in flight. ## Next steps [Upgrade Goiabada](https://goiabada.dev/deploy/upgrade-goiabada/): Move to a new release, and back again. [Monitoring](https://goiabada.dev/deploy/monitoring/): Watch what /health doesn't tell you. [High availability](https://goiabada.dev/deploy/kubernetes/high-availability/): Run more than one pod, and keep serving through drains. # Native binaries Source: https://goiabada.dev/deploy/native-binaries/ This page helps you run Goiabada without Docker: the two binaries from a release, one configuration file, and systemd to keep them running. You need a Linux server, a database (a MySQL, PostgreSQL or SQL Server you run, or SQLite for a single server), two hostnames under one domain, such as `auth.example.com` and `admin.example.com`, and the [setup wizard](https://goiabada.dev/deploy/setup-wizard/). Both hostnames must share a registrable domain: see [What every method needs](https://goiabada.dev/deploy/choose-a-method/#what-every-method-needs). A reverse proxy on the same machine, such as Nginx, serves HTTPS in front of them, unless the servers [serve HTTPS themselves](https://goiabada.dev/deploy/native-binaries/#serve-https-without-a-proxy). ## Set it up 1. **Download the release** for your platform from the [releases page](https://github.com/leodip/goiabada/releases). Each ZIP holds both binaries: | Platform | ZIP | | - | - | | Linux, x86-64 | `goiabada--linux-amd64.zip` | | Linux, ARM64 | `goiabada--linux-arm64.zip` | | macOS, Intel | `goiabada--darwin-amd64.zip` | | macOS, Apple silicon | `goiabada--darwin-arm64.zip` | | Windows | `goiabada--windows-amd64.zip` | ```bash unzip goiabada--linux-amd64.zip ``` 2. **Run the setup wizard, and choose Native binaries.** Give it the two public URLs, the database, and whether a reverse proxy on this machine forwards to Goiabada (yes by default). Or answer with flags: ```bash ./goiabada-setup-linux-amd64 --type=native --db=postgres \ --auth-url=https://auth.example.com --admin-url=https://admin.example.com \ --db-host=127.0.0.1 ``` It writes one file, `goiabada.env`, which both servers read. It holds every secret, so **never** commit it, and [back up the AES key](https://goiabada.dev/deploy/secrets/#back-up-the-aes-key) in it before anything else. 3. **Install the binaries and the file,** under a user of their own: ```bash sudo useradd --system --shell /usr/sbin/nologin goiabada sudo install -d -o goiabada -g goiabada -m 0750 /var/lib/goiabada sudo install -d -m 0755 /opt/goiabada /etc/goiabada sudo install -m 0755 goiabada-authserver goiabada-adminconsole /opt/goiabada/ sudo install -o goiabada -g goiabada -m 0600 goiabada.env /etc/goiabada/goiabada.env ``` 4. **Add a systemd unit for each server.** `/etc/systemd/system/goiabada-authserver.service`: ```ini [Unit] Description=Goiabada auth server After=network-online.target Wants=network-online.target [Service] User=goiabada Group=goiabada WorkingDirectory=/var/lib/goiabada EnvironmentFile=/etc/goiabada/goiabada.env ExecStart=/opt/goiabada/goiabada-authserver Restart=on-failure RestartSec=5s [Install] WantedBy=multi-user.target ``` `/etc/systemd/system/goiabada-adminconsole.service` is the same, with its own description, `ExecStart=/opt/goiabada/goiabada-adminconsole`, and `After=goiabada-authserver.service` added to its `[Unit]`. 5. **Start both:** ```bash sudo systemctl daemon-reload sudo systemctl enable --now goiabada-authserver goiabada-adminconsole until curl -sf http://127.0.0.1:9090/health; do sleep 2; done; echo # healthy until curl -sf http://127.0.0.1:9091/health; do sleep 2; done; echo # healthy ``` The first start migrates and seeds the database before the auth server listens, which takes a few seconds. The admin console listens once the auth server answers it at `GOIABADA_AUTHSERVER_INTERNALBASEURL`; until then, `sudo journalctl -u goiabada-adminconsole` shows `waiting for the auth server to issue the admin console's token`. Without a reverse proxy, check only the auth server here: the admin console reaches it at its public URL, so it listens once you [serve HTTPS](https://goiabada.dev/deploy/native-binaries/#serve-https-without-a-proxy). `sudo journalctl -u goiabada-authserver -f` follows the auth server’s log. 6. **Put the reverse proxy in front.** Without one, skip this step and [serve HTTPS directly](https://goiabada.dev/deploy/native-binaries/#serve-https-without-a-proxy) instead; your URLs then name ports 9443 and 9444. Proxy `auth.example.com` to `http://127.0.0.1:9090` and `admin.example.com` to `http://127.0.0.1:9091`, with the DNS records, Nginx configuration and certificates of [Reverse proxy](https://goiabada.dev/deploy/reverse-proxy/#set-it-up), steps 1 and 3 to 6. Skip its step 2, which starts the Docker Compose files. 7. **Sign in** at `https://admin.example.com`, as [First sign-in](https://goiabada.dev/get-started/first-sign-in/) describes. To try the binaries before installing them, load the file into a shell and start each one, as the wizard’s last message shows: ```bash set -a && . ./goiabada.env && set +a && ./goiabada-authserver ``` ## What the wizard’s answers set - **With a reverse proxy on this machine,** both servers listen on `127.0.0.1` alone, so nothing past the proxy reaches the plain HTTP they serve, and they trust the forwarded headers of a connection from `127.0.0.1` alone: see [Client IP and proxy trust](https://goiabada.dev/deploy/client-ip-and-proxy-trust/#native-binaries). - **With none** (`--local-proxy=false`), both servers listen on every interface and trust no forwarded header. They serve plain HTTP until you [serve HTTPS directly](https://goiabada.dev/deploy/native-binaries/#serve-https-without-a-proxy). - **The rate limiter** is on unless you answered no (`--rate-limiter=false`). See [Rate limits](https://goiabada.dev/reference/environment-variables/#rate-limits). - **With SQLite,** the database is `goiabada.db` in the auth server’s working directory, `/var/lib/goiabada` with the unit above. SQLite suits one auth server: see [Database](https://goiabada.dev/deploy/database/). ## The hop to the auth server The admin console calls the auth server at `GOIABADA_AUTHSERVER_INTERNALBASEURL`, and listens only once the auth server answers it there. - **With a reverse proxy on this machine,** the wizard sets it to `http://127.0.0.1:9090`, the auth server’s own listener. That hop is plain HTTP, but it never leaves the machine, and it works before the proxy, the DNS records and the certificates do. If you change the auth server’s listen host or port, change this URL too. - **Without one,** it’s the auth server’s public URL, over HTTPS, so the admin console’s client secret and administrators’ tokens are encrypted on the way, and the admin console listens once the auth server serves HTTPS there. ## Serve HTTPS without a proxy Each server serves HTTPS itself once it has a certificate and a key. Set both in `goiabada.env`, and set each HTTP listen host empty to stop serving plain HTTP: ```bash GOIABADA_AUTHSERVER_CERTFILE="/etc/goiabada/auth.example.com.pem" GOIABADA_AUTHSERVER_KEYFILE="/etc/goiabada/auth.example.com-key.pem" GOIABADA_AUTHSERVER_LISTEN_HOST_HTTP="" GOIABADA_ADMINCONSOLE_CERTFILE="/etc/goiabada/admin.example.com.pem" GOIABADA_ADMINCONSOLE_KEYFILE="/etc/goiabada/admin.example.com-key.pem" GOIABADA_ADMINCONSOLE_LISTEN_HOST_HTTP="" ``` The auth server then listens on port 9443 and the admin console on 9444, on every interface; `GOIABADA_AUTHSERVER_LISTEN_PORT_HTTPS` and `GOIABADA_ADMINCONSOLE_LISTEN_PORT_HTTPS` change them. Your public URLs must name those ports, or something must forward 443 to them. The `goiabada` user must be able to read both keys, and no one else: ```bash sudo chown goiabada:goiabada /etc/goiabada/*-key.pem sudo chmod 600 /etc/goiabada/*-key.pem ``` Renewing a certificate takes a restart, since each server reads its files when it starts. > **macOS and Windows** > > The releases include macOS and Windows binaries, which read the same variables. Load `goiabada.env` into their environment and run them under the service manager you use there. ## Next steps [Reverse proxy](https://goiabada.dev/deploy/reverse-proxy/): Nginx and Let's Encrypt in front of Goiabada. [Secrets](https://goiabada.dev/deploy/secrets/): What goiabada.env protects, and backing up the AES key. [Upgrade Goiabada](https://goiabada.dev/deploy/upgrade-goiabada/): Install a new release, and roll back. # Client IP and proxy trust Source: https://goiabada.dev/deploy/client-ip-and-proxy-trust/ This page helps you make Goiabada see each client’s own IP address when a proxy sits in front of it. Goiabada counts [rate limits](https://goiabada.dev/reference/environment-variables/#rate-limits) by client IP address, and records the address in the [audit log](https://goiabada.dev/concepts/audit-log/), in each user’s sessions and in the request log. Behind a proxy, the connection comes from the proxy, so Goiabada has to read the client’s address from the `X-Forwarded-For` header the proxy adds. Get it wrong and either every user shares one rate limit, or a caller picks the address it’s counted under. ## Set it up 1. **Turn on forwarded headers** on both servers, behind a proxy and only behind one: ```bash GOIABADA_AUTHSERVER_TRUST_PROXY_HEADERS=true GOIABADA_ADMINCONSOLE_TRUST_PROXY_HEADERS=true ``` 2. **Leave the trusted proxy lists empty when one proxy connects to Goiabada.** That’s every setup on this page but [a second proxy hop](https://goiabada.dev/deploy/client-ip-and-proxy-trust/#a-second-proxy-hop). For Docker Compose and Kubernetes the setup wizard writes them empty, side by side with the switch: ```bash GOIABADA_AUTHSERVER_TRUSTED_PROXIES= GOIABADA_ADMINCONSOLE_TRUSTED_PROXIES= ``` For [native binaries](https://goiabada.dev/deploy/client-ip-and-proxy-trust/#native-binaries) it lists `127.0.0.1`, the address Nginx connects from on the same host, which resolves the same client address. 3. **Make the proxy the only way in.** Nothing but the proxy may reach ports 9090 and 9091. The section for [your setup](https://goiabada.dev/deploy/client-ip-and-proxy-trust/#your-setup) says how. 4. **Check it.** Open the sign-in page from your own browser, then read the auth server’s request log. The `ip` of your request should be your own public address, not your proxy’s or Cloudflare’s. ## Your setup ### Reverse proxy The [Reverse proxy](https://goiabada.dev/deploy/reverse-proxy/) setup has Nginx on the host in front of the Docker Compose file. Its `proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;` line appends the address Nginx received the connection from, so the rightmost entry is the client’s. The wizard’s Compose file trusts one hop with empty lists, which is right here. The Compose file publishes 9090 and 9091 on `127.0.0.1` alone, so nothing outside the host reaches them. Keep it that way. > **Never list 127.0.0.1 under Docker Compose** > > Nginx connects to `127.0.0.1:9090`, but a Compose service sees the connection come from the Compose network’s gateway, an address such as `172.21.0.1`. Goiabada reads forwarded headers only from an address in its list, so a list naming `127.0.0.1` makes it ignore them, and every user shares one rate limit. Leave the list empty. ### Cloudflare Tunnel With a [Cloudflare Tunnel](https://goiabada.dev/deploy/cloudflare-tunnel/), Cloudflare appends the address each request came from to `X-Forwarded-For`, and `cloudflared` on the host passes it on. One hop with empty lists is right, as for a reverse proxy, and so is the caution above about `127.0.0.1`. Nothing is published to the internet: `cloudflared` connects out to Cloudflare. ### Cloudflare + Nginx With [Cloudflare + Nginx](https://goiabada.dev/deploy/cloudflare-nginx/), every request reaches Nginx from a Cloudflare address. Left alone, Nginx appends that address, and Goiabada sees Cloudflare’s edge as the client: not forgeable, but shared by everyone that edge serves, so many users count against one rate limit. Have Nginx resolve Cloudflare instead, so that Goiabada still sees one hop and needs no list. Nginx’s `real_ip` module replaces the address of a connection from one of Cloudflare’s published ranges with the one in the `CF-Connecting-IP` header. Write those ranges into a file Nginx loads in its `http` context: ```bash { echo "# Cloudflare's ranges, from https://www.cloudflare.com/ips/ ($(date -I))" for range in $(curl -fsS https://www.cloudflare.com/ips-v4) $(curl -fsS https://www.cloudflare.com/ips-v6); do echo "set_real_ip_from $range;" done echo "real_ip_header CF-Connecting-IP;" } | sudo tee /etc/nginx/conf.d/cloudflare-real-ip.conf ``` The file looks like this, with every range Cloudflare lists: ```nginx # Cloudflare's ranges, from https://www.cloudflare.com/ips/ (2026-10-04) set_real_ip_from 173.245.48.0/20; set_real_ip_from 103.21.244.0/22; # ... set_real_ip_from 2400:cb00::/32; # ... real_ip_header CF-Connecting-IP; ``` Then test and reload: ```bash sudo nginx -t && sudo nginx -s reload ``` In the `http` context the file applies to every site this Nginx serves, which is what you want when Cloudflare fronts them all. To limit it to Goiabada’s, write it to `/etc/nginx/snippets/cloudflare-real-ip.conf` instead, and add `include /etc/nginx/snippets/cloudflare-real-ip.conf;` to Goiabada’s two `server` blocks that listen on 443. Debian’s and Ubuntu’s Nginx packages load `/etc/nginx/conf.d/*.conf` in the `http` context and include the `real_ip` module: `nginx -V 2>&1 | grep -o with-http_realip_module` shows it. Cloudflare changes its ranges rarely; run the command again when it does, or from a monthly cron job. Now Nginx’s `$remote_addr` is the client’s address, and the `$proxy_add_x_forwarded_for` line of the Nginx configuration passes it on. A request that reaches Nginx from outside Cloudflare’s ranges keeps its own address, so a forged `CF-Connecting-IP` changes nothing. **Never** list Cloudflare’s ranges in `TRUSTED_PROXIES`. Under Docker Compose, Goiabada’s peer is the Compose network’s gateway, not Cloudflare, so the list would make it ignore the forwarded headers altogether. ### Native binaries With the [native binaries](https://goiabada.dev/deploy/native-binaries/) and a reverse proxy on the same machine, Nginx reaches each server over loopback, with no Compose network between them. Here the wizard lists `127.0.0.1`, and has both servers listen on it alone: ```bash GOIABADA_AUTHSERVER_LISTEN_HOST_HTTP="127.0.0.1" GOIABADA_AUTHSERVER_TRUST_PROXY_HEADERS="true" GOIABADA_AUTHSERVER_TRUSTED_PROXIES="127.0.0.1" ``` The admin console’s three variables are the same. For a proxy on another host, set each listen host to an address that host reaches and each list to that host’s address, and keep the ports closed to everything else. With no proxy in front (`--local-proxy=false`), the wizard turns forwarded headers off, and each client is the address it connects from. ### Kubernetes The [Kubernetes](https://goiabada.dev/deploy/kubernetes/overview/) manifests the wizard generates trust one hop, the gateway, with empty lists. Envoy appends the address it received each connection from to `X-Forwarded-For`, so whatever a client sends ends up to the left of Envoy’s entry and is never read. Which address Envoy sees depends on the gateway’s external traffic policy. Under `Cluster`, it’s a node’s address, so the clients a node forwards share one rate limit. Under `Local`, with Envoy on every node, it’s the client’s when the load balancer passes connections through, and the load balancer’s own when it proxies them: [The gateway’s traffic policy](https://goiabada.dev/deploy/kubernetes/gateway-and-certificates/#the-gateways-traffic-policy) has how to tell. The comment above each pair in the manifest says which, following your answer to the wizard. Leave the lists empty. The lists that come to mind for a cluster both go wrong: the private ranges hold the node addresses Envoy records under `Cluster`, so Goiabada would walk past Envoy’s entry and adopt whatever the client wrote to its left, and the pod range does the same on clusters where pods and nodes share a subnet. A pod that calls the `goiabada-authserver` or `goiabada-adminconsole` Service directly, around Envoy, can send any `X-Forwarded-For` it likes. No list stops that, since Envoy’s pods are pods too. A NetworkPolicy admitting only Envoy does: see [Who can reach Goiabada](https://goiabada.dev/deploy/kubernetes/security/#who-can-reach-goiabada). ### A second proxy hop When a second proxy that appends `X-Forwarded-For` sits in front of the one that connects to Goiabada, such as a load balancer in front of Nginx, or a CDN in front of Envoy, the rightmost entry is that second proxy’s address. List every hop you control in `TRUSTED_PROXIES`, **including the one that connects to Goiabada**: ```bash # Nginx on the internal network, and a load balancer in front of it GOIABADA_AUTHSERVER_TRUSTED_PROXIES=10.0.0.0/8,192.168.1.5 GOIABADA_ADMINCONSOLE_TRUSTED_PROXIES=10.0.0.0/8,192.168.1.5 ``` Under Docker Compose, the one that connects to Goiabada is the Compose network’s gateway, so list the network’s range. For Cloudflare in front of Nginx, [resolve Cloudflare in Nginx](https://goiabada.dev/deploy/client-ip-and-proxy-trust/#cloudflare--nginx) instead. ## How the client’s address is resolved Both servers resolve the client’s address the same way, each from its own two variables. The auth server’s rate limiter, audit records, session records and request log, and the admin console’s request log, all read the one address it resolves. ### Forwarded headers off With `TRUST_PROXY_HEADERS` off, the default, the client is the address that connected. That’s right with nothing in front of Goiabada. Behind a proxy, it’s the proxy, so every request looks the same: see [Too many attempts or 429](https://goiabada.dev/troubleshooting/too-many-attempts-or-429/). It fails safe, throttling everyone rather than letting anyone through. ### One hop With `TRUST_PROXY_HEADERS` on and `TRUSTED_PROXIES` empty, the client is the rightmost `X-Forwarded-For` entry, or `X-Real-IP` when there’s no `X-Forwarded-For`. An entry that isn’t an IP address is skipped. That’s sound behind one proxy that sets or appends `X-Forwarded-For`, as Nginx, Envoy and Cloudflare do: the rightmost entry is the address that proxy received the connection from, and anything the client sent sits to its left. What one hop doesn’t survive is a caller that reaches Goiabada **without passing the proxy**. It connects directly, sends any `X-Forwarded-For` it likes, and chooses the address it’s rate limited and audited under. No setting can tell that caller from the proxy, which is why each setup above closes the ports instead. ### A list of trusted proxies With `TRUSTED_PROXIES` set, Goiabada reads the forwarded headers only from a connection whose address is in the list, and ignores them from anywhere else. It then walks `X-Forwarded-For` from the right, past each listed address, to the first one that isn’t listed: the client. A forged entry to the left of that one is never reached. Each entry is an IP address or a CIDR range, separated by commas. An entry that’s neither stops the server at start, even with `TRUST_PROXY_HEADERS` off. ### The startup warnings With the [rate limiter](https://goiabada.dev/reference/environment-variables/#rate-limits) on, the auth server logs a warning at every start about two settings it can’t check from the inside: - **Forwarded headers off.** Behind a proxy, every request resolves to the proxy, and everyone shares one rate limit. With nothing in front of the auth server, as for native binaries with no proxy, the warning is expected. - **One hop with no list.** It’s sound behind one proxy that sets or appends `X-Forwarded-For`, and defeated by a caller that reaches the auth server around it. The setup wizard’s Compose file and Kubernetes manifests trust one hop on purpose, so expect this warning with either. The auth server can’t tell a proxy that appends from one that passes the header through untouched, nor see whether anything reaches it around the proxy. Once you’ve checked both against your setup, leave the warning as it is. ## Next steps [Environment variables](https://goiabada.dev/reference/environment-variables/#proxies): The proxy and rate limiter settings, and the limits. [Too many attempts or 429](https://goiabada.dev/troubleshooting/too-many-attempts-or-429/): When every user shares one rate limit. [Production checklist](https://goiabada.dev/deploy/production-checklist/): What to check before going live. # Database Source: https://goiabada.dev/deploy/database/ This page helps you prepare the database Goiabada keeps everything in: users, clients, sessions, tokens and settings. Only the auth server talks to the database. Goiabada supports PostgreSQL, MySQL, SQL Server and SQLite, and the setup wizard asks which one you use. You set the connection through the `GOIABADA_DB_*` [environment variables](https://goiabada.dev/reference/environment-variables/#database). ## Create the database yourself By default the auth server creates its database at its first start, which needs a login allowed to create databases. To run with a login that isn’t, create the database once yourself: 1. Create the database with your engine’s statement, from its section below. 2. Give the login Goiabada uses full rights inside that database, and nothing outside it. 3. Set `GOIABADA_DB_CREATE=false`, so the auth server doesn’t look for the database through `postgres` or `master`, which such a login may not reach. It then issues no `CREATE DATABASE` either. The auth server creates every table itself, at each start, as [Upgrade Goiabada](https://goiabada.dev/deploy/upgrade-goiabada/#how-migrations-run) describes. ## Where the database sits The connection to PostgreSQL, MySQL or SQL Server carries the database password and everything Goiabada stores, apart from the secrets the [AES key](https://goiabada.dev/deploy/secrets/#what-each-secret-protects) encrypts. You protect it with two settings, which work the same way on all three engines. SQLite has no connection to protect, so it ignores both. `GOIABADA_DB_TLS_MODE` picks one of five modes. They’re PostgreSQL’s `sslmode` names: | Mode | Encrypted | Certificate checked | | - | - | - | | `disable` | Never, even when the server offers TLS. On SQL Server the login travels in plain text too. | No | | `prefer`, the default | On PostgreSQL and MySQL, when the server offers TLS, and in plain text when it offers none. On SQL Server, the login, and the whole session when the server forces encryption, as Azure SQL Database does. | No | | `require` | Always, the whole session. A server that offers no TLS is refused. | No | | `verify-ca` | Always, the whole session. | The certificate must chain to a trusted authority. The host name isn’t checked. | | `verify-full` | Always, the whole session. | The certificate must chain to a trusted authority and name `GOIABADA_DB_HOST`. An IP address must be one of the certificate’s IP addresses. | Only `verify-full` makes sure the auth server reached your database and nothing in between, so use it when you can. Use `verify-ca` only when the auth server reaches the database by a name its certificate doesn’t carry, such as an IP address or a connection pooler’s host name. > **Three of the modes check no certificate** > > **Never** use `disable`, `prefer` or `require` across a network you don’t trust. They check no certificate, so anyone on the path can pose as your database and read or change the traffic. Unless you use `verify-full`, keep the database on the auth server’s host, or on a private network that only the auth server and your administrators reach. The Docker Compose files the setup wizard generates write `prefer`, since their database runs on the file’s own network. If you leave `GOIABADA_DB_TLS_MODE` unset, the auth server connects as `prefer`. Every start then writes this warning until you set a mode: `the database tls mode is unset, so the auth server does not check whose database it reached`. Set `prefer` yourself to keep the same behavior without the warning. To see which mode a start used, read `tls_mode` on its `using database` record. If the database’s certificate doesn’t verify, or the database offers no TLS, the start stops with `unable to create the database connection`. [Database TLS connection fails](https://goiabada.dev/troubleshooting/database-tls-connection-fails/) lists each error and its fix. ### The CA file `GOIABADA_DB_TLS_CA_FILE` names a PEM file of the authorities you trust to sign the database’s certificate. The auth server reads it once, at start, and only in `verify-ca` and `verify-full`. - **Leave it empty** when an authority the system already trusts signed the certificate. - **Set it** when an authority the system doesn’t trust signed it, such as your database provider’s own or yours. The auth server then trusts only the authorities in the file, not the system’s. The auth server won’t start if the file can’t be read, holds no certificate, or is set with `disable`, `prefer` or `require`, which check no certificate. On Kubernetes, the setup wizard puts the file in [a ConfigMap](https://goiabada.dev/deploy/kubernetes/security/#check-the-databases-certificate). ### PostgreSQL’s own variables On PostgreSQL, these two settings are the whole of the connection’s TLS. The auth server doesn’t read PostgreSQL’s own `PGSSL*` variables, such as `PGSSLMODE` and `PGSSLROOTCERT`, and won’t start while one is set: see [PostgreSQL TLS variables stop startup](https://goiabada.dev/troubleshooting/postgresql-tls-variables/). ## PostgreSQL ```sql CREATE DATABASE goiabada OWNER goiabada ENCODING 'UTF8'; ``` Make the login the owner, as above. A login that isn’t the owner can’t create tables from PostgreSQL 15 on, even with every right on the database: also run `GRANT ALL ON SCHEMA public TO goiabada;` connected to that database. It needs no `CREATEDB` attribute and no access to the `postgres` database. Quote the name if `GOIABADA_DB_NAME` isn’t all lower case. PostgreSQL folds an unquoted identifier, so `CREATE DATABASE Goiabada` makes a database called `goiabada`, which isn’t the one Goiabada connects to. ## MySQL ```sql CREATE DATABASE goiabada CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_as_cs; ``` `GRANT ALL PRIVILEGES ON goiabada.* TO 'goiabada'@'%';` is enough. No server-wide privilege is needed. Keep MySQL’s strict SQL mode, which is its default: `sql_mode` includes `STRICT_TRANS_TABLES`. Without it, MySQL doesn’t refuse a statement that leaves out a `NOT NULL` column with no default, or that writes a value too long for its column: it stores a value it makes up for the first, and cuts the second short. ## SQL Server ```sql CREATE DATABASE goiabada COLLATE Latin1_General_100_CS_AS_KS_WS_SC_UTF8; ``` Map the login into the database and add it to `db_owner`. It needs no rights in `master` beyond the access every login has there, and no server role such as `dbcreator`. > **Caution** > > On SQL Server, spell out the `COLLATE` clause. A database’s default can’t be changed afterwards, because `ALTER DATABASE ... COLLATE` blocks while the application is connected, and every text column a future migration adds inherits it. `READ_COMMITTED_SNAPSHOT` is supported, on or off, and both settings are tested. Goiabada never asks for `SNAPSHOT` isolation, so `ALLOW_SNAPSHOT_ISOLATION` makes no difference to it either way. ## A PostgreSQL to try Goiabada on Kubernetes A cluster you’re trying Goiabada on may have no database to give it. This runs one PostgreSQL in the cluster, in a namespace of its own, with its data on a volume from the cluster’s default storage class. It’s for trying Goiabada out: one replica, no backups and no tuning. For production, use a database you run properly, in the cluster or outside it. 1. Save this as `postgres.yaml`: ```yaml apiVersion: v1 kind: Namespace metadata: name: postgres --- apiVersion: v1 kind: Service metadata: name: postgres namespace: postgres spec: selector: app: postgres ports: - port: 5432 --- apiVersion: apps/v1 kind: StatefulSet metadata: name: postgres namespace: postgres spec: serviceName: postgres replicas: 1 selector: matchLabels: app: postgres template: metadata: labels: app: postgres spec: containers: - name: postgres image: postgres:18 env: - name: POSTGRES_USER value: goiabada - name: POSTGRES_DB value: goiabada - name: POSTGRES_PASSWORD valueFrom: secretKeyRef: name: postgres key: password # A subdirectory, since a fresh volume can hold lost+found, which initdb refuses. - name: PGDATA value: /var/lib/postgresql/data/pgdata ports: - containerPort: 5432 readinessProbe: exec: command: ["pg_isready", "-U", "goiabada", "-d", "goiabada"] periodSeconds: 5 volumeMounts: - name: data mountPath: /var/lib/postgresql/data volumeClaimTemplates: - metadata: name: data spec: accessModes: ["ReadWriteOnce"] resources: requests: storage: 5Gi ``` 2. Apply it with a generated password, and wait for it: ```bash kubectl apply -f postgres.yaml kubectl create secret generic postgres -n postgres --from-literal=password="$(openssl rand -hex 24)" kubectl rollout status statefulset/postgres -n postgres --timeout=5m ``` 3. Read the password back for the setup wizard: ```bash kubectl get secret postgres -n postgres -o jsonpath='{.data.password}' | base64 -d; echo ``` 4. Answer the wizard’s database questions with **PostgreSQL**, the host `postgres.postgres.svc.cluster.local`, the port `5432`, the database `goiabada`, the username `goiabada` and that password. The host resolves inside the cluster only, so skip the wizard’s connection test. To remove it, with everything Goiabada stored: `kubectl delete namespace postgres`. ## SQLite There’s nothing to create, and `GOIABADA_DB_CREATE` doesn’t apply: the driver creates the file. To refuse a missing file instead of creating one, write `GOIABADA_DB_DSN` as a `file:` URI with `mode=rw`, such as `file:/data/goiabada.db?mode=rw`. After a plain path, like the one the setup wizard writes, the driver ignores `mode=rw`. [SQLite](https://goiabada.dev/reference/environment-variables/#sqlite) has the connection string for a file that survives restarts. The auth server runs as uid 10001, so the directory holding the file must be writable by it. In Docker, mount the volume at `/data`, which the image creates owned by that user. A volume whose files belong to another user needs a [one-time `chown`](https://goiabada.dev/troubleshooting/attempt-to-write-a-readonly-database/). ### Back up and restore SQLite While the auth server runs, part of the database is in `goiabada.db-wal` beside `goiabada.db`, so a copy of the file alone, or a copy taken while it writes, isn’t a backup. Stop the auth server first: a clean stop writes everything into `goiabada.db` and removes the other two files. Copy the whole directory anyway, since a stop that wasn’t clean leaves them, and they belong to the backup then: a server that was killed, or one whose requests or background work outlived its shutdown timeouts, which leaves the database to the process exit rather than close it under them. **Docker Compose** From the directory holding your `docker-compose.yml`: ```bash docker compose stop goiabada-authserver docker compose cp goiabada-authserver:/data ./goiabada-backup docker compose start goiabada-authserver ``` To restore it, stop the auth server again, then copy the backup in as the image’s own user, which keeps the files its own, removing any WAL files the current database left first: ```bash docker compose stop goiabada-authserver docker compose run --rm --no-deps -v "$PWD/goiabada-backup:/backup:ro" --entrypoint sh \ goiabada-authserver -c 'rm -f /data/goiabada.db-wal /data/goiabada.db-shm && cp /backup/* /data/' docker compose start goiabada-authserver ``` **Native binaries** ```bash sudo systemctl stop goiabada-authserver sudo cp -a /var/lib/goiabada/. /root/goiabada-backup/ sudo systemctl start goiabada-authserver ``` To restore it, stop the auth server, remove `goiabada.db-wal` and `goiabada.db-shm` from `/var/lib/goiabada` if they’re there, copy the backup’s files back with `cp -a`, and start it. A restored database still needs the AES key it was taken under: see [Back up the AES key](https://goiabada.dev/deploy/secrets/#back-up-the-aes-key). ## Refresh token storage The `refresh_tokens` table keeps a revoked refresh token until the token itself would have expired, rather than deleting it once it is revoked. The stored row is what lets the auth server recognize a [replayed refresh token](https://goiabada.dev/concepts/refresh-tokens/#replay) at all: without it a replay would be refused, but the rest of its family would never be revoked. So the table grows with how often clients refresh and how long their refresh tokens live. With the default 30-day offline idle timeout, a grant refreshed every five minutes holds about 8,600 rows before the oldest start to age out. A background job runs every 12 hours and deletes a row once its expiry or its maximum lifetime has passed. Watch the table if clients hold many offline grants that refresh often. The offline idle timeout and maximum lifetime, under **Admin**, **Tokens** and on each client’s **Tokens** tab, bound how long rows are kept, but only the one that actually ends your grants changes the count: lowering a maximum lifetime that the idle timeout always reaches first changes nothing. > **Caution** > > When importing refresh tokens from another system, fill in `expires_at`. A row with both `expires_at` and `max_lifetime` empty is never deleted by the background job, since neither comparison can find it expired, and removing it takes an explicit statement. ## Next steps [Upgrade Goiabada](https://goiabada.dev/deploy/upgrade-goiabada/): How migrations run at start, and how to roll back. [Secrets](https://goiabada.dev/deploy/secrets/): What the database password and the AES key protect. [Unable to create the database connection](https://goiabada.dev/troubleshooting/crashloopbackoff-or-unable-to-create-the-database-connection/): When the auth server can't reach the database. # Secrets Source: https://goiabada.dev/deploy/secrets/ This page helps you keep Goiabada’s secrets safe, and back up the one you can’t afford to lose. The [setup wizard](https://goiabada.dev/deploy/setup-wizard/) generates every secret a deployment needs. Most of them can be replaced later at the cost of a restart, or of everybody signing in again. One can’t: the AES key, which encrypts what the database holds. Back it up before anything else. ## Back up the AES key > **Lose this key and the database's secrets are lost with it** > > Without `GOIABADA_AES_ENCRYPTION_KEY`, a database backup restores to an auth server that can decrypt none of its client secrets, authenticator seeds or signing keys. **Never** keep the key only beside the database, and never in the same backup: a backup holding both gives every secret to whoever reads it. 1. Read the key where your setup keeps it: **Docker Compose** It’s `GOIABADA_AES_ENCRYPTION_KEY`, under `goiabada-authserver` in `docker-compose.override.yml`. **Native binaries** It’s `GOIABADA_AES_ENCRYPTION_KEY` in the env file, `goiabada.env` as the wizard writes it. **Kubernetes** It’s in the `goiabada-encryption-key` Secret. Read it out of the cluster: ```bash kubectl get secret goiabada-encryption-key -n goiabada -o jsonpath='{.data.aes-encryption-key}' | base64 -d ``` 2. Store it where your database backups aren’t: a password manager, a secret manager, or an offline copy. Not in a repository, and not beside the backups. 3. Do it again after every [rotation of the key](https://goiabada.dev/deploy/rotate-secrets/#the-aes-key), since the rotation puts the database under the new one. 4. Keep each old key as long as you keep a database backup taken under it. A rotation re-encrypts the live database only, so a backup from before it still needs the old key. ## What each secret protects Each server reads its own secrets from its environment, and only when it starts. | Secret | Read by | What it protects | Replaced without a rotation | | - | - | - | - | | `GOIABADA_AES_ENCRYPTION_KEY` | auth server | Everything secret the database stores: client secrets, the SMTP password, authenticator seeds, email verification, password reset and registration codes, and the private keys that sign tokens | The auth server can decrypt none of it | | `GOIABADA_AUTHSERVER_SESSION_AUTHENTICATION_KEY`, `GOIABADA_AUTHSERVER_SESSION_ENCRYPTION_KEY` | auth server | The auth server’s session cookie, and the session contents it stores in the database | Every user is signed out once | | `GOIABADA_ADMINCONSOLE_SESSION_AUTHENTICATION_KEY`, `GOIABADA_ADMINCONSOLE_SESSION_ENCRYPTION_KEY` | admin console | The admin console’s session cookie, and its session contents, administrators’ tokens included, which the auth server stores for it | Every administrator is signed out of the admin console once | | `GOIABADA_DB_PASSWORD` | auth server | The database user’s password. Whoever has it and can reach the database reads and changes everything Goiabada stores | The auth server can’t connect until the database user has the same password | | `GOIABADA_ADMINCONSOLE_OAUTH_CLIENT_SECRET` | both | The secret of the admin console’s client, `admin-console-client`. The admin console presents it at every sign-in and token refresh; the auth server reads it only at its first start, to create that client | The admin console refuses to start, and one already running can’t sign anybody in, until the client in the database has the same secret | | `GOIABADA_ADMIN_PASSWORD` | auth server | The first administrator’s password, read only by the first start, which creates the account | Nothing. Change the administrator’s password in the admin console | [Rotate secrets](https://goiabada.dev/deploy/rotate-secrets/) replaces each one without those costs. ## Where your setup keeps them **Docker Compose** The wizard writes every secret into `docker-compose.override.yml`, under the service that reads it, and none into `docker-compose.yml`, which you can therefore commit. The override is plain text, and its mode, `0600`, is its only protection: keep it out of version control and out of shared directories. Anyone who can run `docker` on the host reads the secrets too, since `docker inspect` shows a container’s environment, and so does root. **Native binaries** The wizard writes one file, `goiabada.env`, holding the configuration and every secret, which both servers read. It’s plain text, and its mode, `0600`, is its only protection: keep it out of version control, owned by the user the servers run as, and readable by that user alone. Root reads it whatever its mode, and the environment of the running processes too. **Kubernetes** The wizard writes two Secrets into `goiabada-secrets.yaml`: `goiabada-encryption-key`, holding the AES key alone, and `goiabada-secrets`, holding every other secret. A Secret’s data is base64, which anyone can decode, so the file’s mode, `0600`, is its only protection on disk. [Kubernetes secrets](https://goiabada.dev/deploy/kubernetes/secrets/) has the Secrets the manifest reads, other ways to create them, and who in the cluster can read the AES key. ## When a secret changes A server reads a secret when it starts and never again, so a change reaches it at its next restart, and not before. That’s what lets the rotations change a secret first and restart second. Generate each key with `openssl`, as the wizard does: `openssl rand -hex 64` for the two session authentication keys, and `openssl rand -hex 32` for the session encryption keys and the AES key. A server refuses to start with a key that isn’t hex or isn’t that long, and names the variable. To restore a database backup taken under an older AES key, start the auth server with that key as `GOIABADA_AES_ENCRYPTION_KEY`. Or start it with today’s key there and the old one as `GOIABADA_AES_ENCRYPTION_KEY_PREVIOUS`: it then re-encrypts the restored data to today’s key before it listens, as a [rotation](https://goiabada.dev/deploy/rotate-secrets/#the-aes-key) does. > **Caution** > > Running the setup wizard again generates new secrets, and where its files already are it warns and asks before writing over them. If you use its output for a deployment that already has a database, copy your existing values over the new ones first: the AES key above all, since a new one can’t decrypt the database and the auth server refuses to start with it, then the database password and the admin console’s client secret, which must match what the database already holds, and the session keys, unless everybody signing in again is fine. ## Next steps [Rotate secrets](https://goiabada.dev/deploy/rotate-secrets/): Replace each secret without signing anybody out. [Upgrade Goiabada](https://goiabada.dev/deploy/upgrade-goiabada/): Update to a new release, and roll back. [Environment variables](https://goiabada.dev/reference/environment-variables/): Every setting both servers read. # Rotate secrets Source: https://goiabada.dev/deploy/rotate-secrets/ This page helps you replace each of Goiabada’s secrets without signing anybody out, or with the shortest interruption that secret allows. Every rotation works the same way: a server reads its secrets only when it starts, so you change a secret where your setup keeps it, then restart the server that reads it. Each step has a tab per platform for its commands. Pick yours once, and every tab on the page follows. Generate a new key with `openssl rand -hex 64` for a session authentication key and `openssl rand -hex 32` for the rest. [Secrets](https://goiabada.dev/deploy/secrets/#what-each-secret-protects) says what each one protects. > **Note** > > On Kubernetes, the steps change the Secrets with `kubectl patch`, which suits Secrets nothing else writes. When the External Secrets Operator, the SealedSecrets controller or a GitOps tool owns them, make each step’s change in its source instead, as [Rotating Secrets something else owns](https://goiabada.dev/deploy/kubernetes/secrets/#rotating-secrets-something-else-owns) describes. If you kept the generated `goiabada-secrets.yaml`, it still holds the keys from before the rotation, and applying it again puts them back: delete it once the Secrets are in the cluster. ## The session keys Each server seals its browser sessions with its current session key pair, and opens them with that pair and then, when it’s set, a previous pair. So you make the new pair current and keep the old one as the previous pair for as long as a session sealed under it can live. Done this way, nobody is signed out. 1. **Make new pairs current, and the old pairs previous, then restart both servers.** **Docker Compose** In `docker-compose.override.yml`, give each server’s current pair to its previous pair, and new keys to the current pair: ```yaml goiabada-authserver: environment: # ...the other entries stay as they are - "GOIABADA_AUTHSERVER_SESSION_AUTHENTICATION_KEY=" - "GOIABADA_AUTHSERVER_SESSION_ENCRYPTION_KEY=" - "GOIABADA_AUTHSERVER_SESSION_AUTHENTICATION_KEY_PREVIOUS=" - "GOIABADA_AUTHSERVER_SESSION_ENCRYPTION_KEY_PREVIOUS=" goiabada-adminconsole: environment: # ...the other entries stay as they are - "GOIABADA_ADMINCONSOLE_SESSION_AUTHENTICATION_KEY=" - "GOIABADA_ADMINCONSOLE_SESSION_ENCRYPTION_KEY=" - "GOIABADA_ADMINCONSOLE_SESSION_AUTHENTICATION_KEY_PREVIOUS=" - "GOIABADA_ADMINCONSOLE_SESSION_ENCRYPTION_KEY_PREVIOUS=" ``` Then restart: ```bash docker compose up -d ``` Compose stops a changed service’s container before it starts the new one, so two containers of one service never run together, and one restart does it. **Native binaries** In the env file, give each server’s current pair to its previous pair, and new keys to the current pair: ```bash GOIABADA_AUTHSERVER_SESSION_AUTHENTICATION_KEY="" GOIABADA_AUTHSERVER_SESSION_ENCRYPTION_KEY="" GOIABADA_AUTHSERVER_SESSION_AUTHENTICATION_KEY_PREVIOUS="" GOIABADA_AUTHSERVER_SESSION_ENCRYPTION_KEY_PREVIOUS="" GOIABADA_ADMINCONSOLE_SESSION_AUTHENTICATION_KEY="" GOIABADA_ADMINCONSOLE_SESSION_ENCRYPTION_KEY="" GOIABADA_ADMINCONSOLE_SESSION_AUTHENTICATION_KEY_PREVIOUS="" GOIABADA_ADMINCONSOLE_SESSION_ENCRYPTION_KEY_PREVIOUS="" ``` Then restart both servers: ```bash sudo systemctl restart goiabada-authserver goiabada-adminconsole ``` Each server runs as one process, which a restart stops before it starts the new one, so one restart does it. **Kubernetes** A rollout runs old and new pods side by side, and each must open what the other seals. So the new pairs go in first as the previous pairs, and the two are then swapped, in two rollouts. **Add the new pairs as the previous pairs.** Every pod still seals under the current pair, and can now open the new one, which nothing uses yet: ```bash kubectl patch secret goiabada-secrets -n goiabada --type=merge --patch-file=/dev/stdin <" - "GOIABADA_AES_ENCRYPTION_KEY_PREVIOUS=" ``` **Native binaries** In the env file, set `GOIABADA_AES_ENCRYPTION_KEY` to a new key, `openssl rand -hex 32`, and add the key it held before: ```bash GOIABADA_AES_ENCRYPTION_KEY="" GOIABADA_AES_ENCRYPTION_KEY_PREVIOUS="" ``` **Kubernetes** ```bash kubectl patch secret goiabada-encryption-key -n goiabada --type=merge --patch-file=/dev/stdin < ``` The record `rotated data-at-rest encryption to the new GOIABADA_AES_ENCRYPTION_KEY` says the re-encryption committed. From then on the database is under the new key alone, and `GOIABADA_AES_ENCRYPTION_KEY` must go on holding it: an auth server started with the old key there refuses to start, with `the data encryption key does not decrypt the stored data, so the auth server cannot start`. If the auth server stops instead with `data-at-rest decrypts under neither GOIABADA_AES_ENCRYPTION_KEY nor GOIABADA_AES_ENCRYPTION_KEY_PREVIOUS`, the previous key isn’t the one the database is under, and nothing was re-encrypted. The start migrates the database before it checks the keys, so a start that is also an upgrade has migrated its schema all the same. 6. **Remove the previous key, and restart the auth server,** once you’ve checked that sign-ins and your clients work. Until then it’s harmless: at each start the auth server finds the database under the current key and re-encrypts nothing. **Docker Compose** Delete the `GOIABADA_AES_ENCRYPTION_KEY_PREVIOUS` line from `docker-compose.override.yml`, then: ```bash docker compose up -d goiabada-authserver ``` **Native binaries** Delete the `GOIABADA_AES_ENCRYPTION_KEY_PREVIOUS` line from the env file, then: ```bash sudo systemctl restart goiabada-authserver ``` **Kubernetes** ```bash kubectl patch secret goiabada-encryption-key -n goiabada --type=json \ -p='[{"op": "remove", "path": "/data/aes-encryption-key-previous"}]' kubectl rollout restart deployment/goiabada-authserver -n goiabada kubectl rollout status deployment/goiabada-authserver -n goiabada ``` The restart takes the old key out of the pods’ environment. When this ends, `goiabada-encryption-key` holds the new key alone. Keep the old key as long as you keep a database backup taken before the rotation. ## The database password `GOIABADA_DB_PASSWORD` is the password of the database user the auth server signs in as, `GOIABADA_DB_USERNAME`. The database checks a password only when a connection opens, so a running auth server keeps the connections it holds through a change, and opens every new one with the password it started with. What a rotation costs depends on whether the database accepts two passwords for one user at once: MySQL 8.0.14 and later do, PostgreSQL and SQL Server don’t. > **On Docker Compose** > > The generated Compose file has the auth server sign in as the database service’s administrator: `root` on MySQL, `postgres` on PostgreSQL, `sa` on SQL Server. The override holds the password twice, as `GOIABADA_DB_PASSWORD` under `goiabada-authserver` and as the database service’s own variable, `MYSQL_ROOT_PASSWORD`, `POSTGRES_PASSWORD` or `MSSQL_SA_PASSWORD`. The image reads that variable only to set up an empty volume, so changing it changes nothing in the database: the steps change the password in the database, and keep both variables matching it. ### On MySQL 1. **Give the user its new password, keeping the current one** as a second password that’s still accepted. `openssl rand -hex 32` makes one. **Docker Compose** The image creates `root` twice: for connections from other containers (`%`), which is the auth server’s, and from inside its own (`localhost`). In `docker compose exec mysql-server mysql -uroot -p`: ```sql ALTER USER 'root'@'%' IDENTIFIED BY '' RETAIN CURRENT PASSWORD; ALTER USER 'root'@'localhost' IDENTIFIED BY '' RETAIN CURRENT PASSWORD; ``` **Native binaries** Name the user and host your deployment signs in as: ```sql ALTER USER 'goiabada'@'%' IDENTIFIED BY '' RETAIN CURRENT PASSWORD; ``` Run it as an account with the `CREATE USER` privilege; the user itself can run it on its own account only with `APPLICATION_PASSWORD_ADMIN`. **Kubernetes** Name the user and host your deployment signs in as, the `GOIABADA_DB_USERNAME` of `goiabada-authserver-config`: ```sql ALTER USER 'goiabada'@'%' IDENTIFIED BY '' RETAIN CURRENT PASSWORD; ``` Run it as an account with the `CREATE USER` privilege; the user itself can run it on its own account only with `APPLICATION_PASSWORD_ADMIN`. 2. **Store the new password where the auth server reads it, and restart the auth server.** During the restart the old password and the new one are both accepted, so nothing fails. **Docker Compose** In `docker-compose.override.yml`, set `GOIABADA_DB_PASSWORD` and `MYSQL_ROOT_PASSWORD` to the new password, then restart the auth server alone: ```bash docker compose up -d --no-deps goiabada-authserver ``` Without `--no-deps`, Compose would also recreate the database’s container, whose variable changed, restarting the database under the auth server. That container takes the new value the next time it’s recreated, which changes nothing in the database. **Native binaries** Set `GOIABADA_DB_PASSWORD` to the new password in the env file, then: ```bash sudo systemctl restart goiabada-authserver ``` **Kubernetes** ```bash read -rsp 'New database password: ' DB_PASSWORD; echo kubectl patch secret goiabada-secrets -n goiabada --type=merge --patch-file=/dev/stdin <` and `leodip/goiabada:adminconsole-`, then: ```bash docker compose pull docker compose up -d ``` A setup wizard built from source writes the moving `latest` tag instead of a version, so `docker compose pull` fetches whatever release `latest` names that day. Replace it with a [release version](https://github.com/leodip/goiabada/releases), so that you choose when to upgrade. **Native binaries** Download the release’s ZIP for your platform from the [releases page](https://github.com/leodip/goiabada/releases), put both binaries where the old ones are, and restart. With the layout on [Native binaries](https://goiabada.dev/deploy/native-binaries/#set-it-up): ```bash unzip goiabada--linux-amd64.zip sudo install -m 0755 goiabada-authserver goiabada-adminconsole /opt/goiabada/ sudo systemctl restart goiabada-authserver goiabada-adminconsole ``` Keep the old binaries until the new release works: a [rollback](https://goiabada.dev/deploy/upgrade-goiabada/#roll-back-to-an-earlier-release) installs them again. **Kubernetes** A release of the setup wizard writes its own version into both image lines, with `imagePullPolicy: IfNotPresent`, since a version tag names one build. A wizard built from source writes `latest` with `imagePullPolicy: Always`, under a comment, and warns you when it finishes: `latest` moves, so pods started at different times can run different releases. Replace it with a [release version](https://github.com/leodip/goiabada/releases) before production. Set both image lines in `goiabada-k8s.yaml` to the new release, then apply it. It alone carries the image tags, so the Secrets need no applying again: ```bash kubectl apply -f goiabada-k8s.yaml kubectl rollout status deployment/goiabada-authserver deployment/goiabada-adminconsole -n goiabada ``` If you regenerate the manifest with the setup wizard instead, apply only the new `goiabada-k8s.yaml`: the `goiabada-secrets.yaml` written beside it holds new secrets, which the database isn’t under. 4. **Check the start.** The auth server writes `database migrated` once it has run the release’s migrations, or `no need to migrate the database` when it had none to run, and then starts its listeners. > **Caution** > > Running the setup wizard again generates new secrets. Before you use its output for a deployment that already has a database, copy your existing secret values over the new ones, as [Secrets](https://goiabada.dev/deploy/secrets/#when-a-secret-changes) explains: a new AES key can’t decrypt the database. ## How migrations run The auth server applies whatever migrations its release carries when it starts, between opening the database and starting its listeners. It refuses to start on a database that is half migrated, and on one a newer release has already migrated, since it can’t know what that release changed. [Waiting for the migration lock, or marked dirty](https://goiabada.dev/troubleshooting/waiting-for-the-migration-lock-or-marked-dirty/) covers both refusals. A start writes these Info records about the schema: | Record | When | Attributes | | - | - | - | | `waiting for the migration lock` | Another process holds the lock. Written once, before the wait | none | | `migrating the database` | Before the first migration runs | `from_version`, `to_version`, `pending` | | `database migrated` | After the last migration has run | `from_version`, `to_version`, `applied`, `duration` | | `no need to migrate the database` | The schema is already current | none | | `database migration stopped` | A stop signal ended the migrations between two files | `from_version`, `reached_version`, `applied`, `remaining` | `from_version` is the schema version the database recorded, 0 for one that was never migrated, and `to_version` the one this release expects. `pending` and `applied` count migration files. A replica that waited behind another one’s migration writes `waiting for the migration lock` and then `no need to migrate the database`. ### The migration lock Only one process migrates a database at a time. A process takes the migration lock before it migrates and releases it when it’s done, so with several replicas the first to start migrates, and the others wait for it, then find the schema current and start. The `migrate` command takes the same lock, and waits the same way. A waiting process waits for as long as the lock is held, on every engine; nothing in Goiabada gives up. A start that writes `waiting for the migration lock` and nothing more is waiting for a process that’s still migrating, not hung. The lock belongs to the holder’s database session, so a holder that dies releases it and can’t strand the others. On SQLite the lock covers one process only, so a start there never waits for it. Your platform decides how long a start may take: - **Kubernetes:** the generated manifest’s startup probe allows 5 minutes, and restarts the container after that. An upgrade whose migrations take longer needs a higher `failureThreshold`, as [First start and upgrades](https://goiabada.dev/deploy/kubernetes/probes-and-shutdown/#first-start-and-upgrades) explains. - **Docker Compose:** the auth server’s healthcheck gives a start 5 minutes, `start_period: 300s`, before its failures count, and the admin console waits for that healthcheck to pass before it starts. ### A start that’s stopped A stop signal (SIGTERM, which Kubernetes and Compose send, or SIGINT) that reaches the auth server while it’s still starting stops it cleanly, whatever it’s doing: - A wait ends at once: for the database connection, for the database to be created, or for the migration lock. - A migration file already running runs to its end, and the next one doesn’t start. The lock is released and the schema is left clean at the version it reached, which the next start carries on from, so a start stopped again and again still moves forward file by file. - The AES key’s re-encryption and a first start’s seed each commit whole: one under way completes, and one not yet begun doesn’t begin. The auth server then writes `shutdown signal received`, `database migration stopped` if migrations were under way, and `auth server stopped`, and exits 0. In `database migration stopped`, `reached_version` is where the schema now is, and `remaining` how many files the next start will run. The one thing the auth server can’t finish is a migration file that runs longer than the grace period the platform allows a stop, `terminationGracePeriodSeconds` on Kubernetes or `stop_grace_period` in Compose. The platform then ends it with SIGKILL, which no process can catch, and that file is left marked dirty: on MySQL and SQL Server, which run a file without an enclosing transaction, partly applied. The next start refuses until the schema is repaired by hand, as [A migration was cut short](https://goiabada.dev/troubleshooting/waiting-for-the-migration-lock-or-marked-dirty/#a-migration-was-cut-short) walks through. Before an upgrade whose release notes announce a long migration, give the start enough time that the platform doesn’t stop it, and don’t stop it yourself. ## Roll back to an earlier release The `migrate` command steps the schema without starting the server, against the database in `GOIABADA_DB_*`. The `--db-*` flags override those variables, given before or after `migrate`; any other flag goes before it. A mistyped command, such as `migrat`, is refused rather than starting the server. | Command | What it does | | - | - | | `goiabada-authserver migrate version` | Prints the schema version this binary expects and the one the database records | | `goiabada-authserver migrate to ` | Steps the schema to that version, up or down | 1. **Back up the database.** A rollback discards whatever the newer release wrote into columns and tables the older one doesn’t have. 2. **Step the schema down with the current binary,** naming the schema version the target release expects, which its release notes state. The current binary is the one that must run it, because it’s the only one carrying the migrations being rolled back. **Docker Compose** ```bash docker compose run --rm goiabada-authserver migrate to ``` **Native binaries** ```bash sudo -u goiabada sh -c 'cd /var/lib/goiabada && set -a && . /etc/goiabada/goiabada.env && set +a && exec /opt/goiabada/goiabada-authserver migrate to ' ``` It runs as the service’s user, from its working directory and with its `goiabada.env`, as the unit on [Native binaries](https://goiabada.dev/deploy/native-binaries/#set-it-up) does, so a SQLite database named by a relative path is the one the service uses. If the file can’t be read, nothing runs. **Kubernetes** ```bash kubectl exec -n goiabada deployment/goiabada-authserver -- /app/goiabada-authserver migrate to ``` 3. **Install the target release straight away,** as in [Update to a new release](https://goiabada.dev/deploy/upgrade-goiabada/#update-to-a-new-release), with its version in place of the new one. `migrate to` refuses any version below `000044`: releases older than the one carrying that schema changed stored data in ways no migration reverses, so no rollback reaches them. Ctrl-C or SIGTERM stops `migrate to` the way it stops a starting auth server: a wait ends at once, a migration file already running runs to its end, and the next one doesn’t start, so the schema is left clean. The command then prints the schema version the database is at, how many migrations it applied and how many remain, and exits 1, because it didn’t reach the version you asked for. Run the same command again to carry on from there. `migrate version` only reads, so Ctrl-C simply ends it. ## Next steps [Waiting for the migration lock, or marked dirty](https://goiabada.dev/troubleshooting/waiting-for-the-migration-lock-or-marked-dirty/): When a start waits, or refuses the database. [Database](https://goiabada.dev/deploy/database/): Prepare the database for each engine. [Secrets](https://goiabada.dev/deploy/secrets/): What each secret protects, and backing up the AES key. # Monitoring Source: https://goiabada.dev/deploy/monitoring/ This page helps you watch a running Goiabada through its Prometheus metrics, and alert when something goes wrong. The metrics say how many requests each route answers and how fast, how close the database pool is to its cap, how often the rate limiter refuses, how many tokens are issued and refused, whether the cleanup run succeeds, and, from the admin console, how the auth server answers it. For which request failed and why, read the [logs](https://goiabada.dev/deploy/logs/). ## Turn the metrics on Each server has a metrics listener of its own, off until you turn it on: | Server | Turned on by | Port | | - | - | - | | Auth server | `GOIABADA_AUTHSERVER_METRICS_ENABLED=true` | `9190`, set by `GOIABADA_AUTHSERVER_LISTEN_PORT_METRICS` | | Admin console | `GOIABADA_ADMINCONSOLE_METRICS_ENABLED=true` | `9191`, set by `GOIABADA_ADMINCONSOLE_LISTEN_PORT_METRICS` | 1. Turn the listeners on. On Kubernetes, answer the setup wizard’s metrics question, as [Scrape on Kubernetes](https://goiabada.dev/deploy/monitoring/#scrape-on-kubernetes) describes. With Docker Compose or native binaries, add the two variables above to each server’s environment: the Compose files and the env file the wizard writes leave them off. 2. Restart both servers. 3. Check what the auth server serves, from beside it. On Kubernetes: ```bash kubectl port-forward -n goiabada deploy/goiabada-authserver 9190:9190 curl -s http://localhost:9190/metrics | grep goiabada_build_info ``` With Docker Compose, from a container on the Compose network: `curl -s http://goiabada-authserver:9190/metrics`. 4. Point your scraper at both servers, on Kubernetes or [outside it](https://goiabada.dev/deploy/monitoring/#scrape-outside-kubernetes). > **Keep the metrics ports off the internet** > > The metrics listener has no authentication. What it serves holds no user data, but it does show your routes, how much traffic each one gets, how many tokens you issue and which release you run. **Never** publish its port through a Service, route, proxy or Compose `ports:` entry: let only your scraper reach it. The manifests the setup wizard generates publish it nowhere. Each listener listens on every address by default, because a scraper in another container or pod reaches it over the network. When the scraper runs on the same host, set `GOIABADA_AUTHSERVER_LISTEN_HOST_METRICS` and `GOIABADA_ADMINCONSOLE_LISTEN_HOST_METRICS` to `127.0.0.1`. The [environment variables](https://goiabada.dev/reference/environment-variables/#metrics) list every setting and its flag. ## Scrape on Kubernetes The setup wizard asks **Expose Prometheus metrics?**, with three answers: | Answer | Flag | What the manifest gets | | - | - | - | | None (default) | `--metrics=none` | Nothing: the metrics listeners stay off | | Pod annotations | `--metrics=annotations` | Both listeners on, a container port named `metrics` on each server, 9190 and 9191, and the `prometheus.io/scrape`, `port` and `path` annotations on both pod templates | | PodMonitor | `--metrics=podmonitor` | Both listeners on, the `metrics` container ports, and one `monitoring.coreos.com/v1` PodMonitor selecting both servers by that port | Pick the answer that matches what scrapes your cluster: | Scraper | Answer | Finds Goiabada by | | - | - | - | | The prometheus-community `prometheus` Helm chart | Pod annotations | [The annotations](https://goiabada.dev/deploy/monitoring/#pod-annotations) | | kube-prometheus-stack, or any Prometheus the Prometheus Operator runs | PodMonitor | [A PodMonitor](https://goiabada.dev/deploy/monitoring/#a-podmonitor) | | Grafana Alloy | Pod annotations | [The `metrics` container port](https://goiabada.dev/deploy/monitoring/#grafana-alloy) | | The OpenTelemetry Collector | Pod annotations | [The `metrics` container port](https://goiabada.dev/deploy/monitoring/#the-opentelemetry-collector) | Alloy and the Collector don’t read the annotations; that answer is simply the one that turns the listeners on and names the port without writing a PodMonitor, which `kubectl apply` refuses on a cluster without the Operator. With the NetworkPolicies on, the wizard also asks which namespace the scraper runs in (`--metrics-namespace`, `monitoring` by default). Each policy then gains a second ingress rule admitting that namespace to its server’s metrics port alone. Give it the namespace your scraper’s pods actually run in, which `kubectl get pods -A` shows: a policy naming any other admits nothing. To admit a scraper in another namespace too, add its `namespaceSelector` to that second rule’s `from`. Without the NetworkPolicies, any pod in the cluster can read the metrics. ### Pod annotations The wizard writes these on each server’s pod template, here the auth server’s: ```yaml annotations: prometheus.io/scrape: "true" prometheus.io/port: "9190" prometheus.io/path: "/metrics" ``` They’re a convention, not part of Kubernetes or Prometheus. The prometheus-community `prometheus` chart’s default configuration reads them, and kube-prometheus-stack ignores them. For the NetworkPolicy, the namespace is the one the chart’s Prometheus runs in. ### A PodMonitor The Prometheus Operator, which kube-prometheus-stack installs, scrapes only what a PodMonitor or a ServiceMonitor names. The wizard writes one PodMonitor for both servers: ```yaml apiVersion: monitoring.coreos.com/v1 kind: PodMonitor metadata: name: goiabada namespace: goiabada labels: release: kube-prometheus-stack spec: selector: matchExpressions: - key: app operator: In values: - goiabada-authserver - goiabada-adminconsole podMetricsEndpoints: - port: metrics path: /metrics ``` A Prometheus the Operator runs selects only the PodMonitors its `podMonitorSelector` matches, in the namespaces its `podMonitorNamespaceSelector` matches. kube-prometheus-stack, by default, selects only those labeled `release: `. The wizard asks for the labels to give the PodMonitor (`--podmonitor-labels`), such as `release=kube-prometheus-stack`, blank for none. Read what your Prometheus selects by with: ```bash kubectl get prometheus -A -o jsonpath='{..podMonitorSelector}' ``` A PodMonitor no Prometheus selects is accepted and scraped by nothing. PodMonitor is one of the Operator’s CRDs: on a cluster without them, `kubectl apply` exits 1 after applying everything else in the file. For the NetworkPolicy, the namespace is the one Prometheus runs in, `monitoring` in the usual kube-prometheus-stack install. ### Grafana Alloy Alloy discovers pods itself. Keep the targets on the `metrics` container port, and forward them to the `prometheus.remote_write` component your configuration already has: ```hcl discovery.kubernetes "goiabada" { role = "pod" namespaces { names = ["goiabada"] } } discovery.relabel "goiabada" { targets = discovery.kubernetes.goiabada.targets rule { source_labels = ["__meta_kubernetes_pod_container_port_name"] regex = "metrics" action = "keep" } rule { source_labels = ["__meta_kubernetes_pod_name"] target_label = "pod" } rule { source_labels = ["__meta_kubernetes_pod_container_name"] target_label = "container" } } prometheus.scrape "goiabada" { targets = discovery.relabel.goiabada.output forward_to = [prometheus.remote_write.default.receiver] } ``` Alloy’s service account needs to list and watch pods in the `goiabada` namespace, which the Alloy Helm chart grants. For the NetworkPolicy, the namespace is the one Alloy runs in. ### The OpenTelemetry Collector The Collector’s `prometheus` receiver takes a Prometheus scrape configuration, so it finds the pods the same way: ```yaml receivers: prometheus: config: scrape_configs: - job_name: goiabada kubernetes_sd_configs: - role: pod namespaces: names: [goiabada] relabel_configs: - source_labels: [__meta_kubernetes_pod_container_port_name] regex: metrics action: keep - source_labels: [__meta_kubernetes_pod_name] target_label: pod - source_labels: [__meta_kubernetes_pod_container_name] target_label: container ``` Add the receiver to a metrics pipeline, and give the Collector’s service account permission to list and watch pods in the `goiabada` namespace. For the NetworkPolicy, the namespace is the one the Collector runs in. ### A manifest generated without metrics Run the wizard again with the answer you want and compare. Or set `GOIABADA_AUTHSERVER_METRICS_ENABLED` and `GOIABADA_ADMINCONSOLE_METRICS_ENABLED` to `"true"` in the two servers’ ConfigMaps, and add the `metrics` port to each container yourself, here the auth server’s, with 9191 for the admin console: ```yaml ports: - containerPort: 9090 - name: metrics containerPort: 9190 ``` ## Scrape outside Kubernetes A Prometheus on the same Compose network as Goiabada scrapes both servers by their service names, with the ports published nowhere: ```yaml scrape_configs: - job_name: goiabada static_configs: - targets: - goiabada-authserver:9190 - goiabada-adminconsole:9191 ``` For native binaries, list the hosts the servers run on, and set the listen hosts to `127.0.0.1` when Prometheus runs on the same host. ## Set up alerts A starting set, as a Prometheus rules file. The thresholds are where to begin, not measurements: tune them once you have a few weeks of traffic. With the Prometheus Operator, put the `groups` under the `spec` of a `PrometheusRule` carrying the labels your Prometheus selects rules by, as for the PodMonitor. ```yaml groups: - name: goiabada rules: - alert: GoiabadaDatabasePoolWaits expr: sum by (instance) (rate(goiabada_db_wait_duration_seconds_total[5m])) > 0.1 for: 10m annotations: summary: Requests are waiting for a database connection. - alert: GoiabadaServerErrors expr: | sum(rate(goiabada_http_requests_total{status=~"5.."}[5m])) / sum(rate(goiabada_http_requests_total[5m])) > 0.02 for: 10m annotations: summary: More than 2% of requests are answered with a server error. - alert: GoiabadaRateLimitRefusals expr: sum by (limiter) (increase(goiabada_rate_limit_refusals_total[10m])) > 50 annotations: summary: The rate limiter is refusing a burst of requests. - alert: GoiabadaNoSuccessfulCleanup expr: | time() - max(max_over_time(goiabada_cleanup_last_success_timestamp_seconds[1d])) > 86400 and on () count(goiabada_db_max_open_connections offset 1d) > 0 annotations: summary: No cleanup run has completed in 24 hours. - alert: GoiabadaAfterResponseJobsDropped expr: sum by (class) (increase(goiabada_after_response_jobs_dropped_total[10m])) > 0 annotations: summary: Mail was not sent because too many jobs were in flight. - alert: GoiabadaAuthServerFailingTheConsole expr: | sum(rate(goiabada_upstream_requests_total{status=~"error|5.."}[5m])) / sum(rate(goiabada_upstream_requests_total[5m])) > 0.05 for: 5m annotations: summary: The admin console's calls to the auth server are failing. ``` What each one is for: - **Pool waits.** The database pool is capped at `GOIABADA_DB_MAX_OPEN_CONNS` connections per pod, and a request that finds every connection in use waits for one. The expression is the time spent waiting, per second, on one pod: above 0.1, requests wait a tenth of a second for every second that passes. Compare `goiabada_db_connections{state="in_use"}` with `goiabada_db_max_open_connections`: a pool that sits at its cap needs a larger cap, if the database’s connection limit has room for it across every pod, or more pods. The average wait is `rate(goiabada_db_wait_duration_seconds_total[5m]) / rate(goiabada_db_wait_count_total[5m])`. - **Server errors.** A 5xx is a fault on Goiabada’s side or in what it depends on, the database or the mail server, rather than a client’s mistake. Find which route with `sum by (route, status) (rate(goiabada_http_requests_total{status=~"5.."}[5m]))`, and the cause in the `ERROR` records the same requests wrote to the [logs](https://goiabada.dev/deploy/logs/). - **Rate-limit refusals.** Every refusal is counted, so a credential-stuffing run or a flood shows as a spike on the limiter it hit, as it happens. The [rate limits](https://goiabada.dev/reference/environment-variables/#rate-limits) give each limiter’s budget, and the [audit log](https://goiabada.dev/concepts/audit-log/)’s `rate_limit_exceeded` events say which accounts and addresses the refusals were about. The limits counted by a user or an email always apply. Those counted by an IP address apply, and show here, only once `GOIABADA_AUTHSERVER_RATELIMITER_ENABLED` is `true`. The setup wizard asks, and turns it on by default, except on Kubernetes under the Cluster traffic policy, where every client shares a node’s address. - **No successful cleanup.** One auth server pod claims a cleanup run every 12 hours, deleting expired sessions, tokens, codes and audit records. Each pod reports the last run it completed, so the deployment’s latest is the maximum across pods, kept for a day so a rollout doesn’t lose it. The second line holds the alert back until the metrics go back a day, since a new deployment has had no run yet. A run that keeps failing writes an `ERROR` record for the step that failed. - **Dropped jobs.** Forgot-password, self-registration and the notice of an email change send their mail after the response, at most 64 of each at once. A dropped job’s mail is never sent: the user waits for an email that doesn’t arrive. A sustained `goiabada_after_response_jobs_in_flight` near 64 is the same thing about to happen, usually a slow mail server. - **The auth server failing the console.** The admin console’s failure mode is the auth server. A `status` of `error` is a call that got no response at all: refused, reset or timed out. Alert as well on the scrape itself failing, Prometheus’s `up` series at 0 for Goiabada’s targets, and on pods restarting, which Kubernetes reports through kube-state-metrics rather than Goiabada. When a scrape fails, see [Metrics are not scraped](https://goiabada.dev/troubleshooting/metrics-are-not-scraped/). ## What the listener serves The metrics listener answers `GET /metrics` in the Prometheus text exposition format, and any other path with a 404. It isn’t part of the application’s routes: a scrape is neither logged nor counted in the request metrics. A label only ever takes a value from a set declared in the code when the metric is registered, and a value outside that set is recorded as `other`. So no metric can produce more series than the product of its label sets, whatever requests arrive, and nothing taken from a request or from the database reaches a label: no user, client, session, email address, IP address, path, query, user agent or error description. The route label is the server’s own route table, read at startup, and a request no route answered is recorded under `unmatched`. Prometheus adds the `job`, `instance` and, on Kubernetes, `pod` and `container` labels itself. A metric both servers expose has the same name, labels and meaning on each, so one query covers both, and those labels tell them apart. ## Metrics catalog Every metric Goiabada exposes, with each label and the values it can take: | metric | type | labels | server | meaning | | - | - | - | - | - | | `goiabada_http_requests_total` | counter | `route`: the route table, `unmatched`; `method`: `GET`, `HEAD`, `POST`, `PUT`, `PATCH`, `DELETE`, `CONNECT`, `OPTIONS`, `TRACE`; `status`: the response’s status code | both | HTTP requests answered, by the route that answered them, the method and the exact status code. Every request the server’s main listeners answer is counted, `/health` and the static files included; a scrape of the metrics listener is not. | | `goiabada_http_request_duration_seconds` | histogram | `route`: the route table, `unmatched`; `method`: `GET`, `HEAD`, `POST`, `PUT`, `PATCH`, `DELETE`, `CONNECT`, `OPTIONS`, `TRACE` | both | How long requests took to answer, in seconds. The buckets are 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10, 30 and 60: the slowest requests send mail and can take up to 40 seconds. | | `goiabada_db_max_open_connections` | gauge | none | auth server | The most connections the database pool may hold open at once, `GOIABADA_DB_MAX_OPEN_CONNS` on PostgreSQL, MySQL and SQL Server, and 1 on SQLite. | | `goiabada_db_connections` | gauge | `state`: `in_use`, `idle` | auth server | Connections the database pool holds, by whether a request is using them or they are idle. `in_use` reaching the cap is the pool fully taken. | | `goiabada_db_wait_count_total` | counter | none | auth server | Requests that waited for a database connection because the pool was at its cap. Its rate rising is the pool becoming too small for the load. | | `goiabada_db_wait_duration_seconds_total` | counter | none | auth server | Time spent waiting for a database connection, in seconds, summed over every wait. Its rate divided by the wait count’s is the average wait. | | `goiabada_db_connections_closed_total` | counter | `reason`: `max_idle`, `max_idle_time`, `max_lifetime` | auth server | Connections the database pool closed, by the limit that closed them: the idle cap, the idle time or the lifetime. | | `goiabada_rate_limit_refusals_total` | counter | `limiter`: `pwd_account_net`, `pwd_account`, `pwd_ip`, `otp`, `email_verification`, `email_verification_send`, `account_password`, `activate`, `register`, `register_email`, `reset_pwd`, `forgot_pwd_email`, `forgot_pwd_ip`, `dcr`, `ropc_ip` | auth server | Requests the rate limiter refused with 429, by the limiter that refused them. Every refusal is counted, where the audit log records one per key per window, so a spike in its rate is a credential-stuffing or flooding attempt as it happens. | | `goiabada_tokens_issued_total` | counter | `grant_type`: `authorization_code`, `refresh_token`, `client_credentials`, `password`, `implicit` | auth server | Token responses the server answered with, by the grant that issued them: the token endpoint’s four grants, and the implicit grant’s tokens from the authorization endpoint. | | `goiabada_token_requests_refused_total` | counter | `grant_type`: `authorization_code`, `refresh_token`, `client_credentials`, `password`; `error`: `invalid_request`, `invalid_client`, `invalid_grant`, `unauthorized_client`, `unsupported_grant_type`, `invalid_scope`, `server_error` | auth server | Token requests the token endpoint refused, by the `grant_type` the request named and the error code it was answered with. A grant type the endpoint does not redeem is recorded as `other`. A request the rate limiter refused is counted in `goiabada_rate_limit_refusals_total` instead. | | `goiabada_cleanup_runs_total` | counter | `outcome`: `completed`, `failed`, `interrupted` | auth server | Cleanup runs this instance performed, by how they ended: every step succeeded, a step failed, or shutdown cut the run short. The run is claimed by one instance every 12 hours, so on a deployment of several pods each counts only the runs it won. | | `goiabada_cleanup_last_run_duration_seconds` | gauge | none | auth server | How long this instance’s last cleanup run took, in seconds, however it ended. The worker’s `worker task completed` log record carries the same duration. | | `goiabada_cleanup_last_success_timestamp_seconds` | gauge | none | auth server | When this instance’s last cleanup run completed, as a Unix timestamp in seconds, or 0 when none has since it started. Across a deployment the latest success is the maximum over its pods. | | `goiabada_after_response_jobs_in_flight` | gauge | `class`: `recovery`, `registration`, `account_notice` | auth server | Work handed off to run after a response that is running now, by class: forgot-password’s code and mail, self-registration’s mail, and the notice of an email change. Each class runs at most 64 at once. | | `goiabada_after_response_jobs_dropped_total` | counter | `class`: `recovery`, `registration`, `account_notice` | auth server | Work handed off to run after a response that was dropped because its class already had 64 running, by class. A dropped job’s mail is never sent. | | `goiabada_upstream_requests_total` | counter | `target`: `admin_api`, `settings`, `token`, `jwks`, `sessions`; `status`: the response’s status code, or `error` | admin console | Calls the admin console made to the auth server, by the client that made them and the status code the auth server answered, or `error` when no response arrived: a connection refused or reset, or a timeout. `admin_api` is every call to the admin and account APIs, `settings` the public settings, `token` the sign-in’s code exchange, the refresh and the client credentials grant, `jwks` the signing keys, and `sessions` the administrators’ browser sessions, which the auth server stores; a retry is a call of its own. A rising rate of `error` or 5xx is the auth server failing as the console sees it. | | `goiabada_upstream_request_duration_seconds` | histogram | `target`: `admin_api`, `settings`, `token`, `jwks`, `sessions` | admin console | How long the admin console’s calls to the auth server took until the response headers arrived, in seconds, by the client that made them, with the buckets of `goiabada_http_request_duration_seconds`. | | `goiabada_settings_cache_requests_total` | counter | `result`: `hit`, `miss` | admin console | Lookups of the auth server’s public settings, which every console page needs, by whether a cached value answered them. A miss found no fresh value, whether it started the call to the auth server or waited on one already under way; the value is kept 30 seconds. | | `goiabada_build_info` | gauge | `version`: this binary’s version; `commit`: this binary’s commit | both | Always 1. The labels say which release is running. | | `go_goroutines` | gauge | none | both | Goroutines that currently exist. | | `go_memstats_heap_inuse_bytes` | gauge | none | both | Heap bytes in use. | ### Rate limiters The values of the `limiter` label, and what each one refuses: | `limiter` | Refuses | | - | - | | `pwd_account_net` | Failed sign-ins on one account from one address, at the sign-in form and the password grant together | | `pwd_account` | Failed sign-ins on one account from any address, at the sign-in form and the password grant together | | `pwd_ip` | Every sign-in form submission from one address | | `otp` | Failed OTP codes for one user | | `email_verification` | Failed email verification codes for one account | | `email_verification_send` | Requests to send a verification email for one account | | `account_password` | Failed passwords on one account’s password, OTP and email changes together | | `activate` | Account activation requests from one address | | `register` | Self-registrations from one address | | `register_email` | Self-registrations for one email address | | `reset_pwd` | Password reset requests from one address | | `forgot_pwd_email` | Forgot-password requests for one email address | | `forgot_pwd_ip` | Forgot-password requests from one address | | `dcr` | Dynamic client registrations from one address | | `ropc_ip` | Every password grant request from one address | ## Next steps [Logs](https://goiabada.dev/deploy/logs/): Which request failed and why, and the records worth knowing. [Metrics are not scraped](https://goiabada.dev/troubleshooting/metrics-are-not-scraped/): When the scraper shows Goiabada down, or not at all. [Production checklist](https://goiabada.dev/deploy/production-checklist/): Everything to check before going live. # Logs Source: https://goiabada.dev/deploy/logs/ This page helps you collect Goiabada’s logs and find what you need in them. The [metrics](https://goiabada.dev/deploy/monitoring/) count; the logs say which request and why. Both servers write one record per line to standard error. ## Collect the logs 1. **Capture standard error.** Every record goes there, request records included. If your setup captures only standard output, you’ll see nothing, and silence looks like no traffic rather than a stream nobody reads. 2. **Write JSON,** which most log pipelines parse without a pattern: set `GOIABADA_AUTHSERVER_LOG_FORMAT=json` and `GOIABADA_ADMINCONSOLE_LOG_FORMAT=json`. The default, `text`, writes one `key=value` line per record. 3. **Log each request,** with `GOIABADA_AUTHSERVER_LOG_HTTP_REQUESTS=true` and `GOIABADA_ADMINCONSOLE_LOG_HTTP_REQUESTS=true`. Every deployment the setup wizard generates already sets both. 4. **Count the `ERROR` records.** Each one is something that went wrong on Goiabada’s side, so put their rate on the same dashboard as the [5xx rate](https://goiabada.dev/deploy/monitoring/#set-up-alerts). 5. **Send the audit log to your pipeline** if you alert on security events, as [the audit log](https://goiabada.dev/deploy/logs/#the-audit-log) below describes. Every record written while serving a request carries its `request_id`, so you can gather the records of one request. It’s the `X-Request-Id` your client or proxy sent, or one Goiabada made up. ## Records worth knowing The level says who should look: `ERROR` means someone must act, `WARN` a request that was refused or handled, `INFO` startup, shutdown and the records below, and `DEBUG` diagnostic detail. | Record (`msg`) | Level | What it says | | - | - | - | | `http request` | `INFO` | One per request, with its `method`, `target`, `ip`, `status`, `bytes` and `duration`. Written only with request logging on | | `audit event` | `INFO` | One audit event, with its `event` name and `details`, written when the audit log’s console target is on | | `worker task completed` | `INFO` | A cleanup run that reached every step, with its `duration`. A failed step writes an `ERROR` record first | | `a job to run after its response was dropped, too many in flight` | `WARN` | A forgot-password, registration or email-change mail that wasn’t sent | | `cross-origin request refused` | `WARN` | A state-changing request the CSRF check refused as cross-origin, with its `explanation` and `remedy`. A run of them after a change of hostname or proxy is a misconfiguration rather than an attack | | `client authentication refused at the token endpoint` | `WARN` | A token request answered `invalid_client`, with the `reason` the client was given, the `client_identifier` as sent and the `client_auth_method` it used. A run of them from one client is usually a wrong or rotated secret | | `client registration refused` | `WARN` | A [dynamic client registration](https://goiabada.dev/reference/endpoints/dynamic-client-registration/) refused with `400` or `403`, with the `error_code` and the `reason` the registrant was given, such as a redirect URI it may not use. Written for the `403` of a server taking no registrations too | | `database migrated` | `INFO` | How many migrations a starting auth server `applied`, and their `duration` | ## The audit log The security events are in the [audit log](https://goiabada.dev/concepts/audit-log/), not the metrics: every sign-in, failed password, token issued, rate-limit refusal and administrative change, each with who did it. It goes to standard error as the `audit event` records above, to the database, where the admin console browses it, or to both, as the **Audit log settings** page in the admin console says. New installations start with both. Watch for a run of `auth_failed_pwd` or `ropc_auth_failed` events against one account, and for `rate_limit_exceeded`, which each pod writes at most once per key and window, so it shows which accounts and addresses the rate limiter’s refusals were about. ## Levels Each server writes records at its level and above: `GOIABADA_AUTHSERVER_LOG_LEVEL` and `GOIABADA_ADMINCONSOLE_LOG_LEVEL`, `info` by default, or `debug`, `warn` or `error`. A level or format spelled any other way, uppercase included, stops the server at start. The [environment variables](https://goiabada.dev/reference/environment-variables/#logging) list every logging setting. ## Request records With request logging on, every request gets a record but three kinds, which are never logged: health checks (`/health`), static files (`/static/`) and `/favicon.ico`. A record’s `request_id`, `method` and `ip` are each clipped at 128 bytes, with the true length in a marker, so a very long request id sent by a proxy shows up shortened rather than missing. ## What a request record leaves out A request record names every query parameter that arrived, but not every value. Every query value is replaced by `[redacted]`, apart from those of these names, whose value is kept: `client_id`, `response_type`, `response_mode`, `scope`, `prompt`, `max_age`, `acr_values`, `code_challenge_method`, `ui_locales`, `error`, `page` and `size`. That keeps an `id_token_hint`, an activation code and a password reset code out of the log, along with anything a client puts in a parameter nobody has assessed. Names are matched exactly, so `Client_ID` is redacted like any other unknown name. Activation and password reset links reach the log once each: following one checks the code and redirects to the same page with no query string, so the code never reaches the address bar, the browser’s history or the `Referer` of anything the page loads. > **A bound, not a promise** > > A value kept under one of those names is written whatever it holds. If a client misbuilds its request and puts a token where `client_id` goes, the log gets up to 512 bytes of it. The recorded target is an inventory, not a copy: - **It’s re-encoded.** The query is parsed and written back, so parameters appear in alphabetical order and escaping is canonical: `scope=openid%20profile` is recorded as `scope=openid+profile`. - **It’s bounded.** Each parameter name and each kept value is clipped at 512 bytes, and the whole target at 4096, each with a marker giving the true length. A target past 4096 bytes loses its later parameters. - **A query that can’t be parsed isn’t guessed at.** It’s recorded as `?[unparsable query, N bytes]`, with no names at all. ## Verbose API logging `GOIABADA_AUTHSERVER_DEBUG_API_REQUESTS=true` writes the request and response bodies of every `/api/v1/admin` and `/api/v1/account` call to the auth server’s log. It’s off by default and meant for development. Credential values are replaced by `[redacted]` before anything is written: passwords, OTP codes and secrets, the authenticator enrollment image, client secrets, the SMTP password, email verification codes, the logout URL carrying a signed `id_token_hint`, and any field whose name contains `password`, `secret`, `otp` or `token`. Field names are kept, so the body’s shape is still readable. The `Authorization` header is written as `Bearer [redacted]`, and the URL’s query is redacted as in a request record. A body that isn’t valid JSON, is larger than 256 KB, or nests more than 32 levels deep is replaced by a one-line note giving its size and why it wasn’t logged. ## Next steps [Monitoring](https://goiabada.dev/deploy/monitoring/): Turn on the metrics, scrape them and set up alerts. [Audit log](https://goiabada.dev/concepts/audit-log/): Every security event, and where it goes. [Production checklist](https://goiabada.dev/deploy/production-checklist/): Everything to check before going live. # Production checklist Source: https://goiabada.dev/deploy/production-checklist/ This page helps you check a deployment before people start signing in to it. Each item links to the page that explains it. A deployment the [setup wizard](https://goiabada.dev/deploy/setup-wizard/) generated already meets several of them, but check them all anyway: a file edited by hand can undo any of them. ## Secrets - [ ] **The AES key is backed up,** apart from the database backups. Without it, a restored database decrypts nothing. See [Back up the AES key](https://goiabada.dev/deploy/secrets/#back-up-the-aes-key). - [ ] **Every secret is your own,** generated for this deployment and never copied from an example. See [What each secret protects](https://goiabada.dev/deploy/secrets/#what-each-secret-protects). - [ ] **The secrets are readable only by what needs them:** out of version control and shared directories. See [Where your setup keeps them](https://goiabada.dev/deploy/secrets/#where-your-setup-keeps-them). - [ ] **The administrator’s password is strong,** at least 15 characters. Changing `GOIABADA_ADMIN_PASSWORD` after the first start does nothing; see [The admin password](https://goiabada.dev/deploy/rotate-secrets/#the-admin-password). - [ ] **The administrator has two-factor authentication,** set up from their account in the admin console. ## Network - [ ] **Both public URLs are `https://`.** `GOIABADA_AUTHSERVER_BASEURL` and `GOIABADA_ADMINCONSOLE_BASEURL` decide whether each server marks its cookies `Secure`, and there’s no other setting for it. See [URLs and listeners](https://goiabada.dev/reference/environment-variables/#urls-and-listeners). - [ ] **Only your proxy reaches ports 9090 and 9091.** Publish 443, and 80 if you redirect it, and nothing else. - [ ] **The metrics ports, 9190 and 9191, are published nowhere.** Only your scraper reaches them. See [Monitoring](https://goiabada.dev/deploy/monitoring/#turn-the-metrics-on). - [ ] **Forwarded headers are trusted only from your proxy.** Turn on `GOIABADA_AUTHSERVER_TRUST_PROXY_HEADERS` and `GOIABADA_ADMINCONSOLE_TRUST_PROXY_HEADERS` only behind a proxy, and list your proxies in `GOIABADA_AUTHSERVER_TRUSTED_PROXIES` and `GOIABADA_ADMINCONSOLE_TRUSTED_PROXIES` when there’s more than one hop. Turned on without a proxy, anyone can choose the address Goiabada rate limits and audits them under; behind a second hop with no list, everyone shares the outer proxy’s address. See [Client IP and proxy trust](https://goiabada.dev/deploy/client-ip-and-proxy-trust/). - [ ] **Certificates renew themselves,** whatever issues them. ## The hop from the admin console to the auth server - [ ] **The admin console reaches the auth server over a network you trust.** With an `http` internal URL, the hop is plain HTTP, and it carries the admin console’s client secret, its refresh token and administrators’ tokens. > **The internal hop isn't encrypted** > > The Docker Compose files and Kubernetes manifests the setup wizard generates set `GOIABADA_AUTHSERVER_INTERNALBASEURL` to a plain HTTP address. That’s sound on a network only your deployment uses, such as a Docker Compose network or a cluster’s pod network. When it isn’t, encrypt the hop: a network plugin that encrypts pod traffic, a service mesh with mutual TLS, or an `https://` internal URL to the auth server’s own HTTPS listener on port 9443, whose certificate names the host the admin console calls. The admin console trusts the system’s certificate authorities, and also the ones `SSL_CERT_FILE` or `SSL_CERT_DIR` names. Set both, and they replace the system’s. See [Docker Compose](https://goiabada.dev/deploy/docker-compose/#the-hop-to-the-auth-server) or [Kubernetes](https://goiabada.dev/deploy/kubernetes/security/#encrypt-the-hop-to-the-auth-server) for the steps. ## Database - [ ] **The auth server checks whose database it reached,** with `GOIABADA_DB_TLS_MODE` set to `verify-full`. If the system doesn’t trust the authority that signed the database’s certificate, `GOIABADA_DB_TLS_CA_FILE` names it. Without `verify-full`, the database sits where nobody else can read its traffic: on the auth server’s host, or on a private network only the auth server and your administrators reach. > **By default, the database server isn't checked** > > **Never** use `disable`, `prefer` or `require` across a network you don’t trust. They check no certificate, and `prefer` is the default. > > With `GOIABADA_DB_TLS_MODE` unset, Goiabada encrypts its connection to PostgreSQL and MySQL when the server offers TLS, and falls back to plain text when it doesn’t. On SQL Server it encrypts the login, and the rest of the session only when the server forces encryption. It checks no certificate, and warns at every start. See [Where the database sits](https://goiabada.dev/deploy/database/#where-the-database-sits). - [ ] **The database login has rights in Goiabada’s database and nowhere else.** See [Create the database yourself](https://goiabada.dev/deploy/database/#create-the-database-yourself). - [ ] **The database is backed up,** and you’ve restored a backup at least once, with the AES key it was taken under. - [ ] **One auth server, if you use SQLite.** SQLite is a file on one host, used through one connection. For several instances, use PostgreSQL, MySQL or SQL Server. See [Database](https://goiabada.dev/deploy/database/). ## Abuse - [ ] **The rate limiter is on,** `GOIABADA_AUTHSERVER_RATELIMITER_ENABLED=true`. The limits counted by a user or an email, 100 wrong passwords an hour for one account among them, apply either way, and it adds the limits counted by an IP address, on password sign-in among them. The setup wizard asks. See [Rate limits](https://goiabada.dev/reference/environment-variables/#rate-limits). ## Monitoring and logs - [ ] **Something checks `/health` on both servers.** It says the process is up, not that the database or the auth server answers. - [ ] **Both servers’ metrics are scraped.** See [Monitoring](https://goiabada.dev/deploy/monitoring/). - [ ] **Alerts fire** at least on server errors, database pool waits, rate-limit refusals and the cleanup run. See [Set up alerts](https://goiabada.dev/deploy/monitoring/#set-up-alerts). - [ ] **Both servers’ logs are collected,** standard error included, with request logging on. See [Collect the logs](https://goiabada.dev/deploy/logs/#collect-the-logs). - [ ] **The audit log goes where you need it, for as long as you need it.** New installations write it to the logs and the database and keep it 180 days; change that on the **Audit log settings** page. See [Audit log](https://goiabada.dev/concepts/audit-log/). ## After it’s live - [ ] **Sign in to the admin console,** with the administrator’s account. - [ ] **Add sign-in to a test client** and run its whole flow, from the sign-in page to the token. - [ ] **Write down** how the deployment is configured, where its secrets are kept, and how it’s backed up. ## Next steps [Secrets](https://goiabada.dev/deploy/secrets/): What each secret protects, and backing up the AES key. [Monitoring](https://goiabada.dev/deploy/monitoring/): Turn on the metrics, scrape them and set up alerts. [Upgrade Goiabada](https://goiabada.dev/deploy/upgrade-goiabada/): Move to a new release, and back again. # Authorize Source: https://goiabada.dev/reference/endpoints/authorize/ This page helps you send a user to sign in at the authorization endpoint, `/auth/authorize`, and read what comes back. Your app doesn’t call this endpoint itself: it sends the user’s browser there. The auth server signs the user in, asks for a one-time code or their consent when it needs to, and sends the browser back to your redirect URI with an authorization code, which your app redeems at the [token endpoint](https://goiabada.dev/reference/endpoints/token/). ## Send an authorization request 1. Send the browser to `/auth/authorize` with your request in the query string. Make a new random `state` and [PKCE](https://goiabada.dev/concepts/pkce/) code verifier for each request, and keep both: ```http GET /auth/authorize?client_id=my-app&redirect_uri=https%3A%2F%2Fapp.example.com%2Fcallback&response_type=code&scope=openid%20profile%20email&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&code_challenge_method=S256&state=af0ifjsldkj&nonce=n-0S6_WzA2Mj HTTP/1.1 Host: auth.example.com ``` 2. The user signs in. When the browser comes back, check that `state` is the one you sent: ```http HTTP/1.1 302 Found Location: https://app.example.com/callback?code=SplxlOBeZQQYbYS6WxSbIA&state=af0ifjsldkj ``` 3. Redeem the code at the [token endpoint](https://goiabada.dev/reference/endpoints/token/) within 60 seconds, with the same `redirect_uri` and your code verifier. If the request is refused, the browser comes back with `error` and `error_description` in place of `code`, for example `?error=invalid_scope&error_description=...&state=af0ifjsldkj`. Some refusals never come back: see [When an error reaches your app](https://goiabada.dev/reference/endpoints/authorize/#when-an-error-reaches-your-app). ## GET or POST Both work, as OpenID Connect requires. A GET carries the parameters in the query string. A POST carries them in an `application/x-www-form-urlencoded` body and is answered with a `303 See Other` to a GET that carries a one-time `request_handle` in their place. The browser follows it on its own, and the sign-in continues from that GET. The link works once, for five minutes, and only on its own: a `request_handle` sent twice, or beside any of the parameters below, is refused on a page at `400 Bad Request` with “This sign-in link is no longer valid. It may have expired, it may have been used already, or it may have been changed. Go back to the application you were signing in to and start again.” ## Parameters | Parameter | Required | | | - | - | - | | `client_id` | Yes | The client’s identifier | | `redirect_uri` | Yes | One of the client’s [redirect URIs](https://goiabada.dev/concepts/clients/#redirect-uris), matched exactly | | `response_type` | Yes | `code`. The legacy [implicit flow](https://goiabada.dev/legacy-flows/implicit/) takes `token`, `id_token` or `id_token token` instead. | | `scope` | Yes | One or more [scopes](https://goiabada.dev/concepts/scopes/), separated by single spaces, at most 2048 bytes. Include `openid` to get an ID token. | | `code_challenge` | When PKCE is required | 43 to 128 characters of `A-Z`, `a-z`, `0-9`, `-`, `.`, `_` and `~`. See [PKCE](https://goiabada.dev/concepts/pkce/). | | `code_challenge_method` | With `code_challenge` | `S256`, the only method supported | | `state` | Recommended | Any value up to 2048 bytes. It comes back unchanged with the code or the error, so your app can check the answer is to its own request. | | `nonce` | Recommended | Any value up to 2048 bytes. The ID token carries it in its `nonce` claim. | | `response_mode` | No | `query`, `fragment` or `form_post`. See [The response](https://goiabada.dev/reference/endpoints/authorize/#the-response). | | `prompt` | No | `none`, `login` or `consent`. See [prompt](https://goiabada.dev/concepts/prompt/). | | `max_age` | No | A number of seconds. A user who signed in longer ago than that signs in again. | | `acr_values` | No | The level of authentication to ask for. See [ACR and AMR](https://goiabada.dev/concepts/acr-and-amr/#which-level-a-request-asks-for). | | `id_token_hint` | No | An ID token naming the user your app expects. See [id_token_hint](https://goiabada.dev/concepts/id-token-hint/). | | `ui_locales` | No | The languages for the sign-in pages, as space-separated language tags in order of preference, such as `pt-BR en` | PKCE is required for every [public client](https://goiabada.dev/concepts/clients/#public-and-confidential-clients), and for a confidential one unless an administrator made it optional. `acr_values` and `ui_locales` are never refused for their value, though sent twice they’re refused like any repeated parameter. An `acr_values` with no level the auth server knows is ignored, and so is a malformed `ui_locales`; beyond ten language tags or 256 bytes, the rest is ignored. A parameter the auth server doesn’t read, such as `login_hint`, `display` or `claims`, is ignored too. ## What the auth server checks The auth server checks the request before it shows the user anything. A check it can’t answer by sending the browser back to your app is answered on a page, with nothing sent to your app. Those come first, in this order: | Check | Status | What the page says | | - | - | - | | The parameters can be read | 400 | “The authorization request could not be read: one of its parameters is not correctly encoded.” | | `client_id`, `redirect_uri`, `response_type` and `response_mode` are each sent once | 400 | “The *name* parameter was included more than once.” | | `client_id` names a client that exists, is enabled and has the authorization code flow on (for the implicit flow, it’s checked later) | 200 | “Invalid client_id parameter.” and why | | `redirect_uri` is absolute and registered on the client | 200 | “Invalid redirect_uri parameter.” and why. See [Invalid redirect_uri](https://goiabada.dev/troubleshooting/invalid-redirect-uri/). | | `response_mode` is `query`, `fragment` or `form_post` | 400 | “Invalid response_mode parameter. Supported values are: query, fragment, form_post.” | A missing `client_id` or `redirect_uri` is refused the same way. The page is shown in the language `ui_locales` asks for. An unsupported `response_mode` gets no error response at all, whoever is signed in. Understanding the mode is what tells the auth server how to encode a response, so it answers `400 Bad Request` with no `error`, `error_description` or `state`, as [OpenID Connect Core 1.0 section 3.1.2.6](https://openid.net/specs/openid-connect-core-1_0.html#AuthError) requires. Every other check is answered with an error response to your redirect URI, carrying your `state`. The first that fails is the answer: | Check | `error` | | - | - | | Every other parameter is sent once. A repeated `state` is dropped, so that error comes back with no `state`. | `invalid_request` | | There’s no `request` or `request_uri`: request objects aren’t supported | `request_not_supported`, `request_uri_not_supported` | | `response_type` is there, separated by single spaces, and one the auth server supports | `invalid_request`, `unsupported_response_type` | | The implicit flow is on for the client, when `response_type` asks for it | `unauthorized_client` | | `scope` has `openid`, when `response_type` asks for an ID token | `invalid_request` | | `nonce` is there, when the implicit flow asks for an ID token | `invalid_request` | | `state` and `nonce` are at most 2048 bytes | `invalid_request` | | `code_challenge` and `code_challenge_method` are there when PKCE is required, and right when they’re sent | `invalid_request` | | The implicit flow doesn’t ask for `response_mode=query`, since tokens never travel in the query | `invalid_request` | | `max_age` is digits only | `invalid_request` | | `scope` is there, separated by single spaces, at most 2048 bytes, more than `offline_access` alone, and every scope in it exists | `invalid_scope` | | The client may ask for each [administrative scope](https://goiabada.dev/reference/endpoints/authorize/#administrative-scopes) in it | `invalid_scope` | | `prompt` is separated by single spaces, has only `none`, `login` and `consent`, and `none` alone | `invalid_request`, `account_selection_required` for `select_account` | | `id_token_hint` is an ID token this auth server signed | `invalid_request`. See [id_token_hint](https://goiabada.dev/concepts/id-token-hint/#what-the-auth-server-checks). | Once the request passes, the user signs in, unless the browser’s session is enough, and goes through the one-time code and consent steps the request needs. See [prompt](https://goiabada.dev/concepts/prompt/), [ACR and AMR](https://goiabada.dev/concepts/acr-and-amr/) and [Sessions](https://goiabada.dev/concepts/sessions/). ## When an error reaches your app The auth server never sends a browser to a client’s redirect URI with an error before the person at that browser has signed in, as [RFC 9700 section 4.11.2](https://www.rfc-editor.org/rfc/rfc9700.html#section-4.11.2) requires. Otherwise a link carrying one bad parameter would turn the auth server into a redirector, sending anyone who clicked it to an address of the client’s choosing. So an error from the second table above reaches your app in one of these ways, the first that applies: | When | What happens | | - | - | | The client [registered itself](https://goiabada.dev/concepts/clients/#self-registered-clients) | No error is sent, ever. The user sees a page naming the address the client asked to send them to, and the request stops there. | | The request has `prompt=none` | The error is sent at once, since a [silent request](https://goiabada.dev/concepts/glossary/#silent-request) must never show a page | | The browser has a valid session, and the request doesn’t have `prompt=login` | The error is sent at once | | Anything else, `prompt=login` included | The user is asked for their password, and the error is sent once they’ve entered it, before any one-time code. That sign-in creates no session. | The error, its description, the response mode and `state` are the same whichever way it’s sent. See [The app gets no error back](https://goiabada.dev/troubleshooting/the-app-gets-no-error-back/) for what each case looks like. ## Administrative scopes Only a client [allowed to request the administrative scopes](https://goiabada.dev/concepts/clients/#administrative-scopes), the admin console’s own client or one an operator has allowed, may ask for `authserver:manage`, `authserver:admin-read`, `authserver:manage-users`, `authserver:manage-clients`, `authserver:manage-settings` or `authserver:browser-sessions`. Any other client asking for one is refused with `invalid_scope`, naming the first such scope it asked for, and gets no code and no token. The request is refused, never narrowed to the scopes that remain: ```plaintext error=invalid_scope error_description=The client is not allowed to request the administrative scope 'authserver:manage'. ``` It’s delivered as every other refusal above is, in the response mode the request asked for, the implicit flow’s fragment included. When the browser has a valid session, the refusal also leaves an `administrative_scope_refused` entry in the [audit log](https://goiabada.dev/concepts/audit-log/#events-to-alert-on). Without one, it leaves a log record but no audit entry, since anyone can reach this endpoint. The allowance is read again just before the code or the implicit flow’s tokens are issued, so a sign-in under way when an operator switches a client’s allowance off ends with the same answer. ## The response A successful response carries `code`, and `state` when your app sent one. An error response carries `error`, `error_description`, and `state` when your app sent one. `error_description` is English, holds no character outside those RFC 6749 allows in it, and is at most 512 bytes. `response_mode` says how they travel: | `response_mode` | How the browser comes back | | - | - | | `query`, the default for `response_type=code` | A `302 Found` to the redirect URI with the parameters in its query string. The redirect URI’s own query is kept. | | `fragment`, the default for the implicit flow | A `302 Found` to the redirect URI with the parameters after `#`. The browser keeps them away from your server, so your page’s JavaScript reads them. | | `form_post` | A page that posts the parameters to the redirect URI as a form, on its own, as [OAuth 2.0 Form Post Response Mode](https://openid.net/specs/oauth-v2-form-post-response-mode-1_0.html) defines | The implicit flow’s tokens and errors never travel in the query. See [Implicit](https://goiabada.dev/legacy-flows/implicit/). The code works once, for 60 seconds, and only with the `redirect_uri` it was issued for. ## Next steps [Token](https://goiabada.dev/reference/endpoints/token/): Redeem the code for tokens. [The app gets no error back](https://goiabada.dev/troubleshooting/the-app-gets-no-error-back/): Why a refused request doesn't come back to your app. [Scopes](https://goiabada.dev/concepts/scopes/): What to ask for, and what each scope gets your app. # Token Source: https://goiabada.dev/reference/endpoints/token/ This page helps you get tokens from the token endpoint, `POST /auth/token`. Your app calls it directly, not through the browser, with a `grant_type` saying what it’s trading for tokens: | `grant_type` | What your app sends | What it’s for | | - | - | - | | `authorization_code` | The code the [authorization endpoint](https://goiabada.dev/reference/endpoints/authorize/) gave it | Signing a user in | | `refresh_token` | A refresh token | New tokens without asking the user again | | `client_credentials` | Nothing but its own credentials | A client acting for itself, such as a service calling an API | | `password` | A user’s email and password | The deprecated [ROPC](https://goiabada.dev/legacy-flows/ropc/) flow, off unless an administrator turns it on | ## Redeem an authorization code 1. Send the code, the redirect URI it was issued for and your PKCE code verifier, with your client’s credentials. A [public client](https://goiabada.dev/concepts/clients/#public-and-confidential-clients) leaves `client_secret` out: ```bash curl -X POST https://auth.example.com/auth/token \ -d grant_type=authorization_code \ -d client_id=my-app \ -d client_secret=my-secret \ -d code=SplxlOBeZQQYbYS6WxSbIA \ -d redirect_uri=https://app.example.com/callback \ -d code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk ``` 2. Read the tokens from the answer: ```http HTTP/1.1 200 OK Content-Type: application/json Cache-Control: no-store Pragma: no-cache { "access_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6Ii4uLiJ9...", "id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6Ii4uLiJ9...", "token_type": "Bearer", "expires_in": 300, "refresh_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6Ii4uLiJ9...", "refresh_expires_in": 1800, "scope": "openid profile email" } ``` 3. Send the access token to your API as `Authorization: Bearer ...`, and keep the refresh token for when it expires. Redeem the code straight away: it works once, and for 60 seconds. ## The request The parameters go in an `application/x-www-form-urlencoded` body, as RFC 6749 asks. The auth server reads nothing from the query string. Each parameter goes in once: one sent twice is refused with `invalid_request`, “The ‘*name*’ parameter was included more than once.”, even when both copies agree. A body that can’t be read, such as one with a broken `%` escape or one over 64 KiB, is refused with `invalid_request`, “The request body could not be parsed.” ## Client authentication A [confidential client](https://goiabada.dev/concepts/clients/#public-and-confidential-clients) proves who it is with its client secret, in one of two ways: - **`client_secret_basic`**: an `Authorization: Basic` header holding `client_id:client_secret` in Base64. curl builds it with `-u my-app:my-secret`. - **`client_secret_post`**: `client_id` and `client_secret` in the body. Use one. Sending a secret both ways is refused with `invalid_request`. With the header, the auth server reads the client from it and ignores a `client_id` in the body. A public client sends `client_id` in the body and no secret, which discovery calls `none`. Sending a secret for a public client is refused with `invalid_request`, since it has none to check. RFC 6749 section 2.3.1 has a client form-encode its identifier and its secret before it builds the header, and some libraries, such as openid-client, escape every character but a letter or a digit, writing `my-app` as `my%2Dapp`. The auth server decodes `%` escapes in the header, so a client that encodes and one that doesn’t, like curl, are read the same. When the client can’t be authenticated, the answer is `401 Unauthorized` with `invalid_client` and a `WWW-Authenticate: Basic realm="goiabada"` header, whichever way the credentials came: | `error_description` | Why | | - | - | | “Client does not exist.” | No client has that identifier | | “Client is disabled.” | An administrator disabled the client | | “This client is configured as confidential (not public), which means a client_secret is required for authentication. Please provide a valid client_secret to proceed.” | A confidential client sent no secret | | “Client authentication failed. Please review your client_secret.” | The secret is wrong | The auth server’s log has a `client authentication refused at the token endpoint` warning for each, with the same reason, the client identifier the request named and how its credentials came. See [Logs](https://goiabada.dev/deploy/logs/#records-worth-knowing). A request with no `client_id` at all is refused with `invalid_request`, “Missing required client_id parameter.”, before `grant_type` is read. ## Authorization code grant | Parameter | Required | | | - | - | - | | `code` | Yes | The code from the authorization response | | `redirect_uri` | Yes | The `redirect_uri` of the authorization request, character for character | | `code_verifier` | When the request sent a `code_challenge` | The string the challenge was made from. See [PKCE](https://goiabada.dev/concepts/pkce/). | There’s no `scope` parameter: the tokens get the scope the user granted when the code was issued, and one sent here is ignored. A code is spent the first time it’s redeemed. Redeeming it again is refused, and revokes the tokens the first redemption issued, since a code presented twice may have been stolen, as RFC 6749 section 4.1.2 recommends. A code the auth server doesn’t know, one already redeemed or revoked, and one whose user has since been disabled or had their credentials changed are all refused with `invalid_grant`, “Code is invalid.”, so the answer says nothing more about the code. The other refusals: | `error` | `error_description` | Why | | - | - | - | | `unauthorized_client` | “The client associated with the provided client_id does not support authorization code flow.” | The flow is off for the client | | `invalid_grant` | “Invalid redirect_uri.” | `redirect_uri` differs from the authorization request’s | | `invalid_grant` | “The client_id provided does not match the client_id from code.” | The code was issued to another client | | `invalid_grant` | “Code has expired.” | More than 60 seconds have passed | | `invalid_request` | “Missing required code_verifier parameter.” | The request sent a challenge, and this one sends no verifier | | `invalid_grant` | “Invalid code_verifier (PKCE).” | The verifier doesn’t match the challenge | | `invalid_grant` | “The code_verifier parameter is incorrect. It should be 43 to 128 characters long and may only contain A-Z, a-z, 0-9, ‘-’, ‘.’, ‘\_’ and ‘\~’.” | The verifier isn’t one PKCE allows | | `invalid_grant` | “This code was issued without PKCE, and public clients are required to use PKCE. Please start a new authorization request with a code_challenge.” | A public client’s code has no challenge | | `invalid_request` | “The code_verifier parameter was provided, but PKCE was not used during authorization.” | A verifier for a code issued without a challenge | | `invalid_grant` | “The redirect URI recorded on this authorization code is no longer registered on the client, so the code can no longer be redeemed.” | An administrator removed the redirect URI after the code was issued | ## Refresh token grant | Parameter | Required | | | - | - | - | | `refresh_token` | Yes | The refresh token | | `scope` | No | A narrower scope for this access token, out of the refresh token’s own | Every refresh rotates the token: the answer carries a new refresh token, and the one you sent stops working. Store the new one before you use the answer. Presenting a refresh token that was already rotated is treated as a replay: it revokes the live tokens of its rotation family, the one your app holds now included, and is refused with `invalid_grant`, “This refresh token has been revoked.” See [This refresh token has been revoked](https://goiabada.dev/troubleshooting/this-refresh-token-has-been-revoked/). `scope` narrows only the access token of this answer. The new refresh token keeps the original scope, so a later refresh can ask for all of it again. A scope the refresh token doesn’t hold is refused with `invalid_scope`. The refresh is refused with `invalid_grant` when the token can’t be used any more, with a description saying why: the user’s session has ended or expired, the user was disabled or their credentials changed, an offline refresh token reached its maximum lifetime, or the user withdrew their consent. So is a refresh token issued to another client, with “The refresh token is invalid because it does not belong to the client.” [Refresh tokens](https://goiabada.dev/concepts/refresh-tokens/#how-long-a-refresh-token-lasts) explains how long each kind lasts. A refresh renewing `authserver:manage-account` is checked against the user’s consent whatever the client’s **Consent required** says, unless the client is the admin console’s, since the user approves that scope on the [consent screen](https://goiabada.dev/concepts/clients/#consent-required) before any other client gets it. While the consent doesn’t cover it, the refresh is refused with `invalid_grant`, and the refresh token isn’t spent: | `error_description` | Why | | - | - | | “The user has either not given consent to this client or the previously granted consent has been revoked.” | The user has no consent to the client | | “Scope ‘authserver:manage-account’ is not recognized. The user has not consented to the ‘authserver:manage-account’ permission.” | The user’s consent leaves `authserver:manage-account` out | A refresh whose `scope` leaves `authserver:manage-account` out isn’t checked for it, so it still works. A refresh token from the [password grant](https://goiabada.dev/legacy-flows/ropc/) isn’t checked either. ## Client credentials grant | Parameter | Required | | | - | - | - | | `scope` | No | One or more [permission scopes](https://goiabada.dev/concepts/scopes/#permission-scopes) the client holds. Leave it out to get every permission the client holds, which a client holding none is refused. | Only a confidential client with **Client credentials** turned on can use it. The token is the client’s own: it names no user, and gets no ID token and no refresh token. | `error` | Why | | - | - | | `unauthorized_client` | The flow is off for the client, or it’s a public client | | `invalid_scope` | A scope isn’t `resource:permission`, names a resource or permission that doesn’t exist, or names a permission the client doesn’t hold. Or it’s an OpenID Connect scope, such as `openid`, or `offline_access`: there’s no user to describe. Or `scope` is left out and the client holds no permissions, so there’s nothing to grant | ## Password grant | Parameter | Required | | | - | - | - | | `username` | Yes | The user’s email address | | `password` | Yes | The user’s password | | `scope` | No | `openid` when left out | It’s refused with `unauthorized_client` unless the flow is on for the client, and with `invalid_grant` for a user with two-factor authentication. See [ROPC](https://goiabada.dev/legacy-flows/ropc/) for every rule. ## Administrative scopes On the grants where a client acts for a user, a client that isn’t [allowed to request the administrative scopes](https://goiabada.dev/concepts/clients/#administrative-scopes) is refused one, never handed a narrower token, and the refusal leaves an `administrative_scope_refused` entry in the [audit log](https://goiabada.dev/concepts/audit-log/#events-to-alert-on). The client credentials grant isn’t affected. The **password grant** answers `invalid_scope`, as it answers a scope the user doesn’t hold: ```json { "error": "invalid_scope", "error_description": "The client is not allowed to request the administrative scope 'authserver:manage'." } ``` The **authorization code grant** answers `invalid_grant` when the code carries an administrative scope and the client is no longer allowed one. The allowance is read when the code is redeemed, so a code issued just before an operator switched the client’s allowance off isn’t exchanged for tokens: ```json { "error": "invalid_grant", "error_description": "The client is not allowed to request the administrative scope 'authserver:manage'." } ``` Start a new authorization request without the administrative scope. The **refresh token grant** answers `invalid_grant`, in the shape of its other per-scope checks. It judges the scope the refresh would issue, read now rather than when the token was issued, so a refresh token issued before an operator switched the client’s allowance off isn’t renewed with an administrative scope: ```json { "error": "invalid_grant", "error_description": "Scope 'authserver:manage' is not recognized. The client is not allowed to request the administrative scope 'authserver:manage'." } ``` The refresh token isn’t spent: refresh again with a `scope` that leaves every administrative scope out, which [RFC 6749 section 6](https://www.rfc-editor.org/rfc/rfc6749#section-6) allows as a narrower request. ## The answer A successful answer is `200 OK` with a JSON body, `Cache-Control: no-store` and `Pragma: no-cache`, as RFC 6749 section 5.1 requires. A field with no value is left out. | Field | | | - | - | | `access_token` | The access token, a signed JWT | | `token_type` | Always `Bearer` | | `expires_in` | Seconds until the access token expires: the client’s own setting, or the server-wide one | | `id_token` | When the scope has `openid` and a user signed in | | `refresh_token` | On every grant but client credentials | | `refresh_expires_in` | Seconds until the refresh token expires if it isn’t used | | `scope` | The scope the access token carries | ## Errors An error is a JSON body with `error` and `error_description`, as RFC 6749 section 5.2 defines, with `Cache-Control: no-store` and `Pragma: no-cache`: ```http HTTP/1.1 400 Bad Request Content-Type: application/json Cache-Control: no-store Pragma: no-cache { "error": "invalid_grant", "error_description": "Code has expired." } ``` | `error` | Status | | | - | - | - | | `invalid_request` | 400 | A parameter is missing, repeated or not allowed for this client | | `invalid_client` | 401 | The client couldn’t be authenticated, with `WWW-Authenticate: Basic realm="goiabada"` | | `invalid_grant` | 400 | The code, refresh token or user’s credentials can’t be used | | `unauthorized_client` | 400 | The grant is off for this client | | `unsupported_grant_type` | 400 | `grant_type` is missing or isn’t one of the four: “Unsupported grant_type.” | | `invalid_scope` | 400 | A scope is malformed, unknown, or more than the client or user may have | | `server_error` | 500 | Something failed on the server. The description ends with a request id to find it in the logs | `error_description` is English, with no character outside the ones RFC 6749 allows in it, and at most 512 bytes. With rate limiting on, the password grant is limited per IP address and per account, and a request over the limit is answered `429 Too Many Requests` with a `Retry-After` header. See [Too many attempts or 429](https://goiabada.dev/troubleshooting/too-many-attempts-or-429/). The other grants aren’t limited. ## Calling it from a browser JavaScript in a browser, such as a single-page app, can call the token endpoint when its origin is registered as a [web origin](https://goiabada.dev/concepts/clients/#web-origins) on a client. Without one, the browser blocks the call. ## Next steps [Tokens](https://goiabada.dev/concepts/tokens/): What each token carries, and how long it lasts. [Authorize](https://goiabada.dev/reference/endpoints/authorize/): Get the code this endpoint redeems. [UserInfo](https://goiabada.dev/reference/endpoints/userinfo/): Read the signed-in user's claims with the access token. # Logout Source: https://goiabada.dev/reference/endpoints/logout/ This page helps you sign a user out of the auth server from your app, with the logout endpoint, `/auth/logout`. It’s OpenID Connect’s [RP-Initiated Logout 1.0](https://openid.net/specs/openid-connect-rpinitiated-1_0.html) endpoint, the `end_session_endpoint` in the [discovery document](https://goiabada.dev/reference/endpoints/discovery-and-jwks/). What a sign-out ends, and what it leaves alone, is on [Ending sessions](https://goiabada.dev/concepts/ending-sessions/#signing-out). ## Sign a user out 1. Keep the ID token the user signed in with. You send it back as the `id_token_hint`, which tells the auth server who’s asking, so the user isn’t asked to confirm. 2. Send the browser to `/auth/logout` with the hint, where to come back to, and a `state`: ```http GET /auth/logout?id_token_hint=eyJhbGciOiJSUzI1NiIsImtpZCI6Ii4uLiJ9...&post_logout_redirect_uri=https%3A%2F%2Fmyapp.example.com%2Fsigned-out&state=af0ifjsldkj HTTP/1.1 Host: auth.example.com ``` The return address must be one of the client’s registered [redirect URIs](https://goiabada.dev/concepts/clients/#redirect-uris), exactly. 3. The auth server signs the user out and sends the browser back, with your `state`: ```http HTTP/1.1 302 Found Location: https://myapp.example.com/signed-out?state=af0ifjsldkj ``` 4. Check that `state` is the one you sent, and end your app’s own session. ## The request `/auth/logout` takes a GET and a POST, with the same parameters. On a GET they go in the query string. On a POST they go in an `application/x-www-form-urlencoded` body, and the query string is read too, so send each parameter once. | Parameter | Required | What it does | | - | - | - | | `id_token_hint` | Recommended | An ID token the auth server issued to your app, signed or [encrypted](https://goiabada.dev/reference/endpoints/logout/#encrypting-the-hint). One it can confirm signs the user out with no question asked. It may have expired. | | `post_logout_redirect_uri` | No | Where to send the browser afterwards. It must exactly match one of the client’s redirect URIs, and it’s honoured only on the terms in [Where the user lands](https://goiabada.dev/reference/endpoints/logout/#where-the-user-lands). | | `client_id` | Sometimes | The client’s identifier. Required with an encrypted hint, to say whose secret decrypts it, and with a `post_logout_redirect_uri` sent without a hint. With a hint, it must match the hint’s `aud`. | | `state` | No | Any value. It comes back on the redirect exactly as you sent it. | | `ui_locales` | No | The languages for the pages the user sees, such as `pt-BR en`. | There’s no separate list of post-logout redirect URIs: the client’s redirect URIs are the list. ## Whether the user is asked A sign-out the auth server can tie to your app happens at once. Any other asks the user first, with “Are you sure you want to sign out?”, as RP-Initiated Logout requires, and happens when they click **Yes**: | Request | What happens | | - | - | | A GET or a POST with a hint the auth server confirms | The user is signed out at once | | A GET with no hint, or with a hint it can’t confirm | The user is asked, then signed out | | A POST with a hint it can’t confirm | `303 See Other` to `GET /auth/logout`, which asks the user | | A POST from another origin with no hint, another site or another host of the same domain | `403 Forbidden`. See [Sign-out answers 403](https://goiabada.dev/troubleshooting/sign-out-answers-403/). | A hint that can’t be confirmed isn’t an error your app sees: it only means the user is asked. The `303` keeps `post_logout_redirect_uri`, `state` and `ui_locales`, and drops `id_token_hint` and `client_id`. A POST is worth it when you send a hint, because it keeps the ID token out of the address bar, the browser’s history and the `Referer` header. A self-submitting form on your app’s page does it. ## What a hint must pass The auth server confirms a hint when all of these hold: - It isn’t empty. - An encrypted hint comes with a `client_id`, and decrypts with that client’s secret. - Its signature is the auth server’s, and it’s an ID token: an access token or a refresh token isn’t one. - It has `sub` and `iat`, `iss` is the auth server’s issuer, and `aud` is the identifier of a client that exists. - A `client_id` sent beside it equals its `aud`. An empty `client_id=` doesn’t. - Its `nbf`, when there is one, has passed. - It has an `exp` and a `sid`. An `exp` that has passed is fine while the session `sid` names is still there, as RP-Initiated Logout recommends: a user who pauses before signing out shouldn’t be refused. - When the browser holds a session, `sid` names that one. - When the session `sid` names is still there, it belongs to the user `sub` names. The ID token from a user’s sign-in to your app passes all of these while the browser holds that session or none, until it has both expired and lost its session. ## What it ends **With a confirmed hint,** the auth server removes your app from the session the hint’s `sid` names. The session ends when your app was the last client on it, so the user stays signed in to the other apps sharing it. **Without one,** once the user clicks **Yes**, the browser’s whole session ends. Either way, the browser’s session cookie is cleared, so its next sign-in starts with the password. ## Where the user lands The browser goes to your `post_logout_redirect_uri` with a `302 Found` when: - the request carries a hint the auth server confirmed, and the URI is a redirect URI of the client the hint’s `aud` names; or - the request carries no hint, and the URI is a redirect URI of the client `client_id` names. The auth server adds `state` and nothing else. It comes back byte for byte, `+`, `/`, `=`, `#` and `&` included; sent empty, it comes back empty, and not sent, there’s none. A query the registered URI already has is kept. Otherwise the user lands on the auth server’s “Signed out” page, “You have been signed out.” When you sent a `post_logout_redirect_uri` that wasn’t honoured, it adds “We could not return you to the application that signed you out.” That’s the case for a URI that isn’t registered, a `client_id` that’s missing or names no client, and a hint that couldn’t be confirmed, even beside a valid `client_id`. The sign-out has already happened by then: a request the auth server can’t honour in full still signs the user out. ## Encrypting the hint An encrypted hint keeps the ID token unreadable in logs and the browser’s history. It’s a JWE, the signed ID token encrypted as [OpenID Connect Core 1.0 section 2](https://openid.net/specs/openid-connect-core-1_0.html#IDToken) describes a nested JWT, with this protected header: | Member | Value | What it means | | - | - | - | | `alg` | `dir` | The key below is the content encryption key. There’s no encrypted key. | | `enc` | `A256GCM` | AES-256 in GCM mode, with a 96-bit IV and a 128-bit tag | | `cty` | `JWT` | The plaintext is a JWT. Optional: a header without it is accepted too. | The key is the SHA-256 of the client’s secret, as UTF-8, which is 32 bytes. The JWE is in compact serialization, five parts with the second empty. A header with `zip` or `crit` is refused. Use a JOSE library for your platform rather than writing it yourself, and send `client_id` beside the hint. Only a confidential client can encrypt a hint, since a public client has no secret. ## Errors `/auth/logout` never sends your app an error. Apart from the `403` above, the one failure is the auth server’s own: when its database or session store fails, it shows an error page with `500 Internal Server Error`, and the sign-out may not have happened. ## Calling it from a browser JavaScript can call `/auth/logout` across origins when its origin is registered as a [web origin](https://goiabada.dev/concepts/clients/#web-origins) on a client. Sending the browser there with a link or a form needs nothing more. ## The audit log A sign-out with a confirmed hint leaves `deleted_user_session_client` in the [audit log](https://goiabada.dev/concepts/audit-log/), for your app leaving the session, and also `logout` when yours was the session’s last client and the session ended. One without a hint leaves `logout`, even when there was no session, and `deleted_user_session` when there was one. ## Next steps [Ending sessions](https://goiabada.dev/concepts/ending-sessions/#signing-out): What signing out ends, and what it leaves alone. [id_token_hint](https://goiabada.dev/concepts/id-token-hint/): What the hint is, and how the auth server checks it. [Sign-out answers 403](https://goiabada.dev/troubleshooting/sign-out-answers-403/): When a POST to /auth/logout is refused. # UserInfo Source: https://goiabada.dev/reference/endpoints/userinfo/ This page helps you read what the auth server knows about a signed-in user, from the UserInfo endpoint, `/userinfo`. It’s the OpenID Connect endpoint for a user’s [claims](https://goiabada.dev/concepts/glossary/#claim): your app calls it with the user’s access token, and gets back the claims of the scopes that token carries. ## Read a user’s claims 1. Sign the user in with a scope that has `openid`, plus the scopes for the claims you want, such as `openid profile email`. See [Authorize](https://goiabada.dev/reference/endpoints/authorize/). 2. Call `/userinfo` with the access token: ```bash curl https://auth.example.com/userinfo \ -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6Ii4uLiJ9..." ``` 3. **Check `sub` before you use any claim.** It must be exactly the `sub` of the ID token you validated at sign-in. If it isn’t, discard the whole answer and use none of its claims: the answer is about another user than the one who signed in. [OpenID Connect Core section 5.3.2](https://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse) requires this check. 4. Read the claims: ```json { "email": "jane@example.com", "email_verified": true, "family_name": "Doe", "given_name": "Jane", "name": "Jane Doe", "picture": "https://auth.example.com/userinfo/picture/2bd9ff17-7cb1-4c11-8d6d-6a3e1a0f5a6b", "preferred_username": "jane", "profile": "https://auth.example.com/account/profile", "sub": "2bd9ff17-7cb1-4c11-8d6d-6a3e1a0f5a6b", "updated_at": 1759900000 } ``` ## The request Send the access token one way: - **In the `Authorization` header,** as `Bearer` and the token, on a GET or a POST. This is the way to use. - **In a POST body,** as the `access_token` parameter, with `Content-Type: application/x-www-form-urlencoded`, as [RFC 6750 section 2.2](https://www.rfc-editor.org/rfc/rfc6750#section-2.2) defines. A token in the query string isn’t read, since it would end up in logs and the browser’s history. Sending the token twice, in two headers, two parameters or both ways, is refused with `invalid_request`. The token must be an access token the auth server issued for a user, with `openid` in its scope. A client credentials token never has `openid`, and an ID token or a refresh token isn’t accepted. ## The answer The answer is `200 OK` with the claims as a JSON object. `sub`, the user’s [subject](https://goiabada.dev/concepts/glossary/#subject), is always there. The rest come from the token’s scope: | Scope | Claims | | - | - | | `profile` | `name`, `given_name`, `middle_name`, `family_name`, `nickname`, `preferred_username`, `profile`, `picture`, `website`, `gender`, `birthdate`, `zoneinfo`, `locale` and `updated_at` | | `email` | `email` and `email_verified` | | `address` | `address`, an object with `street_address`, `locality`, `region`, `postal_code`, `country` and, when the country is set, `formatted` | | `phone` | `phone_number` and `phone_number_verified` | | `groups` | `groups`, the identifiers of the user’s groups set to be included in the ID token | | `attributes` | `attributes`, the user’s attributes and their groups’ set to be included in the ID token, as keys and values | A claim with no value is left out, as [Scopes](https://goiabada.dev/concepts/scopes/#openid-connect-scopes-and-their-claims) explains. `profile` is `/account/profile` and `picture` the user’s [profile picture](https://goiabada.dev/reference/endpoints/logo-and-picture/), both on the auth server’s base URL. `updated_at` is the last time the user’s details changed, in seconds since 1970. The claims are read when you call, so they’re current even when the token is older than the user’s last change. The answer is plain JSON, never a signed JWT. ## Errors An error is answered with a `WWW-Authenticate: Bearer realm="goiabada"` header naming the `error` and its `error_description`, and the same two in a JSON body, as [RFC 6750 section 3](https://www.rfc-editor.org/rfc/rfc6750#section-3) defines: ```http HTTP/1.1 401 Unauthorized WWW-Authenticate: Bearer realm="goiabada", error="invalid_token", error_description="Session has been terminated" Content-Type: application/json Cache-Control: no-store Pragma: no-cache { "error": "invalid_token", "error_description": "Session has been terminated" } ``` | Status | `error` | `error_description` | Why | | - | - | - | - | | 401 | none | none | No token was sent. The answer has only `WWW-Authenticate: Bearer realm="goiabada"` and no body. | | 400 | `invalid_request` | “The Authorization header must be sent once.”, “The access_token parameter must be sent once.” or “The access token must be sent by one method only.” | The token was sent twice | | 400 | `invalid_request` | “The request body could not be parsed.” | A POST body that can’t be read | | 401 | `invalid_token` | “The access token is invalid.” | The token is expired, its signature doesn’t verify, or it isn’t an access token | | 403 | `insufficient_scope` | “Insufficient scope.” | The token’s scope has no `openid` | | 401 | `invalid_token` | “Session has been terminated” | The user signed out, was disabled or deleted, or their credentials changed | | 401 | `invalid_token` | “Session has expired” | The session the token was issued in reached its idle timeout or maximum lifetime | The last two make `/userinfo` stricter than the token’s own `exp`: an access token stops working here as soon as the session it came from ends. A token with no session, from an offline refresh token or the password grant, stops when the user is disabled or their credentials change. See [Ending sessions](https://goiabada.dev/concepts/ending-sessions/#what-it-doesnt-reach). ## Calling it from a browser JavaScript in a browser can call `/userinfo` when its origin is registered as a [web origin](https://goiabada.dev/concepts/clients/#web-origins) on a client. Without one, the browser blocks the call. ## Next steps [Scopes](https://goiabada.dev/concepts/scopes/): Which scope gets your app which claims. [Logo and picture](https://goiabada.dev/reference/endpoints/logo-and-picture/): Show a user's profile picture or a client's logo. [Token](https://goiabada.dev/reference/endpoints/token/): Get the access token this endpoint takes. # Dynamic client registration Source: https://goiabada.dev/reference/endpoints/dynamic-client-registration/ This page helps you register an app as a client by calling the auth server, rather than creating it in the admin console. That’s dynamic client registration (DCR), `POST /connect/register`, as [RFC 7591](https://www.rfc-editor.org/rfc/rfc7591) defines it. It’s how tools like MCP clients get a client of their own. What a self-registered client may do once it exists is on [Clients](https://goiabada.dev/concepts/clients/#self-registered-clients). ## Register a client 1. In the admin console, turn on **Dynamic client registration enabled** under **Admin**, **General**. It’s off until you do. 2. Send the app’s metadata as JSON: ```bash 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"] }' ``` 3. Keep the `client_id` from the answer. Your app signs users in with it, as any other client does: ```http HTTP/1.1 201 Created Content-Type: application/json Cache-Control: no-store { "client_id": "dcr_6f1c2a9e-3b7d-4e0a-9c55-2d8b1e4f7a10", "client_id_issued_at": 1759900000, "client_secret_expires_at": 0, "redirect_uris": ["http://127.0.0.1/callback"], "token_endpoint_auth_method": "none", "grant_types": ["authorization_code"], "client_name": "My CLI tool" } ``` > **Caution** > > While DCR is on, **anyone** who can reach `/connect/register` can create a client, with no token and no sign-in. Limit the endpoint at your reverse proxy, or turn on the rate limiter, which then allows 10 registrations a minute from each IP address. See [rate limits](https://goiabada.dev/reference/environment-variables/#rate-limits). ## The request The body is a JSON object. The auth server reads four fields and ignores every other one, as RFC 7591 asks: | Field | What it does | | - | - | | `redirect_uris` | Where the auth server may send the browser back. Required when the client uses the authorization code flow. See [redirect URIs](https://goiabada.dev/reference/endpoints/dynamic-client-registration/#redirect-uris) below. | | `token_endpoint_auth_method` | `none` makes a public client, with no secret. `client_secret_basic`, the default, or `client_secret_post` makes a confidential one, with a secret. | | `grant_types` | Any of `authorization_code`, the default, `refresh_token` and `client_credentials`. A public client can’t ask for `client_credentials`, and the [legacy flows](https://goiabada.dev/legacy-flows/implicit/) can’t be asked for at all: the client is registered with both off, whatever the global settings under **Admin**, **General**. | | `client_name` | The name the consent screen shows, up to 100 characters, the bound of the client’s description it’s stored as, with no `<` or `>`. | `refresh_token` is accepted and sets nothing of its own: a client that may use the authorization code flow can redeem the refresh tokens it gets. A body larger than 64 KiB is refused as one that can’t be read. ## Redirect URIs A self-registered client’s redirect URIs follow the [rules for every client](https://goiabada.dev/concepts/clients/#redirect-uris), and the stricter ones on [Redirect URIs for self-registered clients](https://goiabada.dev/concepts/clients/#redirect-uris-for-self-registered-clients): a public client gets loopback `http` and custom schemes, a confidential one `https` and loopback `http`. On top of those, a registration holds at most 60 redirect URIs, each up to 2,048 bytes, and none listed twice. ## The client it creates The new client is enabled straight away, and: - **Its identifier is generated,** `dcr_` followed by a UUID, and can’t be changed later. - **A confidential client gets a secret** of 60 characters, which the answer carries once. A public client gets none. - **Consent is on,** so users see what it asks for before it gets a token. - **Its sign-in level is `urn:goiabada:level2_optional`,** so a user with two-factor authentication set up is asked for their code. See [ACR and AMR](https://goiabada.dev/concepts/acr-and-amr/). - **A public client always uses [PKCE](https://goiabada.dev/concepts/pkce/).** - **Its token lifetimes are copied** from **Admin**, **Tokens** onto its own **Tokens** tab when it registers, so a later change to the server’s settings doesn’t reach it. It starts with no permissions, and isn’t allowed to request the administrative scopes. In the admin console it’s marked **Self-registered**, and an administrator can change its settings like any other client’s once they’ve reviewed it. There’s no endpoint to read, update or delete a registration afterwards, so the answer carries no `registration_access_token`. To change a self-registered client, an administrator does it in the admin console. Each registration leaves a `dynamic_client_registration` entry in the [audit log](https://goiabada.dev/concepts/audit-log/). ## The answer A registration is answered `201 Created`, with `Cache-Control: no-store`, and the client’s metadata as JSON: | Field | What it holds | | - | - | | `client_id` | The client’s identifier, `dcr_` followed by a UUID | | `client_secret` | The client’s secret, for a confidential client only. It isn’t sent again: an administrator can read it later on the client’s **Authentication** tab. | | `client_id_issued_at` | When the client was created, in seconds since 1970 | | `client_secret_expires_at` | Always `0`: the secret never expires | | `redirect_uris` | The redirect URIs, as registered | | `token_endpoint_auth_method` | The method, with the default filled in | | `grant_types` | The grant types, with the default filled in | | `client_name` | The name, when one was sent | ## Errors An error is answered with a JSON body holding `error` and `error_description`, as RFC 7591 section 3.2.2 defines: ```http HTTP/1.1 400 Bad Request Content-Type: application/json Cache-Control: no-store { "error": "invalid_redirect_uri", "error_description": "redirect_uris required for authorization_code grant type" } ``` | `error` | Status | Why | | - | - | - | | `access_denied` | 403 | DCR is off: “Dynamic client registration is not enabled” | | `invalid_client_metadata` | 400 | The body isn’t JSON or is too large, or a field holds a value the auth server doesn’t take: an unknown `token_endpoint_auth_method`, a grant type it doesn’t take, `implicit` and `password` included, `client_credentials` on a public client, or a `client_name` too long or holding `<` or `>` | | `invalid_redirect_uri` | 400 | A redirect URI is missing, malformed, refused for this kind of client, too long, listed twice, or one too many | | `server_error` | 500 | The auth server couldn’t save the client. Nothing was registered. | The `error_description` says which value was refused, so it’s worth showing to whoever is setting the app up. With the rate limiter on, an IP address past its budget is answered `429 Too Many Requests` with `invalid_request`, “Too many requests. Please wait and try again later.” ## Discovery While DCR is on, the [discovery document](https://goiabada.dev/reference/endpoints/discovery-and-jwks/) carries `registration_endpoint`, the URL of `/connect/register`. While it’s off, the field isn’t there, so an app can tell from discovery whether it may register. ## Next steps [Self-registered clients](https://goiabada.dev/concepts/clients/#self-registered-clients): What a self-registered client may do, and how it's treated. [Authorize](https://goiabada.dev/reference/endpoints/authorize/): Sign users in with the new client. [The app gets no error back](https://goiabada.dev/troubleshooting/the-app-gets-no-error-back/): Why a self-registered client is never sent an error. # Discovery and JWKS Source: https://goiabada.dev/reference/endpoints/discovery-and-jwks/ This page helps you configure a client or an API from the auth server’s discovery document, and check the signature of a token it issued. Two endpoints do this, and neither needs a token: - **`GET /.well-known/openid-configuration`**, the discovery document: where every endpoint is and what the auth server supports. Most OpenID Connect libraries need nothing else to configure themselves. - **`GET /certs`**, the JSON Web Key Set (JWKS): the public keys the auth server signs tokens with. Its URL is the discovery document’s `jwks_uri`. ## Configure from discovery 1. Give your library the auth server’s base URL, or the discovery URL itself: ```bash curl https://auth.example.com/.well-known/openid-configuration ``` 2. It reads the endpoints and the issuer from the answer: ```json { "issuer": "https://auth.example.com", "authorization_endpoint": "https://auth.example.com/auth/authorize", "token_endpoint": "https://auth.example.com/auth/token", "userinfo_endpoint": "https://auth.example.com/userinfo", "end_session_endpoint": "https://auth.example.com/auth/logout", "jwks_uri": "https://auth.example.com/certs", ... } ``` 3. To check a token’s signature, it fetches the key set and picks the key whose `kid` matches the one in the token’s header: ```bash curl https://auth.example.com/certs ``` ```json { "keys": [ { "alg": "RS256", "kid": "0e0c3f6a-...", "kty": "RSA", "use": "sig", "n": "w8Zk...", "e": "AQAB" } ] } ``` Then it checks the token’s `iss` against the discovery document’s `issuer`, and its `aud` and `exp`. See [Tokens](https://goiabada.dev/concepts/tokens/). ## The discovery document The auth server describes what it implements, which no setting changes but two: the issuer, and whether `registration_endpoint` is there. Every grant and response type is listed even while the implicit flow or the password grant is off, as OpenID Connect Discovery 1.0 and RFC 8414 define these fields: a client that isn’t allowed one is refused at the endpoint. | Field | Value | | - | - | | `issuer` | **Issuer**, under **Admin**, **General** in the admin console. A new installation starts with the auth server’s base URL. Every token’s `iss` is this value. | | `authorization_endpoint` | `/auth/authorize`, the [authorization endpoint](https://goiabada.dev/reference/endpoints/authorize/) | | `token_endpoint` | `/auth/token`, the [token endpoint](https://goiabada.dev/reference/endpoints/token/) | | `userinfo_endpoint` | `/userinfo`, the [UserInfo endpoint](https://goiabada.dev/reference/endpoints/userinfo/) | | `end_session_endpoint` | `/auth/logout`, the [logout endpoint](https://goiabada.dev/reference/endpoints/logout/) | | `jwks_uri` | `/certs`, the key set below | | `registration_endpoint` | `/connect/register`, only while dynamic client registration is on. See [Dynamic client registration](https://goiabada.dev/reference/endpoints/dynamic-client-registration/). | | `grant_types_supported` | `authorization_code`, `refresh_token`, `client_credentials`, `password` and `implicit` | | `response_types_supported` | `code`, `token`, `id_token` and `id_token token` | | `response_modes_supported` | `query`, `fragment` and `form_post` | | `prompt_values_supported` | `none`, `login` and `consent`. See [prompt](https://goiabada.dev/concepts/prompt/). | | `acr_values_supported` | `urn:goiabada:level1`, `urn:goiabada:level2_optional` and `urn:goiabada:level2_mandatory`. See [ACR and AMR](https://goiabada.dev/concepts/acr-and-amr/). | | `subject_types_supported` | `public`: a user’s `sub` is the same for every client | | `id_token_signing_alg_values_supported` | `RS256` | | `scopes_supported` | `openid`, `profile`, `email`, `address`, `phone`, `groups`, `attributes` and `offline_access`. Permission scopes aren’t listed. See [Scopes](https://goiabada.dev/concepts/scopes/). | | `claims_supported` | The claims about the user and the sign-in the auth server can supply, in ID tokens, access tokens and `/userinfo` | | `token_endpoint_auth_methods_supported` | `client_secret_post`, `client_secret_basic` and `none`, the last for a public client | | `code_challenge_methods_supported` | `S256`. See [PKCE](https://goiabada.dev/concepts/pkce/). | | `request_parameter_supported` | `false`: a `request` object is refused | | `request_uri_parameter_supported` | `false`: a `request_uri` is refused | Each endpoint is the auth server’s base URL, `GOIABADA_AUTHSERVER_BASEURL`, followed by its path. The admin console’s home page links to the document. ## The key set `/certs` answers with the public half of every signing key the auth server keeps, in this order: - **The next key**, which will sign tokens after the next rotation. It’s published before it signs anything, so a client that caches the set already has it when it starts being used. - **The current key**, which signs every token now. - **The previous key**, while there is one: it signed tokens before the last rotation, and some of them may still be valid. Each key is a 4096-bit RSA key with `alg` `RS256`, `use` `sig`, and a `kid` that the header of every token it signs carries. To rotate, choose **Rotate key** under **Admin**, **Keys** in the admin console. The next key becomes the current one, the current one becomes the previous one, the old previous key is deleted, and a new next key is created. A token signed by the deleted key no longer verifies, refresh tokens included, so don’t rotate twice within the lifetime of your longest-lived token. ## Calling them from a browser Both endpoints answer cross-origin requests from any origin, with nothing to register, so JavaScript in a browser can read them. See [web origins](https://goiabada.dev/concepts/clients/#web-origins) for the endpoints that need one. ## Next steps [Tokens](https://goiabada.dev/concepts/tokens/): What's in each token, and how to validate one. [Authorize](https://goiabada.dev/reference/endpoints/authorize/): Start a sign-in at the authorization endpoint. [Token](https://goiabada.dev/reference/endpoints/token/): Exchange a code, refresh, or get a client's own token. # Logo and picture Source: https://goiabada.dev/reference/endpoints/logo-and-picture/ This page helps you show a client’s logo or a user’s profile picture in your app. The auth server serves both images, with no sign-in and no token: - **`GET /client/logo/{clientIdentifier}`**, the logo an administrator uploaded for a client. - **`GET /userinfo/picture/{subject}`**, a user’s profile picture. It’s the URL in the user’s `picture` claim. ## Show a profile picture 1. Sign the user in with the `profile` scope, and read the `picture` claim from the ID token or [`/userinfo`](https://goiabada.dev/reference/endpoints/userinfo/). It’s there only when the user has a picture: ```json { "sub": "2bd9ff17-7cb1-4c11-8d6d-6a3e1a0f5a6b", "picture": "https://auth.example.com/userinfo/picture/2bd9ff17-7cb1-4c11-8d6d-6a3e1a0f5a6b" } ``` 2. Use the URL as it is, in an `` tag: ```html ``` A client’s logo works the same way: ``. ## The client logo `GET /client/logo/{clientIdentifier}` answers with the client’s logo, under the image type it was uploaded as, such as `image/png`. The logo is public whatever the client’s state. It’s served while the client is disabled, and while **Show logo** is off: that switch decides whether the sign-in screens show the logo, not whether anyone may fetch it. The admin console’s **Logo** tab previews it through this same URL. The answer carries an `ETag` and `Cache-Control: public, max-age=300, must-revalidate`, so a browser or a proxy keeps it for five minutes and then asks again. A request with an `If-None-Match` naming the current `ETag`, or `*`, is answered `304 Not Modified` with no body. A `HEAD` request is answered as a `GET` is, with the same headers and no body, here and for the profile picture. A client that doesn’t exist, or has no logo, is answered `404 Not Found`. The identifier is matched exactly, case included. ## The profile picture `GET /userinfo/picture/{subject}` answers with the picture of the user whose [subject](https://goiabada.dev/concepts/glossary/#subject) is in the path, under the image type it was uploaded as. It’s served whatever the user’s state, a disabled user included, with `Cache-Control: no-store, no-cache, must-revalidate` and no `ETag`, so a browser fetches it again every time and a user who replaces their picture never keeps showing the old one. A subject that names no user, or a user with no picture, is answered `404 Not Found`. > **Note** > > Anyone who knows a user’s subject can fetch their picture. A subject is a random identifier nobody can guess, but every client the user signs in to receives it, so treat a profile picture as visible to all of them. ## Uploading An administrator uploads a client’s logo on the client’s **Logo** tab, and a user’s picture on the user’s **Picture** tab. Users upload their own under **Picture** on their account pages in the admin console. See [Clients](https://goiabada.dev/concepts/clients/#display-name-description-and-logo) and [Users and groups](https://goiabada.dev/concepts/users-and-groups/). In the admin console, both take a JPEG, PNG, GIF or WebP file of up to 3 MiB, and let you crop it before it’s uploaded. A picture is uploaded as a 512x512 JPEG. A logo keeps its shape, scaled down to at most 512 pixels on its longest side, and its type, but for a GIF, which is uploaded as PNG. The [Admin API](https://goiabada.dev/reference/api/authentication/) checks the image itself: JPEG, PNG, GIF or WebP, read from the file’s content rather than its name, from 10x10 to 512x512 pixels, and up to `GOIABADA_PROFILE_PICTURE_MAX_SIZE_BYTES`, 3 MiB by default, for logos too. Raising that setting doesn’t raise the admin console’s 3 MiB. ## Calling them from a browser Both endpoints work in an `` tag from any site, which needs nothing more. They send no CORS headers, so JavaScript on another origin can’t read the image’s bytes with `fetch`. ## Next steps [UserInfo](https://goiabada.dev/reference/endpoints/userinfo/): Read the picture claim and the user's other claims. [Clients](https://goiabada.dev/concepts/clients/#display-name-description-and-logo): A client's display name, description and logo. # Authentication Source: https://goiabada.dev/reference/api/authentication/ This page shows you how to get an access token for Goiabada’s APIs and call them with it. Goiabada has two APIs, both served by the auth server: - The **Admin API**, under `/api/v1/admin/`, manages users, groups, clients, resources, permissions and settings. It’s the API the admin console itself calls. A service calls it with a token of its own. - The **Account API**, under `/api/v1/account/`, lets a signed-in user manage their own account: profile, email, password, two-factor authentication, sessions and consents. Your app calls it with the user’s token. Every operation is in the [Admin API](https://goiabada.dev/reference/api/admin/) and [Account API](https://goiabada.dev/reference/api/account/) reference, with a curl example. ## Call the Admin API A service that manages Goiabada on its own, with no user present, gets its token through the client credentials flow: it signs in as a client, with the client’s own permissions. 1. In the admin console, open **Admin**, **Clients** and click **Create new**. Enter a **Client identifier**, such as `provisioning-service`, turn on **Client credentials flow**, turn off **Authorization code flow with PKCE**, and click **Create**. 2. Click **Manage** next to the new client, open **Authentication** and copy the **Client secret**. 3. Open **Permissions**, choose the `authserver` resource and the permission your service needs, such as `manage-users`, click **Grant permission** and then **Save**. [Scopes](https://goiabada.dev/reference/api/scopes/) explains what each permission allows. 4. Get an access token, asking for the scope that matches the permission: ```bash curl -X POST https://auth.example.com/auth/token \ -d grant_type=client_credentials \ -d client_id=provisioning-service \ -d client_secret=YOUR_CLIENT_SECRET \ -d scope=authserver:manage-users ``` The answer carries the token in `access_token`, and its lifetime in seconds in `expires_in`. 5. Call the API with the token in the `Authorization` header: ```bash curl "https://auth.example.com/api/v1/admin/users/search?query=ana" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` Ask for several scopes at once by separating them with spaces, as in `scope=authserver:manage-users authserver:manage-clients`. When the token expires, request a new one the same way. Each of `manage`, `admin-read`, `manage-users`, `manage-clients` and `manage-settings` makes the client an [administrator](https://goiabada.dev/reference/api/administrators/), so only an administrator holding `authserver:manage` can grant one. The administrator the setup created holds it. ## Call the Account API The Account API acts on the account of the user the token was issued for, so it needs a token your app got for a signed-in user, through the authorization code flow. A token from the client credentials flow is refused, as below. 1. Send the user to the authorization endpoint, asking for `authserver:manage-account` beside `openid`: ```plaintext GET https://auth.example.com/auth/authorize?client_id=my-app &redirect_uri=https://my-app.example.com/callback &response_type=code &scope=openid%20authserver:manage-account &code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM &code_challenge_method=S256 &state=abc123 ``` The first time, the user sees the consent screen, which names your app and says what `authserver:manage-account` lets it do, and approves it. Their answer is kept, so each user is asked once. 2. Exchange the code the user comes back with for tokens: ```bash curl -X POST https://auth.example.com/auth/token \ -d grant_type=authorization_code \ -d client_id=my-app \ -d client_secret=YOUR_CLIENT_SECRET \ -d code=THE_CODE \ -d redirect_uri=https://my-app.example.com/callback \ -d code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk ``` 3. Call the API with the `access_token` from the answer: ```bash curl https://auth.example.com/api/v1/account/profile \ -H "Authorization: Bearer USER_ACCESS_TOKEN" ``` Every user holds the `manage-account` permission from the moment they’re created, and it isn’t an administrative permission, so your client needs no permission of its own, and no allowance, to ask for it. What it needs is the user’s approval. The consent screen is shown the first time each user signs in to your app asking for `authserver:manage-account`, whatever your client’s **Consent required** says, and again if they untick it or withdraw it under **Account**, **Manage consents**. [Add sign-in to a web app](https://goiabada.dev/guides/add-sign-in-to-a-web-app/) covers the sign-in itself, PKCE included. Until a user has approved it, your app can’t get the scope without them: - a `prompt=none` request asking for it is answered `consent_required`; - a refresh renewing it is refused with `invalid_grant`, and the refresh token isn’t spent, so a refresh whose `scope` leaves `authserver:manage-account` out still works. A session or a live refresh token from an earlier authorization-code sign-in can encounter these checks until the user approves the scope. Send the user through an ordinary sign-in: they approve once, and the refreshes and silent requests after it work. Refresh tokens from the [password grant](https://goiabada.dev/legacy-flows/ropc/) don’t need this consent. ## Change a user’s account from your app There are three ways, and which one fits depends on who’s making the change: - **The Account API, with the user’s token.** For an app that changes the signed-in user’s own account on their behalf. It asks for `authserver:manage-account` as [above](https://goiabada.dev/reference/api/authentication/#call-the-account-api), and each user approves that once on the consent screen. The token reaches that user’s account and nothing else. - **The Admin API, with your client’s own token.** For a service that changes users’ accounts with no user present, such as provisioning from an HR system. It gets its token through the client credentials flow, as in [Call the Admin API](https://goiabada.dev/reference/api/authentication/#call-the-admin-api), holding `authserver:manage-users`, which only an administrator with `authserver:manage` can grant it. That makes the client an [administrator](https://goiabada.dev/reference/api/administrators/), and it still can’t change an administrator. - **A link to the admin console’s Account pages.** For a change the user makes themselves. Link to `/account/profile` on the admin console, such as `https://admin.example.com/account/profile`. The user signs in there if they need to and edits their profile, email, phone, address, picture, password, two-factor authentication, sessions and consents. Your app needs no scope at all. ## The bearer token Every operation under `/api/v1/` takes an access token in the `Authorization` header, as `Bearer` followed by the token. The reference also lists one public operation outside `/api/v1/`, the client logo, `GET /client/logo/{clientIdentifier}`, which takes none. The token must be an access token the auth server issued for the `authserver` resource, which is what asking for an `authserver:` scope gets you. An ID token or a refresh token is refused. ## Which tokens each API accepts The Admin API accepts a client’s token from the client credentials flow, and a user’s token from the authorization code flow when the client may [request the administrative scopes on a user’s behalf](https://goiabada.dev/reference/api/scopes/#administrative-scopes-on-a-users-behalf). The Account API accepts only a token issued for a user. A token from the client credentials flow is answered `403`: with `INSUFFICIENT_SCOPE` when it doesn’t carry `authserver:manage-account`, which is the usual case, and with `USER_CONTEXT_REQUIRED` when its client was granted that permission. ## When a token stops working A user’s token is checked against the user and their session on every call, so it stops working before it expires once the user is disabled or the session it was issued through ends. The call is then answered `401` with `INVALID_TOKEN`. The other refusals: | Answer | Error code | Why | | - | - | - | | `401` | `ACCESS_TOKEN_REQUIRED` | No token was sent. | | `401` | `INVALID_TOKEN` | The token is malformed, expired, not an access token for `authserver`, or its user or session is gone. | | `403` | `INSUFFICIENT_SCOPE` | The token carries none of the scopes the operation accepts. | The [errors](https://goiabada.dev/reference/api/errors/) page has the format of every error answer. ## The OpenAPI document The auth server serves the whole API as an OpenAPI 3.0 document at `/openapi.yaml`, with no authentication: ```bash curl https://auth.example.com/openapi.yaml ``` This reference is generated from it, so the two always say the same thing. Import it into Postman or Swagger UI, or generate a client from it. ## Next steps [Scopes](https://goiabada.dev/reference/api/scopes/): Pick the smallest scope that does the job. [Administrators](https://goiabada.dev/reference/api/administrators/): What only authserver:manage can do. [Admin API](https://goiabada.dev/reference/api/admin/): Every Admin API operation. [Account API](https://goiabada.dev/reference/api/account/): Every Account API operation. # Scopes Source: https://goiabada.dev/reference/api/scopes/ This page helps you pick the scope a token needs for the operations you call. A scope is what a client asks for when it requests a token. For the APIs, each scope is a permission on the `authserver` resource, written `authserver:` and the permission, and a token carries only the scopes its client or user holds. Give each integration the smallest scope that does its job. A service that creates and disables users needs `authserver:manage-users`, not `authserver:manage`: it can then manage every user who isn’t an administrator, and nothing else. ## The Admin API scopes | Scope | Reads | Writes | | - | - | - | | `authserver:admin-read` | Every Admin API operation that reads, administrators included, except a client’s secret | Nothing | | `authserver:manage-users` | Users and groups, with their attributes, sessions, consents, memberships and permissions | The same, except administrators and administrative permissions | | `authserver:manage-clients` | Clients, their secrets included | Clients, except administrator clients | | `authserver:manage-settings` | Settings, resources and their permissions, signing keys, and the audit log | Settings except email and audit logging, resources and their permissions, signing keys | | `authserver:manage` | Everything | Everything, administrators included | Every operation that reads accepts `authserver:admin-read`, the scope of its domain, or `authserver:manage`. Every operation that writes accepts the scope of its domain or `authserver:manage`. Two reads are different: - **A client’s secret** (`GET /api/v1/admin/clients/{id}/secret`) is a credential, so `authserver:admin-read` doesn’t reach it. It takes `authserver:manage-clients` or `authserver:manage`. - **The phone countries** (`GET /api/v1/admin/phone-countries`) belong to no domain, so they take `authserver:admin-read` or `authserver:manage` only. A token with none of the scopes an operation accepts is answered `403` with the error code `INSUFFICIENT_SCOPE`. The four granular scopes stop short of administrators: they read them, but they can’t create, change or delete one. Only `authserver:manage` does that, as [Administrators](https://goiabada.dev/reference/api/administrators/) explains. ## The Account API scope Every Account API operation takes `authserver:manage-account`, on a token issued for a user. It isn’t an administrative scope: a token carrying it reaches the user’s own account and nothing else. That still means the user’s profile, sessions and consents, and every user holds the `manage-account` permission from the moment they’re created, so the user decides which clients get it. Any client that signs users in, with the authorization code or the implicit flow, can ask for it, and gets it once the user has approved it on the consent screen. Whatever the client’s **Consent required** says, the user is asked the first time, and their answer is kept, so later sign-ins, `prompt=none` included, and refreshes go through without asking. A user who unticks it, or withdraws it under **Account**, **Manage consents**, is asked again. Until they’ve approved it, `prompt=none` is answered `consent_required` and a refresh renewing it `invalid_grant`. [Consent required](https://goiabada.dev/concepts/clients/#consent-required) has the whole rule. The admin console’s own client is the one exception: it gets `authserver:manage-account` with no screen, since that’s how users reach their own **Account** pages. A client allowed to [request the administrative scopes](https://goiabada.dev/reference/api/scopes/#administrative-scopes-on-a-users-behalf) is asked like any other. The [password grant](https://goiabada.dev/legacy-flows/ropc/) shows no screen at all, so a client using it gets `authserver:manage-account` as it gets any scope the user holds: the user has typed their password into it. ## The domains Each Admin API operation belongs to one domain, and the domain decides which granular scope reaches it: | Domain | Operations | | - | - | | Users | Users, user attributes, user sessions, user consents, groups, group members, group attributes, and the permissions of users and groups | | Clients | Clients, with their authentication, flows, redirect URIs, web origins, tokens, permissions, sessions, logo and secret | | Settings | General, email, session, token, UI theme and audit log settings, signing keys, resources and their permissions, and the audit log | ## Administrative scopes on a user’s behalf A token from the client credentials flow carries the client’s own permissions. A token your app gets for a signed-in user, through the authorization code flow, carries the scopes that user holds. For the six administrative scopes, `authserver:manage`, `authserver:admin-read`, `authserver:manage-users`, `authserver:manage-clients`, `authserver:manage-settings` and `authserver:browser-sessions`, that token would reach the Admin API with the user’s administrative rights. So only a client allowed to request the administrative scopes can get one on a user’s behalf: - the admin console’s own client, always; - any other client an operator has allowed, with the **May request administrative scopes** switch on the client’s settings in the admin console, or with `PUT /api/v1/admin/clients/{id}/administrative-scopes`. Only `authserver:manage` turns it on or off. A new client starts not allowed, including one that registered itself. The rule holds on the authorization code flow, `prompt=none` included, on the implicit flow, and on the refresh token and password grants. A client that isn’t allowed and asks for one of these scopes is refused, never handed a narrower token: `invalid_scope` from the authorization endpoint and on the password grant, and `invalid_grant` when it redeems a code or refreshes. [The authorization endpoint](https://goiabada.dev/reference/endpoints/authorize/#administrative-scopes) and [the token endpoint](https://goiabada.dev/reference/endpoints/token/#administrative-scopes) have the exact answers. The client credentials flow isn’t affected: a client holding an administrative permission of its own keeps asking for it there. > **Caution** > > An allowed client is an administrator client, so every change to it, and reading its secret, takes `authserver:manage`. Allow only clients you trust with your users’ administrative rights. [Clients](https://goiabada.dev/concepts/clients/#administrative-scopes) has the whole rule. ## Next steps [Administrators](https://goiabada.dev/reference/api/administrators/): What only authserver:manage can do. [Authentication](https://goiabada.dev/reference/api/authentication/): Get a token and call the API. # Administrators Source: https://goiabada.dev/reference/api/administrators/ This page explains which users, groups and clients are administrators, and what the Admin API lets each scope do to them. The short version: a granular scope such as `authserver:manage-users` manages everyone else, but it can’t make anyone an administrator, change an administrator, or change what reaches one. Only a token with `authserver:manage` can. ## Change an administrator Use a token carrying `authserver:manage` for any change to an administrator, or to who is one. The admin console’s sign-in gets one for an administrator who holds the `manage` permission, such as the administrator the setup created. A service that only manages ordinary users and clients should keep its granular scope. When one of its requests touches an administrator, it’s refused with `403` and `MANAGE_SCOPE_REQUIRED`, and nothing changes. That refusal is the design working, not something to retry with a bigger scope. ## Who is an administrator An administrator is a user, group or client holding any of the six administrative permissions on the `authserver` resource: `manage`, `admin-read`, `manage-users`, `manage-clients`, `manage-settings` and `browser-sessions`. - A **user** is one when they hold one of them directly or through any of their groups. - A **group** is one when it holds one of them. - A **client** is one when it holds one of them, when it’s the admin console’s own client, whatever it holds, or when it’s [allowed to request the administrative scopes](https://goiabada.dev/reference/api/scopes/#administrative-scopes-on-a-users-behalf). `manage-account` and the permissions you add to the `authserver` resource yourself aren’t administrative. [Resources and permissions](https://goiabada.dev/concepts/resources-and-permissions/#the-authserver-resource) lists the built-in permissions. ## What only `authserver:manage` does A granular scope reaching an operation is still refused when the request would: - **Grant or revoke an administrative permission**, to a user, a group or a client, or move a user into or out of a group holding one, through `POST` or `DELETE` on a group’s members or `PUT` on a user’s groups, or delete such a group. - **Write to an administrator**: any change to an administrator user (enabled, profile, address, email, verification code, phone, password, two-factor authentication, picture, attributes, sessions, consents, groups, permissions, deletion), to an administrative group (settings, attributes, members, permissions, deletion), or to an administrator client (settings, authentication, flows, redirect URIs, web origins, token settings, permissions, logo, deletion). - **Switch whether a client may request the administrative scopes**, on or off, on any client. - **Read an administrator client’s secret**, the admin console’s own included. - **Change the email or the audit-log settings.** The email settings decide where password reset links go, and the audit-log settings decide what’s recorded. `authserver:manage-settings` still reads them, and changes every other setting. - **Change the description of an administrative permission.** The admin console shows that description to an operator choosing what to grant, so rewriting it could pass `authserver:manage` off as something harmless. `authserver:manage-settings` still changes the description of `manage-account` and of your own permissions. These rules judge the object a request writes. A change to an ordinary group or resource isn’t a write to an administrator, even when an administrator is a member of the group or holds one of the resource’s permissions. So `authserver:manage-users` can still rename an ordinary group with an administrator in it, change its permissions or delete it. That changes the administrator’s groups and the claims in their tokens, but it can’t make or unmake an administrator, because an ordinary group holds no administrative permission. Creating a user, group or client isn’t restricted. None of the three requests carries a permission or a group, and a new user is granted only `manage-account`, so nothing new starts as an administrator. ## The refusal A refused request writes nothing. It’s answered after the request’s own `400` and `404` checks, with `403`, the error code `MANAGE_SCOPE_REQUIRED`, and a challenge naming the one scope that would do: ```plaintext WWW-Authenticate: Bearer realm="goiabada", error="insufficient_scope", error_description="Only a token with the authserver:manage scope may act on administrators or administrative permissions.", scope="authserver:manage" ``` Don’t confuse it with `INSUFFICIENT_SCOPE`. That one means the token lacks the scope the operation accepts, and asking for that scope fixes the call. `MANAGE_SCOPE_REQUIRED` means no granular scope will ever be enough: use a token with `authserver:manage`, or leave the administrator alone. ## The last administrator A token with `authserver:manage` passes every rule above, but meets one more. A change that would leave no enabled user holding `authserver:manage`, directly or through a group, is answered `409` with the error code `LAST_ADMINISTRATOR`, and nothing changes. A client holding the permission doesn’t count, and neither does a disabled user. Seven operations can meet it: setting a user’s permissions, setting a group’s permissions, removing a group member, setting a user’s groups, deleting a group, disabling a user and deleting a user. In the admin console, that’s revoking `manage` from a user or a group, removing a user from a group that gives it, deleting such a group, and disabling or deleting a user. The console shows the refusal’s sentence on the page you made the change from: “This change would leave no enabled user holding authserver:manage. Grant it to another user first.” Grant `authserver:manage` to another user first, then repeat the request. What has to survive is a person who can sign in to the admin console and repair things, which is why a client doesn’t count. The check reads permissions, not whether that person can still sign in: a sole administrator who has forgotten their password or lost their authenticator still counts. Keep `manage` on at least two people you trust, so one can always let the other back in. [Locked out of the admin console](https://goiabada.dev/troubleshooting/locked-out-of-the-admin-console/#the-last-administrator-cant-sign-in) covers what to do when that’s already happened. ## What the audit log records - A refused request leaves an `administrator_change_refused` entry, naming the caller, the route, the target and the rule that refused it. - An administrative permission granted or revoked, on a user, a group or a client, or a user joining or leaving a group that holds one, leaves an `administrative_permission_changed` entry beside the change’s own entry. - Switching whether a client may request the administrative scopes, on or off, leaves an `updated_client_administrative_scopes` entry, on every save. - Deleting an administrator or an administrative group leaves its own deletion entry, `deleted_user`, `deleted_client` or `deleted_group`, and no `administrative_permission_changed`. An alert on who is an administrator watches all of these. [Audit log](https://goiabada.dev/concepts/audit-log/#events-to-alert-on) lists the events to alert on. > **Tip** > > A user provisioning service needs only `authserver:manage-users`. It can create, change, disable and delete users, put them in groups, and grant them `manage-account` and your own permissions, but it can never act on an administrator or make anyone one. ## Next steps [Scopes](https://goiabada.dev/reference/api/scopes/): Which scope each operation accepts. [Errors](https://goiabada.dev/reference/api/errors/): The format of every error answer. # Errors Source: https://goiabada.dev/reference/api/errors/ This page helps you handle the errors the Admin API and the Account API answer. Every error comes back as a JSON body with the same two fields: ```json { "error_code": "VALIDATION_ERROR", "error_description": "SMTP host is required." } ``` Decide what to do from the HTTP status first: a `4xx` is something your request can fix, and a `5xx` is a problem on the server. Then read `error_code` for the specific case, and show or log `error_description`. ## The fields - **`error_code`** is a stable identifier for the failure. It’s either an `UPPER_SNAKE` code from the table below, or, for a refused field the auth server can describe in several languages, a catalog key in dotted lowercase, such as `validator.email.invalid_format`. - **`error_description`** is a sentence for people. For a catalog key it’s in the request’s language; otherwise it’s in English. Don’t match on it: the wording can change, the code won’t. The protocol endpoints, `/auth/authorize`, `/auth/token`, `/connect/register` and `/userinfo`, answer errors in the format their standards define, not this one. [Authorize](https://goiabada.dev/reference/endpoints/authorize/), [Token](https://goiabada.dev/reference/endpoints/token/) and [UserInfo](https://goiabada.dev/reference/endpoints/userinfo/) cover theirs, and [Dynamic client registration](https://goiabada.dev/reference/endpoints/dynamic-client-registration/#errors) the registration endpoint’s. ## Error codes This is every `UPPER_SNAKE` code the two APIs answer. | Code | Status | Meaning | | - | - | - | | `VALIDATION_ERROR` | `400` | A value was refused. `error_description` says which and why. | | `INVALID_REQUEST_BODY` | `400` | The body isn’t the JSON the operation expects. | | `INVALID_REQUEST` | `400` | The `Authorization` header or the `access_token` parameter was sent twice, the token was sent two ways at once, or a form body doesn’t parse. | | `EMAIL_TOO_LONG` | `400` | The new user’s email address is longer than 60 characters. | | `VALUE_TOO_LONG` | `400` | A user attribute’s value is longer than 250 characters. | | `FILE_TOO_LARGE` | `400` | The uploaded picture or logo is over the size limit, or the form doesn’t parse. | | `NO_FILE` | `400` | The upload has no `picture` field. | | `AUTHENTICATION_FAILED` | `400` | The password sent to confirm a password change, an email change, or turning two-factor authentication on or off is wrong. For two-factor authentication, a missing password answers it too. | | `OTP_CODE_REQUIRED` | `400` | Two-factor authentication was turned on without a code. | | `INVALID_OTP_CODE` | `400` | The code isn’t six digits, or it doesn’t match the authenticator. | | `OTP_ENROLLMENT_NOT_PENDING` | `400` | No enrollment is pending, or it expired. Start a new one with `GET /api/v1/account/otp/enrollment`. | | `SECRET_KEY_NOT_ACCEPTED` | `400` | The request sent a `secretKey`. Start an enrollment and send only the code from the authenticator. | | `OTP_ALREADY_ENABLED` | `400` | Two-factor authentication is already on. | | `OTP_NOT_ENABLED` | `400` | Two-factor authentication is already off. | | `INVALID_OR_EXPIRED_VERIFICATION_CODE` | `400` | The email verification code is wrong or has expired. | | `SMTP_NOT_ENABLED` | `400` | The operation needs to send email, and SMTP is off: a test email, or sending or checking an email verification code. | | `SEND_FAILED` | `400` | The test email couldn’t be sent. `error_description` says what to check. | | `ACCESS_TOKEN_REQUIRED` | `401` | No access token was sent. | | `INVALID_TOKEN` | `401` | The token is malformed, expired, not an access token for `authserver`, or its user or session is gone. | | `INVALID_SESSION` | `401` | On `POST /api/v1/account/logout-request`: the token carries no session id (`sid`), its session is gone, or the token’s client isn’t part of that session. | | `INSUFFICIENT_SCOPE` | `403` | The token carries none of the scopes the operation accepts. The `WWW-Authenticate` header says `error="insufficient_scope"`. | | `USER_CONTEXT_REQUIRED` | `403` | An Account API call was made with a token that wasn’t issued for a user, such as one from the client credentials flow, carrying `authserver:manage-account`. Without it, the answer is `INSUFFICIENT_SCOPE`. | | `FORBIDDEN` | `403` | The token is valid, but the consent or session it names belongs to another user. | | `MANAGE_SCOPE_REQUIRED` | `403` | The request acts on an administrator or an administrative permission, which only `authserver:manage` may do. See [Administrators](https://goiabada.dev/reference/api/administrators/). | | `NOT_FOUND` | `404` | Nothing has that id. | | `CONCURRENT_UPDATE` | `409` | Another request changed what yours was based on. Nothing was saved: read it again and retry. | | `EMAIL_ALREADY_EXISTS` | `409` | Another user already has that email address. | | `LAST_ADMINISTRATOR` | `409` | The change would leave no enabled user holding `authserver:manage`. Grant it to another user first. | | `ROTATION_IN_PROGRESS` | `409` | Another signing key rotation won the race, and this call rotated nothing. Don’t retry: the rotation it ran into already happened. | | `TOO_MANY_REQUESTS` | `429` | A rate limit was reached. The `Retry-After` header says how many seconds to wait. | | `INTERNAL_SERVER_ERROR` | `500` | The request failed inside the server. `error_description` ends with a request id an operator can find in the server’s log. Retrying is reasonable. | | `KEY_SET_INCOMPLETE` | `500` | A signing key rotation found no current or no next key. That’s a deployment problem a retry won’t clear. `error_description` ends with a request id, as for `INTERNAL_SERVER_ERROR`. | ## Saving a whole list An operation that replaces a whole list, such as a client’s redirect URIs or a user’s groups, also takes the list as you last read it, in a field named `expected` and the list’s name, such as `expectedRedirectURIs`. When the stored list is no longer that one, because someone else saved it in between, the save is answered `409` with `CONCURRENT_UPDATE` and changes nothing. Read the list again, apply your change to it, and save again. ## Next steps [Authentication](https://goiabada.dev/reference/api/authentication/): Get a token and call the API. [Admin API](https://goiabada.dev/reference/api/admin/): Every Admin API operation. # Overview Source: https://goiabada.dev/reference/api/admin/ ## Admin API 1.0.0 The REST API of the Goiabada auth server. ## Authentication Every operation under `/api/v1` requires a valid JWT access token in the `Authorization` header: ``` Authorization: Bearer ``` `getClientLogoImage`, at `/client/logo/{clientIdentifier}` outside `/api/v1`, is public and marked `security: []`. ## API Groups - **Admin API** (`/api/v1/admin/*`): administrative control. Each operation accepts any one of a small set of scopes rather than a single one: reads accept `authserver:admin-read` alongside the domain’s own scope, and writes accept the domain scope (`authserver:manage-users`, `authserver:manage-clients` or `authserver:manage-settings`). `authserver:manage` is accepted everywhere. `getClientSecret` is the one read `authserver:admin-read` does not reach: it answers a credential, so it accepts `authserver:manage-clients` or `authserver:manage`. A token from the client credentials flow is accepted here. - **Account API** (`/api/v1/account/*`): Self-service account management. Requires `authserver:manage-account` scope, **and** a token issued for a user: these operations resolve the acting user from `sub`, so a client credentials token is refused with 403 and `error_code` `USER_CONTEXT_REQUIRED`. ## Caching Every response from an operation in either API group carries these headers, error responses and the token and scope refusals included: ``` Cache-Control: no-store Pragma: no-cache ``` Do not cache these responses. Two operations return a credential in the body: `getClientSecret` returns the client secret decrypted, and `getAccountOTPEnrollment` returns a new authenticator’s setup key. The bearer token on the request does not make a cached copy safe: a private cache may still store a response it authenticated. A few responses under these paths are answered before an operation is selected and carry neither header: a CORS preflight, and the errors returned when the server cannot read its own settings or the caller’s session. None of them carries a credential, and none of them is described by this document. The headers are not declared per operation, since every operation sends them. Goiabada - [https://goiabada.dev](https://goiabada.dev/) Information - License: [MIT](https://opensource.org/licenses/MIT) - OpenAPI version: `3.0.3` ## Operations GET [/api/v1/admin/users/search](https://goiabada.dev/reference/api/admin/operations/searchusers/) POST [/api/v1/admin/users/create](https://goiabada.dev/reference/api/admin/operations/createuser/) GET [/api/v1/admin/users/{id}](https://goiabada.dev/reference/api/admin/operations/getuser/) DELETE [/api/v1/admin/users/{id}](https://goiabada.dev/reference/api/admin/operations/deleteuser/) PUT [/api/v1/admin/users/{id}/profile](https://goiabada.dev/reference/api/admin/operations/updateuserprofile/) PUT [/api/v1/admin/users/{id}/email](https://goiabada.dev/reference/api/admin/operations/updateuseremail/) POST [/api/v1/admin/users/{id}/email/verification-code](https://goiabada.dev/reference/api/admin/operations/generateuseremailverificationcode/) PUT [/api/v1/admin/users/{id}/phone](https://goiabada.dev/reference/api/admin/operations/updateuserphone/) PUT [/api/v1/admin/users/{id}/address](https://goiabada.dev/reference/api/admin/operations/updateuseraddress/) PUT [/api/v1/admin/users/{id}/password](https://goiabada.dev/reference/api/admin/operations/updateuserpassword/) PUT [/api/v1/admin/users/{id}/enabled](https://goiabada.dev/reference/api/admin/operations/updateuserenabled/) PUT [/api/v1/admin/users/{id}/otp](https://goiabada.dev/reference/api/admin/operations/updateuserotp/) GET [/api/v1/admin/users/{id}/profile-picture](https://goiabada.dev/reference/api/admin/operations/getuserprofilepictureinfo/) POST [/api/v1/admin/users/{id}/profile-picture](https://goiabada.dev/reference/api/admin/operations/uploaduserprofilepicture/) DELETE [/api/v1/admin/users/{id}/profile-picture](https://goiabada.dev/reference/api/admin/operations/deleteuserprofilepicture/) GET [/api/v1/admin/users/{id}/groups](https://goiabada.dev/reference/api/admin/operations/getusergroups/) PUT [/api/v1/admin/users/{id}/groups](https://goiabada.dev/reference/api/admin/operations/updateusergroups/) GET [/api/v1/admin/users/{id}/attributes](https://goiabada.dev/reference/api/admin/operations/getuserattributes/) POST [/api/v1/admin/user-attributes](https://goiabada.dev/reference/api/admin/operations/createuserattribute/) GET [/api/v1/admin/user-attributes/{id}](https://goiabada.dev/reference/api/admin/operations/getuserattribute/) PUT [/api/v1/admin/user-attributes/{id}](https://goiabada.dev/reference/api/admin/operations/updateuserattribute/) DELETE [/api/v1/admin/user-attributes/{id}](https://goiabada.dev/reference/api/admin/operations/deleteuserattribute/) GET [/api/v1/admin/users/{id}/sessions](https://goiabada.dev/reference/api/admin/operations/getusersessions/) GET [/api/v1/admin/user-sessions/{sessionIdentifier}](https://goiabada.dev/reference/api/admin/operations/getusersession/) DELETE [/api/v1/admin/user-sessions/{sessionIdentifier}](https://goiabada.dev/reference/api/admin/operations/deleteusersession/) GET [/api/v1/admin/users/{id}/consents](https://goiabada.dev/reference/api/admin/operations/getuserconsents/) DELETE [/api/v1/admin/user-consents/{id}](https://goiabada.dev/reference/api/admin/operations/deleteuserconsent/) GET [/api/v1/admin/groups](https://goiabada.dev/reference/api/admin/operations/getgroups/) POST [/api/v1/admin/groups](https://goiabada.dev/reference/api/admin/operations/creategroup/) GET [/api/v1/admin/groups/search](https://goiabada.dev/reference/api/admin/operations/searchgroupswithpermissionannotation/) GET [/api/v1/admin/groups/{id}](https://goiabada.dev/reference/api/admin/operations/getgroup/) PUT [/api/v1/admin/groups/{id}](https://goiabada.dev/reference/api/admin/operations/updategroup/) DELETE [/api/v1/admin/groups/{id}](https://goiabada.dev/reference/api/admin/operations/deletegroup/) GET [/api/v1/admin/groups/{id}/members](https://goiabada.dev/reference/api/admin/operations/getgroupmembers/) POST [/api/v1/admin/groups/{id}/members](https://goiabada.dev/reference/api/admin/operations/addgroupmember/) DELETE [/api/v1/admin/groups/{id}/members/{userId}](https://goiabada.dev/reference/api/admin/operations/removegroupmember/) GET [/api/v1/admin/groups/{id}/attributes](https://goiabada.dev/reference/api/admin/operations/getgroupattributes/) POST [/api/v1/admin/group-attributes](https://goiabada.dev/reference/api/admin/operations/creategroupattribute/) GET [/api/v1/admin/group-attributes/{id}](https://goiabada.dev/reference/api/admin/operations/getgroupattribute/) PUT [/api/v1/admin/group-attributes/{id}](https://goiabada.dev/reference/api/admin/operations/updategroupattribute/) DELETE [/api/v1/admin/group-attributes/{id}](https://goiabada.dev/reference/api/admin/operations/deletegroupattribute/) GET [/api/v1/admin/resources](https://goiabada.dev/reference/api/admin/operations/getresources/) POST [/api/v1/admin/resources](https://goiabada.dev/reference/api/admin/operations/createresource/) GET [/api/v1/admin/resources/{id}](https://goiabada.dev/reference/api/admin/operations/getresource/) PUT [/api/v1/admin/resources/{id}](https://goiabada.dev/reference/api/admin/operations/updateresource/) DELETE [/api/v1/admin/resources/{id}](https://goiabada.dev/reference/api/admin/operations/deleteresource/) GET [/api/v1/admin/resources/{id}/permissions](https://goiabada.dev/reference/api/admin/operations/getresourcepermissions/) PUT [/api/v1/admin/resources/{id}/permissions](https://goiabada.dev/reference/api/admin/operations/updateresourcepermissions/) GET [/api/v1/admin/users/{id}/permissions](https://goiabada.dev/reference/api/admin/operations/getuserpermissions/) PUT [/api/v1/admin/users/{id}/permissions](https://goiabada.dev/reference/api/admin/operations/updateuserpermissions/) GET [/api/v1/admin/groups/{id}/permissions](https://goiabada.dev/reference/api/admin/operations/getgrouppermissions/) PUT [/api/v1/admin/groups/{id}/permissions](https://goiabada.dev/reference/api/admin/operations/updategrouppermissions/) GET [/api/v1/admin/permissions/{id}/users](https://goiabada.dev/reference/api/admin/operations/getusersbypermission/) GET [/api/v1/admin/clients](https://goiabada.dev/reference/api/admin/operations/getclients/) POST [/api/v1/admin/clients](https://goiabada.dev/reference/api/admin/operations/createclient/) GET [/api/v1/admin/clients/{id}](https://goiabada.dev/reference/api/admin/operations/getclient/) PUT [/api/v1/admin/clients/{id}](https://goiabada.dev/reference/api/admin/operations/updateclientsettings/) DELETE [/api/v1/admin/clients/{id}](https://goiabada.dev/reference/api/admin/operations/deleteclient/) GET [/api/v1/admin/clients/{id}/secret](https://goiabada.dev/reference/api/admin/operations/getclientsecret/) PUT [/api/v1/admin/clients/{id}/authentication](https://goiabada.dev/reference/api/admin/operations/updateclientauthentication/) PUT [/api/v1/admin/clients/{id}/oauth2-flows](https://goiabada.dev/reference/api/admin/operations/updateclientoauth2flows/) PUT [/api/v1/admin/clients/{id}/redirect-uris](https://goiabada.dev/reference/api/admin/operations/updateclientredirecturis/) PUT [/api/v1/admin/clients/{id}/web-origins](https://goiabada.dev/reference/api/admin/operations/updateclientweborigins/) PUT [/api/v1/admin/clients/{id}/tokens](https://goiabada.dev/reference/api/admin/operations/updateclienttokens/) GET [/api/v1/admin/clients/{id}/permissions](https://goiabada.dev/reference/api/admin/operations/getclientpermissions/) PUT [/api/v1/admin/clients/{id}/permissions](https://goiabada.dev/reference/api/admin/operations/updateclientpermissions/) PUT [/api/v1/admin/clients/{id}/administrative-scopes](https://goiabada.dev/reference/api/admin/operations/updateclientadministrativescopes/) GET [/api/v1/admin/clients/{id}/sessions](https://goiabada.dev/reference/api/admin/operations/getclientsessions/) GET [/api/v1/admin/clients/{id}/logo](https://goiabada.dev/reference/api/admin/operations/getclientlogoinfo/) POST [/api/v1/admin/clients/{id}/logo](https://goiabada.dev/reference/api/admin/operations/uploadclientlogo/) DELETE [/api/v1/admin/clients/{id}/logo](https://goiabada.dev/reference/api/admin/operations/deleteclientlogo/) GET [/client/logo/{clientIdentifier}](https://goiabada.dev/reference/api/admin/operations/getclientlogoimage/) GET [/api/v1/admin/audit-logs](https://goiabada.dev/reference/api/admin/operations/getauditlogs/) GET [/api/v1/admin/audit-logs/event-types](https://goiabada.dev/reference/api/admin/operations/getauditeventtypes/) GET [/api/v1/admin/settings/general](https://goiabada.dev/reference/api/admin/operations/getsettingsgeneral/) PUT [/api/v1/admin/settings/general](https://goiabada.dev/reference/api/admin/operations/updatesettingsgeneral/) GET [/api/v1/admin/settings/email](https://goiabada.dev/reference/api/admin/operations/getsettingsemail/) PUT [/api/v1/admin/settings/email](https://goiabada.dev/reference/api/admin/operations/updatesettingsemail/) POST [/api/v1/admin/settings/email/send-test](https://goiabada.dev/reference/api/admin/operations/sendtestemail/) GET [/api/v1/admin/settings/sessions](https://goiabada.dev/reference/api/admin/operations/getsettingssessions/) PUT [/api/v1/admin/settings/sessions](https://goiabada.dev/reference/api/admin/operations/updatesettingssessions/) GET [/api/v1/admin/settings/ui-theme](https://goiabada.dev/reference/api/admin/operations/getsettingsuitheme/) PUT [/api/v1/admin/settings/ui-theme](https://goiabada.dev/reference/api/admin/operations/updatesettingsuitheme/) GET [/api/v1/admin/settings/tokens](https://goiabada.dev/reference/api/admin/operations/getsettingstokens/) PUT [/api/v1/admin/settings/tokens](https://goiabada.dev/reference/api/admin/operations/updatesettingstokens/) GET [/api/v1/admin/settings/audit-logs](https://goiabada.dev/reference/api/admin/operations/getsettingsauditlogs/) PUT [/api/v1/admin/settings/audit-logs](https://goiabada.dev/reference/api/admin/operations/updatesettingsauditlogs/) GET [/api/v1/admin/settings/keys](https://goiabada.dev/reference/api/admin/operations/getsettingskeys/) POST [/api/v1/admin/settings/keys/rotate](https://goiabada.dev/reference/api/admin/operations/rotatesettingskeys/) DELETE [/api/v1/admin/settings/keys/{id}](https://goiabada.dev/reference/api/admin/operations/deletesettingskey/) GET [/api/v1/admin/phone-countries](https://goiabada.dev/reference/api/admin/operations/getphonecountries/) ## Authentication ### BearerAuth JWT access token. Admin API: any one of `authserver:admin-read`, the domain scope for the operation (`authserver:manage-users`, `authserver:manage-clients`, `authserver:manage-settings`) or `authserver:manage`. A client credentials token is accepted. Reads accept `authserver:admin-read`, except `getClientSecret`; writes need the domain scope. The granular scopes stop short of administrators. An administrator is a user, group or client holding any of the six administrative permissions on the `authserver` resource (`manage`, `admin-read`, `manage-users`, `manage-clients`, `manage-settings`, `browser-sessions`), a user directly or through any of its groups, the admin console’s own client whatever it holds, and a client allowed to request the administrative scopes. Only `authserver:manage` creates an administrator, changes one, or changes what reaches one: a granular scope reaching an operation is still refused, with 403 MANAGE_SCOPE_REQUIRED, when the request grants or revokes an administrative permission (directly, or by moving a user into or out of a group holding one, or by deleting such a group), writes to an administrator, switches whether a client may request the administrative scopes, reads an administrator client’s secret, changes the email or audit-log settings, or changes an administrative permission’s description. It keeps full control of everyone else, and still reads administrators. `authserver:manage` alone meets 409 LAST_ADMINISTRATOR, on the operations that could leave no enabled user holding it. Account API: scope `authserver:manage-account`, on a token issued for a user. A client credentials token carries no `auth_time` claim and is refused with 403 `USER_CONTEXT_REQUIRED`. Browser sessions: scope `authserver:browser-sessions`, and only that scope. It is deliberately not one of the manage-\* scopes, so holding the admin console’s client secret is not a way to drive the admin API with no user present. **Security scheme type: **http **Bearer format: **JWT # Overview Source: https://goiabada.dev/reference/api/admin/operations/tags/users/ ## Users User management (Admin API) ## Operations GET [/api/v1/admin/users/search](https://goiabada.dev/reference/api/admin/operations/searchusers/) POST [/api/v1/admin/users/create](https://goiabada.dev/reference/api/admin/operations/createuser/) GET [/api/v1/admin/users/{id}](https://goiabada.dev/reference/api/admin/operations/getuser/) DELETE [/api/v1/admin/users/{id}](https://goiabada.dev/reference/api/admin/operations/deleteuser/) PUT [/api/v1/admin/users/{id}/profile](https://goiabada.dev/reference/api/admin/operations/updateuserprofile/) PUT [/api/v1/admin/users/{id}/email](https://goiabada.dev/reference/api/admin/operations/updateuseremail/) POST [/api/v1/admin/users/{id}/email/verification-code](https://goiabada.dev/reference/api/admin/operations/generateuseremailverificationcode/) PUT [/api/v1/admin/users/{id}/phone](https://goiabada.dev/reference/api/admin/operations/updateuserphone/) PUT [/api/v1/admin/users/{id}/address](https://goiabada.dev/reference/api/admin/operations/updateuseraddress/) PUT [/api/v1/admin/users/{id}/password](https://goiabada.dev/reference/api/admin/operations/updateuserpassword/) PUT [/api/v1/admin/users/{id}/enabled](https://goiabada.dev/reference/api/admin/operations/updateuserenabled/) PUT [/api/v1/admin/users/{id}/otp](https://goiabada.dev/reference/api/admin/operations/updateuserotp/) GET [/api/v1/admin/users/{id}/profile-picture](https://goiabada.dev/reference/api/admin/operations/getuserprofilepictureinfo/) POST [/api/v1/admin/users/{id}/profile-picture](https://goiabada.dev/reference/api/admin/operations/uploaduserprofilepicture/) DELETE [/api/v1/admin/users/{id}/profile-picture](https://goiabada.dev/reference/api/admin/operations/deleteuserprofilepicture/) GET [/api/v1/admin/users/{id}/groups](https://goiabada.dev/reference/api/admin/operations/getusergroups/) PUT [/api/v1/admin/users/{id}/groups](https://goiabada.dev/reference/api/admin/operations/updateusergroups/) # Search users Source: https://goiabada.dev/reference/api/admin/operations/searchusers/ GET /api/v1/admin/users/search Shell / cURL ```sh curl --request GET \ --url 'http://localhost:9090/api/v1/admin/users/search?page=1&size=10' \ --header 'Authorization: Bearer ' ``` - Auth server base URL{baseUrl}/api/v1/admin/users/search Search for users with pagination. Each returned user can be annotated against one group or one permission. The two annotations are mutually exclusive: sending both is refused with 400. ## Authorizations - **[BearerAuth](https://goiabada.dev/reference/api/admin/#bearerauth)** ## Parameters ### Query Parameters **query** string Search term (matches email, username, name) **annotateGroupMembership** integer format: int64 Group id to annotate each returned user against. Selects the SearchUsersWithGroupAnnotationResponse shape. A group id that matches nothing is answered 404. **annotatePermissionId** integer format: int64 Permission id to annotate each returned user against. Selects the SearchUsersWithPermissionAnnotationResponse shape. A permission id that matches nothing is answered 404. **page** integer default: 1 Page number (default 1) **size** integer default: 10 Results per page (default 10) ## Responses ### 200 Search results. The shape depends on the annotation parameter: neither gives SearchUsersResponse, annotateGroupMembership gives SearchUsersWithGroupAnnotationResponse, annotatePermissionId gives SearchUsersWithPermissionAnnotationResponse. application/json Any of: **object** object **users** required Null when this page has no users. Array\ nullable object **id** required integer format: int64 **createdAt** required string format: date-time nullable **updatedAt** required string format: date-time nullable **enabled** required boolean **subject** required string format: uuid **username** required string **givenName** required string **middleName** required string **familyName** required string **nickname** required string **website** required string **gender** required “female”, “male” or “other”, or empty when unset string **email** required string format: email **emailVerified** required boolean **zoneInfoCountryName** required string **zoneInfo** required string **locale** required string **birthDate** required string format: date-time nullable **phoneNumberCountryUniqueId** required string **phoneNumberCountryCallingCode** required string **phoneNumber** required string **phoneNumberVerified** required boolean **addressLine1** required string **addressLine2** required string **addressLocality** required string **addressRegion** required string **addressPostalCode** required string **addressCountry** required ISO 3166-1 alpha-2 country code (e.g. US, BR), or empty string. string /^(\[A-Z]{2})?$/ **otpEnabled** required boolean **total** required integer **page** required integer **size** required integer **query** required string **object** Answered by searchUsers when annotateGroupMembership is set. object **users** required Array object **id** required integer format: int64 **createdAt** required string format: date-time nullable **updatedAt** required string format: date-time nullable **enabled** required boolean **subject** required string format: uuid **username** required string **givenName** required string **middleName** required string **familyName** required string **nickname** required string **website** required string **gender** required “female”, “male” or “other”, or empty when unset string **email** required string format: email **emailVerified** required boolean **zoneInfoCountryName** required string **zoneInfo** required string **locale** required string **birthDate** required string format: date-time nullable **phoneNumberCountryUniqueId** required string **phoneNumberCountryCallingCode** required string **phoneNumber** required string **phoneNumberVerified** required boolean **addressLine1** required string **addressLine2** required string **addressLocality** required string **addressRegion** required string **addressPostalCode** required string **addressCountry** required ISO 3166-1 alpha-2 country code (e.g. US, BR), or empty string. string /^(\[A-Z]{2})?$/ **otpEnabled** required boolean **inGroup** required Whether this user belongs to the annotated group boolean **total** required integer **page** required integer **size** required integer **query** required string **object** Answered by searchUsers when annotatePermissionId is set. object **users** required Array object **id** required integer format: int64 **createdAt** required string format: date-time nullable **updatedAt** required string format: date-time nullable **enabled** required boolean **subject** required string format: uuid **username** required string **givenName** required string **middleName** required string **familyName** required string **nickname** required string **website** required string **gender** required “female”, “male” or “other”, or empty when unset string **email** required string format: email **emailVerified** required boolean **zoneInfoCountryName** required string **zoneInfo** required string **locale** required string **birthDate** required string format: date-time nullable **phoneNumberCountryUniqueId** required string **phoneNumberCountryCallingCode** required string **phoneNumber** required string **phoneNumberVerified** required boolean **addressLine1** required string **addressLine2** required string **addressLocality** required string **addressRegion** required string **addressPostalCode** required string **addressCountry** required ISO 3166-1 alpha-2 country code (e.g. US, BR), or empty string. string /^(\[A-Z]{2})?$/ **otpEnabled** required boolean **hasPermission** required Whether this user has the annotated permission assigned boolean **total** required integer **page** required integer **size** required integer **query** required string #### Example generated ```json { "users": [ { "id": 1, "createdAt": "2026-04-15T12:00:00Z", "updatedAt": "2026-04-15T12:00:00Z", "enabled": true, "subject": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0", "username": "example", "givenName": "example", "middleName": "example", "familyName": "example", "nickname": "example", "website": "example", "gender": "example", "email": "hello@example.com", "emailVerified": true, "zoneInfoCountryName": "example", "zoneInfo": "example", "locale": "example", "birthDate": "2026-04-15T12:00:00Z", "phoneNumberCountryUniqueId": "example", "phoneNumberCountryCallingCode": "example", "phoneNumber": "example", "phoneNumberVerified": true, "addressLine1": "example", "addressLine2": "example", "addressLocality": "example", "addressRegion": "example", "addressPostalCode": "example", "addressCountry": "example", "otpEnabled": true } ], "total": 1, "page": 1, "size": 1, "query": "example" } ``` ### 400 Invalid request body or parameters application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge when token transport is malformed; ordinary validation errors omit it. ### 401 Missing or invalid bearer token application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on token refusals. An operation may also return a handler-level 401 without this header. ### 403 The token is valid but does not authorise this request. Four checks answer here, and a caller distinguishes them by error_code. INSUFFICIENT_SCOPE comes from the route’s authorisation middleware, before the handler runs: an Admin operation needs one of the scopes its route names, and an Account operation needs `authserver:manage-account`. USER_CONTEXT_REQUIRED comes from the same middleware on the Account operations, which also require the token to have been issued for a user: one obtained through the client credentials grant carries no auth_time claim and is refused here. FORBIDDEN comes from the handler, on the account endpoints that resolve a resource by id and then check it belongs to the caller, so a valid token for the wrong user reaches this rather than a 404. MANAGE_SCOPE_REQUIRED comes from the handler, on the Admin operations the administrative policy guards. Only `authserver:manage` creates an administrator, changes one, or changes what reaches one, so no granular scope will ever be enough: the remedy is a token holding `authserver:manage`, not requesting the route’s scope as for INSUFFICIENT_SCOPE. An administrator is a user, group or client holding any of the six administrative permissions (`manage`, `admin-read`, `manage-users`, `manage-clients`, `manage-settings`, `browser-sessions`), a user directly or through any of its groups, the admin console’s own client whatever it holds, and a client allowed to request the administrative scopes. A granular scope is refused here when it: - grants or revokes an administrative permission, directly or by moving a user into or out of a group that holds one, or deletes such a group; - writes to an administrator user (profile, credentials, enabled, sessions, consents, attributes, memberships, permissions, deletion), an administrative group (settings, attributes, members, permissions, deletion), or an administrator client (settings, authentication, flows, redirect URIs, web origins, token settings, permissions, logo, deletion); - switches whether a client may request the administrative scopes, on or off, on any client; - reads an administrator client’s secret; - changes the email or the audit-log settings; - changes an administrative permission’s description. Granular scopes still read administrators. The refusal is answered after the request’s 400 and 404, writes nothing, carries `WWW-Authenticate: Bearer realm="goiabada", error="insufficient_scope", error_description="...", scope="authserver:manage"`, and leaves an `administrator_change_refused` audit record. Every operation this document secures declares a 403. The public client logo endpoint, which carries `security: []` and authorises nothing, is the one that does not. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on scope and administrative-policy refusals; other forbidden responses omit it. ### 404 Resource not found application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` ### 500 The request failed inside the server, most often a storage error. error_code is INTERNAL_SERVER_ERROR: no detail about the failure, only the request id an operator can find the log line by, and retrying is reasonable. Key rotation’s 500 also answers KEY_SET_INCOMPLETE, which names its cause and no retry clears. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` # Create user Source: https://goiabada.dev/reference/api/admin/operations/createuser/ POST /api/v1/admin/users/create Shell / cURL ```sh curl --request POST \ --url http://localhost:9090/api/v1/admin/users/create \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "email": "hello@example.com", "emailVerified": true, "givenName": "example", "middleName": "example", "familyName": "example", "setPasswordType": "now", "password": "example" }' ``` - Auth server base URL{baseUrl}/api/v1/admin/users/create Create a new user as admin ## Authorizations - **[BearerAuth](https://goiabada.dev/reference/api/admin/#bearerauth)** ## Request Bodyrequired application/json object **email** required string format: email **emailVerified** boolean **givenName** string **middleName** string **familyName** string **setPasswordType** “now” to set the password immediately, “email” to send a setup link. Optional: absent is treated as “now”. A present value outside the two is refused with 400. string default: now Allowed values: now email **password** Required if setPasswordType is “now”, and required whatever setPasswordType says on a deployment with no SMTP configured, since the setup email cannot be sent. string ## Responses ### 201 User created application/json object **user** required object **id** required integer format: int64 **createdAt** required string format: date-time nullable **updatedAt** required string format: date-time nullable **enabled** required boolean **subject** required string format: uuid **username** required string **givenName** required string **middleName** required string **familyName** required string **nickname** required string **website** required string **gender** required “female”, “male” or “other”, or empty when unset string **email** required string format: email **emailVerified** required boolean **zoneInfoCountryName** required string **zoneInfo** required string **locale** required string **birthDate** required string format: date-time nullable **phoneNumberCountryUniqueId** required string **phoneNumberCountryCallingCode** required string **phoneNumber** required string **phoneNumberVerified** required boolean **addressLine1** required string **addressLine2** required string **addressLocality** required string **addressRegion** required string **addressPostalCode** required string **addressCountry** required ISO 3166-1 alpha-2 country code (e.g. US, BR), or empty string. string /^(\[A-Z]{2})?$/ **otpEnabled** required boolean #### Example generated ```json { "user": { "id": 1, "createdAt": "2026-04-15T12:00:00Z", "updatedAt": "2026-04-15T12:00:00Z", "enabled": true, "subject": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0", "username": "example", "givenName": "example", "middleName": "example", "familyName": "example", "nickname": "example", "website": "example", "gender": "example", "email": "hello@example.com", "emailVerified": true, "zoneInfoCountryName": "example", "zoneInfo": "example", "locale": "example", "birthDate": "2026-04-15T12:00:00Z", "phoneNumberCountryUniqueId": "example", "phoneNumberCountryCallingCode": "example", "phoneNumber": "example", "phoneNumberVerified": true, "addressLine1": "example", "addressLine2": "example", "addressLocality": "example", "addressRegion": "example", "addressPostalCode": "example", "addressCountry": "example", "otpEnabled": true } } ``` ### 400 Invalid request body or parameters application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge when token transport is malformed; ordinary validation errors omit it. ### 401 Missing or invalid bearer token application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on token refusals. An operation may also return a handler-level 401 without this header. ### 403 The token is valid but does not authorise this request. Four checks answer here, and a caller distinguishes them by error_code. INSUFFICIENT_SCOPE comes from the route’s authorisation middleware, before the handler runs: an Admin operation needs one of the scopes its route names, and an Account operation needs `authserver:manage-account`. USER_CONTEXT_REQUIRED comes from the same middleware on the Account operations, which also require the token to have been issued for a user: one obtained through the client credentials grant carries no auth_time claim and is refused here. FORBIDDEN comes from the handler, on the account endpoints that resolve a resource by id and then check it belongs to the caller, so a valid token for the wrong user reaches this rather than a 404. MANAGE_SCOPE_REQUIRED comes from the handler, on the Admin operations the administrative policy guards. Only `authserver:manage` creates an administrator, changes one, or changes what reaches one, so no granular scope will ever be enough: the remedy is a token holding `authserver:manage`, not requesting the route’s scope as for INSUFFICIENT_SCOPE. An administrator is a user, group or client holding any of the six administrative permissions (`manage`, `admin-read`, `manage-users`, `manage-clients`, `manage-settings`, `browser-sessions`), a user directly or through any of its groups, the admin console’s own client whatever it holds, and a client allowed to request the administrative scopes. A granular scope is refused here when it: - grants or revokes an administrative permission, directly or by moving a user into or out of a group that holds one, or deletes such a group; - writes to an administrator user (profile, credentials, enabled, sessions, consents, attributes, memberships, permissions, deletion), an administrative group (settings, attributes, members, permissions, deletion), or an administrator client (settings, authentication, flows, redirect URIs, web origins, token settings, permissions, logo, deletion); - switches whether a client may request the administrative scopes, on or off, on any client; - reads an administrator client’s secret; - changes the email or the audit-log settings; - changes an administrative permission’s description. Granular scopes still read administrators. The refusal is answered after the request’s 400 and 404, writes nothing, carries `WWW-Authenticate: Bearer realm="goiabada", error="insufficient_scope", error_description="...", scope="authserver:manage"`, and leaves an `administrator_change_refused` audit record. Every operation this document secures declares a 403. The public client logo endpoint, which carries `security: []` and authorises nothing, is the one that does not. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on scope and administrative-policy refusals; other forbidden responses omit it. ### 409 The email address is already registered. error_code is EMAIL_ALREADY_EXISTS. Answered both by the explicit duplicate check and by the user creator, which derives the username from the email. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` ### 500 The request failed inside the server, most often a storage error. error_code is INTERNAL_SERVER_ERROR: no detail about the failure, only the request id an operator can find the log line by, and retrying is reasonable. Key rotation’s 500 also answers KEY_SET_INCOMPLETE, which names its cause and no retry clears. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` # Get user Source: https://goiabada.dev/reference/api/admin/operations/getuser/ GET /api/v1/admin/users/{id} Shell / cURL ```sh curl --request GET \ --url http://localhost:9090/api/v1/admin/users/1 \ --header 'Authorization: Bearer ' ``` - Auth server base URL{baseUrl}/api/v1/admin/users/{id} Get one user’s own fields: profile, email, phone, address and whether OTP is enabled. Groups, permissions and attributes are not included; each has its own operation: `getUserGroups`, `getUserPermissions` and `getUserAttributes`. ## Authorizations - **[BearerAuth](https://goiabada.dev/reference/api/admin/#bearerauth)** ## Parameters ### Path Parameters **id** required integer format: int64 User ID ## Responses ### 200 User details application/json object **user** required object **id** required integer format: int64 **createdAt** required string format: date-time nullable **updatedAt** required string format: date-time nullable **enabled** required boolean **subject** required string format: uuid **username** required string **givenName** required string **middleName** required string **familyName** required string **nickname** required string **website** required string **gender** required “female”, “male” or “other”, or empty when unset string **email** required string format: email **emailVerified** required boolean **zoneInfoCountryName** required string **zoneInfo** required string **locale** required string **birthDate** required string format: date-time nullable **phoneNumberCountryUniqueId** required string **phoneNumberCountryCallingCode** required string **phoneNumber** required string **phoneNumberVerified** required boolean **addressLine1** required string **addressLine2** required string **addressLocality** required string **addressRegion** required string **addressPostalCode** required string **addressCountry** required ISO 3166-1 alpha-2 country code (e.g. US, BR), or empty string. string /^(\[A-Z]{2})?$/ **otpEnabled** required boolean #### Example generated ```json { "user": { "id": 1, "createdAt": "2026-04-15T12:00:00Z", "updatedAt": "2026-04-15T12:00:00Z", "enabled": true, "subject": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0", "username": "example", "givenName": "example", "middleName": "example", "familyName": "example", "nickname": "example", "website": "example", "gender": "example", "email": "hello@example.com", "emailVerified": true, "zoneInfoCountryName": "example", "zoneInfo": "example", "locale": "example", "birthDate": "2026-04-15T12:00:00Z", "phoneNumberCountryUniqueId": "example", "phoneNumberCountryCallingCode": "example", "phoneNumber": "example", "phoneNumberVerified": true, "addressLine1": "example", "addressLine2": "example", "addressLocality": "example", "addressRegion": "example", "addressPostalCode": "example", "addressCountry": "example", "otpEnabled": true } } ``` ### 400 Invalid request body or parameters application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge when token transport is malformed; ordinary validation errors omit it. ### 401 Missing or invalid bearer token application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on token refusals. An operation may also return a handler-level 401 without this header. ### 403 The token is valid but does not authorise this request. Four checks answer here, and a caller distinguishes them by error_code. INSUFFICIENT_SCOPE comes from the route’s authorisation middleware, before the handler runs: an Admin operation needs one of the scopes its route names, and an Account operation needs `authserver:manage-account`. USER_CONTEXT_REQUIRED comes from the same middleware on the Account operations, which also require the token to have been issued for a user: one obtained through the client credentials grant carries no auth_time claim and is refused here. FORBIDDEN comes from the handler, on the account endpoints that resolve a resource by id and then check it belongs to the caller, so a valid token for the wrong user reaches this rather than a 404. MANAGE_SCOPE_REQUIRED comes from the handler, on the Admin operations the administrative policy guards. Only `authserver:manage` creates an administrator, changes one, or changes what reaches one, so no granular scope will ever be enough: the remedy is a token holding `authserver:manage`, not requesting the route’s scope as for INSUFFICIENT_SCOPE. An administrator is a user, group or client holding any of the six administrative permissions (`manage`, `admin-read`, `manage-users`, `manage-clients`, `manage-settings`, `browser-sessions`), a user directly or through any of its groups, the admin console’s own client whatever it holds, and a client allowed to request the administrative scopes. A granular scope is refused here when it: - grants or revokes an administrative permission, directly or by moving a user into or out of a group that holds one, or deletes such a group; - writes to an administrator user (profile, credentials, enabled, sessions, consents, attributes, memberships, permissions, deletion), an administrative group (settings, attributes, members, permissions, deletion), or an administrator client (settings, authentication, flows, redirect URIs, web origins, token settings, permissions, logo, deletion); - switches whether a client may request the administrative scopes, on or off, on any client; - reads an administrator client’s secret; - changes the email or the audit-log settings; - changes an administrative permission’s description. Granular scopes still read administrators. The refusal is answered after the request’s 400 and 404, writes nothing, carries `WWW-Authenticate: Bearer realm="goiabada", error="insufficient_scope", error_description="...", scope="authserver:manage"`, and leaves an `administrator_change_refused` audit record. Every operation this document secures declares a 403. The public client logo endpoint, which carries `security: []` and authorises nothing, is the one that does not. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on scope and administrative-policy refusals; other forbidden responses omit it. ### 404 Resource not found application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` ### 500 The request failed inside the server, most often a storage error. error_code is INTERNAL_SERVER_ERROR: no detail about the failure, only the request id an operator can find the log line by, and retrying is reasonable. Key rotation’s 500 also answers KEY_SET_INCOMPLETE, which names its cause and no retry clears. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` # Delete user Source: https://goiabada.dev/reference/api/admin/operations/deleteuser/ DELETE /api/v1/admin/users/{id} Shell / cURL ```sh curl --request DELETE \ --url http://localhost:9090/api/v1/admin/users/1 \ --header 'Authorization: Bearer ' ``` - Auth server base URL{baseUrl}/api/v1/admin/users/{id} Delete a user by ID ## Authorizations - **[BearerAuth](https://goiabada.dev/reference/api/admin/#bearerauth)** ## Parameters ### Path Parameters **id** required integer format: int64 User ID ## Responses ### 200 User deleted application/json object **success** required boolean #### Example generated ```json { "success": true } ``` ### 400 Invalid request body or parameters application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge when token transport is malformed; ordinary validation errors omit it. ### 401 Missing or invalid bearer token application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on token refusals. An operation may also return a handler-level 401 without this header. ### 403 The token is valid but does not authorise this request. Four checks answer here, and a caller distinguishes them by error_code. INSUFFICIENT_SCOPE comes from the route’s authorisation middleware, before the handler runs: an Admin operation needs one of the scopes its route names, and an Account operation needs `authserver:manage-account`. USER_CONTEXT_REQUIRED comes from the same middleware on the Account operations, which also require the token to have been issued for a user: one obtained through the client credentials grant carries no auth_time claim and is refused here. FORBIDDEN comes from the handler, on the account endpoints that resolve a resource by id and then check it belongs to the caller, so a valid token for the wrong user reaches this rather than a 404. MANAGE_SCOPE_REQUIRED comes from the handler, on the Admin operations the administrative policy guards. Only `authserver:manage` creates an administrator, changes one, or changes what reaches one, so no granular scope will ever be enough: the remedy is a token holding `authserver:manage`, not requesting the route’s scope as for INSUFFICIENT_SCOPE. An administrator is a user, group or client holding any of the six administrative permissions (`manage`, `admin-read`, `manage-users`, `manage-clients`, `manage-settings`, `browser-sessions`), a user directly or through any of its groups, the admin console’s own client whatever it holds, and a client allowed to request the administrative scopes. A granular scope is refused here when it: - grants or revokes an administrative permission, directly or by moving a user into or out of a group that holds one, or deletes such a group; - writes to an administrator user (profile, credentials, enabled, sessions, consents, attributes, memberships, permissions, deletion), an administrative group (settings, attributes, members, permissions, deletion), or an administrator client (settings, authentication, flows, redirect URIs, web origins, token settings, permissions, logo, deletion); - switches whether a client may request the administrative scopes, on or off, on any client; - reads an administrator client’s secret; - changes the email or the audit-log settings; - changes an administrative permission’s description. Granular scopes still read administrators. The refusal is answered after the request’s 400 and 404, writes nothing, carries `WWW-Authenticate: Bearer realm="goiabada", error="insufficient_scope", error_description="...", scope="authserver:manage"`, and leaves an `administrator_change_refused` audit record. Every operation this document secures declares a 403. The public client logo endpoint, which carries `security: []` and authorises nothing, is the one that does not. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on scope and administrative-policy refusals; other forbidden responses omit it. ### 404 Resource not found application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` ### 409 The change would leave no enabled user holding `authserver:manage`, directly or through a group. error_code is LAST_ADMINISTRATOR; nothing was changed. Grant `authserver:manage` to another user first, then repeat the request. A client holding the permission does not count, and neither does a disabled user. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` ### 500 The request failed inside the server, most often a storage error. error_code is INTERNAL_SERVER_ERROR: no detail about the failure, only the request id an operator can find the log line by, and retrying is reasonable. Key rotation’s 500 also answers KEY_SET_INCOMPLETE, which names its cause and no retry clears. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` # Update user profile Source: https://goiabada.dev/reference/api/admin/operations/updateuserprofile/ PUT /api/v1/admin/users/{id}/profile Shell / cURL ```sh curl --request PUT \ --url http://localhost:9090/api/v1/admin/users/1/profile \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "username": "example", "givenName": "example", "middleName": "example", "familyName": "example", "nickname": "example", "website": "example", "gender": "example", "dateOfBirth": "example", "zoneInfoCountryName": "example", "zoneInfo": "example", "locale": "example" }' ``` - Auth server base URL{baseUrl}/api/v1/admin/users/{id}/profile Update user profile information ## Authorizations - **[BearerAuth](https://goiabada.dev/reference/api/admin/#bearerauth)** ## Parameters ### Path Parameters **id** required integer format: int64 User ID ## Request Bodyrequired application/json object **username** string **givenName** string **middleName** string **familyName** string **nickname** string **website** string **gender** “female”, “male” or “other”, as GET returns it, or “0”, “1” or “2” for the same three; stored as the word. Empty clears it string **dateOfBirth** Format YYYY-MM-DD string **zoneInfoCountryName** The country zoneInfo is listed under in the time zone table; sent with zoneInfo, or both empty string **zoneInfo** string **locale** string ### Example generated ```json { "username": "example", "givenName": "example", "middleName": "example", "familyName": "example", "nickname": "example", "website": "example", "gender": "example", "dateOfBirth": "example", "zoneInfoCountryName": "example", "zoneInfo": "example", "locale": "example" } ``` ## Responses ### 200 Profile updated application/json object **user** required object **id** required integer format: int64 **createdAt** required string format: date-time nullable **updatedAt** required string format: date-time nullable **enabled** required boolean **subject** required string format: uuid **username** required string **givenName** required string **middleName** required string **familyName** required string **nickname** required string **website** required string **gender** required “female”, “male” or “other”, or empty when unset string **email** required string format: email **emailVerified** required boolean **zoneInfoCountryName** required string **zoneInfo** required string **locale** required string **birthDate** required string format: date-time nullable **phoneNumberCountryUniqueId** required string **phoneNumberCountryCallingCode** required string **phoneNumber** required string **phoneNumberVerified** required boolean **addressLine1** required string **addressLine2** required string **addressLocality** required string **addressRegion** required string **addressPostalCode** required string **addressCountry** required ISO 3166-1 alpha-2 country code (e.g. US, BR), or empty string. string /^(\[A-Z]{2})?$/ **otpEnabled** required boolean #### Example generated ```json { "user": { "id": 1, "createdAt": "2026-04-15T12:00:00Z", "updatedAt": "2026-04-15T12:00:00Z", "enabled": true, "subject": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0", "username": "example", "givenName": "example", "middleName": "example", "familyName": "example", "nickname": "example", "website": "example", "gender": "example", "email": "hello@example.com", "emailVerified": true, "zoneInfoCountryName": "example", "zoneInfo": "example", "locale": "example", "birthDate": "2026-04-15T12:00:00Z", "phoneNumberCountryUniqueId": "example", "phoneNumberCountryCallingCode": "example", "phoneNumber": "example", "phoneNumberVerified": true, "addressLine1": "example", "addressLine2": "example", "addressLocality": "example", "addressRegion": "example", "addressPostalCode": "example", "addressCountry": "example", "otpEnabled": true } } ``` ### 400 Invalid request body or parameters application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge when token transport is malformed; ordinary validation errors omit it. ### 401 Missing or invalid bearer token application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on token refusals. An operation may also return a handler-level 401 without this header. ### 403 The token is valid but does not authorise this request. Four checks answer here, and a caller distinguishes them by error_code. INSUFFICIENT_SCOPE comes from the route’s authorisation middleware, before the handler runs: an Admin operation needs one of the scopes its route names, and an Account operation needs `authserver:manage-account`. USER_CONTEXT_REQUIRED comes from the same middleware on the Account operations, which also require the token to have been issued for a user: one obtained through the client credentials grant carries no auth_time claim and is refused here. FORBIDDEN comes from the handler, on the account endpoints that resolve a resource by id and then check it belongs to the caller, so a valid token for the wrong user reaches this rather than a 404. MANAGE_SCOPE_REQUIRED comes from the handler, on the Admin operations the administrative policy guards. Only `authserver:manage` creates an administrator, changes one, or changes what reaches one, so no granular scope will ever be enough: the remedy is a token holding `authserver:manage`, not requesting the route’s scope as for INSUFFICIENT_SCOPE. An administrator is a user, group or client holding any of the six administrative permissions (`manage`, `admin-read`, `manage-users`, `manage-clients`, `manage-settings`, `browser-sessions`), a user directly or through any of its groups, the admin console’s own client whatever it holds, and a client allowed to request the administrative scopes. A granular scope is refused here when it: - grants or revokes an administrative permission, directly or by moving a user into or out of a group that holds one, or deletes such a group; - writes to an administrator user (profile, credentials, enabled, sessions, consents, attributes, memberships, permissions, deletion), an administrative group (settings, attributes, members, permissions, deletion), or an administrator client (settings, authentication, flows, redirect URIs, web origins, token settings, permissions, logo, deletion); - switches whether a client may request the administrative scopes, on or off, on any client; - reads an administrator client’s secret; - changes the email or the audit-log settings; - changes an administrative permission’s description. Granular scopes still read administrators. The refusal is answered after the request’s 400 and 404, writes nothing, carries `WWW-Authenticate: Bearer realm="goiabada", error="insufficient_scope", error_description="...", scope="authserver:manage"`, and leaves an `administrator_change_refused` audit record. Every operation this document secures declares a 403. The public client logo endpoint, which carries `security: []` and authorises nothing, is the one that does not. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on scope and administrative-policy refusals; other forbidden responses omit it. ### 404 Resource not found application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` ### 500 The request failed inside the server, most often a storage error. error_code is INTERNAL_SERVER_ERROR: no detail about the failure, only the request id an operator can find the log line by, and retrying is reasonable. Key rotation’s 500 also answers KEY_SET_INCOMPLETE, which names its cause and no retry clears. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` # Update user email Source: https://goiabada.dev/reference/api/admin/operations/updateuseremail/ PUT /api/v1/admin/users/{id}/email Shell / cURL ```sh curl --request PUT \ --url http://localhost:9090/api/v1/admin/users/1/email \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "email": "hello@example.com", "emailVerified": true }' ``` - Auth server base URL{baseUrl}/api/v1/admin/users/{id}/email Update user email address and verification status. Clears any pending verification code, and any outstanding password reset code, so a reset link mailed to the previous address stops working. ## Authorizations - **[BearerAuth](https://goiabada.dev/reference/api/admin/#bearerauth)** ## Parameters ### Path Parameters **id** required integer format: int64 User ID ## Request Bodyrequired application/json object **email** string format: email **emailVerified** boolean ### Example generated ```json { "email": "hello@example.com", "emailVerified": true } ``` ## Responses ### 200 Email updated application/json object **user** required object **id** required integer format: int64 **createdAt** required string format: date-time nullable **updatedAt** required string format: date-time nullable **enabled** required boolean **subject** required string format: uuid **username** required string **givenName** required string **middleName** required string **familyName** required string **nickname** required string **website** required string **gender** required “female”, “male” or “other”, or empty when unset string **email** required string format: email **emailVerified** required boolean **zoneInfoCountryName** required string **zoneInfo** required string **locale** required string **birthDate** required string format: date-time nullable **phoneNumberCountryUniqueId** required string **phoneNumberCountryCallingCode** required string **phoneNumber** required string **phoneNumberVerified** required boolean **addressLine1** required string **addressLine2** required string **addressLocality** required string **addressRegion** required string **addressPostalCode** required string **addressCountry** required ISO 3166-1 alpha-2 country code (e.g. US, BR), or empty string. string /^(\[A-Z]{2})?$/ **otpEnabled** required boolean #### Example generated ```json { "user": { "id": 1, "createdAt": "2026-04-15T12:00:00Z", "updatedAt": "2026-04-15T12:00:00Z", "enabled": true, "subject": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0", "username": "example", "givenName": "example", "middleName": "example", "familyName": "example", "nickname": "example", "website": "example", "gender": "example", "email": "hello@example.com", "emailVerified": true, "zoneInfoCountryName": "example", "zoneInfo": "example", "locale": "example", "birthDate": "2026-04-15T12:00:00Z", "phoneNumberCountryUniqueId": "example", "phoneNumberCountryCallingCode": "example", "phoneNumber": "example", "phoneNumberVerified": true, "addressLine1": "example", "addressLine2": "example", "addressLocality": "example", "addressRegion": "example", "addressPostalCode": "example", "addressCountry": "example", "otpEnabled": true } } ``` ### 400 Invalid request body or parameters application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge when token transport is malformed; ordinary validation errors omit it. ### 401 Missing or invalid bearer token application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on token refusals. An operation may also return a handler-level 401 without this header. ### 403 The token is valid but does not authorise this request. Four checks answer here, and a caller distinguishes them by error_code. INSUFFICIENT_SCOPE comes from the route’s authorisation middleware, before the handler runs: an Admin operation needs one of the scopes its route names, and an Account operation needs `authserver:manage-account`. USER_CONTEXT_REQUIRED comes from the same middleware on the Account operations, which also require the token to have been issued for a user: one obtained through the client credentials grant carries no auth_time claim and is refused here. FORBIDDEN comes from the handler, on the account endpoints that resolve a resource by id and then check it belongs to the caller, so a valid token for the wrong user reaches this rather than a 404. MANAGE_SCOPE_REQUIRED comes from the handler, on the Admin operations the administrative policy guards. Only `authserver:manage` creates an administrator, changes one, or changes what reaches one, so no granular scope will ever be enough: the remedy is a token holding `authserver:manage`, not requesting the route’s scope as for INSUFFICIENT_SCOPE. An administrator is a user, group or client holding any of the six administrative permissions (`manage`, `admin-read`, `manage-users`, `manage-clients`, `manage-settings`, `browser-sessions`), a user directly or through any of its groups, the admin console’s own client whatever it holds, and a client allowed to request the administrative scopes. A granular scope is refused here when it: - grants or revokes an administrative permission, directly or by moving a user into or out of a group that holds one, or deletes such a group; - writes to an administrator user (profile, credentials, enabled, sessions, consents, attributes, memberships, permissions, deletion), an administrative group (settings, attributes, members, permissions, deletion), or an administrator client (settings, authentication, flows, redirect URIs, web origins, token settings, permissions, logo, deletion); - switches whether a client may request the administrative scopes, on or off, on any client; - reads an administrator client’s secret; - changes the email or the audit-log settings; - changes an administrative permission’s description. Granular scopes still read administrators. The refusal is answered after the request’s 400 and 404, writes nothing, carries `WWW-Authenticate: Bearer realm="goiabada", error="insufficient_scope", error_description="...", scope="authserver:manage"`, and leaves an `administrator_change_refused` audit record. Every operation this document secures declares a 403. The public client logo endpoint, which carries `security: []` and authorises nothing, is the one that does not. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on scope and administrative-policy refusals; other forbidden responses omit it. ### 404 Resource not found application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` ### 409 The email address was registered to another user between the duplicate check and the write. error_code is EMAIL_ALREADY_EXISTS. An address already taken when the request arrives is a 400 with error_code validator.email.already_registered. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` ### 500 The request failed inside the server, most often a storage error. error_code is INTERNAL_SERVER_ERROR: no detail about the failure, only the request id an operator can find the log line by, and retrying is reasonable. Key rotation’s 500 also answers KEY_SET_INCOMPLETE, which names its cause and no retry clears. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` # Generate email verification code Source: https://goiabada.dev/reference/api/admin/operations/generateuseremailverificationcode/ POST /api/v1/admin/users/{id}/email/verification-code Shell / cURL ```sh curl --request POST \ --url http://localhost:9090/api/v1/admin/users/1/email/verification-code \ --header 'Authorization: Bearer ' ``` - Auth server base URL{baseUrl}/api/v1/admin/users/{id}/email/verification-code Generate a new email verification code for a user and return it in the response. Every call generates a new code, and sets the user’s emailVerified flag to false. The code is 8 characters, four uppercase letters then four digits, and expires 5 minutes after it is generated, at `verificationCodeExpiresAt`. ## Authorizations - **[BearerAuth](https://goiabada.dev/reference/api/admin/#bearerauth)** ## Parameters ### Path Parameters **id** required integer format: int64 User ID ## Responses ### 200 Verification code generated application/json object **verificationCode** required string **verificationCodeExpiresAt** required string format: date-time **userId** required integer format: int64 **email** required string format: email #### Example generated ```json { "verificationCode": "example", "verificationCodeExpiresAt": "2026-04-15T12:00:00Z", "userId": 1, "email": "hello@example.com" } ``` ### 400 Invalid request body or parameters application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge when token transport is malformed; ordinary validation errors omit it. ### 401 Missing or invalid bearer token application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on token refusals. An operation may also return a handler-level 401 without this header. ### 403 The token is valid but does not authorise this request. Four checks answer here, and a caller distinguishes them by error_code. INSUFFICIENT_SCOPE comes from the route’s authorisation middleware, before the handler runs: an Admin operation needs one of the scopes its route names, and an Account operation needs `authserver:manage-account`. USER_CONTEXT_REQUIRED comes from the same middleware on the Account operations, which also require the token to have been issued for a user: one obtained through the client credentials grant carries no auth_time claim and is refused here. FORBIDDEN comes from the handler, on the account endpoints that resolve a resource by id and then check it belongs to the caller, so a valid token for the wrong user reaches this rather than a 404. MANAGE_SCOPE_REQUIRED comes from the handler, on the Admin operations the administrative policy guards. Only `authserver:manage` creates an administrator, changes one, or changes what reaches one, so no granular scope will ever be enough: the remedy is a token holding `authserver:manage`, not requesting the route’s scope as for INSUFFICIENT_SCOPE. An administrator is a user, group or client holding any of the six administrative permissions (`manage`, `admin-read`, `manage-users`, `manage-clients`, `manage-settings`, `browser-sessions`), a user directly or through any of its groups, the admin console’s own client whatever it holds, and a client allowed to request the administrative scopes. A granular scope is refused here when it: - grants or revokes an administrative permission, directly or by moving a user into or out of a group that holds one, or deletes such a group; - writes to an administrator user (profile, credentials, enabled, sessions, consents, attributes, memberships, permissions, deletion), an administrative group (settings, attributes, members, permissions, deletion), or an administrator client (settings, authentication, flows, redirect URIs, web origins, token settings, permissions, logo, deletion); - switches whether a client may request the administrative scopes, on or off, on any client; - reads an administrator client’s secret; - changes the email or the audit-log settings; - changes an administrative permission’s description. Granular scopes still read administrators. The refusal is answered after the request’s 400 and 404, writes nothing, carries `WWW-Authenticate: Bearer realm="goiabada", error="insufficient_scope", error_description="...", scope="authserver:manage"`, and leaves an `administrator_change_refused` audit record. Every operation this document secures declares a 403. The public client logo endpoint, which carries `security: []` and authorises nothing, is the one that does not. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on scope and administrative-policy refusals; other forbidden responses omit it. ### 404 Resource not found application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` ### 409 Another request changed the user’s address while the code was being generated. error_code is CONCURRENT_UPDATE; nothing was stored. Retry. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` ### 500 The request failed inside the server, most often a storage error. error_code is INTERNAL_SERVER_ERROR: no detail about the failure, only the request id an operator can find the log line by, and retrying is reasonable. Key rotation’s 500 also answers KEY_SET_INCOMPLETE, which names its cause and no retry clears. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` # Update user phone Source: https://goiabada.dev/reference/api/admin/operations/updateuserphone/ PUT /api/v1/admin/users/{id}/phone Shell / cURL ```sh curl --request PUT \ --url http://localhost:9090/api/v1/admin/users/1/phone \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "phoneCountryUniqueId": "example", "phoneNumber": "example", "phoneNumberVerified": true }' ``` - Auth server base URL{baseUrl}/api/v1/admin/users/{id}/phone Update user phone number ## Authorizations - **[BearerAuth](https://goiabada.dev/reference/api/admin/#bearerauth)** ## Parameters ### Path Parameters **id** required integer format: int64 User ID ## Request Bodyrequired application/json object **phoneCountryUniqueId** An entry’s uniqueId from GET /api/v1/admin/phone-countries, e.g. USA_0 string **phoneNumber** string **phoneNumberVerified** boolean ### Example generated ```json { "phoneCountryUniqueId": "example", "phoneNumber": "example", "phoneNumberVerified": true } ``` ## Responses ### 200 Phone updated application/json object **user** required object **id** required integer format: int64 **createdAt** required string format: date-time nullable **updatedAt** required string format: date-time nullable **enabled** required boolean **subject** required string format: uuid **username** required string **givenName** required string **middleName** required string **familyName** required string **nickname** required string **website** required string **gender** required “female”, “male” or “other”, or empty when unset string **email** required string format: email **emailVerified** required boolean **zoneInfoCountryName** required string **zoneInfo** required string **locale** required string **birthDate** required string format: date-time nullable **phoneNumberCountryUniqueId** required string **phoneNumberCountryCallingCode** required string **phoneNumber** required string **phoneNumberVerified** required boolean **addressLine1** required string **addressLine2** required string **addressLocality** required string **addressRegion** required string **addressPostalCode** required string **addressCountry** required ISO 3166-1 alpha-2 country code (e.g. US, BR), or empty string. string /^(\[A-Z]{2})?$/ **otpEnabled** required boolean #### Example generated ```json { "user": { "id": 1, "createdAt": "2026-04-15T12:00:00Z", "updatedAt": "2026-04-15T12:00:00Z", "enabled": true, "subject": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0", "username": "example", "givenName": "example", "middleName": "example", "familyName": "example", "nickname": "example", "website": "example", "gender": "example", "email": "hello@example.com", "emailVerified": true, "zoneInfoCountryName": "example", "zoneInfo": "example", "locale": "example", "birthDate": "2026-04-15T12:00:00Z", "phoneNumberCountryUniqueId": "example", "phoneNumberCountryCallingCode": "example", "phoneNumber": "example", "phoneNumberVerified": true, "addressLine1": "example", "addressLine2": "example", "addressLocality": "example", "addressRegion": "example", "addressPostalCode": "example", "addressCountry": "example", "otpEnabled": true } } ``` ### 400 Invalid request body or parameters application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge when token transport is malformed; ordinary validation errors omit it. ### 401 Missing or invalid bearer token application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on token refusals. An operation may also return a handler-level 401 without this header. ### 403 The token is valid but does not authorise this request. Four checks answer here, and a caller distinguishes them by error_code. INSUFFICIENT_SCOPE comes from the route’s authorisation middleware, before the handler runs: an Admin operation needs one of the scopes its route names, and an Account operation needs `authserver:manage-account`. USER_CONTEXT_REQUIRED comes from the same middleware on the Account operations, which also require the token to have been issued for a user: one obtained through the client credentials grant carries no auth_time claim and is refused here. FORBIDDEN comes from the handler, on the account endpoints that resolve a resource by id and then check it belongs to the caller, so a valid token for the wrong user reaches this rather than a 404. MANAGE_SCOPE_REQUIRED comes from the handler, on the Admin operations the administrative policy guards. Only `authserver:manage` creates an administrator, changes one, or changes what reaches one, so no granular scope will ever be enough: the remedy is a token holding `authserver:manage`, not requesting the route’s scope as for INSUFFICIENT_SCOPE. An administrator is a user, group or client holding any of the six administrative permissions (`manage`, `admin-read`, `manage-users`, `manage-clients`, `manage-settings`, `browser-sessions`), a user directly or through any of its groups, the admin console’s own client whatever it holds, and a client allowed to request the administrative scopes. A granular scope is refused here when it: - grants or revokes an administrative permission, directly or by moving a user into or out of a group that holds one, or deletes such a group; - writes to an administrator user (profile, credentials, enabled, sessions, consents, attributes, memberships, permissions, deletion), an administrative group (settings, attributes, members, permissions, deletion), or an administrator client (settings, authentication, flows, redirect URIs, web origins, token settings, permissions, logo, deletion); - switches whether a client may request the administrative scopes, on or off, on any client; - reads an administrator client’s secret; - changes the email or the audit-log settings; - changes an administrative permission’s description. Granular scopes still read administrators. The refusal is answered after the request’s 400 and 404, writes nothing, carries `WWW-Authenticate: Bearer realm="goiabada", error="insufficient_scope", error_description="...", scope="authserver:manage"`, and leaves an `administrator_change_refused` audit record. Every operation this document secures declares a 403. The public client logo endpoint, which carries `security: []` and authorises nothing, is the one that does not. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on scope and administrative-policy refusals; other forbidden responses omit it. ### 404 Resource not found application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` ### 500 The request failed inside the server, most often a storage error. error_code is INTERNAL_SERVER_ERROR: no detail about the failure, only the request id an operator can find the log line by, and retrying is reasonable. Key rotation’s 500 also answers KEY_SET_INCOMPLETE, which names its cause and no retry clears. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` # Update user address Source: https://goiabada.dev/reference/api/admin/operations/updateuseraddress/ PUT /api/v1/admin/users/{id}/address Shell / cURL ```sh curl --request PUT \ --url http://localhost:9090/api/v1/admin/users/1/address \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "addressLine1": "example", "addressLine2": "example", "addressLocality": "example", "addressRegion": "example", "addressPostalCode": "example", "addressCountry": "example" }' ``` - Auth server base URL{baseUrl}/api/v1/admin/users/{id}/address Update user address information ## Authorizations - **[BearerAuth](https://goiabada.dev/reference/api/admin/#bearerauth)** ## Parameters ### Path Parameters **id** required integer format: int64 User ID ## Request Bodyrequired application/json object **addressLine1** string **addressLine2** string **addressLocality** string **addressRegion** string **addressPostalCode** string **addressCountry** ISO 3166-1 alpha-2 country code (e.g. US, BR), or empty string. string /^(\[A-Z]{2})?$/ ### Example generated ```json { "addressLine1": "example", "addressLine2": "example", "addressLocality": "example", "addressRegion": "example", "addressPostalCode": "example", "addressCountry": "example" } ``` ## Responses ### 200 Address updated application/json object **user** required object **id** required integer format: int64 **createdAt** required string format: date-time nullable **updatedAt** required string format: date-time nullable **enabled** required boolean **subject** required string format: uuid **username** required string **givenName** required string **middleName** required string **familyName** required string **nickname** required string **website** required string **gender** required “female”, “male” or “other”, or empty when unset string **email** required string format: email **emailVerified** required boolean **zoneInfoCountryName** required string **zoneInfo** required string **locale** required string **birthDate** required string format: date-time nullable **phoneNumberCountryUniqueId** required string **phoneNumberCountryCallingCode** required string **phoneNumber** required string **phoneNumberVerified** required boolean **addressLine1** required string **addressLine2** required string **addressLocality** required string **addressRegion** required string **addressPostalCode** required string **addressCountry** required ISO 3166-1 alpha-2 country code (e.g. US, BR), or empty string. string /^(\[A-Z]{2})?$/ **otpEnabled** required boolean #### Example generated ```json { "user": { "id": 1, "createdAt": "2026-04-15T12:00:00Z", "updatedAt": "2026-04-15T12:00:00Z", "enabled": true, "subject": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0", "username": "example", "givenName": "example", "middleName": "example", "familyName": "example", "nickname": "example", "website": "example", "gender": "example", "email": "hello@example.com", "emailVerified": true, "zoneInfoCountryName": "example", "zoneInfo": "example", "locale": "example", "birthDate": "2026-04-15T12:00:00Z", "phoneNumberCountryUniqueId": "example", "phoneNumberCountryCallingCode": "example", "phoneNumber": "example", "phoneNumberVerified": true, "addressLine1": "example", "addressLine2": "example", "addressLocality": "example", "addressRegion": "example", "addressPostalCode": "example", "addressCountry": "example", "otpEnabled": true } } ``` ### 400 Invalid request body or parameters application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge when token transport is malformed; ordinary validation errors omit it. ### 401 Missing or invalid bearer token application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on token refusals. An operation may also return a handler-level 401 without this header. ### 403 The token is valid but does not authorise this request. Four checks answer here, and a caller distinguishes them by error_code. INSUFFICIENT_SCOPE comes from the route’s authorisation middleware, before the handler runs: an Admin operation needs one of the scopes its route names, and an Account operation needs `authserver:manage-account`. USER_CONTEXT_REQUIRED comes from the same middleware on the Account operations, which also require the token to have been issued for a user: one obtained through the client credentials grant carries no auth_time claim and is refused here. FORBIDDEN comes from the handler, on the account endpoints that resolve a resource by id and then check it belongs to the caller, so a valid token for the wrong user reaches this rather than a 404. MANAGE_SCOPE_REQUIRED comes from the handler, on the Admin operations the administrative policy guards. Only `authserver:manage` creates an administrator, changes one, or changes what reaches one, so no granular scope will ever be enough: the remedy is a token holding `authserver:manage`, not requesting the route’s scope as for INSUFFICIENT_SCOPE. An administrator is a user, group or client holding any of the six administrative permissions (`manage`, `admin-read`, `manage-users`, `manage-clients`, `manage-settings`, `browser-sessions`), a user directly or through any of its groups, the admin console’s own client whatever it holds, and a client allowed to request the administrative scopes. A granular scope is refused here when it: - grants or revokes an administrative permission, directly or by moving a user into or out of a group that holds one, or deletes such a group; - writes to an administrator user (profile, credentials, enabled, sessions, consents, attributes, memberships, permissions, deletion), an administrative group (settings, attributes, members, permissions, deletion), or an administrator client (settings, authentication, flows, redirect URIs, web origins, token settings, permissions, logo, deletion); - switches whether a client may request the administrative scopes, on or off, on any client; - reads an administrator client’s secret; - changes the email or the audit-log settings; - changes an administrative permission’s description. Granular scopes still read administrators. The refusal is answered after the request’s 400 and 404, writes nothing, carries `WWW-Authenticate: Bearer realm="goiabada", error="insufficient_scope", error_description="...", scope="authserver:manage"`, and leaves an `administrator_change_refused` audit record. Every operation this document secures declares a 403. The public client logo endpoint, which carries `security: []` and authorises nothing, is the one that does not. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on scope and administrative-policy refusals; other forbidden responses omit it. ### 404 Resource not found application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` ### 500 The request failed inside the server, most often a storage error. error_code is INTERNAL_SERVER_ERROR: no detail about the failure, only the request id an operator can find the log line by, and retrying is reasonable. Key rotation’s 500 also answers KEY_SET_INCOMPLETE, which names its cause and no retry clears. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` # Update user password Source: https://goiabada.dev/reference/api/admin/operations/updateuserpassword/ PUT /api/v1/admin/users/{id}/password Shell / cURL ```sh curl --request PUT \ --url http://localhost:9090/api/v1/admin/users/1/password \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "newPassword": "example" }' ``` - Auth server base URL{baseUrl}/api/v1/admin/users/{id}/password Set a new password for the user ## Authorizations - **[BearerAuth](https://goiabada.dev/reference/api/admin/#bearerauth)** ## Parameters ### Path Parameters **id** required integer format: int64 User ID ## Request Bodyrequired application/json object **newPassword** required string ### Example generated ```json { "newPassword": "example" } ``` ## Responses ### 200 Password updated application/json object **user** required object **id** required integer format: int64 **createdAt** required string format: date-time nullable **updatedAt** required string format: date-time nullable **enabled** required boolean **subject** required string format: uuid **username** required string **givenName** required string **middleName** required string **familyName** required string **nickname** required string **website** required string **gender** required “female”, “male” or “other”, or empty when unset string **email** required string format: email **emailVerified** required boolean **zoneInfoCountryName** required string **zoneInfo** required string **locale** required string **birthDate** required string format: date-time nullable **phoneNumberCountryUniqueId** required string **phoneNumberCountryCallingCode** required string **phoneNumber** required string **phoneNumberVerified** required boolean **addressLine1** required string **addressLine2** required string **addressLocality** required string **addressRegion** required string **addressPostalCode** required string **addressCountry** required ISO 3166-1 alpha-2 country code (e.g. US, BR), or empty string. string /^(\[A-Z]{2})?$/ **otpEnabled** required boolean #### Example generated ```json { "user": { "id": 1, "createdAt": "2026-04-15T12:00:00Z", "updatedAt": "2026-04-15T12:00:00Z", "enabled": true, "subject": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0", "username": "example", "givenName": "example", "middleName": "example", "familyName": "example", "nickname": "example", "website": "example", "gender": "example", "email": "hello@example.com", "emailVerified": true, "zoneInfoCountryName": "example", "zoneInfo": "example", "locale": "example", "birthDate": "2026-04-15T12:00:00Z", "phoneNumberCountryUniqueId": "example", "phoneNumberCountryCallingCode": "example", "phoneNumber": "example", "phoneNumberVerified": true, "addressLine1": "example", "addressLine2": "example", "addressLocality": "example", "addressRegion": "example", "addressPostalCode": "example", "addressCountry": "example", "otpEnabled": true } } ``` ### 400 Invalid request body or parameters application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge when token transport is malformed; ordinary validation errors omit it. ### 401 Missing or invalid bearer token application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on token refusals. An operation may also return a handler-level 401 without this header. ### 403 The token is valid but does not authorise this request. Four checks answer here, and a caller distinguishes them by error_code. INSUFFICIENT_SCOPE comes from the route’s authorisation middleware, before the handler runs: an Admin operation needs one of the scopes its route names, and an Account operation needs `authserver:manage-account`. USER_CONTEXT_REQUIRED comes from the same middleware on the Account operations, which also require the token to have been issued for a user: one obtained through the client credentials grant carries no auth_time claim and is refused here. FORBIDDEN comes from the handler, on the account endpoints that resolve a resource by id and then check it belongs to the caller, so a valid token for the wrong user reaches this rather than a 404. MANAGE_SCOPE_REQUIRED comes from the handler, on the Admin operations the administrative policy guards. Only `authserver:manage` creates an administrator, changes one, or changes what reaches one, so no granular scope will ever be enough: the remedy is a token holding `authserver:manage`, not requesting the route’s scope as for INSUFFICIENT_SCOPE. An administrator is a user, group or client holding any of the six administrative permissions (`manage`, `admin-read`, `manage-users`, `manage-clients`, `manage-settings`, `browser-sessions`), a user directly or through any of its groups, the admin console’s own client whatever it holds, and a client allowed to request the administrative scopes. A granular scope is refused here when it: - grants or revokes an administrative permission, directly or by moving a user into or out of a group that holds one, or deletes such a group; - writes to an administrator user (profile, credentials, enabled, sessions, consents, attributes, memberships, permissions, deletion), an administrative group (settings, attributes, members, permissions, deletion), or an administrator client (settings, authentication, flows, redirect URIs, web origins, token settings, permissions, logo, deletion); - switches whether a client may request the administrative scopes, on or off, on any client; - reads an administrator client’s secret; - changes the email or the audit-log settings; - changes an administrative permission’s description. Granular scopes still read administrators. The refusal is answered after the request’s 400 and 404, writes nothing, carries `WWW-Authenticate: Bearer realm="goiabada", error="insufficient_scope", error_description="...", scope="authserver:manage"`, and leaves an `administrator_change_refused` audit record. Every operation this document secures declares a 403. The public client logo endpoint, which carries `security: []` and authorises nothing, is the one that does not. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on scope and administrative-policy refusals; other forbidden responses omit it. ### 404 Resource not found application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` ### 500 The request failed inside the server, most often a storage error. error_code is INTERNAL_SERVER_ERROR: no detail about the failure, only the request id an operator can find the log line by, and retrying is reasonable. Key rotation’s 500 also answers KEY_SET_INCOMPLETE, which names its cause and no retry clears. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` # Enable/disable user Source: https://goiabada.dev/reference/api/admin/operations/updateuserenabled/ PUT /api/v1/admin/users/{id}/enabled Shell / cURL ```sh curl --request PUT \ --url http://localhost:9090/api/v1/admin/users/1/enabled \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "enabled": true }' ``` - Auth server base URL{baseUrl}/api/v1/admin/users/{id}/enabled Enable or disable a user account ## Authorizations - **[BearerAuth](https://goiabada.dev/reference/api/admin/#bearerauth)** ## Parameters ### Path Parameters **id** required integer format: int64 User ID ## Request Bodyrequired application/json object **enabled** boolean ### Example generated ```json { "enabled": true } ``` ## Responses ### 200 User status updated application/json object **user** required object **id** required integer format: int64 **createdAt** required string format: date-time nullable **updatedAt** required string format: date-time nullable **enabled** required boolean **subject** required string format: uuid **username** required string **givenName** required string **middleName** required string **familyName** required string **nickname** required string **website** required string **gender** required “female”, “male” or “other”, or empty when unset string **email** required string format: email **emailVerified** required boolean **zoneInfoCountryName** required string **zoneInfo** required string **locale** required string **birthDate** required string format: date-time nullable **phoneNumberCountryUniqueId** required string **phoneNumberCountryCallingCode** required string **phoneNumber** required string **phoneNumberVerified** required boolean **addressLine1** required string **addressLine2** required string **addressLocality** required string **addressRegion** required string **addressPostalCode** required string **addressCountry** required ISO 3166-1 alpha-2 country code (e.g. US, BR), or empty string. string /^(\[A-Z]{2})?$/ **otpEnabled** required boolean #### Example generated ```json { "user": { "id": 1, "createdAt": "2026-04-15T12:00:00Z", "updatedAt": "2026-04-15T12:00:00Z", "enabled": true, "subject": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0", "username": "example", "givenName": "example", "middleName": "example", "familyName": "example", "nickname": "example", "website": "example", "gender": "example", "email": "hello@example.com", "emailVerified": true, "zoneInfoCountryName": "example", "zoneInfo": "example", "locale": "example", "birthDate": "2026-04-15T12:00:00Z", "phoneNumberCountryUniqueId": "example", "phoneNumberCountryCallingCode": "example", "phoneNumber": "example", "phoneNumberVerified": true, "addressLine1": "example", "addressLine2": "example", "addressLocality": "example", "addressRegion": "example", "addressPostalCode": "example", "addressCountry": "example", "otpEnabled": true } } ``` ### 400 Invalid request body or parameters application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge when token transport is malformed; ordinary validation errors omit it. ### 401 Missing or invalid bearer token application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on token refusals. An operation may also return a handler-level 401 without this header. ### 403 The token is valid but does not authorise this request. Four checks answer here, and a caller distinguishes them by error_code. INSUFFICIENT_SCOPE comes from the route’s authorisation middleware, before the handler runs: an Admin operation needs one of the scopes its route names, and an Account operation needs `authserver:manage-account`. USER_CONTEXT_REQUIRED comes from the same middleware on the Account operations, which also require the token to have been issued for a user: one obtained through the client credentials grant carries no auth_time claim and is refused here. FORBIDDEN comes from the handler, on the account endpoints that resolve a resource by id and then check it belongs to the caller, so a valid token for the wrong user reaches this rather than a 404. MANAGE_SCOPE_REQUIRED comes from the handler, on the Admin operations the administrative policy guards. Only `authserver:manage` creates an administrator, changes one, or changes what reaches one, so no granular scope will ever be enough: the remedy is a token holding `authserver:manage`, not requesting the route’s scope as for INSUFFICIENT_SCOPE. An administrator is a user, group or client holding any of the six administrative permissions (`manage`, `admin-read`, `manage-users`, `manage-clients`, `manage-settings`, `browser-sessions`), a user directly or through any of its groups, the admin console’s own client whatever it holds, and a client allowed to request the administrative scopes. A granular scope is refused here when it: - grants or revokes an administrative permission, directly or by moving a user into or out of a group that holds one, or deletes such a group; - writes to an administrator user (profile, credentials, enabled, sessions, consents, attributes, memberships, permissions, deletion), an administrative group (settings, attributes, members, permissions, deletion), or an administrator client (settings, authentication, flows, redirect URIs, web origins, token settings, permissions, logo, deletion); - switches whether a client may request the administrative scopes, on or off, on any client; - reads an administrator client’s secret; - changes the email or the audit-log settings; - changes an administrative permission’s description. Granular scopes still read administrators. The refusal is answered after the request’s 400 and 404, writes nothing, carries `WWW-Authenticate: Bearer realm="goiabada", error="insufficient_scope", error_description="...", scope="authserver:manage"`, and leaves an `administrator_change_refused` audit record. Every operation this document secures declares a 403. The public client logo endpoint, which carries `security: []` and authorises nothing, is the one that does not. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on scope and administrative-policy refusals; other forbidden responses omit it. ### 404 Resource not found application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` ### 409 The change would leave no enabled user holding `authserver:manage`, directly or through a group. error_code is LAST_ADMINISTRATOR; nothing was changed. Grant `authserver:manage` to another user first, then repeat the request. A client holding the permission does not count, and neither does a disabled user. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` ### 500 The request failed inside the server, most often a storage error. error_code is INTERNAL_SERVER_ERROR: no detail about the failure, only the request id an operator can find the log line by, and retrying is reasonable. Key rotation’s 500 also answers KEY_SET_INCOMPLETE, which names its cause and no retry clears. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` # Disable user OTP Source: https://goiabada.dev/reference/api/admin/operations/updateuserotp/ PUT /api/v1/admin/users/{id}/otp Shell / cURL ```sh curl --request PUT \ --url http://localhost:9090/api/v1/admin/users/1/otp \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "enabled": true }' ``` - Auth server base URL{baseUrl}/api/v1/admin/users/{id}/otp Disable OTP (two-factor authentication) for a user. OTP can only be disabled via Admin API, not enabled: a user enrolls through the Account API or their own account pages. The removal lands only while the user still has the authenticator this request read; when another request removed it first, the answer is 400 OTP_NOT_ENABLED, and when it was replaced, 409 CONCURRENT_UPDATE with nothing removed. ## Authorizations - **[BearerAuth](https://goiabada.dev/reference/api/admin/#bearerauth)** ## Parameters ### Path Parameters **id** required integer format: int64 User ID ## Request Bodyrequired application/json object **enabled** Can only be set to false (disable OTP) boolean ### Example generated ```json { "enabled": true } ``` ## Responses ### 200 OTP disabled application/json object **user** required object **id** required integer format: int64 **createdAt** required string format: date-time nullable **updatedAt** required string format: date-time nullable **enabled** required boolean **subject** required string format: uuid **username** required string **givenName** required string **middleName** required string **familyName** required string **nickname** required string **website** required string **gender** required “female”, “male” or “other”, or empty when unset string **email** required string format: email **emailVerified** required boolean **zoneInfoCountryName** required string **zoneInfo** required string **locale** required string **birthDate** required string format: date-time nullable **phoneNumberCountryUniqueId** required string **phoneNumberCountryCallingCode** required string **phoneNumber** required string **phoneNumberVerified** required boolean **addressLine1** required string **addressLine2** required string **addressLocality** required string **addressRegion** required string **addressPostalCode** required string **addressCountry** required ISO 3166-1 alpha-2 country code (e.g. US, BR), or empty string. string /^(\[A-Z]{2})?$/ **otpEnabled** required boolean #### Example generated ```json { "user": { "id": 1, "createdAt": "2026-04-15T12:00:00Z", "updatedAt": "2026-04-15T12:00:00Z", "enabled": true, "subject": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0", "username": "example", "givenName": "example", "middleName": "example", "familyName": "example", "nickname": "example", "website": "example", "gender": "example", "email": "hello@example.com", "emailVerified": true, "zoneInfoCountryName": "example", "zoneInfo": "example", "locale": "example", "birthDate": "2026-04-15T12:00:00Z", "phoneNumberCountryUniqueId": "example", "phoneNumberCountryCallingCode": "example", "phoneNumber": "example", "phoneNumberVerified": true, "addressLine1": "example", "addressLine2": "example", "addressLocality": "example", "addressRegion": "example", "addressPostalCode": "example", "addressCountry": "example", "otpEnabled": true } } ``` ### 400 Invalid request body or parameters application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge when token transport is malformed; ordinary validation errors omit it. ### 401 Missing or invalid bearer token application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on token refusals. An operation may also return a handler-level 401 without this header. ### 403 The token is valid but does not authorise this request. Four checks answer here, and a caller distinguishes them by error_code. INSUFFICIENT_SCOPE comes from the route’s authorisation middleware, before the handler runs: an Admin operation needs one of the scopes its route names, and an Account operation needs `authserver:manage-account`. USER_CONTEXT_REQUIRED comes from the same middleware on the Account operations, which also require the token to have been issued for a user: one obtained through the client credentials grant carries no auth_time claim and is refused here. FORBIDDEN comes from the handler, on the account endpoints that resolve a resource by id and then check it belongs to the caller, so a valid token for the wrong user reaches this rather than a 404. MANAGE_SCOPE_REQUIRED comes from the handler, on the Admin operations the administrative policy guards. Only `authserver:manage` creates an administrator, changes one, or changes what reaches one, so no granular scope will ever be enough: the remedy is a token holding `authserver:manage`, not requesting the route’s scope as for INSUFFICIENT_SCOPE. An administrator is a user, group or client holding any of the six administrative permissions (`manage`, `admin-read`, `manage-users`, `manage-clients`, `manage-settings`, `browser-sessions`), a user directly or through any of its groups, the admin console’s own client whatever it holds, and a client allowed to request the administrative scopes. A granular scope is refused here when it: - grants or revokes an administrative permission, directly or by moving a user into or out of a group that holds one, or deletes such a group; - writes to an administrator user (profile, credentials, enabled, sessions, consents, attributes, memberships, permissions, deletion), an administrative group (settings, attributes, members, permissions, deletion), or an administrator client (settings, authentication, flows, redirect URIs, web origins, token settings, permissions, logo, deletion); - switches whether a client may request the administrative scopes, on or off, on any client; - reads an administrator client’s secret; - changes the email or the audit-log settings; - changes an administrative permission’s description. Granular scopes still read administrators. The refusal is answered after the request’s 400 and 404, writes nothing, carries `WWW-Authenticate: Bearer realm="goiabada", error="insufficient_scope", error_description="...", scope="authserver:manage"`, and leaves an `administrator_change_refused` audit record. Every operation this document secures declares a 403. The public client logo endpoint, which carries `security: []` and authorises nothing, is the one that does not. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on scope and administrative-policy refusals; other forbidden responses omit it. ### 404 Resource not found application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` ### 409 Another request changed the user’s authenticator after this one read it. error_code is CONCURRENT_UPDATE; nothing was saved. Read the user again and retry. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` ### 500 The request failed inside the server, most often a storage error. error_code is INTERNAL_SERVER_ERROR: no detail about the failure, only the request id an operator can find the log line by, and retrying is reasonable. Key rotation’s 500 also answers KEY_SET_INCOMPLETE, which names its cause and no retry clears. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` # Get user profile picture info Source: https://goiabada.dev/reference/api/admin/operations/getuserprofilepictureinfo/ GET /api/v1/admin/users/{id}/profile-picture Shell / cURL ```sh curl --request GET \ --url http://localhost:9090/api/v1/admin/users/1/profile-picture \ --header 'Authorization: Bearer ' ``` - Auth server base URL{baseUrl}/api/v1/admin/users/{id}/profile-picture Check whether a user has a profile picture and get the picture URL ## Authorizations - **[BearerAuth](https://goiabada.dev/reference/api/admin/#bearerauth)** ## Parameters ### Path Parameters **id** required integer format: int64 User ID ## Responses ### 200 Profile picture info application/json object **hasPicture** required boolean **pictureUrl** Public URL to serve the picture (only present when hasPicture is true) string #### Example generated ```json { "hasPicture": true, "pictureUrl": "example" } ``` ### 400 Invalid request body or parameters application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge when token transport is malformed; ordinary validation errors omit it. ### 401 Missing or invalid bearer token application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on token refusals. An operation may also return a handler-level 401 without this header. ### 403 The token is valid but does not authorise this request. Four checks answer here, and a caller distinguishes them by error_code. INSUFFICIENT_SCOPE comes from the route’s authorisation middleware, before the handler runs: an Admin operation needs one of the scopes its route names, and an Account operation needs `authserver:manage-account`. USER_CONTEXT_REQUIRED comes from the same middleware on the Account operations, which also require the token to have been issued for a user: one obtained through the client credentials grant carries no auth_time claim and is refused here. FORBIDDEN comes from the handler, on the account endpoints that resolve a resource by id and then check it belongs to the caller, so a valid token for the wrong user reaches this rather than a 404. MANAGE_SCOPE_REQUIRED comes from the handler, on the Admin operations the administrative policy guards. Only `authserver:manage` creates an administrator, changes one, or changes what reaches one, so no granular scope will ever be enough: the remedy is a token holding `authserver:manage`, not requesting the route’s scope as for INSUFFICIENT_SCOPE. An administrator is a user, group or client holding any of the six administrative permissions (`manage`, `admin-read`, `manage-users`, `manage-clients`, `manage-settings`, `browser-sessions`), a user directly or through any of its groups, the admin console’s own client whatever it holds, and a client allowed to request the administrative scopes. A granular scope is refused here when it: - grants or revokes an administrative permission, directly or by moving a user into or out of a group that holds one, or deletes such a group; - writes to an administrator user (profile, credentials, enabled, sessions, consents, attributes, memberships, permissions, deletion), an administrative group (settings, attributes, members, permissions, deletion), or an administrator client (settings, authentication, flows, redirect URIs, web origins, token settings, permissions, logo, deletion); - switches whether a client may request the administrative scopes, on or off, on any client; - reads an administrator client’s secret; - changes the email or the audit-log settings; - changes an administrative permission’s description. Granular scopes still read administrators. The refusal is answered after the request’s 400 and 404, writes nothing, carries `WWW-Authenticate: Bearer realm="goiabada", error="insufficient_scope", error_description="...", scope="authserver:manage"`, and leaves an `administrator_change_refused` audit record. Every operation this document secures declares a 403. The public client logo endpoint, which carries `security: []` and authorises nothing, is the one that does not. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on scope and administrative-policy refusals; other forbidden responses omit it. ### 404 Resource not found application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` ### 500 The request failed inside the server, most often a storage error. error_code is INTERNAL_SERVER_ERROR: no detail about the failure, only the request id an operator can find the log line by, and retrying is reasonable. Key rotation’s 500 also answers KEY_SET_INCOMPLETE, which names its cause and no retry clears. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` # Upload user profile picture Source: https://goiabada.dev/reference/api/admin/operations/uploaduserprofilepicture/ POST /api/v1/admin/users/{id}/profile-picture Shell / cURL ```sh curl --request POST \ --url http://localhost:9090/api/v1/admin/users/1/profile-picture \ --header 'Authorization: Bearer ' \ --header 'Content-Type: multipart/form-data' \ --form picture=@file ``` - Auth server base URL{baseUrl}/api/v1/admin/users/{id}/profile-picture Upload or replace the user’s profile picture. Send it as `multipart/form-data` in the field `picture`: a JPEG, PNG, GIF or WebP image, from 10x10 to 512x512 pixels, of at most `GOIABADA_PROFILE_PICTURE_MAX_SIZE_BYTES` bytes, 3 MiB by default. ## Authorizations - **[BearerAuth](https://goiabada.dev/reference/api/admin/#bearerauth)** ## Parameters ### Path Parameters **id** required integer format: int64 User ID ## Request Bodyrequired multipart/form-data object **picture** required Profile picture file (JPEG, PNG, GIF, or WebP) string format: binary ## Responses ### 200 Profile picture uploaded application/json object **success** required boolean **pictureUrl** required Public URL to the uploaded picture string #### Example generated ```json { "success": true, "pictureUrl": "example" } ``` ### 400 Invalid request body or parameters application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge when token transport is malformed; ordinary validation errors omit it. ### 401 Missing or invalid bearer token application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on token refusals. An operation may also return a handler-level 401 without this header. ### 403 The token is valid but does not authorise this request. Four checks answer here, and a caller distinguishes them by error_code. INSUFFICIENT_SCOPE comes from the route’s authorisation middleware, before the handler runs: an Admin operation needs one of the scopes its route names, and an Account operation needs `authserver:manage-account`. USER_CONTEXT_REQUIRED comes from the same middleware on the Account operations, which also require the token to have been issued for a user: one obtained through the client credentials grant carries no auth_time claim and is refused here. FORBIDDEN comes from the handler, on the account endpoints that resolve a resource by id and then check it belongs to the caller, so a valid token for the wrong user reaches this rather than a 404. MANAGE_SCOPE_REQUIRED comes from the handler, on the Admin operations the administrative policy guards. Only `authserver:manage` creates an administrator, changes one, or changes what reaches one, so no granular scope will ever be enough: the remedy is a token holding `authserver:manage`, not requesting the route’s scope as for INSUFFICIENT_SCOPE. An administrator is a user, group or client holding any of the six administrative permissions (`manage`, `admin-read`, `manage-users`, `manage-clients`, `manage-settings`, `browser-sessions`), a user directly or through any of its groups, the admin console’s own client whatever it holds, and a client allowed to request the administrative scopes. A granular scope is refused here when it: - grants or revokes an administrative permission, directly or by moving a user into or out of a group that holds one, or deletes such a group; - writes to an administrator user (profile, credentials, enabled, sessions, consents, attributes, memberships, permissions, deletion), an administrative group (settings, attributes, members, permissions, deletion), or an administrator client (settings, authentication, flows, redirect URIs, web origins, token settings, permissions, logo, deletion); - switches whether a client may request the administrative scopes, on or off, on any client; - reads an administrator client’s secret; - changes the email or the audit-log settings; - changes an administrative permission’s description. Granular scopes still read administrators. The refusal is answered after the request’s 400 and 404, writes nothing, carries `WWW-Authenticate: Bearer realm="goiabada", error="insufficient_scope", error_description="...", scope="authserver:manage"`, and leaves an `administrator_change_refused` audit record. Every operation this document secures declares a 403. The public client logo endpoint, which carries `security: []` and authorises nothing, is the one that does not. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on scope and administrative-policy refusals; other forbidden responses omit it. ### 404 Resource not found application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` ### 500 The request failed inside the server, most often a storage error. error_code is INTERNAL_SERVER_ERROR: no detail about the failure, only the request id an operator can find the log line by, and retrying is reasonable. Key rotation’s 500 also answers KEY_SET_INCOMPLETE, which names its cause and no retry clears. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` # Delete user profile picture Source: https://goiabada.dev/reference/api/admin/operations/deleteuserprofilepicture/ DELETE /api/v1/admin/users/{id}/profile-picture Shell / cURL ```sh curl --request DELETE \ --url http://localhost:9090/api/v1/admin/users/1/profile-picture \ --header 'Authorization: Bearer ' ``` - Auth server base URL{baseUrl}/api/v1/admin/users/{id}/profile-picture Remove the user’s profile picture ## Authorizations - **[BearerAuth](https://goiabada.dev/reference/api/admin/#bearerauth)** ## Parameters ### Path Parameters **id** required integer format: int64 User ID ## Responses ### 200 Profile picture deleted application/json object **success** required boolean #### Example generated ```json { "success": true } ``` ### 400 Invalid request body or parameters application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge when token transport is malformed; ordinary validation errors omit it. ### 401 Missing or invalid bearer token application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on token refusals. An operation may also return a handler-level 401 without this header. ### 403 The token is valid but does not authorise this request. Four checks answer here, and a caller distinguishes them by error_code. INSUFFICIENT_SCOPE comes from the route’s authorisation middleware, before the handler runs: an Admin operation needs one of the scopes its route names, and an Account operation needs `authserver:manage-account`. USER_CONTEXT_REQUIRED comes from the same middleware on the Account operations, which also require the token to have been issued for a user: one obtained through the client credentials grant carries no auth_time claim and is refused here. FORBIDDEN comes from the handler, on the account endpoints that resolve a resource by id and then check it belongs to the caller, so a valid token for the wrong user reaches this rather than a 404. MANAGE_SCOPE_REQUIRED comes from the handler, on the Admin operations the administrative policy guards. Only `authserver:manage` creates an administrator, changes one, or changes what reaches one, so no granular scope will ever be enough: the remedy is a token holding `authserver:manage`, not requesting the route’s scope as for INSUFFICIENT_SCOPE. An administrator is a user, group or client holding any of the six administrative permissions (`manage`, `admin-read`, `manage-users`, `manage-clients`, `manage-settings`, `browser-sessions`), a user directly or through any of its groups, the admin console’s own client whatever it holds, and a client allowed to request the administrative scopes. A granular scope is refused here when it: - grants or revokes an administrative permission, directly or by moving a user into or out of a group that holds one, or deletes such a group; - writes to an administrator user (profile, credentials, enabled, sessions, consents, attributes, memberships, permissions, deletion), an administrative group (settings, attributes, members, permissions, deletion), or an administrator client (settings, authentication, flows, redirect URIs, web origins, token settings, permissions, logo, deletion); - switches whether a client may request the administrative scopes, on or off, on any client; - reads an administrator client’s secret; - changes the email or the audit-log settings; - changes an administrative permission’s description. Granular scopes still read administrators. The refusal is answered after the request’s 400 and 404, writes nothing, carries `WWW-Authenticate: Bearer realm="goiabada", error="insufficient_scope", error_description="...", scope="authserver:manage"`, and leaves an `administrator_change_refused` audit record. Every operation this document secures declares a 403. The public client logo endpoint, which carries `security: []` and authorises nothing, is the one that does not. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on scope and administrative-policy refusals; other forbidden responses omit it. ### 404 Resource not found application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` ### 500 The request failed inside the server, most often a storage error. error_code is INTERNAL_SERVER_ERROR: no detail about the failure, only the request id an operator can find the log line by, and retrying is reasonable. Key rotation’s 500 also answers KEY_SET_INCOMPLETE, which names its cause and no retry clears. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` # Get user groups Source: https://goiabada.dev/reference/api/admin/operations/getusergroups/ GET /api/v1/admin/users/{id}/groups Shell / cURL ```sh curl --request GET \ --url http://localhost:9090/api/v1/admin/users/1/groups \ --header 'Authorization: Bearer ' ``` - Auth server base URL{baseUrl}/api/v1/admin/users/{id}/groups Get groups that a user belongs to ## Authorizations - **[BearerAuth](https://goiabada.dev/reference/api/admin/#bearerauth)** ## Parameters ### Path Parameters **id** required integer format: int64 User ID ## Responses ### 200 User groups application/json object **user** required object **id** required integer format: int64 **createdAt** required string format: date-time nullable **updatedAt** required string format: date-time nullable **enabled** required boolean **subject** required string format: uuid **username** required string **givenName** required string **middleName** required string **familyName** required string **nickname** required string **website** required string **gender** required “female”, “male” or “other”, or empty when unset string **email** required string format: email **emailVerified** required boolean **zoneInfoCountryName** required string **zoneInfo** required string **locale** required string **birthDate** required string format: date-time nullable **phoneNumberCountryUniqueId** required string **phoneNumberCountryCallingCode** required string **phoneNumber** required string **phoneNumberVerified** required boolean **addressLine1** required string **addressLine2** required string **addressLocality** required string **addressRegion** required string **addressPostalCode** required string **addressCountry** required ISO 3166-1 alpha-2 country code (e.g. US, BR), or empty string. string /^(\[A-Z]{2})?$/ **otpEnabled** required boolean **groups** required Array\ object **id** required integer format: int64 **createdAt** required string format: date-time nullable **updatedAt** required string format: date-time nullable **groupIdentifier** required string **description** required string **includeInIdToken** required boolean **includeInAccessToken** required boolean **memberCount** required integer #### Example generated ```json { "user": { "id": 1, "createdAt": "2026-04-15T12:00:00Z", "updatedAt": "2026-04-15T12:00:00Z", "enabled": true, "subject": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0", "username": "example", "givenName": "example", "middleName": "example", "familyName": "example", "nickname": "example", "website": "example", "gender": "example", "email": "hello@example.com", "emailVerified": true, "zoneInfoCountryName": "example", "zoneInfo": "example", "locale": "example", "birthDate": "2026-04-15T12:00:00Z", "phoneNumberCountryUniqueId": "example", "phoneNumberCountryCallingCode": "example", "phoneNumber": "example", "phoneNumberVerified": true, "addressLine1": "example", "addressLine2": "example", "addressLocality": "example", "addressRegion": "example", "addressPostalCode": "example", "addressCountry": "example", "otpEnabled": true }, "groups": [ { "id": 1, "createdAt": "2026-04-15T12:00:00Z", "updatedAt": "2026-04-15T12:00:00Z", "groupIdentifier": "example", "description": "example", "includeInIdToken": true, "includeInAccessToken": true, "memberCount": 1 } ] } ``` ### 400 Invalid request body or parameters application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge when token transport is malformed; ordinary validation errors omit it. ### 401 Missing or invalid bearer token application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on token refusals. An operation may also return a handler-level 401 without this header. ### 403 The token is valid but does not authorise this request. Four checks answer here, and a caller distinguishes them by error_code. INSUFFICIENT_SCOPE comes from the route’s authorisation middleware, before the handler runs: an Admin operation needs one of the scopes its route names, and an Account operation needs `authserver:manage-account`. USER_CONTEXT_REQUIRED comes from the same middleware on the Account operations, which also require the token to have been issued for a user: one obtained through the client credentials grant carries no auth_time claim and is refused here. FORBIDDEN comes from the handler, on the account endpoints that resolve a resource by id and then check it belongs to the caller, so a valid token for the wrong user reaches this rather than a 404. MANAGE_SCOPE_REQUIRED comes from the handler, on the Admin operations the administrative policy guards. Only `authserver:manage` creates an administrator, changes one, or changes what reaches one, so no granular scope will ever be enough: the remedy is a token holding `authserver:manage`, not requesting the route’s scope as for INSUFFICIENT_SCOPE. An administrator is a user, group or client holding any of the six administrative permissions (`manage`, `admin-read`, `manage-users`, `manage-clients`, `manage-settings`, `browser-sessions`), a user directly or through any of its groups, the admin console’s own client whatever it holds, and a client allowed to request the administrative scopes. A granular scope is refused here when it: - grants or revokes an administrative permission, directly or by moving a user into or out of a group that holds one, or deletes such a group; - writes to an administrator user (profile, credentials, enabled, sessions, consents, attributes, memberships, permissions, deletion), an administrative group (settings, attributes, members, permissions, deletion), or an administrator client (settings, authentication, flows, redirect URIs, web origins, token settings, permissions, logo, deletion); - switches whether a client may request the administrative scopes, on or off, on any client; - reads an administrator client’s secret; - changes the email or the audit-log settings; - changes an administrative permission’s description. Granular scopes still read administrators. The refusal is answered after the request’s 400 and 404, writes nothing, carries `WWW-Authenticate: Bearer realm="goiabada", error="insufficient_scope", error_description="...", scope="authserver:manage"`, and leaves an `administrator_change_refused` audit record. Every operation this document secures declares a 403. The public client logo endpoint, which carries `security: []` and authorises nothing, is the one that does not. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on scope and administrative-policy refusals; other forbidden responses omit it. ### 404 Resource not found application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` ### 500 The request failed inside the server, most often a storage error. error_code is INTERNAL_SERVER_ERROR: no detail about the failure, only the request id an operator can find the log line by, and retrying is reasonable. Key rotation’s 500 also answers KEY_SET_INCOMPLETE, which names its cause and no retry clears. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` # Set user groups Source: https://goiabada.dev/reference/api/admin/operations/updateusergroups/ PUT /api/v1/admin/users/{id}/groups Shell / cURL ```sh curl --request PUT \ --url http://localhost:9090/api/v1/admin/users/1/groups \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "groupIds": [ 1 ], "expectedGroupIds": [ 1 ] }' ``` - Auth server base URL{baseUrl}/api/v1/admin/users/{id}/groups Replace all groups for a user ## Authorizations - **[BearerAuth](https://goiabada.dev/reference/api/admin/#bearerauth)** ## Parameters ### Path Parameters **id** required integer format: int64 User ID ## Request Bodyrequired application/json object **groupIds** The complete set of groups the user belongs to after the call. An array naming more than 1000 groups is refused with VALIDATION_ERROR before any of them is read back, because every id is validated against the database and the list is read in statement-sized batches. Array\ <= 1000 items **expectedGroupIds** required The user’s group ids as last read; \[] if there were none. Array\ ### Example generated ```json { "groupIds": [ 1 ], "expectedGroupIds": [ 1 ] } ``` ## Responses ### 200 Groups updated application/json object **success** required boolean #### Example generated ```json { "success": true } ``` ### 400 Invalid request body or parameters application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge when token transport is malformed; ordinary validation errors omit it. ### 401 Missing or invalid bearer token application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on token refusals. An operation may also return a handler-level 401 without this header. ### 403 The token is valid but does not authorise this request. Four checks answer here, and a caller distinguishes them by error_code. INSUFFICIENT_SCOPE comes from the route’s authorisation middleware, before the handler runs: an Admin operation needs one of the scopes its route names, and an Account operation needs `authserver:manage-account`. USER_CONTEXT_REQUIRED comes from the same middleware on the Account operations, which also require the token to have been issued for a user: one obtained through the client credentials grant carries no auth_time claim and is refused here. FORBIDDEN comes from the handler, on the account endpoints that resolve a resource by id and then check it belongs to the caller, so a valid token for the wrong user reaches this rather than a 404. MANAGE_SCOPE_REQUIRED comes from the handler, on the Admin operations the administrative policy guards. Only `authserver:manage` creates an administrator, changes one, or changes what reaches one, so no granular scope will ever be enough: the remedy is a token holding `authserver:manage`, not requesting the route’s scope as for INSUFFICIENT_SCOPE. An administrator is a user, group or client holding any of the six administrative permissions (`manage`, `admin-read`, `manage-users`, `manage-clients`, `manage-settings`, `browser-sessions`), a user directly or through any of its groups, the admin console’s own client whatever it holds, and a client allowed to request the administrative scopes. A granular scope is refused here when it: - grants or revokes an administrative permission, directly or by moving a user into or out of a group that holds one, or deletes such a group; - writes to an administrator user (profile, credentials, enabled, sessions, consents, attributes, memberships, permissions, deletion), an administrative group (settings, attributes, members, permissions, deletion), or an administrator client (settings, authentication, flows, redirect URIs, web origins, token settings, permissions, logo, deletion); - switches whether a client may request the administrative scopes, on or off, on any client; - reads an administrator client’s secret; - changes the email or the audit-log settings; - changes an administrative permission’s description. Granular scopes still read administrators. The refusal is answered after the request’s 400 and 404, writes nothing, carries `WWW-Authenticate: Bearer realm="goiabada", error="insufficient_scope", error_description="...", scope="authserver:manage"`, and leaves an `administrator_change_refused` audit record. Every operation this document secures declares a 403. The public client logo endpoint, which carries `security: []` and authorises nothing, is the one that does not. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on scope and administrative-policy refusals; other forbidden responses omit it. ### 404 Resource not found application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` ### 409 Another save changed the user’s groups after expectedGroupIds was read. error_code is CONCURRENT_UPDATE; nothing was saved. Read them again and retry. Or the save would leave no enabled user holding `authserver:manage`, directly or through a group. error_code is LAST_ADMINISTRATOR; nothing was saved. Grant `authserver:manage` to another user first. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` ### 500 The request failed inside the server, most often a storage error. error_code is INTERNAL_SERVER_ERROR: no detail about the failure, only the request id an operator can find the log line by, and retrying is reasonable. Key rotation’s 500 also answers KEY_SET_INCOMPLETE, which names its cause and no retry clears. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` # Overview Source: https://goiabada.dev/reference/api/admin/operations/tags/user-attributes/ ## User Attributes Custom user attributes (Admin API) ## Operations GET [/api/v1/admin/users/{id}/attributes](https://goiabada.dev/reference/api/admin/operations/getuserattributes/) POST [/api/v1/admin/user-attributes](https://goiabada.dev/reference/api/admin/operations/createuserattribute/) GET [/api/v1/admin/user-attributes/{id}](https://goiabada.dev/reference/api/admin/operations/getuserattribute/) PUT [/api/v1/admin/user-attributes/{id}](https://goiabada.dev/reference/api/admin/operations/updateuserattribute/) DELETE [/api/v1/admin/user-attributes/{id}](https://goiabada.dev/reference/api/admin/operations/deleteuserattribute/) # List user attributes Source: https://goiabada.dev/reference/api/admin/operations/getuserattributes/ GET /api/v1/admin/users/{id}/attributes Shell / cURL ```sh curl --request GET \ --url http://localhost:9090/api/v1/admin/users/1/attributes \ --header 'Authorization: Bearer ' ``` - Auth server base URL{baseUrl}/api/v1/admin/users/{id}/attributes Get all custom attributes for a user ## Authorizations - **[BearerAuth](https://goiabada.dev/reference/api/admin/#bearerauth)** ## Parameters ### Path Parameters **id** required integer format: int64 User ID ## Responses ### 200 User attributes application/json object **attributes** required Null when the user has no attributes. Array\ nullable object **id** required integer format: int64 **createdAt** required string format: date-time nullable **updatedAt** required string format: date-time nullable **key** required string **value** required string **includeInIdToken** required boolean **includeInAccessToken** required boolean **userId** required integer format: int64 #### Example generated ```json { "attributes": [ { "id": 1, "createdAt": "2026-04-15T12:00:00Z", "updatedAt": "2026-04-15T12:00:00Z", "key": "example", "value": "example", "includeInIdToken": true, "includeInAccessToken": true, "userId": 1 } ] } ``` ### 400 Invalid request body or parameters application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge when token transport is malformed; ordinary validation errors omit it. ### 401 Missing or invalid bearer token application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on token refusals. An operation may also return a handler-level 401 without this header. ### 403 The token is valid but does not authorise this request. Four checks answer here, and a caller distinguishes them by error_code. INSUFFICIENT_SCOPE comes from the route’s authorisation middleware, before the handler runs: an Admin operation needs one of the scopes its route names, and an Account operation needs `authserver:manage-account`. USER_CONTEXT_REQUIRED comes from the same middleware on the Account operations, which also require the token to have been issued for a user: one obtained through the client credentials grant carries no auth_time claim and is refused here. FORBIDDEN comes from the handler, on the account endpoints that resolve a resource by id and then check it belongs to the caller, so a valid token for the wrong user reaches this rather than a 404. MANAGE_SCOPE_REQUIRED comes from the handler, on the Admin operations the administrative policy guards. Only `authserver:manage` creates an administrator, changes one, or changes what reaches one, so no granular scope will ever be enough: the remedy is a token holding `authserver:manage`, not requesting the route’s scope as for INSUFFICIENT_SCOPE. An administrator is a user, group or client holding any of the six administrative permissions (`manage`, `admin-read`, `manage-users`, `manage-clients`, `manage-settings`, `browser-sessions`), a user directly or through any of its groups, the admin console’s own client whatever it holds, and a client allowed to request the administrative scopes. A granular scope is refused here when it: - grants or revokes an administrative permission, directly or by moving a user into or out of a group that holds one, or deletes such a group; - writes to an administrator user (profile, credentials, enabled, sessions, consents, attributes, memberships, permissions, deletion), an administrative group (settings, attributes, members, permissions, deletion), or an administrator client (settings, authentication, flows, redirect URIs, web origins, token settings, permissions, logo, deletion); - switches whether a client may request the administrative scopes, on or off, on any client; - reads an administrator client’s secret; - changes the email or the audit-log settings; - changes an administrative permission’s description. Granular scopes still read administrators. The refusal is answered after the request’s 400 and 404, writes nothing, carries `WWW-Authenticate: Bearer realm="goiabada", error="insufficient_scope", error_description="...", scope="authserver:manage"`, and leaves an `administrator_change_refused` audit record. Every operation this document secures declares a 403. The public client logo endpoint, which carries `security: []` and authorises nothing, is the one that does not. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on scope and administrative-policy refusals; other forbidden responses omit it. ### 404 Resource not found application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` ### 500 The request failed inside the server, most often a storage error. error_code is INTERNAL_SERVER_ERROR: no detail about the failure, only the request id an operator can find the log line by, and retrying is reasonable. Key rotation’s 500 also answers KEY_SET_INCOMPLETE, which names its cause and no retry clears. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` # Create user attribute Source: https://goiabada.dev/reference/api/admin/operations/createuserattribute/ POST /api/v1/admin/user-attributes Shell / cURL ```sh curl --request POST \ --url http://localhost:9090/api/v1/admin/user-attributes \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "key": "example", "value": "example", "includeInIdToken": true, "includeInAccessToken": true, "userId": 1 }' ``` - Auth server base URL{baseUrl}/api/v1/admin/user-attributes Create a new custom attribute for a user ## Authorizations - **[BearerAuth](https://goiabada.dev/reference/api/admin/#bearerauth)** ## Request Bodyrequired application/json object **key** required string **value** required string **includeInIdToken** boolean **includeInAccessToken** boolean **userId** required integer format: int64 ### Example generated ```json { "key": "example", "value": "example", "includeInIdToken": true, "includeInAccessToken": true, "userId": 1 } ``` ## Responses ### 201 Attribute created application/json object **attribute** required object **id** required integer format: int64 **createdAt** required string format: date-time nullable **updatedAt** required string format: date-time nullable **key** required string **value** required string **includeInIdToken** required boolean **includeInAccessToken** required boolean **userId** required integer format: int64 #### Example generated ```json { "attribute": { "id": 1, "createdAt": "2026-04-15T12:00:00Z", "updatedAt": "2026-04-15T12:00:00Z", "key": "example", "value": "example", "includeInIdToken": true, "includeInAccessToken": true, "userId": 1 } } ``` ### 400 Invalid request body or parameters application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge when token transport is malformed; ordinary validation errors omit it. ### 401 Missing or invalid bearer token application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on token refusals. An operation may also return a handler-level 401 without this header. ### 403 The token is valid but does not authorise this request. Four checks answer here, and a caller distinguishes them by error_code. INSUFFICIENT_SCOPE comes from the route’s authorisation middleware, before the handler runs: an Admin operation needs one of the scopes its route names, and an Account operation needs `authserver:manage-account`. USER_CONTEXT_REQUIRED comes from the same middleware on the Account operations, which also require the token to have been issued for a user: one obtained through the client credentials grant carries no auth_time claim and is refused here. FORBIDDEN comes from the handler, on the account endpoints that resolve a resource by id and then check it belongs to the caller, so a valid token for the wrong user reaches this rather than a 404. MANAGE_SCOPE_REQUIRED comes from the handler, on the Admin operations the administrative policy guards. Only `authserver:manage` creates an administrator, changes one, or changes what reaches one, so no granular scope will ever be enough: the remedy is a token holding `authserver:manage`, not requesting the route’s scope as for INSUFFICIENT_SCOPE. An administrator is a user, group or client holding any of the six administrative permissions (`manage`, `admin-read`, `manage-users`, `manage-clients`, `manage-settings`, `browser-sessions`), a user directly or through any of its groups, the admin console’s own client whatever it holds, and a client allowed to request the administrative scopes. A granular scope is refused here when it: - grants or revokes an administrative permission, directly or by moving a user into or out of a group that holds one, or deletes such a group; - writes to an administrator user (profile, credentials, enabled, sessions, consents, attributes, memberships, permissions, deletion), an administrative group (settings, attributes, members, permissions, deletion), or an administrator client (settings, authentication, flows, redirect URIs, web origins, token settings, permissions, logo, deletion); - switches whether a client may request the administrative scopes, on or off, on any client; - reads an administrator client’s secret; - changes the email or the audit-log settings; - changes an administrative permission’s description. Granular scopes still read administrators. The refusal is answered after the request’s 400 and 404, writes nothing, carries `WWW-Authenticate: Bearer realm="goiabada", error="insufficient_scope", error_description="...", scope="authserver:manage"`, and leaves an `administrator_change_refused` audit record. Every operation this document secures declares a 403. The public client logo endpoint, which carries `security: []` and authorises nothing, is the one that does not. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on scope and administrative-policy refusals; other forbidden responses omit it. ### 404 Resource not found application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` ### 500 The request failed inside the server, most often a storage error. error_code is INTERNAL_SERVER_ERROR: no detail about the failure, only the request id an operator can find the log line by, and retrying is reasonable. Key rotation’s 500 also answers KEY_SET_INCOMPLETE, which names its cause and no retry clears. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` # Get user attribute Source: https://goiabada.dev/reference/api/admin/operations/getuserattribute/ GET /api/v1/admin/user-attributes/{id} Shell / cURL ```sh curl --request GET \ --url http://localhost:9090/api/v1/admin/user-attributes/1 \ --header 'Authorization: Bearer ' ``` - Auth server base URL{baseUrl}/api/v1/admin/user-attributes/{id} Get a specific user attribute by ID ## Authorizations - **[BearerAuth](https://goiabada.dev/reference/api/admin/#bearerauth)** ## Parameters ### Path Parameters **id** required integer format: int64 Attribute ID ## Responses ### 200 Attribute details application/json object **attribute** required object **id** required integer format: int64 **createdAt** required string format: date-time nullable **updatedAt** required string format: date-time nullable **key** required string **value** required string **includeInIdToken** required boolean **includeInAccessToken** required boolean **userId** required integer format: int64 #### Example generated ```json { "attribute": { "id": 1, "createdAt": "2026-04-15T12:00:00Z", "updatedAt": "2026-04-15T12:00:00Z", "key": "example", "value": "example", "includeInIdToken": true, "includeInAccessToken": true, "userId": 1 } } ``` ### 400 Invalid request body or parameters application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge when token transport is malformed; ordinary validation errors omit it. ### 401 Missing or invalid bearer token application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on token refusals. An operation may also return a handler-level 401 without this header. ### 403 The token is valid but does not authorise this request. Four checks answer here, and a caller distinguishes them by error_code. INSUFFICIENT_SCOPE comes from the route’s authorisation middleware, before the handler runs: an Admin operation needs one of the scopes its route names, and an Account operation needs `authserver:manage-account`. USER_CONTEXT_REQUIRED comes from the same middleware on the Account operations, which also require the token to have been issued for a user: one obtained through the client credentials grant carries no auth_time claim and is refused here. FORBIDDEN comes from the handler, on the account endpoints that resolve a resource by id and then check it belongs to the caller, so a valid token for the wrong user reaches this rather than a 404. MANAGE_SCOPE_REQUIRED comes from the handler, on the Admin operations the administrative policy guards. Only `authserver:manage` creates an administrator, changes one, or changes what reaches one, so no granular scope will ever be enough: the remedy is a token holding `authserver:manage`, not requesting the route’s scope as for INSUFFICIENT_SCOPE. An administrator is a user, group or client holding any of the six administrative permissions (`manage`, `admin-read`, `manage-users`, `manage-clients`, `manage-settings`, `browser-sessions`), a user directly or through any of its groups, the admin console’s own client whatever it holds, and a client allowed to request the administrative scopes. A granular scope is refused here when it: - grants or revokes an administrative permission, directly or by moving a user into or out of a group that holds one, or deletes such a group; - writes to an administrator user (profile, credentials, enabled, sessions, consents, attributes, memberships, permissions, deletion), an administrative group (settings, attributes, members, permissions, deletion), or an administrator client (settings, authentication, flows, redirect URIs, web origins, token settings, permissions, logo, deletion); - switches whether a client may request the administrative scopes, on or off, on any client; - reads an administrator client’s secret; - changes the email or the audit-log settings; - changes an administrative permission’s description. Granular scopes still read administrators. The refusal is answered after the request’s 400 and 404, writes nothing, carries `WWW-Authenticate: Bearer realm="goiabada", error="insufficient_scope", error_description="...", scope="authserver:manage"`, and leaves an `administrator_change_refused` audit record. Every operation this document secures declares a 403. The public client logo endpoint, which carries `security: []` and authorises nothing, is the one that does not. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on scope and administrative-policy refusals; other forbidden responses omit it. ### 404 Resource not found application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` ### 500 The request failed inside the server, most often a storage error. error_code is INTERNAL_SERVER_ERROR: no detail about the failure, only the request id an operator can find the log line by, and retrying is reasonable. Key rotation’s 500 also answers KEY_SET_INCOMPLETE, which names its cause and no retry clears. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` # Update user attribute Source: https://goiabada.dev/reference/api/admin/operations/updateuserattribute/ PUT /api/v1/admin/user-attributes/{id} Shell / cURL ```sh curl --request PUT \ --url http://localhost:9090/api/v1/admin/user-attributes/1 \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "key": "example", "value": "example", "includeInIdToken": true, "includeInAccessToken": true }' ``` - Auth server base URL{baseUrl}/api/v1/admin/user-attributes/{id} Update a user attribute ## Authorizations - **[BearerAuth](https://goiabada.dev/reference/api/admin/#bearerauth)** ## Parameters ### Path Parameters **id** required integer format: int64 Attribute ID ## Request Bodyrequired application/json object **key** string **value** string **includeInIdToken** boolean **includeInAccessToken** boolean ### Example generated ```json { "key": "example", "value": "example", "includeInIdToken": true, "includeInAccessToken": true } ``` ## Responses ### 200 Attribute updated application/json object **attribute** required object **id** required integer format: int64 **createdAt** required string format: date-time nullable **updatedAt** required string format: date-time nullable **key** required string **value** required string **includeInIdToken** required boolean **includeInAccessToken** required boolean **userId** required integer format: int64 #### Example generated ```json { "attribute": { "id": 1, "createdAt": "2026-04-15T12:00:00Z", "updatedAt": "2026-04-15T12:00:00Z", "key": "example", "value": "example", "includeInIdToken": true, "includeInAccessToken": true, "userId": 1 } } ``` ### 400 Invalid request body or parameters application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge when token transport is malformed; ordinary validation errors omit it. ### 401 Missing or invalid bearer token application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on token refusals. An operation may also return a handler-level 401 without this header. ### 403 The token is valid but does not authorise this request. Four checks answer here, and a caller distinguishes them by error_code. INSUFFICIENT_SCOPE comes from the route’s authorisation middleware, before the handler runs: an Admin operation needs one of the scopes its route names, and an Account operation needs `authserver:manage-account`. USER_CONTEXT_REQUIRED comes from the same middleware on the Account operations, which also require the token to have been issued for a user: one obtained through the client credentials grant carries no auth_time claim and is refused here. FORBIDDEN comes from the handler, on the account endpoints that resolve a resource by id and then check it belongs to the caller, so a valid token for the wrong user reaches this rather than a 404. MANAGE_SCOPE_REQUIRED comes from the handler, on the Admin operations the administrative policy guards. Only `authserver:manage` creates an administrator, changes one, or changes what reaches one, so no granular scope will ever be enough: the remedy is a token holding `authserver:manage`, not requesting the route’s scope as for INSUFFICIENT_SCOPE. An administrator is a user, group or client holding any of the six administrative permissions (`manage`, `admin-read`, `manage-users`, `manage-clients`, `manage-settings`, `browser-sessions`), a user directly or through any of its groups, the admin console’s own client whatever it holds, and a client allowed to request the administrative scopes. A granular scope is refused here when it: - grants or revokes an administrative permission, directly or by moving a user into or out of a group that holds one, or deletes such a group; - writes to an administrator user (profile, credentials, enabled, sessions, consents, attributes, memberships, permissions, deletion), an administrative group (settings, attributes, members, permissions, deletion), or an administrator client (settings, authentication, flows, redirect URIs, web origins, token settings, permissions, logo, deletion); - switches whether a client may request the administrative scopes, on or off, on any client; - reads an administrator client’s secret; - changes the email or the audit-log settings; - changes an administrative permission’s description. Granular scopes still read administrators. The refusal is answered after the request’s 400 and 404, writes nothing, carries `WWW-Authenticate: Bearer realm="goiabada", error="insufficient_scope", error_description="...", scope="authserver:manage"`, and leaves an `administrator_change_refused` audit record. Every operation this document secures declares a 403. The public client logo endpoint, which carries `security: []` and authorises nothing, is the one that does not. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on scope and administrative-policy refusals; other forbidden responses omit it. ### 404 Resource not found application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` ### 500 The request failed inside the server, most often a storage error. error_code is INTERNAL_SERVER_ERROR: no detail about the failure, only the request id an operator can find the log line by, and retrying is reasonable. Key rotation’s 500 also answers KEY_SET_INCOMPLETE, which names its cause and no retry clears. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` # Delete user attribute Source: https://goiabada.dev/reference/api/admin/operations/deleteuserattribute/ DELETE /api/v1/admin/user-attributes/{id} Shell / cURL ```sh curl --request DELETE \ --url http://localhost:9090/api/v1/admin/user-attributes/1 \ --header 'Authorization: Bearer ' ``` - Auth server base URL{baseUrl}/api/v1/admin/user-attributes/{id} Delete a user attribute ## Authorizations - **[BearerAuth](https://goiabada.dev/reference/api/admin/#bearerauth)** ## Parameters ### Path Parameters **id** required integer format: int64 Attribute ID ## Responses ### 200 Attribute deleted application/json object **success** required boolean #### Example generated ```json { "success": true } ``` ### 400 Invalid request body or parameters application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge when token transport is malformed; ordinary validation errors omit it. ### 401 Missing or invalid bearer token application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on token refusals. An operation may also return a handler-level 401 without this header. ### 403 The token is valid but does not authorise this request. Four checks answer here, and a caller distinguishes them by error_code. INSUFFICIENT_SCOPE comes from the route’s authorisation middleware, before the handler runs: an Admin operation needs one of the scopes its route names, and an Account operation needs `authserver:manage-account`. USER_CONTEXT_REQUIRED comes from the same middleware on the Account operations, which also require the token to have been issued for a user: one obtained through the client credentials grant carries no auth_time claim and is refused here. FORBIDDEN comes from the handler, on the account endpoints that resolve a resource by id and then check it belongs to the caller, so a valid token for the wrong user reaches this rather than a 404. MANAGE_SCOPE_REQUIRED comes from the handler, on the Admin operations the administrative policy guards. Only `authserver:manage` creates an administrator, changes one, or changes what reaches one, so no granular scope will ever be enough: the remedy is a token holding `authserver:manage`, not requesting the route’s scope as for INSUFFICIENT_SCOPE. An administrator is a user, group or client holding any of the six administrative permissions (`manage`, `admin-read`, `manage-users`, `manage-clients`, `manage-settings`, `browser-sessions`), a user directly or through any of its groups, the admin console’s own client whatever it holds, and a client allowed to request the administrative scopes. A granular scope is refused here when it: - grants or revokes an administrative permission, directly or by moving a user into or out of a group that holds one, or deletes such a group; - writes to an administrator user (profile, credentials, enabled, sessions, consents, attributes, memberships, permissions, deletion), an administrative group (settings, attributes, members, permissions, deletion), or an administrator client (settings, authentication, flows, redirect URIs, web origins, token settings, permissions, logo, deletion); - switches whether a client may request the administrative scopes, on or off, on any client; - reads an administrator client’s secret; - changes the email or the audit-log settings; - changes an administrative permission’s description. Granular scopes still read administrators. The refusal is answered after the request’s 400 and 404, writes nothing, carries `WWW-Authenticate: Bearer realm="goiabada", error="insufficient_scope", error_description="...", scope="authserver:manage"`, and leaves an `administrator_change_refused` audit record. Every operation this document secures declares a 403. The public client logo endpoint, which carries `security: []` and authorises nothing, is the one that does not. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on scope and administrative-policy refusals; other forbidden responses omit it. ### 404 Resource not found application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` ### 500 The request failed inside the server, most often a storage error. error_code is INTERNAL_SERVER_ERROR: no detail about the failure, only the request id an operator can find the log line by, and retrying is reasonable. Key rotation’s 500 also answers KEY_SET_INCOMPLETE, which names its cause and no retry clears. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` # Overview Source: https://goiabada.dev/reference/api/admin/operations/tags/user-sessions/ ## User Sessions User session management (Admin API) ## Operations GET [/api/v1/admin/users/{id}/sessions](https://goiabada.dev/reference/api/admin/operations/getusersessions/) GET [/api/v1/admin/user-sessions/{sessionIdentifier}](https://goiabada.dev/reference/api/admin/operations/getusersession/) DELETE [/api/v1/admin/user-sessions/{sessionIdentifier}](https://goiabada.dev/reference/api/admin/operations/deleteusersession/) # List user sessions Source: https://goiabada.dev/reference/api/admin/operations/getusersessions/ GET /api/v1/admin/users/{id}/sessions Shell / cURL ```sh curl --request GET \ --url http://localhost:9090/api/v1/admin/users/1/sessions \ --header 'Authorization: Bearer ' ``` - Auth server base URL{baseUrl}/api/v1/admin/users/{id}/sessions Get the user’s live sessions, each with its device, IP address and the identifiers of the clients it authorized. Sessions that have passed the configured idle timeout or maximum lifetime are left out. `isCurrent` marks the session the calling token was issued through. A token carrying no `sid` claim, such as one from the client credentials grant, gets `false` on every session. ## Authorizations - **[BearerAuth](https://goiabada.dev/reference/api/admin/#bearerauth)** ## Parameters ### Path Parameters **id** required integer format: int64 User ID ## Responses ### 200 User sessions application/json object **sessions** required Array object **id** required integer format: int64 **createdAt** required string format: date-time nullable **updatedAt** required string format: date-time nullable **sessionIdentifier** required string **started** required string format: date-time nullable **lastAccessed** required string format: date-time nullable **authMethods** required string **acrLevel** required string **authTime** required string format: date-time nullable **ipAddress** required string **deviceName** required string **deviceType** required string **deviceOS** required string **userAgent** required string **userId** required integer format: int64 **isCurrent** required boolean **clientIdentifiers** required Array\ #### Example generated ```json { "sessions": [ { "id": 1, "createdAt": "2026-04-15T12:00:00Z", "updatedAt": "2026-04-15T12:00:00Z", "sessionIdentifier": "example", "started": "2026-04-15T12:00:00Z", "lastAccessed": "2026-04-15T12:00:00Z", "authMethods": "example", "acrLevel": "example", "authTime": "2026-04-15T12:00:00Z", "ipAddress": "example", "deviceName": "example", "deviceType": "example", "deviceOS": "example", "userAgent": "example", "userId": 1, "isCurrent": true, "clientIdentifiers": [ "example" ] } ] } ``` ### 400 Invalid request body or parameters application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge when token transport is malformed; ordinary validation errors omit it. ### 401 Missing or invalid bearer token application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on token refusals. An operation may also return a handler-level 401 without this header. ### 403 The token is valid but does not authorise this request. Four checks answer here, and a caller distinguishes them by error_code. INSUFFICIENT_SCOPE comes from the route’s authorisation middleware, before the handler runs: an Admin operation needs one of the scopes its route names, and an Account operation needs `authserver:manage-account`. USER_CONTEXT_REQUIRED comes from the same middleware on the Account operations, which also require the token to have been issued for a user: one obtained through the client credentials grant carries no auth_time claim and is refused here. FORBIDDEN comes from the handler, on the account endpoints that resolve a resource by id and then check it belongs to the caller, so a valid token for the wrong user reaches this rather than a 404. MANAGE_SCOPE_REQUIRED comes from the handler, on the Admin operations the administrative policy guards. Only `authserver:manage` creates an administrator, changes one, or changes what reaches one, so no granular scope will ever be enough: the remedy is a token holding `authserver:manage`, not requesting the route’s scope as for INSUFFICIENT_SCOPE. An administrator is a user, group or client holding any of the six administrative permissions (`manage`, `admin-read`, `manage-users`, `manage-clients`, `manage-settings`, `browser-sessions`), a user directly or through any of its groups, the admin console’s own client whatever it holds, and a client allowed to request the administrative scopes. A granular scope is refused here when it: - grants or revokes an administrative permission, directly or by moving a user into or out of a group that holds one, or deletes such a group; - writes to an administrator user (profile, credentials, enabled, sessions, consents, attributes, memberships, permissions, deletion), an administrative group (settings, attributes, members, permissions, deletion), or an administrator client (settings, authentication, flows, redirect URIs, web origins, token settings, permissions, logo, deletion); - switches whether a client may request the administrative scopes, on or off, on any client; - reads an administrator client’s secret; - changes the email or the audit-log settings; - changes an administrative permission’s description. Granular scopes still read administrators. The refusal is answered after the request’s 400 and 404, writes nothing, carries `WWW-Authenticate: Bearer realm="goiabada", error="insufficient_scope", error_description="...", scope="authserver:manage"`, and leaves an `administrator_change_refused` audit record. Every operation this document secures declares a 403. The public client logo endpoint, which carries `security: []` and authorises nothing, is the one that does not. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on scope and administrative-policy refusals; other forbidden responses omit it. ### 404 Resource not found application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` ### 500 The request failed inside the server, most often a storage error. error_code is INTERNAL_SERVER_ERROR: no detail about the failure, only the request id an operator can find the log line by, and retrying is reasonable. Key rotation’s 500 also answers KEY_SET_INCOMPLETE, which names its cause and no retry clears. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` # Get session Source: https://goiabada.dev/reference/api/admin/operations/getusersession/ GET /api/v1/admin/user-sessions/{sessionIdentifier} Shell / cURL ```sh curl --request GET \ --url http://localhost:9090/api/v1/admin/user-sessions/example \ --header 'Authorization: Bearer ' ``` - Auth server base URL{baseUrl}/api/v1/admin/user-sessions/{sessionIdentifier} Get session details by identifier ## Authorizations - **[BearerAuth](https://goiabada.dev/reference/api/admin/#bearerauth)** ## Parameters ### Path Parameters **sessionIdentifier** required string Session identifier ## Responses ### 200 Session details application/json object **session** required object **id** required integer format: int64 **createdAt** required string format: date-time nullable **updatedAt** required string format: date-time nullable **sessionIdentifier** required string **started** required string format: date-time nullable **lastAccessed** required string format: date-time nullable **authMethods** required string **acrLevel** required string **authTime** required string format: date-time nullable **ipAddress** required string **deviceName** required string **deviceType** required string **deviceOS** required string **userAgent** required string **userId** required integer format: int64 #### Example generated ```json { "session": { "id": 1, "createdAt": "2026-04-15T12:00:00Z", "updatedAt": "2026-04-15T12:00:00Z", "sessionIdentifier": "example", "started": "2026-04-15T12:00:00Z", "lastAccessed": "2026-04-15T12:00:00Z", "authMethods": "example", "acrLevel": "example", "authTime": "2026-04-15T12:00:00Z", "ipAddress": "example", "deviceName": "example", "deviceType": "example", "deviceOS": "example", "userAgent": "example", "userId": 1 } } ``` ### 400 Invalid request body or parameters application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge when token transport is malformed; ordinary validation errors omit it. ### 401 Missing or invalid bearer token application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on token refusals. An operation may also return a handler-level 401 without this header. ### 403 The token is valid but does not authorise this request. Four checks answer here, and a caller distinguishes them by error_code. INSUFFICIENT_SCOPE comes from the route’s authorisation middleware, before the handler runs: an Admin operation needs one of the scopes its route names, and an Account operation needs `authserver:manage-account`. USER_CONTEXT_REQUIRED comes from the same middleware on the Account operations, which also require the token to have been issued for a user: one obtained through the client credentials grant carries no auth_time claim and is refused here. FORBIDDEN comes from the handler, on the account endpoints that resolve a resource by id and then check it belongs to the caller, so a valid token for the wrong user reaches this rather than a 404. MANAGE_SCOPE_REQUIRED comes from the handler, on the Admin operations the administrative policy guards. Only `authserver:manage` creates an administrator, changes one, or changes what reaches one, so no granular scope will ever be enough: the remedy is a token holding `authserver:manage`, not requesting the route’s scope as for INSUFFICIENT_SCOPE. An administrator is a user, group or client holding any of the six administrative permissions (`manage`, `admin-read`, `manage-users`, `manage-clients`, `manage-settings`, `browser-sessions`), a user directly or through any of its groups, the admin console’s own client whatever it holds, and a client allowed to request the administrative scopes. A granular scope is refused here when it: - grants or revokes an administrative permission, directly or by moving a user into or out of a group that holds one, or deletes such a group; - writes to an administrator user (profile, credentials, enabled, sessions, consents, attributes, memberships, permissions, deletion), an administrative group (settings, attributes, members, permissions, deletion), or an administrator client (settings, authentication, flows, redirect URIs, web origins, token settings, permissions, logo, deletion); - switches whether a client may request the administrative scopes, on or off, on any client; - reads an administrator client’s secret; - changes the email or the audit-log settings; - changes an administrative permission’s description. Granular scopes still read administrators. The refusal is answered after the request’s 400 and 404, writes nothing, carries `WWW-Authenticate: Bearer realm="goiabada", error="insufficient_scope", error_description="...", scope="authserver:manage"`, and leaves an `administrator_change_refused` audit record. Every operation this document secures declares a 403. The public client logo endpoint, which carries `security: []` and authorises nothing, is the one that does not. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on scope and administrative-policy refusals; other forbidden responses omit it. ### 404 Resource not found application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` ### 500 The request failed inside the server, most often a storage error. error_code is INTERNAL_SERVER_ERROR: no detail about the failure, only the request id an operator can find the log line by, and retrying is reasonable. Key rotation’s 500 also answers KEY_SET_INCOMPLETE, which names its cause and no retry clears. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` # Delete session Source: https://goiabada.dev/reference/api/admin/operations/deleteusersession/ DELETE /api/v1/admin/user-sessions/{sessionIdentifier} Shell / cURL ```sh curl --request DELETE \ --url http://localhost:9090/api/v1/admin/user-sessions/1 \ --header 'Authorization: Bearer ' ``` - Auth server base URL{baseUrl}/api/v1/admin/user-sessions/{sessionIdentifier} Terminate a user session. The session’s authorization codes are marked revoked and the refresh tokens descended from them are swept, offline refresh tokens included, in one transaction. Note: while GET takes the sessionIdentifier, DELETE takes the numeric session id at the same position. It does not accept the identifier: a value that is not an integer is refused with 400 and error_code VALIDATION_ERROR. ## Authorizations - **[BearerAuth](https://goiabada.dev/reference/api/admin/#bearerauth)** ## Parameters ### Path Parameters **sessionIdentifier** required integer format: int64 Session ID (numeric) ## Responses ### 200 Session deleted application/json object **success** required boolean #### Example generated ```json { "success": true } ``` ### 400 Invalid request body or parameters application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge when token transport is malformed; ordinary validation errors omit it. ### 401 Missing or invalid bearer token application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on token refusals. An operation may also return a handler-level 401 without this header. ### 403 The token is valid but does not authorise this request. Four checks answer here, and a caller distinguishes them by error_code. INSUFFICIENT_SCOPE comes from the route’s authorisation middleware, before the handler runs: an Admin operation needs one of the scopes its route names, and an Account operation needs `authserver:manage-account`. USER_CONTEXT_REQUIRED comes from the same middleware on the Account operations, which also require the token to have been issued for a user: one obtained through the client credentials grant carries no auth_time claim and is refused here. FORBIDDEN comes from the handler, on the account endpoints that resolve a resource by id and then check it belongs to the caller, so a valid token for the wrong user reaches this rather than a 404. MANAGE_SCOPE_REQUIRED comes from the handler, on the Admin operations the administrative policy guards. Only `authserver:manage` creates an administrator, changes one, or changes what reaches one, so no granular scope will ever be enough: the remedy is a token holding `authserver:manage`, not requesting the route’s scope as for INSUFFICIENT_SCOPE. An administrator is a user, group or client holding any of the six administrative permissions (`manage`, `admin-read`, `manage-users`, `manage-clients`, `manage-settings`, `browser-sessions`), a user directly or through any of its groups, the admin console’s own client whatever it holds, and a client allowed to request the administrative scopes. A granular scope is refused here when it: - grants or revokes an administrative permission, directly or by moving a user into or out of a group that holds one, or deletes such a group; - writes to an administrator user (profile, credentials, enabled, sessions, consents, attributes, memberships, permissions, deletion), an administrative group (settings, attributes, members, permissions, deletion), or an administrator client (settings, authentication, flows, redirect URIs, web origins, token settings, permissions, logo, deletion); - switches whether a client may request the administrative scopes, on or off, on any client; - reads an administrator client’s secret; - changes the email or the audit-log settings; - changes an administrative permission’s description. Granular scopes still read administrators. The refusal is answered after the request’s 400 and 404, writes nothing, carries `WWW-Authenticate: Bearer realm="goiabada", error="insufficient_scope", error_description="...", scope="authserver:manage"`, and leaves an `administrator_change_refused` audit record. Every operation this document secures declares a 403. The public client logo endpoint, which carries `security: []` and authorises nothing, is the one that does not. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on scope and administrative-policy refusals; other forbidden responses omit it. ### 404 Resource not found application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` ### 500 The request failed inside the server, most often a storage error. error_code is INTERNAL_SERVER_ERROR: no detail about the failure, only the request id an operator can find the log line by, and retrying is reasonable. Key rotation’s 500 also answers KEY_SET_INCOMPLETE, which names its cause and no retry clears. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` # Overview Source: https://goiabada.dev/reference/api/admin/operations/tags/user-consents/ ## User Consents OAuth consent management (Admin API) ## Operations GET [/api/v1/admin/users/{id}/consents](https://goiabada.dev/reference/api/admin/operations/getuserconsents/) DELETE [/api/v1/admin/user-consents/{id}](https://goiabada.dev/reference/api/admin/operations/deleteuserconsent/) # List user consents Source: https://goiabada.dev/reference/api/admin/operations/getuserconsents/ GET /api/v1/admin/users/{id}/consents Shell / cURL ```sh curl --request GET \ --url http://localhost:9090/api/v1/admin/users/1/consents \ --header 'Authorization: Bearer ' ``` - Auth server base URL{baseUrl}/api/v1/admin/users/{id}/consents Get all OAuth consents granted by a user ## Authorizations - **[BearerAuth](https://goiabada.dev/reference/api/admin/#bearerauth)** ## Parameters ### Path Parameters **id** required integer format: int64 User ID ## Responses ### 200 User consents application/json object **consents** required Null when the user has no consents. Array\ nullable object **id** required integer format: int64 **createdAt** required string format: date-time nullable **updatedAt** required string format: date-time nullable **clientId** required integer format: int64 **userId** required integer format: int64 **scope** required string **grantedAt** required string format: date-time nullable **clientIdentifier** required string **clientDescription** required string #### Example generated ```json { "consents": [ { "id": 1, "createdAt": "2026-04-15T12:00:00Z", "updatedAt": "2026-04-15T12:00:00Z", "clientId": 1, "userId": 1, "scope": "example", "grantedAt": "2026-04-15T12:00:00Z", "clientIdentifier": "example", "clientDescription": "example" } ] } ``` ### 400 Invalid request body or parameters application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge when token transport is malformed; ordinary validation errors omit it. ### 401 Missing or invalid bearer token application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on token refusals. An operation may also return a handler-level 401 without this header. ### 403 The token is valid but does not authorise this request. Four checks answer here, and a caller distinguishes them by error_code. INSUFFICIENT_SCOPE comes from the route’s authorisation middleware, before the handler runs: an Admin operation needs one of the scopes its route names, and an Account operation needs `authserver:manage-account`. USER_CONTEXT_REQUIRED comes from the same middleware on the Account operations, which also require the token to have been issued for a user: one obtained through the client credentials grant carries no auth_time claim and is refused here. FORBIDDEN comes from the handler, on the account endpoints that resolve a resource by id and then check it belongs to the caller, so a valid token for the wrong user reaches this rather than a 404. MANAGE_SCOPE_REQUIRED comes from the handler, on the Admin operations the administrative policy guards. Only `authserver:manage` creates an administrator, changes one, or changes what reaches one, so no granular scope will ever be enough: the remedy is a token holding `authserver:manage`, not requesting the route’s scope as for INSUFFICIENT_SCOPE. An administrator is a user, group or client holding any of the six administrative permissions (`manage`, `admin-read`, `manage-users`, `manage-clients`, `manage-settings`, `browser-sessions`), a user directly or through any of its groups, the admin console’s own client whatever it holds, and a client allowed to request the administrative scopes. A granular scope is refused here when it: - grants or revokes an administrative permission, directly or by moving a user into or out of a group that holds one, or deletes such a group; - writes to an administrator user (profile, credentials, enabled, sessions, consents, attributes, memberships, permissions, deletion), an administrative group (settings, attributes, members, permissions, deletion), or an administrator client (settings, authentication, flows, redirect URIs, web origins, token settings, permissions, logo, deletion); - switches whether a client may request the administrative scopes, on or off, on any client; - reads an administrator client’s secret; - changes the email or the audit-log settings; - changes an administrative permission’s description. Granular scopes still read administrators. The refusal is answered after the request’s 400 and 404, writes nothing, carries `WWW-Authenticate: Bearer realm="goiabada", error="insufficient_scope", error_description="...", scope="authserver:manage"`, and leaves an `administrator_change_refused` audit record. Every operation this document secures declares a 403. The public client logo endpoint, which carries `security: []` and authorises nothing, is the one that does not. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on scope and administrative-policy refusals; other forbidden responses omit it. ### 404 Resource not found application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` ### 500 The request failed inside the server, most often a storage error. error_code is INTERNAL_SERVER_ERROR: no detail about the failure, only the request id an operator can find the log line by, and retrying is reasonable. Key rotation’s 500 also answers KEY_SET_INCOMPLETE, which names its cause and no retry clears. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` # Revoke consent Source: https://goiabada.dev/reference/api/admin/operations/deleteuserconsent/ DELETE /api/v1/admin/user-consents/{id} Shell / cURL ```sh curl --request DELETE \ --url http://localhost:9090/api/v1/admin/user-consents/1 \ --header 'Authorization: Bearer ' ``` - Auth server base URL{baseUrl}/api/v1/admin/user-consents/{id} Revoke an OAuth consent ## Authorizations - **[BearerAuth](https://goiabada.dev/reference/api/admin/#bearerauth)** ## Parameters ### Path Parameters **id** required integer format: int64 Consent ID ## Responses ### 200 Consent revoked application/json object **success** required boolean #### Example generated ```json { "success": true } ``` ### 400 Invalid request body or parameters application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge when token transport is malformed; ordinary validation errors omit it. ### 401 Missing or invalid bearer token application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on token refusals. An operation may also return a handler-level 401 without this header. ### 403 The token is valid but does not authorise this request. Four checks answer here, and a caller distinguishes them by error_code. INSUFFICIENT_SCOPE comes from the route’s authorisation middleware, before the handler runs: an Admin operation needs one of the scopes its route names, and an Account operation needs `authserver:manage-account`. USER_CONTEXT_REQUIRED comes from the same middleware on the Account operations, which also require the token to have been issued for a user: one obtained through the client credentials grant carries no auth_time claim and is refused here. FORBIDDEN comes from the handler, on the account endpoints that resolve a resource by id and then check it belongs to the caller, so a valid token for the wrong user reaches this rather than a 404. MANAGE_SCOPE_REQUIRED comes from the handler, on the Admin operations the administrative policy guards. Only `authserver:manage` creates an administrator, changes one, or changes what reaches one, so no granular scope will ever be enough: the remedy is a token holding `authserver:manage`, not requesting the route’s scope as for INSUFFICIENT_SCOPE. An administrator is a user, group or client holding any of the six administrative permissions (`manage`, `admin-read`, `manage-users`, `manage-clients`, `manage-settings`, `browser-sessions`), a user directly or through any of its groups, the admin console’s own client whatever it holds, and a client allowed to request the administrative scopes. A granular scope is refused here when it: - grants or revokes an administrative permission, directly or by moving a user into or out of a group that holds one, or deletes such a group; - writes to an administrator user (profile, credentials, enabled, sessions, consents, attributes, memberships, permissions, deletion), an administrative group (settings, attributes, members, permissions, deletion), or an administrator client (settings, authentication, flows, redirect URIs, web origins, token settings, permissions, logo, deletion); - switches whether a client may request the administrative scopes, on or off, on any client; - reads an administrator client’s secret; - changes the email or the audit-log settings; - changes an administrative permission’s description. Granular scopes still read administrators. The refusal is answered after the request’s 400 and 404, writes nothing, carries `WWW-Authenticate: Bearer realm="goiabada", error="insufficient_scope", error_description="...", scope="authserver:manage"`, and leaves an `administrator_change_refused` audit record. Every operation this document secures declares a 403. The public client logo endpoint, which carries `security: []` and authorises nothing, is the one that does not. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on scope and administrative-policy refusals; other forbidden responses omit it. ### 404 Resource not found application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` ### 500 The request failed inside the server, most often a storage error. error_code is INTERNAL_SERVER_ERROR: no detail about the failure, only the request id an operator can find the log line by, and retrying is reasonable. Key rotation’s 500 also answers KEY_SET_INCOMPLETE, which names its cause and no retry clears. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` # Overview Source: https://goiabada.dev/reference/api/admin/operations/tags/groups/ ## Groups Group management (Admin API) ## Operations GET [/api/v1/admin/groups](https://goiabada.dev/reference/api/admin/operations/getgroups/) POST [/api/v1/admin/groups](https://goiabada.dev/reference/api/admin/operations/creategroup/) GET [/api/v1/admin/groups/search](https://goiabada.dev/reference/api/admin/operations/searchgroupswithpermissionannotation/) GET [/api/v1/admin/groups/{id}](https://goiabada.dev/reference/api/admin/operations/getgroup/) PUT [/api/v1/admin/groups/{id}](https://goiabada.dev/reference/api/admin/operations/updategroup/) DELETE [/api/v1/admin/groups/{id}](https://goiabada.dev/reference/api/admin/operations/deletegroup/) # List groups Source: https://goiabada.dev/reference/api/admin/operations/getgroups/ GET /api/v1/admin/groups Shell / cURL ```sh curl --request GET \ --url http://localhost:9090/api/v1/admin/groups \ --header 'Authorization: Bearer ' ``` - Auth server base URL{baseUrl}/api/v1/admin/groups Get all groups ## Authorizations - **[BearerAuth](https://goiabada.dev/reference/api/admin/#bearerauth)** ## Responses ### 200 Groups list application/json object **groups** required Array\ object **id** required integer format: int64 **createdAt** required string format: date-time nullable **updatedAt** required string format: date-time nullable **groupIdentifier** required string **description** required string **includeInIdToken** required boolean **includeInAccessToken** required boolean **memberCount** required integer #### Example generated ```json { "groups": [ { "id": 1, "createdAt": "2026-04-15T12:00:00Z", "updatedAt": "2026-04-15T12:00:00Z", "groupIdentifier": "example", "description": "example", "includeInIdToken": true, "includeInAccessToken": true, "memberCount": 1 } ] } ``` ### 401 Missing or invalid bearer token application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on token refusals. An operation may also return a handler-level 401 without this header. ### 403 The token is valid but does not authorise this request. Four checks answer here, and a caller distinguishes them by error_code. INSUFFICIENT_SCOPE comes from the route’s authorisation middleware, before the handler runs: an Admin operation needs one of the scopes its route names, and an Account operation needs `authserver:manage-account`. USER_CONTEXT_REQUIRED comes from the same middleware on the Account operations, which also require the token to have been issued for a user: one obtained through the client credentials grant carries no auth_time claim and is refused here. FORBIDDEN comes from the handler, on the account endpoints that resolve a resource by id and then check it belongs to the caller, so a valid token for the wrong user reaches this rather than a 404. MANAGE_SCOPE_REQUIRED comes from the handler, on the Admin operations the administrative policy guards. Only `authserver:manage` creates an administrator, changes one, or changes what reaches one, so no granular scope will ever be enough: the remedy is a token holding `authserver:manage`, not requesting the route’s scope as for INSUFFICIENT_SCOPE. An administrator is a user, group or client holding any of the six administrative permissions (`manage`, `admin-read`, `manage-users`, `manage-clients`, `manage-settings`, `browser-sessions`), a user directly or through any of its groups, the admin console’s own client whatever it holds, and a client allowed to request the administrative scopes. A granular scope is refused here when it: - grants or revokes an administrative permission, directly or by moving a user into or out of a group that holds one, or deletes such a group; - writes to an administrator user (profile, credentials, enabled, sessions, consents, attributes, memberships, permissions, deletion), an administrative group (settings, attributes, members, permissions, deletion), or an administrator client (settings, authentication, flows, redirect URIs, web origins, token settings, permissions, logo, deletion); - switches whether a client may request the administrative scopes, on or off, on any client; - reads an administrator client’s secret; - changes the email or the audit-log settings; - changes an administrative permission’s description. Granular scopes still read administrators. The refusal is answered after the request’s 400 and 404, writes nothing, carries `WWW-Authenticate: Bearer realm="goiabada", error="insufficient_scope", error_description="...", scope="authserver:manage"`, and leaves an `administrator_change_refused` audit record. Every operation this document secures declares a 403. The public client logo endpoint, which carries `security: []` and authorises nothing, is the one that does not. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on scope and administrative-policy refusals; other forbidden responses omit it. ### 500 The request failed inside the server, most often a storage error. error_code is INTERNAL_SERVER_ERROR: no detail about the failure, only the request id an operator can find the log line by, and retrying is reasonable. Key rotation’s 500 also answers KEY_SET_INCOMPLETE, which names its cause and no retry clears. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` # Create group Source: https://goiabada.dev/reference/api/admin/operations/creategroup/ POST /api/v1/admin/groups Shell / cURL ```sh curl --request POST \ --url http://localhost:9090/api/v1/admin/groups \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "groupIdentifier": "example", "description": "example", "includeInIdToken": true, "includeInAccessToken": true }' ``` - Auth server base URL{baseUrl}/api/v1/admin/groups Create a new group ## Authorizations - **[BearerAuth](https://goiabada.dev/reference/api/admin/#bearerauth)** ## Request Bodyrequired application/json object **groupIdentifier** required string **description** At most 100 UTF-16 code units. string **includeInIdToken** boolean **includeInAccessToken** boolean ### Example generated ```json { "groupIdentifier": "example", "description": "example", "includeInIdToken": true, "includeInAccessToken": true } ``` ## Responses ### 201 Group created application/json object **group** required object **id** required integer format: int64 **createdAt** required string format: date-time nullable **updatedAt** required string format: date-time nullable **groupIdentifier** required string **description** required string **includeInIdToken** required boolean **includeInAccessToken** required boolean **memberCount** required integer #### Example generated ```json { "group": { "id": 1, "createdAt": "2026-04-15T12:00:00Z", "updatedAt": "2026-04-15T12:00:00Z", "groupIdentifier": "example", "description": "example", "includeInIdToken": true, "includeInAccessToken": true, "memberCount": 1 } } ``` ### 400 Invalid request body or parameters application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge when token transport is malformed; ordinary validation errors omit it. ### 401 Missing or invalid bearer token application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on token refusals. An operation may also return a handler-level 401 without this header. ### 403 The token is valid but does not authorise this request. Four checks answer here, and a caller distinguishes them by error_code. INSUFFICIENT_SCOPE comes from the route’s authorisation middleware, before the handler runs: an Admin operation needs one of the scopes its route names, and an Account operation needs `authserver:manage-account`. USER_CONTEXT_REQUIRED comes from the same middleware on the Account operations, which also require the token to have been issued for a user: one obtained through the client credentials grant carries no auth_time claim and is refused here. FORBIDDEN comes from the handler, on the account endpoints that resolve a resource by id and then check it belongs to the caller, so a valid token for the wrong user reaches this rather than a 404. MANAGE_SCOPE_REQUIRED comes from the handler, on the Admin operations the administrative policy guards. Only `authserver:manage` creates an administrator, changes one, or changes what reaches one, so no granular scope will ever be enough: the remedy is a token holding `authserver:manage`, not requesting the route’s scope as for INSUFFICIENT_SCOPE. An administrator is a user, group or client holding any of the six administrative permissions (`manage`, `admin-read`, `manage-users`, `manage-clients`, `manage-settings`, `browser-sessions`), a user directly or through any of its groups, the admin console’s own client whatever it holds, and a client allowed to request the administrative scopes. A granular scope is refused here when it: - grants or revokes an administrative permission, directly or by moving a user into or out of a group that holds one, or deletes such a group; - writes to an administrator user (profile, credentials, enabled, sessions, consents, attributes, memberships, permissions, deletion), an administrative group (settings, attributes, members, permissions, deletion), or an administrator client (settings, authentication, flows, redirect URIs, web origins, token settings, permissions, logo, deletion); - switches whether a client may request the administrative scopes, on or off, on any client; - reads an administrator client’s secret; - changes the email or the audit-log settings; - changes an administrative permission’s description. Granular scopes still read administrators. The refusal is answered after the request’s 400 and 404, writes nothing, carries `WWW-Authenticate: Bearer realm="goiabada", error="insufficient_scope", error_description="...", scope="authserver:manage"`, and leaves an `administrator_change_refused` audit record. Every operation this document secures declares a 403. The public client logo endpoint, which carries `security: []` and authorises nothing, is the one that does not. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on scope and administrative-policy refusals; other forbidden responses omit it. ### 500 The request failed inside the server, most often a storage error. error_code is INTERNAL_SERVER_ERROR: no detail about the failure, only the request id an operator can find the log line by, and retrying is reasonable. Key rotation’s 500 also answers KEY_SET_INCOMPLETE, which names its cause and no retry clears. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` # Search groups annotated with a permission Source: https://goiabada.dev/reference/api/admin/operations/searchgroupswithpermissionannotation/ GET /api/v1/admin/groups/search Shell / cURL ```sh curl --request GET \ --url 'http://localhost:9090/api/v1/admin/groups/search?annotatePermissionId=1&page=1&size=10' \ --header 'Authorization: Bearer ' ``` - Auth server base URL{baseUrl}/api/v1/admin/groups/search Get a page of groups, each annotated with whether it has the given permission assigned. The annotation permission is required: without it there is nothing to annotate and the request is refused. ## Authorizations - **[BearerAuth](https://goiabada.dev/reference/api/admin/#bearerauth)** ## Parameters ### Query Parameters **annotatePermissionId** required integer format: int64 The permission each returned group is annotated against **page** integer default: 1 Page number (default 1) **size** integer default: 10 Results per page (default 10) ## Responses ### 200 Annotated groups application/json object **groups** required Array object **id** required integer format: int64 **createdAt** required string format: date-time nullable **updatedAt** required string format: date-time nullable **groupIdentifier** required string **description** required string **includeInIdToken** required boolean **includeInAccessToken** required boolean **memberCount** required integer **hasPermission** required Whether this group has the annotated permission assigned boolean **total** required integer **page** required integer **size** required integer #### Example generated ```json { "groups": [ { "id": 1, "createdAt": "2026-04-15T12:00:00Z", "updatedAt": "2026-04-15T12:00:00Z", "groupIdentifier": "example", "description": "example", "includeInIdToken": true, "includeInAccessToken": true, "memberCount": 1, "hasPermission": true } ], "total": 1, "page": 1, "size": 1 } ``` ### 400 Invalid request body or parameters application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge when token transport is malformed; ordinary validation errors omit it. ### 401 Missing or invalid bearer token application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on token refusals. An operation may also return a handler-level 401 without this header. ### 403 The token is valid but does not authorise this request. Four checks answer here, and a caller distinguishes them by error_code. INSUFFICIENT_SCOPE comes from the route’s authorisation middleware, before the handler runs: an Admin operation needs one of the scopes its route names, and an Account operation needs `authserver:manage-account`. USER_CONTEXT_REQUIRED comes from the same middleware on the Account operations, which also require the token to have been issued for a user: one obtained through the client credentials grant carries no auth_time claim and is refused here. FORBIDDEN comes from the handler, on the account endpoints that resolve a resource by id and then check it belongs to the caller, so a valid token for the wrong user reaches this rather than a 404. MANAGE_SCOPE_REQUIRED comes from the handler, on the Admin operations the administrative policy guards. Only `authserver:manage` creates an administrator, changes one, or changes what reaches one, so no granular scope will ever be enough: the remedy is a token holding `authserver:manage`, not requesting the route’s scope as for INSUFFICIENT_SCOPE. An administrator is a user, group or client holding any of the six administrative permissions (`manage`, `admin-read`, `manage-users`, `manage-clients`, `manage-settings`, `browser-sessions`), a user directly or through any of its groups, the admin console’s own client whatever it holds, and a client allowed to request the administrative scopes. A granular scope is refused here when it: - grants or revokes an administrative permission, directly or by moving a user into or out of a group that holds one, or deletes such a group; - writes to an administrator user (profile, credentials, enabled, sessions, consents, attributes, memberships, permissions, deletion), an administrative group (settings, attributes, members, permissions, deletion), or an administrator client (settings, authentication, flows, redirect URIs, web origins, token settings, permissions, logo, deletion); - switches whether a client may request the administrative scopes, on or off, on any client; - reads an administrator client’s secret; - changes the email or the audit-log settings; - changes an administrative permission’s description. Granular scopes still read administrators. The refusal is answered after the request’s 400 and 404, writes nothing, carries `WWW-Authenticate: Bearer realm="goiabada", error="insufficient_scope", error_description="...", scope="authserver:manage"`, and leaves an `administrator_change_refused` audit record. Every operation this document secures declares a 403. The public client logo endpoint, which carries `security: []` and authorises nothing, is the one that does not. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on scope and administrative-policy refusals; other forbidden responses omit it. ### 404 Resource not found application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` ### 500 The request failed inside the server, most often a storage error. error_code is INTERNAL_SERVER_ERROR: no detail about the failure, only the request id an operator can find the log line by, and retrying is reasonable. Key rotation’s 500 also answers KEY_SET_INCOMPLETE, which names its cause and no retry clears. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` # Get group Source: https://goiabada.dev/reference/api/admin/operations/getgroup/ GET /api/v1/admin/groups/{id} Shell / cURL ```sh curl --request GET \ --url http://localhost:9090/api/v1/admin/groups/1 \ --header 'Authorization: Bearer ' ``` - Auth server base URL{baseUrl}/api/v1/admin/groups/{id} Get group details by ID ## Authorizations - **[BearerAuth](https://goiabada.dev/reference/api/admin/#bearerauth)** ## Parameters ### Path Parameters **id** required integer format: int64 Group ID ## Responses ### 200 Group details application/json object **group** required object **id** required integer format: int64 **createdAt** required string format: date-time nullable **updatedAt** required string format: date-time nullable **groupIdentifier** required string **description** required string **includeInIdToken** required boolean **includeInAccessToken** required boolean **memberCount** required integer #### Example generated ```json { "group": { "id": 1, "createdAt": "2026-04-15T12:00:00Z", "updatedAt": "2026-04-15T12:00:00Z", "groupIdentifier": "example", "description": "example", "includeInIdToken": true, "includeInAccessToken": true, "memberCount": 1 } } ``` ### 400 Invalid request body or parameters application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge when token transport is malformed; ordinary validation errors omit it. ### 401 Missing or invalid bearer token application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on token refusals. An operation may also return a handler-level 401 without this header. ### 403 The token is valid but does not authorise this request. Four checks answer here, and a caller distinguishes them by error_code. INSUFFICIENT_SCOPE comes from the route’s authorisation middleware, before the handler runs: an Admin operation needs one of the scopes its route names, and an Account operation needs `authserver:manage-account`. USER_CONTEXT_REQUIRED comes from the same middleware on the Account operations, which also require the token to have been issued for a user: one obtained through the client credentials grant carries no auth_time claim and is refused here. FORBIDDEN comes from the handler, on the account endpoints that resolve a resource by id and then check it belongs to the caller, so a valid token for the wrong user reaches this rather than a 404. MANAGE_SCOPE_REQUIRED comes from the handler, on the Admin operations the administrative policy guards. Only `authserver:manage` creates an administrator, changes one, or changes what reaches one, so no granular scope will ever be enough: the remedy is a token holding `authserver:manage`, not requesting the route’s scope as for INSUFFICIENT_SCOPE. An administrator is a user, group or client holding any of the six administrative permissions (`manage`, `admin-read`, `manage-users`, `manage-clients`, `manage-settings`, `browser-sessions`), a user directly or through any of its groups, the admin console’s own client whatever it holds, and a client allowed to request the administrative scopes. A granular scope is refused here when it: - grants or revokes an administrative permission, directly or by moving a user into or out of a group that holds one, or deletes such a group; - writes to an administrator user (profile, credentials, enabled, sessions, consents, attributes, memberships, permissions, deletion), an administrative group (settings, attributes, members, permissions, deletion), or an administrator client (settings, authentication, flows, redirect URIs, web origins, token settings, permissions, logo, deletion); - switches whether a client may request the administrative scopes, on or off, on any client; - reads an administrator client’s secret; - changes the email or the audit-log settings; - changes an administrative permission’s description. Granular scopes still read administrators. The refusal is answered after the request’s 400 and 404, writes nothing, carries `WWW-Authenticate: Bearer realm="goiabada", error="insufficient_scope", error_description="...", scope="authserver:manage"`, and leaves an `administrator_change_refused` audit record. Every operation this document secures declares a 403. The public client logo endpoint, which carries `security: []` and authorises nothing, is the one that does not. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on scope and administrative-policy refusals; other forbidden responses omit it. ### 404 Resource not found application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` ### 500 The request failed inside the server, most often a storage error. error_code is INTERNAL_SERVER_ERROR: no detail about the failure, only the request id an operator can find the log line by, and retrying is reasonable. Key rotation’s 500 also answers KEY_SET_INCOMPLETE, which names its cause and no retry clears. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` # Update group Source: https://goiabada.dev/reference/api/admin/operations/updategroup/ PUT /api/v1/admin/groups/{id} Shell / cURL ```sh curl --request PUT \ --url http://localhost:9090/api/v1/admin/groups/1 \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "groupIdentifier": "example", "description": "example", "includeInIdToken": true, "includeInAccessToken": true }' ``` - Auth server base URL{baseUrl}/api/v1/admin/groups/{id} Update group details ## Authorizations - **[BearerAuth](https://goiabada.dev/reference/api/admin/#bearerauth)** ## Parameters ### Path Parameters **id** required integer format: int64 Group ID ## Request Bodyrequired application/json object **groupIdentifier** string **description** At most 100 UTF-16 code units. string **includeInIdToken** boolean **includeInAccessToken** boolean ### Example generated ```json { "groupIdentifier": "example", "description": "example", "includeInIdToken": true, "includeInAccessToken": true } ``` ## Responses ### 200 Group updated application/json object **group** required object **id** required integer format: int64 **createdAt** required string format: date-time nullable **updatedAt** required string format: date-time nullable **groupIdentifier** required string **description** required string **includeInIdToken** required boolean **includeInAccessToken** required boolean **memberCount** required integer #### Example generated ```json { "group": { "id": 1, "createdAt": "2026-04-15T12:00:00Z", "updatedAt": "2026-04-15T12:00:00Z", "groupIdentifier": "example", "description": "example", "includeInIdToken": true, "includeInAccessToken": true, "memberCount": 1 } } ``` ### 400 Invalid request body or parameters application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge when token transport is malformed; ordinary validation errors omit it. ### 401 Missing or invalid bearer token application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on token refusals. An operation may also return a handler-level 401 without this header. ### 403 The token is valid but does not authorise this request. Four checks answer here, and a caller distinguishes them by error_code. INSUFFICIENT_SCOPE comes from the route’s authorisation middleware, before the handler runs: an Admin operation needs one of the scopes its route names, and an Account operation needs `authserver:manage-account`. USER_CONTEXT_REQUIRED comes from the same middleware on the Account operations, which also require the token to have been issued for a user: one obtained through the client credentials grant carries no auth_time claim and is refused here. FORBIDDEN comes from the handler, on the account endpoints that resolve a resource by id and then check it belongs to the caller, so a valid token for the wrong user reaches this rather than a 404. MANAGE_SCOPE_REQUIRED comes from the handler, on the Admin operations the administrative policy guards. Only `authserver:manage` creates an administrator, changes one, or changes what reaches one, so no granular scope will ever be enough: the remedy is a token holding `authserver:manage`, not requesting the route’s scope as for INSUFFICIENT_SCOPE. An administrator is a user, group or client holding any of the six administrative permissions (`manage`, `admin-read`, `manage-users`, `manage-clients`, `manage-settings`, `browser-sessions`), a user directly or through any of its groups, the admin console’s own client whatever it holds, and a client allowed to request the administrative scopes. A granular scope is refused here when it: - grants or revokes an administrative permission, directly or by moving a user into or out of a group that holds one, or deletes such a group; - writes to an administrator user (profile, credentials, enabled, sessions, consents, attributes, memberships, permissions, deletion), an administrative group (settings, attributes, members, permissions, deletion), or an administrator client (settings, authentication, flows, redirect URIs, web origins, token settings, permissions, logo, deletion); - switches whether a client may request the administrative scopes, on or off, on any client; - reads an administrator client’s secret; - changes the email or the audit-log settings; - changes an administrative permission’s description. Granular scopes still read administrators. The refusal is answered after the request’s 400 and 404, writes nothing, carries `WWW-Authenticate: Bearer realm="goiabada", error="insufficient_scope", error_description="...", scope="authserver:manage"`, and leaves an `administrator_change_refused` audit record. Every operation this document secures declares a 403. The public client logo endpoint, which carries `security: []` and authorises nothing, is the one that does not. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on scope and administrative-policy refusals; other forbidden responses omit it. ### 404 Resource not found application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` ### 500 The request failed inside the server, most often a storage error. error_code is INTERNAL_SERVER_ERROR: no detail about the failure, only the request id an operator can find the log line by, and retrying is reasonable. Key rotation’s 500 also answers KEY_SET_INCOMPLETE, which names its cause and no retry clears. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` # Delete group Source: https://goiabada.dev/reference/api/admin/operations/deletegroup/ DELETE /api/v1/admin/groups/{id} Shell / cURL ```sh curl --request DELETE \ --url http://localhost:9090/api/v1/admin/groups/1 \ --header 'Authorization: Bearer ' ``` - Auth server base URL{baseUrl}/api/v1/admin/groups/{id} Delete a group ## Authorizations - **[BearerAuth](https://goiabada.dev/reference/api/admin/#bearerauth)** ## Parameters ### Path Parameters **id** required integer format: int64 Group ID ## Responses ### 200 Group deleted application/json object **success** required boolean #### Example generated ```json { "success": true } ``` ### 400 Invalid request body or parameters application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge when token transport is malformed; ordinary validation errors omit it. ### 401 Missing or invalid bearer token application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on token refusals. An operation may also return a handler-level 401 without this header. ### 403 The token is valid but does not authorise this request. Four checks answer here, and a caller distinguishes them by error_code. INSUFFICIENT_SCOPE comes from the route’s authorisation middleware, before the handler runs: an Admin operation needs one of the scopes its route names, and an Account operation needs `authserver:manage-account`. USER_CONTEXT_REQUIRED comes from the same middleware on the Account operations, which also require the token to have been issued for a user: one obtained through the client credentials grant carries no auth_time claim and is refused here. FORBIDDEN comes from the handler, on the account endpoints that resolve a resource by id and then check it belongs to the caller, so a valid token for the wrong user reaches this rather than a 404. MANAGE_SCOPE_REQUIRED comes from the handler, on the Admin operations the administrative policy guards. Only `authserver:manage` creates an administrator, changes one, or changes what reaches one, so no granular scope will ever be enough: the remedy is a token holding `authserver:manage`, not requesting the route’s scope as for INSUFFICIENT_SCOPE. An administrator is a user, group or client holding any of the six administrative permissions (`manage`, `admin-read`, `manage-users`, `manage-clients`, `manage-settings`, `browser-sessions`), a user directly or through any of its groups, the admin console’s own client whatever it holds, and a client allowed to request the administrative scopes. A granular scope is refused here when it: - grants or revokes an administrative permission, directly or by moving a user into or out of a group that holds one, or deletes such a group; - writes to an administrator user (profile, credentials, enabled, sessions, consents, attributes, memberships, permissions, deletion), an administrative group (settings, attributes, members, permissions, deletion), or an administrator client (settings, authentication, flows, redirect URIs, web origins, token settings, permissions, logo, deletion); - switches whether a client may request the administrative scopes, on or off, on any client; - reads an administrator client’s secret; - changes the email or the audit-log settings; - changes an administrative permission’s description. Granular scopes still read administrators. The refusal is answered after the request’s 400 and 404, writes nothing, carries `WWW-Authenticate: Bearer realm="goiabada", error="insufficient_scope", error_description="...", scope="authserver:manage"`, and leaves an `administrator_change_refused` audit record. Every operation this document secures declares a 403. The public client logo endpoint, which carries `security: []` and authorises nothing, is the one that does not. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on scope and administrative-policy refusals; other forbidden responses omit it. ### 404 Resource not found application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` ### 409 The change would leave no enabled user holding `authserver:manage`, directly or through a group. error_code is LAST_ADMINISTRATOR; nothing was changed. Grant `authserver:manage` to another user first, then repeat the request. A client holding the permission does not count, and neither does a disabled user. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` ### 500 The request failed inside the server, most often a storage error. error_code is INTERNAL_SERVER_ERROR: no detail about the failure, only the request id an operator can find the log line by, and retrying is reasonable. Key rotation’s 500 also answers KEY_SET_INCOMPLETE, which names its cause and no retry clears. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` # Overview Source: https://goiabada.dev/reference/api/admin/operations/tags/group-members/ ## Group Members Group membership management (Admin API) ## Operations GET [/api/v1/admin/groups/{id}/members](https://goiabada.dev/reference/api/admin/operations/getgroupmembers/) POST [/api/v1/admin/groups/{id}/members](https://goiabada.dev/reference/api/admin/operations/addgroupmember/) DELETE [/api/v1/admin/groups/{id}/members/{userId}](https://goiabada.dev/reference/api/admin/operations/removegroupmember/) # List group members Source: https://goiabada.dev/reference/api/admin/operations/getgroupmembers/ GET /api/v1/admin/groups/{id}/members Shell / cURL ```sh curl --request GET \ --url 'http://localhost:9090/api/v1/admin/groups/1/members?page=1&size=10' \ --header 'Authorization: Bearer ' ``` - Auth server base URL{baseUrl}/api/v1/admin/groups/{id}/members Get members of a group with pagination ## Authorizations - **[BearerAuth](https://goiabada.dev/reference/api/admin/#bearerauth)** ## Parameters ### Path Parameters **id** required integer format: int64 Group ID ### Query Parameters **page** integer default: 1 Page number (default 1) **size** integer default: 10 Results per page (default 10) ## Responses ### 200 Group members application/json object **members** required Null when the group has no members on this page. Array\ nullable object **id** required integer format: int64 **createdAt** required string format: date-time nullable **updatedAt** required string format: date-time nullable **enabled** required boolean **subject** required string format: uuid **username** required string **givenName** required string **middleName** required string **familyName** required string **nickname** required string **website** required string **gender** required “female”, “male” or “other”, or empty when unset string **email** required string format: email **emailVerified** required boolean **zoneInfoCountryName** required string **zoneInfo** required string **locale** required string **birthDate** required string format: date-time nullable **phoneNumberCountryUniqueId** required string **phoneNumberCountryCallingCode** required string **phoneNumber** required string **phoneNumberVerified** required boolean **addressLine1** required string **addressLine2** required string **addressLocality** required string **addressRegion** required string **addressPostalCode** required string **addressCountry** required ISO 3166-1 alpha-2 country code (e.g. US, BR), or empty string. string /^(\[A-Z]{2})?$/ **otpEnabled** required boolean **total** required integer **page** required integer **size** required integer #### Example generated ```json { "members": [ { "id": 1, "createdAt": "2026-04-15T12:00:00Z", "updatedAt": "2026-04-15T12:00:00Z", "enabled": true, "subject": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0", "username": "example", "givenName": "example", "middleName": "example", "familyName": "example", "nickname": "example", "website": "example", "gender": "example", "email": "hello@example.com", "emailVerified": true, "zoneInfoCountryName": "example", "zoneInfo": "example", "locale": "example", "birthDate": "2026-04-15T12:00:00Z", "phoneNumberCountryUniqueId": "example", "phoneNumberCountryCallingCode": "example", "phoneNumber": "example", "phoneNumberVerified": true, "addressLine1": "example", "addressLine2": "example", "addressLocality": "example", "addressRegion": "example", "addressPostalCode": "example", "addressCountry": "example", "otpEnabled": true } ], "total": 1, "page": 1, "size": 1 } ``` ### 400 Invalid request body or parameters application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge when token transport is malformed; ordinary validation errors omit it. ### 401 Missing or invalid bearer token application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on token refusals. An operation may also return a handler-level 401 without this header. ### 403 The token is valid but does not authorise this request. Four checks answer here, and a caller distinguishes them by error_code. INSUFFICIENT_SCOPE comes from the route’s authorisation middleware, before the handler runs: an Admin operation needs one of the scopes its route names, and an Account operation needs `authserver:manage-account`. USER_CONTEXT_REQUIRED comes from the same middleware on the Account operations, which also require the token to have been issued for a user: one obtained through the client credentials grant carries no auth_time claim and is refused here. FORBIDDEN comes from the handler, on the account endpoints that resolve a resource by id and then check it belongs to the caller, so a valid token for the wrong user reaches this rather than a 404. MANAGE_SCOPE_REQUIRED comes from the handler, on the Admin operations the administrative policy guards. Only `authserver:manage` creates an administrator, changes one, or changes what reaches one, so no granular scope will ever be enough: the remedy is a token holding `authserver:manage`, not requesting the route’s scope as for INSUFFICIENT_SCOPE. An administrator is a user, group or client holding any of the six administrative permissions (`manage`, `admin-read`, `manage-users`, `manage-clients`, `manage-settings`, `browser-sessions`), a user directly or through any of its groups, the admin console’s own client whatever it holds, and a client allowed to request the administrative scopes. A granular scope is refused here when it: - grants or revokes an administrative permission, directly or by moving a user into or out of a group that holds one, or deletes such a group; - writes to an administrator user (profile, credentials, enabled, sessions, consents, attributes, memberships, permissions, deletion), an administrative group (settings, attributes, members, permissions, deletion), or an administrator client (settings, authentication, flows, redirect URIs, web origins, token settings, permissions, logo, deletion); - switches whether a client may request the administrative scopes, on or off, on any client; - reads an administrator client’s secret; - changes the email or the audit-log settings; - changes an administrative permission’s description. Granular scopes still read administrators. The refusal is answered after the request’s 400 and 404, writes nothing, carries `WWW-Authenticate: Bearer realm="goiabada", error="insufficient_scope", error_description="...", scope="authserver:manage"`, and leaves an `administrator_change_refused` audit record. Every operation this document secures declares a 403. The public client logo endpoint, which carries `security: []` and authorises nothing, is the one that does not. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on scope and administrative-policy refusals; other forbidden responses omit it. ### 404 Resource not found application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` ### 500 The request failed inside the server, most often a storage error. error_code is INTERNAL_SERVER_ERROR: no detail about the failure, only the request id an operator can find the log line by, and retrying is reasonable. Key rotation’s 500 also answers KEY_SET_INCOMPLETE, which names its cause and no retry clears. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` # Add group member Source: https://goiabada.dev/reference/api/admin/operations/addgroupmember/ POST /api/v1/admin/groups/{id}/members Shell / cURL ```sh curl --request POST \ --url http://localhost:9090/api/v1/admin/groups/1/members \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "userId": 1 }' ``` - Auth server base URL{baseUrl}/api/v1/admin/groups/{id}/members Add a user to a group ## Authorizations - **[BearerAuth](https://goiabada.dev/reference/api/admin/#bearerauth)** ## Parameters ### Path Parameters **id** required integer format: int64 Group ID ## Request Bodyrequired application/json object **userId** required integer format: int64 ### Example generated ```json { "userId": 1 } ``` ## Responses ### 201 Member added application/json object **success** required boolean #### Example generated ```json { "success": true } ``` ### 400 Invalid request body or parameters application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge when token transport is malformed; ordinary validation errors omit it. ### 401 Missing or invalid bearer token application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on token refusals. An operation may also return a handler-level 401 without this header. ### 403 The token is valid but does not authorise this request. Four checks answer here, and a caller distinguishes them by error_code. INSUFFICIENT_SCOPE comes from the route’s authorisation middleware, before the handler runs: an Admin operation needs one of the scopes its route names, and an Account operation needs `authserver:manage-account`. USER_CONTEXT_REQUIRED comes from the same middleware on the Account operations, which also require the token to have been issued for a user: one obtained through the client credentials grant carries no auth_time claim and is refused here. FORBIDDEN comes from the handler, on the account endpoints that resolve a resource by id and then check it belongs to the caller, so a valid token for the wrong user reaches this rather than a 404. MANAGE_SCOPE_REQUIRED comes from the handler, on the Admin operations the administrative policy guards. Only `authserver:manage` creates an administrator, changes one, or changes what reaches one, so no granular scope will ever be enough: the remedy is a token holding `authserver:manage`, not requesting the route’s scope as for INSUFFICIENT_SCOPE. An administrator is a user, group or client holding any of the six administrative permissions (`manage`, `admin-read`, `manage-users`, `manage-clients`, `manage-settings`, `browser-sessions`), a user directly or through any of its groups, the admin console’s own client whatever it holds, and a client allowed to request the administrative scopes. A granular scope is refused here when it: - grants or revokes an administrative permission, directly or by moving a user into or out of a group that holds one, or deletes such a group; - writes to an administrator user (profile, credentials, enabled, sessions, consents, attributes, memberships, permissions, deletion), an administrative group (settings, attributes, members, permissions, deletion), or an administrator client (settings, authentication, flows, redirect URIs, web origins, token settings, permissions, logo, deletion); - switches whether a client may request the administrative scopes, on or off, on any client; - reads an administrator client’s secret; - changes the email or the audit-log settings; - changes an administrative permission’s description. Granular scopes still read administrators. The refusal is answered after the request’s 400 and 404, writes nothing, carries `WWW-Authenticate: Bearer realm="goiabada", error="insufficient_scope", error_description="...", scope="authserver:manage"`, and leaves an `administrator_change_refused` audit record. Every operation this document secures declares a 403. The public client logo endpoint, which carries `security: []` and authorises nothing, is the one that does not. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on scope and administrative-policy refusals; other forbidden responses omit it. ### 404 Resource not found application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` ### 500 The request failed inside the server, most often a storage error. error_code is INTERNAL_SERVER_ERROR: no detail about the failure, only the request id an operator can find the log line by, and retrying is reasonable. Key rotation’s 500 also answers KEY_SET_INCOMPLETE, which names its cause and no retry clears. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` # Remove group member Source: https://goiabada.dev/reference/api/admin/operations/removegroupmember/ DELETE /api/v1/admin/groups/{id}/members/{userId} Shell / cURL ```sh curl --request DELETE \ --url http://localhost:9090/api/v1/admin/groups/1/members/1 \ --header 'Authorization: Bearer ' ``` - Auth server base URL{baseUrl}/api/v1/admin/groups/{id}/members/{userId} Remove a user from a group ## Authorizations - **[BearerAuth](https://goiabada.dev/reference/api/admin/#bearerauth)** ## Parameters ### Path Parameters **id** required integer format: int64 Group ID **userId** required integer format: int64 User ID to remove ## Responses ### 200 Member removed application/json object **success** required boolean #### Example generated ```json { "success": true } ``` ### 400 Invalid request body or parameters application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge when token transport is malformed; ordinary validation errors omit it. ### 401 Missing or invalid bearer token application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on token refusals. An operation may also return a handler-level 401 without this header. ### 403 The token is valid but does not authorise this request. Four checks answer here, and a caller distinguishes them by error_code. INSUFFICIENT_SCOPE comes from the route’s authorisation middleware, before the handler runs: an Admin operation needs one of the scopes its route names, and an Account operation needs `authserver:manage-account`. USER_CONTEXT_REQUIRED comes from the same middleware on the Account operations, which also require the token to have been issued for a user: one obtained through the client credentials grant carries no auth_time claim and is refused here. FORBIDDEN comes from the handler, on the account endpoints that resolve a resource by id and then check it belongs to the caller, so a valid token for the wrong user reaches this rather than a 404. MANAGE_SCOPE_REQUIRED comes from the handler, on the Admin operations the administrative policy guards. Only `authserver:manage` creates an administrator, changes one, or changes what reaches one, so no granular scope will ever be enough: the remedy is a token holding `authserver:manage`, not requesting the route’s scope as for INSUFFICIENT_SCOPE. An administrator is a user, group or client holding any of the six administrative permissions (`manage`, `admin-read`, `manage-users`, `manage-clients`, `manage-settings`, `browser-sessions`), a user directly or through any of its groups, the admin console’s own client whatever it holds, and a client allowed to request the administrative scopes. A granular scope is refused here when it: - grants or revokes an administrative permission, directly or by moving a user into or out of a group that holds one, or deletes such a group; - writes to an administrator user (profile, credentials, enabled, sessions, consents, attributes, memberships, permissions, deletion), an administrative group (settings, attributes, members, permissions, deletion), or an administrator client (settings, authentication, flows, redirect URIs, web origins, token settings, permissions, logo, deletion); - switches whether a client may request the administrative scopes, on or off, on any client; - reads an administrator client’s secret; - changes the email or the audit-log settings; - changes an administrative permission’s description. Granular scopes still read administrators. The refusal is answered after the request’s 400 and 404, writes nothing, carries `WWW-Authenticate: Bearer realm="goiabada", error="insufficient_scope", error_description="...", scope="authserver:manage"`, and leaves an `administrator_change_refused` audit record. Every operation this document secures declares a 403. The public client logo endpoint, which carries `security: []` and authorises nothing, is the one that does not. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on scope and administrative-policy refusals; other forbidden responses omit it. ### 404 Resource not found application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` ### 409 The change would leave no enabled user holding `authserver:manage`, directly or through a group. error_code is LAST_ADMINISTRATOR; nothing was changed. Grant `authserver:manage` to another user first, then repeat the request. A client holding the permission does not count, and neither does a disabled user. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` ### 500 The request failed inside the server, most often a storage error. error_code is INTERNAL_SERVER_ERROR: no detail about the failure, only the request id an operator can find the log line by, and retrying is reasonable. Key rotation’s 500 also answers KEY_SET_INCOMPLETE, which names its cause and no retry clears. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` # Overview Source: https://goiabada.dev/reference/api/admin/operations/tags/group-attributes/ ## Group Attributes Custom group attributes (Admin API) ## Operations GET [/api/v1/admin/groups/{id}/attributes](https://goiabada.dev/reference/api/admin/operations/getgroupattributes/) POST [/api/v1/admin/group-attributes](https://goiabada.dev/reference/api/admin/operations/creategroupattribute/) GET [/api/v1/admin/group-attributes/{id}](https://goiabada.dev/reference/api/admin/operations/getgroupattribute/) PUT [/api/v1/admin/group-attributes/{id}](https://goiabada.dev/reference/api/admin/operations/updategroupattribute/) DELETE [/api/v1/admin/group-attributes/{id}](https://goiabada.dev/reference/api/admin/operations/deletegroupattribute/) # List group attributes Source: https://goiabada.dev/reference/api/admin/operations/getgroupattributes/ GET /api/v1/admin/groups/{id}/attributes Shell / cURL ```sh curl --request GET \ --url http://localhost:9090/api/v1/admin/groups/1/attributes \ --header 'Authorization: Bearer ' ``` - Auth server base URL{baseUrl}/api/v1/admin/groups/{id}/attributes Get all custom attributes for a group ## Authorizations - **[BearerAuth](https://goiabada.dev/reference/api/admin/#bearerauth)** ## Parameters ### Path Parameters **id** required integer format: int64 Group ID ## Responses ### 200 Group attributes application/json object **attributes** required Array\ object **id** required integer format: int64 **createdAt** required string format: date-time nullable **updatedAt** required string format: date-time nullable **key** required string **value** required string **includeInIdToken** required boolean **includeInAccessToken** required boolean **groupId** required integer format: int64 #### Example generated ```json { "attributes": [ { "id": 1, "createdAt": "2026-04-15T12:00:00Z", "updatedAt": "2026-04-15T12:00:00Z", "key": "example", "value": "example", "includeInIdToken": true, "includeInAccessToken": true, "groupId": 1 } ] } ``` ### 400 Invalid request body or parameters application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge when token transport is malformed; ordinary validation errors omit it. ### 401 Missing or invalid bearer token application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on token refusals. An operation may also return a handler-level 401 without this header. ### 403 The token is valid but does not authorise this request. Four checks answer here, and a caller distinguishes them by error_code. INSUFFICIENT_SCOPE comes from the route’s authorisation middleware, before the handler runs: an Admin operation needs one of the scopes its route names, and an Account operation needs `authserver:manage-account`. USER_CONTEXT_REQUIRED comes from the same middleware on the Account operations, which also require the token to have been issued for a user: one obtained through the client credentials grant carries no auth_time claim and is refused here. FORBIDDEN comes from the handler, on the account endpoints that resolve a resource by id and then check it belongs to the caller, so a valid token for the wrong user reaches this rather than a 404. MANAGE_SCOPE_REQUIRED comes from the handler, on the Admin operations the administrative policy guards. Only `authserver:manage` creates an administrator, changes one, or changes what reaches one, so no granular scope will ever be enough: the remedy is a token holding `authserver:manage`, not requesting the route’s scope as for INSUFFICIENT_SCOPE. An administrator is a user, group or client holding any of the six administrative permissions (`manage`, `admin-read`, `manage-users`, `manage-clients`, `manage-settings`, `browser-sessions`), a user directly or through any of its groups, the admin console’s own client whatever it holds, and a client allowed to request the administrative scopes. A granular scope is refused here when it: - grants or revokes an administrative permission, directly or by moving a user into or out of a group that holds one, or deletes such a group; - writes to an administrator user (profile, credentials, enabled, sessions, consents, attributes, memberships, permissions, deletion), an administrative group (settings, attributes, members, permissions, deletion), or an administrator client (settings, authentication, flows, redirect URIs, web origins, token settings, permissions, logo, deletion); - switches whether a client may request the administrative scopes, on or off, on any client; - reads an administrator client’s secret; - changes the email or the audit-log settings; - changes an administrative permission’s description. Granular scopes still read administrators. The refusal is answered after the request’s 400 and 404, writes nothing, carries `WWW-Authenticate: Bearer realm="goiabada", error="insufficient_scope", error_description="...", scope="authserver:manage"`, and leaves an `administrator_change_refused` audit record. Every operation this document secures declares a 403. The public client logo endpoint, which carries `security: []` and authorises nothing, is the one that does not. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on scope and administrative-policy refusals; other forbidden responses omit it. ### 404 Resource not found application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` ### 500 The request failed inside the server, most often a storage error. error_code is INTERNAL_SERVER_ERROR: no detail about the failure, only the request id an operator can find the log line by, and retrying is reasonable. Key rotation’s 500 also answers KEY_SET_INCOMPLETE, which names its cause and no retry clears. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` # Create group attribute Source: https://goiabada.dev/reference/api/admin/operations/creategroupattribute/ POST /api/v1/admin/group-attributes Shell / cURL ```sh curl --request POST \ --url http://localhost:9090/api/v1/admin/group-attributes \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "key": "example", "value": "example", "includeInIdToken": true, "includeInAccessToken": true, "groupId": 1 }' ``` - Auth server base URL{baseUrl}/api/v1/admin/group-attributes Create a new custom attribute for a group ## Authorizations - **[BearerAuth](https://goiabada.dev/reference/api/admin/#bearerauth)** ## Request Bodyrequired application/json object **key** required string **value** required string **includeInIdToken** boolean **includeInAccessToken** boolean **groupId** required integer format: int64 ### Example generated ```json { "key": "example", "value": "example", "includeInIdToken": true, "includeInAccessToken": true, "groupId": 1 } ``` ## Responses ### 201 Attribute created application/json object **attribute** required object **id** required integer format: int64 **createdAt** required string format: date-time nullable **updatedAt** required string format: date-time nullable **key** required string **value** required string **includeInIdToken** required boolean **includeInAccessToken** required boolean **groupId** required integer format: int64 #### Example generated ```json { "attribute": { "id": 1, "createdAt": "2026-04-15T12:00:00Z", "updatedAt": "2026-04-15T12:00:00Z", "key": "example", "value": "example", "includeInIdToken": true, "includeInAccessToken": true, "groupId": 1 } } ``` ### 400 Invalid request body or parameters application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge when token transport is malformed; ordinary validation errors omit it. ### 401 Missing or invalid bearer token application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on token refusals. An operation may also return a handler-level 401 without this header. ### 403 The token is valid but does not authorise this request. Four checks answer here, and a caller distinguishes them by error_code. INSUFFICIENT_SCOPE comes from the route’s authorisation middleware, before the handler runs: an Admin operation needs one of the scopes its route names, and an Account operation needs `authserver:manage-account`. USER_CONTEXT_REQUIRED comes from the same middleware on the Account operations, which also require the token to have been issued for a user: one obtained through the client credentials grant carries no auth_time claim and is refused here. FORBIDDEN comes from the handler, on the account endpoints that resolve a resource by id and then check it belongs to the caller, so a valid token for the wrong user reaches this rather than a 404. MANAGE_SCOPE_REQUIRED comes from the handler, on the Admin operations the administrative policy guards. Only `authserver:manage` creates an administrator, changes one, or changes what reaches one, so no granular scope will ever be enough: the remedy is a token holding `authserver:manage`, not requesting the route’s scope as for INSUFFICIENT_SCOPE. An administrator is a user, group or client holding any of the six administrative permissions (`manage`, `admin-read`, `manage-users`, `manage-clients`, `manage-settings`, `browser-sessions`), a user directly or through any of its groups, the admin console’s own client whatever it holds, and a client allowed to request the administrative scopes. A granular scope is refused here when it: - grants or revokes an administrative permission, directly or by moving a user into or out of a group that holds one, or deletes such a group; - writes to an administrator user (profile, credentials, enabled, sessions, consents, attributes, memberships, permissions, deletion), an administrative group (settings, attributes, members, permissions, deletion), or an administrator client (settings, authentication, flows, redirect URIs, web origins, token settings, permissions, logo, deletion); - switches whether a client may request the administrative scopes, on or off, on any client; - reads an administrator client’s secret; - changes the email or the audit-log settings; - changes an administrative permission’s description. Granular scopes still read administrators. The refusal is answered after the request’s 400 and 404, writes nothing, carries `WWW-Authenticate: Bearer realm="goiabada", error="insufficient_scope", error_description="...", scope="authserver:manage"`, and leaves an `administrator_change_refused` audit record. Every operation this document secures declares a 403. The public client logo endpoint, which carries `security: []` and authorises nothing, is the one that does not. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on scope and administrative-policy refusals; other forbidden responses omit it. ### 404 Resource not found application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` ### 500 The request failed inside the server, most often a storage error. error_code is INTERNAL_SERVER_ERROR: no detail about the failure, only the request id an operator can find the log line by, and retrying is reasonable. Key rotation’s 500 also answers KEY_SET_INCOMPLETE, which names its cause and no retry clears. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` # Get group attribute Source: https://goiabada.dev/reference/api/admin/operations/getgroupattribute/ GET /api/v1/admin/group-attributes/{id} Shell / cURL ```sh curl --request GET \ --url http://localhost:9090/api/v1/admin/group-attributes/1 \ --header 'Authorization: Bearer ' ``` - Auth server base URL{baseUrl}/api/v1/admin/group-attributes/{id} Get a specific group attribute by ID ## Authorizations - **[BearerAuth](https://goiabada.dev/reference/api/admin/#bearerauth)** ## Parameters ### Path Parameters **id** required integer format: int64 Attribute ID ## Responses ### 200 Attribute details application/json object **attribute** required object **id** required integer format: int64 **createdAt** required string format: date-time nullable **updatedAt** required string format: date-time nullable **key** required string **value** required string **includeInIdToken** required boolean **includeInAccessToken** required boolean **groupId** required integer format: int64 #### Example generated ```json { "attribute": { "id": 1, "createdAt": "2026-04-15T12:00:00Z", "updatedAt": "2026-04-15T12:00:00Z", "key": "example", "value": "example", "includeInIdToken": true, "includeInAccessToken": true, "groupId": 1 } } ``` ### 400 Invalid request body or parameters application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge when token transport is malformed; ordinary validation errors omit it. ### 401 Missing or invalid bearer token application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on token refusals. An operation may also return a handler-level 401 without this header. ### 403 The token is valid but does not authorise this request. Four checks answer here, and a caller distinguishes them by error_code. INSUFFICIENT_SCOPE comes from the route’s authorisation middleware, before the handler runs: an Admin operation needs one of the scopes its route names, and an Account operation needs `authserver:manage-account`. USER_CONTEXT_REQUIRED comes from the same middleware on the Account operations, which also require the token to have been issued for a user: one obtained through the client credentials grant carries no auth_time claim and is refused here. FORBIDDEN comes from the handler, on the account endpoints that resolve a resource by id and then check it belongs to the caller, so a valid token for the wrong user reaches this rather than a 404. MANAGE_SCOPE_REQUIRED comes from the handler, on the Admin operations the administrative policy guards. Only `authserver:manage` creates an administrator, changes one, or changes what reaches one, so no granular scope will ever be enough: the remedy is a token holding `authserver:manage`, not requesting the route’s scope as for INSUFFICIENT_SCOPE. An administrator is a user, group or client holding any of the six administrative permissions (`manage`, `admin-read`, `manage-users`, `manage-clients`, `manage-settings`, `browser-sessions`), a user directly or through any of its groups, the admin console’s own client whatever it holds, and a client allowed to request the administrative scopes. A granular scope is refused here when it: - grants or revokes an administrative permission, directly or by moving a user into or out of a group that holds one, or deletes such a group; - writes to an administrator user (profile, credentials, enabled, sessions, consents, attributes, memberships, permissions, deletion), an administrative group (settings, attributes, members, permissions, deletion), or an administrator client (settings, authentication, flows, redirect URIs, web origins, token settings, permissions, logo, deletion); - switches whether a client may request the administrative scopes, on or off, on any client; - reads an administrator client’s secret; - changes the email or the audit-log settings; - changes an administrative permission’s description. Granular scopes still read administrators. The refusal is answered after the request’s 400 and 404, writes nothing, carries `WWW-Authenticate: Bearer realm="goiabada", error="insufficient_scope", error_description="...", scope="authserver:manage"`, and leaves an `administrator_change_refused` audit record. Every operation this document secures declares a 403. The public client logo endpoint, which carries `security: []` and authorises nothing, is the one that does not. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on scope and administrative-policy refusals; other forbidden responses omit it. ### 404 Resource not found application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` ### 500 The request failed inside the server, most often a storage error. error_code is INTERNAL_SERVER_ERROR: no detail about the failure, only the request id an operator can find the log line by, and retrying is reasonable. Key rotation’s 500 also answers KEY_SET_INCOMPLETE, which names its cause and no retry clears. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` # Update group attribute Source: https://goiabada.dev/reference/api/admin/operations/updategroupattribute/ PUT /api/v1/admin/group-attributes/{id} Shell / cURL ```sh curl --request PUT \ --url http://localhost:9090/api/v1/admin/group-attributes/1 \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "key": "example", "value": "example", "includeInIdToken": true, "includeInAccessToken": true }' ``` - Auth server base URL{baseUrl}/api/v1/admin/group-attributes/{id} Update a group attribute ## Authorizations - **[BearerAuth](https://goiabada.dev/reference/api/admin/#bearerauth)** ## Parameters ### Path Parameters **id** required integer format: int64 Attribute ID ## Request Bodyrequired application/json object **key** string **value** string **includeInIdToken** boolean **includeInAccessToken** boolean ### Example generated ```json { "key": "example", "value": "example", "includeInIdToken": true, "includeInAccessToken": true } ``` ## Responses ### 200 Attribute updated application/json object **attribute** required object **id** required integer format: int64 **createdAt** required string format: date-time nullable **updatedAt** required string format: date-time nullable **key** required string **value** required string **includeInIdToken** required boolean **includeInAccessToken** required boolean **groupId** required integer format: int64 #### Example generated ```json { "attribute": { "id": 1, "createdAt": "2026-04-15T12:00:00Z", "updatedAt": "2026-04-15T12:00:00Z", "key": "example", "value": "example", "includeInIdToken": true, "includeInAccessToken": true, "groupId": 1 } } ``` ### 400 Invalid request body or parameters application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge when token transport is malformed; ordinary validation errors omit it. ### 401 Missing or invalid bearer token application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on token refusals. An operation may also return a handler-level 401 without this header. ### 403 The token is valid but does not authorise this request. Four checks answer here, and a caller distinguishes them by error_code. INSUFFICIENT_SCOPE comes from the route’s authorisation middleware, before the handler runs: an Admin operation needs one of the scopes its route names, and an Account operation needs `authserver:manage-account`. USER_CONTEXT_REQUIRED comes from the same middleware on the Account operations, which also require the token to have been issued for a user: one obtained through the client credentials grant carries no auth_time claim and is refused here. FORBIDDEN comes from the handler, on the account endpoints that resolve a resource by id and then check it belongs to the caller, so a valid token for the wrong user reaches this rather than a 404. MANAGE_SCOPE_REQUIRED comes from the handler, on the Admin operations the administrative policy guards. Only `authserver:manage` creates an administrator, changes one, or changes what reaches one, so no granular scope will ever be enough: the remedy is a token holding `authserver:manage`, not requesting the route’s scope as for INSUFFICIENT_SCOPE. An administrator is a user, group or client holding any of the six administrative permissions (`manage`, `admin-read`, `manage-users`, `manage-clients`, `manage-settings`, `browser-sessions`), a user directly or through any of its groups, the admin console’s own client whatever it holds, and a client allowed to request the administrative scopes. A granular scope is refused here when it: - grants or revokes an administrative permission, directly or by moving a user into or out of a group that holds one, or deletes such a group; - writes to an administrator user (profile, credentials, enabled, sessions, consents, attributes, memberships, permissions, deletion), an administrative group (settings, attributes, members, permissions, deletion), or an administrator client (settings, authentication, flows, redirect URIs, web origins, token settings, permissions, logo, deletion); - switches whether a client may request the administrative scopes, on or off, on any client; - reads an administrator client’s secret; - changes the email or the audit-log settings; - changes an administrative permission’s description. Granular scopes still read administrators. The refusal is answered after the request’s 400 and 404, writes nothing, carries `WWW-Authenticate: Bearer realm="goiabada", error="insufficient_scope", error_description="...", scope="authserver:manage"`, and leaves an `administrator_change_refused` audit record. Every operation this document secures declares a 403. The public client logo endpoint, which carries `security: []` and authorises nothing, is the one that does not. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on scope and administrative-policy refusals; other forbidden responses omit it. ### 404 Resource not found application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` ### 500 The request failed inside the server, most often a storage error. error_code is INTERNAL_SERVER_ERROR: no detail about the failure, only the request id an operator can find the log line by, and retrying is reasonable. Key rotation’s 500 also answers KEY_SET_INCOMPLETE, which names its cause and no retry clears. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` # Delete group attribute Source: https://goiabada.dev/reference/api/admin/operations/deletegroupattribute/ DELETE /api/v1/admin/group-attributes/{id} Shell / cURL ```sh curl --request DELETE \ --url http://localhost:9090/api/v1/admin/group-attributes/1 \ --header 'Authorization: Bearer ' ``` - Auth server base URL{baseUrl}/api/v1/admin/group-attributes/{id} Delete a group attribute ## Authorizations - **[BearerAuth](https://goiabada.dev/reference/api/admin/#bearerauth)** ## Parameters ### Path Parameters **id** required integer format: int64 Attribute ID ## Responses ### 200 Attribute deleted application/json object **success** required boolean #### Example generated ```json { "success": true } ``` ### 400 Invalid request body or parameters application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge when token transport is malformed; ordinary validation errors omit it. ### 401 Missing or invalid bearer token application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on token refusals. An operation may also return a handler-level 401 without this header. ### 403 The token is valid but does not authorise this request. Four checks answer here, and a caller distinguishes them by error_code. INSUFFICIENT_SCOPE comes from the route’s authorisation middleware, before the handler runs: an Admin operation needs one of the scopes its route names, and an Account operation needs `authserver:manage-account`. USER_CONTEXT_REQUIRED comes from the same middleware on the Account operations, which also require the token to have been issued for a user: one obtained through the client credentials grant carries no auth_time claim and is refused here. FORBIDDEN comes from the handler, on the account endpoints that resolve a resource by id and then check it belongs to the caller, so a valid token for the wrong user reaches this rather than a 404. MANAGE_SCOPE_REQUIRED comes from the handler, on the Admin operations the administrative policy guards. Only `authserver:manage` creates an administrator, changes one, or changes what reaches one, so no granular scope will ever be enough: the remedy is a token holding `authserver:manage`, not requesting the route’s scope as for INSUFFICIENT_SCOPE. An administrator is a user, group or client holding any of the six administrative permissions (`manage`, `admin-read`, `manage-users`, `manage-clients`, `manage-settings`, `browser-sessions`), a user directly or through any of its groups, the admin console’s own client whatever it holds, and a client allowed to request the administrative scopes. A granular scope is refused here when it: - grants or revokes an administrative permission, directly or by moving a user into or out of a group that holds one, or deletes such a group; - writes to an administrator user (profile, credentials, enabled, sessions, consents, attributes, memberships, permissions, deletion), an administrative group (settings, attributes, members, permissions, deletion), or an administrator client (settings, authentication, flows, redirect URIs, web origins, token settings, permissions, logo, deletion); - switches whether a client may request the administrative scopes, on or off, on any client; - reads an administrator client’s secret; - changes the email or the audit-log settings; - changes an administrative permission’s description. Granular scopes still read administrators. The refusal is answered after the request’s 400 and 404, writes nothing, carries `WWW-Authenticate: Bearer realm="goiabada", error="insufficient_scope", error_description="...", scope="authserver:manage"`, and leaves an `administrator_change_refused` audit record. Every operation this document secures declares a 403. The public client logo endpoint, which carries `security: []` and authorises nothing, is the one that does not. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on scope and administrative-policy refusals; other forbidden responses omit it. ### 404 Resource not found application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` ### 500 The request failed inside the server, most often a storage error. error_code is INTERNAL_SERVER_ERROR: no detail about the failure, only the request id an operator can find the log line by, and retrying is reasonable. Key rotation’s 500 also answers KEY_SET_INCOMPLETE, which names its cause and no retry clears. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` # Overview Source: https://goiabada.dev/reference/api/admin/operations/tags/resources/ ## Resources Resource management (Admin API) ## Operations GET [/api/v1/admin/resources](https://goiabada.dev/reference/api/admin/operations/getresources/) POST [/api/v1/admin/resources](https://goiabada.dev/reference/api/admin/operations/createresource/) GET [/api/v1/admin/resources/{id}](https://goiabada.dev/reference/api/admin/operations/getresource/) PUT [/api/v1/admin/resources/{id}](https://goiabada.dev/reference/api/admin/operations/updateresource/) DELETE [/api/v1/admin/resources/{id}](https://goiabada.dev/reference/api/admin/operations/deleteresource/) GET [/api/v1/admin/resources/{id}/permissions](https://goiabada.dev/reference/api/admin/operations/getresourcepermissions/) PUT [/api/v1/admin/resources/{id}/permissions](https://goiabada.dev/reference/api/admin/operations/updateresourcepermissions/) # List resources Source: https://goiabada.dev/reference/api/admin/operations/getresources/ GET /api/v1/admin/resources Shell / cURL ```sh curl --request GET \ --url http://localhost:9090/api/v1/admin/resources \ --header 'Authorization: Bearer ' ``` - Auth server base URL{baseUrl}/api/v1/admin/resources Get all resources ## Authorizations - **[BearerAuth](https://goiabada.dev/reference/api/admin/#bearerauth)** ## Responses ### 200 Resources list application/json object **resources** required Null when no resources are stored. Array\ nullable object **id** required integer format: int64 **resourceIdentifier** required string **description** required string **isSystemLevelResource** required True for a resource this server owns and enforces as system level: rename and delete are refused on it with 403. A consumer should disable those controls rather than recompute the rule. boolean #### Example generated ```json { "resources": [ { "id": 1, "resourceIdentifier": "example", "description": "example", "isSystemLevelResource": true } ] } ``` ### 401 Missing or invalid bearer token application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on token refusals. An operation may also return a handler-level 401 without this header. ### 403 The token is valid but does not authorise this request. Four checks answer here, and a caller distinguishes them by error_code. INSUFFICIENT_SCOPE comes from the route’s authorisation middleware, before the handler runs: an Admin operation needs one of the scopes its route names, and an Account operation needs `authserver:manage-account`. USER_CONTEXT_REQUIRED comes from the same middleware on the Account operations, which also require the token to have been issued for a user: one obtained through the client credentials grant carries no auth_time claim and is refused here. FORBIDDEN comes from the handler, on the account endpoints that resolve a resource by id and then check it belongs to the caller, so a valid token for the wrong user reaches this rather than a 404. MANAGE_SCOPE_REQUIRED comes from the handler, on the Admin operations the administrative policy guards. Only `authserver:manage` creates an administrator, changes one, or changes what reaches one, so no granular scope will ever be enough: the remedy is a token holding `authserver:manage`, not requesting the route’s scope as for INSUFFICIENT_SCOPE. An administrator is a user, group or client holding any of the six administrative permissions (`manage`, `admin-read`, `manage-users`, `manage-clients`, `manage-settings`, `browser-sessions`), a user directly or through any of its groups, the admin console’s own client whatever it holds, and a client allowed to request the administrative scopes. A granular scope is refused here when it: - grants or revokes an administrative permission, directly or by moving a user into or out of a group that holds one, or deletes such a group; - writes to an administrator user (profile, credentials, enabled, sessions, consents, attributes, memberships, permissions, deletion), an administrative group (settings, attributes, members, permissions, deletion), or an administrator client (settings, authentication, flows, redirect URIs, web origins, token settings, permissions, logo, deletion); - switches whether a client may request the administrative scopes, on or off, on any client; - reads an administrator client’s secret; - changes the email or the audit-log settings; - changes an administrative permission’s description. Granular scopes still read administrators. The refusal is answered after the request’s 400 and 404, writes nothing, carries `WWW-Authenticate: Bearer realm="goiabada", error="insufficient_scope", error_description="...", scope="authserver:manage"`, and leaves an `administrator_change_refused` audit record. Every operation this document secures declares a 403. The public client logo endpoint, which carries `security: []` and authorises nothing, is the one that does not. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on scope and administrative-policy refusals; other forbidden responses omit it. ### 500 The request failed inside the server, most often a storage error. error_code is INTERNAL_SERVER_ERROR: no detail about the failure, only the request id an operator can find the log line by, and retrying is reasonable. Key rotation’s 500 also answers KEY_SET_INCOMPLETE, which names its cause and no retry clears. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` # Create resource Source: https://goiabada.dev/reference/api/admin/operations/createresource/ POST /api/v1/admin/resources Shell / cURL ```sh curl --request POST \ --url http://localhost:9090/api/v1/admin/resources \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "resourceIdentifier": "example", "description": "example" }' ``` - Auth server base URL{baseUrl}/api/v1/admin/resources Create a new resource ## Authorizations - **[BearerAuth](https://goiabada.dev/reference/api/admin/#bearerauth)** ## Request Bodyrequired application/json object **resourceIdentifier** required string **description** At most 100 UTF-16 code units. string ### Example generated ```json { "resourceIdentifier": "example", "description": "example" } ``` ## Responses ### 201 Resource created application/json object **resource** required object **id** required integer format: int64 **resourceIdentifier** required string **description** required string **isSystemLevelResource** required True for a resource this server owns and enforces as system level: rename and delete are refused on it with 403. A consumer should disable those controls rather than recompute the rule. boolean #### Example generated ```json { "resource": { "id": 1, "resourceIdentifier": "example", "description": "example", "isSystemLevelResource": true } } ``` ### 400 Invalid request body or parameters application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge when token transport is malformed; ordinary validation errors omit it. ### 401 Missing or invalid bearer token application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on token refusals. An operation may also return a handler-level 401 without this header. ### 403 The token is valid but does not authorise this request. Four checks answer here, and a caller distinguishes them by error_code. INSUFFICIENT_SCOPE comes from the route’s authorisation middleware, before the handler runs: an Admin operation needs one of the scopes its route names, and an Account operation needs `authserver:manage-account`. USER_CONTEXT_REQUIRED comes from the same middleware on the Account operations, which also require the token to have been issued for a user: one obtained through the client credentials grant carries no auth_time claim and is refused here. FORBIDDEN comes from the handler, on the account endpoints that resolve a resource by id and then check it belongs to the caller, so a valid token for the wrong user reaches this rather than a 404. MANAGE_SCOPE_REQUIRED comes from the handler, on the Admin operations the administrative policy guards. Only `authserver:manage` creates an administrator, changes one, or changes what reaches one, so no granular scope will ever be enough: the remedy is a token holding `authserver:manage`, not requesting the route’s scope as for INSUFFICIENT_SCOPE. An administrator is a user, group or client holding any of the six administrative permissions (`manage`, `admin-read`, `manage-users`, `manage-clients`, `manage-settings`, `browser-sessions`), a user directly or through any of its groups, the admin console’s own client whatever it holds, and a client allowed to request the administrative scopes. A granular scope is refused here when it: - grants or revokes an administrative permission, directly or by moving a user into or out of a group that holds one, or deletes such a group; - writes to an administrator user (profile, credentials, enabled, sessions, consents, attributes, memberships, permissions, deletion), an administrative group (settings, attributes, members, permissions, deletion), or an administrator client (settings, authentication, flows, redirect URIs, web origins, token settings, permissions, logo, deletion); - switches whether a client may request the administrative scopes, on or off, on any client; - reads an administrator client’s secret; - changes the email or the audit-log settings; - changes an administrative permission’s description. Granular scopes still read administrators. The refusal is answered after the request’s 400 and 404, writes nothing, carries `WWW-Authenticate: Bearer realm="goiabada", error="insufficient_scope", error_description="...", scope="authserver:manage"`, and leaves an `administrator_change_refused` audit record. Every operation this document secures declares a 403. The public client logo endpoint, which carries `security: []` and authorises nothing, is the one that does not. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on scope and administrative-policy refusals; other forbidden responses omit it. ### 500 The request failed inside the server, most often a storage error. error_code is INTERNAL_SERVER_ERROR: no detail about the failure, only the request id an operator can find the log line by, and retrying is reasonable. Key rotation’s 500 also answers KEY_SET_INCOMPLETE, which names its cause and no retry clears. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` # Get resource Source: https://goiabada.dev/reference/api/admin/operations/getresource/ GET /api/v1/admin/resources/{id} Shell / cURL ```sh curl --request GET \ --url http://localhost:9090/api/v1/admin/resources/1 \ --header 'Authorization: Bearer ' ``` - Auth server base URL{baseUrl}/api/v1/admin/resources/{id} Get resource details by ID ## Authorizations - **[BearerAuth](https://goiabada.dev/reference/api/admin/#bearerauth)** ## Parameters ### Path Parameters **id** required integer format: int64 Resource ID ## Responses ### 200 Resource details application/json object **resource** required object **id** required integer format: int64 **resourceIdentifier** required string **description** required string **isSystemLevelResource** required True for a resource this server owns and enforces as system level: rename and delete are refused on it with 403. A consumer should disable those controls rather than recompute the rule. boolean #### Example generated ```json { "resource": { "id": 1, "resourceIdentifier": "example", "description": "example", "isSystemLevelResource": true } } ``` ### 400 Invalid request body or parameters application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge when token transport is malformed; ordinary validation errors omit it. ### 401 Missing or invalid bearer token application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on token refusals. An operation may also return a handler-level 401 without this header. ### 403 The token is valid but does not authorise this request. Four checks answer here, and a caller distinguishes them by error_code. INSUFFICIENT_SCOPE comes from the route’s authorisation middleware, before the handler runs: an Admin operation needs one of the scopes its route names, and an Account operation needs `authserver:manage-account`. USER_CONTEXT_REQUIRED comes from the same middleware on the Account operations, which also require the token to have been issued for a user: one obtained through the client credentials grant carries no auth_time claim and is refused here. FORBIDDEN comes from the handler, on the account endpoints that resolve a resource by id and then check it belongs to the caller, so a valid token for the wrong user reaches this rather than a 404. MANAGE_SCOPE_REQUIRED comes from the handler, on the Admin operations the administrative policy guards. Only `authserver:manage` creates an administrator, changes one, or changes what reaches one, so no granular scope will ever be enough: the remedy is a token holding `authserver:manage`, not requesting the route’s scope as for INSUFFICIENT_SCOPE. An administrator is a user, group or client holding any of the six administrative permissions (`manage`, `admin-read`, `manage-users`, `manage-clients`, `manage-settings`, `browser-sessions`), a user directly or through any of its groups, the admin console’s own client whatever it holds, and a client allowed to request the administrative scopes. A granular scope is refused here when it: - grants or revokes an administrative permission, directly or by moving a user into or out of a group that holds one, or deletes such a group; - writes to an administrator user (profile, credentials, enabled, sessions, consents, attributes, memberships, permissions, deletion), an administrative group (settings, attributes, members, permissions, deletion), or an administrator client (settings, authentication, flows, redirect URIs, web origins, token settings, permissions, logo, deletion); - switches whether a client may request the administrative scopes, on or off, on any client; - reads an administrator client’s secret; - changes the email or the audit-log settings; - changes an administrative permission’s description. Granular scopes still read administrators. The refusal is answered after the request’s 400 and 404, writes nothing, carries `WWW-Authenticate: Bearer realm="goiabada", error="insufficient_scope", error_description="...", scope="authserver:manage"`, and leaves an `administrator_change_refused` audit record. Every operation this document secures declares a 403. The public client logo endpoint, which carries `security: []` and authorises nothing, is the one that does not. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on scope and administrative-policy refusals; other forbidden responses omit it. ### 404 Resource not found application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` ### 500 The request failed inside the server, most often a storage error. error_code is INTERNAL_SERVER_ERROR: no detail about the failure, only the request id an operator can find the log line by, and retrying is reasonable. Key rotation’s 500 also answers KEY_SET_INCOMPLETE, which names its cause and no retry clears. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` # Update resource Source: https://goiabada.dev/reference/api/admin/operations/updateresource/ PUT /api/v1/admin/resources/{id} Shell / cURL ```sh curl --request PUT \ --url http://localhost:9090/api/v1/admin/resources/1 \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "resourceIdentifier": "example", "description": "example" }' ``` - Auth server base URL{baseUrl}/api/v1/admin/resources/{id} Update resource details ## Authorizations - **[BearerAuth](https://goiabada.dev/reference/api/admin/#bearerauth)** ## Parameters ### Path Parameters **id** required integer format: int64 Resource ID ## Request Bodyrequired application/json object **resourceIdentifier** string **description** At most 100 UTF-16 code units. string ### Example generated ```json { "resourceIdentifier": "example", "description": "example" } ``` ## Responses ### 200 Resource updated application/json object **resource** required object **id** required integer format: int64 **resourceIdentifier** required string **description** required string **isSystemLevelResource** required True for a resource this server owns and enforces as system level: rename and delete are refused on it with 403. A consumer should disable those controls rather than recompute the rule. boolean #### Example generated ```json { "resource": { "id": 1, "resourceIdentifier": "example", "description": "example", "isSystemLevelResource": true } } ``` ### 400 Invalid request body or parameters application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge when token transport is malformed; ordinary validation errors omit it. ### 401 Missing or invalid bearer token application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on token refusals. An operation may also return a handler-level 401 without this header. ### 403 The token is valid but does not authorise this request. Four checks answer here, and a caller distinguishes them by error_code. INSUFFICIENT_SCOPE comes from the route’s authorisation middleware, before the handler runs: an Admin operation needs one of the scopes its route names, and an Account operation needs `authserver:manage-account`. USER_CONTEXT_REQUIRED comes from the same middleware on the Account operations, which also require the token to have been issued for a user: one obtained through the client credentials grant carries no auth_time claim and is refused here. FORBIDDEN comes from the handler, on the account endpoints that resolve a resource by id and then check it belongs to the caller, so a valid token for the wrong user reaches this rather than a 404. MANAGE_SCOPE_REQUIRED comes from the handler, on the Admin operations the administrative policy guards. Only `authserver:manage` creates an administrator, changes one, or changes what reaches one, so no granular scope will ever be enough: the remedy is a token holding `authserver:manage`, not requesting the route’s scope as for INSUFFICIENT_SCOPE. An administrator is a user, group or client holding any of the six administrative permissions (`manage`, `admin-read`, `manage-users`, `manage-clients`, `manage-settings`, `browser-sessions`), a user directly or through any of its groups, the admin console’s own client whatever it holds, and a client allowed to request the administrative scopes. A granular scope is refused here when it: - grants or revokes an administrative permission, directly or by moving a user into or out of a group that holds one, or deletes such a group; - writes to an administrator user (profile, credentials, enabled, sessions, consents, attributes, memberships, permissions, deletion), an administrative group (settings, attributes, members, permissions, deletion), or an administrator client (settings, authentication, flows, redirect URIs, web origins, token settings, permissions, logo, deletion); - switches whether a client may request the administrative scopes, on or off, on any client; - reads an administrator client’s secret; - changes the email or the audit-log settings; - changes an administrative permission’s description. Granular scopes still read administrators. The refusal is answered after the request’s 400 and 404, writes nothing, carries `WWW-Authenticate: Bearer realm="goiabada", error="insufficient_scope", error_description="...", scope="authserver:manage"`, and leaves an `administrator_change_refused` audit record. Every operation this document secures declares a 403. The public client logo endpoint, which carries `security: []` and authorises nothing, is the one that does not. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on scope and administrative-policy refusals; other forbidden responses omit it. ### 404 Resource not found application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` ### 500 The request failed inside the server, most often a storage error. error_code is INTERNAL_SERVER_ERROR: no detail about the failure, only the request id an operator can find the log line by, and retrying is reasonable. Key rotation’s 500 also answers KEY_SET_INCOMPLETE, which names its cause and no retry clears. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` # Delete resource Source: https://goiabada.dev/reference/api/admin/operations/deleteresource/ DELETE /api/v1/admin/resources/{id} Shell / cURL ```sh curl --request DELETE \ --url http://localhost:9090/api/v1/admin/resources/1 \ --header 'Authorization: Bearer ' ``` - Auth server base URL{baseUrl}/api/v1/admin/resources/{id} Delete a resource ## Authorizations - **[BearerAuth](https://goiabada.dev/reference/api/admin/#bearerauth)** ## Parameters ### Path Parameters **id** required integer format: int64 Resource ID ## Responses ### 200 Resource deleted application/json object **success** required boolean #### Example generated ```json { "success": true } ``` ### 400 Invalid request body or parameters application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge when token transport is malformed; ordinary validation errors omit it. ### 401 Missing or invalid bearer token application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on token refusals. An operation may also return a handler-level 401 without this header. ### 403 The token is valid but does not authorise this request. Four checks answer here, and a caller distinguishes them by error_code. INSUFFICIENT_SCOPE comes from the route’s authorisation middleware, before the handler runs: an Admin operation needs one of the scopes its route names, and an Account operation needs `authserver:manage-account`. USER_CONTEXT_REQUIRED comes from the same middleware on the Account operations, which also require the token to have been issued for a user: one obtained through the client credentials grant carries no auth_time claim and is refused here. FORBIDDEN comes from the handler, on the account endpoints that resolve a resource by id and then check it belongs to the caller, so a valid token for the wrong user reaches this rather than a 404. MANAGE_SCOPE_REQUIRED comes from the handler, on the Admin operations the administrative policy guards. Only `authserver:manage` creates an administrator, changes one, or changes what reaches one, so no granular scope will ever be enough: the remedy is a token holding `authserver:manage`, not requesting the route’s scope as for INSUFFICIENT_SCOPE. An administrator is a user, group or client holding any of the six administrative permissions (`manage`, `admin-read`, `manage-users`, `manage-clients`, `manage-settings`, `browser-sessions`), a user directly or through any of its groups, the admin console’s own client whatever it holds, and a client allowed to request the administrative scopes. A granular scope is refused here when it: - grants or revokes an administrative permission, directly or by moving a user into or out of a group that holds one, or deletes such a group; - writes to an administrator user (profile, credentials, enabled, sessions, consents, attributes, memberships, permissions, deletion), an administrative group (settings, attributes, members, permissions, deletion), or an administrator client (settings, authentication, flows, redirect URIs, web origins, token settings, permissions, logo, deletion); - switches whether a client may request the administrative scopes, on or off, on any client; - reads an administrator client’s secret; - changes the email or the audit-log settings; - changes an administrative permission’s description. Granular scopes still read administrators. The refusal is answered after the request’s 400 and 404, writes nothing, carries `WWW-Authenticate: Bearer realm="goiabada", error="insufficient_scope", error_description="...", scope="authserver:manage"`, and leaves an `administrator_change_refused` audit record. Every operation this document secures declares a 403. The public client logo endpoint, which carries `security: []` and authorises nothing, is the one that does not. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on scope and administrative-policy refusals; other forbidden responses omit it. ### 404 Resource not found application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` ### 500 The request failed inside the server, most often a storage error. error_code is INTERNAL_SERVER_ERROR: no detail about the failure, only the request id an operator can find the log line by, and retrying is reasonable. Key rotation’s 500 also answers KEY_SET_INCOMPLETE, which names its cause and no retry clears. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` # Get resource permissions Source: https://goiabada.dev/reference/api/admin/operations/getresourcepermissions/ GET /api/v1/admin/resources/{id}/permissions Shell / cURL ```sh curl --request GET \ --url http://localhost:9090/api/v1/admin/resources/1/permissions \ --header 'Authorization: Bearer ' ``` - Auth server base URL{baseUrl}/api/v1/admin/resources/{id}/permissions Get permissions defined for a resource ## Authorizations - **[BearerAuth](https://goiabada.dev/reference/api/admin/#bearerauth)** ## Parameters ### Path Parameters **id** required integer format: int64 Resource ID ## Responses ### 200 Resource permissions. An id that matches no resource is answered here with an empty list rather than a 404: the handler queries the permissions by resource id and never looks the resource up. application/json object **permissions** required Array\ object **id** required integer format: int64 **permissionIdentifier** required string **description** required string **resourceId** required integer format: int64 **resource** required object **id** required integer format: int64 **resourceIdentifier** required string **description** required string **isSystemLevelResource** required True for a resource this server owns and enforces as system level: rename and delete are refused on it with 403. A consumer should disable those controls rather than recompute the rule. boolean #### Example generated ```json { "permissions": [ { "id": 1, "permissionIdentifier": "example", "description": "example", "resourceId": 1, "resource": { "id": 1, "resourceIdentifier": "example", "description": "example", "isSystemLevelResource": true } } ] } ``` ### 400 Invalid request body or parameters application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge when token transport is malformed; ordinary validation errors omit it. ### 401 Missing or invalid bearer token application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on token refusals. An operation may also return a handler-level 401 without this header. ### 403 The token is valid but does not authorise this request. Four checks answer here, and a caller distinguishes them by error_code. INSUFFICIENT_SCOPE comes from the route’s authorisation middleware, before the handler runs: an Admin operation needs one of the scopes its route names, and an Account operation needs `authserver:manage-account`. USER_CONTEXT_REQUIRED comes from the same middleware on the Account operations, which also require the token to have been issued for a user: one obtained through the client credentials grant carries no auth_time claim and is refused here. FORBIDDEN comes from the handler, on the account endpoints that resolve a resource by id and then check it belongs to the caller, so a valid token for the wrong user reaches this rather than a 404. MANAGE_SCOPE_REQUIRED comes from the handler, on the Admin operations the administrative policy guards. Only `authserver:manage` creates an administrator, changes one, or changes what reaches one, so no granular scope will ever be enough: the remedy is a token holding `authserver:manage`, not requesting the route’s scope as for INSUFFICIENT_SCOPE. An administrator is a user, group or client holding any of the six administrative permissions (`manage`, `admin-read`, `manage-users`, `manage-clients`, `manage-settings`, `browser-sessions`), a user directly or through any of its groups, the admin console’s own client whatever it holds, and a client allowed to request the administrative scopes. A granular scope is refused here when it: - grants or revokes an administrative permission, directly or by moving a user into or out of a group that holds one, or deletes such a group; - writes to an administrator user (profile, credentials, enabled, sessions, consents, attributes, memberships, permissions, deletion), an administrative group (settings, attributes, members, permissions, deletion), or an administrator client (settings, authentication, flows, redirect URIs, web origins, token settings, permissions, logo, deletion); - switches whether a client may request the administrative scopes, on or off, on any client; - reads an administrator client’s secret; - changes the email or the audit-log settings; - changes an administrative permission’s description. Granular scopes still read administrators. The refusal is answered after the request’s 400 and 404, writes nothing, carries `WWW-Authenticate: Bearer realm="goiabada", error="insufficient_scope", error_description="...", scope="authserver:manage"`, and leaves an `administrator_change_refused` audit record. Every operation this document secures declares a 403. The public client logo endpoint, which carries `security: []` and authorises nothing, is the one that does not. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on scope and administrative-policy refusals; other forbidden responses omit it. ### 500 The request failed inside the server, most often a storage error. error_code is INTERNAL_SERVER_ERROR: no detail about the failure, only the request id an operator can find the log line by, and retrying is reasonable. Key rotation’s 500 also answers KEY_SET_INCOMPLETE, which names its cause and no retry clears. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` # Update resource permissions Source: https://goiabada.dev/reference/api/admin/operations/updateresourcepermissions/ PUT /api/v1/admin/resources/{id}/permissions Shell / cURL ```sh curl --request PUT \ --url http://localhost:9090/api/v1/admin/resources/1/permissions \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "permissions": [ { "id": 1, "permissionIdentifier": "example", "description": "example" } ], "expectedPermissions": [ { "id": 1, "permissionIdentifier": "example", "description": "example" } ] }' ``` - Auth server base URL{baseUrl}/api/v1/admin/resources/{id}/permissions Replace the permissions of a resource. An entry with an `id` updates that permission, one without creates it, and a stored permission the list leaves out is deleted. The built-in permissions of the `authserver` resource cannot be renamed or deleted, by any caller. Changing the description of one of its administrative permissions needs `authserver:manage`, because the admin console shows that description to an operator choosing a permission to grant: `authserver:manage-settings` is refused with 403 MANAGE_SCOPE_REQUIRED. ## Authorizations - **[BearerAuth](https://goiabada.dev/reference/api/admin/#bearerauth)** ## Parameters ### Path Parameters **id** required integer format: int64 Resource ID ## Request Bodyrequired application/json object **permissions** Array\ object **id** Include to update existing, omit to create new integer format: int64 **permissionIdentifier** required string **description** string **expectedPermissions** required The resource’s permissions as last read, each with its id; \[] if there were none. Array\ object **id** Include to update existing, omit to create new integer format: int64 **permissionIdentifier** required string **description** string ### Example generated ```json { "permissions": [ { "id": 1, "permissionIdentifier": "example", "description": "example" } ], "expectedPermissions": [ { "id": 1, "permissionIdentifier": "example", "description": "example" } ] } ``` ## Responses ### 200 Permissions updated application/json object **success** required boolean #### Example generated ```json { "success": true } ``` ### 400 Invalid request body or parameters application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge when token transport is malformed; ordinary validation errors omit it. ### 401 Missing or invalid bearer token application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on token refusals. An operation may also return a handler-level 401 without this header. ### 403 The token is valid but does not authorise this request. Four checks answer here, and a caller distinguishes them by error_code. INSUFFICIENT_SCOPE comes from the route’s authorisation middleware, before the handler runs: an Admin operation needs one of the scopes its route names, and an Account operation needs `authserver:manage-account`. USER_CONTEXT_REQUIRED comes from the same middleware on the Account operations, which also require the token to have been issued for a user: one obtained through the client credentials grant carries no auth_time claim and is refused here. FORBIDDEN comes from the handler, on the account endpoints that resolve a resource by id and then check it belongs to the caller, so a valid token for the wrong user reaches this rather than a 404. MANAGE_SCOPE_REQUIRED comes from the handler, on the Admin operations the administrative policy guards. Only `authserver:manage` creates an administrator, changes one, or changes what reaches one, so no granular scope will ever be enough: the remedy is a token holding `authserver:manage`, not requesting the route’s scope as for INSUFFICIENT_SCOPE. An administrator is a user, group or client holding any of the six administrative permissions (`manage`, `admin-read`, `manage-users`, `manage-clients`, `manage-settings`, `browser-sessions`), a user directly or through any of its groups, the admin console’s own client whatever it holds, and a client allowed to request the administrative scopes. A granular scope is refused here when it: - grants or revokes an administrative permission, directly or by moving a user into or out of a group that holds one, or deletes such a group; - writes to an administrator user (profile, credentials, enabled, sessions, consents, attributes, memberships, permissions, deletion), an administrative group (settings, attributes, members, permissions, deletion), or an administrator client (settings, authentication, flows, redirect URIs, web origins, token settings, permissions, logo, deletion); - switches whether a client may request the administrative scopes, on or off, on any client; - reads an administrator client’s secret; - changes the email or the audit-log settings; - changes an administrative permission’s description. Granular scopes still read administrators. The refusal is answered after the request’s 400 and 404, writes nothing, carries `WWW-Authenticate: Bearer realm="goiabada", error="insufficient_scope", error_description="...", scope="authserver:manage"`, and leaves an `administrator_change_refused` audit record. Every operation this document secures declares a 403. The public client logo endpoint, which carries `security: []` and authorises nothing, is the one that does not. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on scope and administrative-policy refusals; other forbidden responses omit it. ### 404 Resource not found application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` ### 409 Another save changed the resource’s permissions after expectedPermissions was read, or added the same identifier at the same moment. error_code is CONCURRENT_UPDATE; nothing was saved. Read them again and retry. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` ### 500 The request failed inside the server, most often a storage error. error_code is INTERNAL_SERVER_ERROR: no detail about the failure, only the request id an operator can find the log line by, and retrying is reasonable. Key rotation’s 500 also answers KEY_SET_INCOMPLETE, which names its cause and no retry clears. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` # Overview Source: https://goiabada.dev/reference/api/admin/operations/tags/permissions/ ## Permissions Permission management (Admin API) ## Operations GET [/api/v1/admin/users/{id}/permissions](https://goiabada.dev/reference/api/admin/operations/getuserpermissions/) PUT [/api/v1/admin/users/{id}/permissions](https://goiabada.dev/reference/api/admin/operations/updateuserpermissions/) GET [/api/v1/admin/groups/{id}/permissions](https://goiabada.dev/reference/api/admin/operations/getgrouppermissions/) PUT [/api/v1/admin/groups/{id}/permissions](https://goiabada.dev/reference/api/admin/operations/updategrouppermissions/) GET [/api/v1/admin/permissions/{id}/users](https://goiabada.dev/reference/api/admin/operations/getusersbypermission/) # Get user permissions Source: https://goiabada.dev/reference/api/admin/operations/getuserpermissions/ GET /api/v1/admin/users/{id}/permissions Shell / cURL ```sh curl --request GET \ --url http://localhost:9090/api/v1/admin/users/1/permissions \ --header 'Authorization: Bearer ' ``` - Auth server base URL{baseUrl}/api/v1/admin/users/{id}/permissions Get permissions assigned to a user ## Authorizations - **[BearerAuth](https://goiabada.dev/reference/api/admin/#bearerauth)** ## Parameters ### Path Parameters **id** required integer format: int64 User ID ## Responses ### 200 User permissions application/json object **user** required object **id** required integer format: int64 **createdAt** required string format: date-time nullable **updatedAt** required string format: date-time nullable **enabled** required boolean **subject** required string format: uuid **username** required string **givenName** required string **middleName** required string **familyName** required string **nickname** required string **website** required string **gender** required “female”, “male” or “other”, or empty when unset string **email** required string format: email **emailVerified** required boolean **zoneInfoCountryName** required string **zoneInfo** required string **locale** required string **birthDate** required string format: date-time nullable **phoneNumberCountryUniqueId** required string **phoneNumberCountryCallingCode** required string **phoneNumber** required string **phoneNumberVerified** required boolean **addressLine1** required string **addressLine2** required string **addressLocality** required string **addressRegion** required string **addressPostalCode** required string **addressCountry** required ISO 3166-1 alpha-2 country code (e.g. US, BR), or empty string. string /^(\[A-Z]{2})?$/ **otpEnabled** required boolean **permissions** required Null when the user has no permissions. Array\ nullable object **id** required integer format: int64 **permissionIdentifier** required string **description** required string **resourceId** required integer format: int64 **resource** required object **id** required integer format: int64 **resourceIdentifier** required string **description** required string **isSystemLevelResource** required True for a resource this server owns and enforces as system level: rename and delete are refused on it with 403. A consumer should disable those controls rather than recompute the rule. boolean #### Example generated ```json { "user": { "id": 1, "createdAt": "2026-04-15T12:00:00Z", "updatedAt": "2026-04-15T12:00:00Z", "enabled": true, "subject": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0", "username": "example", "givenName": "example", "middleName": "example", "familyName": "example", "nickname": "example", "website": "example", "gender": "example", "email": "hello@example.com", "emailVerified": true, "zoneInfoCountryName": "example", "zoneInfo": "example", "locale": "example", "birthDate": "2026-04-15T12:00:00Z", "phoneNumberCountryUniqueId": "example", "phoneNumberCountryCallingCode": "example", "phoneNumber": "example", "phoneNumberVerified": true, "addressLine1": "example", "addressLine2": "example", "addressLocality": "example", "addressRegion": "example", "addressPostalCode": "example", "addressCountry": "example", "otpEnabled": true }, "permissions": [ { "id": 1, "permissionIdentifier": "example", "description": "example", "resourceId": 1, "resource": { "id": 1, "resourceIdentifier": "example", "description": "example", "isSystemLevelResource": true } } ] } ``` ### 400 Invalid request body or parameters application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge when token transport is malformed; ordinary validation errors omit it. ### 401 Missing or invalid bearer token application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on token refusals. An operation may also return a handler-level 401 without this header. ### 403 The token is valid but does not authorise this request. Four checks answer here, and a caller distinguishes them by error_code. INSUFFICIENT_SCOPE comes from the route’s authorisation middleware, before the handler runs: an Admin operation needs one of the scopes its route names, and an Account operation needs `authserver:manage-account`. USER_CONTEXT_REQUIRED comes from the same middleware on the Account operations, which also require the token to have been issued for a user: one obtained through the client credentials grant carries no auth_time claim and is refused here. FORBIDDEN comes from the handler, on the account endpoints that resolve a resource by id and then check it belongs to the caller, so a valid token for the wrong user reaches this rather than a 404. MANAGE_SCOPE_REQUIRED comes from the handler, on the Admin operations the administrative policy guards. Only `authserver:manage` creates an administrator, changes one, or changes what reaches one, so no granular scope will ever be enough: the remedy is a token holding `authserver:manage`, not requesting the route’s scope as for INSUFFICIENT_SCOPE. An administrator is a user, group or client holding any of the six administrative permissions (`manage`, `admin-read`, `manage-users`, `manage-clients`, `manage-settings`, `browser-sessions`), a user directly or through any of its groups, the admin console’s own client whatever it holds, and a client allowed to request the administrative scopes. A granular scope is refused here when it: - grants or revokes an administrative permission, directly or by moving a user into or out of a group that holds one, or deletes such a group; - writes to an administrator user (profile, credentials, enabled, sessions, consents, attributes, memberships, permissions, deletion), an administrative group (settings, attributes, members, permissions, deletion), or an administrator client (settings, authentication, flows, redirect URIs, web origins, token settings, permissions, logo, deletion); - switches whether a client may request the administrative scopes, on or off, on any client; - reads an administrator client’s secret; - changes the email or the audit-log settings; - changes an administrative permission’s description. Granular scopes still read administrators. The refusal is answered after the request’s 400 and 404, writes nothing, carries `WWW-Authenticate: Bearer realm="goiabada", error="insufficient_scope", error_description="...", scope="authserver:manage"`, and leaves an `administrator_change_refused` audit record. Every operation this document secures declares a 403. The public client logo endpoint, which carries `security: []` and authorises nothing, is the one that does not. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on scope and administrative-policy refusals; other forbidden responses omit it. ### 404 Resource not found application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` ### 500 The request failed inside the server, most often a storage error. error_code is INTERNAL_SERVER_ERROR: no detail about the failure, only the request id an operator can find the log line by, and retrying is reasonable. Key rotation’s 500 also answers KEY_SET_INCOMPLETE, which names its cause and no retry clears. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` # Set user permissions Source: https://goiabada.dev/reference/api/admin/operations/updateuserpermissions/ PUT /api/v1/admin/users/{id}/permissions Shell / cURL ```sh curl --request PUT \ --url http://localhost:9090/api/v1/admin/users/1/permissions \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "permissionIds": [ 1 ], "expectedPermissionIds": [ 1 ] }' ``` - Auth server base URL{baseUrl}/api/v1/admin/users/{id}/permissions Replace all permissions for a user ## Authorizations - **[BearerAuth](https://goiabada.dev/reference/api/admin/#bearerauth)** ## Parameters ### Path Parameters **id** required integer format: int64 User ID ## Request Bodyrequired application/json object **permissionIds** Array\ **expectedPermissionIds** required The user’s permission ids as last read; \[] if there were none. Array\ ### Example generated ```json { "permissionIds": [ 1 ], "expectedPermissionIds": [ 1 ] } ``` ## Responses ### 200 Permissions updated application/json object **success** required boolean #### Example generated ```json { "success": true } ``` ### 400 Invalid request body or parameters application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge when token transport is malformed; ordinary validation errors omit it. ### 401 Missing or invalid bearer token application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on token refusals. An operation may also return a handler-level 401 without this header. ### 403 The token is valid but does not authorise this request. Four checks answer here, and a caller distinguishes them by error_code. INSUFFICIENT_SCOPE comes from the route’s authorisation middleware, before the handler runs: an Admin operation needs one of the scopes its route names, and an Account operation needs `authserver:manage-account`. USER_CONTEXT_REQUIRED comes from the same middleware on the Account operations, which also require the token to have been issued for a user: one obtained through the client credentials grant carries no auth_time claim and is refused here. FORBIDDEN comes from the handler, on the account endpoints that resolve a resource by id and then check it belongs to the caller, so a valid token for the wrong user reaches this rather than a 404. MANAGE_SCOPE_REQUIRED comes from the handler, on the Admin operations the administrative policy guards. Only `authserver:manage` creates an administrator, changes one, or changes what reaches one, so no granular scope will ever be enough: the remedy is a token holding `authserver:manage`, not requesting the route’s scope as for INSUFFICIENT_SCOPE. An administrator is a user, group or client holding any of the six administrative permissions (`manage`, `admin-read`, `manage-users`, `manage-clients`, `manage-settings`, `browser-sessions`), a user directly or through any of its groups, the admin console’s own client whatever it holds, and a client allowed to request the administrative scopes. A granular scope is refused here when it: - grants or revokes an administrative permission, directly or by moving a user into or out of a group that holds one, or deletes such a group; - writes to an administrator user (profile, credentials, enabled, sessions, consents, attributes, memberships, permissions, deletion), an administrative group (settings, attributes, members, permissions, deletion), or an administrator client (settings, authentication, flows, redirect URIs, web origins, token settings, permissions, logo, deletion); - switches whether a client may request the administrative scopes, on or off, on any client; - reads an administrator client’s secret; - changes the email or the audit-log settings; - changes an administrative permission’s description. Granular scopes still read administrators. The refusal is answered after the request’s 400 and 404, writes nothing, carries `WWW-Authenticate: Bearer realm="goiabada", error="insufficient_scope", error_description="...", scope="authserver:manage"`, and leaves an `administrator_change_refused` audit record. Every operation this document secures declares a 403. The public client logo endpoint, which carries `security: []` and authorises nothing, is the one that does not. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on scope and administrative-policy refusals; other forbidden responses omit it. ### 404 Resource not found application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` ### 409 Another save changed the user’s permissions after expectedPermissionIds was read. error_code is CONCURRENT_UPDATE; nothing was saved. Read them again and retry. Or the save would leave no enabled user holding `authserver:manage`, directly or through a group. error_code is LAST_ADMINISTRATOR; nothing was saved. Grant `authserver:manage` to another user first. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` ### 500 The request failed inside the server, most often a storage error. error_code is INTERNAL_SERVER_ERROR: no detail about the failure, only the request id an operator can find the log line by, and retrying is reasonable. Key rotation’s 500 also answers KEY_SET_INCOMPLETE, which names its cause and no retry clears. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` # Get group permissions Source: https://goiabada.dev/reference/api/admin/operations/getgrouppermissions/ GET /api/v1/admin/groups/{id}/permissions Shell / cURL ```sh curl --request GET \ --url http://localhost:9090/api/v1/admin/groups/1/permissions \ --header 'Authorization: Bearer ' ``` - Auth server base URL{baseUrl}/api/v1/admin/groups/{id}/permissions Get permissions assigned to a group ## Authorizations - **[BearerAuth](https://goiabada.dev/reference/api/admin/#bearerauth)** ## Parameters ### Path Parameters **id** required integer format: int64 Group ID ## Responses ### 200 Group permissions application/json object **group** required object **id** required integer format: int64 **createdAt** required string format: date-time nullable **updatedAt** required string format: date-time nullable **groupIdentifier** required string **description** required string **includeInIdToken** required boolean **includeInAccessToken** required boolean **memberCount** required integer **permissions** required Array\ object **id** required integer format: int64 **permissionIdentifier** required string **description** required string **resourceId** required integer format: int64 **resource** required object **id** required integer format: int64 **resourceIdentifier** required string **description** required string **isSystemLevelResource** required True for a resource this server owns and enforces as system level: rename and delete are refused on it with 403. A consumer should disable those controls rather than recompute the rule. boolean #### Example generated ```json { "group": { "id": 1, "createdAt": "2026-04-15T12:00:00Z", "updatedAt": "2026-04-15T12:00:00Z", "groupIdentifier": "example", "description": "example", "includeInIdToken": true, "includeInAccessToken": true, "memberCount": 1 }, "permissions": [ { "id": 1, "permissionIdentifier": "example", "description": "example", "resourceId": 1, "resource": { "id": 1, "resourceIdentifier": "example", "description": "example", "isSystemLevelResource": true } } ] } ``` ### 400 Invalid request body or parameters application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge when token transport is malformed; ordinary validation errors omit it. ### 401 Missing or invalid bearer token application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on token refusals. An operation may also return a handler-level 401 without this header. ### 403 The token is valid but does not authorise this request. Four checks answer here, and a caller distinguishes them by error_code. INSUFFICIENT_SCOPE comes from the route’s authorisation middleware, before the handler runs: an Admin operation needs one of the scopes its route names, and an Account operation needs `authserver:manage-account`. USER_CONTEXT_REQUIRED comes from the same middleware on the Account operations, which also require the token to have been issued for a user: one obtained through the client credentials grant carries no auth_time claim and is refused here. FORBIDDEN comes from the handler, on the account endpoints that resolve a resource by id and then check it belongs to the caller, so a valid token for the wrong user reaches this rather than a 404. MANAGE_SCOPE_REQUIRED comes from the handler, on the Admin operations the administrative policy guards. Only `authserver:manage` creates an administrator, changes one, or changes what reaches one, so no granular scope will ever be enough: the remedy is a token holding `authserver:manage`, not requesting the route’s scope as for INSUFFICIENT_SCOPE. An administrator is a user, group or client holding any of the six administrative permissions (`manage`, `admin-read`, `manage-users`, `manage-clients`, `manage-settings`, `browser-sessions`), a user directly or through any of its groups, the admin console’s own client whatever it holds, and a client allowed to request the administrative scopes. A granular scope is refused here when it: - grants or revokes an administrative permission, directly or by moving a user into or out of a group that holds one, or deletes such a group; - writes to an administrator user (profile, credentials, enabled, sessions, consents, attributes, memberships, permissions, deletion), an administrative group (settings, attributes, members, permissions, deletion), or an administrator client (settings, authentication, flows, redirect URIs, web origins, token settings, permissions, logo, deletion); - switches whether a client may request the administrative scopes, on or off, on any client; - reads an administrator client’s secret; - changes the email or the audit-log settings; - changes an administrative permission’s description. Granular scopes still read administrators. The refusal is answered after the request’s 400 and 404, writes nothing, carries `WWW-Authenticate: Bearer realm="goiabada", error="insufficient_scope", error_description="...", scope="authserver:manage"`, and leaves an `administrator_change_refused` audit record. Every operation this document secures declares a 403. The public client logo endpoint, which carries `security: []` and authorises nothing, is the one that does not. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on scope and administrative-policy refusals; other forbidden responses omit it. ### 404 Resource not found application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` ### 500 The request failed inside the server, most often a storage error. error_code is INTERNAL_SERVER_ERROR: no detail about the failure, only the request id an operator can find the log line by, and retrying is reasonable. Key rotation’s 500 also answers KEY_SET_INCOMPLETE, which names its cause and no retry clears. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` # Set group permissions Source: https://goiabada.dev/reference/api/admin/operations/updategrouppermissions/ PUT /api/v1/admin/groups/{id}/permissions Shell / cURL ```sh curl --request PUT \ --url http://localhost:9090/api/v1/admin/groups/1/permissions \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "permissionIds": [ 1 ], "expectedPermissionIds": [ 1 ] }' ``` - Auth server base URL{baseUrl}/api/v1/admin/groups/{id}/permissions Replace all permissions for a group ## Authorizations - **[BearerAuth](https://goiabada.dev/reference/api/admin/#bearerauth)** ## Parameters ### Path Parameters **id** required integer format: int64 Group ID ## Request Bodyrequired application/json object **permissionIds** Array\ **expectedPermissionIds** required The group’s permission ids as last read; \[] if there were none. Array\ ### Example generated ```json { "permissionIds": [ 1 ], "expectedPermissionIds": [ 1 ] } ``` ## Responses ### 200 Permissions updated application/json object **success** required boolean #### Example generated ```json { "success": true } ``` ### 400 Invalid request body or parameters application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge when token transport is malformed; ordinary validation errors omit it. ### 401 Missing or invalid bearer token application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on token refusals. An operation may also return a handler-level 401 without this header. ### 403 The token is valid but does not authorise this request. Four checks answer here, and a caller distinguishes them by error_code. INSUFFICIENT_SCOPE comes from the route’s authorisation middleware, before the handler runs: an Admin operation needs one of the scopes its route names, and an Account operation needs `authserver:manage-account`. USER_CONTEXT_REQUIRED comes from the same middleware on the Account operations, which also require the token to have been issued for a user: one obtained through the client credentials grant carries no auth_time claim and is refused here. FORBIDDEN comes from the handler, on the account endpoints that resolve a resource by id and then check it belongs to the caller, so a valid token for the wrong user reaches this rather than a 404. MANAGE_SCOPE_REQUIRED comes from the handler, on the Admin operations the administrative policy guards. Only `authserver:manage` creates an administrator, changes one, or changes what reaches one, so no granular scope will ever be enough: the remedy is a token holding `authserver:manage`, not requesting the route’s scope as for INSUFFICIENT_SCOPE. An administrator is a user, group or client holding any of the six administrative permissions (`manage`, `admin-read`, `manage-users`, `manage-clients`, `manage-settings`, `browser-sessions`), a user directly or through any of its groups, the admin console’s own client whatever it holds, and a client allowed to request the administrative scopes. A granular scope is refused here when it: - grants or revokes an administrative permission, directly or by moving a user into or out of a group that holds one, or deletes such a group; - writes to an administrator user (profile, credentials, enabled, sessions, consents, attributes, memberships, permissions, deletion), an administrative group (settings, attributes, members, permissions, deletion), or an administrator client (settings, authentication, flows, redirect URIs, web origins, token settings, permissions, logo, deletion); - switches whether a client may request the administrative scopes, on or off, on any client; - reads an administrator client’s secret; - changes the email or the audit-log settings; - changes an administrative permission’s description. Granular scopes still read administrators. The refusal is answered after the request’s 400 and 404, writes nothing, carries `WWW-Authenticate: Bearer realm="goiabada", error="insufficient_scope", error_description="...", scope="authserver:manage"`, and leaves an `administrator_change_refused` audit record. Every operation this document secures declares a 403. The public client logo endpoint, which carries `security: []` and authorises nothing, is the one that does not. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on scope and administrative-policy refusals; other forbidden responses omit it. ### 404 Resource not found application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` ### 409 Another save changed the group’s permissions after expectedPermissionIds was read. error_code is CONCURRENT_UPDATE; nothing was saved. Read them again and retry. Or the save would leave no enabled user holding `authserver:manage`, directly or through a group. error_code is LAST_ADMINISTRATOR; nothing was saved. Grant `authserver:manage` to another user first. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` ### 500 The request failed inside the server, most often a storage error. error_code is INTERNAL_SERVER_ERROR: no detail about the failure, only the request id an operator can find the log line by, and retrying is reasonable. Key rotation’s 500 also answers KEY_SET_INCOMPLETE, which names its cause and no retry clears. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` # Get users with permission Source: https://goiabada.dev/reference/api/admin/operations/getusersbypermission/ GET /api/v1/admin/permissions/{id}/users Shell / cURL ```sh curl --request GET \ --url 'http://localhost:9090/api/v1/admin/permissions/1/users?page=1&size=10' \ --header 'Authorization: Bearer ' ``` - Auth server base URL{baseUrl}/api/v1/admin/permissions/{id}/users Get users that have a specific permission ## Authorizations - **[BearerAuth](https://goiabada.dev/reference/api/admin/#bearerauth)** ## Parameters ### Path Parameters **id** required integer format: int64 Permission ID ### Query Parameters **page** integer default: 1 Page number (default 1) **size** integer default: 10 Results per page (default 10) ## Responses ### 200 Users with permission application/json object **users** required Null when no users hold the permission on this page. Array\ nullable object **id** required integer format: int64 **createdAt** required string format: date-time nullable **updatedAt** required string format: date-time nullable **enabled** required boolean **subject** required string format: uuid **username** required string **givenName** required string **middleName** required string **familyName** required string **nickname** required string **website** required string **gender** required “female”, “male” or “other”, or empty when unset string **email** required string format: email **emailVerified** required boolean **zoneInfoCountryName** required string **zoneInfo** required string **locale** required string **birthDate** required string format: date-time nullable **phoneNumberCountryUniqueId** required string **phoneNumberCountryCallingCode** required string **phoneNumber** required string **phoneNumberVerified** required boolean **addressLine1** required string **addressLine2** required string **addressLocality** required string **addressRegion** required string **addressPostalCode** required string **addressCountry** required ISO 3166-1 alpha-2 country code (e.g. US, BR), or empty string. string /^(\[A-Z]{2})?$/ **otpEnabled** required boolean **total** required integer **page** required integer **size** required integer #### Example generated ```json { "users": [ { "id": 1, "createdAt": "2026-04-15T12:00:00Z", "updatedAt": "2026-04-15T12:00:00Z", "enabled": true, "subject": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0", "username": "example", "givenName": "example", "middleName": "example", "familyName": "example", "nickname": "example", "website": "example", "gender": "example", "email": "hello@example.com", "emailVerified": true, "zoneInfoCountryName": "example", "zoneInfo": "example", "locale": "example", "birthDate": "2026-04-15T12:00:00Z", "phoneNumberCountryUniqueId": "example", "phoneNumberCountryCallingCode": "example", "phoneNumber": "example", "phoneNumberVerified": true, "addressLine1": "example", "addressLine2": "example", "addressLocality": "example", "addressRegion": "example", "addressPostalCode": "example", "addressCountry": "example", "otpEnabled": true } ], "total": 1, "page": 1, "size": 1 } ``` ### 400 Invalid request body or parameters application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge when token transport is malformed; ordinary validation errors omit it. ### 401 Missing or invalid bearer token application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on token refusals. An operation may also return a handler-level 401 without this header. ### 403 The token is valid but does not authorise this request. Four checks answer here, and a caller distinguishes them by error_code. INSUFFICIENT_SCOPE comes from the route’s authorisation middleware, before the handler runs: an Admin operation needs one of the scopes its route names, and an Account operation needs `authserver:manage-account`. USER_CONTEXT_REQUIRED comes from the same middleware on the Account operations, which also require the token to have been issued for a user: one obtained through the client credentials grant carries no auth_time claim and is refused here. FORBIDDEN comes from the handler, on the account endpoints that resolve a resource by id and then check it belongs to the caller, so a valid token for the wrong user reaches this rather than a 404. MANAGE_SCOPE_REQUIRED comes from the handler, on the Admin operations the administrative policy guards. Only `authserver:manage` creates an administrator, changes one, or changes what reaches one, so no granular scope will ever be enough: the remedy is a token holding `authserver:manage`, not requesting the route’s scope as for INSUFFICIENT_SCOPE. An administrator is a user, group or client holding any of the six administrative permissions (`manage`, `admin-read`, `manage-users`, `manage-clients`, `manage-settings`, `browser-sessions`), a user directly or through any of its groups, the admin console’s own client whatever it holds, and a client allowed to request the administrative scopes. A granular scope is refused here when it: - grants or revokes an administrative permission, directly or by moving a user into or out of a group that holds one, or deletes such a group; - writes to an administrator user (profile, credentials, enabled, sessions, consents, attributes, memberships, permissions, deletion), an administrative group (settings, attributes, members, permissions, deletion), or an administrator client (settings, authentication, flows, redirect URIs, web origins, token settings, permissions, logo, deletion); - switches whether a client may request the administrative scopes, on or off, on any client; - reads an administrator client’s secret; - changes the email or the audit-log settings; - changes an administrative permission’s description. Granular scopes still read administrators. The refusal is answered after the request’s 400 and 404, writes nothing, carries `WWW-Authenticate: Bearer realm="goiabada", error="insufficient_scope", error_description="...", scope="authserver:manage"`, and leaves an `administrator_change_refused` audit record. Every operation this document secures declares a 403. The public client logo endpoint, which carries `security: []` and authorises nothing, is the one that does not. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on scope and administrative-policy refusals; other forbidden responses omit it. ### 404 Resource not found application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` ### 500 The request failed inside the server, most often a storage error. error_code is INTERNAL_SERVER_ERROR: no detail about the failure, only the request id an operator can find the log line by, and retrying is reasonable. Key rotation’s 500 also answers KEY_SET_INCOMPLETE, which names its cause and no retry clears. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` # Overview Source: https://goiabada.dev/reference/api/admin/operations/tags/clients/ ## Clients OAuth client management (Admin API) ## Operations GET [/api/v1/admin/clients](https://goiabada.dev/reference/api/admin/operations/getclients/) POST [/api/v1/admin/clients](https://goiabada.dev/reference/api/admin/operations/createclient/) GET [/api/v1/admin/clients/{id}](https://goiabada.dev/reference/api/admin/operations/getclient/) PUT [/api/v1/admin/clients/{id}](https://goiabada.dev/reference/api/admin/operations/updateclientsettings/) DELETE [/api/v1/admin/clients/{id}](https://goiabada.dev/reference/api/admin/operations/deleteclient/) GET [/api/v1/admin/clients/{id}/secret](https://goiabada.dev/reference/api/admin/operations/getclientsecret/) PUT [/api/v1/admin/clients/{id}/authentication](https://goiabada.dev/reference/api/admin/operations/updateclientauthentication/) PUT [/api/v1/admin/clients/{id}/oauth2-flows](https://goiabada.dev/reference/api/admin/operations/updateclientoauth2flows/) PUT [/api/v1/admin/clients/{id}/redirect-uris](https://goiabada.dev/reference/api/admin/operations/updateclientredirecturis/) PUT [/api/v1/admin/clients/{id}/web-origins](https://goiabada.dev/reference/api/admin/operations/updateclientweborigins/) PUT [/api/v1/admin/clients/{id}/tokens](https://goiabada.dev/reference/api/admin/operations/updateclienttokens/) GET [/api/v1/admin/clients/{id}/permissions](https://goiabada.dev/reference/api/admin/operations/getclientpermissions/) PUT [/api/v1/admin/clients/{id}/permissions](https://goiabada.dev/reference/api/admin/operations/updateclientpermissions/) PUT [/api/v1/admin/clients/{id}/administrative-scopes](https://goiabada.dev/reference/api/admin/operations/updateclientadministrativescopes/) GET [/api/v1/admin/clients/{id}/sessions](https://goiabada.dev/reference/api/admin/operations/getclientsessions/) GET [/api/v1/admin/clients/{id}/logo](https://goiabada.dev/reference/api/admin/operations/getclientlogoinfo/) POST [/api/v1/admin/clients/{id}/logo](https://goiabada.dev/reference/api/admin/operations/uploadclientlogo/) DELETE [/api/v1/admin/clients/{id}/logo](https://goiabada.dev/reference/api/admin/operations/deleteclientlogo/) GET [/client/logo/{clientIdentifier}](https://goiabada.dev/reference/api/admin/operations/getclientlogoimage/) # List clients Source: https://goiabada.dev/reference/api/admin/operations/getclients/ GET /api/v1/admin/clients Shell / cURL ```sh curl --request GET \ --url http://localhost:9090/api/v1/admin/clients \ --header 'Authorization: Bearer ' ``` - Auth server base URL{baseUrl}/api/v1/admin/clients Get all OAuth2/OIDC clients ## Authorizations - **[BearerAuth](https://goiabada.dev/reference/api/admin/#bearerauth)** ## Responses ### 200 Clients list application/json object **clients** required Array\ object **id** required integer format: int64 **createdAt** required string format: date-time nullable **updatedAt** required string format: date-time nullable **clientIdentifier** required string **description** required string **websiteUrl** required The client’s website URL string **displayName** required A user-friendly name for the client string **enabled** required boolean **consentRequired** required boolean **createdViaDcr** required Read-only. True when the client registered itself through /connect/register boolean **administrativeScopesAllowed** required Read-only. True when the client may request the administrative authserver scopes on a user’s behalf; always true for the admin console’s own client. Switched through PUT /api/v1/admin/clients/{id}/administrative-scopes, with authserver:manage alone boolean **showLogo** required When true, the client logo is displayed on sign-in and consent screens boolean **showDisplayName** required When true, the display name is shown instead of the client identifier boolean **showDescription** required When true, the client description is displayed on sign-in and consent screens boolean **showWebsiteUrl** required When true, the client website URL is displayed on sign-in and consent screens boolean **isPublic** required boolean **isSystemLevelClient** required boolean **authorizationCodeEnabled** required boolean **clientCredentialsEnabled** required boolean **pkceRequired** required Null inherits the global setting, true requires PKCE, false makes it optional. Ignored for public clients, which always require PKCE boolean nullable **implicitGrantEnabled** required Null inherits the global setting, true enables the implicit flow, false disables it. Deprecated in OAuth 2.1 boolean nullable **resourceOwnerPasswordCredentialsEnabled** required Null inherits the global setting, true enables grant_type=password (RFC 6749 section 4.3), false disables it. Deprecated in OAuth 2.1 boolean nullable **tokenExpirationInSeconds** required integer **refreshTokenOfflineIdleTimeoutInSeconds** required integer **refreshTokenOfflineMaxLifetimeInSeconds** required integer **includeOpenIDConnectClaimsInAccessToken** required string **includeOpenIDConnectClaimsInIdToken** required string **defaultAcrLevel** required string **redirectURIs** required Null when this collection was not loaded for the response. Array\ nullable The `redirectURIs` entries of ClientResponse. The schema name is unchanged, but the shape below is not what releases up to and including v1.6.0 put on the wire: this position carried the persistence row directly, so its keys arrived capitalised and its `createdAt` arrived as a two-field `{"Time":...,"Valid":...}` object. The keys below are the ones every published contract has always declared. object **id** required integer format: int64 **createdAt** required string format: date-time nullable **uri** required string **clientId** required integer format: int64 **webOrigins** required Null when this collection was not loaded or the client has no web origins. Array\ nullable The `webOrigins` entries of ClientResponse. Its keys moved in the same release and for the same reason as RedirectURI above. object **id** required integer format: int64 **createdAt** required string format: date-time nullable **origin** required string **clientId** required integer format: int64 #### Example generated ```json { "clients": [ { "id": 1, "createdAt": "2026-04-15T12:00:00Z", "updatedAt": "2026-04-15T12:00:00Z", "clientIdentifier": "example", "description": "example", "websiteUrl": "example", "displayName": "example", "enabled": true, "consentRequired": true, "createdViaDcr": true, "administrativeScopesAllowed": true, "showLogo": true, "showDisplayName": true, "showDescription": true, "showWebsiteUrl": true, "isPublic": true, "isSystemLevelClient": true, "authorizationCodeEnabled": true, "clientCredentialsEnabled": true, "pkceRequired": true, "implicitGrantEnabled": true, "resourceOwnerPasswordCredentialsEnabled": true, "tokenExpirationInSeconds": 1, "refreshTokenOfflineIdleTimeoutInSeconds": 1, "refreshTokenOfflineMaxLifetimeInSeconds": 1, "includeOpenIDConnectClaimsInAccessToken": "example", "includeOpenIDConnectClaimsInIdToken": "example", "defaultAcrLevel": "example", "redirectURIs": [ { "id": 1, "createdAt": "2026-04-15T12:00:00Z", "uri": "example", "clientId": 1 } ], "webOrigins": [ { "id": 1, "createdAt": "2026-04-15T12:00:00Z", "origin": "example", "clientId": 1 } ] } ] } ``` ### 401 Missing or invalid bearer token application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on token refusals. An operation may also return a handler-level 401 without this header. ### 403 The token is valid but does not authorise this request. Four checks answer here, and a caller distinguishes them by error_code. INSUFFICIENT_SCOPE comes from the route’s authorisation middleware, before the handler runs: an Admin operation needs one of the scopes its route names, and an Account operation needs `authserver:manage-account`. USER_CONTEXT_REQUIRED comes from the same middleware on the Account operations, which also require the token to have been issued for a user: one obtained through the client credentials grant carries no auth_time claim and is refused here. FORBIDDEN comes from the handler, on the account endpoints that resolve a resource by id and then check it belongs to the caller, so a valid token for the wrong user reaches this rather than a 404. MANAGE_SCOPE_REQUIRED comes from the handler, on the Admin operations the administrative policy guards. Only `authserver:manage` creates an administrator, changes one, or changes what reaches one, so no granular scope will ever be enough: the remedy is a token holding `authserver:manage`, not requesting the route’s scope as for INSUFFICIENT_SCOPE. An administrator is a user, group or client holding any of the six administrative permissions (`manage`, `admin-read`, `manage-users`, `manage-clients`, `manage-settings`, `browser-sessions`), a user directly or through any of its groups, the admin console’s own client whatever it holds, and a client allowed to request the administrative scopes. A granular scope is refused here when it: - grants or revokes an administrative permission, directly or by moving a user into or out of a group that holds one, or deletes such a group; - writes to an administrator user (profile, credentials, enabled, sessions, consents, attributes, memberships, permissions, deletion), an administrative group (settings, attributes, members, permissions, deletion), or an administrator client (settings, authentication, flows, redirect URIs, web origins, token settings, permissions, logo, deletion); - switches whether a client may request the administrative scopes, on or off, on any client; - reads an administrator client’s secret; - changes the email or the audit-log settings; - changes an administrative permission’s description. Granular scopes still read administrators. The refusal is answered after the request’s 400 and 404, writes nothing, carries `WWW-Authenticate: Bearer realm="goiabada", error="insufficient_scope", error_description="...", scope="authserver:manage"`, and leaves an `administrator_change_refused` audit record. Every operation this document secures declares a 403. The public client logo endpoint, which carries `security: []` and authorises nothing, is the one that does not. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on scope and administrative-policy refusals; other forbidden responses omit it. ### 500 The request failed inside the server, most often a storage error. error_code is INTERNAL_SERVER_ERROR: no detail about the failure, only the request id an operator can find the log line by, and retrying is reasonable. Key rotation’s 500 also answers KEY_SET_INCOMPLETE, which names its cause and no retry clears. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` # Create client Source: https://goiabada.dev/reference/api/admin/operations/createclient/ POST /api/v1/admin/clients Shell / cURL ```sh curl --request POST \ --url http://localhost:9090/api/v1/admin/clients \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "clientIdentifier": "example", "description": "example", "displayName": "example", "authorizationCodeEnabled": true, "clientCredentialsEnabled": true }' ``` - Auth server base URL{baseUrl}/api/v1/admin/clients Create a new OAuth2/OIDC client ## Authorizations - **[BearerAuth](https://goiabada.dev/reference/api/admin/#bearerauth)** ## Request Bodyrequired application/json object **clientIdentifier** required string **description** string **displayName** A user-friendly name for the client (max 100 chars after trimming). When the trimmed value is non-empty, showDisplayName is automatically set to true; otherwise false. string **authorizationCodeEnabled** boolean **clientCredentialsEnabled** boolean ### Example generated ```json { "clientIdentifier": "example", "description": "example", "displayName": "example", "authorizationCodeEnabled": true, "clientCredentialsEnabled": true } ``` ## Responses ### 201 Client created application/json object **client** required object **id** required integer format: int64 **createdAt** required string format: date-time nullable **updatedAt** required string format: date-time nullable **clientIdentifier** required string **description** required string **websiteUrl** required The client’s website URL string **displayName** required A user-friendly name for the client string **enabled** required boolean **consentRequired** required boolean **createdViaDcr** required Read-only. True when the client registered itself through /connect/register boolean **administrativeScopesAllowed** required Read-only. True when the client may request the administrative authserver scopes on a user’s behalf; always true for the admin console’s own client. Switched through PUT /api/v1/admin/clients/{id}/administrative-scopes, with authserver:manage alone boolean **showLogo** required When true, the client logo is displayed on sign-in and consent screens boolean **showDisplayName** required When true, the display name is shown instead of the client identifier boolean **showDescription** required When true, the client description is displayed on sign-in and consent screens boolean **showWebsiteUrl** required When true, the client website URL is displayed on sign-in and consent screens boolean **isPublic** required boolean **isSystemLevelClient** required boolean **authorizationCodeEnabled** required boolean **clientCredentialsEnabled** required boolean **pkceRequired** required Null inherits the global setting, true requires PKCE, false makes it optional. Ignored for public clients, which always require PKCE boolean nullable **implicitGrantEnabled** required Null inherits the global setting, true enables the implicit flow, false disables it. Deprecated in OAuth 2.1 boolean nullable **resourceOwnerPasswordCredentialsEnabled** required Null inherits the global setting, true enables grant_type=password (RFC 6749 section 4.3), false disables it. Deprecated in OAuth 2.1 boolean nullable **tokenExpirationInSeconds** required integer **refreshTokenOfflineIdleTimeoutInSeconds** required integer **refreshTokenOfflineMaxLifetimeInSeconds** required integer **includeOpenIDConnectClaimsInAccessToken** required string **includeOpenIDConnectClaimsInIdToken** required string **defaultAcrLevel** required string **redirectURIs** required Null when this collection was not loaded for the response. Array\ nullable The `redirectURIs` entries of ClientResponse. The schema name is unchanged, but the shape below is not what releases up to and including v1.6.0 put on the wire: this position carried the persistence row directly, so its keys arrived capitalised and its `createdAt` arrived as a two-field `{"Time":...,"Valid":...}` object. The keys below are the ones every published contract has always declared. object **id** required integer format: int64 **createdAt** required string format: date-time nullable **uri** required string **clientId** required integer format: int64 **webOrigins** required Null when this collection was not loaded or the client has no web origins. Array\ nullable The `webOrigins` entries of ClientResponse. Its keys moved in the same release and for the same reason as RedirectURI above. object **id** required integer format: int64 **createdAt** required string format: date-time nullable **origin** required string **clientId** required integer format: int64 #### Example generated ```json { "client": { "id": 1, "createdAt": "2026-04-15T12:00:00Z", "updatedAt": "2026-04-15T12:00:00Z", "clientIdentifier": "example", "description": "example", "websiteUrl": "example", "displayName": "example", "enabled": true, "consentRequired": true, "createdViaDcr": true, "administrativeScopesAllowed": true, "showLogo": true, "showDisplayName": true, "showDescription": true, "showWebsiteUrl": true, "isPublic": true, "isSystemLevelClient": true, "authorizationCodeEnabled": true, "clientCredentialsEnabled": true, "pkceRequired": true, "implicitGrantEnabled": true, "resourceOwnerPasswordCredentialsEnabled": true, "tokenExpirationInSeconds": 1, "refreshTokenOfflineIdleTimeoutInSeconds": 1, "refreshTokenOfflineMaxLifetimeInSeconds": 1, "includeOpenIDConnectClaimsInAccessToken": "example", "includeOpenIDConnectClaimsInIdToken": "example", "defaultAcrLevel": "example", "redirectURIs": [ { "id": 1, "createdAt": "2026-04-15T12:00:00Z", "uri": "example", "clientId": 1 } ], "webOrigins": [ { "id": 1, "createdAt": "2026-04-15T12:00:00Z", "origin": "example", "clientId": 1 } ] } } ``` ### 400 Invalid request body or parameters application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge when token transport is malformed; ordinary validation errors omit it. ### 401 Missing or invalid bearer token application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on token refusals. An operation may also return a handler-level 401 without this header. ### 403 The token is valid but does not authorise this request. Four checks answer here, and a caller distinguishes them by error_code. INSUFFICIENT_SCOPE comes from the route’s authorisation middleware, before the handler runs: an Admin operation needs one of the scopes its route names, and an Account operation needs `authserver:manage-account`. USER_CONTEXT_REQUIRED comes from the same middleware on the Account operations, which also require the token to have been issued for a user: one obtained through the client credentials grant carries no auth_time claim and is refused here. FORBIDDEN comes from the handler, on the account endpoints that resolve a resource by id and then check it belongs to the caller, so a valid token for the wrong user reaches this rather than a 404. MANAGE_SCOPE_REQUIRED comes from the handler, on the Admin operations the administrative policy guards. Only `authserver:manage` creates an administrator, changes one, or changes what reaches one, so no granular scope will ever be enough: the remedy is a token holding `authserver:manage`, not requesting the route’s scope as for INSUFFICIENT_SCOPE. An administrator is a user, group or client holding any of the six administrative permissions (`manage`, `admin-read`, `manage-users`, `manage-clients`, `manage-settings`, `browser-sessions`), a user directly or through any of its groups, the admin console’s own client whatever it holds, and a client allowed to request the administrative scopes. A granular scope is refused here when it: - grants or revokes an administrative permission, directly or by moving a user into or out of a group that holds one, or deletes such a group; - writes to an administrator user (profile, credentials, enabled, sessions, consents, attributes, memberships, permissions, deletion), an administrative group (settings, attributes, members, permissions, deletion), or an administrator client (settings, authentication, flows, redirect URIs, web origins, token settings, permissions, logo, deletion); - switches whether a client may request the administrative scopes, on or off, on any client; - reads an administrator client’s secret; - changes the email or the audit-log settings; - changes an administrative permission’s description. Granular scopes still read administrators. The refusal is answered after the request’s 400 and 404, writes nothing, carries `WWW-Authenticate: Bearer realm="goiabada", error="insufficient_scope", error_description="...", scope="authserver:manage"`, and leaves an `administrator_change_refused` audit record. Every operation this document secures declares a 403. The public client logo endpoint, which carries `security: []` and authorises nothing, is the one that does not. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on scope and administrative-policy refusals; other forbidden responses omit it. ### 500 The request failed inside the server, most often a storage error. error_code is INTERNAL_SERVER_ERROR: no detail about the failure, only the request id an operator can find the log line by, and retrying is reasonable. Key rotation’s 500 also answers KEY_SET_INCOMPLETE, which names its cause and no retry clears. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` # Get client Source: https://goiabada.dev/reference/api/admin/operations/getclient/ GET /api/v1/admin/clients/{id} Shell / cURL ```sh curl --request GET \ --url http://localhost:9090/api/v1/admin/clients/1 \ --header 'Authorization: Bearer ' ``` - Auth server base URL{baseUrl}/api/v1/admin/clients/{id} Get client details by ID. The response carries no client secret: read it from `getClientSecret`. `administrativeScopesAllowed` says whether the client may request the administrative scopes on a user’s behalf. It is read-only here and everywhere else but `updateClientAdministrativeScopes`. ## Authorizations - **[BearerAuth](https://goiabada.dev/reference/api/admin/#bearerauth)** ## Parameters ### Path Parameters **id** required integer format: int64 Client ID ## Responses ### 200 Client details application/json object **client** required object **id** required integer format: int64 **createdAt** required string format: date-time nullable **updatedAt** required string format: date-time nullable **clientIdentifier** required string **description** required string **websiteUrl** required The client’s website URL string **displayName** required A user-friendly name for the client string **enabled** required boolean **consentRequired** required boolean **createdViaDcr** required Read-only. True when the client registered itself through /connect/register boolean **administrativeScopesAllowed** required Read-only. True when the client may request the administrative authserver scopes on a user’s behalf; always true for the admin console’s own client. Switched through PUT /api/v1/admin/clients/{id}/administrative-scopes, with authserver:manage alone boolean **showLogo** required When true, the client logo is displayed on sign-in and consent screens boolean **showDisplayName** required When true, the display name is shown instead of the client identifier boolean **showDescription** required When true, the client description is displayed on sign-in and consent screens boolean **showWebsiteUrl** required When true, the client website URL is displayed on sign-in and consent screens boolean **isPublic** required boolean **isSystemLevelClient** required boolean **authorizationCodeEnabled** required boolean **clientCredentialsEnabled** required boolean **pkceRequired** required Null inherits the global setting, true requires PKCE, false makes it optional. Ignored for public clients, which always require PKCE boolean nullable **implicitGrantEnabled** required Null inherits the global setting, true enables the implicit flow, false disables it. Deprecated in OAuth 2.1 boolean nullable **resourceOwnerPasswordCredentialsEnabled** required Null inherits the global setting, true enables grant_type=password (RFC 6749 section 4.3), false disables it. Deprecated in OAuth 2.1 boolean nullable **tokenExpirationInSeconds** required integer **refreshTokenOfflineIdleTimeoutInSeconds** required integer **refreshTokenOfflineMaxLifetimeInSeconds** required integer **includeOpenIDConnectClaimsInAccessToken** required string **includeOpenIDConnectClaimsInIdToken** required string **defaultAcrLevel** required string **redirectURIs** required Null when this collection was not loaded for the response. Array\ nullable The `redirectURIs` entries of ClientResponse. The schema name is unchanged, but the shape below is not what releases up to and including v1.6.0 put on the wire: this position carried the persistence row directly, so its keys arrived capitalised and its `createdAt` arrived as a two-field `{"Time":...,"Valid":...}` object. The keys below are the ones every published contract has always declared. object **id** required integer format: int64 **createdAt** required string format: date-time nullable **uri** required string **clientId** required integer format: int64 **webOrigins** required Null when this collection was not loaded or the client has no web origins. Array\ nullable The `webOrigins` entries of ClientResponse. Its keys moved in the same release and for the same reason as RedirectURI above. object **id** required integer format: int64 **createdAt** required string format: date-time nullable **origin** required string **clientId** required integer format: int64 #### Example generated ```json { "client": { "id": 1, "createdAt": "2026-04-15T12:00:00Z", "updatedAt": "2026-04-15T12:00:00Z", "clientIdentifier": "example", "description": "example", "websiteUrl": "example", "displayName": "example", "enabled": true, "consentRequired": true, "createdViaDcr": true, "administrativeScopesAllowed": true, "showLogo": true, "showDisplayName": true, "showDescription": true, "showWebsiteUrl": true, "isPublic": true, "isSystemLevelClient": true, "authorizationCodeEnabled": true, "clientCredentialsEnabled": true, "pkceRequired": true, "implicitGrantEnabled": true, "resourceOwnerPasswordCredentialsEnabled": true, "tokenExpirationInSeconds": 1, "refreshTokenOfflineIdleTimeoutInSeconds": 1, "refreshTokenOfflineMaxLifetimeInSeconds": 1, "includeOpenIDConnectClaimsInAccessToken": "example", "includeOpenIDConnectClaimsInIdToken": "example", "defaultAcrLevel": "example", "redirectURIs": [ { "id": 1, "createdAt": "2026-04-15T12:00:00Z", "uri": "example", "clientId": 1 } ], "webOrigins": [ { "id": 1, "createdAt": "2026-04-15T12:00:00Z", "origin": "example", "clientId": 1 } ] } } ``` ### 400 Invalid request body or parameters application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge when token transport is malformed; ordinary validation errors omit it. ### 401 Missing or invalid bearer token application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on token refusals. An operation may also return a handler-level 401 without this header. ### 403 The token is valid but does not authorise this request. Four checks answer here, and a caller distinguishes them by error_code. INSUFFICIENT_SCOPE comes from the route’s authorisation middleware, before the handler runs: an Admin operation needs one of the scopes its route names, and an Account operation needs `authserver:manage-account`. USER_CONTEXT_REQUIRED comes from the same middleware on the Account operations, which also require the token to have been issued for a user: one obtained through the client credentials grant carries no auth_time claim and is refused here. FORBIDDEN comes from the handler, on the account endpoints that resolve a resource by id and then check it belongs to the caller, so a valid token for the wrong user reaches this rather than a 404. MANAGE_SCOPE_REQUIRED comes from the handler, on the Admin operations the administrative policy guards. Only `authserver:manage` creates an administrator, changes one, or changes what reaches one, so no granular scope will ever be enough: the remedy is a token holding `authserver:manage`, not requesting the route’s scope as for INSUFFICIENT_SCOPE. An administrator is a user, group or client holding any of the six administrative permissions (`manage`, `admin-read`, `manage-users`, `manage-clients`, `manage-settings`, `browser-sessions`), a user directly or through any of its groups, the admin console’s own client whatever it holds, and a client allowed to request the administrative scopes. A granular scope is refused here when it: - grants or revokes an administrative permission, directly or by moving a user into or out of a group that holds one, or deletes such a group; - writes to an administrator user (profile, credentials, enabled, sessions, consents, attributes, memberships, permissions, deletion), an administrative group (settings, attributes, members, permissions, deletion), or an administrator client (settings, authentication, flows, redirect URIs, web origins, token settings, permissions, logo, deletion); - switches whether a client may request the administrative scopes, on or off, on any client; - reads an administrator client’s secret; - changes the email or the audit-log settings; - changes an administrative permission’s description. Granular scopes still read administrators. The refusal is answered after the request’s 400 and 404, writes nothing, carries `WWW-Authenticate: Bearer realm="goiabada", error="insufficient_scope", error_description="...", scope="authserver:manage"`, and leaves an `administrator_change_refused` audit record. Every operation this document secures declares a 403. The public client logo endpoint, which carries `security: []` and authorises nothing, is the one that does not. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on scope and administrative-policy refusals; other forbidden responses omit it. ### 404 Resource not found application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` ### 500 The request failed inside the server, most often a storage error. error_code is INTERNAL_SERVER_ERROR: no detail about the failure, only the request id an operator can find the log line by, and retrying is reasonable. Key rotation’s 500 also answers KEY_SET_INCOMPLETE, which names its cause and no retry clears. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` # Update client settings Source: https://goiabada.dev/reference/api/admin/operations/updateclientsettings/ PUT /api/v1/admin/clients/{id} Shell / cURL ```sh curl --request PUT \ --url http://localhost:9090/api/v1/admin/clients/1 \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "clientIdentifier": "example", "description": "example", "websiteUrl": "example", "displayName": "example", "enabled": true, "consentRequired": true, "showLogo": true, "showDisplayName": true, "showDescription": true, "showWebsiteUrl": true, "defaultAcrLevel": "example" }' ``` - Auth server base URL{baseUrl}/api/v1/admin/clients/{id} Update client general settings ## Authorizations - **[BearerAuth](https://goiabada.dev/reference/api/admin/#bearerauth)** ## Parameters ### Path Parameters **id** required integer format: int64 Client ID ## Request Bodyrequired application/json object **clientIdentifier** required string **description** string **websiteUrl** The client’s website URL (http or https, max 256 chars) string **displayName** A user-friendly name for the client (max 100 chars) string **enabled** boolean **consentRequired** boolean **showLogo** When true, the client logo is displayed on sign-in and consent screens boolean **showDisplayName** When true, the display name is shown instead of the client identifier boolean **showDescription** When true, the client description is displayed on sign-in and consent screens boolean **showWebsiteUrl** When true, the client website URL is displayed on sign-in and consent screens boolean **defaultAcrLevel** string ### Example generated ```json { "clientIdentifier": "example", "description": "example", "websiteUrl": "example", "displayName": "example", "enabled": true, "consentRequired": true, "showLogo": true, "showDisplayName": true, "showDescription": true, "showWebsiteUrl": true, "defaultAcrLevel": "example" } ``` ## Responses ### 200 Client updated application/json object **client** required object **id** required integer format: int64 **createdAt** required string format: date-time nullable **updatedAt** required string format: date-time nullable **clientIdentifier** required string **description** required string **websiteUrl** required The client’s website URL string **displayName** required A user-friendly name for the client string **enabled** required boolean **consentRequired** required boolean **createdViaDcr** required Read-only. True when the client registered itself through /connect/register boolean **administrativeScopesAllowed** required Read-only. True when the client may request the administrative authserver scopes on a user’s behalf; always true for the admin console’s own client. Switched through PUT /api/v1/admin/clients/{id}/administrative-scopes, with authserver:manage alone boolean **showLogo** required When true, the client logo is displayed on sign-in and consent screens boolean **showDisplayName** required When true, the display name is shown instead of the client identifier boolean **showDescription** required When true, the client description is displayed on sign-in and consent screens boolean **showWebsiteUrl** required When true, the client website URL is displayed on sign-in and consent screens boolean **isPublic** required boolean **isSystemLevelClient** required boolean **authorizationCodeEnabled** required boolean **clientCredentialsEnabled** required boolean **pkceRequired** required Null inherits the global setting, true requires PKCE, false makes it optional. Ignored for public clients, which always require PKCE boolean nullable **implicitGrantEnabled** required Null inherits the global setting, true enables the implicit flow, false disables it. Deprecated in OAuth 2.1 boolean nullable **resourceOwnerPasswordCredentialsEnabled** required Null inherits the global setting, true enables grant_type=password (RFC 6749 section 4.3), false disables it. Deprecated in OAuth 2.1 boolean nullable **tokenExpirationInSeconds** required integer **refreshTokenOfflineIdleTimeoutInSeconds** required integer **refreshTokenOfflineMaxLifetimeInSeconds** required integer **includeOpenIDConnectClaimsInAccessToken** required string **includeOpenIDConnectClaimsInIdToken** required string **defaultAcrLevel** required string **redirectURIs** required Null when this collection was not loaded for the response. Array\ nullable The `redirectURIs` entries of ClientResponse. The schema name is unchanged, but the shape below is not what releases up to and including v1.6.0 put on the wire: this position carried the persistence row directly, so its keys arrived capitalised and its `createdAt` arrived as a two-field `{"Time":...,"Valid":...}` object. The keys below are the ones every published contract has always declared. object **id** required integer format: int64 **createdAt** required string format: date-time nullable **uri** required string **clientId** required integer format: int64 **webOrigins** required Null when this collection was not loaded or the client has no web origins. Array\ nullable The `webOrigins` entries of ClientResponse. Its keys moved in the same release and for the same reason as RedirectURI above. object **id** required integer format: int64 **createdAt** required string format: date-time nullable **origin** required string **clientId** required integer format: int64 #### Example generated ```json { "client": { "id": 1, "createdAt": "2026-04-15T12:00:00Z", "updatedAt": "2026-04-15T12:00:00Z", "clientIdentifier": "example", "description": "example", "websiteUrl": "example", "displayName": "example", "enabled": true, "consentRequired": true, "createdViaDcr": true, "administrativeScopesAllowed": true, "showLogo": true, "showDisplayName": true, "showDescription": true, "showWebsiteUrl": true, "isPublic": true, "isSystemLevelClient": true, "authorizationCodeEnabled": true, "clientCredentialsEnabled": true, "pkceRequired": true, "implicitGrantEnabled": true, "resourceOwnerPasswordCredentialsEnabled": true, "tokenExpirationInSeconds": 1, "refreshTokenOfflineIdleTimeoutInSeconds": 1, "refreshTokenOfflineMaxLifetimeInSeconds": 1, "includeOpenIDConnectClaimsInAccessToken": "example", "includeOpenIDConnectClaimsInIdToken": "example", "defaultAcrLevel": "example", "redirectURIs": [ { "id": 1, "createdAt": "2026-04-15T12:00:00Z", "uri": "example", "clientId": 1 } ], "webOrigins": [ { "id": 1, "createdAt": "2026-04-15T12:00:00Z", "origin": "example", "clientId": 1 } ] } } ``` ### 400 Invalid request body or parameters application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge when token transport is malformed; ordinary validation errors omit it. ### 401 Missing or invalid bearer token application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on token refusals. An operation may also return a handler-level 401 without this header. ### 403 The token is valid but does not authorise this request. Four checks answer here, and a caller distinguishes them by error_code. INSUFFICIENT_SCOPE comes from the route’s authorisation middleware, before the handler runs: an Admin operation needs one of the scopes its route names, and an Account operation needs `authserver:manage-account`. USER_CONTEXT_REQUIRED comes from the same middleware on the Account operations, which also require the token to have been issued for a user: one obtained through the client credentials grant carries no auth_time claim and is refused here. FORBIDDEN comes from the handler, on the account endpoints that resolve a resource by id and then check it belongs to the caller, so a valid token for the wrong user reaches this rather than a 404. MANAGE_SCOPE_REQUIRED comes from the handler, on the Admin operations the administrative policy guards. Only `authserver:manage` creates an administrator, changes one, or changes what reaches one, so no granular scope will ever be enough: the remedy is a token holding `authserver:manage`, not requesting the route’s scope as for INSUFFICIENT_SCOPE. An administrator is a user, group or client holding any of the six administrative permissions (`manage`, `admin-read`, `manage-users`, `manage-clients`, `manage-settings`, `browser-sessions`), a user directly or through any of its groups, the admin console’s own client whatever it holds, and a client allowed to request the administrative scopes. A granular scope is refused here when it: - grants or revokes an administrative permission, directly or by moving a user into or out of a group that holds one, or deletes such a group; - writes to an administrator user (profile, credentials, enabled, sessions, consents, attributes, memberships, permissions, deletion), an administrative group (settings, attributes, members, permissions, deletion), or an administrator client (settings, authentication, flows, redirect URIs, web origins, token settings, permissions, logo, deletion); - switches whether a client may request the administrative scopes, on or off, on any client; - reads an administrator client’s secret; - changes the email or the audit-log settings; - changes an administrative permission’s description. Granular scopes still read administrators. The refusal is answered after the request’s 400 and 404, writes nothing, carries `WWW-Authenticate: Bearer realm="goiabada", error="insufficient_scope", error_description="...", scope="authserver:manage"`, and leaves an `administrator_change_refused` audit record. Every operation this document secures declares a 403. The public client logo endpoint, which carries `security: []` and authorises nothing, is the one that does not. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on scope and administrative-policy refusals; other forbidden responses omit it. ### 404 Resource not found application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` ### 500 The request failed inside the server, most often a storage error. error_code is INTERNAL_SERVER_ERROR: no detail about the failure, only the request id an operator can find the log line by, and retrying is reasonable. Key rotation’s 500 also answers KEY_SET_INCOMPLETE, which names its cause and no retry clears. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` # Delete client Source: https://goiabada.dev/reference/api/admin/operations/deleteclient/ DELETE /api/v1/admin/clients/{id} Shell / cURL ```sh curl --request DELETE \ --url http://localhost:9090/api/v1/admin/clients/1 \ --header 'Authorization: Bearer ' ``` - Auth server base URL{baseUrl}/api/v1/admin/clients/{id} Delete an OAuth2/OIDC client. The admin console’s own client, the system-level client, cannot be deleted: the request is refused with 400 and error_code VALIDATION_ERROR. ## Authorizations - **[BearerAuth](https://goiabada.dev/reference/api/admin/#bearerauth)** ## Parameters ### Path Parameters **id** required integer format: int64 Client ID ## Responses ### 200 Client deleted application/json object **success** required boolean #### Example generated ```json { "success": true } ``` ### 400 Invalid request body or parameters application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge when token transport is malformed; ordinary validation errors omit it. ### 401 Missing or invalid bearer token application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on token refusals. An operation may also return a handler-level 401 without this header. ### 403 The token is valid but does not authorise this request. Four checks answer here, and a caller distinguishes them by error_code. INSUFFICIENT_SCOPE comes from the route’s authorisation middleware, before the handler runs: an Admin operation needs one of the scopes its route names, and an Account operation needs `authserver:manage-account`. USER_CONTEXT_REQUIRED comes from the same middleware on the Account operations, which also require the token to have been issued for a user: one obtained through the client credentials grant carries no auth_time claim and is refused here. FORBIDDEN comes from the handler, on the account endpoints that resolve a resource by id and then check it belongs to the caller, so a valid token for the wrong user reaches this rather than a 404. MANAGE_SCOPE_REQUIRED comes from the handler, on the Admin operations the administrative policy guards. Only `authserver:manage` creates an administrator, changes one, or changes what reaches one, so no granular scope will ever be enough: the remedy is a token holding `authserver:manage`, not requesting the route’s scope as for INSUFFICIENT_SCOPE. An administrator is a user, group or client holding any of the six administrative permissions (`manage`, `admin-read`, `manage-users`, `manage-clients`, `manage-settings`, `browser-sessions`), a user directly or through any of its groups, the admin console’s own client whatever it holds, and a client allowed to request the administrative scopes. A granular scope is refused here when it: - grants or revokes an administrative permission, directly or by moving a user into or out of a group that holds one, or deletes such a group; - writes to an administrator user (profile, credentials, enabled, sessions, consents, attributes, memberships, permissions, deletion), an administrative group (settings, attributes, members, permissions, deletion), or an administrator client (settings, authentication, flows, redirect URIs, web origins, token settings, permissions, logo, deletion); - switches whether a client may request the administrative scopes, on or off, on any client; - reads an administrator client’s secret; - changes the email or the audit-log settings; - changes an administrative permission’s description. Granular scopes still read administrators. The refusal is answered after the request’s 400 and 404, writes nothing, carries `WWW-Authenticate: Bearer realm="goiabada", error="insufficient_scope", error_description="...", scope="authserver:manage"`, and leaves an `administrator_change_refused` audit record. Every operation this document secures declares a 403. The public client logo endpoint, which carries `security: []` and authorises nothing, is the one that does not. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on scope and administrative-policy refusals; other forbidden responses omit it. ### 404 Resource not found application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` ### 500 The request failed inside the server, most often a storage error. error_code is INTERNAL_SERVER_ERROR: no detail about the failure, only the request id an operator can find the log line by, and retrying is reasonable. Key rotation’s 500 also answers KEY_SET_INCOMPLETE, which names its cause and no retry clears. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` # Get client secret Source: https://goiabada.dev/reference/api/admin/operations/getclientsecret/ GET /api/v1/admin/clients/{id}/secret Shell / cURL ```sh curl --request GET \ --url http://localhost:9090/api/v1/admin/clients/1/secret \ --header 'Authorization: Bearer ' ``` - Auth server base URL{baseUrl}/api/v1/admin/clients/{id}/secret Get a client’s secret, decrypted. This is the one operation that answers a client secret, and it requires `authserver:manage-clients` or `authserver:manage`: `authserver:admin-read` reads every client and is refused here. The secret of an administrator client, one holding an administrative permission or the admin console’s own client, is read with `authserver:manage` alone, and `authserver:manage-clients` is refused it with MANAGE_SCOPE_REQUIRED. A client holding no secret, a public one, is answered an empty `clientSecret`. Every read that answers a secret leaves a `viewed_client_secret` audit record naming the client and the caller. A read of a client holding none leaves no record. ## Authorizations - **[BearerAuth](https://goiabada.dev/reference/api/admin/#bearerauth)** ## Parameters ### Path Parameters **id** required integer format: int64 Client ID ## Responses ### 200 The client’s secret application/json object **clientSecret** required The client’s secret, decrypted; empty for a client that holds none string #### Example generated ```json { "clientSecret": "example" } ``` ### 400 Invalid request body or parameters application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge when token transport is malformed; ordinary validation errors omit it. ### 401 Missing or invalid bearer token application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on token refusals. An operation may also return a handler-level 401 without this header. ### 403 The token is valid but does not authorise this request. Four checks answer here, and a caller distinguishes them by error_code. INSUFFICIENT_SCOPE comes from the route’s authorisation middleware, before the handler runs: an Admin operation needs one of the scopes its route names, and an Account operation needs `authserver:manage-account`. USER_CONTEXT_REQUIRED comes from the same middleware on the Account operations, which also require the token to have been issued for a user: one obtained through the client credentials grant carries no auth_time claim and is refused here. FORBIDDEN comes from the handler, on the account endpoints that resolve a resource by id and then check it belongs to the caller, so a valid token for the wrong user reaches this rather than a 404. MANAGE_SCOPE_REQUIRED comes from the handler, on the Admin operations the administrative policy guards. Only `authserver:manage` creates an administrator, changes one, or changes what reaches one, so no granular scope will ever be enough: the remedy is a token holding `authserver:manage`, not requesting the route’s scope as for INSUFFICIENT_SCOPE. An administrator is a user, group or client holding any of the six administrative permissions (`manage`, `admin-read`, `manage-users`, `manage-clients`, `manage-settings`, `browser-sessions`), a user directly or through any of its groups, the admin console’s own client whatever it holds, and a client allowed to request the administrative scopes. A granular scope is refused here when it: - grants or revokes an administrative permission, directly or by moving a user into or out of a group that holds one, or deletes such a group; - writes to an administrator user (profile, credentials, enabled, sessions, consents, attributes, memberships, permissions, deletion), an administrative group (settings, attributes, members, permissions, deletion), or an administrator client (settings, authentication, flows, redirect URIs, web origins, token settings, permissions, logo, deletion); - switches whether a client may request the administrative scopes, on or off, on any client; - reads an administrator client’s secret; - changes the email or the audit-log settings; - changes an administrative permission’s description. Granular scopes still read administrators. The refusal is answered after the request’s 400 and 404, writes nothing, carries `WWW-Authenticate: Bearer realm="goiabada", error="insufficient_scope", error_description="...", scope="authserver:manage"`, and leaves an `administrator_change_refused` audit record. Every operation this document secures declares a 403. The public client logo endpoint, which carries `security: []` and authorises nothing, is the one that does not. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on scope and administrative-policy refusals; other forbidden responses omit it. ### 404 Resource not found application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` ### 500 The request failed inside the server, most often a storage error. error_code is INTERNAL_SERVER_ERROR: no detail about the failure, only the request id an operator can find the log line by, and retrying is reasonable. Key rotation’s 500 also answers KEY_SET_INCOMPLETE, which names its cause and no retry clears. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` # Update client authentication Source: https://goiabada.dev/reference/api/admin/operations/updateclientauthentication/ PUT /api/v1/admin/clients/{id}/authentication Shell / cURL ```sh curl --request PUT \ --url http://localhost:9090/api/v1/admin/clients/1/authentication \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "isPublic": true, "clientSecret": "example" }' ``` - Auth server base URL{baseUrl}/api/v1/admin/clients/{id}/authentication Update client public/confidential mode and secret. Switching a confidential client to public also revokes every authorization code and refresh token the client holds, in the same transaction as the write: after it the client authenticates with nothing, so the grants issued on the understanding that redeeming them took a secret are no longer honoured. It sets pkceRequired to true for the same reason, and disables the client credentials flow. The reverse transition and a secret rotation revoke nothing. ## Authorizations - **[BearerAuth](https://goiabada.dev/reference/api/admin/#bearerauth)** ## Parameters ### Path Parameters **id** required integer format: int64 Client ID ## Request Bodyrequired application/json object **isPublic** boolean **clientSecret** Required for confidential clients string ### Example generated ```json { "isPublic": true, "clientSecret": "example" } ``` ## Responses ### 200 Authentication updated application/json object **client** required object **id** required integer format: int64 **createdAt** required string format: date-time nullable **updatedAt** required string format: date-time nullable **clientIdentifier** required string **description** required string **websiteUrl** required The client’s website URL string **displayName** required A user-friendly name for the client string **enabled** required boolean **consentRequired** required boolean **createdViaDcr** required Read-only. True when the client registered itself through /connect/register boolean **administrativeScopesAllowed** required Read-only. True when the client may request the administrative authserver scopes on a user’s behalf; always true for the admin console’s own client. Switched through PUT /api/v1/admin/clients/{id}/administrative-scopes, with authserver:manage alone boolean **showLogo** required When true, the client logo is displayed on sign-in and consent screens boolean **showDisplayName** required When true, the display name is shown instead of the client identifier boolean **showDescription** required When true, the client description is displayed on sign-in and consent screens boolean **showWebsiteUrl** required When true, the client website URL is displayed on sign-in and consent screens boolean **isPublic** required boolean **isSystemLevelClient** required boolean **authorizationCodeEnabled** required boolean **clientCredentialsEnabled** required boolean **pkceRequired** required Null inherits the global setting, true requires PKCE, false makes it optional. Ignored for public clients, which always require PKCE boolean nullable **implicitGrantEnabled** required Null inherits the global setting, true enables the implicit flow, false disables it. Deprecated in OAuth 2.1 boolean nullable **resourceOwnerPasswordCredentialsEnabled** required Null inherits the global setting, true enables grant_type=password (RFC 6749 section 4.3), false disables it. Deprecated in OAuth 2.1 boolean nullable **tokenExpirationInSeconds** required integer **refreshTokenOfflineIdleTimeoutInSeconds** required integer **refreshTokenOfflineMaxLifetimeInSeconds** required integer **includeOpenIDConnectClaimsInAccessToken** required string **includeOpenIDConnectClaimsInIdToken** required string **defaultAcrLevel** required string **redirectURIs** required Null when this collection was not loaded for the response. Array\ nullable The `redirectURIs` entries of ClientResponse. The schema name is unchanged, but the shape below is not what releases up to and including v1.6.0 put on the wire: this position carried the persistence row directly, so its keys arrived capitalised and its `createdAt` arrived as a two-field `{"Time":...,"Valid":...}` object. The keys below are the ones every published contract has always declared. object **id** required integer format: int64 **createdAt** required string format: date-time nullable **uri** required string **clientId** required integer format: int64 **webOrigins** required Null when this collection was not loaded or the client has no web origins. Array\ nullable The `webOrigins` entries of ClientResponse. Its keys moved in the same release and for the same reason as RedirectURI above. object **id** required integer format: int64 **createdAt** required string format: date-time nullable **origin** required string **clientId** required integer format: int64 #### Example generated ```json { "client": { "id": 1, "createdAt": "2026-04-15T12:00:00Z", "updatedAt": "2026-04-15T12:00:00Z", "clientIdentifier": "example", "description": "example", "websiteUrl": "example", "displayName": "example", "enabled": true, "consentRequired": true, "createdViaDcr": true, "administrativeScopesAllowed": true, "showLogo": true, "showDisplayName": true, "showDescription": true, "showWebsiteUrl": true, "isPublic": true, "isSystemLevelClient": true, "authorizationCodeEnabled": true, "clientCredentialsEnabled": true, "pkceRequired": true, "implicitGrantEnabled": true, "resourceOwnerPasswordCredentialsEnabled": true, "tokenExpirationInSeconds": 1, "refreshTokenOfflineIdleTimeoutInSeconds": 1, "refreshTokenOfflineMaxLifetimeInSeconds": 1, "includeOpenIDConnectClaimsInAccessToken": "example", "includeOpenIDConnectClaimsInIdToken": "example", "defaultAcrLevel": "example", "redirectURIs": [ { "id": 1, "createdAt": "2026-04-15T12:00:00Z", "uri": "example", "clientId": 1 } ], "webOrigins": [ { "id": 1, "createdAt": "2026-04-15T12:00:00Z", "origin": "example", "clientId": 1 } ] } } ``` ### 400 Invalid request body or parameters application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge when token transport is malformed; ordinary validation errors omit it. ### 401 Missing or invalid bearer token application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on token refusals. An operation may also return a handler-level 401 without this header. ### 403 The token is valid but does not authorise this request. Four checks answer here, and a caller distinguishes them by error_code. INSUFFICIENT_SCOPE comes from the route’s authorisation middleware, before the handler runs: an Admin operation needs one of the scopes its route names, and an Account operation needs `authserver:manage-account`. USER_CONTEXT_REQUIRED comes from the same middleware on the Account operations, which also require the token to have been issued for a user: one obtained through the client credentials grant carries no auth_time claim and is refused here. FORBIDDEN comes from the handler, on the account endpoints that resolve a resource by id and then check it belongs to the caller, so a valid token for the wrong user reaches this rather than a 404. MANAGE_SCOPE_REQUIRED comes from the handler, on the Admin operations the administrative policy guards. Only `authserver:manage` creates an administrator, changes one, or changes what reaches one, so no granular scope will ever be enough: the remedy is a token holding `authserver:manage`, not requesting the route’s scope as for INSUFFICIENT_SCOPE. An administrator is a user, group or client holding any of the six administrative permissions (`manage`, `admin-read`, `manage-users`, `manage-clients`, `manage-settings`, `browser-sessions`), a user directly or through any of its groups, the admin console’s own client whatever it holds, and a client allowed to request the administrative scopes. A granular scope is refused here when it: - grants or revokes an administrative permission, directly or by moving a user into or out of a group that holds one, or deletes such a group; - writes to an administrator user (profile, credentials, enabled, sessions, consents, attributes, memberships, permissions, deletion), an administrative group (settings, attributes, members, permissions, deletion), or an administrator client (settings, authentication, flows, redirect URIs, web origins, token settings, permissions, logo, deletion); - switches whether a client may request the administrative scopes, on or off, on any client; - reads an administrator client’s secret; - changes the email or the audit-log settings; - changes an administrative permission’s description. Granular scopes still read administrators. The refusal is answered after the request’s 400 and 404, writes nothing, carries `WWW-Authenticate: Bearer realm="goiabada", error="insufficient_scope", error_description="...", scope="authserver:manage"`, and leaves an `administrator_change_refused` audit record. Every operation this document secures declares a 403. The public client logo endpoint, which carries `security: []` and authorises nothing, is the one that does not. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on scope and administrative-policy refusals; other forbidden responses omit it. ### 404 Resource not found application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` ### 500 The request failed inside the server, most often a storage error. error_code is INTERNAL_SERVER_ERROR: no detail about the failure, only the request id an operator can find the log line by, and retrying is reasonable. Key rotation’s 500 also answers KEY_SET_INCOMPLETE, which names its cause and no retry clears. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` # Update OAuth2 flows Source: https://goiabada.dev/reference/api/admin/operations/updateclientoauth2flows/ PUT /api/v1/admin/clients/{id}/oauth2-flows Shell / cURL ```sh curl --request PUT \ --url http://localhost:9090/api/v1/admin/clients/1/oauth2-flows \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "authorizationCodeEnabled": true, "clientCredentialsEnabled": true, "pkceRequired": true, "implicitGrantEnabled": true, "resourceOwnerPasswordCredentialsEnabled": true }' ``` - Auth server base URL{baseUrl}/api/v1/admin/clients/{id}/oauth2-flows Enable/disable OAuth2 flows for a client ## Authorizations - **[BearerAuth](https://goiabada.dev/reference/api/admin/#bearerauth)** ## Parameters ### Path Parameters **id** required integer format: int64 Client ID ## Request Bodyrequired application/json object **authorizationCodeEnabled** boolean **clientCredentialsEnabled** boolean **pkceRequired** Null inherits the global setting, true requires PKCE, false makes it optional. Ignored for public clients, which always require PKCE boolean nullable **implicitGrantEnabled** Null inherits the global setting, true enables the implicit flow, false disables it. Deprecated in OAuth 2.1 boolean nullable **resourceOwnerPasswordCredentialsEnabled** Null inherits the global setting, true enables grant_type=password (RFC 6749 section 4.3), false disables it. Deprecated in OAuth 2.1 boolean nullable ### Example generated ```json { "authorizationCodeEnabled": true, "clientCredentialsEnabled": true, "pkceRequired": true, "implicitGrantEnabled": true, "resourceOwnerPasswordCredentialsEnabled": true } ``` ## Responses ### 200 Flows updated application/json object **client** required object **id** required integer format: int64 **createdAt** required string format: date-time nullable **updatedAt** required string format: date-time nullable **clientIdentifier** required string **description** required string **websiteUrl** required The client’s website URL string **displayName** required A user-friendly name for the client string **enabled** required boolean **consentRequired** required boolean **createdViaDcr** required Read-only. True when the client registered itself through /connect/register boolean **administrativeScopesAllowed** required Read-only. True when the client may request the administrative authserver scopes on a user’s behalf; always true for the admin console’s own client. Switched through PUT /api/v1/admin/clients/{id}/administrative-scopes, with authserver:manage alone boolean **showLogo** required When true, the client logo is displayed on sign-in and consent screens boolean **showDisplayName** required When true, the display name is shown instead of the client identifier boolean **showDescription** required When true, the client description is displayed on sign-in and consent screens boolean **showWebsiteUrl** required When true, the client website URL is displayed on sign-in and consent screens boolean **isPublic** required boolean **isSystemLevelClient** required boolean **authorizationCodeEnabled** required boolean **clientCredentialsEnabled** required boolean **pkceRequired** required Null inherits the global setting, true requires PKCE, false makes it optional. Ignored for public clients, which always require PKCE boolean nullable **implicitGrantEnabled** required Null inherits the global setting, true enables the implicit flow, false disables it. Deprecated in OAuth 2.1 boolean nullable **resourceOwnerPasswordCredentialsEnabled** required Null inherits the global setting, true enables grant_type=password (RFC 6749 section 4.3), false disables it. Deprecated in OAuth 2.1 boolean nullable **tokenExpirationInSeconds** required integer **refreshTokenOfflineIdleTimeoutInSeconds** required integer **refreshTokenOfflineMaxLifetimeInSeconds** required integer **includeOpenIDConnectClaimsInAccessToken** required string **includeOpenIDConnectClaimsInIdToken** required string **defaultAcrLevel** required string **redirectURIs** required Null when this collection was not loaded for the response. Array\ nullable The `redirectURIs` entries of ClientResponse. The schema name is unchanged, but the shape below is not what releases up to and including v1.6.0 put on the wire: this position carried the persistence row directly, so its keys arrived capitalised and its `createdAt` arrived as a two-field `{"Time":...,"Valid":...}` object. The keys below are the ones every published contract has always declared. object **id** required integer format: int64 **createdAt** required string format: date-time nullable **uri** required string **clientId** required integer format: int64 **webOrigins** required Null when this collection was not loaded or the client has no web origins. Array\ nullable The `webOrigins` entries of ClientResponse. Its keys moved in the same release and for the same reason as RedirectURI above. object **id** required integer format: int64 **createdAt** required string format: date-time nullable **origin** required string **clientId** required integer format: int64 #### Example generated ```json { "client": { "id": 1, "createdAt": "2026-04-15T12:00:00Z", "updatedAt": "2026-04-15T12:00:00Z", "clientIdentifier": "example", "description": "example", "websiteUrl": "example", "displayName": "example", "enabled": true, "consentRequired": true, "createdViaDcr": true, "administrativeScopesAllowed": true, "showLogo": true, "showDisplayName": true, "showDescription": true, "showWebsiteUrl": true, "isPublic": true, "isSystemLevelClient": true, "authorizationCodeEnabled": true, "clientCredentialsEnabled": true, "pkceRequired": true, "implicitGrantEnabled": true, "resourceOwnerPasswordCredentialsEnabled": true, "tokenExpirationInSeconds": 1, "refreshTokenOfflineIdleTimeoutInSeconds": 1, "refreshTokenOfflineMaxLifetimeInSeconds": 1, "includeOpenIDConnectClaimsInAccessToken": "example", "includeOpenIDConnectClaimsInIdToken": "example", "defaultAcrLevel": "example", "redirectURIs": [ { "id": 1, "createdAt": "2026-04-15T12:00:00Z", "uri": "example", "clientId": 1 } ], "webOrigins": [ { "id": 1, "createdAt": "2026-04-15T12:00:00Z", "origin": "example", "clientId": 1 } ] } } ``` ### 400 Invalid request body or parameters application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge when token transport is malformed; ordinary validation errors omit it. ### 401 Missing or invalid bearer token application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on token refusals. An operation may also return a handler-level 401 without this header. ### 403 The token is valid but does not authorise this request. Four checks answer here, and a caller distinguishes them by error_code. INSUFFICIENT_SCOPE comes from the route’s authorisation middleware, before the handler runs: an Admin operation needs one of the scopes its route names, and an Account operation needs `authserver:manage-account`. USER_CONTEXT_REQUIRED comes from the same middleware on the Account operations, which also require the token to have been issued for a user: one obtained through the client credentials grant carries no auth_time claim and is refused here. FORBIDDEN comes from the handler, on the account endpoints that resolve a resource by id and then check it belongs to the caller, so a valid token for the wrong user reaches this rather than a 404. MANAGE_SCOPE_REQUIRED comes from the handler, on the Admin operations the administrative policy guards. Only `authserver:manage` creates an administrator, changes one, or changes what reaches one, so no granular scope will ever be enough: the remedy is a token holding `authserver:manage`, not requesting the route’s scope as for INSUFFICIENT_SCOPE. An administrator is a user, group or client holding any of the six administrative permissions (`manage`, `admin-read`, `manage-users`, `manage-clients`, `manage-settings`, `browser-sessions`), a user directly or through any of its groups, the admin console’s own client whatever it holds, and a client allowed to request the administrative scopes. A granular scope is refused here when it: - grants or revokes an administrative permission, directly or by moving a user into or out of a group that holds one, or deletes such a group; - writes to an administrator user (profile, credentials, enabled, sessions, consents, attributes, memberships, permissions, deletion), an administrative group (settings, attributes, members, permissions, deletion), or an administrator client (settings, authentication, flows, redirect URIs, web origins, token settings, permissions, logo, deletion); - switches whether a client may request the administrative scopes, on or off, on any client; - reads an administrator client’s secret; - changes the email or the audit-log settings; - changes an administrative permission’s description. Granular scopes still read administrators. The refusal is answered after the request’s 400 and 404, writes nothing, carries `WWW-Authenticate: Bearer realm="goiabada", error="insufficient_scope", error_description="...", scope="authserver:manage"`, and leaves an `administrator_change_refused` audit record. Every operation this document secures declares a 403. The public client logo endpoint, which carries `security: []` and authorises nothing, is the one that does not. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on scope and administrative-policy refusals; other forbidden responses omit it. ### 404 Resource not found application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` ### 500 The request failed inside the server, most often a storage error. error_code is INTERNAL_SERVER_ERROR: no detail about the failure, only the request id an operator can find the log line by, and retrying is reasonable. Key rotation’s 500 also answers KEY_SET_INCOMPLETE, which names its cause and no retry clears. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` # Update redirect URIs Source: https://goiabada.dev/reference/api/admin/operations/updateclientredirecturis/ PUT /api/v1/admin/clients/{id}/redirect-uris Shell / cURL ```sh curl --request PUT \ --url http://localhost:9090/api/v1/admin/clients/1/redirect-uris \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "redirectURIs": [ "example" ], "expectedRedirectURIs": [ "example" ] }' ``` - Auth server base URL{baseUrl}/api/v1/admin/clients/{id}/redirect-uris Replace redirect URIs for a client ## Authorizations - **[BearerAuth](https://goiabada.dev/reference/api/admin/#bearerauth)** ## Parameters ### Path Parameters **id** required integer format: int64 Client ID ## Request Bodyrequired application/json object **redirectURIs** The complete set of redirect URIs the client has after the call. More than 60, one longer than 2048 bytes, or one listed twice is refused with VALIDATION_ERROR before anything is written. Array\ <= 60 items **expectedRedirectURIs** required The redirect URIs as last read; \[] if there were none. Array\ ### Example generated ```json { "redirectURIs": [ "example" ], "expectedRedirectURIs": [ "example" ] } ``` ## Responses ### 200 Redirect URIs updated application/json object **client** required object **id** required integer format: int64 **createdAt** required string format: date-time nullable **updatedAt** required string format: date-time nullable **clientIdentifier** required string **description** required string **websiteUrl** required The client’s website URL string **displayName** required A user-friendly name for the client string **enabled** required boolean **consentRequired** required boolean **createdViaDcr** required Read-only. True when the client registered itself through /connect/register boolean **administrativeScopesAllowed** required Read-only. True when the client may request the administrative authserver scopes on a user’s behalf; always true for the admin console’s own client. Switched through PUT /api/v1/admin/clients/{id}/administrative-scopes, with authserver:manage alone boolean **showLogo** required When true, the client logo is displayed on sign-in and consent screens boolean **showDisplayName** required When true, the display name is shown instead of the client identifier boolean **showDescription** required When true, the client description is displayed on sign-in and consent screens boolean **showWebsiteUrl** required When true, the client website URL is displayed on sign-in and consent screens boolean **isPublic** required boolean **isSystemLevelClient** required boolean **authorizationCodeEnabled** required boolean **clientCredentialsEnabled** required boolean **pkceRequired** required Null inherits the global setting, true requires PKCE, false makes it optional. Ignored for public clients, which always require PKCE boolean nullable **implicitGrantEnabled** required Null inherits the global setting, true enables the implicit flow, false disables it. Deprecated in OAuth 2.1 boolean nullable **resourceOwnerPasswordCredentialsEnabled** required Null inherits the global setting, true enables grant_type=password (RFC 6749 section 4.3), false disables it. Deprecated in OAuth 2.1 boolean nullable **tokenExpirationInSeconds** required integer **refreshTokenOfflineIdleTimeoutInSeconds** required integer **refreshTokenOfflineMaxLifetimeInSeconds** required integer **includeOpenIDConnectClaimsInAccessToken** required string **includeOpenIDConnectClaimsInIdToken** required string **defaultAcrLevel** required string **redirectURIs** required Null when this collection was not loaded for the response. Array\ nullable The `redirectURIs` entries of ClientResponse. The schema name is unchanged, but the shape below is not what releases up to and including v1.6.0 put on the wire: this position carried the persistence row directly, so its keys arrived capitalised and its `createdAt` arrived as a two-field `{"Time":...,"Valid":...}` object. The keys below are the ones every published contract has always declared. object **id** required integer format: int64 **createdAt** required string format: date-time nullable **uri** required string **clientId** required integer format: int64 **webOrigins** required Null when this collection was not loaded or the client has no web origins. Array\ nullable The `webOrigins` entries of ClientResponse. Its keys moved in the same release and for the same reason as RedirectURI above. object **id** required integer format: int64 **createdAt** required string format: date-time nullable **origin** required string **clientId** required integer format: int64 #### Example generated ```json { "client": { "id": 1, "createdAt": "2026-04-15T12:00:00Z", "updatedAt": "2026-04-15T12:00:00Z", "clientIdentifier": "example", "description": "example", "websiteUrl": "example", "displayName": "example", "enabled": true, "consentRequired": true, "createdViaDcr": true, "administrativeScopesAllowed": true, "showLogo": true, "showDisplayName": true, "showDescription": true, "showWebsiteUrl": true, "isPublic": true, "isSystemLevelClient": true, "authorizationCodeEnabled": true, "clientCredentialsEnabled": true, "pkceRequired": true, "implicitGrantEnabled": true, "resourceOwnerPasswordCredentialsEnabled": true, "tokenExpirationInSeconds": 1, "refreshTokenOfflineIdleTimeoutInSeconds": 1, "refreshTokenOfflineMaxLifetimeInSeconds": 1, "includeOpenIDConnectClaimsInAccessToken": "example", "includeOpenIDConnectClaimsInIdToken": "example", "defaultAcrLevel": "example", "redirectURIs": [ { "id": 1, "createdAt": "2026-04-15T12:00:00Z", "uri": "example", "clientId": 1 } ], "webOrigins": [ { "id": 1, "createdAt": "2026-04-15T12:00:00Z", "origin": "example", "clientId": 1 } ] } } ``` ### 400 Invalid request body or parameters application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge when token transport is malformed; ordinary validation errors omit it. ### 401 Missing or invalid bearer token application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on token refusals. An operation may also return a handler-level 401 without this header. ### 403 The token is valid but does not authorise this request. Four checks answer here, and a caller distinguishes them by error_code. INSUFFICIENT_SCOPE comes from the route’s authorisation middleware, before the handler runs: an Admin operation needs one of the scopes its route names, and an Account operation needs `authserver:manage-account`. USER_CONTEXT_REQUIRED comes from the same middleware on the Account operations, which also require the token to have been issued for a user: one obtained through the client credentials grant carries no auth_time claim and is refused here. FORBIDDEN comes from the handler, on the account endpoints that resolve a resource by id and then check it belongs to the caller, so a valid token for the wrong user reaches this rather than a 404. MANAGE_SCOPE_REQUIRED comes from the handler, on the Admin operations the administrative policy guards. Only `authserver:manage` creates an administrator, changes one, or changes what reaches one, so no granular scope will ever be enough: the remedy is a token holding `authserver:manage`, not requesting the route’s scope as for INSUFFICIENT_SCOPE. An administrator is a user, group or client holding any of the six administrative permissions (`manage`, `admin-read`, `manage-users`, `manage-clients`, `manage-settings`, `browser-sessions`), a user directly or through any of its groups, the admin console’s own client whatever it holds, and a client allowed to request the administrative scopes. A granular scope is refused here when it: - grants or revokes an administrative permission, directly or by moving a user into or out of a group that holds one, or deletes such a group; - writes to an administrator user (profile, credentials, enabled, sessions, consents, attributes, memberships, permissions, deletion), an administrative group (settings, attributes, members, permissions, deletion), or an administrator client (settings, authentication, flows, redirect URIs, web origins, token settings, permissions, logo, deletion); - switches whether a client may request the administrative scopes, on or off, on any client; - reads an administrator client’s secret; - changes the email or the audit-log settings; - changes an administrative permission’s description. Granular scopes still read administrators. The refusal is answered after the request’s 400 and 404, writes nothing, carries `WWW-Authenticate: Bearer realm="goiabada", error="insufficient_scope", error_description="...", scope="authserver:manage"`, and leaves an `administrator_change_refused` audit record. Every operation this document secures declares a 403. The public client logo endpoint, which carries `security: []` and authorises nothing, is the one that does not. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on scope and administrative-policy refusals; other forbidden responses omit it. ### 404 Resource not found application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` ### 409 The stored list differs from expectedRedirectURIs: another save changed it after it was read. error_code is CONCURRENT_UPDATE; nothing was saved. Read it again and retry. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` ### 500 The request failed inside the server, most often a storage error. error_code is INTERNAL_SERVER_ERROR: no detail about the failure, only the request id an operator can find the log line by, and retrying is reasonable. Key rotation’s 500 also answers KEY_SET_INCOMPLETE, which names its cause and no retry clears. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` # Update web origins Source: https://goiabada.dev/reference/api/admin/operations/updateclientweborigins/ PUT /api/v1/admin/clients/{id}/web-origins Shell / cURL ```sh curl --request PUT \ --url http://localhost:9090/api/v1/admin/clients/1/web-origins \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "webOrigins": [ "example" ], "expectedWebOrigins": [ "example" ] }' ``` - Auth server base URL{baseUrl}/api/v1/admin/clients/{id}/web-origins Replace web origins for a client ## Authorizations - **[BearerAuth](https://goiabada.dev/reference/api/admin/#bearerauth)** ## Parameters ### Path Parameters **id** required integer format: int64 Client ID ## Request Bodyrequired application/json object **webOrigins** Array\ **expectedWebOrigins** required The web origins as last read; \[] if there were none. Array\ ### Example generated ```json { "webOrigins": [ "example" ], "expectedWebOrigins": [ "example" ] } ``` ## Responses ### 200 Web origins updated application/json object **client** required object **id** required integer format: int64 **createdAt** required string format: date-time nullable **updatedAt** required string format: date-time nullable **clientIdentifier** required string **description** required string **websiteUrl** required The client’s website URL string **displayName** required A user-friendly name for the client string **enabled** required boolean **consentRequired** required boolean **createdViaDcr** required Read-only. True when the client registered itself through /connect/register boolean **administrativeScopesAllowed** required Read-only. True when the client may request the administrative authserver scopes on a user’s behalf; always true for the admin console’s own client. Switched through PUT /api/v1/admin/clients/{id}/administrative-scopes, with authserver:manage alone boolean **showLogo** required When true, the client logo is displayed on sign-in and consent screens boolean **showDisplayName** required When true, the display name is shown instead of the client identifier boolean **showDescription** required When true, the client description is displayed on sign-in and consent screens boolean **showWebsiteUrl** required When true, the client website URL is displayed on sign-in and consent screens boolean **isPublic** required boolean **isSystemLevelClient** required boolean **authorizationCodeEnabled** required boolean **clientCredentialsEnabled** required boolean **pkceRequired** required Null inherits the global setting, true requires PKCE, false makes it optional. Ignored for public clients, which always require PKCE boolean nullable **implicitGrantEnabled** required Null inherits the global setting, true enables the implicit flow, false disables it. Deprecated in OAuth 2.1 boolean nullable **resourceOwnerPasswordCredentialsEnabled** required Null inherits the global setting, true enables grant_type=password (RFC 6749 section 4.3), false disables it. Deprecated in OAuth 2.1 boolean nullable **tokenExpirationInSeconds** required integer **refreshTokenOfflineIdleTimeoutInSeconds** required integer **refreshTokenOfflineMaxLifetimeInSeconds** required integer **includeOpenIDConnectClaimsInAccessToken** required string **includeOpenIDConnectClaimsInIdToken** required string **defaultAcrLevel** required string **redirectURIs** required Null when this collection was not loaded for the response. Array\ nullable The `redirectURIs` entries of ClientResponse. The schema name is unchanged, but the shape below is not what releases up to and including v1.6.0 put on the wire: this position carried the persistence row directly, so its keys arrived capitalised and its `createdAt` arrived as a two-field `{"Time":...,"Valid":...}` object. The keys below are the ones every published contract has always declared. object **id** required integer format: int64 **createdAt** required string format: date-time nullable **uri** required string **clientId** required integer format: int64 **webOrigins** required Null when this collection was not loaded or the client has no web origins. Array\ nullable The `webOrigins` entries of ClientResponse. Its keys moved in the same release and for the same reason as RedirectURI above. object **id** required integer format: int64 **createdAt** required string format: date-time nullable **origin** required string **clientId** required integer format: int64 #### Example generated ```json { "client": { "id": 1, "createdAt": "2026-04-15T12:00:00Z", "updatedAt": "2026-04-15T12:00:00Z", "clientIdentifier": "example", "description": "example", "websiteUrl": "example", "displayName": "example", "enabled": true, "consentRequired": true, "createdViaDcr": true, "administrativeScopesAllowed": true, "showLogo": true, "showDisplayName": true, "showDescription": true, "showWebsiteUrl": true, "isPublic": true, "isSystemLevelClient": true, "authorizationCodeEnabled": true, "clientCredentialsEnabled": true, "pkceRequired": true, "implicitGrantEnabled": true, "resourceOwnerPasswordCredentialsEnabled": true, "tokenExpirationInSeconds": 1, "refreshTokenOfflineIdleTimeoutInSeconds": 1, "refreshTokenOfflineMaxLifetimeInSeconds": 1, "includeOpenIDConnectClaimsInAccessToken": "example", "includeOpenIDConnectClaimsInIdToken": "example", "defaultAcrLevel": "example", "redirectURIs": [ { "id": 1, "createdAt": "2026-04-15T12:00:00Z", "uri": "example", "clientId": 1 } ], "webOrigins": [ { "id": 1, "createdAt": "2026-04-15T12:00:00Z", "origin": "example", "clientId": 1 } ] } } ``` ### 400 Invalid request body or parameters application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge when token transport is malformed; ordinary validation errors omit it. ### 401 Missing or invalid bearer token application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on token refusals. An operation may also return a handler-level 401 without this header. ### 403 The token is valid but does not authorise this request. Four checks answer here, and a caller distinguishes them by error_code. INSUFFICIENT_SCOPE comes from the route’s authorisation middleware, before the handler runs: an Admin operation needs one of the scopes its route names, and an Account operation needs `authserver:manage-account`. USER_CONTEXT_REQUIRED comes from the same middleware on the Account operations, which also require the token to have been issued for a user: one obtained through the client credentials grant carries no auth_time claim and is refused here. FORBIDDEN comes from the handler, on the account endpoints that resolve a resource by id and then check it belongs to the caller, so a valid token for the wrong user reaches this rather than a 404. MANAGE_SCOPE_REQUIRED comes from the handler, on the Admin operations the administrative policy guards. Only `authserver:manage` creates an administrator, changes one, or changes what reaches one, so no granular scope will ever be enough: the remedy is a token holding `authserver:manage`, not requesting the route’s scope as for INSUFFICIENT_SCOPE. An administrator is a user, group or client holding any of the six administrative permissions (`manage`, `admin-read`, `manage-users`, `manage-clients`, `manage-settings`, `browser-sessions`), a user directly or through any of its groups, the admin console’s own client whatever it holds, and a client allowed to request the administrative scopes. A granular scope is refused here when it: - grants or revokes an administrative permission, directly or by moving a user into or out of a group that holds one, or deletes such a group; - writes to an administrator user (profile, credentials, enabled, sessions, consents, attributes, memberships, permissions, deletion), an administrative group (settings, attributes, members, permissions, deletion), or an administrator client (settings, authentication, flows, redirect URIs, web origins, token settings, permissions, logo, deletion); - switches whether a client may request the administrative scopes, on or off, on any client; - reads an administrator client’s secret; - changes the email or the audit-log settings; - changes an administrative permission’s description. Granular scopes still read administrators. The refusal is answered after the request’s 400 and 404, writes nothing, carries `WWW-Authenticate: Bearer realm="goiabada", error="insufficient_scope", error_description="...", scope="authserver:manage"`, and leaves an `administrator_change_refused` audit record. Every operation this document secures declares a 403. The public client logo endpoint, which carries `security: []` and authorises nothing, is the one that does not. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on scope and administrative-policy refusals; other forbidden responses omit it. ### 404 Resource not found application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` ### 409 Another save changed the list after expectedWebOrigins was read, or added the same origin at the same moment. error_code is CONCURRENT_UPDATE; nothing was saved. Read it again and retry. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` ### 500 The request failed inside the server, most often a storage error. error_code is INTERNAL_SERVER_ERROR: no detail about the failure, only the request id an operator can find the log line by, and retrying is reasonable. Key rotation’s 500 also answers KEY_SET_INCOMPLETE, which names its cause and no retry clears. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` # Update token settings Source: https://goiabada.dev/reference/api/admin/operations/updateclienttokens/ PUT /api/v1/admin/clients/{id}/tokens Shell / cURL ```sh curl --request PUT \ --url http://localhost:9090/api/v1/admin/clients/1/tokens \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "tokenExpirationInSeconds": 1, "refreshTokenOfflineIdleTimeoutInSeconds": 1, "refreshTokenOfflineMaxLifetimeInSeconds": 1, "includeOpenIDConnectClaimsInAccessToken": "example", "includeOpenIDConnectClaimsInIdToken": "example" }' ``` - Auth server base URL{baseUrl}/api/v1/admin/clients/{id}/tokens Update token expiration settings for a client. `includeOpenIDConnectClaimsInAccessToken` and `includeOpenIDConnectClaimsInIdToken` each take “on”, “off” or “default”. “default” follows the global setting of the same name in `updateSettingsTokens`. ## Authorizations - **[BearerAuth](https://goiabada.dev/reference/api/admin/#bearerauth)** ## Parameters ### Path Parameters **id** required integer format: int64 Client ID ## Request Bodyrequired application/json object **tokenExpirationInSeconds** integer **refreshTokenOfflineIdleTimeoutInSeconds** integer **refreshTokenOfflineMaxLifetimeInSeconds** integer **includeOpenIDConnectClaimsInAccessToken** string **includeOpenIDConnectClaimsInIdToken** string ### Example generated ```json { "tokenExpirationInSeconds": 1, "refreshTokenOfflineIdleTimeoutInSeconds": 1, "refreshTokenOfflineMaxLifetimeInSeconds": 1, "includeOpenIDConnectClaimsInAccessToken": "example", "includeOpenIDConnectClaimsInIdToken": "example" } ``` ## Responses ### 200 Token settings updated application/json object **client** required object **id** required integer format: int64 **createdAt** required string format: date-time nullable **updatedAt** required string format: date-time nullable **clientIdentifier** required string **description** required string **websiteUrl** required The client’s website URL string **displayName** required A user-friendly name for the client string **enabled** required boolean **consentRequired** required boolean **createdViaDcr** required Read-only. True when the client registered itself through /connect/register boolean **administrativeScopesAllowed** required Read-only. True when the client may request the administrative authserver scopes on a user’s behalf; always true for the admin console’s own client. Switched through PUT /api/v1/admin/clients/{id}/administrative-scopes, with authserver:manage alone boolean **showLogo** required When true, the client logo is displayed on sign-in and consent screens boolean **showDisplayName** required When true, the display name is shown instead of the client identifier boolean **showDescription** required When true, the client description is displayed on sign-in and consent screens boolean **showWebsiteUrl** required When true, the client website URL is displayed on sign-in and consent screens boolean **isPublic** required boolean **isSystemLevelClient** required boolean **authorizationCodeEnabled** required boolean **clientCredentialsEnabled** required boolean **pkceRequired** required Null inherits the global setting, true requires PKCE, false makes it optional. Ignored for public clients, which always require PKCE boolean nullable **implicitGrantEnabled** required Null inherits the global setting, true enables the implicit flow, false disables it. Deprecated in OAuth 2.1 boolean nullable **resourceOwnerPasswordCredentialsEnabled** required Null inherits the global setting, true enables grant_type=password (RFC 6749 section 4.3), false disables it. Deprecated in OAuth 2.1 boolean nullable **tokenExpirationInSeconds** required integer **refreshTokenOfflineIdleTimeoutInSeconds** required integer **refreshTokenOfflineMaxLifetimeInSeconds** required integer **includeOpenIDConnectClaimsInAccessToken** required string **includeOpenIDConnectClaimsInIdToken** required string **defaultAcrLevel** required string **redirectURIs** required Null when this collection was not loaded for the response. Array\ nullable The `redirectURIs` entries of ClientResponse. The schema name is unchanged, but the shape below is not what releases up to and including v1.6.0 put on the wire: this position carried the persistence row directly, so its keys arrived capitalised and its `createdAt` arrived as a two-field `{"Time":...,"Valid":...}` object. The keys below are the ones every published contract has always declared. object **id** required integer format: int64 **createdAt** required string format: date-time nullable **uri** required string **clientId** required integer format: int64 **webOrigins** required Null when this collection was not loaded or the client has no web origins. Array\ nullable The `webOrigins` entries of ClientResponse. Its keys moved in the same release and for the same reason as RedirectURI above. object **id** required integer format: int64 **createdAt** required string format: date-time nullable **origin** required string **clientId** required integer format: int64 #### Example generated ```json { "client": { "id": 1, "createdAt": "2026-04-15T12:00:00Z", "updatedAt": "2026-04-15T12:00:00Z", "clientIdentifier": "example", "description": "example", "websiteUrl": "example", "displayName": "example", "enabled": true, "consentRequired": true, "createdViaDcr": true, "administrativeScopesAllowed": true, "showLogo": true, "showDisplayName": true, "showDescription": true, "showWebsiteUrl": true, "isPublic": true, "isSystemLevelClient": true, "authorizationCodeEnabled": true, "clientCredentialsEnabled": true, "pkceRequired": true, "implicitGrantEnabled": true, "resourceOwnerPasswordCredentialsEnabled": true, "tokenExpirationInSeconds": 1, "refreshTokenOfflineIdleTimeoutInSeconds": 1, "refreshTokenOfflineMaxLifetimeInSeconds": 1, "includeOpenIDConnectClaimsInAccessToken": "example", "includeOpenIDConnectClaimsInIdToken": "example", "defaultAcrLevel": "example", "redirectURIs": [ { "id": 1, "createdAt": "2026-04-15T12:00:00Z", "uri": "example", "clientId": 1 } ], "webOrigins": [ { "id": 1, "createdAt": "2026-04-15T12:00:00Z", "origin": "example", "clientId": 1 } ] } } ``` ### 400 Invalid request body or parameters application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge when token transport is malformed; ordinary validation errors omit it. ### 401 Missing or invalid bearer token application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on token refusals. An operation may also return a handler-level 401 without this header. ### 403 The token is valid but does not authorise this request. Four checks answer here, and a caller distinguishes them by error_code. INSUFFICIENT_SCOPE comes from the route’s authorisation middleware, before the handler runs: an Admin operation needs one of the scopes its route names, and an Account operation needs `authserver:manage-account`. USER_CONTEXT_REQUIRED comes from the same middleware on the Account operations, which also require the token to have been issued for a user: one obtained through the client credentials grant carries no auth_time claim and is refused here. FORBIDDEN comes from the handler, on the account endpoints that resolve a resource by id and then check it belongs to the caller, so a valid token for the wrong user reaches this rather than a 404. MANAGE_SCOPE_REQUIRED comes from the handler, on the Admin operations the administrative policy guards. Only `authserver:manage` creates an administrator, changes one, or changes what reaches one, so no granular scope will ever be enough: the remedy is a token holding `authserver:manage`, not requesting the route’s scope as for INSUFFICIENT_SCOPE. An administrator is a user, group or client holding any of the six administrative permissions (`manage`, `admin-read`, `manage-users`, `manage-clients`, `manage-settings`, `browser-sessions`), a user directly or through any of its groups, the admin console’s own client whatever it holds, and a client allowed to request the administrative scopes. A granular scope is refused here when it: - grants or revokes an administrative permission, directly or by moving a user into or out of a group that holds one, or deletes such a group; - writes to an administrator user (profile, credentials, enabled, sessions, consents, attributes, memberships, permissions, deletion), an administrative group (settings, attributes, members, permissions, deletion), or an administrator client (settings, authentication, flows, redirect URIs, web origins, token settings, permissions, logo, deletion); - switches whether a client may request the administrative scopes, on or off, on any client; - reads an administrator client’s secret; - changes the email or the audit-log settings; - changes an administrative permission’s description. Granular scopes still read administrators. The refusal is answered after the request’s 400 and 404, writes nothing, carries `WWW-Authenticate: Bearer realm="goiabada", error="insufficient_scope", error_description="...", scope="authserver:manage"`, and leaves an `administrator_change_refused` audit record. Every operation this document secures declares a 403. The public client logo endpoint, which carries `security: []` and authorises nothing, is the one that does not. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on scope and administrative-policy refusals; other forbidden responses omit it. ### 404 Resource not found application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` ### 500 The request failed inside the server, most often a storage error. error_code is INTERNAL_SERVER_ERROR: no detail about the failure, only the request id an operator can find the log line by, and retrying is reasonable. Key rotation’s 500 also answers KEY_SET_INCOMPLETE, which names its cause and no retry clears. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` # Get client permissions Source: https://goiabada.dev/reference/api/admin/operations/getclientpermissions/ GET /api/v1/admin/clients/{id}/permissions Shell / cURL ```sh curl --request GET \ --url http://localhost:9090/api/v1/admin/clients/1/permissions \ --header 'Authorization: Bearer ' ``` - Auth server base URL{baseUrl}/api/v1/admin/clients/{id}/permissions Get permissions assigned to a client ## Authorizations - **[BearerAuth](https://goiabada.dev/reference/api/admin/#bearerauth)** ## Parameters ### Path Parameters **id** required integer format: int64 Client ID ## Responses ### 200 Client permissions application/json object **client** required object **id** required integer format: int64 **createdAt** required string format: date-time nullable **updatedAt** required string format: date-time nullable **clientIdentifier** required string **description** required string **websiteUrl** required The client’s website URL string **displayName** required A user-friendly name for the client string **enabled** required boolean **consentRequired** required boolean **createdViaDcr** required Read-only. True when the client registered itself through /connect/register boolean **administrativeScopesAllowed** required Read-only. True when the client may request the administrative authserver scopes on a user’s behalf; always true for the admin console’s own client. Switched through PUT /api/v1/admin/clients/{id}/administrative-scopes, with authserver:manage alone boolean **showLogo** required When true, the client logo is displayed on sign-in and consent screens boolean **showDisplayName** required When true, the display name is shown instead of the client identifier boolean **showDescription** required When true, the client description is displayed on sign-in and consent screens boolean **showWebsiteUrl** required When true, the client website URL is displayed on sign-in and consent screens boolean **isPublic** required boolean **isSystemLevelClient** required boolean **authorizationCodeEnabled** required boolean **clientCredentialsEnabled** required boolean **pkceRequired** required Null inherits the global setting, true requires PKCE, false makes it optional. Ignored for public clients, which always require PKCE boolean nullable **implicitGrantEnabled** required Null inherits the global setting, true enables the implicit flow, false disables it. Deprecated in OAuth 2.1 boolean nullable **resourceOwnerPasswordCredentialsEnabled** required Null inherits the global setting, true enables grant_type=password (RFC 6749 section 4.3), false disables it. Deprecated in OAuth 2.1 boolean nullable **tokenExpirationInSeconds** required integer **refreshTokenOfflineIdleTimeoutInSeconds** required integer **refreshTokenOfflineMaxLifetimeInSeconds** required integer **includeOpenIDConnectClaimsInAccessToken** required string **includeOpenIDConnectClaimsInIdToken** required string **defaultAcrLevel** required string **redirectURIs** required Null when this collection was not loaded for the response. Array\ nullable The `redirectURIs` entries of ClientResponse. The schema name is unchanged, but the shape below is not what releases up to and including v1.6.0 put on the wire: this position carried the persistence row directly, so its keys arrived capitalised and its `createdAt` arrived as a two-field `{"Time":...,"Valid":...}` object. The keys below are the ones every published contract has always declared. object **id** required integer format: int64 **createdAt** required string format: date-time nullable **uri** required string **clientId** required integer format: int64 **webOrigins** required Null when this collection was not loaded or the client has no web origins. Array\ nullable The `webOrigins` entries of ClientResponse. Its keys moved in the same release and for the same reason as RedirectURI above. object **id** required integer format: int64 **createdAt** required string format: date-time nullable **origin** required string **clientId** required integer format: int64 **permissions** required Null when the client has no permissions. Array\ nullable object **id** required integer format: int64 **permissionIdentifier** required string **description** required string **resourceId** required integer format: int64 **resource** required object **id** required integer format: int64 **resourceIdentifier** required string **description** required string **isSystemLevelResource** required True for a resource this server owns and enforces as system level: rename and delete are refused on it with 403. A consumer should disable those controls rather than recompute the rule. boolean #### Example generated ```json { "client": { "id": 1, "createdAt": "2026-04-15T12:00:00Z", "updatedAt": "2026-04-15T12:00:00Z", "clientIdentifier": "example", "description": "example", "websiteUrl": "example", "displayName": "example", "enabled": true, "consentRequired": true, "createdViaDcr": true, "administrativeScopesAllowed": true, "showLogo": true, "showDisplayName": true, "showDescription": true, "showWebsiteUrl": true, "isPublic": true, "isSystemLevelClient": true, "authorizationCodeEnabled": true, "clientCredentialsEnabled": true, "pkceRequired": true, "implicitGrantEnabled": true, "resourceOwnerPasswordCredentialsEnabled": true, "tokenExpirationInSeconds": 1, "refreshTokenOfflineIdleTimeoutInSeconds": 1, "refreshTokenOfflineMaxLifetimeInSeconds": 1, "includeOpenIDConnectClaimsInAccessToken": "example", "includeOpenIDConnectClaimsInIdToken": "example", "defaultAcrLevel": "example", "redirectURIs": [ { "id": 1, "createdAt": "2026-04-15T12:00:00Z", "uri": "example", "clientId": 1 } ], "webOrigins": [ { "id": 1, "createdAt": "2026-04-15T12:00:00Z", "origin": "example", "clientId": 1 } ] }, "permissions": [ { "id": 1, "permissionIdentifier": "example", "description": "example", "resourceId": 1, "resource": { "id": 1, "resourceIdentifier": "example", "description": "example", "isSystemLevelResource": true } } ] } ``` ### 400 Invalid request body or parameters application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge when token transport is malformed; ordinary validation errors omit it. ### 401 Missing or invalid bearer token application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on token refusals. An operation may also return a handler-level 401 without this header. ### 403 The token is valid but does not authorise this request. Four checks answer here, and a caller distinguishes them by error_code. INSUFFICIENT_SCOPE comes from the route’s authorisation middleware, before the handler runs: an Admin operation needs one of the scopes its route names, and an Account operation needs `authserver:manage-account`. USER_CONTEXT_REQUIRED comes from the same middleware on the Account operations, which also require the token to have been issued for a user: one obtained through the client credentials grant carries no auth_time claim and is refused here. FORBIDDEN comes from the handler, on the account endpoints that resolve a resource by id and then check it belongs to the caller, so a valid token for the wrong user reaches this rather than a 404. MANAGE_SCOPE_REQUIRED comes from the handler, on the Admin operations the administrative policy guards. Only `authserver:manage` creates an administrator, changes one, or changes what reaches one, so no granular scope will ever be enough: the remedy is a token holding `authserver:manage`, not requesting the route’s scope as for INSUFFICIENT_SCOPE. An administrator is a user, group or client holding any of the six administrative permissions (`manage`, `admin-read`, `manage-users`, `manage-clients`, `manage-settings`, `browser-sessions`), a user directly or through any of its groups, the admin console’s own client whatever it holds, and a client allowed to request the administrative scopes. A granular scope is refused here when it: - grants or revokes an administrative permission, directly or by moving a user into or out of a group that holds one, or deletes such a group; - writes to an administrator user (profile, credentials, enabled, sessions, consents, attributes, memberships, permissions, deletion), an administrative group (settings, attributes, members, permissions, deletion), or an administrator client (settings, authentication, flows, redirect URIs, web origins, token settings, permissions, logo, deletion); - switches whether a client may request the administrative scopes, on or off, on any client; - reads an administrator client’s secret; - changes the email or the audit-log settings; - changes an administrative permission’s description. Granular scopes still read administrators. The refusal is answered after the request’s 400 and 404, writes nothing, carries `WWW-Authenticate: Bearer realm="goiabada", error="insufficient_scope", error_description="...", scope="authserver:manage"`, and leaves an `administrator_change_refused` audit record. Every operation this document secures declares a 403. The public client logo endpoint, which carries `security: []` and authorises nothing, is the one that does not. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on scope and administrative-policy refusals; other forbidden responses omit it. ### 404 Resource not found application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` ### 500 The request failed inside the server, most often a storage error. error_code is INTERNAL_SERVER_ERROR: no detail about the failure, only the request id an operator can find the log line by, and retrying is reasonable. Key rotation’s 500 also answers KEY_SET_INCOMPLETE, which names its cause and no retry clears. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` # Set client permissions Source: https://goiabada.dev/reference/api/admin/operations/updateclientpermissions/ PUT /api/v1/admin/clients/{id}/permissions Shell / cURL ```sh curl --request PUT \ --url http://localhost:9090/api/v1/admin/clients/1/permissions \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "permissionIds": [ 1 ], "expectedPermissionIds": [ 1 ] }' ``` - Auth server base URL{baseUrl}/api/v1/admin/clients/{id}/permissions Replace all permissions for a client ## Authorizations - **[BearerAuth](https://goiabada.dev/reference/api/admin/#bearerauth)** ## Parameters ### Path Parameters **id** required integer format: int64 Client ID ## Request Bodyrequired application/json object **permissionIds** Array\ **expectedPermissionIds** required The client’s permission ids as last read; \[] if there were none. Array\ ### Example generated ```json { "permissionIds": [ 1 ], "expectedPermissionIds": [ 1 ] } ``` ## Responses ### 200 Permissions updated application/json object **success** required boolean #### Example generated ```json { "success": true } ``` ### 400 Invalid request body or parameters application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge when token transport is malformed; ordinary validation errors omit it. ### 401 Missing or invalid bearer token application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on token refusals. An operation may also return a handler-level 401 without this header. ### 403 The token is valid but does not authorise this request. Four checks answer here, and a caller distinguishes them by error_code. INSUFFICIENT_SCOPE comes from the route’s authorisation middleware, before the handler runs: an Admin operation needs one of the scopes its route names, and an Account operation needs `authserver:manage-account`. USER_CONTEXT_REQUIRED comes from the same middleware on the Account operations, which also require the token to have been issued for a user: one obtained through the client credentials grant carries no auth_time claim and is refused here. FORBIDDEN comes from the handler, on the account endpoints that resolve a resource by id and then check it belongs to the caller, so a valid token for the wrong user reaches this rather than a 404. MANAGE_SCOPE_REQUIRED comes from the handler, on the Admin operations the administrative policy guards. Only `authserver:manage` creates an administrator, changes one, or changes what reaches one, so no granular scope will ever be enough: the remedy is a token holding `authserver:manage`, not requesting the route’s scope as for INSUFFICIENT_SCOPE. An administrator is a user, group or client holding any of the six administrative permissions (`manage`, `admin-read`, `manage-users`, `manage-clients`, `manage-settings`, `browser-sessions`), a user directly or through any of its groups, the admin console’s own client whatever it holds, and a client allowed to request the administrative scopes. A granular scope is refused here when it: - grants or revokes an administrative permission, directly or by moving a user into or out of a group that holds one, or deletes such a group; - writes to an administrator user (profile, credentials, enabled, sessions, consents, attributes, memberships, permissions, deletion), an administrative group (settings, attributes, members, permissions, deletion), or an administrator client (settings, authentication, flows, redirect URIs, web origins, token settings, permissions, logo, deletion); - switches whether a client may request the administrative scopes, on or off, on any client; - reads an administrator client’s secret; - changes the email or the audit-log settings; - changes an administrative permission’s description. Granular scopes still read administrators. The refusal is answered after the request’s 400 and 404, writes nothing, carries `WWW-Authenticate: Bearer realm="goiabada", error="insufficient_scope", error_description="...", scope="authserver:manage"`, and leaves an `administrator_change_refused` audit record. Every operation this document secures declares a 403. The public client logo endpoint, which carries `security: []` and authorises nothing, is the one that does not. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on scope and administrative-policy refusals; other forbidden responses omit it. ### 404 Resource not found application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` ### 409 Another save changed the client’s permissions after expectedPermissionIds was read. error_code is CONCURRENT_UPDATE; nothing was saved. Read them again and retry. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` ### 500 The request failed inside the server, most often a storage error. error_code is INTERNAL_SERVER_ERROR: no detail about the failure, only the request id an operator can find the log line by, and retrying is reasonable. Key rotation’s 500 also answers KEY_SET_INCOMPLETE, which names its cause and no retry clears. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` # Switch whether a client may request the administrative scopes Source: https://goiabada.dev/reference/api/admin/operations/updateclientadministrativescopes/ PUT /api/v1/admin/clients/{id}/administrative-scopes Shell / cURL ```sh curl --request PUT \ --url http://localhost:9090/api/v1/admin/clients/1/administrative-scopes \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "allowed": true }' ``` - Auth server base URL{baseUrl}/api/v1/admin/clients/{id}/administrative-scopes Switch whether the client may request the six administrative `authserver` scopes (`manage`, `admin-read`, `manage-users`, `manage-clients`, `manage-settings` and `browser-sessions`) on a user’s behalf, through the authorization code, implicit, refresh token and password grants. A client that is not allowed is answered invalid_scope when it asks for one, or invalid_grant when it redeems a code or refreshes. A new client starts not allowed; the admin console’s own client is always allowed. The route admits `authserver:manage-clients` and `authserver:manage`, and the switch itself is reserved to `authserver:manage`, either way and on any client: `authserver:manage-clients` is refused with `MANAGE_SCOPE_REQUIRED`. An allowed client is an administrator client, so every other write to it, and reading its secret, is reserved to `authserver:manage` too. `allowed` is required: a body without it, or with null, is refused with `VALIDATION_ERROR` rather than read as false. Switching the admin console’s own client off is refused with `VALIDATION_ERROR` too. Every switch, one to the value already stored included, leaves an `updated_client_administrative_scopes` audit record naming the client, the new value and the caller. The answer is the client as it now is, with `administrativeScopesAllowed` holding the new value. No other operation sets or clears that field. ## Authorizations - **[BearerAuth](https://goiabada.dev/reference/api/admin/#bearerauth)** ## Parameters ### Path Parameters **id** required integer format: int64 Client ID ## Request Bodyrequired application/json object **allowed** required Whether the client may request the administrative authserver scopes. Required; false on the admin console’s own client is refused boolean ### Example generated ```json { "allowed": true } ``` ## Responses ### 200 Allowance switched application/json object **client** required object **id** required integer format: int64 **createdAt** required string format: date-time nullable **updatedAt** required string format: date-time nullable **clientIdentifier** required string **description** required string **websiteUrl** required The client’s website URL string **displayName** required A user-friendly name for the client string **enabled** required boolean **consentRequired** required boolean **createdViaDcr** required Read-only. True when the client registered itself through /connect/register boolean **administrativeScopesAllowed** required Read-only. True when the client may request the administrative authserver scopes on a user’s behalf; always true for the admin console’s own client. Switched through PUT /api/v1/admin/clients/{id}/administrative-scopes, with authserver:manage alone boolean **showLogo** required When true, the client logo is displayed on sign-in and consent screens boolean **showDisplayName** required When true, the display name is shown instead of the client identifier boolean **showDescription** required When true, the client description is displayed on sign-in and consent screens boolean **showWebsiteUrl** required When true, the client website URL is displayed on sign-in and consent screens boolean **isPublic** required boolean **isSystemLevelClient** required boolean **authorizationCodeEnabled** required boolean **clientCredentialsEnabled** required boolean **pkceRequired** required Null inherits the global setting, true requires PKCE, false makes it optional. Ignored for public clients, which always require PKCE boolean nullable **implicitGrantEnabled** required Null inherits the global setting, true enables the implicit flow, false disables it. Deprecated in OAuth 2.1 boolean nullable **resourceOwnerPasswordCredentialsEnabled** required Null inherits the global setting, true enables grant_type=password (RFC 6749 section 4.3), false disables it. Deprecated in OAuth 2.1 boolean nullable **tokenExpirationInSeconds** required integer **refreshTokenOfflineIdleTimeoutInSeconds** required integer **refreshTokenOfflineMaxLifetimeInSeconds** required integer **includeOpenIDConnectClaimsInAccessToken** required string **includeOpenIDConnectClaimsInIdToken** required string **defaultAcrLevel** required string **redirectURIs** required Null when this collection was not loaded for the response. Array\ nullable The `redirectURIs` entries of ClientResponse. The schema name is unchanged, but the shape below is not what releases up to and including v1.6.0 put on the wire: this position carried the persistence row directly, so its keys arrived capitalised and its `createdAt` arrived as a two-field `{"Time":...,"Valid":...}` object. The keys below are the ones every published contract has always declared. object **id** required integer format: int64 **createdAt** required string format: date-time nullable **uri** required string **clientId** required integer format: int64 **webOrigins** required Null when this collection was not loaded or the client has no web origins. Array\ nullable The `webOrigins` entries of ClientResponse. Its keys moved in the same release and for the same reason as RedirectURI above. object **id** required integer format: int64 **createdAt** required string format: date-time nullable **origin** required string **clientId** required integer format: int64 #### Example generated ```json { "client": { "id": 1, "createdAt": "2026-04-15T12:00:00Z", "updatedAt": "2026-04-15T12:00:00Z", "clientIdentifier": "example", "description": "example", "websiteUrl": "example", "displayName": "example", "enabled": true, "consentRequired": true, "createdViaDcr": true, "administrativeScopesAllowed": true, "showLogo": true, "showDisplayName": true, "showDescription": true, "showWebsiteUrl": true, "isPublic": true, "isSystemLevelClient": true, "authorizationCodeEnabled": true, "clientCredentialsEnabled": true, "pkceRequired": true, "implicitGrantEnabled": true, "resourceOwnerPasswordCredentialsEnabled": true, "tokenExpirationInSeconds": 1, "refreshTokenOfflineIdleTimeoutInSeconds": 1, "refreshTokenOfflineMaxLifetimeInSeconds": 1, "includeOpenIDConnectClaimsInAccessToken": "example", "includeOpenIDConnectClaimsInIdToken": "example", "defaultAcrLevel": "example", "redirectURIs": [ { "id": 1, "createdAt": "2026-04-15T12:00:00Z", "uri": "example", "clientId": 1 } ], "webOrigins": [ { "id": 1, "createdAt": "2026-04-15T12:00:00Z", "origin": "example", "clientId": 1 } ] } } ``` ### 400 Invalid request body or parameters application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge when token transport is malformed; ordinary validation errors omit it. ### 401 Missing or invalid bearer token application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on token refusals. An operation may also return a handler-level 401 without this header. ### 403 The token is valid but does not authorise this request. Four checks answer here, and a caller distinguishes them by error_code. INSUFFICIENT_SCOPE comes from the route’s authorisation middleware, before the handler runs: an Admin operation needs one of the scopes its route names, and an Account operation needs `authserver:manage-account`. USER_CONTEXT_REQUIRED comes from the same middleware on the Account operations, which also require the token to have been issued for a user: one obtained through the client credentials grant carries no auth_time claim and is refused here. FORBIDDEN comes from the handler, on the account endpoints that resolve a resource by id and then check it belongs to the caller, so a valid token for the wrong user reaches this rather than a 404. MANAGE_SCOPE_REQUIRED comes from the handler, on the Admin operations the administrative policy guards. Only `authserver:manage` creates an administrator, changes one, or changes what reaches one, so no granular scope will ever be enough: the remedy is a token holding `authserver:manage`, not requesting the route’s scope as for INSUFFICIENT_SCOPE. An administrator is a user, group or client holding any of the six administrative permissions (`manage`, `admin-read`, `manage-users`, `manage-clients`, `manage-settings`, `browser-sessions`), a user directly or through any of its groups, the admin console’s own client whatever it holds, and a client allowed to request the administrative scopes. A granular scope is refused here when it: - grants or revokes an administrative permission, directly or by moving a user into or out of a group that holds one, or deletes such a group; - writes to an administrator user (profile, credentials, enabled, sessions, consents, attributes, memberships, permissions, deletion), an administrative group (settings, attributes, members, permissions, deletion), or an administrator client (settings, authentication, flows, redirect URIs, web origins, token settings, permissions, logo, deletion); - switches whether a client may request the administrative scopes, on or off, on any client; - reads an administrator client’s secret; - changes the email or the audit-log settings; - changes an administrative permission’s description. Granular scopes still read administrators. The refusal is answered after the request’s 400 and 404, writes nothing, carries `WWW-Authenticate: Bearer realm="goiabada", error="insufficient_scope", error_description="...", scope="authserver:manage"`, and leaves an `administrator_change_refused` audit record. Every operation this document secures declares a 403. The public client logo endpoint, which carries `security: []` and authorises nothing, is the one that does not. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on scope and administrative-policy refusals; other forbidden responses omit it. ### 404 Resource not found application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` ### 500 The request failed inside the server, most often a storage error. error_code is INTERNAL_SERVER_ERROR: no detail about the failure, only the request id an operator can find the log line by, and retrying is reasonable. Key rotation’s 500 also answers KEY_SET_INCOMPLETE, which names its cause and no retry clears. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` # List client sessions Source: https://goiabada.dev/reference/api/admin/operations/getclientsessions/ GET /api/v1/admin/clients/{id}/sessions Shell / cURL ```sh curl --request GET \ --url 'http://localhost:9090/api/v1/admin/clients/1/sessions?page=1&size=50' \ --header 'Authorization: Bearer ' ``` - Auth server base URL{baseUrl}/api/v1/admin/clients/{id}/sessions Get a page of the user sessions associated with a client, together with the people they belong to. Sessions that have passed the configured idle timeout or maximum lifetime are left out, so the list is the live ones. A session’s `userId` resolves against the normalized `users` array, where a person holding several sessions appears once. Those records carry the name parts and the email and nothing else, because this route is reached with the clients scopes alone. ## Authorizations - **[BearerAuth](https://goiabada.dev/reference/api/admin/#bearerauth)** ## Parameters ### Path Parameters **id** required integer format: int64 Client ID ### Query Parameters **page** integer default: 1 Page number (default 1) **size** integer default: 50 <= 100 Results per page (default 50, capped at 100) ## Responses ### 200 Client sessions application/json object **sessions** required Array object **id** required integer format: int64 **createdAt** required string format: date-time nullable **updatedAt** required string format: date-time nullable **sessionIdentifier** required string **started** required string format: date-time nullable **lastAccessed** required string format: date-time nullable **authMethods** required string **acrLevel** required string **authTime** required string format: date-time nullable **ipAddress** required string **deviceName** required string **deviceType** required string **deviceOS** required string **userAgent** required string **userId** required integer format: int64 **isCurrent** required boolean **clientIdentifiers** required Array\ **users** required The owners of the sessions listed, normalized: a user holding several sessions appears once. Every session’s userId is a key into this array. Array\ Who a listed session belongs to: the name parts and the email, which is what a session page shows. Not a user profile, because the client sessions endpoint is reached with the clients scopes alone. object **id** required integer format: int64 **email** required string **givenName** required string **middleName** required string **familyName** required string #### Example generated ```json { "sessions": [ { "id": 1, "createdAt": "2026-04-15T12:00:00Z", "updatedAt": "2026-04-15T12:00:00Z", "sessionIdentifier": "example", "started": "2026-04-15T12:00:00Z", "lastAccessed": "2026-04-15T12:00:00Z", "authMethods": "example", "acrLevel": "example", "authTime": "2026-04-15T12:00:00Z", "ipAddress": "example", "deviceName": "example", "deviceType": "example", "deviceOS": "example", "userAgent": "example", "userId": 1, "isCurrent": true, "clientIdentifiers": [ "example" ] } ], "users": [ { "id": 1, "email": "example", "givenName": "example", "middleName": "example", "familyName": "example" } ] } ``` ### 400 Invalid request body or parameters application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge when token transport is malformed; ordinary validation errors omit it. ### 401 Missing or invalid bearer token application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on token refusals. An operation may also return a handler-level 401 without this header. ### 403 The token is valid but does not authorise this request. Four checks answer here, and a caller distinguishes them by error_code. INSUFFICIENT_SCOPE comes from the route’s authorisation middleware, before the handler runs: an Admin operation needs one of the scopes its route names, and an Account operation needs `authserver:manage-account`. USER_CONTEXT_REQUIRED comes from the same middleware on the Account operations, which also require the token to have been issued for a user: one obtained through the client credentials grant carries no auth_time claim and is refused here. FORBIDDEN comes from the handler, on the account endpoints that resolve a resource by id and then check it belongs to the caller, so a valid token for the wrong user reaches this rather than a 404. MANAGE_SCOPE_REQUIRED comes from the handler, on the Admin operations the administrative policy guards. Only `authserver:manage` creates an administrator, changes one, or changes what reaches one, so no granular scope will ever be enough: the remedy is a token holding `authserver:manage`, not requesting the route’s scope as for INSUFFICIENT_SCOPE. An administrator is a user, group or client holding any of the six administrative permissions (`manage`, `admin-read`, `manage-users`, `manage-clients`, `manage-settings`, `browser-sessions`), a user directly or through any of its groups, the admin console’s own client whatever it holds, and a client allowed to request the administrative scopes. A granular scope is refused here when it: - grants or revokes an administrative permission, directly or by moving a user into or out of a group that holds one, or deletes such a group; - writes to an administrator user (profile, credentials, enabled, sessions, consents, attributes, memberships, permissions, deletion), an administrative group (settings, attributes, members, permissions, deletion), or an administrator client (settings, authentication, flows, redirect URIs, web origins, token settings, permissions, logo, deletion); - switches whether a client may request the administrative scopes, on or off, on any client; - reads an administrator client’s secret; - changes the email or the audit-log settings; - changes an administrative permission’s description. Granular scopes still read administrators. The refusal is answered after the request’s 400 and 404, writes nothing, carries `WWW-Authenticate: Bearer realm="goiabada", error="insufficient_scope", error_description="...", scope="authserver:manage"`, and leaves an `administrator_change_refused` audit record. Every operation this document secures declares a 403. The public client logo endpoint, which carries `security: []` and authorises nothing, is the one that does not. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on scope and administrative-policy refusals; other forbidden responses omit it. ### 404 Resource not found application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` ### 500 The request failed inside the server, most often a storage error. error_code is INTERNAL_SERVER_ERROR: no detail about the failure, only the request id an operator can find the log line by, and retrying is reasonable. Key rotation’s 500 also answers KEY_SET_INCOMPLETE, which names its cause and no retry clears. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` # Get client logo info Source: https://goiabada.dev/reference/api/admin/operations/getclientlogoinfo/ GET /api/v1/admin/clients/{id}/logo Shell / cURL ```sh curl --request GET \ --url http://localhost:9090/api/v1/admin/clients/1/logo \ --header 'Authorization: Bearer ' ``` - Auth server base URL{baseUrl}/api/v1/admin/clients/{id}/logo Check if a client has a logo and get the logo URL ## Authorizations - **[BearerAuth](https://goiabada.dev/reference/api/admin/#bearerauth)** ## Parameters ### Path Parameters **id** required integer format: int64 Client ID ## Responses ### 200 Client logo info application/json object **hasLogo** required boolean **logoUrl** Public URL to serve the logo image (only present when hasLogo is true) string #### Example generated ```json { "hasLogo": true, "logoUrl": "example" } ``` ### 400 Invalid request body or parameters application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge when token transport is malformed; ordinary validation errors omit it. ### 401 Missing or invalid bearer token application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on token refusals. An operation may also return a handler-level 401 without this header. ### 403 The token is valid but does not authorise this request. Four checks answer here, and a caller distinguishes them by error_code. INSUFFICIENT_SCOPE comes from the route’s authorisation middleware, before the handler runs: an Admin operation needs one of the scopes its route names, and an Account operation needs `authserver:manage-account`. USER_CONTEXT_REQUIRED comes from the same middleware on the Account operations, which also require the token to have been issued for a user: one obtained through the client credentials grant carries no auth_time claim and is refused here. FORBIDDEN comes from the handler, on the account endpoints that resolve a resource by id and then check it belongs to the caller, so a valid token for the wrong user reaches this rather than a 404. MANAGE_SCOPE_REQUIRED comes from the handler, on the Admin operations the administrative policy guards. Only `authserver:manage` creates an administrator, changes one, or changes what reaches one, so no granular scope will ever be enough: the remedy is a token holding `authserver:manage`, not requesting the route’s scope as for INSUFFICIENT_SCOPE. An administrator is a user, group or client holding any of the six administrative permissions (`manage`, `admin-read`, `manage-users`, `manage-clients`, `manage-settings`, `browser-sessions`), a user directly or through any of its groups, the admin console’s own client whatever it holds, and a client allowed to request the administrative scopes. A granular scope is refused here when it: - grants or revokes an administrative permission, directly or by moving a user into or out of a group that holds one, or deletes such a group; - writes to an administrator user (profile, credentials, enabled, sessions, consents, attributes, memberships, permissions, deletion), an administrative group (settings, attributes, members, permissions, deletion), or an administrator client (settings, authentication, flows, redirect URIs, web origins, token settings, permissions, logo, deletion); - switches whether a client may request the administrative scopes, on or off, on any client; - reads an administrator client’s secret; - changes the email or the audit-log settings; - changes an administrative permission’s description. Granular scopes still read administrators. The refusal is answered after the request’s 400 and 404, writes nothing, carries `WWW-Authenticate: Bearer realm="goiabada", error="insufficient_scope", error_description="...", scope="authserver:manage"`, and leaves an `administrator_change_refused` audit record. Every operation this document secures declares a 403. The public client logo endpoint, which carries `security: []` and authorises nothing, is the one that does not. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on scope and administrative-policy refusals; other forbidden responses omit it. ### 404 Resource not found application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` ### 500 The request failed inside the server, most often a storage error. error_code is INTERNAL_SERVER_ERROR: no detail about the failure, only the request id an operator can find the log line by, and retrying is reasonable. Key rotation’s 500 also answers KEY_SET_INCOMPLETE, which names its cause and no retry clears. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` # Upload client logo Source: https://goiabada.dev/reference/api/admin/operations/uploadclientlogo/ POST /api/v1/admin/clients/{id}/logo Shell / cURL ```sh curl --request POST \ --url http://localhost:9090/api/v1/admin/clients/1/logo \ --header 'Authorization: Bearer ' \ --header 'Content-Type: multipart/form-data' \ --form picture=@file ``` - Auth server base URL{baseUrl}/api/v1/admin/clients/{id}/logo Upload or replace the client logo image. Send it as `multipart/form-data` in the field `picture`: a JPEG, PNG, GIF or WebP image, from 10x10 to 512x512 pixels, of at most `GOIABADA_PROFILE_PICTURE_MAX_SIZE_BYTES` bytes, 3 MiB by default. The limit is the one profile pictures have. ## Authorizations - **[BearerAuth](https://goiabada.dev/reference/api/admin/#bearerauth)** ## Parameters ### Path Parameters **id** required integer format: int64 Client ID ## Request Bodyrequired multipart/form-data object **picture** required Logo image file (JPEG, PNG, GIF, or WebP), up to the configured maximum size string format: binary ## Responses ### 200 Logo uploaded successfully application/json object **success** required boolean **pictureUrl** required Public URL to the uploaded logo string #### Example generated ```json { "success": true, "pictureUrl": "example" } ``` ### 400 Invalid request body or parameters application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge when token transport is malformed; ordinary validation errors omit it. ### 401 Missing or invalid bearer token application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on token refusals. An operation may also return a handler-level 401 without this header. ### 403 The token is valid but does not authorise this request. Four checks answer here, and a caller distinguishes them by error_code. INSUFFICIENT_SCOPE comes from the route’s authorisation middleware, before the handler runs: an Admin operation needs one of the scopes its route names, and an Account operation needs `authserver:manage-account`. USER_CONTEXT_REQUIRED comes from the same middleware on the Account operations, which also require the token to have been issued for a user: one obtained through the client credentials grant carries no auth_time claim and is refused here. FORBIDDEN comes from the handler, on the account endpoints that resolve a resource by id and then check it belongs to the caller, so a valid token for the wrong user reaches this rather than a 404. MANAGE_SCOPE_REQUIRED comes from the handler, on the Admin operations the administrative policy guards. Only `authserver:manage` creates an administrator, changes one, or changes what reaches one, so no granular scope will ever be enough: the remedy is a token holding `authserver:manage`, not requesting the route’s scope as for INSUFFICIENT_SCOPE. An administrator is a user, group or client holding any of the six administrative permissions (`manage`, `admin-read`, `manage-users`, `manage-clients`, `manage-settings`, `browser-sessions`), a user directly or through any of its groups, the admin console’s own client whatever it holds, and a client allowed to request the administrative scopes. A granular scope is refused here when it: - grants or revokes an administrative permission, directly or by moving a user into or out of a group that holds one, or deletes such a group; - writes to an administrator user (profile, credentials, enabled, sessions, consents, attributes, memberships, permissions, deletion), an administrative group (settings, attributes, members, permissions, deletion), or an administrator client (settings, authentication, flows, redirect URIs, web origins, token settings, permissions, logo, deletion); - switches whether a client may request the administrative scopes, on or off, on any client; - reads an administrator client’s secret; - changes the email or the audit-log settings; - changes an administrative permission’s description. Granular scopes still read administrators. The refusal is answered after the request’s 400 and 404, writes nothing, carries `WWW-Authenticate: Bearer realm="goiabada", error="insufficient_scope", error_description="...", scope="authserver:manage"`, and leaves an `administrator_change_refused` audit record. Every operation this document secures declares a 403. The public client logo endpoint, which carries `security: []` and authorises nothing, is the one that does not. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on scope and administrative-policy refusals; other forbidden responses omit it. ### 404 Resource not found application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` ### 500 The request failed inside the server, most often a storage error. error_code is INTERNAL_SERVER_ERROR: no detail about the failure, only the request id an operator can find the log line by, and retrying is reasonable. Key rotation’s 500 also answers KEY_SET_INCOMPLETE, which names its cause and no retry clears. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` # Delete client logo Source: https://goiabada.dev/reference/api/admin/operations/deleteclientlogo/ DELETE /api/v1/admin/clients/{id}/logo Shell / cURL ```sh curl --request DELETE \ --url http://localhost:9090/api/v1/admin/clients/1/logo \ --header 'Authorization: Bearer ' ``` - Auth server base URL{baseUrl}/api/v1/admin/clients/{id}/logo Remove the client logo ## Authorizations - **[BearerAuth](https://goiabada.dev/reference/api/admin/#bearerauth)** ## Parameters ### Path Parameters **id** required integer format: int64 Client ID ## Responses ### 200 Logo deleted application/json object **success** required boolean #### Example generated ```json { "success": true } ``` ### 400 Invalid request body or parameters application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge when token transport is malformed; ordinary validation errors omit it. ### 401 Missing or invalid bearer token application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on token refusals. An operation may also return a handler-level 401 without this header. ### 403 The token is valid but does not authorise this request. Four checks answer here, and a caller distinguishes them by error_code. INSUFFICIENT_SCOPE comes from the route’s authorisation middleware, before the handler runs: an Admin operation needs one of the scopes its route names, and an Account operation needs `authserver:manage-account`. USER_CONTEXT_REQUIRED comes from the same middleware on the Account operations, which also require the token to have been issued for a user: one obtained through the client credentials grant carries no auth_time claim and is refused here. FORBIDDEN comes from the handler, on the account endpoints that resolve a resource by id and then check it belongs to the caller, so a valid token for the wrong user reaches this rather than a 404. MANAGE_SCOPE_REQUIRED comes from the handler, on the Admin operations the administrative policy guards. Only `authserver:manage` creates an administrator, changes one, or changes what reaches one, so no granular scope will ever be enough: the remedy is a token holding `authserver:manage`, not requesting the route’s scope as for INSUFFICIENT_SCOPE. An administrator is a user, group or client holding any of the six administrative permissions (`manage`, `admin-read`, `manage-users`, `manage-clients`, `manage-settings`, `browser-sessions`), a user directly or through any of its groups, the admin console’s own client whatever it holds, and a client allowed to request the administrative scopes. A granular scope is refused here when it: - grants or revokes an administrative permission, directly or by moving a user into or out of a group that holds one, or deletes such a group; - writes to an administrator user (profile, credentials, enabled, sessions, consents, attributes, memberships, permissions, deletion), an administrative group (settings, attributes, members, permissions, deletion), or an administrator client (settings, authentication, flows, redirect URIs, web origins, token settings, permissions, logo, deletion); - switches whether a client may request the administrative scopes, on or off, on any client; - reads an administrator client’s secret; - changes the email or the audit-log settings; - changes an administrative permission’s description. Granular scopes still read administrators. The refusal is answered after the request’s 400 and 404, writes nothing, carries `WWW-Authenticate: Bearer realm="goiabada", error="insufficient_scope", error_description="...", scope="authserver:manage"`, and leaves an `administrator_change_refused` audit record. Every operation this document secures declares a 403. The public client logo endpoint, which carries `security: []` and authorises nothing, is the one that does not. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on scope and administrative-policy refusals; other forbidden responses omit it. ### 404 Resource not found application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` ### 500 The request failed inside the server, most often a storage error. error_code is INTERNAL_SERVER_ERROR: no detail about the failure, only the request id an operator can find the log line by, and retrying is reasonable. Key rotation’s 500 also answers KEY_SET_INCOMPLETE, which names its cause and no retry clears. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` # Get client logo image Source: https://goiabada.dev/reference/api/admin/operations/getclientlogoimage/ GET /client/logo/{clientIdentifier} Shell / cURL ```sh curl --request GET \ --url http://localhost:9090/client/logo/example ``` - Auth server base URL{baseUrl}/client/logo/{clientIdentifier} Serve the client logo image. This is a public endpoint that does not require authentication. Supports ETag-based caching. ## Parameters ### Path Parameters **clientIdentifier** required string The OAuth2 client identifier ### Header Parameters **If-None-Match** string Return 304 when this matches the logo’s current ETag. Weak tags and \* are accepted. ## Responses ### 200 Logo image image/\* string format: binary #### Headers **ETag** string Content hash for caching **Cache-Control** string Public, max-age=300, must-revalidate ### 304 Not modified (ETag match) #### Headers **ETag** string Current content hash **Cache-Control** string Public, max-age=300, must-revalidate ### 404 Client or logo not found ### 500 The request failed inside the server. Unlike every other failure on this endpoint, the body is the server’s HTML error page rather than the JSON error envelope, because this handler renders the shared error template. text/html string # Overview Source: https://goiabada.dev/reference/api/admin/operations/tags/audit-logs/ ## Audit Logs Audit log query (Admin API) ## Operations GET [/api/v1/admin/audit-logs](https://goiabada.dev/reference/api/admin/operations/getauditlogs/) GET [/api/v1/admin/audit-logs/event-types](https://goiabada.dev/reference/api/admin/operations/getauditeventtypes/) # Query audit logs Source: https://goiabada.dev/reference/api/admin/operations/getauditlogs/ GET /api/v1/admin/audit-logs Shell / cURL ```sh curl --request GET \ --url 'http://localhost:9090/api/v1/admin/audit-logs?page=1&size=20' \ --header 'Authorization: Bearer ' ``` - Auth server base URL{baseUrl}/api/v1/admin/audit-logs Get a page of audit log entries, most recent first, optionally narrowed to a single audit event, a single request id, or both. Entries are only present when audit logging to the database is enabled. ## Authorizations - **[BearerAuth](https://goiabada.dev/reference/api/admin/#bearerauth)** ## Parameters ### Query Parameters **auditEvent** string Return only entries for this audit event, matched exactly. The events are those `getAuditEventTypes` lists; a value outside them answers an empty page rather than an error. **requestId** string Return only entries carrying this request id, matched exactly **page** integer default: 1 Page number (default 1) **size** integer default: 20 <= 200 Results per page. A value outside 1 to 200 is not clamped to the nearest bound, it is discarded and the default of 20 is used, so a caller asking for 500 gets 20 rather than 200. ## Responses ### 200 Audit log entries application/json object **auditLogs** required Array\ object **id** required integer format: int64 **createdAt** required string format: date-time **auditEvent** required string **details** required The event payload, as a JSON string string **requestId** required The request’s id as the application log carries it, so the same value appears in the log’s request_id attribute. Empty when the entry was not written on a request. string **total** required integer **page** required integer **size** required integer #### Example generated ```json { "auditLogs": [ { "id": 1, "createdAt": "2026-04-15T12:00:00Z", "auditEvent": "example", "details": "example", "requestId": "example" } ], "total": 1, "page": 1, "size": 1 } ``` ### 401 Missing or invalid bearer token application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on token refusals. An operation may also return a handler-level 401 without this header. ### 403 The token is valid but does not authorise this request. Four checks answer here, and a caller distinguishes them by error_code. INSUFFICIENT_SCOPE comes from the route’s authorisation middleware, before the handler runs: an Admin operation needs one of the scopes its route names, and an Account operation needs `authserver:manage-account`. USER_CONTEXT_REQUIRED comes from the same middleware on the Account operations, which also require the token to have been issued for a user: one obtained through the client credentials grant carries no auth_time claim and is refused here. FORBIDDEN comes from the handler, on the account endpoints that resolve a resource by id and then check it belongs to the caller, so a valid token for the wrong user reaches this rather than a 404. MANAGE_SCOPE_REQUIRED comes from the handler, on the Admin operations the administrative policy guards. Only `authserver:manage` creates an administrator, changes one, or changes what reaches one, so no granular scope will ever be enough: the remedy is a token holding `authserver:manage`, not requesting the route’s scope as for INSUFFICIENT_SCOPE. An administrator is a user, group or client holding any of the six administrative permissions (`manage`, `admin-read`, `manage-users`, `manage-clients`, `manage-settings`, `browser-sessions`), a user directly or through any of its groups, the admin console’s own client whatever it holds, and a client allowed to request the administrative scopes. A granular scope is refused here when it: - grants or revokes an administrative permission, directly or by moving a user into or out of a group that holds one, or deletes such a group; - writes to an administrator user (profile, credentials, enabled, sessions, consents, attributes, memberships, permissions, deletion), an administrative group (settings, attributes, members, permissions, deletion), or an administrator client (settings, authentication, flows, redirect URIs, web origins, token settings, permissions, logo, deletion); - switches whether a client may request the administrative scopes, on or off, on any client; - reads an administrator client’s secret; - changes the email or the audit-log settings; - changes an administrative permission’s description. Granular scopes still read administrators. The refusal is answered after the request’s 400 and 404, writes nothing, carries `WWW-Authenticate: Bearer realm="goiabada", error="insufficient_scope", error_description="...", scope="authserver:manage"`, and leaves an `administrator_change_refused` audit record. Every operation this document secures declares a 403. The public client logo endpoint, which carries `security: []` and authorises nothing, is the one that does not. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on scope and administrative-policy refusals; other forbidden responses omit it. ### 500 The request failed inside the server, most often a storage error. error_code is INTERNAL_SERVER_ERROR: no detail about the failure, only the request id an operator can find the log line by, and retrying is reasonable. Key rotation’s 500 also answers KEY_SET_INCOMPLETE, which names its cause and no retry clears. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` # Get audit event types Source: https://goiabada.dev/reference/api/admin/operations/getauditeventtypes/ GET /api/v1/admin/audit-logs/event-types Shell / cURL ```sh curl --request GET \ --url http://localhost:9090/api/v1/admin/audit-logs/event-types \ --header 'Authorization: Bearer ' ``` - Auth server base URL{baseUrl}/api/v1/admin/audit-logs/event-types Get the catalog of audit event names this server can write, which is the set the auditEvent filter on GET /api/v1/admin/audit-logs accepts. The list is fixed at build time, not derived from the rows present, so it includes events that have not yet occurred on this deployment. The names are sorted by identifier. ## Authorizations - **[BearerAuth](https://goiabada.dev/reference/api/admin/#bearerauth)** ## Responses ### 200 Audit event types application/json object **auditEventTypes** required Array\ #### Example generated ```json { "auditEventTypes": [ "example" ] } ``` ### 401 Missing or invalid bearer token application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on token refusals. An operation may also return a handler-level 401 without this header. ### 403 The token is valid but does not authorise this request. Four checks answer here, and a caller distinguishes them by error_code. INSUFFICIENT_SCOPE comes from the route’s authorisation middleware, before the handler runs: an Admin operation needs one of the scopes its route names, and an Account operation needs `authserver:manage-account`. USER_CONTEXT_REQUIRED comes from the same middleware on the Account operations, which also require the token to have been issued for a user: one obtained through the client credentials grant carries no auth_time claim and is refused here. FORBIDDEN comes from the handler, on the account endpoints that resolve a resource by id and then check it belongs to the caller, so a valid token for the wrong user reaches this rather than a 404. MANAGE_SCOPE_REQUIRED comes from the handler, on the Admin operations the administrative policy guards. Only `authserver:manage` creates an administrator, changes one, or changes what reaches one, so no granular scope will ever be enough: the remedy is a token holding `authserver:manage`, not requesting the route’s scope as for INSUFFICIENT_SCOPE. An administrator is a user, group or client holding any of the six administrative permissions (`manage`, `admin-read`, `manage-users`, `manage-clients`, `manage-settings`, `browser-sessions`), a user directly or through any of its groups, the admin console’s own client whatever it holds, and a client allowed to request the administrative scopes. A granular scope is refused here when it: - grants or revokes an administrative permission, directly or by moving a user into or out of a group that holds one, or deletes such a group; - writes to an administrator user (profile, credentials, enabled, sessions, consents, attributes, memberships, permissions, deletion), an administrative group (settings, attributes, members, permissions, deletion), or an administrator client (settings, authentication, flows, redirect URIs, web origins, token settings, permissions, logo, deletion); - switches whether a client may request the administrative scopes, on or off, on any client; - reads an administrator client’s secret; - changes the email or the audit-log settings; - changes an administrative permission’s description. Granular scopes still read administrators. The refusal is answered after the request’s 400 and 404, writes nothing, carries `WWW-Authenticate: Bearer realm="goiabada", error="insufficient_scope", error_description="...", scope="authserver:manage"`, and leaves an `administrator_change_refused` audit record. Every operation this document secures declares a 403. The public client logo endpoint, which carries `security: []` and authorises nothing, is the one that does not. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on scope and administrative-policy refusals; other forbidden responses omit it. ### 500 The request failed inside the server, most often a storage error. error_code is INTERNAL_SERVER_ERROR: no detail about the failure, only the request id an operator can find the log line by, and retrying is reasonable. Key rotation’s 500 also answers KEY_SET_INCOMPLETE, which names its cause and no retry clears. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` # Overview Source: https://goiabada.dev/reference/api/admin/operations/tags/settings/ ## Settings System settings (Admin API) ## Operations GET [/api/v1/admin/settings/general](https://goiabada.dev/reference/api/admin/operations/getsettingsgeneral/) PUT [/api/v1/admin/settings/general](https://goiabada.dev/reference/api/admin/operations/updatesettingsgeneral/) GET [/api/v1/admin/settings/email](https://goiabada.dev/reference/api/admin/operations/getsettingsemail/) PUT [/api/v1/admin/settings/email](https://goiabada.dev/reference/api/admin/operations/updatesettingsemail/) POST [/api/v1/admin/settings/email/send-test](https://goiabada.dev/reference/api/admin/operations/sendtestemail/) GET [/api/v1/admin/settings/sessions](https://goiabada.dev/reference/api/admin/operations/getsettingssessions/) PUT [/api/v1/admin/settings/sessions](https://goiabada.dev/reference/api/admin/operations/updatesettingssessions/) GET [/api/v1/admin/settings/ui-theme](https://goiabada.dev/reference/api/admin/operations/getsettingsuitheme/) PUT [/api/v1/admin/settings/ui-theme](https://goiabada.dev/reference/api/admin/operations/updatesettingsuitheme/) GET [/api/v1/admin/settings/tokens](https://goiabada.dev/reference/api/admin/operations/getsettingstokens/) PUT [/api/v1/admin/settings/tokens](https://goiabada.dev/reference/api/admin/operations/updatesettingstokens/) GET [/api/v1/admin/settings/audit-logs](https://goiabada.dev/reference/api/admin/operations/getsettingsauditlogs/) PUT [/api/v1/admin/settings/audit-logs](https://goiabada.dev/reference/api/admin/operations/updatesettingsauditlogs/) GET [/api/v1/admin/settings/keys](https://goiabada.dev/reference/api/admin/operations/getsettingskeys/) POST [/api/v1/admin/settings/keys/rotate](https://goiabada.dev/reference/api/admin/operations/rotatesettingskeys/) DELETE [/api/v1/admin/settings/keys/{id}](https://goiabada.dev/reference/api/admin/operations/deletesettingskey/) GET [/api/v1/admin/phone-countries](https://goiabada.dev/reference/api/admin/operations/getphonecountries/) # Get general settings Source: https://goiabada.dev/reference/api/admin/operations/getsettingsgeneral/ GET /api/v1/admin/settings/general Shell / cURL ```sh curl --request GET \ --url http://localhost:9090/api/v1/admin/settings/general \ --header 'Authorization: Bearer ' ``` - Auth server base URL{baseUrl}/api/v1/admin/settings/general Get general system settings ## Authorizations - **[BearerAuth](https://goiabada.dev/reference/api/admin/#bearerauth)** ## Responses ### 200 General settings application/json object **appName** required string **issuer** required string **selfRegistrationEnabled** required boolean **selfRegistrationRequiresEmailVerification** required boolean **dynamicClientRegistrationEnabled** required boolean **passwordPolicy** required string **pkceRequired** required When true, confidential clients must use PKCE. Public clients always require PKCE regardless of this setting boolean **implicitFlowEnabled** required When true, the implicit flow is enabled server-wide. Deprecated in OAuth 2.1 boolean **resourceOwnerPasswordCredentialsEnabled** required When true, grant_type=password is enabled server-wide (RFC 6749 section 4.3). Deprecated in OAuth 2.1 boolean #### Example generated ```json { "appName": "example", "issuer": "example", "selfRegistrationEnabled": true, "selfRegistrationRequiresEmailVerification": true, "dynamicClientRegistrationEnabled": true, "passwordPolicy": "example", "pkceRequired": true, "implicitFlowEnabled": true, "resourceOwnerPasswordCredentialsEnabled": true } ``` ### 401 Missing or invalid bearer token application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on token refusals. An operation may also return a handler-level 401 without this header. ### 403 The token is valid but does not authorise this request. Four checks answer here, and a caller distinguishes them by error_code. INSUFFICIENT_SCOPE comes from the route’s authorisation middleware, before the handler runs: an Admin operation needs one of the scopes its route names, and an Account operation needs `authserver:manage-account`. USER_CONTEXT_REQUIRED comes from the same middleware on the Account operations, which also require the token to have been issued for a user: one obtained through the client credentials grant carries no auth_time claim and is refused here. FORBIDDEN comes from the handler, on the account endpoints that resolve a resource by id and then check it belongs to the caller, so a valid token for the wrong user reaches this rather than a 404. MANAGE_SCOPE_REQUIRED comes from the handler, on the Admin operations the administrative policy guards. Only `authserver:manage` creates an administrator, changes one, or changes what reaches one, so no granular scope will ever be enough: the remedy is a token holding `authserver:manage`, not requesting the route’s scope as for INSUFFICIENT_SCOPE. An administrator is a user, group or client holding any of the six administrative permissions (`manage`, `admin-read`, `manage-users`, `manage-clients`, `manage-settings`, `browser-sessions`), a user directly or through any of its groups, the admin console’s own client whatever it holds, and a client allowed to request the administrative scopes. A granular scope is refused here when it: - grants or revokes an administrative permission, directly or by moving a user into or out of a group that holds one, or deletes such a group; - writes to an administrator user (profile, credentials, enabled, sessions, consents, attributes, memberships, permissions, deletion), an administrative group (settings, attributes, members, permissions, deletion), or an administrator client (settings, authentication, flows, redirect URIs, web origins, token settings, permissions, logo, deletion); - switches whether a client may request the administrative scopes, on or off, on any client; - reads an administrator client’s secret; - changes the email or the audit-log settings; - changes an administrative permission’s description. Granular scopes still read administrators. The refusal is answered after the request’s 400 and 404, writes nothing, carries `WWW-Authenticate: Bearer realm="goiabada", error="insufficient_scope", error_description="...", scope="authserver:manage"`, and leaves an `administrator_change_refused` audit record. Every operation this document secures declares a 403. The public client logo endpoint, which carries `security: []` and authorises nothing, is the one that does not. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on scope and administrative-policy refusals; other forbidden responses omit it. ### 500 The request failed inside the server, most often a storage error. error_code is INTERNAL_SERVER_ERROR: no detail about the failure, only the request id an operator can find the log line by, and retrying is reasonable. Key rotation’s 500 also answers KEY_SET_INCOMPLETE, which names its cause and no retry clears. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` # Update general settings Source: https://goiabada.dev/reference/api/admin/operations/updatesettingsgeneral/ PUT /api/v1/admin/settings/general Shell / cURL ```sh curl --request PUT \ --url http://localhost:9090/api/v1/admin/settings/general \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "appName": "example", "issuer": "example", "selfRegistrationEnabled": true, "selfRegistrationRequiresEmailVerification": true, "dynamicClientRegistrationEnabled": true, "passwordPolicy": "example", "pkceRequired": true, "implicitFlowEnabled": true, "resourceOwnerPasswordCredentialsEnabled": true }' ``` - Auth server base URL{baseUrl}/api/v1/admin/settings/general Update general system settings ## Authorizations - **[BearerAuth](https://goiabada.dev/reference/api/admin/#bearerauth)** ## Request Bodyrequired application/json object **appName** string **issuer** string **selfRegistrationEnabled** boolean **selfRegistrationRequiresEmailVerification** boolean **dynamicClientRegistrationEnabled** boolean **passwordPolicy** string **pkceRequired** When true, confidential clients must use PKCE. Public clients always require PKCE regardless of this setting boolean **implicitFlowEnabled** When true, allows the implicit flow (response_type=token, id_token, id_token token). Deprecated in OAuth 2.1 boolean **resourceOwnerPasswordCredentialsEnabled** When true, allows grant_type=password at the token endpoint (RFC 6749 section 4.3). Deprecated in OAuth 2.1 boolean ### Example generated ```json { "appName": "example", "issuer": "example", "selfRegistrationEnabled": true, "selfRegistrationRequiresEmailVerification": true, "dynamicClientRegistrationEnabled": true, "passwordPolicy": "example", "pkceRequired": true, "implicitFlowEnabled": true, "resourceOwnerPasswordCredentialsEnabled": true } ``` ## Responses ### 200 Settings updated application/json object **appName** required string **issuer** required string **selfRegistrationEnabled** required boolean **selfRegistrationRequiresEmailVerification** required boolean **dynamicClientRegistrationEnabled** required boolean **passwordPolicy** required string **pkceRequired** required When true, confidential clients must use PKCE. Public clients always require PKCE regardless of this setting boolean **implicitFlowEnabled** required When true, the implicit flow is enabled server-wide. Deprecated in OAuth 2.1 boolean **resourceOwnerPasswordCredentialsEnabled** required When true, grant_type=password is enabled server-wide (RFC 6749 section 4.3). Deprecated in OAuth 2.1 boolean #### Example generated ```json { "appName": "example", "issuer": "example", "selfRegistrationEnabled": true, "selfRegistrationRequiresEmailVerification": true, "dynamicClientRegistrationEnabled": true, "passwordPolicy": "example", "pkceRequired": true, "implicitFlowEnabled": true, "resourceOwnerPasswordCredentialsEnabled": true } ``` ### 400 Invalid request body or parameters application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge when token transport is malformed; ordinary validation errors omit it. ### 401 Missing or invalid bearer token application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on token refusals. An operation may also return a handler-level 401 without this header. ### 403 The token is valid but does not authorise this request. Four checks answer here, and a caller distinguishes them by error_code. INSUFFICIENT_SCOPE comes from the route’s authorisation middleware, before the handler runs: an Admin operation needs one of the scopes its route names, and an Account operation needs `authserver:manage-account`. USER_CONTEXT_REQUIRED comes from the same middleware on the Account operations, which also require the token to have been issued for a user: one obtained through the client credentials grant carries no auth_time claim and is refused here. FORBIDDEN comes from the handler, on the account endpoints that resolve a resource by id and then check it belongs to the caller, so a valid token for the wrong user reaches this rather than a 404. MANAGE_SCOPE_REQUIRED comes from the handler, on the Admin operations the administrative policy guards. Only `authserver:manage` creates an administrator, changes one, or changes what reaches one, so no granular scope will ever be enough: the remedy is a token holding `authserver:manage`, not requesting the route’s scope as for INSUFFICIENT_SCOPE. An administrator is a user, group or client holding any of the six administrative permissions (`manage`, `admin-read`, `manage-users`, `manage-clients`, `manage-settings`, `browser-sessions`), a user directly or through any of its groups, the admin console’s own client whatever it holds, and a client allowed to request the administrative scopes. A granular scope is refused here when it: - grants or revokes an administrative permission, directly or by moving a user into or out of a group that holds one, or deletes such a group; - writes to an administrator user (profile, credentials, enabled, sessions, consents, attributes, memberships, permissions, deletion), an administrative group (settings, attributes, members, permissions, deletion), or an administrator client (settings, authentication, flows, redirect URIs, web origins, token settings, permissions, logo, deletion); - switches whether a client may request the administrative scopes, on or off, on any client; - reads an administrator client’s secret; - changes the email or the audit-log settings; - changes an administrative permission’s description. Granular scopes still read administrators. The refusal is answered after the request’s 400 and 404, writes nothing, carries `WWW-Authenticate: Bearer realm="goiabada", error="insufficient_scope", error_description="...", scope="authserver:manage"`, and leaves an `administrator_change_refused` audit record. Every operation this document secures declares a 403. The public client logo endpoint, which carries `security: []` and authorises nothing, is the one that does not. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on scope and administrative-policy refusals; other forbidden responses omit it. ### 500 The request failed inside the server, most often a storage error. error_code is INTERNAL_SERVER_ERROR: no detail about the failure, only the request id an operator can find the log line by, and retrying is reasonable. Key rotation’s 500 also answers KEY_SET_INCOMPLETE, which names its cause and no retry clears. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` # Get email settings Source: https://goiabada.dev/reference/api/admin/operations/getsettingsemail/ GET /api/v1/admin/settings/email Shell / cURL ```sh curl --request GET \ --url http://localhost:9090/api/v1/admin/settings/email \ --header 'Authorization: Bearer ' ``` - Auth server base URL{baseUrl}/api/v1/admin/settings/email Get SMTP/email settings. The SMTP password is never answered: `hasSmtpPassword` says whether one is stored. ## Authorizations - **[BearerAuth](https://goiabada.dev/reference/api/admin/#bearerauth)** ## Responses ### 200 Email settings application/json object **smtpEnabled** required boolean **smtpHost** required string **smtpPort** required integer **smtpUsername** required string **smtpEncryption** required string **smtpFromName** required string **smtpFromEmail** required string **hasSmtpPassword** required boolean #### Example generated ```json { "smtpEnabled": true, "smtpHost": "example", "smtpPort": 1, "smtpUsername": "example", "smtpEncryption": "example", "smtpFromName": "example", "smtpFromEmail": "example", "hasSmtpPassword": true } ``` ### 401 Missing or invalid bearer token application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on token refusals. An operation may also return a handler-level 401 without this header. ### 403 The token is valid but does not authorise this request. Four checks answer here, and a caller distinguishes them by error_code. INSUFFICIENT_SCOPE comes from the route’s authorisation middleware, before the handler runs: an Admin operation needs one of the scopes its route names, and an Account operation needs `authserver:manage-account`. USER_CONTEXT_REQUIRED comes from the same middleware on the Account operations, which also require the token to have been issued for a user: one obtained through the client credentials grant carries no auth_time claim and is refused here. FORBIDDEN comes from the handler, on the account endpoints that resolve a resource by id and then check it belongs to the caller, so a valid token for the wrong user reaches this rather than a 404. MANAGE_SCOPE_REQUIRED comes from the handler, on the Admin operations the administrative policy guards. Only `authserver:manage` creates an administrator, changes one, or changes what reaches one, so no granular scope will ever be enough: the remedy is a token holding `authserver:manage`, not requesting the route’s scope as for INSUFFICIENT_SCOPE. An administrator is a user, group or client holding any of the six administrative permissions (`manage`, `admin-read`, `manage-users`, `manage-clients`, `manage-settings`, `browser-sessions`), a user directly or through any of its groups, the admin console’s own client whatever it holds, and a client allowed to request the administrative scopes. A granular scope is refused here when it: - grants or revokes an administrative permission, directly or by moving a user into or out of a group that holds one, or deletes such a group; - writes to an administrator user (profile, credentials, enabled, sessions, consents, attributes, memberships, permissions, deletion), an administrative group (settings, attributes, members, permissions, deletion), or an administrator client (settings, authentication, flows, redirect URIs, web origins, token settings, permissions, logo, deletion); - switches whether a client may request the administrative scopes, on or off, on any client; - reads an administrator client’s secret; - changes the email or the audit-log settings; - changes an administrative permission’s description. Granular scopes still read administrators. The refusal is answered after the request’s 400 and 404, writes nothing, carries `WWW-Authenticate: Bearer realm="goiabada", error="insufficient_scope", error_description="...", scope="authserver:manage"`, and leaves an `administrator_change_refused` audit record. Every operation this document secures declares a 403. The public client logo endpoint, which carries `security: []` and authorises nothing, is the one that does not. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on scope and administrative-policy refusals; other forbidden responses omit it. ### 500 The request failed inside the server, most often a storage error. error_code is INTERNAL_SERVER_ERROR: no detail about the failure, only the request id an operator can find the log line by, and retrying is reasonable. Key rotation’s 500 also answers KEY_SET_INCOMPLETE, which names its cause and no retry clears. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` # Update email settings Source: https://goiabada.dev/reference/api/admin/operations/updatesettingsemail/ PUT /api/v1/admin/settings/email Shell / cURL ```sh curl --request PUT \ --url http://localhost:9090/api/v1/admin/settings/email \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "smtpEnabled": true, "smtpHost": "example", "smtpPort": 1, "smtpUsername": "example", "smtpPassword": "example", "clearSmtpPassword": false, "smtpEncryption": "example", "smtpFromName": "example", "smtpFromEmail": "example" }' ``` - Auth server base URL{baseUrl}/api/v1/admin/settings/email Update SMTP/email settings. A change needs `authserver:manage`: these settings decide where password reset links are sent, so `authserver:manage-settings` reads them but is refused a change with 403 MANAGE_SCOPE_REQUIRED. A save with `smtpEnabled: false` clears every SMTP field, the stored password included. The stored SMTP password is kept, replaced or removed: an absent or empty `smtpPassword` keeps it, a non-empty one replaces it, and `clearSmtpPassword: true` removes it. The two together are refused with 400 `VALIDATION_ERROR`. A stored password reaches only the host it was entered for: a save that changes `smtpHost` while a password is stored, carrying neither a new `smtpPassword` nor `clearSmtpPassword: true`, is refused with 400 `VALIDATION_ERROR` and writes nothing. The hosts are compared with surrounding space trimmed, one pair of brackets taken off an IPv6 literal, and without regard to case. A change of port, username, encryption or sender keeps the stored password. Once every other check has passed, a save with `smtpEnabled: true` opens a TCP connection to `smtpHost` on `smtpPort`, with a 3-second timeout. It only checks that something answers there: it does not speak SMTP, negotiate TLS or authenticate, which is what the send-test operation is for. A connection that fails is a 400; see that response. ## Authorizations - **[BearerAuth](https://goiabada.dev/reference/api/admin/#bearerauth)** ## Request Bodyrequired application/json object **smtpEnabled** boolean **smtpHost** string **smtpPort** integer **smtpUsername** string **smtpPassword** A new SMTP password, at most 256 bytes. The stored password is never returned, only `hasSmtpPassword`, so an absent or empty value keeps the stored password; a non-empty value replaces it. Sending one together with `clearSmtpPassword: true` is refused with 400 `VALIDATION_ERROR` and nothing is written. string **clearSmtpPassword** `true` removes the stored SMTP password. Removing it when none is stored is an ordinary save. Refused with 400 `VALIDATION_ERROR` together with a non-empty `smtpPassword`. Ignored when `smtpEnabled` is false, which clears every SMTP field, the password included. boolean **smtpEncryption** string **smtpFromName** string **smtpFromEmail** string ## Responses ### 200 Settings updated application/json object **smtpEnabled** required boolean **smtpHost** required string **smtpPort** required integer **smtpUsername** required string **smtpEncryption** required string **smtpFromName** required string **smtpFromEmail** required string **hasSmtpPassword** required boolean #### Example generated ```json { "smtpEnabled": true, "smtpHost": "example", "smtpPort": 1, "smtpUsername": "example", "smtpEncryption": "example", "smtpFromName": "example", "smtpFromEmail": "example", "hasSmtpPassword": true } ``` ### 400 Error_code is INVALID_REQUEST_BODY for a body that will not parse; a refused field answers VALIDATION_ERROR, or its catalog key, such as validator.email.invalid_format, when the refusal is localized; and a connection that fails answers VALIDATION_ERROR. Nothing is written. A failed connection answers one of these fixed messages, naming at most its coarse cause: - “Unable to connect to the SMTP server: host name not found.” Check the host name: it is misspelt, or the auth server cannot resolve it, or its resolver did not answer in time. The auth server’s resolver may not be the one your own machine uses. - “Unable to connect to the SMTP server: connection timed out.” Nothing answered within 3 seconds: check the port, and any firewall between the auth server and the SMTP server. - “Unable to connect to the SMTP server: connection refused.” The host answered but nothing listens on that port: check the port. - “Unable to connect to the SMTP server.” Any other failure, such as a network the auth server has no route to. No address, port or operating-system error is answered; the full error goes to the auth server’s log, at Warn, as “unable to connect to the smtp server” under the request’s request_id. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` ### 401 Missing or invalid bearer token application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on token refusals. An operation may also return a handler-level 401 without this header. ### 403 The token is valid but does not authorise this request. Four checks answer here, and a caller distinguishes them by error_code. INSUFFICIENT_SCOPE comes from the route’s authorisation middleware, before the handler runs: an Admin operation needs one of the scopes its route names, and an Account operation needs `authserver:manage-account`. USER_CONTEXT_REQUIRED comes from the same middleware on the Account operations, which also require the token to have been issued for a user: one obtained through the client credentials grant carries no auth_time claim and is refused here. FORBIDDEN comes from the handler, on the account endpoints that resolve a resource by id and then check it belongs to the caller, so a valid token for the wrong user reaches this rather than a 404. MANAGE_SCOPE_REQUIRED comes from the handler, on the Admin operations the administrative policy guards. Only `authserver:manage` creates an administrator, changes one, or changes what reaches one, so no granular scope will ever be enough: the remedy is a token holding `authserver:manage`, not requesting the route’s scope as for INSUFFICIENT_SCOPE. An administrator is a user, group or client holding any of the six administrative permissions (`manage`, `admin-read`, `manage-users`, `manage-clients`, `manage-settings`, `browser-sessions`), a user directly or through any of its groups, the admin console’s own client whatever it holds, and a client allowed to request the administrative scopes. A granular scope is refused here when it: - grants or revokes an administrative permission, directly or by moving a user into or out of a group that holds one, or deletes such a group; - writes to an administrator user (profile, credentials, enabled, sessions, consents, attributes, memberships, permissions, deletion), an administrative group (settings, attributes, members, permissions, deletion), or an administrator client (settings, authentication, flows, redirect URIs, web origins, token settings, permissions, logo, deletion); - switches whether a client may request the administrative scopes, on or off, on any client; - reads an administrator client’s secret; - changes the email or the audit-log settings; - changes an administrative permission’s description. Granular scopes still read administrators. The refusal is answered after the request’s 400 and 404, writes nothing, carries `WWW-Authenticate: Bearer realm="goiabada", error="insufficient_scope", error_description="...", scope="authserver:manage"`, and leaves an `administrator_change_refused` audit record. Every operation this document secures declares a 403. The public client logo endpoint, which carries `security: []` and authorises nothing, is the one that does not. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on scope and administrative-policy refusals; other forbidden responses omit it. ### 500 The request failed inside the server, most often a storage error. error_code is INTERNAL_SERVER_ERROR: no detail about the failure, only the request id an operator can find the log line by, and retrying is reasonable. Key rotation’s 500 also answers KEY_SET_INCOMPLETE, which names its cause and no retry clears. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` # Send test email Source: https://goiabada.dev/reference/api/admin/operations/sendtestemail/ POST /api/v1/admin/settings/email/send-test Shell / cURL ```sh curl --request POST \ --url http://localhost:9090/api/v1/admin/settings/email/send-test \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "to": "hello@example.com" }' ``` - Auth server base URL{baseUrl}/api/v1/admin/settings/email/send-test Send a short message to `to` through the stored SMTP settings, the stored password included, to check what a save’s connection does not: the encryption, the certificate, the credentials and the sender. Answers 200 once the SMTP server has accepted the message. The send waits up to 10 seconds to connect, including the TLS handshake for SSL/TLS, and up to 30 seconds for the rest of the conversation. ## Authorizations - **[BearerAuth](https://goiabada.dev/reference/api/admin/#bearerauth)** ## Request Bodyrequired application/json object **to** required string format: email ### Example generated ```json { "to": "hello@example.com" } ``` ## Responses ### 200 Test email sent application/json object **success** required boolean #### Example generated ```json { "success": true } ``` ### 400 Error_code is SMTP_NOT_ENABLED while SMTP is disabled, INVALID_REQUEST_BODY for a body that will not parse, VALIDATION_ERROR when `to` is missing, validator.email.invalid_format when it is not an email address, and SEND_FAILED when the send fails. A SEND_FAILED answers one of these fixed messages, chosen by what failed: - “Unable to send the test email: host name not found.” Check the host name, as for a save. - “Unable to send the test email: connection timed out.” Check the port and any firewall, as for a save. A server that accepted the connection and then went quiet also ends here, which is what STARTTLS or None against an SSL/TLS port such as 465 looks like. - “Unable to send the test email: connection refused.” Check the port. - “The SMTP server did not offer STARTTLS; set the encryption to None only if the server has no TLS.” The encryption is STARTTLS and the server does not offer it. Goiabada does not continue unencrypted. - “The SMTP server would receive the password unencrypted; set the encryption to STARTTLS or SSL/TLS.” The encryption is None, a username is set, and the server would be sent the password in the clear by PLAIN or LOGIN. Only a server on `localhost`, `127.0.0.1` or `::1` is exempt. - “SMTP credentials are configured but the server offers no authentication.” A username is set and the server offers no AUTH: the wrong host or port, or a server that offers authentication only after STARTTLS, reached with the encryption set to None. - “The SMTP server offers none of PLAIN, LOGIN or CRAM-MD5.” The server offers only mechanisms Goiabada does not implement, such as XOAUTH2: use a password the server accepts through one of these three, often called an app password or an SMTP key. - “The TLS connection failed; check the encryption setting and the server’s certificate.” SSL/TLS against a port that expects STARTTLS, such as 587; a certificate the auth server does not trust, that has expired, or that names a host other than `smtpHost`; or a server that refused the STARTTLS command. Certificates are always verified. - “The SMTP server rejected the username or password.” Check the username and password. - “The SMTP server refused the message.” The server refused the sender, the recipient or the message itself, most often because the account may not send from `smtpFromEmail`. - “Unable to send the test email.” Any other failure: a connection that dropped partway through the conversation, a network the auth server has no route to, or a stored password the auth server cannot decrypt, such as one encrypted under a `GOIABADA_AES_ENCRYPTION_KEY` it no longer has. Nothing from the error itself is answered, no address, operating-system error or server reply; the full error goes to the auth server’s log, at Warn, as “unable to send the test email” under the request’s request_id. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` ### 401 Missing or invalid bearer token application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on token refusals. An operation may also return a handler-level 401 without this header. ### 403 The token is valid but does not authorise this request. Four checks answer here, and a caller distinguishes them by error_code. INSUFFICIENT_SCOPE comes from the route’s authorisation middleware, before the handler runs: an Admin operation needs one of the scopes its route names, and an Account operation needs `authserver:manage-account`. USER_CONTEXT_REQUIRED comes from the same middleware on the Account operations, which also require the token to have been issued for a user: one obtained through the client credentials grant carries no auth_time claim and is refused here. FORBIDDEN comes from the handler, on the account endpoints that resolve a resource by id and then check it belongs to the caller, so a valid token for the wrong user reaches this rather than a 404. MANAGE_SCOPE_REQUIRED comes from the handler, on the Admin operations the administrative policy guards. Only `authserver:manage` creates an administrator, changes one, or changes what reaches one, so no granular scope will ever be enough: the remedy is a token holding `authserver:manage`, not requesting the route’s scope as for INSUFFICIENT_SCOPE. An administrator is a user, group or client holding any of the six administrative permissions (`manage`, `admin-read`, `manage-users`, `manage-clients`, `manage-settings`, `browser-sessions`), a user directly or through any of its groups, the admin console’s own client whatever it holds, and a client allowed to request the administrative scopes. A granular scope is refused here when it: - grants or revokes an administrative permission, directly or by moving a user into or out of a group that holds one, or deletes such a group; - writes to an administrator user (profile, credentials, enabled, sessions, consents, attributes, memberships, permissions, deletion), an administrative group (settings, attributes, members, permissions, deletion), or an administrator client (settings, authentication, flows, redirect URIs, web origins, token settings, permissions, logo, deletion); - switches whether a client may request the administrative scopes, on or off, on any client; - reads an administrator client’s secret; - changes the email or the audit-log settings; - changes an administrative permission’s description. Granular scopes still read administrators. The refusal is answered after the request’s 400 and 404, writes nothing, carries `WWW-Authenticate: Bearer realm="goiabada", error="insufficient_scope", error_description="...", scope="authserver:manage"`, and leaves an `administrator_change_refused` audit record. Every operation this document secures declares a 403. The public client logo endpoint, which carries `security: []` and authorises nothing, is the one that does not. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on scope and administrative-policy refusals; other forbidden responses omit it. ### 500 The request failed inside the server, most often a storage error. error_code is INTERNAL_SERVER_ERROR: no detail about the failure, only the request id an operator can find the log line by, and retrying is reasonable. Key rotation’s 500 also answers KEY_SET_INCOMPLETE, which names its cause and no retry clears. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` # Get session settings Source: https://goiabada.dev/reference/api/admin/operations/getsettingssessions/ GET /api/v1/admin/settings/sessions Shell / cURL ```sh curl --request GET \ --url http://localhost:9090/api/v1/admin/settings/sessions \ --header 'Authorization: Bearer ' ``` - Auth server base URL{baseUrl}/api/v1/admin/settings/sessions Get session timeout settings ## Authorizations - **[BearerAuth](https://goiabada.dev/reference/api/admin/#bearerauth)** ## Responses ### 200 Session settings application/json object **userSessionIdleTimeoutInSeconds** required integer **userSessionMaxLifetimeInSeconds** required integer #### Example generated ```json { "userSessionIdleTimeoutInSeconds": 1, "userSessionMaxLifetimeInSeconds": 1 } ``` ### 401 Missing or invalid bearer token application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on token refusals. An operation may also return a handler-level 401 without this header. ### 403 The token is valid but does not authorise this request. Four checks answer here, and a caller distinguishes them by error_code. INSUFFICIENT_SCOPE comes from the route’s authorisation middleware, before the handler runs: an Admin operation needs one of the scopes its route names, and an Account operation needs `authserver:manage-account`. USER_CONTEXT_REQUIRED comes from the same middleware on the Account operations, which also require the token to have been issued for a user: one obtained through the client credentials grant carries no auth_time claim and is refused here. FORBIDDEN comes from the handler, on the account endpoints that resolve a resource by id and then check it belongs to the caller, so a valid token for the wrong user reaches this rather than a 404. MANAGE_SCOPE_REQUIRED comes from the handler, on the Admin operations the administrative policy guards. Only `authserver:manage` creates an administrator, changes one, or changes what reaches one, so no granular scope will ever be enough: the remedy is a token holding `authserver:manage`, not requesting the route’s scope as for INSUFFICIENT_SCOPE. An administrator is a user, group or client holding any of the six administrative permissions (`manage`, `admin-read`, `manage-users`, `manage-clients`, `manage-settings`, `browser-sessions`), a user directly or through any of its groups, the admin console’s own client whatever it holds, and a client allowed to request the administrative scopes. A granular scope is refused here when it: - grants or revokes an administrative permission, directly or by moving a user into or out of a group that holds one, or deletes such a group; - writes to an administrator user (profile, credentials, enabled, sessions, consents, attributes, memberships, permissions, deletion), an administrative group (settings, attributes, members, permissions, deletion), or an administrator client (settings, authentication, flows, redirect URIs, web origins, token settings, permissions, logo, deletion); - switches whether a client may request the administrative scopes, on or off, on any client; - reads an administrator client’s secret; - changes the email or the audit-log settings; - changes an administrative permission’s description. Granular scopes still read administrators. The refusal is answered after the request’s 400 and 404, writes nothing, carries `WWW-Authenticate: Bearer realm="goiabada", error="insufficient_scope", error_description="...", scope="authserver:manage"`, and leaves an `administrator_change_refused` audit record. Every operation this document secures declares a 403. The public client logo endpoint, which carries `security: []` and authorises nothing, is the one that does not. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on scope and administrative-policy refusals; other forbidden responses omit it. ### 500 The request failed inside the server, most often a storage error. error_code is INTERNAL_SERVER_ERROR: no detail about the failure, only the request id an operator can find the log line by, and retrying is reasonable. Key rotation’s 500 also answers KEY_SET_INCOMPLETE, which names its cause and no retry clears. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` # Update session settings Source: https://goiabada.dev/reference/api/admin/operations/updatesettingssessions/ PUT /api/v1/admin/settings/sessions Shell / cURL ```sh curl --request PUT \ --url http://localhost:9090/api/v1/admin/settings/sessions \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "userSessionIdleTimeoutInSeconds": 1, "userSessionMaxLifetimeInSeconds": 1 }' ``` - Auth server base URL{baseUrl}/api/v1/admin/settings/sessions Update session timeout settings ## Authorizations - **[BearerAuth](https://goiabada.dev/reference/api/admin/#bearerauth)** ## Request Bodyrequired application/json object **userSessionIdleTimeoutInSeconds** integer **userSessionMaxLifetimeInSeconds** integer ### Example generated ```json { "userSessionIdleTimeoutInSeconds": 1, "userSessionMaxLifetimeInSeconds": 1 } ``` ## Responses ### 200 Settings updated application/json object **userSessionIdleTimeoutInSeconds** required integer **userSessionMaxLifetimeInSeconds** required integer #### Example generated ```json { "userSessionIdleTimeoutInSeconds": 1, "userSessionMaxLifetimeInSeconds": 1 } ``` ### 400 Invalid request body or parameters application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge when token transport is malformed; ordinary validation errors omit it. ### 401 Missing or invalid bearer token application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on token refusals. An operation may also return a handler-level 401 without this header. ### 403 The token is valid but does not authorise this request. Four checks answer here, and a caller distinguishes them by error_code. INSUFFICIENT_SCOPE comes from the route’s authorisation middleware, before the handler runs: an Admin operation needs one of the scopes its route names, and an Account operation needs `authserver:manage-account`. USER_CONTEXT_REQUIRED comes from the same middleware on the Account operations, which also require the token to have been issued for a user: one obtained through the client credentials grant carries no auth_time claim and is refused here. FORBIDDEN comes from the handler, on the account endpoints that resolve a resource by id and then check it belongs to the caller, so a valid token for the wrong user reaches this rather than a 404. MANAGE_SCOPE_REQUIRED comes from the handler, on the Admin operations the administrative policy guards. Only `authserver:manage` creates an administrator, changes one, or changes what reaches one, so no granular scope will ever be enough: the remedy is a token holding `authserver:manage`, not requesting the route’s scope as for INSUFFICIENT_SCOPE. An administrator is a user, group or client holding any of the six administrative permissions (`manage`, `admin-read`, `manage-users`, `manage-clients`, `manage-settings`, `browser-sessions`), a user directly or through any of its groups, the admin console’s own client whatever it holds, and a client allowed to request the administrative scopes. A granular scope is refused here when it: - grants or revokes an administrative permission, directly or by moving a user into or out of a group that holds one, or deletes such a group; - writes to an administrator user (profile, credentials, enabled, sessions, consents, attributes, memberships, permissions, deletion), an administrative group (settings, attributes, members, permissions, deletion), or an administrator client (settings, authentication, flows, redirect URIs, web origins, token settings, permissions, logo, deletion); - switches whether a client may request the administrative scopes, on or off, on any client; - reads an administrator client’s secret; - changes the email or the audit-log settings; - changes an administrative permission’s description. Granular scopes still read administrators. The refusal is answered after the request’s 400 and 404, writes nothing, carries `WWW-Authenticate: Bearer realm="goiabada", error="insufficient_scope", error_description="...", scope="authserver:manage"`, and leaves an `administrator_change_refused` audit record. Every operation this document secures declares a 403. The public client logo endpoint, which carries `security: []` and authorises nothing, is the one that does not. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on scope and administrative-policy refusals; other forbidden responses omit it. ### 500 The request failed inside the server, most often a storage error. error_code is INTERNAL_SERVER_ERROR: no detail about the failure, only the request id an operator can find the log line by, and retrying is reasonable. Key rotation’s 500 also answers KEY_SET_INCOMPLETE, which names its cause and no retry clears. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` # Get UI theme Source: https://goiabada.dev/reference/api/admin/operations/getsettingsuitheme/ GET /api/v1/admin/settings/ui-theme Shell / cURL ```sh curl --request GET \ --url http://localhost:9090/api/v1/admin/settings/ui-theme \ --header 'Authorization: Bearer ' ``` - Auth server base URL{baseUrl}/api/v1/admin/settings/ui-theme Get current UI theme and available themes ## Authorizations - **[BearerAuth](https://goiabada.dev/reference/api/admin/#bearerauth)** ## Responses ### 200 UI theme settings application/json object **uiTheme** required string **availableThemes** required Array\ #### Example generated ```json { "uiTheme": "example", "availableThemes": [ "example" ] } ``` ### 401 Missing or invalid bearer token application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on token refusals. An operation may also return a handler-level 401 without this header. ### 403 The token is valid but does not authorise this request. Four checks answer here, and a caller distinguishes them by error_code. INSUFFICIENT_SCOPE comes from the route’s authorisation middleware, before the handler runs: an Admin operation needs one of the scopes its route names, and an Account operation needs `authserver:manage-account`. USER_CONTEXT_REQUIRED comes from the same middleware on the Account operations, which also require the token to have been issued for a user: one obtained through the client credentials grant carries no auth_time claim and is refused here. FORBIDDEN comes from the handler, on the account endpoints that resolve a resource by id and then check it belongs to the caller, so a valid token for the wrong user reaches this rather than a 404. MANAGE_SCOPE_REQUIRED comes from the handler, on the Admin operations the administrative policy guards. Only `authserver:manage` creates an administrator, changes one, or changes what reaches one, so no granular scope will ever be enough: the remedy is a token holding `authserver:manage`, not requesting the route’s scope as for INSUFFICIENT_SCOPE. An administrator is a user, group or client holding any of the six administrative permissions (`manage`, `admin-read`, `manage-users`, `manage-clients`, `manage-settings`, `browser-sessions`), a user directly or through any of its groups, the admin console’s own client whatever it holds, and a client allowed to request the administrative scopes. A granular scope is refused here when it: - grants or revokes an administrative permission, directly or by moving a user into or out of a group that holds one, or deletes such a group; - writes to an administrator user (profile, credentials, enabled, sessions, consents, attributes, memberships, permissions, deletion), an administrative group (settings, attributes, members, permissions, deletion), or an administrator client (settings, authentication, flows, redirect URIs, web origins, token settings, permissions, logo, deletion); - switches whether a client may request the administrative scopes, on or off, on any client; - reads an administrator client’s secret; - changes the email or the audit-log settings; - changes an administrative permission’s description. Granular scopes still read administrators. The refusal is answered after the request’s 400 and 404, writes nothing, carries `WWW-Authenticate: Bearer realm="goiabada", error="insufficient_scope", error_description="...", scope="authserver:manage"`, and leaves an `administrator_change_refused` audit record. Every operation this document secures declares a 403. The public client logo endpoint, which carries `security: []` and authorises nothing, is the one that does not. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on scope and administrative-policy refusals; other forbidden responses omit it. ### 500 The request failed inside the server, most often a storage error. error_code is INTERNAL_SERVER_ERROR: no detail about the failure, only the request id an operator can find the log line by, and retrying is reasonable. Key rotation’s 500 also answers KEY_SET_INCOMPLETE, which names its cause and no retry clears. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` # Update UI theme Source: https://goiabada.dev/reference/api/admin/operations/updatesettingsuitheme/ PUT /api/v1/admin/settings/ui-theme Shell / cURL ```sh curl --request PUT \ --url http://localhost:9090/api/v1/admin/settings/ui-theme \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "uiTheme": "example" }' ``` - Auth server base URL{baseUrl}/api/v1/admin/settings/ui-theme Update UI theme ## Authorizations - **[BearerAuth](https://goiabada.dev/reference/api/admin/#bearerauth)** ## Request Bodyrequired application/json object **uiTheme** string ### Example generated ```json { "uiTheme": "example" } ``` ## Responses ### 200 Theme updated application/json object **uiTheme** required string **availableThemes** required Array\ #### Example generated ```json { "uiTheme": "example", "availableThemes": [ "example" ] } ``` ### 400 Invalid request body or parameters application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge when token transport is malformed; ordinary validation errors omit it. ### 401 Missing or invalid bearer token application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on token refusals. An operation may also return a handler-level 401 without this header. ### 403 The token is valid but does not authorise this request. Four checks answer here, and a caller distinguishes them by error_code. INSUFFICIENT_SCOPE comes from the route’s authorisation middleware, before the handler runs: an Admin operation needs one of the scopes its route names, and an Account operation needs `authserver:manage-account`. USER_CONTEXT_REQUIRED comes from the same middleware on the Account operations, which also require the token to have been issued for a user: one obtained through the client credentials grant carries no auth_time claim and is refused here. FORBIDDEN comes from the handler, on the account endpoints that resolve a resource by id and then check it belongs to the caller, so a valid token for the wrong user reaches this rather than a 404. MANAGE_SCOPE_REQUIRED comes from the handler, on the Admin operations the administrative policy guards. Only `authserver:manage` creates an administrator, changes one, or changes what reaches one, so no granular scope will ever be enough: the remedy is a token holding `authserver:manage`, not requesting the route’s scope as for INSUFFICIENT_SCOPE. An administrator is a user, group or client holding any of the six administrative permissions (`manage`, `admin-read`, `manage-users`, `manage-clients`, `manage-settings`, `browser-sessions`), a user directly or through any of its groups, the admin console’s own client whatever it holds, and a client allowed to request the administrative scopes. A granular scope is refused here when it: - grants or revokes an administrative permission, directly or by moving a user into or out of a group that holds one, or deletes such a group; - writes to an administrator user (profile, credentials, enabled, sessions, consents, attributes, memberships, permissions, deletion), an administrative group (settings, attributes, members, permissions, deletion), or an administrator client (settings, authentication, flows, redirect URIs, web origins, token settings, permissions, logo, deletion); - switches whether a client may request the administrative scopes, on or off, on any client; - reads an administrator client’s secret; - changes the email or the audit-log settings; - changes an administrative permission’s description. Granular scopes still read administrators. The refusal is answered after the request’s 400 and 404, writes nothing, carries `WWW-Authenticate: Bearer realm="goiabada", error="insufficient_scope", error_description="...", scope="authserver:manage"`, and leaves an `administrator_change_refused` audit record. Every operation this document secures declares a 403. The public client logo endpoint, which carries `security: []` and authorises nothing, is the one that does not. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on scope and administrative-policy refusals; other forbidden responses omit it. ### 500 The request failed inside the server, most often a storage error. error_code is INTERNAL_SERVER_ERROR: no detail about the failure, only the request id an operator can find the log line by, and retrying is reasonable. Key rotation’s 500 also answers KEY_SET_INCOMPLETE, which names its cause and no retry clears. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` # Get token settings Source: https://goiabada.dev/reference/api/admin/operations/getsettingstokens/ GET /api/v1/admin/settings/tokens Shell / cURL ```sh curl --request GET \ --url http://localhost:9090/api/v1/admin/settings/tokens \ --header 'Authorization: Bearer ' ``` - Auth server base URL{baseUrl}/api/v1/admin/settings/tokens Get default token expiration settings ## Authorizations - **[BearerAuth](https://goiabada.dev/reference/api/admin/#bearerauth)** ## Responses ### 200 Token settings application/json object **tokenExpirationInSeconds** required integer **refreshTokenOfflineIdleTimeoutInSeconds** required integer **refreshTokenOfflineMaxLifetimeInSeconds** required integer **includeOpenIDConnectClaimsInAccessToken** required boolean **includeOpenIDConnectClaimsInIdToken** required boolean #### Example generated ```json { "tokenExpirationInSeconds": 1, "refreshTokenOfflineIdleTimeoutInSeconds": 1, "refreshTokenOfflineMaxLifetimeInSeconds": 1, "includeOpenIDConnectClaimsInAccessToken": true, "includeOpenIDConnectClaimsInIdToken": true } ``` ### 401 Missing or invalid bearer token application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on token refusals. An operation may also return a handler-level 401 without this header. ### 403 The token is valid but does not authorise this request. Four checks answer here, and a caller distinguishes them by error_code. INSUFFICIENT_SCOPE comes from the route’s authorisation middleware, before the handler runs: an Admin operation needs one of the scopes its route names, and an Account operation needs `authserver:manage-account`. USER_CONTEXT_REQUIRED comes from the same middleware on the Account operations, which also require the token to have been issued for a user: one obtained through the client credentials grant carries no auth_time claim and is refused here. FORBIDDEN comes from the handler, on the account endpoints that resolve a resource by id and then check it belongs to the caller, so a valid token for the wrong user reaches this rather than a 404. MANAGE_SCOPE_REQUIRED comes from the handler, on the Admin operations the administrative policy guards. Only `authserver:manage` creates an administrator, changes one, or changes what reaches one, so no granular scope will ever be enough: the remedy is a token holding `authserver:manage`, not requesting the route’s scope as for INSUFFICIENT_SCOPE. An administrator is a user, group or client holding any of the six administrative permissions (`manage`, `admin-read`, `manage-users`, `manage-clients`, `manage-settings`, `browser-sessions`), a user directly or through any of its groups, the admin console’s own client whatever it holds, and a client allowed to request the administrative scopes. A granular scope is refused here when it: - grants or revokes an administrative permission, directly or by moving a user into or out of a group that holds one, or deletes such a group; - writes to an administrator user (profile, credentials, enabled, sessions, consents, attributes, memberships, permissions, deletion), an administrative group (settings, attributes, members, permissions, deletion), or an administrator client (settings, authentication, flows, redirect URIs, web origins, token settings, permissions, logo, deletion); - switches whether a client may request the administrative scopes, on or off, on any client; - reads an administrator client’s secret; - changes the email or the audit-log settings; - changes an administrative permission’s description. Granular scopes still read administrators. The refusal is answered after the request’s 400 and 404, writes nothing, carries `WWW-Authenticate: Bearer realm="goiabada", error="insufficient_scope", error_description="...", scope="authserver:manage"`, and leaves an `administrator_change_refused` audit record. Every operation this document secures declares a 403. The public client logo endpoint, which carries `security: []` and authorises nothing, is the one that does not. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on scope and administrative-policy refusals; other forbidden responses omit it. ### 500 The request failed inside the server, most often a storage error. error_code is INTERNAL_SERVER_ERROR: no detail about the failure, only the request id an operator can find the log line by, and retrying is reasonable. Key rotation’s 500 also answers KEY_SET_INCOMPLETE, which names its cause and no retry clears. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` # Update token settings Source: https://goiabada.dev/reference/api/admin/operations/updatesettingstokens/ PUT /api/v1/admin/settings/tokens Shell / cURL ```sh curl --request PUT \ --url http://localhost:9090/api/v1/admin/settings/tokens \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "tokenExpirationInSeconds": 1, "refreshTokenOfflineIdleTimeoutInSeconds": 1, "refreshTokenOfflineMaxLifetimeInSeconds": 1, "includeOpenIDConnectClaimsInAccessToken": true, "includeOpenIDConnectClaimsInIdToken": true }' ``` - Auth server base URL{baseUrl}/api/v1/admin/settings/tokens Update default token expiration settings ## Authorizations - **[BearerAuth](https://goiabada.dev/reference/api/admin/#bearerauth)** ## Request Bodyrequired application/json object **tokenExpirationInSeconds** integer **refreshTokenOfflineIdleTimeoutInSeconds** integer **refreshTokenOfflineMaxLifetimeInSeconds** integer **includeOpenIDConnectClaimsInAccessToken** boolean **includeOpenIDConnectClaimsInIdToken** boolean ### Example generated ```json { "tokenExpirationInSeconds": 1, "refreshTokenOfflineIdleTimeoutInSeconds": 1, "refreshTokenOfflineMaxLifetimeInSeconds": 1, "includeOpenIDConnectClaimsInAccessToken": true, "includeOpenIDConnectClaimsInIdToken": true } ``` ## Responses ### 200 Settings updated application/json object **tokenExpirationInSeconds** required integer **refreshTokenOfflineIdleTimeoutInSeconds** required integer **refreshTokenOfflineMaxLifetimeInSeconds** required integer **includeOpenIDConnectClaimsInAccessToken** required boolean **includeOpenIDConnectClaimsInIdToken** required boolean #### Example generated ```json { "tokenExpirationInSeconds": 1, "refreshTokenOfflineIdleTimeoutInSeconds": 1, "refreshTokenOfflineMaxLifetimeInSeconds": 1, "includeOpenIDConnectClaimsInAccessToken": true, "includeOpenIDConnectClaimsInIdToken": true } ``` ### 400 Invalid request body or parameters application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge when token transport is malformed; ordinary validation errors omit it. ### 401 Missing or invalid bearer token application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on token refusals. An operation may also return a handler-level 401 without this header. ### 403 The token is valid but does not authorise this request. Four checks answer here, and a caller distinguishes them by error_code. INSUFFICIENT_SCOPE comes from the route’s authorisation middleware, before the handler runs: an Admin operation needs one of the scopes its route names, and an Account operation needs `authserver:manage-account`. USER_CONTEXT_REQUIRED comes from the same middleware on the Account operations, which also require the token to have been issued for a user: one obtained through the client credentials grant carries no auth_time claim and is refused here. FORBIDDEN comes from the handler, on the account endpoints that resolve a resource by id and then check it belongs to the caller, so a valid token for the wrong user reaches this rather than a 404. MANAGE_SCOPE_REQUIRED comes from the handler, on the Admin operations the administrative policy guards. Only `authserver:manage` creates an administrator, changes one, or changes what reaches one, so no granular scope will ever be enough: the remedy is a token holding `authserver:manage`, not requesting the route’s scope as for INSUFFICIENT_SCOPE. An administrator is a user, group or client holding any of the six administrative permissions (`manage`, `admin-read`, `manage-users`, `manage-clients`, `manage-settings`, `browser-sessions`), a user directly or through any of its groups, the admin console’s own client whatever it holds, and a client allowed to request the administrative scopes. A granular scope is refused here when it: - grants or revokes an administrative permission, directly or by moving a user into or out of a group that holds one, or deletes such a group; - writes to an administrator user (profile, credentials, enabled, sessions, consents, attributes, memberships, permissions, deletion), an administrative group (settings, attributes, members, permissions, deletion), or an administrator client (settings, authentication, flows, redirect URIs, web origins, token settings, permissions, logo, deletion); - switches whether a client may request the administrative scopes, on or off, on any client; - reads an administrator client’s secret; - changes the email or the audit-log settings; - changes an administrative permission’s description. Granular scopes still read administrators. The refusal is answered after the request’s 400 and 404, writes nothing, carries `WWW-Authenticate: Bearer realm="goiabada", error="insufficient_scope", error_description="...", scope="authserver:manage"`, and leaves an `administrator_change_refused` audit record. Every operation this document secures declares a 403. The public client logo endpoint, which carries `security: []` and authorises nothing, is the one that does not. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on scope and administrative-policy refusals; other forbidden responses omit it. ### 500 The request failed inside the server, most often a storage error. error_code is INTERNAL_SERVER_ERROR: no detail about the failure, only the request id an operator can find the log line by, and retrying is reasonable. Key rotation’s 500 also answers KEY_SET_INCOMPLETE, which names its cause and no retry clears. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` # Get audit log settings Source: https://goiabada.dev/reference/api/admin/operations/getsettingsauditlogs/ GET /api/v1/admin/settings/audit-logs Shell / cURL ```sh curl --request GET \ --url http://localhost:9090/api/v1/admin/settings/audit-logs \ --header 'Authorization: Bearer ' ``` - Auth server base URL{baseUrl}/api/v1/admin/settings/audit-logs Get audit log destinations and retention ## Authorizations - **[BearerAuth](https://goiabada.dev/reference/api/admin/#bearerauth)** ## Responses ### 200 Audit log settings application/json object **auditLogsInConsoleEnabled** required boolean **auditLogsInDatabaseEnabled** required boolean **auditLogRetentionDays** required Days to keep entries for. 0 means keep them forever. integer #### Example generated ```json { "auditLogsInConsoleEnabled": true, "auditLogsInDatabaseEnabled": true, "auditLogRetentionDays": 1 } ``` ### 401 Missing or invalid bearer token application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on token refusals. An operation may also return a handler-level 401 without this header. ### 403 The token is valid but does not authorise this request. Four checks answer here, and a caller distinguishes them by error_code. INSUFFICIENT_SCOPE comes from the route’s authorisation middleware, before the handler runs: an Admin operation needs one of the scopes its route names, and an Account operation needs `authserver:manage-account`. USER_CONTEXT_REQUIRED comes from the same middleware on the Account operations, which also require the token to have been issued for a user: one obtained through the client credentials grant carries no auth_time claim and is refused here. FORBIDDEN comes from the handler, on the account endpoints that resolve a resource by id and then check it belongs to the caller, so a valid token for the wrong user reaches this rather than a 404. MANAGE_SCOPE_REQUIRED comes from the handler, on the Admin operations the administrative policy guards. Only `authserver:manage` creates an administrator, changes one, or changes what reaches one, so no granular scope will ever be enough: the remedy is a token holding `authserver:manage`, not requesting the route’s scope as for INSUFFICIENT_SCOPE. An administrator is a user, group or client holding any of the six administrative permissions (`manage`, `admin-read`, `manage-users`, `manage-clients`, `manage-settings`, `browser-sessions`), a user directly or through any of its groups, the admin console’s own client whatever it holds, and a client allowed to request the administrative scopes. A granular scope is refused here when it: - grants or revokes an administrative permission, directly or by moving a user into or out of a group that holds one, or deletes such a group; - writes to an administrator user (profile, credentials, enabled, sessions, consents, attributes, memberships, permissions, deletion), an administrative group (settings, attributes, members, permissions, deletion), or an administrator client (settings, authentication, flows, redirect URIs, web origins, token settings, permissions, logo, deletion); - switches whether a client may request the administrative scopes, on or off, on any client; - reads an administrator client’s secret; - changes the email or the audit-log settings; - changes an administrative permission’s description. Granular scopes still read administrators. The refusal is answered after the request’s 400 and 404, writes nothing, carries `WWW-Authenticate: Bearer realm="goiabada", error="insufficient_scope", error_description="...", scope="authserver:manage"`, and leaves an `administrator_change_refused` audit record. Every operation this document secures declares a 403. The public client logo endpoint, which carries `security: []` and authorises nothing, is the one that does not. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on scope and administrative-policy refusals; other forbidden responses omit it. ### 500 The request failed inside the server, most often a storage error. error_code is INTERNAL_SERVER_ERROR: no detail about the failure, only the request id an operator can find the log line by, and retrying is reasonable. Key rotation’s 500 also answers KEY_SET_INCOMPLETE, which names its cause and no retry clears. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` # Update audit log settings Source: https://goiabada.dev/reference/api/admin/operations/updatesettingsauditlogs/ PUT /api/v1/admin/settings/audit-logs Shell / cURL ```sh curl --request PUT \ --url http://localhost:9090/api/v1/admin/settings/audit-logs \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "auditLogsInConsoleEnabled": true, "auditLogsInDatabaseEnabled": true, "auditLogRetentionDays": 1 }' ``` - Auth server base URL{baseUrl}/api/v1/admin/settings/audit-logs Update audit log destinations and retention. A change needs `authserver:manage`: these settings decide what is recorded, so `authserver:manage-settings` reads them but is refused a change with 403 MANAGE_SCOPE_REQUIRED. `auditLogsInConsoleEnabled` writes each audit event to the application log as one record, with the event name and its details as fields. `auditLogsInDatabaseEnabled` stores each one in the `audit_logs` table, which `getAuditLogs` reads. A background worker deletes entries older than `auditLogRetentionDays`. ## Authorizations - **[BearerAuth](https://goiabada.dev/reference/api/admin/#bearerauth)** ## Request Bodyrequired application/json object **auditLogsInConsoleEnabled** boolean **auditLogsInDatabaseEnabled** boolean **auditLogRetentionDays** Days to keep entries for. 0 means keep them forever, and the maximum is 3650. integer ### Example generated ```json { "auditLogsInConsoleEnabled": true, "auditLogsInDatabaseEnabled": true, "auditLogRetentionDays": 1 } ``` ## Responses ### 200 Audit log settings updated application/json object **auditLogsInConsoleEnabled** required boolean **auditLogsInDatabaseEnabled** required boolean **auditLogRetentionDays** required Days to keep entries for. 0 means keep them forever. integer #### Example generated ```json { "auditLogsInConsoleEnabled": true, "auditLogsInDatabaseEnabled": true, "auditLogRetentionDays": 1 } ``` ### 400 Invalid request body or parameters application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge when token transport is malformed; ordinary validation errors omit it. ### 401 Missing or invalid bearer token application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on token refusals. An operation may also return a handler-level 401 without this header. ### 403 The token is valid but does not authorise this request. Four checks answer here, and a caller distinguishes them by error_code. INSUFFICIENT_SCOPE comes from the route’s authorisation middleware, before the handler runs: an Admin operation needs one of the scopes its route names, and an Account operation needs `authserver:manage-account`. USER_CONTEXT_REQUIRED comes from the same middleware on the Account operations, which also require the token to have been issued for a user: one obtained through the client credentials grant carries no auth_time claim and is refused here. FORBIDDEN comes from the handler, on the account endpoints that resolve a resource by id and then check it belongs to the caller, so a valid token for the wrong user reaches this rather than a 404. MANAGE_SCOPE_REQUIRED comes from the handler, on the Admin operations the administrative policy guards. Only `authserver:manage` creates an administrator, changes one, or changes what reaches one, so no granular scope will ever be enough: the remedy is a token holding `authserver:manage`, not requesting the route’s scope as for INSUFFICIENT_SCOPE. An administrator is a user, group or client holding any of the six administrative permissions (`manage`, `admin-read`, `manage-users`, `manage-clients`, `manage-settings`, `browser-sessions`), a user directly or through any of its groups, the admin console’s own client whatever it holds, and a client allowed to request the administrative scopes. A granular scope is refused here when it: - grants or revokes an administrative permission, directly or by moving a user into or out of a group that holds one, or deletes such a group; - writes to an administrator user (profile, credentials, enabled, sessions, consents, attributes, memberships, permissions, deletion), an administrative group (settings, attributes, members, permissions, deletion), or an administrator client (settings, authentication, flows, redirect URIs, web origins, token settings, permissions, logo, deletion); - switches whether a client may request the administrative scopes, on or off, on any client; - reads an administrator client’s secret; - changes the email or the audit-log settings; - changes an administrative permission’s description. Granular scopes still read administrators. The refusal is answered after the request’s 400 and 404, writes nothing, carries `WWW-Authenticate: Bearer realm="goiabada", error="insufficient_scope", error_description="...", scope="authserver:manage"`, and leaves an `administrator_change_refused` audit record. Every operation this document secures declares a 403. The public client logo endpoint, which carries `security: []` and authorises nothing, is the one that does not. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on scope and administrative-policy refusals; other forbidden responses omit it. ### 500 The request failed inside the server, most often a storage error. error_code is INTERNAL_SERVER_ERROR: no detail about the failure, only the request id an operator can find the log line by, and retrying is reasonable. Key rotation’s 500 also answers KEY_SET_INCOMPLETE, which names its cause and no retry clears. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` # Get signing keys Source: https://goiabada.dev/reference/api/admin/operations/getsettingskeys/ GET /api/v1/admin/settings/keys Shell / cURL ```sh curl --request GET \ --url http://localhost:9090/api/v1/admin/settings/keys \ --header 'Authorization: Bearer ' ``` - Auth server base URL{baseUrl}/api/v1/admin/settings/keys Get all signing keys (public key material only) ## Authorizations - **[BearerAuth](https://goiabada.dev/reference/api/admin/#bearerauth)** ## Responses ### 200 Signing keys application/json object **keys** required Array\ object **id** required integer format: int64 **createdAt** required string format: date-time nullable **state** required string **keyIdentifier** required string **type** required string **algorithm** required string **publicKeyASN1DER** required string **publicKeyPEM** required string **publicKeyJWK** required string #### Example generated ```json { "keys": [ { "id": 1, "createdAt": "2026-04-15T12:00:00Z", "state": "example", "keyIdentifier": "example", "type": "example", "algorithm": "example", "publicKeyASN1DER": "example", "publicKeyPEM": "example", "publicKeyJWK": "example" } ] } ``` ### 401 Missing or invalid bearer token application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on token refusals. An operation may also return a handler-level 401 without this header. ### 403 The token is valid but does not authorise this request. Four checks answer here, and a caller distinguishes them by error_code. INSUFFICIENT_SCOPE comes from the route’s authorisation middleware, before the handler runs: an Admin operation needs one of the scopes its route names, and an Account operation needs `authserver:manage-account`. USER_CONTEXT_REQUIRED comes from the same middleware on the Account operations, which also require the token to have been issued for a user: one obtained through the client credentials grant carries no auth_time claim and is refused here. FORBIDDEN comes from the handler, on the account endpoints that resolve a resource by id and then check it belongs to the caller, so a valid token for the wrong user reaches this rather than a 404. MANAGE_SCOPE_REQUIRED comes from the handler, on the Admin operations the administrative policy guards. Only `authserver:manage` creates an administrator, changes one, or changes what reaches one, so no granular scope will ever be enough: the remedy is a token holding `authserver:manage`, not requesting the route’s scope as for INSUFFICIENT_SCOPE. An administrator is a user, group or client holding any of the six administrative permissions (`manage`, `admin-read`, `manage-users`, `manage-clients`, `manage-settings`, `browser-sessions`), a user directly or through any of its groups, the admin console’s own client whatever it holds, and a client allowed to request the administrative scopes. A granular scope is refused here when it: - grants or revokes an administrative permission, directly or by moving a user into or out of a group that holds one, or deletes such a group; - writes to an administrator user (profile, credentials, enabled, sessions, consents, attributes, memberships, permissions, deletion), an administrative group (settings, attributes, members, permissions, deletion), or an administrator client (settings, authentication, flows, redirect URIs, web origins, token settings, permissions, logo, deletion); - switches whether a client may request the administrative scopes, on or off, on any client; - reads an administrator client’s secret; - changes the email or the audit-log settings; - changes an administrative permission’s description. Granular scopes still read administrators. The refusal is answered after the request’s 400 and 404, writes nothing, carries `WWW-Authenticate: Bearer realm="goiabada", error="insufficient_scope", error_description="...", scope="authserver:manage"`, and leaves an `administrator_change_refused` audit record. Every operation this document secures declares a 403. The public client logo endpoint, which carries `security: []` and authorises nothing, is the one that does not. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on scope and administrative-policy refusals; other forbidden responses omit it. ### 500 The request failed inside the server, most often a storage error. error_code is INTERNAL_SERVER_ERROR: no detail about the failure, only the request id an operator can find the log line by, and retrying is reasonable. Key rotation’s 500 also answers KEY_SET_INCOMPLETE, which names its cause and no retry clears. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` # Rotate signing keys Source: https://goiabada.dev/reference/api/admin/operations/rotatesettingskeys/ POST /api/v1/admin/settings/keys/rotate Shell / cURL ```sh curl --request POST \ --url http://localhost:9090/api/v1/admin/settings/keys/rotate \ --header 'Authorization: Bearer ' ``` - Auth server base URL{baseUrl}/api/v1/admin/settings/keys/rotate Generate a new signing key. Old key remains for validation. ## Authorizations - **[BearerAuth](https://goiabada.dev/reference/api/admin/#bearerauth)** ## Responses ### 200 Keys rotated application/json object **success** required boolean #### Example generated ```json { "success": true } ``` ### 401 Missing or invalid bearer token application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on token refusals. An operation may also return a handler-level 401 without this header. ### 403 The token is valid but does not authorise this request. Four checks answer here, and a caller distinguishes them by error_code. INSUFFICIENT_SCOPE comes from the route’s authorisation middleware, before the handler runs: an Admin operation needs one of the scopes its route names, and an Account operation needs `authserver:manage-account`. USER_CONTEXT_REQUIRED comes from the same middleware on the Account operations, which also require the token to have been issued for a user: one obtained through the client credentials grant carries no auth_time claim and is refused here. FORBIDDEN comes from the handler, on the account endpoints that resolve a resource by id and then check it belongs to the caller, so a valid token for the wrong user reaches this rather than a 404. MANAGE_SCOPE_REQUIRED comes from the handler, on the Admin operations the administrative policy guards. Only `authserver:manage` creates an administrator, changes one, or changes what reaches one, so no granular scope will ever be enough: the remedy is a token holding `authserver:manage`, not requesting the route’s scope as for INSUFFICIENT_SCOPE. An administrator is a user, group or client holding any of the six administrative permissions (`manage`, `admin-read`, `manage-users`, `manage-clients`, `manage-settings`, `browser-sessions`), a user directly or through any of its groups, the admin console’s own client whatever it holds, and a client allowed to request the administrative scopes. A granular scope is refused here when it: - grants or revokes an administrative permission, directly or by moving a user into or out of a group that holds one, or deletes such a group; - writes to an administrator user (profile, credentials, enabled, sessions, consents, attributes, memberships, permissions, deletion), an administrative group (settings, attributes, members, permissions, deletion), or an administrator client (settings, authentication, flows, redirect URIs, web origins, token settings, permissions, logo, deletion); - switches whether a client may request the administrative scopes, on or off, on any client; - reads an administrator client’s secret; - changes the email or the audit-log settings; - changes an administrative permission’s description. Granular scopes still read administrators. The refusal is answered after the request’s 400 and 404, writes nothing, carries `WWW-Authenticate: Bearer realm="goiabada", error="insufficient_scope", error_description="...", scope="authserver:manage"`, and leaves an `administrator_change_refused` audit record. Every operation this document secures declares a 403. The public client logo endpoint, which carries `security: []` and authorises nothing, is the one that does not. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on scope and administrative-policy refusals; other forbidden responses omit it. ### 409 Another rotation won the race and this call rotated nothing. error_code is ROTATION_IN_PROGRESS. Do not retry: the rotation it conflicted with already happened. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` ### 500 Error_code is KEY_SET_INCOMPLETE when the deployment has no current or no next key, which is an operator problem rather than a transient one and a retry will not clear it, or INTERNAL_SERVER_ERROR for a storage or key generation failure. Either way the description ends with the request id of the log line, and nothing was rotated: the whole transition is one transaction. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` # Delete signing key Source: https://goiabada.dev/reference/api/admin/operations/deletesettingskey/ DELETE /api/v1/admin/settings/keys/{id} Shell / cURL ```sh curl --request DELETE \ --url http://localhost:9090/api/v1/admin/settings/keys/1 \ --header 'Authorization: Bearer ' ``` - Auth server base URL{baseUrl}/api/v1/admin/settings/keys/{id} Delete a signing key ## Authorizations - **[BearerAuth](https://goiabada.dev/reference/api/admin/#bearerauth)** ## Parameters ### Path Parameters **id** required integer format: int64 Key ID ## Responses ### 200 Key deleted application/json object **success** required boolean #### Example generated ```json { "success": true } ``` ### 400 Error_code is VALIDATION_ERROR. The id is malformed, matches no key, or names a key that is not in the previous state, since only a previous key can be revoked. A key that does not exist is answered here rather than with a 404. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` ### 401 Missing or invalid bearer token application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on token refusals. An operation may also return a handler-level 401 without this header. ### 403 The token is valid but does not authorise this request. Four checks answer here, and a caller distinguishes them by error_code. INSUFFICIENT_SCOPE comes from the route’s authorisation middleware, before the handler runs: an Admin operation needs one of the scopes its route names, and an Account operation needs `authserver:manage-account`. USER_CONTEXT_REQUIRED comes from the same middleware on the Account operations, which also require the token to have been issued for a user: one obtained through the client credentials grant carries no auth_time claim and is refused here. FORBIDDEN comes from the handler, on the account endpoints that resolve a resource by id and then check it belongs to the caller, so a valid token for the wrong user reaches this rather than a 404. MANAGE_SCOPE_REQUIRED comes from the handler, on the Admin operations the administrative policy guards. Only `authserver:manage` creates an administrator, changes one, or changes what reaches one, so no granular scope will ever be enough: the remedy is a token holding `authserver:manage`, not requesting the route’s scope as for INSUFFICIENT_SCOPE. An administrator is a user, group or client holding any of the six administrative permissions (`manage`, `admin-read`, `manage-users`, `manage-clients`, `manage-settings`, `browser-sessions`), a user directly or through any of its groups, the admin console’s own client whatever it holds, and a client allowed to request the administrative scopes. A granular scope is refused here when it: - grants or revokes an administrative permission, directly or by moving a user into or out of a group that holds one, or deletes such a group; - writes to an administrator user (profile, credentials, enabled, sessions, consents, attributes, memberships, permissions, deletion), an administrative group (settings, attributes, members, permissions, deletion), or an administrator client (settings, authentication, flows, redirect URIs, web origins, token settings, permissions, logo, deletion); - switches whether a client may request the administrative scopes, on or off, on any client; - reads an administrator client’s secret; - changes the email or the audit-log settings; - changes an administrative permission’s description. Granular scopes still read administrators. The refusal is answered after the request’s 400 and 404, writes nothing, carries `WWW-Authenticate: Bearer realm="goiabada", error="insufficient_scope", error_description="...", scope="authserver:manage"`, and leaves an `administrator_change_refused` audit record. Every operation this document secures declares a 403. The public client logo endpoint, which carries `security: []` and authorises nothing, is the one that does not. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on scope and administrative-policy refusals; other forbidden responses omit it. ### 500 The request failed inside the server, most often a storage error. error_code is INTERNAL_SERVER_ERROR: no detail about the failure, only the request id an operator can find the log line by, and retrying is reasonable. Key rotation’s 500 also answers KEY_SET_INCOMPLETE, which names its cause and no retry clears. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` # Get phone countries Source: https://goiabada.dev/reference/api/admin/operations/getphonecountries/ GET /api/v1/admin/phone-countries Shell / cURL ```sh curl --request GET \ --url http://localhost:9090/api/v1/admin/phone-countries \ --header 'Authorization: Bearer ' ``` - Auth server base URL{baseUrl}/api/v1/admin/phone-countries Get the list of countries with their calling codes. An entry’s `uniqueId` is what `phoneCountryUniqueId` takes when a user’s phone number is set. Unlike the other reads, it accepts only `authserver:admin-read` or `authserver:manage`: a token with a domain scope alone, such as `authserver:manage-users`, is refused with 403 INSUFFICIENT_SCOPE. ## Authorizations - **[BearerAuth](https://goiabada.dev/reference/api/admin/#bearerauth)** ## Responses ### 200 Phone countries application/json object **phoneCountries** required Array\ object **uniqueId** required string **alpha2** required string **emoji** required string **callingCode** required string **name** required string #### Example generated ```json { "phoneCountries": [ { "uniqueId": "example", "alpha2": "example", "emoji": "example", "callingCode": "example", "name": "example" } ] } ``` ### 401 Missing or invalid bearer token application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on token refusals. An operation may also return a handler-level 401 without this header. ### 403 The token is valid but does not authorise this request. Four checks answer here, and a caller distinguishes them by error_code. INSUFFICIENT_SCOPE comes from the route’s authorisation middleware, before the handler runs: an Admin operation needs one of the scopes its route names, and an Account operation needs `authserver:manage-account`. USER_CONTEXT_REQUIRED comes from the same middleware on the Account operations, which also require the token to have been issued for a user: one obtained through the client credentials grant carries no auth_time claim and is refused here. FORBIDDEN comes from the handler, on the account endpoints that resolve a resource by id and then check it belongs to the caller, so a valid token for the wrong user reaches this rather than a 404. MANAGE_SCOPE_REQUIRED comes from the handler, on the Admin operations the administrative policy guards. Only `authserver:manage` creates an administrator, changes one, or changes what reaches one, so no granular scope will ever be enough: the remedy is a token holding `authserver:manage`, not requesting the route’s scope as for INSUFFICIENT_SCOPE. An administrator is a user, group or client holding any of the six administrative permissions (`manage`, `admin-read`, `manage-users`, `manage-clients`, `manage-settings`, `browser-sessions`), a user directly or through any of its groups, the admin console’s own client whatever it holds, and a client allowed to request the administrative scopes. A granular scope is refused here when it: - grants or revokes an administrative permission, directly or by moving a user into or out of a group that holds one, or deletes such a group; - writes to an administrator user (profile, credentials, enabled, sessions, consents, attributes, memberships, permissions, deletion), an administrative group (settings, attributes, members, permissions, deletion), or an administrator client (settings, authentication, flows, redirect URIs, web origins, token settings, permissions, logo, deletion); - switches whether a client may request the administrative scopes, on or off, on any client; - reads an administrator client’s secret; - changes the email or the audit-log settings; - changes an administrative permission’s description. Granular scopes still read administrators. The refusal is answered after the request’s 400 and 404, writes nothing, carries `WWW-Authenticate: Bearer realm="goiabada", error="insufficient_scope", error_description="...", scope="authserver:manage"`, and leaves an `administrator_change_refused` audit record. Every operation this document secures declares a 403. The public client logo endpoint, which carries `security: []` and authorises nothing, is the one that does not. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on scope and administrative-policy refusals; other forbidden responses omit it. ### 500 The request failed inside the server, most often a storage error. error_code is INTERNAL_SERVER_ERROR: no detail about the failure, only the request id an operator can find the log line by, and retrying is reasonable. Key rotation’s 500 also answers KEY_SET_INCOMPLETE, which names its cause and no retry clears. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` # Overview Source: https://goiabada.dev/reference/api/account/ ## Account API 1.0.0 The REST API of the Goiabada auth server. ## Authentication Every operation under `/api/v1` requires a valid JWT access token in the `Authorization` header: ``` Authorization: Bearer ``` `getClientLogoImage`, at `/client/logo/{clientIdentifier}` outside `/api/v1`, is public and marked `security: []`. ## API Groups - **Admin API** (`/api/v1/admin/*`): administrative control. Each operation accepts any one of a small set of scopes rather than a single one: reads accept `authserver:admin-read` alongside the domain’s own scope, and writes accept the domain scope (`authserver:manage-users`, `authserver:manage-clients` or `authserver:manage-settings`). `authserver:manage` is accepted everywhere. `getClientSecret` is the one read `authserver:admin-read` does not reach: it answers a credential, so it accepts `authserver:manage-clients` or `authserver:manage`. A token from the client credentials flow is accepted here. - **Account API** (`/api/v1/account/*`): Self-service account management. Requires `authserver:manage-account` scope, **and** a token issued for a user: these operations resolve the acting user from `sub`, so a client credentials token is refused with 403 and `error_code` `USER_CONTEXT_REQUIRED`. ## Caching Every response from an operation in either API group carries these headers, error responses and the token and scope refusals included: ``` Cache-Control: no-store Pragma: no-cache ``` Do not cache these responses. Two operations return a credential in the body: `getClientSecret` returns the client secret decrypted, and `getAccountOTPEnrollment` returns a new authenticator’s setup key. The bearer token on the request does not make a cached copy safe: a private cache may still store a response it authenticated. A few responses under these paths are answered before an operation is selected and carry neither header: a CORS preflight, and the errors returned when the server cannot read its own settings or the caller’s session. None of them carries a credential, and none of them is described by this document. The headers are not declared per operation, since every operation sends them. Goiabada - [https://goiabada.dev](https://goiabada.dev/) Information - License: [MIT](https://opensource.org/licenses/MIT) - OpenAPI version: `3.0.3` ## Operations GET [/api/v1/account/profile](https://goiabada.dev/reference/api/account/operations/getaccountprofile/) PUT [/api/v1/account/profile](https://goiabada.dev/reference/api/account/operations/updateaccountprofile/) PUT [/api/v1/account/email](https://goiabada.dev/reference/api/account/operations/updateaccountemail/) POST [/api/v1/account/email/verification/send](https://goiabada.dev/reference/api/account/operations/sendaccountemailverification/) POST [/api/v1/account/email/verification](https://goiabada.dev/reference/api/account/operations/verifyaccountemail/) PUT [/api/v1/account/phone](https://goiabada.dev/reference/api/account/operations/updateaccountphone/) PUT [/api/v1/account/address](https://goiabada.dev/reference/api/account/operations/updateaccountaddress/) PUT [/api/v1/account/password](https://goiabada.dev/reference/api/account/operations/updateaccountpassword/) GET [/api/v1/account/profile-picture](https://goiabada.dev/reference/api/account/operations/getaccountprofilepictureinfo/) POST [/api/v1/account/profile-picture](https://goiabada.dev/reference/api/account/operations/uploadaccountprofilepicture/) DELETE [/api/v1/account/profile-picture](https://goiabada.dev/reference/api/account/operations/deleteaccountprofilepicture/) GET [/api/v1/account/otp/enrollment](https://goiabada.dev/reference/api/account/operations/getaccountotpenrollment/) PUT [/api/v1/account/otp](https://goiabada.dev/reference/api/account/operations/updateaccountotp/) GET [/api/v1/account/consents](https://goiabada.dev/reference/api/account/operations/getaccountconsents/) DELETE [/api/v1/account/consents/{id}](https://goiabada.dev/reference/api/account/operations/deleteaccountconsent/) GET [/api/v1/account/sessions](https://goiabada.dev/reference/api/account/operations/getaccountsessions/) DELETE [/api/v1/account/sessions/{id}](https://goiabada.dev/reference/api/account/operations/deleteaccountsession/) POST [/api/v1/account/logout-request](https://goiabada.dev/reference/api/account/operations/requestaccountlogout/) ## Authentication ### BearerAuth JWT access token. Admin API: any one of `authserver:admin-read`, the domain scope for the operation (`authserver:manage-users`, `authserver:manage-clients`, `authserver:manage-settings`) or `authserver:manage`. A client credentials token is accepted. Reads accept `authserver:admin-read`, except `getClientSecret`; writes need the domain scope. The granular scopes stop short of administrators. An administrator is a user, group or client holding any of the six administrative permissions on the `authserver` resource (`manage`, `admin-read`, `manage-users`, `manage-clients`, `manage-settings`, `browser-sessions`), a user directly or through any of its groups, the admin console’s own client whatever it holds, and a client allowed to request the administrative scopes. Only `authserver:manage` creates an administrator, changes one, or changes what reaches one: a granular scope reaching an operation is still refused, with 403 MANAGE_SCOPE_REQUIRED, when the request grants or revokes an administrative permission (directly, or by moving a user into or out of a group holding one, or by deleting such a group), writes to an administrator, switches whether a client may request the administrative scopes, reads an administrator client’s secret, changes the email or audit-log settings, or changes an administrative permission’s description. It keeps full control of everyone else, and still reads administrators. `authserver:manage` alone meets 409 LAST_ADMINISTRATOR, on the operations that could leave no enabled user holding it. Account API: scope `authserver:manage-account`, on a token issued for a user. A client credentials token carries no `auth_time` claim and is refused with 403 `USER_CONTEXT_REQUIRED`. Browser sessions: scope `authserver:browser-sessions`, and only that scope. It is deliberately not one of the manage-\* scopes, so holding the admin console’s client secret is not a way to drive the admin API with no user present. **Security scheme type: **http **Bearer format: **JWT # Get own profile Source: https://goiabada.dev/reference/api/account/operations/getaccountprofile/ GET /api/v1/account/profile Shell / cURL ```sh curl --request GET \ --url http://localhost:9090/api/v1/account/profile \ --header 'Authorization: Bearer ' ``` - Auth server base URL{baseUrl}/api/v1/account/profile Get the authenticated user’s profile ## Authorizations - **[BearerAuth](https://goiabada.dev/reference/api/account/#bearerauth)** ## Responses ### 200 User profile application/json object **user** required object **id** required integer format: int64 **createdAt** required string format: date-time nullable **updatedAt** required string format: date-time nullable **enabled** required boolean **subject** required string format: uuid **username** required string **givenName** required string **middleName** required string **familyName** required string **nickname** required string **website** required string **gender** required “female”, “male” or “other”, or empty when unset string **email** required string format: email **emailVerified** required boolean **zoneInfoCountryName** required string **zoneInfo** required string **locale** required string **birthDate** required string format: date-time nullable **phoneNumberCountryUniqueId** required string **phoneNumberCountryCallingCode** required string **phoneNumber** required string **phoneNumberVerified** required boolean **addressLine1** required string **addressLine2** required string **addressLocality** required string **addressRegion** required string **addressPostalCode** required string **addressCountry** required ISO 3166-1 alpha-2 country code (e.g. US, BR), or empty string. string /^(\[A-Z]{2})?$/ **otpEnabled** required boolean #### Example generated ```json { "user": { "id": 1, "createdAt": "2026-04-15T12:00:00Z", "updatedAt": "2026-04-15T12:00:00Z", "enabled": true, "subject": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0", "username": "example", "givenName": "example", "middleName": "example", "familyName": "example", "nickname": "example", "website": "example", "gender": "example", "email": "hello@example.com", "emailVerified": true, "zoneInfoCountryName": "example", "zoneInfo": "example", "locale": "example", "birthDate": "2026-04-15T12:00:00Z", "phoneNumberCountryUniqueId": "example", "phoneNumberCountryCallingCode": "example", "phoneNumber": "example", "phoneNumberVerified": true, "addressLine1": "example", "addressLine2": "example", "addressLocality": "example", "addressRegion": "example", "addressPostalCode": "example", "addressCountry": "example", "otpEnabled": true } } ``` ### 401 Missing or invalid bearer token application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on token refusals. An operation may also return a handler-level 401 without this header. ### 403 The token is valid but does not authorise this request. Four checks answer here, and a caller distinguishes them by error_code. INSUFFICIENT_SCOPE comes from the route’s authorisation middleware, before the handler runs: an Admin operation needs one of the scopes its route names, and an Account operation needs `authserver:manage-account`. USER_CONTEXT_REQUIRED comes from the same middleware on the Account operations, which also require the token to have been issued for a user: one obtained through the client credentials grant carries no auth_time claim and is refused here. FORBIDDEN comes from the handler, on the account endpoints that resolve a resource by id and then check it belongs to the caller, so a valid token for the wrong user reaches this rather than a 404. MANAGE_SCOPE_REQUIRED comes from the handler, on the Admin operations the administrative policy guards. Only `authserver:manage` creates an administrator, changes one, or changes what reaches one, so no granular scope will ever be enough: the remedy is a token holding `authserver:manage`, not requesting the route’s scope as for INSUFFICIENT_SCOPE. An administrator is a user, group or client holding any of the six administrative permissions (`manage`, `admin-read`, `manage-users`, `manage-clients`, `manage-settings`, `browser-sessions`), a user directly or through any of its groups, the admin console’s own client whatever it holds, and a client allowed to request the administrative scopes. A granular scope is refused here when it: - grants or revokes an administrative permission, directly or by moving a user into or out of a group that holds one, or deletes such a group; - writes to an administrator user (profile, credentials, enabled, sessions, consents, attributes, memberships, permissions, deletion), an administrative group (settings, attributes, members, permissions, deletion), or an administrator client (settings, authentication, flows, redirect URIs, web origins, token settings, permissions, logo, deletion); - switches whether a client may request the administrative scopes, on or off, on any client; - reads an administrator client’s secret; - changes the email or the audit-log settings; - changes an administrative permission’s description. Granular scopes still read administrators. The refusal is answered after the request’s 400 and 404, writes nothing, carries `WWW-Authenticate: Bearer realm="goiabada", error="insufficient_scope", error_description="...", scope="authserver:manage"`, and leaves an `administrator_change_refused` audit record. Every operation this document secures declares a 403. The public client logo endpoint, which carries `security: []` and authorises nothing, is the one that does not. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on scope and administrative-policy refusals; other forbidden responses omit it. ### 404 Resource not found application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` ### 500 The request failed inside the server, most often a storage error. error_code is INTERNAL_SERVER_ERROR: no detail about the failure, only the request id an operator can find the log line by, and retrying is reasonable. Key rotation’s 500 also answers KEY_SET_INCOMPLETE, which names its cause and no retry clears. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` # Update own profile Source: https://goiabada.dev/reference/api/account/operations/updateaccountprofile/ PUT /api/v1/account/profile Shell / cURL ```sh curl --request PUT \ --url http://localhost:9090/api/v1/account/profile \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "username": "example", "givenName": "example", "middleName": "example", "familyName": "example", "nickname": "example", "website": "example", "gender": "example", "dateOfBirth": "example", "zoneInfoCountryName": "example", "zoneInfo": "example", "locale": "example" }' ``` - Auth server base URL{baseUrl}/api/v1/account/profile Update the authenticated user’s profile ## Authorizations - **[BearerAuth](https://goiabada.dev/reference/api/account/#bearerauth)** ## Request Bodyrequired application/json object **username** string **givenName** string **middleName** string **familyName** string **nickname** string **website** string **gender** “female”, “male” or “other”, as GET returns it, or “0”, “1” or “2” for the same three; stored as the word. Empty clears it string **dateOfBirth** Format YYYY-MM-DD string **zoneInfoCountryName** The country zoneInfo is listed under in the time zone table; sent with zoneInfo, or both empty string **zoneInfo** string **locale** string ### Example generated ```json { "username": "example", "givenName": "example", "middleName": "example", "familyName": "example", "nickname": "example", "website": "example", "gender": "example", "dateOfBirth": "example", "zoneInfoCountryName": "example", "zoneInfo": "example", "locale": "example" } ``` ## Responses ### 200 Profile updated application/json object **user** required object **id** required integer format: int64 **createdAt** required string format: date-time nullable **updatedAt** required string format: date-time nullable **enabled** required boolean **subject** required string format: uuid **username** required string **givenName** required string **middleName** required string **familyName** required string **nickname** required string **website** required string **gender** required “female”, “male” or “other”, or empty when unset string **email** required string format: email **emailVerified** required boolean **zoneInfoCountryName** required string **zoneInfo** required string **locale** required string **birthDate** required string format: date-time nullable **phoneNumberCountryUniqueId** required string **phoneNumberCountryCallingCode** required string **phoneNumber** required string **phoneNumberVerified** required boolean **addressLine1** required string **addressLine2** required string **addressLocality** required string **addressRegion** required string **addressPostalCode** required string **addressCountry** required ISO 3166-1 alpha-2 country code (e.g. US, BR), or empty string. string /^(\[A-Z]{2})?$/ **otpEnabled** required boolean #### Example generated ```json { "user": { "id": 1, "createdAt": "2026-04-15T12:00:00Z", "updatedAt": "2026-04-15T12:00:00Z", "enabled": true, "subject": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0", "username": "example", "givenName": "example", "middleName": "example", "familyName": "example", "nickname": "example", "website": "example", "gender": "example", "email": "hello@example.com", "emailVerified": true, "zoneInfoCountryName": "example", "zoneInfo": "example", "locale": "example", "birthDate": "2026-04-15T12:00:00Z", "phoneNumberCountryUniqueId": "example", "phoneNumberCountryCallingCode": "example", "phoneNumber": "example", "phoneNumberVerified": true, "addressLine1": "example", "addressLine2": "example", "addressLocality": "example", "addressRegion": "example", "addressPostalCode": "example", "addressCountry": "example", "otpEnabled": true } } ``` ### 400 Invalid request body or parameters application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge when token transport is malformed; ordinary validation errors omit it. ### 401 Missing or invalid bearer token application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on token refusals. An operation may also return a handler-level 401 without this header. ### 403 The token is valid but does not authorise this request. Four checks answer here, and a caller distinguishes them by error_code. INSUFFICIENT_SCOPE comes from the route’s authorisation middleware, before the handler runs: an Admin operation needs one of the scopes its route names, and an Account operation needs `authserver:manage-account`. USER_CONTEXT_REQUIRED comes from the same middleware on the Account operations, which also require the token to have been issued for a user: one obtained through the client credentials grant carries no auth_time claim and is refused here. FORBIDDEN comes from the handler, on the account endpoints that resolve a resource by id and then check it belongs to the caller, so a valid token for the wrong user reaches this rather than a 404. MANAGE_SCOPE_REQUIRED comes from the handler, on the Admin operations the administrative policy guards. Only `authserver:manage` creates an administrator, changes one, or changes what reaches one, so no granular scope will ever be enough: the remedy is a token holding `authserver:manage`, not requesting the route’s scope as for INSUFFICIENT_SCOPE. An administrator is a user, group or client holding any of the six administrative permissions (`manage`, `admin-read`, `manage-users`, `manage-clients`, `manage-settings`, `browser-sessions`), a user directly or through any of its groups, the admin console’s own client whatever it holds, and a client allowed to request the administrative scopes. A granular scope is refused here when it: - grants or revokes an administrative permission, directly or by moving a user into or out of a group that holds one, or deletes such a group; - writes to an administrator user (profile, credentials, enabled, sessions, consents, attributes, memberships, permissions, deletion), an administrative group (settings, attributes, members, permissions, deletion), or an administrator client (settings, authentication, flows, redirect URIs, web origins, token settings, permissions, logo, deletion); - switches whether a client may request the administrative scopes, on or off, on any client; - reads an administrator client’s secret; - changes the email or the audit-log settings; - changes an administrative permission’s description. Granular scopes still read administrators. The refusal is answered after the request’s 400 and 404, writes nothing, carries `WWW-Authenticate: Bearer realm="goiabada", error="insufficient_scope", error_description="...", scope="authserver:manage"`, and leaves an `administrator_change_refused` audit record. Every operation this document secures declares a 403. The public client logo endpoint, which carries `security: []` and authorises nothing, is the one that does not. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on scope and administrative-policy refusals; other forbidden responses omit it. ### 404 Resource not found application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` ### 500 The request failed inside the server, most often a storage error. error_code is INTERNAL_SERVER_ERROR: no detail about the failure, only the request id an operator can find the log line by, and retrying is reasonable. Key rotation’s 500 also answers KEY_SET_INCOMPLETE, which names its cause and no retry clears. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` # Update own email Source: https://goiabada.dev/reference/api/account/operations/updateaccountemail/ PUT /api/v1/account/email Shell / cURL ```sh curl --request PUT \ --url http://localhost:9090/api/v1/account/email \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "email": "hello@example.com", "currentPassword": "example" }' ``` - Auth server base URL{baseUrl}/api/v1/account/email Update the authenticated user’s email (marks as unverified). Requires the current password, checked before the address: a blank one is a 400 with error_code VALIDATION_ERROR, a wrong one a 400 with error_code AUTHENTICATION_FAILED that counts against the failure limit shared with the password and OTP changes. Submitting the address the account already has changes nothing, keeps it verified, and answers 200 with the user as stored. A change clears any pending verification code, and any password reset link already sent stops working, since it was mailed to the previous address. Verify the new address with `sendAccountEmailVerification` and `verifyAccountEmail`. When SMTP is enabled and the previous address was verified, it is sent a notice that the address was changed, in the user’s language, after the response. The notice does not name the new address, and one that cannot be sent does not fail the change. ## Authorizations - **[BearerAuth](https://goiabada.dev/reference/api/account/#bearerauth)** ## Request Bodyrequired application/json object **email** required string format: email **currentPassword** required string ### Example generated ```json { "email": "hello@example.com", "currentPassword": "example" } ``` ## Responses ### 200 Email updated application/json object **user** required object **id** required integer format: int64 **createdAt** required string format: date-time nullable **updatedAt** required string format: date-time nullable **enabled** required boolean **subject** required string format: uuid **username** required string **givenName** required string **middleName** required string **familyName** required string **nickname** required string **website** required string **gender** required “female”, “male” or “other”, or empty when unset string **email** required string format: email **emailVerified** required boolean **zoneInfoCountryName** required string **zoneInfo** required string **locale** required string **birthDate** required string format: date-time nullable **phoneNumberCountryUniqueId** required string **phoneNumberCountryCallingCode** required string **phoneNumber** required string **phoneNumberVerified** required boolean **addressLine1** required string **addressLine2** required string **addressLocality** required string **addressRegion** required string **addressPostalCode** required string **addressCountry** required ISO 3166-1 alpha-2 country code (e.g. US, BR), or empty string. string /^(\[A-Z]{2})?$/ **otpEnabled** required boolean #### Example generated ```json { "user": { "id": 1, "createdAt": "2026-04-15T12:00:00Z", "updatedAt": "2026-04-15T12:00:00Z", "enabled": true, "subject": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0", "username": "example", "givenName": "example", "middleName": "example", "familyName": "example", "nickname": "example", "website": "example", "gender": "example", "email": "hello@example.com", "emailVerified": true, "zoneInfoCountryName": "example", "zoneInfo": "example", "locale": "example", "birthDate": "2026-04-15T12:00:00Z", "phoneNumberCountryUniqueId": "example", "phoneNumberCountryCallingCode": "example", "phoneNumber": "example", "phoneNumberVerified": true, "addressLine1": "example", "addressLine2": "example", "addressLocality": "example", "addressRegion": "example", "addressPostalCode": "example", "addressCountry": "example", "otpEnabled": true } } ``` ### 400 Invalid request body or parameters application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge when token transport is malformed; ordinary validation errors omit it. ### 401 Missing or invalid bearer token application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on token refusals. An operation may also return a handler-level 401 without this header. ### 403 The token is valid but does not authorise this request. Four checks answer here, and a caller distinguishes them by error_code. INSUFFICIENT_SCOPE comes from the route’s authorisation middleware, before the handler runs: an Admin operation needs one of the scopes its route names, and an Account operation needs `authserver:manage-account`. USER_CONTEXT_REQUIRED comes from the same middleware on the Account operations, which also require the token to have been issued for a user: one obtained through the client credentials grant carries no auth_time claim and is refused here. FORBIDDEN comes from the handler, on the account endpoints that resolve a resource by id and then check it belongs to the caller, so a valid token for the wrong user reaches this rather than a 404. MANAGE_SCOPE_REQUIRED comes from the handler, on the Admin operations the administrative policy guards. Only `authserver:manage` creates an administrator, changes one, or changes what reaches one, so no granular scope will ever be enough: the remedy is a token holding `authserver:manage`, not requesting the route’s scope as for INSUFFICIENT_SCOPE. An administrator is a user, group or client holding any of the six administrative permissions (`manage`, `admin-read`, `manage-users`, `manage-clients`, `manage-settings`, `browser-sessions`), a user directly or through any of its groups, the admin console’s own client whatever it holds, and a client allowed to request the administrative scopes. A granular scope is refused here when it: - grants or revokes an administrative permission, directly or by moving a user into or out of a group that holds one, or deletes such a group; - writes to an administrator user (profile, credentials, enabled, sessions, consents, attributes, memberships, permissions, deletion), an administrative group (settings, attributes, members, permissions, deletion), or an administrator client (settings, authentication, flows, redirect URIs, web origins, token settings, permissions, logo, deletion); - switches whether a client may request the administrative scopes, on or off, on any client; - reads an administrator client’s secret; - changes the email or the audit-log settings; - changes an administrative permission’s description. Granular scopes still read administrators. The refusal is answered after the request’s 400 and 404, writes nothing, carries `WWW-Authenticate: Bearer realm="goiabada", error="insufficient_scope", error_description="...", scope="authserver:manage"`, and leaves an `administrator_change_refused` audit record. Every operation this document secures declares a 403. The public client logo endpoint, which carries `security: []` and authorises nothing, is the one that does not. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on scope and administrative-policy refusals; other forbidden responses omit it. ### 404 Resource not found application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` ### 409 The email address was registered to another user between the duplicate check and the write. error_code is EMAIL_ALREADY_EXISTS. An address already taken when the request arrives is a 400 with error_code validator.email.already_registered. Or another request changed the account’s address or its verification after this one read it: error_code is CONCURRENT_UPDATE, nothing was saved and no notice was sent. Read the account again and retry. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` ### 429 Rate limit reached. Every limit on the account API is counted by the token’s user and applies whether or not the rate limiter (GOIABADA_AUTHSERVER_RATELIMITER_ENABLED) is on. error_code is TOO_MANY_REQUESTS and a Retry-After header carries the window length in seconds. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **Retry-After** integer Seconds to wait before retrying ### 500 The request failed inside the server, most often a storage error. error_code is INTERNAL_SERVER_ERROR: no detail about the failure, only the request id an operator can find the log line by, and retrying is reasonable. Key rotation’s 500 also answers KEY_SET_INCOMPLETE, which names its cause and no retry clears. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` # Send verification email Source: https://goiabada.dev/reference/api/account/operations/sendaccountemailverification/ POST /api/v1/account/email/verification/send Shell / cURL ```sh curl --request POST \ --url http://localhost:9090/api/v1/account/email/verification/send \ --header 'Authorization: Bearer ' ``` - Auth server base URL{baseUrl}/api/v1/account/email/verification/send Send an email verification code to the account’s address. A code lives 5 minutes, and an account is sent at most one per 5 minutes, so it has at most one live code at a time. Inside that cooldown nothing is sent: the answer is 200 with `tooManyRequests` true and `waitInSeconds` holding the seconds left. Changing or verifying the address does not reset the cooldown, and of requests sent at the same moment exactly one sends a code. The operation is also limited to 5 requests per hour per account, answered 429, whether or not the rate limiter (GOIABADA_AUTHSERVER_RATELIMITER_ENABLED) is on. ## Authorizations - **[BearerAuth](https://goiabada.dev/reference/api/account/#bearerauth)** ## Responses ### 200 Verification status application/json object **emailVerificationSent** required boolean **emailDestination** required string **tooManyRequests** required boolean **waitInSeconds** required integer **emailVerified** required boolean #### Example generated ```json { "emailVerificationSent": true, "emailDestination": "example", "tooManyRequests": true, "waitInSeconds": 1, "emailVerified": true } ``` ### 400 Invalid request body or parameters application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge when token transport is malformed; ordinary validation errors omit it. ### 401 Missing or invalid bearer token application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on token refusals. An operation may also return a handler-level 401 without this header. ### 403 The token is valid but does not authorise this request. Four checks answer here, and a caller distinguishes them by error_code. INSUFFICIENT_SCOPE comes from the route’s authorisation middleware, before the handler runs: an Admin operation needs one of the scopes its route names, and an Account operation needs `authserver:manage-account`. USER_CONTEXT_REQUIRED comes from the same middleware on the Account operations, which also require the token to have been issued for a user: one obtained through the client credentials grant carries no auth_time claim and is refused here. FORBIDDEN comes from the handler, on the account endpoints that resolve a resource by id and then check it belongs to the caller, so a valid token for the wrong user reaches this rather than a 404. MANAGE_SCOPE_REQUIRED comes from the handler, on the Admin operations the administrative policy guards. Only `authserver:manage` creates an administrator, changes one, or changes what reaches one, so no granular scope will ever be enough: the remedy is a token holding `authserver:manage`, not requesting the route’s scope as for INSUFFICIENT_SCOPE. An administrator is a user, group or client holding any of the six administrative permissions (`manage`, `admin-read`, `manage-users`, `manage-clients`, `manage-settings`, `browser-sessions`), a user directly or through any of its groups, the admin console’s own client whatever it holds, and a client allowed to request the administrative scopes. A granular scope is refused here when it: - grants or revokes an administrative permission, directly or by moving a user into or out of a group that holds one, or deletes such a group; - writes to an administrator user (profile, credentials, enabled, sessions, consents, attributes, memberships, permissions, deletion), an administrative group (settings, attributes, members, permissions, deletion), or an administrator client (settings, authentication, flows, redirect URIs, web origins, token settings, permissions, logo, deletion); - switches whether a client may request the administrative scopes, on or off, on any client; - reads an administrator client’s secret; - changes the email or the audit-log settings; - changes an administrative permission’s description. Granular scopes still read administrators. The refusal is answered after the request’s 400 and 404, writes nothing, carries `WWW-Authenticate: Bearer realm="goiabada", error="insufficient_scope", error_description="...", scope="authserver:manage"`, and leaves an `administrator_change_refused` audit record. Every operation this document secures declares a 403. The public client logo endpoint, which carries `security: []` and authorises nothing, is the one that does not. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on scope and administrative-policy refusals; other forbidden responses omit it. ### 404 Resource not found application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` ### 409 Another request changed the account’s address while the code was being issued. error_code is CONCURRENT_UPDATE; nothing was sent. Retry. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` ### 429 Rate limit reached. Every limit on the account API is counted by the token’s user and applies whether or not the rate limiter (GOIABADA_AUTHSERVER_RATELIMITER_ENABLED) is on. error_code is TOO_MANY_REQUESTS and a Retry-After header carries the window length in seconds. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **Retry-After** integer Seconds to wait before retrying ### 500 The request failed inside the server, most often a storage error. error_code is INTERNAL_SERVER_ERROR: no detail about the failure, only the request id an operator can find the log line by, and retrying is reasonable. Key rotation’s 500 also answers KEY_SET_INCOMPLETE, which names its cause and no retry clears. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` # Verify email Source: https://goiabada.dev/reference/api/account/operations/verifyaccountemail/ POST /api/v1/account/email/verification Shell / cURL ```sh curl --request POST \ --url http://localhost:9090/api/v1/account/email/verification \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "verificationCode": "example" }' ``` - Auth server base URL{baseUrl}/api/v1/account/email/verification Verify the account’s address with the code `sendAccountEmailVerification` sent: 8 characters, four letters then four digits, compared without regard to case, valid for 5 minutes. ## Authorizations - **[BearerAuth](https://goiabada.dev/reference/api/account/#bearerauth)** ## Request Bodyrequired application/json object **verificationCode** required 8-character code (4 letters + 4 numbers), case-insensitive string ### Example generated ```json { "verificationCode": "example" } ``` ## Responses ### 200 Email verified application/json object **user** required object **id** required integer format: int64 **createdAt** required string format: date-time nullable **updatedAt** required string format: date-time nullable **enabled** required boolean **subject** required string format: uuid **username** required string **givenName** required string **middleName** required string **familyName** required string **nickname** required string **website** required string **gender** required “female”, “male” or “other”, or empty when unset string **email** required string format: email **emailVerified** required boolean **zoneInfoCountryName** required string **zoneInfo** required string **locale** required string **birthDate** required string format: date-time nullable **phoneNumberCountryUniqueId** required string **phoneNumberCountryCallingCode** required string **phoneNumber** required string **phoneNumberVerified** required boolean **addressLine1** required string **addressLine2** required string **addressLocality** required string **addressRegion** required string **addressPostalCode** required string **addressCountry** required ISO 3166-1 alpha-2 country code (e.g. US, BR), or empty string. string /^(\[A-Z]{2})?$/ **otpEnabled** required boolean #### Example generated ```json { "user": { "id": 1, "createdAt": "2026-04-15T12:00:00Z", "updatedAt": "2026-04-15T12:00:00Z", "enabled": true, "subject": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0", "username": "example", "givenName": "example", "middleName": "example", "familyName": "example", "nickname": "example", "website": "example", "gender": "example", "email": "hello@example.com", "emailVerified": true, "zoneInfoCountryName": "example", "zoneInfo": "example", "locale": "example", "birthDate": "2026-04-15T12:00:00Z", "phoneNumberCountryUniqueId": "example", "phoneNumberCountryCallingCode": "example", "phoneNumber": "example", "phoneNumberVerified": true, "addressLine1": "example", "addressLine2": "example", "addressLocality": "example", "addressRegion": "example", "addressPostalCode": "example", "addressCountry": "example", "otpEnabled": true } } ``` ### 400 Invalid request body or parameters application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge when token transport is malformed; ordinary validation errors omit it. ### 401 Missing or invalid bearer token application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on token refusals. An operation may also return a handler-level 401 without this header. ### 403 The token is valid but does not authorise this request. Four checks answer here, and a caller distinguishes them by error_code. INSUFFICIENT_SCOPE comes from the route’s authorisation middleware, before the handler runs: an Admin operation needs one of the scopes its route names, and an Account operation needs `authserver:manage-account`. USER_CONTEXT_REQUIRED comes from the same middleware on the Account operations, which also require the token to have been issued for a user: one obtained through the client credentials grant carries no auth_time claim and is refused here. FORBIDDEN comes from the handler, on the account endpoints that resolve a resource by id and then check it belongs to the caller, so a valid token for the wrong user reaches this rather than a 404. MANAGE_SCOPE_REQUIRED comes from the handler, on the Admin operations the administrative policy guards. Only `authserver:manage` creates an administrator, changes one, or changes what reaches one, so no granular scope will ever be enough: the remedy is a token holding `authserver:manage`, not requesting the route’s scope as for INSUFFICIENT_SCOPE. An administrator is a user, group or client holding any of the six administrative permissions (`manage`, `admin-read`, `manage-users`, `manage-clients`, `manage-settings`, `browser-sessions`), a user directly or through any of its groups, the admin console’s own client whatever it holds, and a client allowed to request the administrative scopes. A granular scope is refused here when it: - grants or revokes an administrative permission, directly or by moving a user into or out of a group that holds one, or deletes such a group; - writes to an administrator user (profile, credentials, enabled, sessions, consents, attributes, memberships, permissions, deletion), an administrative group (settings, attributes, members, permissions, deletion), or an administrator client (settings, authentication, flows, redirect URIs, web origins, token settings, permissions, logo, deletion); - switches whether a client may request the administrative scopes, on or off, on any client; - reads an administrator client’s secret; - changes the email or the audit-log settings; - changes an administrative permission’s description. Granular scopes still read administrators. The refusal is answered after the request’s 400 and 404, writes nothing, carries `WWW-Authenticate: Bearer realm="goiabada", error="insufficient_scope", error_description="...", scope="authserver:manage"`, and leaves an `administrator_change_refused` audit record. Every operation this document secures declares a 403. The public client logo endpoint, which carries `security: []` and authorises nothing, is the one that does not. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on scope and administrative-policy refusals; other forbidden responses omit it. ### 404 Resource not found application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` ### 429 Rate limit reached. Every limit on the account API is counted by the token’s user and applies whether or not the rate limiter (GOIABADA_AUTHSERVER_RATELIMITER_ENABLED) is on. error_code is TOO_MANY_REQUESTS and a Retry-After header carries the window length in seconds. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **Retry-After** integer Seconds to wait before retrying ### 500 The request failed inside the server, most often a storage error. error_code is INTERNAL_SERVER_ERROR: no detail about the failure, only the request id an operator can find the log line by, and retrying is reasonable. Key rotation’s 500 also answers KEY_SET_INCOMPLETE, which names its cause and no retry clears. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` # Update own phone Source: https://goiabada.dev/reference/api/account/operations/updateaccountphone/ PUT /api/v1/account/phone Shell / cURL ```sh curl --request PUT \ --url http://localhost:9090/api/v1/account/phone \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "phoneCountryUniqueId": "example", "phoneNumber": "example" }' ``` - Auth server base URL{baseUrl}/api/v1/account/phone Update the authenticated user’s phone number ## Authorizations - **[BearerAuth](https://goiabada.dev/reference/api/account/#bearerauth)** ## Request Bodyrequired application/json object **phoneCountryUniqueId** An entry’s uniqueId from GET /api/v1/admin/phone-countries, e.g. USA_0 string **phoneNumber** Empty string to clear string ### Example generated ```json { "phoneCountryUniqueId": "example", "phoneNumber": "example" } ``` ## Responses ### 200 Phone updated application/json object **user** required object **id** required integer format: int64 **createdAt** required string format: date-time nullable **updatedAt** required string format: date-time nullable **enabled** required boolean **subject** required string format: uuid **username** required string **givenName** required string **middleName** required string **familyName** required string **nickname** required string **website** required string **gender** required “female”, “male” or “other”, or empty when unset string **email** required string format: email **emailVerified** required boolean **zoneInfoCountryName** required string **zoneInfo** required string **locale** required string **birthDate** required string format: date-time nullable **phoneNumberCountryUniqueId** required string **phoneNumberCountryCallingCode** required string **phoneNumber** required string **phoneNumberVerified** required boolean **addressLine1** required string **addressLine2** required string **addressLocality** required string **addressRegion** required string **addressPostalCode** required string **addressCountry** required ISO 3166-1 alpha-2 country code (e.g. US, BR), or empty string. string /^(\[A-Z]{2})?$/ **otpEnabled** required boolean #### Example generated ```json { "user": { "id": 1, "createdAt": "2026-04-15T12:00:00Z", "updatedAt": "2026-04-15T12:00:00Z", "enabled": true, "subject": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0", "username": "example", "givenName": "example", "middleName": "example", "familyName": "example", "nickname": "example", "website": "example", "gender": "example", "email": "hello@example.com", "emailVerified": true, "zoneInfoCountryName": "example", "zoneInfo": "example", "locale": "example", "birthDate": "2026-04-15T12:00:00Z", "phoneNumberCountryUniqueId": "example", "phoneNumberCountryCallingCode": "example", "phoneNumber": "example", "phoneNumberVerified": true, "addressLine1": "example", "addressLine2": "example", "addressLocality": "example", "addressRegion": "example", "addressPostalCode": "example", "addressCountry": "example", "otpEnabled": true } } ``` ### 400 Invalid request body or parameters application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge when token transport is malformed; ordinary validation errors omit it. ### 401 Missing or invalid bearer token application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on token refusals. An operation may also return a handler-level 401 without this header. ### 403 The token is valid but does not authorise this request. Four checks answer here, and a caller distinguishes them by error_code. INSUFFICIENT_SCOPE comes from the route’s authorisation middleware, before the handler runs: an Admin operation needs one of the scopes its route names, and an Account operation needs `authserver:manage-account`. USER_CONTEXT_REQUIRED comes from the same middleware on the Account operations, which also require the token to have been issued for a user: one obtained through the client credentials grant carries no auth_time claim and is refused here. FORBIDDEN comes from the handler, on the account endpoints that resolve a resource by id and then check it belongs to the caller, so a valid token for the wrong user reaches this rather than a 404. MANAGE_SCOPE_REQUIRED comes from the handler, on the Admin operations the administrative policy guards. Only `authserver:manage` creates an administrator, changes one, or changes what reaches one, so no granular scope will ever be enough: the remedy is a token holding `authserver:manage`, not requesting the route’s scope as for INSUFFICIENT_SCOPE. An administrator is a user, group or client holding any of the six administrative permissions (`manage`, `admin-read`, `manage-users`, `manage-clients`, `manage-settings`, `browser-sessions`), a user directly or through any of its groups, the admin console’s own client whatever it holds, and a client allowed to request the administrative scopes. A granular scope is refused here when it: - grants or revokes an administrative permission, directly or by moving a user into or out of a group that holds one, or deletes such a group; - writes to an administrator user (profile, credentials, enabled, sessions, consents, attributes, memberships, permissions, deletion), an administrative group (settings, attributes, members, permissions, deletion), or an administrator client (settings, authentication, flows, redirect URIs, web origins, token settings, permissions, logo, deletion); - switches whether a client may request the administrative scopes, on or off, on any client; - reads an administrator client’s secret; - changes the email or the audit-log settings; - changes an administrative permission’s description. Granular scopes still read administrators. The refusal is answered after the request’s 400 and 404, writes nothing, carries `WWW-Authenticate: Bearer realm="goiabada", error="insufficient_scope", error_description="...", scope="authserver:manage"`, and leaves an `administrator_change_refused` audit record. Every operation this document secures declares a 403. The public client logo endpoint, which carries `security: []` and authorises nothing, is the one that does not. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on scope and administrative-policy refusals; other forbidden responses omit it. ### 404 Resource not found application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` ### 500 The request failed inside the server, most often a storage error. error_code is INTERNAL_SERVER_ERROR: no detail about the failure, only the request id an operator can find the log line by, and retrying is reasonable. Key rotation’s 500 also answers KEY_SET_INCOMPLETE, which names its cause and no retry clears. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` # Update own address Source: https://goiabada.dev/reference/api/account/operations/updateaccountaddress/ PUT /api/v1/account/address Shell / cURL ```sh curl --request PUT \ --url http://localhost:9090/api/v1/account/address \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "addressLine1": "example", "addressLine2": "example", "addressLocality": "example", "addressRegion": "example", "addressPostalCode": "example", "addressCountry": "example" }' ``` - Auth server base URL{baseUrl}/api/v1/account/address Update the authenticated user’s address ## Authorizations - **[BearerAuth](https://goiabada.dev/reference/api/account/#bearerauth)** ## Request Bodyrequired application/json object **addressLine1** string **addressLine2** string **addressLocality** string **addressRegion** string **addressPostalCode** string **addressCountry** ISO 3166-1 alpha-2 country code (e.g. US, BR), or empty string. string /^(\[A-Z]{2})?$/ ### Example generated ```json { "addressLine1": "example", "addressLine2": "example", "addressLocality": "example", "addressRegion": "example", "addressPostalCode": "example", "addressCountry": "example" } ``` ## Responses ### 200 Address updated application/json object **user** required object **id** required integer format: int64 **createdAt** required string format: date-time nullable **updatedAt** required string format: date-time nullable **enabled** required boolean **subject** required string format: uuid **username** required string **givenName** required string **middleName** required string **familyName** required string **nickname** required string **website** required string **gender** required “female”, “male” or “other”, or empty when unset string **email** required string format: email **emailVerified** required boolean **zoneInfoCountryName** required string **zoneInfo** required string **locale** required string **birthDate** required string format: date-time nullable **phoneNumberCountryUniqueId** required string **phoneNumberCountryCallingCode** required string **phoneNumber** required string **phoneNumberVerified** required boolean **addressLine1** required string **addressLine2** required string **addressLocality** required string **addressRegion** required string **addressPostalCode** required string **addressCountry** required ISO 3166-1 alpha-2 country code (e.g. US, BR), or empty string. string /^(\[A-Z]{2})?$/ **otpEnabled** required boolean #### Example generated ```json { "user": { "id": 1, "createdAt": "2026-04-15T12:00:00Z", "updatedAt": "2026-04-15T12:00:00Z", "enabled": true, "subject": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0", "username": "example", "givenName": "example", "middleName": "example", "familyName": "example", "nickname": "example", "website": "example", "gender": "example", "email": "hello@example.com", "emailVerified": true, "zoneInfoCountryName": "example", "zoneInfo": "example", "locale": "example", "birthDate": "2026-04-15T12:00:00Z", "phoneNumberCountryUniqueId": "example", "phoneNumberCountryCallingCode": "example", "phoneNumber": "example", "phoneNumberVerified": true, "addressLine1": "example", "addressLine2": "example", "addressLocality": "example", "addressRegion": "example", "addressPostalCode": "example", "addressCountry": "example", "otpEnabled": true } } ``` ### 400 Invalid request body or parameters application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge when token transport is malformed; ordinary validation errors omit it. ### 401 Missing or invalid bearer token application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on token refusals. An operation may also return a handler-level 401 without this header. ### 403 The token is valid but does not authorise this request. Four checks answer here, and a caller distinguishes them by error_code. INSUFFICIENT_SCOPE comes from the route’s authorisation middleware, before the handler runs: an Admin operation needs one of the scopes its route names, and an Account operation needs `authserver:manage-account`. USER_CONTEXT_REQUIRED comes from the same middleware on the Account operations, which also require the token to have been issued for a user: one obtained through the client credentials grant carries no auth_time claim and is refused here. FORBIDDEN comes from the handler, on the account endpoints that resolve a resource by id and then check it belongs to the caller, so a valid token for the wrong user reaches this rather than a 404. MANAGE_SCOPE_REQUIRED comes from the handler, on the Admin operations the administrative policy guards. Only `authserver:manage` creates an administrator, changes one, or changes what reaches one, so no granular scope will ever be enough: the remedy is a token holding `authserver:manage`, not requesting the route’s scope as for INSUFFICIENT_SCOPE. An administrator is a user, group or client holding any of the six administrative permissions (`manage`, `admin-read`, `manage-users`, `manage-clients`, `manage-settings`, `browser-sessions`), a user directly or through any of its groups, the admin console’s own client whatever it holds, and a client allowed to request the administrative scopes. A granular scope is refused here when it: - grants or revokes an administrative permission, directly or by moving a user into or out of a group that holds one, or deletes such a group; - writes to an administrator user (profile, credentials, enabled, sessions, consents, attributes, memberships, permissions, deletion), an administrative group (settings, attributes, members, permissions, deletion), or an administrator client (settings, authentication, flows, redirect URIs, web origins, token settings, permissions, logo, deletion); - switches whether a client may request the administrative scopes, on or off, on any client; - reads an administrator client’s secret; - changes the email or the audit-log settings; - changes an administrative permission’s description. Granular scopes still read administrators. The refusal is answered after the request’s 400 and 404, writes nothing, carries `WWW-Authenticate: Bearer realm="goiabada", error="insufficient_scope", error_description="...", scope="authserver:manage"`, and leaves an `administrator_change_refused` audit record. Every operation this document secures declares a 403. The public client logo endpoint, which carries `security: []` and authorises nothing, is the one that does not. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on scope and administrative-policy refusals; other forbidden responses omit it. ### 404 Resource not found application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` ### 500 The request failed inside the server, most often a storage error. error_code is INTERNAL_SERVER_ERROR: no detail about the failure, only the request id an operator can find the log line by, and retrying is reasonable. Key rotation’s 500 also answers KEY_SET_INCOMPLETE, which names its cause and no retry clears. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` # Change own password Source: https://goiabada.dev/reference/api/account/operations/updateaccountpassword/ PUT /api/v1/account/password Shell / cURL ```sh curl --request PUT \ --url http://localhost:9090/api/v1/account/password \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "currentPassword": "example", "newPassword": "example" }' ``` - Auth server base URL{baseUrl}/api/v1/account/password Change the password. Requires the current password: a wrong one is a 400 with error_code AUTHENTICATION_FAILED that counts against the failure limit shared with the email and OTP changes. The new password must meet the configured password policy. ## Authorizations - **[BearerAuth](https://goiabada.dev/reference/api/account/#bearerauth)** ## Request Bodyrequired application/json object **currentPassword** required string **newPassword** required string ### Example generated ```json { "currentPassword": "example", "newPassword": "example" } ``` ## Responses ### 200 Password changed application/json object **user** required object **id** required integer format: int64 **createdAt** required string format: date-time nullable **updatedAt** required string format: date-time nullable **enabled** required boolean **subject** required string format: uuid **username** required string **givenName** required string **middleName** required string **familyName** required string **nickname** required string **website** required string **gender** required “female”, “male” or “other”, or empty when unset string **email** required string format: email **emailVerified** required boolean **zoneInfoCountryName** required string **zoneInfo** required string **locale** required string **birthDate** required string format: date-time nullable **phoneNumberCountryUniqueId** required string **phoneNumberCountryCallingCode** required string **phoneNumber** required string **phoneNumberVerified** required boolean **addressLine1** required string **addressLine2** required string **addressLocality** required string **addressRegion** required string **addressPostalCode** required string **addressCountry** required ISO 3166-1 alpha-2 country code (e.g. US, BR), or empty string. string /^(\[A-Z]{2})?$/ **otpEnabled** required boolean #### Example generated ```json { "user": { "id": 1, "createdAt": "2026-04-15T12:00:00Z", "updatedAt": "2026-04-15T12:00:00Z", "enabled": true, "subject": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0", "username": "example", "givenName": "example", "middleName": "example", "familyName": "example", "nickname": "example", "website": "example", "gender": "example", "email": "hello@example.com", "emailVerified": true, "zoneInfoCountryName": "example", "zoneInfo": "example", "locale": "example", "birthDate": "2026-04-15T12:00:00Z", "phoneNumberCountryUniqueId": "example", "phoneNumberCountryCallingCode": "example", "phoneNumber": "example", "phoneNumberVerified": true, "addressLine1": "example", "addressLine2": "example", "addressLocality": "example", "addressRegion": "example", "addressPostalCode": "example", "addressCountry": "example", "otpEnabled": true } } ``` ### 400 Invalid request body or parameters application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge when token transport is malformed; ordinary validation errors omit it. ### 401 Missing or invalid bearer token application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on token refusals. An operation may also return a handler-level 401 without this header. ### 403 The token is valid but does not authorise this request. Four checks answer here, and a caller distinguishes them by error_code. INSUFFICIENT_SCOPE comes from the route’s authorisation middleware, before the handler runs: an Admin operation needs one of the scopes its route names, and an Account operation needs `authserver:manage-account`. USER_CONTEXT_REQUIRED comes from the same middleware on the Account operations, which also require the token to have been issued for a user: one obtained through the client credentials grant carries no auth_time claim and is refused here. FORBIDDEN comes from the handler, on the account endpoints that resolve a resource by id and then check it belongs to the caller, so a valid token for the wrong user reaches this rather than a 404. MANAGE_SCOPE_REQUIRED comes from the handler, on the Admin operations the administrative policy guards. Only `authserver:manage` creates an administrator, changes one, or changes what reaches one, so no granular scope will ever be enough: the remedy is a token holding `authserver:manage`, not requesting the route’s scope as for INSUFFICIENT_SCOPE. An administrator is a user, group or client holding any of the six administrative permissions (`manage`, `admin-read`, `manage-users`, `manage-clients`, `manage-settings`, `browser-sessions`), a user directly or through any of its groups, the admin console’s own client whatever it holds, and a client allowed to request the administrative scopes. A granular scope is refused here when it: - grants or revokes an administrative permission, directly or by moving a user into or out of a group that holds one, or deletes such a group; - writes to an administrator user (profile, credentials, enabled, sessions, consents, attributes, memberships, permissions, deletion), an administrative group (settings, attributes, members, permissions, deletion), or an administrator client (settings, authentication, flows, redirect URIs, web origins, token settings, permissions, logo, deletion); - switches whether a client may request the administrative scopes, on or off, on any client; - reads an administrator client’s secret; - changes the email or the audit-log settings; - changes an administrative permission’s description. Granular scopes still read administrators. The refusal is answered after the request’s 400 and 404, writes nothing, carries `WWW-Authenticate: Bearer realm="goiabada", error="insufficient_scope", error_description="...", scope="authserver:manage"`, and leaves an `administrator_change_refused` audit record. Every operation this document secures declares a 403. The public client logo endpoint, which carries `security: []` and authorises nothing, is the one that does not. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on scope and administrative-policy refusals; other forbidden responses omit it. ### 404 Resource not found application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` ### 429 Rate limit reached. Every limit on the account API is counted by the token’s user and applies whether or not the rate limiter (GOIABADA_AUTHSERVER_RATELIMITER_ENABLED) is on. error_code is TOO_MANY_REQUESTS and a Retry-After header carries the window length in seconds. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **Retry-After** integer Seconds to wait before retrying ### 500 The request failed inside the server, most often a storage error. error_code is INTERNAL_SERVER_ERROR: no detail about the failure, only the request id an operator can find the log line by, and retrying is reasonable. Key rotation’s 500 also answers KEY_SET_INCOMPLETE, which names its cause and no retry clears. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` # Get own profile picture info Source: https://goiabada.dev/reference/api/account/operations/getaccountprofilepictureinfo/ GET /api/v1/account/profile-picture Shell / cURL ```sh curl --request GET \ --url http://localhost:9090/api/v1/account/profile-picture \ --header 'Authorization: Bearer ' ``` - Auth server base URL{baseUrl}/api/v1/account/profile-picture Check whether the authenticated user has a profile picture and get the picture URL ## Authorizations - **[BearerAuth](https://goiabada.dev/reference/api/account/#bearerauth)** ## Responses ### 200 Profile picture info application/json object **hasPicture** required boolean **pictureUrl** Public URL to serve the picture (only present when hasPicture is true) string #### Example generated ```json { "hasPicture": true, "pictureUrl": "example" } ``` ### 401 Missing or invalid bearer token application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on token refusals. An operation may also return a handler-level 401 without this header. ### 403 The token is valid but does not authorise this request. Four checks answer here, and a caller distinguishes them by error_code. INSUFFICIENT_SCOPE comes from the route’s authorisation middleware, before the handler runs: an Admin operation needs one of the scopes its route names, and an Account operation needs `authserver:manage-account`. USER_CONTEXT_REQUIRED comes from the same middleware on the Account operations, which also require the token to have been issued for a user: one obtained through the client credentials grant carries no auth_time claim and is refused here. FORBIDDEN comes from the handler, on the account endpoints that resolve a resource by id and then check it belongs to the caller, so a valid token for the wrong user reaches this rather than a 404. MANAGE_SCOPE_REQUIRED comes from the handler, on the Admin operations the administrative policy guards. Only `authserver:manage` creates an administrator, changes one, or changes what reaches one, so no granular scope will ever be enough: the remedy is a token holding `authserver:manage`, not requesting the route’s scope as for INSUFFICIENT_SCOPE. An administrator is a user, group or client holding any of the six administrative permissions (`manage`, `admin-read`, `manage-users`, `manage-clients`, `manage-settings`, `browser-sessions`), a user directly or through any of its groups, the admin console’s own client whatever it holds, and a client allowed to request the administrative scopes. A granular scope is refused here when it: - grants or revokes an administrative permission, directly or by moving a user into or out of a group that holds one, or deletes such a group; - writes to an administrator user (profile, credentials, enabled, sessions, consents, attributes, memberships, permissions, deletion), an administrative group (settings, attributes, members, permissions, deletion), or an administrator client (settings, authentication, flows, redirect URIs, web origins, token settings, permissions, logo, deletion); - switches whether a client may request the administrative scopes, on or off, on any client; - reads an administrator client’s secret; - changes the email or the audit-log settings; - changes an administrative permission’s description. Granular scopes still read administrators. The refusal is answered after the request’s 400 and 404, writes nothing, carries `WWW-Authenticate: Bearer realm="goiabada", error="insufficient_scope", error_description="...", scope="authserver:manage"`, and leaves an `administrator_change_refused` audit record. Every operation this document secures declares a 403. The public client logo endpoint, which carries `security: []` and authorises nothing, is the one that does not. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on scope and administrative-policy refusals; other forbidden responses omit it. ### 404 Resource not found application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` ### 500 The request failed inside the server, most often a storage error. error_code is INTERNAL_SERVER_ERROR: no detail about the failure, only the request id an operator can find the log line by, and retrying is reasonable. Key rotation’s 500 also answers KEY_SET_INCOMPLETE, which names its cause and no retry clears. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` # Upload own profile picture Source: https://goiabada.dev/reference/api/account/operations/uploadaccountprofilepicture/ POST /api/v1/account/profile-picture Shell / cURL ```sh curl --request POST \ --url http://localhost:9090/api/v1/account/profile-picture \ --header 'Authorization: Bearer ' \ --header 'Content-Type: multipart/form-data' \ --form picture=@file ``` - Auth server base URL{baseUrl}/api/v1/account/profile-picture Upload or replace the authenticated user’s profile picture. Send it as `multipart/form-data` in the field `picture`: a JPEG, PNG, GIF or WebP image, from 10x10 to 512x512 pixels, of at most `GOIABADA_PROFILE_PICTURE_MAX_SIZE_BYTES` bytes, 3 MiB by default. ## Authorizations - **[BearerAuth](https://goiabada.dev/reference/api/account/#bearerauth)** ## Request Bodyrequired multipart/form-data object **picture** required Profile picture file (JPEG, PNG, GIF, or WebP) string format: binary ## Responses ### 200 Profile picture uploaded application/json object **success** required boolean **pictureUrl** required Public URL to the uploaded picture string #### Example generated ```json { "success": true, "pictureUrl": "example" } ``` ### 400 Invalid request body or parameters application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge when token transport is malformed; ordinary validation errors omit it. ### 401 Missing or invalid bearer token application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on token refusals. An operation may also return a handler-level 401 without this header. ### 403 The token is valid but does not authorise this request. Four checks answer here, and a caller distinguishes them by error_code. INSUFFICIENT_SCOPE comes from the route’s authorisation middleware, before the handler runs: an Admin operation needs one of the scopes its route names, and an Account operation needs `authserver:manage-account`. USER_CONTEXT_REQUIRED comes from the same middleware on the Account operations, which also require the token to have been issued for a user: one obtained through the client credentials grant carries no auth_time claim and is refused here. FORBIDDEN comes from the handler, on the account endpoints that resolve a resource by id and then check it belongs to the caller, so a valid token for the wrong user reaches this rather than a 404. MANAGE_SCOPE_REQUIRED comes from the handler, on the Admin operations the administrative policy guards. Only `authserver:manage` creates an administrator, changes one, or changes what reaches one, so no granular scope will ever be enough: the remedy is a token holding `authserver:manage`, not requesting the route’s scope as for INSUFFICIENT_SCOPE. An administrator is a user, group or client holding any of the six administrative permissions (`manage`, `admin-read`, `manage-users`, `manage-clients`, `manage-settings`, `browser-sessions`), a user directly or through any of its groups, the admin console’s own client whatever it holds, and a client allowed to request the administrative scopes. A granular scope is refused here when it: - grants or revokes an administrative permission, directly or by moving a user into or out of a group that holds one, or deletes such a group; - writes to an administrator user (profile, credentials, enabled, sessions, consents, attributes, memberships, permissions, deletion), an administrative group (settings, attributes, members, permissions, deletion), or an administrator client (settings, authentication, flows, redirect URIs, web origins, token settings, permissions, logo, deletion); - switches whether a client may request the administrative scopes, on or off, on any client; - reads an administrator client’s secret; - changes the email or the audit-log settings; - changes an administrative permission’s description. Granular scopes still read administrators. The refusal is answered after the request’s 400 and 404, writes nothing, carries `WWW-Authenticate: Bearer realm="goiabada", error="insufficient_scope", error_description="...", scope="authserver:manage"`, and leaves an `administrator_change_refused` audit record. Every operation this document secures declares a 403. The public client logo endpoint, which carries `security: []` and authorises nothing, is the one that does not. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on scope and administrative-policy refusals; other forbidden responses omit it. ### 404 Resource not found application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` ### 500 The request failed inside the server, most often a storage error. error_code is INTERNAL_SERVER_ERROR: no detail about the failure, only the request id an operator can find the log line by, and retrying is reasonable. Key rotation’s 500 also answers KEY_SET_INCOMPLETE, which names its cause and no retry clears. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` # Delete own profile picture Source: https://goiabada.dev/reference/api/account/operations/deleteaccountprofilepicture/ DELETE /api/v1/account/profile-picture Shell / cURL ```sh curl --request DELETE \ --url http://localhost:9090/api/v1/account/profile-picture \ --header 'Authorization: Bearer ' ``` - Auth server base URL{baseUrl}/api/v1/account/profile-picture Remove the authenticated user’s profile picture ## Authorizations - **[BearerAuth](https://goiabada.dev/reference/api/account/#bearerauth)** ## Responses ### 200 Profile picture deleted application/json object **success** required boolean #### Example generated ```json { "success": true } ``` ### 401 Missing or invalid bearer token application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on token refusals. An operation may also return a handler-level 401 without this header. ### 403 The token is valid but does not authorise this request. Four checks answer here, and a caller distinguishes them by error_code. INSUFFICIENT_SCOPE comes from the route’s authorisation middleware, before the handler runs: an Admin operation needs one of the scopes its route names, and an Account operation needs `authserver:manage-account`. USER_CONTEXT_REQUIRED comes from the same middleware on the Account operations, which also require the token to have been issued for a user: one obtained through the client credentials grant carries no auth_time claim and is refused here. FORBIDDEN comes from the handler, on the account endpoints that resolve a resource by id and then check it belongs to the caller, so a valid token for the wrong user reaches this rather than a 404. MANAGE_SCOPE_REQUIRED comes from the handler, on the Admin operations the administrative policy guards. Only `authserver:manage` creates an administrator, changes one, or changes what reaches one, so no granular scope will ever be enough: the remedy is a token holding `authserver:manage`, not requesting the route’s scope as for INSUFFICIENT_SCOPE. An administrator is a user, group or client holding any of the six administrative permissions (`manage`, `admin-read`, `manage-users`, `manage-clients`, `manage-settings`, `browser-sessions`), a user directly or through any of its groups, the admin console’s own client whatever it holds, and a client allowed to request the administrative scopes. A granular scope is refused here when it: - grants or revokes an administrative permission, directly or by moving a user into or out of a group that holds one, or deletes such a group; - writes to an administrator user (profile, credentials, enabled, sessions, consents, attributes, memberships, permissions, deletion), an administrative group (settings, attributes, members, permissions, deletion), or an administrator client (settings, authentication, flows, redirect URIs, web origins, token settings, permissions, logo, deletion); - switches whether a client may request the administrative scopes, on or off, on any client; - reads an administrator client’s secret; - changes the email or the audit-log settings; - changes an administrative permission’s description. Granular scopes still read administrators. The refusal is answered after the request’s 400 and 404, writes nothing, carries `WWW-Authenticate: Bearer realm="goiabada", error="insufficient_scope", error_description="...", scope="authserver:manage"`, and leaves an `administrator_change_refused` audit record. Every operation this document secures declares a 403. The public client logo endpoint, which carries `security: []` and authorises nothing, is the one that does not. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on scope and administrative-policy refusals; other forbidden responses omit it. ### 404 Resource not found application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` ### 500 The request failed inside the server, most often a storage error. error_code is INTERNAL_SERVER_ERROR: no detail about the failure, only the request id an operator can find the log line by, and retrying is reasonable. Key rotation’s 500 also answers KEY_SET_INCOMPLETE, which names its cause and no retry clears. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` # Start OTP enrollment Source: https://goiabada.dev/reference/api/account/operations/getaccountotpenrollment/ GET /api/v1/account/otp/enrollment Shell / cURL ```sh curl --request GET \ --url http://localhost:9090/api/v1/account/otp/enrollment \ --header 'Authorization: Bearer ' ``` - Auth server base URL{baseUrl}/api/v1/account/otp/enrollment Issues the TOTP enrollment for the authenticated user and returns the QR code and secret to set up an authenticator with. Fails if OTP is already enabled. The server records what it issued, and `PUT /api/v1/account/otp` enrolls that seed and no other. While an enrollment is pending the call is idempotent: repeating it returns the same secret and the same QR code, so reloading an enrollment page does not invalidate a code the user has already scanned. A pending enrollment lasts 15 minutes, after which the next call issues a new one. ## Authorizations - **[BearerAuth](https://goiabada.dev/reference/api/account/#bearerauth)** ## Responses ### 200 OTP enrollment data application/json object **base64Image** required QR code as base64-encoded PNG string **secretKey** required TOTP secret key (base32), for a user who cannot scan the QR code. The server has recorded this value; do not substitute another when enabling. string #### Example generated ```json { "base64Image": "example", "secretKey": "example" } ``` ### 400 Invalid request body or parameters application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge when token transport is malformed; ordinary validation errors omit it. ### 401 Missing or invalid bearer token application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on token refusals. An operation may also return a handler-level 401 without this header. ### 403 The token is valid but does not authorise this request. Four checks answer here, and a caller distinguishes them by error_code. INSUFFICIENT_SCOPE comes from the route’s authorisation middleware, before the handler runs: an Admin operation needs one of the scopes its route names, and an Account operation needs `authserver:manage-account`. USER_CONTEXT_REQUIRED comes from the same middleware on the Account operations, which also require the token to have been issued for a user: one obtained through the client credentials grant carries no auth_time claim and is refused here. FORBIDDEN comes from the handler, on the account endpoints that resolve a resource by id and then check it belongs to the caller, so a valid token for the wrong user reaches this rather than a 404. MANAGE_SCOPE_REQUIRED comes from the handler, on the Admin operations the administrative policy guards. Only `authserver:manage` creates an administrator, changes one, or changes what reaches one, so no granular scope will ever be enough: the remedy is a token holding `authserver:manage`, not requesting the route’s scope as for INSUFFICIENT_SCOPE. An administrator is a user, group or client holding any of the six administrative permissions (`manage`, `admin-read`, `manage-users`, `manage-clients`, `manage-settings`, `browser-sessions`), a user directly or through any of its groups, the admin console’s own client whatever it holds, and a client allowed to request the administrative scopes. A granular scope is refused here when it: - grants or revokes an administrative permission, directly or by moving a user into or out of a group that holds one, or deletes such a group; - writes to an administrator user (profile, credentials, enabled, sessions, consents, attributes, memberships, permissions, deletion), an administrative group (settings, attributes, members, permissions, deletion), or an administrator client (settings, authentication, flows, redirect URIs, web origins, token settings, permissions, logo, deletion); - switches whether a client may request the administrative scopes, on or off, on any client; - reads an administrator client’s secret; - changes the email or the audit-log settings; - changes an administrative permission’s description. Granular scopes still read administrators. The refusal is answered after the request’s 400 and 404, writes nothing, carries `WWW-Authenticate: Bearer realm="goiabada", error="insufficient_scope", error_description="...", scope="authserver:manage"`, and leaves an `administrator_change_refused` audit record. Every operation this document secures declares a 403. The public client logo endpoint, which carries `security: []` and authorises nothing, is the one that does not. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on scope and administrative-policy refusals; other forbidden responses omit it. ### 404 Resource not found application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` ### 500 The request failed inside the server, most often a storage error. error_code is INTERNAL_SERVER_ERROR: no detail about the failure, only the request id an operator can find the log line by, and retrying is reasonable. Key rotation’s 500 also answers KEY_SET_INCOMPLETE, which names its cause and no retry clears. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` # Enable/disable OTP Source: https://goiabada.dev/reference/api/account/operations/updateaccountotp/ PUT /api/v1/account/otp Shell / cURL ```sh curl --request PUT \ --url http://localhost:9090/api/v1/account/otp \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "enabled": true, "password": "example", "otpCode": "example" }' ``` - Auth server base URL{baseUrl}/api/v1/account/otp Enables or disables TOTP for the authenticated user. Requires password verification, and when enabling, a current code from the authenticator. Enabling completes the enrollment issued by `GET /api/v1/account/otp/enrollment`: the code is verified against the seed the server issued and that seed is what gets enrolled. Call the enrollment endpoint first. Returns 400 if no enrollment is pending or the one started has expired (`OTP_ENROLLMENT_NOT_PENDING`). This request must not carry a `secretKey`. One that does is refused with 400 (`SECRET_KEY_NOT_ACCEPTED`) rather than having the field ignored, because a caller sending one believes it is choosing which authenticator is installed, and it is not. The change lands only while the authenticator is still as this request read it. When another request changed it in between, the answer is read from the account as it is now: 400 (`OTP_ALREADY_ENABLED` or `OTP_NOT_ENABLED`) when OTP is already in the state asked for, otherwise 409 (`CONCURRENT_UPDATE`) with nothing saved. The code submitted with an enable is spent either way. ## Authorizations - **[BearerAuth](https://goiabada.dev/reference/api/account/#bearerauth)** ## Request Bodyrequired application/json object **enabled** required boolean **password** required Current password for verification string **otpCode** 6-digit TOTP code (required when enabling), generated from the secret returned by GET /api/v1/account/otp/enrollment string ### Example generated ```json { "enabled": true, "password": "example", "otpCode": "example" } ``` ## Responses ### 200 OTP status updated application/json object **user** required object **id** required integer format: int64 **createdAt** required string format: date-time nullable **updatedAt** required string format: date-time nullable **enabled** required boolean **subject** required string format: uuid **username** required string **givenName** required string **middleName** required string **familyName** required string **nickname** required string **website** required string **gender** required “female”, “male” or “other”, or empty when unset string **email** required string format: email **emailVerified** required boolean **zoneInfoCountryName** required string **zoneInfo** required string **locale** required string **birthDate** required string format: date-time nullable **phoneNumberCountryUniqueId** required string **phoneNumberCountryCallingCode** required string **phoneNumber** required string **phoneNumberVerified** required boolean **addressLine1** required string **addressLine2** required string **addressLocality** required string **addressRegion** required string **addressPostalCode** required string **addressCountry** required ISO 3166-1 alpha-2 country code (e.g. US, BR), or empty string. string /^(\[A-Z]{2})?$/ **otpEnabled** required boolean #### Example generated ```json { "user": { "id": 1, "createdAt": "2026-04-15T12:00:00Z", "updatedAt": "2026-04-15T12:00:00Z", "enabled": true, "subject": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0", "username": "example", "givenName": "example", "middleName": "example", "familyName": "example", "nickname": "example", "website": "example", "gender": "example", "email": "hello@example.com", "emailVerified": true, "zoneInfoCountryName": "example", "zoneInfo": "example", "locale": "example", "birthDate": "2026-04-15T12:00:00Z", "phoneNumberCountryUniqueId": "example", "phoneNumberCountryCallingCode": "example", "phoneNumber": "example", "phoneNumberVerified": true, "addressLine1": "example", "addressLine2": "example", "addressLocality": "example", "addressRegion": "example", "addressPostalCode": "example", "addressCountry": "example", "otpEnabled": true } } ``` ### 400 Invalid request body or parameters application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge when token transport is malformed; ordinary validation errors omit it. ### 401 Missing or invalid bearer token application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on token refusals. An operation may also return a handler-level 401 without this header. ### 403 The token is valid but does not authorise this request. Four checks answer here, and a caller distinguishes them by error_code. INSUFFICIENT_SCOPE comes from the route’s authorisation middleware, before the handler runs: an Admin operation needs one of the scopes its route names, and an Account operation needs `authserver:manage-account`. USER_CONTEXT_REQUIRED comes from the same middleware on the Account operations, which also require the token to have been issued for a user: one obtained through the client credentials grant carries no auth_time claim and is refused here. FORBIDDEN comes from the handler, on the account endpoints that resolve a resource by id and then check it belongs to the caller, so a valid token for the wrong user reaches this rather than a 404. MANAGE_SCOPE_REQUIRED comes from the handler, on the Admin operations the administrative policy guards. Only `authserver:manage` creates an administrator, changes one, or changes what reaches one, so no granular scope will ever be enough: the remedy is a token holding `authserver:manage`, not requesting the route’s scope as for INSUFFICIENT_SCOPE. An administrator is a user, group or client holding any of the six administrative permissions (`manage`, `admin-read`, `manage-users`, `manage-clients`, `manage-settings`, `browser-sessions`), a user directly or through any of its groups, the admin console’s own client whatever it holds, and a client allowed to request the administrative scopes. A granular scope is refused here when it: - grants or revokes an administrative permission, directly or by moving a user into or out of a group that holds one, or deletes such a group; - writes to an administrator user (profile, credentials, enabled, sessions, consents, attributes, memberships, permissions, deletion), an administrative group (settings, attributes, members, permissions, deletion), or an administrator client (settings, authentication, flows, redirect URIs, web origins, token settings, permissions, logo, deletion); - switches whether a client may request the administrative scopes, on or off, on any client; - reads an administrator client’s secret; - changes the email or the audit-log settings; - changes an administrative permission’s description. Granular scopes still read administrators. The refusal is answered after the request’s 400 and 404, writes nothing, carries `WWW-Authenticate: Bearer realm="goiabada", error="insufficient_scope", error_description="...", scope="authserver:manage"`, and leaves an `administrator_change_refused` audit record. Every operation this document secures declares a 403. The public client logo endpoint, which carries `security: []` and authorises nothing, is the one that does not. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on scope and administrative-policy refusals; other forbidden responses omit it. ### 404 Resource not found application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` ### 409 Another request changed the account’s authenticator after this one read it. error_code is CONCURRENT_UPDATE; nothing was saved. Read the account again and retry. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` ### 429 Rate limit reached. Every limit on the account API is counted by the token’s user and applies whether or not the rate limiter (GOIABADA_AUTHSERVER_RATELIMITER_ENABLED) is on. error_code is TOO_MANY_REQUESTS and a Retry-After header carries the window length in seconds. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **Retry-After** integer Seconds to wait before retrying ### 500 The request failed inside the server, most often a storage error. error_code is INTERNAL_SERVER_ERROR: no detail about the failure, only the request id an operator can find the log line by, and retrying is reasonable. Key rotation’s 500 also answers KEY_SET_INCOMPLETE, which names its cause and no retry clears. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` # List own consents Source: https://goiabada.dev/reference/api/account/operations/getaccountconsents/ GET /api/v1/account/consents Shell / cURL ```sh curl --request GET \ --url http://localhost:9090/api/v1/account/consents \ --header 'Authorization: Bearer ' ``` - Auth server base URL{baseUrl}/api/v1/account/consents Get all OAuth consents granted by the authenticated user ## Authorizations - **[BearerAuth](https://goiabada.dev/reference/api/account/#bearerauth)** ## Responses ### 200 User consents application/json object **consents** required Null when the user has no consents. Array\ nullable object **id** required integer format: int64 **createdAt** required string format: date-time nullable **updatedAt** required string format: date-time nullable **clientId** required integer format: int64 **userId** required integer format: int64 **scope** required string **grantedAt** required string format: date-time nullable **clientIdentifier** required string **clientDescription** required string #### Example generated ```json { "consents": [ { "id": 1, "createdAt": "2026-04-15T12:00:00Z", "updatedAt": "2026-04-15T12:00:00Z", "clientId": 1, "userId": 1, "scope": "example", "grantedAt": "2026-04-15T12:00:00Z", "clientIdentifier": "example", "clientDescription": "example" } ] } ``` ### 401 Missing or invalid bearer token application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on token refusals. An operation may also return a handler-level 401 without this header. ### 403 The token is valid but does not authorise this request. Four checks answer here, and a caller distinguishes them by error_code. INSUFFICIENT_SCOPE comes from the route’s authorisation middleware, before the handler runs: an Admin operation needs one of the scopes its route names, and an Account operation needs `authserver:manage-account`. USER_CONTEXT_REQUIRED comes from the same middleware on the Account operations, which also require the token to have been issued for a user: one obtained through the client credentials grant carries no auth_time claim and is refused here. FORBIDDEN comes from the handler, on the account endpoints that resolve a resource by id and then check it belongs to the caller, so a valid token for the wrong user reaches this rather than a 404. MANAGE_SCOPE_REQUIRED comes from the handler, on the Admin operations the administrative policy guards. Only `authserver:manage` creates an administrator, changes one, or changes what reaches one, so no granular scope will ever be enough: the remedy is a token holding `authserver:manage`, not requesting the route’s scope as for INSUFFICIENT_SCOPE. An administrator is a user, group or client holding any of the six administrative permissions (`manage`, `admin-read`, `manage-users`, `manage-clients`, `manage-settings`, `browser-sessions`), a user directly or through any of its groups, the admin console’s own client whatever it holds, and a client allowed to request the administrative scopes. A granular scope is refused here when it: - grants or revokes an administrative permission, directly or by moving a user into or out of a group that holds one, or deletes such a group; - writes to an administrator user (profile, credentials, enabled, sessions, consents, attributes, memberships, permissions, deletion), an administrative group (settings, attributes, members, permissions, deletion), or an administrator client (settings, authentication, flows, redirect URIs, web origins, token settings, permissions, logo, deletion); - switches whether a client may request the administrative scopes, on or off, on any client; - reads an administrator client’s secret; - changes the email or the audit-log settings; - changes an administrative permission’s description. Granular scopes still read administrators. The refusal is answered after the request’s 400 and 404, writes nothing, carries `WWW-Authenticate: Bearer realm="goiabada", error="insufficient_scope", error_description="...", scope="authserver:manage"`, and leaves an `administrator_change_refused` audit record. Every operation this document secures declares a 403. The public client logo endpoint, which carries `security: []` and authorises nothing, is the one that does not. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on scope and administrative-policy refusals; other forbidden responses omit it. ### 404 Resource not found application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` ### 500 The request failed inside the server, most often a storage error. error_code is INTERNAL_SERVER_ERROR: no detail about the failure, only the request id an operator can find the log line by, and retrying is reasonable. Key rotation’s 500 also answers KEY_SET_INCOMPLETE, which names its cause and no retry clears. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` # Revoke own consent Source: https://goiabada.dev/reference/api/account/operations/deleteaccountconsent/ DELETE /api/v1/account/consents/{id} Shell / cURL ```sh curl --request DELETE \ --url http://localhost:9090/api/v1/account/consents/1 \ --header 'Authorization: Bearer ' ``` - Auth server base URL{baseUrl}/api/v1/account/consents/{id} Revoke an OAuth consent ## Authorizations - **[BearerAuth](https://goiabada.dev/reference/api/account/#bearerauth)** ## Parameters ### Path Parameters **id** required integer format: int64 Consent ID ## Responses ### 200 Consent revoked application/json object **success** required boolean #### Example generated ```json { "success": true } ``` ### 400 Invalid request body or parameters application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge when token transport is malformed; ordinary validation errors omit it. ### 401 Missing or invalid bearer token application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on token refusals. An operation may also return a handler-level 401 without this header. ### 403 The token is valid but does not authorise this request. Four checks answer here, and a caller distinguishes them by error_code. INSUFFICIENT_SCOPE comes from the route’s authorisation middleware, before the handler runs: an Admin operation needs one of the scopes its route names, and an Account operation needs `authserver:manage-account`. USER_CONTEXT_REQUIRED comes from the same middleware on the Account operations, which also require the token to have been issued for a user: one obtained through the client credentials grant carries no auth_time claim and is refused here. FORBIDDEN comes from the handler, on the account endpoints that resolve a resource by id and then check it belongs to the caller, so a valid token for the wrong user reaches this rather than a 404. MANAGE_SCOPE_REQUIRED comes from the handler, on the Admin operations the administrative policy guards. Only `authserver:manage` creates an administrator, changes one, or changes what reaches one, so no granular scope will ever be enough: the remedy is a token holding `authserver:manage`, not requesting the route’s scope as for INSUFFICIENT_SCOPE. An administrator is a user, group or client holding any of the six administrative permissions (`manage`, `admin-read`, `manage-users`, `manage-clients`, `manage-settings`, `browser-sessions`), a user directly or through any of its groups, the admin console’s own client whatever it holds, and a client allowed to request the administrative scopes. A granular scope is refused here when it: - grants or revokes an administrative permission, directly or by moving a user into or out of a group that holds one, or deletes such a group; - writes to an administrator user (profile, credentials, enabled, sessions, consents, attributes, memberships, permissions, deletion), an administrative group (settings, attributes, members, permissions, deletion), or an administrator client (settings, authentication, flows, redirect URIs, web origins, token settings, permissions, logo, deletion); - switches whether a client may request the administrative scopes, on or off, on any client; - reads an administrator client’s secret; - changes the email or the audit-log settings; - changes an administrative permission’s description. Granular scopes still read administrators. The refusal is answered after the request’s 400 and 404, writes nothing, carries `WWW-Authenticate: Bearer realm="goiabada", error="insufficient_scope", error_description="...", scope="authserver:manage"`, and leaves an `administrator_change_refused` audit record. Every operation this document secures declares a 403. The public client logo endpoint, which carries `security: []` and authorises nothing, is the one that does not. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on scope and administrative-policy refusals; other forbidden responses omit it. ### 404 Resource not found application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` ### 500 The request failed inside the server, most often a storage error. error_code is INTERNAL_SERVER_ERROR: no detail about the failure, only the request id an operator can find the log line by, and retrying is reasonable. Key rotation’s 500 also answers KEY_SET_INCOMPLETE, which names its cause and no retry clears. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` # List own sessions Source: https://goiabada.dev/reference/api/account/operations/getaccountsessions/ GET /api/v1/account/sessions Shell / cURL ```sh curl --request GET \ --url http://localhost:9090/api/v1/account/sessions \ --header 'Authorization: Bearer ' ``` - Auth server base URL{baseUrl}/api/v1/account/sessions Get the caller’s live sessions, each with its device, IP address and the identifiers of the clients it authorized. Sessions that have passed the configured idle timeout or maximum lifetime are left out. `isCurrent` marks the session the calling token was issued through. ## Authorizations - **[BearerAuth](https://goiabada.dev/reference/api/account/#bearerauth)** ## Responses ### 200 User sessions application/json object **sessions** required Array object **id** required integer format: int64 **createdAt** required string format: date-time nullable **updatedAt** required string format: date-time nullable **sessionIdentifier** required string **started** required string format: date-time nullable **lastAccessed** required string format: date-time nullable **authMethods** required string **acrLevel** required string **authTime** required string format: date-time nullable **ipAddress** required string **deviceName** required string **deviceType** required string **deviceOS** required string **userAgent** required string **userId** required integer format: int64 **isCurrent** required boolean **clientIdentifiers** required Array\ #### Example generated ```json { "sessions": [ { "id": 1, "createdAt": "2026-04-15T12:00:00Z", "updatedAt": "2026-04-15T12:00:00Z", "sessionIdentifier": "example", "started": "2026-04-15T12:00:00Z", "lastAccessed": "2026-04-15T12:00:00Z", "authMethods": "example", "acrLevel": "example", "authTime": "2026-04-15T12:00:00Z", "ipAddress": "example", "deviceName": "example", "deviceType": "example", "deviceOS": "example", "userAgent": "example", "userId": 1, "isCurrent": true, "clientIdentifiers": [ "example" ] } ] } ``` ### 401 Missing or invalid bearer token application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on token refusals. An operation may also return a handler-level 401 without this header. ### 403 The token is valid but does not authorise this request. Four checks answer here, and a caller distinguishes them by error_code. INSUFFICIENT_SCOPE comes from the route’s authorisation middleware, before the handler runs: an Admin operation needs one of the scopes its route names, and an Account operation needs `authserver:manage-account`. USER_CONTEXT_REQUIRED comes from the same middleware on the Account operations, which also require the token to have been issued for a user: one obtained through the client credentials grant carries no auth_time claim and is refused here. FORBIDDEN comes from the handler, on the account endpoints that resolve a resource by id and then check it belongs to the caller, so a valid token for the wrong user reaches this rather than a 404. MANAGE_SCOPE_REQUIRED comes from the handler, on the Admin operations the administrative policy guards. Only `authserver:manage` creates an administrator, changes one, or changes what reaches one, so no granular scope will ever be enough: the remedy is a token holding `authserver:manage`, not requesting the route’s scope as for INSUFFICIENT_SCOPE. An administrator is a user, group or client holding any of the six administrative permissions (`manage`, `admin-read`, `manage-users`, `manage-clients`, `manage-settings`, `browser-sessions`), a user directly or through any of its groups, the admin console’s own client whatever it holds, and a client allowed to request the administrative scopes. A granular scope is refused here when it: - grants or revokes an administrative permission, directly or by moving a user into or out of a group that holds one, or deletes such a group; - writes to an administrator user (profile, credentials, enabled, sessions, consents, attributes, memberships, permissions, deletion), an administrative group (settings, attributes, members, permissions, deletion), or an administrator client (settings, authentication, flows, redirect URIs, web origins, token settings, permissions, logo, deletion); - switches whether a client may request the administrative scopes, on or off, on any client; - reads an administrator client’s secret; - changes the email or the audit-log settings; - changes an administrative permission’s description. Granular scopes still read administrators. The refusal is answered after the request’s 400 and 404, writes nothing, carries `WWW-Authenticate: Bearer realm="goiabada", error="insufficient_scope", error_description="...", scope="authserver:manage"`, and leaves an `administrator_change_refused` audit record. Every operation this document secures declares a 403. The public client logo endpoint, which carries `security: []` and authorises nothing, is the one that does not. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on scope and administrative-policy refusals; other forbidden responses omit it. ### 404 Resource not found application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` ### 500 The request failed inside the server, most often a storage error. error_code is INTERNAL_SERVER_ERROR: no detail about the failure, only the request id an operator can find the log line by, and retrying is reasonable. Key rotation’s 500 also answers KEY_SET_INCOMPLETE, which names its cause and no retry clears. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` # Terminate own session Source: https://goiabada.dev/reference/api/account/operations/deleteaccountsession/ DELETE /api/v1/account/sessions/{id} Shell / cURL ```sh curl --request DELETE \ --url http://localhost:9090/api/v1/account/sessions/1 \ --header 'Authorization: Bearer ' ``` - Auth server base URL{baseUrl}/api/v1/account/sessions/{id} Terminate one of the caller’s own sessions, the current one included. The session’s authorization codes are marked revoked and the refresh tokens descended from them are swept, offline refresh tokens included, in one transaction. A session of another user is refused with 403 FORBIDDEN. ## Authorizations - **[BearerAuth](https://goiabada.dev/reference/api/account/#bearerauth)** ## Parameters ### Path Parameters **id** required integer format: int64 Session ID ## Responses ### 200 Session terminated application/json object **success** required boolean #### Example generated ```json { "success": true } ``` ### 400 Invalid request body or parameters application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge when token transport is malformed; ordinary validation errors omit it. ### 401 Missing or invalid bearer token application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on token refusals. An operation may also return a handler-level 401 without this header. ### 403 The token is valid but does not authorise this request. Four checks answer here, and a caller distinguishes them by error_code. INSUFFICIENT_SCOPE comes from the route’s authorisation middleware, before the handler runs: an Admin operation needs one of the scopes its route names, and an Account operation needs `authserver:manage-account`. USER_CONTEXT_REQUIRED comes from the same middleware on the Account operations, which also require the token to have been issued for a user: one obtained through the client credentials grant carries no auth_time claim and is refused here. FORBIDDEN comes from the handler, on the account endpoints that resolve a resource by id and then check it belongs to the caller, so a valid token for the wrong user reaches this rather than a 404. MANAGE_SCOPE_REQUIRED comes from the handler, on the Admin operations the administrative policy guards. Only `authserver:manage` creates an administrator, changes one, or changes what reaches one, so no granular scope will ever be enough: the remedy is a token holding `authserver:manage`, not requesting the route’s scope as for INSUFFICIENT_SCOPE. An administrator is a user, group or client holding any of the six administrative permissions (`manage`, `admin-read`, `manage-users`, `manage-clients`, `manage-settings`, `browser-sessions`), a user directly or through any of its groups, the admin console’s own client whatever it holds, and a client allowed to request the administrative scopes. A granular scope is refused here when it: - grants or revokes an administrative permission, directly or by moving a user into or out of a group that holds one, or deletes such a group; - writes to an administrator user (profile, credentials, enabled, sessions, consents, attributes, memberships, permissions, deletion), an administrative group (settings, attributes, members, permissions, deletion), or an administrator client (settings, authentication, flows, redirect URIs, web origins, token settings, permissions, logo, deletion); - switches whether a client may request the administrative scopes, on or off, on any client; - reads an administrator client’s secret; - changes the email or the audit-log settings; - changes an administrative permission’s description. Granular scopes still read administrators. The refusal is answered after the request’s 400 and 404, writes nothing, carries `WWW-Authenticate: Bearer realm="goiabada", error="insufficient_scope", error_description="...", scope="authserver:manage"`, and leaves an `administrator_change_refused` audit record. Every operation this document secures declares a 403. The public client logo endpoint, which carries `security: []` and authorises nothing, is the one that does not. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on scope and administrative-policy refusals; other forbidden responses omit it. ### 404 Resource not found application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` ### 500 The request failed inside the server, most often a storage error. error_code is INTERNAL_SERVER_ERROR: no detail about the failure, only the request id an operator can find the log line by, and retrying is reasonable. Key rotation’s 500 also answers KEY_SET_INCOMPLETE, which names its cause and no retry clears. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` # Prepare a sign-out Source: https://goiabada.dev/reference/api/account/operations/requestaccountlogout/ POST /api/v1/account/logout-request Shell / cURL ```sh curl --request POST \ --url http://localhost:9090/api/v1/account/logout-request \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "postLogoutRedirectUri": "https://example.com", "state": "example", "clientIdentifier": "example", "responseMode": "example" }' ``` - Auth server base URL{baseUrl}/api/v1/account/logout-request Validate the sign-out target, mint a short-lived id_token_hint and return the prepared sign-out operation, as either a self-submitting form’s parameters or a URL to follow. ## Authorizations - **[BearerAuth](https://goiabada.dev/reference/api/account/#bearerauth)** ## Request Bodyrequired application/json object **postLogoutRedirectUri** required string format: uri **state** string **clientIdentifier** Auto-resolved if omitted string **responseMode** Which response shape to prepare. “form_post” answers the parameters of a self-submitting POST form, which keeps the id_token_hint out of a top-level navigation’s URL, its history and its referrers. Any other value, absent included, answers a redirect URL. The type is deliberately open rather than a closed set: the endpoint refuses nothing here, so an enum would tell a validating client to withhold a request this server answers. string ### Example generated ```json { "postLogoutRedirectUri": "https://example.com", "state": "example", "clientIdentifier": "example", "responseMode": "example" } ``` ## Responses ### 200 The prepared sign-out. The shape depends on the responseMode field of the request: “form_post” gives AccountLogoutFormPostResponse, and anything else, absent included, gives AccountLogoutRedirectResponse. application/json One of: **object** object **logoutUrl** required The end-session URL to follow. It carries the id_token_hint in its query string, so it reaches the address bar, the browser history and any intermediary access log; request form_post instead where that matters. string format: uri **object** The parameters of a POST to the end-session endpoint. Render them as hidden inputs of a form the page submits itself, so the id_token_hint travels in the request body. object **method** required Always POST. string Allowed values: POST **endpoint** required The end-session endpoint to submit to, with no query string of its own. string format: uri **params** required The form fields, as name to value: id_token_hint, post_logout_redirect_uri, and state when the request sent one. object **_key_** additional properties string #### Example ```json { "method": "POST" } ``` ### 400 Invalid request body or parameters application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge when token transport is malformed; ordinary validation errors omit it. ### 401 Missing or invalid bearer token application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on token refusals. An operation may also return a handler-level 401 without this header. ### 403 The token is valid but does not authorise this request. Four checks answer here, and a caller distinguishes them by error_code. INSUFFICIENT_SCOPE comes from the route’s authorisation middleware, before the handler runs: an Admin operation needs one of the scopes its route names, and an Account operation needs `authserver:manage-account`. USER_CONTEXT_REQUIRED comes from the same middleware on the Account operations, which also require the token to have been issued for a user: one obtained through the client credentials grant carries no auth_time claim and is refused here. FORBIDDEN comes from the handler, on the account endpoints that resolve a resource by id and then check it belongs to the caller, so a valid token for the wrong user reaches this rather than a 404. MANAGE_SCOPE_REQUIRED comes from the handler, on the Admin operations the administrative policy guards. Only `authserver:manage` creates an administrator, changes one, or changes what reaches one, so no granular scope will ever be enough: the remedy is a token holding `authserver:manage`, not requesting the route’s scope as for INSUFFICIENT_SCOPE. An administrator is a user, group or client holding any of the six administrative permissions (`manage`, `admin-read`, `manage-users`, `manage-clients`, `manage-settings`, `browser-sessions`), a user directly or through any of its groups, the admin console’s own client whatever it holds, and a client allowed to request the administrative scopes. A granular scope is refused here when it: - grants or revokes an administrative permission, directly or by moving a user into or out of a group that holds one, or deletes such a group; - writes to an administrator user (profile, credentials, enabled, sessions, consents, attributes, memberships, permissions, deletion), an administrative group (settings, attributes, members, permissions, deletion), or an administrator client (settings, authentication, flows, redirect URIs, web origins, token settings, permissions, logo, deletion); - switches whether a client may request the administrative scopes, on or off, on any client; - reads an administrator client’s secret; - changes the email or the audit-log settings; - changes an administrative permission’s description. Granular scopes still read administrators. The refusal is answered after the request’s 400 and 404, writes nothing, carries `WWW-Authenticate: Bearer realm="goiabada", error="insufficient_scope", error_description="...", scope="authserver:manage"`, and leaves an `administrator_change_refused` audit record. Every operation this document secures declares a 403. The public client logo endpoint, which carries `security: []` and authorises nothing, is the one that does not. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` #### Headers **WWW-Authenticate** string Bearer challenge on scope and administrative-policy refusals; other forbidden responses omit it. ### 500 The request failed inside the server, most often a storage error. error_code is INTERNAL_SERVER_ERROR: no detail about the failure, only the request id an operator can find the log line by, and retrying is reasonable. Key rotation’s 500 also answers KEY_SET_INCOMPLETE, which names its cause and no retry clears. application/json Admin/account API error envelope. Routing by HTTP status code: 4xx is a client-correctable failure (validation, not found, permission denied, rate limited); 5xx is a server-side problem. The body provides a stable error_code and an error_description for people to read. Protocol endpoints (/auth/token, /auth/authorize, /connect/register, /userinfo) use their RFC-defined error envelopes instead of this one. object **error_code** Stable identifier for the failure. UPPER_SNAKE for legacy codes (e.g. VALIDATION_ERROR), dotted lowercase for catalog-keyed localized codes (e.g. validator.email.required). string **error_description** required A sentence for people to read: in the request’s language when error_code is a catalog key, in English otherwise. Route on error_code, not on this text. string #### Example generated ```json { "error_code": "example", "error_description": "example" } ``` # Environment variables Source: https://goiabada.dev/reference/environment-variables/ This page lists every setting Goiabada’s two servers read, and what each one does. Goiabada runs as two servers, the auth server (`goiabada-authserver`) and the admin console (`goiabada-adminconsole`), and you configure both with environment variables. Most variables also have a command-line flag, and the flag wins when you set both: ```bash GOIABADA_AUTHSERVER_LOG_LEVEL=debug ./goiabada-authserver ./goiabada-authserver --authserver-log-level=debug ``` You rarely need to start from scratch: the [setup wizard](https://goiabada.dev/deploy/setup-wizard/) writes a working set for your deployment, keys included. ## Every variable **Read by** says which server reads a variable. A server ignores the variables it doesn’t read, so you can hand both servers the same environment. ### Initial setup The auth server reads these when it starts on an empty database, to create the first administrator and the admin console’s client. See [the first start](https://goiabada.dev/reference/environment-variables/#the-first-start). | Variable | What it does | | - | - | | `GOIABADA_ADMIN_EMAIL` Flag: `--admin-email` Default: `admin@example.com` Read by: auth server | The first administrator’s email address. | | `GOIABADA_ADMIN_PASSWORD` Flag: `--admin-password` Default: empty Read by: auth server | The first administrator’s password. **Required on the first start**: at least 15 characters and at most 72 bytes. | | `GOIABADA_APPNAME` Flag: `--appname` Default: `Goiabada` Read by: auth server | The name the UI shows. Change it later under **Admin**, **General** in the admin console. | | `GOIABADA_ADMINCONSOLE_OAUTH_CLIENT_SECRET` Flag: `--adminconsole-oauth-client-secret` Default: empty Read by: both | The admin console’s client secret. The admin console needs it to call the auth server’s API, and refuses to start when the auth server refuses it. The auth server reads it only on the first start, as the secret of the client it creates for the admin console. | | `GOIABADA_AUTHSERVER_BOOTSTRAP_ENV_OUTFILE` Flag: `--authserver-bootstrap-env-outfile` Default: empty Read by: auth server | A file for the two-step first start. The auth server writes the credentials it generated there, then exits. | ### Database Only the auth server talks to the database. The admin console reaches your data through the auth server’s API. | Variable | What it does | | - | - | | `GOIABADA_DB_TYPE` Flag: `--db-type` Default: `sqlite` Read by: auth server | The database engine: `sqlite`, `mysql`, `postgres` or `mssql`. | | `GOIABADA_DB_HOST` Flag: `--db-host` Default: `localhost` Read by: auth server | The database host. An IPv6 address works with or without brackets. | | `GOIABADA_DB_PORT` Flag: `--db-port` Default: `3306` Read by: auth server | The database port. | | `GOIABADA_DB_NAME` Flag: `--db-name` Default: `goiabada` Read by: auth server | The database name. | | `GOIABADA_DB_USERNAME` Flag: `--db-username` Default: `root` Read by: auth server | The database user. | | `GOIABADA_DB_PASSWORD` Flag: `--db-password` Default: empty Read by: auth server | The database user’s password. | | `GOIABADA_DB_TLS_MODE` Flag: `--db-tls-mode` Default: `prefer` Read by: auth server | How the auth server protects its connection to MySQL, PostgreSQL or SQL Server: `disable`, `prefer`, `require`, `verify-ca` or `verify-full`. Only `verify-full` checks that it reached your own database: [Where the database sits](https://goiabada.dev/deploy/database/#where-the-database-sits) compares the five. `prefer` checks no certificate, and encrypts when the server offers TLS; on SQL Server it encrypts the login, and the rest only when the server forces encryption. Left unset, it’s `prefer`, and every start writes a warning. SQLite ignores it. | | `GOIABADA_DB_TLS_CA_FILE` Flag: `--db-tls-ca-file` Default: empty Read by: auth server | A PEM file of the authorities you trust to sign the database’s certificate. It’s read once at start, in `verify-ca` and `verify-full` only, and trusted in place of the system’s authorities. The auth server won’t start if the file can’t be read, holds no certificate, or is set with another mode. SQLite ignores it. | | `GOIABADA_DB_CREATE` Flag: `--db-create` Default: `true` Read by: auth server | Create the database if it doesn’t exist. Set it to `false` when you [create the database yourself](https://goiabada.dev/deploy/database/). | | `GOIABADA_DB_DSN` Flag: `--db-dsn` Default: `file::memory:?cache=shared` Read by: auth server | The SQLite connection string. The default keeps everything in memory, so it’s gone when the server stops. See [SQLite](https://goiabada.dev/reference/environment-variables/#sqlite). | | `GOIABADA_DB_MAX_OPEN_CONNS` Flag: `--db-max-open-conns` Default: `20` Read by: auth server | The most connections open at once. At least `1`. | | `GOIABADA_DB_MAX_IDLE_CONNS` Flag: `--db-max-idle-conns` Default: `20` Read by: auth server | The most idle connections kept for reuse, from `0` up to the open limit. Unset, it follows `GOIABADA_DB_MAX_OPEN_CONNS`. | | `GOIABADA_DB_CONN_MAX_LIFETIME` Flag: `--db-conn-max-lifetime` Default: `30m` Read by: auth server | How long a connection is reused before it’s replaced. `0` means no limit. | | `GOIABADA_DB_CONN_MAX_IDLE_TIME` Flag: `--db-conn-max-idle-time` Default: `5m` Read by: auth server | How long a connection can sit idle before it’s closed. `0` means no limit. | The last four size the [connection pool](https://goiabada.dev/reference/environment-variables/#the-connection-pool). ### URLs and listeners A base URL is the address browsers and clients use. A listener is where the server accepts connections on its own machine, which can be quite different behind a proxy. | Variable | What it does | | - | - | | `GOIABADA_AUTHSERVER_BASEURL` Flag: `--authserver-baseurl` Default: `http://localhost:9090` Read by: both | The auth server’s public URL. The first start also makes it the issuer that tokens carry. An `https://` URL marks the auth server’s cookies `Secure`. | | `GOIABADA_AUTHSERVER_INTERNALBASEURL` Flag: `--authserver-internalbaseurl` Default: empty Read by: both | Where the admin console calls the auth server’s API, such as `http://goiabada-authserver:9090` inside a cluster. Empty, it uses the public URL. The auth server only reports it in its startup log. | | `GOIABADA_ADMINCONSOLE_BASEURL` Flag: `--adminconsole-baseurl` Default: `http://localhost:9091` Read by: both | The admin console’s public URL. The auth server links to it in its emails and sends visitors of `/` there. An `https://` URL marks the admin console’s cookies `Secure`. The auth server’s first start also registers it, and it followed by `/auth/callback`, as the admin console client’s redirect URIs; change it later and update those on the client’s **Redirect URIs** tab too. | | `GOIABADA_AUTHSERVER_LISTEN_HOST_HTTP` Flag: `--authserver-listen-host-http` Default: `0.0.0.0` Read by: auth server | The address the auth server’s HTTP listener binds. Set it empty to turn that listener off. See [listen addresses](https://goiabada.dev/reference/environment-variables/#listen-addresses). | | `GOIABADA_AUTHSERVER_LISTEN_PORT_HTTP` Flag: `--authserver-listen-port-http` Default: `9090` Read by: auth server | The auth server’s HTTP port. | | `GOIABADA_AUTHSERVER_LISTEN_HOST_HTTPS` Flag: `--authserver-listen-host-https` Default: `0.0.0.0` Read by: auth server | The address the auth server’s HTTPS listener binds. It listens only once the certificate and key files are set. | | `GOIABADA_AUTHSERVER_LISTEN_PORT_HTTPS` Flag: `--authserver-listen-port-https` Default: `9443` Read by: auth server | The auth server’s HTTPS port. | | `GOIABADA_AUTHSERVER_CERTFILE` Flag: `--authserver-certfile` Default: empty Read by: auth server | The TLS certificate file for the auth server’s HTTPS listener. Behind a proxy that handles TLS, you don’t need it. | | `GOIABADA_AUTHSERVER_KEYFILE` Flag: `--authserver-keyfile` Default: empty Read by: auth server | The TLS private key file for the auth server’s HTTPS listener. | | `GOIABADA_ADMINCONSOLE_LISTEN_HOST_HTTP` Flag: `--adminconsole-listen-host-http` Default: `0.0.0.0` Read by: admin console | The address the admin console’s HTTP listener binds. Set it empty to turn that listener off. | | `GOIABADA_ADMINCONSOLE_LISTEN_PORT_HTTP` Flag: `--adminconsole-listen-port-http` Default: `9091` Read by: admin console | The admin console’s HTTP port. | | `GOIABADA_ADMINCONSOLE_LISTEN_HOST_HTTPS` Flag: `--adminconsole-listen-host-https` Default: `0.0.0.0` Read by: admin console | The address the admin console’s HTTPS listener binds. It listens only once the certificate and key files are set. | | `GOIABADA_ADMINCONSOLE_LISTEN_PORT_HTTPS` Flag: `--adminconsole-listen-port-https` Default: `9444` Read by: admin console | The admin console’s HTTPS port. | | `GOIABADA_ADMINCONSOLE_CERTFILE` Flag: `--adminconsole-certfile` Default: empty Read by: admin console | The TLS certificate file for the admin console’s HTTPS listener. | | `GOIABADA_ADMINCONSOLE_KEYFILE` Flag: `--adminconsole-keyfile` Default: empty Read by: admin console | The TLS private key file for the admin console’s HTTPS listener. | ### Proxies | Variable | What it does | | - | - | | `GOIABADA_AUTHSERVER_TRUST_PROXY_HEADERS` Flag: `--authserver-trust-proxy-headers` Default: `false` Read by: auth server | Take the client’s IP address from `X-Forwarded-For` or `X-Real-IP`. Turn it on behind a reverse proxy, and leave it off without one. See [client IP addresses](https://goiabada.dev/reference/environment-variables/#client-ip-addresses). | | `GOIABADA_AUTHSERVER_TRUSTED_PROXIES` Flag: `--authserver-trusted-proxies` Default: empty Read by: auth server | Your proxies’ addresses, as comma-separated IP addresses or CIDR ranges, such as `10.0.0.0/8,192.168.1.5`. | | `GOIABADA_ADMINCONSOLE_TRUST_PROXY_HEADERS` Flag: `--adminconsole-trust-proxy-headers` Default: `false` Read by: admin console | The same switch for the admin console, which records the address in its logs. | | `GOIABADA_ADMINCONSOLE_TRUSTED_PROXIES` Flag: `--adminconsole-trusted-proxies` Default: empty Read by: admin console | The same list for the admin console. | ### Keys Each of these is random bytes written as hex. Generate them as [Session keys](https://goiabada.dev/reference/environment-variables/#session-keys) and [the data encryption key](https://goiabada.dev/reference/environment-variables/#the-data-encryption-key) show. They have no flags, so they stay out of process listings. | Variable | What it does | | - | - | | `GOIABADA_AUTHSERVER_SESSION_AUTHENTICATION_KEY` Flag: none Default: empty Read by: auth server | **Required.** 64 bytes that seal the auth server’s sessions. | | `GOIABADA_AUTHSERVER_SESSION_ENCRYPTION_KEY` Flag: none Default: empty Read by: auth server | **Required.** 32 bytes that encrypt the auth server’s sessions. | | `GOIABADA_AUTHSERVER_SESSION_AUTHENTICATION_KEY_PREVIOUS` Flag: none Default: empty Read by: auth server | The old authentication key, set only while you rotate the session keys. | | `GOIABADA_AUTHSERVER_SESSION_ENCRYPTION_KEY_PREVIOUS` Flag: none Default: empty Read by: auth server | The old encryption key, set only while you rotate the session keys. | | `GOIABADA_ADMINCONSOLE_SESSION_AUTHENTICATION_KEY` Flag: none Default: empty Read by: admin console | **Required.** 64 bytes that seal the admin console’s sessions. | | `GOIABADA_ADMINCONSOLE_SESSION_ENCRYPTION_KEY` Flag: none Default: empty Read by: admin console | **Required.** 32 bytes that encrypt the admin console’s sessions. | | `GOIABADA_ADMINCONSOLE_SESSION_AUTHENTICATION_KEY_PREVIOUS` Flag: none Default: empty Read by: admin console | The old authentication key, set only while you rotate the session keys. | | `GOIABADA_ADMINCONSOLE_SESSION_ENCRYPTION_KEY_PREVIOUS` Flag: none Default: empty Read by: admin console | The old encryption key, set only while you rotate the session keys. | | `GOIABADA_AES_ENCRYPTION_KEY` Flag: none Default: empty Read by: auth server | **Required.** 32 bytes that encrypt secrets in the database. | | `GOIABADA_AES_ENCRYPTION_KEY_PREVIOUS` Flag: none Default: empty Read by: auth server | The old data encryption key, set only while you rotate it. | ### Logging Both servers write their logs to standard error. | Variable | What it does | | - | - | | `GOIABADA_AUTHSERVER_LOG_LEVEL` Flag: `--authserver-log-level` Default: `info` Read by: auth server | The lowest level written: `debug`, `info`, `warn` or `error`. `debug` adds a few diagnostic records; for one record per request, turn on `GOIABADA_AUTHSERVER_LOG_HTTP_REQUESTS`. | | `GOIABADA_AUTHSERVER_LOG_FORMAT` Flag: `--authserver-log-format` Default: `text` Read by: auth server | `text` for one `key=value` line per record, or `json` for one JSON object per record. | | `GOIABADA_AUTHSERVER_LOG_HTTP_REQUESTS` Flag: `--authserver-log-http-requests` Default: `false` Read by: auth server | Write one record per request, except health checks, static files and the favicon. Query values are redacted, apart from a short list of safe ones. | | `GOIABADA_AUTHSERVER_LOG_SQL` Flag: `--authserver-log-sql` Default: `false` Read by: auth server | Write one record per SQL statement, with its text but not the values it ran with. | | `GOIABADA_AUTHSERVER_DEBUG_API_REQUESTS` Flag: `--authserver-debug-api-requests` Default: `false` Read by: auth server | Write one record per API call, with its method, target, status, duration and both bodies. Credentials in the bodies and the `Authorization` header are redacted. | | `GOIABADA_ADMINCONSOLE_LOG_LEVEL` Flag: `--adminconsole-log-level` Default: `info` Read by: admin console | The lowest level written: `debug`, `info`, `warn` or `error`. | | `GOIABADA_ADMINCONSOLE_LOG_FORMAT` Flag: `--adminconsole-log-format` Default: `text` Read by: admin console | `text` or `json`, as for the auth server. | | `GOIABADA_ADMINCONSOLE_LOG_HTTP_REQUESTS` Flag: `--adminconsole-log-http-requests` Default: `false` Read by: admin console | Write one record per request, as the auth server’s switch does. | A level or format spelled any other way, uppercase included, stops the server at start. To set the time zone of the timestamps, see [time zone](https://goiabada.dev/reference/environment-variables/#time-zone). ### Metrics Each server can serve Prometheus metrics on a listener of its own, which answers `GET /metrics` and nothing else. It has no authentication, so **never** publish its port to the internet. [Monitoring](https://goiabada.dev/deploy/monitoring/) covers what it reports. | Variable | What it does | | - | - | | `GOIABADA_AUTHSERVER_METRICS_ENABLED` Flag: `--authserver-metrics-enabled` Default: `false` Read by: auth server | Serve the auth server’s metrics listener. Off, nothing listens on its port. | | `GOIABADA_AUTHSERVER_LISTEN_HOST_METRICS` Flag: `--authserver-listen-host-metrics` Default: `0.0.0.0` Read by: auth server | The address the auth server’s metrics listener binds. Use `127.0.0.1` when the scraper runs on the same host. | | `GOIABADA_AUTHSERVER_LISTEN_PORT_METRICS` Flag: `--authserver-listen-port-metrics` Default: `9190` Read by: auth server | The auth server’s metrics port. | | `GOIABADA_ADMINCONSOLE_METRICS_ENABLED` Flag: `--adminconsole-metrics-enabled` Default: `false` Read by: admin console | Serve the admin console’s metrics listener. Off, nothing listens on its port. | | `GOIABADA_ADMINCONSOLE_LISTEN_HOST_METRICS` Flag: `--adminconsole-listen-host-metrics` Default: `0.0.0.0` Read by: admin console | The address the admin console’s metrics listener binds. | | `GOIABADA_ADMINCONSOLE_LISTEN_PORT_METRICS` Flag: `--adminconsole-listen-port-metrics` Default: `9191` Read by: admin console | The admin console’s metrics port. | ### Customization | Variable | What it does | | - | - | | `GOIABADA_AUTHSERVER_STATICDIR` Flag: `--authserver-staticdir` Default: empty Read by: auth server | A directory to serve the auth server’s static files from, instead of the built-in ones. See [Change the templates](https://goiabada.dev/guides/customize-and-translate-the-pages/#change-the-templates). | | `GOIABADA_AUTHSERVER_TEMPLATEDIR` Flag: `--authserver-templatedir` Default: empty Read by: auth server | A directory to load the auth server’s page templates from, instead of the built-in ones. | | `GOIABADA_ADMINCONSOLE_STATICDIR` Flag: `--adminconsole-staticdir` Default: empty Read by: admin console | The same, for the admin console’s static files. | | `GOIABADA_ADMINCONSOLE_TEMPLATEDIR` Flag: `--adminconsole-templatedir` Default: empty Read by: admin console | The same, for the admin console’s page templates. | | `GOIABADA_I18N_OVERRIDES_DIR` Flag: none Default: empty Read by: both | A directory whose `catalogs` subfolder holds `active..toml` files, merged key by key over the built-in text. Without a `catalogs` subfolder nothing is loaded. Set it on both servers. See [Add a language](https://goiabada.dev/guides/customize-and-translate-the-pages/#add-a-language). | | `GOIABADA_PROFILE_PICTURE_MAX_SIZE_BYTES` Flag: none Default: `3145728` Read by: auth server | The largest profile picture or client logo the API accepts, in bytes. The default is 3 MiB. The admin console’s upload form stops at 3 MiB, whatever this says. | ### Rate limiting | Variable | What it does | | - | - | | `GOIABADA_AUTHSERVER_RATELIMITER_ENABLED` Flag: `--authserver-ratelimiter-enabled` Default: `false` Read by: auth server | Turn on the [rate limits](https://goiabada.dev/reference/environment-variables/#rate-limits) below that are counted by an IP address. Those counted by a user or an email apply whether or not it’s on. The setup wizard asks whether you want them. | ## Rate limits The auth server limits these endpoints. The limits counted by a user or an email always apply, and `GOIABADA_AUTHSERVER_RATELIMITER_ENABLED` adds the limits counted by an IP address: the **Applies** column says `always` or `switch on`. A refused request gets `429 Too Many Requests`, in the format the endpoint usually answers with. The limiter is the name the `rate_limit_exceeded` [audit event](https://goiabada.dev/concepts/audit-log/) and the `goiabada_rate_limit_refusals_total` [metric](https://goiabada.dev/deploy/monitoring/) give it. | Endpoint | Limit | Counts | Counted by | Applies | | - | - | - | - | - | | `POST /auth/pwd` (password sign-in) Limiter: `pwd_account_net` | 10 per 15 minutes | failed sign-ins | IP address and email, shared with the password grant | switch on | | `POST /auth/pwd` Limiter: `pwd_account` | 100 per 60 minutes | failed sign-ins | email, shared with the password grant | always | | `POST /auth/pwd` Limiter: `pwd_ip` | 30 per minute | every request | IP address | switch on | | `POST /auth/otp` (authenticator code) Limiter: `otp` | 5 per 15 minutes | failed codes | user | always | | `POST /api/v1/account/email/verification` Limiter: `email_verification` | 5 per 15 minutes | failed codes | the token’s user | always | | `POST /api/v1/account/email/verification/send` Limiter: `email_verification_send` | 5 per 60 minutes | every request | the token’s user | always | | `PUT /api/v1/account/password`, `PUT /api/v1/account/otp` and `PUT /api/v1/account/email` Limiter: `account_password` | 5 per 15 minutes, for the three together | failed passwords | the token’s user | always | | `POST /forgot-password` Limiter: `forgot_pwd_email` | 5 per 5 minutes | every request | email | always | | `POST /forgot-password` Limiter: `forgot_pwd_ip` | 20 per 5 minutes | every request | IP address | switch on | | `GET /reset-password` and `POST /reset-password` Limiter: `reset_pwd` | 30 per 5 minutes, for the two together | every request | IP address | switch on | | `POST /account/register` (self-registration) Limiter: `register_email` | 5 per 5 minutes | every request | email | always | | `POST /account/register` Limiter: `register` | 20 per 5 minutes | every request | IP address | switch on | | `GET /account/activate` and `POST /account/activate` Limiter: `activate` | 30 per 5 minutes, for the two together | every request | IP address | switch on | | `POST /connect/register` (dynamic client registration) Limiter: `dcr` | 10 per minute | every request | IP address | switch on | | `POST /auth/token` with `grant_type=password` Limiter: `pwd_account_net` | 10 per 15 minutes | failed sign-ins | IP address and username, shared with `POST /auth/pwd` | switch on | | `POST /auth/token` with `grant_type=password` Limiter: `pwd_account` | 100 per 60 minutes | failed sign-ins | username, shared with `POST /auth/pwd` | always | | `POST /auth/token` with `grant_type=password` Limiter: `ropc_ip` | 30 per minute | every request | IP address | switch on | A few rules make these work: - **A limit on failures isn’t spent by a success.** Signing in normally never uses it up. - **Shared means one budget.** A wrong password counts once against the same budget whether it arrives at the sign-in form or at the password grant. - **Emails and usernames are compared in lowercase**, and IPv6 addresses are grouped by their `/64`, so a client can’t step through its own network to reset a limit. - **Limits on failures hold across replicas** on PostgreSQL, MySQL and SQL Server, where they’re counted in the database, so a rollout doesn’t refill them either. If the database doesn’t answer the check within 5 seconds, the request gets a server error rather than going through. On SQLite, which runs one replica, they’re counted in the process. - **Limits on every request are per process**, on every database. With three replicas, each one allows the full budget, and a restart resets it. For a limit that holds across your whole deployment, set one at your gateway, proxy or CDN, which also stops a flood before it reaches Goiabada. - **A limit counted by IP address is only as good as the address.** Behind a proxy, set up [client IP addresses](https://goiabada.dev/reference/environment-variables/#client-ip-addresses) first. - **Anyone who knows an email can block its password sign-in.** 100 wrong passwords in an hour block that account’s password sign-in, at the form and through the password grant, for up to an hour, even with the right password, and about 100 more an hour keep it blocked. A password reset doesn’t clear the count, and anyone already signed in stays signed in. That’s the price of a bound on guessing that holds whatever address the server sees. If it happens, block the attacker at your proxy or CDN, which sees their address. Each limit that trips writes a `rate_limit_exceeded` [audit event](https://goiabada.dev/concepts/audit-log/), once per key and window on each replica. ## Values The servers trim spaces around every value. An empty number or switch counts as unset and takes its default, but an empty text value is just empty: that’s how an empty listen host turns a listener off. A number, switch or duration that doesn’t parse stops the server at start. It prints one line naming every bad value and exits with code `2`, so a typo can’t leave a setting quietly on its default. A log level or log format other than the values listed, spelled in lowercase, stops it too, with `unable to install the log handler` and code `1`. A switch is `true` or `false`, and a duration needs its unit, as in `30m`, `1h` or `90s`. Each server accepts only the flags of the variables it reads. Give the admin console a flag only the auth server reads, or the other way round, and it refuses to start. ## Listen addresses A listen host is an address on the server’s own machine, not its public URL. It decides which of the machine’s addresses accept connections. - **`0.0.0.0`, the default, serves IPv4 and IPv6 alike.** `::` means the same. On a host with IPv6 switched off, it serves IPv4 alone. - **A specific address serves only its own family.** `127.0.0.1` takes no IPv6 connection, and `::1` no IPv4 one. A hostname such as `localhost` binds only the first address it resolves to, so prefer a literal address. - **Write an IPv6 address bare or in brackets.** `::1` and `[::1]` are the same address. A link-local address takes its zone, as in `fe80::1%eth0`. Each server needs at least one listener. With both off, it refuses to start. ## Client IP addresses The rate limiter counts by client IP address, and the audit log records it. With `TRUST_PROXY_HEADERS` off, the server uses the address of whatever connected to it. Behind a proxy, that’s the proxy, so every client looks the same, and every user shares one rate limit: see [Too many attempts or 429](https://goiabada.dev/troubleshooting/too-many-attempts-or-429/). Turn `TRUST_PROXY_HEADERS` on behind a proxy. With `TRUSTED_PROXIES` empty, the server trusts one hop: the last address in `X-Forwarded-For`, or `X-Real-IP` when there’s none. That’s right behind a single proxy that sets or appends `X-Forwarded-For`. With more than one hop in front, such as a CDN and a proxy, list them in `TRUSTED_PROXIES`, and the server walks `X-Forwarded-For` from the right across them. Once you set `TRUSTED_PROXIES`, it must include the proxy that connects to Goiabada: forwarded headers from any other address are ignored. An entry that’s neither an IP address nor a CIDR range stops the server at start, even with `TRUST_PROXY_HEADERS` off. > **Caution** > > A client that reaches Goiabada without going through your proxy can claim any address it likes. Make sure the proxy is the only way in. See [Client IP and proxy trust](https://goiabada.dev/deploy/client-ip-and-proxy-trust/). ## Session keys Each server seals its sessions with its own pair of keys: a 64-byte authentication key and a 32-byte encryption key. Generate a fresh pair for each server, and for each deployment: ```bash openssl rand -hex 64 # authentication key openssl rand -hex 32 # encryption key ``` **Never** reuse the example keys from a page or a sample file. To rotate the keys without signing anyone out: 1. Move each server’s current pair into its two `_PREVIOUS` variables. 2. Set the current variables to freshly generated keys. 3. Restart both servers. They seal new sessions with the new pair and still open sessions sealed with the old one. On Kubernetes, where a rollout runs old and new pods side by side, this takes two rollouts instead: see below. 4. Once the longest session lifetime has passed, remove the `_PREVIOUS` variables. A sign-in still in progress when you remove them may have to start again. Skip the `_PREVIOUS` variables altogether and everyone simply signs in again once. [Rotate secrets](https://goiabada.dev/deploy/rotate-secrets/#the-session-keys) has the commands for each platform. ## The data encryption key `GOIABADA_AES_ENCRYPTION_KEY` encrypts the secrets the auth server stores: client secrets, the SMTP password, verification and password-reset codes, authenticator seeds, and the private keys that sign tokens. Only the auth server needs it. ```bash openssl rand -hex 32 ``` > **Back up this key, apart from the database** > > If you lose this key, every secret and signing key in the database is lost with it, and nothing can decrypt them. **Never** keep the key only beside the database it protects. To rotate it, set `GOIABADA_AES_ENCRYPTION_KEY` to a new key and `GOIABADA_AES_ENCRYPTION_KEY_PREVIOUS` to the old one, then restart the auth server. At start, it re-encrypts everything under the new key in one transaction. Leaving the previous key set across restarts is harmless, but remove it once the rotation is done. If the data decrypts under neither key, the auth server refuses to start rather than risk damaging it. [Rotate secrets](https://goiabada.dev/deploy/rotate-secrets/#the-aes-key) has the procedure for each platform, and [Back up the AES key](https://goiabada.dev/deploy/secrets/#back-up-the-aes-key) where to keep the key. ## The first start When the auth server starts on an empty database, it creates the first administrator from `GOIABADA_ADMIN_EMAIL` and `GOIABADA_ADMIN_PASSWORD`, and a client for the admin console. How it gets that client’s secret depends on what you set: - **`GOIABADA_ADMINCONSOLE_OAUTH_CLIENT_SECRET` set:** it uses that secret and carries on running. Give both servers the same value. This is what the setup wizard writes. - **Only `GOIABADA_AUTHSERVER_BOOTSTRAP_ENV_OUTFILE` set:** it generates the secret and both servers’ session keys, writes them to that file, and exits. Copy them into your configuration and start again. - **Neither set:** it refuses to start, and the database stays empty. On every later start, the database already holds the administrator and the client, and none of these variables does anything on the auth server. Changed one and can’t sign in? See [Locked out of the admin console](https://goiabada.dev/troubleshooting/locked-out-of-the-admin-console/). ## The connection pool The four pool settings apply to PostgreSQL, MySQL and SQL Server. SQLite always uses one connection, though the values are still checked there. A value out of range stops the server at start, as does an idle limit above the open limit. A request that finds every connection busy waits for one. The lifetime and idle time hand connections back over time, so they move to a new endpoint after a failover and a quiet replica doesn’t hold on to them. Every start writes one `database connection pool` log record with the pool it opened, and the `migrate` subcommand reads the same settings. With several replicas, each opens its own pool against the one database. [High availability](https://goiabada.dev/deploy/kubernetes/high-availability/#scale-out) shows how to size it against your database’s connection limit. ## SQLite For a SQLite file that survives restarts, point `GOIABADA_DB_DSN` at a file, with a busy timeout and write-ahead logging: ```bash GOIABADA_DB_DSN='file:/var/lib/goiabada/goiabada.db?_pragma=busy_timeout=5000&_pragma=journal_mode=WAL' ``` The busy timeout makes a request wait up to 5 seconds for a lock instead of failing at once. Write-ahead logging lets reads carry on while a write is in progress. ## Time zone Both servers also read `TZ`, the standard variable, with no `GOIABADA_` prefix and no flag. It sets the zone of the log timestamps and of the times the servers format. Use an IANA name such as `Europe/Lisbon`, or an absolute path to a zone file, either one optionally after a leading `:`. The servers carry their own zone database, so the host needs none, and `TZ` works on Windows too. Unset, the host’s zone is used, and empty means UTC. A name that’s no zone, `Local`, or a zone file that doesn’t load stops the server at start. ## Next steps [Setup wizard](https://goiabada.dev/deploy/setup-wizard/): Generate a working configuration, keys included. [Docker Compose](https://goiabada.dev/deploy/docker-compose/): Run both servers in production with Compose. [Monitoring](https://goiabada.dev/deploy/monitoring/): Scrape the metrics listeners and alert on them. [Security](https://goiabada.dev/reference/security/): What Goiabada does to keep your deployment safe. # Security Source: https://goiabada.dev/reference/security/ This page helps you report a security issue in Goiabada, and shows what it does to keep sign-ins, tokens and sessions safe. ## Report a vulnerability 1. Email with what you found: what an attacker can do, how to reproduce it, and the Goiabada version you tried it on. 2. Keep it private, out of GitHub issues and discussions, until a fix is released, so people running Goiabada can upgrade before the details are public. ## Sign-in - **Passwords are hashed** with bcrypt, and never stored or logged as typed. See [passwords](https://goiabada.dev/concepts/users-and-groups/#passwords). - **Two-factor authentication** uses one-time codes from an authenticator app, optional or required per client. See [ACR and AMR](https://goiabada.dev/concepts/acr-and-amr/). - **A one-time code works once.** Once a code has been accepted, it’s refused everywhere, at sign-in, while setting up an authenticator and through the account API, with the same error a wrong code gets. Two submissions of the same code at the same moment get at most one success. A refused replay leaves an `otp_code_replay_detected` entry in the [audit log](https://goiabada.dev/concepts/audit-log/). Removing an authenticator clears that history. - **[PKCE](https://goiabada.dev/concepts/pkce/) is required** for every public client, and by default for every other one. - **Guessing is rate limited.** Wrong passwords for one account, at the sign-in form and through the password grant together, wrong one-time codes, email verification, credential changes on the Account pages, and password-reset and registration mails to one address each get a fixed budget that always applies. `GOIABADA_AUTHSERVER_RATELIMITER_ENABLED`, off by default, adds the limits counted by an IP address, among them dynamic client registration. See [rate limits](https://goiabada.dev/reference/environment-variables/#rate-limits). - **Anyone who knows an email can block its password sign-in** for up to an hour, with 100 wrong passwords, since the limit on wrong passwords for one account holds whatever address they come from. Anyone already signed in stays signed in. If it happens, block the attacker at your proxy or CDN. See [rate limits](https://goiabada.dev/reference/environment-variables/#rate-limits). - **No error is redirected before the user signs in.** A request that fails while nobody is signed in sends the user to sign in first, and the error reaches the client afterwards, so one crafted link can’t send a visitor straight to any address a client has registered, as [RFC 9700](https://www.rfc-editor.org/rfc/rfc9700.html) section 4.11.2 requires. A silent request, which must never show a page, is answered at once. See [When an error reaches your app](https://goiabada.dev/reference/endpoints/authorize/#when-an-error-reaches-your-app). - **A self-registered client is never sent an error.** Anyone can register a client and point it anywhere, so instead of redirecting, the auth server shows the user a page naming the address. See [self-registered clients](https://goiabada.dev/concepts/clients/#self-registered-clients). ## Tokens - **Access tokens are short-lived:** 5 minutes by default. - **Tokens are signed** with RS256, by keys you can rotate in the admin console. See [Discovery and JWKS](https://goiabada.dev/reference/endpoints/discovery-and-jwks/). - **A refresh token works once.** Each refresh replaces it, and two refreshes with the same token at the same moment get at most one new token. See [rotation](https://goiabada.dev/concepts/refresh-tokens/#rotation). - **A replayed refresh token revokes its family.** Presenting one that a refresh already replaced revokes every other token in its line of refreshes, and leaves a `refresh_token_replay_detected` entry in the audit log. See [replay](https://goiabada.dev/concepts/refresh-tokens/#replay). - **Ending a session revokes what it authorized:** its refresh tokens, offline ones included, and its unredeemed authorization codes. See [what ending a session revokes](https://goiabada.dev/concepts/ending-sessions/#what-ending-a-session-revokes). ## Sessions and cookies - **The cookie holds an identifier, nothing else.** The session’s contents stay on the server. - **The cookie is `HttpOnly`,** so no script can read it, **`SameSite=Lax`,** so the browser leaves it off another site’s form posts, and **`Secure`** when the server’s base URL is `https`, so it never travels over plain HTTP. - **Sessions expire:** after 2 hours idle or 24 hours in all, by default. See [Sessions](https://goiabada.dev/concepts/sessions/#how-long-a-session-lasts). - **The admin console’s cookie has no expiry,** so the browser drops it when it closes. A browser that restores its last session can bring it back. ## Cross-site request forgery Every form the auth server and the admin console serve, the sign-in, two-factor and consent screens and every admin console page, accepts a POST only from its own origin. A request the browser says came from another site, a sibling subdomain included, is refused with `403 Forbidden`. The browser’s `Sec-Fetch-Site` header decides: `same-origin` and `none`, which a browser sends only for an address the user typed or a bookmark they picked, are accepted, and `same-site` and `cross-site` are refused. A browser that sends no `Sec-Fetch-Site` has its `Origin` header’s host compared with the host it called. There’s no CSRF token and no CSRF cookie, so there’s nothing to expire in a page left open, and no setting that trusts another origin. The protocol endpoints are called from other sites by design, so the auth server leaves them out of the check. Each is protected by something else: | Path | Why another site may POST to it | | - | - | | `/auth/authorize` | OpenID Connect requires it to take a POST as well as a GET, and a POST only starts a sign-in, as a link would. Every step after it is checked. | | `/auth/token` | A confidential client presents its secret. A public client presents a one-time code, bound to its redirect URI and to a PKCE verifier only it holds. | | `/userinfo` | It takes an access token, never a cookie. | | `/connect/register` | It creates a client and acts on no signed-in user. It’s off by default. | | `/auth/logout` | Only a POST carrying an `id_token_hint`, which [RP-initiated logout](https://goiabada.dev/reference/endpoints/logout/) requires. A POST without one is checked. | | `/api/` and everything under it | Every API route takes an access token, never a cookie. | | `/static/` and everything under it | Images, styles and scripts, read with a GET, which the check never applies to. | The admin console leaves out only `/auth/callback`, its sign-in callback, which the OAuth `state` parameter protects, and `/static/`. ## Response headers Both servers add these headers to every answer: | Header | Value | What it does | | - | - | - | | `X-Frame-Options` | `DENY` | No other site can show a page in a frame, so it can’t trick a user into clicking. | | `Content-Security-Policy` | `frame-ancestors 'none'` | The same, for browsers that read the policy. It sets nothing else. | | `X-Content-Type-Options` | `nosniff` | The browser takes each answer, an uploaded image included, as the type it’s labelled with. | | `Referrer-Policy` | `same-origin` | A URL carrying a code or a `state` never reaches another site in the `Referer` header. | | `Strict-Transport-Security` | `max-age=31536000; includeSubDomains` | The browser uses only `https` for a year, subdomains included. Only when the server’s base URL is `https`. | > **Caution** > > **Never** replace `Referrer-Policy` with `no-referrer` at a proxy. A browser then sends `Origin: null` on form posts, and the cross-site check refuses every form on a browser that sends no `Sec-Fetch-Site`. ## Stored data - **Secrets are encrypted** in the database with AES-256-GCM, under `GOIABADA_AES_ENCRYPTION_KEY`: client secrets, the SMTP password, authenticator seeds, emailed codes and the keys that sign tokens. See [Secrets](https://goiabada.dev/deploy/secrets/#what-each-secret-protects). - **Authorization codes are stored as a digest.** The database holds each code’s SHA-256, so a copy of it holds no code anyone could redeem. The identifier in a session cookie is stored the same way. - **The audit log leaves out what users type.** An email address typed into the sign-in, registration or forgot-password form is recorded only as a digest, unless it becomes an account’s address, as when registering creates the account: `created_user` names it. See [the audit log’s details](https://goiabada.dev/concepts/audit-log/#details). ## The network - **Forwarded headers are ignored** until you turn on `GOIABADA_AUTHSERVER_TRUST_PROXY_HEADERS`, so a client can’t pick the IP address the rate limiter counts and the audit log records. See [client IP addresses](https://goiabada.dev/reference/environment-variables/#client-ip-addresses). - **The admin console’s calls to the auth server** are plain HTTP with the generated setups, carrying its client secret and administrators’ tokens, which is sound only on a network you trust. See [the hop from the admin console to the auth server](https://goiabada.dev/deploy/production-checklist/#the-hop-from-the-admin-console-to-the-auth-server). - **The auth server’s connection to the database** checks the database’s certificate only when `GOIABADA_DB_TLS_MODE` is `verify-ca` or `verify-full`. It checks it against the authorities in `GOIABADA_DB_TLS_CA_FILE`, or the system’s when that’s empty. Left unset, it’s `prefer`, which checks nothing and warns at every start. It encrypts when the server offers TLS, though on SQL Server only the login unless the server forces encryption. See [Where the database sits](https://goiabada.dev/deploy/database/#where-the-database-sits). - **Browser calls across origins** to `/auth/token`, `/auth/logout` and `/userinfo` are allowed only from the [web origins](https://goiabada.dev/concepts/clients/#web-origins) registered on clients. Discovery and the JWKS answer any origin, since they’re public, and nothing else answers a cross-origin call. ## Logs Request records redact every query value but a short list of safe ones, and verbose API logging redacts credentials. See [What a request record leaves out](https://goiabada.dev/deploy/logs/#what-a-request-record-leaves-out) and [Verbose API logging](https://goiabada.dev/deploy/logs/#verbose-api-logging). ## Thanks [Adrean Boyadzhiev](https://www.linkedin.com/in/adrean-boyadzhiev/) of [Lambda Bit](https://www.lambdabit.io/) assessed Goiabada’s security and advised on it. Thank you, Adrean. ## Next steps [Production checklist](https://goiabada.dev/deploy/production-checklist/): Everything to check before Goiabada goes live. [Audit log](https://goiabada.dev/concepts/audit-log/): What Goiabada records, and the events to alert on. [Rotate secrets](https://goiabada.dev/deploy/rotate-secrets/): Replace each secret without signing everyone out. # Invalid redirect_uri Source: https://goiabada.dev/troubleshooting/invalid-redirect-uri/ This page helps you when the auth server refuses the redirect URI your app sends. ## What you see Your app sends the browser to `/auth/authorize`, and instead of a sign-in page the browser shows **Unable to authorize** with one of these messages: - “Invalid redirect_uri parameter. The client does not have this redirect URI registered.” - “Invalid redirect_uri parameter. The redirect URI must be an absolute URI: a scheme is required, a fragment is not permitted, percent-escapes must be well formed, and an http or https URI must name a host.” - “The redirect_uri parameter is missing.” Nothing is sent back to your app. The auth server can’t trust an address the client hasn’t registered, so it never redirects to one, not even with an error. At the token endpoint, exchanging a code answers `400` with `invalid_grant` and “Invalid redirect_uri.” when the `redirect_uri` differs from the one in the authorization request. ## Why it happens A redirect URI must match one registered on the client **exactly**. The auth server compares it byte for byte: no wildcards, no case folding, and no tolerance for a trailing slash. `https://app.example.com/callback` and `https://app.example.com/callback/` are two different addresses. The usual causes: - **It was never registered,** or it was registered on another client. - **It differs in a detail:** `http` against `https`, a port, a trailing slash, a path’s case, or a query. - **Your app moved.** The app now runs on a new hostname or port, and the client still holds the old address. - **The database was set up for other URLs.** You pointed a deployment at a database created for another one, such as a copy from staging. The clients in it hold the other deployment’s addresses, the admin console’s own client among them. ## Fix it 1. Copy the `redirect_uri` exactly as your app sends it, from the address bar of the “Unable to authorize” page. 2. In the admin console, open **Admin**, **Clients**, click **Manage** next to your client and open **Redirect URIs**. 3. Add the address exactly as your app sends it, or change your app to send one already in the list, and click **Save**. At the token endpoint, send the same `redirect_uri` you sent in the authorization request. The code is bound to it. ### When it’s the admin console that’s refused The admin console signs in as the client `admin-console-client`, with the redirect URI `GOIABADA_ADMINCONSOLE_BASEURL` followed by `/auth/callback`. The auth server registers that address, and the base URL itself for signing out, on its first start, from its own `GOIABADA_ADMINCONSOLE_BASEURL`. It never updates them. So when you change the admin console’s URL after the first start, or the database was created for another deployment, the admin console’s own sign-in stops on “Invalid redirect_uri parameter”. Fix it in one of these ways: - **Put the old URL back** for a moment, sign in, and add the new addresses on the client’s **Redirect URIs** tab, then switch to the new URL. - **Call the Admin API** with a client of your own allowed `authserver:manage`: [`PUT /api/v1/admin/clients/{id}/redirect-uris`](https://goiabada.dev/reference/api/admin/operations/updateclientredirecturis/). The admin console’s client is an [administrator](https://goiabada.dev/reference/api/administrators/), so a token with only `authserver:manage-clients` is refused. - **Start from an empty database,** when the one you have holds nothing worth keeping. ## How redirect URIs are checked The checks run in this order, and each failure shows the page: 1. `client_id` is missing, names no client, names a disabled client, or names a client without the authorization code flow. 2. `redirect_uri` is missing. 3. `redirect_uri` isn’t absolute: it has no scheme, has a fragment, has a malformed percent-escape, or is `http` or `https` with no host. 4. `redirect_uri` isn’t registered on the client. These pages answer `200`, not an error status. Two checks come before them and answer `400` on the same page: a request the auth server can’t read, and `client_id`, `redirect_uri`, `response_type` or `response_mode` sent twice. **Loopback addresses are the one exception to the exact match.** When a registered redirect URI is `http` on `127.0.0.1`, `[::1]` or `localhost`, the port may differ, for `response_type=code` only. See [loopback redirect URIs](https://goiabada.dev/concepts/clients/#loopback-redirect-uris). **Removing a redirect URI takes effect on sign-ins already under way.** A user signing in when you remove it sees “You have not been sent anywhere” and the address the app asked for, instead of being sent back, and the auth server writes an `issuance_refused_redirect_uri` [audit event](https://goiabada.dev/concepts/audit-log/). A code issued before you removed it can’t be redeemed any more: the token endpoint answers `invalid_grant` with “The redirect URI recorded on this authorization code is no longer registered on the client, so the code can no longer be redeemed.”, and writes `redemption_refused_redirect_uri`. ## Next steps [Redirect URIs](https://goiabada.dev/concepts/clients/#redirect-uris): Every rule a redirect URI must meet. [The app gets no error back](https://goiabada.dev/troubleshooting/the-app-gets-no-error-back/): When a sign-in fails and nothing returns to your app. # The app gets no error back Source: https://goiabada.dev/troubleshooting/the-app-gets-no-error-back/ This page helps you when your app sends a user to sign in with a request the auth server refuses, and no `error` ever arrives at your redirect URI. Find your case by what the user sees. ## The user sees the sign-in page Your request had a mistake, such as a scope that doesn’t exist or a missing `code_challenge`, but the user is asked for their password as if nothing were wrong. Only once they sign in does your app get the `error` and `error_description`. That’s on purpose. The auth server won’t send an unauthenticated browser to a client’s redirect URI with an error, because an attacker could use that to bounce visitors through a trusted sign-in page to wherever the redirect URI points ([RFC 9700 section 4.11.2](https://www.rfc-editor.org/rfc/rfc9700.html#section-4.11.2)). So it keeps the error, signs the user in, and then redirects with it. That sign-in creates no session. If the user never signs in, your app gets nothing at all. The error is sent straight away, without asking for a password, when: - the request has `prompt=none`; - the browser already has a valid session, and the request doesn’t have `prompt=login`. **Fix it:** read the error once it arrives, and fix the request. While you develop, sign in with a test user, or sign in once beforehand so the browser has a session and the error comes back at once. ## The user sees “Unable to authorize” The auth server couldn’t tell where to send an error, so it shows it to the user instead. That covers: - a missing, unknown or disabled `client_id`, or a client without the authorization code flow; - a missing, malformed or unregistered `redirect_uri`, as [Invalid redirect_uri](https://goiabada.dev/troubleshooting/invalid-redirect-uri/) explains; - a `response_mode` other than `query`, `fragment` or `form_post`: “Invalid response_mode parameter. Supported values are: query, fragment, form_post.”; - `client_id`, `redirect_uri`, `response_type` or `response_mode` sent twice, or a request that isn’t correctly encoded; - **“This sign-in link is no longer valid.”**: your app sent a POST to `/auth/authorize`, and the browser came back to its one-time link after it expired or was used. **Fix it:** the message on the page names the problem. Fix the request. ## The user sees “You have not been sent anywhere” The page says “This application asked to send you to” an address, and “The request stops here.” The auth server had an error, or the user declined consent, and it wouldn’t redirect, because: - **the client registered itself.** A self-registered client is never sent an error: anyone can register one pointing anywhere, so an error redirect would be an open door. Every error, a declined consent included, ends on this page. - **the redirect URI was removed from the client** while the user was signing in. **Fix it:** for a self-registered client, treat a sign-in that doesn’t come back in a reasonable time as abandoned. If you need error responses, ask an administrator to create your client in the admin console instead. See [self-registered clients](https://goiabada.dev/concepts/clients/#self-registered-clients). ## The user sees another error page These end the sign-in on a page, and nothing is sent to your app: - **“This request is no longer active”**: another sign-in started in the same browser, such as in a second tab, and replaced this one. - **“This step is no longer available”**: the user went back in the browser to a step they had finished. - **“Your two-factor authentication settings changed”**: the user’s authenticator changed while they were setting one up. **Fix it:** each one asks the user to go back to your app and start again, so let them. A new authorization request starts a new sign-in. ## Next steps [Clients](https://goiabada.dev/concepts/clients/): Redirect URIs, self-registered clients and consent. [login_required](https://goiabada.dev/troubleshooting/login-required/): The errors a silent request gets back. # invalid_scope Source: https://goiabada.dev/troubleshooting/invalid-scope/ This page helps you when a scope your app asks for is refused, or quietly left out of the token. ## What you see One of these: - your app gets `error=invalid_scope` at its redirect URI, or from the token endpoint with `400`; - a refresh or a code exchange answers `invalid_grant`, with an `error_description` about a scope; - the token arrives, but its `scope` lacks something you asked for; - your app gets `access_denied` with “The user is not authorized to access any of the requested scopes”. The `error_description` names the scope and the reason. Read it first: it’s usually the whole answer. ## Why it happens ### The scope doesn’t exist A scope is either an OpenID Connect scope, `openid`, `profile`, `email`, `address`, `phone`, `groups` or `attributes`, or a permission, written `resource:permission`, such as `backend-service:create-product`. Scopes are separated by exactly one space. The auth server refuses: | `error_description` starts with | The problem | | - | - | | “Invalid scope format” | It’s neither an OpenID Connect scope nor `resource:permission`. | | “Invalid scope: … Could not find a resource with identifier” | No resource has that identifier. | | “Scope … is invalid.” or “Scope … is not recognized. The resource identified by” | The resource has no permission with that identifier. | | “The ‘scope’ parameter is malformed.” | Two spaces between scopes, or a space at either end. | | “The ‘scope’ parameter is missing.” | The authorization request has no scope. | | “The ‘scope’ parameter holds only ‘offline_access’” | `offline_access` grants nothing by itself. Ask for `openid` or a permission beside it. | **Fix it:** check the spelling against the resource’s **Permissions** tab in the admin console, or create the permission. ### The client may not have it **With the client credentials flow,** a client gets only the permissions granted to the client itself, on its **Permissions** tab. Asking for another answers “Permission to access scope ‘…’ is not granted to the client.” Asking for an OpenID Connect scope answers “Id token scopes … are not supported in the client credentials flow”, since there’s no user to describe. Leave `scope` out, and the token carries every permission the client holds; a client that holds none gets “The client holds no permissions, so a request without a scope has nothing to grant.” **The administrative scopes,** `authserver:manage` and the other permissions that administer Goiabada, need the client to be allowed to request them: “The client is not allowed to request the administrative scope”. See [administrative scopes](https://goiabada.dev/concepts/clients/#administrative-scopes). **Fix it:** grant the permission on the client’s **Permissions** tab, or turn on **May request administrative scopes** for that one. ### The user doesn’t hold it When a client signs a user in, the token carries only the permissions the user holds, directly or through a group. A permission the user lacks is **left out of the token without an error**, and the `scope` in the token response says what was granted. Only when nothing is left does the auth server refuse with `access_denied` and “The user is not authorized to access any of the requested scopes”. The [password grant](https://goiabada.dev/legacy-flows/ropc/) is the exception: it refuses a permission the user lacks with `invalid_scope` and “The user does not have permission for scope ‘…’.”, rather than leave it out. **Fix it:** grant the user the permission, or add them to a group that has it, and have your app send a new authorization request. The scope is decided during the sign-in, and a refresh keeps it. ### A refresh asked for more A refresh can ask for the same scope as the original grant or less, never more: “Scope ‘…’ is not recognized. The original access token does not grant the ‘…’ permission.” A refresh also checks again that the user still holds each permission and, for a client that requires consent or an `offline_access` token, still consents to it, and answers `invalid_grant` when they don’t. `authserver:manage-account` is checked against the user’s consent for every client but the admin console’s, whatever **Consent required** says. **Fix it:** send the user through the authorization request again with the wider scope. ### The Admin API or the Account API refuses the token These answer with an error code rather than an OAuth error: - `403 INSUFFICIENT_SCOPE`: the token carries none of the scopes the operation accepts. - `403 USER_CONTEXT_REQUIRED`: the Account API needs a token issued for a user, not one from the client credentials flow. - `403 MANAGE_SCOPE_REQUIRED`: the request acts on an administrator, which only `authserver:manage` may do. See [Errors](https://goiabada.dev/reference/api/errors/) and [Scopes](https://goiabada.dev/reference/api/scopes/). ## When and where scopes are checked At `/auth/authorize`, a scope error is sent to your redirect URI. When the browser has no session yet, the auth server first asks the user to sign in, and sends the error after: [The app gets no error back](https://goiabada.dev/troubleshooting/the-app-gets-no-error-back/#the-user-sees-the-sign-in-page) explains why. The permissions a user holds are checked again just before the code is issued, so a permission revoked while they sign in is left out too. Every `invalid_scope` from the token endpoint for a client that authenticated writes a `token_scope_denied` [audit event](https://goiabada.dev/concepts/audit-log/), and every refused administrative scope reaching a signed-in user or an authenticated client writes `administrative_scope_refused`. ## Next steps [Scopes](https://goiabada.dev/concepts/scopes/): The OpenID Connect scopes and the permission scopes. [Resources and permissions](https://goiabada.dev/concepts/resources-and-permissions/): Create the permissions your scopes name. # login_required Source: https://goiabada.dev/troubleshooting/login-required/ This page helps you when your app gets `error=login_required` back at its redirect URI. ## What you see Your app sent an authorization request, usually with `prompt=none` to check for a session without showing the user anything, and got `error=login_required` back, with one of these `error_description`s: | `error_description` | What it means | | - | - | | “User authentication is required” | The browser has no session with the auth server, or the session it had ended while the request was under way. | | “User session has expired” | The session was idle too long, or reached its maximum lifetime. | | “Session age exceeds max_age” | The session is fine, but the user signed in longer ago than the request’s `max_age` allows. | | “The current session user does not match the id_token_hint” | The browser is signed in as someone other than the user the `id_token_hint` names. | | “The authenticated user does not match the id_token_hint” | The user who signed in isn’t the user the `id_token_hint` names. | ## Why it happens `login_required` means the user has to sign in, and your request said not to ask them. That’s the expected answer to `prompt=none` whenever there’s no usable session, so it isn’t a fault on its own: it’s how your app learns the user is signed out. It’s surprising when you expected a session. The common reasons: - **The session timed out.** A session ends after 2 hours without activity, or 24 hours after sign-in, unless an administrator changed **User session - idle timeout in seconds** or **User session - max lifetime in seconds** under **Admin**, **Sessions**. - **The session was ended,** by the user signing out, by an administrator, by another user signing in on the same browser, because the user’s password was changed or reset, or because the user was disabled. - **The browser didn’t send the session cookie.** The auth server’s session cookie is `SameSite=Lax`, so a browser never sends it to a hidden iframe on a page of another site, whatever its third-party cookie setting. A silent request in an iframe works only when your app and the auth server are on the same site, such as `app.example.com` and `auth.example.com`. - **The hint names another user.** Your app sent the `id_token_hint` of a user who has since signed out, and someone else signed in on that browser. ## Fix it Treat `login_required` as “show the user a sign-in”: send the same authorization request again without `prompt=none`. The user signs in, and your app gets its code. For the hint mismatch, drop the `id_token_hint` of the previous user, or sign that user out of your app first. If your app is on another site than the auth server, silent requests in an iframe never carry the session, so don’t rely on them. Use a refresh token to keep the user signed in to your app, or redirect the whole page with `prompt=none` rather than using an iframe. ## What `prompt=none` checks With `prompt=none`, the auth server runs every check it would otherwise show the user a page for, and answers with an error instead of the page. `login_required` is one of them. The others: | `error` | `error_description` | Send the user through without `prompt=none` to | | - | - | - | | `interaction_required` | “Higher authentication level required” | enter their authenticator code, for the client’s ACR level. | | `interaction_required` | “Additional authentication setup required” | set up two-factor authentication, which `urn:goiabada:level2_mandatory` requires. | | `interaction_required` | “Authentication configuration has changed” | enter their code again, since their two-factor authentication changed. | | `consent_required` | “User consent is required” or “Additional consent is required” | approve the scopes on the consent screen. | | `access_denied` | “The user account is disabled” | nothing: an administrator disabled the user. Disabling a user also ends their sessions, so this request usually gets `login_required` instead. | | `access_denied` | “The user is not authorized to access any of the requested scopes” | nothing: the user holds none of the permissions asked for. | An `id_token_hint` without `prompt=none` changes what the user sees, not the result: a browser signed in as another user is asked to sign in again, and if the person who signs in isn’t the hint’s user, the request ends in `login_required` too. A user whose password was changed while they were signing in is sent back to sign in again, and a request with `prompt=none` gets “User authentication is required”. ## Next steps [prompt](https://goiabada.dev/concepts/prompt/): prompt=none, login and consent. [Sessions](https://goiabada.dev/concepts/sessions/): How long a session lasts, and what ends it. # This refresh token has been revoked Source: https://goiabada.dev/troubleshooting/this-refresh-token-has-been-revoked/ This page helps you when refreshing a token fails and takes your app’s newest refresh token down with it, so the user has to sign in again. ## What you see Your app refreshes, and the token endpoint answers `400`: ```json {"error": "invalid_grant", "error_description": "This refresh token has been revoked."} ``` Then the refresh token your app got from its previous refresh fails too, with the same “This refresh token has been revoked.” The audit log may hold a `refresh_token_replay_detected` [event](https://goiabada.dev/concepts/audit-log/) for your client. ## Why it happens A refresh token is good for one refresh. Each refresh returns a new one and retires the old one, and that’s called rotation. When a retired refresh token comes back, the auth server can’t tell whether an attacker stole it or your own app sent it twice. So it assumes the worst: it revokes every live refresh token descended from the same sign-in, the newest one included. That’s what stops someone holding a stolen token from refreshing for as long as they like. Your own app usually causes it in one of two ways: - **Parallel refreshes.** Two threads, tabs or instances notice the access token has expired, and each refreshes with the same refresh token. One wins. The other is either refused, or, when it reads the token after the winner retired it, treated as a replay. - **Retrying after a lost response.** The auth server rotated the token, but the response never reached your app. Retrying sends the retired token. There’s no grace period for either: a retired token is retired the moment the refresh succeeds. ## Fix it > **Caution** > > Refresh through **one** path at a time, and store the new refresh token in place of the old one **before** you use anything else from the response. - **Serialize refreshes.** Share one refresh between every thread, tab or instance that needs a token, with a lock or a single in-flight request the others wait for. - **Treat a lost response as “sign in again”, not “retry”.** Without the response, your app doesn’t have the new refresh token, and the old one is retired. Once a family is revoked, the user has to sign in to your app again. Their session with the auth server survives, so they usually go straight through. ## What’s revoked, and what isn’t **Only that one grant is revoked.** The user’s session with the auth server, their sign-ins to other clients and their other grants keep working. **Access tokens already issued stay valid** until they expire. They’re signed JWTs your APIs check on their own, so revoking the refresh token stops the next refresh, not an access token already handed out. Keep access token lifetimes short. **The replay check comes last.** A refresh that fails for another reason first, such as a user who was disabled or whose password changed, gets that reason, and nothing is revoked. The `refresh_token_replay_detected` event is written when the auth server revokes something, not on every repeat, and its `first_refresh_token_jti` identifies the grant. It doesn’t prove an attack: an app that refreshes in parallel triggers it too. ## Other reasons a refresh fails These refreshes answer `invalid_grant` too: | `error_description` | Why | | - | - | | “The refresh token is invalid (token has invalid claims: token is expired).” | The refresh token expired. An offline one expires after going unused for its idle timeout. | | “The refresh token is invalid because it was superseded.” | The user’s password was changed or reset, or the user was disabled and has since been enabled again. | | “The refresh token is invalid because the associated session has expired or been terminated.” | The session it was issued through ended, or the client was made public. | | “The refresh token is invalid because it has expired (offline_access_max_lifetime).” | An offline refresh token reached its maximum lifetime. | | “The refresh token is invalid.” | The user is disabled, or the token is unknown. Rarely, a token issued while its family was being revoked. | | “The user has either not given consent to this client or the previously granted consent has been revoked.” | The user withdrew their consent, under **Account**, **Manage consents**, or it was never given. Checked for an offline refresh token, for any refresh token of a client that requires consent, and for a refresh renewing `authserver:manage-account` from a sign-in to any client but the admin console’s. | | “Scope ‘authserver:manage-account’ is not recognized. The user has not consented to the ‘authserver:manage-account’ permission.” | The user’s consent leaves `authserver:manage-account` out: they unticked it on the consent screen. Send them through an ordinary sign-in to approve it, or refresh with a `scope` that leaves it out. | | “The refresh token is invalid because it does not belong to the client.” | The request named a client other than the one the token was issued to. Each client refreshes only its own tokens. | ## Next steps [Refresh tokens](https://goiabada.dev/concepts/refresh-tokens/#rotation): Refresh token rotation and what your client must do. [Audit log](https://goiabada.dev/concepts/audit-log/): The refresh_token_replay_detected event and its details. # Sign-out answers 403 Source: https://goiabada.dev/troubleshooting/sign-out-answers-403/ This page helps you when your app signs a user out with a POST to `/auth/logout` and gets `403 Forbidden` back. ## What you see The auth server answers `403 Forbidden` with a plain-text body, not a page: ```plaintext Your request was refused for security reasons. Please reload the page and try again. ``` The user stays signed in. The auth server’s log has a `cross-origin request refused` warning, whose `explanation` and `remedy` fields say which check refused the request. No audit event is written. ## Why it happens The auth server refuses a POST that comes from another site, unless it can tell the request is safe. That’s what stops any web page from submitting forms to the auth server on a user’s behalf. For `/auth/logout`, a POST is safe when it carries an `id_token_hint`. A POST **without** one is exactly what the auth server’s own “Are you sure you want to sign out?” screen submits when the user clicks **Yes**. Accepting it from another site would let any page sign your users out without asking them, so the auth server refuses it. So the usual cause is a self-submitting form on your app’s page that posts to `/auth/logout` without an `id_token_hint`. Your app’s origin differs from the auth server’s, whether it’s another site or another host on the same domain, such as `app.example.com` beside `auth.example.com`. On the same domain, the warning’s `explanation` speaks of a sibling host and a deployment misconfiguration, and its `remedy` of serving the form from one origin. That’s meant for the auth server’s own forms, as in [When the confirmation screen itself answers 403](https://goiabada.dev/troubleshooting/sign-out-answers-403/#when-the-confirmation-screen-itself-answers-403). When the form is your app’s, use the fix that follows. ## Fix it Pick one: - **Send the `id_token_hint`** in the form, with the ID token the user signed in with. A cross-site POST with a hint is accepted. If the auth server can confirm the hint, the user is signed out straight away. If it can’t, it answers `303 See Other` back to `GET /auth/logout`, which shows the confirmation screen. - **Use a GET.** Send the browser to `/auth/logout` with a link or a redirect. A GET is never refused this way, and without a hint the user sees the confirmation screen. POST is worth it when you have an `id_token_hint`, because it keeps the ID token out of the address bar, the browser’s history and the `Referer` header. ## When the confirmation screen itself answers 403 If clicking **Yes** on the auth server’s own “Are you sure you want to sign out?” screen answers 403, the form and the auth server look like two different sites to the browser, though they’re the same. The same refusal then hits every form the auth server serves, the sign-in page included. The warning’s `explanation` names one of these: - **The browser reached the auth server on another hostname, port or scheme** than the one it’s configured with, such as a bare domain beside a `www` host, or `http` beside `https`. Use exactly `GOIABADA_AUTHSERVER_BASEURL`. - **A proxy rewrote the `Host` header**, so it no longer matches the `Origin` the browser sent. Configure the proxy to pass the original `Host` on, such as `proxy_set_header Host $host;` in Nginx. - **The page was served with `Referrer-Policy: no-referrer`**, so the browser sent `Origin: null`. The auth server sets `same-origin` itself: check that nothing in front of it replaces that header. > **Caution** > > **Never** work around the refusal by stripping `Origin` or `Sec-Fetch-Site` at the proxy. The check is what keeps other sites from submitting forms as your users. ## Next steps [Logout endpoint](https://goiabada.dev/reference/endpoints/logout/): Every /auth/logout parameter and answer. [id_token_hint](https://goiabada.dev/concepts/id-token-hint/): What the hint is and how the auth server checks it. # Too many attempts or 429 Source: https://goiabada.dev/troubleshooting/too-many-attempts-or-429/ This page helps you when Goiabada’s rate limiter refuses a request, and especially when it refuses everyone at once. ## What you see The limits counted by a user or an email always apply: wrong passwords for one account, one-time codes, the Account pages’ password checks, email verification, and password-reset and registration mails to one address. The limits counted by an IP address apply once you set `GOIABADA_AUTHSERVER_RATELIMITER_ENABLED` to `true`. A refused request gets `429 Too Many Requests` with a `Retry-After` header, in the format the endpoint usually answers with: - **A sign-in, two-factor, registration, activation or password reset page** shows “Too many attempts” and “Too many requests have come from this browser, or for this account, in a short time. Please wait a few minutes and try again.” - **The token endpoint’s password grant and `POST /connect/register`** answer `{"error": "invalid_request", "error_description": "Too many requests. Please wait and try again later."}`. - **The Account API** answers the error code `TOO_MANY_REQUESTS`. The auth server logs a `rate limit reached` warning naming the limiter, and writes a `rate_limit_exceeded` [audit event](https://goiabada.dev/concepts/audit-log/), once per key and window on each replica. ## Why it happens Most of the time it’s the limiter doing its job: someone typed a wrong password too often, or a script is trying addresses. Each limit counts by something, an IP address, an email or a user, and refuses the next request once that one has used its budget. The [rate limits table](https://goiabada.dev/reference/environment-variables/#rate-limits) lists every limit, what it counts and what it counts by. When **every user** sees “Too many attempts” at once, with `GOIABADA_AUTHSERVER_RATELIMITER_ENABLED` on, the auth server is almost certainly behind a proxy it doesn’t trust. It then sees the proxy’s address on every request, so all your users share one IP address’s budget, and a handful of sign-ins uses it up for everyone. ## Fix it If one user or one address is refused, wait. The `Retry-After` header gives the limit’s window in seconds, and the budget comes back gradually over the window after it. Restarting the auth server resets only the limits it keeps in the process, as described below, so on PostgreSQL, MySQL and SQL Server it doesn’t reset the limits on wrong passwords. If one account can’t sign in with its password and its owner didn’t cause it, someone is guessing that password: block their address at your proxy or CDN. The `rate_limit_exceeded` audit event names the account by `email_digest`. If everyone is refused behind a proxy, tell the auth server to read the client’s address from the proxy’s headers: 1. Look for a `rate limiter configuration warning` record in the auth server’s startup log, whose `warning` reads as below. It’s there when the limiter is on and forwarded headers are not trusted: ```plaintext config: GOIABADA_AUTHSERVER_RATELIMITER_ENABLED is true but GOIABADA_AUTHSERVER_TRUST_PROXY_HEADERS is false; if this server sits behind a reverse proxy, every request resolves to the proxy's address and the whole deployment shares one per-IP bucket. Set GOIABADA_AUTHSERVER_TRUST_PROXY_HEADERS, and GOIABADA_AUTHSERVER_TRUSTED_PROXIES with it ``` 2. Set `GOIABADA_AUTHSERVER_TRUST_PROXY_HEADERS` to `true`. 3. If more than one proxy sits in front of the auth server, such as a CDN and a load balancer, set `GOIABADA_AUTHSERVER_TRUSTED_PROXIES` to the addresses of every hop you control, including the one that connects to the auth server. Behind a single proxy, leave it empty. 4. Restart the auth server. > **Danger** > > Trust forwarded headers only when the proxy is the **only** way to reach the auth server. A client that connects directly can send any `X-Forwarded-For` it likes, and picks the address it’s limited under. [Client IP addresses](https://goiabada.dev/reference/environment-variables/#client-ip-addresses) has the precise rules, and [Client IP and proxy trust](https://goiabada.dev/deploy/client-ip-and-proxy-trust/) shows them for each setup. ## How the limits count A limit that counts failures, such as wrong passwords, isn’t spent by a success, so signing in normally never uses it up. A limit that counts every request is spent by every request, successful or not. Limits on failures are kept in the database on PostgreSQL, MySQL and SQL Server, so they hold across replicas and a restart doesn’t refill them. Limits on every request are kept in each process: each replica allows the full budget, and a restart resets it. On SQLite, which runs one replica, both are kept in the process. Wrong passwords meet up to two limits. `pwd_account` counts by email alone, as a ceiling across every network, and always applies. 100 wrong passwords in an hour, from anywhere, use it up, and then the account can’t sign in with its password for the rest of the hour, whoever is typing and even with the right password. A password reset doesn’t clear it, and anyone already signed in stays signed in. That’s the price of having a ceiling per account at all, and the remedy for someone doing it on purpose is blocking them at your proxy or CDN. With `GOIABADA_AUTHSERVER_RATELIMITER_ENABLED` on, `pwd_account_net` counts by IP address and email together, 10 every 15 minutes, so someone guessing from one network runs out of their own budget long before the account’s, and the account’s owner signs in normally from anywhere else. ## Next steps [Rate limits](https://goiabada.dev/reference/environment-variables/#rate-limits): Every limit, its budget and what it counts by. [Audit log](https://goiabada.dev/concepts/audit-log/): Find rate_limit_exceeded and the limiter that tripped. # Locked out of the admin console Source: https://goiabada.dev/troubleshooting/locked-out-of-the-admin-console/ This page helps you get back into the admin console when signing in to it stops working. Find your case first: - **Signing in fails for every administrator** with **Sign-in failed** or **Server error**: [the admin console’s client secret changed](https://goiabada.dev/troubleshooting/locked-out-of-the-admin-console/#the-admin-consoles-client-secret-changed). - **The password you set in `GOIABADA_ADMIN_PASSWORD` is refused:** [you changed it after the first start](https://goiabada.dev/troubleshooting/locked-out-of-the-admin-console/#you-changed-goiabada_admin_password). - **The only administrator can’t sign in**, because they forgot their password or lost their authenticator: [the last administrator can’t sign in](https://goiabada.dev/troubleshooting/locked-out-of-the-admin-console/#the-last-administrator-cant-sign-in). ## The admin console’s client secret changed The admin console signs administrators in as a client of the auth server, `admin-console-client`, and proves who it is with that client’s secret. It uses the same secret for the token it reaches its own sessions with, which the auth server stores. The auth server keeps the secret in its database. The admin console reads its copy from `GOIABADA_ADMINCONSOLE_OAUTH_CLIENT_SECRET`. When the two differ, the auth server refuses the admin console with `invalid_client`, and: - a sign-in fails after the password, on **Sign-in failed**: “The admin console could not complete the sign-in with the auth server. This usually means the console’s client secret or the auth server’s address is wrong.”; - an administrator already signed in is signed out once their access token falls due; - once the token the admin console holds for its sessions expires, within the access token lifetime, 5 minutes unless you changed it, every page that needs a session answers `500` on **Server error**, starting a sign-in included; - an admin console that starts, or restarts, doesn’t start at all. It asks the auth server for that token before it listens, and stops when it’s refused: on Kubernetes its pod never becomes ready, so a rollout waits with the earlier pods still serving, and under Docker Compose its container keeps restarting. The admin console’s log has the cause: `the auth server's token endpoint answered 401 (invalid_client: Client authentication failed. Please review your client_secret.)`. When it refused to start, the record is `the auth server does not accept the admin console's client secret, so the admin console cannot start`, with that cause as its `error`. The auth server and every other client keep working. That happens when someone generated a new secret on the client’s **Authentication** tab and saved it without giving the admin console the same one, or when the admin console’s configuration lost its value. The auth server reads `GOIABADA_ADMINCONSOLE_OAUTH_CLIENT_SECRET` only on its first start, so changing the variable later never changes the database. ### Fix it If you know the secret the database holds, put it back in the admin console’s configuration and restart the admin console. Otherwise, set a secret you know through the [Admin API](https://goiabada.dev/reference/api/authentication/), with a client of your own allowed `authserver:manage`. It has to be `manage`. The admin console’s client is an [administrator](https://goiabada.dev/reference/api/administrators/), and only a token with `authserver:manage` writes to one, so a `manage-clients` token is refused `403 MANAGE_SCOPE_REQUIRED`. Holding `manage` makes your client an administrator too, so keep its secret with the care you give the admin console’s own. > **Caution** > > Create that client **before** you need it: a client with the **Client credentials flow** on, and the `authserver` resource’s `manage` permission granted on its **Permissions** tab. Once the admin console is locked out, it’s the only way back in. 1. Get a token for your client, and find the admin console client’s id: ```bash read -rsp 'API client secret: ' API_CLIENT_SECRET; echo TOKEN=$(printf %s "$API_CLIENT_SECRET" | curl -s https://auth.example.com/auth/token \ -d grant_type=client_credentials -d client_id= \ --data-urlencode client_secret@- -d scope=authserver:manage | jq -r .access_token) unset API_CLIENT_SECRET CLIENT_ID=$(curl -s https://auth.example.com/api/v1/admin/clients -H "Authorization: Bearer $TOKEN" \ | jq '.clients[] | select(.clientIdentifier == "admin-console-client") | .id') CLIENT_SECRET=$(openssl rand -hex 32) ``` 2. Store the new secret where the admin console reads it, before the auth server holds it: **Docker Compose** Set `GOIABADA_ADMINCONSOLE_OAUTH_CLIENT_SECRET` to `$CLIENT_SECRET` under both services in `docker-compose.override.yml`. **Native binaries** Set `GOIABADA_ADMINCONSOLE_OAUTH_CLIENT_SECRET` to `$CLIENT_SECRET` in `/etc/goiabada/goiabada.env`, the env file both servers read. **Kubernetes** ```bash kubectl patch secret goiabada-secrets -n goiabada --type=merge --patch-file=/dev/stdin < If you’re a registered user, a password reset link has been sent to your email address. Make sure to search both your inbox and spam/junk folder for the link. That’s on purpose: a different answer would tell anyone which addresses have an account. The reason stays in the audit log, as the `outcome` of the `requested_password_reset` [event](https://goiabada.dev/concepts/audit-log/): | `outcome` | Why no email was sent | Fix it | | - | - | - | | `unknown_address` | No user has that email address. | Check the address the user typed. They may have signed up with another. | | `unverified_address` | The user’s email address isn’t verified, so the auth server won’t trust it with a reset link. | Mark it verified on the user’s **Email** tab, with **Email verified**, or set their password yourself. | | `account_disabled` | The user is disabled. | Enable the user on their **Details** tab, if they should be able to sign in. | | `code_issued` | The link was made and the email was sent, or the sending failed. | If the user still has nothing, check the auth server’s log for `unable to send the password reset email`, and your SMTP settings. Then check their spam folder. | When sending fails, the auth server’s log has an error such as `unable to send the password reset email`, with the cause. A form sent while the auth server is very busy can be dropped, with the warning `a job to run after its response was dropped, too many in flight`. An address can ask for 5 links every 5 minutes, whether or not the rate limiter is on, and with it on an IP address can ask for 20. Past that the form answers “Too many attempts”: see [Too many attempts or 429](https://goiabada.dev/troubleshooting/too-many-attempts-or-429/). ## The link says the code is invalid or expired The user follows the link and sees: > Unable to set the password. The verification code appears to be invalid or expired. Please click this link and attempt the verification process again. A reset link works once, for 5 minutes after it was sent, and the user then has 5 more minutes to fill in the form. A link to an account with no password yet, such as the one an administrator’s setup email sends, works for 24 hours instead. It stops working when: - it’s past that lifetime; - it was already used; - the user asked for another one since, which replaces it; - the user was disabled in the meantime; - the user’s email address was changed since it was sent, by the user or by an administrator. The user should ask for a new link and use it straight away. The `reason` of the `failed_reset_password_code` audit event narrows it down: `code_expired` for a link past its lifetime, `marker_expired` for a form sent more than 5 minutes after the link was followed, `account_disabled` for a disabled user, and `unknown_code` for a link that was used, replaced by a newer one, or retired by an address change, which it can’t tell apart. ## The new password is refused The form says why, such as “The minimum length for the password is 8 characters” or “As per our policy, an uppercase character is required in the password.” **Password policy** under **Admin**, **General** decides what’s needed: | Policy | At least | Also needs | | - | - | - | | No policy | 1 character | Nothing | | Low strength | 6 characters | Nothing | | Medium strength | 8 characters | A lowercase letter, an uppercase letter and a digit | | High strength | 10 characters | A lowercase letter, an uppercase letter, a digit and a symbol | Every policy allows at most 64 bytes. An accented or non-English character counts as two bytes or more. ## Set the password yourself When email isn’t an option, an administrator can set the password: open **Admin**, **Users**, the user, and their **Authentication** tab, then use **Set password**. Setting it signs the user out of every session. The Admin API does the same with [`PUT /api/v1/admin/users/{id}/password`](https://goiabada.dev/reference/api/admin/operations/updateuserpassword/). ## Next steps [Password recovery](https://goiabada.dev/concepts/password-recovery/): How reset links work, and who gets one. [Locked out of the admin console](https://goiabada.dev/troubleshooting/locked-out-of-the-admin-console/): When it's the administrator who can't sign in. # Certificates are not issued Source: https://goiabada.dev/troubleshooting/certificates-are-not-issued/ This page helps you when cert-manager doesn’t issue the certificates for a Kubernetes deployment set up with [Envoy Gateway and cert-manager](https://goiabada.dev/deploy/kubernetes/gateway-and-certificates/). ## What you see `kubectl get certificates -n goiabada` shows `goiabada-tls-auth` and `goiabada-tls-admin` with `READY` at `False`, or lists nothing at all. Meanwhile, `https://auth.example.com` fails in the browser with a certificate or connection error, since the Gateway has no certificate to serve. ## Why it happens The certificates come from a chain of four pieces, and any of them can stop it: 1. The generated Gateway carries the annotation `cert-manager.io/cluster-issuer: "letsencrypt-prod"`. cert-manager reads it, and creates one Certificate for each HTTPS listener, named after the listener’s `certificateRefs`: `goiabada-tls-auth` and `goiabada-tls-admin`. It does that only with its Gateway API support turned on. 2. The ClusterIssuer named `letsencrypt-prod` asks Let’s Encrypt for each certificate. 3. Let’s Encrypt checks you control each host name with an HTTP-01 challenge: it fetches a token from `http:///.well-known/acme-challenge/` on port 80. 4. cert-manager answers that request through a temporary HTTPRoute it attaches to the Gateway named in the ClusterIssuer’s solver, and a solver pod it starts in the `goiabada` namespace. The Gateway’s `http` listener on port 80 carries it. So the usual causes are: - **No Certificates at all:** cert-manager runs without `--enable-gateway-api`, so it never reads the Gateway. Step 2 of [Gateway and certificates](https://goiabada.dev/deploy/kubernetes/gateway-and-certificates/#set-it-up) turns it on, after Envoy Gateway has brought the Gateway API’s resources. - **The issuer doesn’t match:** the ClusterIssuer isn’t named `letsencrypt-prod`, isn’t ready, or its solver’s `parentRefs` names another Gateway or namespace than the one you deployed into. - **DNS doesn’t point at the Gateway yet,** so Let’s Encrypt fetches the token from somewhere else. - **The names were looked up before their records existed.** Before it asks Let’s Encrypt, cert-manager fetches the token itself, and a resolver that answered “no such name” then keeps that answer for as long as your zone allows, often 30 minutes, even once the records exist. The Challenge’s reason ends in `no such host`, while `nslookup` from your own machine finds the address. It clears by itself; creating the ClusterIssuer only once both names resolve, as [Gateway and certificates](https://goiabada.dev/deploy/kubernetes/gateway-and-certificates/#set-it-up) does, avoids it. - **Port 80 doesn’t reach Envoy:** a firewall or security group in front of the load balancer, or the `Local` traffic policy with nodes that have no Envoy pod. - **The solver pod is refused,** because the namespace enforces the `restricted` Pod Security standard and the pod doesn’t meet it. The generated namespace only warns, and cert-manager’s own solver pod meets `restricted`, but a `podTemplate` in the ClusterIssuer’s solver that sets its own security context, or cert-manager run with `--acme-http01-solver-run-as-non-root=false`, can break that. `kubectl get events -n goiabada` shows a refused pod. ## Fix it 1. Follow the chain to where it stops: ```bash kubectl get certificates,certificaterequests,orders,challenges -n goiabada kubectl describe challenges -n goiabada ``` A Challenge’s status and events say what Let’s Encrypt or cert-manager’s own check got back, such as a host that doesn’t resolve or a connection that timed out. 2. Check the issuer is ready and named as the Gateway expects: ```bash kubectl get clusterissuer letsencrypt-prod ``` 3. Check that the Gateway has an address and that DNS points at it: ```bash kubectl get gateway goiabada -n goiabada nslookup auth.example.com ``` `PROGRAMMED` should be `True`, and both host names should resolve to the `ADDRESS` it shows. Check what the cluster resolves too, since it can remember a name as missing after your machine finds it: ```bash kubectl run dnscheck --rm -i --restart=Never --image=busybox -- nslookup auth.example.com ``` 4. Under the `Local` traffic policy, check that Envoy runs a pod on every node: ```bash kubectl get daemonset -n envoy-gateway-system ``` If your load balancer still can’t reach Envoy, switch to the `Cluster` traffic policy, whose `gatewayclass.yaml` has no `envoyDaemonSet`. Goiabada then sees a node’s address for every client. If your `eg` GatewayClass has no `parametersRef` to the EnvoyProxy at all, apply the `gatewayclass.yaml` for your traffic policy from [Gateway and certificates](https://goiabada.dev/deploy/kubernetes/gateway-and-certificates/#set-it-up). 5. Once the cause is fixed, a challenge still pending passes at cert-manager’s next check. One Let’s Encrypt has already refused ends that attempt, and cert-manager waits before the next one, an hour after the first failure and longer after each later one. To try again at once, renew with [cmctl](https://cert-manager.io/docs/reference/cmctl/), cert-manager’s command-line tool: ```bash cmctl renew --namespace goiabada --all ``` > **Caution** > > Let’s Encrypt limits how many failed validations it accepts for one host name in an hour. Fix the cause before you retry, rather than renewing in a loop. ## How the certificates are served and renewed The Gateway terminates TLS for both host names, with the certificate in each listener’s Secret. On the `http` listener on port 80, Goiabada’s one HTTPRoute, `goiabada-https-redirect`, answers every request with a `301` to HTTPS. cert-manager’s challenge route matches the exact path `/.well-known/acme-challenge/`, which takes precedence over it, so the challenges still reach the solver. cert-manager renews each certificate before it expires the same way, so port 80 has to stay reachable after the first issue too. ## Next steps [Gateway and certificates](https://goiabada.dev/deploy/kubernetes/gateway-and-certificates/): The Envoy Gateway and cert-manager setup, step by step. [Invalid redirect_uri](https://goiabada.dev/troubleshooting/invalid-redirect-uri/): When the site answers, but signing in fails. # CrashLoopBackOff or unable to create the database connection Source: https://goiabada.dev/troubleshooting/crashloopbackoff-or-unable-to-create-the-database-connection/ This page helps you when the auth server stops as soon as it starts, over and over. ## What you see On Kubernetes, `kubectl get pods -n goiabada` shows the auth server’s pod in `CrashLoopBackOff`, with its restarts climbing. Under Docker Compose, the container keeps restarting. Either way, the auth server exits with status 1 before it listens, so `/health` never answers. Its last record says why. On Kubernetes, read the run that stopped: ```bash kubectl logs -n goiabada deployment/goiabada-authserver --previous ``` Under Docker Compose, `docker compose logs goiabada-authserver`. Most often the record is `unable to create the database connection`, and its `error` starts with one of these: | Engine | `GOIABADA_DB_CREATE` on, the default | `GOIABADA_DB_CREATE` off | | - | - | - | | PostgreSQL | `unable to check whether the database exists` | `unable to connect to database` | | MySQL | `unable to create database` | `unable to connect to database` | | SQL Server | `unable to connect to master database` | `unable to connect to database` | The rest of the `error` is the database driver’s own words, such as `connection refused`, `no such host`, a refused password or a database that doesn’t exist. Words starting `x509:`, or saying the server doesn’t support TLS, mean the auth server [refused TLS or the database’s certificate](https://goiabada.dev/troubleshooting/database-tls-connection-fails/). When the database doesn’t exist yet and the login can’t create one, the `error` starts `unable to create database` on all three engines: create it yourself, as [Database](https://goiabada.dev/deploy/database/) describes. A login that reaches the server but lacks a right in Goiabada’s database gets further, and the database names the right: `permission denied for schema public` on PostgreSQL, `CREATE command denied` or `SELECT command denied` on MySQL, after `unable to prepare the migration runner` or `unable to migrate the database`. On MySQL with `GOIABADA_DB_CREATE` on, a login without the `CREATE` privilege on the database stops earlier, at `unable to create database` with `Error 1044 ... Access denied ... to database`, since `CREATE DATABASE IF NOT EXISTS` needs it even when the database exists. Check what the user may do in `GOIABADA_DB_NAME`: see [Database](https://goiabada.dev/deploy/database/). ## Why it happens The auth server opens the database before anything else, and it doesn’t retry: if the first connection fails, it exits, and Kubernetes or Compose starts it again. Kubernetes waits longer before each restart, which is the back-off in `CrashLoopBackOff`. With `GOIABADA_DB_CREATE` on, which is the default, the auth server first connects to the server’s own database to create Goiabada’s when it’s missing: `postgres` on PostgreSQL, `master` on SQL Server. That’s why the message differs. With it off, it connects straight to `GOIABADA_DB_NAME`. The usual causes: - **The host or port is wrong** for where the auth server runs. In Docker Compose, use the database’s service name. In Kubernetes, use the database’s Service name, or the managed database’s host name. - **The database refuses the connection,** because its firewall or allowed networks don’t include your cluster’s or host’s addresses. - **The username or password is wrong,** or the database doesn’t exist and `GOIABADA_DB_CREATE` is off. - **The database’s endpoint is IPv6 and your cluster isn’t.** Some managed services give direct connections over IPv6 only, and a connection pooler endpoint over IPv4. A database that doesn’t answer at all, as when a firewall drops packets, makes the start wait for the connection attempt to time out instead. On Kubernetes, a pod that hasn’t answered `/health` within its startup probe’s 5 minutes is restarted. On SQLite, `unable to connect to database` means the file couldn’t be opened. When the rest of the message is `attempt to write a readonly database`, see [attempt to write a readonly database](https://goiabada.dev/troubleshooting/attempt-to-write-a-readonly-database/). ## Fix it 1. Check the database variables the auth server reads: `GOIABADA_DB_TYPE`, `GOIABADA_DB_HOST`, `GOIABADA_DB_PORT`, `GOIABADA_DB_NAME`, `GOIABADA_DB_USERNAME` and `GOIABADA_DB_PASSWORD`. On Kubernetes the first five are in the `goiabada-authserver-config` ConfigMap, and the password is in the `goiabada-secrets` Secret. 2. Check that the database answers from where the auth server runs. On Kubernetes, from a throwaway pod in the same namespace, with your host and port: ```bash kubectl run -it --rm debug -n goiabada --image=busybox --restart=Never -- \ nc -zv your-db-host 5432 ``` If it doesn’t connect, fix the network first: the host name, the database’s allowed networks, or a pooler endpoint over IPv4. 3. If it connects, the credentials or the database name are what’s wrong. Connect with the same username and password from your own client to check them, and see [Database](https://goiabada.dev/deploy/database/) for what each engine needs prepared, including what `GOIABADA_DB_CREATE` asks of the user. 4. Fix the value, then restart the auth server: `kubectl rollout restart deployment/goiabada-authserver -n goiabada`, or `docker compose up -d`. ## When it isn’t the database The auth server refuses to start on these too, each with its own record and exit status 1: - `the data encryption key is missing or malformed, so the auth server cannot start`: `GOIABADA_AES_ENCRYPTION_KEY` isn’t set to 64 hex characters, or `GOIABADA_AES_ENCRYPTION_KEY_PREVIOUS` is set to something else. - `the data encryption key does not decrypt the stored data, so the auth server cannot start`: `GOIABADA_AES_ENCRYPTION_KEY` isn’t the key the stored data is encrypted under, typically because a newly generated `goiabada-secrets.yaml`, or the output of a second setup wizard run, replaced it. Set it to the last key the data was encrypted under, from your [backup of it](https://goiabada.dev/deploy/secrets/#back-up-the-aes-key): the key the database was set up with or, after a [rotation](https://goiabada.dev/deploy/rotate-secrets/), the key you rotated to. On Kubernetes the pod never becomes ready, so a rollout waits with the earlier pods still serving. The record’s `error` is `the data encryption key check failed: the stored data does not decrypt under GOIABADA_AES_ENCRYPTION_KEY`. When `GOIABADA_AES_ENCRYPTION_KEY_PREVIOUS` holds another key, as during a rotation, it’s `the data encryption key check failed: data-at-rest decrypts under neither GOIABADA_AES_ENCRYPTION_KEY nor GOIABADA_AES_ENCRYPTION_KEY_PREVIOUS: the stored data does not decrypt under GOIABADA_AES_ENCRYPTION_KEY` instead. - `the auth server session keys are missing or malformed, so the auth server cannot start`: the session keys aren’t set, or aren’t the length they must be. - `the previous session key pair is incomplete or malformed, so the auth server cannot start`: only one of the two `_PREVIOUS` session key variables is set, or one is malformed. Set both while you [rotate the keys](https://goiabada.dev/deploy/rotate-secrets/), or neither once you’re done. - `bootstrap credentials are not configured, so the auth server cannot start`: `GOIABADA_AUTHSERVER_BOOTSTRAP_ENV_OUTFILE` is set and the credentials it wrote haven’t been copied into the two servers’ configuration yet. Copy them, then restart both. - `unable to bootstrap the database`, with an `error` starting `GOIABADA_ADMIN_PASSWORD cannot be the first administrator's password`: on a first start, the password is one the auth server refuses, such as one that’s empty, shorter than 15 characters or longer than 72 bytes. The rest of the error says why. - `the trusted proxy list is malformed, so the auth server cannot start`: `GOIABADA_AUTHSERVER_TRUSTED_PROXIES` has an entry that isn’t an address or a CIDR range. - `initial setup is required, because the database is empty and neither bootstrap mode is configured`: the database is empty, and neither `GOIABADA_ADMINCONSOLE_OAUTH_CLIENT_SECRET` nor `GOIABADA_AUTHSERVER_BOOTSTRAP_ENV_OUTFILE` is set to seed it with. When the record is `unable to create the database connection` and its `error` starts `unable to migrate the database`, the database answered but its schema couldn’t be brought up to date: see [Waiting for the migration lock, or marked dirty](https://goiabada.dev/troubleshooting/waiting-for-the-migration-lock-or-marked-dirty/), or [attempt to write a readonly database](https://goiabada.dev/troubleshooting/attempt-to-write-a-readonly-database/) on SQLite. A variable that doesn’t parse, such as a port that isn’t a number, stops it even sooner, with one line on standard error starting `malformed configuration:` that names the variable, and exit status 2. If the line names a `PGSSL` variable, see [PostgreSQL TLS variables stop startup](https://goiabada.dev/troubleshooting/postgresql-tls-variables/). ## While the database is unreachable `/health` answers only once the auth server listens, and it opens and migrates the database before it does. So a pod that can’t reach the database never becomes ready, and nothing else in the cluster calls it in the meantime. Once it runs, `/health` doesn’t check the database at all, so a database outage later doesn’t restart every pod at once. The admin console doesn’t open the database. On Kubernetes, one already running keeps running while the auth server crash-loops, and answers every page with [Unable to load the configuration from the auth server](https://goiabada.dev/troubleshooting/unable-to-load-the-configuration-from-the-auth-server/). One that starts meanwhile waits for the auth server before it listens, so its pod doesn’t become ready either. Under Docker Compose, the generated file starts it only once the auth server is healthy, so it doesn’t start at all. ## When it’s the admin console that crash-loops The admin console stops at start, with exit status 1, when the auth server refuses its client secret. Its record is `the auth server does not accept the admin console's client secret, so the admin console cannot start`: see [the admin console’s client secret changed](https://goiabada.dev/troubleshooting/locked-out-of-the-admin-console/#the-admin-consoles-client-secret-changed). It also stops on missing or malformed session keys, or a missing client secret, each with a record saying which. ## Next steps [Database](https://goiabada.dev/deploy/database/): Preparing PostgreSQL, MySQL, SQL Server and SQLite. [Environment variables](https://goiabada.dev/reference/environment-variables/#database): Every database variable and its default. [Database TLS connection fails](https://goiabada.dev/troubleshooting/database-tls-connection-fails/): Fix TLS or certificate refusals. [PostgreSQL TLS variables](https://goiabada.dev/troubleshooting/postgresql-tls-variables/): Fix a PGSSL configuration refusal. # Database TLS connection fails Source: https://goiabada.dev/troubleshooting/database-tls-connection-fails/ This page helps you fix a database connection that stops the auth server because TLS or certificate verification fails. ## What you see The auth server stops with `unable to create the database connection`. Its `error` names a TLS refusal or starts with `x509:`. Read `tls_mode` on the `using database` record to see which mode the start used; the `error` ends with it too, such as `(tls mode verify-full)`. `GOIABADA_DB_TLS_MODE` decides what the auth server asks of the database. In `require`, `verify-ca` and `verify-full`, it never falls back to plain text; the last two also check the certificate. [Where the database sits](https://goiabada.dev/deploy/database/#where-the-database-sits) compares the modes. ## Fix the refusal | The `error` says | Why | Fix it | | - | - | - | | `x509: certificate signed by unknown authority` | The auth server doesn’t trust the authority that signed the certificate. | Set `GOIABADA_DB_TLS_CA_FILE` to a PEM file holding that authority, and any intermediate one. It replaces the system’s authorities. | | `x509: certificate is valid for db-1.internal.example, not db.example.com` | `verify-full`, and the certificate doesn’t name `GOIABADA_DB_HOST`, here `db.example.com`. | Set `GOIABADA_DB_HOST` to a name the certificate carries. If you can’t, such as behind a connection pooler, use `verify-ca`. | | `x509: cannot validate certificate for 10.0.0.5 because it doesn't contain any IP SANs` | `verify-full`, and `GOIABADA_DB_HOST` is an IP address, here `10.0.0.5`, that the certificate doesn’t carry. | Set `GOIABADA_DB_HOST` to a host name the certificate carries, or use `verify-ca`. | | `x509: certificate has expired or is not yet valid` | A certificate in the chain has expired or doesn’t start yet, or the auth server’s clock is wrong. | See [Check the certificate dates](https://goiabada.dev/troubleshooting/database-tls-connection-fails/#check-the-certificate-dates). | | `server refused TLS connection` (PostgreSQL), `TLS requested but server does not support TLS` (MySQL) or `server does not support encryption` (SQL Server) | The database serves no TLS. | Turn TLS on at the database. If nobody else can reach its network, you can set `GOIABADA_DB_TLS_MODE=prefer` instead. | On MySQL and SQL Server in `verify-ca`, an `x509:` error comes after `unable to verify the database server's certificate chain`. The auth server reads the CA file only at start, so restart it after you change the file. On Kubernetes, the file is in [the `goiabada-db-ca` ConfigMap](https://goiabada.dev/deploy/kubernetes/security/#check-the-databases-certificate). A CA file the auth server can’t use stops it before it connects, with a `malformed configuration:` line naming `GOIABADA_DB_TLS_CA_FILE`. That happens when the file can’t be read, holds no certificate, or is set with `disable`, `prefer` or `require`, which read none. ## Check the certificate dates After `x509: certificate has expired or is not yet valid`, the `error` gives the auth server’s time and the certificate’s date: `current time ... is after ...` for a certificate that has expired, `is before ...` for one that doesn’t start yet. 1. Read the dates of the certificates the database serves. On PostgreSQL and MySQL, read them from the database itself, with your host and port: ```bash openssl s_client -connect db.example.com:5432 -starttls postgres -showcerts /dev/null \ | sed -n '/BEGIN CERT/,/END CERT/p' > served.pem openssl storeutl -noout -text -certs served.pem | grep -E 'Subject:|Not (Before|After)' ``` On MySQL, use `-starttls mysql`. On SQL Server, or a managed database, read them in the database’s or your provider’s tools: SQL Server on Windows can keep its certificate in the Windows certificate store. 2. If you set `GOIABADA_DB_TLS_CA_FILE`, read the dates of every authority in it: `openssl storeutl -noout -text -certs` and the file, as above. Don’t use `openssl x509` for this: it reads only a file’s first certificate. 3. Replace whatever has expired or doesn’t start yet: - **A certificate the database serves:** renew it at the database, or have your provider renew it, and have the database serve the new one. - **An authority in `GOIABADA_DB_TLS_CA_FILE`:** put the current one in the file, then restart the auth server. 4. If every date is valid, check the auth server host’s clock. On Kubernetes, check the node’s clock. ## Next steps [Database](https://goiabada.dev/deploy/database/#where-the-database-sits): Choose a TLS mode and trusted authorities. [CrashLoopBackOff](https://goiabada.dev/troubleshooting/crashloopbackoff-or-unable-to-create-the-database-connection/): Other reasons the auth server stops at startup. # PostgreSQL TLS variables stop startup Source: https://goiabada.dev/troubleshooting/postgresql-tls-variables/ This page helps you start the auth server when PostgreSQL’s `PGSSL` environment variables cause a configuration refusal. ## What you see With `GOIABADA_DB_TYPE=postgres`, the auth server won’t start while one of these variables is set and not empty: `PGSSLMODE`, `PGSSLROOTCERT`, `PGSSLCERT`, `PGSSLKEY`, `PGSSLPASSWORD`, `PGSSLSNI` or `PGSSLNEGOTIATION`. It prints one `malformed configuration:` line naming each variable, never its value, and exits with status 2. For example: - `malformed configuration: PGSSLMODE is set, which the auth server no longer reads: unset it and set GOIABADA_DB_TLS_MODE (--db-tls-mode) instead` - `malformed configuration: PGSSLROOTCERT is set, which the auth server no longer reads: unset it and set GOIABADA_DB_TLS_CA_FILE (--db-tls-ca-file) instead` ## Fix the configuration 1. Unset each variable the line names, wherever the auth server’s environment comes from: the Compose file, `goiabada.env`, or the auth server’s ConfigMap. 2. Set `GOIABADA_DB_TLS_MODE` to the protection your database needs. [Where the database sits](https://goiabada.dev/deploy/database/#where-the-database-sits) explains the five modes. Only `verify-full` checks the database’s certificate and host name. 3. For `verify-ca` or `verify-full`, set `GOIABADA_DB_TLS_CA_FILE` if the certificate’s signing authority isn’t in the system’s roots. 4. Restart the auth server. The auth server presents no client certificate, so `PGSSLCERT`, `PGSSLKEY` and `PGSSLPASSWORD` have nothing to replace them. ## Next steps [Database](https://goiabada.dev/deploy/database/#where-the-database-sits): Choose a TLS mode and trusted authorities. [Database TLS connection fails](https://goiabada.dev/troubleshooting/database-tls-connection-fails/): Fix a TLS or certificate refusal. # attempt to write a readonly database Source: https://goiabada.dev/troubleshooting/attempt-to-write-a-readonly-database/ This page helps you when the auth server can’t write its SQLite database. ## What you see The auth server stops right after it opens the database, and its log has `unable to create the database connection`, with one of these as its `error`: - `unable to connect to database: Attempt to write a readonly database (SQLITE_READONLY): attempt to write a readonly database (1544)`, when it can’t write the **directory** holding the database file, where SQLite creates the files it keeps beside it. That’s the usual case: an auth server that stops cleanly removes those files. - `unable to migrate the database: unable to clear schema_migrations: attempt to write a readonly database (8)`, when it can’t write the **file**, and the new release has a migration to run. Under Docker Compose, the container restarts and stops again with the same record. When there’s nothing to migrate, a read-only file doesn’t stop the start, and neither does a read-only directory that still holds the files SQLite keeps beside the database, as it does when the last auth server there didn’t stop cleanly: it was killed, for instance because its stop outlasted the platform’s grace period, it crashed, or its requests or background work outlived its own shutdown timeouts. The auth server starts, and every request that writes to the database fails instead, such as a sign-in, which shows the user an error page. Its error record ends in `attempt to write a readonly database (8)`, and so do the background cleanup’s. ## Why it happens The auth server runs as a user that can’t write the database. SQLite needs to write the file and to create the files it keeps beside it in the same directory, `goiabada.db-wal` and `goiabada.db-shm`. The image runs as uid `10001` and gid `10001`. Its `/data` directory belongs to that user, and Docker copies a mount point’s owner into an empty named volume, so a new volume mounted at `/data` is writable from the first start. A volume whose files belong to another user isn’t: - **The volume was written by a container that ran as root,** such as an older image, or a `docker compose run` as root. - **The volume is mounted somewhere other than `/data`,** at a path the image doesn’t have. Docker creates that directory owned by root. - **It’s a bind mount of a host directory** that belongs to another user. With native binaries, it’s the same story when the service runs as a user other than the one owning the database’s directory. Kubernetes doesn’t run into this: SQLite isn’t supported there, and the generated manifest mounts no volume. ## Fix it Give the database’s directory, and every file in it, to the user the auth server runs as. **Docker Compose** 1. Stop the auth server, from the directory holding your `docker-compose.yml`: ```bash docker compose stop goiabada-authserver ``` 2. Change the owner, once, in a throwaway container of the auth server’s service: ```bash docker compose run --rm --no-deps --user 0:0 --cap-add CHOWN --entrypoint chown \ goiabada-authserver -R 10001:10001 /data ``` This runs `chown` as root, so it reaches the volume wherever the service mounts it. `--cap-add CHOWN` is what lets root change the owner in a service that drops every capability. Use the path your file mounts the volume at, `/data` in the file the setup wizard writes. 3. Start everything again: ```bash docker compose up -d ``` **Native binaries** Give the directory to the user the service runs as, here `goiabada` with the database in `/var/lib/goiabada`: ```bash sudo systemctl stop goiabada-authserver sudo chown -R goiabada:goiabada /var/lib/goiabada sudo systemctl start goiabada-authserver ``` > **Caution** > > Change the owner of the whole directory, `-R` included. A database file you give to the right user beside a `goiabada.db-wal` that still belongs to root fails the same way. ## Why it fails at the first write SQLite opens a file it can’t write read-only rather than refusing it. A read-only database connects, so the auth server only finds out at its first write. On an upgrade, that’s the migration’s first step, recording the version it’s about to apply, which is why the second message names `schema_migrations`. A directory it can’t write fails sooner, since the database runs in WAL mode and SQLite can’t create its WAL files. When an auth server that didn’t stop cleanly left them there, SQLite opens them read-only too, and the first write is again what fails. The auth server checks nothing about the file’s owner before it opens it. Nothing is written when a start fails this way, so the start after the fix migrates as usual. ## Next steps [Database](https://goiabada.dev/deploy/database/#sqlite): Preparing SQLite and the other engines. [The user the images run as](https://goiabada.dev/deploy/docker-compose/#the-user-the-images-run-as): Which files the containers must read and write. # Waiting for the migration lock, or marked dirty Source: https://goiabada.dev/troubleshooting/waiting-for-the-migration-lock-or-marked-dirty/ This page helps you when the auth server’s start stops at the database schema: it waits for another process, or it refuses to migrate. Find your case first: - **The start’s last record is `waiting for the migration lock`:** [another process is migrating](https://goiabada.dev/troubleshooting/waiting-for-the-migration-lock-or-marked-dirty/#another-process-is-migrating). - **The start stops with `is marked dirty, so a migration did not finish`:** [a migration was cut short](https://goiabada.dev/troubleshooting/waiting-for-the-migration-lock-or-marked-dirty/#a-migration-was-cut-short). - **The start stops with `which this release of Goiabada does not carry`:** [a newer release migrated the database](https://goiabada.dev/troubleshooting/waiting-for-the-migration-lock-or-marked-dirty/#a-newer-release-migrated-the-database). ## Another process is migrating ### What you see The auth server’s last record is `waiting for the migration lock`, a few lines after `opening the database`. It doesn’t listen yet, so `/health` doesn’t answer, and on Kubernetes the pod isn’t ready. ### Why it happens Only one process migrates a database at a time, and it holds the migration lock while it does. Another process got there first: another auth server replica starting on the new release, or a `goiabada-authserver migrate` command. A waiting start waits for as long as the lock is held, and then finds the schema current and starts, with `no need to migrate the database`. So a start that writes `waiting for the migration lock` and nothing more is waiting, not hung. SQLite never writes it: its lock covers one process. ### Fix it Usually, wait. Find the process holding the lock. An auth server’s log has `migrating the database`, with how many files it has to run, and `database migrated` once it’s done. A `goiabada-authserver migrate` command prints `migrations to run, in order:` and then `done: the database is now at schema version`. Every waiting replica starts within moments of that. The lock belongs to the holder’s database session, so a holder that dies releases it. If the holder is stuck rather than slow, stop it, and the next waiting process takes the lock and migrates. > **Caution** > > On Kubernetes, the startup probe has to cover the wait. The generated manifest allows 5 minutes, 60 failures 5 seconds apart. When an upgrade’s migrations take longer, Kubernetes restarts the container before they finish. Raise the auth server’s startup probe `failureThreshold` until `failureThreshold × periodSeconds` covers them. ## A migration was cut short ### What you see The auth server stops at start, and its log has `unable to create the database connection`, with an `error` that starts like this, with your own version number: ```plaintext unable to migrate the database: the database records version 000047 and is marked dirty, so a migration did not finish. ``` The rest of the message says which versions the schema can be at, and what to record once you’ve repaired it. ### Why it happens Before each migration file runs, the auth server marks the version it’s applying as dirty in the `schema_migrations` table, and clears the mark once the file has run. A mark left in place means a file started and didn’t finish, so the schema sits somewhere between two versions. The auth server won’t guess how far it got, so it refuses to migrate. A file stops partway when: - **The file failed,** on one of its statements or on a lost connection to the database. The start that ran it said so: its `error` names the migration file that failed and the database’s own error, before the dirty refusal. - **The process was killed mid-file,** by SIGKILL or a crash. A stop signal doesn’t do it: the auth server lets the running file finish first. But when the platform’s grace period runs out, `terminationGracePeriodSeconds` on Kubernetes or `stop_grace_period` in Compose, the platform kills it. On MySQL and SQL Server a file doesn’t run inside one transaction, so some of its statements may have applied. ### Fix it 1. Back up the database. 2. Read the whole message. It names the migration that was running, or the two it could have been, and the version the schema is at if its statements applied and if they didn’t. 3. Look at the schema by hand, and compare it with the migration file the message names, in the source of the release you run, under `src/authserver/internal/data/db/migrations`. Then either finish the file’s statements or undo the ones that applied, so the schema matches one of the two versions. 4. Record that version, clean, as the only row in `schema_migrations`. For version 47: ```sql DELETE FROM schema_migrations; INSERT INTO schema_migrations (version, dirty) VALUES (47, false); ``` On SQL Server, write `0` in place of `false`. When the state is a database that was never migrated, delete the row and insert none. 5. Start the auth server. It carries on from the version you recorded. > **Caution** > > Before an upgrade carrying a long migration, raise the startup budget so the platform doesn’t stop the start mid-file, and don’t stop it yourself. ## A newer release migrated the database ### What you see The auth server stops at start, and its `error` starts like this: ```plaintext unable to migrate the database: this database records schema version 999999, which this release of Goiabada does not carry ``` It goes on to name the highest migration this release has and the release itself. ### Why it happens The database is at a schema version this release has no migration for, so a newer release migrated it. That happens when an earlier release starts after a later one has migrated the database, such as when you roll an upgrade back by installing the earlier release again. This release can’t know what the newer one changed, so it refuses to touch it. ### Fix it Install the newer release again. To go back to this release for good, run the newer release’s `goiabada-authserver migrate to ` first, naming the version the message gives, then install this one. [Roll back to an earlier release](https://goiabada.dev/deploy/upgrade-goiabada/#roll-back-to-an-earlier-release) has the procedure. ## How a start migrates the database A starting auth server opens the database, takes the migration lock, runs every migration its release carries that the database hasn’t had, and releases the lock, all before it listens. It writes these records on the way: | Record | When | | - | - | | `waiting for the migration lock` | Another process holds the lock, written once before the wait | | `migrating the database` | Before the first migration runs, with `from_version`, `to_version` and `pending` | | `database migrated` | After the last one, with `applied` and `duration` | | `no need to migrate the database` | The schema is already current | | `database migration stopped` | A stop signal arrived between two files | A stop signal during a start ends a wait at once and lets a running file finish, so the schema is left clean at the version it reached, and the next start carries on from there. ## Next steps [Upgrade Goiabada](https://goiabada.dev/deploy/upgrade-goiabada/): The lock, the records and the migrate command. [First start and upgrades](https://goiabada.dev/deploy/kubernetes/probes-and-shutdown/#first-start-and-upgrades): The startup probe and how long a start may take on Kubernetes. # Metrics are not scraped Source: https://goiabada.dev/troubleshooting/metrics-are-not-scraped/ This page helps you when your scraper doesn’t collect Goiabada’s metrics. ## What you see One of these: - **Goiabada isn’t among the scraper’s targets,** and no `goiabada_` series exist. - **The targets are there, but down:** Prometheus’s `up` is `0` for them, with a connection refused, a timeout or a `404` as the error. ## Why it happens Each server serves its metrics on a listener of its own, `GET /metrics` on port 9190 for the auth server and 9191 for the admin console, and the listener is off until you turn it on. Most failures are one of these: - **The listener is off.** Every start writes a `metrics listener configuration` record with `enabled`, and only an enabled listener writes `starting the metrics listener`. The Docker Compose files and the native env file the setup wizard writes leave both off, and so does a Kubernetes manifest generated without metrics. - **The scraper calls the wrong port or path.** Any path but `/metrics` answers `404`, and the application’s own port, 9090 or 9091, serves no metrics. - **The listener only listens on `127.0.0.1`,** because `GOIABADA_AUTHSERVER_LISTEN_HOST_METRICS` or `GOIABADA_ADMINCONSOLE_LISTEN_HOST_METRICS` says so, and the scraper runs elsewhere. - **The scraper doesn’t read what the manifest gives it.** kube-prometheus-stack ignores the `prometheus.io/scrape` pod annotations and scrapes only what a PodMonitor or ServiceMonitor names. The prometheus-community `prometheus` chart reads the annotations. - **No Prometheus selects the PodMonitor.** A Prometheus the Prometheus Operator runs scrapes only the PodMonitors its `podMonitorSelector` matches, in the namespaces its `podMonitorNamespaceSelector` matches. kube-prometheus-stack, by default, only those labeled `release: `. - **A NetworkPolicy refuses the scraper.** With the generated NetworkPolicies, only the namespace you named for the scraper, `monitoring` unless you named another, reaches the metrics ports. ## Fix it 1. Check the listener answers, from beside the server. On Kubernetes: ```bash kubectl port-forward -n goiabada deploy/goiabada-authserver 9190:9190 curl -s http://localhost:9190/metrics | grep goiabada_build_info ``` With Docker Compose, from a container on the Compose network, `curl -s http://goiabada-authserver:9190/metrics`. If nothing answers, turn the listener on: set `GOIABADA_AUTHSERVER_METRICS_ENABLED` to `true` on the auth server and `GOIABADA_ADMINCONSOLE_METRICS_ENABLED` to `true` on the admin console, and restart them. On Kubernetes, also give each container its `metrics` port, as [A manifest generated without metrics](https://goiabada.dev/deploy/monitoring/#a-manifest-generated-without-metrics) shows, or run the setup wizard again with metrics on and compare. 2. If the listener answers but the scraper can’t reach it, check the listen host. Leave `GOIABADA_AUTHSERVER_LISTEN_HOST_METRICS` and `GOIABADA_ADMINCONSOLE_LISTEN_HOST_METRICS` unset, which listens on every address, unless the scraper runs on the same host. 3. On Kubernetes, match the manifest to what scrapes your cluster. For kube-prometheus-stack, or any Prometheus the Operator runs, use a PodMonitor and give it the labels your Prometheus selects by: ```bash kubectl get prometheus -A -o jsonpath='{..podMonitorSelector}' ``` Add them under the PodMonitor’s `metadata.labels`, or answer the setup wizard’s labels question with them. For the prometheus-community chart, use the pod annotations. For Grafana Alloy or the OpenTelemetry Collector, keep the targets on the `metrics` container port and let the scraper’s service account list and watch pods in the `goiabada` namespace. 4. With the NetworkPolicies on, check the namespace your scraper’s pods actually run in: ```bash kubectl get pods -A | grep -i -e prometheus -e alloy -e collector ``` If it isn’t the one the policies admit, change the `kubernetes.io/metadata.name` in the second rule of both `goiabada-authserver` and `goiabada-adminconsole` NetworkPolicies to it, or run the setup wizard again with the right namespace. > **Caution** > > Don’t publish the metrics ports to fix a scrape. The listener has no authentication, so keep it off every Service, route, proxy and Compose `ports:` entry, and let only your scraper reach it. ## The metrics listener The metrics listener answers only `GET /metrics`, and isn’t part of the application’s routes. A scrape is neither logged nor counted in the request metrics, so the server’s own logs don’t show your scraper’s attempts. A port it can’t bind stops the server at start, as the other listeners’ do. Alert on the scrape itself failing, Prometheus’s `up` at `0` for Goiabada’s targets, so a broken scrape doesn’t go unnoticed. ## Next steps [Monitoring](https://goiabada.dev/deploy/monitoring/): Turning the metrics on, scraping them and what to alert on. [Scrape on Kubernetes](https://goiabada.dev/deploy/monitoring/#scrape-on-kubernetes): What the setup wizard writes for each answer. # Implicit flow Source: https://goiabada.dev/legacy-flows/implicit/ This page helps you keep an old browser app that uses the implicit flow working, and move it off. > **Deprecated, and off by default** > > The implicit flow is deprecated. OAuth 2.1 drops it, and [RFC 9700 section 2.1.2](https://www.rfc-editor.org/rfc/rfc9700.html#section-2.1.2) says clients should not use it. It’s off in a new install. **Never** turn it on for a new app: use the [authorization code flow with PKCE](https://goiabada.dev/guides/add-sign-in-to-a-spa-or-mobile-app/), which works in the browser too. The implicit flow skips the authorization code. The browser comes back from the auth server with the tokens themselves, after the `#` in your redirect URI, and your app’s JavaScript reads them from there. It was made for browser apps from before they could call a token endpoint on another origin. They all can now, so the only reason left to use it is an app you can’t change yet. ## When to turn it on Turn it on only for an app that speaks nothing but the implicit flow and that you can’t update yet, and only for that app’s client, while you [move it off](https://goiabada.dev/legacy-flows/implicit/#move-to-the-authorization-code-flow). Leave the global setting off, so no other client gets it by accident. ## Turn it on for one client 1. In the admin console, open **Admin**, **Clients** and click **Manage** beside your app’s client. 2. On **OAuth2 flows**, under **Legacy flows (deprecated in OAuth 2.1)**, set **Implicit flow** to **Enabled**, and click **Save**. 3. On **Redirect URIs**, add the page that receives the tokens, such as `https://app.example.com/callback`, and click **Save**. 4. Make a new random `state` and `nonce`, keep them, and send the browser to the authorization endpoint with `response_type=id_token token`: ```http GET /auth/authorize?client_id=legacy-spa&redirect_uri=https%3A%2F%2Fapp.example.com%2Fcallback&response_type=id_token%20token&scope=openid%20email&state=af0ifjsldkj&nonce=n-0S6_WzA2Mj HTTP/1.1 Host: auth.example.com ``` 5. The user signs in, and the browser comes back with the tokens after the `#`: ```http HTTP/1.1 302 Found Location: https://app.example.com/callback#access_token=eyJhbGciOiJSUzI1NiIs...&token_type=Bearer&expires_in=300&id_token=eyJhbGciOiJSUzI1NiIs...&scope=openid+email&state=af0ifjsldkj ``` 6. Check that `state` is the one you kept. Check the ID token as [a web app does](https://goiabada.dev/guides/add-sign-in-to-a-web-app/#what-the-id-token-says), `nonce` included, and check its `at_hash` against the access token. Then replace the address in the browser’s history with one that has no `#` part, so the tokens don’t stay there. **Implicit flow** on the **OAuth2 flows** tab has three choices: **Enabled**, **Disabled**, or inherit the global **Implicit flow enabled** under **Admin**, **General**, which is off in a new install. ## Response types | `response_type` | What comes back | | - | - | | `id_token token` | An ID token and an access token. The ID token carries `at_hash`, a hash of the access token. | | `id_token` | An ID token only | | `token` | An access token only | `token id_token` works too, since the order doesn’t matter. A request asking for an ID token needs `openid` in its `scope` and a `nonce`, as [OpenID Connect Core 1.0 section 3.2.2.1](https://openid.net/specs/openid-connect-core-1_0.html#ImplicitAuthRequest) requires, or it’s refused with `invalid_request`. Ask for `id_token token` when your app needs an access token. `at_hash` ties the access token to an ID token that carries your `nonce`, which is the one way your app can tell the access token was issued for its own request. With `id_token` alone there’s no access token to call [UserInfo](https://goiabada.dev/reference/endpoints/userinfo/) with, so the user’s claims reach your app only when **Include OpenID Connect claims in the ID token** is on. It’s on in a new install. See [Scopes](https://goiabada.dev/concepts/scopes/). ## What comes back The browser comes back to your redirect URI with these parameters after the `#`: | Parameter | When | | - | - | | `access_token` | `response_type` has `token` | | `token_type` | With `access_token`. It’s `Bearer`. | | `expires_in` | With `access_token`: its lifetime in seconds, the client’s token expiration or else the global one, 300 in a new install | | `id_token` | `response_type` has `id_token` | | `scope` | The scopes granted, which can be fewer than your app asked for | | `state` | When your app sent one, unchanged | There’s **never a refresh token**: [RFC 6749 section 4.2.2](https://www.rfc-editor.org/rfc/rfc6749.html#section-4.2.2) forbids one, so `offline_access` is dropped from the scope, as [OpenID Connect Core 1.0 section 11](https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess) asks. When the access token expires, send the browser back to the auth server. A user whose [session](https://goiabada.dev/concepts/sessions/) is still valid isn’t asked for their password again. With `response_mode=form_post`, the same parameters are posted to your redirect URI by a page that sends a form on its own. `response_mode=query` is refused with `invalid_request`, since tokens never travel in the query. Errors come back the same way as tokens, after the `#` or posted: `error`, `error_description`, and `state` when your app sent one. ## What the auth server checks The implicit flow goes through the same [authorization endpoint](https://goiabada.dev/reference/endpoints/authorize/) as the authorization code flow, with the same checks, sign-in, [ACR levels](https://goiabada.dev/concepts/acr-and-amr/), consent and [prompt](https://goiabada.dev/concepts/prompt/). These rules are its own: - **The client needs the implicit flow on,** or the request is refused with `unauthorized_client`. It doesn’t need the authorization code flow on. - **The redirect URI matches exactly.** A loopback redirect URI’s port isn’t ignored, as it is for the authorization code flow, since tokens rather than a code would go to whatever program is listening there. - **PKCE doesn’t apply.** There’s no code to tie it to, and a `code_challenge` is ignored. - **The setting is read again just before the tokens are issued,** so a sign-in under way when an administrator turns the flow off ends with `unauthorized_client` and no tokens. - **The tokens are tied to the user’s session,** which must still be valid when they’re issued. Each issue leaves a `token_issued_implicit_response` entry in the [audit log](https://goiabada.dev/concepts/audit-log/). ## Why it’s deprecated > **The tokens travel in the address** > > The tokens are part of the address the browser lands on, so they can stay in the browser’s history, leak to other sites through the `Referer` header, or be carried to an attacker through an open redirect on your site, since browsers keep the `#` part across a redirect ([RFC 9700 sections 4.1 to 4.3](https://www.rfc-editor.org/rfc/rfc9700.html#section-4.1)). **Never** use the implicit flow on a page that loads scripts or images from other sites. > **An access token can be swapped in** > > Nothing in a `token` response proves the access token was issued for your request, so an attacker can put a stolen one in its place ([RFC 9700 section 4.6](https://www.rfc-editor.org/rfc/rfc9700.html#section-4.6)). **Never** ask for `token` alone: ask for `id_token token`, and check `at_hash` and `nonce`. ## Move to the authorization code flow 1. On the client’s **Authentication** tab, choose **Public client** if it isn’t one, click **Save**, and confirm with **Yes**. Your app runs in the browser, so it can’t keep a secret. 2. On **OAuth2 flows**, turn on **Authorization code with PKCE**, and click **Save**. 3. On **Web origins**, add your app’s origin, such as `https://app.example.com`, so the browser lets it call the token endpoint. 4. Change your app to send `response_type=code` with a PKCE challenge, and redeem the code at the token endpoint. [Add sign-in to a SPA or mobile app](https://goiabada.dev/guides/add-sign-in-to-a-spa-or-mobile-app/) walks through it. Your app gets refresh tokens now too. 5. Once no user is on the old version, set **Implicit flow** to **Disabled**. ## Next steps [Add sign-in to a SPA or mobile app](https://goiabada.dev/guides/add-sign-in-to-a-spa-or-mobile-app/): The flow to use instead, for an app in the browser. [Authorize](https://goiabada.dev/reference/endpoints/authorize/): Every parameter and check of the authorization endpoint. [ROPC](https://goiabada.dev/legacy-flows/ropc/): The other legacy flow, and why to avoid it too. # Resource Owner Password Credentials (ROPC) Source: https://goiabada.dev/legacy-flows/ropc/ This page helps you keep an old app that signs users in with the password grant working, and move it off. > **Deprecated, and off by default** > > The Resource Owner Password Credentials (ROPC) flow, or password grant, is deprecated. OAuth 2.1 drops it, and [RFC 9700 section 2.4](https://www.rfc-editor.org/rfc/rfc9700.html#section-2.4) says it **must not** be used. It’s off in a new install. **Never** turn it on for a new app. Use instead: > > - [Add sign-in to a web app](https://goiabada.dev/guides/add-sign-in-to-a-web-app/) or [Add sign-in to a SPA or mobile app](https://goiabada.dev/guides/add-sign-in-to-a-spa-or-mobile-app/), for an app users sign in to. A desktop app or a command-line tool opens the system browser and receives the code on a loopback address. > - [The client credentials flow](https://goiabada.dev/guides/protect-an-api/#let-a-service-call-your-api), for a service that acts for itself rather than for a user. In the password grant, your app asks the user for their email and password, and sends them to the token endpoint, which answers with tokens. The user never sees the auth server’s sign-in page, so your app sees their password, and nothing but a password can be checked. ## When to turn it on Turn it on only for an app of your own that signs users in this way and that you can’t change yet, and only for that app’s client, while you move it to the authorization code flow. Leave the global setting off, so no other client gets it by accident. Never turn it on for an app someone else wrote: a user should type their password into the auth server and nowhere else. ## Turn it on for one client 1. In the admin console, open **Admin**, **Clients** and click **Manage** beside your app’s client. 2. On **OAuth2 flows**, under **Legacy flows (deprecated in OAuth 2.1)**, set **Resource Owner Password Credentials (ROPC)** to **Enabled**, and click **Save**. 3. Send the user’s email and password to the token endpoint, with your client’s credentials. A [public client](https://goiabada.dev/concepts/clients/#public-and-confidential-clients) leaves `client_secret` out: ```bash curl -X POST https://auth.example.com/auth/token \ -d grant_type=password \ -d client_id=legacy-app \ -d client_secret=my-secret \ --data-urlencode username=user@example.com \ --data-urlencode password=the-users-password \ -d "scope=openid email" ``` 4. Read the tokens from the answer: ```http HTTP/1.1 200 OK Content-Type: application/json Cache-Control: no-store Pragma: no-cache { "access_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6Ii4uLiJ9...", "id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6Ii4uLiJ9...", "token_type": "Bearer", "expires_in": 300, "refresh_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6Ii4uLiJ9...", "refresh_expires_in": 2592000, "scope": "openid email" } ``` 5. Forget the password as soon as the answer comes back. Use the refresh token when the access token expires, as for any other grant. See [Token](https://goiabada.dev/reference/endpoints/token/). **Resource Owner Password Credentials (ROPC)** on the **OAuth2 flows** tab has three choices: **Enabled**, **Disabled**, or inherit the global **Resource owner password credentials flow enabled** under **Admin**, **General**, which is off in a new install. ## The request | Parameter | Required | | | - | - | - | | `grant_type` | Yes | `password` | | `username` | Yes | The user’s email address. Spaces around it are trimmed, and case doesn’t matter. | | `password` | Yes | The user’s password | | `scope` | No | The [scopes](https://goiabada.dev/concepts/scopes/) to ask for, separated by single spaces. `openid` when left out. | | `client_id` | Yes | The client’s identifier | | `client_secret` | For a confidential client | Its secret, in the form body or as `Authorization: Basic` | The scope can hold the OpenID Connect scopes, such as `openid`, `profile` and `email`, `offline_access`, and `resource:permission` scopes the user holds, directly or through a group. A scope the user doesn’t hold is refused, never dropped. An [administrative scope](https://goiabada.dev/concepts/clients/#administrative-scopes) needs a client allowed to request one. ## What your app gets - **An access token and a refresh token, always,** and an ID token when the scope has `openid`. - **An offline refresh token,** whether or not your app asked for `offline_access`, since there’s no session for it to belong to. It follows the offline refresh token’s idle timeout and maximum lifetime, 30 days and a year in a new install. See [Refresh tokens](https://goiabada.dev/concepts/refresh-tokens/). - **No consent screen.** Sending the password counts as the user’s consent to the scope asked for. - **`acr` is `urn:goiabada:level1` and `amr` is `["pwd"]`,** whatever the client’s default ACR level, since a password is all that was checked. An API that requires two-factor authentication refuses these tokens by their `acr`. See [ACR and AMR](https://goiabada.dev/concepts/acr-and-amr/). - **No `sid`.** The tokens belong to no session, so signing out of the browser doesn’t touch them. - **`auth_time` is when the password was checked,** and stays that across every refresh. Each grant leaves a `token_issued_ropc_response` entry in the [audit log](https://goiabada.dev/concepts/audit-log/). Each `invalid_grant` below leaves a `ropc_auth_failed` entry, which records the address only as `email_digest`. ## Who can’t use it - **A user with two-factor authentication.** The grant can’t ask for a one-time code, so it refuses every user who has an authenticator rather than skip the second factor. They sign in through the browser instead. - **A disabled user,** even with the right password. ## Turning it off Setting the flow to **Disabled**, for the client or globally, stops new grants and refresh tokens already issued alike: the next refresh is refused with `unauthorized_client`, and the user has to sign in another way. ## Rate limits [RFC 6749 section 4.3.2](https://www.rfc-editor.org/rfc/rfc6749.html#section-4.3.2) requires the endpoint to be protected against guessing. A wrong password counts against the same budgets as one typed into the sign-in page, and the one for each account, 100 wrong passwords an hour, always applies. With the rate limiter on, the password grant also has a budget per IP address for every request, and a tighter one for wrong passwords for one username from one network. See [rate limits](https://goiabada.dev/reference/environment-variables/#rate-limits) and [Too many attempts or 429](https://goiabada.dev/troubleshooting/too-many-attempts-or-429/). ## Errors Errors are JSON, as for every grant, with `error` and `error_description`: | `error` | Status | When | | - | - | - | | `unauthorized_client` | 400 | The flow is off for the client | | `invalid_request` | 400 | `username`, `password` or `client_id` is missing, a public client sent a `client_secret`, or the secret came both in the `Authorization` header and in the body | | `invalid_client` | 401 | The client secret is missing or wrong, or the client doesn’t exist or is disabled. The answer carries `WWW-Authenticate: Basic realm="goiabada"`. | | `invalid_grant` | 400 | The email or password is wrong, the user is disabled, or the user has two-factor authentication | | `invalid_scope` | 400 | A scope doesn’t exist or the user doesn’t hold it, or an administrative scope the client may not request | A user with two-factor authentication who sends the right password is told why: ```json { "error": "invalid_grant", "error_description": "Resource owner password credentials grant is not available for accounts with two-factor authentication enabled. Please use the authorization code flow instead." } ``` ## Why it’s deprecated > **Your app sees the password** > > The app holds the user’s password, so a bug, a log line or a breach in the app gives it away, and the user can’t tell a real app from a fake one asking for it. **Never** log the token request’s body, and never store the password. > **It weakens sign-in** > > It teaches users to type their password into apps rather than into the auth server, and it can’t ask for a second factor ([RFC 9700 section 2.4](https://www.rfc-editor.org/rfc/rfc9700.html#section-2.4)). **Never** turn it on to get around two-factor authentication: users with an authenticator are refused anyway. ## Next steps [Add sign-in to a web app](https://goiabada.dev/guides/add-sign-in-to-a-web-app/): The flow to use instead, for an app users sign in to. [Token](https://goiabada.dev/reference/endpoints/token/): Every grant of the token endpoint, and refreshing tokens. [Implicit flow](https://goiabada.dev/legacy-flows/implicit/): The other legacy flow, and why to avoid it too. # About Source: https://goiabada.dev/about/ This page tells you why Goiabada exists, where its name comes from, and how to reach the people behind it. Goiabada is an auth server you run yourself, made to be easy to set up and easy to live with. There are plenty of alternatives; this is one person’s take on how it should work. It’s free and open source, and it’ll stay that way. You host it, so the code and the data are yours. ## The name Goiabada is a Brazilian sweet made from guava. It also starts with “Go”, the language Goiabada is written in. 😉 ![A block of goiabada, the Brazilian guava sweet](https://goiabada.dev/img/about1.png) ## Who makes it **Leonardo D’Ippolito** created Goiabada and maintains it, with fixes and ideas from the people who use it. You can join them: see [Contributing](https://goiabada.dev/about/contributing/). ## Get in touch - **A bug or a feature request:** open an issue on [GitHub](https://github.com/leodip/goiabada/issues), so everyone can follow it. - **A security vulnerability:** email , as [Report a vulnerability](https://goiabada.dev/reference/security/#report-a-vulnerability) describes. - **Anything else:** email , or find Leonardo on [LinkedIn](https://www.linkedin.com/in/leodip/). ## Help with your project **Leonardo D’Ippolito**, who makes Goiabada, is available for freelance work: - **Goiabada:** integrating it with your apps, deploying it, or building a feature you need. - **Full-stack development:** .NET, Go, Vue.js and JavaScript. - **Infrastructure:** Docker, Kubernetes and SQL databases. - **Software in general:** architecture, implementation and performance. Email to talk about your project. ## Next steps [Introduction](https://goiabada.dev/get-started/introduction/): What Goiabada does for your apps. [Contributing](https://goiabada.dev/about/contributing/): Build Goiabada from source and send a change. [GitHub issues](https://github.com/leodip/goiabada/issues): Report a bug or follow one. # Contributing Source: https://goiabada.dev/about/contributing/ This page helps you build Goiabada from source, test a change and send it as a pull request. Bug reports and pull requests are welcome. Code and contributions made with AI tools are welcome too, as long as they’re good quality: correct, tested and in keeping with the code around them, as any change must be. Not sure where to start? Pick something from the [GitHub issues](https://github.com/leodip/goiabada/issues). ## Open the dev container Everything you need, Go, the linters, the Tailwind CLI and the four databases, runs in a dev container, so the only thing your machine needs is Docker and an editor that opens dev containers, such as VS Code with the [Dev Containers](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) extension. 1. Clone the repository: ```sh git clone https://github.com/leodip/goiabada.git ``` 2. Open the repository’s `src` folder in your editor, not the repository itself. The dev container is defined in `src/.devcontainer`, and an editor looks for it in the folder you open. 3. Reopen the folder in the container. In VS Code, that’s **Dev Containers: Reopen in Container** from the command palette. The first build takes a few minutes. Every command below runs inside the container, from the directory it names. ## Run Goiabada from source 1. Start the auth server, from `src/authserver`: ```sh make serve ``` 2. Start the admin console in a second terminal, from `src/adminconsole`: ```sh make serve ``` 3. Open the admin console at [http://localhost:19091](http://localhost:19091/) and sign in as the first administrator: email `admin@example.com`, password `devcontainer-admin-password`. The auth server is at [http://localhost:19090](http://localhost:19090/). Each rebuilds and restarts when you save a Go file or a template in its own module. A change under `src/core` isn’t watched: save a file in the server’s module, or run `make serve` again, to pick it up. > **Danger** > > The dev container’s administrator password, keys and secrets are published in the repository. **Never** use them in a deployment. ## Run the tests The test suite is one script, `run-tests.sh`. Run all of it, from `src/authserver`: ```sh ./run-tests.sh ``` That’s every tier below, the data and integration tiers on all four databases, so it takes a while. While you work, run the tier your change touches with `--type`: | Tier (`--type`) | What it runs | | - | - | | `internal` | The auth server’s unit tests | | `core` | The core module’s unit tests | | `adminconsole` | The admin console’s unit tests | | `setup` | The setup wizard’s tests | | `modules` | `internal`, `core` and `adminconsole` together | | `data` | The data layer against a real database | | `integration` | End-to-end tests against a running auth server, which the script builds, starts and stops | | `lint` | The linters, and the checks that generated files are committed | | `all` | Everything. The default | For example, the three unit tiers: ```sh ./run-tests.sh --type modules ``` Three more options narrow a run: - **`--db`** picks the database for `data` and `integration`: `mysql`, `postgres`, `mssql`, `sqlite`, or `all`, the default. SQLite is the quickest. - **`--run`** takes a `go test -run` pattern. A pattern that matches no test fails the run, so a typo doesn’t pass as green. - **`--race`** runs the unit tiers and the setup wizard’s tests under Go’s race detector. ```sh ./run-tests.sh --type integration --db sqlite --run 'TestToken_' ``` `./run-tests.sh --help` lists every option. ## Commit what you generate Some committed files are written by a tool. When your change touches what one is written from, run its command and commit what it changes: | When you change | Run | From | | - | - | - | | A database migration | `go run ./cmd/schemadump` | `src/authserver` | | An exported symbol in a core package | `go run ./cmd/ownershipdump` | `src/core` | | An interface a mock is generated for | `./generate-mocks.sh` | `src/authserver` | | A template’s CSS classes | `./build.sh` | `src/authserver` or `src/adminconsole`, whichever owns the template | | A tool version in `versions.yaml` | `./version-manager.sh update` | `src/authserver` | `schemadump` regenerates the `schema.golden` file of all four databases, which the data tier compares against a freshly migrated database. The `lint` tier reruns the mocks, the ownership table and the CSS, and fails when any of them differs from what’s committed. ## Write docs The docs site is in `site/`, and every page follows [`site/STYLE.md`](https://github.com/leodip/goiabada/blob/main/site/STYLE.md): the voice, the shape of a page and the words the [glossary](https://goiabada.dev/concepts/glossary/) gives each concept. The dev container has no Node.js, so build the site on your own machine, with Node.js 22.12 or later, from `site/`: ```sh npm ci npm run dev ``` That serves a live preview at [http://localhost:4321](http://localhost:4321/). Before you send a change, build the site and run its checks’ tests: ```sh npm run build npm test ``` The build fails on a broken link, including a link to the site from the Go code under `src/`, so move a page or rename a heading in the same change as every link to it. ## The project Goiabada is three Go modules and the setup wizard: ```plaintext src/ ├── authserver/ The auth server: OAuth2 and OpenID Connect, sign-in, the APIs ├── adminconsole/ The admin console, a client of the auth server ├── core/ What both servers share ├── cmd/goiabada-setup/ The setup wizard, a module of its own └── build/ The release Dockerfiles and build scripts ``` `ARCHITECTURE.md` at the repository root says which module owns what, and the unit tiers hold the code to it. `AGENTS.md` describes the code in depth: the sign-in state machine, the patterns every handler follows and every guard the tests run. It’s written for AI coding assistants, and reads just as well for people. `CLAUDE.md` is the same file. ## Many checks are tests Much of what a reviewer would otherwise check is a test that fails: the architecture rules, the logging convention, gofmt, the API error codes and audit events listed in the docs, and the docs pages that state a fact the code decides, this one included. When you change one of those facts, change the page in the same pull request. ## The databases and mail The dev container runs MySQL, PostgreSQL and SQL Server beside it, and `make serve` uses SQL Server. The tests leave that database alone: each data and integration run starts from an empty database of its own. `./run-tests.sh` does stop a running `make serve`, though: it frees ports 19090, 19091 and 19190 before the data and integration tiers and again when it exits, so start `make serve` again afterwards. Mailpit runs beside them too. Under **Email - SMTP** in the admin console’s settings, turn on **SMTP enabled** and set the host to `mailpit`, the port to 1025, the encryption to **None**, and **From email** to any address, such as `noreply@example.com`. Every mail Goiabada sends then lands in Mailpit, at [http://localhost:8025](http://localhost:8025/). ## CI CI runs the same tiers on every pull request, a job each, and builds the site. The data, integration and race jobs wait until the pull request is out of draft. ## Next steps [GitHub issues](https://github.com/leodip/goiabada/issues): Find something to work on, or report a bug. [Security](https://goiabada.dev/reference/security/#report-a-vulnerability): How to report a vulnerability privately. [Glossary](https://goiabada.dev/concepts/glossary/): The one name for each concept, in the code and the docs. # License Source: https://goiabada.dev/about/license/ This page tells you what Goiabada’s license lets you do. Goiabada is free and open source under the MIT License. You can use it, copy it, change it and sell it, in any project, commercial or not. Keep the copyright notice and the license text with every copy, and that’s it. It comes with no warranty: if something goes wrong, nobody behind Goiabada is liable for it. ## The license This is the `LICENSE` file at the root of the [repository](https://github.com/leodip/goiabada): ```text MIT License Copyright (c) 2023 Leonardo D'Ippolito (leo@goiabada.dev) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ``` ## Next steps [Quickstart](https://goiabada.dev/get-started/quickstart/): Run Goiabada on your machine. [Contributing](https://goiabada.dev/about/contributing/): Build Goiabada from source and send a change. # Welcome Source: https://goiabada.dev/ ![Goiabada logo](https://goiabada.dev/_astro/goiaba.CbTu3AT5_11UYE1.webp) # Goiabada Sign-in, single sign-on and permissions for your apps, on a server you run yourself. [Get started](https://goiabada.dev/get-started/introduction/)[Quickstart](https://goiabada.dev/get-started/quickstart/)[View on GitHub](https://github.com/leodip/goiabada) Goiabada signs your users in, so your apps don’t have to. It’s an **OAuth2** and **OpenID Connect** server: your apps send users to it, and get back tokens that say who each user is and what they can do. ## What you get OAuth2 and OpenID Connect The standards your languages and frameworks already have libraries for. Single sign-on Users sign in once and use all your apps. Two-factor authentication Codes from an authenticator app, required for the apps that need them. Permissions Decide who can do what in your APIs, by user, group or client. Custom claims Add groups and your own user and group attributes to ID tokens, access tokens or both. Self-service accounts Users manage their own profile, picture, email, phone, address, password and two-factor authentication, end their sessions and revoke consents. Self-registration and password recovery People create their own accounts, with or without email verification, and reset a forgotten password. Dynamic client registration Apps such as MCP clients register themselves, when you turn it on. Admin API Script everything the admin console does, with an OpenAPI reference and permissions as narrow as read-only. Audit log Sign-ins, failed passwords and every change an administrator makes, recorded and shown in the admin console. Your choice of database MySQL, PostgreSQL, SQL Server or SQLite. Light to run Two small Go servers, as Docker images for x86_64 and ARM64, or as native binaries. Setup wizard Answer a few questions and get a ready-to-run setup for Docker Compose, Kubernetes or native binaries, keys and passwords included. Kubernetes friendly Generated manifests with probes, graceful shutdown and disruption budgets, Prometheus metrics, and servers that scale out to several replicas. ## Why Goiabada - **Your data stays with you.** You host it, so your users’ data lives on your servers. - **Free and open source.** MIT licensed, with no fees or subscriptions. - **Standard.** Anything that speaks OAuth2 or OpenID Connect works with it. ## Have a look ![The sign-in page, where users enter their email and password](https://goiabada.dev/_astro/screenshot-signin.1Nxzy5wg_29xAfw.webp) ![The admin console’s account pages, where a user updates their phone number](https://goiabada.dev/_astro/screenshot2.BNkMAbhH_ZnxvML.webp) ![The admin console’s list of users](https://goiabada.dev/_astro/screenshot3.BzPx5e-9_ZQ4KIw.webp) ## Docs for AI agents These docs are also published as plain text, for AI agents and tools that read documentation: - [`/llms.txt`](https://goiabada.dev/llms.txt) lists every page, with its title, URL and a one-line description. - [`/llms-full.txt`](https://goiabada.dev/llms-full.txt) holds every page in full, as Markdown, in one file. ## Start here [Quickstart](https://goiabada.dev/get-started/quickstart/): Run it on your machine in a few minutes. [Add sign-in to a web app](https://goiabada.dev/guides/add-sign-in-to-a-web-app/): Then let users of your web app sign in with it.