Identity Provider
OIDC Discovery
サポートされる機能に関する情報を提供します。エンドポイントは、GET /.well-known/openid-configurationです。
一部フィールドのみを紹介します。詳細は、OpenID Connect Discovery 1.0
を参照してください。
| フィールド | 説明 |
|---|---|
| issuer | 発行者のURL |
| authorization_endpoint | 認可エンドポイントのURL |
| token_endpoint | トークンエンドポイントのURL |
| jwks_uri | JWKセットのURL |
| response_types_supported | サポートするレスポンスタイプ |
| subject_types_supported | サポートするサブジェクトタイプ |
| id_token_signing_alg_values_supported | サポートする署名アルゴリズム |
JWK Set
JWTを検証するための公開鍵を提供します。エンドポイントは、OIDC Discoveryのjwks_uriフィールドで指定します。
例えば、https://example.com/jwks のようにします。
トークン発行
トークンを発行するエンドポイントを作成します。通常、認証確認が行われると、トークンを発行します。今回は、検証のため認証は行わず、単なるGETリクエストでトークンを発行します。 また、subject(一般的にユーザーIDなど)をクエリパラメーターで指定できるようにします。
トークン発行の際には、issuerとaudienceを正しく設定してください。
issuerは、誰が発行したトークンであるかを示します。
audienceは、誰のために発行したトークンであるかを示します。
APIサーバー
発行されたJWSを検証して、認可されたユーザーのみがアクセスできるようなAPIサーバーを作成します。
JWSの検証には、様々なライブラリが用意されています。OIDC用のライブラリであれば、ライブラリが公開鍵の解決と署名の検証を行なってくれるでしょう。
OIDC用のものがなければ、自力でJWK Setを取得して、署名の検証ができます。署名の検証自体は、JWTライブラリで行えるでしょう。
公開鍵の場所は、JWTのissuerとOIDC Discoveryのjwks_uriフィールドで確認できます。
コード
.envrc
export ISSUER="http://localhost:3000"
export AUDIENCE="http://localhost:3001"
authサーバー
package main
import (
"crypto"
"crypto/rand"
"crypto/rsa"
"fmt"
"net/url"
"os"
"strings"
"time"
"github.com/go-jose/go-jose/v4"
"github.com/go-jose/go-jose/v4/jwt"
"github.com/gofiber/fiber/v2"
"github.com/google/uuid"
)
type (
publicKey struct {
publicKey crypto.PublicKey
id string
algorithm string
}
discovery struct {
Issuer string `json:"issuer"`
Auth string `json:"authorization_endpoint"`
Token string `json:"token_endpoint"`
JWKs string `json:"jwks_uri"`
ResponseTypes []string `json:"response_types_supported"`
SubjectTypes []string `json:"subject_types_supported"`
Algorithms []string `json:"id_token_signing_alg_values_supported"`
}
handler struct {
issuerURL *url.URL
algorithms []string
privateKey crypto.PrivateKey
publicKeys []publicKey
}
)
func main() {
issuer := os.Getenv("ISSUER")
port := strings.TrimPrefix(issuer, "http://localhost:")
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
panic(err)
}
issuerURL, err := url.Parse(issuer)
if err != nil {
panic(err)
}
h := handler{
issuerURL: issuerURL,
privateKey: privateKey,
algorithms: []string{string(jose.RS256)},
publicKeys: []publicKey{
{
publicKey: privateKey.Public(),
id: uuid.NewString(),
algorithm: string(jose.RS256),
},
},
}
app := fiber.New()
app.Get("/.well-known/openid-configuration", h.openIDConfig)
app.Get("/keys", h.keys)
app.Get("/token", h.token)
if err := app.Listen(fmt.Sprintf(":%s", port)); err != nil {
panic(err)
}
}
func (h handler) newToken(claims jwt.Claims) (string, error) {
key := jose.SigningKey{
Algorithm: jose.SignatureAlgorithm(h.algorithms[0]),
Key: h.privateKey,
}
opts := &jose.SignerOptions{
ExtraHeaders: map[jose.HeaderKey]interface{}{
"kid": h.publicKeys[0].id,
},
}
signer, err := jose.NewSigner(key, opts)
if err != nil {
return "", fmt.Errorf("creating signer: %v", err)
}
return jwt.Signed(signer).Claims(claims).Serialize()
}
func (h handler) openIDConfig(c *fiber.Ctx) error {
disc := discovery{
Issuer: h.issuerURL.String(),
Auth: h.issuerURL.JoinPath("/auth").String(),
Token: h.issuerURL.JoinPath("/token").String(),
JWKs: h.issuerURL.JoinPath("/keys").String(),
ResponseTypes: []string{"code", "id_token", "token id_token"},
SubjectTypes: []string{"public"},
Algorithms: h.algorithms,
}
return c.Status(200).JSON(disc)
}
func (h handler) keys(c *fiber.Ctx) error {
set := &jose.JSONWebKeySet{}
for _, pub := range h.publicKeys {
k := jose.JSONWebKey{
Key: pub.publicKey,
KeyID: pub.id,
Algorithm: pub.algorithm,
Use: "sig",
}
set.Keys = append(set.Keys, k)
}
return c.Status(200).JSON(set)
}
func (h handler) token(c *fiber.Ctx) error {
now := time.Now()
subject := c.Query("subject")
claims := jwt.Claims{
Issuer: h.issuerURL.String(),
Subject: subject,
Audience: []string{os.Getenv("AUDIENCE")},
Expiry: jwt.NewNumericDate(now.Add(time.Hour * 24)),
NotBefore: jwt.NewNumericDate(now),
IssuedAt: jwt.NewNumericDate(now),
ID: uuid.NewString(),
}
token, err := h.newToken(claims)
if err != nil {
return err
}
return c.Status(200).JSON(map[string]any{
"token": token,
})
}
APIサーバー
package main
import (
"context"
"fmt"
"net/http"
"os"
"strings"
"github.com/coreos/go-oidc/v3/oidc"
"github.com/gofiber/fiber/v2"
)
type (
handler struct {
verifier *oidc.IDTokenVerifier
}
userIDKey struct {
}
)
func main() {
ctx := context.Background()
audience := os.Getenv("AUDIENCE")
port := strings.TrimPrefix(audience, "http://localhost:")
provider, err := oidc.NewProvider(ctx, os.Getenv("ISSUER"))
if err != nil {
panic(err)
}
h := handler{
verifier: provider.Verifier(
&oidc.Config{
ClientID: audience,
},
),
}
app := fiber.New()
app.Use(h.authMiddleware)
app.Get("/check", h.check)
if err := app.Listen(fmt.Sprintf(":%s", port)); err != nil {
panic(err)
}
}
func (h handler) authMiddleware(c *fiber.Ctx) error {
rawToken := strings.TrimPrefix(c.Get(fiber.HeaderAuthorization), "Bearer ")
token, err := h.verifier.Verify(c.Context(), rawToken)
if err != nil {
return c.Status(http.StatusUnauthorized).SendString(http.StatusText(http.StatusUnauthorized))
}
c.Locals(userIDKey{}, token.Subject)
return c.Next()
}
func (h handler) check(c *fiber.Ctx) error {
v, ok := c.Locals(userIDKey{}).(string)
message := "Hello unknown subject"
if ok {
message = fmt.Sprintf("Hello %s", v)
}
return c.Status(http.StatusOK).SendString(message)
}
### OIDC Discoveryを確認
GET http://localhost:3000/.well-known/openid-configuration
### JWK Setsを確認
GET http://localhost:3000/keys
### トークンを発行
GET http://localhost:3000/token?subject={{$uuid}}
### OK
GET http://localhost:3001/check
Authorization: Bearer TOKEN
### Unauthorized
GET http://localhost:3001/check