OAuth2
OAuth2 authorization flows, token management, and OpenID Connect userinfo.
OAuth2 Provider
TotalAccess implements an OAuth2 authorization server, allowing third-party applications to securely access the TotalAccess API on behalf of a user.
Supported Flows
| Flow | Use Case | User Context |
|---|---|---|
| Authorization Code + PKCE | Web apps, mobile apps, SPAs | Yes |
| Client Credentials | Server-to-server, background jobs | No |
Endpoints
| Endpoint | Method | Description |
|---|---|---|
/api/v1/oauth/authorize | GET | Authorization endpoint — redirects user to consent screen |
/api/v1/oauth/authorize | POST | Consent decision — approve or deny authorization |
/api/v1/oauth/token | POST | Token endpoint — exchange code, refresh token, or client credentials |
/api/v1/oauth/revoke | POST | Token revocation (RFC 7009) |
/api/v1/oauth/userinfo | GET | OIDC UserInfo endpoint |
Scopes
OAuth2 scopes are identical to API key scopes. See the API Keys page for the full list.
The openid, profile, and email scopes are available for OIDC-compatible flows.
Authorization Code Flow with PKCE
Step 1 — Register an OAuth Application
Before using OAuth2, you need to register an application to obtain a client_id and client_secret. This is done via the developer portal or API.
Step 2 — Build the Authorization URL
Redirect the user to the authorization endpoint:
GET /api/v1/oauth/authorizeParameters:
| Parameter | Required | Description |
|---|---|---|
response_type | Yes | Must be code |
client_id | Yes | Your OAuth application client ID |
redirect_uri | Yes | Must match a registered redirect URI |
scope | Yes | Space-separated list of requested scopes |
state | Recommended | Random string for CSRF protection |
code_challenge | Yes (PKCE) | SHA-256 hash of the code verifier, base64url-encoded |
code_challenge_method | Yes (PKCE) | Must be S256 |
Example URL:
https://app.totalaccess.co.za/api/v1/oauth/authorize?
response_type=code&
client_id=ta_oa_abc123&
redirect_uri=https://myapp.com/callback&
scope=employees:read%20attendance:read&
state=randomstate123&
code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSst-cTY&
code_challenge_method=S256Step 3 — User Consent
The user will be redirected to a branded consent screen showing:
- Application name and logo
- Requested scopes with descriptions
- Authorize / Deny buttons
If the user is not logged in, they will be redirected to the login page first, then back to the consent screen.
Step 4 — Handle the Redirect
After the user approves or denies, TotalAccess redirects to your redirect_uri:
On approval:
https://myapp.com/callback?code=ta_ac_xxx&state=randomstate123On denial:
https://myapp.com/callback?error=access_denied&error_description=The+user+denied+the+authorization+request&state=randomstate123Always verify the state parameter matches what you sent. This prevents CSRF attacks.
Step 5 — Exchange the Code for Tokens
POST /api/v1/oauth/tokenThe token endpoint accepts both application/x-www-form-urlencoded and application/json content types. Client credentials can also be passed via HTTP Basic authentication (Authorization: Basic base64(client_id:client_secret)).
Parameters:
| Parameter | Required | Description |
|---|---|---|
grant_type | Yes | authorization_code |
code | Yes | The authorization code from step 4 |
redirect_uri | Yes | Must match the one from step 2 |
client_id | Yes | Your client ID (or via Basic auth) |
client_secret | Yes | Your client secret (or via Basic auth) |
code_verifier | Yes (PKCE) | The original code verifier used to generate the challenge |
Example:
curl -X POST https://app.totalaccess.co.za/api/v1/oauth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=authorization_code" \
-d "code=ta_ac_xxx" \
-d "redirect_uri=https://myapp.com/callback" \
-d "client_id=ta_oa_abc123" \
-d "client_secret=ta_os_secret123" \
-d "code_verifier=dBjftJeZbCkVYyoWjKqYJgkKqKqKqKqKqKqKqKqKqK"Or using JSON body:
curl -X POST https://app.totalaccess.co.za/api/v1/oauth/token \
-H "Content-Type: application/json" \
-d '{
"grant_type": "authorization_code",
"code": "ta_ac_xxx",
"redirect_uri": "https://myapp.com/callback",
"client_id": "ta_oa_abc123",
"client_secret": "ta_os_secret123",
"code_verifier": "dBjftJeZbCkVYyoWjKqYJgkKqKqKqKqKqKqKqKqKqK"
}'Or using HTTP Basic authentication:
curl -X POST https://app.totalaccess.co.za/api/v1/oauth/token \
-u "ta_oa_abc123:ta_os_secret123" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=authorization_code" \
-d "code=ta_ac_xxx" \
-d "redirect_uri=https://myapp.com/callback" \
-d "code_verifier=dBjftJeZbCkVYyoWjKqYJgkKqKqKqKqKqKqKqKqKqK"const response = await fetch('https://app.totalaccess.co.za/api/v1/oauth/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'authorization_code',
code: 'ta_ac_xxx',
redirect_uri: 'https://myapp.com/callback',
client_id: 'ta_oa_abc123',
client_secret: 'ta_os_secret123',
code_verifier: 'dBjftJeZbCkVYyoWjKqYJgkKqKqKqKqKqKqKqKqKqK',
}),
});
const tokens = await response.json();Response (200 OK):
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 900,
"refresh_token": "ta_rt_xxx",
"scope": "employees:read attendance:read"
}Step 6 — Use the Access Token
GET /api/v1/employees
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...Refreshing Tokens
Refresh tokens are rotated on each use — you receive a new refresh token with every refresh. If a refresh token is used twice, the entire token chain is revoked (reuse detection).
POST /api/v1/oauth/tokenParameters:
| Parameter | Required | Description |
|---|---|---|
grant_type | Yes | refresh_token |
refresh_token | Yes | The refresh token from the previous response |
client_id | Yes | Your client ID |
client_secret | Yes | Your client secret (for confidential clients) |
Example:
curl -X POST https://app.totalaccess.co.za/api/v1/oauth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=refresh_token" \
-d "refresh_token=ta_rt_xxx" \
-d "client_id=ta_oa_abc123" \
-d "client_secret=ta_os_secret123"Always store the new refresh_token from the response. The previous one is invalidated immediately.
Client Credentials Flow
For server-to-server integrations without user context:
POST /api/v1/oauth/tokenParameters:
| Parameter | Required | Description |
|---|---|---|
grant_type | Yes | client_credentials |
client_id | Yes | Your client ID |
client_secret | Yes | Your client secret |
scope | Optional | Requested scopes |
Example:
curl -X POST https://app.totalaccess.co.za/api/v1/oauth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials" \
-d "client_id=ta_oa_abc123" \
-d "client_secret=ta_os_secret123" \
-d "scope=employees:read"Response:
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 900,
"scope": "employees:read"
}Client credentials tokens do not include a refresh token. Request a new token when the access token expires.
Token Revocation
Revoke an access or refresh token (RFC 7009):
POST /api/v1/oauth/revokeParameters:
| Parameter | Required | Description |
|---|---|---|
token | Yes | The token to revoke |
token_type_hint | Optional | access_token or refresh_token |
Example:
curl -X POST https://app.totalaccess.co.za/api/v1/oauth/revoke \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "token=ta_rt_xxx" \
-d "token_type_hint=refresh_token"The revocation endpoint also accepts application/json content type. Client authentication is not required — the endpoint revokes tokens based solely on the token parameter, as per RFC 7009.
The endpoint returns 200 OK regardless of whether the token was valid (to prevent information leakage).
UserInfo Endpoint (OIDC)
Retrieve profile information about the authenticated user:
GET /api/v1/oauth/userinfo
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...Response (user-context tokens, based on granted scopes):
{
"sub": "123",
"name": "John Doe",
"given_name": "John",
"family_name": "Doe",
"picture": "https://cdn.totalaccess.co.za/avatars/123.jpg",
"email": "[email protected]",
"email_verified": true
}Response (client_credentials tokens, no user context):
{
"sub": "ta_oa_abc123",
"client_id": "ta_oa_abc123",
"scope": "employees:read"
}| Field | Scope Required |
|---|---|
sub | openid |
email, email_verified | email (or profile) |
name, given_name, family_name, picture | profile |
Token Lifetimes
| Token Type | Lifetime |
|---|---|
| Authorization code | 10 minutes |
| Access token | 15 minutes |
| Refresh token | 30 days |
PKCE Implementation
PKCE (Proof Key for Code Exchange) prevents authorization code interception attacks. It is required for all authorization code flows.
Generating the Code Verifier and Challenge
import crypto from 'crypto';
// 1. Generate a random code verifier (43-128 chars)
const codeVerifier = crypto.randomBytes(32).toString('base64url');
// 2. Compute the code challenge (S256)
const codeChallenge = crypto
.createHash('sha256')
.update(codeVerifier)
.digest('base64url');// 1. Generate a random code verifier
const array = new Uint8Array(32);
crypto.getRandomValues(array);
const codeVerifier = btoa(String.fromCharCode(...array))
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '');
// 2. Compute the code challenge
const encoded = new TextEncoder().encode(codeVerifier);
const hash = await crypto.subtle.digest('SHA-256', encoded);
const codeChallenge = btoa(String.fromCharCode(...new Uint8Array(hash)))
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '');Using OAuth2 Tokens with API Routes
API routes that support OAuth2 token authentication will accept Bearer tokens from OAuth2 in the Authorization header. The route must explicitly enable allowOAuthTokenAuth:
const authResult = await authenticateAndGetClient(request, {
allowOAuthTokenAuth: true,
requiredScopes: ['employees:read'],
});The middleware verifies:
- JWT signature and expiry
- Token exists in the database and is not revoked
- Granted scopes satisfy the required scopes
- User still exists (for user-context tokens)