🌟

簡易なOIDCサーバー作ってみた。ッヨ!!

2023/12/12に公開

はじめに

基本的にGoogleやAmazon等のプロバイダーを使えば自作する必要性はないが、全体の流れを掴むためにめちゃ簡易に作ってみた。ZennもGoogleのアカウントでしかログインできないのでOIDCを使ってる。はず。もしくはOAuth。

※OAuthで認証させるのはあまり良くない。下記参照
https://www.sakimura.org/2012/02/1487/

OIDC is 何
って人はこの方はこの記事を見れば大体理解できると思う。
https://tech-lab.sios.jp/archives/8651

作ったソースコードはココ
https://github.com/sou1991/golang_authentication_server

前提条件

・Go+Ginで作った
・データべースは使わない。InMemory(ハードコーディング)で代用する。のでアソシエーションとかは意識してない
・テストコードはかなりの最低限。

Here We Go

1.クライアントチェック&return login page

まずOIDCサーバーを使用するためにはプロバイダーと契約して、クライアントIDとクライアントシークレットを発行してもらう必要があるが、今回は登録済みの設定で進める。

entity/client.go

// In Memory Data source
var clients = map[string]string{
	"client_id": "abcde", "client_secret": "hogehogefoookgem=",
}

httpリクエスト

curl http://localhost:8080/outh2/v1/auth?client_id=abcde&response_type=code

response_typeはcodeやtokenといった値を設定することで用途別に返却する値を変える規格の模様。今回はとりあえずcodeのみをサポートする。

// In Memory Data source
var clients = map[string]string{
	"client_id": "abcde", "client_secret": "hogehogefoookgem=",
}

func CheckAvailableClients(c *gin.Context) {
	var params clientAuthParams
	if err := c.ShouldBindQuery(&params); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"message": "Bad Request", "reason": "missing parameter"})
		return
	}

	if params.ResponseType != "code" {
		c.JSON(http.StatusBadRequest, gin.H{"message": "Bad Request", "reason": "invalid response type"})
		return
	}

	if clients["client_id"] != params.ClientId {
		c.JSON(http.StatusBadRequest, gin.H{"message": "Bad Request", "reason": "missing client id"})
	} else {
		c.JSON(http.StatusOK, gin.H{"message": "Success"})
	}

	log.Println("return login page")
}

有効なクライアントだった場合、ログインぺージを返す。今回はページがないのでlogだけで表現

2.ログイン&認可コードの返却

ログイン画面がレスポンスされたら普通にOIDCクライアントに事前に登録してるメールアドレスやパスワードをPOSTする。

curl http://localhost:8080/outh2/v1/auth/login \
    -i \
    -H "Content-Type: application/json" \
    -X "POST" \
    -d '{"email": "auth@example.com","password": "password"}'

entity/user.go

// In memory data
var users = []User{
	//auth@example.com/passwordを暗号化した値
	{
		Email:    "b565fcc69617dde89f5c5d82796ef890",
		Password: "a624f8389727f067e9b89933e4f77d1f",
	},
}
// In memory data
var authorized = []authorizationData

func Authenticate(c *gin.Context) {
	var u User

	if err := c.BindJSON(&u); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"message": "Bad Request"})
		return
	}

	for _, v := range users {
		if v.Email != encryptUserData(u.Email) || v.Password != encryptUserData(u.Password) {
			c.JSON(http.StatusOK, gin.H{"message": "Not Found Account"})
		} else {
			a := authorizationData{Code: "akjd783jek", Expire: time.Now().Add(1 * time.Hour).Unix()}

			authorized = append(authorized, a)

			//https://example.com/auth?code={a.Code}
			log.Println("redirect to client server")

			c.JSON(http.StatusMovedPermanently, gin.H{"message": "redirect to client server"})
		}
	}
}

ログイン成功の場合、認可コードと有効期限を保存して、クライアントサーバーに認可コードを返却する。今回はデータベースはないのでappend(authorized, a)が保存処理の代わり。

3.認証チケット(IDトークン)の返却

クライアントサーバーは先程受け取った認可コードを引き換えにIDトークンをOIDCサーバーに要求します。以下のパラメーターをリクエストします。

curl -X POST "http://localhost:8080/outh2/v1/auth/token" \
	-H 'Content-Type:application/json' \
	-d '{\"code\": \"akjd783jek\", \"client_id\": \"abcde\", \"client_secret\": \"hogehogefoookgem\"}'

各パラメータが一致すればjwt(IDトークン)を返す。暗号化のアルゴリズムはHS256を採用する。
※Google等のプロバイダーは秘密鍵と公開鍵を使って暗号化・復号するの一般的なのでRS256でやってるはず。

entity/user.go

//省略
claims := jwt.MapClaims{
		"user_id": v.Uuid,
		"exp":time.Now().Add(1 * time.Hour).Unix(),
	 }

//HeaderとPayloadを結合
t := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)

//署名をつける
idToken, _ := t.SignedString([]byte("my_sign"))
r := Response{IdToken: Access{idToken}}

c.JSON(http.StatusOK, r)

レスポンス jwt(IDトークン)

{
 "access": {
  "id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MDIyMTE3MzUsInVzZXJfaWQiOiJjMWEzNjFkNjFjZjgzOWZlNzliZjYzNTc0NTRhODhhZSJ9.qRetrfcTtN5yLnbSkw-fMwE9qMJvIEuECZePKh6iUKc"
 }
}

これを受け取ったクライアントサーバーがjwtの署名の部分を共有の鍵で復号して、一致すれば認証される。

最後に

データベースを使ってないので、コードは固有なものになったがちょっとは理解を深めれた

Discussion