0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

OIDCを試してみよう

Posted at

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
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?