コンテンツにスキップ

高度なフロー

概要

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

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

Consentフロー

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

API

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

型定義

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[]; // 承認するスコープのサブセット
}
interface ConsentSubmitResult {
redirectUri: string;
}

SvelteKitページ例

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, { approved });
window.location.href = result.redirectUri;
} 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.clientName} がアクセスを要求しています</h1>
{#if consentData.client.logoUri}
<img src={consentData.client.logoUri} alt={consentData.client.clientName} />
{/if}
<h2>要求された権限:</h2>
<ul>
{#each consentData.scopes as scope}
<li>
<strong>{scope.displayName}</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
clientInfo={consentData.client}
scopes={consentData.scopes}
userInfo={consentData.user}
loading={submitting}
on:approve={() => handleSubmit(true)}
on:deny={() => handleSubmit(false)}
/>
{/if}

Device Flow(RFC 8628)

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

API

interface DeviceFlowNamespace {
submit(userCode: string, approve?: boolean): Promise<DeviceFlowSubmitResult>;
}
interface DeviceFlowSubmitResult {
success: boolean;
message?: string;
}
// Device Flow固有エラークラス
class DeviceFlowVerificationError extends Error {
code: string;
}

SvelteKitページ例

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 = 'デバイスが正常に認可されました。このページを閉じて構いません。';
} else {
error = 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:submit={(e) => handleSubmit(e.detail.userCode)}
/>

CIBA(Client Initiated Backchannel Authentication)

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

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ページ例

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

Login Challenge

Login Challenge APIは、Authrimが中間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;
}

使用例

<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.clientName} にサインイン</h1>
<!-- ここにログインフォームを表示 -->
{/if}

次のステップ