Rails API と Okta で SCIM 機能を実装してみた(前半)
概要
Rails(API モード) と Okta を使用して、 User に関するSCIM 機能を実装してみました。
本記事ではこちらにあるチュートリアルにて紹介されている Runscope のテストが通るように実装しています。
今回作成したものは以下になります。
後半の記事は以下になります
用意するもの
- Okta アカウント https://developer.okta.com/
- ngrok アカウント https://ngrok.com/
- Runscope アカウント https://www.runscope.com
- Rails アプリ(API モード)
前準備
1. API token を作成する
Rails と Okta 間で、認証を行う際に使用する API token を作成します。
Okta のダッシュボードにて、サイドメニューより Security > API
と選択し、Create token
ボタンから作成することができます。作成したトークンは手元に控えておいてください。
2. Rails の初期設定を行う
API エンドポイントは、こちらに記載されているようにhttps
通信で行うように定められています。そのため、ngrok を使用して3000 port を外部公開します。
ngrok http 3000
そして、ngrok にて生成されたドメインをconfig/enviroments/development.rb
に設定します。
# config/enviroments/development.rb
Rails.application.configure do
# ....
config.hosts << '#{your_ngrok_domain}'
end
設定が済みましたら Rails server を立ち上げて、ngrok の url をブラウザで開くと Rails の初期画面が表示されると思います。
次に、先程 Okta にて作成した API token を credentials ファイルに登録し、保存します。
okta:
scim: your_api_token
最後に Rails で SCIM 機能を実装するのに必要な Gem を Gemfile に設定し、bundle install して前準備は終わりになります。使用する Gem は scimitorを使用します。
Runscope の準備を行う
こちらを参考に前準備を行います。
詳細
-
SCIM 2.0 用の JSON ファイルをダウンロードします(https://developer.okta.com/standards/SCIM/SCIMFiles/Okta-SCIM-20-SPEC-Test.json)
-
Runscope(https://www.runscope.com/okta)にアクセスし、左下にある
Import Text
をクリックします。
-
Runscope API Tests
を選択し、「ファイル選択」より先程ダウンロードした JSON ファイルをします。そして、Import API Test
をクリックします。
-
画面右上の
Test
タブをクリックし、Okta SCIM 2.0 SPEC Test
が作成されていることを確認します。
-
Okta SCIM 2.0 SPEC
内に進んで頂き、サイドメニューのEditor
を選択し、Initial Variables
を選択します。設定する値は以下になります。
Name | Value |
---|---|
auth | Bearer {your_access_token} |
SCIMBaseURL | {your_ngrok_url}/scim_v2 |
-
Initiral Script
タブを選択し、以下の内容をペーストした後、画面上のSave
ボタンを押して設定を反映します。
function generate_lastname()
{
var lastname = "";
var lower_alphabets = "abcdefghijklmnopqrstuvwxyz";
var upper_alphabets = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
var digits = "0123456789";
lastname += upper_alphabets.charAt(Math.floor(Math.random() * upper_alphabets.length));
for( var i=0; i < 8; i++ )
lastname += lower_alphabets.charAt(Math.floor(Math.random() * lower_alphabets.length));
for( var j=0; j < 3; j++ )
lastname += digits.charAt(Math.floor(Math.random() * digits.length));
return lastname;
}
function generate_firstname()
{
var firstname = "Runscope";
var digits = "0123456789";
for( var j=0; j < 3; j++ )
firstname += digits.charAt(Math.floor(Math.random() * digits.length));
return firstname;
}
var firstname = generate_firstname();
var lastname = generate_lastname();
var email = firstname + lastname + "@atko.com";
variables.set("randomGivenName", firstname);
variables.set("randomFamilyName", lastname);
variables.set("randomEmail", email);
variables.set("randomUsername", email);
variables.set("InvalidUserEmail", "abcdefgh@atko.com");
variables.set("UserIdThatDoesNotExist", "010101001010101011001010101011");
variables.set("randomUsernameCaps",email.toUpperCase());
- 最後に今回は User に関する SCIM 機能のみを実装するので、グループに関するテストは Skip するようにします。
READ, CREATE 機能を実装する
scimitar の 初期設定ファイルを作成
config/initializers/scimitar.rb
を作成し、こちらを参考に以下の内容を設定します。
1つは CSRT token の確認を省略する設定で、もう1つは Bearer 認証を行う設定です。
# config/initializers/scimitar.rb
Rails.application.config.to_prepare do # (required for >= Rails 7 / Zeitwerk)
Scimitar.engine_configuration =
Scimitar::EngineConfiguration.new(
{
application_controller_mixin:
Module.new do
def self.included(base)
base.class_eval { skip_before_action :verify_authenticity_token }
end
end,
token_authenticator:
proc do |token, _options|
Rack::Utils.secure_compare(
token,
Rails.application.credentials.dig(:okta, :scim)
)
end
}
)
end
メモ
Scimitar::EngineConfiguration は ActiveModel::Model モジュールをインクルードしたクラス。
Scimitarはモジュールとして定義してあり、ゲッターとセッターが定義してある。
それぞれの設定はScimitar::ApplicationControllerで呼び出される。
User モデルの作成
users テーブル用の migration ファイルは以下のようになります。
ActiveRecord::Schema[7.0].define(version: 2023_04_03_141442) do
enable_extension "plpgsql"
create_table "users", force: :cascade do |t|
t.string "first_name", null: false
t.string "last_name", null: false
t.string "email", null: false
t.string "okta_user_name", null: false, comment: "Okta上で一意なユーザー名"
t.boolean "active", default: false, null: false, comment: "Okta上で有効かどうか"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["okta_user_name"], name: "index_users_on_okta_user_name", unique: true
end
end
User::OktaScim モジュールを作成
SCIM のリソースと User モデルをマッピングするためのモジュール(app/models/user/okta_scim.rb
)を作成し、以下を参考に実装します。
# app/models/user/okta_scim.rb
module User::OktaScim
extend ActiveSupport::Concern
included { include Scimitar::Resources::Mixin }
module ClassMethods
# SCIM と 紐づく ActiveRecord のクラスを指定
def scim_resource_type
Scimitar::Resources::User
end
# SCIM の属性と User クラスの属性をマッピングするためのメソッド
# 返却すべき基本的な形式が下記のURLから確認できる
# https://developer.okta.com/docs/reference/scim/scim-20/#retrieve-a-specific-user
def scim_attributes_map
{
id: :id,
userName: :okta_user_name,
name: {
givenName: :first_name,
familyName: :last_name
},
emails: [
{
match: 'primary',
with: true,
using: {
value: :email,
primary: true
}
}
],
active: :active
}
end
# SCIM オブジェクト(User インスタンス)を作成、更新する際にどの属性を変更可能かを決定するためのメソッド
# 今回は全ての属性を変更可能としている
def scim_mutable_attributes; end
# SCIM フィルタークエリ と User モデルの属性をマッピングするためのメソッド
# Okta では userName 属性を一意な値として管理しており、この値を元に一意なユーザーを検索する。
# https://developer.okta.com/docs/reference/scim/scim-20/#retrieve-users
def scim_queryable_attributes
{ 'userName' => { column: :okta_user_name } }
end
# こちらはオプション
def scim_timestamps_map
{ created: :created_at, lastModified: :updated_at }
end
end
end
このモジュールを User モデルにインクルードします。
# app/models/user.rb
class User < ApplicationRecord
include User::OktaScim
validates :first_name, presence: true
validates :last_name, presence: true
validates :email, presence: true
validates :okta_user_name, presence: true, uniqueness: true
end
コントローラーの作成
コントローラーを作成します。
bin/rails g controller ScimV2::Users index show create
作成したコントローラーは Scimitar::ResourcesController
を継承します。
module ScimV2
class UsersController < Scimitar::ResourcesController
def index; end
def show; end
def create; end
end
end
routes.rb に以下の設定を行います
Rails.application.routes.draw do
namespace :scim_v2 do
get 'Users', to: 'users#index'
get 'Users/:id', to: 'users#show'
post 'Users', to: 'users#create'
end
end
コントラーラーの実装は以下のファイルを参考に行います。
Index アクションの実装
module ScimV2
class UsersController < Scimitar::ResourcesController
def index
# SCIM では ページネーション機能を実装する必要があり、Response に startIndex, count, totalResult が必要である。
# それらの値を scim_pagination_info メソッドで計算している
# https://github.com/RIPAGlobal/scimitar/blob/bb8fb75061c0e2cf1729dac28b2c90ddec6749b9/app/models/scimitar/engine_configuration.rb
pagination_info = scim_pagination_info(query.count)
page_of_results =
query.offset(pagination_info.offset).limit(pagination_info.limit).to_a
# 親クラスのメソッドでは、JSON 形式で Response を組み立ている
super(pagination_info, page_of_results) do |record|
record.to_scim(location: url_for(action: :show, id: record.id))
end
end
private
def query
@query ||= if params[:filter].present?
# params[:filter] 例: "userName eq \"abcdefgh@atko.com\""
parser = ::Scimitar::Lists::QueryParser.new(storage_class.new.scim_queryable_attributes)
# RPN 記法に変換する(演算子が一番後ろにくるという浅い認識です)
# 例: ["userName", "\"abcdefgh@atko.com\"", "eq"]
parsed_query = parser.parse(params[:filter])
# Acitive Record のクエリが組み立てられる
# 引数には base scope を設定する。今回は全ユーザーを対象としている
parsed_query.to_activerecord_query(storage_class.all)
else
storage_class.all
end
end
# Scimitar::Resources::Mixin モジュールをインクルードしたモデルを指定
def storage_class
User
end
end
end
親クラスの定義は以下のようになっています。to_scim
メソッドは User モデルのインスタンスから SCIM 用のハッシュ形式への変換を行なっています。
下記がレスポンスの例になります。
{
"schemas": [
"urn:ietf:params:scim:api:messages:2.0:ListResponse"
],
"totalResults": 7,
"startIndex": 1,
"itemsPerPage": 1,
"Resources": [
{
"id": "6",
"userName": "abmqtynjoj",
"name": {
"givenName": "Chase",
"familyName": "Schuster"
},
"emails": [
{
"primary": true,
"value": "jared.balistreri@rutherford-turcotte.net"
}
],
"active": false,
"meta": {
"location": "https://93f5-240d-1e-458-7f00-f0de-20a6-f288-a94.ngrok-free.app/scim_v2/Users/6",
"created": "2023-04-09T05:20:22Z",
"lastModified": "2023-04-09T05:20:22Z",
"resourceType": "User"
},
"schemas": [
"urn:ietf:params:scim:schemas:core:2.0:User"
]
}
]
}
show アクションの実装
親クラスの定義は以下のようにシンプルな実装になっています。
実装は以下のようになります。
module ScimV2
class UsersController < Scimitar::ResourcesController
...
def show
super do |user_id|
user = storage_class.find(user_id)
user.to_scim(location: url_for(action: :show, id: user_id))
end
end
end
end
下記がレスポンスの例になります。
{
"id": "6",
"userName": "abmqtynjoj",
"name": {
"givenName": "Chase",
"familyName": "Schuster"
},
"emails": [
{
"primary": true,
"value": "jared.balistreri@rutherford-turcotte.net"
}
],
"active": false,
"meta": {
"location": "https://93f5-240d-1e-458-7f00-f0de-20a6-f288-a94.ngrok-free.app/scim_v2/Users/6",
"created": "2023-04-09T05:20:22Z",
"lastModified": "2023-04-09T05:20:22Z",
"resourceType": "User"
},
"schemas": [
"urn:ietf:params:scim:schemas:core:2.0:User"
]
}
次にユーザーが見つからない場合の処理を実装します。
Okta では以下のような形式でレスポンスが返ってくるのを期待しています。
{
"schemas": ["urn:ietf:params:scim:api:messages:2.0:Error"],
"detail": "User not found",
"status": 404
}
scimitar には上記の形式でレスポンスを返すメソッドが用意されているので、ユーザーが見つからない場合にそちらのメソッドを呼び出します。
module ScimV2
class UsersController < Scimitar::ResourcesController
rescue_from ActiveRecord::RecordNotFound, with: :handle_resource_not_found
...
end
end
Create 機能の実装
親クラスの実装は以下のようになっており、クエリパラメータの内容をScimitar::Schema::User を元にScimitar::Resources::Userのインスタンスを作成します。そして、そのリソースが有効であれば子クラスに渡されます。
子クラスの create アクションの実装は以下のようになります。
module ScimV2
class UsersController < Scimitar::ResourcesController
...
def create
super do |scim_resource|
user = storage_class.new
user.save_from_scim!(scim_resource)
user.to_scim(location: url_for(action: :show, id: user.id))
end
end
end
end
また、 user に対して呼び出している#save_from_scim!
メソッドに関しては、User::OktaScim
モジュールに実装します。この時、何らかの理由でデータの作成に失敗した場合(対象のユーザーが既に存在していたなど)は、409
エラーを返すようにします。
module User::OktaScim
...
def save_from_scim!(scim_resource)
ActiveRecord::Base.transaction do
from_scim!(scim_hash: scim_resource.as_json)
save!
end
rescue ActiveRecord::RecordInvalid
raise Scimitar::ErrorResponse.new(status: 409, detail: 'User already exists in the database.' )
end
end
以上が全ての実装になります。Runscope を実行して、全てのテストが通れば完了です🥳
Okta 上にアプリケーションを作成し、動作確認を行う
アプリケーションの準備
- サイドメニューの
Appications > Applications
をクリックし、Create App Integration
を選択します。
- 認証は
SAML 2.0
を指定し、次へ進みます。 - 任意の名前を設定し、
Addvisibility
のチェックを外して次へ進みます。 - 認証機能は使用しないので下図のように任意の値を設定し次へ進みます。
- 最後は
I'm a software vendor. I'd like to integrate my app with Okta
の方を選択してFinish
ボタンを押して完了です。(作成したアプリケーションの詳細ページに遷移します) -
General
タブをクリックし、 App Settings の項目にあるProvisioning
を有効にします。これで SCIM 機能が利用できるようになります。
-
Provisioning
タブをクリックし、SCIM Connection の設定を行います。設定を終えた後、保存を行います。
-
Provisioning
タブをクリックし、To App
を選択します。そして、設定内にあるCreate Users
を有効化して保存します。
動作確認
アプリへのユーザーの追加は、作成したアプリケーションのAssignments
タブから行うことができます。
今回は予め用意していた 田中太郎というユーザーをアサインしてみたいと思います。
ログを確認すると以下のようになっており、users テーブルにデータが作成できたのが確認できます。また、 Okta の Create Users のフロー図と同じ処理が行われているのも確認できると思います。
https://developer.okta.com/docs/reference/scim/scim-20/
参考
Discussion