Skip to content

最小構成 OP ​

OP を起動する最短の手順です。op.New は WithIssuer、WithStore、WithKeyset の 3 つが揃わないと起動を拒否します。既定の grant 集合には、ユーザをブラウザ経由で認可する authorization_code が含まれます。この grant を使うときは、ブラウザの cookie を暗号化する WithCookieKeys と、サインイン方法を決める WithLoginFlow も必要です。client_credentials だけを提供する OP なら、最初の 3 つで足ります。下のコードでは WithLogger も渡しています。WithLogger を渡さないと、リクエスト処理中のログと監査レコードは破棄され(保存されたパスワードハッシュが argon2id でない場合に一度だけ出る「PrimaryPassword.LegacySchemes を設定してください」という警告も、その 1 つです)、起動時にその旨の警告が出ます。セキュリティプロファイルは宣言していません。WithProfile なしで構築した OP は profile.OAuth21 で動くため、すべての authorization-code request に PKCE が必要です(セキュリティプロファイルの宣言 を参照)。

5 つの値を渡すと handler が 1 つ返る
最小構成の OP は issuer・store・keyset・cookie 鍵・ログインフローの 5 つを op.New に渡します。op.New はそれらを検証し、通常の HTTP handler を返します。IssuerOP の公開 https URLStore認可コード · session · クライアント · 連鎖KeysetES256 署名鍵Cookie keysAES-256-GCM による暗号化Login flowユーザのサインイン方法op.New(…)構成を起動時に検証し、不備があれば起動を拒否するHTTP handlerhttp.Handlerマウントしても直接使ってもよい
ここにフレームワークはありません。返ってくるのは他と変わらない http.Handler です。だからこそ、既にあるルータのどこにでも OP を置けます。
go
package main

import (
  "crypto/ecdsa"
  "crypto/elliptic"
  "crypto/rand"
  "log"
  "log/slog"
  "net/http"

  "github.com/libraz/go-oidc-provider/op"
  "github.com/libraz/go-oidc-provider/op/storeadapter/inmem"
)

func main() {
  // 揮発性 ECDSA P-256 (ES256) — 本番では Vault / KMS の鍵に差し替えてください。
  // Keyset は {KeyID, Signer} のスライスです。
  priv, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
  cookieKey := make([]byte, 32) // AES-256-GCM
  if _, err := rand.Read(cookieKey); err != nil {
    log.Fatal(err)
  }

  st := inmem.New()
  handler, err := op.New(
    op.WithIssuer("http://127.0.0.1:8080"),
    op.WithLogger(slog.Default()),
    op.WithStore(st),
    op.WithKeyset(op.Keyset{{KeyID: "k1", Signer: priv}}),
    op.WithCookieKeys(cookieKey),
    op.WithLoginFlow(op.LoginFlow{
      Primary: op.PrimaryPassword{Store: st.UserPasswords()},
    }),
  )
  if err != nil {
    log.Fatal(err)
  }
  log.Fatal(http.ListenAndServe(":8080", handler))
}
go
package main

import (
  "crypto/ecdsa"
  "crypto/elliptic"
  "crypto/rand"
  "log"
  "log/slog"
  "net/http"

  "github.com/go-chi/chi/v5"
  "github.com/libraz/go-oidc-provider/op"
  "github.com/libraz/go-oidc-provider/op/storeadapter/inmem"
)

func main() {
  priv, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
  cookieKey := make([]byte, 32)
  if _, err := rand.Read(cookieKey); err != nil {
    log.Fatal(err)
  }

  st := inmem.New()
  handler, err := op.New(
    op.WithIssuer("http://127.0.0.1:8080"),
    op.WithLogger(slog.Default()),
    op.WithStore(st),
    op.WithKeyset(op.Keyset{{KeyID: "k1", Signer: priv}}),
    op.WithCookieKeys(cookieKey),
    op.WithLoginFlow(op.LoginFlow{
      Primary: op.PrimaryPassword{Store: st.UserPasswords()},
    }),
  )
  if err != nil {
    log.Fatal(err)
  }

  r := chi.NewRouter()
  r.Mount("/", handler)
  log.Fatal(http.ListenAndServe(":8080", r))
}
go
package main

import (
  "crypto/ecdsa"
  "crypto/elliptic"
  "crypto/rand"
  "log"
  "log/slog"
  "net/http"

  "github.com/gin-gonic/gin"
  "github.com/libraz/go-oidc-provider/op"
  "github.com/libraz/go-oidc-provider/op/storeadapter/inmem"
)

func main() {
  priv, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
  cookieKey := make([]byte, 32)
  if _, err := rand.Read(cookieKey); err != nil {
    log.Fatal(err)
  }

  st := inmem.New()
  handler, err := op.New(
    op.WithIssuer("http://127.0.0.1:8080"),
    op.WithLogger(slog.Default()),
    op.WithStore(st),
    op.WithKeyset(op.Keyset{{KeyID: "k1", Signer: priv}}),
    op.WithCookieKeys(cookieKey),
    op.WithLoginFlow(op.LoginFlow{
      Primary: op.PrimaryPassword{Store: st.UserPasswords()},
    }),
  )
  if err != nil {
    log.Fatal(err)
  }

  r := gin.New()
  r.Any("/*path", gin.WrapH(handler))
  log.Fatal(http.ListenAndServe(":8080", r))
}

本番運用の注意

  • 鍵: ここでは起動のたびに生成しています。本番では Vault / KMS から読み込み、再起動後もトークンが有効なままにします。
  • ストア: ここでは in-memory です。本番では op/storeadapter/sql、op/storeadapter/dynamodb、composite のいずれかを使います。Redis は composite 構成の揮発層で、単独のストアにはなりません。
  • issuer: 下の curl が動くよう、ここではループバックの http URL にしています。本番では公開する https URL にします。
  • リスナ: ここでは平文 HTTP です。前段に TLS 終端の ingress を置いてください。
この OP で今すぐ試せること
  1. curl http://127.0.0.1:8080/.well-known/openid-configuration で discovery ドキュメントを取得できます。パスを含まない issuer なら、mount prefix に関わらずホストのルート直下に公開されます。
  2. curl http://127.0.0.1:8080/oidc/jwks で ID トークンの検証に使う公開鍵を取得できます。既定の mount prefix は /oidc で、op.WithMountPrefix("/") を渡すとエンドポイントがルート直下に移ります。
  3. /oidc/auth は、クライアントを登録するまで何を送ってもエラーを返します。次の節でクライアントとユーザを 1 つずつ追加します。

クライアントとユーザを 1 つずつ追加する ​

RP(Relying Party)は、OP にユーザのサインインを依頼するアプリケーションです。OP は登録済みのクライアントからのリクエストしか受け付けません。RP を登録し、パスワード認証の対象になるユーザも用意します。

go
// 追加の import: "context", "github.com/libraz/go-oidc-provider/op/store"
hash, err := op.HashPassword("demo")
if err != nil {
  log.Fatal(err)
}
st.PutUserWithPassword(context.Background(), &store.User{
  Subject: "demo-user",
  Claims:  map[string]any{"name": "Demo User", "email": "[email protected]"},
}, "demo", hash)

// 上のオプションと並べて op.New に渡す
op.WithStaticClients(op.PublicClient{
  ID:           "demo-rp",
  RedirectURIs: []string{"http://127.0.0.1:9090/callback"},
  Scopes:       []string{"openid", "profile"},
})

PutUserWithPassword はハッシュをそのまま保存するため、平文のパスワードを渡すと一致しません。op.HashPassword でハッシュ化してください。PublicClient はシークレットを持たず PKCE に依存するので、RP は code challenge を送る必要があります。次の例には、それを行う RP が付属しています。

付属の例の実行 ​

sh
git clone https://github.com/libraz/go-oidc-provider.git
cd go-oidc-provider
(cd examples/01-minimal && GOWORK=off go run -tags example .)

01-minimal は 1 つのプロセスで 2 つのリスナを起動します。:8080 が OP、:9090 が小さな RP です。http://127.0.0.1:9090/ を開いてログインを選び、demo / demo でサインインして同意画面を承認すると、RP が検証済みの ID トークンの claim を表示します。鍵の生成(examples/internal/devkeys)とリスナの定型コード(examples/internal/serve)は別パッケージに出してあるので、main.go は op.New に集中できます。

次へ ​