diff --git a/pkg/auth/auth.go b/pkg/auth/auth.go index ae706986..176b4d9f 100644 --- a/pkg/auth/auth.go +++ b/pkg/auth/auth.go @@ -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 } diff --git a/pkg/auth/oidc.go b/pkg/auth/oidc.go index 40ce060f..f241ff46 100644 --- a/pkg/auth/oidc.go +++ b/pkg/auth/oidc.go @@ -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) diff --git a/pkg/auth/oidc_test.go b/pkg/auth/oidc_test.go index 58054186..66ff7fb9 100644 --- a/pkg/auth/oidc_test.go +++ b/pkg/auth/oidc_test.go @@ -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", }) diff --git a/pkg/config/v1/client.go b/pkg/config/v1/client.go index d43ec1bc..1ad194f1 100644 --- a/pkg/config/v1/client.go +++ b/pkg/config/v1/client.go @@ -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"` } diff --git a/pkg/config/v1/server.go b/pkg/config/v1/server.go index 3108cd34..93281cf5 100644 --- a/pkg/config/v1/server.go +++ b/pkg/config/v1/server.go @@ -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 {