Skip to main content

Authentication

Curupira implements the OAuth2 Authorization Code flow with PKCE (RFC 6749 + RFC 7636) and OpenID Connect. This page walks the full flow end to end with concrete requests and responses against the live endpoints.

All examples use the production auth host:

https://auth.curupira.api.br

1. OIDC discovery

Any OIDC-compatible client can bootstrap from the discovery document. Fetch it once and read the endpoint URLs from it rather than hard-coding them:

curl -s https://auth.curupira.api.br/.well-known/openid-configuration | jq .
{
"issuer": "https://auth.curupira.api.br",
"authorization_endpoint": "https://auth.curupira.api.br/oauth2/authorize",
"token_endpoint": "https://auth.curupira.api.br/oauth2/token",
"userinfo_endpoint": "https://auth.curupira.api.br/oauth2/userinfo",
"jwks_uri": "https://auth.curupira.api.br/.well-known/jwks.json",
"response_types_supported": ["code"],
"id_token_signing_alg_values_supported": ["RS256"],
"scopes_supported": ["openid", "email", "profile"],
"claims_supported": [
"sub", "iss", "aud", "exp", "iat",
"email", "email_verified", "name", "given_name", "family_name",
"tenant", "roles", "permissions"
],
"token_endpoint_auth_methods_supported": ["none", "api_key"],
"code_challenge_methods_supported": ["S256"]
}

Key facts encoded here:

  • Only the code response type is supported — the Authorization Code flow. (No implicit flow.)
  • id_tokens are signed with RS256 — verify them against the jwks_uri.
  • PKCE uses S256 — the only supported code-challenge method.
  • permissions is a first-class claim (aggregated from the user's roles).

2. Generate a PKCE verifier and challenge

PKCE protects the authorization code from interception. Before redirecting the user, create a random code verifier and derive the code challenge (S256 = base64url(SHA-256(verifier))):

# code_verifier: 43–128 chars, URL-safe
code_verifier=$(openssl rand -base64 60 | tr -d '\n=+/' | cut -c1-64)

# code_challenge = base64url( sha256( code_verifier ) )
code_challenge=$(printf '%s' "$code_verifier" \
| openssl dgst -sha256 -binary \
| openssl base64 -A | tr '+/' '-_' | tr -d '=')

echo "verifier=$code_verifier"
echo "challenge=$code_challenge"

Keep the code_verifier in the user's session — you will need it in step 4.

3. Redirect the user to the authorization endpoint

Send the user's browser to /oauth2/authorize with your client parameters. Generate a random state (CSRF protection) and store it in the session too:

https://auth.curupira.api.br/oauth2/authorize
?response_type=code
&client_id=<YOUR_CLIENT_ID>
&redirect_uri=https://app.example.com/callback
&scope=openid%20email%20profile
&state=<RANDOM_STATE>
&code_challenge=<CODE_CHALLENGE>
&code_challenge_method=S256
ParameterRequiredNotes
response_typeAlways code.
client_idYour application's client_id (a UUID).
redirect_uriMust exactly match a URI registered on the application.
scopeSpace-separated. Include openid to get an id_token.
state✅ (recommended)Opaque value echoed back; compare on return to stop CSRF.
code_challengeThe S256 challenge from step 2.
code_challenge_methodAlways S256.

Curupira serves its own login/consent UI. The user authenticates (password or, if enabled for the tenant, SSO), then Curupira redirects back to your redirect_uri:

https://app.example.com/callback?code=<AUTHORIZATION_CODE>&state=<RANDOM_STATE>

Verify state matches what you stored before continuing. The authorization code is single-use and short-lived.

4. Exchange the code for tokens

POST to the token endpoint with grant_type=authorization_code. The body is application/x-www-form-urlencoded. Include the code_verifier from step 2 — Curupira recomputes the challenge and rejects a mismatch.

Public clients (SPA / mobile) — PKCE only

Public clients cannot keep a secret, so they authenticate with PKCE alone (no API key):

curl -s -X POST https://auth.curupira.api.br/oauth2/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=authorization_code" \
-d "code=<AUTHORIZATION_CODE>" \
-d "redirect_uri=https://app.example.com/callback" \
-d "client_id=<YOUR_CLIENT_ID>" \
-d "code_verifier=<CODE_VERIFIER>"

Confidential clients (server-side) — PKCE + API key

Confidential clients additionally present their application API key in the X-API-Key header:

curl -s -X POST https://auth.curupira.api.br/oauth2/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-H "X-API-Key: <YOUR_APPLICATION_API_KEY>" \
-d "grant_type=authorization_code" \
-d "code=<AUTHORIZATION_CODE>" \
-d "redirect_uri=https://app.example.com/callback" \
-d "client_id=<YOUR_CLIENT_ID>" \
-d "code_verifier=<CODE_VERIFIER>"

Token response

{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVC...",
"id_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVC...",
"refresh_token": "def50200a1b2c3...",
"token_type": "Bearer",
"expires_in": 3600
}
  • access_token — send to your APIs and to /oauth2/userinfo as Authorization: Bearer.
  • id_token — an OIDC identity JWT (see below). Present only when openid was requested.
  • refresh_token — exchange for fresh tokens without re-prompting (see step 6). Rotated on every use.
  • expires_in — access-token lifetime in seconds (default 3600).

Error responses

Errors follow RFC 6749 (400 / 401 with a JSON body):

{ "error": "invalid_grant", "error_description": "authorization code is invalid or expired" }

Common cases: invalid_request (missing/malformed params), invalid_grant (bad/expired code or PKCE mismatch), unsupported_grant_type, invalid_client (bad/missing API key or a disabled application → 401).

5. Inspect the ID token and fetch UserInfo

The id_token is a signed JWT. Always verify its signature against the JWKS before trusting any claim:

curl -s https://auth.curupira.api.br/.well-known/jwks.json | jq .

Decoded id_token payload (illustrative):

{
"iss": "https://auth.curupira.api.br",
"sub": "b3f1c0de-0000-4a1b-9c2d-1234567890ab",
"aud": "<YOUR_CLIENT_ID>",
"exp": 1735689600,
"iat": 1735686000,
"email": "user@example.com",
"email_verified": true,
"name": "Ada Lovelace",
"tenant": "acme",
"roles": ["editor", "viewer"],
"permissions": ["documents:read", "documents:write"]
}

Verify at minimum: the signature (RS256 against JWKS), iss equals https://auth.curupira.api.br, aud equals your client_id, and exp is in the future. Then use roles / permissions for authorization decisions.

You can also call the UserInfo endpoint with the access token:

curl -s https://auth.curupira.api.br/oauth2/userinfo \
-H "Authorization: Bearer <ACCESS_TOKEN>"
{
"sub": "b3f1c0de-0000-4a1b-9c2d-1234567890ab",
"email": "user@example.com",
"email_verified": true,
"name": "Ada Lovelace",
"tenant": "acme",
"roles": ["editor", "viewer"],
"permissions": ["documents:read", "documents:write"]
}

6. Refresh the access token

When the access token nears expiry, exchange the refresh token for a new set. Confidential clients include the X-API-Key header; public clients rely on PKCE-issued refresh tokens.

curl -s -X POST https://auth.curupira.api.br/oauth2/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-H "X-API-Key: <YOUR_APPLICATION_API_KEY>" \
-d "grant_type=refresh_token" \
-d "refresh_token=<REFRESH_TOKEN>" \
-d "client_id=<YOUR_CLIENT_ID>"

:::warning Refresh tokens rotate Curupira rotates the refresh token on every use — the response contains a new refresh_token, and the one you just sent is invalidated. Always persist the newest refresh token and discard the previous value, or the next refresh will fail with invalid_grant. :::

Token lifetimes

TokenDefault lifetimeConfigurable via
Access token3600 s (1 hour)DEFAULT_ACCESS_TTL_SECS (operator)
Refresh token43200 min (30 days)DEFAULT_REFRESH_TTL_MINS (operator)
ID tokenmatches access token

Security notes

  • PKCE (S256) is mandatory on the authorization-code grant — for every client type.
  • Redirect URIs are exact-match against the registered set; there is no wildcard matching.
  • Always validate state on the callback to defend against CSRF.
  • Verify id_token signatures against the JWKS; never trust an unverified JWT.
  • Confidential clients must keep the API key secret — never ship it in a browser or mobile bundle. Public clients omit it and rely on PKCE.
  • Login is protected by per-IP rate limiting and per-account lockout (exponential backoff after repeated failures).