コンテンツにスキップ

高度なフロー

標準的な認証(Passkey、メールコード、ソーシャル)に加えて、SDKは4つの高度なOAuth/OIDCフローをサポートします:

フロー名前空間ユースケース
Consentauth.consentOAuth同意画面 — 要求スコープの表示、許可または拒否
Device Flowauth.deviceFlowRFC 8628 — 入力制約デバイス(スマートTV、CLI)
CIBAauth.cibaClient Initiated Backchannel Auth — 別デバイスからの承認
Login Challengeauth.loginChallengeサードパーティIdP連携のカスタムログイン画面

外部アプリケーションがOAuth経由でユーザーデータへのアクセスを要求すると、Authrimはユーザーを同意ページにリダイレクトします。SDKは同意データの取得とユーザーの決定を送信するAPIを提供します。

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 = 'チャレンジIDがありません';
loading = false;
return;
}
try {
consentData = await auth.consent.getData(challengeId);
} catch (e) {
error = e instanceof Error ? e.message : '同意データの読み込みに失敗しました';
}
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 : '同意の送信に失敗しました';
submitting = false;
}
}
</script>
{#if loading}
<p>同意データを読み込み中...</p>
{:else if error}
<p class="error">{error}</p>
{:else if consentData}
<div class="consent-page">
<h1>{consentData.client.client_name} がアクセスを要求しています</h1>
{#if consentData.client.logo_uri}
<img src={consentData.client.logo_uri} alt={consentData.client.client_name} />
{/if}
<h2>要求された権限:</h2>
<ul>
{#each consentData.scopes as scope}
<li>
<strong>{scope.title}</strong>
{#if scope.required}<span>(必須)</span>{/if}
<p>{scope.description}</p>
</li>
{/each}
</ul>
<div class="actions">
<button onclick={() => handleSubmit(true)} disabled={submitting}>
許可
</button>
<button onclick={() => handleSubmit(false)} disabled={submitting}>
拒否
</button>
</div>
</div>
{/if}

または ConsentTemplate を使用:

<script lang="ts">
import { ConsentTemplate } from '@authrim/sveltekit/ui/templates';
// ... 上記と同じセットアップ
</script>
{#if consentData}
<ConsentTemplate
client={consentData.client}
scopes={consentData.scopes}
user={consentData.user}
loading={submitting}
on:allow={() => handleSubmit(true)}
on:deny={() => handleSubmit(false)}
/>
{/if}

ブラウザのないデバイス(スマートTV、CLIツール)向け。ユーザーは別のデバイスでコードを入力します。

interface DeviceFlowNamespace {
submit(userCode: string, approve?: boolean): Promise<DeviceFlowSubmitResult>;
}
interface DeviceFlowSubmitResult {
message: string;
}
// Device Flow固有エラークラス
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 : 'デバイスフローエラー';
}
loading = false;
}
</script>
<h1>デバイスの認可</h1>
<p>デバイスに表示されているコードを入力してください:</p>
<form onsubmit={handleSubmit}>
<input
type="text"
bind:value={userCode}
placeholder="ABCD-1234"
maxlength="9"
required
/>
<button type="submit" disabled={loading}>
{loading ? '認可中...' : '認可'}
</button>
</form>
{#if result}<p class="success">{result}</p>{/if}
{#if error}<p class="error">{error}</p>{/if}

または 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により、クライアントアプリケーションがユーザーの代わりに認証を開始し、ユーザーは別のデバイス(例:モバイルプッシュ通知)で承認します。

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>保留中の認証リクエスト</h1>
{#if loading}
<p>読み込み中...</p>
{:else if requests.length === 0}
<p>保留中のリクエストはありません。</p>
{:else}
{#each requests as req}
<div class="request-card">
<h2>{req.client_name}</h2>
<p>スコープ: {req.scope}</p>
{#if req.binding_message}
<p>メッセージ: {req.binding_message}</p>
{/if}
<p>リクエスト日時: {new Date(req.created_at * 1000).toLocaleString()}</p>
<div class="actions">
<button onclick={() => approve(req)}>承認</button>
<button onclick={() => reject(req)}>拒否</button>
</div>
</div>
{/each}
{/if}

Login Challenge APIは、Authrimが中間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>{challengeData.client.client_name} にサインイン</h1>
<!-- ここにログインフォームを表示 -->
{/if}