OAuth 2.1 + PKCE
OAuth 2.1 + PKCE
Falcata's authorization server implements OAuth 2.1 with mandatory PKCE and RFC 7591 dynamic client registration. Use it when a third-party application needs to act on behalf of a Falcata user without ever holding that user's password or a long-lived credential they typed in themselves.
When to use this (vs. device flow)
Use this flow when your application is a web app, browser extension, or native app that can receive an HTTP redirect back from the user's browser. If your program can't receive a redirect — a CLI, a headless script, a machine without a browser of its own — use Device authorization instead. If you're building a personal script and don't need a separate registered client at all, see Personal access tokens.
Tokens issued through this flow are Personal Access Tokens under the hood, so anything documented for PATs — how to send them, how to check who they resolve to — applies here too.
Discovery documents
Before registering a client, fetch the two standard discovery documents so you don't hardcode endpoint URLs:
grant_types_supported lists only authorization_code — there is no client-credentials or implicit grant, and token_endpoint_auth_methods_supported is none, meaning clients are public (no client secret) and authenticate purely through PKCE.
Dynamic client registration
Register your application once, ahead of time, to get a client_id. There's no manual dashboard step — register it programmatically:
curl -X POST https://api.falcata.io/oauth/register \
-H "Content-Type: application/json" \
-d '{
"client_name": "My Integration",
"redirect_uris": ["https://myapp.example.com/oauth/callback"]
}'
Response (201 Created):
{
"client_id": "fclient_...redacted",
"client_name": "My Integration",
"redirect_uris": ["https://myapp.example.com/oauth/callback"],
"token_endpoint_auth_method": "none",
"grant_types": ["authorization_code"],
"response_types": ["code"],
"client_id_issued_at": 1732550400
}
Only redirect URIs you register here are ever accepted at the authorization endpoint — an unregistered redirect_uri fails before the user sees a consent screen. Store client_id; there is no client secret to store alongside it.
If registration fails, you get 400 with {"error": "invalid_client_metadata", "error_description": "..."} describing what was wrong with the request body.
Authorization request (PKCE required)
Generate a PKCE code verifier and its S256 challenge, then send the user's browser to the authorization endpoint:
open "https://api.falcata.io/oauth/authorize?\
response_type=code&\
client_id=fclient_...redacted&\
redirect_uri=https://myapp.example.com/oauth/callback&\
code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&\
code_challenge_method=S256&\
scope=full&\
state=$(openssl rand -hex 16)"
code_challengeandcode_challenge_method=S256are required on every request — there is no unauthenticated fallback and no plain challenge method. Omitting either fails the request.scopecurrently supports onlyfull; requesting anything else fails withinvalid_scope. Omit it to getfullby default.stateis not required by the server but you should always send one and verify it comes back unchanged, to protect against CSRF.
If client_id is unknown or redirect_uri isn't one of the client's registered URIs, the server can't safely redirect anywhere and instead renders a plain error page directly — that's expected for those two failure modes only. Every other error (bad response_type, missing PKCE, unknown scope) redirects back to your redirect_uri with error, error_description, and state query parameters so your app can handle it without hanging.
The user lands on a Falcata ID consent screen, signs in if needed, and approves or denies. On approval, their browser is redirected back to your redirect_uri with ?code=...&state=....
Token exchange
Exchange the authorization code for a token from your backend, using the PKCE verifier that matches the challenge you sent:
curl -X POST https://api.falcata.io/oauth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=authorization_code" \
-d "code=$AUTH_CODE" \
-d "client_id=fclient_...redacted" \
-d "redirect_uri=https://myapp.example.com/oauth/callback" \
-d "code_verifier=$PKCE_VERIFIER"
The endpoint also accepts a JSON body (Content-Type: application/json) with the same fields, if that's easier for your HTTP client.
Response:
{
"access_token": "fpat_...redacted",
"token_type": "Bearer",
"scope": "full"
}
Send access_token as Authorization: Bearer $FURNACE_TOKEN on subsequent API calls, and confirm it resolves to the user you expect:
curl https://api.falcata.io/api/v1/auth/whoami \
-H "Authorization: Bearer $FURNACE_TOKEN"
Refresh
There is no refresh grant — grant_types_supported advertises only authorization_code, and the token endpoint rejects any other grant_type with unsupported_grant_type. The access token you get back is a full Personal Access Token: it doesn't expire on its own, so there's nothing to refresh. Treat it like any other PAT — store it the way you'd store a long-lived credential, and revoke it from Personal access tokens if it's compromised or no longer needed rather than expecting it to expire.
Errors
error | When | What to do |
|---|---|---|
invalid_client_metadata | Registration body was malformed or missing required fields | Fix the request body — error_description says what's wrong |
unsupported_response_type | response_type on /oauth/authorize wasn't code | Use response_type=code |
invalid_request | Missing or malformed PKCE parameters, or missing token-exchange parameters | Check code_challenge/code_challenge_method on the authorize call, or code/client_id/redirect_uri/code_verifier on the token call |
invalid_scope | Requested a scope other than full | Drop the scope parameter or set it to full |
invalid_target | An unrecognized resource parameter was sent | Remove the resource parameter or use a resource URI the server recognizes |
invalid_grant | The authorization code was wrong, already used, expired, or the code_verifier didn't match the original code_challenge | Restart the authorization request from /oauth/authorize — codes are single-use |
unsupported_grant_type | grant_type on /oauth/token wasn't authorization_code | This server only supports the authorization-code grant |