Skip to content

Endpoints

You can find a link to the well-known discovery URL by going to the root of the admin console. The URL will look like this:

https://auth.example.com/.well-known/openid-configuration

This endpoint shows the capabilities supported by Goiabada.

The authorize endpoint is used to request authorization codes via the browser. This process normally involves authentication of the end-user and optionally obtaining consent.

ParameterRequiredDescription
client_idYesThe client identifier.
redirect_uriYesThe redirect URI is the callback entry point of the app. This must exactly match one of the allowed redirect URIs for the client.
response_typeYes

code for the authorization code flow with PKCE (recommended).
token, id_token, or id_token token for the implicit flow (legacy, must be enabled).

code_challenge_methodConditionalS256 is the only value supported. Required when PKCE is enforced (see PKCE configuration).
code_challengeConditionalA random string between 43 and 128 characters long. Required when PKCE is enforced.
scopeYesOne or more registered scopes, separated by a space. A scope can be either a resource:permission or an OIDC scope. Must include openid for OpenID Connect flows.
response_modeNoSupported values: query (default for code flow), fragment (default and required for implicit flow), or form_post.
stateRecommendedAny string. Goiabada will echo back the state value on the token response, for CSRF/replay protection.
nonceConditionalAny string. Goiabada will echo back the nonce value in the ID token, as a claim, for replay protection. Required when using implicit flow with id_token.
max_ageNoIf the user’s authentication timestamp exceeds the max age (in seconds), they will have to re-authenticate.
acr_valuesNoSupported values: urn:goiabada:level1, urn:goiabada:level2_optional or urn:goiabada:level2_mandatory.
promptNo

Controls authentication and consent behavior. Supported values: none (silent authentication — no UI allowed), login (force re-authentication), consent (force consent screen). Values can be combined with spaces (e.g., login consent), except none which must be used alone. See Prompt parameter for details.

ui_localesNo

Preferred languages for the login and consent screens, as space-separated BCP 47 tags in order of preference (e.g. pt-BR en). See Localization.

id_token_hintNo

A previously issued ID token that serves as a hint about the user’s authenticated session. The server validates the token’s signature and issuer, accepts expired tokens (for recent sessions), and enforces that authorization can only proceed for the user identified in the hint. This prevents session fixation attacks by ensuring tokens cannot be issued for a different user than specified in the hint. See ID Token Hint for details.

GET /auth/authorize?
client_id=my-app&
redirect_uri=https://my-app.com/callback&
response_type=code&
scope=openid profile email&
code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&
code_challenge_method=S256&
state=abc123&
nonce=xyz789

The token endpoint serves the purpose of requesting tokens. This can happen either through the authorization code flow (exchanging an authorization code for tokens), the client credentials flow (client directly requests tokens), or using a refresh token.

Confidential clients must authenticate when calling the token endpoint. Goiabada supports two authentication methods:

MethodDescription
client_secret_postSend client_id and client_secret as form parameters in the HTTP request body
client_secret_basicSend credentials via HTTP Basic authentication header
ParameterDescription
grant_typeauthorization_code, client_credentials, or refresh_token
client_idThe client identifier.
client_secretThe client secret, if it’s a confidential client.
redirect_uriRequired for the authorization_code grant type.
codeThe authorization code. Required for authorization_code grant type.
code_verifierThe original string from which the code_challenge was derived. Required if PKCE was used in the authorization request.
scopeFor client_credentials: required, one or more resource:permission scopes. For refresh_token: optional, to restrict the original scope.
refresh_tokenRequired for the refresh_token grant type.
Terminal window
curl -X POST https://auth.example.com/auth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=authorization_code" \
-d "client_id=my-app" \
-d "client_secret=my-secret" \
-d "code=SplxlOBeZQQYbYS6WxSbIA" \
-d "redirect_uri=https://my-app.com/callback" \
-d "code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"
Terminal window
curl -X POST https://auth.example.com/auth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials" \
-d "client_id=my-service" \
-d "client_secret=service-secret" \
-d "scope=product-api:read product-api:write"

Instead of sending credentials in the request body, you can use the Authorization header with Basic authentication. The header value is Basic followed by the Base64 encoding of client_id:client_secret:

Terminal window
# Base64 of "my-service:service-secret" is "bXktc2VydmljZTpzZXJ2aWNlLXNlY3JldA=="
curl -X POST https://auth.example.com/auth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-H "Authorization: Basic bXktc2VydmljZTpzZXJ2aWNlLXNlY3JldA==" \
-d "grant_type=client_credentials" \
-d "scope=product-api:read product-api:write"

Or using curl’s -u shorthand which does the encoding automatically:

Terminal window
curl -X POST https://auth.example.com/auth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-u "my-service:service-secret" \
-d "grant_type=client_credentials" \
-d "scope=product-api:read product-api:write"

A successful token response includes the following headers per RFC 6749 Section 5.1:

HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: no-store
Pragma: no-cache

The response body contains the tokens:

{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 300,
"refresh_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"id_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"scope": "openid profile email"
}

Error responses follow RFC 6749 Section 5.2 and include cache prevention headers:

HTTP/1.1 400 Bad Request
Content-Type: application/json
Cache-Control: no-store
Pragma: no-cache

For client authentication failures, the response uses HTTP 401 status code with a WWW-Authenticate header when HTTP Basic authentication was attempted:

HTTP/1.1 401 Unauthorized
Content-Type: application/json
Cache-Control: no-store
Pragma: no-cache
WWW-Authenticate: Basic

The response body contains error details:

{
"error": "invalid_client",
"error_description": "Client authentication failed. Please review your client_secret."
}
Error codeHTTP statusDescription
invalid_request400Missing required parameter, invalid parameter value, or malformed request.
invalid_client401Client authentication failed (unknown client, no credentials, or invalid secret).
invalid_grant400Authorization code or refresh token is invalid, expired, revoked, or doesn’t match the redirect URI.
unauthorized_client400Client is not authorized for this grant type.
unsupported_grant_type400The grant type is not supported.
invalid_scope400The requested scope is invalid, unknown, or exceeds the scope granted.

This endpoint enables the client application to initiate a logout. This implementation aligns with the OpenID Connect RP-Initiated Logout 1.0 protocol.

id_token_hint is recommended but not required. Without one, Goiabada cannot tell which client is asking, so it asks the user instead:

  • GET /auth/logout - Displays a logout consent screen, prompting the user to confirm their intention to log out.
  • POST /auth/logout - What that screen submits. The user is logged out: the session record is deleted and the session cookie is cleared.

Once the user confirms, they land on a page telling them they have been logged out.

Ending the session reaches the tokens that depend on it, and no further. A normal refresh token from that session stops working, because there is no session behind it any more. Offline refresh tokens are not tied to a browser session and keep working. Access tokens that were already issued stay cryptographically valid: Goiabada’s own endpoints, /userinfo and the account and admin APIs, reject one whose session is gone on the very next request, but a third-party resource server validating a token by signature alone cannot see that the session ended and keeps accepting it until it expires.

You can still be redirected back to your application on this path, by sending post_logout_redirect_uri together with client_id. The URI must exactly match one registered for that client, and only state is added to it. Your state comes back byte for byte, including any +, /, =, # or &; sending it empty gives you back an empty state, and not sending it gives you no state at all. If the URI cannot be validated, because client_id is missing or unknown, or because the URI is not registered for that client, the user is still logged out and lands on the logged-out page with a short note saying we could not return them to the application.

Both bindings accept the same parameter names. Send them where the method puts them: in the query string on a GET, and in an application/x-www-form-urlencoded body on a POST. A GET’s body is never read, so a parameter sent only there is ignored.

A POST reads the query string as well as its body, so send each parameter once. Which copy wins when the same one arrives in both places is not worth relying on.

What differs between the two methods is not the parameters but whether the request is accepted at all, which is what the rest of this section is about.

POST is worth using when you have an id_token_hint, because it keeps the ID token out of the browser’s address bar, its history and the referrer headers it sends on to other sites. A self-submitting form from your own page is the usual way to do it, and that form is a cross-site request: it comes from your origin and posts to Goiabada’s.

A cross-site POST needs an id_token_hint. Without one Goiabada answers 403 Forbidden, because a POST with no hint is exactly what the logout consent screen submits, and treating an arbitrary site’s request as a user’s confirmation would let any page sign your users out. With a hint the request is accepted and evaluated on the hint’s merits: a hint Goiabada can confirm logs the user straight out, and one it cannot is answered with a 303 See Other back to GET /auth/logout, which is where the consent screen comes from. Follow the redirect and the user is asked to confirm, as they would have been on a GET.

That 303 carries your post_logout_redirect_uri, state and ui_locales onward, and deliberately drops id_token_hint and client_id. Dropping client_id is why a hint that fails validation earns no redirect even when a valid client_id was sent beside it.

For proper logout with redirection back to your application, use either GET or POST with the following parameters:

ParameterRequiredDescription
id_token_hintRecommendedThe previously issued ID token (can be encrypted or unencrypted). Send it when you have it: it identifies the client and session, so the user is not asked to confirm. Without it, the logout still happens, after the consent screen.
post_logout_redirect_uriNoA redirect URI that must be pre-registered with the client. Optional in every case: leave it out and the user is logged out and lands on the logged-out page.
client_idConditionalRequired if id_token_hint is encrypted with the client secret, to select whose secret derives the key. Also required to authorize post_logout_redirect_uri when you send no id_token_hint.
stateNoAny arbitrary string that will be echoed back in the redirect.
ui_localesNoSpace-separated list of preferred locales for the pages this endpoint renders, such as pt-BR en. Read from the query string, and on a POST from the form body as well.

When a post_logout_redirect_uri is supplied and can be validated, the user is redirected to it after the logout. Exactly one parameter is added:

  • state - only if you sent one, and byte for byte what you sent

Anything the registered URI already carried in its own query is preserved.

Example redirect: https://your-app.com/logged-out?state=xyz

With no post_logout_redirect_uri, or when the URI cannot be validated, the user lands on Goiabada’s logged-out page instead. Either way the logout has already happened.

Goiabada checks the hint’s signature, issuer, audience, session and expiry, that the session it names belongs to the user it names, and that client_id matches the audience when you send both. If any of those fail, the request is not an error: the user sees the logout consent screen, and once they confirm, they are logged out.

The session check has two halves. The hint must name a session, and that session must belong to the user the hint is about, so a hint cannot end a session that was never its user’s. Sending back a hint you were issued satisfies both without thinking about it. The spec requires the user to be asked whenever a hint does not belong to the session or the user currently signed in, and asking is exactly what a hint Goiabada cannot confirm gets.

A hint that fails these checks earns no redirect, even if you also sent client_id and a registered post_logout_redirect_uri. The user is logged out and lands on the logged-out page with the note. Send a hint you can vouch for, or send none at all and let client_id authorize the redirect.

Encrypting the id_token_hint prevents the ID token from being exposed in browser history, logs, and referrer headers. Goiabada expects a standard JWE (JSON Web Encryption): per OpenID Connect Core 1.0 §2 an encrypted ID token is a Nested JWT, i.e. the signed ID token wrapped in a JWE.

The scheme is:

Parameter Value
Key management (alg) dir (direct)
Content encryption (enc) A256GCM
Content type (cty) JWT
Encryption key SHA-256 of the UTF-8 client secret (32 bytes)
Plaintext the signed ID token previously issued to the client
Serialization JWE Compact Serialization

Send client_id alongside the encrypted hint so Goiabada knows whose client secret to derive the key from.

using Jose; // jose-jwt NuGet package
using System.Security.Cryptography;
using System.Text;
private static string EncryptIdTokenHint(string signedIdToken, string clientSecret)
{
// dir + A256GCM, key = SHA-256(client_secret)
byte[] key = SHA256.HashData(Encoding.UTF8.GetBytes(clientSecret));
return JWT.Encode(
signedIdToken,
key,
JweAlgorithm.DIR,
JweEncryption.A256GCM,
extraHeaders: new Dictionary<string, object> { { "cty", "JWT" } });
}

The UserInfo endpoint is an OpenID Connect standard endpoint used to retrieve identity information about an authenticated user.

Send a valid access token using the Authorization header:

Authorization: Bearer <access-token>

The endpoint requires the authserver:userinfo scope to be present in the access token.

The response is a JSON object containing user claims. The sub (subject) claim is always included. Additional claims depend on the scopes in the access token:

Scope in access tokenClaims returned
profilename, given_name, middle_name, family_name, nickname, preferred_username, profile, picture, website, gender, birthdate, zoneinfo, locale, updated_at
emailemail, email_verified
addressaddress (structured claim)
phonephone_number, phone_number_verified
groupsgroups (array of group identifiers configured to be included in ID tokens)
attributesattributes (map of user and group attributes configured to be included in ID tokens)

Request with scopes profile and email:

{
"sub": "248289761001",
"name": "Jane Doe",
"given_name": "Jane",
"family_name": "Doe",
"preferred_username": "j.doe",
"email": "[email protected]",
"email_verified": true,
"profile": "https://auth.example.com/account/profile",
"picture": "https://auth.example.com/userinfo/picture/248289761001",
"updated_at": 1311280970
}
  • 401 Unauthorized - Missing or invalid access token
  • 403 Forbidden - Access token lacks authserver:userinfo scope
  • 500 Internal Server Error - User account is disabled or not found

Returns the logo image for a client. This endpoint is publicly accessible (no authentication required).

If the client has a logo uploaded, the response is the raw image data with the appropriate Content-Type header (e.g., image/png, image/jpeg). The response includes ETag and Cache-Control headers for efficient browser caching.

Returns 404 Not Found if the client does not exist or has no logo uploaded.

GET /client/logo/my-web-app