KeycloakのAdmin REST APIでユーザを作成する

前回の記事でAdmin REST APIを使ってレルムの作成を行いました。

今回はユーザの作成をして、そのユーザにレルムの作成権限をマッピングしてみようと思います。

マスターレルムにユーザを作成して、そのユーザでレルムを作成することで作成したレルムの管理をそのユーザで行うことができます。 (※レルムを作成したユーザには自動でそのレルムのアクセス権限が付与されます。)

They will be granted full access to any new realm they create.

環境

  • マシン:Windows11 
  • Keycloakのバージョン:Keycloak 22.0.0

前回dockerで起動したKeycloakの環境を使用します。

また、javascriptの動作環境も前回のものをそのまま使用します。

ユーザの作成

公式のAPIリファレンスで、Userの作成方法を確認します。

以下のように記載がある項目がユーザ作成用のAPIです。

Create a new user Username must be unique.

実装

まずはユーザ作成を実行するjavascriptファイルを「create-user.js」という名前で作成します。

【実装する内容】

  1. 前回同様adminユーザでアクセストークンを取得
  2. パスパラメータでユーザを作成するレルムを指定したURIを構築
  3. ボディにUserRepresentationで定義されているオブジェクトをセット
  4. POSTで送信

※今回はユーザ名のみを指定して作成します。その他のフィールドを追加したい場合はUserRepresentationを確認してPOSTするデータに適宜追加してください。

また今回はユーザの作成後に、更新の処理を追加してユーザを有効化しています。(※作成時に同フィールド({enabled: true})を追加すれば作成時点で有効にすることも可能)

更新のAPIをコールする際、必要なユーザIDを取得するためにユーザ情報の取得APIをコールしています。 ユーザ情報の取得APIでは「{search:userName}」を指定してクエリパラメータを設定することでフィルターをかけることができます。詳細は公式ドキュメントを参照してください。以下の記載があるAPIがユーザ情報の取得APIです。(取得できる値はユーザ情報の配列です。)

最後にパスワードの設定をして、管理画面にログインできるようにしています。

Get users Returns a stream of users, filtered according to query parameters.

import axios from 'axios';


// アクセストークンを取得
const requestToken = async (username="admin", password="admin") => {
  const url = "http://localhost:8080/realms/master/protocol/openid-connect/token";
  const data = new URLSearchParams({
    client_id: 'admin-cli',
    password: password,
    username: username,
    grant_type: 'password'
  }).toString();
  const headers = {
    headers : {
      'Content-Type':'application/x-www-form-urlencoded'
    }
  };
  return await axios.post(url, data, headers);
};

// ユーザの作成
const crateUser = async (access_token, user,  realmName="master") => {
  try {
    const url = `http://localhost:8080/admin/realms/${realmName}/users`;

    const data = {
      username: user
    }
    const headers = {
      headers : {
        'Authorization': `bearer ${access_token}`,
      }
    }
    
    await axios.post(url, data, headers);
    console.log("success create user")
 } catch (e) {
   console.log(e.response.data)
 }
}

// ユーザ情報の取得
const getUser = async (access_token, userName, realmName="master") => {
  try {
    const url = `http://localhost:8080/admin/realms/${realmName}/users`;

    const config = {
      headers : {
        'Authorization': `bearer ${access_token}`,
        'Content-Type':'application/json'
      },
      params : {
        BriefRepresentation : true,
        search: userName
      }
    }
    
    const result = await axios.get(url, config);
    const target = result.data;
    console.log(target);
    console.log("get success");
    return target
  } catch (e) {
    console.log(e.response.data)
  }
}

// ユーザ情報の更新
const updateUser = async (access_token, userInfo, realmName="master") => {
  try {
    const url = `http://localhost:8080/admin/realms/${realmName}/users/${userInfo.id}`;

    const config = {
      headers : {
        'Authorization': `bearer ${access_token}`,
        // 'Content-Type':'application/json'
      }
    };

    const data = {
      enabled: true
    };
    
    await axios.put(url, data, config);
    console.log("success update user");
  } catch (e) {
    console.log(e.response.data);
  };
};

// パスワードの設定
const settingNewUserPassword = async (access_token, userInfo, realmName = "master") => {
  try {
    const url = `http://localhost:8080/admin/realms/${realmName}/users/${userInfo.id}/reset-password`;

    const config = {
      headers : {
        'Authorization': `bearer ${access_token}`,
      }
    }

    const data = {
      value: "Password"
    };
    
    await axios.put(url, data, config);
    console.log("success setting user password");
  } catch (e) {
    console.log(e.response.data);
  };
};

const exec = async (userName) => {
  // アクセストークンを取得
  const result = await requestToken();
  const accessToken = result.data.access_token;

  // ユーザの作成
  await crateUser(accessToken, userName);

  // ユーザ情報の取得
  const userInfo = await getUser(accessToken, userName);

  // ユーザ情報の更新
  await updateUser(accessToken, userInfo[0]);

  // パスワードの設定
  await settingNewUserPassword(accessToken, userInfo[0]);
};

exec("practice-user")

実行

作成したスクリプトを実行します。以下のような結果が取得できると思います。

> node .\create-user.js

success create user
[
  {
    id: '1aff3c17-76d6-4bdb-b358-243841d8e029',
    createdTimestamp: 1689319884773,
    username: 'practice-user',
    enabled: false,
    totp: false,
    emailVerified: false,
    disableableCredentialTypes: [],
    requiredActions: [],
    notBefore: 0,
    access: {
      manageGroupMembership: true,
      view: true,
      mapRoles: true,
      impersonate: true,
      manage: true
    }
  }
]
get success
success update user
success setting user password

ユーザへのロールのマッピング

ロールまたはユーザ ロール マッピングとは以下のように説明されています。 今回は上記ユーザにロールをマッピングしてレルムの作成権限を与えてみます。

roles

Roles identify a type or category of user. Admin, user, manager, and employee are all typical roles that may exist in an organization. Applications often assign access and permissions to specific roles rather than individual users as dealing with users can be too fine-grained and hard to manage.

user role mapping

A user role mapping defines a mapping between a role and a user. A user can be associated with zero or more roles. This role mapping information can be encapsulated into tokens and assertions so that applications can decide access permissions on various resources they manage.

与えるロールはレルムレベルのロールで「create-realm」というものです。 詳細はこちらを参照してください。

例によって、公式のAPIリファレンスから利用可能なロールの取得APIと、ユーザへのロールのマッピングAPIの利用方法について確認します。

以下の記述があるAPIを参照してください。

※ユーザへロールをマッピングするAPIにはRoleRepresentationデータ形式でデータを送信する必要があります。(ロールの情報を配列にしてPOSTする必要があります。)

  • 利用可能なロールの取得をするAPI

    Get realm-level roles that can be mapped

  • ユーザへロールをマッピングするAPI

    Add realm-level role mappings to the user

実装

先ほどのcreate-user.jsに関数を追加して実行します。

// マッピング可能なロールの中から指定のロール情報を取得
const getAvailableRoleMapping = async (access_token, userInfo, targetRole="create-realm", realmName="master") => {
  try {
    const url = `http://localhost:8080/admin/realms/${realmName}/users/${userInfo.id}/role-mappings/realm/available`;

    const headers = {
      headers : {
        'Authorization': `bearer ${access_token}`
      }
    };
    const resultList = await axios.get(url, headers);
    console.log(resultList.data);
    const targetRoleInfo = resultList.data.filter(v => v.name == targetRole);
    console.log("get available role list success");
    return targetRoleInfo;
  } catch (e) {
    console.log(e)
  };
};

// ロールのマッピング
const mappingRoles = async (access_token, userInfo, targetRoleInfo, realmName="master") => {
  try {
    const url = `http://localhost:8080/admin/realms/${realmName}/users/${userInfo.id}/role-mappings/realm`;
    const target = targetRoleInfo;
    const headers = {
      headers : {
        'Authorization': `bearer ${access_token}`
      }
    };
    if (target.length) {
      console.log(target);
      await axios.post(url, target, headers);
      console.log("mapping success");
    }

  } catch (e) {
    console.log(e.response.data);
  }
};
.
.
.
const exec = async (userName) => {
  // アクセストークンを取得
  const result = await requestToken();
  const accessToken = result.data.access_token;

  // ユーザの作成
  await crateUser(accessToken, userName);

  // ユーザ情報の取得
  const userInfo = await getUser(accessToken, userName);

  // ユーザ情報の更新
  await updateUser(accessToken, userInfo[0]);

  // パスワードの設定
  await settingNewUserPassword(accessToken, userInfo[0]);

  // 追加⇒ マッピング可能なロールの中から指定のロール情報を取得
  const targetRoleInfo = await getAvailableRoleMapping(accessToken, userInfo[0]);

  // 追加⇒ ロールのマッピング
  await mappingRoles(accessToken, userInfo[0], targetRoleInfo);

};

実行

ロールの情報は以下のようになっています。

> node create-user.js

[
  {
    id: 'b4205b96-6851-4864-acad-4900d5de3b85',
    name: 'create-realm',
    description: '${role_create-realm}',
    composite: false,
    clientRole: false,
    containerId: '6e586675-7faa-4bc3-add4-2f6beac27c9a'
  },
  {
    id: 'b558d655-e4cd-4e69-8dee-e0de416d7144',
    name: 'uma_authorization',
    description: '${role_uma_authorization}',
    composite: false,
    clientRole: false,
    containerId: '6e586675-7faa-4bc3-add4-2f6beac27c9a'
  },
  {
    id: '08738779-e1e7-4556-a3d8-5ae557ca39fa',
    name: 'offline_access',
    description: '${role_offline-access}',
    composite: false,
    clientRole: false,
    containerId: '6e586675-7faa-4bc3-add4-2f6beac27c9a'
  },
  {
    id: '803abb4d-b999-4e3a-ad7e-87a9caf963f0',
    name: 'admin',
    description: '${role_admin}',
    composite: true,
    clientRole: false,
    containerId: '6e586675-7faa-4bc3-add4-2f6beac27c9a'
  }
]
get available role list success
[
  {
    id: 'b4205b96-6851-4864-acad-4900d5de3b85',
    name: 'create-realm',
    description: '${role_create-realm}',
    composite: false,
    clientRole: false,
    containerId: '6e586675-7faa-4bc3-add4-2f6beac27c9a'
  }
]
mapping success

管理画面で確認してみると、対象のユーザ(practice-user)にcreate-realmのロールが付与されていることがわかります。

role-mapping
role

まとめ

ユーザ作成~ロールのマッピングをAdmin REST APIで実行してみました。

POSTするデータの形式(RoleRepresentationとか...)が公式ドキュメントを読んでもわかりにくく、またすぐ忘れてしまいそうなので今回ブログに残せてよかったです。

作成したユーザでアクセストークンを取得後、そのユーザで前回の記事で記載したレルムの作成を実行すればレルム毎に管理ユーザを分けたりすることができると思います。

参考

Keycloak Admin REST API