PHP Quickstart
Add "Sign in with UniAuth" to your PHP application using the official single-file SDK. This guide covers everything from basic login/logout to Laravel integration and account linking.
Prerequisites
- PHP 8.0 or later
- The
curlandjsonPHP extensions (enabled by default on most installations) - A registered UniAuth OAuth application (create one in the Developer Console)
- Your application's client ID and client secret
https://yourapp.com/callback.php). For local development, use http://localhost:8000/callback.php.Download the SDK
The PHP SDK is a single file with zero external dependencies. Download it directly into your project:
curl -O https://uniauth.id/sdk/UniAuth.phpOr download it from the SDKs page. Place the file in your project directory and include it where needed.
Configuration
Create a UniAuth instance with your OAuth credentials:
<?php
require_once __DIR__ . '/UniAuth.php';
$auth = new UniAuth(
clientId: 'your-client-id',
clientSecret: 'your-client-secret',
redirectUri: 'https://yourapp.com/callback.php',
// Optional: defaults to https://uniauth.id
// baseUrl: 'https://your-uniauth-instance.com'
);| Parameter | Required | Description |
|---|---|---|
clientId | Yes | Your OAuth application's client ID |
clientSecret | Yes | Your OAuth application's client secret |
redirectUri | Yes | The URL of your callback script (must match the registered URI) |
baseUrl | No | UniAuth server URL. Defaults to https://uniauth.id |
For production, store your credentials outside of source control. Use environment variables or a configuration file excluded from version control:
<?php
// config.php — add to .gitignore
return [
'client_id' => 'your-client-id',
'client_secret' => 'your-client-secret',
'redirect_uri' => 'https://yourapp.com/callback.php',
];
// Usage:
$config = require __DIR__ . '/config.php';
$auth = new UniAuth(
clientId: $config['client_id'],
clientSecret: $config['client_secret'],
redirectUri: $config['redirect_uri'],
);Login Page (login.php)
Create a login script that generates a PKCE challenge, stores the verifier in the PHP session, and redirects the user to UniAuth:
<?php
// login.php
session_start();
require_once __DIR__ . '/UniAuth.php';
$auth = new UniAuth(
clientId: 'your-client-id',
clientSecret: 'your-client-secret',
redirectUri: 'https://yourapp.com/callback.php'
);
// getLoginUrl() handles:
// 1. Generates a cryptographic PKCE code verifier and challenge
// 2. Generates a CSRF state token
// 3. Stores both in $_SESSION
// 4. Returns the full authorization URL
$loginUrl = $auth->getLoginUrl([
'scope' => 'openid profile email',
]);
header('Location: ' . $loginUrl);
exit;The getLoginUrl() method accepts an optional array with the following keys:
| Option | Default | Description |
|---|---|---|
scope | openid profile email | Space-separated OAuth scopes to request |
prompt | (none) | Set to consent to force the consent screen |
login_hint | (none) | Pre-fill the email field on the login form |
Callback Page (callback.php)
After the user signs in at UniAuth, they are redirected back to your callback URL with an authorization code. The SDK exchanges this code for tokens and fetches the user profile:
<?php
// callback.php
session_start();
require_once __DIR__ . '/UniAuth.php';
$auth = new UniAuth(
clientId: 'your-client-id',
clientSecret: 'your-client-secret',
redirectUri: 'https://yourapp.com/callback.php'
);
try {
// handleCallback() performs:
// 1. Verifies the CSRF state parameter
// 2. Exchanges the authorization code + PKCE verifier for tokens
// 3. Fetches the user profile from the UserInfo endpoint
// 4. Returns user data with tokens
$user = $auth->handleCallback($_GET['code']);
// The returned array contains OIDC standard claims:
// [
// 'sub' => 'app-specific-user-id',
// 'email' => '[email protected]',
// 'email_verified' => true,
// 'name' => 'Jane Doe',
// 'given_name' => 'Jane',
// 'family_name' => 'Doe',
// 'picture' => 'https://uniauth.id/api/avatar/...',
// '_tokens' => [
// 'access_token' => 'eyJ...',
// 'refresh_token' => '...',
// 'id_token' => 'eyJ...',
// 'expires_in' => 3600,
// ]
// ]
// Store user data in your session
$_SESSION['user'] = $user;
$_SESSION['access_token'] = $user['_tokens']['access_token'];
$_SESSION['id_token'] = $user['_tokens']['id_token'];
$_SESSION['refresh_token'] = $user['_tokens']['refresh_token'] ?? null;
header('Location: /dashboard.php');
exit;
} catch (Exception $e) {
// Handle errors: invalid code, CSRF mismatch, network failure, etc.
http_response_code(400);
echo 'Login failed: ' . htmlspecialchars($e->getMessage());
}Get User Info
After authentication, you can display user information from the session or fetch fresh data from UniAuth's UserInfo endpoint:
<?php
// dashboard.php
session_start();
// Check if user is authenticated
if (!isset($_SESSION['user'])) {
header('Location: /login.php');
exit;
}
$user = $_SESSION['user'];
?>
<!DOCTYPE html>
<html>
<head><title>Dashboard</title></head>
<body>
<h1>Welcome, <?= htmlspecialchars($user['name'] ?? 'User') ?></h1>
<?php if (!empty($user['picture'])): ?>
<img src="<?= htmlspecialchars($user['picture']) ?>" alt="Avatar"
style="width: 64px; height: 64px; border-radius: 50%;">
<?php endif; ?>
<p>Email: <?= htmlspecialchars($user['email'] ?? 'N/A') ?></p>
<p>User ID: <?= htmlspecialchars($user['sub']) ?></p>
<a href="/logout.php">Sign out</a>
</body>
</html>To fetch a fresh user profile (e.g., to check for updated information), use the access token with the UserInfo endpoint:
<?php
function getUserInfo(string $accessToken): ?array {
$ch = curl_init('https://uniauth.id/api/oauth/userinfo');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $accessToken,
'Accept: application/json',
],
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode !== 200) {
return null; // Token may be expired — refresh it
}
return json_decode($response, true);
}
// Usage:
$freshUser = getUserInfo($_SESSION['access_token']);Logout
To sign the user out, destroy the PHP session and redirect to UniAuth's end-session endpoint. This ensures the user is logged out of both your application and UniAuth:
<?php
// logout.php
session_start();
// Save the ID token before destroying the session
$idToken = $_SESSION['id_token'] ?? null;
// Destroy the local session
$_SESSION = [];
session_destroy();
// Redirect to UniAuth end-session endpoint
if ($idToken) {
$logoutUrl = 'https://uniauth.id/api/oauth/end-session?' . http_build_query([
'id_token_hint' => $idToken,
'post_logout_redirect_uri' => 'https://yourapp.com',
]);
header('Location: ' . $logoutUrl);
exit;
}
// Fallback: redirect to home
header('Location: /');
exit;id_token_hint parameter lets UniAuth identify which session to terminate. The post_logout_redirect_uri must be a registered redirect URI for your OAuth application.Token Refresh
Access tokens expire after 1 hour. Use the refresh token to obtain a new access token without requiring the user to sign in again:
<?php
function refreshAccessToken(string $refreshToken, string $clientId, string $clientSecret): ?array {
$ch = curl_init('https://uniauth.id/api/oauth/token');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Content-Type: application/x-www-form-urlencoded'],
CURLOPT_POSTFIELDS => http_build_query([
'grant_type' => 'refresh_token',
'refresh_token' => $refreshToken,
'client_id' => $clientId,
'client_secret' => $clientSecret,
]),
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode !== 200) {
return null; // Refresh token may be expired or revoked
}
return json_decode($response, true);
}
// Usage:
session_start();
if (isset($_SESSION['refresh_token'])) {
$tokens = refreshAccessToken(
$_SESSION['refresh_token'],
'your-client-id',
'your-client-secret'
);
if ($tokens) {
$_SESSION['access_token'] = $tokens['access_token'];
// UniAuth rotates refresh tokens — always update the stored token
if (isset($tokens['refresh_token'])) {
$_SESSION['refresh_token'] = $tokens['refresh_token'];
}
} else {
// Refresh failed — redirect to login
header('Location: /login.php');
exit;
}
}Account Linking
To link UniAuth identities with your existing user database, store the sub claim from the user profile. This is a unique, app-specific identifier that remains stable across sessions. Each application receives a different sub for the same user, ensuring privacy across services.
MySQL Example
-- Add a column to your users table for the UniAuth identifier
ALTER TABLE users ADD COLUMN uniauth_id VARCHAR(128) UNIQUE;
-- Create an index for fast lookups
CREATE INDEX idx_users_uniauth_id ON users(uniauth_id);PostgreSQL Example
-- Add a column with a unique constraint
ALTER TABLE users ADD COLUMN uniauth_id VARCHAR(128) UNIQUE;PHP Account Linking Logic
<?php
// After successful callback
$user = $auth->handleCallback($_GET['code']);
$uniAuthSub = $user['sub'];
// Look up existing user by UniAuth identifier
$stmt = $pdo->prepare('SELECT * FROM users WHERE uniauth_id = ?');
$stmt->execute([$uniAuthSub]);
$localUser = $stmt->fetch(PDO::FETCH_ASSOC);
if ($localUser) {
// Existing user — update profile if needed and log in
$stmt = $pdo->prepare(
'UPDATE users SET name = ?, email = ?, last_login = NOW() WHERE id = ?'
);
$stmt->execute([$user['name'], $user['email'], $localUser['id']]);
$_SESSION['user_id'] = $localUser['id'];
} else {
// Check if email already exists (link existing account)
$stmt = $pdo->prepare('SELECT * FROM users WHERE email = ?');
$stmt->execute([$user['email']]);
$emailUser = $stmt->fetch(PDO::FETCH_ASSOC);
if ($emailUser) {
// Link UniAuth to existing account
$stmt = $pdo->prepare('UPDATE users SET uniauth_id = ? WHERE id = ?');
$stmt->execute([$uniAuthSub, $emailUser['id']]);
$_SESSION['user_id'] = $emailUser['id'];
} else {
// Create new user
$stmt = $pdo->prepare(
'INSERT INTO users (name, email, uniauth_id, created_at) VALUES (?, ?, ?, NOW())'
);
$stmt->execute([$user['name'], $user['email'], $uniAuthSub]);
$_SESSION['user_id'] = $pdo->lastInsertId();
}
}
header('Location: /dashboard.php');
exit;Laravel Integration
For Laravel applications, you can integrate UniAuth using middleware, a controller, and routes. Here is a complete setup:
Configuration
Add UniAuth credentials to your .env file:
UNIAUTH_DOMAIN=https://uniauth.id
UNIAUTH_CLIENT_ID=your-client-id
UNIAUTH_CLIENT_SECRET=your-client-secret
UNIAUTH_REDIRECT_URI=https://yourapp.com/auth/callbackCreate a config file at config/uniauth.php:
<?php
// config/uniauth.php
return [
'domain' => env('UNIAUTH_DOMAIN', 'https://uniauth.id'),
'client_id' => env('UNIAUTH_CLIENT_ID'),
'client_secret' => env('UNIAUTH_CLIENT_SECRET'),
'redirect_uri' => env('UNIAUTH_REDIRECT_URI'),
];Controller
<?php
// app/Http/Controllers/AuthController.php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str;
class AuthController extends Controller
{
public function login(Request $request)
{
// Generate PKCE
$codeVerifier = Str::random(64);
$codeChallenge = rtrim(strtr(
base64_encode(hash('sha256', $codeVerifier, true)),
'+/', '-_'
), '=');
$state = Str::random(32);
// Store in session
$request->session()->put('uniauth_code_verifier', $codeVerifier);
$request->session()->put('uniauth_state', $state);
$params = http_build_query([
'response_type' => 'code',
'client_id' => config('uniauth.client_id'),
'redirect_uri' => config('uniauth.redirect_uri'),
'scope' => 'openid profile email',
'state' => $state,
'code_challenge' => $codeChallenge,
'code_challenge_method' => 'S256',
]);
return redirect(config('uniauth.domain') . '/api/oauth/authorize?' . $params);
}
public function callback(Request $request)
{
// Verify state
$savedState = $request->session()->pull('uniauth_state');
if ($request->get('state') !== $savedState) {
abort(403, 'Invalid state parameter');
}
$codeVerifier = $request->session()->pull('uniauth_code_verifier');
// Exchange code for tokens
$tokenResponse = Http::asForm()->post(
config('uniauth.domain') . '/api/oauth/token',
[
'grant_type' => 'authorization_code',
'code' => $request->get('code'),
'redirect_uri' => config('uniauth.redirect_uri'),
'client_id' => config('uniauth.client_id'),
'client_secret' => config('uniauth.client_secret'),
'code_verifier' => $codeVerifier,
]
);
if (!$tokenResponse->successful()) {
abort(401, 'Token exchange failed');
}
$tokens = $tokenResponse->json();
// Fetch user info
$userResponse = Http::withToken($tokens['access_token'])
->get(config('uniauth.domain') . '/api/oauth/userinfo');
if (!$userResponse->successful()) {
abort(401, 'Failed to fetch user info');
}
$uniAuthUser = $userResponse->json();
// Find or create local user
$user = \App\Models\User::updateOrCreate(
['uniauth_id' => $uniAuthUser['sub']],
[
'name' => $uniAuthUser['name'] ?? '',
'email' => $uniAuthUser['email'] ?? '',
]
);
// Store tokens in session
$request->session()->put('uniauth_tokens', $tokens);
// Log the user in
auth()->login($user);
return redirect()->intended('/dashboard');
}
public function logout(Request $request)
{
$idToken = $request->session()->get('uniauth_tokens.id_token');
auth()->logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
if ($idToken) {
$logoutUrl = config('uniauth.domain') . '/api/oauth/end-session?'
. http_build_query([
'id_token_hint' => $idToken,
'post_logout_redirect_uri' => url('/'),
]);
return redirect($logoutUrl);
}
return redirect('/');
}
}Routes
<?php
// routes/web.php
use App\Http\Controllers\AuthController;
Route::get('/auth/login', [AuthController::class, 'login'])->name('login');
Route::get('/auth/callback', [AuthController::class, 'callback']);
Route::post('/auth/logout', [AuthController::class, 'logout'])->name('logout');
Route::middleware('auth')->group(function () {
Route::get('/dashboard', function () {
return view('dashboard');
});
});User Migration
Add the uniauth_id column to your users table:
<?php
// database/migrations/xxxx_add_uniauth_id_to_users.php
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->string('uniauth_id', 128)->nullable()->unique();
});
}Blade Template
{{-- resources/views/dashboard.blade.php --}}
@extends('layouts.app')
@section('content')
<div class="container mx-auto py-12">
<h1 class="text-3xl font-bold">Dashboard</h1>
<p class="mt-4">Welcome, {{ auth()->user()->name }}!</p>
<p class="text-gray-600">{{ auth()->user()->email }}</p>
<form method="POST" action="{{ route('logout') }}" class="mt-6">
@csrf
<button type="submit" class="px-4 py-2 bg-red-600 text-white rounded-lg">
Sign out
</button>
</form>
</div>
@endsectionSecurity Best Practices
- Always use HTTPS in production. OAuth tokens and authorization codes are transmitted via URL parameters and HTTP headers. Without HTTPS, they can be intercepted. Configure your web server with a valid TLS certificate.
- Validate the state parameter. The SDK handles this automatically, but if you implement the flow manually, always verify that the
statereturned by UniAuth matches the one you stored in the session. This prevents CSRF attacks. - Store tokens server-side only. Never expose access tokens, refresh tokens, or ID tokens in HTML, JavaScript, or cookies accessible to client-side code. Keep them in the PHP session, which is stored on the server.
- Use prepared statements for account linking. When querying your database with the
subclaim or email, always use parameterized queries to prevent SQL injection:// Good — parameterized query $stmt = $pdo->prepare('SELECT * FROM users WHERE uniauth_id = ?'); $stmt->execute([$uniAuthSub]); // Bad — vulnerable to SQL injection $pdo->query("SELECT * FROM users WHERE uniauth_id = '$uniAuthSub'"); - Keep your client secret confidential. Never commit it to version control, expose it in client-side code, or log it. Use environment variables or a secrets management service.
- Handle refresh token rotation. Always replace the stored refresh token with the new one returned by the token endpoint. Using a stale refresh token will trigger UniAuth's replay detection and revoke all tokens in the family.
- Set secure session configuration. Configure PHP sessions for security:
; php.ini recommended settings session.cookie_httponly = 1 session.cookie_secure = 1 session.cookie_samesite = Lax session.use_strict_mode = 1
Next Steps
- Review the SDK reference for the complete PHP SDK API
- Learn about OAuth scopes and pairwise identifiers
- Configure multi-factor authentication for your users
- Set up webhooks to sync user events to your PHP backend
- Explore the API reference for direct endpoint integration