140 lines
4.1 KiB
Go
140 lines
4.1 KiB
Go
package usertoken
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rsa"
|
|
"fmt"
|
|
"net/http"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// Config selects one verify mode.
|
|
// Set PublicKey for static RS256, or OIDCIssuer for discovery. Not both.
|
|
// Skew of zero uses 1 minute. GroupsClaim empty uses "groups" (OIDC only).
|
|
// Audience empty is allowed only for OIDC, where it skips the audience check.
|
|
type Config struct {
|
|
PublicKey *rsa.PublicKey
|
|
Issuer string
|
|
Audience string
|
|
OIDCIssuer string
|
|
OIDCAudience string
|
|
GroupsClaim string
|
|
Skew time.Duration
|
|
}
|
|
|
|
// Auth verifies bearers for one Microservice process.
|
|
type Auth struct {
|
|
v tokenVerifier
|
|
}
|
|
|
|
// FromEnv reads the process environment and builds an Auth.
|
|
// Set USER_TOKEN_PUBLIC_KEY_FILE or OIDC_ISSUER, not both and not neither.
|
|
// Static mode also reads USER_TOKEN_ISSUER and USER_TOKEN_AUDIENCE.
|
|
// OIDC mode reads OIDC_AUDIENCE and optional OIDC_GROUPS_CLAIM.
|
|
// Optional USER_TOKEN_SKEW is a time.Duration; empty uses 1 minute.
|
|
// It returns an error when the mode is ambiguous, the key file is unreadable,
|
|
// USER_TOKEN_SKEW cannot be parsed, or OIDC discovery fails.
|
|
// ctx is used for OIDC discovery.
|
|
func FromEnv(ctx context.Context) (*Auth, error) {
|
|
pubFile := strings.TrimSpace(os.Getenv("USER_TOKEN_PUBLIC_KEY_FILE"))
|
|
oidcIss := strings.TrimSpace(os.Getenv("OIDC_ISSUER"))
|
|
switch {
|
|
case pubFile != "" && oidcIss != "":
|
|
return nil, fmt.Errorf("set USER_TOKEN_PUBLIC_KEY_FILE or OIDC_ISSUER, not both")
|
|
case pubFile == "" && oidcIss == "":
|
|
return nil, fmt.Errorf("set USER_TOKEN_PUBLIC_KEY_FILE or OIDC_ISSUER")
|
|
}
|
|
var skew time.Duration
|
|
if raw := strings.TrimSpace(os.Getenv("USER_TOKEN_SKEW")); raw != "" {
|
|
var err error
|
|
skew, err = time.ParseDuration(raw)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("USER_TOKEN_SKEW: %w", err)
|
|
}
|
|
}
|
|
cfg := Config{
|
|
Issuer: os.Getenv("USER_TOKEN_ISSUER"),
|
|
Audience: os.Getenv("USER_TOKEN_AUDIENCE"),
|
|
OIDCIssuer: oidcIss,
|
|
OIDCAudience: os.Getenv("OIDC_AUDIENCE"),
|
|
GroupsClaim: os.Getenv("OIDC_GROUPS_CLAIM"),
|
|
Skew: skew,
|
|
}
|
|
if pubFile != "" {
|
|
key, err := readPublicKeyFile(pubFile)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
cfg.PublicKey = key
|
|
}
|
|
return New(ctx, cfg)
|
|
}
|
|
|
|
// New builds an Auth from explicit configuration. ctx is used for OIDC discovery.
|
|
func New(ctx context.Context, cfg Config) (*Auth, error) {
|
|
static := cfg.PublicKey != nil
|
|
oidcOn := strings.TrimSpace(cfg.OIDCIssuer) != ""
|
|
switch {
|
|
case static && oidcOn:
|
|
return nil, fmt.Errorf("set a public key or an OIDC issuer, not both")
|
|
case !static && !oidcOn:
|
|
return nil, fmt.Errorf("set a public key or an OIDC issuer")
|
|
}
|
|
var v tokenVerifier
|
|
var err error
|
|
if static {
|
|
v, err = newStaticVerifier(cfg.PublicKey, cfg.Issuer, cfg.Audience, cfg.Skew)
|
|
} else {
|
|
v, err = newOIDCVerifier(ctx, cfg.OIDCIssuer, cfg.OIDCAudience, cfg.GroupsClaim, cfg.Skew)
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &Auth{v: v}, nil
|
|
}
|
|
|
|
// Verify checks a raw JWT. raw has no "Bearer " prefix.
|
|
// A failed check wraps ErrUnauthorized.
|
|
// Startup and configuration failures do not.
|
|
func (a *Auth) Verify(ctx context.Context, raw string) (Caller, error) {
|
|
return a.v.verify(ctx, raw)
|
|
}
|
|
|
|
// Middleware requires a bearer on every path except /health.
|
|
// Success stores the Caller and the raw JWT on the request context.
|
|
func (a *Auth) Middleware() func(http.Handler) http.Handler {
|
|
return func(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path == "/health" {
|
|
next.ServeHTTP(w, r)
|
|
return
|
|
}
|
|
raw, ok := bearerToken(r.Header.Get("Authorization"))
|
|
if !ok {
|
|
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
caller, err := a.Verify(r.Context(), raw)
|
|
if err != nil {
|
|
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
next.ServeHTTP(w, r.WithContext(withAuth(r.Context(), caller, raw)))
|
|
})
|
|
}
|
|
}
|
|
|
|
func bearerToken(h string) (string, bool) {
|
|
scheme, rest, ok := strings.Cut(h, " ")
|
|
if !ok || !strings.EqualFold(scheme, "Bearer") {
|
|
return "", false
|
|
}
|
|
rest = strings.TrimSpace(rest)
|
|
if rest == "" {
|
|
return "", false
|
|
}
|
|
return rest, true
|
|
}
|