🤖

oktaのパスワードとMFAリセットするSlackBOTを作った

2024/07/12に公開

概要

本記事の内容は、oktaが日々従業員に使われるようにしていった結果発生した
🤵‍♂️「パスワードを忘れてしまったのでリセットして欲しい」
🤵‍♀️ 「スマホを機種変更したらログインできなくなった」
といった情シスへの問い合わせ頻度No.1(※)の事象に対して、SlackBOTを使って対処してみた話になります
また、作ったのはかなり前なのでSlack New Platformではなく自分でデプロイするタイプのSlackBOTになっています

※No.1は適当に書いてますが、よく聞く話ではありますよね😉

概要図

何で作ったか

ライブラリについては、当時調べたらFlaskよりFastapiがきてる的な記事を見かけて習作のために選定しました
UnicornはHeroku上で動かすのにwebサーバーが必要だったので選定しました
デプロイ先としては、たまたま他のプログラムがHeroku上で動いていたので相乗りした形で特に意思なく決めました

結果

UI

  1. 起動時の画面
  2. 情シスによる承認操作の画面
  3. ユーザーへの通知画面

Slackアプリの準備

1. BOT設定の実施

  1. アプリの作成
    3. https://api.slack.com/apps

  2. Interactivity & Shortcutsの設定
    4. InteractivityをON変更する
    5. Create Shortcutsからショートカットを作成
    6. callback_idがSlack上で呼び出す時のIDになるので分かりやすいものにしておくと良いです

  3. Request URLの設定
    8. 開発時はngrokで作られたURL
    9. 本番はデプロイ先のURL

2. 必要権限の設定

  1. OAuth & Permissions
    2. メッセージの投稿やユーザーデータの読み取りなどの権限を付与する

3. ワークスペースへインストール

  1. Basic Information
    2. BOTをワークスペースにインストールする
    3. 発行されたBot User OAuth Tokenを控える
    4. この値は環境変数に設定しておきます

実装

処理の流れ

処理コード

ファイル構成

処理内容は多くないのでmain.pyに集約させています
定数と、Slack上のUIはviewsフォルダにjsonファイルとして格納しました

├── constants
│   ├── __init__.py
│   └── constants.py
├── main.py #各処理はここに記述
├── requirements.txt
└── views #Slack上のUIはここにファイルで集約
    ├── confirm_message.json
    └── open_modal.json

コード部分

全文を記載すると長くなるのですごくざっくりと記載します

定数の定義constants/constants.py

from os import environ as env
# BOTトークン
SLACK_BOT_TOKEN=env["SLACK_BOT_TOKEN"]
ユーザー選択値で処理分けるため値の定義
OKTA_RESET_TYPE = {
    "1": "パスワードリセット",
    "2": "MFAリセット"
}
# その他投稿先やoktaのAPIキーなどの設定
OKTA_API_KEY=env["OKTA_API_KEY"]

main.py

from fastapi import FastAPI, Request
from slack_sdk import WebClient
# import constants
from constants.constants import *

app = FastAPI()
slack_client = WebClient(token=SLACK_BOT_TOKEN)

def request_parse_to_action(body):
    """ リクエスト内容に応じたアクションを実行する """
    r_type = body["type"]
    r_callback_id = body.get("view", {}).get("callback_id")
    
    if r_type == "shortcut":
        # XXX: ショートカット(スラッシュコマンド)を実行した時の挙動はここに記述
        return open_request_modal(body["trigger_id"])
    elif r_type == "view_submission" and r_callback_id == "okta_reset_request":
        # XXX: ユーザー操作が行われた時と情シス操作完了時の通知処理
        post_notification_message(body["user"])
        return post_confirm_infomation_system(body["view"]["state"], body["user"])
    elif r_type == "block_actions":
        # XXX: 情シスが承認した後の処理, oktaの操作を行う処理
        return okta_reset_process(body)

@app.post("/")
async def read_root(request: Request):
    body_row = await request.body()
    body = json.loads(urllib.parse.parse_qs(body_row.decode('utf-8'))["payload"][0])
    return request_parse_to_action(body)

具体の処理部分は以下のような感じで実装しました

def load_json_file(file_name):
    """ SlackのView用JSONファイルを読み込む """
    file_path = f"views/{file_name}.json"
    with open(file_path, "r") as fh:
        data = json.load(fh)
    return data

def open_request_modal(trigger_id):
    """ okta申請のモーダルを開く """
    view = load_json_file("open_modal")
    slack_client.views_open(trigger_id=trigger_id, view=view)
    return

def post_notification_message(submit_user):
    """ 申請者に受付メッセージを送信する """
    # 送りたいメッセージを定義する
    msg = "hoge"
    slack_client.chat_postMessage(channel=HELPDESK_CHANNEL_ID,text=msg)

def post_confirm_infomation_system(state, submit_user):
    """ 情シス確認用チャンネルにメッセージ送信 """
    # XXX: ユーザーからの送信情報を取得して必要情報を取り出す処理を記述
    values = state["values"]
    # XXX: 承認/否認を行うためのメッセージブロックをjsonファイルから取得する、誰からどんな内容みたいな加工は読み込み後に実施
    blocks = load_json_file("confirm_message")
    # インタラクティブ操作した時に誰からの申請でどのアカウントに何の処理するかのメタ情報を作る
    metadata = {
        "event_type": "okta_reset",
        "event_payload":{
            "type": select_reset_type,
            "reset_user": select_reset_user,
            "request_user": user_id
        }
    }
    # 必要情報をSlackに投稿
    slack_client.chat_postMessage(channel=RECIEVE_REQUEST_CHANNEL, text="post", blocks=blocks ,metadata=metadata)
    return {}

def okta_reset_process(body):
    """ 依頼に応じたリセットを実施する """
    message = body["message"]
    actions = body["actions"]
    action_id = actions[0]["action_id"]
    if action_id == "ok":
        # XXX: メタデータから情報を取り出し、対象oktaアカウントに選択された操作を実行する
        reset_type = message["metadata"]["event_payload"]["type"]
        reset_user = message["metadata"]["event_payload"]["reset_user"]
        okta_reset(reset_type, reset_user)
    else:
        # 否認された場合の処理はここに書き、IF文を抜けた先で必要な通知などを記述する
        
    return {}

def okta_reset(type, user_id):
    """ oktaのリセットを実行する """
    # XXX: タイプに応じて処理分岐し、パスワードリセットorMFAリセット処理を実行する。IF文を抜けたら必要な情報を通知する
    # パスワードリセット時などはテンポラリーのPWが発行されるのでDM通知にしておくとベター
    if type == "1":
        # パスワードリセット
    elif type == "2":
        # MFAリセット
    else:
        # エラー処理

実装してみて

このBOT自体は1年以上前に作ったのですが、様々なSaasをSSO/SCIM連携を進めていくなかでoktaの運用が定着していく過渡期においてリセット処理にかかる運用負荷をかなり下げてくれるBOTになりました😊
直近では昨年10月〜年末にかけてiPhone15へ機種変更した従業員が毎日のように使ってくれました📱

ちなみに

当時もあったと思うんですが、現在この実装方法するよりslack公式のFWの Slack Bolt for Pythonを利用するほうがより簡単に実装できます
https://slack.dev/bolt-python/ja-jp/tutorial/getting-started

例えばリクエストからショートカットや送信ボタン押された時の処理をif文で書いていましたが、FWの機能を使うことで分かりやすく記述することができます

# ショートカット起動でモーダル開く
@app.shortcut("open_modal")
def start_form(ack, shortcut, client):
    ack()
    client.views_open(
        trigger_id=shortcut["trigger_id"],
        view={...block }
    )

# フォームが送信された時の処理
@app.view("view_1")
def form_submission(ack, view, logger):
    ack()

また、選択肢としてslackのプレミアムワークフロー (呼び名が分かりませんがnew plat form)も使えるので、デプロイ先をslackに寄せたいなど状況に応じて使い分けていくと良さそうです

おわりに

一部定数などを直せばそのまま公開できそうなので、いつか公開できたらいいなとは思ってます
Chatbotは夢があるので機会があればまた何か作ってみます 🙇

dely Tech Blog

Discussion