Skip to main content

Register an application

In the developer workspace, enter an application name and exact callback URIs, then choose whether approval requires a verified email. Callbacks must use HTTPS; HTTP is allowed only on loopback addresses for local development. The client secret is shown once after creation. Save it securely if your server will use it.

Authorization code flow

CP OAuth supports the OAuth 2.0 authorization code flow with a client secret, PKCE, or both.

  1. Redirect the user to /oauth/authorize with your client ID, registered callback URI and requested scopes. Include state to verify the callback.
  2. The user reviews the requested access and chooses whether to approve it.
  3. On approval, your callback receives an authorization code. Validate the returned state before continuing.
  4. Exchange the single-use code for an access token and refresh token at POST /api/oauth/token.
  5. Call GET /api/oauth/userinfo with the Bearer access token to read the data covered by the granted scopes.
  6. Refresh at POST /api/oauth/token with grant_type=refresh_token. Omit client_secret only if the initial PKCE code exchange omitted it.

This is not a full OpenID Connect provider. The openid scope returns a user ID, not an ID token; no JWKS endpoint is published.

Server metadata:

The previous metadata URL is also supported:

CP OAuth's own pages use HttpOnly session cookies and CSRF protection. Your integration uses grant credentials for token exchange and revocation, and a Bearer access token for userinfo. Do not use or forward the site's session cookie.

PKCE

Public applications can exchange authorization codes using PKCE without a client secret. Always send code_verifier when exchanging a PKCE code.

The verifier must be 43–128 characters: letters, digits, dot, underscore, tilde or hyphen. An S256 challenge is a 43-character base64url SHA-256 hash. The plain method is also supported and is the default when the method is omitted.

When the initial PKCE exchange omits the secret, refresh and revoke requests for that grant can also omit it. Grants created before the migration still require a secret until the user authorizes again.

// 32 random bytes give a 43-character base64url verifier.
const verifierBytes = crypto.getRandomValues(new Uint8Array(32))
const codeVerifier = btoa(String.fromCharCode(...verifierBytes))
  .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(codeVerifier))
const codeChallenge = btoa(String.fromCharCode(...new Uint8Array(digest)))
  .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')

// Keep state and verifier securely for the same-browser callback; validate state there.
const authorizationUrl = new URL('https://www.cpoauth.com/oauth/authorize')
authorizationUrl.search = new URLSearchParams({
  response_type: 'code',
  client_id: 'YOUR_CLIENT_ID',
  redirect_uri: 'https://yourapp.com/callback',
  scope: 'openid profile',
  state: crypto.randomUUID(),
  code_challenge: codeChallenge,
  code_challenge_method: 'S256'
}).toString()
// Exchange the returned code with code_verifier: codeVerifier.
// Omit client_secret entirely for a public grant; its refresh/revoke also need no secret.
// An empty or incorrect client_secret is never accepted, even with valid PKCE.

Exchange a code without a secret

// Exchange a PKCE authorization code; do not send client_secret (not even an empty value).
const response = await fetch('https://www.cpoauth.com/api/oauth/token', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    grant_type: 'authorization_code',
    code: 'AUTHORIZATION_CODE',
    redirect_uri: 'https://yourapp.com/callback',
    client_id: 'YOUR_CLIENT_ID',
    code_verifier: codeVerifier
  })
})
const { access_token, refresh_token, expires_in, scope } = await response.json()
// This grant's refresh and revoke requests also omit client_secret.

Server-side authentication

Keep client_secret on your server, not in browser code. Client secret authentication can be used with or without PKCE. If the authorization request used PKCE, include code_verifier in the code exchange.

If the initial code exchange includes a secret, refresh and revoke requests for that grant must also include the client secret. Missing, empty or incorrect secrets are rejected.

// Secret-authenticated applications: run this on your server, never expose the secret in a browser.
const response = await fetch('https://www.cpoauth.com/api/oauth/token', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    grant_type: 'authorization_code',
    code: 'AUTHORIZATION_CODE',
    redirect_uri: 'https://yourapp.com/callback',
    client_id: 'YOUR_CLIENT_ID',
    client_secret: 'YOUR_CLIENT_SECRET',
    code_verifier: 'YOUR_CODE_VERIFIER' // required if the authorization request used PKCE
    // Only a secret-authenticated grant requested without PKCE can omit code_verifier.
  })
})

const {
  access_token,   // JWT, expires in 1 hour
  refresh_token,  // opaque, expires in 30 days
  token_type,
  expires_in,
  scope
} = await response.json()

API endpoints

GET /oauth/authorize

Starts the consent flow. The callback URI must exactly match a registered URI: HTTPS, or HTTP on localhost, 127.0.0.1 or [::1]. A user can deny access without signing in; the callback receives access_denied and the original state.

GET https://www.cpoauth.com/oauth/authorize?
  response_type=code
  &client_id=YOUR_CLIENT_ID
  &redirect_uri=https://yourapp.com/callback
  &scope=openid%20profile
  &state=random_state_string
  &code_challenge=BASE64URL_SHA256_HASH
  &code_challenge_method=S256

POST /api/oauth/token — Refresh a token

Accepts authorization_code and rotating refresh_token grants as JSON or URL-encoded scalar form fields. Public PKCE grants must omit client_secret entirely; empty or incorrect secrets are rejected. A refresh request may narrow the granted scopes, but cannot add scopes.

Access tokens expire after 1 hour; refresh tokens expire after 30 days. Save both new tokens returned by each refresh.

// Authentication follows the original grant, not a guessed client type.
const response = await fetch('https://www.cpoauth.com/api/oauth/token', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    grant_type: 'refresh_token',
    refresh_token: 'YOUR_REFRESH_TOKEN',
    client_id: 'YOUR_CLIENT_ID',
    client_secret: 'YOUR_CLIENT_SECRET' // omit only when the original PKCE exchange omitted it
  })
})

// Returns new access_token + new refresh_token (rotation)
const { access_token, refresh_token, expires_in } = await response.json()

GET /api/oauth/userinfo

Returns only the user data covered by the granted scopes.

// Fetch user data with access token
const userinfo = await fetch('https://www.cpoauth.com/api/oauth/userinfo', {
  headers: { Authorization: 'Bearer {access_token}' }
})

const data = await userinfo.json()
// Response varies based on granted scopes:
// {
//   sub,
//   username,
//   display_name,
//   avatar_url,
//   bio,
//   email,
//   email_verified,
//   linked_accounts,
//   cp_summary,
//   cp_details
// }

POST /api/oauth/revoke

Revokes your application's access or refresh token (RFC 7009). Secret authentication follows the original grant. Revoking a refresh token also invalidates all access tokens for that user and application. A valid request returns 200 for an unknown token; token_type_hint only affects lookup order.

// Revoke a token (RFC 7009)
await fetch('https://www.cpoauth.com/api/oauth/revoke', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    client_id: 'YOUR_CLIENT_ID',
    client_secret: 'YOUR_CLIENT_SECRET', // omit only for a grant originally exchanged without it
    token: 'TOKEN_TO_REVOKE',
    token_type_hint: 'refresh_token' // or 'access_token'
  })
})
// Validly authenticated requests return 200 even for an unknown or already revoked token.
// JSON and application/x-www-form-urlencoded are supported; Basic authentication is not.

Permission scopes

Request a non-empty set of only the scopes your application needs. The openid scope is optional. The userinfo response follows stored granted scopes, independently of public-profile privacy settings.

ScopeReturned data
openidUser ID (sub only; no ID token)
profileUsername, display name, avatar URL and bio
emailEmail address and verification status
cp:linkedAll linked platform accounts (platform, UID and username)
link:luoguLinked Luogu account (UID and username)
link:atcoderLinked AtCoder account (UID and username)
link:leetcodeLinked LeetCode account (UID and username)
link:codeforcesLinked Codeforces account (UID and username)
link:githubLinked GitHub account (UID and username)
link:googleLinked Google account (UID and username)
link:clistLinked Clist.by account (UID and username)
cp:summaryRating, contest count and rank from Clist.by for accounts that exactly match locally linked identities
cp:detailsUp to 200 recent rated contest changes from Clist.by for accounts that exactly match locally linked identities

User profile card

Embed an SVG card of a user's public platform accounts in a GitHub README or blog. The card shows only accounts the user has made public.

Endpoint:GET /api/users/{username}/card.svg

Parameters: width (default 480, range 300–800), theme (light or dark, default light), lang (en/zh/ja, default en).

Markdown example

![CP OAuth Profile](https://www.cpoauth.com/api/users/YOUR_USERNAME/card.svg)

<!-- Dark theme -->
![CP OAuth Profile](https://www.cpoauth.com/api/users/YOUR_USERNAME/card.svg?theme=dark)

<!-- Custom width -->
![CP OAuth Profile](https://www.cpoauth.com/api/users/YOUR_USERNAME/card.svg?width=600&theme=dark)