コンテンツにスキップ

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',
});

設定オプション

オプション必須説明
baseUrlstringはいSCIM 2.0エンドポイントのベースURL
accessTokenstringはいSCIM API認証用のBearerトークン
httpHttpProviderいいえカスタム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'],
userName: '[email protected]',
name: {
givenName: 'Jane',
familyName: 'Doe',
},
emails: [
{ value: '[email protected]', primary: true },
],
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({
filter: 'userName eq "[email protected]"',
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'],
userName: '[email protected]',
name: {
givenName: 'Jane',
familyName: 'Smith',
},
emails: [
{ value: '[email protected]', primary: true },
],
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 または listGroupsScimFilterOptions を渡します:

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

オプション説明
filterstringSCIMフィルター式(RFC 7644 Section 3.4.2.2)
sortBystringソート対象の属性
sortOrder'ascending' | 'descending'ソート方向
startIndexnumber最初の結果の1ベースインデックス
countnumberページあたりの最大結果数
attributesstring[]結果に含める属性のみを指定
excludedAttributesstring[]結果から除外する属性を指定

一般的なフィルター例

フィルター説明
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を使用して、変更されていないリソースのフェッチを回避します。getUserConditionalgetGroupConditional メソッドは ScimConditionalGetResult を返します:

import type { ScimConditionalGetResult } from '@authrim/server';
// First request — no ETag yet
const 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 ETag
const 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値を使用して、updateUserpatchUser での同時変更の競合を防止します:

// Fetch the current user with its ETag
const 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');
}
}

同じパターンが patchUserupdateGrouppatchGroup にも適用されます:

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 },
);

次のステップ