UniAuth OAuth2 / OpenID Connect

Integrate "Sign in with UniAuth" into your application using standard OAuth2 and OpenID Connect protocols.

Quick Start

  1. Go to Account → Developer and register your app to get a client_id and client_secret
  2. Redirect users to the authorization endpoint with PKCE
  3. Exchange the authorization code for tokens at the token endpoint
  4. Use the access token to fetch user info

Endpoints

EndpointURL
Discoveryhttps://uniauth.id/.well-known/openid-configuration
Authorizationhttps://uniauth.id/api/oauth/authorize
Tokenhttps://uniauth.id/api/oauth/token
UserInfohttps://uniauth.id/api/oauth/userinfo
Revocationhttps://uniauth.id/api/oauth/revoke
Introspectionhttps://uniauth.id/api/oauth/introspect
End-Sessionhttps://uniauth.id/api/oauth/end-session
Pushed Authorization RequestsPOST https://uniauth.id/api/oauth/par
Dynamic Client RegistrationPOST https://uniauth.id/api/oauth/register
JWKShttps://uniauth.id/.well-known/jwks.json

Scopes

ScopeClaims
openidsub, iss, aud, exp, iat, auth_time
profilename, given_name, family_name, preferred_username, nickname, picture, profile, website, gender, birthdate, locale, zoneinfo, updated_at
emailemail, email_verified
phonephone_number, phone_number_verified
addressaddress (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.

ParameterTypeDescription
response_typeRequiredMust be code for the authorization code flow.
client_idRequiredYour application's client identifier.
redirect_uriRequiredMust exactly match one of your registered redirect URIs.
scopeRequiredSpace-delimited scopes. Must include openid for OIDC flows.
stateRecommendedAn opaque CSRF token. Returned unchanged in the callback. Generate a random value and verify it on return.
code_challengeRequiredBase64url-encoded SHA-256 hash of the code_verifier. PKCE is mandatory on all flows.
code_challenge_methodRequiredMust be S256. Plain challenges are not accepted.
promptOptionalControls 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_ageOptionalMaximum 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_hintOptionalAn 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).
nonceOptionalA 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-value

ID 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

ScopeClaims Included in ID Token
openidsub, iss, aud, exp, iat, auth_time, amr, sid, at_hash, nonce
profilename, given_name, family_name, preferred_username, nickname, picture, profile, website, gender, birthdate, locale, zoneinfo, updated_at
emailemail, email_verified
phonephone_number, phone_number_verified
addressaddress (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=openid

Response

{
  "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_SECRET

Active 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
ParameterDescription
id_token_hintThe ID token previously issued to the client. Used to identify the session and the client. Recommended.
post_logout_redirect_uriWhere to send the user after logout. Must be registered in the client's post_logout_redirect_uris list.
stateAn 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:

  1. Validates the id_token_hint and identifies the client and user
  2. Revokes all active access tokens and refresh tokens for that client-user pair
  3. Invalidates the user's UniAuth session
  4. Triggers back-channel logout notifications to other registered clients (if configured)
  5. Redirects the user to the post_logout_redirect_uri with the state parameter

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:

ContextIdentifier
User's profile (visible only to the user)550e8400-e29b-41d4-a716-446655440000
sub received by App A2b7f16373f76cbdf9c0241ae59903ee2d143830d4edf74693b608fd2357cc219
sub received by App B6a928c03d0e6cb00a35a31f769348d20a15e20eba64cdba9f088fac9917e18b7

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=consent in 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 CodeCauseFix
invalid_requestMissing or malformed required parameterCheck that all required parameters (response_type, client_id, redirect_uri, code_challenge) are present and correctly formatted.
invalid_clientUnknown client_id or incorrect client_secretVerify your client credentials. Check that the client has not been deactivated in the Developer Console.
invalid_grantAuthorization code expired, already used, or code_verifier mismatchAuthorization codes are single-use and expire after 10 minutes. Ensure the code_verifier matches the code_challenge sent in the authorize request.
invalid_scopeRequested scope is not recognized or not allowedUse only supported scopes: openid, profile, email, phone, address.
unauthorized_clientClient is not authorized for the requested grant typeCheck that your application supports the grant type you are using (authorization_code, client_credentials, refresh_token).
access_deniedUser denied the consent requestHandle this gracefully in your callback. Show the user a message explaining they need to approve access to use your app.
redirect_uri_mismatchredirect_uri does not match any registered URIThe redirect_uri must exactly match (including trailing slashes and query parameters) one of the URIs registered in the Developer Console.
login_requiredUsed prompt=none but no active session existsThe user is not logged in. Redirect them through the normal login flow without prompt=none.
consent_requiredUsed prompt=none but consent has not been grantedThe user has not previously approved these scopes. Show the consent screen by removing prompt=none.