Skip to main content

Knox.chat OAuth2 — Developer & User Documentation

Version: 1.0
Last Updated: February 2026
Standards: RFC 6749 · RFC 7636 (PKCE) · RFC 7009 (Revocation) · RFC 7662 (Introspection)

Table of Contents

Overview

Knox.chat provides a full OAuth2 2.0 Authorization Server, enabling third-party applications to authenticate Knox.chat users securely — similar to "Sign in with GitHub" or "Sign in with Google".

What You Can Do

  • Authenticate users — Let users sign into your app with their Knox.chat account
  • Access user data — Read profile info, email, and usage stats with user consent
  • Manage API tokens — Create and manage API tokens on behalf of users
  • Build integrations — Create bots, dashboards, or tools that interact with Knox.chat

Supported Grant Types

Grant TypeSupportedUse Case
Authorization CodeServer-side web apps
Authorization Code + PKCESPAs, mobile apps, CLI tools
Refresh TokenLong-lived sessions (with token rotation)
Client Credentials
Implicit— (deprecated by OAuth 2.1)

Key Concepts

TermDescription
Application (Client)A third-party app registered with Knox.chat that wants to access user data
Client IDPublic identifier for your application (format: knox_<32chars>)
Client SecretConfidential key used by server-side apps (format: knoxsec_<48chars>)
Authorization CodeTemporary code exchanged for tokens after user consent
Access TokenShort-lived token used to access protected resources (format: knoxat_<48chars>)
Refresh TokenLong-lived token used to obtain new access tokens (format: knoxrt_<48chars>)
ScopeSpecific permissions your app is requesting
ConsentUser's explicit approval to grant your app the requested permissions
PKCEProof Key for Code Exchange — additional security for public clients
Confidential ClientAn app that can securely store a client secret (e.g., server-side apps)
Public ClientAn app that cannot securely store secrets (e.g., SPAs, mobile apps). PKCE is mandatory.

Quick Start

1. Register Your Application

Navigate to Settings → OAuth2 Apps → My Applications in the Knox.chat dashboard, or use the API:

curl -X POST https://api.knox.chat/api/oauth2/applications \
-H "Authorization: Bearer <session_cookie>" \
-H "Content-Type: application/json" \
-d '{
"name": "My App",
"description": "A cool integration with Knox.chat",
"homepage_url": "https://myapp.com",
"logo_url": "https://myapp.com/logo.png",
"redirect_uris": ["https://myapp.com/callback"],
"scopes": "openid email profile",
"app_type": "confidential",
"webhook_url": "https://myapp.com/webhooks/knox"
}'

⚠️ Save your Client Secret immediately! It is shown only once at creation time and cannot be retrieved later. If lost, you must rotate it.

2. Redirect Users to Authorize

https://knox.chat/oauth2/authorize?
response_type=code
&client_id=knox_your_client_id
&redirect_uri=https://myapp.com/callback
&scope=openid email profile
&state=random_csrf_token

3. Exchange the Code for Tokens

curl -X POST https://api.knox.chat/api/oauth2/token \
-H "Content-Type: application/json" \
-d '{
"grant_type": "authorization_code",
"code": "received_auth_code",
"redirect_uri": "https://myapp.com/callback",
"client_id": "knox_your_client_id",
"client_secret": "knoxsec_your_client_secret"
}'

4. Access User Data

curl https://api.knox.chat/api/oauth2/userinfo \
-H "Authorization: Bearer knoxat_your_access_token"

OAuth2 Flows

Authorization Code Grant

The standard flow for confidential (server-side) clients.

Authorization Code + PKCE

For public clients (SPAs, mobile apps, CLI tools) that cannot securely store a client secret. PKCE is mandatory for public clients and recommended for all clients.

Additional Steps:

  1. Generate a random code_verifier (43–128 characters)
  2. Compute code_challenge = BASE64URL(SHA256(code_verifier))
  3. Include code_challenge and code_challenge_method=S256 in the authorization request
  4. Include code_verifier in the token exchange request
Authorization URL:
/oauth2/authorize?
response_type=code
&client_id=knox_xxx
&redirect_uri=https://myapp.com/callback
&scope=openid
&state=random_state
&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
&code_challenge_method=S256

Token Exchange:
POST /oauth2/token
{
"grant_type": "authorization_code",
"code": "auth_code_here",
"redirect_uri": "https://myapp.com/callback",
"client_id": "knox_xxx",
"code_verifier": "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"
}

Refresh Token Flow

Access tokens expire after 1 hour by default. Use the refresh token to obtain new tokens without user interaction.

⚠️ Token Rotation: Each refresh request issues a new refresh token and revokes the old one. Always store the new refresh token from the response.

curl -X POST https://api.knox.chat/api/oauth2/token \
-H "Content-Type: application/json" \
-d '{
"grant_type": "refresh_token",
"refresh_token": "knoxrt_your_refresh_token",
"client_id": "knox_your_client_id",
"client_secret": "knoxsec_your_client_secret"
}'

Response:

{
"access_token": "knoxat_new_access_token",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "knoxrt_new_refresh_token",
"scope": "openid email profile"
}

Scopes & Permissions

Scopes define what data and actions your application can access. Users will see a description of each scope on the consent screen.

ScopeDescriptionData Accessible
openidRead basic account information — Username, display name, and avatarsub, username, display_name, avatar_url, role
emailRead email addressemail, email_verified
profileRead and update profile informationFull profile including group, created_at
tokens:readList API tokensToken names, creation dates, status
tokens:writeCreate and manage API tokensCreate new tokens, delete existing tokens
usage:readRead API usage statistics and quotaquota, used_quota, request_count

Scope Rules

  • openid is always included — Even if not explicitly requested, the openid scope is automatically added as the minimum scope.
  • Request minimal scopes — Only request the scopes your application actually needs.
  • Scope validation — Requested scopes must be a subset of your app's registered allowed_scopes.
  • Re-consent — If your app requests new scopes not previously granted, the user will be shown the consent screen again.

API Reference

Base URLs

ServiceURL
Authorization Page (Browser)https://knox.chat/oauth2/authorize
API Endpointshttps://api.knox.chat/api/oauth2/

1. OpenID Discovery

Returns the OpenID Connect discovery document with all endpoint URLs and supported features.

GET /api/oauth2/.well-known/openid-configuration

Authentication: None

Response 200 OK:

{
"issuer": "https://api.knox.chat",
"authorization_endpoint": "https://knox.chat/oauth2/authorize",
"token_endpoint": "https://api.knox.chat/api/oauth2/token",
"userinfo_endpoint": "https://api.knox.chat/api/oauth2/userinfo",
"revocation_endpoint": "https://api.knox.chat/api/oauth2/revoke",
"introspection_endpoint": "https://api.knox.chat/api/oauth2/introspect",
"scopes_supported": ["openid", "email", "profile", "tokens:read", "tokens:write", "usage:read"],
"response_types_supported": ["code"],
"grant_types_supported": ["authorization_code", "refresh_token"],
"token_endpoint_auth_methods_supported": ["client_secret_basic", "client_secret_post"],
"code_challenge_methods_supported": ["S256", "plain"],
"subject_types_supported": ["public"]
}

2. Authorization Endpoint

GET — Fetch Authorization Info

Returns application info and consent status for the consent screen.

GET /api/oauth2/authorize

Authentication: Session cookie (user must be logged in)

Query Parameters:

ParameterRequiredDescription
response_typeMust be code
client_idYour application's client ID
redirect_uriMust match a registered redirect URI
scopeSpace-separated scopes (defaults to openid)
stateOpaque CSRF protection value (highly recommended)
code_challengePKCE code challenge (required for public clients)
code_challenge_methodS256 (recommended) or plain

Response 200 OK:

{
"success": true,
"data": {
"application": {
"id": 1,
"name": "My App",
"description": "A cool integration",
"homepage_url": "https://myapp.com",
"logo_url": "https://myapp.com/logo.png",
"client_id": "knox_abc123...",
"is_verified": true
},
"requested_scopes": [
{ "name": "openid", "description": "Read basic account information" },
{ "name": "email", "description": "Read email address" }
],
"has_existing_consent": false,
"existing_scopes": null,
"needs_reconsent": false,
"redirect_uri": "https://myapp.com/callback",
"state": "random_state_value"
}
}

Error Responses:

StatusCondition
400Invalid response_type, missing client_id, invalid redirect_uri, invalid scopes
401User not logged in
403OAuth2 disabled, app suspended, unverified app (when approval required)
404Unknown client_id

POST — Submit Authorization Decision

Processes the user's approve/deny decision and returns a redirect URL.

POST /api/oauth2/authorize

Authentication: Session cookie

Request Body (application/json):

{
"client_id": "knox_abc123...",
"redirect_uri": "https://myapp.com/callback",
"scope": "openid email",
"state": "random_state_value",
"approved": true,
"code_challenge": "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM",
"code_challenge_method": "S256"
}
FieldTypeRequiredDescription
client_idstringApplication's client ID
redirect_uristringMust match the GET request
scopestringSpace-separated scopes
statestringOpaque state value
approvedbooleantrue to approve, false to deny
code_challengestringPKCE code challenge
code_challenge_methodstringS256 or plain

Response on Approve 200 OK:

{
"success": true,
"data": {
"redirect_url": "https://myapp.com/callback?code=AUTH_CODE_HERE&state=random_state_value"
}
}

Response on Deny 200 OK:

{
"success": true,
"data": {
"redirect_url": "https://myapp.com/callback?error=access_denied&error_description=User+denied+authorization&state=random_state_value"
}
}

3. Token Endpoint

Exchange an authorization code for tokens, or refresh an existing token pair.

POST /api/oauth2/token

Authentication: Client credentials via one of:

  • Basic Auth: Authorization: Basic base64(client_id:client_secret)
  • Body Parameters: client_id and client_secret in the request body

Content Types: application/json or application/x-www-form-urlencoded

Rate Limit: 60 requests/minute per IP

Grant Type: authorization_code

Request:

{
"grant_type": "authorization_code",
"code": "AUTH_CODE_HERE",
"redirect_uri": "https://myapp.com/callback",
"client_id": "knox_abc123...",
"client_secret": "knoxsec_xyz789...",
"code_verifier": "optional_pkce_verifier"
}
FieldTypeRequiredDescription
grant_typestringMust be authorization_code
codestringThe authorization code received
redirect_uristringMust match the original authorization request
client_idstringYour application's client ID
client_secretstring❌*Required for confidential clients
code_verifierstring❌*Required if code_challenge was sent during authorization

Grant Type: refresh_token

Request:

{
"grant_type": "refresh_token",
"refresh_token": "knoxrt_your_refresh_token",
"client_id": "knox_abc123...",
"client_secret": "knoxsec_xyz789..."
}
FieldTypeRequiredDescription
grant_typestringMust be refresh_token
refresh_tokenstringThe refresh token
client_idstringYour application's client ID
client_secretstring❌*Required for confidential clients

Success Response 200 OK:

{
"access_token": "knoxat_new_access_token...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "knoxrt_new_refresh_token...",
"scope": "openid email profile"
}

Headers: Cache-Control: no-store is always set on token responses.

Error Response 400 Bad Request:

{
"success": false,
"message": "Invalid authorization code",
"error": "invalid_grant",
"error_description": "The authorization code is invalid, expired, or has already been used"
}
Error CodeDescription
invalid_requestMissing or invalid parameters
invalid_clientClient authentication failed
invalid_grantCode expired, already used, or PKCE mismatch
unsupported_grant_typeGrant type not supported
invalid_scopeRequested scope is invalid

4. Token Revocation

Revoke an access token or refresh token (RFC 7009).

POST /api/oauth2/revoke

Authentication: Optional client credentials (Basic auth or body params)

Rate Limit: 60 requests/minute per IP

Request:

{
"token": "knoxat_or_knoxrt_token_here",
"token_type_hint": "access_token"
}
FieldTypeRequiredDescription
tokenstringThe token to revoke
token_type_hintstringaccess_token or refresh_token (optimization hint)

Response: Always 200 OK per RFC 7009 — even if the token is invalid or already revoked.

{
"success": true,
"message": "Token revoked successfully"
}

5. Token Introspection

Inspect a token to determine if it is active and retrieve its metadata (RFC 7662).

POST /api/oauth2/introspect

Authentication: Required — Basic auth with client credentials (Authorization: Basic base64(client_id:client_secret))

Rate Limit: 120 requests/minute per IP

Request:

{
"token": "knoxat_token_to_inspect",
"token_type_hint": "access_token"
}

Response — Active Token 200 OK:

{
"active": true,
"scope": "openid email profile",
"client_id": "knox_abc123...",
"username": "johndoe",
"token_type": "Bearer",
"exp": 1740000000,
"iat": 1739996400,
"sub": "42"
}

Response — Inactive Token 200 OK:

{
"active": false
}

6. UserInfo Endpoint

Retrieve information about the authenticated user based on the granted scopes.

GET /api/oauth2/userinfo

Authentication: Authorization: Bearer <access_token>

Rate Limit: 300 requests/minute per IP

Response 200 OK (fields vary by granted scopes):

{
"sub": "42",
"username": "johndoe",
"display_name": "John Doe",
"avatar_url": "https://knox.chat/avatars/johndoe.png",
"email": "john@example.com",
"email_verified": true,
"quota": 1000000,
"used_quota": 250000,
"request_count": 1500,
"group": "premium",
"role": 10,
"created_at": 1700000000
}

Fields by Scope:

FieldRequired Scope
subopenid
usernameopenid
display_nameopenid
avatar_urlopenid
roleopenid
emailemail
email_verifiedemail
groupprofile
created_atprofile
quotausage:read
used_quotausage:read
request_countusage:read

7. API Token Management

Manage Knox.chat API tokens on behalf of the user via OAuth2 Bearer authentication.

List API Tokens

GET /api/oauth2/tokens

Authentication: Bearer token with tokens:read scope

Response 200 OK:

{
"success": true,
"data": [
{
"id": 1,
"name": "My API Token",
"key": "sk-...xxxx",
"created_time": 1700000000,
"accessed_time": 1700100000,
"expired_time": -1,
"remain_quota": 500000,
"unlimited_quota": false,
"used_quota": 250000
}
]
}

Create API Token

POST /api/oauth2/tokens

Authentication: Bearer token with tokens:write scope

Request:

{
"name": "My New Token",
"expired_time": -1,
"remain_quota": 100000,
"unlimited_quota": false
}

Response 200 OK:

{
"success": true,
"data": {
"id": 2,
"name": "My New Token",
"key": "sk-full_token_key_shown_once"
}
}

⚠️ The full API token key is only returned once at creation time.

Delete API Token

DELETE /api/oauth2/tokens/{token_id}

Authentication: Bearer token with tokens:write scope

Response 200 OK:

{
"success": true,
"message": "Token deleted successfully"
}

8. Application Management

Manage your registered OAuth2 applications. These endpoints require session-based authentication (logged-in user).

List Your Applications

GET /api/oauth2/applications

Authentication: Session cookie

Response 200 OK:

{
"success": true,
"data": {
"applications": [
{
"id": 1,
"name": "My App",
"description": "A cool integration",
"homepage_url": "https://myapp.com",
"logo_url": "https://myapp.com/logo.png",
"client_id": "knox_abc123...",
"redirect_uris": "[\"https://myapp.com/callback\"]",
"allowed_scopes": "openid email profile",
"app_type": "confidential",
"status": 1,
"created_at": 1700000000,
"updated_at": 1700000000,
"is_verified": false,
"webhook_url": "https://myapp.com/webhooks/knox"
}
],
"total": 1,
"page": 1,
"page_size": 20
}
}

Create Application

POST /api/oauth2/applications

Authentication: Session cookie

Request (application/json):

{
"name": "My App",
"description": "A description of my application",
"homepage_url": "https://myapp.com",
"logo_url": "https://myapp.com/logo.png",
"redirect_uris": [
"https://myapp.com/callback",
"https://myapp.com/auth/callback"
],
"scopes": "openid email profile",
"app_type": "confidential",
"webhook_url": "https://myapp.com/webhooks/knox"
}

Validation Rules:

FieldRule
name1–64 characters
description0–500 characters
homepage_urlValid URL
logo_urlValid URL
redirect_uris1–10 valid URIs (HTTPS required; HTTP allowed for localhost; custom schemes allowed for mobile)
scopesSpace-separated valid scopes (1–256 chars)
app_type"confidential" or "public"
webhook_urlValid URL (optional)

Response 200 OK:

{
"success": true,
"data": {
"id": 1,
"name": "My App",
"client_id": "knox_abc123...",
"client_secret_plain": "knoxsec_xyz789...",
"redirect_uris": "[\"https://myapp.com/callback\"]",
"allowed_scopes": "openid email profile",
"app_type": "confidential",
"homepage_url": "https://myapp.com",
"logo_url": "https://myapp.com/logo.png",
"description": "A description of my application",
"is_verified": false,
"created_at": 1700000000,
"webhook_url": "https://myapp.com/webhooks/knox"
}
}

⚠️ client_secret_plain is only returned once! Save it securely immediately.

Update Application

PUT /api/oauth2/applications/{app_id}

Authentication: Session cookie (must be app owner)

Request: Same fields as create, all optional except app_id in the URL.

Delete Application

DELETE /api/oauth2/applications/{app_id}

Authentication: Session cookie (must be app owner)

This performs a soft delete (sets status to 3) and revokes all tokens and consents associated with the application.

Rotate Client Secret

POST /api/oauth2/applications/{app_id}/rotate-secret

Authentication: Session cookie (must be app owner)

Response 200 OK:

{
"success": true,
"data": {
"client_secret_plain": "knoxsec_new_secret..."
}
}

⚠️ The old secret is immediately invalidated. Update your application configuration before the rotation.

Users can view and revoke their authorized applications.

List Authorized Applications

GET /api/oauth2/consents

Authentication: Session cookie

Response 200 OK:

{
"success": true,
"data": {
"consents": [
{
"id": 1,
"user_id": 42,
"application_id": 1,
"scopes": "openid email",
"created_at": 1700000000,
"updated_at": 1700000000,
"app_name": "My App",
"app_description": "A cool integration",
"app_logo_url": "https://myapp.com/logo.png",
"app_homepage_url": "https://myapp.com",
"app_is_verified": true
}
],
"total": 1,
"page": 1,
"page_size": 20
}
}

Revoke Application Access

DELETE /api/oauth2/consents/{application_id}

Authentication: Session cookie

Revoking consent deletes all tokens (access + refresh) and removes the consent record. The application will need to request authorization again.

10. Admin Endpoints

Administrative endpoints for managing all OAuth2 applications and viewing audit logs. Requires admin-level session authentication.

List All Applications (Admin)

GET /api/oauth2/admin/applications?page=1&page_size=20

Response includes owner_username resolved via JOIN.

Verify Application

POST /api/oauth2/admin/applications/verify

Request: { "app_id": 1 }

Verified applications display a verification badge on the consent screen, increasing user trust.

Suspend Application

POST /api/oauth2/admin/applications/suspend

Request: { "app_id": 1 }

Suspended applications cannot issue new tokens, and existing tokens become invalid on validation.

Activate Application

POST /api/oauth2/admin/applications/activate

Request: { "app_id": 1 }

Reactivates a previously suspended application.

View Audit Logs

GET /api/oauth2/admin/audit?page=1&page_size=20&event_type=token_issued
ParameterDescription
pagePage number
page_sizeItems per page
event_typeFilter by event type (optional)
formatSet to csv for CSV export

Export Audit Logs (CSV)

GET /api/oauth2/admin/audit?format=csv

Returns a CSV file with up to 10,000 records.

Token Reference

Token TypeFormatLifetimeStorage Method
Client IDknox_<32 chars>PermanentPlaintext in DB
Client Secretknoxsec_<48 chars>Until rotatedbcrypt hash in DB
Authorization Code40 random characters10 minutesSHA-256 hash in DB, one-time use
Access Tokenknoxat_<48 chars>1 hourSHA-256 hash in DB
Refresh Tokenknoxrt_<48 chars>30 daysSHA-256 hash in DB
API Token (via tokens:write)sk-<48 chars>ConfigurablePlaintext in DB

Token Security

  • All tokens are stored as SHA-256 hashes in the database — never in plaintext.
  • Client secrets are stored as bcrypt hashes.
  • Authorization codes are single-use — enforced atomically at the database level.
  • Refresh token rotation — every refresh invalidates the old refresh token and its associated access token.
  • App status validation — tokens are checked against app status on every use; suspended app tokens are rejected.
  • User status validation — disabled users cannot refresh tokens.

Webhooks

If your application registers a webhook_url, Knox.chat will send HTTP POST notifications for key events.

Webhook Format

POST <your_webhook_url>
Content-Type: application/json
X-Knox-Event: <event_type>
X-Knox-Timestamp: <unix_timestamp>
{
"event": "consent_granted",
"application_id": 1,
"user_id": 42,
"timestamp": 1700000000,
"data": {
"scopes": "openid email profile"
}
}

Supported Events

EventTrigger
consent_grantedUser approves authorization for your app
consent_revokedUser revokes your app's access from their settings

Webhook Behavior

  • Fire-and-forget — Knox.chat does not retry failed webhook deliveries.
  • 10-second timeout — Webhook requests timeout after 10 seconds.
  • Async delivery — Webhooks are sent asynchronously and do not block the OAuth2 flow.

Rate Limits

EndpointLimitKey
POST /oauth2/token60 requests/minutePer IP
POST /oauth2/revoke60 requests/minutePer IP
POST /oauth2/introspect120 requests/minutePer IP
GET /oauth2/userinfo300 requests/minutePer IP
GET /oauth2/authorize30 requests/minutePer user
POST /oauth2/authorize30 requests/minutePer user

When rate limited, the API returns 429 Too Many Requests.

Error Handling

Standard OAuth2 Error Response

Token endpoint errors follow RFC 6749 §5.2:

{
"success": false,
"message": "Human-readable error message",
"error": "error_code",
"error_description": "Detailed description of the error"
}

OAuth2 Error Codes

Error CodeHTTP StatusDescription
invalid_request400Missing or invalid required parameters
invalid_client401Client authentication failed (wrong secret, unknown client)
invalid_grant400Authorization code expired, already used, redirect_uri mismatch, or PKCE verification failed
unsupported_grant_type400Grant type not supported (only authorization_code and refresh_token are supported)
invalid_scope400Requested scope is invalid or exceeds the app's allowed scopes
access_denied403User denied authorization, app suspended, or OAuth2 disabled

Authorization Redirect Errors

When errors occur during the authorization flow, the user is redirected back to your redirect_uri with error parameters:

https://myapp.com/callback?error=access_denied&error_description=User+denied+authorization&state=your_state

HTTP Error Codes

StatusMeaning
200Success
400Bad Request — Invalid parameters
401Unauthorized — Missing or invalid authentication
403Forbidden — Insufficient permissions, app suspended, or OAuth2 disabled
404Not Found — Unknown resource
429Too Many Requests — Rate limit exceeded
500Internal Server Error

Security Best Practices

For All Clients

  1. Always use the state parameter — Generate a random, unguessable value and validate it on the callback to prevent CSRF attacks.
  2. Use PKCE — Even for confidential clients, PKCE provides additional security against authorization code interception.
  3. Request minimal scopes — Only request the permissions your app actually needs.
  4. Use HTTPS for redirect URIs — HTTP is only acceptable for localhost during development.
  5. Validate tokens server-side — Never trust client-side token validation alone.

For Confidential Clients (Server-Side)

  1. Never expose your client secret — Keep it server-side only; never include it in frontend code, URLs, or logs.
  2. Rotate secrets periodically — Use the secret rotation endpoint to update credentials.
  3. Store tokens encrypted — Encrypt access and refresh tokens at rest in your database.
  4. Use Basic Auth for the token endpoint — Prefer Authorization: Basic over body parameters.

For Public Clients (SPAs, Mobile)

  1. PKCE is mandatory — The server enforces PKCE for public clients.
  2. Store tokens in memory only — For SPAs, never use localStorage or sessionStorage for tokens.
  3. Use platform secure storage — For mobile apps, use iOS Keychain or Android Keystore.

Token Storage Recommendations

PlatformRecommended Storage
Server-sideEncrypted database column
SPA (Browser)In-memory only (JavaScript variables)
Mobile (iOS)Keychain Services
Mobile (Android)EncryptedSharedPreferences / Keystore
DesktopOS credential store (Keychain, Credential Manager, libsecret)

Webhook Security

  • Validate the X-Knox-Timestamp header to prevent replay attacks.
  • Consider implementing HMAC signature verification if you handle sensitive webhook data.

Code Examples

Full Authorization Flow (JavaScript)

// Step 1: Generate state for CSRF protection
const state = crypto.randomUUID();
sessionStorage.setItem('oauth_state', state);

// Step 2: Redirect user to authorize
const authUrl = new URL('https://knox.chat/oauth2/authorize');
authUrl.searchParams.set('response_type', 'code');
authUrl.searchParams.set('client_id', 'knox_your_client_id');
authUrl.searchParams.set('redirect_uri', 'https://myapp.com/callback');
authUrl.searchParams.set('scope', 'openid email profile');
authUrl.searchParams.set('state', state);

window.location.href = authUrl.toString();

// Step 3: Handle the callback (on your callback page)
const params = new URLSearchParams(window.location.search);
const code = params.get('code');
const returnedState = params.get('state');

// Verify state matches
if (returnedState !== sessionStorage.getItem('oauth_state')) {
throw new Error('State mismatch — possible CSRF attack');
}

// Step 4: Exchange code for tokens (do this server-side!)
const tokenResponse = await fetch('https://api.knox.chat/api/oauth2/token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
grant_type: 'authorization_code',
code: code,
redirect_uri: 'https://myapp.com/callback',
client_id: 'knox_your_client_id',
client_secret: 'knoxsec_your_secret', // Server-side only!
}),
});
const tokens = await tokenResponse.json();

// Step 5: Get user info
const userResponse = await fetch('https://api.knox.chat/api/oauth2/userinfo', {
headers: { 'Authorization': `Bearer ${tokens.access_token}` },
});
const user = await userResponse.json();
console.log(`Welcome, ${user.display_name}!`);

// Step 6: Refresh when needed
const refreshResponse = await fetch('https://api.knox.chat/api/oauth2/token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
grant_type: 'refresh_token',
refresh_token: tokens.refresh_token,
client_id: 'knox_your_client_id',
client_secret: 'knoxsec_your_secret',
}),
});
const newTokens = await refreshResponse.json();
// ⚠️ Store newTokens.refresh_token — the old one is now revoked!

PKCE Helper Functions

// Generate a random code verifier (43-128 characters)
function generateCodeVerifier() {
const array = new Uint8Array(32);
crypto.getRandomValues(array);
return btoa(String.fromCharCode(...array))
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '');
}

// Generate code challenge from verifier (S256 method)
async function generateCodeChallenge(verifier) {
const encoder = new TextEncoder();
const data = encoder.encode(verifier);
const digest = await crypto.subtle.digest('SHA-256', data);
return btoa(String.fromCharCode(...new Uint8Array(digest)))
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '');
}

// Usage:
const codeVerifier = generateCodeVerifier();
const codeChallenge = await generateCodeChallenge(codeVerifier);

// Store verifier securely for later use
sessionStorage.setItem('pkce_verifier', codeVerifier);

// Include in authorization URL:
authUrl.searchParams.set('code_challenge', codeChallenge);
authUrl.searchParams.set('code_challenge_method', 'S256');

// Include verifier in token exchange:
// { ..., code_verifier: sessionStorage.getItem('pkce_verifier') }

Node.js (Express) Integration

const express = require('express');
const crypto = require('crypto');
const app = express();

const CLIENT_ID = 'knox_your_client_id';
const CLIENT_SECRET = 'knoxsec_your_client_secret';
const REDIRECT_URI = 'http://localhost:3000/auth/callback';
const KNOX_API = 'https://api.knox.chat/api/oauth2';

// Step 1: Start authorization
app.get('/auth/login', (req, res) => {
const state = crypto.randomBytes(16).toString('hex');
const verifier = crypto.randomBytes(32).toString('base64url');
const challenge = crypto
.createHash('sha256')
.update(verifier)
.digest('base64url');

// Store in session
req.session.oauth_state = state;
req.session.pkce_verifier = verifier;

const authUrl = new URL('https://knox.chat/oauth2/authorize');
authUrl.searchParams.set('response_type', 'code');
authUrl.searchParams.set('client_id', CLIENT_ID);
authUrl.searchParams.set('redirect_uri', REDIRECT_URI);
authUrl.searchParams.set('scope', 'openid email profile');
authUrl.searchParams.set('state', state);
authUrl.searchParams.set('code_challenge', challenge);
authUrl.searchParams.set('code_challenge_method', 'S256');

res.redirect(authUrl.toString());
});

// Step 2: Handle callback
app.get('/auth/callback', async (req, res) => {
const { code, state, error } = req.query;

if (error) return res.status(400).send(`Authorization error: ${error}`);
if (state !== req.session.oauth_state) return res.status(403).send('State mismatch');

// Exchange code for tokens
const tokenRes = await fetch(`${KNOX_API}/token`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Basic ${Buffer.from(`${CLIENT_ID}:${CLIENT_SECRET}`).toString('base64')}`,
},
body: JSON.stringify({
grant_type: 'authorization_code',
code,
redirect_uri: REDIRECT_URI,
code_verifier: req.session.pkce_verifier,
}),
});
const tokens = await tokenRes.json();

// Get user info
const userRes = await fetch(`${KNOX_API}/userinfo`, {
headers: { 'Authorization': `Bearer ${tokens.access_token}` },
});
const user = await userRes.json();

// Store tokens and user in session
req.session.tokens = tokens;
req.session.user = user;

res.redirect('/dashboard');
});

app.listen(3000, () => console.log('Server running on http://localhost:3000'));

Python (Flask) Integration

import secrets
import hashlib
import base64
import requests
from flask import Flask, redirect, request, session, url_for

app = Flask(__name__)
app.secret_key = secrets.token_hex(32)

CLIENT_ID = "knox_your_client_id"
CLIENT_SECRET = "knoxsec_your_client_secret"
REDIRECT_URI = "http://localhost:5000/auth/callback"
KNOX_API = "https://api.knox.chat/api/oauth2"


def generate_pkce():
verifier = secrets.token_urlsafe(32)
challenge = base64.urlsafe_b64encode(
hashlib.sha256(verifier.encode()).digest()
).rstrip(b"=").decode()
return verifier, challenge


@app.route("/auth/login")
def login():
state = secrets.token_hex(16)
verifier, challenge = generate_pkce()

session["oauth_state"] = state
session["pkce_verifier"] = verifier

auth_url = (
f"https://knox.chat/oauth2/authorize?"
f"response_type=code"
f"&client_id={CLIENT_ID}"
f"&redirect_uri={REDIRECT_URI}"
f"&scope=openid email profile"
f"&state={state}"
f"&code_challenge={challenge}"
f"&code_challenge_method=S256"
)
return redirect(auth_url)


@app.route("/auth/callback")
def callback():
error = request.args.get("error")
if error:
return f"Authorization error: {error}", 400

code = request.args.get("code")
state = request.args.get("state")

if state != session.get("oauth_state"):
return "State mismatch", 403

# Exchange code for tokens
token_res = requests.post(
f"{KNOX_API}/token",
json={
"grant_type": "authorization_code",
"code": code,
"redirect_uri": REDIRECT_URI,
"client_id": CLIENT_ID,
"client_secret": CLIENT_SECRET,
"code_verifier": session["pkce_verifier"],
},
)
tokens = token_res.json()

# Get user info
user_res = requests.get(
f"{KNOX_API}/userinfo",
headers={"Authorization": f"Bearer {tokens['access_token']}"},
)
user = user_res.json()

session["user"] = user
session["tokens"] = tokens

return redirect("/dashboard")


if __name__ == "__main__":
app.run(port=5000, debug=True)

React SPA (Public Client)

import { useState, useEffect } from 'react';

const CLIENT_ID = 'knox_your_public_client_id';
const REDIRECT_URI = 'http://localhost:3000/callback';
const KNOX_AUTH = 'https://knox.chat/oauth2/authorize';
const KNOX_API = 'https://api.knox.chat/api/oauth2';

// PKCE helpers
function generateCodeVerifier() {
const array = new Uint8Array(32);
crypto.getRandomValues(array);
return btoa(String.fromCharCode(...array))
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}

async function generateCodeChallenge(verifier) {
const digest = await crypto.subtle.digest(
'SHA-256',
new TextEncoder().encode(verifier)
);
return btoa(String.fromCharCode(...new Uint8Array(digest)))
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}

// Login button component
function LoginButton() {
const handleLogin = async () => {
const state = crypto.randomUUID();
const verifier = generateCodeVerifier();
const challenge = await generateCodeChallenge(verifier);

// Store for callback verification
sessionStorage.setItem('oauth_state', state);
sessionStorage.setItem('pkce_verifier', verifier);

const url = new URL(KNOX_AUTH);
url.searchParams.set('response_type', 'code');
url.searchParams.set('client_id', CLIENT_ID);
url.searchParams.set('redirect_uri', REDIRECT_URI);
url.searchParams.set('scope', 'openid email');
url.searchParams.set('state', state);
url.searchParams.set('code_challenge', challenge);
url.searchParams.set('code_challenge_method', 'S256');

window.location.href = url.toString();
};

return <button onClick={handleLogin}>Sign in with Knox.chat</button>;
}

// Callback page component
function CallbackPage() {
const [user, setUser] = useState(null);
const [error, setError] = useState(null);

useEffect(() => {
const params = new URLSearchParams(window.location.search);
const code = params.get('code');
const state = params.get('state');
const err = params.get('error');

if (err) { setError(err); return; }
if (state !== sessionStorage.getItem('oauth_state')) {
setError('State mismatch'); return;
}

// Exchange code for tokens (no client_secret for public clients)
fetch(`${KNOX_API}/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
grant_type: 'authorization_code',
code,
redirect_uri: REDIRECT_URI,
client_id: CLIENT_ID,
code_verifier: sessionStorage.getItem('pkce_verifier'),
}),
})
.then(r => r.json())
.then(tokens => {
// Store tokens IN MEMORY only — never localStorage!
window.__tokens = tokens;

return fetch(`${KNOX_API}/userinfo`, {
headers: { 'Authorization': `Bearer ${tokens.access_token}` },
});
})
.then(r => r.json())
.then(setUser)
.catch(e => setError(e.message));
}, []);

if (error) return <div>Error: {error}</div>;
if (!user) return <div>Loading...</div>;
return <div>Welcome, {user.display_name}!</div>;
}

cURL Examples

# 1. Discover endpoints
curl https://api.knox.chat/api/oauth2/.well-known/openid-configuration

# 2. Exchange authorization code for tokens
curl -X POST https://api.knox.chat/api/oauth2/token \
-H "Content-Type: application/json" \
-d '{
"grant_type": "authorization_code",
"code": "YOUR_AUTH_CODE",
"redirect_uri": "https://myapp.com/callback",
"client_id": "knox_your_client_id",
"client_secret": "knoxsec_your_secret"
}'

# 3. Exchange code with PKCE (public client)
curl -X POST https://api.knox.chat/api/oauth2/token \
-H "Content-Type: application/json" \
-d '{
"grant_type": "authorization_code",
"code": "YOUR_AUTH_CODE",
"redirect_uri": "https://myapp.com/callback",
"client_id": "knox_your_client_id",
"code_verifier": "your_code_verifier"
}'

# 4. Refresh tokens
curl -X POST https://api.knox.chat/api/oauth2/token \
-H "Content-Type: application/json" \
-d '{
"grant_type": "refresh_token",
"refresh_token": "knoxrt_your_refresh_token",
"client_id": "knox_your_client_id",
"client_secret": "knoxsec_your_secret"
}'

# 5. Get user info
curl https://api.knox.chat/api/oauth2/userinfo \
-H "Authorization: Bearer knoxat_your_access_token"

# 6. Revoke a token
curl -X POST https://api.knox.chat/api/oauth2/revoke \
-H "Content-Type: application/json" \
-d '{"token": "knoxat_your_access_token"}'

# 7. Introspect a token
curl -X POST https://api.knox.chat/api/oauth2/introspect \
-u "knox_your_client_id:knoxsec_your_secret" \
-H "Content-Type: application/json" \
-d '{"token": "knoxat_your_access_token"}'

# 8. List API tokens (via OAuth2)
curl https://api.knox.chat/api/oauth2/tokens \
-H "Authorization: Bearer knoxat_your_access_token"

# 9. Create API token (via OAuth2)
curl -X POST https://api.knox.chat/api/oauth2/tokens \
-H "Authorization: Bearer knoxat_your_access_token" \
-H "Content-Type: application/json" \
-d '{"name": "My Token", "expired_time": -1, "remain_quota": 100000, "unlimited_quota": false}'

# 10. Delete API token (via OAuth2)
curl -X DELETE https://api.knox.chat/api/oauth2/tokens/123 \
-H "Authorization: Bearer knoxat_your_access_token"

Configuration Reference

Environment Variables

VariableDefaultDescription
OAUTH2_ACCESS_TOKEN_TTL3600 (1 hour)Access token lifetime in seconds
OAUTH2_REFRESH_TOKEN_TTL2592000 (30 days)Refresh token lifetime in seconds
OAUTH2_CODE_TTL600 (10 minutes)Authorization code lifetime in seconds

Database Options (Hot-Reloadable)

These can be changed at runtime via the options database table:

KeyDefaultDescription
OAuth2EnabledtrueEnable/disable the entire OAuth2 system
OAuth2RequireApprovalfalseWhen true, only admin-verified apps can request authorization
OAuth2AccessTokenTTL3600Access token lifetime (seconds)
OAuth2RefreshTokenTTL2592000Refresh token lifetime (seconds)
OAuth2CodeTTL600Authorization code lifetime (seconds)
OAuth2AllowedScopesopenid email profile tokens:read tokens:write usage:readGlobal allowed scopes

Database Schema

Tables

oauth2_applications

Stores registered OAuth2 client applications.

ColumnTypeDescription
idSERIAL PRIMARY KEYAuto-incrementing ID
nameVARCHAR(255)Application name
descriptionTEXTApplication description
homepage_urlVARCHAR(500)Application homepage
logo_urlVARCHAR(500)Application logo
client_idVARCHAR(255) UNIQUEOAuth2 client identifier
client_secretVARCHAR(255)bcrypt-hashed client secret
redirect_urisTEXTJSON array of registered redirect URIs
allowed_scopesVARCHAR(500)Space-separated allowed scopes
app_typeVARCHAR(50)"confidential" or "public"
user_idINTEGER REFERENCES users(id)Owner user ID
statusINTEGER DEFAULT 11=Active, 2=Suspended, 3=Deleted
rate_limitINTEGER DEFAULT 1000Requests per hour
created_atBIGINTUnix timestamp
updated_atBIGINTUnix timestamp
is_verifiedBOOLEAN DEFAULT FALSEAdmin verification status
verified_byINTEGERAdmin user who verified
verified_atBIGINTVerification timestamp
webhook_urlVARCHAR(500)Webhook URL for events

oauth2_authorization_codes

Temporary authorization codes (short-lived, single-use).

ColumnTypeDescription
idSERIAL PRIMARY KEYAuto-incrementing ID
code_hashVARCHAR(255) UNIQUESHA-256 hash of the code
application_idINTEGER REFERENCES oauth2_applications(id)Associated application
user_idINTEGER REFERENCES users(id)Authorizing user
redirect_uriTEXTRedirect URI used
scopeVARCHAR(500)Granted scopes
stateVARCHAR(500)State parameter
code_challengeVARCHAR(500)PKCE code challenge
code_challenge_methodVARCHAR(10)PKCE method (S256 or plain)
expires_atBIGINTExpiration timestamp
usedBOOLEAN DEFAULT FALSEWhether code has been consumed
created_atBIGINTCreation timestamp

oauth2_access_tokens

Active access tokens.

ColumnTypeDescription
idSERIAL PRIMARY KEYAuto-incrementing ID
token_hashVARCHAR(255) UNIQUESHA-256 hash of the token
application_idINTEGER REFERENCES oauth2_applications(id)Associated application
user_idINTEGER REFERENCES users(id)Token owner
scopeVARCHAR(500)Granted scopes
expires_atBIGINTExpiration timestamp
created_atBIGINTCreation timestamp
last_used_atBIGINTLast usage timestamp

oauth2_refresh_tokens

Refresh tokens with revocation support.

ColumnTypeDescription
idSERIAL PRIMARY KEYAuto-incrementing ID
token_hashVARCHAR(255) UNIQUESHA-256 hash of the token
access_token_idINTEGERAssociated access token
application_idINTEGER REFERENCES oauth2_applications(id)Associated application
user_idINTEGER REFERENCES users(id)Token owner
scopeVARCHAR(500)Granted scopes
expires_atBIGINTExpiration timestamp
revokedBOOLEAN DEFAULT FALSEWhether token is revoked
created_atBIGINTCreation timestamp

oauth2_user_consents

Records user consent decisions (unique per user + application pair).

ColumnTypeDescription
idSERIAL PRIMARY KEYAuto-incrementing ID
user_idINTEGER REFERENCES users(id)User who consented
application_idINTEGER REFERENCES oauth2_applications(id)Application granted access
scopesVARCHAR(500)Consented scopes
created_atBIGINTInitial consent timestamp
updated_atBIGINTLast updated (scope expansion)

Unique constraint: (user_id, application_id) — one consent record per user-app pair.

oauth2_audit_log

Comprehensive audit trail of all OAuth2 events.

ColumnTypeDescription
idSERIAL PRIMARY KEYAuto-incrementing ID
event_typeVARCHAR(100)Event type identifier
application_idINTEGERRelated application (nullable)
user_idINTEGERRelated user (nullable)
ip_addressVARCHAR(100)Client IP address
user_agentTEXTClient user agent
metadataJSONBAdditional event data
created_atBIGINTEvent timestamp

Audit Event Types

Event TypeDescriptionMetadata
authorization_grantedUser approved an appScopes, redirect URI
authorization_deniedUser denied an appRedirect URI
token_issuedTokens issued via code exchangeGrant type, scopes
token_refreshedTokens refreshedScopes
token_revokedToken revokedToken type
consent_revokedUser revoked app accessApplication ID
app_createdNew application registeredApp name
app_updatedApplication details changedChanged fields
app_deletedApplication soft-deletedApp name
secret_rotatedClient secret rotated
app_verifiedAdmin verified an appAdmin user ID
app_suspendedAdmin suspended an appAdmin user ID
app_activatedAdmin activated an appAdmin user ID

Admin Guide

Dashboard Locations

PagePathAccess
My ApplicationsSettings → OAuth2 Apps → My ApplicationsAll users
Authorized AppsSettings → OAuth2 Apps → Authorized AppsAll users
Integration GuideSettings → OAuth2 Apps → Integration GuideAll users
Admin Panel/oauth2-adminAdmin only

Application Status Lifecycle

                  ┌──────────┐
Created ────> │ Active │ (status=1)
│ (1) │
└────┬─────┘

┌────────┼────────┐
│ │ │
v │ v
┌──────────┐ │ ┌──────────┐
│Suspended │ │ │ Deleted │ (status=3)
│ (2) │ │ │ (soft) │
└────┬─────┘ │ └──────────┘
│ │ All tokens revoked
└─────────┘ All consents deleted
Reactivate

Admin Actions

  • Verify: Adds a verification badge to the app on the consent screen. Increases user trust.
  • Suspend: Immediately invalidates all existing tokens. The app cannot issue new tokens or request authorization.
  • Activate: Restores a suspended app to active status. Users will need to re-authorize.
  • Audit Logs: View all OAuth2 events with filtering by event type. Export to CSV for compliance.

Security Monitoring

Monitor the audit log for suspicious patterns:

  • Excessive token_issued events from a single app (possible abuse)
  • authorization_denied spikes (possible phishing)
  • token_revoked in bulk (compromised app)
  • secret_rotated without matching app_updated (unauthorized rotation)

FAQ

For Users

Q: How do I see which apps have access to my account?
A: Go to Settings → OAuth2 Apps → Authorized Apps. You'll see all apps you've authorized, their permissions, and the date you granted access.

Q: How do I revoke an app's access?
A: In Authorized Apps, click the Revoke button next to the app. This immediately invalidates all tokens and removes the app's access.

Q: What does "unverified" mean on the consent screen?
A: Unverified apps have not been reviewed by Knox.chat administrators. Exercise caution when authorizing unverified apps and only grant the minimum necessary permissions.

Q: Can I change which permissions an app has?
A: Revoke the app's access, then re-authorize it. The app will ask for permissions again, and you can review them at that time.

For Developers

Q: How do I register an OAuth2 application?
A: Go to Settings → OAuth2 Apps → My Applications and click "Create Application". Fill in the required fields and save your client secret.

Q: I lost my client secret. What do I do?
A: Use the Rotate Secret action on your application. This generates a new secret and invalidates the old one. Update your application configuration immediately.

Q: Should I use a confidential or public client?
A: Use confidential if your app runs on a server where the secret can be kept safe. Use public for SPAs, mobile apps, or any client where the source code is visible. Public clients must use PKCE.

Q: How long do tokens last?
A: Access tokens expire after 1 hour, refresh tokens after 30 days. Use the refresh token to get new access tokens without user re-authorization.

Q: What happens when I refresh a token?
A: You receive a new access token AND a new refresh token. The old refresh token is immediately revoked (token rotation). Always store the new refresh token.

Q: My app needs to work when the user is offline. How?
A: Use refresh tokens. As long as the refresh token is valid (30 days) and the user hasn't revoked access, you can obtain new access tokens without user interaction.

Q: What redirect URIs are allowed?
A: HTTPS URIs are required for production. HTTP is allowed for localhost during development. Custom URI schemes (e.g., myapp://callback) are allowed for mobile apps.

Q: How do webhooks work?
A: Register a webhook_url when creating your app. Knox.chat will POST consent_granted and consent_revoked events to this URL. Webhooks are fire-and-forget with a 10-second timeout.

Q: Is there an OpenID Connect discovery endpoint?
A: Yes. GET /api/oauth2/.well-known/openid-configuration returns the full OIDC discovery document.

Q: What's the rate limit for the token endpoint?
A: 60 requests/minute per IP. The userinfo endpoint allows 300 requests/minute. See the Rate Limits section for full details.

Supported Standards

StandardStatusNotes
RFC 6749 — OAuth 2.0 FrameworkAuthorization Code + Refresh Token grants
RFC 7636 — PKCES256 and plain methods; mandatory for public clients
RFC 7009 — Token RevocationAlways returns 200 per spec
RFC 7662 — Token IntrospectionRequires client authentication
OpenID Connect Discovery⚠️ PartialDiscovery document + userinfo; no ID tokens