SCIM 2.0クライアント
概要
@authrim/server には、プログラムによるユーザーおよびグループのプロビジョニングのためのSCIM 2.0クライアントが含まれています。ScimClient クラスは、完全なCRUD操作、フィルタリング、ページネーション、ETagによる条件付きGET、楽観的ロックを提供します。
ScimClientの初期化
import { ScimClient } from '@authrim/server';
const scim = new ScimClient({ baseUrl: 'https://auth.example.com/scim/v2', accessToken: 'scim-bearer-token',});設定オプション
| オプション | 型 | 必須 | 説明 |
|---|---|---|---|
baseUrl | string | はい | SCIM 2.0エンドポイントのベースURL |
accessToken | string | はい | SCIM API認証用のBearerトークン |
http | HttpProvider | いいえ | カスタムHTTPプロバイダー(デフォルトはfetch) |
カスタムHTTPプロバイダー
import { fetchHttpProvider } from '@authrim/server';
const scim = new ScimClient({ baseUrl: 'https://auth.example.com/scim/v2', accessToken: 'scim-bearer-token', http: fetchHttpProvider({ timeoutMs: 10000 }),});ユーザー操作
createUser
新しいユーザーを作成します:
const user = await scim.createUser({ schemas: ['urn:ietf:params:scim:schemas:core:2.0:User'], name: { givenName: 'Jane', familyName: 'Doe', }, emails: [ ], active: true,});
console.log('Created user:', user.id);getUser
IDでユーザーを取得します:
const user = await scim.getUser('user-id-123');
console.log('User:', user.userName);console.log('Active:', user.active);listUsers
オプションのフィルタリングとページネーションでユーザーを一覧表示します:
const result = await scim.listUsers({ startIndex: 1, count: 25,});
console.log('Total results:', result.totalResults);for (const user of result.Resources) { console.log(user.userName);}updateUser
ユーザーリソースを完全に置換します(PUT):
const updated = await scim.updateUser('user-id-123', { schemas: ['urn:ietf:params:scim:schemas:core:2.0:User'], name: { givenName: 'Jane', familyName: 'Smith', }, emails: [ ], active: true,});patchUser
部分的な変更を適用します(PATCH):
await scim.patchUser('user-id-123', { schemas: ['urn:ietf:params:scim:api:messages:2.0:PatchOp'], Operations: [ { op: 'replace', path: 'name.familyName', value: 'Smith', }, { op: 'replace', path: 'active', value: false, }, ],});deleteUser
IDでユーザーを削除します:
await scim.deleteUser('user-id-123');グループ操作
createGroup
const group = await scim.createGroup({ schemas: ['urn:ietf:params:scim:schemas:core:2.0:Group'], displayName: 'Engineering', members: [ { value: 'user-id-123' }, { value: 'user-id-456' }, ],});
console.log('Created group:', group.id);getGroup
const group = await scim.getGroup('group-id-789');console.log('Group:', group.displayName);console.log('Members:', group.members?.length);listGroups
const result = await scim.listGroups({ filter: 'displayName co "Eng"', count: 50,});
for (const group of result.Resources) { console.log(group.displayName);}updateGroup
グループリソースを完全に置換します(PUT):
const updated = await scim.updateGroup('group-id-789', { schemas: ['urn:ietf:params:scim:schemas:core:2.0:Group'], displayName: 'Engineering Team', members: [ { value: 'user-id-123' }, { value: 'user-id-456' }, { value: 'user-id-789' }, ],});patchGroup
グループに部分的な変更を適用します(PATCH):
await scim.patchGroup('group-id-789', { schemas: ['urn:ietf:params:scim:api:messages:2.0:PatchOp'], Operations: [ { op: 'add', path: 'members', value: [{ value: 'user-id-new' }], }, ],});deleteGroup
await scim.deleteGroup('group-id-789');フィルタリングとソート
SCIM 2.0は豊富なフィルター構文をサポートしています。listUsers または listGroups に ScimFilterOptions を渡します:
import type { ScimFilterOptions } from '@authrim/server';
const options: ScimFilterOptions = { filter: 'active eq true and name.familyName co "Doe"', sortBy: 'userName', sortOrder: 'ascending', startIndex: 1, count: 25, attributes: ['userName', 'name', 'emails'],};
const result = await scim.listUsers(options);ScimFilterOptions
| オプション | 型 | 説明 |
|---|---|---|
filter | string | SCIMフィルター式(RFC 7644 Section 3.4.2.2) |
sortBy | string | ソート対象の属性 |
sortOrder | 'ascending' | 'descending' | ソート方向 |
startIndex | number | 最初の結果の1ベースインデックス |
count | number | ページあたりの最大結果数 |
attributes | string[] | 結果に含める属性のみを指定 |
excludedAttributes | string[] | 結果から除外する属性を指定 |
一般的なフィルター例
| フィルター | 説明 |
|---|---|
userName eq "[email protected]" | 完全一致 |
name.familyName co "Doe" | 部分文字列を含む |
active eq true | ブール値の一致 |
emails.value sw "admin" | 前方一致 |
meta.created gt "2025-01-01T00:00:00Z" | 日付比較 |
active eq true and name.familyName eq "Doe" | 論理AND |
userName eq "alice" or userName eq "bob" | 論理OR |
ページネーション
SCIMは1ベースインデックスのページネーションを使用します:
async function getAllUsers(scim: ScimClient): Promise<ScimUser[]> { const allUsers: ScimUser[] = []; let startIndex = 1; const pageSize = 100;
while (true) { const result = await scim.listUsers({ startIndex, count: pageSize, });
allUsers.push(...result.Resources);
if (allUsers.length >= result.totalResults) { break; }
startIndex += pageSize; }
return allUsers;}ETagによる条件付きGET
条件付きGETを使用して、変更されていないリソースのフェッチを回避します。getUserConditional と getGroupConditional メソッドは ScimConditionalGetResult を返します:
import type { ScimConditionalGetResult } from '@authrim/server';
// First request — no ETag yetconst result: ScimConditionalGetResult = await scim.getUserConditional( 'user-id-123',);
if (result.status === 'ok') { console.log('User:', result.resource); console.log('ETag:', result.etag);}
// Subsequent request — pass the ETagconst cached = await scim.getUserConditional('user-id-123', { ifNoneMatch: result.etag,});
if (cached.status === 'not_modified') { console.log('Resource unchanged, use cached version');} else if (cached.status === 'ok') { console.log('Resource updated:', cached.resource);}グループも同様に動作します:
const groupResult = await scim.getGroupConditional('group-id-789');
const cachedGroup = await scim.getGroupConditional('group-id-789', { ifNoneMatch: groupResult.etag,});楽観的ロック
If-Match ヘッダーとETag値を使用して、updateUser や patchUser での同時変更の競合を防止します:
// Fetch the current user with its ETagconst result = await scim.getUserConditional('user-id-123');
if (result.status === 'ok') { try { // Update only if the resource hasn't changed const updated = await scim.updateUser( 'user-id-123', { ...result.resource, name: { ...result.resource.name, familyName: 'Updated' }, }, { ifMatch: result.etag }, ); } catch (error) { // 412 Precondition Failed — resource was modified by someone else console.error('Conflict: resource was modified concurrently'); }}同じパターンが patchUser、updateGroup、patchGroup にも適用されます:
await scim.patchUser( 'user-id-123', { schemas: ['urn:ietf:params:scim:api:messages:2.0:PatchOp'], Operations: [{ op: 'replace', path: 'active', value: false }], }, { ifMatch: etag },);次のステップ
- バックチャネルログアウト — サーバーサイドのログアウト通知の処理
- Express & Fastifyアダプター — トークン検証のフレームワーク統合
- エラーハンドリング — SCIMエラーレスポンスと処理
- 設定リファレンス — HTTPプロバイダーのカスタマイズ