rCTF Docs
Overview

External auth

Public routes that let an external service sign users in with their rCTF account.

These routes power the “Sign in with rCTF” flow for external services. The user-facing consent page lives at /external-auth/authorize; the API routes here are what that page (and the external service’s backend) call.

The admin-side routes for registering and revoking external-auth clients are documented in Admin. The operator walkthrough lives at External apps.

Warning (Not OAuth2)

The wire-level field names (client_id, redirect_uri, code, state) match what integrators expect, but the flow is not OAuth2. There are no scopes, refresh tokens, PKCE, id_token/OIDC discovery, token introspection, or revocation. The access token returned by /token is a regular rCTF auth token - identical to one minted on login - and grants full account access to the signing-in user.

Flow#

  1. The external service sends the user to /external-auth/authorize?client_id=...&redirect_uri=...&state=....
  2. The consent page calls GET /api/v2/external-auth/clients/:id to render the client name and verify the redirect URI.
  3. The user logs into rCTF if needed, then approves. The page calls POST /api/v2/external-auth/authorize with the user’s session token and receives a redirectTo URL.
  4. The browser navigates to redirect_uri?code=...&state=....
  5. The external service’s backend exchanges the code through POST /api/v2/external-auth/token and receives {accessToken, tokenType: "bearer"}.
  6. The service uses the access token in Authorization: Bearer ... against any rCTF endpoint - typically GET /api/v1/users/me to identify the team.

Failure model#

Every failure mode (unknown client, wrong secret, mismatched redirect URI, missing/expired/reused code, mismatched client on a code) returns the same 400 badExternalAuthRequest body. The endpoint deliberately doesn’t distinguish causes so callers can’t probe it. The only authenticated route in the section is POST /api/v2/external-auth/authorize, which additionally returns 401 badToken when the user session token is missing or invalid.

Code lifetime#

Authorization codes live in Redis for 60 seconds and are single-use - the first POST /api/v2/external-auth/token call atomically deletes the code, so a second exchange always fails. There is no per-app token registry: deleting a client through the admin routes blocks future token exchanges but does not revoke access tokens that were already issued.

GET Get client metadata

GET /api/v2/external-auth/clients/:id

Auth
Public
Gate
None
Permissions
No extra permissions
Captcha
No captcha
Rate limit
No rate limit

Public lookup for the consent page. The page calls this route with the client_id from the query string to render the app name and verify the redirect URI before showing the consent prompt. The endpoint never returns the client secret.

Unknown ids return 400 badExternalAuthRequest - the same response used for every other failure mode in the External auth flow.

Path parameters

id requiredstring
Public client id.

Response fields

idstring
Unique identifier.
namestring
Display name.
redirectUristring
Returned redirect uri value.

POST Authorize

POST /api/v2/external-auth/authorize

Auth
Required
Gate
None
Permissions
No extra permissions
Captcha
No captcha
Rate limit
No rate limit

Mints a single-use authorization code for the signed-in user and returns the URL the browser should navigate to next. The consent page calls this when the user clicks Authorize.

The redirectUri must match what the client was registered with byte-for-byte (no wildcards, no path normalization). Any mismatch returns 400 badExternalAuthRequest - the response intentionally doesn’t distinguish “unknown client” from “wrong redirect URI” so the endpoint can’t be probed.

Request body

clientId requiredstring
Request body field for client id.
redirectUri requiredstring
Request body field for redirect uri.
statestring
Request body field for state.

Response fields

redirectTostring
Returned redirect to value.

The returned redirectTo is the registered redirectUri with code=... (and, when provided, state=...) appended using a ? or & separator as needed. The code lives in Redis for 60 seconds and is consumed by the first POST /api/v2/external-auth/token call - a second exchange always fails.

The state value is opaque to rCTF and is passed through verbatim. It is the integrator’s responsibility to use it for CSRF protection.

POST Exchange code for token

POST /api/v2/external-auth/token

Auth
Public
Gate
None
Permissions
No extra permissions
Captcha
No captcha
Rate limit
No rate limit

Server-to-server exchange. The external service’s backend trades the single-use code from the browser redirect for a regular rCTF auth token. Call this from a trusted environment - the client secret must not be exposed to the browser.

The code is consumed atomically: the first call deletes it from Redis, so a replay or any concurrent second exchange always fails.

Note (Failures all look identical)

Unknown client, wrong secret, wrong code, expired code, reused code, and code/client mismatch all return the same 400 badExternalAuthRequest body. Don’t rely on the failure reason to debug an integration - check timestamps, secret rotation, and the exact bytes you sent instead.

Request body

clientId requiredstring
Request body field for client id.
clientSecret requiredstring
Request body field for client secret.
code requiredstring
Request body field for code.

Response fields

accessTokenstring
Returned access token value.
tokenType"bearer"
Returned token type value.

The returned accessToken is a regular rCTF auth token with no expiry - the same kind of token issued at login. Use it in Authorization: Bearer <accessToken> against any rCTF endpoint. There is no per-app token registry: deleting the client through the admin routes prevents future code exchanges but does not revoke tokens that were already minted.

Esc

Start typing to search the docs.