Skip to content

Advanced Flows

Beyond standard authentication (passkey, email code, social), the SDK supports four advanced OAuth/OIDC flows:

FlowNamespaceUse Case
Consentauth.consentOAuth consent screen — display requested scopes, approve or deny
Device Flowauth.deviceFlowRFC 8628 — input-constrained devices (smart TV, CLI)
CIBAauth.cibaClient Initiated Backchannel Auth — approve from another device
Login Challengeauth.loginChallengeCustom login screen for third-party IdP integration

When an external application requests access to user data via OAuth, Authrim redirects the user to a consent page. The SDK provides the API to fetch consent data and submit the user’s decision.

interface ConsentNamespace {
getData(challengeId: string): Promise<ConsentScreenData>;
submit(challengeId: string, options: ConsentSubmitOptions): Promise<ConsentSubmitResult>;
}
interface ConsentScreenData {
challenge_id: string;
client: ConsentClientInfo;
scopes: ConsentScopeInfo[];
user: ConsentUserInfo;
organizations: ConsentOrgInfo[];
primary_org: ConsentOrgInfo | null;
roles: string[];
acting_as: ConsentActingAsInfo | null;
target_org_id: string | null;
features: ConsentFeatureFlags;
}
interface ConsentClientInfo {
client_id: string;
client_name: string;
client_uri?: string;
logo_uri?: string;
policy_uri?: string;
tos_uri?: string;
is_trusted?: boolean;
}
interface ConsentScopeInfo {
name: string;
title: string;
description: string;
required: boolean;
}
interface ConsentSubmitOptions {
approve: boolean;
selectedOrgId?: string | null;
actingAsUserId?: string;
}
interface ConsentSubmitResult {
redirect_url: string;
}
src/routes/consent/+page.svelte
<script lang="ts">
import { onMount } from 'svelte';
import { page } from '$app/stores';
import { getAuthContext } from '@authrim/sveltekit';
import type { ConsentScreenData } from '@authrim/sveltekit';
const auth = getAuthContext();
let consentData: ConsentScreenData | null = $state(null);
let loading = $state(true);
let submitting = $state(false);
let error = $state('');
const challengeId = $derived($page.url.searchParams.get('challenge') ?? '');
onMount(async () => {
if (!challengeId) {
error = 'Missing challenge ID';
loading = false;
return;
}
try {
consentData = await auth.consent.getData(challengeId);
} catch (e) {
error = e instanceof Error ? e.message : 'Failed to load consent data';
}
loading = false;
});
async function handleSubmit(approved: boolean) {
submitting = true;
try {
const result = await auth.consent.submit(challengeId, { approve: approved });
window.location.href = result.redirect_url;
} catch (e) {
error = e instanceof Error ? e.message : 'Consent submission failed';
submitting = false;
}
}
</script>
{#if loading}
<p>Loading consent data...</p>
{:else if error}
<p class="error">{error}</p>
{:else if consentData}
<div class="consent-page">
<h1>{consentData.client.client_name} is requesting access</h1>
{#if consentData.client.logo_uri}
<img src={consentData.client.logo_uri} alt={consentData.client.client_name} />
{/if}
<h2>Requested permissions:</h2>
<ul>
{#each consentData.scopes as scope}
<li>
<strong>{scope.title}</strong>
{#if scope.required}<span>(required)</span>{/if}
<p>{scope.description}</p>
</li>
{/each}
</ul>
<div class="actions">
<button onclick={() => handleSubmit(true)} disabled={submitting}>
Allow
</button>
<button onclick={() => handleSubmit(false)} disabled={submitting}>
Deny
</button>
</div>
</div>
{/if}

Or use the ConsentTemplate:

<script lang="ts">
import { ConsentTemplate } from '@authrim/sveltekit/ui/templates';
// ... same setup as above
</script>
{#if consentData}
<ConsentTemplate
client={consentData.client}
scopes={consentData.scopes}
user={consentData.user}
loading={submitting}
on:allow={() => handleSubmit(true)}
on:deny={() => handleSubmit(false)}
/>
{/if}

For devices without a browser (smart TV, CLI tools), the user enters a code on another device.

interface DeviceFlowNamespace {
submit(userCode: string, approve?: boolean): Promise<DeviceFlowSubmitResult>;
}
interface DeviceFlowSubmitResult {
message: string;
}
// Error class for device flow specific errors
class DeviceFlowVerificationError extends Error {
errorCode: string;
}
src/routes/device/+page.svelte
<script lang="ts">
import { getAuthContext } from '@authrim/sveltekit';
const auth = getAuthContext();
let userCode = $state('');
let loading = $state(false);
let result = $state<string | null>(null);
let error = $state('');
async function handleSubmit() {
loading = true;
error = '';
result = null;
try {
const res = await auth.deviceFlow.submit(userCode, true);
result = res.message;
} catch (e) {
error = e instanceof Error ? e.message : 'Device flow error';
}
loading = false;
}
</script>
<h1>Authorize Device</h1>
<p>Enter the code shown on your device:</p>
<form onsubmit={handleSubmit}>
<input
type="text"
bind:value={userCode}
placeholder="ABCD-1234"
maxlength="9"
required
/>
<button type="submit" disabled={loading}>
{loading ? 'Authorizing...' : 'Authorize'}
</button>
</form>
{#if result}<p class="success">{result}</p>{/if}
{#if error}<p class="error">{error}</p>{/if}

Or use DeviceFlowTemplate:

<DeviceFlowTemplate
{loading}
{error}
on:approve={(e) => handleSubmit(e.detail.userCode)}
on:deny={(e) => auth.deviceFlow.submit(e.detail.userCode, false)}
/>

CIBA (Client Initiated Backchannel Authentication)

Section titled “CIBA (Client Initiated Backchannel Authentication)”

CIBA allows a client application to initiate authentication on behalf of a user, who then approves on a separate device (e.g., mobile push notification).

interface CIBANamespace {
getData(loginHint: string): Promise<CIBAPendingRequest[]>;
approve(authReqId: string, userId: string, sub: string): Promise<CIBAActionResult>;
reject(authReqId: string, reason?: string): Promise<CIBAActionResult>;
}
interface CIBAPendingRequest {
auth_req_id: string;
client_name: string;
client_id: string;
client_logo_uri: string | null;
scope: string;
binding_message?: string;
user_code?: string;
created_at: number;
expires_at: number;
}
interface CIBAActionResult {
success: boolean;
message?: string;
}
src/routes/ciba/+page.svelte
<script lang="ts">
import { onMount } from 'svelte';
import { getAuthContext } from '@authrim/sveltekit';
import type { CIBAPendingRequest } from '@authrim/sveltekit';
const auth = getAuthContext();
const { user } = auth.stores;
let requests: CIBAPendingRequest[] = $state([]);
let loading = $state(true);
onMount(async () => {
if ($user?.email) {
requests = await auth.ciba.getData($user.email);
}
loading = false;
});
async function approve(req: CIBAPendingRequest) {
await auth.ciba.approve(req.auth_req_id, $user!.id, String($user!.sub ?? $user!.id));
requests = requests.filter(r => r.auth_req_id !== req.auth_req_id);
}
async function reject(req: CIBAPendingRequest) {
await auth.ciba.reject(req.auth_req_id);
requests = requests.filter(r => r.auth_req_id !== req.auth_req_id);
}
</script>
<h1>Pending Authentication Requests</h1>
{#if loading}
<p>Loading...</p>
{:else if requests.length === 0}
<p>No pending requests.</p>
{:else}
{#each requests as req}
<div class="request-card">
<h2>{req.client_name}</h2>
<p>Scope: {req.scope}</p>
{#if req.binding_message}
<p>Message: {req.binding_message}</p>
{/if}
<p>Requested: {new Date(req.created_at * 1000).toLocaleString()}</p>
<div class="actions">
<button onclick={() => approve(req)}>Approve</button>
<button onclick={() => reject(req)}>Deny</button>
</div>
</div>
{/each}
{/if}

The Login Challenge API provides data for custom login screens when Authrim acts as an intermediary IdP.

interface LoginChallengeNamespace {
getData(challengeId: string): Promise<LoginChallengeData>;
}
interface LoginChallengeData {
challenge_id: string;
client: LoginChallengeClientInfo;
login_hint?: string;
requested_scopes: string[];
prompt?: string;
max_age?: number;
acr_values?: string[];
}
interface LoginChallengeClientInfo {
client_id: string;
client_name: string;
logo_uri?: string;
client_uri?: string;
policy_uri?: string;
tos_uri?: string;
redirect_uris: string[];
scope: string;
response_type: string;
}
<script lang="ts">
import { onMount } from 'svelte';
import { page } from '$app/stores';
import { getAuthContext } from '@authrim/sveltekit';
const auth = getAuthContext();
let challengeData = $state(null);
const challengeId = $derived($page.url.searchParams.get('challenge') ?? '');
onMount(async () => {
if (challengeId) {
challengeData = await auth.loginChallenge.getData(challengeId);
}
});
</script>
{#if challengeData}
<h1>Sign in to {challengeData.client.client_name}</h1>
<!-- Render your login form here -->
{/if}