KLON IdP と連携するための TypeScript SDK。oauth4webapi をベースに、KLON 固有のパラメータ (ACR, Authorization Details, Scope) をサポートする。
@pocketsign スコープのパッケージは専用レジストリで配布しています。プロジェクトのルートに .npmrc を作成し、レジストリと SDK 取得用トークンを設定してください。
@pocketsign:registry=https://repo.platform.p8n.app
//repo.platform.p8n.app/:_authToken=<YOUR_SDK_TOKEN>
<YOUR_SDK_TOKEN> には SDK 取得用トークンを設定します(取得方法は SDK 取得用トークンの作成 を参照)。トークンが VCS に記録されないよう注意してください。
設定後、パッケージをインストールします。
pnpm add @pocketsign/klon-sdk oauth4webapi
oauth4webapi は peerDependency のため、別途インストールが必要です。
import { createClient, Scopes, Resources, AcrValues, Prompts } from "@pocketsign/klon-sdk";
const client = createClient({
issuer: "https://id.mock.klon.you",
clientId: "your-client-id",
clientSecret: "your-client-secret",
redirectUri: "https://your-app.com/callback",
});
// 1. 認可 URL を生成
const { url, session } = await client.createAuthorizationURL({
scopes: [Scopes.OPENID, Scopes.PROFILE],
authorizationDetails: [
{ identifiers: [Resources.MERGED_FULL_NAME], actions: ["read"], required: true },
],
acrValues: [AcrValues.HIGH],
prompt: [Prompts.CONSENT],
usePAR: true,
});
// session をセッションストレージに保存してからリダイレクト
// redirect(url.toString())
// 2. コールバックでトークン交換
const tokenSet = await client.exchangeCode(code, state, session);
// 3. トークンリフレッシュ
const newTokenSet = await client.refreshToken(tokenSet.refreshToken);
oauth4webapi は内部で crypto.subtle を使用するが、React Native にはネイティブ実装がない。react-native-quick-crypto で polyfill する必要がある。
// polyfills.ts (アプリのエントリポイントより前に import する)
import QuickCrypto, { install } from "react-native-quick-crypto";
install();
// oauth4webapi は内部で `key instanceof CryptoKey` を使うが、install() は
// globalThis.crypto しか設定しないため globalThis.CryptoKey が未定義のまま残る。
// Hermes で "Property CryptoKey does not exist" になるため手動で登録する。
(globalThis as { CryptoKey?: unknown }).CryptoKey = QuickCrypto.CryptoKey;
import { createClient, Scopes, type DPoPKeyStore } from "@pocketsign/klon-sdk";
import { fetch as expoFetch } from "expo/fetch";
// expo/fetch は bodyUsed フラグの扱いが異なるためラッパーが必要
const customFetch: typeof globalThis.fetch = async (input, init) => {
const res = await expoFetch(input, init);
const body = await res.arrayBuffer();
return new Response(body, {
status: res.status,
statusText: res.statusText,
headers: res.headers,
});
};
// DPoP 鍵ペアの永続化実装 (サンプルは AsyncStorage、本番は expo-secure-store 等に差し替え)
const keyStore: DPoPKeyStore = {
/* load / save / clear を実装する(後述の「DPoP」セクション参照) */
};
const client = createClient({
issuer: "https://id.mock.klon.you",
clientId: "your-client-id",
redirectUri: "your-app-scheme://callback",
customFetch,
dpop: { keyStore }, // DPoP (RFC 9449) を有効化
});
const { url, session } = await client.createAuthorizationURL({
scopes: [Scopes.OPENID, Scopes.OFFLINE_ACCESS],
usePAR: true,
});
createClient(config: ClientConfig): OIDCClientinterface ClientConfig {
issuer: string;
clientId: string;
clientSecret?: string; // Confidential Client の場合
redirectUri: string;
customFetch?: typeof globalThis.fetch; // React Native では expo/fetch ラッパーを渡す
dpop?: DPoPOptions; // DPoP (RFC 9449) を有効化
allowInsecureRequests?: boolean; // 開発時のみ: HTTP エンドポイントを許可
}
client.createAuthorizationURL(options?): Promise<{ url: URL; session: AuthorizationSession }>認可 URL を生成する。返された session はコールバック処理に必要なので保存すること。
interface AuthorizeOptions {
scopes?: readonly string[]; // 省略時のみ ["openid"](空配列を渡すとスコープ無し)
authorizationDetails?: AuthorizationDetailInput[]; // RAR (RFC 9396)
acrValues?: string[];
prompt?: string[];
maxAge?: number; // 認証の最大経過時間 (秒)
grantManagementAction?: GrantManagementAction;
usePAR?: boolean; // PAR (RFC 9126)
}
client.exchangeCode(code, state, session): Promise<TokenSet>認可コードをトークンに交換する。code と state はコールバック URL のクエリパラメータ。PKCE 検証、nonce 検証、ID トークン検証を行う。
const tokenSet = await client.exchangeCode(code, state, session);
// デコード済み ID Token クレームにアクセス
console.log(tokenSet.idTokenClaims?.sub); // RP 固有のユーザー識別子
console.log(tokenSet.idTokenClaims?.acr); // 認証コンテキストクラス
console.log(tokenSet.idTokenClaims?.jpki_verified); // JPKI 紐づけ状態
client.refreshToken(refreshToken): Promise<TokenSet>リフレッシュトークンでアクセストークンを更新する。レスポンスに新しい ID Token が含まれる場合は署名・issuer・audience・expiry を検証し、成功した場合のみ idTokenClaims もセットされる。
client.bindNativeSession(bindId, accessToken): Promise<BindNativeSessionResult>ネイティブアプリの WebView セッションをバインドする (KLON 固有の保護リソース)。DPoP が設定されていれば proof を自動付与する。
const { bindCompleteUrl } = await client.bindNativeSession(bindId, accessToken);
// bindCompleteUrl を WebView にロードしてバインド完了させる
createDPoPFetch(options): DPoPFetchRegistry API や IdP の protected API など、KLON access token が必要な API 呼び出しに DPoP proof を自動付与する fetch wrapper を作成する。
import { createDPoPFetch } from "@pocketsign/klon-sdk";
const dpopFetch = createDPoPFetch({
keyStore,
getAccessToken: async () => tokenSet.accessToken,
fetch: customFetch, // 省略時は globalThis.fetch。React Native / Expo では expo/fetch 等を渡す
});
const response = await dpopFetch("https://registry.mock.klon.you/api/...", {
method: "POST",
body: JSON.stringify(payload),
});
ConnectRPC client では transport の fetch に渡す。
const transport = createConnectTransport({
baseUrl: "https://registry.mock.klon.you",
fetch: dpopFetch,
});
ローカル開発・検証で http:// の API を呼ぶ場合のみ、allowInsecureRequests: true を指定できる。本番コードでは使わないこと。
サーバーが DPoP nonce を要求した場合、文字列や Uint8Array など再送可能な body では 1 回だけ自動 retry する。ReadableStream など再送できない body は自動 retry しない。
dpopFetch.resetDPoPKey(): Promise<void>createDPoPFetch() が返した fetch wrapper の保存済み DPoP 鍵と DPoPHandle キャッシュを破棄する。Registry API 等の protected resource 用 fetch wrapper を保持している場合は、ログアウトやアカウント切替時に client.resetDPoPKey() とあわせて呼び出す。
await dpopFetch.resetDPoPKey();
client.resetDPoPKey(): Promise<void>保存済みの DPoP 鍵と SDK 内部の DPoPHandle キャッシュを破棄する。ログアウトやアカウント切替時に呼び出す。
await clearTokens();
await client.resetDPoPKey();
interface TokenSet {
accessToken: string;
tokenType: string; // oauth4webapi により小文字化された "bearer" または "dpop"
expiresIn?: number;
refreshToken?: string;
idToken?: string; // 生の JWT 文字列
idTokenClaims?: IDTokenClaims; // デコード済みクレーム
scope?: string;
authorizationDetails?: AuthorizationDetail[];
}
アクセストークンを sender-constrained にするための仕組み。dpop: { keyStore } を createClient に渡すと、認可リクエストに dpop_jkt が付き、PAR / token / bindNativeSession に DPoP proof が自動付与される。発行されるトークンは token_type: "DPoP" になり得る。
import type { DPoPKeyStore } from "@pocketsign/klon-sdk";
interface DPoPKeyStore {
load(): Promise<CryptoKeyPair | null>;
save(keyPair: CryptoKeyPair): Promise<void>;
clear(): Promise<void>;
}
SDK は初回に load() を呼び、null なら generateKeyPair("ES256", { extractable: true }) で新規生成し save() に渡す。アプリ側はこの interface をプラットフォームのセキュアストレージ (expo-secure-store / react-native-keychain / IndexedDB 等) で実装する。
注意点:
DPoPKeyStore で鍵を永続化するtoken_type は bearer ではなく dpop になり得るcreateClient({ dpop }) が自動で proof を付けるのは SDK 自身が送るリクエストだけcreateDPoPFetch を使うclient.resetDPoPKey() と、作成済みの dpopFetch.resetDPoPKey() で保存済み鍵と SDK 内部キャッシュをクリアするconst logout = async () => {
await clearTokens();
await client.resetDPoPKey();
await dpopFetch.resetDPoPKey();
};
実装例 (React Native / expo-secure-store): CryptoKeyPair はそのまま保存できないため JWK に変換して永続化する。秘密鍵は復元時に extractable: false で取り込み、再エクスポートを防ぐ。AsyncStorage のような平文保存は本番では避けること。
import type { DPoPKeyStore } from "@pocketsign/klon-sdk";
import * as SecureStore from "expo-secure-store";
const STORAGE_KEY = "klon.dpop.keyPair";
const EC_P256 = { name: "ECDSA", namedCurve: "P-256" } as const;
const keyStore: DPoPKeyStore = {
load: async () => {
const raw = await SecureStore.getItemAsync(STORAGE_KEY);
if (!raw) return null;
const { privateJwk, publicJwk } = JSON.parse(raw);
const privateKey = await crypto.subtle.importKey("jwk", privateJwk, EC_P256, false, ["sign"]);
const publicKey = await crypto.subtle.importKey("jwk", publicJwk, EC_P256, true, ["verify"]);
return { privateKey, publicKey };
},
save: async (keyPair) => {
const privateJwk = await crypto.subtle.exportKey("jwk", keyPair.privateKey);
const publicJwk = await crypto.subtle.exportKey("jwk", keyPair.publicKey);
await SecureStore.setItemAsync(STORAGE_KEY, JSON.stringify({ privateJwk, publicJwk }));
},
clear: async () => {
await SecureStore.deleteItemAsync(STORAGE_KEY);
},
};
Web では IndexedDB 等に CryptoKey を直接保存する実装も可能。
exchangeCode / refreshToken で取得される ID Token のデコード済みクレーム。フィールド名は JWT クレーム名 (snake_case) に準拠。
| フィールド | 型 | 説明 | 条件 |
|---|---|---|---|
iss |
string |
Issuer (IdP URL) | 常に存在 |
sub |
string |
Subject (RP 固有) | 常に存在 |
aud |
string | string[] |
Audience (Client ID) | 常に存在 |
iat |
number |
発行日時 (Unix epoch) | 常に存在 |
exp |
number |
有効期限 (Unix epoch) | 常に存在 |
nonce |
string? |
Nonce | リクエスト時 |
auth_time |
number |
認証日時 (Unix epoch) | 常に存在 |
acr |
string |
認証コンテキストクラス | 常に存在 |
amr |
string[] |
認証方法 | 常に存在 |
sid |
string? |
セッション ID | セッション時 |
jpki_verified |
boolean |
JPKI 紐づけ状態 | 常に存在 |
name |
string? |
氏名 | scope: profile |
gender |
string? |
性別 | scope: profile |
birthdate |
string? |
生年月日 | scope: profile |
email |
string? |
メールアドレス | scope: email |
email_verified |
boolean? |
メール確認済み | scope: email |
address |
string? |
住所 | scope: address |
phone_number |
string? |
電話番号 | scope: phone |
phone_number_verified |
boolean? |
電話番号確認済み | scope: phone |
createAuthorizationURL の scopes に配列で渡す。ビルダー関数 (buildScope) も引き続き利用可能。
| 定数 | 値 | 説明 |
|---|---|---|
Scopes.OPENID |
openid |
OpenID Connect |
Scopes.PROFILE |
profile |
氏名/生年月日/性別 |
Scopes.EMAIL |
email |
メールアドレス |
Scopes.OFFLINE_ACCESS |
offline_access |
リフレッシュトークン |
Scopes.ADDRESS |
address |
住所 |
Scopes.PHONE |
phone |
電話番号 |
Scopes.PERSONAL_INFO |
personal_info |
基本4情報 |
createAuthorizationURL の acrValues に配列で渡す。
| 定数 | 値 | 説明 |
|---|---|---|
AcrValues.LOW |
urn:klon:acr:low |
メールアドレス/外部IdP |
AcrValues.HIGH |
urn:klon:acr:high |
JPKI 利用者証明用電子証明書相当 |
AcrValues.VERY_HIGH |
urn:klon:acr:very_high |
JPKI 署名用電子証明書相当 |
createAuthorizationURL の prompt に配列で渡す。
| 定数 | 値 | 説明 |
|---|---|---|
Prompts.NONE |
none |
認証・認可画面を表示しない。必要な場合はエラー応答する。 |
Prompts.LOGIN |
login |
エンドユーザーに再認証を要求する |
Prompts.CONSENT |
consent |
エンドユーザーに明示的な同意を要求する |
none は他の prompt と併用できない。login と consent は併用可能。
createAuthorizationURL の authorizationDetails に配列で渡す。type: "urn:klon:resource_access" は SDK が自動補完する。
import { Resources } from "@pocketsign/klon-sdk";
const { url, session } = await client.createAuthorizationURL({
authorizationDetails: [
{
identifiers: [Resources.MERGED_FULL_NAME, Resources.MERGED_BIRTH_DATE],
actions: ["read"],
required: true,
},
{
identifiers: [Resources.MERGED_FULL_ADDRESS],
actions: ["read"],
prefill: true,
},
],
});
ビルダー関数 (buildAuthorizationDetails) やパーサー (parseAuthorizationDetails) も引き続き利用可能。
Resources に KLON の公式リソースエイリアスが定義されている。
import { Resources } from "@pocketsign/klon-sdk";
Resources.MERGED_FULL_NAME; // "klon/merged_full_name"
Resources.MERGED_BIRTH_DATE; // "klon/merged_birth_date"
Resources.MERGED_FULL_ADDRESS; // "klon/merged_full_address"
Resources.EMAIL_ADDRESS; // "klon/email_address"
// ... 他多数
カテゴリ: SIGNING_* (署名用電子証明書), TICKET_* (券面事項入力補助AP), MANUAL_* (手入力), MERGED_* (最も信頼性が高い値), EMAIL_ADDRESS, PHONE_NUMBER, FACE_IMAGE, 実行リソース (PUSH_NOTIFICATION, ACCESS_CAMERA, GET_CURRENT_POSITION, GET_HIGH_ACCURACY_CURRENT_POSITION, ACCESS_FITNESS_DATA), CHECK_JPKI_* (証明書現況確認)
actions に渡せる値:
| 値 | 説明 |
|---|---|
read |
リソースの読取 |
write |
リソースの書込 |
invoke |
リソースの実行 |
createAuthorizationURL の grantManagementAction に指定する。
import { GrantManagementActions } from "@pocketsign/klon-sdk";
| 定数 | 値 | 説明 |
|---|---|---|
GrantManagementActions.CREATE |
create |
新規グラントを作成する |
GrantManagementActions.REPLACE |
replace |
既存グラントを置換する |
GrantManagementActions.MERGE |
merge |
既存グラントにマージする |
customFetch でラッパーが必要)