Go で OIDC RP が実装できるライブラリを探す

## はじめに

OIDC RP を実装する場合以下のようなことをする必要があります。
※ 認可コードグラントです。

- ディスカバリ構成の管理
- 認可エンドポイントの生成
- トークンエンドポイントへのアクセス
- トークンの検証

etc ...

OIDC はカッチリ仕様が決まっていますし、そこまで複雑な実装にもならないと思いますが、ライブラリがあるなら頼りたいものです。
本記事では Go で OIDC RP をライブラリを用いて実装するにあたり候補となるライブラリを調査し実際に使ってみたのでご紹介します。

## ライブラリ

調べたところ候補となるライブラリは以下の2つがありました。
（ほかにもあったら教えていただきたいです。）

https://github.com/zitadel/oidc

https://github.com/coreos/go-oidc

どちらもライセンスは `Apache-2.0 license` です。

本記事執筆で参照しているそれぞれのライブラリのバージョンは

- `zitadel/oidc` : [v3.10.0](https://github.com/zitadel/oidc/tree/v3.10.0)
- `coreos/go-oidc` : [v3.9.0](https://github.com/coreos/go-oidc/tree/v3.9.0)

です。

## 実装フロー

今回ライブラリを使ってみるにあたり実装したフローは以下となります。
説明のため、一部パラメータなどを省略しているので正確なものではありません。
ご了承ください。

```mermaid
sequenceDiagram
autonumber
participant web as ブラウザ
participant rp as Relying Party
participant op as OpenID Provider

web ->> rp: GET /
rp -->> web: ログイン画面
web ->> rp: GET /auth
rp -->> web: 302
web ->> op: GET /authorization_endpoint?state=xxx&redirect_uri=/callback
Note over web, op: SSO ログイン
op -->> web: 302
web ->> rp: GET /callback?state=xxx&code=xxx
rp ->> op: POST /token_endpoint
Note left of op: code
op -->> rp: 200
Note right of rp: access_token, refresh_token, id_token
rp ->> rp: id_token verify
rp -->> web: 200 
Note right of web: html
```

Go によるサーバーでの Relying Party の実装なので `nonce` や `PKCE` は利用しません。
（あと、この方がよりシンプルに説明できるのかなと思っています。）

OpenAPI でエンドポイントを定義して利用します。
サーバーの実装をサボるために `ogen` による自動生成を活用しています。

https://ogen.dev/

## 準備

実際に動作確認を実施するにあたり Google と連携します。

https://developers.google.com/identity/openid-connect/openid-connect?hl=ja

こちらのドキュメントを参考にプロジェクトを用意しています。

## coreos/go-oidc

`coreos/go-oidc` は以下に示すように `example` が用意されています。

https://github.com/coreos/go-oidc/tree/v3/example

### パッケージインポート

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

### 初期化処理

一連の処理にて [`oidc.Provider`](https://pkg.go.dev/github.com/coreos/go-oidc/v3@v3.9.0/oidc#Provider) と [`oauth2.Config`](https://pkg.go.dev/golang.org/x/oauth2@v0.16.0#Config) という構造体が必要となります。

```golang
ctx := context.Background()

provider := oidc.NewProvider(ctx, "issuer")

config := &oauth2.Config{
    ClientID:     "client_id",
	ClientSecret: "client_secret",
	Endpoint:     provider.Endpoint(),
	RedirectURL:  "redirect_url",
	Scopes:       []string{oidc.ScopeOpenID}
}
```

### 認可エンドポイント生成処理

```golang
// GET /auth へのアクセス

endpoint := config.AuthCodeURL("state")

// 上記で生成したエンドポイントにリダイレクトされるようにクライアントに返す
```

### トークン取得処理

```golang
// 　GET /callback へのアクセス

token, _ := config.Exchange(ctx, "code")

idToken, _ := token.Extra("id_token").(string)
```

### IDトークン検証処理

```golang
if _, err := provider.Verifier(&oidc.Config{
    ClientID: "client_id"
}).Verify(ctx, idToken); err != nil {
    panic(err)
}
```

## zitadel/oidc

`zitadel/oidc` でも以下に示すように `example` が用意されています。

https://github.com/zitadel/oidc/tree/main/example

こちらのサンプルコードでは処理を [`http.HandleFunc()`](https://pkg.go.dev/net/http#HandleFunc) へと渡す形式で実装されています。
が、`ogen` などのライブラリを活用して実装する場合だと参考になりません。
コードを漁っていると他の方法でも OIDC RP としての処理を記述できることがわかったのでそちらの方法で実装します。

### パッケージインポート

```golang
import (
	"github.com/zitadel/oidc/v3/pkg/client/rp"
	"github.com/zitadel/oidc/v3/pkg/oidc"
)
```

### 初期化処理

一連の処理にて構造体 [rp.RelyingParty](https://pkg.go.dev/github.com/zitadel/oidc/v3@v3.10.0/pkg/client/rp#RelyingParty) が必要となります。


```golang
ctx := context.Background()

provider, _ := rp.NewRelyingPartyOIDC(
	ctx,
	"issuer",
	"client_id",
	"client_secret",
	"redirect_uri",
	[]string{"openid"},
)
```

### 認可エンドポイント生成処理

```golang
// GET /auth へのアクセス

endpoint := rp.AuthURL("state", provider)

// 上記で生成したエンドポイントにリダイレクトされるようにクライアントに返す
```

### トークン取得処理

```golang
// 　GET /callback へのアクセス

token, _ := rp.CodeExchange[*oidc.IDTokenClaims](ctx, "code", provider)

idToken, _ := token.Extra("id_token").(string) // coreos と同じ処理
```

### IDトークン検証処理

```golang
if _, err := rp.VerifyIDToken[*oidc.IDTokenClaims](ctx, idToken, provider.IDTokenVerifier()); err != nil {
	panic(error)
}
```

## おわりに

どちらのライブラリを使っても省エネルギーで OIDC RP の実装ができるなと確認できました。
正直どちらも使用感は変わらなかったですが、`zitadel` のほうが [`rp.RelyingParty`](https://pkg.go.dev/github.com/zitadel/oidc/v3@v3.10.0/pkg/client/rp#RelyingParty) という構造体の管理で一連の処理が記述できる点は良いと感じました。

`zitadel/oidc` に以下のような記載があったりするので個人的にはこちらのライブラリを使っていこうと思います。

> Easy to use OpenID Connect client and server library written for Go and certified by the OpenID Foundation

複数プロバイダーや複数のコールバックURIを管理するときの実装をいい感じにするためにはどうしようかなと迷っています ... 


今回実装したコードは以下に置いておきます。ご参考まで。

https://github.com/otakakot/sample-go-oidc-rp