External auth

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

Edit this page View Markdown

These routes implement “Sign in with rCTF” for external services. Users approve access at /external-auth/authorize, and the external service exchanges the resulting code through the APIs documented here.

See Admin for the routes that register and revoke clients. External apps walks operators through the complete setup.

Warning (Not OAuth2)

The flow uses familiar OAuth2 field names, including client_id, redirect_uri, code, and state, but it does not implement OAuth2. It has no scopes, refresh tokens, PKCE, OIDC discovery, token introspection, or revocation. /token returns an ordinary rCTF login token with full access to the user’s account.

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/v2/users/me to identify the team.

Failure model

An unknown client, wrong secret, mismatched redirect URI, or invalid code all return 400 badExternalAuthRequest. The response does not reveal which check failed. POST /api/v2/external-auth/authorize also returns 401 badToken when the user’s session token is missing or invalid, and 403 badPerms when the team is banned.

Code lifetime

Authorization codes expire after 60 seconds and can only be used once. Deleting a client prevents future code exchanges but does not revoke access tokens already issued to that client.

GET Get client metadata

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

Edit this page View Markdown
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*string
Public client id.

Response fields

idstring
OAuth client ID.
namestring
Client display name.
redirectUristring
Registered redirect URI.

POST Authorize

POST /api/v2/external-auth/authorize

Edit this page View Markdown
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 exactly match the URI registered for the client. Wildcards and path normalization are not supported. A mismatch returns 400 badExternalAuthRequest without revealing whether the client or URI was wrong.

Banned teams cannot mint codes. When the session token belongs to a banned team, the route returns 403 badPerms.

Code issuance is rate limited per user with burst 5 and refill window 50000 ms. Exceeding the limit returns 429 badRateLimit with the wait in data.timeLeft.

Request body

clientId*string
Public client ID.
redirectUri*string
HTTP(S) redirect URI; must match the client registration.
statestring
Opaque value echoed back on redirect.

Response fields

redirectTostring
URL to redirect the user agent to.

The returned redirectTo appends code=... to the registered redirectUri, along with state=... when the request provides one. It uses ? or & as needed for the registered URI. The code expires from Redis after 60 seconds and is deleted by the first POST /api/v2/external-auth/token call.

rCTF returns state without reading or changing it. The external service should use this value for CSRF protection.

POST Exchange code for token

POST /api/v2/external-auth/token

Edit this page View Markdown
Auth
Public
Gate
None
Permissions
No extra permissions
Captcha
No captcha
Rate limit
No rate limit

The external service’s backend exchanges the single-use code for an rCTF auth token. Make this request from a trusted server and never expose the client secret to the browser.

The code can only be exchanged once. Repeated or concurrent requests fail.

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. To debug a failure, check the timestamp, current secret, client ID, and exact code sent by the service.

Request body

clientId*string
Public client ID.
clientSecret*string
Client secret.
code*string
Authorization code returned from the authorize step.

Response fields

accessTokenstring
Issued OAuth access token.
tokenType"bearer"
Token type; always bearer.

The returned accessToken is the same non-expiring token issued at login. Use it in Authorization: Bearer <accessToken> against any rCTF endpoint. Deleting the client prevents future code exchanges but does not revoke tokens already issued through that client.

Type to search.