diff --git a/idp/internal/controllers/account_credentials.go b/idp/internal/controllers/account_credentials.go index 6cff281..2d767f4 100644 --- a/idp/internal/controllers/account_credentials.go +++ b/idp/internal/controllers/account_credentials.go @@ -85,8 +85,8 @@ func (c *Controllers) ListAccountCredentials(ctx fiber.Ctx) error { } queryParams := params.PaginationQueryParams{ - Offset: fiber.Query[int](ctx, "offset", 0), - Limit: fiber.Query[int](ctx, "limit", 20), + Offset: fiber.Query(ctx, "offset", 0), + Limit: fiber.Query(ctx, "limit", 20), } if err := c.validate.StructCtx(ctx.Context(), &queryParams); err != nil { return validateQueryParamsErrorResponse(logger, ctx, err) @@ -243,8 +243,8 @@ func (c *Controllers) ListAccountCredentialsSecrets(ctx fiber.Ctx) error { } queryParams := params.PaginationQueryParams{ - Offset: fiber.Query[int](ctx, "offset", 0), - Limit: fiber.Query[int](ctx, "limit", 20), + Offset: fiber.Query(ctx, "offset", 0), + Limit: fiber.Query(ctx, "limit", 20), } if err := c.validate.StructCtx(ctx.Context(), queryParams); err != nil { return validateQueryParamsErrorResponse(logger, ctx, err) diff --git a/idp/internal/controllers/apps.go b/idp/internal/controllers/apps.go index fc5140a..4b58bab 100644 --- a/idp/internal/controllers/apps.go +++ b/idp/internal/controllers/apps.go @@ -456,8 +456,8 @@ func (c *Controllers) ListApps(ctx fiber.Ctx) error { } queryParams := params.GetAppsQueryParams{ - Limit: fiber.Query[int](ctx, "limit", 10), - Offset: fiber.Query[int](ctx, "offset", 0), + Limit: fiber.Query(ctx, "limit", 10), + Offset: fiber.Query(ctx, "offset", 0), Name: ctx.Query("name"), Order: ctx.Query("order", "date"), Type: ctx.Query("type"), @@ -1035,8 +1035,8 @@ func (c *Controllers) ListAppSecrets(ctx fiber.Ctx) error { } queryParams := params.PaginationQueryParams{ - Offset: fiber.Query[int](ctx, "offset", 0), - Limit: fiber.Query[int](ctx, "limit", 20), + Offset: fiber.Query(ctx, "offset", 0), + Limit: fiber.Query(ctx, "limit", 20), } if err := c.validate.StructCtx(ctx.Context(), queryParams); err != nil { return validateQueryParamsErrorResponse(logger, ctx, err) diff --git a/idp/internal/controllers/bodies/oauth_dynamic_registration.go b/idp/internal/controllers/bodies/oauth_dynamic_registration.go index 67995e5..8869e12 100644 --- a/idp/internal/controllers/bodies/oauth_dynamic_registration.go +++ b/idp/internal/controllers/bodies/oauth_dynamic_registration.go @@ -6,42 +6,44 @@ package bodies +import "github.com/tugascript/devlogs/idp/internal/utils" + type OAuthDynamicClientRegistrationBody struct { - RedirectURIs []string `json:"redirect_uris,omitempty" validate:"omitempty,min=1,dive,uri"` - TokenEndpointAuthMethod string `json:"token_endpoint_auth_method,omitempty" validate:"omitempty,oneof=none client_secret_basic client_secret_post client_secret_jwt private_key_jwt"` - ResponseTypes []string `json:"response_types,omitempty" validate:"omitempty,dive,oneof=code 'code id_token'"` - GrantTypes []string `json:"grant_types,omitempty" validate:"omitempty,min=1,dive,oneof=authorization_code refresh_token client_credentials urn:ietf:params:oauth:grant-type:jwt-bearer"` - ApplicationType string `json:"application_type" validate:"required,oneof=native service mcp web spa backend device"` - ClientName string `json:"client_name" validate:"required,min=1,max=255"` - ClientURI string `json:"client_uri" validate:"required,url"` - LogoURI string `json:"logo_uri,omitempty" validate:"omitempty,url"` - Scope string `json:"scope" validate:"required,multiple_scope"` - Contacts []string `json:"contacts,omitempty" validate:"omitempty,unique,dive,email"` - TOSURI string `json:"tos_uri,omitempty" validate:"omitempty,url"` - PolicyURI string `json:"policy_uri,omitempty" validate:"omitempty,url"` - JWKsURI string `json:"jwks_uri,omitempty" validate:"omitempty,url"` - JWKs []string `json:"jwks,omitempty" validate:"omitempty,json"` - SoftwareID string `json:"software_id,omitempty" validate:"omitempty,max=512"` - SoftwareVersion string `json:"software_version,omitempty" validate:"omitempty,max=512"` - SubjectType string `json:"subject_type,omitempty" validate:"omitempty,oneof=public pairwise"` - SectorIdentifierURI string `json:"sector_identifier_uri,omitempty" validate:"omitempty,url"` - DefaultMaxAge int64 `json:"default_max_age,omitempty" validate:"omitempty,min=0"` - RequireAuthTime bool `json:"require_auth_time,omitempty" validate:"omitempty,bool"` - DefaultACRValues []string `json:"default_acr_values,omitempty" validate:"omitempty,unique,dive,max=100"` - InitiateLoginURI string `json:"initiate_login_uri,omitempty" validate:"omitempty,url"` - RequestURIs []string `json:"request_uris,omitempty" validate:"omitempty,unique,dive,url"` - IDTokenSignedResponseAlg string `json:"id_token_signed_response_alg,omitempty" validate:"omitempty,oneof=RS256 ES256 EdDSA"` - IDTokenEncryptedResponseAlg string `json:"id_token_encrypted_response_alg,omitempty" validate:"omitempty,oneof=RSA-OAEP-256 ECDH-ES ECDH-ES+A256KW"` - IDTokenEncryptedResponseEnc string `json:"id_token_encrypted_response_enc,omitempty" validate:"omitempty,oneof=A128CBC-HS256 A192CBC-HS384 A256CBC-HS512 A128GCM A192GCM A256GCM"` - UserInfoSignedResponseAlg string `json:"userinfo_signed_response_alg,omitempty" validate:"omitempty,oneof=RS256 ES256 EdDSA"` - UserInfoEncryptedResponseAlg string `json:"userinfo_encrypted_response_alg,omitempty" validate:"omitempty,oneof=RSA-OAEP-256 ECDH-ES ECDH-ES+A256KW"` - UserInfoEncryptedResponseEnc string `json:"userinfo_encrypted_response_enc,omitempty" validate:"omitempty,oneof=A128CBC-HS256 A192CBC-HS384 A256CBC-HS512 A128GCM A192GCM A256GCM"` - RequestObjectSigningAlg string `json:"request_object_signing_alg,omitempty" validate:"omitempty,oneof=RS256 ES256 EdDSA"` - RequestObjectEncryptionAlg string `json:"request_object_encryption_alg,omitempty" validate:"omitempty,oneof=RSA-OAEP-256 ECDH-ES ECDH-ES+A256KW"` - RequestObjectEncryptionEnc string `json:"request_object_encryption_enc,omitempty" validate:"omitempty,oneof=A128CBC-HS256 A192CBC-HS384 A256CBC-HS512 A128GCM A192GCM A256GCM"` - TokenEndpointAuthSigningAlg string `json:"token_endpoint_auth_signing_alg,omitempty" validate:"omitempty,oneof=RS256 ES256 EdDSA"` - AccessTokenSigningAlg string `json:"access_token_signing_alg,omitempty" validate:"omitempty,oneof=RS256 ES256 EdDSA"` - SoftwareStatement string `json:"software_statement,omitempty" validate:"omitempty,jwt"` + RedirectURIs []string `json:"redirect_uris,omitempty" validate:"omitempty,min=1,dive,uri"` + TokenEndpointAuthMethod string `json:"token_endpoint_auth_method,omitempty" validate:"omitempty,oneof=none client_secret_basic client_secret_post client_secret_jwt private_key_jwt"` + ResponseTypes []string `json:"response_types,omitempty" validate:"omitempty,dive,oneof=code 'code id_token'"` + GrantTypes []string `json:"grant_types,omitempty" validate:"omitempty,min=1,dive,oneof=authorization_code refresh_token client_credentials urn:ietf:params:oauth:grant-type:jwt-bearer"` + ApplicationType string `json:"application_type" validate:"required,oneof=native service mcp web spa backend device"` + ClientName string `json:"client_name" validate:"required,min=1,max=255"` + ClientURI string `json:"client_uri" validate:"required,url"` + LogoURI string `json:"logo_uri,omitempty" validate:"omitempty,url"` + Scope string `json:"scope" validate:"required,multiple_scope"` + Contacts []string `json:"contacts,omitempty" validate:"omitempty,unique,dive,email"` + TOSURI string `json:"tos_uri,omitempty" validate:"omitempty,url"` + PolicyURI string `json:"policy_uri,omitempty" validate:"omitempty,url"` + JWKsURI string `json:"jwks_uri,omitempty" validate:"omitempty,url"` + JWKs *utils.JWKSet `json:"jwks,omitempty" validate:"omitempty,json"` + SoftwareID string `json:"software_id,omitempty" validate:"omitempty,max=512"` + SoftwareVersion string `json:"software_version,omitempty" validate:"omitempty,max=512"` + SubjectType string `json:"subject_type,omitempty" validate:"omitempty,oneof=public pairwise"` + SectorIdentifierURI string `json:"sector_identifier_uri,omitempty" validate:"omitempty,url"` + DefaultMaxAge int64 `json:"default_max_age,omitempty" validate:"omitempty,min=0"` + RequireAuthTime bool `json:"require_auth_time,omitempty" validate:"omitempty,bool"` + DefaultACRValues []string `json:"default_acr_values,omitempty" validate:"omitempty,unique,dive,max=100"` + InitiateLoginURI string `json:"initiate_login_uri,omitempty" validate:"omitempty,url"` + RequestURIs []string `json:"request_uris,omitempty" validate:"omitempty,unique,dive,url"` + IDTokenSignedResponseAlg string `json:"id_token_signed_response_alg,omitempty" validate:"omitempty,oneof=RS256 ES256 EdDSA"` + IDTokenEncryptedResponseAlg string `json:"id_token_encrypted_response_alg,omitempty" validate:"omitempty,oneof=RSA-OAEP-256 ECDH-ES ECDH-ES+A256KW"` + IDTokenEncryptedResponseEnc string `json:"id_token_encrypted_response_enc,omitempty" validate:"omitempty,oneof=A128CBC-HS256 A192CBC-HS384 A256CBC-HS512 A128GCM A192GCM A256GCM"` + UserInfoSignedResponseAlg string `json:"userinfo_signed_response_alg,omitempty" validate:"omitempty,oneof=RS256 ES256 EdDSA"` + UserInfoEncryptedResponseAlg string `json:"userinfo_encrypted_response_alg,omitempty" validate:"omitempty,oneof=RSA-OAEP-256 ECDH-ES ECDH-ES+A256KW"` + UserInfoEncryptedResponseEnc string `json:"userinfo_encrypted_response_enc,omitempty" validate:"omitempty,oneof=A128CBC-HS256 A192CBC-HS384 A256CBC-HS512 A128GCM A192GCM A256GCM"` + RequestObjectSigningAlg string `json:"request_object_signing_alg,omitempty" validate:"omitempty,oneof=RS256 ES256 EdDSA"` + RequestObjectEncryptionAlg string `json:"request_object_encryption_alg,omitempty" validate:"omitempty,oneof=RSA-OAEP-256 ECDH-ES ECDH-ES+A256KW"` + RequestObjectEncryptionEnc string `json:"request_object_encryption_enc,omitempty" validate:"omitempty,oneof=A128CBC-HS256 A192CBC-HS384 A256CBC-HS512 A128GCM A192GCM A256GCM"` + TokenEndpointAuthSigningAlg string `json:"token_endpoint_auth_signing_alg,omitempty" validate:"omitempty,oneof=RS256 ES256 EdDSA"` + AccessTokenSigningAlg string `json:"access_token_signing_alg,omitempty" validate:"omitempty,oneof=RS256 ES256 EdDSA"` + SoftwareStatement string `json:"software_statement,omitempty" validate:"omitempty,jwt"` } type OAuthDynamicRegistrationIATAuthHiddenFieldsBody struct { diff --git a/idp/internal/controllers/dynamic_registration_domains.go b/idp/internal/controllers/dynamic_registration_domains.go index dd35f1e..d240a83 100644 --- a/idp/internal/controllers/dynamic_registration_domains.go +++ b/idp/internal/controllers/dynamic_registration_domains.go @@ -66,8 +66,8 @@ func (c *Controllers) ListAccountCredentialsRegistrationDomains(ctx fiber.Ctx) e } queryParams := params.DynamicRegistrationDomainQueryParams{ - Limit: fiber.Query[int](ctx, "limit", 10), - Offset: fiber.Query[int](ctx, "offset", 0), + Limit: fiber.Query(ctx, "limit", 10), + Offset: fiber.Query(ctx, "offset", 0), Order: ctx.Query("order", "date"), Search: ctx.Query("search"), } diff --git a/idp/internal/controllers/oauth_dynamic_registration.go b/idp/internal/controllers/oauth_dynamic_registration.go index 7008c07..6a08fcd 100644 --- a/idp/internal/controllers/oauth_dynamic_registration.go +++ b/idp/internal/controllers/oauth_dynamic_registration.go @@ -41,6 +41,9 @@ func (c *Controllers) OAuthDynamicRegistration(ctx fiber.Ctx) error { if err := c.validate.StructCtx(ctx.Context(), body); err != nil { return oauthErrorResponse(logger, ctx, exceptions.OAuthErrorInvalidClientMetadata) } + if body.JWKs != nil && body.JWKsURI != "" { + return oauthErrorResponse(logger, ctx, exceptions.OAuthErrorInvalidClientMetadata) + } isAuthenticated, ok := ctx.Locals("isAuthenticated").(bool) if !ok { diff --git a/idp/internal/controllers/users.go b/idp/internal/controllers/users.go index af6060b..33d082c 100644 --- a/idp/internal/controllers/users.go +++ b/idp/internal/controllers/users.go @@ -107,8 +107,8 @@ func (c *Controllers) ListUsers(ctx fiber.Ctx) error { } queryParams := params.ListUsersQueryParams{ - Limit: fiber.Query[int](ctx, "limit", 10), - Offset: fiber.Query[int](ctx, "offset", 0), + Limit: fiber.Query(ctx, "limit", 10), + Offset: fiber.Query(ctx, "offset", 0), Order: ctx.Query("order", "date"), Search: ctx.Query("search"), } diff --git a/idp/internal/providers/crypto/jwk.go b/idp/internal/providers/crypto/jwk.go index c8821e1..7fba577 100644 --- a/idp/internal/providers/crypto/jwk.go +++ b/idp/internal/providers/crypto/jwk.go @@ -195,7 +195,12 @@ func (e *Crypto) GenerateES256KeyPair( } kid := utils.ExtractECDSAKeyID(priv.Public().(*ecdsa.PublicKey)) - publicJwk := utils.EncodeP256Jwk(&priv.PublicKey, kid) + publicJwk, err := utils.EncodeP256Jwk(&priv.PublicKey, kid) + if err != nil { + logger.ErrorContext(ctx, "Failed to encode JWK", "error", err) + return KeyPair{}, exceptions.NewInternalServerError() + } + if _, err := opts.StoreFN(dekID, utils.SupportedCryptoSuiteES256, kid, encryptedPrivateKey, &publicJwk); err != nil { logger.ErrorContext(ctx, "Failed to store private key", "error", err) return KeyPair{}, exceptions.NewInternalServerError() diff --git a/idp/internal/providers/tokens/dynamic_registration_software_statements.go b/idp/internal/providers/tokens/dynamic_registration_software_statements.go index 0770eec..84ccfe3 100644 --- a/idp/internal/providers/tokens/dynamic_registration_software_statements.go +++ b/idp/internal/providers/tokens/dynamic_registration_software_statements.go @@ -17,40 +17,40 @@ import ( const dynamicRegistrationSoftwareStatementsLocation = "dynamic_registration_software_statements" type SoftwareStatementClaims struct { - RedirectURIs []string `json:"redirect_uris,omitempty" validate:"omitempty,min=1,dive,uri"` - TokenEndpointAuthMethod string `json:"token_endpoint_auth_method,omitempty" validate:"omitempty,oneof=none client_secret_basic client_secret_post client_secret_jwt private_key_jwt"` - GrantTypes []string `json:"grant_types,omitempty" validate:"omitempty,min=1,dive,oneof=authorization_code refresh_token client_credentials urn:ietf:params:oauth:grant-type:jwt-bearer"` - ResponseTypes []string `json:"response_types,omitempty" validate:"omitempty,dive,oneof=none code 'code id_token'"` - ApplicationType string `json:"application_type,omitempty" validate:"omitempty,oneof=native service mcp"` - ClientName string `json:"client_name,omitempty" validate:"omitempty,min=1,max=255"` - ClientURI string `json:"client_uri,omitempty" validate:"omitempty,url"` - LogoURI string `json:"logo_uri,omitempty" validate:"omitempty,url"` - Scope string `json:"scope,omitempty" validate:"omitempty,multiple_scope"` - Contacts []string `json:"contacts,omitempty" validate:"omitempty,unique,dive,email"` - TOSURI string `json:"tos_uri,omitempty" validate:"omitempty,url"` - PolicyURI string `json:"policy_uri,omitempty" validate:"omitempty,url"` - JWKsURI string `json:"jwks_uri,omitempty" validate:"omitempty,url"` - JWKs []string `json:"jwks,omitempty" validate:"omitempty,json"` - SoftwareID string `json:"software_id,omitempty" validate:"omitempty,max=512"` - SoftwareVersion string `json:"software_version,omitempty" validate:"omitempty,max=512"` - SubjectType string `json:"subject_type,omitempty" validate:"omitempty,oneof=public pairwise"` - SectorIdentifierURI string `json:"sector_identifier_uri,omitempty" validate:"omitempty,url"` - DefaultMaxAge int64 `json:"default_max_age,omitempty" validate:"omitempty,min=0"` - RequireAuthTime bool `json:"require_auth_time,omitempty" validate:"omitempty,bool"` - DefaultACRValues []string `json:"default_acr_values,omitempty" validate:"omitempty,unique,dive,max=100"` - InitiateLoginURI string `json:"initiate_login_uri,omitempty" validate:"omitempty,url"` - RequestURIs []string `json:"request_uris,omitempty" validate:"omitempty,unique,dive,url"` - IDTokenSignedResponseAlg string `json:"id_token_signed_response_alg,omitempty" validate:"omitempty,oneof=RS256 ES256 EdDSA"` - IDTokenEncryptedResponseAlg string `json:"id_token_encrypted_response_alg,omitempty" validate:"omitempty,oneof=RSA-OAEP-256 ECDH-ES ECDH-ES+A256KW"` - IDTokenEncryptedResponseEnc string `json:"id_token_encrypted_response_enc,omitempty" validate:"omitempty,oneof=A128CBC-HS256 A192CBC-HS384 A256CBC-HS512 A128GCM A192GCM A256GCM"` - UserInfoSignedResponseAlg string `json:"userinfo_signed_response_alg,omitempty" validate:"omitempty,oneof=RS256 ES256 EdDSA"` - UserInfoEncryptedResponseAlg string `json:"userinfo_encrypted_response_alg,omitempty" validate:"omitempty,oneof=RSA-OAEP-256 ECDH-ES ECDH-ES+A256KW"` - UserInfoEncryptedResponseEnc string `json:"userinfo_encrypted_response_enc,omitempty" validate:"omitempty,oneof=A128CBC-HS256 A192CBC-HS384 A256CBC-HS512 A128GCM A192GCM A256GCM"` - RequestObjectSigningAlg string `json:"request_object_signing_alg,omitempty" validate:"omitempty,oneof=RS256 ES256 EdDSA"` - RequestObjectEncryptionAlg string `json:"request_object_encryption_alg,omitempty" validate:"omitempty,oneof=RSA-OAEP-256 ECDH-ES ECDH-ES+A256KW"` - RequestObjectEncryptionEnc string `json:"request_object_encryption_enc,omitempty" validate:"omitempty,oneof=A128CBC-HS256 A192CBC-HS384 A256CBC-HS512 A128GCM A192GCM A256GCM"` - TokenEndpointAuthSigningAlg string `json:"token_endpoint_auth_signing_alg,omitempty" validate:"omitempty,oneof=RS256 ES256 EdDSA"` - AccessTokenSigningAlg string `json:"access_token_signing_alg,omitempty" validate:"omitempty,oneof=RS256 ES256 EdDSA"` + RedirectURIs []string `json:"redirect_uris,omitempty" validate:"omitempty,min=1,dive,uri"` + TokenEndpointAuthMethod string `json:"token_endpoint_auth_method,omitempty" validate:"omitempty,oneof=none client_secret_basic client_secret_post client_secret_jwt private_key_jwt"` + GrantTypes []string `json:"grant_types,omitempty" validate:"omitempty,min=1,dive,oneof=authorization_code refresh_token client_credentials urn:ietf:params:oauth:grant-type:jwt-bearer"` + ResponseTypes []string `json:"response_types,omitempty" validate:"omitempty,dive,oneof=none code 'code id_token'"` + ApplicationType string `json:"application_type,omitempty" validate:"omitempty,oneof=native service mcp"` + ClientName string `json:"client_name,omitempty" validate:"omitempty,min=1,max=255"` + ClientURI string `json:"client_uri,omitempty" validate:"omitempty,url"` + LogoURI string `json:"logo_uri,omitempty" validate:"omitempty,url"` + Scope string `json:"scope,omitempty" validate:"omitempty,multiple_scope"` + Contacts []string `json:"contacts,omitempty" validate:"omitempty,unique,dive,email"` + TOSURI string `json:"tos_uri,omitempty" validate:"omitempty,url"` + PolicyURI string `json:"policy_uri,omitempty" validate:"omitempty,url"` + JWKsURI string `json:"jwks_uri,omitempty" validate:"omitempty,url"` + JWKs *utils.JWKSet `json:"jwks,omitempty" validate:"omitempty"` + SoftwareID string `json:"software_id,omitempty" validate:"omitempty,max=512"` + SoftwareVersion string `json:"software_version,omitempty" validate:"omitempty,max=512"` + SubjectType string `json:"subject_type,omitempty" validate:"omitempty,oneof=public pairwise"` + SectorIdentifierURI string `json:"sector_identifier_uri,omitempty" validate:"omitempty,url"` + DefaultMaxAge int64 `json:"default_max_age,omitempty" validate:"omitempty,min=0"` + RequireAuthTime bool `json:"require_auth_time,omitempty" validate:"omitempty,bool"` + DefaultACRValues []string `json:"default_acr_values,omitempty" validate:"omitempty,unique,dive,max=100"` + InitiateLoginURI string `json:"initiate_login_uri,omitempty" validate:"omitempty,url"` + RequestURIs []string `json:"request_uris,omitempty" validate:"omitempty,unique,dive,url"` + IDTokenSignedResponseAlg string `json:"id_token_signed_response_alg,omitempty" validate:"omitempty,oneof=RS256 ES256 EdDSA"` + IDTokenEncryptedResponseAlg string `json:"id_token_encrypted_response_alg,omitempty" validate:"omitempty,oneof=RSA-OAEP-256 ECDH-ES ECDH-ES+A256KW"` + IDTokenEncryptedResponseEnc string `json:"id_token_encrypted_response_enc,omitempty" validate:"omitempty,oneof=A128CBC-HS256 A192CBC-HS384 A256CBC-HS512 A128GCM A192GCM A256GCM"` + UserInfoSignedResponseAlg string `json:"userinfo_signed_response_alg,omitempty" validate:"omitempty,oneof=RS256 ES256 EdDSA"` + UserInfoEncryptedResponseAlg string `json:"userinfo_encrypted_response_alg,omitempty" validate:"omitempty,oneof=RSA-OAEP-256 ECDH-ES ECDH-ES+A256KW"` + UserInfoEncryptedResponseEnc string `json:"userinfo_encrypted_response_enc,omitempty" validate:"omitempty,oneof=A128CBC-HS256 A192CBC-HS384 A256CBC-HS512 A128GCM A192GCM A256GCM"` + RequestObjectSigningAlg string `json:"request_object_signing_alg,omitempty" validate:"omitempty,oneof=RS256 ES256 EdDSA"` + RequestObjectEncryptionAlg string `json:"request_object_encryption_alg,omitempty" validate:"omitempty,oneof=RSA-OAEP-256 ECDH-ES ECDH-ES+A256KW"` + RequestObjectEncryptionEnc string `json:"request_object_encryption_enc,omitempty" validate:"omitempty,oneof=A128CBC-HS256 A192CBC-HS384 A256CBC-HS512 A128GCM A192GCM A256GCM"` + TokenEndpointAuthSigningAlg string `json:"token_endpoint_auth_signing_alg,omitempty" validate:"omitempty,oneof=RS256 ES256 EdDSA"` + AccessTokenSigningAlg string `json:"access_token_signing_alg,omitempty" validate:"omitempty,oneof=RS256 ES256 EdDSA"` } type GetUnknownPublicJWK = func(kid string) (utils.JWK, error) @@ -78,7 +78,7 @@ func (t *Tokens) VerifySoftwareStatement( logger.DebugContext(ctx, "Verifying software statement token") var claims softwareStatementJWTClaims - if _, err := jwt.ParseWithClaims(opts.SoftwareStatement, &claims, func(token *jwt.Token) (interface{}, error) { + if _, err := jwt.ParseWithClaims(opts.SoftwareStatement, &claims, func(token *jwt.Token) (any, error) { kid, err := extractTokenKID(token) if err != nil { logger.DebugContext(ctx, "Failed to extract KID from software statement token", "error", err) diff --git a/idp/internal/services/account_credentials_registration.go b/idp/internal/services/account_credentials_registration.go index d205124..19cda2b 100644 --- a/idp/internal/services/account_credentials_registration.go +++ b/idp/internal/services/account_credentials_registration.go @@ -165,10 +165,14 @@ func (s *Services) mapAccountCredentialsRegistrationDataToDBParams( return database.CreateAccountCredentialsParams{}, serviceErr } - jwks, serviceErr := mapEmptyJWKs(logger, ctx, opts.data.JWKs) - if serviceErr != nil { - logger.ErrorContext(ctx, "Failed to map JWKs", "serviceError", serviceErr) - return database.CreateAccountCredentialsParams{}, serviceErr + var jsonJwks []byte + if opts.data.JWKs != nil && len(opts.data.JWKs.Keys) > 0 { + var err error + jsonJwks, err = opts.data.JWKs.MarshalJSON() + if err != nil { + logger.ErrorContext(ctx, "Failed to marshal JWKs to JSON", "error", err) + return database.CreateAccountCredentialsParams{}, exceptions.NewInternalServerError() + } } params := database.CreateAccountCredentialsParams{ @@ -194,7 +198,7 @@ func (s *Services) mapAccountCredentialsRegistrationDataToDBParams( TosUri: mapEmptyURL(opts.data.TOSURI), PolicyUri: mapEmptyURL(opts.data.PolicyURI), JwksUri: mapEmptyURL(opts.data.JWKsURI), - Jwks: jwks, + Jwks: jsonJwks, SoftwareID: mapEmptyString(opts.data.SoftwareID), SoftwareVersion: mapEmptyString(opts.data.SoftwareVersion), CredentialsType: opts.applicationType, @@ -242,14 +246,13 @@ func (s *Services) mapAccountCredentialsRegistrationDataToDBParams( if opts.claims.JWKsURI != "" { params.JwksUri = mapEmptyURL(opts.claims.JWKsURI) } - if len(opts.claims.JWKs) > 0 { - jwks, serviceErr := mapEmptyJWKs(logger, ctx, opts.claims.JWKs) - if serviceErr != nil { - logger.ErrorContext(ctx, "Failed to map JWKs", "serviceError", serviceErr) - return database.CreateAccountCredentialsParams{}, serviceErr + if opts.claims.JWKs != nil && len(opts.claims.JWKs.Keys) > 0 { + var err error + if jsonJwks, err = opts.data.JWKs.MarshalJSON(); err != nil { + logger.ErrorContext(ctx, "Failed to marshal JWKs to JSON", "error", err) + return database.CreateAccountCredentialsParams{}, exceptions.NewInternalServerError() } - - params.Jwks = jwks + params.Jwks = jsonJwks } if opts.claims.SoftwareID != "" { params.SoftwareID = mapEmptyString(opts.claims.SoftwareID) @@ -369,7 +372,7 @@ type CreateAccountCredentialsRegistrationOptions struct { SoftwareVersion string SoftwareStatement string JWKsURI string - JWKs []string + JWKs *utils.JWKSet FrontendDomain string BackendDomain string RequireAuthTime bool @@ -575,7 +578,6 @@ func (s *Services) CreateAccountCredentialsRegistration( if serviceErr := s.validateSoftwareStatementClaims(ctx, validateSoftwareStatementClaimsOptions{ requestID: opts.RequestID, claims: &ssClaims, - data: &data, allowedScopes: utils.SliceToHashSet(allowedAccountCredentialsScopes), }); serviceErr != nil { logger.WarnContext(ctx, "Failed to validate software statement claims", "serviceError", serviceErr) diff --git a/idp/internal/services/app_dynamic_registration.go b/idp/internal/services/app_dynamic_registration.go index acb2762..e9af935 100644 --- a/idp/internal/services/app_dynamic_registration.go +++ b/idp/internal/services/app_dynamic_registration.go @@ -271,7 +271,7 @@ type CreateAppCredentialsRegistrationOptions struct { SoftwareVersion string SoftwareStatement string JWKsURI string - JWKs []string + JWKs *utils.JWKSet FrontendDomain string BackendDomain string RequireAuthTime bool @@ -517,7 +517,6 @@ func (s *Services) CreateAppCredentialsRegistration( if serviceErr := s.validateSoftwareStatementClaims(ctx, validateSoftwareStatementClaimsOptions{ requestID: opts.RequestID, claims: &ssClaims, - data: &data, allowedScopes: utils.SliceToHashSet(allowedAppScopes), }); serviceErr != nil { logger.WarnContext(ctx, "Failed to validate software statement claims", "serviceError", serviceErr) diff --git a/idp/internal/services/client_credentials.go b/idp/internal/services/client_credentials.go index 4b4f843..20635aa 100644 --- a/idp/internal/services/client_credentials.go +++ b/idp/internal/services/client_credentials.go @@ -155,7 +155,11 @@ func buildES256Jwk( pub := priv.Public().(*ecdsa.PublicKey) kid := utils.ExtractECDSAKeyID(pub) - dbJwk := utils.EncodeP256Jwk(pub, kid) + dbJwk, err := utils.EncodeP256Jwk(pub, kid) + if err != nil { + logger.ErrorContext(ctx, "Failed to encode ES256 public key to JWK", "error", err) + return "", nil, nil, exceptions.NewInternalServerError() + } jsonJwk, err := json.Marshal(dbJwk) if err != nil { @@ -163,7 +167,12 @@ func buildES256Jwk( return "", nil, nil, exceptions.NewInternalServerError() } - privateJWK := utils.EncodeP256JwkPrivate(priv, kid) + privateJWK, err := utils.EncodeP256JwkPrivate(priv, kid) + if err != nil { + logger.ErrorContext(ctx, "Failed to encode ES256 private key to JWK", "error", err) + return "", nil, nil, exceptions.NewInternalServerError() + } + logger.InfoContext(ctx, "Generated ES256 JWK successfully", "kid", kid) return kid, jsonJwk, &privateJWK, nil } diff --git a/idp/internal/services/dtos/account_credentials.go b/idp/internal/services/dtos/account_credentials.go index 8f88ddb..7d4e667 100644 --- a/idp/internal/services/dtos/account_credentials.go +++ b/idp/internal/services/dtos/account_credentials.go @@ -97,15 +97,14 @@ func (ak *AccountCredentialsDTO) UnmarshalJSON(data []byte) error { } if aux.JWKs != nil { - jwks := make([]utils.JWK, 0, len(aux.JWKs)) - for _, raw := range aux.JWKs { + ak.JWKs = make([]utils.JWK, len(aux.JWKs)) + for i, raw := range aux.JWKs { jwk, err := utils.JsonToJWK(raw) if err != nil { return err } - jwks = append(jwks, jwk) + ak.JWKs[i] = jwk } - ak.JWKs = jwks } return nil diff --git a/idp/internal/services/helpers.go b/idp/internal/services/helpers.go index 98fbeae..39d7de9 100644 --- a/idp/internal/services/helpers.go +++ b/idp/internal/services/helpers.go @@ -8,7 +8,6 @@ package services import ( "context" - "encoding/json" "fmt" "log/slog" "net" @@ -384,36 +383,6 @@ func (s *Services) verifyTXTRecord( return nil } -func mapEmptyJWKs(logger *slog.Logger, ctx context.Context, jsonJWKs []string) ([]byte, *exceptions.ServiceError) { - var jwks []byte - - if len(jsonJWKs) > 0 { - rawJWKs := make([]json.RawMessage, 0, len(jsonJWKs)) - for _, jwk := range jsonJWKs { - jwk, err := utils.JsonToJWK([]byte(jwk)) - if err != nil { - logger.ErrorContext(ctx, "Failed to parse JWK", "error", err) - return nil, exceptions.NewInternalServerError() - } - jwkBytes, err := jwk.MarshalJSON() - if err != nil { - logger.ErrorContext(ctx, "Failed to marshal JWK", "error", err) - return nil, exceptions.NewInternalServerError() - } - rawJWKs = append(rawJWKs, jwkBytes) - } - - var err error - jwks, err = json.Marshal(rawJWKs) - if err != nil { - logger.ErrorContext(ctx, "Failed to marshal JWKS", "error", err) - return nil, exceptions.NewInternalServerError() - } - } - - return jwks, nil -} - func mapGrantType(grantType string) (database.GrantType, *exceptions.ServiceError) { switch utils.Lowered(grantType) { case GrantTypeAuthorizationCode: diff --git a/idp/internal/services/software_statement.go b/idp/internal/services/software_statement.go index a38505f..ea95479 100644 --- a/idp/internal/services/software_statement.go +++ b/idp/internal/services/software_statement.go @@ -40,7 +40,7 @@ type ApplicationRegistrationData struct { TOSURI string PolicyURI string JWKsURI string - JWKs []string + JWKs *utils.JWKSet SoftwareID string SoftwareVersion string SubjectType string @@ -132,7 +132,6 @@ func validateEncryptionAlgorithmPair(alg, enc string) bool { type validateSoftwareStatementClaimsOptions struct { requestID string claims *tokens.SoftwareStatementClaims - data *ApplicationRegistrationData allowedScopes utils.HashSet[string] } @@ -170,57 +169,17 @@ func (s *Services) validateSoftwareStatementClaims( logger.WarnContext(ctx, "Duplicate scopes in software statement", "scopes", scopes) return exceptions.NewValidationError("duplicate scopes") } - - dataScopes := strings.Fields(opts.data.Scope) - if len(dataScopes) != scopesSet.Size() { - logger.WarnContext(ctx, "Scope count mismatch", "expected", len(dataScopes), "got", scopesSet.Size()) - return exceptions.NewValidationError("scope count mismatch") - } - - for _, scope := range dataScopes { - if !scopesSet.Contains(scope) { - logger.WarnContext(ctx, "Scope mismatch", "expected", scope, "got", scopesSet.Contains(scope)) - return exceptions.NewValidationError("scope mismatch") - } - } } - if len(opts.claims.JWKs) > 0 { - jwks := make([]utils.JWK, len(opts.claims.JWKs)) - indexMap := make(map[string]int) - for i, rawJWK := range opts.claims.JWKs { - jwk, err := utils.JsonToJWK([]byte(rawJWK)) - if err != nil { - logger.WarnContext(ctx, "Invalid JWK JSON in software statement", "error", err) - return exceptions.NewValidationError("invalid jwks") - } - jwks[i] = jwk - indexMap[jwk.GetKeyID()] = i - } - - if len(opts.data.JWKs) > 0 { - if len(jwks) != len(opts.data.JWKs) { - logger.WarnContext(ctx, "JWK count mismatch", "expected", len(opts.data.JWKs), "got", len(jwks)) - return exceptions.NewValidationError("jwk count mismatch") - } - - for _, rawJWK := range opts.data.JWKs { - jwk, err := utils.JsonToJWK([]byte(rawJWK)) - if err != nil { - logger.WarnContext(ctx, "Invalid JWK JSON in software statement", "error", err) - return exceptions.NewValidationError("invalid jwks") - } + if opts.claims.JWKs != nil && opts.claims.JWKsURI != "" { + logger.WarnContext(ctx, "Both jwks and jwks_uri are set in software statement") + return exceptions.NewValidationError("both jwks and jwks_uri are set") + } - index, ok := indexMap[jwk.GetKeyID()] - if !ok { - logger.WarnContext(ctx, "JWK not found in software statement", "jwk", jwk.GetKeyID()) - return exceptions.NewValidationError("jwk not found in software statement") - } - if jwks[index].ComparePublicKey(jwk) { - logger.WarnContext(ctx, "JWK mismatch", "expected", jwks[index].GetKeyID(), "got", jwk.GetKeyID()) - return exceptions.NewValidationError("jwk mismatch") - } - } + if opts.claims.JWKs != nil && len(opts.claims.JWKs.Keys) > 0 { + if err := opts.claims.JWKs.Validate(); err != nil { + logger.WarnContext(ctx, "JWKs jet is invalid", "error", err) + return exceptions.NewValidationError("jwks is invalid") } } @@ -239,203 +198,6 @@ func (s *Services) validateSoftwareStatementClaims( return exceptions.NewValidationError("request_object encryption algorithm and encoding mismatch") } - if len(opts.data.RedirectURIs) > 0 && len(opts.claims.RedirectURIs) > 0 { - if len(opts.data.RedirectURIs) != len(opts.claims.RedirectURIs) { - logger.WarnContext(ctx, "Redirect URI count mismatch", "expected", len(opts.data.RedirectURIs), "got", len(opts.claims.RedirectURIs)) - return exceptions.NewValidationError("redirect URI count mismatch") - } - - redirectURIsSet := utils.SliceToHashSet(opts.claims.RedirectURIs) - if redirectURIsSet.Size() != len(opts.claims.RedirectURIs) { - logger.WarnContext(ctx, "Duplicate redirect URIs in software statement", "redirectURIs", opts.claims.RedirectURIs) - return exceptions.NewValidationError("duplicate redirect URIs") - } - - for _, redirectURI := range opts.data.RedirectURIs { - if !redirectURIsSet.Contains(redirectURI) { - logger.WarnContext(ctx, "Redirect URI not found in software statement", "redirectURI", redirectURI) - return exceptions.NewValidationError("redirect URI not found in software statement") - } - } - } - - if opts.claims.TokenEndpointAuthMethod != "" && opts.data.TokenEndpointAuthMethod != "" && opts.claims.TokenEndpointAuthMethod != opts.data.TokenEndpointAuthMethod { - logger.WarnContext(ctx, "Token endpoint auth method mismatch", "expected", opts.data.TokenEndpointAuthMethod, "got", opts.claims.TokenEndpointAuthMethod) - return exceptions.NewValidationError("token endpoint auth method mismatch") - } - - if len(opts.claims.ResponseTypes) > 0 && len(opts.data.ResponseTypes) > 0 { - if len(opts.claims.ResponseTypes) != len(opts.data.ResponseTypes) { - logger.WarnContext(ctx, "Response type count mismatch", "expected", len(opts.data.ResponseTypes), "got", len(opts.claims.ResponseTypes)) - return exceptions.NewValidationError("response type count mismatch") - } - - responseTypesSet := utils.SliceToHashSet(opts.claims.ResponseTypes) - if responseTypesSet.Size() != len(opts.claims.ResponseTypes) { - logger.WarnContext(ctx, "Duplicate response types in software statement", "responseTypes", opts.claims.ResponseTypes) - return exceptions.NewValidationError("duplicate response types") - } - - for _, responseType := range opts.data.ResponseTypes { - if !responseTypesSet.Contains(responseType) { - logger.WarnContext(ctx, "Response type not found in software statement", "responseType", responseType) - return exceptions.NewValidationError("response type not found in software statement") - } - } - } - - if len(opts.claims.GrantTypes) > 0 && len(opts.data.GrantTypes) > 0 { - if len(opts.claims.GrantTypes) != len(opts.data.GrantTypes) { - logger.WarnContext(ctx, "Grant type count mismatch", "expected", len(opts.data.GrantTypes), "got", len(opts.claims.GrantTypes)) - return exceptions.NewValidationError("grant type count mismatch") - } - - grantTypesSet := utils.SliceToHashSet(opts.claims.GrantTypes) - if grantTypesSet.Size() != len(opts.claims.GrantTypes) { - logger.WarnContext(ctx, "Duplicate grant types in software statement", "grantTypes", opts.claims.GrantTypes) - return exceptions.NewValidationError("duplicate grant types") - } - - for _, grantType := range opts.data.GrantTypes { - if !grantTypesSet.Contains(grantType) { - logger.WarnContext(ctx, "Grant type not found in software statement", "grantType", grantType) - return exceptions.NewValidationError("grant type not found in software statement") - } - } - } - - if opts.claims.ApplicationType != "" && opts.data.ApplicationType != "" && opts.claims.ApplicationType != opts.data.ApplicationType { - logger.WarnContext(ctx, "Application type mismatch", "expected", opts.data.ApplicationType, "got", opts.claims.ApplicationType) - return exceptions.NewValidationError("application type mismatch") - } - if opts.claims.ClientName != "" && opts.data.ClientName != "" && opts.claims.ClientName != opts.data.ClientName { - logger.WarnContext(ctx, "Client name mismatch", "expected", opts.data.ClientName, "got", opts.claims.ClientName) - return exceptions.NewValidationError("client name mismatch") - } - if opts.claims.ClientURI != "" && opts.data.ClientURI != "" && opts.claims.ClientURI != opts.data.ClientURI { - logger.WarnContext(ctx, "Client URI mismatch", "expected", opts.data.ClientURI, "got", opts.claims.ClientURI) - return exceptions.NewValidationError("client URI mismatch") - } - if opts.claims.LogoURI != "" && opts.data.LogoURI != "" && opts.claims.LogoURI != opts.data.LogoURI { - logger.WarnContext(ctx, "Logo URI mismatch", "expected", opts.data.LogoURI, "got", opts.claims.LogoURI) - return exceptions.NewValidationError("logo URI mismatch") - } - if opts.claims.TOSURI != "" && opts.data.TOSURI != "" && opts.claims.TOSURI != opts.data.TOSURI { - logger.WarnContext(ctx, "Terms of Service URI mismatch", "expected", opts.data.TOSURI, "got", opts.claims.TOSURI) - return exceptions.NewValidationError("terms of service URI mismatch") - } - if opts.claims.PolicyURI != "" && opts.data.PolicyURI != "" && opts.claims.PolicyURI != opts.data.PolicyURI { - logger.WarnContext(ctx, "Policy URI mismatch", "expected", opts.data.PolicyURI, "got", opts.claims.PolicyURI) - return exceptions.NewValidationError("policy URI mismatch") - } - if opts.claims.SoftwareID != "" && opts.data.SoftwareID != "" && opts.claims.SoftwareID != opts.data.SoftwareID { - logger.WarnContext(ctx, "Software ID mismatch", "expected", opts.data.SoftwareID, "got", opts.claims.SoftwareID) - return exceptions.NewValidationError("software ID mismatch") - } - if opts.claims.SoftwareVersion != "" && opts.data.SoftwareVersion != "" && opts.claims.SoftwareVersion != opts.data.SoftwareVersion { - logger.WarnContext(ctx, "Software version mismatch", "expected", opts.data.SoftwareVersion, "got", opts.claims.SoftwareVersion) - return exceptions.NewValidationError("software version mismatch") - } - if opts.claims.SubjectType != "" && opts.data.SubjectType != "" && opts.claims.SubjectType != opts.data.SubjectType { - logger.WarnContext(ctx, "Subject type mismatch", "expected", opts.data.SubjectType, "got", opts.claims.SubjectType) - return exceptions.NewValidationError("subject type mismatch") - } - if opts.claims.SectorIdentifierURI != "" && opts.data.SectorIdentifierURI != "" && opts.claims.SectorIdentifierURI != opts.data.SectorIdentifierURI { - logger.WarnContext(ctx, "Sector identifier URI mismatch", "expected", opts.data.SectorIdentifierURI, "got", opts.claims.SectorIdentifierURI) - return exceptions.NewValidationError("sector identifier URI mismatch") - } - if opts.claims.DefaultMaxAge != 0 && opts.data.DefaultMaxAge != 0 && opts.claims.DefaultMaxAge != opts.data.DefaultMaxAge { - logger.WarnContext(ctx, "Default max age mismatch", "expected", opts.data.DefaultMaxAge, "got", opts.claims.DefaultMaxAge) - return exceptions.NewValidationError("default max age mismatch") - } - if !opts.claims.RequireAuthTime && opts.claims.RequireAuthTime != opts.data.RequireAuthTime { - logger.WarnContext(ctx, "Require auth time mismatch", "expected", opts.data.RequireAuthTime, "got", opts.claims.RequireAuthTime) - return exceptions.NewValidationError("require auth time mismatch") - } - if len(opts.claims.DefaultACRValues) > 0 && len(opts.data.DefaultACRValues) > 0 { - if len(opts.claims.DefaultACRValues) != len(opts.data.DefaultACRValues) { - logger.WarnContext(ctx, "Default ACR value count mismatch", "expected", len(opts.data.DefaultACRValues), "got", len(opts.claims.DefaultACRValues)) - return exceptions.NewValidationError("default ACR value count mismatch") - } - defaultACRValuesSet := utils.SliceToHashSet(opts.claims.DefaultACRValues) - if defaultACRValuesSet.Size() != len(opts.claims.DefaultACRValues) { - logger.WarnContext(ctx, "Duplicate default ACR values in software statement", "defaultACRValues", opts.claims.DefaultACRValues) - return exceptions.NewValidationError("duplicate default ACR values") - } - for _, defaultACRValue := range opts.data.DefaultACRValues { - if !defaultACRValuesSet.Contains(defaultACRValue) { - logger.WarnContext(ctx, "Default ACR value not found in software statement", "defaultACRValue", defaultACRValue) - return exceptions.NewValidationError("default ACR value not found in software statement") - } - } - } - if opts.claims.InitiateLoginURI != "" && opts.data.InitiateLoginURI != "" && opts.claims.InitiateLoginURI != opts.data.InitiateLoginURI { - logger.WarnContext(ctx, "Initiate login URI mismatch", "expected", opts.data.InitiateLoginURI, "got", opts.claims.InitiateLoginURI) - return exceptions.NewValidationError("initiate login URI mismatch") - } - if len(opts.claims.RequestURIs) > 0 && len(opts.data.RequestURIs) > 0 { - if len(opts.claims.RequestURIs) != len(opts.data.RequestURIs) { - logger.WarnContext(ctx, "Request URI count mismatch", "expected", len(opts.data.RequestURIs), "got", len(opts.claims.RequestURIs)) - return exceptions.NewValidationError("request URI count mismatch") - } - - requestURIsSet := utils.SliceToHashSet(opts.claims.RequestURIs) - if requestURIsSet.Size() != len(opts.claims.RequestURIs) { - logger.WarnContext(ctx, "Duplicate request URIs in software statement", "requestURIs", opts.claims.RequestURIs) - return exceptions.NewValidationError("duplicate request URIs") - } - for _, requestURI := range opts.data.RequestURIs { - if !requestURIsSet.Contains(requestURI) { - logger.WarnContext(ctx, "Request URI not found in software statement", "requestURI", requestURI) - return exceptions.NewValidationError("request URI not found in software statement") - } - } - } - if opts.claims.IDTokenSignedResponseAlg != "" && opts.data.IDTokenSignedResponseAlg != "" && opts.claims.IDTokenSignedResponseAlg != opts.data.IDTokenSignedResponseAlg { - logger.WarnContext(ctx, "ID token signed response algorithm mismatch", "expected", opts.data.IDTokenSignedResponseAlg, "got", opts.claims.IDTokenSignedResponseAlg) - return exceptions.NewValidationError("id token signed response algorithm mismatch") - } - if opts.claims.IDTokenEncryptedResponseAlg != "" && opts.data.IDTokenEncryptedResponseAlg != "" && opts.claims.IDTokenEncryptedResponseAlg != opts.data.IDTokenEncryptedResponseAlg { - logger.WarnContext(ctx, "ID token encrypted response algorithm mismatch", "expected", opts.data.IDTokenEncryptedResponseAlg, "got", opts.claims.IDTokenEncryptedResponseAlg) - return exceptions.NewValidationError("id token encrypted response algorithm mismatch") - } - if opts.claims.IDTokenEncryptedResponseEnc != "" && opts.data.IDTokenEncryptedResponseEnc != "" && opts.claims.IDTokenEncryptedResponseEnc != opts.data.IDTokenEncryptedResponseEnc { - logger.WarnContext(ctx, "ID token encrypted response encoding mismatch", "expected", opts.data.IDTokenEncryptedResponseEnc, "got", opts.claims.IDTokenEncryptedResponseEnc) - return exceptions.NewValidationError("id token encrypted response encoding mismatch") - } - if opts.claims.UserInfoSignedResponseAlg != "" && opts.data.UserInfoSignedResponseAlg != "" && opts.claims.UserInfoSignedResponseAlg != opts.data.UserInfoSignedResponseAlg { - logger.WarnContext(ctx, "User info signed response algorithm mismatch", "expected", opts.data.UserInfoSignedResponseAlg, "got", opts.claims.UserInfoSignedResponseAlg) - return exceptions.NewValidationError("user info signed response algorithm mismatch") - } - if opts.claims.UserInfoEncryptedResponseAlg != "" && opts.data.UserInfoEncryptedResponseAlg != "" && opts.claims.UserInfoEncryptedResponseAlg != opts.data.UserInfoEncryptedResponseAlg { - logger.WarnContext(ctx, "User info encrypted response algorithm mismatch", "expected", opts.data.UserInfoEncryptedResponseAlg, "got", opts.claims.UserInfoEncryptedResponseAlg) - return exceptions.NewValidationError("user info encrypted response algorithm mismatch") - } - if opts.claims.UserInfoEncryptedResponseEnc != "" && opts.data.UserInfoEncryptedResponseEnc != "" && opts.claims.UserInfoEncryptedResponseEnc != opts.data.UserInfoEncryptedResponseEnc { - logger.WarnContext(ctx, "User info encrypted response encoding mismatch", "expected", opts.data.UserInfoEncryptedResponseEnc, "got", opts.claims.UserInfoEncryptedResponseEnc) - return exceptions.NewValidationError("user info encrypted response encoding mismatch") - } - if opts.claims.RequestObjectSigningAlg != "" && opts.data.RequestObjectSigningAlg != "" && opts.claims.RequestObjectSigningAlg != opts.data.RequestObjectSigningAlg { - logger.WarnContext(ctx, "Request object signed response algorithm mismatch", "expected", opts.data.RequestObjectSigningAlg, "got", opts.claims.RequestObjectSigningAlg) - return exceptions.NewValidationError("request object signed response algorithm mismatch") - } - if opts.claims.RequestObjectEncryptionAlg != "" && opts.data.RequestObjectEncryptionAlg != "" && opts.claims.RequestObjectEncryptionAlg != opts.data.RequestObjectEncryptionAlg { - logger.WarnContext(ctx, "Request object encrypted response algorithm mismatch", "expected", opts.data.RequestObjectEncryptionAlg, "got", opts.claims.RequestObjectEncryptionAlg) - return exceptions.NewValidationError("request object encrypted response algorithm mismatch") - } - if opts.claims.RequestObjectEncryptionEnc != "" && opts.data.RequestObjectEncryptionEnc != "" && opts.claims.RequestObjectEncryptionEnc != opts.data.RequestObjectEncryptionEnc { - logger.WarnContext(ctx, "Request object encrypted response encoding mismatch", "expected", opts.data.RequestObjectEncryptionEnc, "got", opts.claims.RequestObjectEncryptionEnc) - return exceptions.NewValidationError("request object encrypted response encoding mismatch") - } - if opts.claims.TokenEndpointAuthSigningAlg != "" && opts.data.TokenEndpointAuthSigningAlg != "" && opts.claims.TokenEndpointAuthSigningAlg != opts.data.TokenEndpointAuthSigningAlg { - logger.WarnContext(ctx, "Token endpoint auth signing algorithm mismatch", "expected", opts.data.TokenEndpointAuthSigningAlg, "got", opts.claims.TokenEndpointAuthSigningAlg) - return exceptions.NewValidationError("token endpoint auth signing algorithm mismatch") - } - if opts.claims.AccessTokenSigningAlg != "" && opts.data.AccessTokenSigningAlg != "" && opts.claims.AccessTokenSigningAlg != opts.data.AccessTokenSigningAlg { - logger.WarnContext(ctx, "Access token signing algorithm mismatch", "expected", opts.data.AccessTokenSigningAlg, "got", opts.claims.AccessTokenSigningAlg) - return exceptions.NewValidationError("access token signing algorithm mismatch") - } - logger.InfoContext(ctx, "Validated software statement claims") return nil } @@ -445,7 +207,7 @@ type buildDynamicRegistrationSoftwareStatementFuncOptions struct { accountPublicID uuid.UUID verificationMethods []database.SoftwareStatementVerificationMethod jwksURI string - jwks []string + jwks *utils.JWKSet domain string baseDomain string } @@ -492,19 +254,9 @@ func (s *Services) buildDynamicRegistrationSoftwareStatementFunc( } } if slices.Contains(opts.verificationMethods, database.SoftwareStatementVerificationMethodManual) { - if len(opts.jwks) > 0 { + if opts.jwks != nil && len(opts.jwks.Keys) > 0 { return func(kid string) (utils.JWK, error) { - jwks := make([]utils.JWK, 0, len(opts.jwks)) - for _, rawJWK := range opts.jwks { - jwk, err := utils.JsonToJWK([]byte(rawJWK)) - if err != nil { - logger.ErrorContext(ctx, "Failed to parse manual JWK", "error", err) - return nil, errors.New("failed to parse manual JWK") - } - jwks = append(jwks, jwk) - } - - jwkIdx := slices.IndexFunc(jwks, func(jwk utils.JWK) bool { + jwkIdx := slices.IndexFunc(opts.jwks.Keys, func(jwk utils.JWK) bool { return jwk.GetKeyID() == kid }) if jwkIdx == -1 { @@ -512,7 +264,7 @@ func (s *Services) buildDynamicRegistrationSoftwareStatementFunc( return nil, errors.New("no matching manual JWK found for KID") } - sliceJWK := jwks[jwkIdx] + sliceJWK := opts.jwks.Keys[jwkIdx] jwkRefEnt, err := s.database.FindDynamicRegistrationSoftwareStatementKeysByCredentialsKeyKIDAndAccountPublicID( ctx, database.FindDynamicRegistrationSoftwareStatementKeysByCredentialsKeyKIDAndAccountPublicIDParams{ diff --git a/idp/internal/utils/encoders.go b/idp/internal/utils/encoders.go index 812f261..55f3765 100644 --- a/idp/internal/utils/encoders.go +++ b/idp/internal/utils/encoders.go @@ -8,8 +8,15 @@ package utils import ( "math/big" + "regexp" ) func Base62Encode(bytes []byte) string { return new(big.Int).SetBytes(bytes).Text(62) } + +var basicBase64URLRegex = regexp.MustCompile(`^[A-Za-z0-9_-]+$`) + +func BasicBase64URLValidator(s string) bool { + return basicBase64URLRegex.MatchString(s) +} diff --git a/idp/internal/utils/jwk.go b/idp/internal/utils/jwk.go index 7fb5700..6300c9c 100644 --- a/idp/internal/utils/jwk.go +++ b/idp/internal/utils/jwk.go @@ -18,6 +18,7 @@ import ( "fmt" "log/slog" "math/big" + "slices" "unsafe" ) @@ -46,19 +47,108 @@ type JWK interface { GetKeyID() string ToUsableKey() (any, error) MarshalJSON() ([]byte, error) + UnmarshalJSON(data []byte) error + Validate() error ToPrivateKey() (any, error) ComparePublicKey(other JWK) bool } +type JWKSet struct { + Keys []JWK `json:"keys"` +} + +func (j *JWKSet) Validate() error { + if j == nil { + return fmt.Errorf("JWK set is nil") + } + + for _, jwk := range j.Keys { + if jwk == nil { + return fmt.Errorf("One jwk is nil") + } + if err := jwk.Validate(); err != nil { + return err + } + } + + return nil +} + +func (j *JWKSet) MarshalJSON() ([]byte, error) { + return json.Marshal(*j) +} + +func (j *JWKSet) UnmarshalJSON(data []byte) error { + type Alias JWKSet + aux := &struct { + Keys []json.RawMessage `json:"keys"` + *Alias + }{ + Alias: (*Alias)(j), + } + + if err := json.Unmarshal(data, &aux); err != nil { + return err + } + + if aux.Keys != nil { + j.Keys = make([]JWK, len(aux.Keys)) + for i, rawKey := range aux.Keys { + key, err := JsonToJWK(rawKey) + if err != nil { + return err + } + j.Keys[i] = key + } + } + + return nil +} + +const ( + okpKty string = "OKP" + ed25519Crv string = "Ed25519" + ed25519CharLen int = 43 + + ecKty string = "EC" + p256Crv string = "P-256" + algES256 string = "ES256" + p256CharLen int = 43 + + useSig string = "sig" + algEdDSA string = "EdDSA" + verify string = "verify" + sign string = "sign" + + rsaKty string = "RSA" + algRS256 string = "RS256" +) + +func validateCommonJWKFields(kid, use string, keyOps []string) error { + if kid == "" { + return fmt.Errorf("kid is required") + } + if use != "" && use != useSig { + return fmt.Errorf("use must be 'sig' or 'enc'") + } + if keyOps != nil && (slices.ContainsFunc(keyOps, func(keyOp string) bool { + return keyOp == sign || keyOp == verify + })) { + return fmt.Errorf("key operation should be sign or verify") + } + + return nil +} + type Ed25519JWK struct { - Kty string `json:"kty"` // Key Type (OKP for Ed25519) - Crv string `json:"crv"` // Curve (Ed25519) - X string `json:"x"` // Public Key - D string `json:"d,omitempty"` // Private Key, omit if public key jwt - Use string `json:"use"` // Usage (e.g., "sig" for signing) - Alg string `json:"alg"` // Algorithm (EdDSA for Ed25519) - Kid string `json:"kid"` // Key ID - KeyOps []string `json:"key_ops"` // Key Operations + Kty string `json:"kty"` // Key Type (OKP for Ed25519) + Crv string `json:"crv"` // Curve (Ed25519) + X string `json:"x"` // Public Key + D string `json:"d,omitempty"` // Private Key, omit if public key jwt + Use string `json:"use,omitempty"` // Usage (e.g., "sig" for signing) + Alg string `json:"alg"` // Algorithm (EdDSA for Ed25519) + Kid string `json:"kid"` // Key ID + KeyOps []string `json:"key_ops,omitempty"` // Key Operations } func (j *Ed25519JWK) GetKeyType() string { @@ -77,6 +167,10 @@ func (j *Ed25519JWK) MarshalJSON() ([]byte, error) { return json.Marshal(*j) } +func (j *Ed25519JWK) UnmarshalJSON(data []byte) error { + return json.Unmarshal(data, j) +} + func (j *Ed25519JWK) ToPrivateKey() (any, error) { return DecodeEd25519JwkPrivate(j) } @@ -90,6 +184,28 @@ func (j *Ed25519JWK) ComparePublicKey(other JWK) bool { return otherEdJwk.X == j.X && otherEdJwk.Kty == j.Kty && otherEdJwk.Crv == j.Crv && otherEdJwk.Alg == j.Alg } +func (j *Ed25519JWK) Validate() error { + if j == nil { + return fmt.Errorf("JWK is nil") + } + + if err := validateCommonJWKFields(j.Kid, j.Use, j.KeyOps); err != nil { + return err + } + + if j.Alg != algEdDSA || j.Kty != okpKty || j.Crv != ed25519Crv { + return fmt.Errorf("invalid algorithm, key type or curve") + } + if len(j.X) != ed25519CharLen || !BasicBase64URLValidator(j.X) { + return fmt.Errorf("invalid x") + } + if j.D != "" && (len(j.D) != ed25519CharLen || !BasicBase64URLValidator(j.D)) { + return fmt.Errorf("invalid d") + } + + return nil +} + type ES256JWK struct { Kty string `json:"kty"` // Key Type (EC for Elliptic Curve) Crv string `json:"crv"` // Curve (P-256) @@ -118,10 +234,37 @@ func (j *ES256JWK) MarshalJSON() ([]byte, error) { return json.Marshal(*j) } +func (j *ES256JWK) UnmarshalJSON(data []byte) error { + return json.Unmarshal(data, j) +} + func (j *ES256JWK) ToPrivateKey() (any, error) { return DecodeP256JwkPrivate(j) } +func (j *ES256JWK) Validate() error { + if j == nil { + return fmt.Errorf("JWK is nil") + } + if err := validateCommonJWKFields(j.Kid, j.Use, j.KeyOps); err != nil { + return err + } + if j.Alg != algES256 || j.Kty != ecKty || j.Crv != p256Crv { + return fmt.Errorf("invalid algorithm, key type or curve") + } + if len(j.X) != p256CharLen || !BasicBase64URLValidator(j.X) { + return fmt.Errorf("invalid x") + } + if len(j.Y) != p256CharLen || !BasicBase64URLValidator(j.Y) { + return fmt.Errorf("invalid y") + } + if j.D != "" && (len(j.D) != p256CharLen || !BasicBase64URLValidator(j.D)) { + return fmt.Errorf("invalid d") + } + + return nil +} + func (j *ES256JWK) ComparePublicKey(other JWK) bool { otherESJwk, ok := other.(*ES256JWK) if !ok { @@ -167,24 +310,33 @@ func (j *RS256JWK) MarshalJSON() ([]byte, error) { return json.Marshal(*j) } +func (j *RS256JWK) UnmarshalJSON(data []byte) error { + return json.Unmarshal(data, j) +} + func (j *RS256JWK) ToPrivateKey() (any, error) { return nil, fmt.Errorf("not implemented") } -const ( - okpKty string = "OKP" - ed25519Crv string = "Ed25519" - - ecKty string = "EC" - p256Crv string = "P-256" - - use string = "sig" - alg string = "EdDSA" - verify string = "verify" - sign string = "sign" +func (j *RS256JWK) Validate() error { + if j == nil { + return fmt.Errorf("JWK is nil") + } + if err := validateCommonJWKFields(j.Kid, j.Use, j.KeyOps); err != nil { + return err + } + if j.Alg != algRS256 || j.Kty != ecKty { + return fmt.Errorf("invalid algorithm or key type") + } + if !BasicBase64URLValidator(j.N) { + return fmt.Errorf("invalid N") + } + if !BasicBase64URLValidator(j.E) { + return fmt.Errorf("invalid E") + } - rsaKty string = "RSA" -) + return nil +} func bigIntToPaddedBytes(n *big.Int, length int) []byte { bytes := n.Bytes() @@ -217,8 +369,8 @@ func EncodeEd25519Jwk(publicKey ed25519.PublicKey, kid string) Ed25519JWK { Kty: okpKty, Crv: ed25519Crv, X: base64.RawURLEncoding.EncodeToString(publicKey), - Use: use, - Alg: alg, + Use: useSig, + Alg: algEdDSA, Kid: kid, KeyOps: []string{verify}, } @@ -233,8 +385,8 @@ func EncodeEd25519JwkPrivate( Kty: okpKty, Crv: ed25519Crv, X: base64.RawURLEncoding.EncodeToString(publicKey), - Use: use, - Alg: alg, + Use: useSig, + Alg: algEdDSA, Kid: kid, D: base64.RawURLEncoding.EncodeToString(privateKey), KeyOps: []string{sign, verify}, @@ -267,33 +419,46 @@ func DecodeEd25519JwkPrivate(jwk *Ed25519JWK) (ed25519.PrivateKey, error) { return privateKey, nil } -func EncodeP256Jwk(publicKey *ecdsa.PublicKey, kid string) ES256JWK { +func EncodeP256Jwk(publicKey *ecdsa.PublicKey, kid string) (ES256JWK, error) { + if publicKey == nil || publicKey.Curve != elliptic.P256() { + return ES256JWK{}, fmt.Errorf("expected a P-256 public key") + } + + raw, err := publicKey.Bytes() + if err != nil { + return ES256JWK{}, fmt.Errorf("encode P-256 public key: %w", err) + } + return ES256JWK{ Kty: ecKty, Crv: p256Crv, - X: base64.RawURLEncoding.EncodeToString(publicKey.X.Bytes()), - Y: base64.RawURLEncoding.EncodeToString(publicKey.Y.Bytes()), - Use: use, - Alg: alg, + X: base64.RawURLEncoding.EncodeToString(raw[1:33]), + Y: base64.RawURLEncoding.EncodeToString(raw[33:65]), + Use: useSig, + Alg: algES256, Kid: kid, KeyOps: []string{verify}, - } + }, nil } -func EncodeP256JwkPrivate(privateKey *ecdsa.PrivateKey, kid string) ES256JWK { - publicKey := privateKey.Public().(*ecdsa.PublicKey) +func EncodeP256JwkPrivate(privateKey *ecdsa.PrivateKey, kid string) (ES256JWK, error) { + if privateKey == nil { + return ES256JWK{}, fmt.Errorf("private key is nil") + } - return ES256JWK{ - Kty: ecKty, - Crv: p256Crv, - D: base64.RawURLEncoding.EncodeToString(privateKey.D.Bytes()), - X: base64.RawURLEncoding.EncodeToString(publicKey.X.Bytes()), - Y: base64.RawURLEncoding.EncodeToString(publicKey.Y.Bytes()), - Use: use, - Alg: alg, - Kid: kid, - KeyOps: []string{sign, verify}, + jwk, err := EncodeP256Jwk(&privateKey.PublicKey, kid) + if err != nil { + return ES256JWK{}, err + } + + d, err := privateKey.Bytes() + if err != nil { + return ES256JWK{}, fmt.Errorf("encode P-256 private key: %w", err) } + + jwk.D = base64.RawURLEncoding.EncodeToString(d) + jwk.KeyOps = []string{sign, verify} + return jwk, nil } func DecodeP256Jwk(jwk *ES256JWK) (*ecdsa.PublicKey, error) { @@ -307,41 +472,50 @@ func DecodeP256Jwk(jwk *ES256JWK) (*ecdsa.PublicKey, error) { return nil, err } - return &ecdsa.PublicKey{ - Curve: elliptic.P256(), - X: new(big.Int).SetBytes(x), - Y: new(big.Int).SetBytes(y), - }, nil + if len(x) != 32 || len(y) != 32 { + return nil, fmt.Errorf("P-256 coordinates must each be 32 bytes") + } + + raw := make([]byte, 65) + raw[0] = 0x04 + copy(raw[1:33], x) + copy(raw[33:65], y) + return ecdsa.ParseUncompressedPublicKey(elliptic.P256(), raw) } func DecodeP256JwkPrivate(jwk *ES256JWK) (*ecdsa.PrivateKey, error) { + if jwk == nil { + return nil, fmt.Errorf("JWK is nil") + } + if jwk.Kty != ecKty || jwk.Crv != p256Crv { + return nil, fmt.Errorf("expected an EC P-256 JWK") + } if jwk.D == "" { return nil, fmt.Errorf("private key not available in JWK") } - dBytes, err := base64.RawURLEncoding.DecodeString(jwk.D) + d, err := base64.RawURLEncoding.DecodeString(jwk.D) if err != nil { return nil, fmt.Errorf("failed to decode private key: %w", err) } + if len(d) != 32 { + return nil, fmt.Errorf("P-256 private key must be 32 bytes") + } - xBytes, err := base64.RawURLEncoding.DecodeString(jwk.X) + privateKey, err := ecdsa.ParseRawPrivateKey(elliptic.P256(), d) if err != nil { - return nil, fmt.Errorf("failed to decode X coordinate: %w", err) + return nil, fmt.Errorf("invalid P-256 private key: %w", err) } - yBytes, err := base64.RawURLEncoding.DecodeString(jwk.Y) + publicKey, err := DecodeP256Jwk(jwk) if err != nil { - return nil, fmt.Errorf("failed to decode Y coordinate: %w", err) + return nil, err + } + if !privateKey.PublicKey.Equal(publicKey) { + return nil, fmt.Errorf("JWK public key does not match private key") } - return &ecdsa.PrivateKey{ - PublicKey: ecdsa.PublicKey{ - Curve: elliptic.P256(), - X: new(big.Int).SetBytes(xBytes), - Y: new(big.Int).SetBytes(yBytes), - }, - D: new(big.Int).SetBytes(dBytes), - }, nil + return privateKey, nil } func DecodeRS256Jwk(jwk *RS256JWK) (*rsa.PublicKey, error) { @@ -405,6 +579,8 @@ func JsonToJWK(jsonBytes []byte) (JWK, error) { } } +// TODO: fix me + //go:noinline func WipeBytes(ctx context.Context, logger *slog.Logger, data []byte) { if len(data) == 0 {