diff --git a/pkg/auth/auth.go b/pkg/auth/auth.go index ae706986..ca9cfc51 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.AllowedClaims) } return authVerifier } diff --git a/pkg/auth/oidc.go b/pkg/auth/oidc.go index 40ce060f..6b926d29 100644 --- a/pkg/auth/oidc.go +++ b/pkg/auth/oidc.go @@ -16,8 +16,12 @@ package auth import ( "context" + "encoding/base64" + "encoding/json" "fmt" "slices" + "strconv" + "strings" "github.com/coreos/go-oidc/v3/oidc" "golang.org/x/oauth2/clientcredentials" @@ -30,6 +34,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 +61,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 +111,9 @@ type OidcAuthConsumer struct { verifier TokenVerifier subjectsFromLogin []string + + // allowedClaims specifies a map of allowed claims for the OIDC token. + allowedClaims map[string]string } func NewTokenVerifier(cfg v1.AuthOIDCServerConfig) TokenVerifier { @@ -112,15 +130,60 @@ 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, allowedClaims map[string]string) *OidcAuthConsumer { return &OidcAuthConsumer{ additionalAuthScopes: additionalAuthScopes, verifier: verifier, subjectsFromLogin: []string{}, + allowedClaims: allowedClaims, } } func (auth *OidcAuthConsumer) VerifyLogin(loginMsg *msg.Login) (err error) { + // Verify allowed claims if configured. + if len(auth.allowedClaims) > 0 { + // Decode token without verifying signature. + 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) + } + + // Iterate over allowed claims and attempt to verify. + for claimName, expectedValue := range auth.allowedClaims { + claimValue, ok := claims[claimName] + if !ok { + return fmt.Errorf("OIDC token missing required claim: %s", claimName) + } + + if strClaimValue, ok := claimValue.(string); ok { + if strClaimValue != expectedValue { + return fmt.Errorf("OIDC token claim '%s' value [%s] does not match expected value [%s]", claimName, strClaimValue, expectedValue) + } + } else if intClaimValue, ok := claimValue.(int); ok { + expectedIntValue, err := strconv.Atoi(expectedValue) + if err != nil { + return fmt.Errorf("OIDC token claim '%s' is number, expected value [%s] not parseable", claimName, expectedValue) + } + if intClaimValue != expectedIntValue { + return fmt.Errorf("OIDC token claim '%s' value [%d] does not match expected value [%d]", claimName, intClaimValue, expectedIntValue) + } + } else { + return fmt.Errorf("claim %s is of unsupported type", claimName) + } + } + } + + // If claim verification 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..dff2b5bc 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{}, map[string]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{}, map[string]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{}, map[string]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..e233d110 100644 --- a/pkg/config/v1/server.go +++ b/pkg/config/v1/server.go @@ -147,6 +147,15 @@ 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"` + // AllowedClaims specifies a map of allowed claims for the OIDC token. + AllowedClaims map[string]string `json:"allowedClaims,omitempty"` +} + +func (c *AuthOIDCServerConfig) Complete() { + // Ensure AllowedClaims is at least an empty map and not nil + if c.AllowedClaims == nil { + c.AllowedClaims = map[string]string{} + } } type ServerTransportConfig struct {