1. 記事一覧 >
  2. ブログ記事
category logo

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 のインストールから全手順の紹介になります。

OpenID Connect関係図


本記事情報の設定不足、誤りにより何らかの問題が生じても、一切責任を負いません。

標準的な手順で 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 に移動して、アプリの登録 をクリックします。 Microsoft Entra ID


アプリの登録


+新規作成 をクリックします。

アプリの登録 +新規作成


アプリ情報を入力し、登録 をクリックします。
名前go-oidc-azure-example(任意です。)
サポートされているアカウントの種類この組織ディレクトリのみに含まれるアカウント (<テナント名> のみ - シングル テナント)
リダイレクト URI (省略可能)Web http://localhost:5556/auth/callback

リダイレクト URI については、何でも良いですが、この後、アプリの実装の方で合わせます。

アプリの実装とここで設定した URI がずれている場合、認証が通りません。

なお、http:// が許されるのは、 localhost だけです。

アプリ情報を入力


概要 をクリックして、
アプリケーション (クライアント) ID から clientID(後で行うアプリ側設定項目)を確認しておきます。

概要 アプリケーション (クライアント) ID


証明書とシークレット をクリックし、+新しいクライアント シークレット をクリックします。 証明書とシークレット +新しいクライアント シークレット


説明有効期限 を任意の値に設定し、追加 をクリックします。 説明 有効期限


ここで出てくる の文字列が clientSecret(後で行うアプリ側設定項目)の文字列になります。(シークレット ID の方ではありません。)
二度と表示されないため、ここで、メモっておきます。

値=clientSecret(後で行うアプリ側設定項目)


アプリケーション実装

アプリケーションを実装します。


アプリケーションは以下の仕様とします。
URLhttp://localhost:5556
表示内容<https://graph.microsoft.com/oidc/userinfo レスポンスのJSON>


$ cd ~/
$ mkdir go-oidc-azure-example
$ cd go-oidc-azure-example
$ vi app.go
go-oidc-azure-example/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/ にアクセスします。


login.microsoftonline.com サインイン


login.microsoftonline.com パスワードの入力


login.microsoftonline.com 要求されているアクセス許可


graph.microsoft.com userinfoレスポンス結果


ヨシっ!

loading...