- 記事一覧 >
- ブログ記事
go-oidcを使ってGo言語アプリケーション実装 Azure AD(Microsoft Entra ID)認証
はじめに
go-oidc(https://github.com/coreos/go-oidc)を使って、Go 言語アプリケーション実装 ~ Azure AD(Microsoft Entra ID) を使った OpenID Connect 認証を行ってみました。
基本的には、go-oidc/example/userinfo/app.go というのを見つけて、ID とか書き換えるだけで終了しました。
app.go は、ログイン後、アクセストークンを使って、https://graph.microsoft.com/oidc/userinfo
からログインユーザ自身の情報を取得し、表示しています。
今回、Go のインストールから全手順の紹介になります。
本記事情報の設定不足、誤りにより何らかの問題が生じても、一切責任を負いません。
標準的な手順で Ubuntu 22.04.3 LTS インストールしたての状況からスタートです。
Azure AD(Azure Active Directory)は、Microsoft Entra ID に名称が変わりましたが、この記事では、Azure AD 表記のままでいきます。
検証環境:
Ubuntu 22.04.3 LTS
Go 1.18.1
golang-go インストール
Go 言語をインストールします。
$ sudo apt update -y
$ sudo apt install golang-go -y
$ go version
go version go1.18.1 linux/amd64
Azure AD - アプリの登録
OP(Azure AD)側の設定を行います。
Azure ポータルから、Microsoft Entra ID に移動して、アプリの登録 をクリックします。
+新規作成 をクリックします。
アプリ情報を入力し、登録 をクリックします。
名前: go-oidc-azure-example
(任意です。)
サポートされているアカウントの種類:この組織ディレクトリのみに含まれるアカウント (<テナント名> のみ - シングル テナント)
リダイレクト URI (省略可能):Web
http://localhost:5556/auth/callback
リダイレクト URI については、何でも良いですが、この後、アプリの実装の方で合わせます。
アプリの実装とここで設定した URI がずれている場合、認証が通りません。
なお、
http://
が許されるのは、localhost
だけです。
概要 をクリックして、
アプリケーション (クライアント) ID から clientID(後で行うアプリ側設定項目)を確認しておきます。
証明書とシークレット をクリックし、+新しいクライアント シークレット をクリックします。
説明、有効期限 を任意の値に設定し、追加 をクリックします。
ここで出てくる 値 の文字列が clientSecret(後で行うアプリ側設定項目)の文字列になります。(シークレット ID の方ではありません。)
二度と表示されないため、ここで、メモっておきます。
アプリケーション実装
アプリケーションを実装します。
アプリケーションは以下の仕様とします。
URL:http://localhost:5556
表示内容:<https://graph.microsoft.com/oidc/userinfo レスポンスのJSON>
$ cd ~/
$ mkdir go-oidc-azure-example
$ cd go-oidc-azure-example
$ vi app.go
package main // このコードがmainパッケージに属していることを示します。
import (
"crypto/rand" // 暗号学的に安全な乱数を生成するためのパッケージ
"encoding/base64" // base64エンコーディングとデコーディングを行うためのパッケージ
"encoding/json" // JSONエンコーディングとデコーディングを行うためのパッケージ
"io" // 入出力操作を行うためのパッケージ
"log" // ログメッセージを出力するためのパッケージ
"net/http" // HTTPクライアントとサーバーの実装を提供するパッケージ
"time" // 時間を扱うための関数や型を提供するパッケージ
"github.com/coreos/go-oidc/v3/oidc" // OpenID Connectクライアントを作成するためのパッケージ
"golang.org/x/net/context" // コンテキストを管理するためのパッケージ
"golang.org/x/oauth2" // OAuth2クライアントを作成するためのパッケージ
)
var (
// clientID = os.Getenv("GOOGLE_OAUTH2_CLIENT_ID")
// clientSecret = os.Getenv("GOOGLE_OAUTH2_CLIENT_SECRET")
clientID = "<アプリケーション (クライアント) ID>" // OAuth2クライアントID
clientSecret = "<証明書とシークレットの「値」>" // OAuth2クライアントシークレット
)
func randString(nByte int) (string, error) { // nByte長のランダムな文字列を生成する関数
b := make([]byte, nByte)
if _, err := io.ReadFull(rand.Reader, b); err != nil {
return "", err
}
return base64.RawURLEncoding.EncodeToString(b), nil
}
func setCallbackCookie(w http.ResponseWriter, r *http.Request, name, value string) {
c := &http.Cookie{
Name: name,
Value: value,
MaxAge: int(time.Hour.Seconds()),
Secure: r.TLS != nil,
HttpOnly: true,
// Path: "/",
}
http.SetCookie(w, c) // クッキーを設定
// クッキーにより、リクエスト時のstateを保持して、リダイレクトURLのstateとの一致を後で確認できます。
// 例: Set-Cookie: state=1L7bJe0tMcF7xEDxTrLDnA; Max-Age=3600; HttpOnly
}
func main() { // メイン関数
ctx := context.Background() // 新しい背景コンテキストを作成
// provider, err := oidc.NewProvider(ctx, "https://accounts.google.com")
provider, err := oidc.NewProvider(ctx, "https://login.microsoftonline.com/<ディレクトリ (テナント) ID>/v2.0") // 新しいOpenID Connectプロバイダーを作成
if err != nil {
log.Fatal(err)
}
config := oauth2.Config{ // OAuth2クライアントの設定を作成
ClientID: clientID,
ClientSecret: clientSecret,
Endpoint: provider.Endpoint(),
// RedirectURL: "http://127.0.0.1:5556/auth/google/callback",
RedirectURL: "http://localhost:5556/auth/callback",// Azure AD アプリの登録で設定した リダイレクト URI←一致している必要あり
Scopes: []string{oidc.ScopeOpenID, "profile", "email"},
}
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { // ルートパスに対するハンドラを登録
state, err := randString(16)
if err != nil {
http.Error(w, "Internal error", http.StatusInternalServerError)
return
}
setCallbackCookie(w, r, "state", state)
http.Redirect(w, r, config.AuthCodeURL(state), http.StatusFound) // ユーザーをOAuth2プロバイダーの認証ページにリダイレクト
})
// http.HandleFunc("/auth/google/callback", func(w http.ResponseWriter, r *http.Request) {
http.HandleFunc("/auth/callback", func(w http.ResponseWriter, r *http.Request) { // 認証コールバックに対するハンドラを登録
// リクエスト時state(クッキー)とコールバックURLのstateと一致しするか確認します。
state, err := r.Cookie("state") // Cookieからstateパラメータを取得
if err != nil {
http.Error(w, "state not found", http.StatusBadRequest) // stateパラメータが見つからない場合はエラーを返す
return
}
// ここで、アプリケーションは以前に送信したstateパラメータと一致するか確認します。
if r.URL.Query().Get("state") != state.Value {
http.Error(w, "state did not match", http.StatusBadRequest) // stateパラメータが一致しない場合はエラーを返す
return
}
oauth2Token, err := config.Exchange(ctx, r.URL.Query().Get("code")) // 認証コードをアクセストークンに交換
if err != nil {
http.Error(w, "Failed to exchange token: "+err.Error(), http.StatusInternalServerError) // トークンの交換に失敗した場合はエラーを返す
return
}
userInfo, err := provider.UserInfo(ctx, oauth2.StaticTokenSource(oauth2Token)) // ユーザー情報を取得
if err != nil {
http.Error(w, "Failed to get userinfo: "+err.Error(), http.StatusInternalServerError) // ユーザー情報の取得に失敗した場合はエラーを返す
return
}
resp := struct { // レスポンスを作成
OAuth2Token *oauth2.Token
UserInfo *oidc.UserInfo
}{oauth2Token, userInfo}
data, err := json.MarshalIndent(resp, "", " ") // レスポンスをJSONに変換
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) // JSONの変換に失敗した場合はエラーを返す
return
}
w.Write(data) // レスポンスを書き込む
})
log.Printf("listening on http://%s/", "127.0.0.1:5556") // サーバーがリッスンしているアドレスをログに出力
log.Fatal(http.ListenAndServe("127.0.0.1:5556", nil)) // HTTPサーバーを起動
}
変更点
go-oidc/example/userinfo/app.go から変えた点は、以下です。
・clientID、clientSecret 直書き
・OpenID Connect プロバイダー URL 変更
・callback URL 変更
・コメントによる説明書き追加
state について
state
についてですが、クロスサイトリクエストフォージェリ(CSRF)を防ぐために、自分でチェックしています。
手順としては、以下です。
http://localhost:5556/
にアクセス
↓
クッキーセット
例:Set-Cookie: state=1L7bJe0tMcF7xEDxTrLDnA; Max-Age=3600; HttpOnly
↓
認証ページにリダイレクト
↓
認証成功
↓http://localhost:5556/auth/callback
にリダイレクト(自動的にアクセス)
↓
クッキーの state
とリダイレクト URL(例:http://localhost:5556/auth/callback?...&state=1L7bJe0tMcF7xEDxTrLDnA&...
)との state
の一致を確認
この実装を行わないと、クロスサイトリクエストフォージェリ(CSRF)脆弱性ありのアプリになります。
余談ですが、Cookie を使うため、
http://127.0.0.1:5556
でアクセス → 認証成功 →http://localhost:5556
でリダイレクトとした場合、URL が異なるとみなされるため、"state not found" になります。http://localhost:5556
でアクセスが必要です。これで小一時間ハマりました...。
実行
実行します。
$ go mod init example.com/go-oidc-azure-example
$ go mod tidy
$ go run app.go
2023/12/29 22:06:13 listening on http://127.0.0.1:5556/
ブラウザで http://localhost:5556/
にアクセスします。
ヨシっ!
その他、宣伝、誹謗中傷等、当方が不適切と判断した書き込みは、理由の如何を問わず、投稿者に断りなく削除します。
書き込み内容について、一切の責任を負いません。
このコメント機能は、予告無く廃止する可能性があります。ご了承ください。
コメントの削除をご依頼の場合はTwitterのDM等でご連絡ください。