Jenkinsで2段階認証コードを取得できるようにしてみた

この記事は[misskey.dev ユーザー Advent Calendar 2023](https://adventar.org/calendars/9688)の24日目の記事です! メリークリスマス🎉
# はじめに
検証に関する業務でログインが必要な外部サービスを利用することがある。検証用のアカウントをよりセキュアな状態で運用したいけど、2段階認証を利用すると複数人でのアクセスがめんどくさくなりそう。なので社内の人なら誰でも使える方法でトークンが生成できるようにしたくなった。社内専用のGoogle Authenticatorを用意する的な感じ。

# やりたかったこと
2段階認証で要求されるTOTPのトークンを社内で利用しているJenkinsで取得できるようにする

# 与えられたシークレットからトークンを生成できるようにする
Google Authenticatorなどで使われているTOTP(Time-based One-time Password)はシークレット+現在時刻から認証トークンを生成する手法らしい。ちょっと調べたらPythonでこれをいい感じにできる素敵なライブラリ[pyotp](https://github.com/pyauth/pyotp)があったのでこれを使ってシークレットからトークンを作るコードをちまっと書いた。
```python:totp_gen.py
import sys
import pyotp

secret = sys.argv[1]
print(pyotp.TOTP(secret).now())
```
実行するとこんな感じでトークンが出力される。
```bash
❯ python totp_gen.py I65VU7K5ZQL7WB4E
286263
```
この[TOTP MFA Authentication Challenge](https://authenticationtest.com/totpChallenge/)というページでトークンが正常に生成できているかお試しすることができる。👈ログイン成功ヨシ!

# Dockerで実行できるようにする
JenkinsにDocker連携プラグインを入れているとDockerfileを指定して最初からdockerで実行してくれるので、とりあえずこのコードだけが動く環境のDockerfileを書く。pyotpがあればいいので簡単じゃ。
```Dockerfile
FROM python:3.11-alpine3.18

RUN pip install --upgrade pip pyotp
```

# Jenkinsジョブを組み立てる
Jenkinsジョブの中身をJenkinsfileで設定することができるのすてき、~~もっと使ってください先輩~~。
```gradle:Jenkinsfile
pipeline {
    agent {
      dockerfile {
        filename 'Dockerfile'
      }
    }
    parameters {
        choice choices: ['hoge', 'fuga'], description: 'ログインするユーザーID', name: 'USERID'
    }
    stages {
      stage('Generate OTP'){
        steps {
          script {
            sh "echo ${USERID}"
            withCredentials([
                string(
                  credentialsId: "2FA_${USERID}", 
                  variable: 'secret')]) {
              def result = sh script: 'python totp_gen.py ${secret} || echo error', returnStdout: true
              currentBuild.displayName = "${result} : ${USERID}"
            }
          }
        }
      }
    }
}
```
特記するとしたら3点。
1. 前述のとおり[Docker Pipeline](https://plugins.jenkins.io/docker-workflow/)があればagentにdockerfileやimageを指定しておくと最初からそのコンテナでビルドしてくれるので、dockerfileをリポジトリルートからのパスで指定した
1. 実行時のパラメーターでトークンを生成するアカウントを指定できるように設定、トークン生成時にはパラメーターのアカウント名を使って認証情報からシークレットを引っ張り出すようにした
1. 実行完了時、ビルドの表示名が"{トークン} : {ユーザーID}"となるようにした

ここで3つのファイル`totp_gen.py`, `Dockerfile`, `Jenkinsfile`を突っ込んだgitリポジトリを社内のgitサーバーをにpushした。

# Jenkins側の準備をする
## 2FAのシークレットを認証情報に登録する
外部サービスでの2FA認証設定時に表示されるsecretをJenkinsのCredentialsに`secret text`として登録。Jenkinsfileと不整合が起こらないようにIDを`2FA_{外部サービスのid}`にした。もし設定時にQRコードしか表示されない場合はQRコードを読み取れば中にシークレットが入ってる。
## Jenkinsジョブを登録する
新規ジョブ作成から、新しくパイプラインのジョブを作成。ジョブの設定に移行したら、一番下のパイプラインの定義を"Pipeline script from SCM"にしてさっきのファイルがあるリポジトリを指定して保存。

# 完成!
作成したジョブで"パラメーター付きビルド"を実行すると、数秒でビルド履歴にこういうのが現れる。
![スクリーンショット 2023-12-24 0.16.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168044/4750e865-cfaf-0019-3cae-f880cbfec2a0.png)
実際にログインするときは直前にこのジョブを実行しておいて、いざ求められたらビルド履歴を確認するって感じで使うことができる:+1:
実行にかかる時間も気にならないレベルのようで、いまのところクレームはない。
![スクリーンショット 2023-12-24 0.20.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168044/933792be-fc58-581d-f3d6-49df2b2cfa3e.png)

# おわりに
実際に構想してから完成まで2時間もかからなかった。こういうことをサクサクできるようにしてくれるPythonのライブラリ群やdockerのすごさとありがたみを感じました。スクショのため手元に環境を再現するのにちょっと時間がかかったのでやや遅刻です(いいわけ)
Qiitadonの滅亡からしばらく経ちましてmisskey.devもいいところです。運営継続とアドカレ企画に感謝ァ...それではよいクリスマスを〜