UniAuth OAuth2 / OpenID Connect
Integrate "Sign in with UniAuth" into your application using standard OAuth2 and OpenID Connect protocols.
Quick Start
- Go to Account → Developer and register your app to get a
client_idandclient_secret - Redirect users to the authorization endpoint with PKCE
- Exchange the authorization code for tokens at the token endpoint
- Use the access token to fetch user info
Endpoints
| Endpoint | URL |
|---|---|
| Discovery | https://uniauth.id/.well-known/openid-configuration |
| Authorization | https://uniauth.id/api/oauth/authorize |
| Token | https://uniauth.id/api/oauth/token |
| UserInfo | https://uniauth.id/api/oauth/userinfo |
| Revocation | https://uniauth.id/api/oauth/revoke |
| Introspection | https://uniauth.id/api/oauth/introspect |
| End-Session | https://uniauth.id/api/oauth/end-session |
| Pushed Authorization Requests | POST https://uniauth.id/api/oauth/par |
| Dynamic Client Registration | POST https://uniauth.id/api/oauth/register |
| JWKS | https://uniauth.id/.well-known/jwks.json |
Scopes
| Scope | Claims |
|---|---|
openid | sub, iss, aud, exp, iat, auth_time |
profile | name, given_name, family_name, preferred_username, nickname, picture, profile, website, gender, birthdate, locale, zoneinfo, updated_at |
email | email, email_verified |
phone | phone_number, phone_number_verified |
address | address (structured: street_address, locality, region, postal_code, country) |
JavaScript Example
// 1. Generate PKCE verifier and challenge
const verifier = crypto.randomUUID() + crypto.randomUUID();
const challenge = btoa(String.fromCharCode(
...new Uint8Array(await crypto.subtle.digest('SHA-256',
new TextEncoder().encode(verifier)))
)).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
// 2. Redirect to authorize
const params = new URLSearchParams({
response_type: 'code',
client_id: 'YOUR_CLIENT_ID',
redirect_uri: 'https://yourapp.com/callback',
scope: 'openid profile email',
state: crypto.randomUUID(),
code_challenge: challenge,
code_challenge_method: 'S256',
});
window.location.href = 'https://uniauth.id/api/oauth/authorize?' + params;
// 3. Exchange code for tokens (server-side)
const tokenRes = await fetch('https://uniauth.id/api/oauth/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'authorization_code',
code: CODE_FROM_CALLBACK,
redirect_uri: 'https://yourapp.com/callback',
client_id: 'YOUR_CLIENT_ID',
client_secret: 'YOUR_CLIENT_SECRET',
code_verifier: verifier,
}),
});
const tokens = await tokenRes.json();
// 4. Fetch user info
const userRes = await fetch('https://uniauth.id/api/oauth/userinfo', {
headers: { Authorization: 'Bearer ' + tokens.access_token },
});
const user = await userRes.json();PHP Example (with SDK)
Download UniAuth.php and drop it into your project. It handles PKCE, CSRF, and token exchange automatically.
// UniAuth.php — single-file SDK, no dependencies (just PHP 8+ with curl)
require_once 'UniAuth.php';
$ua = new UniAuth(
clientId: 'your_client_id',
clientSecret: 'your_client_secret',
redirectUri: 'https://yoursite.com/callback.php'
);
// ── Login page ──────────────────────────────
// Redirect the user to UniAuth:
header('Location: ' . $ua->getLoginUrl());
// ── Callback page (callback.php) ────────────
// Exchange code for user info (one line):
$user = $ua->handleCallback($_GET['code']);
// $user = [
// 'sub' => 'uuid-of-user',
// 'email' => '[email protected]',
// 'email_verified' => true,
// 'name' => 'John Doe',
// 'given_name' => 'John',
// 'family_name' => 'Doe',
// 'picture' => 'https://uniauth.id/api/avatar/...',
// '_tokens' => ['access_token' => '...', 'refresh_token' => '...']
// ]
// Create your session, match/create user in DB, done.
$_SESSION['user'] = $user;Python Example
import hashlib, base64, secrets, requests
# 1. Generate PKCE
verifier = secrets.token_urlsafe(32)
challenge = base64.urlsafe_b64encode(
hashlib.sha256(verifier.encode()).digest()
).rstrip(b'=').decode()
# 2. Redirect user to:
# https://uniauth.id/api/oauth/authorize?response_type=code&client_id=...
# &redirect_uri=...&scope=openid+profile+email
# &state=RANDOM&code_challenge={challenge}&code_challenge_method=S256
# 3. In your callback, exchange the code:
tokens = requests.post('https://uniauth.id/api/oauth/token', data={
'grant_type': 'authorization_code',
'code': code_from_callback,
'redirect_uri': 'https://yourapp.com/callback',
'client_id': 'YOUR_CLIENT_ID',
'client_secret': 'YOUR_CLIENT_SECRET',
'code_verifier': verifier,
}).json()
# 4. Fetch user info:
user = requests.get('https://uniauth.id/api/oauth/userinfo',
headers={'Authorization': f'Bearer {tokens["access_token"]}'}
).json()Authorization Parameters
The authorization endpoint accepts several optional parameters that control the authentication experience. These follow the OpenID Connect Core specification.
| Parameter | Type | Description |
|---|---|---|
response_type | Required | Must be code for the authorization code flow. |
client_id | Required | Your application's client identifier. |
redirect_uri | Required | Must exactly match one of your registered redirect URIs. |
scope | Required | Space-delimited scopes. Must include openid for OIDC flows. |
state | Recommended | An opaque CSRF token. Returned unchanged in the callback. Generate a random value and verify it on return. |
code_challenge | Required | Base64url-encoded SHA-256 hash of the code_verifier. PKCE is mandatory on all flows. |
code_challenge_method | Required | Must be S256. Plain challenges are not accepted. |
prompt | Optional | Controls the authentication UX. Values: none (silent auth — fail if not already logged in), login (force re-authentication even if a session exists), consent (force the consent screen even if previously approved). |
max_age | Optional | Maximum authentication age in seconds. If the user authenticated more than max_age seconds ago, they are prompted to re-authenticate. The resulting ID token includes an auth_time claim. |
login_hint | Optional | An email address to pre-fill on the login form. Useful when you already know the user's email (e.g., from a "Sign in as [email protected]" button). |
nonce | Optional | A random string for ID token replay protection. Included in the ID token's nonce claim. Your application must verify it matches the value you sent. |
Full example URL with all parameters:
https://uniauth.id/api/oauth/authorize?
response_type=code
&client_id=app_abc123
&redirect_uri=https://yourapp.com/callback
&scope=openid profile email
&state=random-csrf-token
&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
&code_challenge_method=S256
&prompt=consent
&max_age=3600
&[email protected]
&nonce=random-nonce-valueID Token Claims
ID tokens are signed with RS256 using a rotating keypair. You can verify them using the public key from the JWKS endpoint. Below is a fully decoded example:
// Header
{
"alg": "RS256",
"typ": "JWT",
"kid": "<jwk-thumbprint>" // dynamically derived from SHA-256 JWK thumbprint
}
// Payload
{
"sub": "2b7f16373f76cbdf9c0241ae59903ee2d143830d4edf74693b608fd2357cc219",
"aud": "app_abc123",
"iss": "https://uniauth.id",
"exp": 1709903600,
"iat": 1709900000,
"auth_time": 1709900000,
"amr": ["pwd", "mfa", "otp"],
"sid": "session-identifier-hash",
"at_hash": "access-token-hash",
"nonce": "random-nonce-value",
"name": "Jane Doe",
"given_name": "Jane",
"family_name": "Doe",
"picture": "https://uniauth.id/api/avatar/...",
"email": "[email protected]",
"email_verified": true,
"updated_at": 1709800000
}Claims by Scope
| Scope | Claims Included in ID Token |
|---|---|
openid | sub, iss, aud, exp, iat, auth_time, amr, sid, at_hash, nonce |
profile | name, given_name, family_name, preferred_username, nickname, picture, profile, website, gender, birthdate, locale, zoneinfo, updated_at |
email | email, email_verified |
phone | phone_number, phone_number_verified |
address | address (structured object with street_address, locality, region, postal_code, country) |
Note: The sub claim is always a pairwise identifier unique to your application, not the user's internal account ID. See the Pairwise Subject Identifiers section below.
Client Credentials Grant
Use the client credentials grant for machine-to-machine communication where no user context is needed. This is ideal for backend services, cron jobs, or microservice-to-microservice authentication.
POST https://uniauth.id/api/oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials
&client_id=YOUR_CLIENT_ID
&client_secret=YOUR_CLIENT_SECRET
&scope=openidResponse
{
"access_token": "eyJhbGciOiJIUzI1NiIs...",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "openid"
}Note: Client credentials grants do not return an ID token or refresh token, since there is no user context. The access token's sub claim contains the client ID rather than a user identifier.
Token Introspection
Introspect an access token or refresh token to check whether it is currently active and retrieve its associated metadata. This is useful for resource servers that need to validate tokens without decoding the JWT themselves.
POST https://uniauth.id/api/oauth/introspect
Content-Type: application/x-www-form-urlencoded
token=eyJhbGciOiJIUzI1NiIs...
&client_id=YOUR_CLIENT_ID
&client_secret=YOUR_CLIENT_SECRETActive Token Response
{
"active": true,
"sub": "2b7f16373f76cbdf9c0241ae59903ee2d143830d4edf74693b608fd2357cc219",
"scope": "openid profile email",
"client_id": "app_abc123",
"token_type": "Bearer",
"exp": 1709903600,
"iat": 1709900000,
"iss": "https://uniauth.id",
"jti": "token-unique-id"
}Inactive Token Response
{
"active": false
}A token is considered inactive if it has expired, been revoked, or was issued to a different client than the one making the introspection request.
End-Session / Logout
To log a user out of UniAuth and revoke their tokens, redirect them to the end-session endpoint. This follows the OpenID Connect RP-Initiated Logout specification.
GET https://uniauth.id/api/oauth/end-session?
id_token_hint=eyJhbGciOiJSUzI1NiIs...
&post_logout_redirect_uri=https://yourapp.com
&state=optional-state-value| Parameter | Description |
|---|---|
id_token_hint | The ID token previously issued to the client. Used to identify the session and the client. Recommended. |
post_logout_redirect_uri | Where to send the user after logout. Must be registered in the client's post_logout_redirect_uris list. |
state | An opaque value passed through to the post-logout redirect URI. Use this to maintain state across the logout flow. |
When the end-session endpoint is called, UniAuth performs the following steps:
- Validates the
id_token_hintand identifies the client and user - Revokes all active access tokens and refresh tokens for that client-user pair
- Invalidates the user's UniAuth session
- Triggers back-channel logout notifications to other registered clients (if configured)
- Redirects the user to the
post_logout_redirect_uriwith thestateparameter
Back-Channel Logout
When a user logs out, UniAuth can notify all other applications the user is signed into via back-channel logout. This follows the OpenID Connect Back-Channel Logout specification.
To enable back-channel logout, configure a backchannel_logout_uri for your OAuth application in the Developer Console. When a logout event occurs, UniAuth sends a signed JWT logout token to each registered client's back-channel URI via an HTTP POST request.
Logout Token
The logout token is a signed JWT (RS256) with the following claims:
{
"iss": "https://uniauth.id",
"sub": "2b7f16373f76cbdf9c0241ae59903ee2d143830d4edf74693b608fd2357cc219",
"aud": "app_abc123",
"iat": 1709900000,
"jti": "unique-logout-token-id",
"sid": "session-identifier-hash",
"events": {
"http://schemas.openid.net/event/backchannel-logout": {}
}
}Your application should verify the logout token signature using the JWKS endpoint, confirm the iss, aud, and events claims, then terminate the local session for the user identified by sub or sid. Respond with HTTP 200 to acknowledge receipt.
Pairwise Subject Identifiers
UniAuth uses pairwise subject identifiers as a core privacy feature. Each application receives a unique, deterministic identifier for each user. The same user gets a completely different sub value when signing into different applications.
This prevents cross-application user tracking — two apps cannot compare sub values to determine if the same person uses both services.
Example
The same user signing into two different applications receives different identifiers:
| Context | Identifier |
|---|---|
| User's profile (visible only to the user) | 550e8400-e29b-41d4-a716-446655440000 |
| sub received by App A | 2b7f16373f76cbdf9c0241ae59903ee2d143830d4edf74693b608fd2357cc219 |
| sub received by App B | 6a928c03d0e6cb00a35a31f769348d20a15e20eba64cdba9f088fac9917e18b7 |
The pairwise sub is stable for a given user-application pair — it never changes across sessions, token refreshes, or re-authorizations. Use it as the primary key to link UniAuth users to records in your own database.
Consent Screen
When a user authorizes your application for the first time, UniAuth displays a consent screen showing the scopes your application is requesting. The user can review the permissions and choose to approve or deny access.
- First authorization: The consent screen is always shown with the requested scopes.
- Subsequent authorizations: If the user has already approved the same set of scopes, consent is auto-approved and the user is redirected immediately.
- New scopes: If your application requests additional scopes that were not previously approved, the consent screen is shown again.
- First-party applications: Applications marked as first-party in the admin dashboard bypass the consent screen entirely.
- Force re-consent: Pass
prompt=consentin the authorization request to always show the consent screen, even if the user previously approved.
Common OAuth Errors
The following errors may be returned during the OAuth flow. Error responses follow RFC 6749 format with error and error_description fields.
| Error Code | Cause | Fix |
|---|---|---|
invalid_request | Missing or malformed required parameter | Check that all required parameters (response_type, client_id, redirect_uri, code_challenge) are present and correctly formatted. |
invalid_client | Unknown client_id or incorrect client_secret | Verify your client credentials. Check that the client has not been deactivated in the Developer Console. |
invalid_grant | Authorization code expired, already used, or code_verifier mismatch | Authorization codes are single-use and expire after 10 minutes. Ensure the code_verifier matches the code_challenge sent in the authorize request. |
invalid_scope | Requested scope is not recognized or not allowed | Use only supported scopes: openid, profile, email, phone, address. |
unauthorized_client | Client is not authorized for the requested grant type | Check that your application supports the grant type you are using (authorization_code, client_credentials, refresh_token). |
access_denied | User denied the consent request | Handle this gracefully in your callback. Show the user a message explaining they need to approve access to use your app. |
redirect_uri_mismatch | redirect_uri does not match any registered URI | The redirect_uri must exactly match (including trailing slashes and query parameters) one of the URIs registered in the Developer Console. |
login_required | Used prompt=none but no active session exists | The user is not logged in. Redirect them through the normal login flow without prompt=none. |
consent_required | Used prompt=none but consent has not been granted | The user has not previously approved these scopes. Show the consent screen by removing prompt=none. |