86 lines
2.4 KiB
Go
86 lines
2.4 KiB
Go
package webauth
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
|
|
"github.com/coreos/go-oidc/v3/oidc"
|
|
"golang.org/x/oauth2"
|
|
)
|
|
|
|
// Claims is what the master needs from the identity provider.
|
|
type Claims struct {
|
|
Subject string
|
|
Name string
|
|
Email string
|
|
Groups []string
|
|
}
|
|
|
|
// Provider performs the OIDC authorization code flow with PKCE. Declared as
|
|
// an interface so handlers can be tested without a real identity provider.
|
|
type Provider interface {
|
|
AuthCodeURL(state, verifier string) string
|
|
Exchange(ctx context.Context, code, verifier string) (Claims, error)
|
|
}
|
|
|
|
// OIDCConfig configures the real provider.
|
|
type OIDCConfig struct {
|
|
Issuer string // e.g. https://auth.example.com/application/o/package-updater/
|
|
ClientID string
|
|
ClientSecret string
|
|
RedirectURL string // PUBLIC_URL + /auth/callback
|
|
}
|
|
|
|
type oidcProvider struct {
|
|
oauth *oauth2.Config
|
|
verifier *oidc.IDTokenVerifier
|
|
}
|
|
|
|
// NewOIDCProvider discovers the issuer's endpoints. It makes a network call,
|
|
// so the master fails at startup if Authentik is unreachable.
|
|
func NewOIDCProvider(ctx context.Context, cfg OIDCConfig) (Provider, error) {
|
|
p, err := oidc.NewProvider(ctx, cfg.Issuer)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("oidc discovery: %w", err)
|
|
}
|
|
return &oidcProvider{
|
|
oauth: &oauth2.Config{
|
|
ClientID: cfg.ClientID,
|
|
ClientSecret: cfg.ClientSecret,
|
|
Endpoint: p.Endpoint(),
|
|
RedirectURL: cfg.RedirectURL,
|
|
Scopes: []string{oidc.ScopeOpenID, "profile", "email"},
|
|
},
|
|
verifier: p.Verifier(&oidc.Config{ClientID: cfg.ClientID}),
|
|
}, nil
|
|
}
|
|
|
|
func (p *oidcProvider) AuthCodeURL(state, verifier string) string {
|
|
return p.oauth.AuthCodeURL(state, oauth2.S256ChallengeOption(verifier))
|
|
}
|
|
|
|
func (p *oidcProvider) Exchange(ctx context.Context, code, verifier string) (Claims, error) {
|
|
tok, err := p.oauth.Exchange(ctx, code, oauth2.VerifierOption(verifier))
|
|
if err != nil {
|
|
return Claims{}, fmt.Errorf("exchange code: %w", err)
|
|
}
|
|
raw, ok := tok.Extra("id_token").(string)
|
|
if !ok {
|
|
return Claims{}, errors.New("token response has no id_token")
|
|
}
|
|
idt, err := p.verifier.Verify(ctx, raw)
|
|
if err != nil {
|
|
return Claims{}, fmt.Errorf("verify id_token: %w", err)
|
|
}
|
|
var c struct {
|
|
Name string `json:"name"`
|
|
Email string `json:"email"`
|
|
Groups []string `json:"groups"`
|
|
}
|
|
if err := idt.Claims(&c); err != nil {
|
|
return Claims{}, fmt.Errorf("read claims: %w", err)
|
|
}
|
|
return Claims{Subject: idt.Subject, Name: c.Name, Email: c.Email, Groups: c.Groups}, nil
|
|
}
|