Implement OIDC raw token and hd claim verification

This commit is contained in:
foresturquhart 2025-02-06 17:03:25 +00:00
parent 450b8393bc
commit cda2cb151e
5 changed files with 72 additions and 5 deletions

View File

@ -51,7 +51,7 @@ func NewAuthVerifier(cfg v1.AuthServerConfig) (authVerifier Verifier) {
authVerifier = NewTokenAuth(cfg.AdditionalScopes, cfg.Token)
case v1.AuthMethodOIDC:
tokenVerifier := NewTokenVerifier(cfg.OIDC)
authVerifier = NewOidcAuthVerifier(cfg.AdditionalScopes, tokenVerifier)
authVerifier = NewOidcAuthVerifier(cfg.AdditionalScopes, tokenVerifier, cfg.OIDC.AllowedHostedDomains)
}
return authVerifier
}

View File

@ -16,8 +16,11 @@ package auth
import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"slices"
"strings"
"github.com/coreos/go-oidc/v3/oidc"
"golang.org/x/oauth2/clientcredentials"
@ -30,6 +33,10 @@ type OidcAuthProvider struct {
additionalAuthScopes []v1.AuthScope
tokenGenerator *clientcredentials.Config
// rawToken is used to specify a raw JWT token for authentication.
// If rawToken is not empty, it will be used directly instead of generating a new token.
rawToken string
}
func NewOidcAuthSetter(additionalAuthScopes []v1.AuthScope, cfg v1.AuthOIDCClientConfig) *OidcAuthProvider {
@ -53,10 +60,17 @@ func NewOidcAuthSetter(additionalAuthScopes []v1.AuthScope, cfg v1.AuthOIDCClien
return &OidcAuthProvider{
additionalAuthScopes: additionalAuthScopes,
tokenGenerator: tokenGenerator,
rawToken: cfg.RawToken,
}
}
func (auth *OidcAuthProvider) generateAccessToken() (accessToken string, err error) {
// If a raw token is provided, use it directly.
if auth.rawToken != "" {
return auth.rawToken, nil
}
// Otherwise, generate a new token using the client credentials flow.
tokenObj, err := auth.tokenGenerator.Token(context.Background())
if err != nil {
return "", fmt.Errorf("couldn't generate OIDC token for login: %v", err)
@ -96,6 +110,9 @@ type OidcAuthConsumer struct {
verifier TokenVerifier
subjectsFromLogin []string
// allowedHostedDomains specifies a list of allowed hosted domains for the "hd" claim in the token.
allowedHostedDomains []string
}
func NewTokenVerifier(cfg v1.AuthOIDCServerConfig) TokenVerifier {
@ -112,15 +129,52 @@ func NewTokenVerifier(cfg v1.AuthOIDCServerConfig) TokenVerifier {
return provider.Verifier(&verifierConf)
}
func NewOidcAuthVerifier(additionalAuthScopes []v1.AuthScope, verifier TokenVerifier) *OidcAuthConsumer {
func NewOidcAuthVerifier(additionalAuthScopes []v1.AuthScope, verifier TokenVerifier, allowedHostedDomains []string) *OidcAuthConsumer {
return &OidcAuthConsumer{
additionalAuthScopes: additionalAuthScopes,
verifier: verifier,
subjectsFromLogin: []string{},
allowedHostedDomains: allowedHostedDomains,
}
}
func (auth *OidcAuthConsumer) VerifyLogin(loginMsg *msg.Login) (err error) {
// Decode token without verifying signature to retrieved 'hd' claim.
parts := strings.Split(loginMsg.PrivilegeKey, ".")
if len(parts) != 3 {
return fmt.Errorf("invalid OIDC token format")
}
payload, err := base64.RawURLEncoding.DecodeString(parts[1])
if err != nil {
return fmt.Errorf("invalid OIDC token: failed to decode payload: %v", err)
}
var claims map[string]any
if err := json.Unmarshal(payload, &claims); err != nil {
return fmt.Errorf("invalid OIDC token: failed to unmarshal payload: %v", err)
}
// Verify hosted domain (hd claim).
if len(auth.allowedHostedDomains) > 0 {
hd, ok := claims["hd"].(string)
if !ok {
return fmt.Errorf("OIDC token missing required 'hd' claim")
}
found := false
for _, domain := range auth.allowedHostedDomains {
if hd == domain {
found = true
break
}
}
if !found {
return fmt.Errorf("OIDC token 'hd' claim [%s] is not in allowed list", hd)
}
}
// If hd check passes, proceed with standard verification.
token, err := auth.verifier.Verify(context.Background(), loginMsg.PrivilegeKey)
if err != nil {
return fmt.Errorf("invalid OIDC token in login: %v", err)

View File

@ -23,7 +23,7 @@ func (m *mockTokenVerifier) Verify(ctx context.Context, subject string) (*oidc.I
func TestPingWithEmptySubjectFromLoginFails(t *testing.T) {
r := require.New(t)
consumer := auth.NewOidcAuthVerifier([]v1.AuthScope{v1.AuthScopeHeartBeats}, &mockTokenVerifier{})
consumer := auth.NewOidcAuthVerifier([]v1.AuthScope{v1.AuthScopeHeartBeats}, &mockTokenVerifier{}, []string{})
err := consumer.VerifyPing(&msg.Ping{
PrivilegeKey: "ping-without-login",
Timestamp: time.Now().UnixMilli(),
@ -34,7 +34,7 @@ func TestPingWithEmptySubjectFromLoginFails(t *testing.T) {
func TestPingAfterLoginWithNewSubjectSucceeds(t *testing.T) {
r := require.New(t)
consumer := auth.NewOidcAuthVerifier([]v1.AuthScope{v1.AuthScopeHeartBeats}, &mockTokenVerifier{})
consumer := auth.NewOidcAuthVerifier([]v1.AuthScope{v1.AuthScopeHeartBeats}, &mockTokenVerifier{}, []string{})
err := consumer.VerifyLogin(&msg.Login{
PrivilegeKey: "ping-after-login",
})
@ -49,7 +49,7 @@ func TestPingAfterLoginWithNewSubjectSucceeds(t *testing.T) {
func TestPingAfterLoginWithDifferentSubjectFails(t *testing.T) {
r := require.New(t)
consumer := auth.NewOidcAuthVerifier([]v1.AuthScope{v1.AuthScopeHeartBeats}, &mockTokenVerifier{})
consumer := auth.NewOidcAuthVerifier([]v1.AuthScope{v1.AuthScopeHeartBeats}, &mockTokenVerifier{}, []string{})
err := consumer.VerifyLogin(&msg.Login{
PrivilegeKey: "login-with-first-subject",
})

View File

@ -203,4 +203,7 @@ type AuthOIDCClientConfig struct {
// AdditionalEndpointParams specifies additional parameters to be sent
// this field will be transfer to map[string][]string in OIDC token generator.
AdditionalEndpointParams map[string]string `json:"additionalEndpointParams,omitempty"`
// RawToken specifies a raw JWT token to use for authentication, bypassing
// the OIDC flow.
RawToken string `json:"rawToken,omitempty"`
}

View File

@ -147,6 +147,16 @@ type AuthOIDCServerConfig struct {
// SkipIssuerCheck specifies whether to skip checking if the OIDC token's
// issuer claim matches the issuer specified in OidcIssuer.
SkipIssuerCheck bool `json:"skipIssuerCheck,omitempty"`
// AllowedHostedDomains specifies a list of allowed hosted domains for the
// "hd" claim in the token.
AllowedHostedDomains []string `json:"allowedHostedDomains,omitempty"`
}
func (c *AuthOIDCServerConfig) Complete() {
// Ensure AllowedHostedDomains is an empty slice and not nil
if c.AllowedHostedDomains == nil {
c.AllowedHostedDomains = []string{}
}
}
type ServerTransportConfig struct {