Skip to content

Advanced Flows

Overview

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.

API

interface ConsentNamespace {
getData(challengeId: string): Promise<ConsentScreenData>;
submit(challengeId: string, options: ConsentSubmitOptions): Promise<ConsentSubmitResult>;
}

Types

interface ConsentScreenData {
client: ConsentClientInfo;
scopes: ConsentScopeInfo[];
user?: ConsentUserInfo;
org?: ConsentOrgInfo;
actingAs?: ConsentActingAsInfo;
features?: ConsentFeatureFlags;
}
interface ConsentClientInfo {
clientId: string;
clientName: string;
clientUri?: string;
logoUri?: string;
policyUri?: string;
tosUri?: string;
}
interface ConsentScopeInfo {
name: string;
displayName: string;
description: string;
required: boolean;
}
interface ConsentSubmitOptions {
approved: boolean;
scopes?: string[]; // Approved scope subset
}
interface ConsentSubmitResult {
redirectUri: string;
}

SvelteKit Page Example

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, { approved });
window.location.href = result.redirectUri;
} 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.clientName} is requesting access</h1>
{#if consentData.client.logoUri}
<img src={consentData.client.logoUri} alt={consentData.client.clientName} />
{/if}
<h2>Requested permissions:</h2>
<ul>
{#each consentData.scopes as scope}
<li>
<strong>{scope.displayName}</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
clientInfo={consentData.client}
scopes={consentData.scopes}
userInfo={consentData.user}
loading={submitting}
on:approve={() => handleSubmit(true)}
on:deny={() => handleSubmit(false)}
/>
{/if}

Device Flow (RFC 8628)

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

API

interface DeviceFlowNamespace {
submit(userCode: string, approve?: boolean): Promise<DeviceFlowSubmitResult>;
}
interface DeviceFlowSubmitResult {
success: boolean;
message?: string;
}
// Error class for device flow specific errors
class DeviceFlowVerificationError extends Error {
code: string;
}

SvelteKit Page Example

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);
if (res.success) {
result = 'Device authorized successfully. You can close this page.';
} else {
error = res.message ?? 'Authorization failed';
}
} 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:submit={(e) => handleSubmit(e.detail.userCode)}
/>

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).

API

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 {
authReqId: string;
clientName: string;
clientId: string;
scope: string;
bindingMessage?: string;
requestedAt: string;
expiresAt: string;
}
interface CIBAActionResult {
success: boolean;
message?: string;
}

SvelteKit Page Example

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.authReqId, $user!.id, $user!.sub);
requests = requests.filter(r => r.authReqId !== req.authReqId);
}
async function reject(req: CIBAPendingRequest) {
await auth.ciba.reject(req.authReqId);
requests = requests.filter(r => r.authReqId !== req.authReqId);
}
</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.clientName}</h2>
<p>Scope: {req.scope}</p>
{#if req.bindingMessage}
<p>Message: {req.bindingMessage}</p>
{/if}
<p>Requested: {new Date(req.requestedAt).toLocaleString()}</p>
<div class="actions">
<button onclick={() => approve(req)}>Approve</button>
<button onclick={() => reject(req)}>Deny</button>
</div>
</div>
{/each}
{/if}

Login Challenge

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

API

interface LoginChallengeNamespace {
getData(challengeId: string): Promise<LoginChallengeData>;
}
interface LoginChallengeData {
challengeId: string;
client: LoginChallengeClientInfo;
loginHint?: string;
requestedScopes: string[];
}
interface LoginChallengeClientInfo {
clientId: string;
clientName: string;
logoUri?: string;
}

Usage

<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.clientName}</h1>
<!-- Render your login form here -->
{/if}

Next Steps