From b34a6a9af3e7d901d38f6a2daf83d796bdd72e2d Mon Sep 17 00:00:00 2001 From: Afonso Barracha Date: Mon, 7 Sep 2026 23:36:19 +1200 Subject: [PATCH 1/4] fix(dynamic-registration): fix software statement validation and JWK set format --- .../controllers/account_credentials.go | 8 +- idp/internal/controllers/apps.go | 8 +- .../bodies/oauth_dynamic_registration.go | 72 +++-- .../dynamic_registration_domains.go | 4 +- .../controllers/oauth_dynamic_registration.go | 3 + idp/internal/controllers/users.go | 4 +- idp/internal/providers/crypto/jwk.go | 7 +- ...ynamic_registration_software_statements.go | 70 ++-- .../account_credentials_registration.go | 30 +- .../services/app_dynamic_registration.go | 3 +- idp/internal/services/client_credentials.go | 13 +- .../services/dtos/account_credentials.go | 7 +- idp/internal/services/helpers.go | 31 -- idp/internal/services/software_statement.go | 274 +--------------- idp/internal/utils/encoders.go | 7 + idp/internal/utils/jwk.go | 298 ++++++++++++++---- 16 files changed, 381 insertions(+), 458 deletions(-) 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 { From b6c5c9933a6331d0d4ce24c082d98521a7d9cb55 Mon Sep 17 00:00:00 2001 From: Afonso Barracha Date: Thu, 10 Sep 2026 17:51:14 +1200 Subject: [PATCH 2/4] feat: add sign for apps iat --- idp/initial_schema.dbdiagram | 15 ++ idp/initial_schema.dbml | 3 - .../account_dynamic_registration_configs.go | 14 +- .../app_dynamic_registration_iat.go | 55 +++++++ .../account_dynamics_registration_configs.go | 10 +- idp/internal/controllers/middleware.go | 57 ++++--- .../controllers/oauth_dynamic_registration.go | 40 ++--- .../controllers/paths/dynamic_registration.go | 1 + .../database/account_2fa_configs.sql.go | 2 +- .../database/account_auth_providers.sql.go | 2 +- .../account_credential_secrets.sql.go | 2 +- .../database/account_credentials.sql.go | 2 +- .../database/account_credentials_keys.sql.go | 2 +- .../account_data_encryption_keys.sql.go | 2 +- ...ccount_dynamic_registration_configs.sql.go | 56 ++----- .../database/account_hmac_secrets.sql.go | 2 +- .../account_key_encryption_keys.sql.go | 2 +- .../account_token_signing_keys.sql.go | 2 +- .../providers/database/account_totps.sql.go | 2 +- .../providers/database/accounts.sql.go | 2 +- .../providers/database/app_designs.sql.go | 2 +- .../app_dynamic_registration_configs.sql.go | 2 +- .../providers/database/app_keys.sql.go | 2 +- .../providers/database/app_profiles.sql.go | 2 +- .../database/app_related_apps.sql.go | 2 +- .../providers/database/app_secrets.sql.go | 2 +- .../database/app_service_configs.sql.go | 2 +- idp/internal/providers/database/apps.sql.go | 2 +- .../database/credentials_keys.sql.go | 2 +- .../database/credentials_secrets.sql.go | 2 +- .../database/data_encryption_keys.sql.go | 2 +- idp/internal/providers/database/db.go | 2 +- .../dynamic_registration_domain_codes.sql.go | 2 +- .../dynamic_registration_domains.sql.go | 2 +- ...egistration_software_statement_keys.sql.go | 2 +- .../database/key_encryption_keys.sql.go | 2 +- ...0241213231542_create_initial_schema.up.sql | 152 +++++++++--------- idp/internal/providers/database/models.go | 22 ++- .../providers/database/oidc_configs.sql.go | 2 +- .../account_dynamic_registration_configs.sql | 12 +- .../providers/database/revoked_tokens.sql.go | 2 +- .../database/token_signing_keys.sql.go | 2 +- idp/internal/providers/database/totps.sql.go | 2 +- .../database/user_auth_providers.sql.go | 2 +- .../providers/database/user_totps.sql.go | 2 +- idp/internal/providers/database/users.sql.go | 2 +- .../tokens/dynamic_registration_iat.go | 8 +- idp/internal/server/routes.go | 1 + idp/internal/server/routes/common.go | 6 - idp/internal/server/routes/oauth.go | 16 +- .../account_credentials_registration.go | 7 - .../account_dynamic_registration_configs.go | 48 ++---- .../account_dynamic_registration_tokens.go | 7 - .../services/app_dynamic_registration_iat.go | 58 ++++++- idp/internal/services/apps_auth.go | 8 +- .../account_dynamic_registration_config.go | 22 ++- idp/internal/services/jwks.go | 29 ++-- idp/internal/services/users_auth.go | 48 +++--- 58 files changed, 404 insertions(+), 359 deletions(-) create mode 100644 idp/initial_schema.dbdiagram create mode 100644 idp/internal/controllers/app_dynamic_registration_iat.go delete mode 100644 idp/internal/services/account_dynamic_registration_tokens.go diff --git a/idp/initial_schema.dbdiagram b/idp/initial_schema.dbdiagram new file mode 100644 index 0000000..283e9c6 --- /dev/null +++ b/idp/initial_schema.dbdiagram @@ -0,0 +1,15 @@ +{ + "version": "3.0.0", + "darkMode": false, + "gridEnabling": false, + "currentViewName": null, + "defaultView": { + "detailLevel": "All", + "relationshipMode": "All", + "tablePositions": [], + "tableGroupCollapseStates": [], + "stickyNoteLayouts": [], + "referencePaths": [] + }, + "views": {} +} \ No newline at end of file diff --git a/idp/initial_schema.dbml b/idp/initial_schema.dbml index 5141891..ef9df09 100644 --- a/idp/initial_schema.dbml +++ b/idp/initial_schema.dbml @@ -1021,9 +1021,6 @@ Table account_dynamic_registration_configs as ADRC { software_statement_verification_methods "software_statement_verification_method[]" [not null] require_verified_domains_credentials_type "account_credentials_type[]" [not null] - require_initial_access_token_credential_types "account_credentials_type[]" [not null] - initial_access_token_generation_methods "initial_access_token_generation_method[]" [not null] - created_at timestamptz [not null, default: `now()`] updated_at timestamptz [not null, default: `now()`] diff --git a/idp/internal/controllers/account_dynamic_registration_configs.go b/idp/internal/controllers/account_dynamic_registration_configs.go index 80aabe9..c19929d 100644 --- a/idp/internal/controllers/account_dynamic_registration_configs.go +++ b/idp/internal/controllers/account_dynamic_registration_configs.go @@ -42,14 +42,12 @@ func (c *Controllers) UpsertAccountDynamicRegistrationConfig(ctx fiber.Ctx) erro dto, created, serviceErr := c.services.SaveAccountDynamicRegistrationConfig( ctx.Context(), services.SaveAccountDynamicRegistrationConfigOptions{ - RequestID: requestID, - AccountPublicID: accountClaims.AccountID, - AccountVersion: accountClaims.AccountVersion, - AccountCredentialsTypes: body.AccountCredentialsTypes, - RequireSoftwareStatementCredentialTypes: body.RequireSoftwareStatementCredentialTypes, - SoftwareStatementVerificationMethods: body.SoftwareStatementVerificationMethods, - RequireInitialAccessTokenCredentialTypes: body.RequireInitialAccessTokenCredentialTypes, - InitialAccessTokenGenerationMethods: body.InitialAccessTokenGenerationMethods, + RequestID: requestID, + AccountPublicID: accountClaims.AccountID, + AccountVersion: accountClaims.AccountVersion, + AccountCredentialsTypes: body.AccountCredentialsTypes, + RequireSoftwareStatementCredentialTypes: body.RequireSoftwareStatementCredentialTypes, + SoftwareStatementVerificationMethods: body.SoftwareStatementVerificationMethods, }, ) if serviceErr != nil { diff --git a/idp/internal/controllers/app_dynamic_registration_iat.go b/idp/internal/controllers/app_dynamic_registration_iat.go new file mode 100644 index 0000000..1bdd3b8 --- /dev/null +++ b/idp/internal/controllers/app_dynamic_registration_iat.go @@ -0,0 +1,55 @@ +// Copyright (c) 2026 Afonso Barracha +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +package controllers + +import ( + "github.com/gofiber/fiber/v3" + + "github.com/tugascript/devlogs/idp/internal/controllers/bodies" + "github.com/tugascript/devlogs/idp/internal/services" +) + +const ( + appDynamicRegistrationIATLocation string = "app_dynamic_registration_iat" +) + +func (c *Controllers) AppDynamicRegistrationIATSign(ctx fiber.Ctx) error { + requestID := getRequestID(ctx) + logger := c.buildLogger(requestID, appDynamicRegistrationIATLocation, "AppDynamicRegistrationIATSign") + logRequest(logger, ctx) + + accountClaims, serviceErr := getAccountClaims(ctx) + if serviceErr != nil { + return serviceErrorResponse(logger, ctx, serviceErr) + } + + body := new(bodies.CreateDynamicRegistrationDomainBody) + if err := ctx.Bind().Body(body); err != nil { + return parseRequestErrorResponse(logger, ctx, err) + } + if err := c.validate.StructCtx(ctx.Context(), body); err != nil { + return validateBodyErrorResponse(logger, ctx, err) + } + + authDTO, serviceErr := c.services.CreateAppCredentialsRegistrationIAT( + ctx.Context(), + services.CreateAppCredentialsRegistrationIATOptions{ + RequestID: requestID, + AccountPublicID: accountClaims.AccountID, + AccountVersion: accountClaims.AccountVersion, + Domain: body.Domain, + BackendDomain: c.backendDomain, + }, + ) + if serviceErr != nil { + return serviceErrorResponse(logger, ctx, serviceErr) + } + + ctx.Set(fiber.HeaderCacheControl, cacheControlNoStore) + logResponse(logger, ctx, fiber.StatusCreated) + return ctx.Status(fiber.StatusCreated).JSON(&authDTO) +} diff --git a/idp/internal/controllers/bodies/account_dynamics_registration_configs.go b/idp/internal/controllers/bodies/account_dynamics_registration_configs.go index 409f3a2..5cd3051 100644 --- a/idp/internal/controllers/bodies/account_dynamics_registration_configs.go +++ b/idp/internal/controllers/bodies/account_dynamics_registration_configs.go @@ -7,10 +7,8 @@ package bodies type AccountDynamicRegistrationConfigBody struct { - AccountCredentialsTypes []string `json:"account_credentials_types" validate:"required,unique,min=1,max=3,oneof=native service mcp"` - WhitelistedDomains []string `json:"whitelisted_domains" validate:"omitempty,unique,min=1,max=250,dive,fqdn"` - RequireSoftwareStatementCredentialTypes []string `json:"require_software_statement_credential_types" validate:"omitempty,unique,min=1,max=3,oneof=native service mcp"` - SoftwareStatementVerificationMethods []string `json:"software_statement_verification_methods" validate:"omitempty,unique,min=1,max=2,oneof=manual jwks_uri"` - RequireInitialAccessTokenCredentialTypes []string `json:"require_initial_access_token_credential_types" validate:"omitempty,unique,min=1,max=3,oneof=native service mcp"` - InitialAccessTokenGenerationMethods []string `json:"initial_access_token_generation_methods" validate:"omitempty,unique,min=1,max=2,oneof=manual authorization_code"` + AccountCredentialsTypes []string `json:"account_credentials_types" validate:"required,unique,min=1,max=3,oneof=native service mcp"` + WhitelistedDomains []string `json:"whitelisted_domains" validate:"omitempty,unique,min=1,max=250,dive,fqdn"` + RequireSoftwareStatementCredentialTypes []string `json:"require_software_statement_credential_types" validate:"omitempty,unique,min=1,max=3,oneof=native service mcp"` + SoftwareStatementVerificationMethods []string `json:"software_statement_verification_methods" validate:"omitempty,unique,min=1,max=2,oneof=manual jwks_uri"` } diff --git a/idp/internal/controllers/middleware.go b/idp/internal/controllers/middleware.go index 6fb0375..d12604a 100644 --- a/idp/internal/controllers/middleware.go +++ b/idp/internal/controllers/middleware.go @@ -173,18 +173,6 @@ func (c *Controllers) AppAccessClaimsMiddleware(ctx fiber.Ctx) error { return continueMiddleware(ctx) } -func processIATIssuerDomain(ctx fiber.Ctx, backendDomain string) (string, *exceptions.ServiceError) { - hasAccountHost, ok := ctx.Locals("hasAccountHost").(bool) - if ok && hasAccountHost { - username, _, serviceErr := getHostAccount(ctx) - if serviceErr != nil { - return "", serviceErr - } - return fmt.Sprintf("%s.%s", username, backendDomain), nil - } - return backendDomain, nil -} - func (c *Controllers) DynamicRegistrationIATMiddleware(ctx fiber.Ctx) error { requestID := getRequestID(ctx) logger := c.buildLogger(requestID, middlewareLocation, "DynamicRegistrationIATMiddleware") @@ -192,27 +180,58 @@ func (c *Controllers) DynamicRegistrationIATMiddleware(ctx fiber.Ctx) error { if authHeader == "" { logger.InfoContext(ctx.Context(), "No Authorization header found") - ctx.Locals("isAuthenticated", false) - return ctx.Next() + return oauthErrorResponse(logger, ctx, exceptions.OAuthErrorAccessDenied) } - issDomain, serviceErr := processIATIssuerDomain(ctx, c.backendDomain) + domain, accountClaims, serviceErr := c.services.ProcessAccountCredentialsRegistrationIATAuth( + ctx.Context(), + services.ProcessAccountCredentialsRegistrationIATAuthOptions{ + RequestID: requestID, + AuthHeader: authHeader, + IssuerDomain: c.backendDomain, + }, + ) + if serviceErr != nil { - return serviceErrorResponse(logger, ctx, serviceErr) + logger.InfoContext(ctx.Context(), "Failed to process account credentials registration IAT auth", "serviceError", serviceErr) + return oauthErrorResponse(logger, ctx, exceptions.OAuthErrorAccessDenied) } - domain, accountClaims, serviceErr := c.services.ProcessAccountCredentialsRegistrationIATAuth( + ctx.Locals("account", accountClaims) + ctx.Locals("domain", domain) + return ctx.Next() +} + +func (c *Controllers) AppDynamicRegistrationIATMiddleware(ctx fiber.Ctx) error { + requestID := getRequestID(ctx) + logger := c.buildLogger(requestID, middlewareLocation, "AppDynamicRegistrationIATMiddleware") + username, accountID, serviceErr := getHostAccount(ctx) + if serviceErr != nil { + logger.InfoContext(ctx.Context(), "Failed to get host account", "serviceError", serviceErr) + return oauthErrorResponse(logger, ctx, exceptions.OAuthErrorServerError) + } + + authHeader := ctx.Get("Authorization") + if authHeader == "" { + logger.InfoContext(ctx.Context(), "No Authorization header found, skipping app dynamic registration IAT middleware") + return ctx.Next() + } + + domain, accountClaims, serviceErr := c.services.ProcessAppDynamicRegistrationIATAuth( ctx.Context(), - services.ProcessAccountCredentialsRegistrationIATAuthOptions{ + services.ProcessAppDynamicRegistrationIATAuthOptions{ RequestID: requestID, AuthHeader: authHeader, - IssuerDomain: issDomain, + AccountID: accountID, + IssuerDomain: fmt.Sprintf("%s.%s", username, c.backendDomain), }, ) if serviceErr != nil { + logger.InfoContext(ctx.Context(), "Failed to process app dynamic registration IAT auth", "serviceError", serviceErr) return oauthErrorResponse(logger, ctx, exceptions.OAuthErrorAccessDenied) } + logger.DebugContext(ctx.Context(), "Processed app dynamic registration IAT auth successfully", "domain", domain, "account", accountClaims) ctx.Locals("account", accountClaims) ctx.Locals("domain", domain) ctx.Locals("isAuthenticated", true) diff --git a/idp/internal/controllers/oauth_dynamic_registration.go b/idp/internal/controllers/oauth_dynamic_registration.go index 6a08fcd..1f4909f 100644 --- a/idp/internal/controllers/oauth_dynamic_registration.go +++ b/idp/internal/controllers/oauth_dynamic_registration.go @@ -8,10 +8,8 @@ package controllers import ( "github.com/gofiber/fiber/v3" - "github.com/google/uuid" "github.com/tugascript/devlogs/idp/internal/controllers/bodies" - "github.com/tugascript/devlogs/idp/internal/controllers/params" "github.com/tugascript/devlogs/idp/internal/exceptions" "github.com/tugascript/devlogs/idp/internal/providers/tokens" "github.com/tugascript/devlogs/idp/internal/services" @@ -24,14 +22,15 @@ func (c *Controllers) OAuthDynamicRegistration(ctx fiber.Ctx) error { logger := c.buildLogger(requestID, oauthDynamicRegistration, "OAuthDynamicRegistration") logRequest(logger, ctx) - urlParams := params.AccountURLParams{AccountPublicID: ctx.Params("accountPublicID")} - if err := c.validate.StructCtx(ctx.Context(), &urlParams); err != nil { - return oauthErrorResponse(logger, ctx, exceptions.OAuthErrorInvalidRequest) + accountClaims, ok := ctx.Locals("account").(tokens.AccountClaims) + if !ok { + logger.ErrorContext(ctx.Context(), "account should be set in context by middleware") + return oauthErrorResponse(logger, ctx, exceptions.OAuthErrorServerError) } - - accountPublicID, err := uuid.Parse(urlParams.AccountPublicID) - if err != nil { - return oauthErrorResponse(logger, ctx, exceptions.OAuthErrorInvalidRequest) + domain, ok := ctx.Locals("domain").(string) + if !ok { + logger.ErrorContext(ctx.Context(), "domain should be set in context by middleware") + return oauthErrorResponse(logger, ctx, exceptions.OAuthErrorServerError) } body := new(bodies.OAuthDynamicClientRegistrationBody) @@ -45,32 +44,13 @@ func (c *Controllers) OAuthDynamicRegistration(ctx fiber.Ctx) error { return oauthErrorResponse(logger, ctx, exceptions.OAuthErrorInvalidClientMetadata) } - isAuthenticated, ok := ctx.Locals("isAuthenticated").(bool) - if !ok { - logger.ErrorContext(ctx.Context(), "isAuthenticated should be set in context by middleware") - return oauthErrorResponse(logger, ctx, exceptions.OAuthErrorServerError) - } - - domain, ok := ctx.Locals("domain").(string) - if isAuthenticated && !ok { - logger.ErrorContext(ctx.Context(), "domain should be set in context by middleware") - return oauthErrorResponse(logger, ctx, exceptions.OAuthErrorServerError) - } - - account, ok := ctx.Locals("account").(tokens.AccountClaims) - if isAuthenticated && !ok { - logger.ErrorContext(ctx.Context(), "account should be set in context by middleware") - return oauthErrorResponse(logger, ctx, exceptions.OAuthErrorServerError) - } - accountCredentialsDTO, serviceErr := c.services.CreateAccountCredentialsRegistration( ctx.Context(), services.CreateAccountCredentialsRegistrationOptions{ RequestID: requestID, - AccountPublicID: accountPublicID, - IsAuthenticated: isAuthenticated, + AccountPublicID: accountClaims.AccountID, IATDomain: domain, - AccountVersion: account.AccountVersion, + AccountVersion: accountClaims.AccountVersion, ApplicationType: body.ApplicationType, RedirectURIs: body.RedirectURIs, TokenEndpointAuthMethod: body.TokenEndpointAuthMethod, diff --git a/idp/internal/controllers/paths/dynamic_registration.go b/idp/internal/controllers/paths/dynamic_registration.go index 82ddc25..162d2af 100644 --- a/idp/internal/controllers/paths/dynamic_registration.go +++ b/idp/internal/controllers/paths/dynamic_registration.go @@ -9,6 +9,7 @@ package paths const ( DynamicRegistrationBase string = "/dynamic-registration" InitialAccessToken string = "/initial-access-token" + InitialAccessTokenSign string = "/sign" InitialAccessTokenAuthEXT string = "/ext" InitialAccessTokenCallback string = "/callback" InitialAccessTokenProvider string = "/:provider" diff --git a/idp/internal/providers/database/account_2fa_configs.sql.go b/idp/internal/providers/database/account_2fa_configs.sql.go index 9d84eae..fdebe54 100644 --- a/idp/internal/providers/database/account_2fa_configs.sql.go +++ b/idp/internal/providers/database/account_2fa_configs.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.31.1 // source: account_2fa_configs.sql package database diff --git a/idp/internal/providers/database/account_auth_providers.sql.go b/idp/internal/providers/database/account_auth_providers.sql.go index 1949655..d58956e 100644 --- a/idp/internal/providers/database/account_auth_providers.sql.go +++ b/idp/internal/providers/database/account_auth_providers.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.31.1 // source: account_auth_providers.sql package database diff --git a/idp/internal/providers/database/account_credential_secrets.sql.go b/idp/internal/providers/database/account_credential_secrets.sql.go index 2ea3eca..aad0c68 100644 --- a/idp/internal/providers/database/account_credential_secrets.sql.go +++ b/idp/internal/providers/database/account_credential_secrets.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.31.1 // source: account_credential_secrets.sql package database diff --git a/idp/internal/providers/database/account_credentials.sql.go b/idp/internal/providers/database/account_credentials.sql.go index d9915cd..4305423 100644 --- a/idp/internal/providers/database/account_credentials.sql.go +++ b/idp/internal/providers/database/account_credentials.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.31.1 // source: account_credentials.sql package database diff --git a/idp/internal/providers/database/account_credentials_keys.sql.go b/idp/internal/providers/database/account_credentials_keys.sql.go index 706474a..8896737 100644 --- a/idp/internal/providers/database/account_credentials_keys.sql.go +++ b/idp/internal/providers/database/account_credentials_keys.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.31.1 // source: account_credentials_keys.sql package database diff --git a/idp/internal/providers/database/account_data_encryption_keys.sql.go b/idp/internal/providers/database/account_data_encryption_keys.sql.go index 4c46c57..4a72de4 100644 --- a/idp/internal/providers/database/account_data_encryption_keys.sql.go +++ b/idp/internal/providers/database/account_data_encryption_keys.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.31.1 // source: account_data_encryption_keys.sql package database diff --git a/idp/internal/providers/database/account_dynamic_registration_configs.sql.go b/idp/internal/providers/database/account_dynamic_registration_configs.sql.go index a850e8d..23fc87a 100644 --- a/idp/internal/providers/database/account_dynamic_registration_configs.sql.go +++ b/idp/internal/providers/database/account_dynamic_registration_configs.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.31.1 // source: account_dynamic_registration_configs.sql package database @@ -18,28 +18,22 @@ INSERT INTO "account_dynamic_registration_configs" ( "account_public_id", "account_credentials_types", "require_software_statement_credential_types", - "software_statement_verification_methods", - "require_initial_access_token_credential_types", - "initial_access_token_generation_methods" + "software_statement_verification_methods" ) VALUES ( $1, $2, $3, $4, - $5, - $6, - $7 -) RETURNING id, account_id, account_public_id, account_credentials_types, require_software_statement_credential_types, software_statement_verification_methods, require_verified_domains_credentials_type, require_initial_access_token_credential_types, initial_access_token_generation_methods, created_at, updated_at + $5 +) RETURNING id, account_id, account_public_id, account_credentials_types, require_software_statement_credential_types, software_statement_verification_methods, require_verified_domains_credentials_type, created_at, updated_at ` type CreateAccountDynamicRegistrationConfigParams struct { - AccountID int32 - AccountPublicID uuid.UUID - AccountCredentialsTypes []AccountCredentialsType - RequireSoftwareStatementCredentialTypes []AccountCredentialsType - SoftwareStatementVerificationMethods []SoftwareStatementVerificationMethod - RequireInitialAccessTokenCredentialTypes []AccountCredentialsType - InitialAccessTokenGenerationMethods []InitialAccessTokenGenerationMethod + AccountID int32 + AccountPublicID uuid.UUID + AccountCredentialsTypes []AccountCredentialsType + RequireSoftwareStatementCredentialTypes []AccountCredentialsType + SoftwareStatementVerificationMethods []SoftwareStatementVerificationMethod } // Copyright (c) 2025 Afonso Barracha @@ -54,8 +48,6 @@ func (q *Queries) CreateAccountDynamicRegistrationConfig(ctx context.Context, ar arg.AccountCredentialsTypes, arg.RequireSoftwareStatementCredentialTypes, arg.SoftwareStatementVerificationMethods, - arg.RequireInitialAccessTokenCredentialTypes, - arg.InitialAccessTokenGenerationMethods, ) var i AccountDynamicRegistrationConfig err := row.Scan( @@ -66,8 +58,6 @@ func (q *Queries) CreateAccountDynamicRegistrationConfig(ctx context.Context, ar &i.RequireSoftwareStatementCredentialTypes, &i.SoftwareStatementVerificationMethods, &i.RequireVerifiedDomainsCredentialsType, - &i.RequireInitialAccessTokenCredentialTypes, - &i.InitialAccessTokenGenerationMethods, &i.CreatedAt, &i.UpdatedAt, ) @@ -84,7 +74,7 @@ func (q *Queries) DeleteAccountDynamicRegistrationConfig(ctx context.Context, id } const findAccountDynamicRegistrationConfigByAccountID = `-- name: FindAccountDynamicRegistrationConfigByAccountID :one -SELECT id, account_id, account_public_id, account_credentials_types, require_software_statement_credential_types, software_statement_verification_methods, require_verified_domains_credentials_type, require_initial_access_token_credential_types, initial_access_token_generation_methods, created_at, updated_at FROM "account_dynamic_registration_configs" +SELECT id, account_id, account_public_id, account_credentials_types, require_software_statement_credential_types, software_statement_verification_methods, require_verified_domains_credentials_type, created_at, updated_at FROM "account_dynamic_registration_configs" WHERE "account_id" = $1 LIMIT 1 ` @@ -99,8 +89,6 @@ func (q *Queries) FindAccountDynamicRegistrationConfigByAccountID(ctx context.Co &i.RequireSoftwareStatementCredentialTypes, &i.SoftwareStatementVerificationMethods, &i.RequireVerifiedDomainsCredentialsType, - &i.RequireInitialAccessTokenCredentialTypes, - &i.InitialAccessTokenGenerationMethods, &i.CreatedAt, &i.UpdatedAt, ) @@ -108,7 +96,7 @@ func (q *Queries) FindAccountDynamicRegistrationConfigByAccountID(ctx context.Co } const findAccountDynamicRegistrationConfigByAccountPublicID = `-- name: FindAccountDynamicRegistrationConfigByAccountPublicID :one -SELECT id, account_id, account_public_id, account_credentials_types, require_software_statement_credential_types, software_statement_verification_methods, require_verified_domains_credentials_type, require_initial_access_token_credential_types, initial_access_token_generation_methods, created_at, updated_at FROM "account_dynamic_registration_configs" +SELECT id, account_id, account_public_id, account_credentials_types, require_software_statement_credential_types, software_statement_verification_methods, require_verified_domains_credentials_type, created_at, updated_at FROM "account_dynamic_registration_configs" WHERE "account_public_id" = $1 LIMIT 1 ` @@ -123,8 +111,6 @@ func (q *Queries) FindAccountDynamicRegistrationConfigByAccountPublicID(ctx cont &i.RequireSoftwareStatementCredentialTypes, &i.SoftwareStatementVerificationMethods, &i.RequireVerifiedDomainsCredentialsType, - &i.RequireInitialAccessTokenCredentialTypes, - &i.InitialAccessTokenGenerationMethods, &i.CreatedAt, &i.UpdatedAt, ) @@ -135,20 +121,16 @@ const updateAccountDynamicRegistrationConfig = `-- name: UpdateAccountDynamicReg UPDATE "account_dynamic_registration_configs" SET "account_credentials_types" = $2, "require_software_statement_credential_types" = $3, - "software_statement_verification_methods" = $4, - "require_initial_access_token_credential_types" = $5, - "initial_access_token_generation_methods" = $6 + "software_statement_verification_methods" = $4 WHERE "id" = $1 -RETURNING id, account_id, account_public_id, account_credentials_types, require_software_statement_credential_types, software_statement_verification_methods, require_verified_domains_credentials_type, require_initial_access_token_credential_types, initial_access_token_generation_methods, created_at, updated_at +RETURNING id, account_id, account_public_id, account_credentials_types, require_software_statement_credential_types, software_statement_verification_methods, require_verified_domains_credentials_type, created_at, updated_at ` type UpdateAccountDynamicRegistrationConfigParams struct { - ID int32 - AccountCredentialsTypes []AccountCredentialsType - RequireSoftwareStatementCredentialTypes []AccountCredentialsType - SoftwareStatementVerificationMethods []SoftwareStatementVerificationMethod - RequireInitialAccessTokenCredentialTypes []AccountCredentialsType - InitialAccessTokenGenerationMethods []InitialAccessTokenGenerationMethod + ID int32 + AccountCredentialsTypes []AccountCredentialsType + RequireSoftwareStatementCredentialTypes []AccountCredentialsType + SoftwareStatementVerificationMethods []SoftwareStatementVerificationMethod } func (q *Queries) UpdateAccountDynamicRegistrationConfig(ctx context.Context, arg UpdateAccountDynamicRegistrationConfigParams) (AccountDynamicRegistrationConfig, error) { @@ -157,8 +139,6 @@ func (q *Queries) UpdateAccountDynamicRegistrationConfig(ctx context.Context, ar arg.AccountCredentialsTypes, arg.RequireSoftwareStatementCredentialTypes, arg.SoftwareStatementVerificationMethods, - arg.RequireInitialAccessTokenCredentialTypes, - arg.InitialAccessTokenGenerationMethods, ) var i AccountDynamicRegistrationConfig err := row.Scan( @@ -169,8 +149,6 @@ func (q *Queries) UpdateAccountDynamicRegistrationConfig(ctx context.Context, ar &i.RequireSoftwareStatementCredentialTypes, &i.SoftwareStatementVerificationMethods, &i.RequireVerifiedDomainsCredentialsType, - &i.RequireInitialAccessTokenCredentialTypes, - &i.InitialAccessTokenGenerationMethods, &i.CreatedAt, &i.UpdatedAt, ) diff --git a/idp/internal/providers/database/account_hmac_secrets.sql.go b/idp/internal/providers/database/account_hmac_secrets.sql.go index 6a63a35..5a4479a 100644 --- a/idp/internal/providers/database/account_hmac_secrets.sql.go +++ b/idp/internal/providers/database/account_hmac_secrets.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.31.1 // source: account_hmac_secrets.sql package database diff --git a/idp/internal/providers/database/account_key_encryption_keys.sql.go b/idp/internal/providers/database/account_key_encryption_keys.sql.go index c633bcb..2904756 100644 --- a/idp/internal/providers/database/account_key_encryption_keys.sql.go +++ b/idp/internal/providers/database/account_key_encryption_keys.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.31.1 // source: account_key_encryption_keys.sql package database diff --git a/idp/internal/providers/database/account_token_signing_keys.sql.go b/idp/internal/providers/database/account_token_signing_keys.sql.go index 09869f8..df0c23c 100644 --- a/idp/internal/providers/database/account_token_signing_keys.sql.go +++ b/idp/internal/providers/database/account_token_signing_keys.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.31.1 // source: account_token_signing_keys.sql package database diff --git a/idp/internal/providers/database/account_totps.sql.go b/idp/internal/providers/database/account_totps.sql.go index 4465f92..4596cf6 100644 --- a/idp/internal/providers/database/account_totps.sql.go +++ b/idp/internal/providers/database/account_totps.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.31.1 // source: account_totps.sql package database diff --git a/idp/internal/providers/database/accounts.sql.go b/idp/internal/providers/database/accounts.sql.go index 8bef59b..25aa2e3 100644 --- a/idp/internal/providers/database/accounts.sql.go +++ b/idp/internal/providers/database/accounts.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.31.1 // source: accounts.sql package database diff --git a/idp/internal/providers/database/app_designs.sql.go b/idp/internal/providers/database/app_designs.sql.go index b3e6f5f..748ac38 100644 --- a/idp/internal/providers/database/app_designs.sql.go +++ b/idp/internal/providers/database/app_designs.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.31.1 // source: app_designs.sql package database diff --git a/idp/internal/providers/database/app_dynamic_registration_configs.sql.go b/idp/internal/providers/database/app_dynamic_registration_configs.sql.go index 518225b..4d0e9d7 100644 --- a/idp/internal/providers/database/app_dynamic_registration_configs.sql.go +++ b/idp/internal/providers/database/app_dynamic_registration_configs.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.31.1 // source: app_dynamic_registration_configs.sql package database diff --git a/idp/internal/providers/database/app_keys.sql.go b/idp/internal/providers/database/app_keys.sql.go index 54d8fa8..e9d37ef 100644 --- a/idp/internal/providers/database/app_keys.sql.go +++ b/idp/internal/providers/database/app_keys.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.31.1 // source: app_keys.sql package database diff --git a/idp/internal/providers/database/app_profiles.sql.go b/idp/internal/providers/database/app_profiles.sql.go index 589260d..703b435 100644 --- a/idp/internal/providers/database/app_profiles.sql.go +++ b/idp/internal/providers/database/app_profiles.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.31.1 // source: app_profiles.sql package database diff --git a/idp/internal/providers/database/app_related_apps.sql.go b/idp/internal/providers/database/app_related_apps.sql.go index 29d7c22..d449d0f 100644 --- a/idp/internal/providers/database/app_related_apps.sql.go +++ b/idp/internal/providers/database/app_related_apps.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.31.1 // source: app_related_apps.sql package database diff --git a/idp/internal/providers/database/app_secrets.sql.go b/idp/internal/providers/database/app_secrets.sql.go index 6394d2c..f83e7e1 100644 --- a/idp/internal/providers/database/app_secrets.sql.go +++ b/idp/internal/providers/database/app_secrets.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.31.1 // source: app_secrets.sql package database diff --git a/idp/internal/providers/database/app_service_configs.sql.go b/idp/internal/providers/database/app_service_configs.sql.go index b8f6c62..fc2eac2 100644 --- a/idp/internal/providers/database/app_service_configs.sql.go +++ b/idp/internal/providers/database/app_service_configs.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.31.1 // source: app_service_configs.sql package database diff --git a/idp/internal/providers/database/apps.sql.go b/idp/internal/providers/database/apps.sql.go index 137f20b..c6c94d2 100644 --- a/idp/internal/providers/database/apps.sql.go +++ b/idp/internal/providers/database/apps.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.31.1 // source: apps.sql package database diff --git a/idp/internal/providers/database/credentials_keys.sql.go b/idp/internal/providers/database/credentials_keys.sql.go index 3dadbcc..0cf0540 100644 --- a/idp/internal/providers/database/credentials_keys.sql.go +++ b/idp/internal/providers/database/credentials_keys.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.31.1 // source: credentials_keys.sql package database diff --git a/idp/internal/providers/database/credentials_secrets.sql.go b/idp/internal/providers/database/credentials_secrets.sql.go index a60803e..7b52fe8 100644 --- a/idp/internal/providers/database/credentials_secrets.sql.go +++ b/idp/internal/providers/database/credentials_secrets.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.31.1 // source: credentials_secrets.sql package database diff --git a/idp/internal/providers/database/data_encryption_keys.sql.go b/idp/internal/providers/database/data_encryption_keys.sql.go index 3d90dcb..6ace41b 100644 --- a/idp/internal/providers/database/data_encryption_keys.sql.go +++ b/idp/internal/providers/database/data_encryption_keys.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.31.1 // source: data_encryption_keys.sql package database diff --git a/idp/internal/providers/database/db.go b/idp/internal/providers/database/db.go index ab5d1b5..486aa36 100644 --- a/idp/internal/providers/database/db.go +++ b/idp/internal/providers/database/db.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.31.1 package database diff --git a/idp/internal/providers/database/dynamic_registration_domain_codes.sql.go b/idp/internal/providers/database/dynamic_registration_domain_codes.sql.go index 5bad396..468714e 100644 --- a/idp/internal/providers/database/dynamic_registration_domain_codes.sql.go +++ b/idp/internal/providers/database/dynamic_registration_domain_codes.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.31.1 // source: dynamic_registration_domain_codes.sql package database diff --git a/idp/internal/providers/database/dynamic_registration_domains.sql.go b/idp/internal/providers/database/dynamic_registration_domains.sql.go index fb0ae56..8b7d21a 100644 --- a/idp/internal/providers/database/dynamic_registration_domains.sql.go +++ b/idp/internal/providers/database/dynamic_registration_domains.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.31.1 // source: dynamic_registration_domains.sql package database diff --git a/idp/internal/providers/database/dynamic_registration_software_statement_keys.sql.go b/idp/internal/providers/database/dynamic_registration_software_statement_keys.sql.go index 02d1de4..e242434 100644 --- a/idp/internal/providers/database/dynamic_registration_software_statement_keys.sql.go +++ b/idp/internal/providers/database/dynamic_registration_software_statement_keys.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.31.1 // source: dynamic_registration_software_statement_keys.sql package database diff --git a/idp/internal/providers/database/key_encryption_keys.sql.go b/idp/internal/providers/database/key_encryption_keys.sql.go index 1553f30..bfc439c 100644 --- a/idp/internal/providers/database/key_encryption_keys.sql.go +++ b/idp/internal/providers/database/key_encryption_keys.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.31.1 // source: key_encryption_keys.sql package database diff --git a/idp/internal/providers/database/migrations/20241213231542_create_initial_schema.up.sql b/idp/internal/providers/database/migrations/20241213231542_create_initial_schema.up.sql index 9a755bb..2690b40 100644 --- a/idp/internal/providers/database/migrations/20241213231542_create_initial_schema.up.sql +++ b/idp/internal/providers/database/migrations/20241213231542_create_initial_schema.up.sql @@ -1,6 +1,6 @@ -- SQL dump generated using DBML (dbml.dbdiagram.io) -- Database: PostgreSQL --- Generated at: 2025-11-04T08:56:30.229Z +-- Generated at: 2026-09-08T22:46:50.769Z CREATE TYPE "kek_usage" AS ENUM ( 'global', @@ -651,8 +651,6 @@ CREATE TABLE "account_dynamic_registration_configs" ( "require_software_statement_credential_types" account_credentials_type[] NOT NULL, "software_statement_verification_methods" software_statement_verification_method[] NOT NULL, "require_verified_domains_credentials_type" account_credentials_type[] NOT NULL, - "require_initial_access_token_credential_types" account_credentials_type[] NOT NULL, - "initial_access_token_generation_methods" initial_access_token_generation_method[] NOT NULL, "created_at" timestamptz NOT NULL DEFAULT (now()), "updated_at" timestamptz NOT NULL DEFAULT (now()) ); @@ -1049,150 +1047,150 @@ CREATE INDEX "revoked_tokens_account_id_idx" ON "revoked_tokens" ("account_id"); CREATE INDEX "revoked_tokens_expires_at_idx" ON "revoked_tokens" ("expires_at"); -ALTER TABLE "data_encryption_keys" ADD FOREIGN KEY ("kek_kid") REFERENCES "key_encryption_keys" ("kid") ON DELETE CASCADE ON UPDATE CASCADE; +ALTER TABLE "data_encryption_keys" ADD FOREIGN KEY ("kek_kid") REFERENCES "key_encryption_keys" ("kid") ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE INITIALLY IMMEDIATE; -ALTER TABLE "token_signing_keys" ADD FOREIGN KEY ("dek_kid") REFERENCES "data_encryption_keys" ("kid") ON DELETE CASCADE ON UPDATE CASCADE; +ALTER TABLE "token_signing_keys" ADD FOREIGN KEY ("dek_kid") REFERENCES "data_encryption_keys" ("kid") ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE INITIALLY IMMEDIATE; -ALTER TABLE "account_2fa_configs" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("id") ON DELETE CASCADE; +ALTER TABLE "account_2fa_configs" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("id") ON DELETE CASCADE DEFERRABLE INITIALLY IMMEDIATE; -ALTER TABLE "totps" ADD FOREIGN KEY ("dek_kid") REFERENCES "data_encryption_keys" ("kid") ON DELETE CASCADE ON UPDATE CASCADE; +ALTER TABLE "totps" ADD FOREIGN KEY ("dek_kid") REFERENCES "data_encryption_keys" ("kid") ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE INITIALLY IMMEDIATE; -ALTER TABLE "totps" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("id") ON DELETE CASCADE; +ALTER TABLE "totps" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("id") ON DELETE CASCADE DEFERRABLE INITIALLY IMMEDIATE; -ALTER TABLE "credentials_secrets" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("id") ON DELETE CASCADE; +ALTER TABLE "credentials_secrets" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("id") ON DELETE CASCADE DEFERRABLE INITIALLY IMMEDIATE; -ALTER TABLE "credentials_secrets" ADD FOREIGN KEY ("dek_kid") REFERENCES "data_encryption_keys" ("kid") ON DELETE CASCADE ON UPDATE CASCADE; +ALTER TABLE "credentials_secrets" ADD FOREIGN KEY ("dek_kid") REFERENCES "data_encryption_keys" ("kid") ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE INITIALLY IMMEDIATE; -ALTER TABLE "credentials_keys" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("id") ON DELETE CASCADE; +ALTER TABLE "credentials_keys" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("id") ON DELETE CASCADE DEFERRABLE INITIALLY IMMEDIATE; -ALTER TABLE "account_key_encryption_keys" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("id") ON DELETE CASCADE; +ALTER TABLE "account_key_encryption_keys" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("id") ON DELETE CASCADE DEFERRABLE INITIALLY IMMEDIATE; -ALTER TABLE "account_key_encryption_keys" ADD FOREIGN KEY ("key_encryption_key_id") REFERENCES "key_encryption_keys" ("id") ON DELETE CASCADE; +ALTER TABLE "account_key_encryption_keys" ADD FOREIGN KEY ("key_encryption_key_id") REFERENCES "key_encryption_keys" ("id") ON DELETE CASCADE DEFERRABLE INITIALLY IMMEDIATE; -ALTER TABLE "account_data_encryption_keys" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("id") ON DELETE CASCADE; +ALTER TABLE "account_data_encryption_keys" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("id") ON DELETE CASCADE DEFERRABLE INITIALLY IMMEDIATE; -ALTER TABLE "account_data_encryption_keys" ADD FOREIGN KEY ("data_encryption_key_id") REFERENCES "data_encryption_keys" ("id") ON DELETE CASCADE; +ALTER TABLE "account_data_encryption_keys" ADD FOREIGN KEY ("data_encryption_key_id") REFERENCES "data_encryption_keys" ("id") ON DELETE CASCADE DEFERRABLE INITIALLY IMMEDIATE; -ALTER TABLE "account_hmac_secrets" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("id") ON DELETE CASCADE; +ALTER TABLE "account_hmac_secrets" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("id") ON DELETE CASCADE DEFERRABLE INITIALLY IMMEDIATE; -ALTER TABLE "account_hmac_secrets" ADD FOREIGN KEY ("dek_kid") REFERENCES "data_encryption_keys" ("kid") ON DELETE CASCADE ON UPDATE CASCADE; +ALTER TABLE "account_hmac_secrets" ADD FOREIGN KEY ("dek_kid") REFERENCES "data_encryption_keys" ("kid") ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE INITIALLY IMMEDIATE; -ALTER TABLE "account_totps" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("id") ON DELETE CASCADE; +ALTER TABLE "account_totps" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("id") ON DELETE CASCADE DEFERRABLE INITIALLY IMMEDIATE; -ALTER TABLE "account_totps" ADD FOREIGN KEY ("totp_id") REFERENCES "totps" ("id") ON DELETE CASCADE; +ALTER TABLE "account_totps" ADD FOREIGN KEY ("totp_id") REFERENCES "totps" ("id") ON DELETE CASCADE DEFERRABLE INITIALLY IMMEDIATE; -ALTER TABLE "account_credentials" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("id") ON DELETE CASCADE; +ALTER TABLE "account_credentials" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("id") ON DELETE CASCADE DEFERRABLE INITIALLY IMMEDIATE; -ALTER TABLE "account_credentials_secrets" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("id") ON DELETE CASCADE; +ALTER TABLE "account_credentials_secrets" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("id") ON DELETE CASCADE DEFERRABLE INITIALLY IMMEDIATE; -ALTER TABLE "account_credentials_secrets" ADD FOREIGN KEY ("credentials_secret_id") REFERENCES "credentials_secrets" ("id") ON DELETE CASCADE; +ALTER TABLE "account_credentials_secrets" ADD FOREIGN KEY ("credentials_secret_id") REFERENCES "credentials_secrets" ("id") ON DELETE CASCADE DEFERRABLE INITIALLY IMMEDIATE; -ALTER TABLE "account_credentials_keys" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("id") ON DELETE CASCADE; +ALTER TABLE "account_credentials_keys" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("id") ON DELETE CASCADE DEFERRABLE INITIALLY IMMEDIATE; -ALTER TABLE "account_credentials_keys" ADD FOREIGN KEY ("account_credentials_id") REFERENCES "account_credentials" ("id") ON DELETE CASCADE; +ALTER TABLE "account_credentials_keys" ADD FOREIGN KEY ("account_credentials_id") REFERENCES "account_credentials" ("id") ON DELETE CASCADE DEFERRABLE INITIALLY IMMEDIATE; -ALTER TABLE "account_credentials_keys" ADD FOREIGN KEY ("credentials_key_id") REFERENCES "credentials_keys" ("id") ON DELETE CASCADE; +ALTER TABLE "account_credentials_keys" ADD FOREIGN KEY ("credentials_key_id") REFERENCES "credentials_keys" ("id") ON DELETE CASCADE DEFERRABLE INITIALLY IMMEDIATE; -ALTER TABLE "account_auth_providers" ADD FOREIGN KEY ("email") REFERENCES "accounts" ("email") ON DELETE CASCADE ON UPDATE CASCADE; +ALTER TABLE "account_auth_providers" ADD FOREIGN KEY ("email") REFERENCES "accounts" ("email") ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE INITIALLY IMMEDIATE; -ALTER TABLE "oidc_configs" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("id") ON DELETE CASCADE; +ALTER TABLE "oidc_configs" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("id") ON DELETE CASCADE DEFERRABLE INITIALLY IMMEDIATE; -ALTER TABLE "account_token_signing_keys" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("id") ON DELETE CASCADE; +ALTER TABLE "account_token_signing_keys" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("id") ON DELETE CASCADE DEFERRABLE INITIALLY IMMEDIATE; -ALTER TABLE "account_token_signing_keys" ADD FOREIGN KEY ("token_signing_key_id") REFERENCES "token_signing_keys" ("id") ON DELETE CASCADE; +ALTER TABLE "account_token_signing_keys" ADD FOREIGN KEY ("token_signing_key_id") REFERENCES "token_signing_keys" ("id") ON DELETE CASCADE DEFERRABLE INITIALLY IMMEDIATE; -ALTER TABLE "users" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("id") ON DELETE CASCADE; +ALTER TABLE "users" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("id") ON DELETE CASCADE DEFERRABLE INITIALLY IMMEDIATE; -ALTER TABLE "user_2fa_configs" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("id") ON DELETE CASCADE; +ALTER TABLE "user_2fa_configs" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("id") ON DELETE CASCADE DEFERRABLE INITIALLY IMMEDIATE; -ALTER TABLE "user_2fa_configs" ADD FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE; +ALTER TABLE "user_2fa_configs" ADD FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE DEFERRABLE INITIALLY IMMEDIATE; -ALTER TABLE "user_data_encryption_keys" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("id") ON DELETE CASCADE; +ALTER TABLE "user_data_encryption_keys" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("id") ON DELETE CASCADE DEFERRABLE INITIALLY IMMEDIATE; -ALTER TABLE "user_data_encryption_keys" ADD FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE; +ALTER TABLE "user_data_encryption_keys" ADD FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE DEFERRABLE INITIALLY IMMEDIATE; -ALTER TABLE "user_data_encryption_keys" ADD FOREIGN KEY ("data_encryption_key_id") REFERENCES "data_encryption_keys" ("id") ON DELETE CASCADE; +ALTER TABLE "user_data_encryption_keys" ADD FOREIGN KEY ("data_encryption_key_id") REFERENCES "data_encryption_keys" ("id") ON DELETE CASCADE DEFERRABLE INITIALLY IMMEDIATE; -ALTER TABLE "user_totps" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("id") ON DELETE CASCADE; +ALTER TABLE "user_totps" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("id") ON DELETE CASCADE DEFERRABLE INITIALLY IMMEDIATE; -ALTER TABLE "user_totps" ADD FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE; +ALTER TABLE "user_totps" ADD FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE DEFERRABLE INITIALLY IMMEDIATE; -ALTER TABLE "user_totps" ADD FOREIGN KEY ("totp_id") REFERENCES "totps" ("id") ON DELETE CASCADE; +ALTER TABLE "user_totps" ADD FOREIGN KEY ("totp_id") REFERENCES "totps" ("id") ON DELETE CASCADE DEFERRABLE INITIALLY IMMEDIATE; -ALTER TABLE "user_auth_providers" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("id") ON DELETE CASCADE; +ALTER TABLE "user_auth_providers" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("id") ON DELETE CASCADE DEFERRABLE INITIALLY IMMEDIATE; -ALTER TABLE "user_auth_providers" ADD FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE; +ALTER TABLE "user_auth_providers" ADD FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE DEFERRABLE INITIALLY IMMEDIATE; -ALTER TABLE "user_credentials" ADD FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE; +ALTER TABLE "user_credentials" ADD FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE DEFERRABLE INITIALLY IMMEDIATE; -ALTER TABLE "user_credentials" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("id") ON DELETE CASCADE; +ALTER TABLE "user_credentials" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("id") ON DELETE CASCADE DEFERRABLE INITIALLY IMMEDIATE; -ALTER TABLE "user_credentials" ADD FOREIGN KEY ("app_id") REFERENCES "apps" ("id") ON DELETE CASCADE; +ALTER TABLE "user_credentials" ADD FOREIGN KEY ("app_id") REFERENCES "apps" ("id") ON DELETE CASCADE DEFERRABLE INITIALLY IMMEDIATE; -ALTER TABLE "user_credentials_secrets" ADD FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE; +ALTER TABLE "user_credentials_secrets" ADD FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE DEFERRABLE INITIALLY IMMEDIATE; -ALTER TABLE "user_credentials_secrets" ADD FOREIGN KEY ("user_credential_id") REFERENCES "user_credentials" ("id") ON DELETE CASCADE; +ALTER TABLE "user_credentials_secrets" ADD FOREIGN KEY ("user_credential_id") REFERENCES "user_credentials" ("id") ON DELETE CASCADE DEFERRABLE INITIALLY IMMEDIATE; -ALTER TABLE "user_credentials_secrets" ADD FOREIGN KEY ("credentials_secret_id") REFERENCES "credentials_secrets" ("id") ON DELETE CASCADE; +ALTER TABLE "user_credentials_secrets" ADD FOREIGN KEY ("credentials_secret_id") REFERENCES "credentials_secrets" ("id") ON DELETE CASCADE DEFERRABLE INITIALLY IMMEDIATE; -ALTER TABLE "user_credentials_secrets" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("id") ON DELETE CASCADE; +ALTER TABLE "user_credentials_secrets" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("id") ON DELETE CASCADE DEFERRABLE INITIALLY IMMEDIATE; -ALTER TABLE "user_credentials_keys" ADD FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE; +ALTER TABLE "user_credentials_keys" ADD FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE DEFERRABLE INITIALLY IMMEDIATE; -ALTER TABLE "user_credentials_keys" ADD FOREIGN KEY ("user_credential_id") REFERENCES "user_credentials" ("id") ON DELETE CASCADE; +ALTER TABLE "user_credentials_keys" ADD FOREIGN KEY ("user_credential_id") REFERENCES "user_credentials" ("id") ON DELETE CASCADE DEFERRABLE INITIALLY IMMEDIATE; -ALTER TABLE "user_credentials_keys" ADD FOREIGN KEY ("credentials_key_id") REFERENCES "credentials_keys" ("id") ON DELETE CASCADE; +ALTER TABLE "user_credentials_keys" ADD FOREIGN KEY ("credentials_key_id") REFERENCES "credentials_keys" ("id") ON DELETE CASCADE DEFERRABLE INITIALLY IMMEDIATE; -ALTER TABLE "user_credentials_keys" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("id") ON DELETE CASCADE; +ALTER TABLE "user_credentials_keys" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("id") ON DELETE CASCADE DEFERRABLE INITIALLY IMMEDIATE; -ALTER TABLE "apps" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("id") ON DELETE CASCADE; +ALTER TABLE "apps" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("id") ON DELETE CASCADE DEFERRABLE INITIALLY IMMEDIATE; -ALTER TABLE "app_secrets" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("id") ON DELETE CASCADE; +ALTER TABLE "app_secrets" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("id") ON DELETE CASCADE DEFERRABLE INITIALLY IMMEDIATE; -ALTER TABLE "app_secrets" ADD FOREIGN KEY ("app_id") REFERENCES "apps" ("id") ON DELETE CASCADE; +ALTER TABLE "app_secrets" ADD FOREIGN KEY ("app_id") REFERENCES "apps" ("id") ON DELETE CASCADE DEFERRABLE INITIALLY IMMEDIATE; -ALTER TABLE "app_secrets" ADD FOREIGN KEY ("credentials_secret_id") REFERENCES "credentials_secrets" ("id") ON DELETE CASCADE; +ALTER TABLE "app_secrets" ADD FOREIGN KEY ("credentials_secret_id") REFERENCES "credentials_secrets" ("id") ON DELETE CASCADE DEFERRABLE INITIALLY IMMEDIATE; -ALTER TABLE "app_keys" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("id") ON DELETE CASCADE; +ALTER TABLE "app_keys" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("id") ON DELETE CASCADE DEFERRABLE INITIALLY IMMEDIATE; -ALTER TABLE "app_keys" ADD FOREIGN KEY ("app_id") REFERENCES "apps" ("id") ON DELETE CASCADE; +ALTER TABLE "app_keys" ADD FOREIGN KEY ("app_id") REFERENCES "apps" ("id") ON DELETE CASCADE DEFERRABLE INITIALLY IMMEDIATE; -ALTER TABLE "app_keys" ADD FOREIGN KEY ("credentials_key_id") REFERENCES "credentials_keys" ("id") ON DELETE CASCADE; +ALTER TABLE "app_keys" ADD FOREIGN KEY ("credentials_key_id") REFERENCES "credentials_keys" ("id") ON DELETE CASCADE DEFERRABLE INITIALLY IMMEDIATE; -ALTER TABLE "app_related_apps" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("id") ON DELETE CASCADE; +ALTER TABLE "app_related_apps" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("id") ON DELETE CASCADE DEFERRABLE INITIALLY IMMEDIATE; -ALTER TABLE "app_related_apps" ADD FOREIGN KEY ("app_id") REFERENCES "apps" ("id") ON DELETE CASCADE; +ALTER TABLE "app_related_apps" ADD FOREIGN KEY ("app_id") REFERENCES "apps" ("id") ON DELETE CASCADE DEFERRABLE INITIALLY IMMEDIATE; -ALTER TABLE "app_related_apps" ADD FOREIGN KEY ("related_app_id") REFERENCES "apps" ("id") ON DELETE CASCADE; +ALTER TABLE "app_related_apps" ADD FOREIGN KEY ("related_app_id") REFERENCES "apps" ("id") ON DELETE CASCADE DEFERRABLE INITIALLY IMMEDIATE; -ALTER TABLE "app_service_configs" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("id") ON DELETE CASCADE; +ALTER TABLE "app_service_configs" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("id") ON DELETE CASCADE DEFERRABLE INITIALLY IMMEDIATE; -ALTER TABLE "app_service_configs" ADD FOREIGN KEY ("app_id") REFERENCES "apps" ("id") ON DELETE CASCADE; +ALTER TABLE "app_service_configs" ADD FOREIGN KEY ("app_id") REFERENCES "apps" ("id") ON DELETE CASCADE DEFERRABLE INITIALLY IMMEDIATE; -ALTER TABLE "app_designs" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("id") ON DELETE CASCADE; +ALTER TABLE "app_designs" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("id") ON DELETE CASCADE DEFERRABLE INITIALLY IMMEDIATE; -ALTER TABLE "app_designs" ADD FOREIGN KEY ("app_id") REFERENCES "apps" ("id") ON DELETE CASCADE; +ALTER TABLE "app_designs" ADD FOREIGN KEY ("app_id") REFERENCES "apps" ("id") ON DELETE CASCADE DEFERRABLE INITIALLY IMMEDIATE; -ALTER TABLE "account_dynamic_registration_configs" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("id") ON DELETE CASCADE; +ALTER TABLE "account_dynamic_registration_configs" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("id") ON DELETE CASCADE DEFERRABLE INITIALLY IMMEDIATE; -ALTER TABLE "app_dynamic_registration_configs" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("id") ON DELETE CASCADE; +ALTER TABLE "app_dynamic_registration_configs" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("id") ON DELETE CASCADE DEFERRABLE INITIALLY IMMEDIATE; -ALTER TABLE "dynamic_registration_domains" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("id") ON DELETE CASCADE; +ALTER TABLE "dynamic_registration_domains" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("id") ON DELETE CASCADE DEFERRABLE INITIALLY IMMEDIATE; -ALTER TABLE "dynamic_registration_domain_codes" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("id") ON DELETE CASCADE; +ALTER TABLE "dynamic_registration_domain_codes" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("id") ON DELETE CASCADE DEFERRABLE INITIALLY IMMEDIATE; -ALTER TABLE "dynamic_registration_domain_codes" ADD FOREIGN KEY ("dynamic_registration_domain_id") REFERENCES "dynamic_registration_domains" ("id") ON DELETE CASCADE; +ALTER TABLE "dynamic_registration_domain_codes" ADD FOREIGN KEY ("dynamic_registration_domain_id") REFERENCES "dynamic_registration_domains" ("id") ON DELETE CASCADE DEFERRABLE INITIALLY IMMEDIATE; -ALTER TABLE "dynamic_registration_domain_codes" ADD FOREIGN KEY ("hmac_secret_id") REFERENCES "account_hmac_secrets" ("secret_id") ON DELETE CASCADE ON UPDATE CASCADE; +ALTER TABLE "dynamic_registration_domain_codes" ADD FOREIGN KEY ("hmac_secret_id") REFERENCES "account_hmac_secrets" ("secret_id") ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE INITIALLY IMMEDIATE; -ALTER TABLE "dynamic_registration_software_statement_keys" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("id") ON DELETE CASCADE; +ALTER TABLE "dynamic_registration_software_statement_keys" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("id") ON DELETE CASCADE DEFERRABLE INITIALLY IMMEDIATE; -ALTER TABLE "dynamic_registration_software_statement_keys" ADD FOREIGN KEY ("credentials_key_id") REFERENCES "credentials_keys" ("id") ON DELETE CASCADE; +ALTER TABLE "dynamic_registration_software_statement_keys" ADD FOREIGN KEY ("credentials_key_id") REFERENCES "credentials_keys" ("id") ON DELETE CASCADE DEFERRABLE INITIALLY IMMEDIATE; -ALTER TABLE "app_profiles" ADD FOREIGN KEY ("app_id") REFERENCES "apps" ("id") ON DELETE CASCADE; +ALTER TABLE "app_profiles" ADD FOREIGN KEY ("app_id") REFERENCES "apps" ("id") ON DELETE CASCADE DEFERRABLE INITIALLY IMMEDIATE; -ALTER TABLE "app_profiles" ADD FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE; +ALTER TABLE "app_profiles" ADD FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE DEFERRABLE INITIALLY IMMEDIATE; -ALTER TABLE "app_profiles" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("id") ON DELETE CASCADE; +ALTER TABLE "app_profiles" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("id") ON DELETE CASCADE DEFERRABLE INITIALLY IMMEDIATE; -ALTER TABLE "revoked_tokens" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("id") ON DELETE CASCADE; +ALTER TABLE "revoked_tokens" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("id") ON DELETE CASCADE DEFERRABLE INITIALLY IMMEDIATE; diff --git a/idp/internal/providers/database/models.go b/idp/internal/providers/database/models.go index 8196938..a7f8194 100644 --- a/idp/internal/providers/database/models.go +++ b/idp/internal/providers/database/models.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.31.1 package database @@ -1491,17 +1491,15 @@ type AccountDataEncryptionKey struct { } type AccountDynamicRegistrationConfig struct { - ID int32 - AccountID int32 - AccountPublicID uuid.UUID - AccountCredentialsTypes []AccountCredentialsType - RequireSoftwareStatementCredentialTypes []AccountCredentialsType - SoftwareStatementVerificationMethods []SoftwareStatementVerificationMethod - RequireVerifiedDomainsCredentialsType []AccountCredentialsType - RequireInitialAccessTokenCredentialTypes []AccountCredentialsType - InitialAccessTokenGenerationMethods []InitialAccessTokenGenerationMethod - CreatedAt time.Time - UpdatedAt time.Time + ID int32 + AccountID int32 + AccountPublicID uuid.UUID + AccountCredentialsTypes []AccountCredentialsType + RequireSoftwareStatementCredentialTypes []AccountCredentialsType + SoftwareStatementVerificationMethods []SoftwareStatementVerificationMethod + RequireVerifiedDomainsCredentialsType []AccountCredentialsType + CreatedAt time.Time + UpdatedAt time.Time } type AccountHmacSecret struct { diff --git a/idp/internal/providers/database/oidc_configs.sql.go b/idp/internal/providers/database/oidc_configs.sql.go index c53dc7a..d95c418 100644 --- a/idp/internal/providers/database/oidc_configs.sql.go +++ b/idp/internal/providers/database/oidc_configs.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.31.1 // source: oidc_configs.sql package database diff --git a/idp/internal/providers/database/queries/account_dynamic_registration_configs.sql b/idp/internal/providers/database/queries/account_dynamic_registration_configs.sql index eeece27..683c1b6 100644 --- a/idp/internal/providers/database/queries/account_dynamic_registration_configs.sql +++ b/idp/internal/providers/database/queries/account_dynamic_registration_configs.sql @@ -10,26 +10,20 @@ INSERT INTO "account_dynamic_registration_configs" ( "account_public_id", "account_credentials_types", "require_software_statement_credential_types", - "software_statement_verification_methods", - "require_initial_access_token_credential_types", - "initial_access_token_generation_methods" + "software_statement_verification_methods" ) VALUES ( $1, $2, $3, $4, - $5, - $6, - $7 + $5 ) RETURNING *; -- name: UpdateAccountDynamicRegistrationConfig :one UPDATE "account_dynamic_registration_configs" SET "account_credentials_types" = $2, "require_software_statement_credential_types" = $3, - "software_statement_verification_methods" = $4, - "require_initial_access_token_credential_types" = $5, - "initial_access_token_generation_methods" = $6 + "software_statement_verification_methods" = $4 WHERE "id" = $1 RETURNING *; diff --git a/idp/internal/providers/database/revoked_tokens.sql.go b/idp/internal/providers/database/revoked_tokens.sql.go index 0e580b6..031b59b 100644 --- a/idp/internal/providers/database/revoked_tokens.sql.go +++ b/idp/internal/providers/database/revoked_tokens.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.31.1 // source: revoked_tokens.sql package database diff --git a/idp/internal/providers/database/token_signing_keys.sql.go b/idp/internal/providers/database/token_signing_keys.sql.go index ff89794..19e5f5f 100644 --- a/idp/internal/providers/database/token_signing_keys.sql.go +++ b/idp/internal/providers/database/token_signing_keys.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.31.1 // source: token_signing_keys.sql package database diff --git a/idp/internal/providers/database/totps.sql.go b/idp/internal/providers/database/totps.sql.go index 7ceab92..1694bff 100644 --- a/idp/internal/providers/database/totps.sql.go +++ b/idp/internal/providers/database/totps.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.31.1 // source: totps.sql package database diff --git a/idp/internal/providers/database/user_auth_providers.sql.go b/idp/internal/providers/database/user_auth_providers.sql.go index 772499c..8adb2b6 100644 --- a/idp/internal/providers/database/user_auth_providers.sql.go +++ b/idp/internal/providers/database/user_auth_providers.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.31.1 // source: user_auth_providers.sql package database diff --git a/idp/internal/providers/database/user_totps.sql.go b/idp/internal/providers/database/user_totps.sql.go index 070927d..7b68856 100644 --- a/idp/internal/providers/database/user_totps.sql.go +++ b/idp/internal/providers/database/user_totps.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.31.1 // source: user_totps.sql package database diff --git a/idp/internal/providers/database/users.sql.go b/idp/internal/providers/database/users.sql.go index 6ecd08f..f817932 100644 --- a/idp/internal/providers/database/users.sql.go +++ b/idp/internal/providers/database/users.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.31.1 // source: users.sql package database diff --git a/idp/internal/providers/tokens/dynamic_registration_iat.go b/idp/internal/providers/tokens/dynamic_registration_iat.go index f97a5fd..0b706db 100644 --- a/idp/internal/providers/tokens/dynamic_registration_iat.go +++ b/idp/internal/providers/tokens/dynamic_registration_iat.go @@ -22,8 +22,6 @@ const dynamicRegistrationIATLocation = "dynamic_registration_iat" type accountCredentialsDynamicRegistrationClaims struct { AccountClaims - Domain string `json:"domain"` - ClientID string `json:"client_id"` jwt.RegisteredClaims } @@ -49,8 +47,6 @@ func (t *Tokens) DynamicRegistrationIAT( AccountID: opts.AccountPublicID, AccountVersion: opts.AccountVersion, }, - Domain: opts.Domain, - ClientID: opts.ClientID, RegisteredClaims: jwt.RegisteredClaims{ Issuer: iss, Audience: []string{iss}, @@ -58,7 +54,7 @@ func (t *Tokens) DynamicRegistrationIAT( IssuedAt: iat, NotBefore: iat, ExpiresAt: exp, - ID: uuid.NewString(), + ID: opts.ClientID, }, }, ) @@ -128,7 +124,7 @@ func (t *Tokens) VerifyDynamicRegistrationIAT( } logger.InfoContext(ctx, "Verified account credentials dynamic registration IAT successfully") - return claims.Domain, claims.AccountClaims, nil + return claims.Subject, claims.AccountClaims, nil } func (t *Tokens) GetDynamicRegistrationTTL() int64 { diff --git a/idp/internal/server/routes.go b/idp/internal/server/routes.go index 2e58750..4d467b5 100644 --- a/idp/internal/server/routes.go +++ b/idp/internal/server/routes.go @@ -6,6 +6,7 @@ package server +// TODO: separate global and app routes func (s *FiberServer) RegisterFiberRoutes() { s.routes.HealthRoutes(s.App) s.routes.AccountDynamicRegistrationConfigurationRoutes(s.App) diff --git a/idp/internal/server/routes/common.go b/idp/internal/server/routes/common.go index b45d1d5..86ad014 100644 --- a/idp/internal/server/routes/common.go +++ b/idp/internal/server/routes/common.go @@ -10,14 +10,8 @@ import ( "github.com/gofiber/fiber/v3" "github.com/tugascript/devlogs/idp/internal/controllers/paths" - "github.com/tugascript/devlogs/idp/internal/exceptions" ) -var errorResponseNotFound = exceptions.ErrorResponse{ - Code: exceptions.StatusNotFound, - Message: exceptions.MessageNotFound, -} - func V1PathRouter(app *fiber.App) fiber.Router { return app.Group(paths.V1) } diff --git a/idp/internal/server/routes/oauth.go b/idp/internal/server/routes/oauth.go index 708bddb..af11571 100644 --- a/idp/internal/server/routes/oauth.go +++ b/idp/internal/server/routes/oauth.go @@ -31,15 +31,25 @@ func (r *Routes) OAuthRoutes(app *fiber.App) { router.Post( paths.OAuthRegister, r.controllers.HostMiddleware, - r.controllers.DynamicRegistrationIATMiddleware, HostAwareRoute( - []fiber.Handler{r.controllers.OAuthDynamicRegistration}, - []fiber.Handler{r.controllers.OAuthAppDynamicRegistration}, + []fiber.Handler{ + r.controllers.DynamicRegistrationIATMiddleware, + r.controllers.OAuthDynamicRegistration, + }, + []fiber.Handler{ + r.controllers.AppDynamicRegistrationIATMiddleware, + r.controllers.OAuthAppDynamicRegistration, + }, ), ) // Initial Access Token (IAT) routes iatRouter := router.Group(paths.InitialAccessToken, r.controllers.HostMiddleware) + iatRouter.Post( + paths.InitialAccessTokenSign, + r.controllers.AccountAccessClaimsMiddleware, + r.controllers.AppDynamicRegistrationIATSign, + ) // Dynamic Registration IAT Code Exchange flow iatRouter.Get(paths.OAuthAuth, r.controllers.OAuthDynamicRegistrationIATAuth) diff --git a/idp/internal/services/account_credentials_registration.go b/idp/internal/services/account_credentials_registration.go index 19cda2b..28710ab 100644 --- a/idp/internal/services/account_credentials_registration.go +++ b/idp/internal/services/account_credentials_registration.go @@ -354,7 +354,6 @@ func (s *Services) mapAccountCredentialsRegistrationDataToDBParams( type CreateAccountCredentialsRegistrationOptions struct { RequestID string AccountPublicID uuid.UUID - IsAuthenticated bool IATDomain string AccountVersion int32 ApplicationType string @@ -459,12 +458,6 @@ func (s *Services) CreateAccountCredentialsRegistration( return dtos.AccountCredentialsDTO{}, serviceErr } - if slices.Contains(accountDRConfigDTO.RequireInitialAccessTokenCredentialTypes, applicationType) && - !opts.IsAuthenticated { - logger.WarnContext(ctx, "Account dynamic registration configuration needs to contain initial access token") - return dtos.AccountCredentialsDTO{}, exceptions.NewUnauthorizedError() - } - if slices.Contains(accountDRConfigDTO.RequireSoftwareStatementCredentialTypes, applicationType) && opts.SoftwareStatement == "" { logger.WarnContext(ctx, "Account dynamic registration configuration needs to contain software statement") diff --git a/idp/internal/services/account_dynamic_registration_configs.go b/idp/internal/services/account_dynamic_registration_configs.go index 8317817..a9736a5 100644 --- a/idp/internal/services/account_dynamic_registration_configs.go +++ b/idp/internal/services/account_dynamic_registration_configs.go @@ -102,14 +102,12 @@ func mapInitialAccessTokenGenerationMethods( } type SaveAccountDynamicRegistrationConfigOptions struct { - RequestID string - AccountPublicID uuid.UUID - AccountVersion int32 - AccountCredentialsTypes []string - RequireSoftwareStatementCredentialTypes []string - SoftwareStatementVerificationMethods []string - RequireInitialAccessTokenCredentialTypes []string - InitialAccessTokenGenerationMethods []string + RequestID string + AccountPublicID uuid.UUID + AccountVersion int32 + AccountCredentialsTypes []string + RequireSoftwareStatementCredentialTypes []string + SoftwareStatementVerificationMethods []string } func (s *Services) SaveAccountDynamicRegistrationConfig( @@ -134,24 +132,12 @@ func (s *Services) SaveAccountDynamicRegistrationConfig( return dtos.AccountDynamicRegistrationConfigDTO{}, false, serviceErr } - requireInitialAccessTokenCredentialTypes, serviceErr := mapAccountCredentialsTypes(opts.RequireInitialAccessTokenCredentialTypes) - if serviceErr != nil { - logger.WarnContext(ctx, "Failed to map require initial access token credential types", "serviceError", serviceErr) - return dtos.AccountDynamicRegistrationConfigDTO{}, false, serviceErr - } - softwareStatementVerificationMethods, serviceErr := mapSoftwareStatementVerificationMethods(opts.SoftwareStatementVerificationMethods) if serviceErr != nil { logger.WarnContext(ctx, "Failed to map software statement verification methods", "serviceError", serviceErr) return dtos.AccountDynamicRegistrationConfigDTO{}, false, serviceErr } - initialAccessTokenGenerationMethods, serviceErr := mapInitialAccessTokenGenerationMethods(opts.InitialAccessTokenGenerationMethods) - if serviceErr != nil { - logger.WarnContext(ctx, "Failed to map initial access token generation methods", "serviceError", serviceErr) - return dtos.AccountDynamicRegistrationConfigDTO{}, false, serviceErr - } - accountID, serviceErr := s.GetAccountIDByPublicIDAndVersion(ctx, GetAccountIDByPublicIDAndVersionOptions{ RequestID: opts.RequestID, PublicID: opts.AccountPublicID, @@ -174,13 +160,11 @@ func (s *Services) SaveAccountDynamicRegistrationConfig( accountDynamicRegistrationConfig, err = s.database.CreateAccountDynamicRegistrationConfig( ctx, database.CreateAccountDynamicRegistrationConfigParams{ - AccountID: accountID, - AccountPublicID: opts.AccountPublicID, - AccountCredentialsTypes: credentialsTypes, - RequireSoftwareStatementCredentialTypes: requireSoftwareStatementCredentialTypes, - SoftwareStatementVerificationMethods: softwareStatementVerificationMethods, - RequireInitialAccessTokenCredentialTypes: requireInitialAccessTokenCredentialTypes, - InitialAccessTokenGenerationMethods: initialAccessTokenGenerationMethods, + AccountID: accountID, + AccountPublicID: opts.AccountPublicID, + AccountCredentialsTypes: credentialsTypes, + RequireSoftwareStatementCredentialTypes: requireSoftwareStatementCredentialTypes, + SoftwareStatementVerificationMethods: softwareStatementVerificationMethods, }, ) if err != nil { @@ -192,12 +176,10 @@ func (s *Services) SaveAccountDynamicRegistrationConfig( } accountDynamicRegistrationConfig, err = s.database.UpdateAccountDynamicRegistrationConfig(ctx, database.UpdateAccountDynamicRegistrationConfigParams{ - ID: accountDynamicRegistrationConfig.ID, - AccountCredentialsTypes: credentialsTypes, - RequireSoftwareStatementCredentialTypes: requireSoftwareStatementCredentialTypes, - SoftwareStatementVerificationMethods: softwareStatementVerificationMethods, - RequireInitialAccessTokenCredentialTypes: requireInitialAccessTokenCredentialTypes, - InitialAccessTokenGenerationMethods: initialAccessTokenGenerationMethods, + ID: accountDynamicRegistrationConfig.ID, + AccountCredentialsTypes: credentialsTypes, + RequireSoftwareStatementCredentialTypes: requireSoftwareStatementCredentialTypes, + SoftwareStatementVerificationMethods: softwareStatementVerificationMethods, }) if err != nil { logger.ErrorContext(ctx, "Failed to update account dynamic registration config", "error", err) diff --git a/idp/internal/services/account_dynamic_registration_tokens.go b/idp/internal/services/account_dynamic_registration_tokens.go deleted file mode 100644 index 1df7cfd..0000000 --- a/idp/internal/services/account_dynamic_registration_tokens.go +++ /dev/null @@ -1,7 +0,0 @@ -// Copyright (c) 2025 Afonso Barracha -// -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -package services diff --git a/idp/internal/services/app_dynamic_registration_iat.go b/idp/internal/services/app_dynamic_registration_iat.go index 770a9db..91fbaff 100644 --- a/idp/internal/services/app_dynamic_registration_iat.go +++ b/idp/internal/services/app_dynamic_registration_iat.go @@ -16,6 +16,7 @@ import ( "github.com/tugascript/devlogs/idp/internal/providers/crypto" "github.com/tugascript/devlogs/idp/internal/providers/database" "github.com/tugascript/devlogs/idp/internal/providers/tokens" + "github.com/tugascript/devlogs/idp/internal/services/dtos" "github.com/tugascript/devlogs/idp/internal/utils" ) @@ -32,7 +33,7 @@ type CreateAppCredentialsRegistrationIATOptions struct { func (s *Services) CreateAppCredentialsRegistrationIAT( ctx context.Context, opts CreateAppCredentialsRegistrationIATOptions, -) (string, *exceptions.ServiceError) { +) (dtos.AuthDTO, *exceptions.ServiceError) { logger := s.buildLogger(opts.RequestID, appDynamicRegistrationIATLocation, "CreateAppCredentialsRegistrationIAT").With( "accountPublicId", opts.AccountPublicID, "domain", opts.Domain, @@ -45,7 +46,7 @@ func (s *Services) CreateAppCredentialsRegistrationIAT( Domain: opts.Domain, }); serviceErr != nil { logger.ErrorContext(ctx, "Failed to get app credentials registration domain", "serviceError", serviceErr) - return "", serviceErr + return dtos.AuthDTO{}, serviceErr } accountDTO, serviceErr := s.GetAccountByPublicIDAndVersion(ctx, GetAccountByPublicIDAndVersionOptions{ @@ -55,10 +56,11 @@ func (s *Services) CreateAppCredentialsRegistrationIAT( }) if serviceErr != nil { logger.ErrorContext(ctx, "Failed to get account", "serviceError", serviceErr) - return "", serviceErr + return dtos.AuthDTO{}, serviceErr } accountID := accountDTO.ID() + tokenTTL := s.jwt.GetDynamicRegistrationTTL() signedToken, serviceErr := s.crypto.SignToken(ctx, crypto.SignTokenOptions{ RequestID: opts.RequestID, Token: s.jwt.DynamicRegistrationIAT(tokens.DynamicRegistrationIATOptions{ @@ -87,9 +89,55 @@ func (s *Services) CreateAppCredentialsRegistrationIAT( }) if serviceErr != nil { logger.ErrorContext(ctx, "Failed to sign app credentials registration IAT", "serviceError", serviceErr) - return "", serviceErr + return dtos.AuthDTO{}, serviceErr } logger.InfoContext(ctx, "Created app credentials registration IAT successfully") - return signedToken, nil + return dtos.NewAuthDTO(signedToken, tokenTTL), nil +} + +type ProcessAppDynamicRegistrationIATAuthOptions struct { + RequestID string + AuthHeader string + AccountID int32 + IssuerDomain string +} + +func (s *Services) ProcessAppDynamicRegistrationIATAuth( + ctx context.Context, + opts ProcessAppDynamicRegistrationIATAuthOptions, +) (string, tokens.AccountClaims, *exceptions.ServiceError) { + logger := s.buildLogger(opts.RequestID, appDynamicRegistrationIATLocation, "ProcessOAuthDynamicRegistrationIATAuth") + logger.InfoContext(ctx, "Processing OAuth dynamic registration IAT auth...") + + token, serviceErr := extractAuthHeaderToken(opts.AuthHeader) + if serviceErr != nil { + logger.WarnContext(ctx, "Failed to extract token from auth header", "serviceError", serviceErr) + return "", tokens.AccountClaims{}, serviceErr + } + + domain, accountClaims, err := s.jwt.VerifyDynamicRegistrationIAT( + ctx, + tokens.VerifyDynamicRegistrationIATOptions{ + RequestID: opts.RequestID, + IAT: token, + IssuerDomain: opts.IssuerDomain, + GetPublicJWK: s.BuildGetAccountPublicKeyFn( + ctx, + BuildGetAccountPublicKeyFnOptions{ + RequestID: opts.RequestID, + AccountID: opts.AccountID, + KeyType: database.TokenKeyTypeDynamicRegistration, + }, + ), + }, + ) + + if err != nil { + logger.WarnContext(ctx, "Failed to verify OAuth dynamic registration IAT", "error", err) + return "", tokens.AccountClaims{}, exceptions.NewUnauthorizedError() + } + + logger.InfoContext(ctx, "Processed OAuth dynamic registration IAT auth successfully") + return domain, accountClaims, nil } diff --git a/idp/internal/services/apps_auth.go b/idp/internal/services/apps_auth.go index 9ad1c0d..de9f9f5 100644 --- a/idp/internal/services/apps_auth.go +++ b/idp/internal/services/apps_auth.go @@ -38,10 +38,10 @@ func (s *Services) ProcessAppAuthHeader( appClaims, err := s.jwt.VerifyAppToken( token, - s.buildVerifyAccountKeyFn(ctx, logger, buildVerifyAccountKeyFnOptions{ - requestID: opts.RequestID, - accountID: opts.AccountID, - keyType: database.TokenKeyTypeClientCredentials, + s.BuildGetAccountPublicKeyFn(ctx, BuildGetAccountPublicKeyFnOptions{ + RequestID: opts.RequestID, + AccountID: opts.AccountID, + KeyType: database.TokenKeyTypeClientCredentials, }), ) if err != nil { diff --git a/idp/internal/services/dtos/account_dynamic_registration_config.go b/idp/internal/services/dtos/account_dynamic_registration_config.go index 9215960..92254cb 100644 --- a/idp/internal/services/dtos/account_dynamic_registration_config.go +++ b/idp/internal/services/dtos/account_dynamic_registration_config.go @@ -11,12 +11,10 @@ import "github.com/tugascript/devlogs/idp/internal/providers/database" type AccountDynamicRegistrationConfigDTO struct { id int32 - CredentialsTypes []database.AccountCredentialsType `json:"credentials_types"` - RequireSoftwareStatementCredentialTypes []database.AccountCredentialsType `json:"require_software_statement_credential_types"` - RequireVerifiedDomainsCredentialsType []database.AccountCredentialsType `json:"require_verified_domains_credentials_type"` - SoftwareStatementVerificationMethods []database.SoftwareStatementVerificationMethod `json:"software_statement_verification_methods"` - RequireInitialAccessTokenCredentialTypes []database.AccountCredentialsType `json:"require_initial_access_token_credential_types"` - InitialAccessTokenGenerationMethods []database.InitialAccessTokenGenerationMethod `json:"initial_access_token_generation_methods"` + CredentialsTypes []database.AccountCredentialsType `json:"credentials_types"` + RequireSoftwareStatementCredentialTypes []database.AccountCredentialsType `json:"require_software_statement_credential_types"` + RequireVerifiedDomainsCredentialsType []database.AccountCredentialsType `json:"require_verified_domains_credentials_type"` + SoftwareStatementVerificationMethods []database.SoftwareStatementVerificationMethod `json:"software_statement_verification_methods"` } func (a *AccountDynamicRegistrationConfigDTO) ID() int32 { @@ -27,12 +25,10 @@ func MapAccountDynamicRegistrationConfigToDTO( config *database.AccountDynamicRegistrationConfig, ) AccountDynamicRegistrationConfigDTO { return AccountDynamicRegistrationConfigDTO{ - id: config.ID, - CredentialsTypes: config.AccountCredentialsTypes, - RequireSoftwareStatementCredentialTypes: config.RequireSoftwareStatementCredentialTypes, - RequireVerifiedDomainsCredentialsType: config.RequireVerifiedDomainsCredentialsType, - SoftwareStatementVerificationMethods: config.SoftwareStatementVerificationMethods, - RequireInitialAccessTokenCredentialTypes: config.RequireInitialAccessTokenCredentialTypes, - InitialAccessTokenGenerationMethods: config.InitialAccessTokenGenerationMethods, + id: config.ID, + CredentialsTypes: config.AccountCredentialsTypes, + RequireSoftwareStatementCredentialTypes: config.RequireSoftwareStatementCredentialTypes, + RequireVerifiedDomainsCredentialsType: config.RequireVerifiedDomainsCredentialsType, + SoftwareStatementVerificationMethods: config.SoftwareStatementVerificationMethods, } } diff --git a/idp/internal/services/jwks.go b/idp/internal/services/jwks.go index 4436cb0..c6388ea 100644 --- a/idp/internal/services/jwks.go +++ b/idp/internal/services/jwks.go @@ -9,7 +9,6 @@ package services import ( "context" "fmt" - "log/slog" "time" "github.com/tugascript/devlogs/idp/internal/exceptions" @@ -606,21 +605,23 @@ func (s *Services) BuildGetEncryptedAccountJWKFn( } } -type buildVerifyAccountKeyFnOptions struct { - requestID string - accountID int32 - keyType database.TokenKeyType +type BuildGetAccountPublicKeyFnOptions struct { + RequestID string + AccountID int32 + KeyType database.TokenKeyType } -func (s *Services) buildVerifyAccountKeyFn( +func (s *Services) BuildGetAccountPublicKeyFn( ctx context.Context, - logger *slog.Logger, - opts buildVerifyAccountKeyFnOptions, + opts BuildGetAccountPublicKeyFnOptions, ) tokens.GetPublicJWK { + logger := s.buildLogger(opts.RequestID, jwkLocation, "BuildGetAccountPublicKeyFn") + logger.InfoContext(ctx, "Building get account public JWK function...") + return func(kid string, cryptoSuite utils.SupportedCryptoSuite) (utils.JWK, error) { - suffix := fmt.Sprintf("account:%d", opts.accountID) + suffix := fmt.Sprintf("account:%d", opts.AccountID) jwk, found, err := s.cache.GetJWK(ctx, cache.GetJWKOptions{ - RequestID: opts.requestID, + RequestID: opts.RequestID, Prefix: suffix, CryptoSuite: cryptoSuite, KeyID: kid, @@ -643,7 +644,7 @@ func (s *Services) buildVerifyAccountKeyFn( jwkEnt, err := s.database.FindAccountTokenSigningKeyByAccountIDAndKID( ctx, database.FindAccountTokenSigningKeyByAccountIDAndKIDParams{ - AccountID: opts.accountID, + AccountID: opts.AccountID, Kid: kid, }, ) @@ -655,8 +656,8 @@ func (s *Services) buildVerifyAccountKeyFn( logger.ErrorContext(ctx, "JWK is not an account JWK", "kid", kid) return nil, exceptions.NewUnauthorizedError() } - if jwkEnt.KeyType != opts.keyType { - logger.ErrorContext(ctx, "JWK is not the expected key type", "kid", kid, "expectedKeyType", opts.keyType, "actualKeyType", jwkEnt.KeyType) + if jwkEnt.KeyType != opts.KeyType { + logger.ErrorContext(ctx, "JWK is not the expected key type", "kid", kid, "expectedKeyType", opts.KeyType, "actualKeyType", jwkEnt.KeyType) return nil, exceptions.NewUnauthorizedError() } if dbCryptoSuite != jwkEnt.CryptoSuite { @@ -673,7 +674,7 @@ func (s *Services) buildVerifyAccountKeyFn( return nil, err } if err := s.cache.SavePublicJWK(ctx, cache.SavePublicJWKOptions{ - RequestID: opts.requestID, + RequestID: opts.RequestID, Prefix: suffix, CryptoSuite: cryptoSuite, KeyID: jwkEnt.Kid, diff --git a/idp/internal/services/users_auth.go b/idp/internal/services/users_auth.go index b56c68e..a9b32f4 100644 --- a/idp/internal/services/users_auth.go +++ b/idp/internal/services/users_auth.go @@ -91,10 +91,10 @@ func (s *Services) ProcessUserAuthHeader( userClaims, appClaims, scopes, _, _, err := s.jwt.VerifyUserAuthToken( token, utils.SupportedCryptoSuiteES256, - s.buildVerifyAccountKeyFn(ctx, logger, buildVerifyAccountKeyFnOptions{ - requestID: opts.RequestID, - accountID: opts.AccountID, - keyType: keyType, + s.BuildGetAccountPublicKeyFn(ctx, BuildGetAccountPublicKeyFnOptions{ + RequestID: opts.RequestID, + AccountID: opts.AccountID, + KeyType: keyType, }), ) if err != nil { @@ -133,10 +133,10 @@ func (s *Services) ProcessUserPurposeHeader( userClaims, appClaims, purpose, err := s.jwt.VerifyUserPurposeToken( token, - s.buildVerifyAccountKeyFn(ctx, logger, buildVerifyAccountKeyFnOptions{ - requestID: opts.RequestID, - accountID: opts.AccountID, - keyType: keyType, + s.BuildGetAccountPublicKeyFn(ctx, BuildGetAccountPublicKeyFnOptions{ + RequestID: opts.RequestID, + AccountID: opts.AccountID, + KeyType: keyType, }), ) if err != nil { @@ -432,10 +432,10 @@ func (s *Services) ConfirmAuthUser( userClaims, appClaims, _, err := s.jwt.VerifyUserPurposeToken( opts.ConfirmationToken, - s.buildVerifyAccountKeyFn(ctx, logger, buildVerifyAccountKeyFnOptions{ - requestID: opts.RequestID, - accountID: opts.AccountID, - keyType: database.TokenKeyTypeEmailVerification, + s.BuildGetAccountPublicKeyFn(ctx, BuildGetAccountPublicKeyFnOptions{ + RequestID: opts.RequestID, + AccountID: opts.AccountID, + KeyType: database.TokenKeyTypeEmailVerification, }), ) if err != nil { @@ -776,10 +776,10 @@ func (s *Services) LogoutUser( userClaims, appClaims, _, tokenID, exp, err := s.jwt.VerifyUserAuthToken( opts.Token, utils.SupportedCryptoSuiteEd25519, - s.buildVerifyAccountKeyFn(ctx, logger, buildVerifyAccountKeyFnOptions{ - requestID: opts.RequestID, - accountID: opts.AccountID, - keyType: database.TokenKeyTypeRefresh, + s.BuildGetAccountPublicKeyFn(ctx, BuildGetAccountPublicKeyFnOptions{ + RequestID: opts.RequestID, + AccountID: opts.AccountID, + KeyType: database.TokenKeyTypeRefresh, }), ) if err != nil { @@ -849,10 +849,10 @@ func (s *Services) RefreshUserAccess( userClaims, appClaims, scopes, tokenID, _, err := s.jwt.VerifyUserAuthToken( opts.Token, utils.SupportedCryptoSuiteEd25519, - s.buildVerifyAccountKeyFn(ctx, logger, buildVerifyAccountKeyFnOptions{ - requestID: opts.RequestID, - accountID: opts.AccountID, - keyType: database.TokenKeyTypeRefresh, + s.BuildGetAccountPublicKeyFn(ctx, BuildGetAccountPublicKeyFnOptions{ + RequestID: opts.RequestID, + AccountID: opts.AccountID, + KeyType: database.TokenKeyTypeRefresh, }), ) if err != nil { @@ -1078,10 +1078,10 @@ func (s *Services) ResetUserPassword( userClaims, appClaims, _, err := s.jwt.VerifyUserPurposeToken( opts.ResetToken, - s.buildVerifyAccountKeyFn(ctx, logger, buildVerifyAccountKeyFnOptions{ - requestID: opts.RequestID, - accountID: opts.AccountID, - keyType: database.TokenKeyTypePasswordReset, + s.BuildGetAccountPublicKeyFn(ctx, BuildGetAccountPublicKeyFnOptions{ + RequestID: opts.RequestID, + AccountID: opts.AccountID, + KeyType: database.TokenKeyTypePasswordReset, }), ) if err != nil { From b9bac4fdb4b58153b119b0250b5e8e12d5eadd3e Mon Sep 17 00:00:00 2001 From: Afonso Barracha Date: Thu, 10 Sep 2026 20:23:13 +1200 Subject: [PATCH 3/4] test: add dynamic registration tests --- .../bodies/oauth_dynamic_registration.go | 12 +- idp/internal/controllers/helpers.go | 7 + idp/internal/controllers/middleware.go | 63 +- .../controllers/oauth_dynamic_registration.go | 202 +++++- .../oauth_dynamic_registration_iat.go | 84 ++- idp/internal/controllers/paths/oauth.go | 17 +- ...ccount_credentials_dynamic_registration.go | 18 +- .../database/account_credentials.sql.go | 171 +++++ ...ccount_dynamic_registration_configs.sql.go | 10 +- .../dynamic_registration_domains.sql.go | 16 +- ...egistration_software_statement_keys.sql.go | 2 +- .../database/queries/account_credentials.sql | 42 ++ .../account_dynamic_registration_configs.sql | 10 +- .../queries/dynamic_registration_domains.sql | 10 +- ...c_registration_software_statement_keys.sql | 2 +- .../database/queries/registered_apps.sql | 144 ++++ .../providers/database/registered_apps.sql.go | 466 ++++++++++++ .../tokens/dynamic_registration_iat.go | 106 ++- .../tokens/dynamic_registration_iat_test.go | 87 +++ ...ynamic_registration_software_statements.go | 81 ++- idp/internal/server/routes/oauth.go | 51 +- idp/internal/server/server.go | 8 +- .../account_credentials_registration.go | 340 +++------ .../account_credentials_registration_iat.go | 102 ++- .../services/app_dynamic_registration.go | 412 ++++++----- .../services/app_dynamic_registration_iat.go | 120 +++- .../services/dtos/account_credentials.go | 41 +- idp/internal/services/dtos/app.go | 7 +- .../services/dtos/client_registration.go | 200 ++++++ idp/internal/services/dtos/well_known.go | 2 +- .../services/dynamic_registration_domains.go | 36 +- .../services/oauth_dynamic_registration.go | 244 ++++--- .../oauth_dynamic_registration_config.go | 310 ++++++++ .../services/registration_metadata.go | 256 +++++++ .../services/registration_metadata_test.go | 72 ++ idp/internal/services/software_statement.go | 144 +--- idp/internal/utils/jwk.go | 9 +- idp/tests/auth_test.go | 301 -------- idp/tests/dynamic_registration_test.go | 669 ++++++++++++++++++ 39 files changed, 3735 insertions(+), 1139 deletions(-) create mode 100644 idp/internal/providers/database/queries/registered_apps.sql create mode 100644 idp/internal/providers/database/registered_apps.sql.go create mode 100644 idp/internal/providers/tokens/dynamic_registration_iat_test.go create mode 100644 idp/internal/services/dtos/client_registration.go create mode 100644 idp/internal/services/oauth_dynamic_registration_config.go create mode 100644 idp/internal/services/registration_metadata.go create mode 100644 idp/internal/services/registration_metadata_test.go create mode 100644 idp/tests/dynamic_registration_test.go diff --git a/idp/internal/controllers/bodies/oauth_dynamic_registration.go b/idp/internal/controllers/bodies/oauth_dynamic_registration.go index 8869e12..27e1086 100644 --- a/idp/internal/controllers/bodies/oauth_dynamic_registration.go +++ b/idp/internal/controllers/bodies/oauth_dynamic_registration.go @@ -13,22 +13,22 @@ type OAuthDynamicClientRegistrationBody struct { 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"` + ApplicationType string `json:"application_type,omitempty" validate:"omitempty,oneof=native service mcp web spa backend device"` + 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" validate:"required,multiple_scope"` + 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,json"` + 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"` + RequireAuthTime bool `json:"require_auth_time,omitempty"` 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"` diff --git a/idp/internal/controllers/helpers.go b/idp/internal/controllers/helpers.go index 43d677e..b05bee6 100644 --- a/idp/internal/controllers/helpers.go +++ b/idp/internal/controllers/helpers.go @@ -100,6 +100,11 @@ func serviceErrorResponse(logger *slog.Logger, ctx fiber.Ctx, serviceErr *except return ctx.Status(status).JSON(&resErr) } +func (c *Controllers) NotFoundHandler(ctx fiber.Ctx) error { + logger := c.buildLogger(getRequestID(ctx), "helpers", "NotFoundHandler") + return serviceErrorResponse(logger, ctx, exceptions.NewNotFoundError()) +} + func serviceErrorWithFieldsResponse(logger *slog.Logger, ctx fiber.Ctx, serviceErr *exceptions.ServicErrorWithFields) error { logResponse(logger, ctx, fiber.StatusBadRequest) return ctx.Status(fiber.StatusBadRequest).JSON(exceptions.NewValidationErrorResponse( @@ -201,6 +206,8 @@ func dynamicRegistrationServiceError( serviceErr *exceptions.ServiceError, ) error { switch serviceErr.Code { + case exceptions.OAuthErrorInvalidRedirectURI: + return oauthErrorResponse(logger, ctx, exceptions.OAuthErrorInvalidRedirectURI) case exceptions.CodeUnauthorized, exceptions.CodeForbidden: return oauthErrorResponse(logger, ctx, exceptions.OAuthErrorUnauthorizedClient) case exceptions.CodeNotFound, exceptions.CodeValidation: diff --git a/idp/internal/controllers/middleware.go b/idp/internal/controllers/middleware.go index d12604a..aeca462 100644 --- a/idp/internal/controllers/middleware.go +++ b/idp/internal/controllers/middleware.go @@ -199,7 +199,7 @@ func (c *Controllers) DynamicRegistrationIATMiddleware(ctx fiber.Ctx) error { ctx.Locals("account", accountClaims) ctx.Locals("domain", domain) - return ctx.Next() + return continueMiddleware(ctx) } func (c *Controllers) AppDynamicRegistrationIATMiddleware(ctx fiber.Ctx) error { @@ -211,10 +211,11 @@ func (c *Controllers) AppDynamicRegistrationIATMiddleware(ctx fiber.Ctx) error { return oauthErrorResponse(logger, ctx, exceptions.OAuthErrorServerError) } + ctx.Locals("isAuthenticated", false) authHeader := ctx.Get("Authorization") if authHeader == "" { logger.InfoContext(ctx.Context(), "No Authorization header found, skipping app dynamic registration IAT middleware") - return ctx.Next() + return continueMiddleware(ctx) } domain, accountClaims, serviceErr := c.services.ProcessAppDynamicRegistrationIATAuth( @@ -235,7 +236,63 @@ func (c *Controllers) AppDynamicRegistrationIATMiddleware(ctx fiber.Ctx) error { ctx.Locals("account", accountClaims) ctx.Locals("domain", domain) ctx.Locals("isAuthenticated", true) - return ctx.Next() + return continueMiddleware(ctx) +} + +func (c *Controllers) DynamicRegistrationAccessTokenMiddleware(ctx fiber.Ctx) error { + requestID := getRequestID(ctx) + logger := c.buildLogger(requestID, middlewareLocation, "DynamicRegistrationAccessTokenMiddleware") + authHeader := ctx.Get("Authorization") + if authHeader == "" { + return oauthErrorResponse(logger, ctx, exceptions.OAuthErrorInvalidToken) + } + + clientID, accountClaims, serviceErr := c.services.ProcessAccountCredentialsRegistrationAccessToken( + ctx.Context(), + services.ProcessAccountCredentialsRegistrationAccessTokenOptions{ + RequestID: requestID, + AuthHeader: authHeader, + IssuerDomain: c.backendDomain, + }, + ) + if serviceErr != nil { + return oauthErrorResponse(logger, ctx, exceptions.OAuthErrorInvalidToken) + } + + ctx.Locals("account", accountClaims) + ctx.Locals("registrationClientID", clientID) + return continueMiddleware(ctx) +} + +func (c *Controllers) AppDynamicRegistrationAccessTokenMiddleware(ctx fiber.Ctx) error { + requestID := getRequestID(ctx) + logger := c.buildLogger(requestID, middlewareLocation, "AppDynamicRegistrationAccessTokenMiddleware") + username, accountID, serviceErr := getHostAccount(ctx) + if serviceErr != nil { + return oauthErrorResponse(logger, ctx, exceptions.OAuthErrorInvalidToken) + } + + authHeader := ctx.Get("Authorization") + if authHeader == "" { + return oauthErrorResponse(logger, ctx, exceptions.OAuthErrorInvalidToken) + } + + clientID, accountClaims, serviceErr := c.services.ProcessAppDynamicRegistrationAccessToken( + ctx.Context(), + services.ProcessAppDynamicRegistrationAccessTokenOptions{ + RequestID: requestID, + AuthHeader: authHeader, + AccountID: accountID, + IssuerDomain: fmt.Sprintf("%s.%s", username, c.backendDomain), + }, + ) + if serviceErr != nil { + return oauthErrorResponse(logger, ctx, exceptions.OAuthErrorInvalidToken) + } + + ctx.Locals("account", accountClaims) + ctx.Locals("registrationClientID", clientID) + return continueMiddleware(ctx) } func (c *Controllers) ScopeMiddleware(scope tokens.AccountScope) func(fiber.Ctx) error { diff --git a/idp/internal/controllers/oauth_dynamic_registration.go b/idp/internal/controllers/oauth_dynamic_registration.go index 1f4909f..bb0bf87 100644 --- a/idp/internal/controllers/oauth_dynamic_registration.go +++ b/idp/internal/controllers/oauth_dynamic_registration.go @@ -21,6 +21,8 @@ func (c *Controllers) OAuthDynamicRegistration(ctx fiber.Ctx) error { requestID := getRequestID(ctx) logger := c.buildLogger(requestID, oauthDynamicRegistration, "OAuthDynamicRegistration") logRequest(logger, ctx) + ctx.Set("Cache-Control", "no-store") + ctx.Set("Pragma", "no-cache") accountClaims, ok := ctx.Locals("account").(tokens.AccountClaims) if !ok { @@ -37,12 +39,6 @@ func (c *Controllers) OAuthDynamicRegistration(ctx fiber.Ctx) error { if err := ctx.Bind().Body(body); err != nil { return oauthErrorResponse(logger, ctx, exceptions.OAuthErrorInvalidClientMetadata) } - 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) - } accountCredentialsDTO, serviceErr := c.services.CreateAccountCredentialsRegistration( ctx.Context(), @@ -95,13 +91,15 @@ func (c *Controllers) OAuthDynamicRegistration(ctx fiber.Ctx) error { } logResponse(logger, ctx, fiber.StatusCreated) - return ctx.Status(fiber.StatusCreated).JSON(&accountCredentialsDTO) + return ctx.Status(fiber.StatusCreated).JSON(accountCredentialsDTO.Registration) } func (c *Controllers) OAuthAppDynamicRegistration(ctx fiber.Ctx) error { requestID := getRequestID(ctx) logger := c.buildLogger(requestID, oauthDynamicRegistration, "OAuthAppDynamicRegistration") logRequest(logger, ctx) + ctx.Set("Cache-Control", "no-store") + ctx.Set("Pragma", "no-cache") _, accountID, serviceErr := getHostAccount(ctx) if serviceErr != nil { @@ -112,9 +110,6 @@ func (c *Controllers) OAuthAppDynamicRegistration(ctx fiber.Ctx) error { if err := ctx.Bind().Body(body); err != nil { return oauthErrorResponse(logger, ctx, exceptions.OAuthErrorInvalidClientMetadata) } - if err := c.validate.StructCtx(ctx.Context(), body); err != nil { - return oauthErrorResponse(logger, ctx, exceptions.OAuthErrorInvalidClientMetadata) - } isAuthenticated, ok := ctx.Locals("isAuthenticated").(bool) if !ok { @@ -186,5 +181,190 @@ func (c *Controllers) OAuthAppDynamicRegistration(ctx fiber.Ctx) error { } logResponse(logger, ctx, fiber.StatusCreated) - return ctx.Status(fiber.StatusCreated).JSON(&appDTO) + return ctx.Status(fiber.StatusCreated).JSON(appDTO.Registration) +} + +func registrationClientIDFromContext(ctx fiber.Ctx) (string, bool) { + clientID, ok := ctx.Locals("registrationClientID").(string) + return clientID, ok && clientID != "" +} + +func (c *Controllers) bindRegistrationBody(ctx fiber.Ctx) (*bodies.OAuthDynamicClientRegistrationBody, error) { + body := new(bodies.OAuthDynamicClientRegistrationBody) + if err := ctx.Bind().Body(body); err != nil { + return nil, err + } + return body, nil +} + +func (c *Controllers) OAuthDynamicRegistrationGet(ctx fiber.Ctx) error { + requestID := getRequestID(ctx) + logger := c.buildLogger(requestID, oauthDynamicRegistration, "OAuthDynamicRegistrationGet") + logRequest(logger, ctx) + ctx.Set("Cache-Control", "no-store") + ctx.Set("Pragma", "no-cache") + + accountClaims, ok := ctx.Locals("account").(tokens.AccountClaims) + tokenClientID, tokenOK := registrationClientIDFromContext(ctx) + if !ok || !tokenOK || tokenClientID != ctx.Params("clientID") { + return oauthErrorResponse(logger, ctx, exceptions.OAuthErrorInvalidToken) + } + + dto, serviceErr := c.services.GetRegisteredAccountCredentials(ctx.Context(), services.GetRegisteredClientOptions{ + RequestID: requestID, AccountPublicID: accountClaims.AccountID, ClientID: tokenClientID, BackendDomain: c.backendDomain, + }) + if serviceErr != nil { + return oauthErrorResponse(logger, ctx, exceptions.OAuthErrorInvalidToken) + } + logResponse(logger, ctx, fiber.StatusOK) + return ctx.Status(fiber.StatusOK).JSON(dto) +} + +func (c *Controllers) OAuthAppDynamicRegistrationGet(ctx fiber.Ctx) error { + requestID := getRequestID(ctx) + logger := c.buildLogger(requestID, oauthDynamicRegistration, "OAuthAppDynamicRegistrationGet") + logRequest(logger, ctx) + ctx.Set("Cache-Control", "no-store") + ctx.Set("Pragma", "no-cache") + + username, _, serviceErr := getHostAccount(ctx) + accountClaims, ok := ctx.Locals("account").(tokens.AccountClaims) + tokenClientID, tokenOK := registrationClientIDFromContext(ctx) + if serviceErr != nil || !ok || !tokenOK || tokenClientID != ctx.Params("clientID") { + return oauthErrorResponse(logger, ctx, exceptions.OAuthErrorInvalidToken) + } + + dto, serviceErr := c.services.GetRegisteredApp(ctx.Context(), services.GetRegisteredClientOptions{ + RequestID: requestID, AccountPublicID: accountClaims.AccountID, ClientID: tokenClientID, + BackendDomain: c.backendDomain, HostUsername: username, + }) + if serviceErr != nil { + return oauthErrorResponse(logger, ctx, exceptions.OAuthErrorInvalidToken) + } + logResponse(logger, ctx, fiber.StatusOK) + return ctx.Status(fiber.StatusOK).JSON(dto) +} + +func (c *Controllers) OAuthDynamicRegistrationUpdate(ctx fiber.Ctx) error { + requestID := getRequestID(ctx) + logger := c.buildLogger(requestID, oauthDynamicRegistration, "OAuthDynamicRegistrationUpdate") + logRequest(logger, ctx) + ctx.Set("Cache-Control", "no-store") + ctx.Set("Pragma", "no-cache") + + accountClaims, ok := ctx.Locals("account").(tokens.AccountClaims) + tokenClientID, tokenOK := registrationClientIDFromContext(ctx) + if !ok || !tokenOK || tokenClientID != ctx.Params("clientID") { + return oauthErrorResponse(logger, ctx, exceptions.OAuthErrorInvalidToken) + } + body, err := c.bindRegistrationBody(ctx) + if err != nil { + return oauthErrorResponse(logger, ctx, exceptions.OAuthErrorInvalidClientMetadata) + } + + dto, serviceErr := c.services.UpdateRegisteredAccountCredentials(ctx.Context(), services.UpdateRegisteredClientOptions{ + ClientID: tokenClientID, + CreateAccountCredentialsRegistrationOptions: services.CreateAccountCredentialsRegistrationOptions{ + RequestID: requestID, AccountPublicID: accountClaims.AccountID, AccountVersion: accountClaims.AccountVersion, + ApplicationType: body.ApplicationType, RedirectURIs: body.RedirectURIs, TokenEndpointAuthMethod: body.TokenEndpointAuthMethod, + GrantTypes: body.GrantTypes, ResponseTypes: body.ResponseTypes, ClientName: body.ClientName, ClientURI: body.ClientURI, + LogoURI: body.LogoURI, TOSURI: body.TOSURI, PolicyURI: body.PolicyURI, Contacts: body.Contacts, SoftwareID: body.SoftwareID, + SoftwareVersion: body.SoftwareVersion, SoftwareStatement: body.SoftwareStatement, JWKsURI: body.JWKsURI, JWKs: body.JWKs, + FrontendDomain: c.frontendDomain, BackendDomain: c.backendDomain, RequireAuthTime: body.RequireAuthTime, + DefaultMaxAge: body.DefaultMaxAge, SubjectType: body.SubjectType, IDTokenSignedResponseAlg: body.IDTokenSignedResponseAlg, + IDTokenEncryptedResponseAlg: body.IDTokenEncryptedResponseAlg, IDTokenEncryptedResponseEnc: body.IDTokenEncryptedResponseEnc, + RequestObjectSigningAlg: body.RequestObjectSigningAlg, RequestObjectEncryptionAlg: body.RequestObjectEncryptionAlg, + RequestObjectEncryptionEnc: body.RequestObjectEncryptionEnc, DefaultACRValues: body.DefaultACRValues, Scope: body.Scope, + SectorIdentifierURI: body.SectorIdentifierURI, InitiateLoginURI: body.InitiateLoginURI, RequestURIs: body.RequestURIs, + UserInfoSignedResponseAlg: body.UserInfoSignedResponseAlg, UserInfoEncryptedResponseAlg: body.UserInfoEncryptedResponseAlg, + UserInfoEncryptedResponseEnc: body.UserInfoEncryptedResponseEnc, TokenEndpointAuthSigningAlg: body.TokenEndpointAuthSigningAlg, + AccessTokenSigningAlg: body.AccessTokenSigningAlg, + }, + }) + if serviceErr != nil { + return dynamicRegistrationServiceError(logger, ctx, serviceErr) + } + logResponse(logger, ctx, fiber.StatusOK) + return ctx.Status(fiber.StatusOK).JSON(dto) +} + +func (c *Controllers) OAuthAppDynamicRegistrationUpdate(ctx fiber.Ctx) error { + requestID := getRequestID(ctx) + logger := c.buildLogger(requestID, oauthDynamicRegistration, "OAuthAppDynamicRegistrationUpdate") + logRequest(logger, ctx) + ctx.Set("Cache-Control", "no-store") + ctx.Set("Pragma", "no-cache") + + username, accountID, serviceErr := getHostAccount(ctx) + tokenClientID, tokenOK := registrationClientIDFromContext(ctx) + if serviceErr != nil || !tokenOK || tokenClientID != ctx.Params("clientID") { + return oauthErrorResponse(logger, ctx, exceptions.OAuthErrorInvalidToken) + } + body, err := c.bindRegistrationBody(ctx) + if err != nil { + return oauthErrorResponse(logger, ctx, exceptions.OAuthErrorInvalidClientMetadata) + } + + dto, serviceErr := c.services.UpdateRegisteredApp(ctx.Context(), services.UpdateRegisteredAppOptions{ + ClientID: tokenClientID, HostUsername: username, + CreateAppCredentialsRegistrationOptions: services.CreateAppCredentialsRegistrationOptions{ + RequestID: requestID, AccountID: accountID, ApplicationType: body.ApplicationType, RedirectURIs: body.RedirectURIs, + TokenEndpointAuthMethod: body.TokenEndpointAuthMethod, GrantTypes: body.GrantTypes, ResponseTypes: body.ResponseTypes, + ClientName: body.ClientName, ClientURI: body.ClientURI, LogoURI: body.LogoURI, TOSURI: body.TOSURI, PolicyURI: body.PolicyURI, + Contacts: body.Contacts, SoftwareID: body.SoftwareID, SoftwareVersion: body.SoftwareVersion, SoftwareStatement: body.SoftwareStatement, + JWKsURI: body.JWKsURI, JWKs: body.JWKs, FrontendDomain: c.frontendDomain, BackendDomain: c.backendDomain, + RequireAuthTime: body.RequireAuthTime, DefaultMaxAge: body.DefaultMaxAge, SubjectType: body.SubjectType, + IDTokenSignedResponseAlg: body.IDTokenSignedResponseAlg, IDTokenEncryptedResponseAlg: body.IDTokenEncryptedResponseAlg, + IDTokenEncryptedResponseEnc: body.IDTokenEncryptedResponseEnc, RequestObjectSigningAlg: body.RequestObjectSigningAlg, + RequestObjectEncryptionAlg: body.RequestObjectEncryptionAlg, RequestObjectEncryptionEnc: body.RequestObjectEncryptionEnc, + DefaultACRValues: body.DefaultACRValues, Scope: body.Scope, SectorIdentifierURI: body.SectorIdentifierURI, + InitiateLoginURI: body.InitiateLoginURI, RequestURIs: body.RequestURIs, UserInfoSignedResponseAlg: body.UserInfoSignedResponseAlg, + UserInfoEncryptedResponseAlg: body.UserInfoEncryptedResponseAlg, UserInfoEncryptedResponseEnc: body.UserInfoEncryptedResponseEnc, + TokenEndpointAuthSigningAlg: body.TokenEndpointAuthSigningAlg, AccessTokenSigningAlg: body.AccessTokenSigningAlg, + }, + }) + if serviceErr != nil { + return dynamicRegistrationServiceError(logger, ctx, serviceErr) + } + logResponse(logger, ctx, fiber.StatusOK) + return ctx.Status(fiber.StatusOK).JSON(dto) +} + +func (c *Controllers) OAuthDynamicRegistrationDelete(ctx fiber.Ctx) error { + requestID := getRequestID(ctx) + logger := c.buildLogger(requestID, oauthDynamicRegistration, "OAuthDynamicRegistrationDelete") + logRequest(logger, ctx) + + accountClaims, ok := ctx.Locals("account").(tokens.AccountClaims) + tokenClientID, tokenOK := registrationClientIDFromContext(ctx) + if !ok || !tokenOK || tokenClientID != ctx.Params("clientID") { + return oauthErrorResponse(logger, ctx, exceptions.OAuthErrorInvalidToken) + } + if serviceErr := c.services.DeleteRegisteredAccountCredentials(ctx.Context(), services.GetRegisteredClientOptions{ + RequestID: requestID, AccountPublicID: accountClaims.AccountID, ClientID: tokenClientID, + }); serviceErr != nil { + return oauthErrorResponse(logger, ctx, exceptions.OAuthErrorInvalidToken) + } + logResponse(logger, ctx, fiber.StatusNoContent) + return ctx.SendStatus(fiber.StatusNoContent) +} + +func (c *Controllers) OAuthAppDynamicRegistrationDelete(ctx fiber.Ctx) error { + requestID := getRequestID(ctx) + logger := c.buildLogger(requestID, oauthDynamicRegistration, "OAuthAppDynamicRegistrationDelete") + logRequest(logger, ctx) + + _, _, serviceErr := getHostAccount(ctx) + accountClaims, ok := ctx.Locals("account").(tokens.AccountClaims) + tokenClientID, tokenOK := registrationClientIDFromContext(ctx) + if serviceErr != nil || !ok || !tokenOK || tokenClientID != ctx.Params("clientID") { + return oauthErrorResponse(logger, ctx, exceptions.OAuthErrorInvalidToken) + } + if serviceErr := c.services.DeleteRegisteredApp(ctx.Context(), services.GetRegisteredClientOptions{ + RequestID: requestID, AccountPublicID: accountClaims.AccountID, ClientID: tokenClientID, + }); serviceErr != nil { + return oauthErrorResponse(logger, ctx, exceptions.OAuthErrorInvalidToken) + } + logResponse(logger, ctx, fiber.StatusNoContent) + return ctx.SendStatus(fiber.StatusNoContent) } diff --git a/idp/internal/controllers/oauth_dynamic_registration_iat.go b/idp/internal/controllers/oauth_dynamic_registration_iat.go index 6c6c647..8fa9c17 100644 --- a/idp/internal/controllers/oauth_dynamic_registration_iat.go +++ b/idp/internal/controllers/oauth_dynamic_registration_iat.go @@ -27,6 +27,18 @@ const ( accountsIAT2FACookieSuffix string = "_acc_iat_2fa" ) +func oauthDynamicRegistrationIATCookiePath() string { + return paths.V1 + paths.AuthBase + paths.OAuthBase + paths.InitialAccessToken +} + +func (c *Controllers) registrationIssuerDomain(ctx fiber.Ctx) string { + username := registrationHostUsername(ctx) + if username == "" { + return c.backendDomain + } + return username + "." + c.backendDomain +} + func (c *Controllers) OAuthDynamicRegistrationIATAuth(ctx fiber.Ctx) error { requestID := getRequestID(ctx) logger := c.buildLogger(requestID, oauthDynamicRegistrationIAT, "OAuthDynamicRegistrationIATAuth") @@ -65,6 +77,7 @@ func (c *Controllers) OAuthDynamicRegistrationIATAuth(ctx fiber.Ctx) error { redirectURL, serviceErr := c.services.InitiateOAuthDynamicRegistrationIATAuth( ctx.Context(), services.InitiateOAuthDynamicRegistrationIATAuthOptions{ + HostUsername: registrationHostUsername(ctx), RequestID: requestID, Domain: baseQPrms.ClientID, State: qPrms.State, @@ -141,7 +154,7 @@ func (c *Controllers) saveAccountIATCookie( ctx.Cookie(&fiber.Cookie{ Name: c.cookieName + accountsIATCookieSuffix, Value: sessionKey, - Path: paths.V1 + paths.AccountsBase + paths.CredentialsBase + paths.InitialAccessToken + paths.OAuthAuth, + Path: oauthDynamicRegistrationIATCookiePath(), HTTPOnly: true, SameSite: fiber.CookieSameSiteLaxMode, Secure: true, @@ -153,7 +166,7 @@ func (c *Controllers) removeAccountIATCookie(ctx fiber.Ctx) { ctx.Cookie(&fiber.Cookie{ Name: c.cookieName + accountsIATCookieSuffix, Value: "", - Path: paths.V1 + paths.AccountsBase + paths.CredentialsBase + paths.InitialAccessToken + paths.OAuthAuth, + Path: oauthDynamicRegistrationIATCookiePath(), HTTPOnly: true, Secure: true, SameSite: fiber.CookieSameSiteNoneMode, @@ -161,11 +174,11 @@ func (c *Controllers) removeAccountIATCookie(ctx fiber.Ctx) { }) } -func (c *Controllers) saveAccountIAT2FACookie(ctx fiber.Ctx, sessionID, clientID string) { +func (c *Controllers) saveAccountIAT2FACookie(ctx fiber.Ctx, sessionID string) { ctx.Cookie(&fiber.Cookie{ Name: c.cookieName + accountsIAT2FACookieSuffix, Value: sessionID, - Path: paths.AccountsBase + paths.CredentialsBase + paths.InitialAccessToken + "/" + clientID + paths.OAuthAuth, + Path: oauthDynamicRegistrationIATCookiePath(), HTTPOnly: true, SameSite: fiber.CookieSameSiteLaxMode, Secure: true, @@ -173,11 +186,11 @@ func (c *Controllers) saveAccountIAT2FACookie(ctx fiber.Ctx, sessionID, clientID }) } -func (c *Controllers) removeAccountIAT2FACookie(ctx fiber.Ctx, clientID string) { +func (c *Controllers) removeAccountIAT2FACookie(ctx fiber.Ctx) { ctx.Cookie(&fiber.Cookie{ Name: c.cookieName + accountsIAT2FACookieSuffix, Value: "", - Path: paths.AccountsBase + paths.CredentialsBase + paths.InitialAccessToken + "/" + clientID + paths.OAuthAuth, + Path: oauthDynamicRegistrationIATCookiePath(), HTTPOnly: true, SameSite: fiber.CookieSameSiteLaxMode, Secure: true, @@ -261,6 +274,7 @@ func (c *Controllers) OAuthDynamicRegistrationIATLoginPost(ctx fiber.Ctx) error Email: loginBody.Email, Password: loginBody.Password, BackendDomain: c.backendDomain, + HostUsername: registrationHostUsername(ctx), }, ) if serviceErr != nil { @@ -294,7 +308,7 @@ func (c *Controllers) OAuthDynamicRegistrationIATLoginPost(ctx fiber.Ctx) error } if loggedIn { - c.saveAccountIAT2FACookie(ctx, sessionKey, uPrms.ACCClientID) + c.saveAccountIAT2FACookie(ctx, sessionKey) logResponse(logger, ctx, fiber.StatusSeeOther) return ctx.Redirect().Status(fiber.StatusSeeOther).To(redirectURL) } @@ -437,6 +451,7 @@ func (c *Controllers) OAuthDynamicRegistrationIAT2FAPost(ctx fiber.Ctx) error { CSRFToken: hiddenFields.CSRFToken, Code: twoFABody.Code, BackendDomain: c.backendDomain, + HostUsername: registrationHostUsername(ctx), }, ) if serviceErr != nil { @@ -470,7 +485,7 @@ func (c *Controllers) OAuthDynamicRegistrationIAT2FAPost(ctx fiber.Ctx) error { return serviceErrorHTMLResponse(logger, ctx, serviceErr) } - c.removeAccountIAT2FACookie(ctx, uPrms.ACCClientID) + c.removeAccountIAT2FACookie(ctx) c.saveAccountIATCookie(ctx, sessionKey) logResponse(logger, ctx, fiber.StatusSeeOther) return ctx.Redirect().Status(fiber.StatusSeeOther).To(redirectURL) @@ -520,10 +535,11 @@ func (c *Controllers) OAuthDynamicRegistrationIATExtAuthGet(ctx fiber.Ctx) error ACCClientID: uPrms.ACCClientID, Provider: uPrms.Provider, Domain: baseQPrms.ClientID, - CallbackURL: baseQPrms.RedirectURI, + CallbackURL: "https://" + c.registrationIssuerDomain(ctx) + paths.V1 + paths.AuthBase + paths.OAuthBase + paths.InitialAccessToken + "/" + uPrms.ACCClientID + paths.InitialAccessTokenAuthEXT + "/" + uPrms.Provider + paths.InitialAccessTokenCallback, RedirectURI: baseQPrms.RedirectURI, State: qPrms.State, BackendDomain: c.backendDomain, + HostUsername: registrationHostUsername(ctx), }, ) if serviceErr != nil { @@ -558,16 +574,14 @@ func (c *Controllers) OAuthDynamicRegistrationIATExtCB(ctx fiber.Ctx) error { cbURL, serviceErr := c.services.OAuthDynamicRegistrationIATExtCB( ctx.Context(), services.OAuthDynamicRegistrationIATExtCBOptions{ - RequestID: requestID, - ACCClientID: uPrms.ACCClientID, - Provider: uPrms.Provider, - State: qPrms.State, - Code: qPrms.Code, - RedirectURL: "https://" + c.backendDomain + paths.V1 + paths.AccountsBase + - paths.CredentialsBase + paths.DynamicRegistrationBase + paths.InitialAccessToken + - "/" + uPrms.ACCClientID + paths.OAuthAuth + paths.InitialAccessTokenAuthEXT + "/" + - uPrms.Provider + paths.InitialAccessTokenCallback, + RequestID: requestID, + ACCClientID: uPrms.ACCClientID, + Provider: uPrms.Provider, + State: qPrms.State, + Code: qPrms.Code, + RedirectURL: "https://" + c.registrationIssuerDomain(ctx) + paths.V1 + paths.AuthBase + paths.OAuthBase + paths.InitialAccessToken + "/" + uPrms.ACCClientID + paths.InitialAccessTokenAuthEXT + "/" + uPrms.Provider + paths.InitialAccessTokenCallback, BackendDomain: c.backendDomain, + HostUsername: registrationHostUsername(ctx), }, ) if serviceErr != nil { @@ -614,16 +628,14 @@ func (c *Controllers) OAuthDynamicRegistrationIATExtAppleCB(ctx fiber.Ctx) error cbURL, serviceErr := c.services.OAuthDynamicRegistrationIATExtAppleCB( ctx.Context(), services.OAuthDynamicRegistrationIATExtAppleCBOptions{ - RequestID: requestID, - ACCClientID: uPrms.ACCClientID, - Email: user.Email, - Code: qPrms.Code, - State: qPrms.State, - RedirectURL: "https://" + c.backendDomain + paths.V1 + paths.AccountsBase + - paths.CredentialsBase + paths.DynamicRegistrationBase + paths.InitialAccessToken + - "/" + uPrms.ACCClientID + paths.OAuthAuth + paths.InitialAccessTokenAuthEXT + "/" + - services.AuthProviderApple + paths.InitialAccessTokenCallback, + RequestID: requestID, + ACCClientID: uPrms.ACCClientID, + Email: user.Email, + Code: qPrms.Code, + State: qPrms.State, + RedirectURL: "https://" + c.registrationIssuerDomain(ctx) + paths.V1 + paths.AuthBase + paths.OAuthBase + paths.InitialAccessToken + "/" + uPrms.ACCClientID + paths.InitialAccessTokenAuthEXT + "/" + services.AuthProviderApple + paths.InitialAccessTokenCallback, BackendDomain: c.backendDomain, + HostUsername: registrationHostUsername(ctx), }, ) if serviceErr != nil { @@ -643,7 +655,7 @@ func (c *Controllers) OAuthDynamicRegistrationIATToken(ctx fiber.Ctx) error { return oauthErrorResponse(logger, ctx, exceptions.OAuthErrorInvalidRequest) } - grantType := ctx.Get("grant_type") + grantType := ctx.FormValue("grant_type") if grantType != "authorization_code" { return oauthErrorResponse(logger, ctx, exceptions.OAuthErrorUnsupportedGrantType) } @@ -661,10 +673,12 @@ func (c *Controllers) OAuthDynamicRegistrationIATToken(ctx fiber.Ctx) error { authDTO, serviceErr := c.services.VerifyOAuthDynamicRegistrationIATCode( ctx.Context(), services.VerifyOAuthDynamicRegistrationIATCodeOptions{ - RequestID: requestID, - Code: body.Code, - CodeVerifier: body.CodeVerifier, - Domain: body.ClientID, + BackendDomain: c.backendDomain, + HostUsername: registrationHostUsername(ctx), + RequestID: requestID, + Code: body.Code, + CodeVerifier: body.CodeVerifier, + Domain: body.ClientID, }, ) if serviceErr != nil { @@ -674,3 +688,9 @@ func (c *Controllers) OAuthDynamicRegistrationIATToken(ctx fiber.Ctx) error { logResponse(logger, ctx, fiber.StatusOK) return ctx.Status(fiber.StatusOK).JSON(authDTO) } + +// HostMiddleware validates the host and sets accountUsername for tenant hosts. +func registrationHostUsername(ctx fiber.Ctx) string { + username, _ := ctx.Locals("accountUsername").(string) + return username +} diff --git a/idp/internal/controllers/paths/oauth.go b/idp/internal/controllers/paths/oauth.go index 61ddfeb..c8078aa 100644 --- a/idp/internal/controllers/paths/oauth.go +++ b/idp/internal/controllers/paths/oauth.go @@ -9,14 +9,15 @@ package paths const ( OAuthBase string = "/oauth2" - OAuthKeys string = "/jwks" - OAuthAuth string = "/auth" - OAuthUserInfo string = "/userinfo" - OAuthToken string = "/token" - OAuthRevoke string = "/revoke" - OAuthRegister string = "/register" - OAuthIntrospect string = "/introspect" - OAuthDeviceAuth string = "/auth/device" + OAuthKeys string = "/jwks" + OAuthAuth string = "/auth" + OAuthUserInfo string = "/userinfo" + OAuthToken string = "/token" + OAuthRevoke string = "/revoke" + OAuthRegister string = "/register" + OAuthRegisterClient string = "/register/:clientID" + OAuthIntrospect string = "/introspect" + OAuthDeviceAuth string = "/auth/device" OAuthAppleCallback string = "/apple/callback" OAuthCallback string = "/:provider/callback" diff --git a/idp/internal/providers/cache/account_credentials_dynamic_registration.go b/idp/internal/providers/cache/account_credentials_dynamic_registration.go index 4b8c0b4..482116d 100644 --- a/idp/internal/providers/cache/account_credentials_dynamic_registration.go +++ b/idp/internal/providers/cache/account_credentials_dynamic_registration.go @@ -231,6 +231,7 @@ func (c *Cache) VerifyAccountCredentialsDynamicRegistrationIATLoginCSRF( } type AccountCredentialsDynamicRegistrationIAT2FAData struct { + Username string `json:"username"` AccountPublicID uuid.UUID `json:"account_public_id"` AccountVersion int32 `json:"account_version"` RedirectURI string `json:"redirect_uri"` @@ -245,6 +246,7 @@ func buildAccountCredentialsDynamicRegistrationIAT2FACacheKey(sessionID string) } type SaveAccountCredentialsDynamicRegistrationIAT2FAOptions struct { + Username string RequestID string AccountPublicID uuid.UUID AccountVersion int32 @@ -272,6 +274,7 @@ func (c *Cache) SaveAccountCredentialsDynamicRegistrationIAT2FA( sessionId := utils.Base64UUID() data := AccountCredentialsDynamicRegistrationIAT2FAData{ + Username: opts.Username, AccountPublicID: opts.AccountPublicID, AccountVersion: opts.AccountVersion, RedirectURI: opts.RedirectURI, @@ -443,6 +446,7 @@ func buildAccountCredentialsDynamicRegistrationIATCodeCacheKey(codeID string) st } type AccountCredentialsDynamicRegistrationIATCodeData struct { + HostUsername string `json:"host_username"` AccountPublicID uuid.UUID `json:"account_public_id"` AccountVersion int32 `json:"account_version"` Domain string `json:"domain"` @@ -452,6 +456,7 @@ type AccountCredentialsDynamicRegistrationIATCodeData struct { } type GenerateAccountCredentialsRegistrationIATCodeOptions struct { + HostUsername string RequestID string ClientID string AccountPublicID uuid.UUID @@ -483,6 +488,7 @@ func (c *Cache) GenerateAccountCredentialsRegistrationIATCode( } data := AccountCredentialsDynamicRegistrationIATCodeData{ + HostUsername: opts.HostUsername, AccountPublicID: opts.AccountPublicID, AccountVersion: opts.AccountVersion, Domain: opts.Domain, @@ -525,13 +531,10 @@ func (c *Cache) VerifyAccountCredentialsRegistrationIATCode( }) logger.DebugContext(ctx, "Verifying account credentials registration IAT code...") - if len(opts.Code) < 45 { - logger.DebugContext(ctx, "Invalid account credentials registration IAT code length") - return AccountCredentialsDynamicRegistrationIATCodeData{}, false, nil - } - + // Codes are "{base62uuid}-{base62secret}". The secret is unpadded base62 of 16 + // bytes, so total length is often 44 or 45. Reject only structurally invalid values. parts := strings.Split(opts.Code, "-") - if len(parts) != 2 { + if len(parts) != 2 || parts[0] == "" || parts[1] == "" { logger.WarnContext(ctx, "Invalid account credentials registration IAT code format") return AccountCredentialsDynamicRegistrationIATCodeData{}, false, nil } @@ -572,6 +575,7 @@ func (c *Cache) VerifyAccountCredentialsRegistrationIATCode( } type AccountCredentialsDynamicRegistrationSessionData struct { + Username string `json:"username"` AccountPublicID uuid.UUID `json:"account_public_id"` AccountVersion int32 `json:"account_version"` SessionKey string `json:"session_key"` @@ -594,6 +598,7 @@ func parseSessionKey(sessionKey string) (string, string, bool) { } type CreateAccountCredentialsRegistrationSessionKeyOptions struct { + Username string RequestID string ClientID string Domain string @@ -623,6 +628,7 @@ func (c *Cache) CreateAccountCredentialsRegistrationSessionKey( } data := AccountCredentialsDynamicRegistrationSessionData{ + Username: opts.Username, AccountPublicID: opts.AccountPublicID, AccountVersion: opts.AccountVersion, SessionKey: utils.Sha256HashHex(sessionKey), diff --git a/idp/internal/providers/database/account_credentials.sql.go b/idp/internal/providers/database/account_credentials.sql.go index 4305423..bfe2b89 100644 --- a/idp/internal/providers/database/account_credentials.sql.go +++ b/idp/internal/providers/database/account_credentials.sql.go @@ -604,3 +604,174 @@ func (q *Queries) UpdateAccountCredentials(ctx context.Context, arg UpdateAccoun ) return i, err } + +const updateRegisteredAccountCredentials = `-- name: UpdateRegisteredAccountCredentials :one +UPDATE "account_credentials" SET + "domain" = $2, + "transport" = $3, + "redirect_uris" = $4, + "token_endpoint_auth_method" = $5, + "grant_types" = $6, + "response_types" = $7, + "client_name" = $8, + "client_uri" = $9, + "logo_uri" = $10, + "scopes" = $11, + "contacts" = $12, + "tos_uri" = $13, + "policy_uri" = $14, + "jwks_uri" = $15, + "jwks" = $16, + "software_id" = $17, + "software_version" = $18, + "sector_identifier_uri" = $19, + "subject_type" = $20, + "id_token_signed_response_alg" = $21, + "id_token_encrypted_response_alg" = $22, + "id_token_encrypted_response_enc" = $23, + "userinfo_signed_response_alg" = $24, + "userinfo_encrypted_response_alg" = $25, + "userinfo_encrypted_response_enc" = $26, + "request_object_signing_alg" = $27, + "request_object_encryption_alg" = $28, + "request_object_encryption_enc" = $29, + "token_endpoint_auth_signing_alg" = $30, + "default_max_age" = $31, + "require_auth_time" = $32, + "default_acr_values" = $33, + "initiate_login_uri" = $34, + "request_uris" = $35, + "access_token_signing_alg" = $36, + "version" = "version" + 1, + "updated_at" = now() +WHERE "id" = $1 +RETURNING id, account_id, account_public_id, domain, creation_method, transport, version, client_id, redirect_uris, token_endpoint_auth_method, grant_types, response_types, client_name, client_uri, logo_uri, scopes, contacts, tos_uri, policy_uri, jwks_uri, jwks, software_id, software_version, credentials_type, sector_identifier_uri, subject_type, id_token_signed_response_alg, id_token_encrypted_response_alg, id_token_encrypted_response_enc, userinfo_signed_response_alg, userinfo_encrypted_response_alg, userinfo_encrypted_response_enc, request_object_signing_alg, request_object_encryption_alg, request_object_encryption_enc, token_endpoint_auth_signing_alg, default_max_age, require_auth_time, default_acr_values, initiate_login_uri, request_uris, access_token_signing_alg, created_at, updated_at +` + +type UpdateRegisteredAccountCredentialsParams struct { + ID int32 + Domain string + Transport Transport + RedirectUris []string + TokenEndpointAuthMethod AuthMethod + GrantTypes []GrantType + ResponseTypes []ResponseType + ClientName string + ClientUri string + LogoUri pgtype.Text + Scopes []AccountCredentialsScope + Contacts []string + TosUri pgtype.Text + PolicyUri pgtype.Text + JwksUri pgtype.Text + Jwks []byte + SoftwareID pgtype.Text + SoftwareVersion pgtype.Text + SectorIdentifierUri pgtype.Text + SubjectType NullClientSubjectType + IDTokenSignedResponseAlg TokenCryptoSuite + IDTokenEncryptedResponseAlg NullTokenEncryptionAlgorithm + IDTokenEncryptedResponseEnc NullTokenEncryptionEncoding + UserinfoSignedResponseAlg NullTokenCryptoSuite + UserinfoEncryptedResponseAlg NullTokenEncryptionAlgorithm + UserinfoEncryptedResponseEnc NullTokenEncryptionEncoding + RequestObjectSigningAlg NullTokenCryptoSuite + RequestObjectEncryptionAlg NullTokenEncryptionAlgorithm + RequestObjectEncryptionEnc NullTokenEncryptionEncoding + TokenEndpointAuthSigningAlg NullTokenCryptoSuite + DefaultMaxAge pgtype.Int8 + RequireAuthTime bool + DefaultAcrValues []string + InitiateLoginUri pgtype.Text + RequestUris []string + AccessTokenSigningAlg TokenCryptoSuite +} + +func (q *Queries) UpdateRegisteredAccountCredentials(ctx context.Context, arg UpdateRegisteredAccountCredentialsParams) (AccountCredential, error) { + row := q.db.QueryRow(ctx, updateRegisteredAccountCredentials, + arg.ID, + arg.Domain, + arg.Transport, + arg.RedirectUris, + arg.TokenEndpointAuthMethod, + arg.GrantTypes, + arg.ResponseTypes, + arg.ClientName, + arg.ClientUri, + arg.LogoUri, + arg.Scopes, + arg.Contacts, + arg.TosUri, + arg.PolicyUri, + arg.JwksUri, + arg.Jwks, + arg.SoftwareID, + arg.SoftwareVersion, + arg.SectorIdentifierUri, + arg.SubjectType, + arg.IDTokenSignedResponseAlg, + arg.IDTokenEncryptedResponseAlg, + arg.IDTokenEncryptedResponseEnc, + arg.UserinfoSignedResponseAlg, + arg.UserinfoEncryptedResponseAlg, + arg.UserinfoEncryptedResponseEnc, + arg.RequestObjectSigningAlg, + arg.RequestObjectEncryptionAlg, + arg.RequestObjectEncryptionEnc, + arg.TokenEndpointAuthSigningAlg, + arg.DefaultMaxAge, + arg.RequireAuthTime, + arg.DefaultAcrValues, + arg.InitiateLoginUri, + arg.RequestUris, + arg.AccessTokenSigningAlg, + ) + var i AccountCredential + err := row.Scan( + &i.ID, + &i.AccountID, + &i.AccountPublicID, + &i.Domain, + &i.CreationMethod, + &i.Transport, + &i.Version, + &i.ClientID, + &i.RedirectUris, + &i.TokenEndpointAuthMethod, + &i.GrantTypes, + &i.ResponseTypes, + &i.ClientName, + &i.ClientUri, + &i.LogoUri, + &i.Scopes, + &i.Contacts, + &i.TosUri, + &i.PolicyUri, + &i.JwksUri, + &i.Jwks, + &i.SoftwareID, + &i.SoftwareVersion, + &i.CredentialsType, + &i.SectorIdentifierUri, + &i.SubjectType, + &i.IDTokenSignedResponseAlg, + &i.IDTokenEncryptedResponseAlg, + &i.IDTokenEncryptedResponseEnc, + &i.UserinfoSignedResponseAlg, + &i.UserinfoEncryptedResponseAlg, + &i.UserinfoEncryptedResponseEnc, + &i.RequestObjectSigningAlg, + &i.RequestObjectEncryptionAlg, + &i.RequestObjectEncryptionEnc, + &i.TokenEndpointAuthSigningAlg, + &i.DefaultMaxAge, + &i.RequireAuthTime, + &i.DefaultAcrValues, + &i.InitiateLoginUri, + &i.RequestUris, + &i.AccessTokenSigningAlg, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} diff --git a/idp/internal/providers/database/account_dynamic_registration_configs.sql.go b/idp/internal/providers/database/account_dynamic_registration_configs.sql.go index 23fc87a..4eed164 100644 --- a/idp/internal/providers/database/account_dynamic_registration_configs.sql.go +++ b/idp/internal/providers/database/account_dynamic_registration_configs.sql.go @@ -18,13 +18,19 @@ INSERT INTO "account_dynamic_registration_configs" ( "account_public_id", "account_credentials_types", "require_software_statement_credential_types", - "software_statement_verification_methods" + "software_statement_verification_methods", + "require_verified_domains_credentials_type", + "require_initial_access_token_credential_types", + "initial_access_token_generation_methods" ) VALUES ( $1, $2, $3, $4, - $5 + $5, + '{}'::account_credentials_type[], + '{}'::account_credentials_type[], + '{}'::initial_access_token_generation_method[] ) RETURNING id, account_id, account_public_id, account_credentials_types, require_software_statement_credential_types, software_statement_verification_methods, require_verified_domains_credentials_type, created_at, updated_at ` diff --git a/idp/internal/providers/database/dynamic_registration_domains.sql.go b/idp/internal/providers/database/dynamic_registration_domains.sql.go index 8b7d21a..a6f7512 100644 --- a/idp/internal/providers/database/dynamic_registration_domains.sql.go +++ b/idp/internal/providers/database/dynamic_registration_domains.sql.go @@ -41,7 +41,7 @@ SELECT COUNT(*) FROM "dynamic_registration_domains" WHERE "account_public_id" = $1 AND "domain" = $2 AND - "usages" = ANY($3) + "usages" && $3::dynamic_registration_usage[] LIMIT 1 ` @@ -62,7 +62,7 @@ const countDynamicRegistrationDomainsByDomainAndUsages = `-- name: CountDynamicR SELECT COUNT(*) FROM "dynamic_registration_domains" WHERE "domain" = $1 AND - "usages" = ANY($2) + "usages" && $2::dynamic_registration_usage[] LIMIT 1 ` @@ -95,7 +95,7 @@ const countDynamicRegistrationDomainsByDomainsAccountPublicIDAndUsages = `-- nam SELECT COUNT(*) FROM "dynamic_registration_domains" WHERE "account_public_id" = $1 AND - "usages" = ANY($2) AND + "usages" && $2::dynamic_registration_usage[] AND "domain" IN ($3) LIMIT 1 ` @@ -116,18 +116,18 @@ func (q *Queries) CountDynamicRegistrationDomainsByDomainsAccountPublicIDAndUsag const countDynamicRegistrationDomainsByDomainsAndUsages = `-- name: CountDynamicRegistrationDomainsByDomainsAndUsages :one SELECT COUNT(*) FROM "dynamic_registration_domains" WHERE - "domain" IN ($2) AND - "usages" = ANY($1) + "domain" IN ($1) AND + "usages" && $2::dynamic_registration_usage[] LIMIT 1 ` type CountDynamicRegistrationDomainsByDomainsAndUsagesParams struct { - Usages []DynamicRegistrationUsage Domains []string + Usages []DynamicRegistrationUsage } func (q *Queries) CountDynamicRegistrationDomainsByDomainsAndUsages(ctx context.Context, arg CountDynamicRegistrationDomainsByDomainsAndUsagesParams) (int64, error) { - row := q.db.QueryRow(ctx, countDynamicRegistrationDomainsByDomainsAndUsages, arg.Usages, arg.Domains) + row := q.db.QueryRow(ctx, countDynamicRegistrationDomainsByDomainsAndUsages, arg.Domains, arg.Usages) var count int64 err := row.Scan(&count) return count, err @@ -171,7 +171,7 @@ SELECT COUNT(*) FROM "dynamic_registration_domains" WHERE "account_public_id" = $1 AND "domain" = $2 AND - "usages" = ANY($3) AND + "usages" && $3::dynamic_registration_usage[] AND "verified_at" IS NOT NULL LIMIT 1 ` diff --git a/idp/internal/providers/database/dynamic_registration_software_statement_keys.sql.go b/idp/internal/providers/database/dynamic_registration_software_statement_keys.sql.go index e242434..f48cb5b 100644 --- a/idp/internal/providers/database/dynamic_registration_software_statement_keys.sql.go +++ b/idp/internal/providers/database/dynamic_registration_software_statement_keys.sql.go @@ -40,7 +40,7 @@ func (q *Queries) FindDynamicRegistrationSoftwareStatementKeysByCredentialsKeyKI const findDynamicRegistrationSoftwareStatementKeysByRootDomainAndAccountPublicID = `-- name: FindDynamicRegistrationSoftwareStatementKeysByRootDomainAndAccountPublicID :one SELECT c.id, c.public_kid, c.public_key, c.crypto_suite, c.is_revoked, c.is_external, c.usage, c.account_id, c.expires_at, c.created_at, c.updated_at FROM "credentials_keys" AS "c" -LEFT JOIN "dynamic_registration_software_statement_keys" AS "d" ON "c"."id" = "d"."credential_key_id" +LEFT JOIN "dynamic_registration_software_statement_keys" AS "d" ON "c"."id" = "d"."credentials_key_id" WHERE "d"."root_domain" = $1 AND "d"."account_public_id" = $2 LIMIT 1 ` diff --git a/idp/internal/providers/database/queries/account_credentials.sql b/idp/internal/providers/database/queries/account_credentials.sql index bb3451e..f1ca418 100644 --- a/idp/internal/providers/database/queries/account_credentials.sql +++ b/idp/internal/providers/database/queries/account_credentials.sql @@ -104,6 +104,48 @@ INSERT INTO "account_credentials" ( $40 ) RETURNING *; +-- name: UpdateRegisteredAccountCredentials :one +UPDATE "account_credentials" SET + "domain" = $2, + "transport" = $3, + "redirect_uris" = $4, + "token_endpoint_auth_method" = $5, + "grant_types" = $6, + "response_types" = $7, + "client_name" = $8, + "client_uri" = $9, + "logo_uri" = $10, + "scopes" = $11, + "contacts" = $12, + "tos_uri" = $13, + "policy_uri" = $14, + "jwks_uri" = $15, + "jwks" = $16, + "software_id" = $17, + "software_version" = $18, + "sector_identifier_uri" = $19, + "subject_type" = $20, + "id_token_signed_response_alg" = $21, + "id_token_encrypted_response_alg" = $22, + "id_token_encrypted_response_enc" = $23, + "userinfo_signed_response_alg" = $24, + "userinfo_encrypted_response_alg" = $25, + "userinfo_encrypted_response_enc" = $26, + "request_object_signing_alg" = $27, + "request_object_encryption_alg" = $28, + "request_object_encryption_enc" = $29, + "token_endpoint_auth_signing_alg" = $30, + "default_max_age" = $31, + "require_auth_time" = $32, + "default_acr_values" = $33, + "initiate_login_uri" = $34, + "request_uris" = $35, + "access_token_signing_alg" = $36, + "version" = "version" + 1, + "updated_at" = now() +WHERE "id" = $1 +RETURNING *; + -- name: UpdateAccountCredentials :one UPDATE "account_credentials" SET "scopes" = $2, diff --git a/idp/internal/providers/database/queries/account_dynamic_registration_configs.sql b/idp/internal/providers/database/queries/account_dynamic_registration_configs.sql index 683c1b6..07903fc 100644 --- a/idp/internal/providers/database/queries/account_dynamic_registration_configs.sql +++ b/idp/internal/providers/database/queries/account_dynamic_registration_configs.sql @@ -10,13 +10,19 @@ INSERT INTO "account_dynamic_registration_configs" ( "account_public_id", "account_credentials_types", "require_software_statement_credential_types", - "software_statement_verification_methods" + "software_statement_verification_methods", + "require_verified_domains_credentials_type", + "require_initial_access_token_credential_types", + "initial_access_token_generation_methods" ) VALUES ( $1, $2, $3, $4, - $5 + $5, + '{}'::account_credentials_type[], + '{}'::account_credentials_type[], + '{}'::initial_access_token_generation_method[] ) RETURNING *; -- name: UpdateAccountDynamicRegistrationConfig :one diff --git a/idp/internal/providers/database/queries/dynamic_registration_domains.sql b/idp/internal/providers/database/queries/dynamic_registration_domains.sql index e8933aa..668a8d0 100644 --- a/idp/internal/providers/database/queries/dynamic_registration_domains.sql +++ b/idp/internal/providers/database/queries/dynamic_registration_domains.sql @@ -118,7 +118,7 @@ SELECT COUNT(*) FROM "dynamic_registration_domains" WHERE "account_public_id" = $1 AND "domain" = $2 AND - "usages" = ANY($3) AND + "usages" && sqlc.arg(usages)::dynamic_registration_usage[] AND "verified_at" IS NOT NULL LIMIT 1; @@ -126,7 +126,7 @@ LIMIT 1; SELECT COUNT(*) FROM "dynamic_registration_domains" WHERE "account_public_id" = $1 AND - "usages" = ANY($2) AND + "usages" && sqlc.arg(usages)::dynamic_registration_usage[] AND "domain" IN (sqlc.slice('domains')) LIMIT 1; @@ -135,21 +135,21 @@ SELECT COUNT(*) FROM "dynamic_registration_domains" WHERE "account_public_id" = $1 AND "domain" = $2 AND - "usages" = ANY($3) + "usages" && sqlc.arg(usages)::dynamic_registration_usage[] LIMIT 1; -- name: CountDynamicRegistrationDomainsByDomainAndUsages :one SELECT COUNT(*) FROM "dynamic_registration_domains" WHERE "domain" = $1 AND - "usages" = ANY($2) + "usages" && sqlc.arg(usages)::dynamic_registration_usage[] LIMIT 1; -- name: CountDynamicRegistrationDomainsByDomainsAndUsages :one SELECT COUNT(*) FROM "dynamic_registration_domains" WHERE "domain" IN (sqlc.slice('domains')) AND - "usages" = ANY($1) + "usages" && sqlc.arg(usages)::dynamic_registration_usage[] LIMIT 1; -- name: DeleteDynamicRegistrationDomain :exec diff --git a/idp/internal/providers/database/queries/dynamic_registration_software_statement_keys.sql b/idp/internal/providers/database/queries/dynamic_registration_software_statement_keys.sql index 7342eed..bddec12 100644 --- a/idp/internal/providers/database/queries/dynamic_registration_software_statement_keys.sql +++ b/idp/internal/providers/database/queries/dynamic_registration_software_statement_keys.sql @@ -6,7 +6,7 @@ -- name: FindDynamicRegistrationSoftwareStatementKeysByRootDomainAndAccountPublicID :one SELECT "c".* FROM "credentials_keys" AS "c" -LEFT JOIN "dynamic_registration_software_statement_keys" AS "d" ON "c"."id" = "d"."credential_key_id" +LEFT JOIN "dynamic_registration_software_statement_keys" AS "d" ON "c"."id" = "d"."credentials_key_id" WHERE "d"."root_domain" = $1 AND "d"."account_public_id" = $2 LIMIT 1; diff --git a/idp/internal/providers/database/queries/registered_apps.sql b/idp/internal/providers/database/queries/registered_apps.sql new file mode 100644 index 0000000..ceee5c9 --- /dev/null +++ b/idp/internal/providers/database/queries/registered_apps.sql @@ -0,0 +1,144 @@ +-- name: CreateRegisteredApp :one +INSERT INTO "apps" ( + "account_id", + "account_public_id", + "app_type", + "client_name", + "client_id", + "client_uri", + "username_column", + "token_endpoint_auth_method", + "creation_method", + "grant_types", + "logo_uri", + "tos_uri", + "policy_uri", + "contacts", + "software_id", + "software_version", + "scopes", + "default_scopes", + "custom_scopes", + "default_custom_scopes", + "domain", + "transport", + "redirect_uris", + "response_types", + "allow_user_registration", + "auth_providers", + "jwks_uri", + "jwks", + "sector_identifier_uri", + "subject_type", + "id_token_signed_response_alg", + "id_token_encrypted_response_alg", + "id_token_encrypted_response_enc", + "userinfo_signed_response_alg", + "userinfo_encrypted_response_alg", + "userinfo_encrypted_response_enc", + "request_object_signing_alg", + "request_object_encryption_alg", + "request_object_encryption_enc", + "token_endpoint_auth_signing_alg", + "default_max_age", + "require_auth_time", + "default_acr_values", + "initiate_login_uri", + "request_uris", + "access_token_signing_alg" +) VALUES ( + $1, + $2, + $3, + $4, + $5, + $6, + $7, + $8, + $9, + $10, + $11, + $12, + $13, + $14, + $15, + $16, + $17, + $18, + $19, + $20, + $21, + $22, + $23, + $24, + $25, + $26, + $27, + $28, + $29, + $30, + $31, + $32, + $33, + $34, + $35, + $36, + $37, + $38, + $39, + $40, + $41, + $42, + $43, + $44, + $45, + $46 +) RETURNING *; + +-- name: UpdateRegisteredApp :one +UPDATE "apps" SET + "client_name" = $2, + "client_uri" = $3, + "username_column" = $4, + "token_endpoint_auth_method" = $5, + "grant_types" = $6, + "logo_uri" = $7, + "tos_uri" = $8, + "policy_uri" = $9, + "contacts" = $10, + "software_id" = $11, + "software_version" = $12, + "scopes" = $13, + "default_scopes" = $14, + "custom_scopes" = $15, + "default_custom_scopes" = $16, + "domain" = $17, + "transport" = $18, + "redirect_uris" = $19, + "response_types" = $20, + "allow_user_registration" = $21, + "auth_providers" = $22, + "jwks_uri" = $23, + "jwks" = $24, + "sector_identifier_uri" = $25, + "subject_type" = $26, + "id_token_signed_response_alg" = $27, + "id_token_encrypted_response_alg" = $28, + "id_token_encrypted_response_enc" = $29, + "userinfo_signed_response_alg" = $30, + "userinfo_encrypted_response_alg" = $31, + "userinfo_encrypted_response_enc" = $32, + "request_object_signing_alg" = $33, + "request_object_encryption_alg" = $34, + "request_object_encryption_enc" = $35, + "token_endpoint_auth_signing_alg" = $36, + "default_max_age" = $37, + "require_auth_time" = $38, + "default_acr_values" = $39, + "initiate_login_uri" = $40, + "request_uris" = $41, + "access_token_signing_alg" = $42, + "version" = "version" + 1, + "updated_at" = now() +WHERE "id" = $1 +RETURNING *; diff --git a/idp/internal/providers/database/registered_apps.sql.go b/idp/internal/providers/database/registered_apps.sql.go new file mode 100644 index 0000000..e0e01ff --- /dev/null +++ b/idp/internal/providers/database/registered_apps.sql.go @@ -0,0 +1,466 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 +// source: registered_apps.sql + +package database + +import ( + "context" + + "github.com/google/uuid" + "github.com/jackc/pgx/v5/pgtype" +) + +const createRegisteredApp = `-- name: CreateRegisteredApp :one +INSERT INTO "apps" ( + "account_id", + "account_public_id", + "app_type", + "client_name", + "client_id", + "client_uri", + "username_column", + "token_endpoint_auth_method", + "creation_method", + "grant_types", + "logo_uri", + "tos_uri", + "policy_uri", + "contacts", + "software_id", + "software_version", + "scopes", + "default_scopes", + "custom_scopes", + "default_custom_scopes", + "domain", + "transport", + "redirect_uris", + "response_types", + "allow_user_registration", + "auth_providers", + "jwks_uri", + "jwks", + "sector_identifier_uri", + "subject_type", + "id_token_signed_response_alg", + "id_token_encrypted_response_alg", + "id_token_encrypted_response_enc", + "userinfo_signed_response_alg", + "userinfo_encrypted_response_alg", + "userinfo_encrypted_response_enc", + "request_object_signing_alg", + "request_object_encryption_alg", + "request_object_encryption_enc", + "token_endpoint_auth_signing_alg", + "default_max_age", + "require_auth_time", + "default_acr_values", + "initiate_login_uri", + "request_uris", + "access_token_signing_alg" +) VALUES ( + $1, + $2, + $3, + $4, + $5, + $6, + $7, + $8, + $9, + $10, + $11, + $12, + $13, + $14, + $15, + $16, + $17, + $18, + $19, + $20, + $21, + $22, + $23, + $24, + $25, + $26, + $27, + $28, + $29, + $30, + $31, + $32, + $33, + $34, + $35, + $36, + $37, + $38, + $39, + $40, + $41, + $42, + $43, + $44, + $45, + $46 +) RETURNING id, account_id, account_public_id, client_id, version, creation_method, redirect_uris, token_endpoint_auth_method, grant_types, response_types, client_name, client_uri, logo_uri, scopes, custom_scopes, contacts, tos_uri, policy_uri, jwks_uri, jwks, software_id, software_version, domain, transport, allow_user_registration, auth_providers, username_column, default_scopes, default_custom_scopes, app_type, sector_identifier_uri, subject_type, id_token_signed_response_alg, id_token_encrypted_response_alg, id_token_encrypted_response_enc, userinfo_signed_response_alg, userinfo_encrypted_response_alg, userinfo_encrypted_response_enc, request_object_signing_alg, request_object_encryption_alg, request_object_encryption_enc, token_endpoint_auth_signing_alg, default_max_age, require_auth_time, default_acr_values, initiate_login_uri, request_uris, access_token_signing_alg, id_token_ttl, token_ttl, refresh_token_ttl, created_at, updated_at +` + +type CreateRegisteredAppParams struct { + AccountID int32 + AccountPublicID uuid.UUID + AppType AppType + ClientName string + ClientID string + ClientUri string + UsernameColumn AppUsernameColumn + TokenEndpointAuthMethod AuthMethod + CreationMethod CreationMethod + GrantTypes []GrantType + LogoUri pgtype.Text + TosUri pgtype.Text + PolicyUri pgtype.Text + Contacts []string + SoftwareID pgtype.Text + SoftwareVersion pgtype.Text + Scopes []Scopes + DefaultScopes []Scopes + CustomScopes []string + DefaultCustomScopes []string + Domain string + Transport Transport + RedirectUris []string + ResponseTypes []ResponseType + AllowUserRegistration bool + AuthProviders []AuthProvider + JwksUri pgtype.Text + Jwks []byte + SectorIdentifierUri pgtype.Text + SubjectType NullClientSubjectType + IDTokenSignedResponseAlg TokenCryptoSuite + IDTokenEncryptedResponseAlg NullTokenEncryptionAlgorithm + IDTokenEncryptedResponseEnc NullTokenEncryptionEncoding + UserinfoSignedResponseAlg NullTokenCryptoSuite + UserinfoEncryptedResponseAlg NullTokenEncryptionAlgorithm + UserinfoEncryptedResponseEnc NullTokenEncryptionEncoding + RequestObjectSigningAlg NullTokenCryptoSuite + RequestObjectEncryptionAlg NullTokenEncryptionAlgorithm + RequestObjectEncryptionEnc NullTokenEncryptionEncoding + TokenEndpointAuthSigningAlg NullTokenCryptoSuite + DefaultMaxAge pgtype.Int4 + RequireAuthTime bool + DefaultAcrValues []string + InitiateLoginUri pgtype.Text + RequestUris []string + AccessTokenSigningAlg TokenCryptoSuite +} + +func (q *Queries) CreateRegisteredApp(ctx context.Context, arg CreateRegisteredAppParams) (App, error) { + row := q.db.QueryRow(ctx, createRegisteredApp, + arg.AccountID, + arg.AccountPublicID, + arg.AppType, + arg.ClientName, + arg.ClientID, + arg.ClientUri, + arg.UsernameColumn, + arg.TokenEndpointAuthMethod, + arg.CreationMethod, + arg.GrantTypes, + arg.LogoUri, + arg.TosUri, + arg.PolicyUri, + arg.Contacts, + arg.SoftwareID, + arg.SoftwareVersion, + arg.Scopes, + arg.DefaultScopes, + arg.CustomScopes, + arg.DefaultCustomScopes, + arg.Domain, + arg.Transport, + arg.RedirectUris, + arg.ResponseTypes, + arg.AllowUserRegistration, + arg.AuthProviders, + arg.JwksUri, + arg.Jwks, + arg.SectorIdentifierUri, + arg.SubjectType, + arg.IDTokenSignedResponseAlg, + arg.IDTokenEncryptedResponseAlg, + arg.IDTokenEncryptedResponseEnc, + arg.UserinfoSignedResponseAlg, + arg.UserinfoEncryptedResponseAlg, + arg.UserinfoEncryptedResponseEnc, + arg.RequestObjectSigningAlg, + arg.RequestObjectEncryptionAlg, + arg.RequestObjectEncryptionEnc, + arg.TokenEndpointAuthSigningAlg, + arg.DefaultMaxAge, + arg.RequireAuthTime, + arg.DefaultAcrValues, + arg.InitiateLoginUri, + arg.RequestUris, + arg.AccessTokenSigningAlg, + ) + var i App + err := row.Scan( + &i.ID, + &i.AccountID, + &i.AccountPublicID, + &i.ClientID, + &i.Version, + &i.CreationMethod, + &i.RedirectUris, + &i.TokenEndpointAuthMethod, + &i.GrantTypes, + &i.ResponseTypes, + &i.ClientName, + &i.ClientUri, + &i.LogoUri, + &i.Scopes, + &i.CustomScopes, + &i.Contacts, + &i.TosUri, + &i.PolicyUri, + &i.JwksUri, + &i.Jwks, + &i.SoftwareID, + &i.SoftwareVersion, + &i.Domain, + &i.Transport, + &i.AllowUserRegistration, + &i.AuthProviders, + &i.UsernameColumn, + &i.DefaultScopes, + &i.DefaultCustomScopes, + &i.AppType, + &i.SectorIdentifierUri, + &i.SubjectType, + &i.IDTokenSignedResponseAlg, + &i.IDTokenEncryptedResponseAlg, + &i.IDTokenEncryptedResponseEnc, + &i.UserinfoSignedResponseAlg, + &i.UserinfoEncryptedResponseAlg, + &i.UserinfoEncryptedResponseEnc, + &i.RequestObjectSigningAlg, + &i.RequestObjectEncryptionAlg, + &i.RequestObjectEncryptionEnc, + &i.TokenEndpointAuthSigningAlg, + &i.DefaultMaxAge, + &i.RequireAuthTime, + &i.DefaultAcrValues, + &i.InitiateLoginUri, + &i.RequestUris, + &i.AccessTokenSigningAlg, + &i.IDTokenTtl, + &i.TokenTtl, + &i.RefreshTokenTtl, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const updateRegisteredApp = `-- name: UpdateRegisteredApp :one +UPDATE "apps" SET + "client_name" = $2, + "client_uri" = $3, + "username_column" = $4, + "token_endpoint_auth_method" = $5, + "grant_types" = $6, + "logo_uri" = $7, + "tos_uri" = $8, + "policy_uri" = $9, + "contacts" = $10, + "software_id" = $11, + "software_version" = $12, + "scopes" = $13, + "default_scopes" = $14, + "custom_scopes" = $15, + "default_custom_scopes" = $16, + "domain" = $17, + "transport" = $18, + "redirect_uris" = $19, + "response_types" = $20, + "allow_user_registration" = $21, + "auth_providers" = $22, + "jwks_uri" = $23, + "jwks" = $24, + "sector_identifier_uri" = $25, + "subject_type" = $26, + "id_token_signed_response_alg" = $27, + "id_token_encrypted_response_alg" = $28, + "id_token_encrypted_response_enc" = $29, + "userinfo_signed_response_alg" = $30, + "userinfo_encrypted_response_alg" = $31, + "userinfo_encrypted_response_enc" = $32, + "request_object_signing_alg" = $33, + "request_object_encryption_alg" = $34, + "request_object_encryption_enc" = $35, + "token_endpoint_auth_signing_alg" = $36, + "default_max_age" = $37, + "require_auth_time" = $38, + "default_acr_values" = $39, + "initiate_login_uri" = $40, + "request_uris" = $41, + "access_token_signing_alg" = $42, + "version" = "version" + 1, + "updated_at" = now() +WHERE "id" = $1 +RETURNING id, account_id, account_public_id, client_id, version, creation_method, redirect_uris, token_endpoint_auth_method, grant_types, response_types, client_name, client_uri, logo_uri, scopes, custom_scopes, contacts, tos_uri, policy_uri, jwks_uri, jwks, software_id, software_version, domain, transport, allow_user_registration, auth_providers, username_column, default_scopes, default_custom_scopes, app_type, sector_identifier_uri, subject_type, id_token_signed_response_alg, id_token_encrypted_response_alg, id_token_encrypted_response_enc, userinfo_signed_response_alg, userinfo_encrypted_response_alg, userinfo_encrypted_response_enc, request_object_signing_alg, request_object_encryption_alg, request_object_encryption_enc, token_endpoint_auth_signing_alg, default_max_age, require_auth_time, default_acr_values, initiate_login_uri, request_uris, access_token_signing_alg, id_token_ttl, token_ttl, refresh_token_ttl, created_at, updated_at +` + +type UpdateRegisteredAppParams struct { + ID int32 + ClientName string + ClientUri string + UsernameColumn AppUsernameColumn + TokenEndpointAuthMethod AuthMethod + GrantTypes []GrantType + LogoUri pgtype.Text + TosUri pgtype.Text + PolicyUri pgtype.Text + Contacts []string + SoftwareID pgtype.Text + SoftwareVersion pgtype.Text + Scopes []Scopes + DefaultScopes []Scopes + CustomScopes []string + DefaultCustomScopes []string + Domain string + Transport Transport + RedirectUris []string + ResponseTypes []ResponseType + AllowUserRegistration bool + AuthProviders []AuthProvider + JwksUri pgtype.Text + Jwks []byte + SectorIdentifierUri pgtype.Text + SubjectType NullClientSubjectType + IDTokenSignedResponseAlg TokenCryptoSuite + IDTokenEncryptedResponseAlg NullTokenEncryptionAlgorithm + IDTokenEncryptedResponseEnc NullTokenEncryptionEncoding + UserinfoSignedResponseAlg NullTokenCryptoSuite + UserinfoEncryptedResponseAlg NullTokenEncryptionAlgorithm + UserinfoEncryptedResponseEnc NullTokenEncryptionEncoding + RequestObjectSigningAlg NullTokenCryptoSuite + RequestObjectEncryptionAlg NullTokenEncryptionAlgorithm + RequestObjectEncryptionEnc NullTokenEncryptionEncoding + TokenEndpointAuthSigningAlg NullTokenCryptoSuite + DefaultMaxAge pgtype.Int4 + RequireAuthTime bool + DefaultAcrValues []string + InitiateLoginUri pgtype.Text + RequestUris []string + AccessTokenSigningAlg TokenCryptoSuite +} + +func (q *Queries) UpdateRegisteredApp(ctx context.Context, arg UpdateRegisteredAppParams) (App, error) { + row := q.db.QueryRow(ctx, updateRegisteredApp, + arg.ID, + arg.ClientName, + arg.ClientUri, + arg.UsernameColumn, + arg.TokenEndpointAuthMethod, + arg.GrantTypes, + arg.LogoUri, + arg.TosUri, + arg.PolicyUri, + arg.Contacts, + arg.SoftwareID, + arg.SoftwareVersion, + arg.Scopes, + arg.DefaultScopes, + arg.CustomScopes, + arg.DefaultCustomScopes, + arg.Domain, + arg.Transport, + arg.RedirectUris, + arg.ResponseTypes, + arg.AllowUserRegistration, + arg.AuthProviders, + arg.JwksUri, + arg.Jwks, + arg.SectorIdentifierUri, + arg.SubjectType, + arg.IDTokenSignedResponseAlg, + arg.IDTokenEncryptedResponseAlg, + arg.IDTokenEncryptedResponseEnc, + arg.UserinfoSignedResponseAlg, + arg.UserinfoEncryptedResponseAlg, + arg.UserinfoEncryptedResponseEnc, + arg.RequestObjectSigningAlg, + arg.RequestObjectEncryptionAlg, + arg.RequestObjectEncryptionEnc, + arg.TokenEndpointAuthSigningAlg, + arg.DefaultMaxAge, + arg.RequireAuthTime, + arg.DefaultAcrValues, + arg.InitiateLoginUri, + arg.RequestUris, + arg.AccessTokenSigningAlg, + ) + var i App + err := row.Scan( + &i.ID, + &i.AccountID, + &i.AccountPublicID, + &i.ClientID, + &i.Version, + &i.CreationMethod, + &i.RedirectUris, + &i.TokenEndpointAuthMethod, + &i.GrantTypes, + &i.ResponseTypes, + &i.ClientName, + &i.ClientUri, + &i.LogoUri, + &i.Scopes, + &i.CustomScopes, + &i.Contacts, + &i.TosUri, + &i.PolicyUri, + &i.JwksUri, + &i.Jwks, + &i.SoftwareID, + &i.SoftwareVersion, + &i.Domain, + &i.Transport, + &i.AllowUserRegistration, + &i.AuthProviders, + &i.UsernameColumn, + &i.DefaultScopes, + &i.DefaultCustomScopes, + &i.AppType, + &i.SectorIdentifierUri, + &i.SubjectType, + &i.IDTokenSignedResponseAlg, + &i.IDTokenEncryptedResponseAlg, + &i.IDTokenEncryptedResponseEnc, + &i.UserinfoSignedResponseAlg, + &i.UserinfoEncryptedResponseAlg, + &i.UserinfoEncryptedResponseEnc, + &i.RequestObjectSigningAlg, + &i.RequestObjectEncryptionAlg, + &i.RequestObjectEncryptionEnc, + &i.TokenEndpointAuthSigningAlg, + &i.DefaultMaxAge, + &i.RequireAuthTime, + &i.DefaultAcrValues, + &i.InitiateLoginUri, + &i.RequestUris, + &i.AccessTokenSigningAlg, + &i.IDTokenTtl, + &i.TokenTtl, + &i.RefreshTokenTtl, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} diff --git a/idp/internal/providers/tokens/dynamic_registration_iat.go b/idp/internal/providers/tokens/dynamic_registration_iat.go index 0b706db..34e053d 100644 --- a/idp/internal/providers/tokens/dynamic_registration_iat.go +++ b/idp/internal/providers/tokens/dynamic_registration_iat.go @@ -10,7 +10,6 @@ import ( "context" "errors" "fmt" - "net/url" "time" "github.com/golang-jwt/jwt/v5" @@ -20,8 +19,26 @@ import ( const dynamicRegistrationIATLocation = "dynamic_registration_iat" -type accountCredentialsDynamicRegistrationClaims struct { +const registrationAccessTokenTTLSeconds int64 = 10 * 365 * 24 * 60 * 60 + +type DynamicRegistrationUsage string + +const ( + DynamicRegistrationUsageAccount DynamicRegistrationUsage = "account" + DynamicRegistrationUsageApp DynamicRegistrationUsage = "app" +) + +type DynamicRegistrationTokenUse string + +const ( + DynamicRegistrationTokenUseInitialAccess DynamicRegistrationTokenUse = "initial_access" + DynamicRegistrationTokenUseRegistration DynamicRegistrationTokenUse = "registration" +) + +type dynamicRegistrationTokenClaims struct { AccountClaims + Usage DynamicRegistrationUsage `json:"usage"` + TokenUse DynamicRegistrationTokenUse `json:"token_use"` jwt.RegisteredClaims } @@ -29,41 +46,62 @@ type DynamicRegistrationIATOptions struct { AccountPublicID uuid.UUID AccountVersion int32 IssuerDomain string - Domain string - ClientID string + Subject string + JTI string + Usage DynamicRegistrationUsage + TokenUse DynamicRegistrationTokenUse + TTL int64 } func (t *Tokens) DynamicRegistrationIAT( opts DynamicRegistrationIATOptions, ) *jwt.Token { + if opts.TokenUse == "" { + opts.TokenUse = DynamicRegistrationTokenUseInitialAccess + } + if opts.TTL == 0 { + opts.TTL = t.dynamicRegistrationTTL + } now := time.Now() iat := jwt.NewNumericDate(now) - exp := jwt.NewNumericDate(now.Add(time.Second * time.Duration(t.dynamicRegistrationTTL))) + exp := jwt.NewNumericDate(now.Add(time.Second * time.Duration(opts.TTL))) iss := fmt.Sprintf("https://%s", opts.IssuerDomain) return jwt.NewWithClaims( jwt.SigningMethodEdDSA, - accountCredentialsDynamicRegistrationClaims{ + dynamicRegistrationTokenClaims{ AccountClaims: AccountClaims{ AccountID: opts.AccountPublicID, AccountVersion: opts.AccountVersion, }, + Usage: opts.Usage, + TokenUse: opts.TokenUse, RegisteredClaims: jwt.RegisteredClaims{ Issuer: iss, Audience: []string{iss}, - Subject: opts.Domain, + Subject: opts.Subject, IssuedAt: iat, NotBefore: iat, ExpiresAt: exp, - ID: opts.ClientID, + ID: opts.JTI, }, }, ) } +func (t *Tokens) DynamicRegistrationAccessToken(opts DynamicRegistrationIATOptions) *jwt.Token { + opts.TokenUse = DynamicRegistrationTokenUseRegistration + if opts.TTL == 0 { + opts.TTL = registrationAccessTokenTTLSeconds + } + return t.DynamicRegistrationIAT(opts) +} + type VerifyDynamicRegistrationIATOptions struct { RequestID string IAT string IssuerDomain string + Usage DynamicRegistrationUsage + TokenUse DynamicRegistrationTokenUse GetPublicJWK GetPublicJWK } @@ -76,57 +114,53 @@ func (t *Tokens) VerifyDynamicRegistrationIAT( Method: "VerifyDynamicRegistrationIAT", RequestID: opts.RequestID, }) - logger.DebugContext(ctx, "Verifying account credentials dynamic registration IAT...") + logger.DebugContext(ctx, "Verifying dynamic registration token...") + + if opts.TokenUse == "" { + opts.TokenUse = DynamicRegistrationTokenUseInitialAccess + } - claims := new(accountCredentialsDynamicRegistrationClaims) + claims := new(dynamicRegistrationTokenClaims) if _, err := jwt.ParseWithClaims(opts.IAT, claims, func(token *jwt.Token) (interface{}, error) { kid, err := extractTokenKID(token) if err != nil { - logger.DebugContext(ctx, "Failed to extract KID from account credentials dynamic registration IAT", "error", err) + logger.DebugContext(ctx, "Failed to extract KID from dynamic registration token", "error", err) return nil, err } jwk, err := opts.GetPublicJWK(kid, utils.SupportedCryptoSuiteEd25519) if err != nil { - logger.WarnContext(ctx, "Failed to get public JWK for account credentials dynamic registration IAT", "error", err, "kid", kid) + logger.WarnContext(ctx, "Failed to get public JWK for dynamic registration token", "error", err, "kid", kid) return nil, err } return jwk.ToUsableKey() - }); err != nil { - logger.WarnContext(ctx, "Failed to verify account credentials dynamic registration IAT", "error", err) + }, jwt.WithValidMethods([]string{jwt.SigningMethodEdDSA.Alg()}), + jwt.WithIssuer("https://"+opts.IssuerDomain), + jwt.WithAudience("https://"+opts.IssuerDomain), + jwt.WithExpirationRequired(), jwt.WithIssuedAt()); err != nil { + logger.WarnContext(ctx, "Failed to verify dynamic registration token", "error", err) return "", AccountClaims{}, err } - issDomain, err := url.Parse(claims.Issuer) - if err != nil { - logger.WarnContext(ctx, "Failed to parse issuer from account credentials dynamic registration IAT", "error", err, "issuer", claims.Issuer) - return "", AccountClaims{}, err + if claims.Subject == "" || claims.ID == "" || claims.AccountID == uuid.Nil { + return "", AccountClaims{}, errors.New("missing registration token binding") } - if issDomain.Host != opts.IssuerDomain { - logger.WarnContext(ctx, "Issuer domain mismatch in account credentials dynamic registration IAT", "expected", opts.IssuerDomain, "actual", issDomain.Host) - return "", AccountClaims{}, errors.New("issuer domain mismatch") + if claims.Usage != opts.Usage { + return "", AccountClaims{}, errors.New("registration token usage mismatch") } - - if len(claims.Audience) == 0 { - logger.WarnContext(ctx, "Missing audience in account credentials dynamic registration IAT") - return "", AccountClaims{}, errors.New("missing audience") + if claims.TokenUse != opts.TokenUse { + return "", AccountClaims{}, errors.New("registration token use mismatch") } - audDomain, err := url.Parse(claims.Audience[0]) - if err != nil { - logger.WarnContext(ctx, "Failed to parse audience from account credentials dynamic registration IAT", "error", err, "audience", claims.Audience[0]) - return "", AccountClaims{}, err - } - if audDomain.Host != opts.IssuerDomain { - logger.WarnContext(ctx, "Audience domain mismatch in account credentials dynamic registration IAT", "expected", opts.IssuerDomain, "actual", audDomain.Host) - return "", AccountClaims{}, errors.New("audience domain mismatch") - } - - logger.InfoContext(ctx, "Verified account credentials dynamic registration IAT successfully") + logger.InfoContext(ctx, "Verified dynamic registration token successfully") return claims.Subject, claims.AccountClaims, nil } func (t *Tokens) GetDynamicRegistrationTTL() int64 { return t.dynamicRegistrationTTL } + +func (t *Tokens) GetRegistrationAccessTokenTTL() int64 { + return registrationAccessTokenTTLSeconds +} diff --git a/idp/internal/providers/tokens/dynamic_registration_iat_test.go b/idp/internal/providers/tokens/dynamic_registration_iat_test.go new file mode 100644 index 0000000..1433cf6 --- /dev/null +++ b/idp/internal/providers/tokens/dynamic_registration_iat_test.go @@ -0,0 +1,87 @@ +package tokens + +import ( + "context" + "crypto/ed25519" + "crypto/rand" + "io" + "log/slog" + "testing" + "time" + + "github.com/golang-jwt/jwt/v5" + "github.com/google/uuid" + "github.com/tugascript/devlogs/idp/internal/utils" +) + +func TestDynamicRegistrationIATBindings(t *testing.T) { + public, private, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + t.Fatal(err) + } + jwk := utils.EncodeEd25519Jwk(public, "test") + provider := &Tokens{logger: slog.New(slog.NewTextHandler(io.Discard, nil)), dynamicRegistrationTTL: 300} + accountID := uuid.New() + for _, tc := range []struct { + name, issuer, verifier string + usage DynamicRegistrationUsage + verifyUsage DynamicRegistrationUsage + tokenUse DynamicRegistrationTokenUse + verifyUse DynamicRegistrationTokenUse + change func(*dynamicRegistrationTokenClaims) + wantError bool + }{ + {name: "account credentials", issuer: "id.example.com", verifier: "id.example.com", usage: DynamicRegistrationUsageAccount, verifyUsage: DynamicRegistrationUsageAccount}, + {name: "apps", issuer: "alice.id.example.com", verifier: "alice.id.example.com", usage: DynamicRegistrationUsageApp, verifyUsage: DynamicRegistrationUsageApp}, + {name: "account token cannot register app", issuer: "id.example.com", verifier: "alice.id.example.com", usage: DynamicRegistrationUsageAccount, verifyUsage: DynamicRegistrationUsageApp, wantError: true}, + {name: "app token cannot register account credentials", issuer: "alice.id.example.com", verifier: "id.example.com", usage: DynamicRegistrationUsageApp, verifyUsage: DynamicRegistrationUsageAccount, wantError: true}, + {name: "usage claim mismatch", issuer: "id.example.com", verifier: "id.example.com", usage: DynamicRegistrationUsageAccount, verifyUsage: DynamicRegistrationUsageApp, wantError: true}, + {name: "iat cannot be used as registration access token", issuer: "id.example.com", verifier: "id.example.com", usage: DynamicRegistrationUsageAccount, verifyUsage: DynamicRegistrationUsageAccount, verifyUse: DynamicRegistrationTokenUseRegistration, wantError: true}, + {name: "other tenant", issuer: "alice.id.example.com", verifier: "bob.id.example.com", usage: DynamicRegistrationUsageApp, verifyUsage: DynamicRegistrationUsageApp, wantError: true}, + {name: "missing expiry", issuer: "id.example.com", verifier: "id.example.com", usage: DynamicRegistrationUsageAccount, verifyUsage: DynamicRegistrationUsageAccount, change: func(c *dynamicRegistrationTokenClaims) { c.ExpiresAt = nil }, wantError: true}, + {name: "expired", issuer: "id.example.com", verifier: "id.example.com", usage: DynamicRegistrationUsageAccount, verifyUsage: DynamicRegistrationUsageAccount, change: func(c *dynamicRegistrationTokenClaims) { + c.ExpiresAt = jwt.NewNumericDate(time.Now().Add(-time.Hour)) + }, wantError: true}, + {name: "wrong audience", issuer: "id.example.com", verifier: "id.example.com", usage: DynamicRegistrationUsageAccount, verifyUsage: DynamicRegistrationUsageAccount, change: func(c *dynamicRegistrationTokenClaims) { + c.Audience = []string{"https://elsewhere.example.com"} + }, wantError: true}, + {name: "issuer path", issuer: "id.example.com", verifier: "id.example.com", usage: DynamicRegistrationUsageAccount, verifyUsage: DynamicRegistrationUsageAccount, change: func(c *dynamicRegistrationTokenClaims) { c.Issuer += "/other" }, wantError: true}, + {name: "missing account", issuer: "id.example.com", verifier: "id.example.com", usage: DynamicRegistrationUsageAccount, verifyUsage: DynamicRegistrationUsageAccount, change: func(c *dynamicRegistrationTokenClaims) { c.AccountID = uuid.Nil }, wantError: true}, + {name: "missing domain", issuer: "id.example.com", verifier: "id.example.com", usage: DynamicRegistrationUsageAccount, verifyUsage: DynamicRegistrationUsageAccount, change: func(c *dynamicRegistrationTokenClaims) { c.Subject = "" }, wantError: true}, + } { + t.Run(tc.name, func(t *testing.T) { + tokenUse := tc.tokenUse + if tokenUse == "" { + tokenUse = DynamicRegistrationTokenUseInitialAccess + } + verifyUse := tc.verifyUse + if verifyUse == "" { + verifyUse = DynamicRegistrationTokenUseInitialAccess + } + token := provider.DynamicRegistrationIAT(DynamicRegistrationIATOptions{ + AccountPublicID: accountID, AccountVersion: 1, IssuerDomain: tc.issuer, + Subject: "client.example.com", JTI: "registration", Usage: tc.usage, TokenUse: tokenUse, + }) + claims := token.Claims.(dynamicRegistrationTokenClaims) + if tc.change != nil { + tc.change(&claims) + } + token.Claims = claims + token.Header["kid"] = "test" + signed, err := token.SignedString(private) + if err != nil { + t.Fatal(err) + } + domain, account, err := provider.VerifyDynamicRegistrationIAT(context.Background(), VerifyDynamicRegistrationIATOptions{ + IAT: signed, IssuerDomain: tc.verifier, Usage: tc.verifyUsage, TokenUse: verifyUse, + GetPublicJWK: func(string, utils.SupportedCryptoSuite) (utils.JWK, error) { return &jwk, nil }, + }) + if (err != nil) != tc.wantError { + t.Fatalf("error = %v, wantError = %v", err, tc.wantError) + } + if !tc.wantError && (domain != "client.example.com" || account.AccountID != accountID) { + t.Fatalf("incorrect bindings: %s, %+v", domain, account) + } + }) + } +} diff --git a/idp/internal/providers/tokens/dynamic_registration_software_statements.go b/idp/internal/providers/tokens/dynamic_registration_software_statements.go index 84ccfe3..8456bf6 100644 --- a/idp/internal/providers/tokens/dynamic_registration_software_statements.go +++ b/idp/internal/providers/tokens/dynamic_registration_software_statements.go @@ -8,6 +8,9 @@ package tokens import ( "context" + "encoding/base64" + "encoding/json" + "strings" "github.com/golang-jwt/jwt/v5" @@ -17,40 +20,41 @@ 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 *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"` + RawMetadata map[string]json.RawMessage `json:"-"` + 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 web spa backend device"` + 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,max=2147483647"` + RequireAuthTime bool `json:"require_auth_time,omitempty"` + 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) @@ -92,10 +96,17 @@ func (t *Tokens) VerifySoftwareStatement( } return jwk.ToUsableKey() - }); err != nil { + }, jwt.WithValidMethods([]string{"RS256", "ES256", "EdDSA"}), jwt.WithIssuedAt()); err != nil { logger.WarnContext(ctx, "Failed to verify software statement token", "error", err) return SoftwareStatementClaims{}, jwt.RegisteredClaims{}, err } + payload, err := base64.RawURLEncoding.DecodeString(strings.Split(opts.SoftwareStatement, ".")[1]) + if err != nil { + return SoftwareStatementClaims{}, jwt.RegisteredClaims{}, err + } + if err := json.Unmarshal(payload, &claims.RawMetadata); err != nil { + return SoftwareStatementClaims{}, jwt.RegisteredClaims{}, err + } return claims.SoftwareStatementClaims, claims.RegisteredClaims, nil } diff --git a/idp/internal/server/routes/oauth.go b/idp/internal/server/routes/oauth.go index af11571..7ba6355 100644 --- a/idp/internal/server/routes/oauth.go +++ b/idp/internal/server/routes/oauth.go @@ -42,13 +42,60 @@ func (r *Routes) OAuthRoutes(app *fiber.App) { }, ), ) + router.Get( + paths.OAuthRegisterClient, + r.controllers.HostMiddleware, + HostAwareRoute( + []fiber.Handler{ + r.controllers.DynamicRegistrationAccessTokenMiddleware, + r.controllers.OAuthDynamicRegistrationGet, + }, + []fiber.Handler{ + r.controllers.AppDynamicRegistrationAccessTokenMiddleware, + r.controllers.OAuthAppDynamicRegistrationGet, + }, + ), + ) + router.Put( + paths.OAuthRegisterClient, + r.controllers.HostMiddleware, + HostAwareRoute( + []fiber.Handler{ + r.controllers.DynamicRegistrationAccessTokenMiddleware, + r.controllers.OAuthDynamicRegistrationUpdate, + }, + []fiber.Handler{ + r.controllers.AppDynamicRegistrationAccessTokenMiddleware, + r.controllers.OAuthAppDynamicRegistrationUpdate, + }, + ), + ) + router.Delete( + paths.OAuthRegisterClient, + r.controllers.HostMiddleware, + HostAwareRoute( + []fiber.Handler{ + r.controllers.DynamicRegistrationAccessTokenMiddleware, + r.controllers.OAuthDynamicRegistrationDelete, + }, + []fiber.Handler{ + r.controllers.AppDynamicRegistrationAccessTokenMiddleware, + r.controllers.OAuthAppDynamicRegistrationDelete, + }, + ), + ) // Initial Access Token (IAT) routes iatRouter := router.Group(paths.InitialAccessToken, r.controllers.HostMiddleware) iatRouter.Post( paths.InitialAccessTokenSign, - r.controllers.AccountAccessClaimsMiddleware, - r.controllers.AppDynamicRegistrationIATSign, + HostAwareRoute( + []fiber.Handler{r.controllers.NotFoundHandler}, + []fiber.Handler{ + r.controllers.AccountAccessClaimsMiddleware, + r.controllers.AppDynamicRegistrationIATSign, + }, + ), ) // Dynamic Registration IAT Code Exchange flow diff --git a/idp/internal/server/server.go b/idp/internal/server/server.go index e6ea971..6c1471b 100644 --- a/idp/internal/server/server.go +++ b/idp/internal/server/server.go @@ -71,9 +71,12 @@ const ( PgTypeSoftwareStatementVerificationMethod PgType = "software_statement_verification_method" PgTypeAppProfileType PgType = "app_profile_type" PgTypeTokenOwner PgType = "token_owner" + PgTypeDynamicRegistrationUsage PgType = "dynamic_registration_usage" + PgTypeDomainVerificationMethod PgType = "domain_verification_method" + PgTypeCreationMethod PgType = "creation_method" ) -var PgTypes = [25]PgType{ +var PgTypes = [28]PgType{ PgTypeKekUsage, PgTypeDekUsage, PgTypeTokenCryptoSuite, @@ -99,6 +102,9 @@ var PgTypes = [25]PgType{ PgTypeSoftwareStatementVerificationMethod, PgTypeAppProfileType, PgTypeTokenOwner, + PgTypeDynamicRegistrationUsage, + PgTypeDomainVerificationMethod, + PgTypeCreationMethod, } func New( diff --git a/idp/internal/services/account_credentials_registration.go b/idp/internal/services/account_credentials_registration.go index 28710ab..ffbbbee 100644 --- a/idp/internal/services/account_credentials_registration.go +++ b/idp/internal/services/account_credentials_registration.go @@ -17,7 +17,6 @@ import ( "github.com/tugascript/devlogs/idp/internal/exceptions" "github.com/tugascript/devlogs/idp/internal/providers/database" - "github.com/tugascript/devlogs/idp/internal/providers/tokens" "github.com/tugascript/devlogs/idp/internal/services/dtos" "github.com/tugascript/devlogs/idp/internal/utils" ) @@ -65,7 +64,6 @@ type mapAccountCredentialsRegistrationDataToDBParamsOptions struct { transport database.Transport scopes []database.AccountCredentialsScope data *ApplicationRegistrationData - claims *tokens.SoftwareStatementClaims } func (s *Services) mapAccountCredentialsRegistrationDataToDBParams( @@ -77,7 +75,6 @@ func (s *Services) mapAccountCredentialsRegistrationDataToDBParams( "accountID", opts.accountID, "domain", opts.domain, "data", opts.data, - "claims", opts.claims, ) logger.InfoContext(ctx, "Mapping account credentials registration data to database params") @@ -87,7 +84,7 @@ func (s *Services) mapAccountCredentialsRegistrationDataToDBParams( return database.CreateAccountCredentialsParams{}, serviceErr } - responseTypes, serviceErr := mapResponseTypesWithDefault(opts.data.ResponseTypes) + responseTypes, serviceErr := mapRegistrationResponseTypes(opts.data.ResponseTypes) if serviceErr != nil { logger.ErrorContext(ctx, "Failed to map response types", "serviceError", serviceErr) return database.CreateAccountCredentialsParams{}, serviceErr @@ -183,7 +180,7 @@ func (s *Services) mapAccountCredentialsRegistrationDataToDBParams( Transport: opts.transport, ClientID: utils.Base62UUID(), RedirectUris: utils.MapSlice(opts.data.RedirectURIs, func(uri *string) string { - return utils.ProcessURL(*uri) + return *uri }), TokenEndpointAuthMethod: opts.tokenEndpointAuthMethod, TokenEndpointAuthSigningAlg: tokenEndpointAuthSigningAlg, @@ -218,136 +215,10 @@ func (s *Services) mapAccountCredentialsRegistrationDataToDBParams( DefaultAcrValues: opts.data.DefaultACRValues, InitiateLoginUri: mapEmptyURL(opts.data.InitiateLoginURI), RequestUris: utils.MapSlice(opts.data.RequestURIs, func(uri *string) string { - return utils.ProcessURL(*uri) + return *uri }), } - if opts.claims != nil { - if opts.claims.ClientName != "" { - params.ClientName = opts.claims.ClientName - } - if opts.claims.ClientURI != "" { - params.ClientUri = utils.ProcessURL(opts.claims.ClientURI) - } - if opts.claims.LogoURI != "" { - params.LogoUri = mapEmptyURL(opts.claims.LogoURI) - } - if len(opts.claims.RedirectURIs) > 0 { - params.RedirectUris = utils.MapSlice(opts.claims.RedirectURIs, func(uri *string) string { - return utils.ProcessURL(*uri) - }) - } - if opts.claims.TOSURI != "" { - params.TosUri = mapEmptyURL(opts.claims.TOSURI) - } - if opts.claims.PolicyURI != "" { - params.PolicyUri = mapEmptyURL(opts.claims.PolicyURI) - } - if opts.claims.JWKsURI != "" { - params.JwksUri = mapEmptyURL(opts.claims.JWKsURI) - } - 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 = jsonJwks - } - if opts.claims.SoftwareID != "" { - params.SoftwareID = mapEmptyString(opts.claims.SoftwareID) - } - if opts.claims.SoftwareVersion != "" { - params.SoftwareVersion = mapEmptyString(opts.claims.SoftwareVersion) - } - if opts.claims.SectorIdentifierURI != "" { - params.SectorIdentifierUri = mapEmptyURL(opts.claims.SectorIdentifierURI) - } - if opts.claims.SubjectType != "" { - subjectType, _ := mapEmptySubjectType(opts.claims.SubjectType) - params.SubjectType = subjectType - } - if len(opts.claims.RequestURIs) > 0 { - params.RequestUris = utils.MapSlice(opts.claims.RequestURIs, func(uri *string) string { - return utils.ProcessURL(*uri) - }) - } - if opts.claims.IDTokenSignedResponseAlg != "" { - idSignAlg, _ := mapTokenCryptoSuiteWithDefault(opts.claims.IDTokenSignedResponseAlg) - params.IDTokenSignedResponseAlg = idSignAlg - } - if opts.claims.IDTokenEncryptedResponseAlg != "" { - idEncAlg, _ := mapEmptyTokenEncryptionAlgorithm(opts.claims.IDTokenEncryptedResponseAlg) - params.IDTokenEncryptedResponseAlg = idEncAlg - } - if opts.claims.IDTokenEncryptedResponseEnc != "" { - idEncEnc, _ := mapEmptyTokenEncryptionEncoding(params.IDTokenEncryptedResponseAlg, opts.claims.IDTokenEncryptedResponseEnc) - params.IDTokenEncryptedResponseEnc = idEncEnc - } - if opts.claims.UserInfoSignedResponseAlg != "" { - userInfoSignAlg, _ := mapEmptyTokenCryptoSuite(opts.claims.UserInfoSignedResponseAlg) - params.UserinfoSignedResponseAlg = userInfoSignAlg - } - if opts.claims.UserInfoEncryptedResponseAlg != "" { - userInfoEncAlg, _ := mapEmptyTokenEncryptionAlgorithm(opts.claims.UserInfoEncryptedResponseAlg) - params.UserinfoEncryptedResponseAlg = userInfoEncAlg - } - if opts.claims.UserInfoEncryptedResponseEnc != "" { - userInfoEncEnc, _ := mapEmptyTokenEncryptionEncoding(params.UserinfoEncryptedResponseAlg, opts.claims.UserInfoEncryptedResponseEnc) - params.UserinfoEncryptedResponseEnc = userInfoEncEnc - } - if opts.claims.RequestObjectSigningAlg != "" { - requestObjectSigningAlg, _ := mapEmptyTokenCryptoSuite(opts.claims.RequestObjectSigningAlg) - params.RequestObjectSigningAlg = requestObjectSigningAlg - } - if opts.claims.RequestObjectEncryptionAlg != "" { - requestObjectEncryptionAlg, _ := mapEmptyTokenEncryptionAlgorithm(opts.claims.RequestObjectEncryptionAlg) - params.RequestObjectEncryptionAlg = requestObjectEncryptionAlg - } - if opts.claims.RequestObjectEncryptionEnc != "" { - requestObjectEncryptionEnc, _ := mapEmptyTokenEncryptionEncoding(params.RequestObjectEncryptionAlg, opts.claims.RequestObjectEncryptionEnc) - params.RequestObjectEncryptionEnc = requestObjectEncryptionEnc - } - if opts.claims.TokenEndpointAuthSigningAlg != "" { - tokenEndpointAuthSigningAlg, _ := mapEmptyTokenCryptoSuite(opts.claims.TokenEndpointAuthSigningAlg) - params.TokenEndpointAuthSigningAlg = tokenEndpointAuthSigningAlg - } - if opts.claims.AccessTokenSigningAlg != "" { - accessTokenSigningAlg, _ := mapTokenCryptoSuiteWithDefault(opts.claims.AccessTokenSigningAlg) - params.AccessTokenSigningAlg = accessTokenSigningAlg - } - if opts.claims.RequireAuthTime { - params.RequireAuthTime = opts.claims.RequireAuthTime - } - if opts.claims.DefaultMaxAge > 0 { - params.DefaultMaxAge = mapEmptyBigInt(opts.claims.DefaultMaxAge) - } - if opts.claims.DefaultACRValues != nil { - params.DefaultAcrValues = opts.claims.DefaultACRValues - } - if opts.claims.InitiateLoginURI != "" { - params.InitiateLoginUri = mapEmptyURL(opts.claims.InitiateLoginURI) - } - if len(opts.claims.GrantTypes) > 0 { - params.GrantTypes = utils.MapSlice(opts.claims.GrantTypes, func(grantType *string) database.GrantType { - return database.GrantType(*grantType) - }) - } - if len(opts.claims.ResponseTypes) > 0 { - params.ResponseTypes = utils.MapSlice(opts.claims.ResponseTypes, func(responseType *string) database.ResponseType { - return database.ResponseType(*responseType) - }) - } - if opts.claims.Scope != "" { - params.Scopes = utils.MapSlice(strings.Fields(opts.claims.Scope), func(scope *string) database.AccountCredentialsScope { - return database.AccountCredentialsScope(*scope) - }) - } - if len(opts.claims.Contacts) > 0 { - params.Contacts = opts.claims.Contacts - } - } - return params, nil } @@ -408,6 +279,85 @@ func (s *Services) CreateAccountCredentialsRegistration( ) logger.InfoContext(ctx, "Creating account credentials registration...") + data := ApplicationRegistrationData{ + RedirectURIs: opts.RedirectURIs, + TokenEndpointAuthMethod: opts.TokenEndpointAuthMethod, + ResponseTypes: opts.ResponseTypes, + GrantTypes: opts.GrantTypes, + ApplicationType: opts.ApplicationType, + ClientName: opts.ClientName, + ClientURI: opts.ClientURI, + LogoURI: opts.LogoURI, + Scope: opts.Scope, + Contacts: opts.Contacts, + TOSURI: opts.TOSURI, + PolicyURI: opts.PolicyURI, + JWKsURI: opts.JWKsURI, + JWKs: opts.JWKs, + SoftwareID: opts.SoftwareID, + SoftwareVersion: opts.SoftwareVersion, + SubjectType: opts.SubjectType, + SectorIdentifierURI: opts.SectorIdentifierURI, + DefaultMaxAge: opts.DefaultMaxAge, + RequireAuthTime: opts.RequireAuthTime, + DefaultACRValues: opts.DefaultACRValues, + InitiateLoginURI: opts.InitiateLoginURI, + RequestURIs: opts.RequestURIs, + IDTokenSignedResponseAlg: opts.IDTokenSignedResponseAlg, + IDTokenEncryptedResponseAlg: opts.IDTokenEncryptedResponseAlg, + IDTokenEncryptedResponseEnc: opts.IDTokenEncryptedResponseEnc, + UserInfoSignedResponseAlg: opts.UserInfoSignedResponseAlg, + UserInfoEncryptedResponseAlg: opts.UserInfoEncryptedResponseAlg, + UserInfoEncryptedResponseEnc: opts.UserInfoEncryptedResponseEnc, + RequestObjectSigningAlg: opts.RequestObjectSigningAlg, + RequestObjectEncryptionAlg: opts.RequestObjectEncryptionAlg, + RequestObjectEncryptionEnc: opts.RequestObjectEncryptionEnc, + TokenEndpointAuthSigningAlg: opts.TokenEndpointAuthSigningAlg, + AccessTokenSigningAlg: opts.AccessTokenSigningAlg, + } + data, preparationErr := s.prepareDynamicRegistration(ctx, prepareDynamicRegistrationOptions{ + requestID: opts.RequestID, accountID: 0, accountPublicID: opts.AccountPublicID, + data: data, softwareStatement: opts.SoftwareStatement, iatDomain: opts.IATDomain, + backendDomain: opts.BackendDomain, frontendDomain: opts.FrontendDomain, app: false, + }) + if preparationErr != nil { + return dtos.AccountCredentialsDTO{}, preparationErr + } + opts.RedirectURIs = data.RedirectURIs + opts.TokenEndpointAuthMethod = data.TokenEndpointAuthMethod + opts.ResponseTypes = data.ResponseTypes + opts.GrantTypes = data.GrantTypes + opts.ApplicationType = data.ApplicationType + opts.ClientName = data.ClientName + opts.ClientURI = data.ClientURI + opts.LogoURI = data.LogoURI + opts.Scope = data.Scope + opts.Contacts = data.Contacts + opts.TOSURI = data.TOSURI + opts.PolicyURI = data.PolicyURI + opts.JWKsURI = data.JWKsURI + opts.JWKs = data.JWKs + opts.SoftwareID = data.SoftwareID + opts.SoftwareVersion = data.SoftwareVersion + opts.SubjectType = data.SubjectType + opts.SectorIdentifierURI = data.SectorIdentifierURI + opts.DefaultMaxAge = data.DefaultMaxAge + opts.RequireAuthTime = data.RequireAuthTime + opts.DefaultACRValues = data.DefaultACRValues + opts.InitiateLoginURI = data.InitiateLoginURI + opts.RequestURIs = data.RequestURIs + opts.IDTokenSignedResponseAlg = data.IDTokenSignedResponseAlg + opts.IDTokenEncryptedResponseAlg = data.IDTokenEncryptedResponseAlg + opts.IDTokenEncryptedResponseEnc = data.IDTokenEncryptedResponseEnc + opts.UserInfoSignedResponseAlg = data.UserInfoSignedResponseAlg + opts.UserInfoEncryptedResponseAlg = data.UserInfoEncryptedResponseAlg + opts.UserInfoEncryptedResponseEnc = data.UserInfoEncryptedResponseEnc + opts.RequestObjectSigningAlg = data.RequestObjectSigningAlg + opts.RequestObjectEncryptionAlg = data.RequestObjectEncryptionAlg + opts.RequestObjectEncryptionEnc = data.RequestObjectEncryptionEnc + opts.TokenEndpointAuthSigningAlg = data.TokenEndpointAuthSigningAlg + opts.AccessTokenSigningAlg = data.AccessTokenSigningAlg + applicationType, serviceErr := mapAccountCredentialsType(opts.ApplicationType) if serviceErr != nil { logger.ErrorContext(ctx, "Failed to map application type", "serviceError", serviceErr) @@ -421,11 +371,7 @@ func (s *Services) CreateAccountCredentialsRegistration( } transport := mapAccountCredentialsDRTransport(applicationType) - tokenEndpointAuthMethod, serviceErr := mapAccountCredentialsTokenEndpointAuthMethod( - opts.TokenEndpointAuthMethod, - applicationType, - transport, - ) + tokenEndpointAuthMethod, serviceErr := mapAuthMethod(opts.TokenEndpointAuthMethod) if serviceErr != nil { logger.ErrorContext(ctx, "Failed to map token endpoint auth method", "serviceError", serviceErr) return dtos.AccountCredentialsDTO{}, serviceErr @@ -488,7 +434,7 @@ func (s *Services) CreateAccountCredentialsRegistration( } domain := parsedClientURI.Hostname() - baseDomain, serviceErr := s.checkClientRegistrationDomain(ctx, checkClientRegistrationDomainOptions{ + _, serviceErr = s.checkClientRegistrationDomain(ctx, checkClientRegistrationDomainOptions{ requestID: opts.RequestID, accountPublicID: opts.AccountPublicID, iatDomain: opts.IATDomain, @@ -501,103 +447,23 @@ func (s *Services) CreateAccountCredentialsRegistration( return dtos.AccountCredentialsDTO{}, serviceErr } - data := ApplicationRegistrationData{ - RedirectURIs: opts.RedirectURIs, - TokenEndpointAuthMethod: opts.TokenEndpointAuthMethod, - ResponseTypes: opts.ResponseTypes, - GrantTypes: opts.GrantTypes, - ApplicationType: opts.ApplicationType, - ClientName: opts.ClientName, - ClientURI: opts.ClientURI, - LogoURI: opts.LogoURI, - Scope: opts.Scope, - Contacts: opts.Contacts, - TOSURI: opts.TOSURI, - PolicyURI: opts.PolicyURI, - JWKsURI: opts.JWKsURI, - JWKs: opts.JWKs, - SoftwareID: opts.SoftwareID, - SoftwareVersion: opts.SoftwareVersion, - SubjectType: opts.SubjectType, - SectorIdentifierURI: opts.SectorIdentifierURI, - DefaultMaxAge: opts.DefaultMaxAge, - RequireAuthTime: opts.RequireAuthTime, - DefaultACRValues: opts.DefaultACRValues, - InitiateLoginURI: opts.InitiateLoginURI, - RequestURIs: opts.RequestURIs, - IDTokenSignedResponseAlg: opts.IDTokenSignedResponseAlg, - IDTokenEncryptedResponseAlg: opts.IDTokenEncryptedResponseAlg, - IDTokenEncryptedResponseEnc: opts.IDTokenEncryptedResponseEnc, - UserInfoSignedResponseAlg: opts.UserInfoSignedResponseAlg, - UserInfoEncryptedResponseAlg: opts.UserInfoEncryptedResponseAlg, - UserInfoEncryptedResponseEnc: opts.UserInfoEncryptedResponseEnc, - RequestObjectSigningAlg: opts.RequestObjectSigningAlg, - RequestObjectEncryptionAlg: opts.RequestObjectEncryptionAlg, - RequestObjectEncryptionEnc: opts.RequestObjectEncryptionEnc, - TokenEndpointAuthSigningAlg: opts.TokenEndpointAuthSigningAlg, - AccessTokenSigningAlg: opts.AccessTokenSigningAlg, - } - var ssClaimsReference *tokens.SoftwareStatementClaims - if opts.SoftwareStatement != "" { - ssClaims, stdClaims, err := s.jwt.VerifySoftwareStatement(ctx, tokens.VerifySoftwareStatementOptions{ - RequestID: opts.RequestID, - SoftwareStatement: opts.SoftwareStatement, - GetPublicJWK: s.buildDynamicRegistrationSoftwareStatementFunc(ctx, buildDynamicRegistrationSoftwareStatementFuncOptions{ - requestID: opts.RequestID, - accountPublicID: opts.AccountPublicID, - verificationMethods: accountDRConfigDTO.SoftwareStatementVerificationMethods, - jwksURI: opts.JWKsURI, - jwks: opts.JWKs, - domain: domain, - baseDomain: baseDomain, - }), - }) - if err != nil { - logger.WarnContext(ctx, "Failed to verify software statement", "error", err) - return dtos.AccountCredentialsDTO{}, exceptions.NewInvalidTokenError("invalid software statement") - } - if serviceErr := s.verifySoftwareStatementSTDClaims(ctx, verifySoftwareStatementSTDClaimsOptions{ - requestID: opts.RequestID, - backendDomain: opts.BackendDomain, - frontendDomain: opts.FrontendDomain, - domain: domain, - baseDomain: baseDomain, - claims: &stdClaims, - }); serviceErr != nil { - logger.WarnContext(ctx, "Failed to verify software statement standard claims", "serviceError", serviceErr) - return dtos.AccountCredentialsDTO{}, serviceErr - } - - if serviceErr := s.validateSoftwareStatementClaims(ctx, validateSoftwareStatementClaimsOptions{ - requestID: opts.RequestID, - claims: &ssClaims, - allowedScopes: utils.SliceToHashSet(allowedAccountCredentialsScopes), - }); serviceErr != nil { - logger.WarnContext(ctx, "Failed to validate software statement claims", "serviceError", serviceErr) - return dtos.AccountCredentialsDTO{}, serviceErr - } - - ssClaimsReference = &ssClaims - } - params, serviceErr := s.mapAccountCredentialsRegistrationDataToDBParams(ctx, mapAccountCredentialsRegistrationDataToDBParamsOptions{ applicationType: applicationType, accountPublicID: opts.AccountPublicID, - accountID: accountDTO.Version(), + accountID: accountDTO.ID(), domain: domain, requestID: opts.RequestID, tokenEndpointAuthMethod: tokenEndpointAuthMethod, transport: transport, scopes: scopes, data: &data, - claims: ssClaimsReference, }) if serviceErr != nil { logger.ErrorContext(ctx, "Failed to map account credentials registration data to database params", "serviceError", serviceErr) return dtos.AccountCredentialsDTO{}, serviceErr } - if tokenEndpointAuthMethod == database.AuthMethodNone { + if tokenEndpointAuthMethod == database.AuthMethodNone || (tokenEndpointAuthMethod == database.AuthMethodPrivateKeyJwt && (data.JWKs != nil || data.JWKsURI != "")) { accountCredentials, err := s.database.CreateAccountCredentials(ctx, params) if err != nil { logger.ErrorContext(ctx, "Failed to create account credentials", "error", err) @@ -605,7 +471,7 @@ func (s *Services) CreateAccountCredentialsRegistration( } logger.InfoContext(ctx, "Created account credentials successfully") - return dtos.MapAccountCredentialsToDTO(&accountCredentials) + return s.finalizeAccountCredentialsRegistration(ctx, opts, &accountCredentials, "", time.Time{}, nil) } qrs, txn, err := s.database.BeginTx(ctx) @@ -618,7 +484,7 @@ func (s *Services) CreateAccountCredentialsRegistration( s.database.FinalizeTx(ctx, txn, err, serviceErr) }() - accountCredentials, err := s.database.CreateAccountCredentials(ctx, params) + accountCredentials, err := qrs.CreateAccountCredentials(ctx, params) if err != nil { logger.ErrorContext(ctx, "Failed to create account credentials", "error", err) return dtos.AccountCredentialsDTO{}, exceptions.FromDBError(err) @@ -661,7 +527,7 @@ func (s *Services) CreateAccountCredentialsRegistration( return dtos.AccountCredentialsDTO{}, serviceErr } - return dtos.MapAccountCredentialsToDTOWithJWK(&accountCredentials, jwk, dbPrms.ExpiresAt) + return s.finalizeAccountCredentialsRegistration(ctx, opts, &accountCredentials, "", dbPrms.ExpiresAt, jwk) case database.AuthMethodClientSecretBasic, database.AuthMethodClientSecretPost, database.AuthMethodClientSecretJwt: var ccID int32 var secretID, secret string @@ -694,10 +560,36 @@ func (s *Services) CreateAccountCredentialsRegistration( return dtos.AccountCredentialsDTO{}, serviceErr } - return dtos.MapAccountCredentialsToDTOWithSecret(&accountCredentials, secretID, secret, exp) + return s.finalizeAccountCredentialsRegistration(ctx, opts, &accountCredentials, secret, exp, nil) default: logger.ErrorContext(ctx, "Invalid token endpoint auth method", "tokenEndpointAuthMethod", tokenEndpointAuthMethod) serviceErr = exceptions.NewInternalServerError() return dtos.AccountCredentialsDTO{}, serviceErr } } + +func (s *Services) finalizeAccountCredentialsRegistration( + ctx context.Context, + opts CreateAccountCredentialsRegistrationOptions, + row *database.AccountCredential, + secret string, + expiry time.Time, + key utils.JWK, +) (dtos.AccountCredentialsDTO, *exceptions.ServiceError) { + dto, serviceErr := dtos.MapRegisteredAccountCredentials(row, opts.SoftwareStatement, secret, expiry, key) + if serviceErr != nil { + return dto, serviceErr + } + token, serviceErr := s.CreateAccountCredentialsRegistrationAccessToken(ctx, CreateAccountCredentialsRegistrationAccessTokenOptions{ + RequestID: opts.RequestID, + AccountPublicID: opts.AccountPublicID, + AccountVersion: opts.AccountVersion, + ClientID: row.ClientID, + BackendDomain: opts.BackendDomain, + }) + if serviceErr != nil { + return dtos.AccountCredentialsDTO{}, serviceErr + } + dto.Registration.WithRegistrationAccess(token, dtos.RegistrationClientURI(opts.BackendDomain, row.ClientID)) + return dto, nil +} diff --git a/idp/internal/services/account_credentials_registration_iat.go b/idp/internal/services/account_credentials_registration_iat.go index 62f2a2c..25df703 100644 --- a/idp/internal/services/account_credentials_registration_iat.go +++ b/idp/internal/services/account_credentials_registration_iat.go @@ -62,8 +62,10 @@ func (s *Services) CreateAccountCredentialsRegistrationIAT( AccountPublicID: opts.AccountPublicID, AccountVersion: opts.AccountVersion, IssuerDomain: opts.BackendDomain, - Domain: opts.Domain, - ClientID: utils.Base62UUID(), + Subject: opts.Domain, + JTI: utils.Base62UUID(), + Usage: tokens.DynamicRegistrationUsageAccount, + TokenUse: tokens.DynamicRegistrationTokenUseInitialAccess, }), GetJWKfn: s.BuildGetGlobalEncryptedJWKFn(ctx, BuildEncryptedJWKFnOptions{ RequestID: opts.RequestID, @@ -114,6 +116,8 @@ func (s *Services) ProcessAccountCredentialsRegistrationIATAuth( RequestID: opts.RequestID, IAT: token, IssuerDomain: opts.IssuerDomain, + Usage: tokens.DynamicRegistrationUsageAccount, + TokenUse: tokens.DynamicRegistrationTokenUseInitialAccess, GetPublicJWK: s.BuildGetGlobalPublicKeyFn(ctx, BuildGetGlobalVerifyKeyFnOptions{ RequestID: opts.RequestID, KeyType: database.TokenKeyTypeDynamicRegistration, @@ -128,3 +132,97 @@ func (s *Services) ProcessAccountCredentialsRegistrationIATAuth( logger.InfoContext(ctx, "Processed account credentials registration IAT auth successfully") return domain, accountClaims, nil } + +type CreateAccountCredentialsRegistrationAccessTokenOptions struct { + RequestID string + AccountPublicID uuid.UUID + AccountVersion int32 + ClientID string + BackendDomain string +} + +func (s *Services) CreateAccountCredentialsRegistrationAccessToken( + ctx context.Context, + opts CreateAccountCredentialsRegistrationAccessTokenOptions, +) (string, *exceptions.ServiceError) { + logger := s.buildLogger(opts.RequestID, accountCredentialsRegistrationIATLocation, "CreateAccountCredentialsRegistrationAccessToken").With( + "accountPublicId", opts.AccountPublicID, + "clientId", opts.ClientID, + ) + logger.InfoContext(ctx, "Creating account credentials registration access token...") + + signedToken, serviceErr := s.crypto.SignToken(ctx, crypto.SignTokenOptions{ + RequestID: opts.RequestID, + Token: s.jwt.DynamicRegistrationAccessToken(tokens.DynamicRegistrationIATOptions{ + AccountPublicID: opts.AccountPublicID, + AccountVersion: opts.AccountVersion, + IssuerDomain: opts.BackendDomain, + Subject: opts.ClientID, + JTI: utils.Base62UUID(), + Usage: tokens.DynamicRegistrationUsageAccount, + TokenUse: tokens.DynamicRegistrationTokenUseRegistration, + TTL: s.jwt.GetRegistrationAccessTokenTTL(), + }), + GetJWKfn: s.BuildGetGlobalEncryptedJWKFn(ctx, BuildEncryptedJWKFnOptions{ + RequestID: opts.RequestID, + KeyType: database.TokenKeyTypeDynamicRegistration, + TTL: s.jwt.GetRegistrationAccessTokenTTL(), + }), + GetDecryptDEKfn: s.BuildGetGlobalDecDEKFn(ctx, BuildGetGlobalDEKFnOptions{ + RequestID: opts.RequestID, + }), + GetEncryptDEKfn: s.BuildGetEncGlobalDEKFn(ctx, BuildGetGlobalDEKFnOptions{ + RequestID: opts.RequestID, + }), + StoreFN: s.BuildUpdateJWKDEKFn(ctx, BuildUpdateJWKDEKFnOptions{ + RequestID: opts.RequestID, + }), + }) + if serviceErr != nil { + logger.ErrorContext(ctx, "Failed to sign account credentials registration access token", "serviceError", serviceErr) + return "", serviceErr + } + + return signedToken, nil +} + +type ProcessAccountCredentialsRegistrationAccessTokenOptions struct { + RequestID string + AuthHeader string + IssuerDomain string +} + +func (s *Services) ProcessAccountCredentialsRegistrationAccessToken( + ctx context.Context, + opts ProcessAccountCredentialsRegistrationAccessTokenOptions, +) (string, tokens.AccountClaims, *exceptions.ServiceError) { + logger := s.buildLogger(opts.RequestID, accountCredentialsRegistrationIATLocation, "ProcessAccountCredentialsRegistrationAccessToken") + logger.InfoContext(ctx, "Processing account credentials registration access token...") + + token, serviceErr := extractAuthHeaderToken(opts.AuthHeader) + if serviceErr != nil { + logger.WarnContext(ctx, "Failed to extract token from auth header", "serviceError", serviceErr) + return "", tokens.AccountClaims{}, serviceErr + } + + clientID, accountClaims, err := s.jwt.VerifyDynamicRegistrationIAT( + ctx, + tokens.VerifyDynamicRegistrationIATOptions{ + RequestID: opts.RequestID, + IAT: token, + IssuerDomain: opts.IssuerDomain, + Usage: tokens.DynamicRegistrationUsageAccount, + TokenUse: tokens.DynamicRegistrationTokenUseRegistration, + GetPublicJWK: s.BuildGetGlobalPublicKeyFn(ctx, BuildGetGlobalVerifyKeyFnOptions{ + RequestID: opts.RequestID, + KeyType: database.TokenKeyTypeDynamicRegistration, + }), + }, + ) + if err != nil { + logger.WarnContext(ctx, "Failed to verify account credentials registration access token", "error", err) + return "", tokens.AccountClaims{}, exceptions.NewUnauthorizedError() + } + + return clientID, accountClaims, nil +} diff --git a/idp/internal/services/app_dynamic_registration.go b/idp/internal/services/app_dynamic_registration.go index e9af935..f78809c 100644 --- a/idp/internal/services/app_dynamic_registration.go +++ b/idp/internal/services/app_dynamic_registration.go @@ -14,10 +14,10 @@ import ( "time" "github.com/google/uuid" + "github.com/jackc/pgx/v5/pgtype" "github.com/tugascript/devlogs/idp/internal/exceptions" "github.com/tugascript/devlogs/idp/internal/providers/database" - "github.com/tugascript/devlogs/idp/internal/providers/tokens" "github.com/tugascript/devlogs/idp/internal/services/dtos" "github.com/tugascript/devlogs/idp/internal/utils" ) @@ -132,48 +132,147 @@ type mapAppRegistrationDataToDBParamsOptions struct { usernameColumn database.AppUsernameColumn authProviders []database.AuthProvider data *ApplicationRegistrationData - claims *tokens.SoftwareStatementClaims } func (s *Services) mapAppRegistrationDataToDBParams( ctx context.Context, opts mapAppRegistrationDataToDBParamsOptions, -) (database.CreateAppParams, *exceptions.ServiceError) { +) (database.CreateRegisteredAppParams, *exceptions.ServiceError) { logger := s.buildLogger(opts.requestID, appDynamicRegistrationLocation, "mapAppRegistrationDataToDBParams").With( "accountPublicID", opts.accountPublicID, "accountID", opts.accountID, "domain", opts.domain, "data", opts.data, - "claims", opts.claims, ) logger.InfoContext(ctx, "Mapping app registration data to database params") - responseTypes, serviceErr := mapResponseTypesWithDefault(opts.data.ResponseTypes) + responseTypes, serviceErr := mapRegistrationResponseTypes(opts.data.ResponseTypes) if serviceErr != nil { logger.ErrorContext(ctx, "Failed to map response types", "serviceError", serviceErr) - return database.CreateAppParams{}, serviceErr + return database.CreateRegisteredAppParams{}, serviceErr } grantTypes, serviceErr := mapAppGrantTypes(opts.appType, opts.data.GrantTypes) if serviceErr != nil { logger.ErrorContext(ctx, "Failed to map grant types", "serviceError", serviceErr) - return database.CreateAppParams{}, serviceErr - } - - params := database.CreateAppParams{ - AccountID: opts.accountID, - AccountPublicID: opts.accountPublicID, - AppType: opts.appType, - ClientName: opts.data.ClientName, - ClientID: utils.Base62UUID(), - ClientUri: utils.ProcessURL(opts.data.ClientURI), - UsernameColumn: opts.usernameColumn, - TokenEndpointAuthMethod: opts.tokenEndpointAuthMethod, - CreationMethod: database.CreationMethodDynamicRegistration, - GrantTypes: grantTypes, - LogoUri: mapEmptyURL(opts.data.LogoURI), - TosUri: mapEmptyURL(opts.data.TOSURI), - PolicyUri: mapEmptyURL(opts.data.PolicyURI), + return database.CreateRegisteredAppParams{}, serviceErr + } + + subjectType, serviceErr := mapEmptySubjectType(opts.data.SubjectType) + if serviceErr != nil { + logger.ErrorContext(ctx, "Failed to map subject type", "serviceError", serviceErr) + return database.CreateRegisteredAppParams{}, serviceErr + } + + tokenEndpointAuthSigningAlg, serviceErr := mapEmptyTokenCryptoSuite(opts.data.TokenEndpointAuthSigningAlg) + if serviceErr != nil { + logger.ErrorContext(ctx, "Failed to map token endpoint auth signing alg", "serviceError", serviceErr) + return database.CreateRegisteredAppParams{}, serviceErr + } + + idSignAlg, serviceErr := mapTokenCryptoSuiteWithDefault(opts.data.IDTokenSignedResponseAlg) + if serviceErr != nil { + logger.ErrorContext(ctx, "Failed to map ID token signed response alg", "serviceError", serviceErr) + return database.CreateRegisteredAppParams{}, serviceErr + } + + idEncAlg, serviceErr := mapEmptyTokenEncryptionAlgorithm(opts.data.IDTokenEncryptedResponseAlg) + if serviceErr != nil { + logger.ErrorContext(ctx, "Failed to map ID token encrypted response alg", "serviceError", serviceErr) + return database.CreateRegisteredAppParams{}, serviceErr + } + + idEncEnc, serviceErr := mapEmptyTokenEncryptionEncoding(idEncAlg, opts.data.IDTokenEncryptedResponseEnc) + if serviceErr != nil { + logger.ErrorContext(ctx, "Failed to map ID token encrypted response enc", "serviceError", serviceErr) + return database.CreateRegisteredAppParams{}, serviceErr + } + + userInfoSignAlg, serviceErr := mapEmptyTokenCryptoSuite(opts.data.UserInfoSignedResponseAlg) + if serviceErr != nil { + logger.ErrorContext(ctx, "Failed to map user info signed response alg", "serviceError", serviceErr) + return database.CreateRegisteredAppParams{}, serviceErr + } + + userInfoEncAlg, serviceErr := mapEmptyTokenEncryptionAlgorithm(opts.data.UserInfoEncryptedResponseAlg) + if serviceErr != nil { + logger.ErrorContext(ctx, "Failed to map user info encrypted response alg", "serviceError", serviceErr) + return database.CreateRegisteredAppParams{}, serviceErr + } + + userInfoEncEnc, serviceErr := mapEmptyTokenEncryptionEncoding(userInfoEncAlg, opts.data.UserInfoEncryptedResponseEnc) + if serviceErr != nil { + logger.ErrorContext(ctx, "Failed to map user info encrypted response enc", "serviceError", serviceErr) + return database.CreateRegisteredAppParams{}, serviceErr + } + + requestObjectSigningAlg, serviceErr := mapEmptyTokenCryptoSuite(opts.data.RequestObjectSigningAlg) + if serviceErr != nil { + logger.ErrorContext(ctx, "Failed to map request object signing alg", "serviceError", serviceErr) + return database.CreateRegisteredAppParams{}, serviceErr + } + + requestObjectEncryptionAlg, serviceErr := mapEmptyTokenEncryptionAlgorithm(opts.data.RequestObjectEncryptionAlg) + if serviceErr != nil { + logger.ErrorContext(ctx, "Failed to map request object encryption alg", "serviceError", serviceErr) + return database.CreateRegisteredAppParams{}, serviceErr + } + + requestObjectEncryptionEnc, serviceErr := mapEmptyTokenEncryptionEncoding(requestObjectEncryptionAlg, opts.data.RequestObjectEncryptionEnc) + if serviceErr != nil { + logger.ErrorContext(ctx, "Failed to map request object encryption enc", "serviceError", serviceErr) + return database.CreateRegisteredAppParams{}, 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.CreateRegisteredAppParams{}, exceptions.NewInternalServerError() + } + } + + accessTokenSigningAlg, serviceErr := mapTokenCryptoSuiteWithDefault(opts.data.AccessTokenSigningAlg) + if serviceErr != nil { + return database.CreateRegisteredAppParams{}, serviceErr + } + + params := database.CreateRegisteredAppParams{ + JwksUri: mapEmptyURL(opts.data.JWKsURI), + Jwks: jsonJwks, + SectorIdentifierUri: mapEmptyURL(opts.data.SectorIdentifierURI), + SubjectType: subjectType, + IDTokenSignedResponseAlg: idSignAlg, + IDTokenEncryptedResponseAlg: idEncAlg, + IDTokenEncryptedResponseEnc: idEncEnc, + UserinfoSignedResponseAlg: userInfoSignAlg, + UserinfoEncryptedResponseAlg: userInfoEncAlg, + UserinfoEncryptedResponseEnc: userInfoEncEnc, + RequestObjectSigningAlg: requestObjectSigningAlg, + RequestObjectEncryptionAlg: requestObjectEncryptionAlg, + RequestObjectEncryptionEnc: requestObjectEncryptionEnc, + TokenEndpointAuthSigningAlg: tokenEndpointAuthSigningAlg, + DefaultMaxAge: pgtype.Int4{Int32: int32(opts.data.DefaultMaxAge), Valid: opts.data.DefaultMaxAge != 0}, + RequireAuthTime: opts.data.RequireAuthTime, + DefaultAcrValues: opts.data.DefaultACRValues, + InitiateLoginUri: mapEmptyURL(opts.data.InitiateLoginURI), + RequestUris: opts.data.RequestURIs, + AccessTokenSigningAlg: accessTokenSigningAlg, + AccountID: opts.accountID, + AccountPublicID: opts.accountPublicID, + AppType: opts.appType, + ClientName: opts.data.ClientName, + ClientID: utils.Base62UUID(), + ClientUri: utils.ProcessURL(opts.data.ClientURI), + UsernameColumn: opts.usernameColumn, + TokenEndpointAuthMethod: opts.tokenEndpointAuthMethod, + CreationMethod: database.CreationMethodDynamicRegistration, + GrantTypes: grantTypes, + LogoUri: mapEmptyURL(opts.data.LogoURI), + TosUri: mapEmptyURL(opts.data.TOSURI), + PolicyUri: mapEmptyURL(opts.data.PolicyURI), Contacts: utils.MapSlice(opts.data.Contacts, func(t *string) string { return utils.Lowered(*t) }), @@ -186,67 +285,13 @@ func (s *Services) mapAppRegistrationDataToDBParams( Domain: opts.domain, Transport: opts.transport, RedirectUris: utils.MapSlice(opts.data.RedirectURIs, func(uri *string) string { - return utils.ProcessURL(*uri) + return *uri }), ResponseTypes: responseTypes, AllowUserRegistration: opts.allowUserRegistration, AuthProviders: opts.authProviders, } - if opts.claims != nil { - if opts.claims.ClientName != "" { - params.ClientName = opts.claims.ClientName - } - if opts.claims.ClientURI != "" { - params.ClientUri = utils.ProcessURL(opts.claims.ClientURI) - } - if opts.claims.LogoURI != "" { - params.LogoUri = mapEmptyURL(opts.claims.LogoURI) - } - if len(opts.claims.RedirectURIs) > 0 { - params.RedirectUris = utils.MapSlice(opts.claims.RedirectURIs, func(uri *string) string { - return utils.ProcessURL(*uri) - }) - } - if opts.claims.TOSURI != "" { - params.TosUri = mapEmptyURL(opts.claims.TOSURI) - } - if opts.claims.PolicyURI != "" { - params.PolicyUri = mapEmptyURL(opts.claims.PolicyURI) - } - if opts.claims.SoftwareID != "" { - params.SoftwareID = mapEmptyString(opts.claims.SoftwareID) - } - if opts.claims.SoftwareVersion != "" { - params.SoftwareVersion = mapEmptyString(opts.claims.SoftwareVersion) - } - if len(opts.claims.GrantTypes) > 0 { - params.GrantTypes = utils.MapSlice(opts.claims.GrantTypes, func(grantType *string) database.GrantType { - return database.GrantType(*grantType) - }) - } - if len(opts.claims.ResponseTypes) > 0 { - params.ResponseTypes = utils.MapSlice(opts.claims.ResponseTypes, func(responseType *string) database.ResponseType { - return database.ResponseType(*responseType) - }) - } - if opts.claims.Scope != "" { - scopesList := strings.Fields(opts.claims.Scope) - stdScopes, customScopes, _, _, serviceErr := mapScopesToStandardAndCustomScopes(scopesList, nil) - if serviceErr != nil { - logger.ErrorContext(ctx, "Failed to map scopes from software statement", "serviceError", serviceErr) - return database.CreateAppParams{}, serviceErr - } - params.Scopes = stdScopes - params.CustomScopes = customScopes - } - if len(opts.claims.Contacts) > 0 { - params.Contacts = utils.MapSlice(opts.claims.Contacts, func(t *string) string { - return utils.Lowered(*t) - }) - } - } - return params, nil } @@ -308,6 +353,85 @@ func (s *Services) CreateAppCredentialsRegistration( ) logger.InfoContext(ctx, "Creating app credentials registration...") + data := ApplicationRegistrationData{ + RedirectURIs: opts.RedirectURIs, + TokenEndpointAuthMethod: opts.TokenEndpointAuthMethod, + ResponseTypes: opts.ResponseTypes, + GrantTypes: opts.GrantTypes, + ApplicationType: opts.ApplicationType, + ClientName: opts.ClientName, + ClientURI: opts.ClientURI, + LogoURI: opts.LogoURI, + Scope: opts.Scope, + Contacts: opts.Contacts, + TOSURI: opts.TOSURI, + PolicyURI: opts.PolicyURI, + JWKsURI: opts.JWKsURI, + JWKs: opts.JWKs, + SoftwareID: opts.SoftwareID, + SoftwareVersion: opts.SoftwareVersion, + SubjectType: opts.SubjectType, + SectorIdentifierURI: opts.SectorIdentifierURI, + DefaultMaxAge: opts.DefaultMaxAge, + RequireAuthTime: opts.RequireAuthTime, + DefaultACRValues: opts.DefaultACRValues, + InitiateLoginURI: opts.InitiateLoginURI, + RequestURIs: opts.RequestURIs, + IDTokenSignedResponseAlg: opts.IDTokenSignedResponseAlg, + IDTokenEncryptedResponseAlg: opts.IDTokenEncryptedResponseAlg, + IDTokenEncryptedResponseEnc: opts.IDTokenEncryptedResponseEnc, + UserInfoSignedResponseAlg: opts.UserInfoSignedResponseAlg, + UserInfoEncryptedResponseAlg: opts.UserInfoEncryptedResponseAlg, + UserInfoEncryptedResponseEnc: opts.UserInfoEncryptedResponseEnc, + RequestObjectSigningAlg: opts.RequestObjectSigningAlg, + RequestObjectEncryptionAlg: opts.RequestObjectEncryptionAlg, + RequestObjectEncryptionEnc: opts.RequestObjectEncryptionEnc, + TokenEndpointAuthSigningAlg: opts.TokenEndpointAuthSigningAlg, + AccessTokenSigningAlg: opts.AccessTokenSigningAlg, + } + data, preparationErr := s.prepareDynamicRegistration(ctx, prepareDynamicRegistrationOptions{ + requestID: opts.RequestID, accountID: opts.AccountID, accountPublicID: uuid.Nil, + data: data, softwareStatement: opts.SoftwareStatement, iatDomain: opts.IATDomain, + backendDomain: opts.BackendDomain, frontendDomain: opts.FrontendDomain, app: true, + }) + if preparationErr != nil { + return dtos.AppDTO{}, preparationErr + } + opts.RedirectURIs = data.RedirectURIs + opts.TokenEndpointAuthMethod = data.TokenEndpointAuthMethod + opts.ResponseTypes = data.ResponseTypes + opts.GrantTypes = data.GrantTypes + opts.ApplicationType = data.ApplicationType + opts.ClientName = data.ClientName + opts.ClientURI = data.ClientURI + opts.LogoURI = data.LogoURI + opts.Scope = data.Scope + opts.Contacts = data.Contacts + opts.TOSURI = data.TOSURI + opts.PolicyURI = data.PolicyURI + opts.JWKsURI = data.JWKsURI + opts.JWKs = data.JWKs + opts.SoftwareID = data.SoftwareID + opts.SoftwareVersion = data.SoftwareVersion + opts.SubjectType = data.SubjectType + opts.SectorIdentifierURI = data.SectorIdentifierURI + opts.DefaultMaxAge = data.DefaultMaxAge + opts.RequireAuthTime = data.RequireAuthTime + opts.DefaultACRValues = data.DefaultACRValues + opts.InitiateLoginURI = data.InitiateLoginURI + opts.RequestURIs = data.RequestURIs + opts.IDTokenSignedResponseAlg = data.IDTokenSignedResponseAlg + opts.IDTokenEncryptedResponseAlg = data.IDTokenEncryptedResponseAlg + opts.IDTokenEncryptedResponseEnc = data.IDTokenEncryptedResponseEnc + opts.UserInfoSignedResponseAlg = data.UserInfoSignedResponseAlg + opts.UserInfoEncryptedResponseAlg = data.UserInfoEncryptedResponseAlg + opts.UserInfoEncryptedResponseEnc = data.UserInfoEncryptedResponseEnc + opts.RequestObjectSigningAlg = data.RequestObjectSigningAlg + opts.RequestObjectEncryptionAlg = data.RequestObjectEncryptionAlg + opts.RequestObjectEncryptionEnc = data.RequestObjectEncryptionEnc + opts.TokenEndpointAuthSigningAlg = data.TokenEndpointAuthSigningAlg + opts.AccessTokenSigningAlg = data.AccessTokenSigningAlg + appType, serviceErr := mapAppTypeToDB(opts.ApplicationType) if serviceErr != nil { logger.ErrorContext(ctx, "Failed to map application type", "serviceError", serviceErr) @@ -315,10 +439,7 @@ func (s *Services) CreateAppCredentialsRegistration( } transport := mapAppDRTransport(appType) - tokenEndpointAuthMethod, serviceErr := mapAppTokenEndpointAuthMethod( - opts.TokenEndpointAuthMethod, - appType, - ) + tokenEndpointAuthMethod, serviceErr := mapAuthMethod(opts.TokenEndpointAuthMethod) if serviceErr != nil { logger.ErrorContext(ctx, "Failed to map token endpoint auth method", "serviceError", serviceErr) return dtos.AppDTO{}, serviceErr @@ -407,7 +528,7 @@ func (s *Services) CreateAppCredentialsRegistration( return dtos.AppDTO{}, serviceErr } - baseDomain, serviceErr := s.checkClientRegistrationDomain(ctx, checkClientRegistrationDomainOptions{ + _, serviceErr = s.checkClientRegistrationDomain(ctx, checkClientRegistrationDomainOptions{ requestID: opts.RequestID, accountPublicID: accountDTO.PublicID, iatDomain: opts.IATDomain, @@ -447,85 +568,6 @@ func (s *Services) CreateAppCredentialsRegistration( usernameColumn := appDRConfigDTO.DefaultUsernameColumn authProviders := appDRConfigDTO.DefaultAuthProviders - data := ApplicationRegistrationData{ - RedirectURIs: opts.RedirectURIs, - TokenEndpointAuthMethod: opts.TokenEndpointAuthMethod, - ResponseTypes: opts.ResponseTypes, - GrantTypes: opts.GrantTypes, - ApplicationType: opts.ApplicationType, - ClientName: opts.ClientName, - ClientURI: opts.ClientURI, - LogoURI: opts.LogoURI, - Scope: opts.Scope, - Contacts: opts.Contacts, - TOSURI: opts.TOSURI, - PolicyURI: opts.PolicyURI, - JWKsURI: opts.JWKsURI, - JWKs: opts.JWKs, - SoftwareID: opts.SoftwareID, - SoftwareVersion: opts.SoftwareVersion, - SubjectType: opts.SubjectType, - SectorIdentifierURI: opts.SectorIdentifierURI, - DefaultMaxAge: opts.DefaultMaxAge, - RequireAuthTime: opts.RequireAuthTime, - DefaultACRValues: opts.DefaultACRValues, - InitiateLoginURI: opts.InitiateLoginURI, - RequestURIs: opts.RequestURIs, - IDTokenSignedResponseAlg: opts.IDTokenSignedResponseAlg, - IDTokenEncryptedResponseAlg: opts.IDTokenEncryptedResponseAlg, - IDTokenEncryptedResponseEnc: opts.IDTokenEncryptedResponseEnc, - UserInfoSignedResponseAlg: opts.UserInfoSignedResponseAlg, - UserInfoEncryptedResponseAlg: opts.UserInfoEncryptedResponseAlg, - UserInfoEncryptedResponseEnc: opts.UserInfoEncryptedResponseEnc, - RequestObjectSigningAlg: opts.RequestObjectSigningAlg, - RequestObjectEncryptionAlg: opts.RequestObjectEncryptionAlg, - RequestObjectEncryptionEnc: opts.RequestObjectEncryptionEnc, - TokenEndpointAuthSigningAlg: opts.TokenEndpointAuthSigningAlg, - AccessTokenSigningAlg: opts.AccessTokenSigningAlg, - } - var ssClaimsReference *tokens.SoftwareStatementClaims - if opts.SoftwareStatement != "" { - ssClaims, stdClaims, err := s.jwt.VerifySoftwareStatement(ctx, tokens.VerifySoftwareStatementOptions{ - RequestID: opts.RequestID, - SoftwareStatement: opts.SoftwareStatement, - GetPublicJWK: s.buildDynamicRegistrationSoftwareStatementFunc(ctx, buildDynamicRegistrationSoftwareStatementFuncOptions{ - requestID: opts.RequestID, - accountPublicID: accountDTO.PublicID, - verificationMethods: appDRConfigDTO.SoftwareStatementVerificationMethods, - jwksURI: opts.JWKsURI, - jwks: opts.JWKs, - domain: domain, - baseDomain: baseDomain, - }), - }) - if err != nil { - logger.WarnContext(ctx, "Failed to verify software statement", "error", err) - return dtos.AppDTO{}, exceptions.NewInvalidTokenError("invalid software statement") - } - if serviceErr := s.verifySoftwareStatementSTDClaims(ctx, verifySoftwareStatementSTDClaimsOptions{ - requestID: opts.RequestID, - backendDomain: opts.BackendDomain, - frontendDomain: opts.FrontendDomain, - domain: domain, - baseDomain: baseDomain, - claims: &stdClaims, - }); serviceErr != nil { - logger.WarnContext(ctx, "Failed to verify software statement standard claims", "serviceError", serviceErr) - return dtos.AppDTO{}, serviceErr - } - - if serviceErr := s.validateSoftwareStatementClaims(ctx, validateSoftwareStatementClaimsOptions{ - requestID: opts.RequestID, - claims: &ssClaims, - allowedScopes: utils.SliceToHashSet(allowedAppScopes), - }); serviceErr != nil { - logger.WarnContext(ctx, "Failed to validate software statement claims", "serviceError", serviceErr) - return dtos.AppDTO{}, serviceErr - } - - ssClaimsReference = &ssClaims - } - params, serviceErr := s.mapAppRegistrationDataToDBParams(ctx, mapAppRegistrationDataToDBParamsOptions{ appType: appType, accountPublicID: accountDTO.PublicID, @@ -542,22 +584,21 @@ func (s *Services) CreateAppCredentialsRegistration( usernameColumn: usernameColumn, authProviders: authProviders, data: &data, - claims: ssClaimsReference, }) if serviceErr != nil { logger.ErrorContext(ctx, "Failed to map app registration data to database params", "serviceError", serviceErr) return dtos.AppDTO{}, serviceErr } - if tokenEndpointAuthMethod == database.AuthMethodNone { - app, err := s.database.CreateApp(ctx, params) + if tokenEndpointAuthMethod == database.AuthMethodNone || (tokenEndpointAuthMethod == database.AuthMethodPrivateKeyJwt && (data.JWKs != nil || data.JWKsURI != "")) { + app, err := s.database.CreateRegisteredApp(ctx, params) if err != nil { logger.ErrorContext(ctx, "Failed to create app", "error", err) return dtos.AppDTO{}, exceptions.FromDBError(err) } logger.InfoContext(ctx, "Created app successfully") - return dtos.MapAppToDTO(&app), nil + return s.finalizeRegisteredApp(ctx, opts, accountDTO, &app, "", time.Time{}, nil) } qrs, txn, err := s.database.BeginTx(ctx) @@ -570,7 +611,7 @@ func (s *Services) CreateAppCredentialsRegistration( s.database.FinalizeTx(ctx, txn, err, serviceErr) }() - app, err := s.database.CreateApp(ctx, params) + app, err := qrs.CreateRegisteredApp(ctx, params) if err != nil { logger.ErrorContext(ctx, "Failed to create app", "error", err) return dtos.AppDTO{}, exceptions.FromDBError(err) @@ -612,15 +653,15 @@ func (s *Services) CreateAppCredentialsRegistration( } if appType == database.AppTypeBackend || appType == database.AppTypeService { - return dtos.MapBackendAppWithJWKToDTO(&app, jwk, dbPrms.ExpiresAt), nil + return s.finalizeRegisteredApp(ctx, opts, accountDTO, &app, "", dbPrms.ExpiresAt, jwk) } - return dtos.MapWebAppWithJWKToDTO(&app, jwk, dbPrms.ExpiresAt), nil + return s.finalizeRegisteredApp(ctx, opts, accountDTO, &app, "", dbPrms.ExpiresAt, jwk) case database.AuthMethodClientSecretBasic, database.AuthMethodClientSecretPost, database.AuthMethodClientSecretJwt: var ccID int32 - var secretID, secret string + var secret string var exp time.Time - ccID, secretID, secret, exp, serviceErr = s.clientCredentialsSecret(ctx, qrs, clientCredentialsSecretOptions{ + ccID, _, secret, exp, serviceErr = s.clientCredentialsSecret(ctx, qrs, clientCredentialsSecretOptions{ requestID: opts.RequestID, accountID: opts.AccountID, storageMode: mapCCSecretStorageMode(string(tokenEndpointAuthMethod)), @@ -647,13 +688,38 @@ func (s *Services) CreateAppCredentialsRegistration( } if appType == database.AppTypeBackend || appType == database.AppTypeService { - return dtos.MapBackendAppWithSecretToDTO(&app, secretID, secret, exp), nil + return s.finalizeRegisteredApp(ctx, opts, accountDTO, &app, secret, exp, nil) } - return dtos.MapWebAppWithSecretToDTO(&app, secretID, secret, exp), nil + return s.finalizeRegisteredApp(ctx, opts, accountDTO, &app, secret, exp, nil) default: logger.ErrorContext(ctx, "Invalid token endpoint auth method", "tokenEndpointAuthMethod", tokenEndpointAuthMethod) serviceErr = exceptions.NewInternalServerError() return dtos.AppDTO{}, serviceErr } } + +func (s *Services) finalizeRegisteredApp( + ctx context.Context, + opts CreateAppCredentialsRegistrationOptions, + accountDTO dtos.AccountDTO, + app *database.App, + secret string, + expiry time.Time, + key utils.JWK, +) (dtos.AppDTO, *exceptions.ServiceError) { + dto := dtos.MapRegisteredApp(app, opts.SoftwareStatement, secret, expiry, key) + token, serviceErr := s.CreateAppCredentialsRegistrationAccessToken(ctx, CreateAppCredentialsRegistrationAccessTokenOptions{ + RequestID: opts.RequestID, + AccountPublicID: accountDTO.PublicID, + AccountVersion: accountDTO.Version(), + ClientID: app.ClientID, + BackendDomain: opts.BackendDomain, + }) + if serviceErr != nil { + return dtos.AppDTO{}, serviceErr + } + issuer := accountDTO.Username + "." + opts.BackendDomain + dto.Registration.WithRegistrationAccess(token, dtos.RegistrationClientURI(issuer, app.ClientID)) + return dto, nil +} diff --git a/idp/internal/services/app_dynamic_registration_iat.go b/idp/internal/services/app_dynamic_registration_iat.go index 91fbaff..bd24463 100644 --- a/idp/internal/services/app_dynamic_registration_iat.go +++ b/idp/internal/services/app_dynamic_registration_iat.go @@ -67,8 +67,10 @@ func (s *Services) CreateAppCredentialsRegistrationIAT( AccountPublicID: opts.AccountPublicID, AccountVersion: opts.AccountVersion, IssuerDomain: fmt.Sprintf("%s.%s", accountDTO.Username, opts.BackendDomain), - Domain: opts.Domain, - ClientID: utils.Base62UUID(), + Subject: opts.Domain, + JTI: utils.Base62UUID(), + Usage: tokens.DynamicRegistrationUsageApp, + TokenUse: tokens.DynamicRegistrationTokenUseInitialAccess, }), GetJWKfn: s.BuildGetEncryptedAccountJWKFn(ctx, BuildGetEncryptedAccountJWKFnOptions{ RequestID: opts.RequestID, @@ -122,6 +124,8 @@ func (s *Services) ProcessAppDynamicRegistrationIATAuth( RequestID: opts.RequestID, IAT: token, IssuerDomain: opts.IssuerDomain, + Usage: tokens.DynamicRegistrationUsageApp, + TokenUse: tokens.DynamicRegistrationTokenUseInitialAccess, GetPublicJWK: s.BuildGetAccountPublicKeyFn( ctx, BuildGetAccountPublicKeyFnOptions{ @@ -141,3 +145,115 @@ func (s *Services) ProcessAppDynamicRegistrationIATAuth( logger.InfoContext(ctx, "Processed OAuth dynamic registration IAT auth successfully") return domain, accountClaims, nil } + +type CreateAppCredentialsRegistrationAccessTokenOptions struct { + RequestID string + AccountPublicID uuid.UUID + AccountVersion int32 + ClientID string + BackendDomain string +} + +func (s *Services) CreateAppCredentialsRegistrationAccessToken( + ctx context.Context, + opts CreateAppCredentialsRegistrationAccessTokenOptions, +) (string, *exceptions.ServiceError) { + logger := s.buildLogger(opts.RequestID, appDynamicRegistrationIATLocation, "CreateAppCredentialsRegistrationAccessToken").With( + "accountPublicId", opts.AccountPublicID, + "clientId", opts.ClientID, + ) + logger.InfoContext(ctx, "Creating app registration access token...") + + accountDTO, serviceErr := s.GetAccountByPublicIDAndVersion(ctx, GetAccountByPublicIDAndVersionOptions{ + RequestID: opts.RequestID, + PublicID: opts.AccountPublicID, + Version: opts.AccountVersion, + }) + if serviceErr != nil { + logger.ErrorContext(ctx, "Failed to get account", "serviceError", serviceErr) + return "", serviceErr + } + + accountID := accountDTO.ID() + signedToken, serviceErr := s.crypto.SignToken(ctx, crypto.SignTokenOptions{ + RequestID: opts.RequestID, + Token: s.jwt.DynamicRegistrationAccessToken(tokens.DynamicRegistrationIATOptions{ + AccountPublicID: opts.AccountPublicID, + AccountVersion: opts.AccountVersion, + IssuerDomain: fmt.Sprintf("%s.%s", accountDTO.Username, opts.BackendDomain), + Subject: opts.ClientID, + JTI: utils.Base62UUID(), + Usage: tokens.DynamicRegistrationUsageApp, + TokenUse: tokens.DynamicRegistrationTokenUseRegistration, + TTL: s.jwt.GetRegistrationAccessTokenTTL(), + }), + GetJWKfn: s.BuildGetEncryptedAccountJWKFn(ctx, BuildGetEncryptedAccountJWKFnOptions{ + RequestID: opts.RequestID, + KeyType: database.TokenKeyTypeDynamicRegistration, + AccountID: accountID, + }), + GetDecryptDEKfn: s.BuildGetDecAccountDEKFn(ctx, BuildGetDecAccountDEKFnOptions{ + RequestID: opts.RequestID, + AccountID: accountID, + }), + GetEncryptDEKfn: s.BuildGetEncAccountDEKfn(ctx, BuildGetEncAccountDEKOptions{ + RequestID: opts.RequestID, + AccountID: accountID, + }), + StoreFN: s.BuildUpdateJWKDEKFn(ctx, BuildUpdateJWKDEKFnOptions{ + RequestID: opts.RequestID, + }), + }) + if serviceErr != nil { + logger.ErrorContext(ctx, "Failed to sign app registration access token", "serviceError", serviceErr) + return "", serviceErr + } + + return signedToken, nil +} + +type ProcessAppDynamicRegistrationAccessTokenOptions struct { + RequestID string + AuthHeader string + AccountID int32 + IssuerDomain string +} + +func (s *Services) ProcessAppDynamicRegistrationAccessToken( + ctx context.Context, + opts ProcessAppDynamicRegistrationAccessTokenOptions, +) (string, tokens.AccountClaims, *exceptions.ServiceError) { + logger := s.buildLogger(opts.RequestID, appDynamicRegistrationIATLocation, "ProcessAppDynamicRegistrationAccessToken") + logger.InfoContext(ctx, "Processing app registration access token...") + + token, serviceErr := extractAuthHeaderToken(opts.AuthHeader) + if serviceErr != nil { + logger.WarnContext(ctx, "Failed to extract token from auth header", "serviceError", serviceErr) + return "", tokens.AccountClaims{}, serviceErr + } + + clientID, accountClaims, err := s.jwt.VerifyDynamicRegistrationIAT( + ctx, + tokens.VerifyDynamicRegistrationIATOptions{ + RequestID: opts.RequestID, + IAT: token, + IssuerDomain: opts.IssuerDomain, + Usage: tokens.DynamicRegistrationUsageApp, + TokenUse: tokens.DynamicRegistrationTokenUseRegistration, + GetPublicJWK: s.BuildGetAccountPublicKeyFn( + ctx, + BuildGetAccountPublicKeyFnOptions{ + RequestID: opts.RequestID, + AccountID: opts.AccountID, + KeyType: database.TokenKeyTypeDynamicRegistration, + }, + ), + }, + ) + if err != nil { + logger.WarnContext(ctx, "Failed to verify app registration access token", "error", err) + return "", tokens.AccountClaims{}, exceptions.NewUnauthorizedError() + } + + return clientID, accountClaims, nil +} diff --git a/idp/internal/services/dtos/account_credentials.go b/idp/internal/services/dtos/account_credentials.go index 7d4e667..08392c6 100644 --- a/idp/internal/services/dtos/account_credentials.go +++ b/idp/internal/services/dtos/account_credentials.go @@ -17,8 +17,9 @@ import ( ) type AccountCredentialsDTO struct { - ClientID string `json:"client_id"` - ClientIDIAT int64 `json:"client_idiat"` + Registration *ClientRegistrationDTO `json:"-"` + ClientID string `json:"client_id"` + ClientIDIAT int64 `json:"client_idiat"` Type database.AccountCredentialsType `json:"application_type"` ClientName string `json:"client_name"` @@ -125,17 +126,11 @@ func MapAccountCredentialsToDTO( jwks := make([]utils.JWK, 0) if accountCredential.Jwks != nil { - var rawJwks []json.RawMessage - if err := json.Unmarshal(accountCredential.Jwks, &rawJwks); err != nil { + var set utils.JWKSet + if err := json.Unmarshal(accountCredential.Jwks, &set); err != nil { return AccountCredentialsDTO{}, exceptions.NewInternalServerError() } - for _, raw := range rawJwks { - jwk, err := utils.JsonToJWK(raw) - if err != nil { - return AccountCredentialsDTO{}, exceptions.NewInternalServerError() - } - jwks = append(jwks, jwk) - } + jwks = set.Keys } return AccountCredentialsDTO{ @@ -193,17 +188,11 @@ func MapAccountCredentialsToDTOWithJWK( jwks := make([]utils.JWK, 0) if accountCredential.Jwks != nil { - var rawJwks []json.RawMessage - if err := json.Unmarshal(accountCredential.Jwks, &rawJwks); err != nil { + var set utils.JWKSet + if err := json.Unmarshal(accountCredential.Jwks, &set); err != nil { return AccountCredentialsDTO{}, exceptions.NewInternalServerError() } - for _, raw := range rawJwks { - jwk, err := utils.JsonToJWK(raw) - if err != nil { - return AccountCredentialsDTO{}, exceptions.NewInternalServerError() - } - jwks = append(jwks, jwk) - } + jwks = set.Keys } return AccountCredentialsDTO{ @@ -265,17 +254,11 @@ func MapAccountCredentialsToDTOWithSecret( jwks := make([]utils.JWK, 0) if accountCredential.Jwks != nil { - var rawJwks []json.RawMessage - if err := json.Unmarshal(accountCredential.Jwks, &rawJwks); err != nil { + var set utils.JWKSet + if err := json.Unmarshal(accountCredential.Jwks, &set); err != nil { return AccountCredentialsDTO{}, exceptions.NewInternalServerError() } - for _, raw := range rawJwks { - jwk, err := utils.JsonToJWK(raw) - if err != nil { - return AccountCredentialsDTO{}, exceptions.NewInternalServerError() - } - jwks = append(jwks, jwk) - } + jwks = set.Keys } return AccountCredentialsDTO{ diff --git a/idp/internal/services/dtos/app.go b/idp/internal/services/dtos/app.go index 2413cfc..f6fa7ed 100644 --- a/idp/internal/services/dtos/app.go +++ b/idp/internal/services/dtos/app.go @@ -37,9 +37,10 @@ func newRelatedAppDTO( } type AppDTO struct { - id int32 - accountID int32 - version int32 + Registration *ClientRegistrationDTO `json:"-"` + id int32 + accountID int32 + version int32 AppType database.AppType `json:"app_type"` ClientName string `json:"client_name"` diff --git a/idp/internal/services/dtos/client_registration.go b/idp/internal/services/dtos/client_registration.go new file mode 100644 index 0000000..8b99a29 --- /dev/null +++ b/idp/internal/services/dtos/client_registration.go @@ -0,0 +1,200 @@ +package dtos + +import ( + "encoding/json" + "fmt" + "strings" + "time" + + "github.com/tugascript/devlogs/idp/internal/controllers/paths" + "github.com/tugascript/devlogs/idp/internal/exceptions" + "github.com/tugascript/devlogs/idp/internal/providers/database" + "github.com/tugascript/devlogs/idp/internal/providers/tokens" + "github.com/tugascript/devlogs/idp/internal/utils" +) + +// ClientRegistrationDTO is the RFC 7591 wire representation, independent of management DTOs. +type ClientRegistrationDTO struct { + tokens.SoftwareStatementClaims + ClientID string `json:"client_id"` + ClientIDIssuedAt int64 `json:"client_id_issued_at"` + ClientSecret string `json:"client_secret,omitempty"` + ClientSecretExpiresAt *int64 `json:"client_secret_expires_at,omitempty"` + SoftwareStatement string `json:"software_statement,omitempty"` + ClientSecretJWK utils.JWK `json:"client_secret_jwk,omitempty"` + RegistrationAccessToken string `json:"registration_access_token,omitempty"` + RegistrationClientURI string `json:"registration_client_uri,omitempty"` +} + +func (r ClientRegistrationDTO) MarshalJSON() ([]byte, error) { + type wire ClientRegistrationDTO + raw, err := json.Marshal(wire(r)) + if err != nil { + return nil, err + } + var fields map[string]json.RawMessage + if err := json.Unmarshal(raw, &fields); err != nil { + return nil, err + } + fields["response_types"], err = json.Marshal(r.ResponseTypes) + if err != nil { + return nil, err + } + return json.Marshal(fields) +} + +func RegistrationClientURI(issuerDomain, clientID string) string { + return fmt.Sprintf("https://%s%s%s%s%s/%s", issuerDomain, paths.V1, paths.AuthBase, paths.OAuthBase, paths.OAuthRegister, clientID) +} + +func (r *ClientRegistrationDTO) WithRegistrationAccess(token, clientURI string) *ClientRegistrationDTO { + if r == nil { + return r + } + r.RegistrationAccessToken = token + r.RegistrationClientURI = clientURI + return r +} + +func (r *ClientRegistrationDTO) WithoutSecrets() *ClientRegistrationDTO { + if r == nil { + return r + } + clone := *r + clone.ClientSecret = "" + clone.ClientSecretExpiresAt = nil + clone.ClientSecretJWK = nil + clone.RegistrationAccessToken = "" + return &clone +} + +func registrationFromApp(row *database.App, statement, secret string, expiry time.Time, key utils.JWK) *ClientRegistrationDTO { + metadata := tokens.SoftwareStatementClaims{ + RedirectURIs: row.RedirectUris, + TokenEndpointAuthMethod: string(row.TokenEndpointAuthMethod), + GrantTypes: utils.MapSlice(row.GrantTypes, func(v *database.GrantType) string { return string(*v) }), + ResponseTypes: utils.MapSlice(row.ResponseTypes, func(v *database.ResponseType) string { return string(*v) }), + ApplicationType: string(row.AppType), + ClientName: row.ClientName, + ClientURI: row.ClientUri, + LogoURI: row.LogoUri.String, + Scope: strings.Join(mapScopes(row.Scopes, row.CustomScopes), " "), + Contacts: row.Contacts, + TOSURI: row.TosUri.String, + PolicyURI: row.PolicyUri.String, + JWKsURI: row.JwksUri.String, + SoftwareID: row.SoftwareID.String, + SoftwareVersion: row.SoftwareVersion.String, + SubjectType: string(row.SubjectType.ClientSubjectType), + SectorIdentifierURI: row.SectorIdentifierUri.String, + DefaultMaxAge: int64(row.DefaultMaxAge.Int32), + RequireAuthTime: row.RequireAuthTime, + DefaultACRValues: row.DefaultAcrValues, + InitiateLoginURI: row.InitiateLoginUri.String, + RequestURIs: row.RequestUris, + IDTokenSignedResponseAlg: string(row.IDTokenSignedResponseAlg), + IDTokenEncryptedResponseAlg: string(row.IDTokenEncryptedResponseAlg.TokenEncryptionAlgorithm), + IDTokenEncryptedResponseEnc: string(row.IDTokenEncryptedResponseEnc.TokenEncryptionEncoding), + UserInfoSignedResponseAlg: string(row.UserinfoSignedResponseAlg.TokenCryptoSuite), + UserInfoEncryptedResponseAlg: string(row.UserinfoEncryptedResponseAlg.TokenEncryptionAlgorithm), + UserInfoEncryptedResponseEnc: string(row.UserinfoEncryptedResponseEnc.TokenEncryptionEncoding), + RequestObjectSigningAlg: string(row.RequestObjectSigningAlg.TokenCryptoSuite), + RequestObjectEncryptionAlg: string(row.RequestObjectEncryptionAlg.TokenEncryptionAlgorithm), + RequestObjectEncryptionEnc: string(row.RequestObjectEncryptionEnc.TokenEncryptionEncoding), + TokenEndpointAuthSigningAlg: string(row.TokenEndpointAuthSigningAlg.TokenCryptoSuite), + AccessTokenSigningAlg: string(row.AccessTokenSigningAlg), + } + if len(row.Jwks) > 0 { + var set utils.JWKSet + if json.Unmarshal(row.Jwks, &set) == nil { + metadata.JWKs = &set + } + } + result := &ClientRegistrationDTO{SoftwareStatementClaims: metadata, ClientID: row.ClientID, ClientIDIssuedAt: row.CreatedAt.Unix(), SoftwareStatement: statement, ClientSecret: secret, ClientSecretJWK: key} + if secret != "" { + var exp int64 + if !expiry.IsZero() { + exp = expiry.Unix() + } + result.ClientSecretExpiresAt = &exp + } + return result +} + +func registrationFromAccountCredential(row *database.AccountCredential, statement, secret string, expiry time.Time, key utils.JWK) *ClientRegistrationDTO { + metadata := tokens.SoftwareStatementClaims{ + RedirectURIs: row.RedirectUris, + TokenEndpointAuthMethod: string(row.TokenEndpointAuthMethod), + GrantTypes: utils.MapSlice(row.GrantTypes, func(v *database.GrantType) string { return string(*v) }), + ResponseTypes: utils.MapSlice(row.ResponseTypes, func(v *database.ResponseType) string { return string(*v) }), + ApplicationType: string(row.CredentialsType), + ClientName: row.ClientName, + ClientURI: row.ClientUri, + LogoURI: row.LogoUri.String, + Scope: strings.Join(utils.MapSlice(row.Scopes, func(s *database.AccountCredentialsScope) string { return string(*s) }), " "), + Contacts: row.Contacts, + TOSURI: row.TosUri.String, + PolicyURI: row.PolicyUri.String, + JWKsURI: row.JwksUri.String, + SoftwareID: row.SoftwareID.String, + SoftwareVersion: row.SoftwareVersion.String, + SubjectType: string(row.SubjectType.ClientSubjectType), + SectorIdentifierURI: row.SectorIdentifierUri.String, + DefaultMaxAge: int64(row.DefaultMaxAge.Int64), + RequireAuthTime: row.RequireAuthTime, + DefaultACRValues: row.DefaultAcrValues, + InitiateLoginURI: row.InitiateLoginUri.String, + RequestURIs: row.RequestUris, + IDTokenSignedResponseAlg: string(row.IDTokenSignedResponseAlg), + IDTokenEncryptedResponseAlg: string(row.IDTokenEncryptedResponseAlg.TokenEncryptionAlgorithm), + IDTokenEncryptedResponseEnc: string(row.IDTokenEncryptedResponseEnc.TokenEncryptionEncoding), + UserInfoSignedResponseAlg: string(row.UserinfoSignedResponseAlg.TokenCryptoSuite), + UserInfoEncryptedResponseAlg: string(row.UserinfoEncryptedResponseAlg.TokenEncryptionAlgorithm), + UserInfoEncryptedResponseEnc: string(row.UserinfoEncryptedResponseEnc.TokenEncryptionEncoding), + RequestObjectSigningAlg: string(row.RequestObjectSigningAlg.TokenCryptoSuite), + RequestObjectEncryptionAlg: string(row.RequestObjectEncryptionAlg.TokenEncryptionAlgorithm), + RequestObjectEncryptionEnc: string(row.RequestObjectEncryptionEnc.TokenEncryptionEncoding), + TokenEndpointAuthSigningAlg: string(row.TokenEndpointAuthSigningAlg.TokenCryptoSuite), + AccessTokenSigningAlg: string(row.AccessTokenSigningAlg), + } + if len(row.Jwks) > 0 { + var set utils.JWKSet + if json.Unmarshal(row.Jwks, &set) == nil { + metadata.JWKs = &set + } + } + result := &ClientRegistrationDTO{SoftwareStatementClaims: metadata, ClientID: row.ClientID, ClientIDIssuedAt: row.CreatedAt.Unix(), SoftwareStatement: statement, ClientSecret: secret, ClientSecretJWK: key} + if secret != "" { + var exp int64 + if !expiry.IsZero() { + exp = expiry.Unix() + } + result.ClientSecretExpiresAt = &exp + } + return result +} + +func MapRegisteredApp(row *database.App, statement, secret string, expiry time.Time, key utils.JWK) AppDTO { + dto := MapAppToDTO(row) + dto.ClientSecret = secret + dto.ClientSecretJWK = key + if !expiry.IsZero() { + dto.ClientSecretExp = expiry.Unix() + } + dto.Registration = registrationFromApp(row, statement, secret, expiry, key) + return dto +} + +func MapRegisteredAccountCredentials(row *database.AccountCredential, statement, secret string, expiry time.Time, key utils.JWK) (AccountCredentialsDTO, *exceptions.ServiceError) { + dto, err := MapAccountCredentialsToDTO(row) + if err != nil { + return dto, err + } + dto.ClientSecret = secret + dto.ClientSecretJWK = key + if !expiry.IsZero() { + dto.ClientSecretExp = expiry.Unix() + } + dto.Registration = registrationFromAccountCredential(row, statement, secret, expiry, key) + return dto, nil +} diff --git a/idp/internal/services/dtos/well_known.go b/idp/internal/services/dtos/well_known.go index 2895034..b504741 100644 --- a/idp/internal/services/dtos/well_known.go +++ b/idp/internal/services/dtos/well_known.go @@ -74,7 +74,7 @@ func MapOIDCConfigDTOToWellKnownOIDCConfigurationDTO(configDTO *OIDCConfigDTO, b AuthEndpoint: baseURL + paths.AppsBase + paths.OAuthBase + paths.OAuthAuth, TokenEndpoint: baseURL + paths.AppsBase + paths.OAuthBase + paths.OAuthToken, UserinfoEndpoint: baseURL + paths.AppsBase + paths.OAuthBase + paths.OAuthUserInfo, - RegistrationEndpoint: baseURL + paths.AppsBase + paths.UsersBase + paths.AuthRegister, + RegistrationEndpoint: baseURL + paths.V1 + paths.AuthBase + paths.OAuthBase + paths.OAuthRegister, RevocationEndpoint: baseURL + paths.AppsBase + paths.OAuthBase + paths.OAuthRevoke, IntrospectionEndpoint: baseURL + paths.AppsBase + paths.OAuthBase + paths.OAuthIntrospect, DeviceAuthorizationEndpoint: baseURL + paths.AppsBase + paths.OAuthBase + paths.OAuthDeviceAuth, diff --git a/idp/internal/services/dynamic_registration_domains.go b/idp/internal/services/dynamic_registration_domains.go index 94467e6..1daa030 100644 --- a/idp/internal/services/dynamic_registration_domains.go +++ b/idp/internal/services/dynamic_registration_domains.go @@ -889,34 +889,19 @@ func (s *Services) checkClientRegistrationDomain( return "", exceptions.NewUnauthorizedError() } - var count int64 + domains := []string{opts.domain} if baseDomain != opts.domain { - if opts.requireVerifiedDomains { - count, err = s.database.CountVerifiedDynamicRegistrationDomainsByDomainsAccountPublicIDAndUsages( - ctx, - database.CountVerifiedDynamicRegistrationDomainsByDomainsAccountPublicIDAndUsagesParams{ - AccountPublicID: opts.accountPublicID, - Usages: opts.usages, - Domains: []string{opts.domain, baseDomain}, - }, - ) - } else { - count, err = s.database.CountDynamicRegistrationDomainsByDomainsAccountPublicIDAndUsages( - ctx, - database.CountDynamicRegistrationDomainsByDomainsAccountPublicIDAndUsagesParams{ - AccountPublicID: opts.accountPublicID, - Usages: opts.usages, - Domains: []string{opts.domain, baseDomain}, - }, - ) - } - } else { + domains = append(domains, baseDomain) + } + + var count int64 + for _, domain := range domains { if opts.requireVerifiedDomains { count, err = s.database.CountVerifiedDynamicRegistrationDomainsByDomainAccountPublicIDAndUsages( ctx, database.CountVerifiedDynamicRegistrationDomainsByDomainAccountPublicIDAndUsagesParams{ AccountPublicID: opts.accountPublicID, - Domain: opts.domain, + Domain: domain, Usages: opts.usages, }, ) @@ -925,11 +910,14 @@ func (s *Services) checkClientRegistrationDomain( ctx, database.CountDynamicRegistrationDomainsByDomainAndAccountPublicIDAndUsagesParams{ AccountPublicID: opts.accountPublicID, - Domain: opts.domain, - Usages: appDynamicRegistrationUsages, + Domain: domain, + Usages: opts.usages, }, ) } + if err != nil || count > 0 { + break + } } if err != nil { diff --git a/idp/internal/services/oauth_dynamic_registration.go b/idp/internal/services/oauth_dynamic_registration.go index cce0ab0..29df0c4 100644 --- a/idp/internal/services/oauth_dynamic_registration.go +++ b/idp/internal/services/oauth_dynamic_registration.go @@ -12,12 +12,10 @@ import ( "slices" "github.com/google/uuid" - "golang.org/x/net/publicsuffix" "github.com/tugascript/devlogs/idp/internal/controllers/paths" "github.com/tugascript/devlogs/idp/internal/exceptions" "github.com/tugascript/devlogs/idp/internal/providers/cache" - "github.com/tugascript/devlogs/idp/internal/providers/crypto" "github.com/tugascript/devlogs/idp/internal/providers/database" "github.com/tugascript/devlogs/idp/internal/providers/mailer" "github.com/tugascript/devlogs/idp/internal/providers/oauth" @@ -34,6 +32,51 @@ const ( oauthDynamicRegistrationIATAuthPath string = oauthDynamicRegistrationIATPath + paths.OAuthAuth ) +func dynamicRegistrationIssuerDomain(hostUsername, backendDomain string) string { + if hostUsername == "" { + return backendDomain + } + return hostUsername + "." + backendDomain +} + +func oauthDynamicRegistrationIATExtCallbackURL(issuerDomain, accClientID, provider string) string { + return "https://" + issuerDomain + oauthDynamicRegistrationIATPath + "/" + accClientID + + paths.InitialAccessTokenAuthEXT + "/" + provider + paths.InitialAccessTokenCallback +} + +func (s *Services) checkIATRegistrationDomain( + ctx context.Context, + requestID string, + hostUsername string, + accountPublicID uuid.UUID, + domain string, +) *exceptions.ServiceError { + if hostUsername != "" { + _, serviceErr := s.GetAppCredentialsRegistrationDomain(ctx, GetAppCredentialsRegistrationDomainOptions{ + RequestID: requestID, + AccountPublicID: accountPublicID, + Domain: domain, + }) + return serviceErr + } + domainDTO, serviceErr := s.GetAccountCredentialsRegistrationDomain(ctx, GetAccountCredentialsRegistrationDomainOptions{ + RequestID: requestID, + AccountPublicID: accountPublicID, + Domain: domain, + }) + if serviceErr != nil { + return serviceErr + } + if !domainDTO.Verified { + return exceptions.NewForbiddenError() + } + return nil +} + +func hostMatchesAccount(hostUsername, accountUsername string) bool { + return hostUsername == "" || hostUsername == accountUsername +} + type buildOAuthDynamicRegistrationIATLoginURLOptions struct { accClientID string domain string @@ -47,6 +90,7 @@ func buildOAuthDynamicRegistrationIATLoginURL(opts buildOAuthDynamicRegistration queryParams := make(url.Values) queryParams.Add("client_id", opts.domain) queryParams.Add("response_type", "code") + queryParams.Add("redirect_uri", opts.redirectURI) queryParams.Add("state", opts.state) queryParams.Add("code_challenge", opts.challenge) if opts.challengeMethod != "" { @@ -56,21 +100,22 @@ func buildOAuthDynamicRegistrationIATLoginURL(opts buildOAuthDynamicRegistration } type buildOAuthDynamicRegistrationIATCallbackURLOptions struct { - redirectURI string - code string - state string - backendDomain string + redirectURI string + code string + state string + issuerDomain string } func buildOAuthDynamicRegistrationIATCallbackURL(opts buildOAuthDynamicRegistrationIATCallbackURLOptions) string { queryParams := make(url.Values) queryParams.Add("code", opts.code) queryParams.Add("state", opts.state) - queryParams.Add("iss", "https://"+opts.backendDomain) + queryParams.Add("iss", "https://"+opts.issuerDomain) return opts.redirectURI + "?" + queryParams.Encode() } type generateOAuthDynamicRegistrationIATCallbackOptions struct { + hostUsername string requestID string clientID string accountPublicID uuid.UUID @@ -99,6 +144,7 @@ func (s *Services) generateOAuthDynamicRegistrationIATCallback( code, err := s.cache.GenerateAccountCredentialsRegistrationIATCode( ctx, cache.GenerateAccountCredentialsRegistrationIATCodeOptions{ + HostUsername: opts.hostUsername, RequestID: opts.requestID, ClientID: opts.clientID, AccountPublicID: opts.accountPublicID, @@ -113,10 +159,10 @@ func (s *Services) generateOAuthDynamicRegistrationIATCallback( } return buildOAuthDynamicRegistrationIATCallbackURL(buildOAuthDynamicRegistrationIATCallbackURLOptions{ - redirectURI: opts.redirectURI, - code: code, - state: opts.state, - backendDomain: opts.backendDomain, + redirectURI: opts.redirectURI, + code: code, + state: opts.state, + issuerDomain: dynamicRegistrationIssuerDomain(opts.hostUsername, opts.backendDomain), }), nil } @@ -206,6 +252,7 @@ func (s *Services) oauthDynamicRegistrationIATAuth( checkDynamicClientRegistrationDomainUsabilityOptions{ requestID: opts.requestID, accountUsername: opts.hostUsername, + domain: opts.domain, }, ); serviceErr != nil { logger.InfoContext(ctx, "Dynamic registration domain not usable", "serviceError", serviceErr) @@ -245,6 +292,7 @@ func (s *Services) oauthDynamicRegistrationIATAuth( } type refreshTokenOAuthDynamicRegistrationIATLoginOptions struct { + hostUsername string requestID string refreshToken string challenge string @@ -330,6 +378,19 @@ func (s *Services) refreshTokenOAuthDynamicRegistrationIATLogin( logger.WarnContext(ctx, "Account not found or version mismatch", "serviceError", serviceErr) return s.oauthDynamicRegistrationIATAuth(ctx, oauthDynamicRegistrationIATAuthOptions{ + hostUsername: opts.hostUsername, + requestID: opts.requestID, + challenge: opts.challenge, + challengeMethod: opts.challengeMethod, + domain: opts.domain, + redirectURI: opts.redirectURI, + state: opts.state, + }) + } + if !hostMatchesAccount(opts.hostUsername, accountDTO.Username) { + logger.WarnContext(ctx, "Refresh token account does not match host") + return s.oauthDynamicRegistrationIATAuth(ctx, oauthDynamicRegistrationIATAuthOptions{ + hostUsername: opts.hostUsername, requestID: opts.requestID, challenge: opts.challenge, challengeMethod: opts.challengeMethod, @@ -348,6 +409,7 @@ func (s *Services) refreshTokenOAuthDynamicRegistrationIATLogin( cbURL, serviceErr := s.generateOAuthDynamicRegistrationIATCallback( ctx, generateOAuthDynamicRegistrationIATCallbackOptions{ + hostUsername: opts.hostUsername, requestID: opts.requestID, clientID: utils.Base62UUID(), accountPublicID: accountDTO.PublicID, @@ -411,6 +473,7 @@ func (s *Services) InitiateOAuthDynamicRegistrationIATAuth( return s.refreshTokenOAuthDynamicRegistrationIATLogin( ctx, refreshTokenOAuthDynamicRegistrationIATLoginOptions{ + hostUsername: opts.HostUsername, requestID: opts.RequestID, refreshToken: opts.RefreshToken, challenge: opts.Challenge, @@ -440,6 +503,10 @@ func (s *Services) InitiateOAuthDynamicRegistrationIATAuth( return "", exceptions.NewUnauthorizedError() } + if data.Username != opts.HostUsername { + return "", exceptions.NewUnauthorizedError() + } + if !verified { logger.WarnContext(ctx, "Account credentials registration session key is not verified") return "", exceptions.NewUnauthorizedError() @@ -476,6 +543,7 @@ func (s *Services) InitiateOAuthDynamicRegistrationIATAuth( cbURL, serviceErr := s.generateOAuthDynamicRegistrationIATCallback( ctx, generateOAuthDynamicRegistrationIATCallbackOptions{ + hostUsername: opts.HostUsername, requestID: opts.RequestID, clientID: credsClientID, accountPublicID: accountDTO.PublicID, @@ -647,6 +715,7 @@ type OAuthDynamicRegistrationIATLoginOptions struct { Email string Password string BackendDomain string + HostUsername string } func (s *Services) OAuthDynamicRegistrationIATLogin( @@ -702,6 +771,10 @@ func (s *Services) OAuthDynamicRegistrationIATLogin( logger.WarnContext(ctx, "OAuth Redirect URI does not match") return "", "", false, exceptions.NewUnauthorizedError() } + if data.Username != opts.HostUsername { + logger.WarnContext(ctx, "OAuth host does not match stored registration host") + return "", "", false, exceptions.NewUnauthorizedError() + } accountDTO, serviceErr := s.GetAccountByEmail(ctx, GetAccountByEmailOptions{ RequestID: opts.RequestID, @@ -745,6 +818,10 @@ func (s *Services) OAuthDynamicRegistrationIATLogin( logger.InfoContext(ctx, "Account is not confirmed") return "", "", false, exceptions.NewForbiddenError() } + if !hostMatchesAccount(opts.HostUsername, accountDTO.Username) { + logger.WarnContext(ctx, "Authenticated account does not match host") + return "", "", false, exceptions.NewForbiddenError() + } default2FAConfig, serviceErr := s.getDefaultAccount2FAConfigInternal(ctx, getDefaultAccount2FAConfigInternalOptions{ requestID: opts.RequestID, @@ -759,6 +836,7 @@ func (s *Services) OAuthDynamicRegistrationIATLogin( sessionID, err := s.cache.SaveAccountCredentialsDynamicRegistrationIAT2FA( ctx, cache.SaveAccountCredentialsDynamicRegistrationIAT2FAOptions{ + Username: data.Username, RequestID: opts.RequestID, AccountPublicID: accountDTO.PublicID, AccountVersion: accountDTO.Version(), @@ -819,17 +897,12 @@ func (s *Services) OAuthDynamicRegistrationIATLogin( queryParams.Add("code_challenge_method", opts.CodeChallengeMethod) } return oauthDynamicRegistrationIATPath + "/" + opts.ACCClientID + paths.OAuthAuth + - paths.AuthLogin + paths.Auth2FA + queryParams.Encode(), sessionID, false, nil + paths.AuthLogin + paths.Auth2FA + "?" + queryParams.Encode(), sessionID, false, nil } - domainDTO, serviceErr := s.GetAccountCredentialsRegistrationDomain(ctx, GetAccountCredentialsRegistrationDomainOptions{ - RequestID: opts.RequestID, - AccountPublicID: accountDTO.PublicID, - Domain: data.Domain, - }) - if serviceErr != nil { + if serviceErr := s.checkIATRegistrationDomain(ctx, opts.RequestID, opts.HostUsername, accountDTO.PublicID, data.Domain); serviceErr != nil { if serviceErr.Code != exceptions.CodeNotFound { - logger.ErrorContext(ctx, "Failed to get account credentials registration domain", "serviceError", serviceErr) + logger.ErrorContext(ctx, "Failed to get registration domain", "serviceError", serviceErr) return "", "", false, serviceErr } @@ -844,17 +917,14 @@ func (s *Services) OAuthDynamicRegistrationIATLogin( return "", "", false, exceptions.NewInternalServerError() } - logger.WarnContext(ctx, "Account credentials registration domain not found") - return "", "", false, exceptions.NewForbiddenError() - } - if !domainDTO.Verified { - logger.ErrorContext(ctx, "Account credentials registration domain is not validCSRF") + logger.WarnContext(ctx, "Registration domain not found") return "", "", false, exceptions.NewForbiddenError() } sessionKey, err := s.cache.CreateAccountCredentialsRegistrationSessionKey( ctx, cache.CreateAccountCredentialsRegistrationSessionKeyOptions{ + Username: data.Username, RequestID: opts.RequestID, ClientID: opts.ACCClientID, Domain: opts.Domain, @@ -1015,6 +1085,7 @@ type OAuthDynamicRegistrationIATVerify2FACodeOptions struct { CSRFToken string Code string BackendDomain string + HostUsername string } func (s *Services) OAuthDynamicRegistrationIATVerify2FACode( @@ -1060,6 +1131,10 @@ func (s *Services) OAuthDynamicRegistrationIATVerify2FACode( logger.WarnContext(ctx, "Client IDs do not match", "sessionClientId", data.ClientID) return "", "", exceptions.NewUnauthorizedError() } + if data.Username != opts.HostUsername { + logger.WarnContext(ctx, "OAuth host does not match stored registration host") + return "", "", exceptions.NewUnauthorizedError() + } twoFAType, serviceErr := map2FATypeTokens(data.TwoFAType) if serviceErr != nil { logger.WarnContext(ctx, "Failed to map two factor type", "serviceError", serviceErr) @@ -1075,6 +1150,10 @@ func (s *Services) OAuthDynamicRegistrationIATVerify2FACode( logger.WarnContext(ctx, "Failed to get account by public ID and version", "serviceError", serviceErr) return "", "", serviceErr } + if !hostMatchesAccount(opts.HostUsername, accountDTO.Username) { + logger.WarnContext(ctx, "Authenticated account does not match host") + return "", "", exceptions.NewForbiddenError() + } if serviceErr := s.verifyAccount2FAInternal(ctx, verifyAccount2FAInternalOptions{ requestID: opts.RequestID, accountID: accountDTO.ID(), @@ -1087,13 +1166,9 @@ func (s *Services) OAuthDynamicRegistrationIATVerify2FACode( return "", "", serviceErr } - if _, serviceErr := s.GetAccountCredentialsRegistrationDomain(ctx, GetAccountCredentialsRegistrationDomainOptions{ - RequestID: opts.RequestID, - AccountPublicID: accountDTO.PublicID, - Domain: data.Domain, - }); serviceErr != nil { + if serviceErr := s.checkIATRegistrationDomain(ctx, opts.RequestID, opts.HostUsername, accountDTO.PublicID, data.Domain); serviceErr != nil { if serviceErr.Code != exceptions.CodeNotFound { - logger.ErrorContext(ctx, "Failed to get account credentials registration domain", "serviceError", serviceErr) + logger.ErrorContext(ctx, "Failed to get registration domain", "serviceError", serviceErr) return "", "", serviceErr } @@ -1108,13 +1183,14 @@ func (s *Services) OAuthDynamicRegistrationIATVerify2FACode( return "", "", exceptions.NewInternalServerError() } - logger.WarnContext(ctx, "Account credentials registration domain not found") + logger.WarnContext(ctx, "Registration domain not found") return "", "", exceptions.NewForbiddenError() } sessionKey, err := s.cache.CreateAccountCredentialsRegistrationSessionKey( ctx, cache.CreateAccountCredentialsRegistrationSessionKeyOptions{ + Username: data.Username, RequestID: opts.RequestID, ClientID: opts.ACCClientID, Domain: opts.Domain, @@ -1133,10 +1209,12 @@ func (s *Services) OAuthDynamicRegistrationIATVerify2FACode( // TODO: add external callbacks type VerifyOAuthDynamicRegistrationIATCodeOptions struct { - RequestID string - Code string - CodeVerifier string - Domain string + BackendDomain string + HostUsername string + RequestID string + Code string + CodeVerifier string + Domain string } func (s *Services) VerifyOAuthDynamicRegistrationIATCode( @@ -1184,70 +1262,27 @@ func (s *Services) VerifyOAuthDynamicRegistrationIATCode( return dtos.AuthDTO{}, serviceErr } - tldOneDomain, err := publicsuffix.EffectiveTLDPlusOne(opts.Domain) - if err != nil { - logger.WarnContext(ctx, "Invalid domain", "error", err) - return dtos.AuthDTO{}, exceptions.NewValidationError("invalid client_id") - } - - var count int64 - if tldOneDomain != data.Domain { - count, err = s.database.CountVerifiedDynamicRegistrationDomainsByDomainsAndAccountPublicID( - ctx, - database.CountVerifiedDynamicRegistrationDomainsByDomainsAndAccountPublicIDParams{ - AccountPublicID: accountDTO.PublicID, - Domains: []string{data.Domain, tldOneDomain}, - }, - ) - } else { - count, err = s.database.CountVerifiedDynamicRegistrationDomainsByDomainAndAccountPublicID( - ctx, - database.CountVerifiedDynamicRegistrationDomainsByDomainAndAccountPublicIDParams{ - AccountPublicID: accountDTO.PublicID, - Domain: data.Domain, - }, - ) - } - if err != nil { - logger.ErrorContext(ctx, "Failed to count verified account dynamic registration domains by domains and account public ID", "error", err) - return dtos.AuthDTO{}, exceptions.NewInternalServerError() + if data.HostUsername != opts.HostUsername { + return dtos.AuthDTO{}, exceptions.NewUnauthorizedError() } - if count == 0 { - logger.WarnContext(ctx, "Account does not have any verified dynamic registration domains matching the OAuth Domain") - return dtos.AuthDTO{}, exceptions.NewForbiddenError() + if opts.HostUsername != "" { + if accountDTO.Username != opts.HostUsername { + return dtos.AuthDTO{}, exceptions.NewForbiddenError() + } + return s.CreateAppCredentialsRegistrationIAT(ctx, CreateAppCredentialsRegistrationIATOptions{ + RequestID: opts.RequestID, AccountPublicID: accountDTO.PublicID, + AccountVersion: accountDTO.Version(), Domain: data.Domain, BackendDomain: opts.BackendDomain, + }) } - - tokenTTL := s.jwt.GetDynamicRegistrationTTL() - signedToken, serviceErr := s.crypto.SignToken(ctx, crypto.SignTokenOptions{ - RequestID: opts.RequestID, - Token: s.jwt.DynamicRegistrationIAT(tokens.DynamicRegistrationIATOptions{ - AccountPublicID: accountDTO.PublicID, - AccountVersion: accountDTO.Version(), - Domain: data.Domain, - ClientID: data.ClientID, - }), - GetJWKfn: s.BuildGetGlobalEncryptedJWKFn(ctx, BuildEncryptedJWKFnOptions{ - RequestID: opts.RequestID, - KeyType: database.TokenKeyTypeDynamicRegistration, - TTL: tokenTTL, - }), - GetDecryptDEKfn: s.BuildGetGlobalDecDEKFn(ctx, BuildGetGlobalDEKFnOptions{ - RequestID: opts.RequestID, - }), - GetEncryptDEKfn: s.BuildGetEncGlobalDEKFn(ctx, BuildGetGlobalDEKFnOptions{ - RequestID: opts.RequestID, - }), - StoreFN: s.BuildUpdateJWKDEKFn(ctx, BuildUpdateJWKDEKFnOptions{ - RequestID: opts.RequestID, - }), + signedToken, serviceErr := s.CreateAccountCredentialsRegistrationIAT(ctx, CreateAccountCredentialsRegistrationIATOptions{ + RequestID: opts.RequestID, AccountPublicID: accountDTO.PublicID, + AccountVersion: accountDTO.Version(), Domain: data.Domain, BackendDomain: opts.BackendDomain, }) if serviceErr != nil { - logger.ErrorContext(ctx, "Failed to sign account credentials registration IAT", "serviceError", serviceErr) return dtos.AuthDTO{}, serviceErr } + return dtos.NewAuthDTO(signedToken, s.jwt.GetDynamicRegistrationTTL()), nil - logger.InfoContext(ctx, "Verified account credentials registration IAT code successfully") - return dtos.NewAuthDTO(signedToken, tokenTTL), nil } type OAuthDynamicRegistrationIATExtGetOptions struct { @@ -1259,6 +1294,7 @@ type OAuthDynamicRegistrationIATExtGetOptions struct { RedirectURI string State string BackendDomain string + HostUsername string } func (s *Services) OAuthDynamicRegistrationIATExtGet( @@ -1322,6 +1358,10 @@ func (s *Services) OAuthDynamicRegistrationIATExtGet( logger.WarnContext(ctx, "OAuth Redirect URI does not match") return "", exceptions.NewUnauthorizedError() } + if data.Username != opts.HostUsername { + logger.WarnContext(ctx, "OAuth host does not match stored registration host") + return "", exceptions.NewUnauthorizedError() + } if err := s.cache.SaveAccountCredentialsDynamicRegistrationIATExtAuth(ctx, cache.SaveAccountCredentialsDynamicRegistrationIATExtAuthOptions{ RequestID: opts.RequestID, @@ -1347,6 +1387,7 @@ type OAuthDynamicRegistrationIATExtCBOptions struct { Code string RedirectURL string BackendDomain string + HostUsername string } func (s *Services) OAuthDynamicRegistrationIATExtCB( @@ -1422,6 +1463,10 @@ func (s *Services) OAuthDynamicRegistrationIATExtCB( logger.WarnContext(ctx, "OAuth State does not match") return "", exceptions.NewUnauthorizedError() } + if authData.Username != opts.HostUsername { + logger.WarnContext(ctx, "OAuth host does not match stored registration host") + return "", exceptions.NewUnauthorizedError() + } userData, serviceErr := s.extOAuthUser(ctx, logger, extOAuthUserOptions{ requestID: opts.RequestID, @@ -1439,6 +1484,10 @@ func (s *Services) OAuthDynamicRegistrationIATExtCB( if serviceErr != nil { return "", serviceErr } + if !hostMatchesAccount(opts.HostUsername, accountDTO.Username) { + logger.WarnContext(ctx, "Authenticated account does not match host") + return "", exceptions.NewForbiddenError() + } if _, serviceErr := s.GetAccountAuthProvider(ctx, GetAccountAuthProviderOptions{ RequestID: opts.RequestID, PublicID: accountDTO.PublicID, @@ -1456,6 +1505,7 @@ func (s *Services) OAuthDynamicRegistrationIATExtCB( cbURL, serviceErr := s.generateOAuthDynamicRegistrationIATCallback( ctx, generateOAuthDynamicRegistrationIATCallbackOptions{ + hostUsername: authData.Username, requestID: opts.RequestID, clientID: opts.ACCClientID, accountPublicID: accountDTO.PublicID, @@ -1483,6 +1533,7 @@ type OAuthDynamicRegistrationIATExtAppleCBOptions struct { State string RedirectURL string BackendDomain string + HostUsername string } func (s *Services) OAuthDynamicRegistrationIATExtAppleCB( @@ -1542,6 +1593,10 @@ func (s *Services) OAuthDynamicRegistrationIATExtAppleCB( logger.WarnContext(ctx, "OAuth State does not match") return "", exceptions.NewUnauthorizedError() } + if authData.Username != opts.HostUsername { + logger.WarnContext(ctx, "OAuth host does not match stored registration host") + return "", exceptions.NewUnauthorizedError() + } ok, serviceErr := s.oauthProviders.ValidateAppleIDToken(ctx, oauth.ValidateAppleIDTokenOptions{ RequestID: opts.RequestID, @@ -1564,6 +1619,10 @@ func (s *Services) OAuthDynamicRegistrationIATExtAppleCB( if serviceErr != nil { return "", serviceErr } + if !hostMatchesAccount(opts.HostUsername, accountDTO.Username) { + logger.WarnContext(ctx, "Authenticated account does not match host") + return "", exceptions.NewForbiddenError() + } if _, serviceErr := s.GetAccountAuthProvider(ctx, GetAccountAuthProviderOptions{ RequestID: opts.RequestID, PublicID: accountDTO.PublicID, @@ -1581,6 +1640,7 @@ func (s *Services) OAuthDynamicRegistrationIATExtAppleCB( cbURL, serviceErr := s.generateOAuthDynamicRegistrationIATCallback( ctx, generateOAuthDynamicRegistrationIATCallbackOptions{ + hostUsername: authData.Username, requestID: opts.RequestID, clientID: opts.ACCClientID, accountPublicID: accountDTO.PublicID, diff --git a/idp/internal/services/oauth_dynamic_registration_config.go b/idp/internal/services/oauth_dynamic_registration_config.go new file mode 100644 index 0000000..25619f9 --- /dev/null +++ b/idp/internal/services/oauth_dynamic_registration_config.go @@ -0,0 +1,310 @@ +package services + +import ( + "context" + "net/url" + "strings" + + "github.com/google/uuid" + + "github.com/tugascript/devlogs/idp/internal/exceptions" + "github.com/tugascript/devlogs/idp/internal/providers/database" + "github.com/tugascript/devlogs/idp/internal/services/dtos" +) + +const oauthDynamicRegistrationConfigLocation = "oauth_dynamic_registration_config" + +type GetRegisteredClientOptions struct { + RequestID string + AccountPublicID uuid.UUID + ClientID string + BackendDomain string + HostUsername string +} + +func (s *Services) GetRegisteredAccountCredentials( + ctx context.Context, + opts GetRegisteredClientOptions, +) (*dtos.ClientRegistrationDTO, *exceptions.ServiceError) { + row, serviceErr := s.findRegisteredAccountCredential(ctx, opts) + if serviceErr != nil { + return nil, serviceErr + } + dto, serviceErr := dtos.MapRegisteredAccountCredentials(row, "", "", row.CreatedAt, nil) + if serviceErr != nil { + return nil, exceptions.NewUnauthorizedError() + } + dto.Registration.WithRegistrationAccess("", dtos.RegistrationClientURI(opts.BackendDomain, row.ClientID)) + return dto.Registration.WithoutSecrets(), nil +} + +func (s *Services) GetRegisteredApp( + ctx context.Context, + opts GetRegisteredClientOptions, +) (*dtos.ClientRegistrationDTO, *exceptions.ServiceError) { + row, serviceErr := s.findRegisteredApp(ctx, opts) + if serviceErr != nil { + return nil, serviceErr + } + dto := dtos.MapRegisteredApp(row, "", "", row.CreatedAt, nil) + issuer := dynamicRegistrationIssuerDomain(opts.HostUsername, opts.BackendDomain) + dto.Registration.WithRegistrationAccess("", dtos.RegistrationClientURI(issuer, row.ClientID)) + return dto.Registration.WithoutSecrets(), nil +} + +func (s *Services) DeleteRegisteredAccountCredentials( + ctx context.Context, + opts GetRegisteredClientOptions, +) *exceptions.ServiceError { + if _, serviceErr := s.findRegisteredAccountCredential(ctx, opts); serviceErr != nil { + return serviceErr + } + if err := s.database.DeleteAccountCredentials(ctx, opts.ClientID); err != nil { + return exceptions.FromDBError(err) + } + return nil +} + +func (s *Services) DeleteRegisteredApp( + ctx context.Context, + opts GetRegisteredClientOptions, +) *exceptions.ServiceError { + row, serviceErr := s.findRegisteredApp(ctx, opts) + if serviceErr != nil { + return serviceErr + } + if err := s.database.DeleteApp(ctx, row.ID); err != nil { + return exceptions.FromDBError(err) + } + return nil +} + +func (s *Services) findRegisteredAccountCredential( + ctx context.Context, + opts GetRegisteredClientOptions, +) (*database.AccountCredential, *exceptions.ServiceError) { + row, err := s.database.FindAccountCredentialsByAccountPublicIDAndClientID(ctx, database.FindAccountCredentialsByAccountPublicIDAndClientIDParams{ + AccountPublicID: opts.AccountPublicID, + ClientID: opts.ClientID, + }) + if err != nil { + return nil, exceptions.NewUnauthorizedError() + } + return &row, nil +} + +func (s *Services) findRegisteredApp( + ctx context.Context, + opts GetRegisteredClientOptions, +) (*database.App, *exceptions.ServiceError) { + row, err := s.database.FindAppByClientIDAndAccountPublicID(ctx, database.FindAppByClientIDAndAccountPublicIDParams{ + AccountPublicID: opts.AccountPublicID, + ClientID: opts.ClientID, + }) + if err != nil { + return nil, exceptions.NewUnauthorizedError() + } + return &row, nil +} + +type UpdateRegisteredClientOptions struct { + CreateAccountCredentialsRegistrationOptions + ClientID string +} + +func (s *Services) UpdateRegisteredAccountCredentials( + ctx context.Context, + opts UpdateRegisteredClientOptions, +) (*dtos.ClientRegistrationDTO, *exceptions.ServiceError) { + logger := s.buildLogger(opts.RequestID, oauthDynamicRegistrationConfigLocation, "UpdateRegisteredAccountCredentials") + existing, serviceErr := s.findRegisteredAccountCredential(ctx, GetRegisteredClientOptions{ + RequestID: opts.RequestID, AccountPublicID: opts.AccountPublicID, ClientID: opts.ClientID, + }) + if serviceErr != nil { + return nil, serviceErr + } + data := registrationDataFromAccountOptions(opts.CreateAccountCredentialsRegistrationOptions) + data.ApplicationType = string(existing.CredentialsType) + data, preparationErr := s.prepareDynamicRegistration(ctx, prepareDynamicRegistrationOptions{ + requestID: opts.RequestID, accountPublicID: opts.AccountPublicID, data: data, + softwareStatement: opts.SoftwareStatement, backendDomain: opts.BackendDomain, + frontendDomain: opts.FrontendDomain, app: false, + }) + if preparationErr != nil { + return nil, preparationErr + } + if data.ApplicationType != string(existing.CredentialsType) { + return nil, exceptions.NewValidationError("application_type cannot be changed") + } + parsedClientURI, err := url.Parse(data.ClientURI) + if err != nil { + return nil, exceptions.NewValidationError("invalid client URI") + } + scopes, serviceErr := mapAccountCredentialsScopes(strings.Fields(data.Scope)) + if serviceErr != nil { + return nil, serviceErr + } + tokenEndpointAuthMethod, serviceErr := mapAuthMethod(data.TokenEndpointAuthMethod) + if serviceErr != nil { + return nil, serviceErr + } + params, serviceErr := s.mapAccountCredentialsRegistrationDataToDBParams(ctx, mapAccountCredentialsRegistrationDataToDBParamsOptions{ + applicationType: existing.CredentialsType, + accountPublicID: opts.AccountPublicID, + accountID: existing.AccountID, + domain: parsedClientURI.Hostname(), + requestID: opts.RequestID, + tokenEndpointAuthMethod: tokenEndpointAuthMethod, + transport: existing.Transport, + scopes: scopes, + data: &data, + }) + if serviceErr != nil { + return nil, serviceErr + } + updated, err := s.database.UpdateRegisteredAccountCredentials(ctx, database.UpdateRegisteredAccountCredentialsParams{ + ID: existing.ID, Domain: params.Domain, Transport: params.Transport, RedirectUris: params.RedirectUris, + TokenEndpointAuthMethod: params.TokenEndpointAuthMethod, GrantTypes: params.GrantTypes, ResponseTypes: params.ResponseTypes, + ClientName: params.ClientName, ClientUri: params.ClientUri, LogoUri: params.LogoUri, Scopes: params.Scopes, + Contacts: params.Contacts, TosUri: params.TosUri, PolicyUri: params.PolicyUri, JwksUri: params.JwksUri, Jwks: params.Jwks, + SoftwareID: params.SoftwareID, SoftwareVersion: params.SoftwareVersion, SectorIdentifierUri: params.SectorIdentifierUri, + SubjectType: params.SubjectType, IDTokenSignedResponseAlg: params.IDTokenSignedResponseAlg, + IDTokenEncryptedResponseAlg: params.IDTokenEncryptedResponseAlg, IDTokenEncryptedResponseEnc: params.IDTokenEncryptedResponseEnc, + UserinfoSignedResponseAlg: params.UserinfoSignedResponseAlg, UserinfoEncryptedResponseAlg: params.UserinfoEncryptedResponseAlg, + UserinfoEncryptedResponseEnc: params.UserinfoEncryptedResponseEnc, RequestObjectSigningAlg: params.RequestObjectSigningAlg, + RequestObjectEncryptionAlg: params.RequestObjectEncryptionAlg, RequestObjectEncryptionEnc: params.RequestObjectEncryptionEnc, + TokenEndpointAuthSigningAlg: params.TokenEndpointAuthSigningAlg, DefaultMaxAge: params.DefaultMaxAge, + RequireAuthTime: params.RequireAuthTime, DefaultAcrValues: params.DefaultAcrValues, InitiateLoginUri: params.InitiateLoginUri, + RequestUris: params.RequestUris, AccessTokenSigningAlg: params.AccessTokenSigningAlg, + }) + if err != nil { + logger.ErrorContext(ctx, "Failed to update account credentials", "error", err) + return nil, exceptions.FromDBError(err) + } + dto, serviceErr := dtos.MapRegisteredAccountCredentials(&updated, opts.SoftwareStatement, "", updated.CreatedAt, nil) + if serviceErr != nil { + return nil, serviceErr + } + dto.Registration.WithRegistrationAccess("", dtos.RegistrationClientURI(opts.BackendDomain, updated.ClientID)) + return dto.Registration.WithoutSecrets(), nil +} + +type UpdateRegisteredAppOptions struct { + CreateAppCredentialsRegistrationOptions + ClientID string + HostUsername string +} + +func (s *Services) UpdateRegisteredApp( + ctx context.Context, + opts UpdateRegisteredAppOptions, +) (*dtos.ClientRegistrationDTO, *exceptions.ServiceError) { + logger := s.buildLogger(opts.RequestID, oauthDynamicRegistrationConfigLocation, "UpdateRegisteredApp") + account, serviceErr := s.GetAccountByID(ctx, GetAccountByIDOptions{RequestID: opts.RequestID, ID: opts.AccountID}) + if serviceErr != nil { + return nil, exceptions.NewUnauthorizedError() + } + existing, serviceErr := s.findRegisteredApp(ctx, GetRegisteredClientOptions{ + RequestID: opts.RequestID, AccountPublicID: account.PublicID, ClientID: opts.ClientID, + }) + if serviceErr != nil { + return nil, serviceErr + } + data := registrationDataFromAppOptions(opts.CreateAppCredentialsRegistrationOptions) + data.ApplicationType = string(existing.AppType) + data, preparationErr := s.prepareDynamicRegistration(ctx, prepareDynamicRegistrationOptions{ + requestID: opts.RequestID, accountID: opts.AccountID, data: data, + softwareStatement: opts.SoftwareStatement, backendDomain: opts.BackendDomain, + frontendDomain: opts.FrontendDomain, app: true, + }) + if preparationErr != nil { + return nil, preparationErr + } + if data.ApplicationType != string(existing.AppType) { + return nil, exceptions.NewValidationError("application_type cannot be changed") + } + parsedClientURI, err := url.Parse(data.ClientURI) + if err != nil { + return nil, exceptions.NewValidationError("invalid client URI") + } + tokenEndpointAuthMethod, serviceErr := mapAuthMethod(data.TokenEndpointAuthMethod) + if serviceErr != nil { + return nil, serviceErr + } + scopesList := strings.Fields(data.Scope) + stdScopes, customScopes, defaultStdScopes, defaultCustomScopes, serviceErr := mapScopesToStandardAndCustomScopes(scopesList, nil) + if serviceErr != nil { + return nil, serviceErr + } + params, serviceErr := s.mapAppRegistrationDataToDBParams(ctx, mapAppRegistrationDataToDBParamsOptions{ + appType: existing.AppType, accountPublicID: account.PublicID, accountID: opts.AccountID, + domain: parsedClientURI.Hostname(), requestID: opts.RequestID, tokenEndpointAuthMethod: tokenEndpointAuthMethod, + transport: existing.Transport, scopes: stdScopes, customScopes: customScopes, defaultScopes: defaultStdScopes, + defaultCustomScopes: defaultCustomScopes, allowUserRegistration: existing.AllowUserRegistration, + usernameColumn: existing.UsernameColumn, authProviders: existing.AuthProviders, data: &data, + }) + if serviceErr != nil { + return nil, serviceErr + } + updated, err := s.database.UpdateRegisteredApp(ctx, database.UpdateRegisteredAppParams{ + ID: existing.ID, ClientName: params.ClientName, ClientUri: params.ClientUri, UsernameColumn: params.UsernameColumn, + TokenEndpointAuthMethod: params.TokenEndpointAuthMethod, GrantTypes: params.GrantTypes, LogoUri: params.LogoUri, + TosUri: params.TosUri, PolicyUri: params.PolicyUri, Contacts: params.Contacts, SoftwareID: params.SoftwareID, + SoftwareVersion: params.SoftwareVersion, Scopes: params.Scopes, DefaultScopes: params.DefaultScopes, + CustomScopes: params.CustomScopes, DefaultCustomScopes: params.DefaultCustomScopes, Domain: params.Domain, + Transport: params.Transport, RedirectUris: params.RedirectUris, ResponseTypes: params.ResponseTypes, + AllowUserRegistration: params.AllowUserRegistration, AuthProviders: params.AuthProviders, JwksUri: params.JwksUri, + Jwks: params.Jwks, SectorIdentifierUri: params.SectorIdentifierUri, SubjectType: params.SubjectType, + IDTokenSignedResponseAlg: params.IDTokenSignedResponseAlg, IDTokenEncryptedResponseAlg: params.IDTokenEncryptedResponseAlg, + IDTokenEncryptedResponseEnc: params.IDTokenEncryptedResponseEnc, UserinfoSignedResponseAlg: params.UserinfoSignedResponseAlg, + UserinfoEncryptedResponseAlg: params.UserinfoEncryptedResponseAlg, UserinfoEncryptedResponseEnc: params.UserinfoEncryptedResponseEnc, + RequestObjectSigningAlg: params.RequestObjectSigningAlg, RequestObjectEncryptionAlg: params.RequestObjectEncryptionAlg, + RequestObjectEncryptionEnc: params.RequestObjectEncryptionEnc, TokenEndpointAuthSigningAlg: params.TokenEndpointAuthSigningAlg, + DefaultMaxAge: params.DefaultMaxAge, RequireAuthTime: params.RequireAuthTime, DefaultAcrValues: params.DefaultAcrValues, + InitiateLoginUri: params.InitiateLoginUri, RequestUris: params.RequestUris, AccessTokenSigningAlg: params.AccessTokenSigningAlg, + }) + if err != nil { + logger.ErrorContext(ctx, "Failed to update app", "error", err) + return nil, exceptions.FromDBError(err) + } + dto := dtos.MapRegisteredApp(&updated, opts.SoftwareStatement, "", updated.CreatedAt, nil) + issuer := dynamicRegistrationIssuerDomain(opts.HostUsername, opts.BackendDomain) + dto.Registration.WithRegistrationAccess("", dtos.RegistrationClientURI(issuer, updated.ClientID)) + return dto.Registration.WithoutSecrets(), nil +} + +func registrationDataFromAccountOptions(opts CreateAccountCredentialsRegistrationOptions) ApplicationRegistrationData { + return ApplicationRegistrationData{ + RedirectURIs: opts.RedirectURIs, TokenEndpointAuthMethod: opts.TokenEndpointAuthMethod, + ResponseTypes: opts.ResponseTypes, GrantTypes: opts.GrantTypes, ApplicationType: opts.ApplicationType, + ClientName: opts.ClientName, ClientURI: opts.ClientURI, LogoURI: opts.LogoURI, Scope: opts.Scope, + Contacts: opts.Contacts, TOSURI: opts.TOSURI, PolicyURI: opts.PolicyURI, JWKsURI: opts.JWKsURI, JWKs: opts.JWKs, + SoftwareID: opts.SoftwareID, SoftwareVersion: opts.SoftwareVersion, SubjectType: opts.SubjectType, + SectorIdentifierURI: opts.SectorIdentifierURI, DefaultMaxAge: opts.DefaultMaxAge, RequireAuthTime: opts.RequireAuthTime, + DefaultACRValues: opts.DefaultACRValues, InitiateLoginURI: opts.InitiateLoginURI, RequestURIs: opts.RequestURIs, + IDTokenSignedResponseAlg: opts.IDTokenSignedResponseAlg, IDTokenEncryptedResponseAlg: opts.IDTokenEncryptedResponseAlg, + IDTokenEncryptedResponseEnc: opts.IDTokenEncryptedResponseEnc, UserInfoSignedResponseAlg: opts.UserInfoSignedResponseAlg, + UserInfoEncryptedResponseAlg: opts.UserInfoEncryptedResponseAlg, UserInfoEncryptedResponseEnc: opts.UserInfoEncryptedResponseEnc, + RequestObjectSigningAlg: opts.RequestObjectSigningAlg, RequestObjectEncryptionAlg: opts.RequestObjectEncryptionAlg, + RequestObjectEncryptionEnc: opts.RequestObjectEncryptionEnc, TokenEndpointAuthSigningAlg: opts.TokenEndpointAuthSigningAlg, + AccessTokenSigningAlg: opts.AccessTokenSigningAlg, + } +} + +func registrationDataFromAppOptions(opts CreateAppCredentialsRegistrationOptions) ApplicationRegistrationData { + return ApplicationRegistrationData{ + RedirectURIs: opts.RedirectURIs, TokenEndpointAuthMethod: opts.TokenEndpointAuthMethod, + ResponseTypes: opts.ResponseTypes, GrantTypes: opts.GrantTypes, ApplicationType: opts.ApplicationType, + ClientName: opts.ClientName, ClientURI: opts.ClientURI, LogoURI: opts.LogoURI, Scope: opts.Scope, + Contacts: opts.Contacts, TOSURI: opts.TOSURI, PolicyURI: opts.PolicyURI, JWKsURI: opts.JWKsURI, JWKs: opts.JWKs, + SoftwareID: opts.SoftwareID, SoftwareVersion: opts.SoftwareVersion, SubjectType: opts.SubjectType, + SectorIdentifierURI: opts.SectorIdentifierURI, DefaultMaxAge: opts.DefaultMaxAge, RequireAuthTime: opts.RequireAuthTime, + DefaultACRValues: opts.DefaultACRValues, InitiateLoginURI: opts.InitiateLoginURI, RequestURIs: opts.RequestURIs, + IDTokenSignedResponseAlg: opts.IDTokenSignedResponseAlg, IDTokenEncryptedResponseAlg: opts.IDTokenEncryptedResponseAlg, + IDTokenEncryptedResponseEnc: opts.IDTokenEncryptedResponseEnc, UserInfoSignedResponseAlg: opts.UserInfoSignedResponseAlg, + UserInfoEncryptedResponseAlg: opts.UserInfoEncryptedResponseAlg, UserInfoEncryptedResponseEnc: opts.UserInfoEncryptedResponseEnc, + RequestObjectSigningAlg: opts.RequestObjectSigningAlg, RequestObjectEncryptionAlg: opts.RequestObjectEncryptionAlg, + RequestObjectEncryptionEnc: opts.RequestObjectEncryptionEnc, TokenEndpointAuthSigningAlg: opts.TokenEndpointAuthSigningAlg, + AccessTokenSigningAlg: opts.AccessTokenSigningAlg, + } +} diff --git a/idp/internal/services/registration_metadata.go b/idp/internal/services/registration_metadata.go new file mode 100644 index 0000000..08824d1 --- /dev/null +++ b/idp/internal/services/registration_metadata.go @@ -0,0 +1,256 @@ +package services + +import ( + "context" + "encoding/json" + "errors" + "net/url" + "slices" + "strings" + + "github.com/golang-jwt/jwt/v5" + "github.com/google/uuid" + "golang.org/x/net/publicsuffix" + + "github.com/tugascript/devlogs/idp/internal/exceptions" + "github.com/tugascript/devlogs/idp/internal/providers/database" + "github.com/tugascript/devlogs/idp/internal/providers/tokens" + "github.com/tugascript/devlogs/idp/internal/utils" +) + +type prepareDynamicRegistrationOptions struct { + requestID string + accountID int32 + accountPublicID uuid.UUID + data ApplicationRegistrationData + softwareStatement, iatDomain, backendDomain, frontendDomain string + app bool +} + +// Merge verified claims by presence, including explicit false, zero and empty arrays. +// Unknown JWT claims are ignored by the typed metadata decoder. +func mergeRegistrationMetadata(body ApplicationRegistrationData, statement tokens.SoftwareStatementClaims) (ApplicationRegistrationData, error) { + encoded, err := json.Marshal(body) + if err != nil { + return ApplicationRegistrationData{}, err + } + var fields map[string]json.RawMessage + if err := json.Unmarshal(encoded, &fields); err != nil { + return ApplicationRegistrationData{}, err + } + if body.ResponseTypes != nil { + fields["response_types"], _ = json.Marshal(body.ResponseTypes) + } + if body.GrantTypes != nil { + fields["grant_types"], _ = json.Marshal(body.GrantTypes) + } + for name, value := range statement.RawMetadata { + fields[name] = value + } + encoded, err = json.Marshal(fields) + if err != nil { + return ApplicationRegistrationData{}, err + } + var merged ApplicationRegistrationData + if err := json.Unmarshal(encoded, &merged); err != nil { + return ApplicationRegistrationData{}, err + } + return merged, nil +} + +func (s *Services) prepareDynamicRegistration(ctx context.Context, opts prepareDynamicRegistrationOptions) (ApplicationRegistrationData, *exceptions.ServiceError) { + data := opts.data + var methods []database.SoftwareStatementVerificationMethod + allowedScopes := allowedAccountCredentialsScopes + if opts.app { + account, err := s.GetAccountByID(ctx, GetAccountByIDOptions{RequestID: opts.requestID, ID: opts.accountID}) + if err != nil { + return data, err + } + opts.accountPublicID = account.PublicID + cfg, err := s.GetAndCacheAppDynamicRegistrationConfig(ctx, GetAndCacheAppDynamicRegistrationConfigOptions{RequestID: opts.requestID, AccountID: opts.accountID}) + if err != nil { + return data, err + } + methods = cfg.SoftwareStatementVerificationMethods + if len(cfg.DefaultAllowedScopes) > 0 { + allowedScopes = utils.MapSlice(cfg.DefaultAllowedScopes, func(scope *database.Scopes) string { return string(*scope) }) + } else { + allowedScopes = allowedAppScopes + } + } else { + cfg, err := s.GetAndCacheAccountDynamicRegistrationConfig(ctx, GetAndCacheAccountDynamicRegistrationConfigOptions{RequestID: opts.requestID, AccountPublicID: opts.accountPublicID}) + if err != nil { + return data, err + } + methods = cfg.SoftwareStatementVerificationMethods + } + if opts.softwareStatement != "" { + // This preview only locates the account's configured verification key/domain. + // No metadata from it is applied until signature verification succeeds. + var preview jwt.MapClaims + if _, _, err := jwt.NewParser().ParseUnverified(opts.softwareStatement, &preview); err != nil { + return data, exceptions.NewInvalidTokenError("invalid software statement") + } + keyURI := data.ClientURI + if claimURI, ok := preview["client_uri"].(string); ok { + keyURI = claimURI + } + jwksURI := data.JWKsURI + if claimJWKS, ok := preview["jwks_uri"].(string); ok && claimJWKS != "" { + jwksURI = claimJWKS + } + domain := registrationDomain(keyURI, data.RedirectURIs, opts.iatDomain) + base, err := publicsuffix.EffectiveTLDPlusOne(domain) + if err != nil { + return data, exceptions.NewInvalidTokenError("invalid software statement domain") + } + claims, standard, err := s.jwt.VerifySoftwareStatement(ctx, tokens.VerifySoftwareStatementOptions{ + RequestID: opts.requestID, SoftwareStatement: opts.softwareStatement, + GetPublicJWK: s.buildDynamicRegistrationSoftwareStatementFunc(ctx, buildDynamicRegistrationSoftwareStatementFuncOptions{ + requestID: opts.requestID, accountPublicID: opts.accountPublicID, verificationMethods: methods, + jwksURI: jwksURI, jwks: data.JWKs, domain: domain, baseDomain: base, + }), + }) + if err != nil { + if errors.Is(err, errUnapprovedSoftwareStatement) { + return data, exceptions.NewUnauthorizedTokenError("unapproved software statement") + } + return data, exceptions.NewInvalidTokenError("invalid software statement") + } + if serviceErr := s.verifySoftwareStatementSTDClaims(ctx, verifySoftwareStatementSTDClaimsOptions{ + requestID: opts.requestID, domain: domain, baseDomain: base, backendDomain: opts.backendDomain, frontendDomain: opts.frontendDomain, claims: &standard, + }); serviceErr != nil { + return data, serviceErr + } + if serviceErr := s.validateSoftwareStatementClaims(ctx, validateSoftwareStatementClaimsOptions{ + requestID: opts.requestID, claims: &claims, allowedScopes: utils.SliceToHashSet(allowedScopes), + }); serviceErr != nil { + return data, exceptions.NewInvalidTokenError("invalid software statement") + } + data, err = mergeRegistrationMetadata(data, claims) + if err != nil { + return data, exceptions.NewInvalidTokenError("invalid software statement metadata") + } + } + if data.ApplicationType == "" { + if opts.app { + data.ApplicationType = "web" + } else if slices.Contains(data.GrantTypes, "client_credentials") { + data.ApplicationType = "service" + } else { + data.ApplicationType = "native" + } + } + if data.ClientName == "" { + data.ClientName = "Client " + utils.Base62UUID() + } + if data.ClientURI == "" { + domain := registrationDomain("", data.RedirectURIs, opts.iatDomain) + if domain == "" { + return data, exceptions.NewValidationError("a client domain could not be determined") + } + data.ClientURI = "https://" + domain + } + if data.Scope == "" && !opts.app { + data.Scope = "profile" + } + if err := normalizeRegistrationMetadata(&data); err != nil { + return data, err + } + if err := s.validate.StructCtx(ctx, &data); err != nil { + return data, exceptions.NewValidationError("invalid client metadata") + } + return data, nil +} + +func registrationDomain(clientURI string, redirects []string, fallback string) string { + if parsed, err := url.Parse(clientURI); err == nil && parsed.Hostname() != "" { + return parsed.Hostname() + } + if fallback != "" { + return fallback + } + for _, redirect := range redirects { + if parsed, err := url.Parse(redirect); err == nil && parsed.Hostname() != "" { + return parsed.Hostname() + } + } + return "" +} + +func normalizeRegistrationMetadata(data *ApplicationRegistrationData) *exceptions.ServiceError { + if data.GrantTypes == nil { + data.GrantTypes = []string{"authorization_code"} + } + if data.ResponseTypes == nil { + data.ResponseTypes = []string{"code"} + } + if data.TokenEndpointAuthMethod == "" { + data.TokenEndpointAuthMethod = "client_secret_basic" + } + if len(data.GrantTypes) == 0 { + return exceptions.NewValidationError("grant_types must not be empty") + } + codeGrant := slices.Contains(data.GrantTypes, "authorization_code") + for _, response := range data.ResponseTypes { + if slices.Contains(strings.Fields(response), "code") && !codeGrant { + return exceptions.NewValidationError("code responses require authorization_code") + } + } + if codeGrant && !slices.ContainsFunc(data.ResponseTypes, func(response string) bool { return slices.Contains(strings.Fields(response), "code") }) { + return exceptions.NewValidationError("authorization_code requires a code response") + } + if codeGrant && len(data.RedirectURIs) == 0 { + return exceptions.NewError(exceptions.OAuthErrorInvalidRedirectURI, "redirect_uris is required for authorization_code") + } + for _, raw := range data.RedirectURIs { + uri, err := url.Parse(raw) + if err != nil || uri.Scheme == "" || uri.User != nil || strings.Contains(raw, "#") || ((uri.Scheme == "https" || uri.Scheme == "http") && uri.Host == "") { + return exceptions.NewError(exceptions.OAuthErrorInvalidRedirectURI, "invalid redirect URI") + } + } + if data.JWKs != nil && data.JWKsURI != "" { + return exceptions.NewValidationError("jwks and jwks_uri are mutually exclusive") + } + if data.JWKs != nil { + if len(data.JWKs.Keys) == 0 { + return exceptions.NewValidationError("jwks must contain keys") + } + for _, key := range data.JWKs.Keys { + if key == nil { + return exceptions.NewValidationError("invalid public JWK") + } + if _, err := key.ToUsableKey(); err != nil { + return exceptions.NewValidationError("invalid public JWK") + } + raw, err := key.MarshalJSON() + if err != nil { + return exceptions.NewValidationError("invalid public JWK") + } + var fields map[string]json.RawMessage + if json.Unmarshal(raw, &fields) != nil { + return exceptions.NewValidationError("invalid public JWK") + } + for _, secret := range []string{"d", "p", "q", "dp", "dq", "qi", "oth", "k"} { + if _, found := fields[secret]; found { + return exceptions.NewValidationError("jwks must contain only public keys") + } + } + } + } + if data.JWKsURI != "" { + uri, err := url.Parse(data.JWKsURI) + if err != nil || uri.Scheme != "https" || uri.Host == "" || uri.User != nil || uri.Fragment != "" { + return exceptions.NewValidationError("jwks_uri must be an HTTPS URL") + } + } + return nil +} + +func mapRegistrationResponseTypes(values []string) ([]database.ResponseType, *exceptions.ServiceError) { + if values != nil && len(values) == 0 { + return []database.ResponseType{}, nil + } + return mapResponseTypesWithDefault(values) +} diff --git a/idp/internal/services/registration_metadata_test.go b/idp/internal/services/registration_metadata_test.go new file mode 100644 index 0000000..9708f25 --- /dev/null +++ b/idp/internal/services/registration_metadata_test.go @@ -0,0 +1,72 @@ +package services + +import ( + "encoding/json" + "reflect" + "testing" + + "github.com/tugascript/devlogs/idp/internal/exceptions" + "github.com/tugascript/devlogs/idp/internal/providers/tokens" + "github.com/tugascript/devlogs/idp/internal/utils" +) + +func TestRegistrationMetadataDefaultsAndValidation(t *testing.T) { + cases := []struct { + name string + data ApplicationRegistrationData + errorCode string + }{ + {name: "defaults", data: ApplicationRegistrationData{RedirectURIs: []string{"https://example.com/callback/"}}}, + {name: "missing redirect", errorCode: exceptions.OAuthErrorInvalidRedirectURI}, + {name: "fragment", data: ApplicationRegistrationData{RedirectURIs: []string{"https://example.com/#fragment"}}, errorCode: exceptions.OAuthErrorInvalidRedirectURI}, + {name: "empty fragment", data: ApplicationRegistrationData{RedirectURIs: []string{"https://example.com/#"}}, errorCode: exceptions.OAuthErrorInvalidRedirectURI}, + {name: "relative redirect", data: ApplicationRegistrationData{RedirectURIs: []string{"/callback"}}, errorCode: exceptions.OAuthErrorInvalidRedirectURI}, + {name: "client credentials without responses", data: ApplicationRegistrationData{GrantTypes: []string{"client_credentials"}, ResponseTypes: []string{}}}, + {name: "inconsistent grant and response", data: ApplicationRegistrationData{GrantTypes: []string{"client_credentials"}}, errorCode: exceptions.CodeValidation}, + {name: "authorization code without code response", data: ApplicationRegistrationData{GrantTypes: []string{"authorization_code"}, ResponseTypes: []string{}}, errorCode: exceptions.CodeValidation}, + {name: "both key sources", data: ApplicationRegistrationData{RedirectURIs: []string{"https://example.com/cb"}, JWKs: &utils.JWKSet{}, JWKsURI: "https://example.com/jwks"}, errorCode: exceptions.CodeValidation}, + {name: "native custom scheme", data: ApplicationRegistrationData{RedirectURIs: []string{"com.example.app:/callback"}}}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + before := append([]string(nil), tc.data.RedirectURIs...) + err := normalizeRegistrationMetadata(&tc.data) + if tc.errorCode != "" { + if err == nil || err.Code != tc.errorCode { + t.Fatalf("error=%v, want %s", err, tc.errorCode) + } + return + } + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(before, tc.data.RedirectURIs) { + t.Fatal("redirect URI changed") + } + if tc.data.TokenEndpointAuthMethod != "client_secret_basic" { + t.Fatal("incorrect authentication default") + } + mapped, mapErr := mapRegistrationResponseTypes(tc.data.ResponseTypes) + if mapErr != nil || len(mapped) != len(tc.data.ResponseTypes) { + t.Fatalf("response types changed: %v %v", mapped, mapErr) + } + }) + } +} + +func TestSoftwareStatementMergeUsesPresence(t *testing.T) { + body := ApplicationRegistrationData{ClientName: "body", ClientURI: "https://example.com", RequireAuthTime: true, DefaultMaxAge: 60, ResponseTypes: []string{}, Contacts: []string{"old@example.com"}} + statement := tokens.SoftwareStatementClaims{RawMetadata: map[string]json.RawMessage{ + "client_name": json.RawMessage(`"signed"`), "require_auth_time": json.RawMessage(`false`), "default_max_age": json.RawMessage(`0`), "contacts": json.RawMessage(`[]`), "unknown_extension": json.RawMessage(`{"ignored":true}`), + }} + merged, err := mergeRegistrationMetadata(body, statement) + if err != nil { + t.Fatal(err) + } + if merged.ClientName != "signed" || merged.RequireAuthTime || merged.DefaultMaxAge != 0 || len(merged.Contacts) != 0 { + t.Fatalf("statement did not override body: %+v", merged) + } + if merged.ClientURI != body.ClientURI || merged.ResponseTypes == nil { + t.Fatal("omitted statement fields did not preserve body metadata") + } +} diff --git a/idp/internal/services/software_statement.go b/idp/internal/services/software_statement.go index ea95479..7c85b55 100644 --- a/idp/internal/services/software_statement.go +++ b/idp/internal/services/software_statement.go @@ -24,44 +24,11 @@ import ( "github.com/tugascript/devlogs/idp/internal/utils" ) +var errUnapprovedSoftwareStatement = errors.New("software statement is not approved") + const softwareStatementLocation = "software_statement" -type ApplicationRegistrationData struct { - RedirectURIs []string - TokenEndpointAuthMethod string - ResponseTypes []string - GrantTypes []string - ApplicationType string - ClientName string - ClientURI string - LogoURI string - Scope string - Contacts []string - TOSURI string - PolicyURI string - JWKsURI string - JWKs *utils.JWKSet - SoftwareID string - SoftwareVersion string - SubjectType string - SectorIdentifierURI string - DefaultMaxAge int64 - RequireAuthTime bool - DefaultACRValues []string - InitiateLoginURI string - RequestURIs []string - IDTokenSignedResponseAlg string - IDTokenEncryptedResponseAlg string - IDTokenEncryptedResponseEnc string - UserInfoSignedResponseAlg string - UserInfoEncryptedResponseAlg string - UserInfoEncryptedResponseEnc string - RequestObjectSigningAlg string - RequestObjectEncryptionAlg string - RequestObjectEncryptionEnc string - TokenEndpointAuthSigningAlg string - AccessTokenSigningAlg string -} +type ApplicationRegistrationData = tokens.SoftwareStatementClaims type verifySoftwareStatementSTDClaimsOptions struct { requestID string @@ -89,7 +56,7 @@ func (s *Services) verifySoftwareStatementSTDClaims( ) return exceptions.NewUnauthorizedTokenError("issuer does not match client URI domain or base domain") } - if opts.claims.Audience == nil || !slices.ContainsFunc(opts.claims.Audience, func(aud string) bool { + if opts.claims.Audience != nil && !slices.ContainsFunc(opts.claims.Audience, func(aud string) bool { return aud == fmt.Sprintf("https://%s", opts.frontendDomain) || aud == fmt.Sprintf("https://%s", opts.backendDomain) }) { logger.WarnContext(ctx, "Software statement audience does not match frontend or backend domain", @@ -97,7 +64,7 @@ func (s *Services) verifySoftwareStatementSTDClaims( ) return exceptions.NewUnauthorizedTokenError("audience does not match frontend or backend") } - if opts.claims.IssuedAt == nil || opts.claims.IssuedAt.Time.IsZero() || opts.claims.IssuedAt.Time.After(time.Now()) { + if opts.claims.IssuedAt != nil && (opts.claims.IssuedAt.Time.IsZero() || opts.claims.IssuedAt.Time.After(time.Now())) { logger.WarnContext(ctx, "Software statement issued at claim is invalid", "issuedAt", opts.claims.IssuedAt, ) @@ -109,7 +76,7 @@ func (s *Services) verifySoftwareStatementSTDClaims( ) return exceptions.NewUnauthorizedTokenError("not before claim is invalid") } - if opts.claims.ExpiresAt == nil || opts.claims.ExpiresAt.Time.IsZero() || opts.claims.ExpiresAt.Time.Before(time.Now()) { + if opts.claims.ExpiresAt != nil && (opts.claims.ExpiresAt.Time.IsZero() || !opts.claims.ExpiresAt.Time.After(time.Now())) { logger.WarnContext(ctx, "Software statement expiration claim is invalid", "expiresAt", opts.claims.ExpiresAt, ) @@ -228,7 +195,7 @@ func (s *Services) buildDynamicRegistrationSoftwareStatementFunc( logger.ErrorContext(ctx, "Failed to parse JWKs URI", "error", err) return nil, errors.New("invalid JWKs URI") } - if parsedURI.Host != opts.baseDomain || !strings.Contains(parsedURI.Host, "."+opts.baseDomain) { + if parsedURI.Scheme != "https" || parsedURI.User != nil || parsedURI.Fragment != "" || (parsedURI.Hostname() != opts.baseDomain && !strings.HasSuffix(parsedURI.Hostname(), "."+opts.baseDomain)) { logger.WarnContext(ctx, "JWKs URI parsedURI does not match client URI parsedURI") return nil, errors.New("JWKs URI parsedURI does not match client URI parsedURI") } @@ -253,99 +220,24 @@ func (s *Services) buildDynamicRegistrationSoftwareStatementFunc( return jwks.Keys[jwkIdx], nil } } - if slices.Contains(opts.verificationMethods, database.SoftwareStatementVerificationMethodManual) { - if opts.jwks != nil && len(opts.jwks.Keys) > 0 { - return func(kid string) (utils.JWK, error) { - jwkIdx := slices.IndexFunc(opts.jwks.Keys, func(jwk utils.JWK) bool { - return jwk.GetKeyID() == kid - }) - if jwkIdx == -1 { - logger.WarnContext(ctx, "No matching manual JWK found for KID", "kid", kid) - return nil, errors.New("no matching manual JWK found for KID") - } - - sliceJWK := opts.jwks.Keys[jwkIdx] - jwkRefEnt, err := s.database.FindDynamicRegistrationSoftwareStatementKeysByCredentialsKeyKIDAndAccountPublicID( - ctx, - database.FindDynamicRegistrationSoftwareStatementKeysByCredentialsKeyKIDAndAccountPublicIDParams{ - CredentialsKeyKid: kid, - AccountPublicID: opts.accountPublicID, - }, - ) - if err != nil { - serviceErr := exceptions.FromDBError(err) - if serviceErr.Code == exceptions.CodeNotFound { - logger.WarnContext(ctx, "No database entry found for manual JWK", "kid", kid, "error", err) - return nil, errors.New("no database entry found for manual JWK") - } - - logger.ErrorContext(ctx, "Failed to find database entry for manual JWK", "kid", kid, "error", err) - return nil, errors.New("failed to find database entry for manual JWK") - } - if jwkRefEnt.RootDomain != opts.baseDomain { - logger.WarnContext(ctx, "Manual JWK root domain does not match client URI base domain", - "kid", kid, "jwkRootDomain", jwkRefEnt.RootDomain, "baseDomain", opts.baseDomain, - ) - return nil, errors.New("manual JWK root domain does not match client URI base domain") - } - - jwkEnt, err := s.database.FindCredentialsKeyByID(ctx, jwkRefEnt.CredentialsKeyID) - if err != nil { - serviceErr := exceptions.FromDBError(err) - if serviceErr.Code == exceptions.CodeNotFound { - logger.WarnContext(ctx, "No credentials key found for manual JWK", "kid", kid, "error", err) - return nil, errors.New("no credentials key found for manual JWK") - } - - logger.ErrorContext(ctx, "Failed to find credentials key for manual JWK", "kid", kid, "error", err) - return nil, errors.New("failed to find credentials key for manual JWK") - } - - entJWK, err := utils.JsonToJWK(jwkEnt.PublicKey) - if err != nil { - logger.ErrorContext(ctx, "Failed to parse manual JWK", "error", err) - return nil, errors.New("failed to parse manual JWK") - } - if !entJWK.ComparePublicKey(sliceJWK) { - logger.WarnContext(ctx, "Manual JWK does not match database credentials key", "kid", kid) - return nil, errors.New("manual JWK does not match database credentials key") - } - - return sliceJWK, nil - } - } + if slices.Contains(opts.verificationMethods, database.SoftwareStatementVerificationMethodManual) { return func(kid string) (utils.JWK, error) { - jwkEntity, err := s.database.FindDynamicRegistrationSoftwareStatementKeysByRootDomainAndAccountPublicID( - ctx, - database.FindDynamicRegistrationSoftwareStatementKeysByRootDomainAndAccountPublicIDParams{ - RootDomain: opts.baseDomain, - AccountPublicID: opts.accountPublicID, - }, - ) + approved, err := s.database.FindDynamicRegistrationSoftwareStatementKeysByCredentialsKeyKIDAndAccountPublicID(ctx, database.FindDynamicRegistrationSoftwareStatementKeysByCredentialsKeyKIDAndAccountPublicIDParams{CredentialsKeyKid: kid, AccountPublicID: opts.accountPublicID}) if err != nil { - if exceptions.FromDBError(err).Code == exceptions.CodeNotFound { - logger.WarnContext(ctx, "No manual JWKs found for software statement", "error", err) - return nil, errors.New("no manual JWKs found for software statement") - } - - logger.ErrorContext(ctx, "Failed to find manual JWKs for software statement", "error", err) - return nil, errors.New("failed to find manual JWKs for software statement") + return nil, errors.Join(errUnapprovedSoftwareStatement, err) } - if jwkEntity.PublicKid != kid { - logger.WarnContext(ctx, "No matching manual JWK found for KID", - "kid", kid, "publicKID", jwkEntity.PublicKid, - ) - return nil, errors.New("no matching manual JWK found for KID") + if approved.RootDomain != opts.baseDomain { + return nil, errUnapprovedSoftwareStatement } - - jwk, err := utils.JsonToJWK(jwkEntity.PublicKey) + key, err := s.database.FindCredentialsKeyByID(ctx, approved.CredentialsKeyID) if err != nil { - logger.ErrorContext(ctx, "Failed to parse manual JWK for software statement", "error", err) - return nil, errors.New("failed to parse manual JWK for software statement") + return nil, err } - - return jwk, nil + if key.IsRevoked || !key.ExpiresAt.After(time.Now()) || key.PublicKid != kid { + return nil, errUnapprovedSoftwareStatement + } + return utils.JsonToJWK(key.PublicKey) } } diff --git a/idp/internal/utils/jwk.go b/idp/internal/utils/jwk.go index 6300c9c..0aa4df1 100644 --- a/idp/internal/utils/jwk.go +++ b/idp/internal/utils/jwk.go @@ -168,7 +168,8 @@ func (j *Ed25519JWK) MarshalJSON() ([]byte, error) { } func (j *Ed25519JWK) UnmarshalJSON(data []byte) error { - return json.Unmarshal(data, j) + type alias Ed25519JWK + return json.Unmarshal(data, (*alias)(j)) } func (j *Ed25519JWK) ToPrivateKey() (any, error) { @@ -235,7 +236,8 @@ func (j *ES256JWK) MarshalJSON() ([]byte, error) { } func (j *ES256JWK) UnmarshalJSON(data []byte) error { - return json.Unmarshal(data, j) + type alias ES256JWK + return json.Unmarshal(data, (*alias)(j)) } func (j *ES256JWK) ToPrivateKey() (any, error) { @@ -311,7 +313,8 @@ func (j *RS256JWK) MarshalJSON() ([]byte, error) { } func (j *RS256JWK) UnmarshalJSON(data []byte) error { - return json.Unmarshal(data, j) + type alias RS256JWK + return json.Unmarshal(data, (*alias)(j)) } func (j *RS256JWK) ToPrivateKey() (any, error) { diff --git a/idp/tests/auth_test.go b/idp/tests/auth_test.go index ee4b153..9112a58 100644 --- a/idp/tests/auth_test.go +++ b/idp/tests/auth_test.go @@ -996,307 +996,6 @@ func TestAccountAuth2FAConfigs(t *testing.T) { t.Cleanup(accountsCleanUp(t)) } -func legacyAccountAuth2FAUpdate(t *testing.T) { - const update2FAPath = v1Path + paths.AuthBase + paths.Auth2FA - - genTwoFactorAccount := func(t *testing.T, twoFactorType string) string { - accountData := GenerateFakeAccountData(t, services.AuthProviderGitHub) - account := CreateTestAccount(t, accountData) - testS := GetTestServices(t) - requestID := uuid.NewString() - - if _, err := testS.CreateAccount2FAConfig(context.Background(), services.CreateAccount2FAConfigOptions{ - RequestID: requestID, - AccountPublicID: account.PublicID, - AccountVersion: account.Version(), - TwoFAType: twoFactorType, - }); err != nil { - t.Fatalf("failed to enable 2FA for account: %v", err) - } - - account, serviceErr := testS.GetAccountByPublicID(context.Background(), services.GetAccountByPublicIDOptions{ - RequestID: requestID, - PublicID: account.PublicID, - }) - if serviceErr != nil { - t.Fatalf("failed to get account by public ID: %v", serviceErr) - } - - accessToken, _ := GenerateTestAccountAuthTokens(t, &account) - return accessToken - } - - testCases := []TestRequestCase[bodies.Update2FABody]{ - { - Name: "Should enable TOTP 2FA for account with password", - ReqFn: func(t *testing.T) (bodies.Update2FABody, string) { - data := GenerateFakeAccountData(t, services.AuthProviderLocal) - account := CreateTestAccount(t, data) - accessToken, _ := GenerateTestAccountAuthTokens(t, &account) - return bodies.Update2FABody{ - TwoFactorType: services.TwoFactorTotp, - Password: data.Password, - }, accessToken - }, - ExpStatus: http.StatusOK, - AssertFn: func(t *testing.T, _ bodies.Update2FABody, res *http.Response) { - resBody := AssertTestResponseBody(t, res, dtos.AuthDTO{}) - AssertEqual(t, resBody.Message, "Please scan QR Code with your authentication app") - }, - }, - { - Name: "Should enable Email 2FA for account without password", - ReqFn: func(t *testing.T) (bodies.Update2FABody, string) { - data := GenerateFakeAccountData(t, services.AuthProviderMicrosoft) - account := CreateTestAccount(t, data) - accessToken, _ := GenerateTestAccountAuthTokens(t, &account) - return bodies.Update2FABody{ - TwoFactorType: services.TwoFactorEmail, - }, accessToken - }, - ExpStatus: http.StatusOK, - AssertFn: func(t *testing.T, _ bodies.Update2FABody, res *http.Response) { - resBody := AssertTestResponseBody(t, res, dtos.AuthDTO{}) - AssertEqual(t, "Please provide email two factor code", resBody.Message) - }, - }, - { - Name: "Should ask for confirmation to enable TOTP 2FA for account with email 2FA", - ReqFn: func(t *testing.T) (bodies.Update2FABody, string) { - accessToken := genTwoFactorAccount(t, services.TwoFactorEmail) - return bodies.Update2FABody{ - TwoFactorType: services.TwoFactorTotp, - }, accessToken - }, - ExpStatus: http.StatusOK, - AssertFn: func(t *testing.T, _ bodies.Update2FABody, res *http.Response) { - resBody := AssertTestResponseBody(t, res, dtos.AuthDTO{}) - AssertEqual(t, resBody.Message, "Please provide two factor code to confirm two factor update") - }, - }, - { - Name: "Should ask for confirmation to enable email 2FA for account with TOTP 2FA", - ReqFn: func(t *testing.T) (bodies.Update2FABody, string) { - accessToken := genTwoFactorAccount(t, services.TwoFactorTotp) - return bodies.Update2FABody{ - TwoFactorType: services.TwoFactorEmail, - }, accessToken - }, - ExpStatus: http.StatusOK, - AssertFn: func(t *testing.T, _ bodies.Update2FABody, res *http.Response) { - resBody := AssertTestResponseBody(t, res, dtos.AuthDTO{}) - AssertEqual(t, resBody.Message, "Please provide two factor code to confirm two factor update") - }, - }, - { - Name: "Should return 400 BAD REQUEST 2FA type is the same as current", - ReqFn: func(t *testing.T) (bodies.Update2FABody, string) { - accessToken := genTwoFactorAccount(t, services.TwoFactorTotp) - return bodies.Update2FABody{ - TwoFactorType: services.TwoFactorTotp, - }, accessToken - }, - ExpStatus: http.StatusBadRequest, - AssertFn: func(t *testing.T, _ bodies.Update2FABody, res *http.Response) { - resBody := AssertTestResponseBody(t, res, exceptions.ErrorResponse{}) - AssertEqual(t, resBody.Message, "Account already uses given 2FA type") - }, - }, - { - Name: "Should return 400 BAD REQUEST if password is invalid", - ReqFn: func(t *testing.T) (bodies.Update2FABody, string) { - data := GenerateFakeAccountData(t, services.AuthProviderLocal) - account := CreateTestAccount(t, data) - accessToken, _ := GenerateTestAccountAuthTokens(t, &account) - return bodies.Update2FABody{ - TwoFactorType: services.TwoFactorTotp, - Password: "wrong-password", - }, accessToken - }, - ExpStatus: http.StatusBadRequest, - AssertFn: func(t *testing.T, req bodies.Update2FABody, res *http.Response) { - resBody := AssertTestResponseBody(t, res, exceptions.ErrorResponse{}) - AssertEqual(t, resBody.Message, "Invalid password") - }, - }, - { - Name: "Should return 401 UNAUTHORIZED if access token is missing", - ReqFn: func(t *testing.T) (bodies.Update2FABody, string) { - data := GenerateFakeAccountData(t, services.AuthProviderLocal) - CreateTestAccount(t, data) - return bodies.Update2FABody{ - TwoFactorType: services.TwoFactorTotp, - Password: data.Password, - }, "asdsad" - }, - ExpStatus: http.StatusUnauthorized, - AssertFn: AssertUnauthorizedError[bodies.Update2FABody], - }, - } - - for _, tc := range testCases { - t.Run(tc.Name, func(t *testing.T) { - PerformTestRequestCase(t, http.MethodPut, update2FAPath, tc) - }) - } - - t.Cleanup(accountsCleanUp(t)) -} - -func legacyAccountAuth2FAUpdateConfirm(t *testing.T) { - const confirm2FAPath = v1Path + paths.AuthBase + paths.Auth2FA + paths.Confirm - - genEmailCode := func(t *testing.T, account dtos.AccountDTO) string { - code, err := GetTestCache(t).AddTwoFactorCode(context.Background(), cache.AddTwoFactorCodeOptions{ - RequestID: uuid.NewString(), - AccountID: account.ID(), - TTL: GetTestTokens(t).Get2FATTL(), - }) - if err != nil { - t.Fatal("Failed to create email code", err) - } - return code - } - - genToptCode := func(t *testing.T, account dtos.AccountDTO) string { - accountTOTP, err := GetTestDatabase(t).FindAccountTotpByAccountID(context.Background(), account.ID()) - if err != nil { - t.Fatal("Failed to find account TOTP", err) - } - - ctx := context.Background() - requestID := uuid.NewString() - secret, serviceErr := GetTestCrypto(t).DecryptWithDEK(context.Background(), crypto.DecryptWithDEKOptions{ - RequestID: requestID, - GetDecryptDEKfn: GetTestServices(t).BuildGetGlobalDecDEKFn( - ctx, - services.BuildGetGlobalDEKFnOptions{RequestID: requestID}, - ), - Ciphertext: accountTOTP.Secret, - }) - if serviceErr != nil { - t.Fatal("Failed to decrypt TOTP secret", serviceErr) - } - - code, err := totp.GenerateCode(secret, time.Now().UTC()) - if err != nil { - t.Fatal("Failed to generate code", err) - } - - return code - } - - gen2FAUpdate := func(t *testing.T, old2FAType, new2FAType string) (string, string) { - accountData := GenerateFakeAccountData(t, services.AuthProviderGitHub) - account := CreateTestAccount(t, accountData) - testS := GetTestServices(t) - testC := GetTestCache(t) - requestID := uuid.NewString() - - authDTO, err := testS.CreateAccount2FAConfig(context.Background(), services.CreateAccount2FAConfigOptions{ - RequestID: requestID, - AccountPublicID: account.PublicID, - AccountVersion: account.Version(), - TwoFAType: old2FAType, - }) - if err != nil { - t.Fatalf("failed to enable 2FA for account: %v", err) - } - - if _, err := testC.SaveDelete2FAConfigRequest(context.Background(), cache.SaveDelete2FAConfigRequestOptions{ - RequestID: requestID, - PrefixType: cache.SensitiveRequestAccountPrefix, - PublicID: account.PublicID, - TwoFAType: new2FAType, - TTL: 300, - }); err != nil { - t.Fatalf("failed to save 2FA delete request: %v", err) - } - - switch old2FAType { - case services.TwoFactorEmail: - return Account2FAAccessToken(t, authDTO), genEmailCode(t, account) - case services.TwoFactorTotp: - return Account2FAAccessToken(t, authDTO), genToptCode(t, account) - default: - t.Fatalf("unsupported 2FA type: %s", old2FAType) - return "", "" - } - } - - testCases := []TestRequestCase[bodies.TwoFactorLoginBody]{ - { - Name: "Should confirm 2FA update from email to TOTP with valid code", - ReqFn: func(t *testing.T) (bodies.TwoFactorLoginBody, string) { - accessToken, code := gen2FAUpdate(t, services.TwoFactorEmail, services.TwoFactorTotp) - return bodies.TwoFactorLoginBody{Code: code}, accessToken - }, - ExpStatus: http.StatusOK, - AssertFn: func(t *testing.T, _ bodies.TwoFactorLoginBody, res *http.Response) { - resBody := AssertTestResponseBody(t, res, dtos.AuthDTO{}) - AssertEqual(t, resBody.Message, "Please scan QR Code with your authentication app") - AssertNotEmpty(t, resBody.Data["image"]) - AssertNotEmpty(t, resBody.Data["recovery_keys"]) - }, - }, - { - Name: "Should confirm 2FA update from TOTP to email with valid code", - ReqFn: func(t *testing.T) (bodies.TwoFactorLoginBody, string) { - accessToken, code := gen2FAUpdate(t, services.TwoFactorTotp, services.TwoFactorEmail) - return bodies.TwoFactorLoginBody{Code: code}, accessToken - }, - ExpStatus: http.StatusOK, - AssertFn: func(t *testing.T, _ bodies.TwoFactorLoginBody, res *http.Response) { - resBody := AssertTestResponseBody(t, res, dtos.AuthDTO{}) - AssertEqual(t, resBody.Message, "Please provide email two factor code") - }, - }, - { - Name: "Should disable 2FA with valid code", - ReqFn: func(t *testing.T) (bodies.TwoFactorLoginBody, string) { - accessToken, code := gen2FAUpdate(t, services.TwoFactorTotp, services.TwoFactorNone) - return bodies.TwoFactorLoginBody{Code: code}, accessToken - }, - ExpStatus: http.StatusOK, - AssertFn: func(t *testing.T, _ bodies.TwoFactorLoginBody, res *http.Response) { - resBody := AssertTestResponseBody(t, res, dtos.AuthDTO{}) - AssertEmpty(t, resBody.Message) - }, - }, - { - Name: "Should return 400 BAD REQUEST with invalid code", - ReqFn: func(t *testing.T) (bodies.TwoFactorLoginBody, string) { - accessToken, _ := gen2FAUpdate(t, services.TwoFactorEmail, services.TwoFactorTotp) - return bodies.TwoFactorLoginBody{Code: "invalid"}, accessToken - }, - ExpStatus: http.StatusBadRequest, - AssertFn: func(t *testing.T, req bodies.TwoFactorLoginBody, res *http.Response) { - resBody := AssertTestResponseBody(t, res, exceptions.ValidationErrorResponse{}) - AssertEqual(t, 1, len(resBody.Fields)) - AssertEqual(t, "code", resBody.Fields[0].Param) - AssertEqual(t, req.Code, resBody.Fields[0].Value.(string)) - }, - }, - { - Name: "Should return 401 UNAUTHORIZED if access token is missing", - ReqFn: func(t *testing.T) (bodies.TwoFactorLoginBody, string) { - _, code := gen2FAUpdate(t, services.TwoFactorEmail, services.TwoFactorTotp) - return bodies.TwoFactorLoginBody{Code: code}, "" - }, - ExpStatus: http.StatusUnauthorized, - AssertFn: AssertUnauthorizedError[bodies.TwoFactorLoginBody], - }, - } - - for _, tc := range testCases { - t.Run(tc.Name, func(t *testing.T) { - PerformTestRequestCase(t, http.MethodPost, confirm2FAPath, tc) - }) - } - - t.Cleanup(accountsCleanUp(t)) -} - func TestForgotAccountPassword(t *testing.T) { const forgotPasswordPath = v1Path + paths.AuthBase + paths.AuthForgotPassword diff --git a/idp/tests/dynamic_registration_test.go b/idp/tests/dynamic_registration_test.go new file mode 100644 index 0000000..d5e6508 --- /dev/null +++ b/idp/tests/dynamic_registration_test.go @@ -0,0 +1,669 @@ +package tests + +import ( + "bytes" + "context" + "crypto/ed25519" + "crypto/rand" + "encoding/json" + "fmt" + "io" + "net/http" + "net/http/httptest" + "net/url" + "strings" + "testing" + "time" + + "github.com/gofiber/fiber/v3" + "github.com/golang-jwt/jwt/v5" + "github.com/google/uuid" + "github.com/tugascript/devlogs/idp/internal/controllers/paths" + "github.com/tugascript/devlogs/idp/internal/exceptions" + "github.com/tugascript/devlogs/idp/internal/providers/cache" + "github.com/tugascript/devlogs/idp/internal/providers/database" + "github.com/tugascript/devlogs/idp/internal/services" + "github.com/tugascript/devlogs/idp/internal/services/dtos" + "github.com/tugascript/devlogs/idp/internal/utils" +) + +func oauthRegisterPath() string { + return paths.V1 + paths.AuthBase + paths.OAuthBase + paths.OAuthRegister +} + +func oauthRegisterClientPath(clientID string) string { + return oauthRegisterPath() + "/" + clientID +} + +func oauthIATTokenPath() string { + return paths.V1 + paths.AuthBase + paths.OAuthBase + paths.InitialAccessToken + paths.OAuthToken +} + +func requestURL(host, path string) string { + return "https://" + host + path +} + +func decodeJSONObject(t *testing.T, res *http.Response) map[string]json.RawMessage { + t.Helper() + body, err := io.ReadAll(res.Body) + if err != nil { + t.Fatal(err) + } + var response map[string]json.RawMessage + if len(bytes.TrimSpace(body)) == 0 { + return response + } + if err = json.Unmarshal(body, &response); err != nil { + t.Fatalf("decode json: %v body=%s", err, body) + } + return response +} + +func jsonString(raw json.RawMessage) string { + var value string + _ = json.Unmarshal(raw, &value) + return value +} + +func doJSONRequest(t *testing.T, method, rawURL, accessToken string, body any) *http.Response { + t.Helper() + var reader io.Reader + if body != nil { + encoded, err := json.Marshal(body) + if err != nil { + t.Fatal(err) + } + reader = bytes.NewReader(encoded) + } + req := httptest.NewRequest(method, rawURL, reader) + if body != nil { + req.Header.Set("Content-Type", "application/json") + } + if accessToken != "" { + req.Header.Set("Authorization", "Bearer "+accessToken) + } + res, err := GetTestServer(t).App.Test(req, fiber.TestConfig{Timeout: 30 * time.Second, FailOnTimeout: true}) + if err != nil { + t.Fatal(err) + } + return res +} + +func signSoftwareStatement(t *testing.T, private ed25519.PrivateKey, kid string, claims jwt.MapClaims) string { + t.Helper() + if _, ok := claims["iat"]; !ok { + claims["iat"] = time.Now().Unix() + } + token := jwt.NewWithClaims(jwt.SigningMethodEdDSA, claims) + token.Header["kid"] = kid + signed, err := token.SignedString(private) + if err != nil { + t.Fatal(err) + } + return signed +} + +func generateEd25519JWK(t *testing.T) (ed25519.PrivateKey, string, utils.Ed25519JWK) { + t.Helper() + public, private, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + t.Fatal(err) + } + kid := utils.Base62UUID() + return private, kid, utils.EncodeEd25519Jwk(public, kid) +} + +func approveSoftwareStatementKey(t *testing.T, account dtos.AccountDTO, publicJSON []byte, kid string, usage database.CredentialsUsage) { + t.Helper() + ctx := context.Background() + db := GetTestDatabase(t) + key, err := db.CreateCredentialsKey(ctx, database.CreateCredentialsKeyParams{ + AccountID: account.ID(), PublicKid: kid, PublicKey: publicJSON, + CryptoSuite: database.TokenCryptoSuiteEdDSA, Usage: usage, ExpiresAt: time.Now().Add(time.Hour), + }) + if err != nil { + t.Fatal(err) + } + var approvedID int32 + err = db.RawQueryRow(ctx, `INSERT INTO dynamic_registration_software_statement_keys (account_id,account_public_id,credentials_key_id,credentials_key_kid,root_domain) VALUES ($1,$2,$3,$4,$5) RETURNING id`, []interface{}{account.ID(), account.PublicID, key.ID, kid, "example.com"}).Scan(&approvedID) + if err != nil { + t.Fatal(err) + } +} + +type dcrSetup struct { + account dtos.AccountDTO + domain string + host string + appClient bool + clientName string + body map[string]any + statement string + accessToken string +} + +func cleanupAccount(t *testing.T, account dtos.AccountDTO) { + t.Helper() + t.Cleanup(func() { + var id int32 + if err := GetTestDatabase(t).RawQueryRow(context.Background(), `DELETE FROM accounts WHERE id=$1 RETURNING id`, []interface{}{account.ID()}).Scan(&id); err != nil { + t.Errorf("cleanup account: %v", err) + } + }) +} + +func setupDynamicRegistration(t *testing.T, appClient, bounded, statement bool) dcrSetup { + t.Helper() + ctx := context.Background() + svc := GetTestServices(t) + db := GetTestDatabase(t) + cfg := GetTestConfig(t) + account := CreateTestAccount(t, GenerateFakeAccountData(t, services.AuthProviderLocal)) + cleanupAccount(t, account) + + domain := utils.Base62UUID() + ".example.com" + domainRow, err := db.CreateDynamicRegistrationDomain(ctx, database.CreateDynamicRegistrationDomainParams{ + AccountID: account.ID(), AccountPublicID: account.PublicID, Domain: domain, + VerificationMethod: database.DomainVerificationMethodDnsTxtRecord, + Usages: []database.DynamicRegistrationUsage{database.DynamicRegistrationUsageApp, database.DynamicRegistrationUsageAccount}, + }) + if err != nil { + t.Fatal(err) + } + if _, err = db.VerifyDynamicRegistrationDomain(ctx, database.VerifyDynamicRegistrationDomainParams{ + ID: domainRow.ID, VerificationMethod: database.DomainVerificationMethodDnsTxtRecord, + }); err != nil { + t.Fatal(err) + } + + var requireIAT, requireSS []string + if bounded { + requireIAT = []string{"web"} + } + if statement { + requireSS = []string{"web"} + } + if appClient { + if _, _, serviceErr := svc.SaveAppDynamicRegistrationConfig(ctx, services.SaveAppDynamicRegistrationConfigOptions{ + RequestID: uuid.NewString(), AccountPublicID: account.PublicID, AccountVersion: account.Version(), + AllowedAppTypes: []string{"web"}, DefaultUsernameColumn: "email", DefaultAuthProviders: []string{"local"}, + DefaultAllowedScopes: []string{"openid", "profile"}, DefaultScopes: []string{"openid"}, + RequireInitialAccessTokenAppTypes: requireIAT, RequireSoftwareStatementAppTypes: requireSS, + SoftwareStatementVerificationMethods: []string{"manual"}, InitialAccessTokenGenerationMethods: []string{"manual", "authorization_code"}, + InitialAccessTokenTtl: 300, InitialAccessTokenMaxUses: 10, + AllowedGrantTypes: []string{"authorization_code", "refresh_token"}, AllowedResponseTypes: []string{"code"}, + AllowedTokenEndpointAuthMethods: []string{"client_secret_basic", "none", "private_key_jwt"}, MaxRedirectUris: 10, + }); serviceErr != nil { + t.Fatal(serviceErr) + } + } else { + required := []string{} + if statement { + required = []string{"service"} + } + if _, _, serviceErr := svc.SaveAccountDynamicRegistrationConfig(ctx, services.SaveAccountDynamicRegistrationConfigOptions{ + RequestID: uuid.NewString(), AccountPublicID: account.PublicID, AccountVersion: account.Version(), + AccountCredentialsTypes: []string{"service"}, RequireSoftwareStatementCredentialTypes: required, + SoftwareStatementVerificationMethods: []string{"manual"}, + }); serviceErr != nil { + t.Fatal(serviceErr) + } + } + + private, kid, jwk := generateEd25519JWK(t) + publicJSON, err := json.Marshal(jwk) + if err != nil { + t.Fatal(err) + } + clientName := "Registration " + utils.Base62UUID() + body := map[string]any{ + "client_name": clientName, "client_uri": "https://" + domain, + "redirect_uris": []string{"https://" + domain + "/callback/"}, + "token_endpoint_auth_method": "client_secret_basic", "grant_types": []string{"authorization_code"}, + "response_types": []string{"code"}, "scope": "profile", "contacts": []string{"developer@example.com"}, + "logo_uri": "https://" + domain + "/logo.png", "tos_uri": "https://" + domain + "/terms", + "policy_uri": "https://" + domain + "/privacy", "software_id": uuid.NewString(), "software_version": "1.0", + "jwks": map[string]any{"keys": []any{jwk}}, + } + if appClient { + body["application_type"] = "web" + body["scope"] = "openid profile" + } else { + body["application_type"] = "service" + body["grant_types"] = []string{"client_credentials"} + body["response_types"] = []string{} + } + + signedStatement := "" + if statement { + usage := database.CredentialsUsageApp + if !appClient { + usage = database.CredentialsUsageAccount + } + approveSoftwareStatementKey(t, account, publicJSON, kid, usage) + signedStatement = signSoftwareStatement(t, private, kid, jwt.MapClaims{ + "iss": "https://" + domain, "iat": time.Now().Unix(), + "client_name": clientName, "client_uri": "https://" + domain, "software_version": "2.0", + }) + body["software_statement"] = signedStatement + delete(body, "client_name") + delete(body, "client_uri") + } + + accessToken := "" + if bounded { + if appClient { + auth, serviceErr := svc.CreateAppCredentialsRegistrationIAT(ctx, services.CreateAppCredentialsRegistrationIATOptions{ + RequestID: uuid.NewString(), AccountPublicID: account.PublicID, AccountVersion: account.Version(), + Domain: domain, BackendDomain: cfg.BackendDomain(), + }) + if serviceErr != nil { + t.Fatal(serviceErr) + } + accessToken = auth.AccessToken + } else { + signed, serviceErr := svc.CreateAccountCredentialsRegistrationIAT(ctx, services.CreateAccountCredentialsRegistrationIATOptions{ + RequestID: uuid.NewString(), AccountPublicID: account.PublicID, AccountVersion: account.Version(), + Domain: domain, BackendDomain: cfg.BackendDomain(), + }) + if serviceErr != nil { + t.Fatal(serviceErr) + } + accessToken = signed + } + } + + host := cfg.BackendDomain() + if appClient { + host = account.Username + "." + host + } + return dcrSetup{ + account: account, domain: domain, host: host, appClient: appClient, + clientName: clientName, body: body, statement: signedStatement, accessToken: accessToken, + } +} + +func postRegister(t *testing.T, setup dcrSetup) *http.Response { + t.Helper() + return doJSONRequest(t, http.MethodPost, requestURL(setup.host, oauthRegisterPath()), setup.accessToken, setup.body) +} + +func assertCreatedRegistration(t *testing.T, setup dcrSetup, res *http.Response) (clientID, rat, registrationURI string) { + t.Helper() + defer res.Body.Close() + response := decodeJSONObject(t, res) + if res.StatusCode != http.StatusCreated { + t.Fatalf("status=%d error=%s", res.StatusCode, response["error"]) + } + clientID = jsonString(response["client_id"]) + returnedName := jsonString(response["client_name"]) + scope := jsonString(response["scope"]) + version := jsonString(response["software_version"]) + returnedStatement := jsonString(response["software_statement"]) + rat = jsonString(response["registration_access_token"]) + registrationURI = jsonString(response["registration_client_uri"]) + if clientID == "" || returnedName != setup.clientName || returnedStatement != setup.statement { + t.Fatalf("unexpected response metadata: id=%q name=%q scope=%q", clientID, returnedName, scope) + } + if !strings.Contains(scope, "profile") { + t.Fatalf("missing profile scope: %q", scope) + } + if setup.statement != "" && version != "2.0" { + t.Fatal("statement did not override body software_version") + } + for _, field := range []string{"client_secret", "client_secret_expires_at", "client_id_issued_at", "grant_types", "response_types", "jwks", "contacts"} { + if _, ok := response[field]; !ok { + t.Errorf("missing %s", field) + } + } + if rat == "" || registrationURI == "" { + t.Fatal("missing registration_access_token or registration_client_uri") + } + if want := dtos.RegistrationClientURI(setup.host, clientID); registrationURI != want { + t.Fatalf("registration_client_uri=%q want %q", registrationURI, want) + } + if res.Header.Get("Cache-Control") != "no-store" { + t.Error("registration response is cacheable") + } + + ctx := context.Background() + db := GetTestDatabase(t) + if setup.appClient { + row, err := db.FindAppByClientID(ctx, clientID) + if err != nil { + t.Fatal(err) + } + if row.AccountID != setup.account.ID() || row.ClientName != setup.clientName || len(row.Jwks) == 0 || row.RedirectUris[0] != "https://"+setup.domain+"/callback/" { + t.Fatal("app metadata was not persisted correctly") + } + if row.CreationMethod != database.CreationMethodDynamicRegistration { + t.Fatalf("creation_method=%q", row.CreationMethod) + } + } else { + row, err := db.FindAccountCredentialsByClientID(ctx, clientID) + if err != nil { + t.Fatal(err) + } + if row.AccountID != setup.account.ID() || row.ClientName != setup.clientName || len(row.Jwks) == 0 || row.RedirectUris[0] != "https://"+setup.domain+"/callback/" { + t.Fatal("account credentials metadata was not persisted correctly") + } + if row.CreationMethod != database.CreationMethodDynamicRegistration { + t.Fatalf("creation_method=%q", row.CreationMethod) + } + } + return clientID, rat, registrationURI +} + +// Exercise the actual HTTP route, IAT verification, software-statement trust, +// credential issuance and database persistence together. +func TestDynamicRegistration(t *testing.T) { + for _, appClient := range []bool{true, false} { + for _, bounded := range []bool{false, true} { + if !appClient && !bounded { + continue + } // Account registration is protected by policy. + for _, statement := range []bool{false, true} { + t.Run(fmt.Sprintf("app=%t/iat=%t/statement=%t", appClient, bounded, statement), func(t *testing.T) { + setup := setupDynamicRegistration(t, appClient, bounded, statement) + res := postRegister(t, setup) + assertCreatedRegistration(t, setup, res) + }) + } + } + } +} + +func TestDynamicRegistrationIATHostIsolation(t *testing.T) { + cfg := GetTestConfig(t) + + t.Run("account IAT cannot register an app", func(t *testing.T) { + setup := setupDynamicRegistration(t, false, true, false) + setup.host = setup.account.Username + "." + cfg.BackendDomain() + setup.body["application_type"] = "web" + if _, _, serviceErr := GetTestServices(t).SaveAppDynamicRegistrationConfig(context.Background(), services.SaveAppDynamicRegistrationConfigOptions{ + RequestID: uuid.NewString(), AccountPublicID: setup.account.PublicID, AccountVersion: setup.account.Version(), + AllowedAppTypes: []string{"web"}, DefaultUsernameColumn: "email", DefaultAuthProviders: []string{"local"}, + DefaultAllowedScopes: []string{"openid", "profile"}, DefaultScopes: []string{"openid"}, + SoftwareStatementVerificationMethods: []string{"manual"}, InitialAccessTokenGenerationMethods: []string{"manual", "authorization_code"}, + InitialAccessTokenTtl: 300, InitialAccessTokenMaxUses: 10, + AllowedGrantTypes: []string{"authorization_code", "refresh_token"}, AllowedResponseTypes: []string{"code"}, + AllowedTokenEndpointAuthMethods: []string{"client_secret_basic", "none", "private_key_jwt"}, MaxRedirectUris: 10, + }); serviceErr != nil { + t.Fatal(serviceErr) + } + res := postRegister(t, setup) + defer res.Body.Close() + response := decodeJSONObject(t, res) + if res.StatusCode != http.StatusUnauthorized { + t.Fatalf("status=%d error=%s", res.StatusCode, response["error"]) + } + if jsonString(response["error"]) != exceptions.OAuthErrorAccessDenied { + t.Fatalf("error=%s", response["error"]) + } + }) + + t.Run("app IAT cannot register account credentials", func(t *testing.T) { + setup := setupDynamicRegistration(t, true, true, false) + setup.host = cfg.BackendDomain() + setup.body["application_type"] = "native" + res := postRegister(t, setup) + defer res.Body.Close() + response := decodeJSONObject(t, res) + if res.StatusCode != http.StatusUnauthorized { + t.Fatalf("status=%d error=%s", res.StatusCode, response["error"]) + } + if jsonString(response["error"]) != exceptions.OAuthErrorAccessDenied { + t.Fatalf("error=%s", response["error"]) + } + }) +} + +func TestDynamicRegistrationSoftwareStatementFailures(t *testing.T) { + setup := setupDynamicRegistration(t, true, false, false) + goodPrivate, goodKid, goodJWK := generateEd25519JWK(t) + goodJSON, err := json.Marshal(goodJWK) + if err != nil { + t.Fatal(err) + } + approveSoftwareStatementKey(t, setup.account, goodJSON, goodKid, database.CredentialsUsageApp) + + cases := []struct { + name string + statement string + wantError string + }{ + { + name: "bad signature", + statement: func() string { + badPrivate, _, _ := generateEd25519JWK(t) + return signSoftwareStatement(t, badPrivate, goodKid, jwt.MapClaims{ + "iss": "https://" + setup.domain, "client_name": setup.clientName, "client_uri": "https://" + setup.domain, + }) + }(), + wantError: exceptions.OAuthErrorInvalidSoftwareStatement, + }, + { + name: "unapproved kid", + statement: func() string { + private, kid, _ := generateEd25519JWK(t) + return signSoftwareStatement(t, private, kid, jwt.MapClaims{ + "iss": "https://" + setup.domain, "client_name": setup.clientName, "client_uri": "https://" + setup.domain, + }) + }(), + wantError: exceptions.OAuthErrorUnapprovedSoftwareStatement, + }, + { + name: "issuer mismatch", + statement: signSoftwareStatement(t, goodPrivate, goodKid, jwt.MapClaims{ + "iss": "https://unrelated.example.net", "client_name": setup.clientName, "client_uri": "https://" + setup.domain, + }), + wantError: exceptions.OAuthErrorUnapprovedSoftwareStatement, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + body := cloneMap(setup.body) + body["software_statement"] = tc.statement + res := doJSONRequest(t, http.MethodPost, requestURL(setup.host, oauthRegisterPath()), "", body) + defer res.Body.Close() + response := decodeJSONObject(t, res) + if res.StatusCode != http.StatusBadRequest { + t.Fatalf("status=%d error=%s", res.StatusCode, response["error"]) + } + if jsonString(response["error"]) != tc.wantError { + t.Fatalf("error=%s want %s", response["error"], tc.wantError) + } + }) + } +} + +func cloneMap(src map[string]any) map[string]any { + out := make(map[string]any, len(src)) + for k, v := range src { + out[k] = v + } + return out +} + +func TestRFC7592ClientConfiguration(t *testing.T) { + cfg := GetTestConfig(t) + + t.Run("app", func(t *testing.T) { + setup := setupDynamicRegistration(t, true, false, false) + res := postRegister(t, setup) + clientID, rat, registrationURI := assertCreatedRegistration(t, setup, res) + if registrationURI != requestURL(setup.host, oauthRegisterClientPath(clientID)) { + t.Fatalf("registration_client_uri=%q", registrationURI) + } + assertRFC7592Lifecycle(t, setup, clientID, rat, cfg.BackendDomain()) + }) + + t.Run("account credentials", func(t *testing.T) { + setup := setupDynamicRegistration(t, false, true, false) + res := postRegister(t, setup) + clientID, rat, _ := assertCreatedRegistration(t, setup, res) + assertRFC7592Lifecycle(t, setup, clientID, rat, cfg.BackendDomain()) + }) + + t.Run("well-known registration_endpoint", func(t *testing.T) { + account := CreateTestAccount(t, GenerateFakeAccountData(t, services.AuthProviderLocal)) + cleanupAccount(t, account) + host := account.Username + "." + cfg.BackendDomain() + res := doJSONRequest(t, http.MethodGet, requestURL(host, paths.WellKnownBase+paths.WellKnownOIDC), "", nil) + defer res.Body.Close() + response := decodeJSONObject(t, res) + if res.StatusCode != http.StatusOK { + t.Fatalf("status=%d error=%s", res.StatusCode, response["error"]) + } + want := requestURL(host, oauthRegisterPath()) + if jsonString(response["registration_endpoint"]) != want { + t.Fatalf("registration_endpoint=%s want %s", response["registration_endpoint"], want) + } + }) +} + +func assertRFC7592Lifecycle(t *testing.T, setup dcrSetup, clientID, rat, backendDomain string) { + t.Helper() + clientURL := requestURL(setup.host, oauthRegisterClientPath(clientID)) + + getRes := doJSONRequest(t, http.MethodGet, clientURL, rat, nil) + defer getRes.Body.Close() + getBody := decodeJSONObject(t, getRes) + if getRes.StatusCode != http.StatusOK { + t.Fatalf("GET status=%d error=%s", getRes.StatusCode, getBody["error"]) + } + if getRes.Header.Get("Cache-Control") != "no-store" { + t.Error("GET response is cacheable") + } + if jsonString(getBody["client_id"]) != clientID || jsonString(getBody["client_name"]) != setup.clientName { + t.Fatalf("GET metadata mismatch: %s", getBody["client_name"]) + } + if _, ok := getBody["client_secret"]; ok { + t.Fatal("GET must not return client_secret") + } + if _, ok := getBody["registration_access_token"]; ok { + t.Fatal("GET must not return registration_access_token") + } + + updatedName := "Updated " + setup.clientName + putBody := cloneMap(setup.body) + putBody["client_name"] = updatedName + putRes := doJSONRequest(t, http.MethodPut, clientURL, rat, putBody) + defer putRes.Body.Close() + putResponse := decodeJSONObject(t, putRes) + if putRes.StatusCode != http.StatusOK { + t.Fatalf("PUT status=%d error=%s", putRes.StatusCode, putResponse["error"]) + } + if jsonString(putResponse["client_name"]) != updatedName { + t.Fatalf("PUT did not update client_name: %s", putResponse["client_name"]) + } + if _, ok := putResponse["client_secret"]; ok { + t.Fatal("PUT must not return client_secret") + } + + confirm := doJSONRequest(t, http.MethodGet, clientURL, rat, nil) + defer confirm.Body.Close() + confirmBody := decodeJSONObject(t, confirm) + if jsonString(confirmBody["client_name"]) != updatedName { + t.Fatal("updated name was not persisted") + } + + unknown := doJSONRequest(t, http.MethodGet, requestURL(setup.host, oauthRegisterClientPath("missing-client")), rat, nil) + defer unknown.Body.Close() + if unknown.StatusCode != http.StatusUnauthorized { + t.Fatalf("unknown client status=%d", unknown.StatusCode) + } + + crossHost := backendDomain + if !setup.appClient { + crossHost = setup.account.Username + "." + backendDomain + } + cross := doJSONRequest(t, http.MethodGet, requestURL(crossHost, oauthRegisterClientPath(clientID)), rat, nil) + defer cross.Body.Close() + if cross.StatusCode != http.StatusUnauthorized { + t.Fatalf("cross-host RAT status=%d", cross.StatusCode) + } + + delRes := doJSONRequest(t, http.MethodDelete, clientURL, rat, nil) + defer delRes.Body.Close() + if delRes.StatusCode != http.StatusNoContent { + t.Fatalf("DELETE status=%d", delRes.StatusCode) + } + after := doJSONRequest(t, http.MethodGet, clientURL, rat, nil) + defer after.Body.Close() + if after.StatusCode != http.StatusUnauthorized { + t.Fatalf("GET after DELETE status=%d", after.StatusCode) + } +} + +func TestOAuthDynamicRegistrationIATTokenExchange(t *testing.T) { + ctx := context.Background() + cfg := GetTestConfig(t) + cacheStore := GetTestCache(t) + + for _, appClient := range []bool{false, true} { + t.Run(fmt.Sprintf("app=%t", appClient), func(t *testing.T) { + setup := setupDynamicRegistration(t, appClient, false, false) + verifier := utils.Base62UUID() + utils.Base62UUID() + challenge := utils.Sha256HashBase64(verifier) + hostUsername := "" + if appClient { + hostUsername = setup.account.Username + } + code, err := cacheStore.GenerateAccountCredentialsRegistrationIATCode(ctx, cache.GenerateAccountCredentialsRegistrationIATCodeOptions{ + HostUsername: hostUsername, RequestID: uuid.NewString(), ClientID: setup.domain, + AccountPublicID: setup.account.PublicID, AccountVersion: setup.account.Version(), + Domain: setup.domain, Challenge: challenge, + }) + if err != nil { + t.Fatal(err) + } + + form := url.Values{} + form.Set("grant_type", "authorization_code") + form.Set("code", code) + form.Set("client_id", setup.domain) + form.Set("code_verifier", verifier) + req := httptest.NewRequest(http.MethodPost, requestURL(setup.host, oauthIATTokenPath()), strings.NewReader(form.Encode())) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + res, err := GetTestServer(t).App.Test(req, fiber.TestConfig{Timeout: 30 * time.Second, FailOnTimeout: true}) + if err != nil { + t.Fatal(err) + } + defer res.Body.Close() + response := decodeJSONObject(t, res) + if res.StatusCode != http.StatusOK { + t.Fatalf("status=%d error=%s", res.StatusCode, response["error"]) + } + accessToken := jsonString(response["access_token"]) + if accessToken == "" { + t.Fatal("missing access_token") + } + + wrongHost := setup.account.Username + "." + cfg.BackendDomain() + if appClient { + wrongHost = cfg.BackendDomain() + } + mismatchCode, err := cacheStore.GenerateAccountCredentialsRegistrationIATCode(ctx, cache.GenerateAccountCredentialsRegistrationIATCodeOptions{ + HostUsername: hostUsername, RequestID: uuid.NewString(), ClientID: setup.domain, + AccountPublicID: setup.account.PublicID, AccountVersion: setup.account.Version(), + Domain: setup.domain, Challenge: challenge, + }) + if err != nil { + t.Fatal(err) + } + form.Set("code", mismatchCode) + badReq := httptest.NewRequest(http.MethodPost, requestURL(wrongHost, oauthIATTokenPath()), strings.NewReader(form.Encode())) + badReq.Header.Set("Content-Type", "application/x-www-form-urlencoded") + badRes, err := GetTestServer(t).App.Test(badReq, fiber.TestConfig{Timeout: 30 * time.Second, FailOnTimeout: true}) + if err != nil { + t.Fatal(err) + } + defer badRes.Body.Close() + if badRes.StatusCode != http.StatusUnauthorized { + t.Fatalf("cross-host token exchange status=%d", badRes.StatusCode) + } + }) + } +} From 0a366efec6ac214ca42bcc4ca5a68c293ef890db Mon Sep 17 00:00:00 2001 From: Afonso Barracha Date: Thu, 10 Sep 2026 23:24:15 +1200 Subject: [PATCH 4/4] fix: fix dynamic registration controller --- idp/internal/controllers/helpers.go | 5 --- idp/internal/controllers/middleware.go | 28 +++++--------- .../controllers/oauth_dynamic_registration.go | 37 ++++++------------- idp/internal/server/routes/oauth.go | 10 ++--- .../account_credentials_registration.go | 6 +-- .../services/app_dynamic_registration.go | 6 +-- .../services/registration_metadata.go | 21 +++++------ 7 files changed, 40 insertions(+), 73 deletions(-) diff --git a/idp/internal/controllers/helpers.go b/idp/internal/controllers/helpers.go index b05bee6..7b35c08 100644 --- a/idp/internal/controllers/helpers.go +++ b/idp/internal/controllers/helpers.go @@ -100,11 +100,6 @@ func serviceErrorResponse(logger *slog.Logger, ctx fiber.Ctx, serviceErr *except return ctx.Status(status).JSON(&resErr) } -func (c *Controllers) NotFoundHandler(ctx fiber.Ctx) error { - logger := c.buildLogger(getRequestID(ctx), "helpers", "NotFoundHandler") - return serviceErrorResponse(logger, ctx, exceptions.NewNotFoundError()) -} - func serviceErrorWithFieldsResponse(logger *slog.Logger, ctx fiber.Ctx, serviceErr *exceptions.ServicErrorWithFields) error { logResponse(logger, ctx, fiber.StatusBadRequest) return ctx.Status(fiber.StatusBadRequest).JSON(exceptions.NewValidationErrorResponse( diff --git a/idp/internal/controllers/middleware.go b/idp/internal/controllers/middleware.go index aeca462..23a8301 100644 --- a/idp/internal/controllers/middleware.go +++ b/idp/internal/controllers/middleware.go @@ -23,14 +23,6 @@ import ( const middlewareLocation string = "middleware" -func continueMiddleware(ctx fiber.Ctx) error { - if hostAware, ok := ctx.Locals("hostAwareRoute").(bool); ok && hostAware { - return nil - } - - return ctx.Next() -} - func (c *Controllers) UserAccessClaimsMiddleware(ctx fiber.Ctx) error { requestID := getRequestID(ctx) logger := c.buildLogger(requestID, middlewareLocation, "UserAccessClaimsMiddleware") @@ -61,7 +53,7 @@ func (c *Controllers) UserAccessClaimsMiddleware(ctx fiber.Ctx) error { ctx.Locals("user", userClaims) ctx.Locals("app", appClaims) ctx.Locals("userScopes", userScopes) - return continueMiddleware(ctx) + return ctx.Next() } func (c *Controllers) User2FAClaimsMiddleware(ctx fiber.Ctx) error { @@ -93,7 +85,7 @@ func (c *Controllers) User2FAClaimsMiddleware(ctx fiber.Ctx) error { ctx.Locals("user", userClaims) ctx.Locals("app", appClaims) - return continueMiddleware(ctx) + return ctx.Next() } func (c *Controllers) AccountAccessClaimsMiddleware(ctx fiber.Ctx) error { @@ -116,7 +108,7 @@ func (c *Controllers) AccountAccessClaimsMiddleware(ctx fiber.Ctx) error { ctx.Locals("account", accountClaims) ctx.Locals("scopes", scopes) - return continueMiddleware(ctx) + return ctx.Next() } func (c *Controllers) TwoFAAccessClaimsMiddleware(ctx fiber.Ctx) error { @@ -140,7 +132,7 @@ func (c *Controllers) TwoFAAccessClaimsMiddleware(ctx fiber.Ctx) error { ctx.Locals("account", accountClaims) ctx.Locals("twoFAType", twoFAType) - return continueMiddleware(ctx) + return ctx.Next() } func (c *Controllers) AppAccessClaimsMiddleware(ctx fiber.Ctx) error { @@ -170,7 +162,7 @@ func (c *Controllers) AppAccessClaimsMiddleware(ctx fiber.Ctx) error { } ctx.Locals("app", appClaims) - return continueMiddleware(ctx) + return ctx.Next() } func (c *Controllers) DynamicRegistrationIATMiddleware(ctx fiber.Ctx) error { @@ -199,7 +191,7 @@ func (c *Controllers) DynamicRegistrationIATMiddleware(ctx fiber.Ctx) error { ctx.Locals("account", accountClaims) ctx.Locals("domain", domain) - return continueMiddleware(ctx) + return ctx.Next() } func (c *Controllers) AppDynamicRegistrationIATMiddleware(ctx fiber.Ctx) error { @@ -215,7 +207,7 @@ func (c *Controllers) AppDynamicRegistrationIATMiddleware(ctx fiber.Ctx) error { authHeader := ctx.Get("Authorization") if authHeader == "" { logger.InfoContext(ctx.Context(), "No Authorization header found, skipping app dynamic registration IAT middleware") - return continueMiddleware(ctx) + return ctx.Next() } domain, accountClaims, serviceErr := c.services.ProcessAppDynamicRegistrationIATAuth( @@ -236,7 +228,7 @@ func (c *Controllers) AppDynamicRegistrationIATMiddleware(ctx fiber.Ctx) error { ctx.Locals("account", accountClaims) ctx.Locals("domain", domain) ctx.Locals("isAuthenticated", true) - return continueMiddleware(ctx) + return ctx.Next() } func (c *Controllers) DynamicRegistrationAccessTokenMiddleware(ctx fiber.Ctx) error { @@ -261,7 +253,7 @@ func (c *Controllers) DynamicRegistrationAccessTokenMiddleware(ctx fiber.Ctx) er ctx.Locals("account", accountClaims) ctx.Locals("registrationClientID", clientID) - return continueMiddleware(ctx) + return ctx.Next() } func (c *Controllers) AppDynamicRegistrationAccessTokenMiddleware(ctx fiber.Ctx) error { @@ -292,7 +284,7 @@ func (c *Controllers) AppDynamicRegistrationAccessTokenMiddleware(ctx fiber.Ctx) ctx.Locals("account", accountClaims) ctx.Locals("registrationClientID", clientID) - return continueMiddleware(ctx) + return ctx.Next() } func (c *Controllers) ScopeMiddleware(scope tokens.AccountScope) func(fiber.Ctx) error { diff --git a/idp/internal/controllers/oauth_dynamic_registration.go b/idp/internal/controllers/oauth_dynamic_registration.go index bb0bf87..576800d 100644 --- a/idp/internal/controllers/oauth_dynamic_registration.go +++ b/idp/internal/controllers/oauth_dynamic_registration.go @@ -21,19 +21,14 @@ func (c *Controllers) OAuthDynamicRegistration(ctx fiber.Ctx) error { requestID := getRequestID(ctx) logger := c.buildLogger(requestID, oauthDynamicRegistration, "OAuthDynamicRegistration") logRequest(logger, ctx) - ctx.Set("Cache-Control", "no-store") - ctx.Set("Pragma", "no-cache") + ctx.Set(fiber.HeaderCacheControl, "no-store") + ctx.Set(fiber.HeaderPragma, "no-cache") accountClaims, ok := ctx.Locals("account").(tokens.AccountClaims) if !ok { logger.ErrorContext(ctx.Context(), "account should be set in context by middleware") return oauthErrorResponse(logger, ctx, exceptions.OAuthErrorServerError) } - domain, ok := ctx.Locals("domain").(string) - if !ok { - logger.ErrorContext(ctx.Context(), "domain should be set in context by middleware") - return oauthErrorResponse(logger, ctx, exceptions.OAuthErrorServerError) - } body := new(bodies.OAuthDynamicClientRegistrationBody) if err := ctx.Bind().Body(body); err != nil { @@ -45,7 +40,6 @@ func (c *Controllers) OAuthDynamicRegistration(ctx fiber.Ctx) error { services.CreateAccountCredentialsRegistrationOptions{ RequestID: requestID, AccountPublicID: accountClaims.AccountID, - IATDomain: domain, AccountVersion: accountClaims.AccountVersion, ApplicationType: body.ApplicationType, RedirectURIs: body.RedirectURIs, @@ -98,8 +92,8 @@ func (c *Controllers) OAuthAppDynamicRegistration(ctx fiber.Ctx) error { requestID := getRequestID(ctx) logger := c.buildLogger(requestID, oauthDynamicRegistration, "OAuthAppDynamicRegistration") logRequest(logger, ctx) - ctx.Set("Cache-Control", "no-store") - ctx.Set("Pragma", "no-cache") + ctx.Set(fiber.HeaderCacheControl, "no-store") + ctx.Set(fiber.HeaderPragma, "no-cache") _, accountID, serviceErr := getHostAccount(ctx) if serviceErr != nil { @@ -117,12 +111,6 @@ func (c *Controllers) OAuthAppDynamicRegistration(ctx fiber.Ctx) error { return oauthErrorResponse(logger, ctx, exceptions.OAuthErrorServerError) } - domain, ok := ctx.Locals("domain").(string) - if isAuthenticated && !ok { - logger.ErrorContext(ctx.Context(), "domain should be set in context by middleware") - return oauthErrorResponse(logger, ctx, exceptions.OAuthErrorServerError) - } - account, ok := ctx.Locals("account").(tokens.AccountClaims) if isAuthenticated && !ok { logger.ErrorContext(ctx.Context(), "account should be set in context by middleware") @@ -135,7 +123,6 @@ func (c *Controllers) OAuthAppDynamicRegistration(ctx fiber.Ctx) error { RequestID: requestID, IsAuthenticated: isAuthenticated, AccountID: accountID, - IATDomain: domain, AccountVersion: account.AccountVersion, ApplicationType: body.ApplicationType, RedirectURIs: body.RedirectURIs, @@ -201,8 +188,8 @@ func (c *Controllers) OAuthDynamicRegistrationGet(ctx fiber.Ctx) error { requestID := getRequestID(ctx) logger := c.buildLogger(requestID, oauthDynamicRegistration, "OAuthDynamicRegistrationGet") logRequest(logger, ctx) - ctx.Set("Cache-Control", "no-store") - ctx.Set("Pragma", "no-cache") + ctx.Set(fiber.HeaderCacheControl, "no-store") + ctx.Set(fiber.HeaderPragma, "no-cache") accountClaims, ok := ctx.Locals("account").(tokens.AccountClaims) tokenClientID, tokenOK := registrationClientIDFromContext(ctx) @@ -224,8 +211,8 @@ func (c *Controllers) OAuthAppDynamicRegistrationGet(ctx fiber.Ctx) error { requestID := getRequestID(ctx) logger := c.buildLogger(requestID, oauthDynamicRegistration, "OAuthAppDynamicRegistrationGet") logRequest(logger, ctx) - ctx.Set("Cache-Control", "no-store") - ctx.Set("Pragma", "no-cache") + ctx.Set(fiber.HeaderCacheControl, "no-store") + ctx.Set(fiber.HeaderPragma, "no-cache") username, _, serviceErr := getHostAccount(ctx) accountClaims, ok := ctx.Locals("account").(tokens.AccountClaims) @@ -249,8 +236,8 @@ func (c *Controllers) OAuthDynamicRegistrationUpdate(ctx fiber.Ctx) error { requestID := getRequestID(ctx) logger := c.buildLogger(requestID, oauthDynamicRegistration, "OAuthDynamicRegistrationUpdate") logRequest(logger, ctx) - ctx.Set("Cache-Control", "no-store") - ctx.Set("Pragma", "no-cache") + ctx.Set(fiber.HeaderCacheControl, "no-store") + ctx.Set(fiber.HeaderPragma, "no-cache") accountClaims, ok := ctx.Locals("account").(tokens.AccountClaims) tokenClientID, tokenOK := registrationClientIDFromContext(ctx) @@ -292,8 +279,8 @@ func (c *Controllers) OAuthAppDynamicRegistrationUpdate(ctx fiber.Ctx) error { requestID := getRequestID(ctx) logger := c.buildLogger(requestID, oauthDynamicRegistration, "OAuthAppDynamicRegistrationUpdate") logRequest(logger, ctx) - ctx.Set("Cache-Control", "no-store") - ctx.Set("Pragma", "no-cache") + ctx.Set(fiber.HeaderCacheControl, "no-store") + ctx.Set(fiber.HeaderPragma, "no-cache") username, accountID, serviceErr := getHostAccount(ctx) tokenClientID, tokenOK := registrationClientIDFromContext(ctx) diff --git a/idp/internal/server/routes/oauth.go b/idp/internal/server/routes/oauth.go index 7ba6355..81ac1c4 100644 --- a/idp/internal/server/routes/oauth.go +++ b/idp/internal/server/routes/oauth.go @@ -89,15 +89,11 @@ func (r *Routes) OAuthRoutes(app *fiber.App) { iatRouter := router.Group(paths.InitialAccessToken, r.controllers.HostMiddleware) iatRouter.Post( paths.InitialAccessTokenSign, - HostAwareRoute( - []fiber.Handler{r.controllers.NotFoundHandler}, - []fiber.Handler{ - r.controllers.AccountAccessClaimsMiddleware, - r.controllers.AppDynamicRegistrationIATSign, - }, - ), + r.controllers.AccountAccessClaimsMiddleware, + r.controllers.AppDynamicRegistrationIATSign, ) + // TODO: add host aware routes to all IAT oauth flow // Dynamic Registration IAT Code Exchange flow iatRouter.Get(paths.OAuthAuth, r.controllers.OAuthDynamicRegistrationIATAuth) iatRouter.Post(paths.OAuthToken, r.controllers.OAuthDynamicRegistrationIATToken) diff --git a/idp/internal/services/account_credentials_registration.go b/idp/internal/services/account_credentials_registration.go index ffbbbee..953560e 100644 --- a/idp/internal/services/account_credentials_registration.go +++ b/idp/internal/services/account_credentials_registration.go @@ -225,7 +225,6 @@ func (s *Services) mapAccountCredentialsRegistrationDataToDBParams( type CreateAccountCredentialsRegistrationOptions struct { RequestID string AccountPublicID uuid.UUID - IATDomain string AccountVersion int32 ApplicationType string RedirectURIs []string @@ -315,9 +314,10 @@ func (s *Services) CreateAccountCredentialsRegistration( TokenEndpointAuthSigningAlg: opts.TokenEndpointAuthSigningAlg, AccessTokenSigningAlg: opts.AccessTokenSigningAlg, } + iatDomain := registrationDomain(opts.ClientURI, opts.RedirectURIs) data, preparationErr := s.prepareDynamicRegistration(ctx, prepareDynamicRegistrationOptions{ requestID: opts.RequestID, accountID: 0, accountPublicID: opts.AccountPublicID, - data: data, softwareStatement: opts.SoftwareStatement, iatDomain: opts.IATDomain, + data: data, softwareStatement: opts.SoftwareStatement, backendDomain: opts.BackendDomain, frontendDomain: opts.FrontendDomain, app: false, }) if preparationErr != nil { @@ -437,7 +437,7 @@ func (s *Services) CreateAccountCredentialsRegistration( _, serviceErr = s.checkClientRegistrationDomain(ctx, checkClientRegistrationDomainOptions{ requestID: opts.RequestID, accountPublicID: opts.AccountPublicID, - iatDomain: opts.IATDomain, + iatDomain: iatDomain, domain: domain, usages: accountCredentialsRegistrationUsages, requireVerifiedDomains: slices.Contains(accountDRConfigDTO.RequireVerifiedDomainsCredentialsType, applicationType), diff --git a/idp/internal/services/app_dynamic_registration.go b/idp/internal/services/app_dynamic_registration.go index f78809c..c7560d4 100644 --- a/idp/internal/services/app_dynamic_registration.go +++ b/idp/internal/services/app_dynamic_registration.go @@ -299,7 +299,6 @@ type CreateAppCredentialsRegistrationOptions struct { RequestID string AccountID int32 IsAuthenticated bool - IATDomain string AccountVersion int32 ApplicationType string RedirectURIs []string @@ -389,9 +388,10 @@ func (s *Services) CreateAppCredentialsRegistration( TokenEndpointAuthSigningAlg: opts.TokenEndpointAuthSigningAlg, AccessTokenSigningAlg: opts.AccessTokenSigningAlg, } + iatDomain := registrationDomain(opts.ClientURI, opts.RedirectURIs) data, preparationErr := s.prepareDynamicRegistration(ctx, prepareDynamicRegistrationOptions{ requestID: opts.RequestID, accountID: opts.AccountID, accountPublicID: uuid.Nil, - data: data, softwareStatement: opts.SoftwareStatement, iatDomain: opts.IATDomain, + data: data, softwareStatement: opts.SoftwareStatement, backendDomain: opts.BackendDomain, frontendDomain: opts.FrontendDomain, app: true, }) if preparationErr != nil { @@ -531,7 +531,7 @@ func (s *Services) CreateAppCredentialsRegistration( _, serviceErr = s.checkClientRegistrationDomain(ctx, checkClientRegistrationDomainOptions{ requestID: opts.RequestID, accountPublicID: accountDTO.PublicID, - iatDomain: opts.IATDomain, + iatDomain: iatDomain, usages: appDynamicRegistrationUsages, domain: domain, requireVerifiedDomains: slices.Contains(appDRConfigDTO.RequireVerifiedDomainsAppTypes, appType), diff --git a/idp/internal/services/registration_metadata.go b/idp/internal/services/registration_metadata.go index 08824d1..6a67c46 100644 --- a/idp/internal/services/registration_metadata.go +++ b/idp/internal/services/registration_metadata.go @@ -19,12 +19,12 @@ import ( ) type prepareDynamicRegistrationOptions struct { - requestID string - accountID int32 - accountPublicID uuid.UUID - data ApplicationRegistrationData - softwareStatement, iatDomain, backendDomain, frontendDomain string - app bool + requestID string + accountID int32 + accountPublicID uuid.UUID + data ApplicationRegistrationData + softwareStatement, backendDomain, frontendDomain string + app bool } // Merge verified claims by presence, including explicit false, zero and empty arrays. @@ -100,7 +100,7 @@ func (s *Services) prepareDynamicRegistration(ctx context.Context, opts prepareD if claimJWKS, ok := preview["jwks_uri"].(string); ok && claimJWKS != "" { jwksURI = claimJWKS } - domain := registrationDomain(keyURI, data.RedirectURIs, opts.iatDomain) + domain := registrationDomain(keyURI, data.RedirectURIs) base, err := publicsuffix.EffectiveTLDPlusOne(domain) if err != nil { return data, exceptions.NewInvalidTokenError("invalid software statement domain") @@ -146,7 +146,7 @@ func (s *Services) prepareDynamicRegistration(ctx context.Context, opts prepareD data.ClientName = "Client " + utils.Base62UUID() } if data.ClientURI == "" { - domain := registrationDomain("", data.RedirectURIs, opts.iatDomain) + domain := registrationDomain("", data.RedirectURIs) if domain == "" { return data, exceptions.NewValidationError("a client domain could not be determined") } @@ -164,13 +164,10 @@ func (s *Services) prepareDynamicRegistration(ctx context.Context, opts prepareD return data, nil } -func registrationDomain(clientURI string, redirects []string, fallback string) string { +func registrationDomain(clientURI string, redirects []string) string { if parsed, err := url.Parse(clientURI); err == nil && parsed.Hostname() != "" { return parsed.Hostname() } - if fallback != "" { - return fallback - } for _, redirect := range redirects { if parsed, err := url.Parse(redirect); err == nil && parsed.Hostname() != "" { return parsed.Hostname()