A confidential client — your Laravel app holds a real client_secret. Uses Laravel's own Session facade for server-side state and Http for HTTP calls. Redirects to this
platform's real hosted login page — see the integration guides overview — which shows every currently-real first-factor option; you never build a login form.
Two different hosts are used below: accounts.onehux.com for the hosted login/logout pages a browser is redirected to, and api-accounts.onehux.com for the actual API
calls your backend makes server-to-server — mixing these up is the single most common
integration mistake, since the wrong host doesn't error loudly, it just 404s.
Prefer the SDK — it already implements everything below (PKCE, the redirect, callback handling, token exchange, automatic refresh-token rotation, `/userinfo`, RP-initiated logout, OIDC Back-Channel Logout, and the public application launcher), real and maintained, rather than hand-rolling the raw API calls this page walks through manually:
composer require onehux/sso packagist.org/packages/onehux/sso — the manual walkthrough below is still worth reading to understand what the SDK is doing on your behalf, and remains the reference if you're integrating in a language without an official SDK.
// routes/web.php
use Illuminate\Support\Facades\Route;
use Illuminate\Support\Facades\Session;
use Illuminate\Support\Str;
const ONEHUX_LOGIN_BASE_URL = 'https://your-org.onehux.com'; // see "Two different hosts" above
const ONEHUX_API_BASE_URL = 'https://api.your-domain.com'; // see "Two different hosts" above
const REDIRECT_URI = 'https://yourapp.example.com/auth/callback';
function base64url(string $data): string {
return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
}
Route::get('/auth/login', function () {
$codeVerifier = base64url(random_bytes(48));
$codeChallenge = base64url(hash('sha256', $codeVerifier, true));
$state = base64url(random_bytes(16));
// PKCE verifier + state live server-side in the session — never in a cookie the
// browser can read, never round-tripped through the client.
Session::put('onehux_pkce_verifier', $codeVerifier);
Session::put('onehux_oauth_state', $state);
$params = http_build_query([
'client_id' => config('services.onehux.client_id'),
'redirect_uri' => REDIRECT_URI,
'code_challenge' => $codeChallenge,
'code_challenge_method' => 'S256',
'scope' => 'openid profile email',
'state' => $state,
]);
return redirect(ONEHUX_LOGIN_BASE_URL . '/login?' . $params);
});use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;
Route::get('/auth/callback', function (Request $request) {
$code = $request->query('code');
$state = $request->query('state');
$expectedState = Session::pull('onehux_oauth_state');
$codeVerifier = Session::pull('onehux_pkce_verifier');
if (!$code || !$state || $state !== $expectedState) {
abort(400, 'Invalid or missing OAuth state.');
}
$tokenRes = Http::post(ONEHUX_API_BASE_URL . '/api/v1/oauth/token/', [
'grant_type' => 'authorization_code',
'code' => $code,
'redirect_uri' => REDIRECT_URI,
'client_id' => config('services.onehux.client_id'),
'client_secret' => config('services.onehux.client_secret'),
'code_verifier' => $codeVerifier,
]);
if ($tokenRes->failed()) {
abort(400, 'Sign-in failed.');
}
$tokens = $tokenRes->json(); // access_token, id_token, refresh_token, token_type, expires_in, scope
// Session establishment — the access token lives server-side only (Laravel's own
// session store), the same BFF discipline this platform's own dashboard follows on
// itself. The browser only ever gets Laravel's session cookie, never the raw token.
Session::put('onehux_access_token', $tokens['access_token']);
Session::put('onehux_refresh_token', $tokens['refresh_token']);
return redirect('/dashboard');
});A confidential client's refresh token rotates on every use and is itself long-lived
(30-day idle timeout, 30-day absolute cap — see Sessions),
so a 401 from /userinfo almost always means
just the 15-minute access token expired, not that the user needs to sign in again:
function refreshTokens(): ?string {
$refreshToken = Session::get('onehux_refresh_token');
if (!$refreshToken) {
return null;
}
$res = Http::post(ONEHUX_API_BASE_URL . '/api/v1/oauth/token/', [
'grant_type' => 'refresh_token',
'refresh_token' => $refreshToken,
'client_id' => config('services.onehux.client_id'),
'client_secret' => config('services.onehux.client_secret'),
]);
if ($res->failed()) {
return null;
}
$tokens = $res->json(); // access_token, id_token, refresh_token, token_type, expires_in, scope
Session::put('onehux_access_token', $tokens['access_token']);
Session::put('onehux_refresh_token', $tokens['refresh_token']);
return $tokens['access_token'];
}
Route::get('/auth/me', function () {
$accessToken = Session::get('onehux_access_token');
if (!$accessToken) {
return redirect('/auth/login');
}
$res = Http::withToken($accessToken)->get(ONEHUX_API_BASE_URL . '/api/v1/oauth/userinfo/');
if ($res->status() === 401) {
$accessToken = refreshTokens();
if (!$accessToken) {
Session::forget('onehux_access_token');
Session::forget('onehux_refresh_token');
return redirect('/auth/login');
}
$res = Http::withToken($accessToken)->get(ONEHUX_API_BASE_URL . '/api/v1/oauth/userinfo/');
}
if ($res->failed()) {
return redirect('/auth/login');
}
$claims = $res->json(); // sub, name, email, picture, roles, permissions, ...
return response()->json($claims);
});Redirect to end the platform-wide session, not just clear your own local one — every other Application relying on that same session is signed out too:
Route::get('/auth/logout', function () {
Session::forget('onehux_access_token');
Session::forget('onehux_refresh_token');
$params = http_build_query([
'client_id' => config('services.onehux.client_id'),
'post_logout_redirect_uri' => 'https://yourapp.example.com/logged-out',
]);
return redirect(ONEHUX_LOGIN_BASE_URL . '/end-session?' . $params);
}); Register this URL too: post_logout_redirect_uri is validated against
the exact same registered redirect_uris list as
your login callback — if https://yourapp.example.com/logged-out isn't in
that list too, /end-session rejects the request
with a real 400, even though your login callback
works fine.
The redirect above only notifies the app the user actually
clicked "log out" in. If they instead log out of a different app,
or directly at accounts.onehux.com, the
shared session is still revoked immediately and correctly — and if this Application
has registered a Back-Channel Logout endpoint, this platform pushes it a real,
spec-compliant logout_token (server-to-server,
HS256-signed with a dedicated backchannel signing secret — deliberately not your OAuth client_secret) as soon as the revocation
happens, rather than waiting for your app to notice on its own:
PATCH /api/v1/applications/{id}/backchannel-logout/
{ "backchannel_logout_uri": "https://yourapp.example.com/auth/backchannel-logout" }
-> { "backchannel_logout_uri": "...", "backchannel_logout_secret": "..." } (shown once) Receiving and verifying that push is its own integration surface — not covered by
this walkthrough. Without registering it, your app's own local session keeps
showing "signed in" until its next real /userinfo call fails, bounded by the access token's 15-minute lifetime. Don't treat a
locally-held session as a live signal of the IdP's true logout state unless you've
wired up the push.