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 curl and json PHP 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
Redirect URI: When registering your OAuth app, set the redirect URI to the full URL of your callback script (e.g., 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.php

Or 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'
);
ParameterRequiredDescription
clientIdYesYour OAuth application's client ID
clientSecretYesYour OAuth application's client secret
redirectUriYesThe URL of your callback script (must match the registered URI)
baseUrlNoUniAuth 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:

OptionDefaultDescription
scopeopenid profile emailSpace-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: The 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;
    }
}
Refresh token rotation: UniAuth rotates refresh tokens on every use. When you exchange a refresh token, you receive a new one. Always update the stored refresh token with the new value. If a refresh token is used twice (indicating potential theft), UniAuth revokes the entire token family for security.

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/callback

Create 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>
@endsection

Security 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 state returned 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 sub claim 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