Simple minds think alike

より多くの可能性を

【Golang】Cookieを認証・暗号化するライブラリ gorilla/cookie を試してみた

最近、仕事でCookie使ったGo言語コードを見かけた際に gorilla/securecookie が使われていたので、このライブラリに関してを調べてみました。その時に調べた内容を書きたいと思います。

ginecho といったGo言語でよく使われているWebフレームワークの内部でも gorilla/securecookie を使っているので、gorilla/securecookie の役割/仕組みを理解することで、goのWebアプリケーションのCookie管理の仕組みを把握できると思います。

前提

概要

公式のREADMEを読むと以下のように書いてありました。

securecookie encodes and decodes authenticated and optionally encrypted cookie values.

securecookie は認証され、オプションで暗号化されたCookieの値をエンコードおよびデコードします。

Secure cookies can't be forged, because their values are validated using HMAC. When encrypted, the content is also inaccessible to malicious eyes. It is still recommended that sensitive data not be stored in cookies, and that HTTPS be used to prevent cookie replay attacks.

セキュアCookieは、その値がHMACを使用して認証されるため、偽造することができません。暗号化された場合、コンテンツは悪意のある目からアクセスすることもできません。

securecookie は、標準パッケージの net/httpのcookie にはない認証と暗号化の仕組みを提供してくれるようです。

標準パッケージの挙動を確認

標準パッケージを使ってCookieを設定するだけのシンプルなコードを試してみました。

package main

import (
    "fmt"
    "log"
    "net/http"
)

func main() {
    http.HandleFunc("/set-cookie", func(w http.ResponseWriter, 
        r *http.Request) {

        cookie := &http.Cookie{
            Name:  "title",
            Value: "SPY x FAMILY",
        }
        http.SetCookie(w, cookie)

        fmt.Fprintf(w, "Cookieをセットしました")
    })

    log.Fatal(http.ListenAndServe(":8080", nil))
}

Webブラウザでパス: /set-cookie、ポート: 8080にアクセスすると以下のようになり、想定したCookieが設定されていることが確認できます。

Cookieはこのようにブラウザツールで簡単に参照、改変できるためセキュリティが低い状態になっています。

securecookieの認証の挙動を確認

securecookieを使ってコードを書き直す

gorilla/securecookie を使ってコードを書き直すと、セキュリティを向上できることを確認します。

securecookie.Newの第1引数にはHMACのハッシュキーを指定します。第2引数には暗号化のためのブロックキーを指定できますが、今回は nil を指定し、暗号化しないようにしてみます。

package main

import (
    "fmt"
    "log"
    "net/http"

    "github.com/gorilla/securecookie"
)

func main() {
    hashKey := []byte("hash-key")
    s := securecookie.New(hashKey, nil)

    http.HandleFunc("/set-cookie", func(w http.ResponseWriter,
        r *http.Request) {

        values := map[string]string{
            "title": "SPY x FAMILY",
        }
        encoded, err := s.Encode("cookie-name", values)
        if err == nil {
            cookie := &http.Cookie{
                Name:     "cookie-name",
                Value:    encoded,
            }
            http.SetCookie(w, cookie)

            fmt.Fprintf(w, "Cookieをセットしました")
        }
    })

    log.Fatal(http.ListenAndServe(":8080", nil))
}

同じようにWebブラウザでアクセスするとCookieの値が変わり、そのままでは読めなくなっていることが分かります。

以下のハンドラーを追加し、Cookie内のハッシュ値を検証し、通れば値をデシリアライズしてCookieの値を表示します。

http.HandleFunc("/show-cookie", func(w http.ResponseWriter,
    r *http.Request) {

    if cookie, err := r.Cookie("cookie-name"); err == nil {
        value := make(map[string]string)

        err = s.Decode("cookie-name", cookie.Value, &value)
        if err == nil {
            fmt.Fprintf(w, "値は%qです。", value["title"])
        } else {
            fmt.Fprintf(w, err.Error())
        }
    }
})

WebブラウザでアクセスするとCookieから取得した値が表示されることを確認できます。

認証の仕組み

上記のコードと同様に、Cookieの名前を"cookie-name"、Cookieのデータ(Name: "title", Value: "SPY x FAMILY")とした場合の認証のフローです。

このフローではオプションである暗号化は考慮していないので機密性が上がるわけではありません。自身のサーバーがハッシュキーを使って保存したものか認証できるようになるだけです。

Cookieの値作成・保存

以下の図のフローでCookieの値を作成、保存します。

ハッシュ値を算出

基本文字列を"cookie-name|【タイムスタンプ】|Cookieデータ"とし、ハッシュキーと合わせてHMACハッシュ値(mac)を算出します。

Cookieに設定する値を作り、保存

次に "【タイムスタンプ】|Cookieデータ|ハッシュ値(mac)"をbase64エンコードし、Cookieとして保存します。

Cookieの取得・検証

以下の図のフローでCookieの値を取得・検証します。

ハッシュ値を取り出し

Cookieから取り出した"【タイムスタンプ】|Cookieデータ|ハッシュ値(mac)"をbase64デコードし、ハッシュ値(mac)を取り出します。

Cookieの検証

次に、"cookie-name"を追加して"cookie-name|タイムスタンプ|Cookieデータ"というデータを作り、ハッシュキーと合わせてHMACでハッシュ値(mac)を算出し、取り出したハッシュ値と値が一致するかで検証できます。

Cookieの内容を改変し、わざと検証失敗させてみる

開発者ツールを使ってCookieの値を改変してみます。

再度Webブラウザ/show-cookie にアクセスすると、検証エラーになるかことを確認できます。

securecookieの暗号化の挙動を確認

securecookieを使ってコードを書き直し、暗号化してみる

securecookie.Newの第2引数には暗号化のためのブロックキーを指定できます。

hashKey := []byte("hash-key")
blockKey := []byte("blocoooooock-key")
s := securecookie.New(hashKey, blockKey)

ぱっと見では、認証だけの場合と何も変わっていないように見えます。

暗号化/複合化の仕組み

オプションである暗号化を有効にすることで機密性が上がります。

以下の図のように認証処理の前に、Cookieのデータに対してAES(CTRモード)で暗号化を行うことで、Webブラウザには平文が保存されないようになります。

複合時は、Cookieの署名検証が問題なければ暗号化されたCookieデータをブロックキーで複合化します。

参考記事

【Golang】slack-goを使ってSlackのリクエスト署名を検証する<解説篇>

以下の記事ではGo言語のライブラリ slack-go を使ってSlackのリクエスト署名検証を行うサンプルアプリを作成しましたが、今回はその検証処理の詳細を確認していきたいと思います。

simple-minds-think-alike.moritamorie.com

なお、Slackのリクエスト署名に関しては以下の公式記事を参考にしました。

前提

  • go: v1.17
  • slack-go: v0.10.3

リクエスト署名の概要

公式の記事によると

リクエスト署名は、Slack がアプリにデータを送信する API メソッドでサポートされています。これには、スラッシュコマンド、Events API リクエスト、インタラクティブ・メッセージ、アクション、メッセージボタン、メッセージメニュー、レガシー Outgoing Webhook が含まれます。

ということで、基本的にSlackからアプリに送信されるHTTPリクエストはリクエスト署名検証できるようになっているようです。

リクエスト署名検証の仕方(理論)

また、リクエスト署名検証には以下の3つが必要なようです。

  • X-Slack-Signature HTTP ヘッダー
  • アプリ固有のシークレットキー( Signing Secret )
  • 基本文字列
    • バージョン番号 (現在は常に v0)
    • X-Slack-Request-Timestamp HTTP ヘッダーの値
    • リクエスト自体のメッセージ本文

X-Slack-Signature HTTP ヘッダー

公式記事に以下のように記載されていました。

このヘッダーには、HMAC-SHA256 キー付きハッシュが設定されています。このハッシュは、アプリ固有のシークレットをキーとして使って計算されます。

具体的にはv0=c11b919c9ea488395441d317....のような値になります。基本文字列とアプリ固有のシークレットでSHA256 ハッシュ計算されたものです。アプリ側でも同じ計算を行い、ハッシュが同じ値になることを確認することで署名検証できます。

アプリ固有のシークレットキー( Signing Secret )

xoxb-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxのような値がアプリ毎に発行され、Slack APIOAuth & Permissionsから取得できます。

基本文字列

バージョン番号 (現在は常に v0)、X-Slack-Request-Timestamp HTTP ヘッダーの値、リクエスト自体のメッセージ本文の順で、コロン (:) で区切って連結し、基本文字列を作成します。

例えば、基本文字列は次のようになります v0:1538352000:token=abc123def456xyz....

リクエスト署名検証の仕方(実装)

上記の検証を行なっている アプリとslack-go の実装を確認していきます。

アプリ側の実装

作成したサンプルアプリmiddleware.goから署名検証を行なっている箇所を抜粋し、処理内容をコメントで追記しています。

以下の①〜③の番号は次の slack-go の実装と関連する箇所を明示しています。

// ①リクエスト署名検証に必要な情報(基本文字列のメッセージ本文以外)を設定し、
//    シークレット検証機を生成
sv, err := slack.NewSecretsVerifier(r.Header,
    os.Getenv("SLACK_SIGNING_SECRET"))
if err != nil {
    w.WriteHeader(http.StatusBadRequest)
    return
}

// ②シークレット検証機に不足情報(基本情報の文字列のメッセージ本文)を追加
if _, err := sv.Write(body); err != nil {
    w.WriteHeader(http.StatusInternalServerError)
    return
}

// ③SHA256 ハッシュ計算し、ハッシュが同じ値になることを確認
if err := sv.Ensure(); err != nil {
    w.WriteHeader(http.StatusUnauthorized)
    return
}

slack-goの実装

署名検証は、security.goの実装に含まれており、そこからの抜粋になります。

①リクエスト署名検証に必要な情報(基本文字列のメッセージ本文以外)を設定し、シークレット検証機を生成

シークレット検証機生成とタイムスタンプのチェックをしています。

func NewSecretsVerifier(header http.Header,
    secret string) (sv SecretsVerifier, err error) {
    var (
        timestamp int64
    )

    stimestamp := header.Get(hTimestamp)

    // シークレット検証機生成
    if sv, err = unsafeSignatureVerifier(header, secret); err != nil {
        return SecretsVerifier{}, err
    }

    // タイムスタンプを10進数(64ビット長)に変換
    if timestamp, err = strconv.ParseInt(stimestamp, 10, 64); err != nil {
        return SecretsVerifier{}, err
    }

    // リクエストヘッダーのタイムスタンプ(X-Slack-Request-Timestamp HTTP ヘッダーの値)と現在時刻を比較し、
    // 5分より長ければエラーにする
    diff := absDuration(time.Since(time.Unix(timestamp, 0)))
    if diff > 5*time.Minute {
        return SecretsVerifier{}, ErrExpiredTimestamp
    }

    return sv, err
}

NewSecretsVerifier の中から呼ばれている unsafeSignatureVerifier の実態は以下の通りです。

const (
    hSignature = "X-Slack-Signature"
    hTimestamp = "X-Slack-Request-Timestamp"
)

func unsafeSignatureVerifier(header http.Header, 
    secret string) (_ SecretsVerifier, err error) {
    var (
        bsignature []byte
    )

    // HTTPヘッダーから署名とリクエストタイムスタンプを取得
    signature := header.Get(hSignature)
    stimestamp := header.Get(hTimestamp)

    if signature == "" || stimestamp == "" {
        return SecretsVerifier{}, ErrMissingHeaders
    }

    // 署名をデコード
    if bsignature, err = hex.DecodeString(
        strings.TrimPrefix(signature, "v0=")); err != nil {
        return SecretsVerifier{}, err
    }

    // シークレットをキーとしてHMACハッシュを生成する
    hash := hmac.New(sha256.New, []byte(secret))

    // ハッシュに"v0:【リクエストタイムスタンプ】"を書き込む
    if _, err = hash.Write(
        []byte(fmt.Sprintf("v0:%s:", stimestamp))); err != nil {
        return SecretsVerifier{}, err
    }

    return SecretsVerifier{
        signature: bsignature,
        hmac:      hash,
    }, nil
}

http.Headerとsecretを引数にとり、検証機を生成して返します。なお、ここで使用している HMAC-SHA-256というアルゴリズムに関しては、別の記事で概要を記載していますので、もしよろしければ参考にしてみてください。

simple-minds-think-alike.moritamorie.com

②シークレット検証機に不足情報(基本情報の文字列のメッセージ本文)を追加
func (v *SecretsVerifier) Write(body []byte) (n int, err error) {
    return v.hmac.Write(body)
}
③SHA256 ハッシュ計算し、ハッシュが同じ値になることを確認
func (v SecretsVerifier) Ensure() error {
    // ハッシュ値を算出
    computed := v.hmac.Sum(nil)

    // 署名と算出したハッシュ値が同じになることを確認
    if hmac.Equal(computed, v.signature) {
        return nil
    }

    if v.d != nil && v.d.Debug() {
        v.d.Debugln(fmt.Sprintf("Expected signing signature: %s, but computed: %s", hex.EncodeToString(v.signature), hex.EncodeToString(computed)))
    }
    return fmt.Errorf("Computed unexpected signature of: %s", hex.EncodeToString(computed))
}

Best Plactice for security

他のSlack APIに関するセキュリティの対応は Best Plactice for security という公式の記事にまとめられています。

api.slack.com

2022年6月時点での目次を転記しておきます。

関連記事

simple-minds-think-alike.moritamorie.com

simple-minds-think-alike.moritamorie.com

【Golang】slack-goを使ってSlackのリクエスト署名を検証する<実装編>

Slack連携アプリのセキュリティを高めたくて、リクエストの署名検証に関して調べてみました。また、調べた内容を元にサンプルアプリを作ってみました。

モチベーション

コロナを機にリモートワークが普及してから、仕事でのコミュニケーションにSlackを使うようになり、便利なSlack連携アプリの使用頻度が上がっています。ただ、Slack連携アプリが公開しているリクエストURLはどこからでもアクセスできるため、リスク対策を十分にする必要性を感じています。

何も対策をしなければ第三者からの攻撃に対して無防備になってしまうため、Slackのリクエスト検証の仕組みに関して正しく知っておきたいというのがモチベーションです。

全体像

この記事では、SlackからのHTTPリクエストを署名検証する処理を含む以下のアプリを実装していきます。

Slackメッセージのショートカットを実行すると、Slackから送られてくるペイロードを受け取り、署名を検証し、問題がなければSlackチャネルにメッセージ投稿する、というシンプルなアプリです。

参考までに、SlackからのHTTPリクエストに含まれるペイロードは以下参照先のデータ形式になります。
Making messages interactive | Slack

Slackでのアプリ設定

Slackチャネルにショートカットを追加し、アプリと連携できるように設定を行なっていきます。

ショートカット設定

Slack APIからアプリを作成

アプリが作成できたら左側メニューの「Interactivity & Shortcuts」を選択し、下図中の①〜③をそれぞれ設定します。

  • ①InteractivityをOnにする
  • ②Request URLを入力
  • ③「Create New Shortcut」を押す

ショートカットの設定ダイアログで「On message」を選択し、メッセージショートカットを設定していきます。

ショートカットの名前、説明、Callback IDにそれぞれ任意の値を設定します。

スコープ追加

画面左メニューの「OAuth & Permissions」を押し、Bot Token Scopesに「chat:write」を追加し、連携アプリの処理結果をSlackチャネルに通知できるようにします。

ワークスペースとチャネルにアプリを追加

「OAuth & Permissions」の「Install to workspace」を押します。

以下2点を控えたら、チャネルのIntegrationの設定から作成したアプリを追加します。

  • Basic Information - App Credentials - Signing Secret
    • 署名を検証するためのシークレット

  • OAuth & Permissions - OAuth Tokens for Your Workspace - Bot User OAuth Token
    • Slackチャネルにメッセージを投稿するためのOAuthトーク

試しに追加したショートカットを実行してみる

Slackチャネルの任意のメッセージに対して先ほど追加した「Verify Request」というショートカットを実行できます。

ただ、今はサーバー側のGoアプリが動いていないので、HTTPリクエストが処理されず、エラーが発生します。

アプリの実装(Goのコード)

コードはmain.goとmiddleware.goの2ファイルに分けて記載しています。

これらはそれぞれ以下の役割を担っています。

  • main.go
    • HTTP80番ポートを待ち受ける
    • Slackからパス/verifing-requestにHTTPリクエストがあれば、handler関数(handleInteraction)を実行する
    • ショートカット独自の処理を行うhandler関数(handleInteraction)を定義
  • middleware.go
    • handlerの処理に介入して、署名検証する

使用ライブラリ

slack-goというライブラリを使用して実装しています。

github.com

また、今回作成したサンプルアプリは slack-go の以下のexampleコードを参考にして作りました。署名検証部分をmiddleware共通処理として切り出していて、保守しやすい綺麗な実装になっていて、参考にするのに良いサンプルのように感じています。

main.go

ショートカット独自の処理を行う handler(handleInteraction) 関数では、SlackからのHTTPリクエストからペイロードを取り出し、メッセージアクションというInteractionTypeであればチャネルに「メッセージアクションが実行されました!」というメッセージ投稿を行っています。

先ほど控えた Bot User OAuth Token環境変数 SLACK_BOT_OAUTH_TOKEN に設定しています。Slack APIクライアント初期化の際に、この環境変数を使用しています。

package main

import (
    "encoding/json"
    "fmt"
    "log"
    "net/http"
    "os"

    "github.com/slack-go/slack"
)

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/verifing-requests", handleInteraction)

    middleware := NewSecretsVerifierMiddleware(mux)
    log.Fatal(http.ListenAndServe(":80", middleware))
}

func handleInteraction(w http.ResponseWriter, r *http.Request) {
    fmt.Println("[START]handleInteraction")

    api := slack.New(os.Getenv("SLACK_BOT_OAUTH_TOKEN"))

    var payload *slack.InteractionCallback
    err := json.Unmarshal([]byte(r.FormValue("payload")), &payload)
    if err != nil {
        log.Println(err)
        w.WriteHeader(http.StatusInternalServerError)
        return
    }

    switch payload.Type {
    case slack.InteractionTypeMessageAction:
        api.PostMessage(payload.Channel.ID,
            slack.MsgOptionText("メッセージアクションが実行されました!", false))
    }

    fmt.Println("[END]handleInteraction")
}

middleware.go

HTTPリクエストのボディ・ヘッダーを読み込み、環境変数 SLACK_SIGNING_SECRET を元に署名検証しています。署名検証の具体的な内容は次の<解説篇>で共有したいと思います。

環境変数 SLACK_SIGNING_SECRET には先ほど控えたSigning Secretを設定します。

package main

import (
    "bytes"
    "fmt"
    "io/ioutil"
    "net/http"
    "os"

    "github.com/slack-go/slack"
)

type SecretsVerifierMiddleware struct {
    handler http.Handler
}

func NewSecretsVerifierMiddleware(
    h http.Handler) *SecretsVerifierMiddleware {
    return &SecretsVerifierMiddleware{h}
}

func (v *SecretsVerifierMiddleware) ServeHTTP(
    w http.ResponseWriter,
    r *http.Request) {

    fmt.Println("[Start]ServeHTTP")

    body, err := ioutil.ReadAll(r.Body)
    if err != nil {
        w.WriteHeader(http.StatusBadRequest)
        return
    }
    r.Body.Close()
    r.Body = ioutil.NopCloser(bytes.NewBuffer(body))

    sv, err := slack.NewSecretsVerifier(r.Header,
        os.Getenv("SLACK_SIGNING_SECRET"))
    if err != nil {
        w.WriteHeader(http.StatusBadRequest)
        return
    }

    if _, err := sv.Write(body); err != nil {
        w.WriteHeader(http.StatusInternalServerError)
        return
    }

    if err := sv.Ensure(); err != nil {
        w.WriteHeader(http.StatusUnauthorized)
        return
    }

    fmt.Println("before hander.ServeHTTP")

    v.handler.ServeHTTP(w, r)

    fmt.Println("[END]ServeHTTP")
}

再度Slack上からショートカットを実行してみる

Goコードのアプリをサーバー上にデプロイして、SlackからのHTTPリクエストを処理できるようになったところで、再度「Verify Request」ショートカットを実行してみます。

メッセージ投稿が表示され、期待した結果になっていることが分かります。

コンソール出力は以下のようになりました。

[Start]ServeHTTP
before hander.ServeHTTP
[START]handleInteraction
[END]handleInteraction
[END]ServeHTTP

SecretsVerifierMiddlewareServeHTTP メソッドが実行され、SlackからのHTTPリクエストの署名検証が行われた後、ショートカット独自のhandlerが動いていることを確認できます。

次の記事では、署名検証の内容を読み解いてみます。

simple-minds-think-alike.moritamorie.com

参考コード

コードの方はGithubにも挙げてみたのでもしよろしければ参考にしてみてください。 github.com

関連記事

simple-minds-think-alike.moritamorie.com

参考資料

Slack APIを通じてメッセージ投稿する

GolangのコードからSlack APIアクセスを試してみたくて、Slackの設定方法を調べてみました。今回はGolangのコードからではなくcurlを叩いてSlackメッセージ投稿できるところまでの手順を記載してみようと思います。

今回curlからアクセスするのはchat.postMessageというメッセージ投稿用のAPIメソッドです。

参考にしたドキュメント

公式Slack API 日本語版ページの「チュートリアル - Bolt フレームワークを使って Slack Bot を作ろう」と「権限 (スコープ) - ボットの OAuth スコープについて」を参考にしてみました。

これらのドキュメントは少し古く、現状のSlackのUIと異なっているところがあるため2022年6月時点のUIを元に記事を書きます。

アプリを作成し、ワークスペースにインストールする

Slack API: https://api.slack.com/appsにアクセスします。

「Create an App」を押します。

表示されるダイアログで「From scrach」を押します。これはSlackのUI上からアプリの設定を行う方法です。下の「From an app manifest」はマニフェストファイルを使ってアプリの設定を行う方法で、同じような設定を複数回行う場合にはこちらの方法が早いです。

アプリの名前とアプリをインストールするワークスペースを選択し、「Create App」を押します。

作成したアプリにOAuthスコープの追加

アプリがSlack APIにアクセスするためには、アプリで使用できるユーザートークンまたはボットトークンに対して適切なOAuthスコープを追加する必要があります。

ユーザートークンとボットトーク

トークンは、ユーザートークンとボットトークンに分かれます。Bolt入門ガイドに記載されている説明を引用します。

・ユーザートークン を使用すると、アプリをインストールまたは認証したユーザーに成り代わって API メソッドを呼び出せます。1 つのワークスペースに複数のユーザートークンが存在する可能性があります。
・ボットトークン はボットユーザーに関連づけられ、1 つのワークスペースでは最初に誰かがそのアプリをインストールした際に一度だけ発行されます。どのユーザーがインストールを実行しても、アプリが使用するボットトークンは同じになります。ほとんどのアプリで使用されるのは、ボットトークンです。

認証したユーザーに成り代わってメッセージ投稿するのではなく、ボットとしてメッセージ投稿させたいのでボットトークンの方にOAuthスコープを設定します。

スコープの概要と適切なスコープの探し方

ボットの OAuth スコープについて、という記事に記載されています。

今回curlからアクセスする chat.postMessage というAPIメソッドのドキュメント(下記リンク)を参照すると、Required scopesに「chat:write」が指定されているため、ボットトークンにこのOAuthスコープを追加します。
https://api.slack.com/methods/chat.postMessage

設定

左側メニューのFeaturesカテゴリにある「OAuth & Permissions」を選択。

ScopesのBot Token Scopesに、チャットへの書き込み権限「chat:write」を追加します。

「Install to Workspace」を押し、選択したワークスペースに対して作成したアプリをインストールします。

画面上部にBot User OAuth Tokenが表示されます。

Slackチャネルにアプリをインストールする

次にSlackワークスペース画面右上のアイコンを押し、チャネルの設定ダイアログを開きます。「インテグレーション」タブにある「アプリを追加する」ボタンを押します。

先ほど作成した「API TEST」アプリを追加します。

無事追加されると参加者としてメッセージが表示されます。

curlでSlackチャネルにメッセージを投稿する

発行したBot User OAuth Token、連携したチャネル名、投稿したいメッセージを指定してPOSTを送ります。これらの引数はAPIメソッドのドキュメントで必須(Required)として指定されている項目です。

$ curl -X POST 'https://slack.com/api/chat.postMessage' \
       -d 'token=your_bot_user_oauth_token' \
       -d 'channel=#general' \
       -d 'text=テスト'

投稿したメッセージが表示されます。

関連記事

simple-minds-think-alike.moritamorie.com

GitLab CI/CDのQuick Startチュートリアルをやってみた

仕事のソースコード管理にGitLabを使っていて、CI/CDに関してもGitLab CI/CDを使っているのですが、いまいち仕組みがよく分からなかったので学習のために以下のQuick Startチュートリアルをやってみました。その時の手順を書いてみます。

また、普段はセルフホストのGitLabを使っていますが、今回は手頃なGitlab.com環境で試してみます。

docs.gitlab.com

事前に行ったこと

作成したGitLabリポジトリ

以下の手順で作成したCI/CDプロジェクトを作ってみました。もし、よろしければ実際にどのように結果が出力されるか等確認してみてください。 gitlab.com

CI/CDプロセスの概要

GitLab CI/CDを使うために以下を行いました。

  • CI/CDジョブを動かすGitLab Runnerが利用可能か確認
  • リポジトリのルートに .gitlab-ci.yml(ジョブを定義するファイル) 作成

CI/CDジョブを動かすGitLab Runnerが利用可能か確認

GitLab公式のドキュメントによると、GitLab RunnerとはGitLab CI/CDと連携し、パイプラインでジョブを実行するためのアプリケーションとのことです。

GitLab.comのリポジトリを開き、[設定]=>[CI/CD]の Runnerの箇所で稼働中のRunnerを確認できます。 f:id:moritamorie:20220123004106p:plain

GitLab Runnerには以下の2種類のRunnerがあるようで、GitLabでリポジトリを作成した時点で最初からShared runnersは利用可能なようでした。

  • Shared runners (複数プロジェクトのジョブを共有のRunnerで処理する方式)
  • Specific runners (特定のプロジェクトのジョブのみ実行する方式)

Shared runnersでCI/CDジョブを処理する分には、gitlab.comでRunnerに関する事前作業は必要なさそうです。次に.gitlab-ci.yml(ジョブを定義するファイル)を作成していきます。

リポジトリのルートに.gitlab-ci.yml 作成

.gitlab-ci.yml はGitLab CI/CDの具体的な指示を設定するYAMLファイルとのことです。

リポジトリのルートに以下内容の .gitlab-ci.yml を作成します。作成方法は、GitLabのUI上で作成する、またはローカルでファイルを作成してgit pushするでもどちらでもOK。

build-job:
  stage: build
  script:
    - echo "Hello, $GITLAB_USER_LOGIN!"

test-job1:
  stage: test
  script:
    - echo "このジョブは何かをテストする"

test-job2:
  stage: test
  script:
    - echo "このジョブは何かをテストしますが、test-job1より時間がかかります"
    - echo "echoコマンドの後、20秒のsleepコマンドを実行します"
    - echo "これはtest-job1より20秒長いテスト実行をシミュレートしてます"
    - sleep 20

deploy-prod:
  stage: deploy
  script:
    - echo "このジョブは $CI_COMMIT_BRANCH ブランチから何かをデプロイします"

commitが作られると自動的にパイプラインが作られます。

パイプラインの確認

CI/CD > パイプラインを選択すると実行されているパイプラインを表示できます。 f:id:moritamorie:20220123013942p:plain

ステータスをクリックすると、パイプラインの詳細情報を表示できます。 f:id:moritamorie:20220123014042p:plain

stageがbuild, test, deployの順でジョブが実行され、全てのジョブが成功するとパイプラインのステータスも「成功」に変わります。

f:id:moritamorie:20220123014715p:plain

.gitlab-ci.yml tips

ここまででQuick Startは終わりですが、記事中の.gitlab-ci.yml tipsという項目を読んでおくと、色々なCI/CDを実装する上でのベースとなる知識が得られて良さそうでした。内容を記載しておきます。

  • .gitlab-ci.ymlファイルを作成したら、それ以降の編集はすべてパイプラインエディタで行ってください。パイプラインエディタを使用すると、次のことができます。
    • パイプラインの設定を、自動的な構文強調表示と検証を使用して編集する。
    • CI/CD設定の視覚化、.gitlab-ci.ymlファイルのグラフィック表示の表示。
  • ランナーがDockerコンテナを使ってジョブを実行する場合は、.gitlab-ci.ymlファイルを編集してイメージ名を記述してください。
    • default:
        image: ruby:2.7.4
      
  • 各ジョブにはスクリプトとステージが含まれます。
    • default キーワードは、before_scriptafter_script などのデフォルトの設定をカスタムするためのものです。
    • ステージは、ジョブの順次実行を記述します。1つのステージのジョブは、利用可能なランナーがある限り、並行して実行されます。
    • ステージの順序から外れてジョブを実行するには、Directed Acyclic Graphs (DAG)キーワードを使用します。
  • ジョブやステージの実行方法をカスタマイズするために、追加で設定できます。
    • rules キーワードを使用して、ジョブを実行またはスキップするタイミングを指定します。従来のキーワードであるonlyとexceptはまだサポートされていますが、同じジョブでルールと一緒に使用することはできません。
    • キャッシュアーティファクトを使用して、ジョブやステージ間の情報をパイプラインで永続的に維持します。これらのキーワードは、各ジョブでエフェメラルランナーを使用している場合でも、依存関係やジョブ出力を保存するための方法です。
    • .gitlab-ci.ymlの完全な構文については、.gitlab-ci.ymlの完全なリファレンス・トピックを参照してください。

実際のプロジェクトへの適用に際して

WEB IDEでテンプレートが用意されているため、プロジェクトに適したテンプレートを選択すると楽にCI/CDを導入できそうです。 f:id:moritamorie:20220123160552p:plain

感想

Github Actionsの.github/workflows とあまり変わらないのでYAMLの定義は難しくなさそうと感じましたが、日本語のドキュメントが少なく細かい設定を調べるのが少し大変そうだなと思いました。

また、Github Actionsと異なる点として、Runnerの種類(Shared Runners・Specific Runners)があったりと色々なことができそうですが、その反面設定が複雑で分かりにくく感じました。

参考資料

【Golang】go-oidcでHS256に対応する

以下の記事でgo-oidcというサードパーティライブラリを使用してOpenID ConnectでAuth0連携アプリケーションを構築する記事を書きましたが、JWT(JSON Web Token)署名に非対称暗号アルゴリズムを使っているRS256の対応方法だけしか書いていませんでした。今回は対称暗号アルゴリズムを使っているHS256に対応する方法を書いてみようと思います。

simple-minds-think-alike.moritamorie.com

前提

仕様

go-oidcを利用した実装の前にどのような署名検証を実現したいか、仕様の面を書いていきたいと思います。

HS256はどんなアルゴリズム

まず、HS256はどのようなアルゴリズムかというOpenID Connectとは直接的には関係がない一般的な話を書いてみようと思います。

HS256はHMAC-SHA-256 のことです。

HMACとは

まず、MAC (Message Authentication Code)はメッセージ認証コードのことです。

メッセージ認証コードは、暗号技術の一つです。対称・非対称暗号を用いてメッセージを暗号化すると機密性を得られますが、例えば通信経路の中で改竄が行われるとそれを検知できないので、暗号化によって完全性は得られません。メッセージ認証コードを使うと完全性を得られるようになります。

メッセージ認証コードは様々な方法で実現可能ですが、HMACは一方向ハッシュ関数(先頭HはHashの頭文字を取ったもの)を使って実現します。 HMAC-SHA-256(HS256) は、一方向ハッシュ関数にSHA-256を使ってメッセージ認証コードを計算する方法のことです。

HMACのプロセスと完全性の検証方法

計算方法など具体的な解説は省きますが、ResearchGateから引用したHMACのプロセス図を掲載しておきます。 https://www.researchgate.net/publication/346634579/figure/fig2/AS:965112139636738@1607112073858/Hash-Message-Authentication-Code-HMAC-process-332-Overview-of-SHA-256-SHA-256.png

メッセージ(Message)と共通鍵(Key)を元に、この図のプロセスで計算すると最終的にMAC値が算出されます。一方向ハッシュ関数の性質により、同じ鍵を持つ人ではないと同じMAC値を算出できないので、メッセージの受信者は改竄されたかを後から検証できます。(完全性が得られる。)

図の一方向ハッシュ関数(HASH Function)にSHA-256を使ったものがHMAC-SHA-256(HS256)というアルゴリズムです。

OpenID ConnectにおけるJWT署名

さて、話を一般的なメッセージ認証コードの話からOpenID Connectに戻し、JWTの署名がどのようなものになっているか、その中でHS256がどのように使われているかを確認していきます。

JWTのEncoding

JWTのEncodingの例は以下に記載されています。

ざっくり書くと以下の図のように、ヘッダー・ペイロード・署名の内容をBase64エンコードして.(ピリオド)で繋げて表現します。

JWTの最後の部分が署名部分だということが分かります。

JWTの具体的な例

ざっくりとしたEncodingの説明では具体的なイメージが湧かないと思いますので、jwt.ioというツールを使ってJWTを作ってみたいと思います。

jwt.ioを開いて、右側のDecodedのところに上からJWTのヘッダー・ペイロード・共通鍵(以下の図ではmy-common-keyの箇所)を入力すると、左側のEncodedでJWTが得られます。

ヘッダーのalgHS256になっているので、ペイロードをメッセージ、my-common-keyを共通鍵としてHS256で算出されたMAC値が署名部分になります。

署名検証の方法は以下に記載されており、JWTの受信者は共通鍵を使用することで署名検証できます。

実装

ここまで go-oidc を使って実現したいHS256の署名検証の方法を書いてきましたが、2022年1月時点で go-oidcHS256 をサポートしていないため部分的に独自で実装する必要があります。(参照)

HS256がサポートされていない背景

以下のissueを読んだところ、対称アルゴリズムと非対称アルゴリズムを混ぜるとAPIの誤用に繋がりかねないため、意図的な制約としてサポートしていないようです。

github.com

しかし、内部で署名検証に利用している go-jose ではHS256をサポートしているため、これを利用して実装できます。

実装

以下のように独自のVerifierを実装すると実現できます。 go-joseのREADMEを読むとSupported key types(Algorithm: HMAC)は []byte と記載されているので、共通鍵をバイトのスライスに変換して Verify関数の引数に渡します。

type HS256KeySet struct {
    CommonKey string
}

func (r *HS256KeySet) verify(_ context.Context, jws *jose.JSONWebSignature) ([]byte, error) {
    return jws.Verify([]byte(r.CommonKey))
}

func (r *HS256KeySet) VerifySignature(ctx context.Context, jwt string) ([]byte, error) {
    jws, err := jose.ParseSigned(jwt)
    if err != nil {
        return nil, fmt.Errorf("oidc: malformed jwt: %v", err)
    }
    return r.verify(ctx, jws)
}

func callback(code, state string) {

    // rawIDToken取得

    oidcConfig := &oidc.Config{
        ClientID:             "my-client-id",
        SupportedSigningAlgs: []string{"HS256"},
    }

    verifier := oidc.NewVerifier("https://issuer-domain.com/", 
        &HS256KeySet{CommonKey: "my-common-key"}, oidcConfig)
    idToken, err := verifier.Verify(context.Background(), rawIDToken)

    // 署名検証後のNonceの検証等
}

独自に作ったVerifierのVerifySignature関数は以下のタイミングで実行されます。

このように実装することで、OIDC認証の要件に必要なExpiryやIssuer, ClientIDのチェック等はサードパーティライブラリ(go-oidc)に任せられるようになり、署名検証の部分だけ独自のVerifierに実装できます。

参考資料

【Golang】Auth0のOpenID Connect連携アプリを作るチュートリアルをやってみた

仕事でOpenID Connect(以降OIDC)連携の実装した際、勉強のためにAuth0連携のサンプルアプリを作ってみたので、その一連の手順をまとめてみたいと思います。

手順は基本的にAuth0のチュートリアル通りなのですが、補足文章をつけて書いてみたいと思います。

手順は2022年1月時点のもので、今後Auth0の仕様変更により手順が変わる可能性がありますので、最新の情報はAuth0のドキュメントを確認してください。

サンプルで実現するもの

以下のような画面遷移のサンプルを作ります

  • ログイン画面を表示し、サインインリンクを押すとOIDC認証を開始
  • Auth0のログイン画面を表示
  • Auth0にログインしたユーザーのプロフィール情報(ニックネーム・写真)をユーザー画面に表示

f:id:moritamorie:20220110141908p:plain

既にAuth0にログインした状態で「サインイン」リンクが押された場合は、Auth0のログイン画面を経由せずにログイン後のユーザー画面に遷移します。

サンプルで実現するシステムの構成

サンプルで作る構成全体は以下のようになり、認可コードフローによってOIDC認証します。

f:id:moritamorie:20220110143321j:plain

環境

Goと使用するOIDCライブラリのバージョンは以下の通りです

  • Go 1.16
  • go-oidc 3.1.0

サンプル作成手順

Auth0側の設定、サンプルアプリケーション(Goプロジェクト側のコード)という順番で手順を書いていきます。

Auth0側の設定

Auth0側でアプリケーションを作成し、Auth0への接続情報を取得します。またAuth0ログイン後のリダイレクト先であるサンプルアプリケーションのコールバックURLを設定します。

アプリケーションの作成

メニューの「Applications」から「+Create Application」ボタンを押して、アプリケーションを作成するダイアログを表示します。

ダイアログ上でアプリケーション名を入力し、「Regular Web Application」を選択後「Create」ボタンを押し、アプリケーションを作成します。 f:id:moritamorie:20220105152048p:plain

アプリケーションの設定から取得する情報

作成されたアプリケーションの「Settings」タブを選択し、以下の3つの情報を控えておきます。

  • Domain
  • Client ID
  • Client Secret

f:id:moritamorie:20220105235044p:plain

アプリケーションに設定する内容

また「Settings」タブにおいて、Auth0にログインした後にリダイレクトするコールバックURLを設定します。

  • Callback URL
    • http://localhost:3000/callback

f:id:moritamorie:20220106115154p:plain

デフォルトの設定に関して

大体の要件では上記の設定でうまくいくと思いますが、要件に応じて変更する必要がありますがある以下の2点に関しては押さえておく必要があると思います。

  • 署名アルゴリズム
    • デフォルトではRS256で署名がされます。
    • HS256にしたい場合は設定変更及びHS256で署名検証するように実装も変更する必要があります。
  • OIDC Comfrontの設定
    • 厳密にOIDCの仕様に従うようになる設定です。デフォルトはONです。
    • 既にOIDC連携のアプリケーションの実装がある状態でAuth0に関しても連携できるようにしたいという場合で、厳密にOIDCの仕様に則っていない場合はOFFにする必要があるかもしれません。

これらはアプリケーションの「Settings」タブの下部にある「Advanced Settings」の「Auth」タブで設定できます。

サンプルアプリケーション(Goプロジェクト)

以下の機能を持つサンプルアプリケーションを作っていきます。

  • アプリケーションのログイン画面(home.html)を返すだけの無名ハンドラ
  • ログイン画面でサインインリンクを押した時のログインハンドラ
  • Auth0ログイン後のコールバック処理のハンドラ
  • コールバック処理でOIDC認証が終わった後にリダイレクトされるユーザー画面(user.html)を返すハンドラ

ディレクトリ構成

最終的なディレクトリ・ファイル構成は以下のようになります。

├── go.mod
├── go.sum
├── .env
├── main.go(認証器、ルーターを生成し、ポート3000でブラウザからのリクエストを待ち受ける)
├── web
│     ├── app
│     │     ├── user
│     │     │     └── user.go (ユーザー画面表示のハンドラ)
│     │     ├── login
│     │     │     └──login.go (ログイン処理のハンドラ)
│     │     └── callback
│     │           └── callback.go (コールバック処理のハンドラ)
│     └── template
│           ├──user.html (ユーザー画面のテンプレート)
│           └──home.html (ログイン画面のテンプレート)
└── platform
      ├── authenticator
      │     └── auth.go (認証器:Outh2設定、OIDCプロバイダー生成)
      └── router
            └── router.go (ルーター:セッション初期設定、HTMLテンプレートパス設定、パスとハンドラの対応付けを行う)

go.mod作成

空のgo.modファイルを作成し、依存関係があるライブラリを記載します。

$ go mod init github.com/moritamori/go-oidc-sample
module github.com/moritamori/go-oidc-sample

go 1.16

require (
  github.com/coreos/go-oidc/v3 v3.1.0
  github.com/gin-contrib/sessions v0.0.3
  github.com/gin-gonic/gin v1.7.4
  github.com/joho/godotenv v1.4.0
  golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
)

go.sum生成

$ go mod download

.envファイル作成

Auth0のsettings tabで取得・設定した内容を.envファイルに記載します。

# Auth0テナントドメイン
AUTH0_DOMAIN='dev-a4jtqeow.us.auth0.com'

# Auth0アプリケーションのClient ID.
AUTH0_CLIENT_ID='YOUR_CLIENT_ID'

# Auth0アプリケーションのClient Secret.
AUTH0_CLIENT_SECRET='YOUR_CLIENT_SECRET'

# アプリケーションのコールバックURL
AUTH0_CALLBACK_URL='http://localhost:3000/callback'

アプリケーションのエントリーポイント( main.go )

main.go では主に認証器、ルーターを生成し、ポート3000でブラウザからのリクエストを待ち受けます。

package main

import (
    "log"
    "net/http"

    "github.com/joho/godotenv"

    "github.com/moritamori/go-oidc-sample/platform/authenticator"
    "github.com/moritamori/go-oidc-sample/platform/router"
)

func main() {
    if err := godotenv.Load(); err != nil {
        log.Fatalf("envファイルの読み込みに失敗しました: %v", err)
    }

    auth, err := authenticator.New()
    if err != nil {
        log.Fatalf("認証器の生成に失敗しました: %v", err)
    }

    rtr := router.New(auth)

    log.Print("Server listening on http://localhost:3000/")
    if err := http.ListenAndServe("0.0.0.0:3000", rtr); err != nil {
        log.Fatalf("HTTPサーバーの起動時にエラーが発生しました: %v", err)
    }
}

ルーター作成

セッションの初期設定とHTMLテンプレートパス設定、パスとハンドラの対応付けをしています。 ログイン画面だけルーターの中で無名関数を作り、ルートパスにアクセスされた時にログイン画面のテンプレート(home.html) を返します。

package router

import (
    "encoding/gob"
    "net/http"

    "github.com/gin-contrib/sessions"
    "github.com/gin-contrib/sessions/cookie"
    "github.com/gin-gonic/gin"

    "github.com/moritamori/go-oidc-sample/platform/authenticator"
    "github.com/moritamori/go-oidc-sample/web/app/callback"
    "github.com/moritamori/go-oidc-sample/web/app/login"
    "github.com/moritamori/go-oidc-sample/web/app/user"
)

// New はroutesを登録し、ルーターを返す
func New(auth *authenticator.Authenticator) *gin.Engine {
    router := gin.Default()

    // セッション初期設定
    gob.Register(map[string]interface{}{})
    store := cookie.NewStore([]byte("secret"))
    router.Use(sessions.Sessions("auth-session", store))

    // HTMLテンプレートパス設定
    router.LoadHTMLGlob("web/template/*")

    // 無名ハンドラ
    router.GET("/", func(ctx *gin.Context) {
        ctx.HTML(http.StatusOK, "home.html", nil)
    })
    // ログイン処理のハンドラ
    router.GET("/login", login.Handler(auth))
    // コールバックハンドラ
    router.GET("/callback", callback.Handler(auth))
    // ユーザーハンドラ
    router.GET("/user", user.Handler)

    return router
}

ログイン画面のHTML(home.html)

ルートパスにアクセスされた時に返るログイン画面のHTML。「サインイン」リンクがあるだけのシンプルな画面。

<div>
    <h3>Auth0 OpenID Connectサンプル</h3>
    <a href="/login">サインイン</a>
</div>

ログイン処理のハンドラ

ログイン画面の「サインイン」リンクを押すと/loginにGETリクエストが送られ、ログイン処理のハンドラで処理されます。

ログイン処理のハンドラでは、ランダムなstate値を生成し、セッションに保存後、AuthCodeURLにリダイレクトします。AuthCodeURLは、Discovery Endpoint(http://dev-a4jtqeow.us.auth0.com/.well-known/openid-configuration)のauthorization_endpointから取得したURLを使います。

package login

import (
    "crypto/rand"
    "encoding/base64"
    "net/http"

    "github.com/gin-contrib/sessions"
    "github.com/gin-gonic/gin"

    "github.com/moritamori/go-oidc-sample/platform/authenticator"
)

// Handler はログイン処理のハンドラ
func Handler(auth *authenticator.Authenticator) gin.HandlerFunc {
    return func(ctx *gin.Context) {
        state, err := generateRandomState()
        if err != nil {
            ctx.String(http.StatusInternalServerError, err.Error())
            return
        }

        // セッションにstateを保存
        session := sessions.Default(ctx)
        session.Set("state", state)
        if err := session.Save(); err != nil {
            ctx.String(http.StatusInternalServerError, err.Error())
            return
        }

        // 認証コードURLにリダイレクト
        ctx.Redirect(http.StatusTemporaryRedirect,
            auth.AuthCodeURL(state))
    }
}

func generateRandomState() (string, error) {
    b := make([]byte, 32)
    _, err := rand.Read(b)
    if err != nil {
        return "", err
    }

    state := base64.StdEncoding.EncodeToString(b)

    return state, nil
}

コールバックのハンドラ

コールバックハンドラは、Auth0の認証後にリダイレクトされてアクセスされます。

クエリパラメータによって渡される認可コードをアクセストークン、IDトークンと交換し、トークン内の署名検証(RS256)して問題がなければ、プロフィール情報をセッションに保存し、ログイン後のユーザー画面にリダイレクトします。

package callback

import (
    "net/http"

    "github.com/gin-contrib/sessions"
    "github.com/gin-gonic/gin"

    "github.com/moritamori/go-oidc-sample/platform/authenticator"
)

// Handler はコールバックのハンドラ
func Handler(auth *authenticator.Authenticator) gin.HandlerFunc {
    return func(ctx *gin.Context) {
        session := sessions.Default(ctx)
        if ctx.Query("state") != session.Get("state") {
            ctx.String(http.StatusBadRequest,
                "stateパラメータが無効です")
            return
        }

        // 認可コードをトークンに交換する
        token, err := auth.Exchange(ctx.Request.Context(), ctx.Query("code"))
        if err != nil {
            ctx.String(http.StatusUnauthorized,
                "認可コードからトークンへの交換に失敗しました")
            return
        }

        idToken, err := auth.VerifyIDToken(ctx.Request.Context(), token)
        if err != nil {
            ctx.String(http.StatusInternalServerError,
                "IDトークンの検証に失敗しました")
            return
        }

        var profile map[string]interface{}
        if err := idToken.Claims(&profile); err != nil {
            ctx.String(http.StatusInternalServerError, err.Error())
            return
        }

        session.Set("access_token", token.AccessToken)
        session.Set("profile", profile)
        if err := session.Save(); err != nil {
            ctx.String(http.StatusInternalServerError, err.Error())
            return
        }

        // ログイン後のユーザー画面にリダイレクト
        ctx.Redirect(http.StatusTemporaryRedirect, "/user")
    }
}

認証情報作成(Outh2設定、OIDCプロバイダー生成)

以下の2つを生成し、それらのデータを持つ認証器を返します。

  • OpenID Discoveryを元にOIDC認証を行うプロバイダー
  • OAuth接続の設定情報(Auth0側で取得・設定したClient ID、Client Secret、Callback URLなどです)

また、この認証器にはIDトークンを検証し、問題がなければIDトークンを返す関数(VerifyIDToken)があります。

package authenticator

import (
    "context"
    "errors"
    "os"

    "github.com/coreos/go-oidc/v3/oidc"
    "golang.org/x/oauth2"
)

// Authenticator はユーザーを認証するための認証器
type Authenticator struct {
    *oidc.Provider
    oauth2.Config
}

// New はユーザーを認証するための認証器を生成する
func New() (*Authenticator, error) {
    provider, err := oidc.NewProvider(
        context.Background(),
        "https://"+os.Getenv("AUTH0_DOMAIN")+"/",
    )
    if err != nil {
        return nil, err
    }

    conf := oauth2.Config{
        ClientID:     os.Getenv("AUTH0_CLIENT_ID"),
        ClientSecret: os.Getenv("AUTH0_CLIENT_SECRET"),
        RedirectURL:  os.Getenv("AUTH0_CALLBACK_URL"),
        Endpoint:     provider.Endpoint(),
        Scopes:       []string{oidc.ScopeOpenID, "profile"},
    }

    return &Authenticator{
        Provider: provider,
        Config:   conf,
    }, nil
}

// VerifyIDToken は*oauth2.Tokenが有効なトークンであることを検証する
func (a *Authenticator) VerifyIDToken(ctx context.Context,
    token *oauth2.Token) (*oidc.IDToken, error) {
    rawIDToken, ok := token.Extra("id_token").(string)
    if !ok {
        return nil, errors.New("oauth2 tokenにid_tokenフィールドがありませんでした")
    }

    oidcConfig := &oidc.Config{
        ClientID: a.ClientID,
    }

    return a.Verifier(oidcConfig).Verify(ctx, rawIDToken)
}

ユーザー画面のハンドラ

OIDC認証後にコールバックハンドラからリダイレクトされてアクセスされます。

セッションからログインユーザーのプロフィール情報を取り出し、値をテンプレートuser.htmlに渡し、生成したHTMLのレスポンスを返します。

package user

import (
    "net/http"

    "github.com/gin-contrib/sessions"
    "github.com/gin-gonic/gin"
)

// Handler はログイン済みユーザー画面のハンドラ
func Handler(ctx *gin.Context) {
    session := sessions.Default(ctx)
    profile := session.Get("profile")

    ctx.HTML(http.StatusOK, "user.html", profile)
}

ユーザー画面のHTML(user.html)

ユーザー画面のハンドラから読み込まれるユーザー画面のHTMLテンプレート。ハンドラからプロフィール情報を受け取り、展開します。

<div>
    <img class="avatar" src="{{ .picture }}"/>
    <h2>Welcome {{.nickname}}</h2>
</div>

gitリポジトリにpushする

事前にGithubリポジトリを作って、PUSHしておきます。

作ったサンプルを動かしてみる

$ go run main.go
[GIN-debug] Loaded HTML Templates (3):
    -
    - home.html
    - user.html

[GIN-debug] GET    /            --> github.com/moritamori/go-oidc-sample/platform/router.New.func1 (4 handlers)
[GIN-debug] GET    /login       --> github.com/moritamori/go-oidc-sample/web/app/login.Handler.func1 (4 handlers)
[GIN-debug] GET    /callback    --> github.com/moritamori/go-oidc-sample/web/app/callback.Handler.func1 (4 handlers)
[GIN-debug] GET    /user        --> github.com/moritamori/go-oidc-sample/web/app/user.Handler (4 handlers)
2022/01/06 14:56:21 Server listening on http://localhost:3000/

http://localhost:3000Webブラウザからアクセスするとログイン画面が表示され、サインインリンクをクリックし、OIDC連携の動作を確認できます。

f:id:moritamorie:20220106102448p:plain

リポジトリ

コードをGithubリポジトリにあげてみたのでよろしければ参考にしてみてください。 github.com

関連記事

本記事内では署名検証にHS256に対応する方法も記載してみましたので、もしよろしければご参考ください。

simple-minds-think-alike.moritamorie.com

参考資料