From b34a6a9af3e7d901d38f6a2daf83d796bdd72e2d Mon Sep 17 00:00:00 2001 From: Afonso Barracha Date: Mon, 7 Sep 2026 23:36:19 +1200 Subject: [PATCH 01/16] 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 02/16] 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 03/16] 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 04/16] 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() From aa238619e3d0776cddbd9506a3634944308b27a9 Mon Sep 17 00:00:00 2001 From: Afonso Barracha Date: Mon, 14 Sep 2026 16:36:54 +1200 Subject: [PATCH 05/16] fix(auth): add grants, sessions and session tokens to accounts --- idp/dbml-error.log | 9 + idp/initial_schema.dbml | 205 +++++- .../controllers/account_2fa_configs.go | 2 + idp/internal/controllers/accounts.go | 14 + idp/internal/controllers/auth.go | 8 + idp/internal/controllers/oauth.go | 4 + .../oauth_dynamic_registration_iat.go | 1 + ...ccount_credentials_dynamic_registration.go | 3 - .../providers/database/account_grants.sql.go | 148 ++++ .../database/account_sessions.sql.go | 174 +++++ .../database/app_related_apps.sql.go | 7 +- idp/internal/providers/database/apps.sql.go | 119 ++- idp/internal/providers/database/grants.sql.go | 55 ++ ...41213231542_create_initial_schema.down.sql | 6 +- ...0241213231542_create_initial_schema.up.sql | 347 ++++++--- idp/internal/providers/database/models.go | 142 +++- .../database/queries/account_grants.sql | 41 ++ .../database/queries/account_sessions.sql | 51 ++ .../providers/database/queries/grants.sql | 20 + .../database/queries/revoked_tokens.sql | 26 - .../database/queries/session_tokens.sql | 30 + .../providers/database/queries/sessions.sql | 32 + .../providers/database/registered_apps.sql.go | 14 +- .../providers/database/revoked_tokens.sql.go | 79 -- .../providers/database/session_tokens.sql.go | 90 +++ .../providers/database/sessions.sql.go | 92 +++ idp/internal/providers/tokens/access.go | 3 +- idp/internal/providers/tokens/accounts.go | 33 +- idp/internal/providers/tokens/refresh.go | 2 +- idp/internal/providers/tokens/users.go | 20 +- idp/internal/services/account_2fa_configs.go | 53 +- idp/internal/services/account_credentials.go | 5 + idp/internal/services/accounts.go | 204 ++++-- idp/internal/services/auth.go | 687 +++++++++++++++--- idp/internal/services/dtos/app.go | 98 +-- idp/internal/services/oauth.go | 27 +- .../services/oauth_dynamic_registration.go | 157 +--- .../oauth_dynamic_registration_accounts.go | 212 ++++++ .../oauth_dynamic_registration_apps.go | 7 + idp/internal/utils/ids.go | 6 +- 40 files changed, 2594 insertions(+), 639 deletions(-) create mode 100644 idp/internal/providers/database/account_grants.sql.go create mode 100644 idp/internal/providers/database/account_sessions.sql.go create mode 100644 idp/internal/providers/database/grants.sql.go create mode 100644 idp/internal/providers/database/queries/account_grants.sql create mode 100644 idp/internal/providers/database/queries/account_sessions.sql create mode 100644 idp/internal/providers/database/queries/grants.sql delete mode 100644 idp/internal/providers/database/queries/revoked_tokens.sql create mode 100644 idp/internal/providers/database/queries/session_tokens.sql create mode 100644 idp/internal/providers/database/queries/sessions.sql delete mode 100644 idp/internal/providers/database/revoked_tokens.sql.go create mode 100644 idp/internal/providers/database/session_tokens.sql.go create mode 100644 idp/internal/providers/database/sessions.sql.go create mode 100644 idp/internal/services/oauth_dynamic_registration_accounts.go create mode 100644 idp/internal/services/oauth_dynamic_registration_apps.go diff --git a/idp/dbml-error.log b/idp/dbml-error.log index 8337ecd..2f5569c 100644 --- a/idp/dbml-error.log +++ b/idp/dbml-error.log @@ -40,3 +40,12 @@ undefined 2025-10-30T18:50:05.002Z undefined +2026-09-10T22:44:27.719Z +undefined + +2026-09-10T22:44:49.809Z +undefined + +2026-09-10T22:45:04.236Z +undefined + diff --git a/idp/initial_schema.dbml b/idp/initial_schema.dbml index ef9df09..c7beaf4 100644 --- a/idp/initial_schema.dbml +++ b/idp/initial_schema.dbml @@ -1,5 +1,5 @@ // 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/. @@ -12,7 +12,7 @@ Enum kek_usage { Table key_encryption_keys as KEK { id serial [pk] - + kid uuid [not null] usage kek_usage [not null] version integer [not null, default: 1] @@ -37,7 +37,7 @@ Enum dek_usage { Table data_encryption_keys as DEK { id serial [pk] - + kid varchar(22) [not null] dek text [not null] kek_kid uuid [not null] @@ -97,7 +97,7 @@ Enum token_key_type { Table token_signing_keys as TS { id serial [pk] - + kid varchar(22) [not null] key_type token_key_type [not null] public_key jsonb [not null] @@ -133,7 +133,7 @@ Enum activity_status { Table accounts as A { id serial [pk] public_id uuid [not null] - + given_name varchar(100) [not null] family_name varchar(100) [not null] username varchar(63) [not null] // maximum length of a DNS label @@ -522,7 +522,7 @@ Enum auth_provider { Table account_auth_providers as AP { id serial [pk] - + email varchar(250) [not null] provider auth_provider [not null] account_public_id uuid [not null] @@ -813,6 +813,11 @@ Enum grant_type { "urn:ietf:params:oauth:grant-type:jwt-bearer" } +Enum session_type { + "sliding" + "fixed" +} + Table apps as APP { id serial [pk] account_id integer [not null] @@ -874,10 +879,13 @@ Table apps as APP { // Custom field for selecting access token signing algorithm access_token_signing_alg token_crypto_suite [not null, default: 'ES256'] - // Tokens TTLs - id_token_ttl integer [not null, default: 300] // 5 minutes - token_ttl integer [not null, default: 300] // 5 minutes - refresh_token_ttl integer [not null, default: 604800] // 7 days + // Tokens TTLs and session type + session_type session_type [not null] + access_token_ttl integer [not null] + id_token_ttl integer [null] + refresh_token_idle_ttl integer [null] + refresh_token_ttl integer [null] + grant_ttl integer [null] created_at timestamptz [not null, default: `now()`] updated_at timestamptz [not null, default: `now()`] @@ -1138,7 +1146,7 @@ Table dynamic_registration_software_statement_keys as DRSK { root_domain varchar(250) [not null] created_at timestamptz [not null, default: `now()`] - + Indexes { (account_id) [name: 'drs_statement_keys_account_id_idx'] (account_public_id) [name: 'drs_statement_keys_account_public_id_idx'] @@ -1182,23 +1190,180 @@ Enum token_owner { "account" } -Table revoked_tokens as RT { +Table grants as G { id serial [pk] + account_id integer [not null] - token_id uuid [not null] + grant_id uuid [not null] + granted_client_id varchar(22) [not null] + granted_scopes "scopes[]" [not null] + granted_custom_scopes "varchar(512)[]" [not null] + + issued_at timestamptz [not null, default: `now()`] + last_active_at timestamptz [not null, default: `now()`] + + created_at timestamptz [not null, default: `now()`] + updated_at timestamptz [not null, default: `now()`] + + Indexes { + (account_id) [name: 'grants_account_id_idx'] + (grant_id) [unique, name: 'grant_id_uidx'] + (granted_client_id) [name: 'grants_granted_client_id_idx'] + } +} +Ref: G.account_id > A.id [delete: cascade] + +Table account_grants as AG { + account_id integer [not null] + account_version integer [not null] + + grant_id integer [not null] + account_credentials_id integer [null] + + granted_client_id varchar(22) [not null] + is_revoked boolean [not null, default: false] + revoked_at timestamptz [null] + expires_at timestamptz [null] + + created_at timestamptz [not null, default: `now()`] + + Indexes { + (account_id, grant_id) [pk] + (account_id) [name: 'account_grants_account_id_idx'] + (grant_id) [unique, name: 'account_grants_grant_id_uidx'] + (account_credentials_id) [name: 'account_grants_account_credentials_id_idx'] + (account_id, granted_client_id) [unique, name: 'account_grants_account_id_granted_client_id_uidx', note: 'WHERE is_revoked = false'] // Partial: WHERE is_revoked = false + } +} +Ref: AG.account_id > A.id [delete: cascade] +Ref: AG.grant_id > G.id [delete: cascade] +Ref: AG.account_credentials_id > AC.id [delete: cascade] + +Table user_grants as UG { + user_id integer [not null] + user_version integer [not null] + grant_id integer [not null] + app_id integer [not null] + account_id integer [not null] + + granted_client_id varchar(22) [not null] + is_revoked boolean [not null, default: false] + revoked_at timestamptz [null] + expires_at timestamptz [null] + + created_at timestamptz [not null, default: `now()`] + + Indexes { + (user_id, grant_id) [pk] + (user_id) [name: 'user_grants_user_id_idx'] + (grant_id) [unique, name: 'user_grants_grant_id_uidx'] + (app_id) [name: 'user_grants_app_id_idx'] + (account_id) [name: 'user_grants_account_id_idx'] + (user_id, granted_client_id) [unique, name: 'user_grants_user_id_granted_client_id_uidx', note: 'WHERE is_revoked = false'] // Partial: WHERE is_revoked = false + } +} +Ref: UG.user_id > U.id [delete: cascade] +Ref: UG.grant_id > G.id [delete: cascade] +Ref: UG.app_id > APP.id [delete: cascade] +Ref: UG.account_id > A.id [delete: cascade] + +Table sessions as S { + id serial [pk] + account_id integer [not null] + grant_id integer [not null] + + session_id uuid [not null] + session_type session_type [not null] + session_client_id varchar(22) [not null] + + ip_address varchar(45) [null] + user_agent text [null] + + issued_at timestamptz [not null, default: `now()`] + expires_at timestamptz [not null] + + created_at timestamptz [not null, default: `now()`] + updated_at timestamptz [not null, default: `now()`] + + Indexes { + (account_id) [name: 'sessions_account_id_idx'] + (grant_id) [name: 'sessions_grant_id_idx'] + (session_id) [unique, name: 'sessions_session_id_uidx'] + (expires_at) [name: 'sessions_expires_at_idx'] + } +} +Ref: S.account_id > A.id [delete: cascade] +Ref: S.grant_id > G.id [delete: cascade] + +Table account_sessions as AS { + account_id integer [not null] + account_version integer [not null] + + session_id integer [not null] + account_credentials_id integer [null] + + session_uuid uuid [not null] + + created_at timestamptz [not null, default: `now()`] + + Indexes { + (account_id, session_id) [pk] + (account_id) [name: 'account_sessions_account_id_idx'] + (session_id) [unique, name: 'account_sessions_session_id_uidx'] + (account_id, session_uuid) [unique, name: 'account_sessions_account_id_session_uuid_uidx'] + (account_credentials_id) [name: 'account_sessions_account_credentials_id_idx'] + } +} +Ref: AS.account_id > A.id [delete: cascade] +Ref: AS.session_id > S.id [delete: cascade] +Ref: AS.account_credentials_id > AC.id [delete: cascade] + +Table user_sessions as US { + user_id integer [not null] + user_version integer [not null] + session_id integer [not null] + app_id integer [not null] + account_id integer [not null] + + session_uuid uuid [not null] + created_at timestamptz [not null, default: `now()`] + + Indexes { + (user_id, session_id) [pk] + (user_id) [name: 'user_sessions_user_id_idx'] + (session_id) [unique, name: 'user_sessions_session_id_uidx'] + (app_id) [name: 'user_sessions_app_id_idx'] + (account_id) [name: 'user_sessions_account_id_idx'] + } +} +Ref: US.user_id > U.id [delete: cascade] +Ref: US.session_id > S.id [delete: cascade] +Ref: US.app_id > APP.id [delete: cascade] +Ref: US.account_id > A.id [delete: cascade] + +Table session_tokens as ST { + id serial [pk] account_id integer [not null] - owner token_owner [not null] - owner_public_id uuid [not null] - issued_at timestamptz [not null] + session_id integer [not null] + grant_id integer [not null] + + session_uuid uuid [not null] + token_id uuid [not null] + + issued_at timestamptz [not null, default: `now()`] expires_at timestamptz [not null] created_at timestamptz [not null, default: `now()`] Indexes { - (token_id) [unique, name: 'revoked_tokens_token_id_uidx'] - (account_id) [name: 'revoked_tokens_account_id_idx'] - (expires_at) [name: 'revoked_tokens_expires_at_idx'] + (token_id) [unique, name: 'allowed_tokens_token_id_uidx'] + (account_id) [name: 'allowed_tokens_account_id_idx'] + (session_id) [name: 'allowed_tokens_session_id_idx'] + (grant_id) [name: 'allowed_tokens_grant_id_idx'] + (expires_at) [name: 'allowed_tokens_expires_at_idx'] } } -Ref: RT.account_id > A.id [delete: cascade] +Ref: ST.account_id > A.id [delete: cascade] +Ref: ST.grant_id > G.id [delete: cascade] +Ref: ST.session_id > S.id [delete: cascade] diff --git a/idp/internal/controllers/account_2fa_configs.go b/idp/internal/controllers/account_2fa_configs.go index ba9f26c..39d0c82 100644 --- a/idp/internal/controllers/account_2fa_configs.go +++ b/idp/internal/controllers/account_2fa_configs.go @@ -202,6 +202,8 @@ func (c *Controllers) ConfirmDeleteAccount2FAConfig(ctx fiber.Ctx) error { Version: accountClaims.AccountVersion, TwoFAType: urlParams.TwoFAType, Code: body.Code, + IPAddress: ctx.IP(), + UserAgent: ctx.Get(fiber.HeaderUserAgent), }, ) if serviceErr != nil { diff --git a/idp/internal/controllers/accounts.go b/idp/internal/controllers/accounts.go index b32cec8..900ba04 100644 --- a/idp/internal/controllers/accounts.go +++ b/idp/internal/controllers/accounts.go @@ -62,6 +62,8 @@ func (c *Controllers) UpdateAccountPassword(ctx fiber.Ctx) error { Version: accountClaims.AccountVersion, Password: body.OldPassword, NewPassword: body.Password, + IPAddress: ctx.IP(), + UserAgent: ctx.Get(fiber.HeaderUserAgent), }) if serviceErr != nil { return serviceErrorResponse(logger, ctx, serviceErr) @@ -99,6 +101,8 @@ func (c *Controllers) ConfirmUpdateAccountPassword(ctx fiber.Ctx) error { Version: accountClaims.AccountVersion, TwoFAType: twoFAType, Code: body.Code, + IPAddress: ctx.IP(), + UserAgent: ctx.Get(fiber.HeaderUserAgent), }) if serviceErr != nil { return serviceErrorResponse(logger, ctx, serviceErr) @@ -132,6 +136,8 @@ func (c *Controllers) CreateAccountPassword(ctx fiber.Ctx) error { PublicID: accountClaims.AccountID, Version: accountClaims.AccountVersion, Password: body.Password, + IPAddress: ctx.IP(), + UserAgent: ctx.Get(fiber.HeaderUserAgent), }) if serviceErr != nil { return serviceErrorResponse(logger, ctx, serviceErr) @@ -166,6 +172,8 @@ func (c *Controllers) UpdateAccountEmail(ctx fiber.Ctx) error { Version: accountClaims.AccountVersion, Email: body.Email, Password: body.Password, + IPAddress: ctx.IP(), + UserAgent: ctx.Get(fiber.HeaderUserAgent), }) if serviceErr != nil { return serviceErrorResponse(logger, ctx, serviceErr) @@ -203,6 +211,8 @@ func (c *Controllers) ConfirmUpdateAccountEmail(ctx fiber.Ctx) error { Version: accountClaims.AccountVersion, TwoFAType: twoFAType, Code: body.Code, + IPAddress: ctx.IP(), + UserAgent: ctx.Get(fiber.HeaderUserAgent), }) if serviceErr != nil { return serviceErrorResponse(logger, ctx, serviceErr) @@ -341,6 +351,8 @@ func (c *Controllers) UpdateAccountUsername(ctx fiber.Ctx) error { Version: accountClaims.AccountVersion, Username: body.Username, Password: body.Password, + IPAddress: ctx.IP(), + UserAgent: ctx.Get(fiber.HeaderUserAgent), }) if serviceErr != nil { return serviceErrorResponse(logger, ctx, serviceErr) @@ -380,6 +392,8 @@ func (c *Controllers) ConfirmUpdateAccountUsername(ctx fiber.Ctx) error { Version: accountClaims.AccountVersion, TwoFAType: twoFAType, Code: body.Code, + IPAddress: ctx.IP(), + UserAgent: ctx.Get(fiber.HeaderUserAgent), }, ) if serviceErr != nil { diff --git a/idp/internal/controllers/auth.go b/idp/internal/controllers/auth.go index 4d9464c..55e460c 100644 --- a/idp/internal/controllers/auth.go +++ b/idp/internal/controllers/auth.go @@ -88,6 +88,8 @@ func (c *Controllers) ConfirmAccount(ctx fiber.Ctx) error { authDTO, serviceErr := c.services.ConfirmAccount(ctx.Context(), services.ConfirmAccountOptions{ RequestID: requestID, ConfirmationToken: body.ConfirmationToken, + IPAddress: ctx.IP(), + UserAgent: ctx.Get(fiber.HeaderUserAgent), }) if serviceErr != nil { return serviceErrorResponse(logger, ctx, serviceErr) @@ -115,6 +117,8 @@ func (c *Controllers) LoginAccount(ctx fiber.Ctx) error { RequestID: requestID, Email: body.Email, Password: body.Password, + IPAddress: ctx.IP(), + UserAgent: ctx.Get(fiber.HeaderUserAgent), }) if serviceErr != nil { return serviceErrorResponse(logger, ctx, serviceErr) @@ -152,6 +156,8 @@ func (c *Controllers) TwoFactorLoginAccount(ctx fiber.Ctx) error { AccountVersion: accountClaims.AccountVersion, TwoFAType: twoFAType, Code: body.Code, + IPAddress: ctx.IP(), + UserAgent: ctx.Get(fiber.HeaderUserAgent), }) if serviceErr != nil { return serviceErrorResponse(logger, ctx, serviceErr) @@ -246,6 +252,8 @@ func (c *Controllers) RefreshAccount(ctx fiber.Ctx) error { authDTO, serviceErr := c.services.RefreshTokenAccount(ctx.Context(), services.RefreshTokenAccountOptions{ RequestID: requestID, RefreshToken: refreshToken, + IPAddress: ctx.IP(), + UserAgent: ctx.Get(fiber.HeaderUserAgent), }) if serviceErr != nil { if isCookie { diff --git a/idp/internal/controllers/oauth.go b/idp/internal/controllers/oauth.go index 356e098..b9e942c 100644 --- a/idp/internal/controllers/oauth.go +++ b/idp/internal/controllers/oauth.go @@ -224,6 +224,8 @@ func (c *Controllers) accountAuthorizationCodeToken(ctx fiber.Ctx, requestID str Code: body.Code, ChallengeVerifier: body.CodeVerifier, Provider: body.ClientID, + IPAddress: ctx.IP(), + UserAgent: ctx.Get(fiber.HeaderUserAgent), }) if serviceErr != nil { return oauthErrorResponseMapper(logger, ctx, serviceErr) @@ -252,6 +254,8 @@ func (c *Controllers) accountRefreshToken(ctx fiber.Ctx, requestID string) error authDTO, serviceErr := c.services.RefreshTokenAccount(ctx.Context(), services.RefreshTokenAccountOptions{ RequestID: requestID, RefreshToken: body.RefreshToken, + IPAddress: ctx.IP(), + UserAgent: ctx.Get(fiber.HeaderUserAgent), }) if serviceErr != nil { return oauthErrorResponseMapper(logger, ctx, serviceErr) diff --git a/idp/internal/controllers/oauth_dynamic_registration_iat.go b/idp/internal/controllers/oauth_dynamic_registration_iat.go index 8fa9c17..078423d 100644 --- a/idp/internal/controllers/oauth_dynamic_registration_iat.go +++ b/idp/internal/controllers/oauth_dynamic_registration_iat.go @@ -80,6 +80,7 @@ func (c *Controllers) OAuthDynamicRegistrationIATAuth(ctx fiber.Ctx) error { HostUsername: registrationHostUsername(ctx), RequestID: requestID, Domain: baseQPrms.ClientID, + Origin: ctx.Get(fiber.HeaderOrigin), State: qPrms.State, SessionKey: sessionKey, RefreshToken: ctx.Cookies(c.cookieName + refreshCookieSuffix), diff --git a/idp/internal/providers/cache/account_credentials_dynamic_registration.go b/idp/internal/providers/cache/account_credentials_dynamic_registration.go index 482116d..b49c98e 100644 --- a/idp/internal/providers/cache/account_credentials_dynamic_registration.go +++ b/idp/internal/providers/cache/account_credentials_dynamic_registration.go @@ -36,7 +36,6 @@ type AccountCredentialsDynamicRegistrationIATAuthData struct { Domain string `json:"domain"` State string `json:"state"` Challenge string `json:"challenge"` - Username string `json:"username,omitempty"` } type SaveAccountCredentialsDynamicRegistrationIATAuthOptions struct { @@ -45,7 +44,6 @@ type SaveAccountCredentialsDynamicRegistrationIATAuthOptions struct { State string RedirectURI string Challenge string - Username string } func (c *Cache) SaveAccountCredentialsDynamicRegistrationIATAuth( @@ -66,7 +64,6 @@ func (c *Cache) SaveAccountCredentialsDynamicRegistrationIATAuth( Domain: opts.Domain, RedirectURI: opts.RedirectURI, Challenge: opts.Challenge, - Username: opts.Username, } dataBytes, err := json.Marshal(data) if err != nil { diff --git a/idp/internal/providers/database/account_grants.sql.go b/idp/internal/providers/database/account_grants.sql.go new file mode 100644 index 0000000..d286361 --- /dev/null +++ b/idp/internal/providers/database/account_grants.sql.go @@ -0,0 +1,148 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 +// source: account_grants.sql + +package database + +import ( + "context" + "time" + + "github.com/jackc/pgx/v5/pgtype" +) + +const createAccountGrantWithAccountCredentials = `-- name: CreateAccountGrantWithAccountCredentials :exec +INSERT INTO "account_grants" ( + "account_id", + "account_version", + "grant_id", + "granted_client_id", + "account_credentials_id" +) VALUES ( + $1, + $2, + $3, + $4, + $5 +) +` + +type CreateAccountGrantWithAccountCredentialsParams struct { + AccountID int32 + AccountVersion int32 + GrantID int32 + GrantedClientID string + AccountCredentialsID pgtype.Int4 +} + +func (q *Queries) CreateAccountGrantWithAccountCredentials(ctx context.Context, arg CreateAccountGrantWithAccountCredentialsParams) error { + _, err := q.db.Exec(ctx, createAccountGrantWithAccountCredentials, + arg.AccountID, + arg.AccountVersion, + arg.GrantID, + arg.GrantedClientID, + arg.AccountCredentialsID, + ) + return err +} + +const createAccountGrantWithoutAccountCredentials = `-- name: CreateAccountGrantWithoutAccountCredentials :exec + +INSERT INTO "account_grants" ( + "account_id", + "account_version", + "grant_id", + "granted_client_id" +) VALUES ( + $1, + $2, + $3, + $4 +) +` + +type CreateAccountGrantWithoutAccountCredentialsParams struct { + AccountID int32 + AccountVersion int32 + GrantID int32 + GrantedClientID string +} + +// 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/. +func (q *Queries) CreateAccountGrantWithoutAccountCredentials(ctx context.Context, arg CreateAccountGrantWithoutAccountCredentialsParams) error { + _, err := q.db.Exec(ctx, createAccountGrantWithoutAccountCredentials, + arg.AccountID, + arg.AccountVersion, + arg.GrantID, + arg.GrantedClientID, + ) + return err +} + +const findAccountGrantByAccountIDAndGrantedClientID = `-- name: FindAccountGrantByAccountIDAndGrantedClientID :one +SELECT a.account_id, a.account_version, a.grant_id, a.account_credentials_id, a.granted_client_id, a.is_revoked, a.revoked_at, a.expires_at, a.created_at, g.id, g.account_id, g.grant_id, g.granted_client_id, g.granted_scopes, g.granted_custom_scopes, g.issued_at, g.last_active_at, g.created_at, g.updated_at +FROM "account_grants" AS "a" +LEFT JOIN "grants" AS "g" ON "a"."grant_id" = "g"."id" +WHERE "a"."account_id" = $1 +AND "a"."granted_client_id" = $2 +LIMIT 1 +` + +type FindAccountGrantByAccountIDAndGrantedClientIDParams struct { + AccountID int32 + GrantedClientID string +} + +type FindAccountGrantByAccountIDAndGrantedClientIDRow struct { + AccountID int32 + AccountVersion int32 + GrantID int32 + AccountCredentialsID pgtype.Int4 + GrantedClientID string + IsRevoked bool + RevokedAt pgtype.Timestamptz + ExpiresAt pgtype.Timestamptz + CreatedAt time.Time + ID pgtype.Int4 + AccountID_2 pgtype.Int4 + GrantID_2 pgtype.UUID + GrantedClientID_2 pgtype.Text + GrantedScopes []Scopes + GrantedCustomScopes []string + IssuedAt pgtype.Timestamptz + LastActiveAt pgtype.Timestamptz + CreatedAt_2 pgtype.Timestamptz + UpdatedAt pgtype.Timestamptz +} + +func (q *Queries) FindAccountGrantByAccountIDAndGrantedClientID(ctx context.Context, arg FindAccountGrantByAccountIDAndGrantedClientIDParams) (FindAccountGrantByAccountIDAndGrantedClientIDRow, error) { + row := q.db.QueryRow(ctx, findAccountGrantByAccountIDAndGrantedClientID, arg.AccountID, arg.GrantedClientID) + var i FindAccountGrantByAccountIDAndGrantedClientIDRow + err := row.Scan( + &i.AccountID, + &i.AccountVersion, + &i.GrantID, + &i.AccountCredentialsID, + &i.GrantedClientID, + &i.IsRevoked, + &i.RevokedAt, + &i.ExpiresAt, + &i.CreatedAt, + &i.ID, + &i.AccountID_2, + &i.GrantID_2, + &i.GrantedClientID_2, + &i.GrantedScopes, + &i.GrantedCustomScopes, + &i.IssuedAt, + &i.LastActiveAt, + &i.CreatedAt_2, + &i.UpdatedAt, + ) + return i, err +} diff --git a/idp/internal/providers/database/account_sessions.sql.go b/idp/internal/providers/database/account_sessions.sql.go new file mode 100644 index 0000000..53588ab --- /dev/null +++ b/idp/internal/providers/database/account_sessions.sql.go @@ -0,0 +1,174 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 +// source: account_sessions.sql + +package database + +import ( + "context" + "time" + + "github.com/google/uuid" + "github.com/jackc/pgx/v5/pgtype" +) + +const createAccountSessionWithAccountCredentials = `-- name: CreateAccountSessionWithAccountCredentials :exec +INSERT INTO "account_sessions" ( + "account_id", + "account_version", + "session_id", + "session_uuid", + "account_credentials_id" +) VALUES ( + $1, + $2, + $3, + $4, + $5 +) +` + +type CreateAccountSessionWithAccountCredentialsParams struct { + AccountID int32 + AccountVersion int32 + SessionID int32 + SessionUuid uuid.UUID + AccountCredentialsID pgtype.Int4 +} + +func (q *Queries) CreateAccountSessionWithAccountCredentials(ctx context.Context, arg CreateAccountSessionWithAccountCredentialsParams) error { + _, err := q.db.Exec(ctx, createAccountSessionWithAccountCredentials, + arg.AccountID, + arg.AccountVersion, + arg.SessionID, + arg.SessionUuid, + arg.AccountCredentialsID, + ) + return err +} + +const createAccountSessionWithoutAccountCredentials = `-- name: CreateAccountSessionWithoutAccountCredentials :exec + +INSERT INTO "account_sessions" ( + "account_id", + "account_version", + "session_id", + "session_uuid" +) VALUES ( + $1, + $2, + $3, + $4 +) +` + +type CreateAccountSessionWithoutAccountCredentialsParams struct { + AccountID int32 + AccountVersion int32 + SessionID int32 + SessionUuid uuid.UUID +} + +// 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/. +func (q *Queries) CreateAccountSessionWithoutAccountCredentials(ctx context.Context, arg CreateAccountSessionWithoutAccountCredentialsParams) error { + _, err := q.db.Exec(ctx, createAccountSessionWithoutAccountCredentials, + arg.AccountID, + arg.AccountVersion, + arg.SessionID, + arg.SessionUuid, + ) + return err +} + +const deleteAccountSessionByAccountIDAndSessionID = `-- name: DeleteAccountSessionByAccountIDAndSessionID :exec +DELETE FROM "account_sessions" +WHERE "account_id" = $1 AND "session_id" = $2 +` + +type DeleteAccountSessionByAccountIDAndSessionIDParams struct { + AccountID int32 + SessionID int32 +} + +func (q *Queries) DeleteAccountSessionByAccountIDAndSessionID(ctx context.Context, arg DeleteAccountSessionByAccountIDAndSessionIDParams) error { + _, err := q.db.Exec(ctx, deleteAccountSessionByAccountIDAndSessionID, arg.AccountID, arg.SessionID) + return err +} + +const deleteAllSessionsByAccountID = `-- name: DeleteAllSessionsByAccountID :exec +DELETE FROM "sessions" AS "s" +USING "account_sessions" AS "a" +WHERE "a"."session_id" = "s"."id" +AND "a"."account_id" = $1 +` + +func (q *Queries) DeleteAllSessionsByAccountID(ctx context.Context, accountID int32) error { + _, err := q.db.Exec(ctx, deleteAllSessionsByAccountID, accountID) + return err +} + +const findAccountSessionByAccountIDAndSessionUUID = `-- name: FindAccountSessionByAccountIDAndSessionUUID :one +SELECT a.account_id, a.account_version, a.session_id, a.account_credentials_id, a.session_uuid, a.created_at, s.id, s.account_id, s.grant_id, s.session_id, s.session_type, s.session_client_id, s.ip_address, s.user_agent, s.issued_at, s.expires_at, s.created_at, s.updated_at +FROM "account_sessions" AS "a" +LEFT JOIN "sessions" AS "s" ON "a"."session_id" = "s"."id" +WHERE "a"."account_id" = $1 +AND "a"."session_uuid" = $2 +LIMIT 1 +` + +type FindAccountSessionByAccountIDAndSessionUUIDParams struct { + AccountID int32 + SessionUuid uuid.UUID +} + +type FindAccountSessionByAccountIDAndSessionUUIDRow struct { + AccountID int32 + AccountVersion int32 + SessionID int32 + AccountCredentialsID pgtype.Int4 + SessionUuid uuid.UUID + CreatedAt time.Time + ID pgtype.Int4 + AccountID_2 pgtype.Int4 + GrantID pgtype.Int4 + SessionID_2 pgtype.UUID + SessionType NullSessionType + SessionClientID pgtype.Text + IpAddress pgtype.Text + UserAgent pgtype.Text + IssuedAt pgtype.Timestamptz + ExpiresAt pgtype.Timestamptz + CreatedAt_2 pgtype.Timestamptz + UpdatedAt pgtype.Timestamptz +} + +func (q *Queries) FindAccountSessionByAccountIDAndSessionUUID(ctx context.Context, arg FindAccountSessionByAccountIDAndSessionUUIDParams) (FindAccountSessionByAccountIDAndSessionUUIDRow, error) { + row := q.db.QueryRow(ctx, findAccountSessionByAccountIDAndSessionUUID, arg.AccountID, arg.SessionUuid) + var i FindAccountSessionByAccountIDAndSessionUUIDRow + err := row.Scan( + &i.AccountID, + &i.AccountVersion, + &i.SessionID, + &i.AccountCredentialsID, + &i.SessionUuid, + &i.CreatedAt, + &i.ID, + &i.AccountID_2, + &i.GrantID, + &i.SessionID_2, + &i.SessionType, + &i.SessionClientID, + &i.IpAddress, + &i.UserAgent, + &i.IssuedAt, + &i.ExpiresAt, + &i.CreatedAt_2, + &i.UpdatedAt, + ) + return i, err +} diff --git a/idp/internal/providers/database/app_related_apps.sql.go b/idp/internal/providers/database/app_related_apps.sql.go index d449d0f..a663a37 100644 --- a/idp/internal/providers/database/app_related_apps.sql.go +++ b/idp/internal/providers/database/app_related_apps.sql.go @@ -54,7 +54,7 @@ func (q *Queries) DeleteAppRelatedAppsByAppIDAndRelatedAppIDs(ctx context.Contex } const findRelatedAppsByAppID = `-- name: FindRelatedAppsByAppID :many -SELECT a.id, a.account_id, a.account_public_id, a.client_id, a.version, a.creation_method, a.redirect_uris, a.token_endpoint_auth_method, a.grant_types, a.response_types, a.client_name, a.client_uri, a.logo_uri, a.scopes, a.custom_scopes, a.contacts, a.tos_uri, a.policy_uri, a.jwks_uri, a.jwks, a.software_id, a.software_version, a.domain, a.transport, a.allow_user_registration, a.auth_providers, a.username_column, a.default_scopes, a.default_custom_scopes, a.app_type, a.sector_identifier_uri, a.subject_type, a.id_token_signed_response_alg, a.id_token_encrypted_response_alg, a.id_token_encrypted_response_enc, a.userinfo_signed_response_alg, a.userinfo_encrypted_response_alg, a.userinfo_encrypted_response_enc, a.request_object_signing_alg, a.request_object_encryption_alg, a.request_object_encryption_enc, a.token_endpoint_auth_signing_alg, a.default_max_age, a.require_auth_time, a.default_acr_values, a.initiate_login_uri, a.request_uris, a.access_token_signing_alg, a.id_token_ttl, a.token_ttl, a.refresh_token_ttl, a.created_at, a.updated_at FROM "apps" a +SELECT a.id, a.account_id, a.account_public_id, a.client_id, a.version, a.creation_method, a.redirect_uris, a.token_endpoint_auth_method, a.grant_types, a.response_types, a.client_name, a.client_uri, a.logo_uri, a.scopes, a.custom_scopes, a.contacts, a.tos_uri, a.policy_uri, a.jwks_uri, a.jwks, a.software_id, a.software_version, a.domain, a.transport, a.allow_user_registration, a.auth_providers, a.username_column, a.default_scopes, a.default_custom_scopes, a.app_type, a.sector_identifier_uri, a.subject_type, a.id_token_signed_response_alg, a.id_token_encrypted_response_alg, a.id_token_encrypted_response_enc, a.userinfo_signed_response_alg, a.userinfo_encrypted_response_alg, a.userinfo_encrypted_response_enc, a.request_object_signing_alg, a.request_object_encryption_alg, a.request_object_encryption_enc, a.token_endpoint_auth_signing_alg, a.default_max_age, a.require_auth_time, a.default_acr_values, a.initiate_login_uri, a.request_uris, a.access_token_signing_alg, a.session_type, a.access_token_ttl, a.id_token_ttl, a.refresh_token_idle_ttl, a.refresh_token_ttl, a.grant_ttl, a.created_at, a.updated_at FROM "apps" a INNER JOIN "app_related_apps" ara ON a.id = ara.related_app_id WHERE ara.app_id = $1 ORDER BY a.client_name ASC @@ -118,9 +118,12 @@ func (q *Queries) FindRelatedAppsByAppID(ctx context.Context, appID int32) ([]Ap &i.InitiateLoginUri, &i.RequestUris, &i.AccessTokenSigningAlg, + &i.SessionType, + &i.AccessTokenTtl, &i.IDTokenTtl, - &i.TokenTtl, + &i.RefreshTokenIdleTtl, &i.RefreshTokenTtl, + &i.GrantTtl, &i.CreatedAt, &i.UpdatedAt, ); err != nil { diff --git a/idp/internal/providers/database/apps.sql.go b/idp/internal/providers/database/apps.sql.go index c6c94d2..09bd928 100644 --- a/idp/internal/providers/database/apps.sql.go +++ b/idp/internal/providers/database/apps.sql.go @@ -193,7 +193,7 @@ INSERT INTO "apps" ( $24, $25, $26 -) 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 +) 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, session_type, access_token_ttl, id_token_ttl, refresh_token_idle_ttl, refresh_token_ttl, grant_ttl, created_at, updated_at ` type CreateAppParams struct { @@ -309,9 +309,12 @@ func (q *Queries) CreateApp(ctx context.Context, arg CreateAppParams) (App, erro &i.InitiateLoginUri, &i.RequestUris, &i.AccessTokenSigningAlg, + &i.SessionType, + &i.AccessTokenTtl, &i.IDTokenTtl, - &i.TokenTtl, + &i.RefreshTokenIdleTtl, &i.RefreshTokenTtl, + &i.GrantTtl, &i.CreatedAt, &i.UpdatedAt, ) @@ -338,7 +341,7 @@ func (q *Queries) DeleteApp(ctx context.Context, id int32) error { } const filterAppsByNameAndByAccountPublicIDOrderedByID = `-- name: FilterAppsByNameAndByAccountPublicIDOrderedByID :many -SELECT 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 FROM "apps" +SELECT 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, session_type, access_token_ttl, id_token_ttl, refresh_token_idle_ttl, refresh_token_ttl, grant_ttl, created_at, updated_at FROM "apps" WHERE "account_public_id" = $1 AND "client_name" ILIKE $2 ORDER BY "id" DESC OFFSET $3 LIMIT $4 @@ -414,9 +417,12 @@ func (q *Queries) FilterAppsByNameAndByAccountPublicIDOrderedByID(ctx context.Co &i.InitiateLoginUri, &i.RequestUris, &i.AccessTokenSigningAlg, + &i.SessionType, + &i.AccessTokenTtl, &i.IDTokenTtl, - &i.TokenTtl, + &i.RefreshTokenIdleTtl, &i.RefreshTokenTtl, + &i.GrantTtl, &i.CreatedAt, &i.UpdatedAt, ); err != nil { @@ -431,7 +437,7 @@ func (q *Queries) FilterAppsByNameAndByAccountPublicIDOrderedByID(ctx context.Co } const filterAppsByNameAndByAccountPublicIDOrderedByName = `-- name: FilterAppsByNameAndByAccountPublicIDOrderedByName :many -SELECT 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 FROM "apps" +SELECT 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, session_type, access_token_ttl, id_token_ttl, refresh_token_idle_ttl, refresh_token_ttl, grant_ttl, created_at, updated_at FROM "apps" WHERE "account_public_id" = $1 AND "client_name" ILIKE $2 ORDER BY "client_name" ASC OFFSET $3 LIMIT $4 @@ -507,9 +513,12 @@ func (q *Queries) FilterAppsByNameAndByAccountPublicIDOrderedByName(ctx context. &i.InitiateLoginUri, &i.RequestUris, &i.AccessTokenSigningAlg, + &i.SessionType, + &i.AccessTokenTtl, &i.IDTokenTtl, - &i.TokenTtl, + &i.RefreshTokenIdleTtl, &i.RefreshTokenTtl, + &i.GrantTtl, &i.CreatedAt, &i.UpdatedAt, ); err != nil { @@ -524,7 +533,7 @@ func (q *Queries) FilterAppsByNameAndByAccountPublicIDOrderedByName(ctx context. } const filterAppsByNameAndTypeAndByAccountPublicIDOrderedByID = `-- name: FilterAppsByNameAndTypeAndByAccountPublicIDOrderedByID :many -SELECT 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 FROM "apps" +SELECT 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, session_type, access_token_ttl, id_token_ttl, refresh_token_idle_ttl, refresh_token_ttl, grant_ttl, created_at, updated_at FROM "apps" WHERE "account_public_id" = $1 AND "client_name" ILIKE $2 AND "app_type" = $3 @@ -604,9 +613,12 @@ func (q *Queries) FilterAppsByNameAndTypeAndByAccountPublicIDOrderedByID(ctx con &i.InitiateLoginUri, &i.RequestUris, &i.AccessTokenSigningAlg, + &i.SessionType, + &i.AccessTokenTtl, &i.IDTokenTtl, - &i.TokenTtl, + &i.RefreshTokenIdleTtl, &i.RefreshTokenTtl, + &i.GrantTtl, &i.CreatedAt, &i.UpdatedAt, ); err != nil { @@ -621,7 +633,7 @@ func (q *Queries) FilterAppsByNameAndTypeAndByAccountPublicIDOrderedByID(ctx con } const filterAppsByNameAndTypeAndByAccountPublicIDOrderedByName = `-- name: FilterAppsByNameAndTypeAndByAccountPublicIDOrderedByName :many -SELECT 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 FROM "apps" +SELECT 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, session_type, access_token_ttl, id_token_ttl, refresh_token_idle_ttl, refresh_token_ttl, grant_ttl, created_at, updated_at FROM "apps" WHERE "account_public_id" = $1 AND "client_name" ILIKE $2 AND "app_type" = $3 @@ -701,9 +713,12 @@ func (q *Queries) FilterAppsByNameAndTypeAndByAccountPublicIDOrderedByName(ctx c &i.InitiateLoginUri, &i.RequestUris, &i.AccessTokenSigningAlg, + &i.SessionType, + &i.AccessTokenTtl, &i.IDTokenTtl, - &i.TokenTtl, + &i.RefreshTokenIdleTtl, &i.RefreshTokenTtl, + &i.GrantTtl, &i.CreatedAt, &i.UpdatedAt, ); err != nil { @@ -718,7 +733,7 @@ func (q *Queries) FilterAppsByNameAndTypeAndByAccountPublicIDOrderedByName(ctx c } const filterAppsByTypeAndByAccountPublicIDOrderedByID = `-- name: FilterAppsByTypeAndByAccountPublicIDOrderedByID :many -SELECT 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 FROM "apps" +SELECT 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, session_type, access_token_ttl, id_token_ttl, refresh_token_idle_ttl, refresh_token_ttl, grant_ttl, created_at, updated_at FROM "apps" WHERE "account_public_id" = $1 AND "app_type" = $2 ORDER BY "id" DESC OFFSET $3 LIMIT $4 @@ -794,9 +809,12 @@ func (q *Queries) FilterAppsByTypeAndByAccountPublicIDOrderedByID(ctx context.Co &i.InitiateLoginUri, &i.RequestUris, &i.AccessTokenSigningAlg, + &i.SessionType, + &i.AccessTokenTtl, &i.IDTokenTtl, - &i.TokenTtl, + &i.RefreshTokenIdleTtl, &i.RefreshTokenTtl, + &i.GrantTtl, &i.CreatedAt, &i.UpdatedAt, ); err != nil { @@ -811,7 +829,7 @@ func (q *Queries) FilterAppsByTypeAndByAccountPublicIDOrderedByID(ctx context.Co } const filterAppsByTypeAndByAccountPublicIDOrderedByName = `-- name: FilterAppsByTypeAndByAccountPublicIDOrderedByName :many -SELECT 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 FROM "apps" +SELECT 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, session_type, access_token_ttl, id_token_ttl, refresh_token_idle_ttl, refresh_token_ttl, grant_ttl, created_at, updated_at FROM "apps" WHERE "account_public_id" = $1 AND "app_type" = $2 ORDER BY "client_name" ASC OFFSET $3 LIMIT $4 @@ -887,9 +905,12 @@ func (q *Queries) FilterAppsByTypeAndByAccountPublicIDOrderedByName(ctx context. &i.InitiateLoginUri, &i.RequestUris, &i.AccessTokenSigningAlg, + &i.SessionType, + &i.AccessTokenTtl, &i.IDTokenTtl, - &i.TokenTtl, + &i.RefreshTokenIdleTtl, &i.RefreshTokenTtl, + &i.GrantTtl, &i.CreatedAt, &i.UpdatedAt, ); err != nil { @@ -904,7 +925,7 @@ func (q *Queries) FilterAppsByTypeAndByAccountPublicIDOrderedByName(ctx context. } const findAppByClientID = `-- name: FindAppByClientID :one -SELECT 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 FROM "apps" +SELECT 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, session_type, access_token_ttl, id_token_ttl, refresh_token_idle_ttl, refresh_token_ttl, grant_ttl, created_at, updated_at FROM "apps" WHERE "client_id" = $1 LIMIT 1 ` @@ -960,9 +981,12 @@ func (q *Queries) FindAppByClientID(ctx context.Context, clientID string) (App, &i.InitiateLoginUri, &i.RequestUris, &i.AccessTokenSigningAlg, + &i.SessionType, + &i.AccessTokenTtl, &i.IDTokenTtl, - &i.TokenTtl, + &i.RefreshTokenIdleTtl, &i.RefreshTokenTtl, + &i.GrantTtl, &i.CreatedAt, &i.UpdatedAt, ) @@ -970,7 +994,7 @@ func (q *Queries) FindAppByClientID(ctx context.Context, clientID string) (App, } const findAppByClientIDAndAccountPublicID = `-- name: FindAppByClientIDAndAccountPublicID :one -SELECT 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 FROM "apps" +SELECT 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, session_type, access_token_ttl, id_token_ttl, refresh_token_idle_ttl, refresh_token_ttl, grant_ttl, created_at, updated_at FROM "apps" WHERE "client_id" = $1 AND "account_public_id" = $2 LIMIT 1 ` @@ -1032,9 +1056,12 @@ func (q *Queries) FindAppByClientIDAndAccountPublicID(ctx context.Context, arg F &i.InitiateLoginUri, &i.RequestUris, &i.AccessTokenSigningAlg, + &i.SessionType, + &i.AccessTokenTtl, &i.IDTokenTtl, - &i.TokenTtl, + &i.RefreshTokenIdleTtl, &i.RefreshTokenTtl, + &i.GrantTtl, &i.CreatedAt, &i.UpdatedAt, ) @@ -1042,7 +1069,7 @@ func (q *Queries) FindAppByClientIDAndAccountPublicID(ctx context.Context, arg F } const findAppByClientIDAndVersion = `-- name: FindAppByClientIDAndVersion :one -SELECT 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 FROM "apps" +SELECT 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, session_type, access_token_ttl, id_token_ttl, refresh_token_idle_ttl, refresh_token_ttl, grant_ttl, created_at, updated_at FROM "apps" WHERE "client_id" = $1 AND "version" = $2 LIMIT 1 ` @@ -1103,9 +1130,12 @@ func (q *Queries) FindAppByClientIDAndVersion(ctx context.Context, arg FindAppBy &i.InitiateLoginUri, &i.RequestUris, &i.AccessTokenSigningAlg, + &i.SessionType, + &i.AccessTokenTtl, &i.IDTokenTtl, - &i.TokenTtl, + &i.RefreshTokenIdleTtl, &i.RefreshTokenTtl, + &i.GrantTtl, &i.CreatedAt, &i.UpdatedAt, ) @@ -1113,7 +1143,7 @@ func (q *Queries) FindAppByClientIDAndVersion(ctx context.Context, arg FindAppBy } const findAppByID = `-- name: FindAppByID :one -SELECT 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 FROM "apps" +SELECT 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, session_type, access_token_ttl, id_token_ttl, refresh_token_idle_ttl, refresh_token_ttl, grant_ttl, created_at, updated_at FROM "apps" WHERE "id" = $1 LIMIT 1 ` @@ -1169,9 +1199,12 @@ func (q *Queries) FindAppByID(ctx context.Context, id int32) (App, error) { &i.InitiateLoginUri, &i.RequestUris, &i.AccessTokenSigningAlg, + &i.SessionType, + &i.AccessTokenTtl, &i.IDTokenTtl, - &i.TokenTtl, + &i.RefreshTokenIdleTtl, &i.RefreshTokenTtl, + &i.GrantTtl, &i.CreatedAt, &i.UpdatedAt, ) @@ -1179,7 +1212,7 @@ func (q *Queries) FindAppByID(ctx context.Context, id int32) (App, error) { } const findAppsByClientIDsAndAccountID = `-- name: FindAppsByClientIDsAndAccountID :many -SELECT 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 FROM "apps" +SELECT 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, session_type, access_token_ttl, id_token_ttl, refresh_token_idle_ttl, refresh_token_ttl, grant_ttl, created_at, updated_at FROM "apps" WHERE "client_id" IN ($3) AND "account_id" = $1 ORDER BY "client_name" ASC LIMIT $2 ` @@ -1248,9 +1281,12 @@ func (q *Queries) FindAppsByClientIDsAndAccountID(ctx context.Context, arg FindA &i.InitiateLoginUri, &i.RequestUris, &i.AccessTokenSigningAlg, + &i.SessionType, + &i.AccessTokenTtl, &i.IDTokenTtl, - &i.TokenTtl, + &i.RefreshTokenIdleTtl, &i.RefreshTokenTtl, + &i.GrantTtl, &i.CreatedAt, &i.UpdatedAt, ); err != nil { @@ -1265,7 +1301,7 @@ func (q *Queries) FindAppsByClientIDsAndAccountID(ctx context.Context, arg FindA } const findPaginatedAppsByAccountPublicIDOrderedByID = `-- name: FindPaginatedAppsByAccountPublicIDOrderedByID :many -SELECT 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 FROM "apps" +SELECT 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, session_type, access_token_ttl, id_token_ttl, refresh_token_idle_ttl, refresh_token_ttl, grant_ttl, created_at, updated_at FROM "apps" WHERE "account_public_id" = $1 ORDER BY "id" DESC OFFSET $2 LIMIT $3 @@ -1335,9 +1371,12 @@ func (q *Queries) FindPaginatedAppsByAccountPublicIDOrderedByID(ctx context.Cont &i.InitiateLoginUri, &i.RequestUris, &i.AccessTokenSigningAlg, + &i.SessionType, + &i.AccessTokenTtl, &i.IDTokenTtl, - &i.TokenTtl, + &i.RefreshTokenIdleTtl, &i.RefreshTokenTtl, + &i.GrantTtl, &i.CreatedAt, &i.UpdatedAt, ); err != nil { @@ -1352,7 +1391,7 @@ func (q *Queries) FindPaginatedAppsByAccountPublicIDOrderedByID(ctx context.Cont } const findPaginatedAppsByAccountPublicIDOrderedByName = `-- name: FindPaginatedAppsByAccountPublicIDOrderedByName :many -SELECT 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 FROM "apps" +SELECT 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, session_type, access_token_ttl, id_token_ttl, refresh_token_idle_ttl, refresh_token_ttl, grant_ttl, created_at, updated_at FROM "apps" WHERE "account_public_id" = $1 ORDER BY "client_name" ASC OFFSET $2 LIMIT $3 @@ -1422,9 +1461,12 @@ func (q *Queries) FindPaginatedAppsByAccountPublicIDOrderedByName(ctx context.Co &i.InitiateLoginUri, &i.RequestUris, &i.AccessTokenSigningAlg, + &i.SessionType, + &i.AccessTokenTtl, &i.IDTokenTtl, - &i.TokenTtl, + &i.RefreshTokenIdleTtl, &i.RefreshTokenTtl, + &i.GrantTtl, &i.CreatedAt, &i.UpdatedAt, ); err != nil { @@ -1457,7 +1499,7 @@ SET "client_name" = $2, "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 +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, session_type, access_token_ttl, id_token_ttl, refresh_token_idle_ttl, refresh_token_ttl, grant_ttl, created_at, updated_at ` type UpdateAppParams struct { @@ -1546,9 +1588,12 @@ func (q *Queries) UpdateApp(ctx context.Context, arg UpdateAppParams) (App, erro &i.InitiateLoginUri, &i.RequestUris, &i.AccessTokenSigningAlg, + &i.SessionType, + &i.AccessTokenTtl, &i.IDTokenTtl, - &i.TokenTtl, + &i.RefreshTokenIdleTtl, &i.RefreshTokenTtl, + &i.GrantTtl, &i.CreatedAt, &i.UpdatedAt, ) @@ -1564,7 +1609,7 @@ SET "scopes" = $2, "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 +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, session_type, access_token_ttl, id_token_ttl, refresh_token_idle_ttl, refresh_token_ttl, grant_ttl, created_at, updated_at ` type UpdateAppScopesParams struct { @@ -1633,9 +1678,12 @@ func (q *Queries) UpdateAppScopes(ctx context.Context, arg UpdateAppScopesParams &i.InitiateLoginUri, &i.RequestUris, &i.AccessTokenSigningAlg, + &i.SessionType, + &i.AccessTokenTtl, &i.IDTokenTtl, - &i.TokenTtl, + &i.RefreshTokenIdleTtl, &i.RefreshTokenTtl, + &i.GrantTtl, &i.CreatedAt, &i.UpdatedAt, ) @@ -1647,7 +1695,7 @@ UPDATE "apps" SET "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 +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, session_type, access_token_ttl, id_token_ttl, refresh_token_idle_ttl, refresh_token_ttl, grant_ttl, created_at, updated_at ` func (q *Queries) UpdateAppVersion(ctx context.Context, id int32) (App, error) { @@ -1702,9 +1750,12 @@ func (q *Queries) UpdateAppVersion(ctx context.Context, id int32) (App, error) { &i.InitiateLoginUri, &i.RequestUris, &i.AccessTokenSigningAlg, + &i.SessionType, + &i.AccessTokenTtl, &i.IDTokenTtl, - &i.TokenTtl, + &i.RefreshTokenIdleTtl, &i.RefreshTokenTtl, + &i.GrantTtl, &i.CreatedAt, &i.UpdatedAt, ) diff --git a/idp/internal/providers/database/grants.sql.go b/idp/internal/providers/database/grants.sql.go new file mode 100644 index 0000000..99adc72 --- /dev/null +++ b/idp/internal/providers/database/grants.sql.go @@ -0,0 +1,55 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 +// source: grants.sql + +package database + +import ( + "context" + + "github.com/google/uuid" +) + +const createGrant = `-- name: CreateGrant :one + +INSERT INTO "grants" ( + "account_id", + "grant_id", + "granted_client_id", + "granted_scopes", + "granted_custom_scopes" +) VALUES ( + $1, + $2, + $3, + $4, + $5 +) RETURNING id +` + +type CreateGrantParams struct { + AccountID int32 + GrantID uuid.UUID + GrantedClientID string + GrantedScopes []Scopes + GrantedCustomScopes []string +} + +// 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/. +func (q *Queries) CreateGrant(ctx context.Context, arg CreateGrantParams) (int32, error) { + row := q.db.QueryRow(ctx, createGrant, + arg.AccountID, + arg.GrantID, + arg.GrantedClientID, + arg.GrantedScopes, + arg.GrantedCustomScopes, + ) + var id int32 + err := row.Scan(&id) + return id, err +} diff --git a/idp/internal/providers/database/migrations/20241213231542_create_initial_schema.down.sql b/idp/internal/providers/database/migrations/20241213231542_create_initial_schema.down.sql index 30a8224..d4147e1 100644 --- a/idp/internal/providers/database/migrations/20241213231542_create_initial_schema.down.sql +++ b/idp/internal/providers/database/migrations/20241213231542_create_initial_schema.down.sql @@ -1,10 +1,11 @@ -- 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/. -DROP TABLE IF EXISTS "revoked_tokens"; +DROP TABLE IF EXISTS "allowed_tokens"; +DROP TABLE IF EXISTS "grants"; DROP TABLE IF EXISTS "app_profiles"; DROP TABLE IF EXISTS "dynamic_registration_configs"; DROP TABLE IF EXISTS "app_designs"; @@ -62,3 +63,4 @@ DROP TYPE IF EXISTS "initial_access_token_generation_method"; DROP TYPE IF EXISTS "software_statement_verification_method"; DROP TYPE IF EXISTS "app_profile_type"; DROP TYPE IF EXISTS "token_owner"; +DROP TYPE IF EXISTS "session_type"; 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 2690b40..7f0e932 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: 2026-09-08T22:46:50.769Z +-- Generated at: 2026-09-14T01:24:14.048Z CREATE TYPE "kek_usage" AS ENUM ( 'global', @@ -194,6 +194,11 @@ CREATE TYPE "grant_type" AS ENUM ( 'urn:ietf:params:oauth:grant-type:jwt-bearer' ); +CREATE TYPE "session_type" AS ENUM ( + 'sliding', + 'fixed' +); + CREATE TYPE "initial_access_token_generation_method" AS ENUM ( 'manual', 'authorization_code' @@ -588,9 +593,12 @@ CREATE TABLE "apps" ( "initiate_login_uri" varchar(512), "request_uris" varchar(2048)[], "access_token_signing_alg" token_crypto_suite NOT NULL DEFAULT 'ES256', - "id_token_ttl" integer NOT NULL DEFAULT 300, - "token_ttl" integer NOT NULL DEFAULT 300, - "refresh_token_ttl" integer NOT NULL DEFAULT 604800, + "session_type" session_type NOT NULL, + "access_token_ttl" integer NOT NULL, + "id_token_ttl" integer, + "refresh_token_idle_ttl" integer, + "refresh_token_ttl" integer, + "grant_ttl" integer, "created_at" timestamptz NOT NULL DEFAULT (now()), "updated_at" timestamptz NOT NULL DEFAULT (now()) ); @@ -724,13 +732,90 @@ CREATE TABLE "app_profiles" ( PRIMARY KEY ("app_id", "user_id") ); -CREATE TABLE "revoked_tokens" ( +CREATE TABLE "grants" ( + "id" serial PRIMARY KEY, + "account_id" integer NOT NULL, + "grant_id" uuid NOT NULL, + "granted_client_id" varchar(22) NOT NULL, + "granted_scopes" scopes[] NOT NULL, + "granted_custom_scopes" varchar(512)[] NOT NULL, + "issued_at" timestamptz NOT NULL DEFAULT (now()), + "last_active_at" timestamptz NOT NULL DEFAULT (now()), + "created_at" timestamptz NOT NULL DEFAULT (now()), + "updated_at" timestamptz NOT NULL DEFAULT (now()) +); + +CREATE TABLE "account_grants" ( + "account_id" integer NOT NULL, + "account_version" integer NOT NULL, + "grant_id" integer NOT NULL, + "account_credentials_id" integer, + "granted_client_id" varchar(22) NOT NULL, + "is_revoked" boolean NOT NULL DEFAULT false, + "revoked_at" timestamptz, + "expires_at" timestamptz, + "created_at" timestamptz NOT NULL DEFAULT (now()), + PRIMARY KEY ("account_id", "grant_id") +); + +CREATE TABLE "user_grants" ( + "user_id" integer NOT NULL, + "user_version" integer NOT NULL, + "grant_id" integer NOT NULL, + "app_id" integer NOT NULL, + "account_id" integer NOT NULL, + "granted_client_id" varchar(22) NOT NULL, + "is_revoked" boolean NOT NULL DEFAULT false, + "revoked_at" timestamptz, + "expires_at" timestamptz, + "created_at" timestamptz NOT NULL DEFAULT (now()), + PRIMARY KEY ("user_id", "grant_id") +); + +CREATE TABLE "sessions" ( "id" serial PRIMARY KEY, - "token_id" uuid NOT NULL, "account_id" integer NOT NULL, - "owner" token_owner NOT NULL, - "owner_public_id" uuid NOT NULL, - "issued_at" timestamptz NOT NULL, + "grant_id" integer NOT NULL, + "session_id" uuid NOT NULL, + "session_type" session_type NOT NULL, + "session_client_id" varchar(22) NOT NULL, + "ip_address" varchar(45), + "user_agent" text, + "issued_at" timestamptz NOT NULL DEFAULT (now()), + "expires_at" timestamptz NOT NULL, + "created_at" timestamptz NOT NULL DEFAULT (now()), + "updated_at" timestamptz NOT NULL DEFAULT (now()) +); + +CREATE TABLE "account_sessions" ( + "account_id" integer NOT NULL, + "account_version" integer NOT NULL, + "session_id" integer NOT NULL, + "account_credentials_id" integer, + "session_uuid" uuid NOT NULL, + "created_at" timestamptz NOT NULL DEFAULT (now()), + PRIMARY KEY ("account_id", "session_id") +); + +CREATE TABLE "user_sessions" ( + "user_id" integer NOT NULL, + "user_version" integer NOT NULL, + "session_id" integer NOT NULL, + "app_id" integer NOT NULL, + "account_id" integer NOT NULL, + "session_uuid" uuid NOT NULL, + "created_at" timestamptz NOT NULL DEFAULT (now()), + PRIMARY KEY ("user_id", "session_id") +); + +CREATE TABLE "session_tokens" ( + "id" serial PRIMARY KEY, + "account_id" integer NOT NULL, + "session_id" integer NOT NULL, + "grant_id" integer NOT NULL, + "session_uuid" uuid NOT NULL, + "token_id" uuid NOT NULL, + "issued_at" timestamptz NOT NULL DEFAULT (now()), "expires_at" timestamptz NOT NULL, "created_at" timestamptz NOT NULL DEFAULT (now()) ); @@ -1041,156 +1126,246 @@ CREATE INDEX "user_profiles_account_id_idx" ON "app_profiles" ("account_id"); CREATE UNIQUE INDEX "user_profiles_user_id_app_id_uidx" ON "app_profiles" ("user_id", "app_id"); -CREATE UNIQUE INDEX "revoked_tokens_token_id_uidx" ON "revoked_tokens" ("token_id"); +CREATE INDEX "grants_account_id_idx" ON "grants" ("account_id"); + +CREATE UNIQUE INDEX "grant_id_uidx" ON "grants" ("grant_id"); + +CREATE INDEX "grants_granted_client_id_idx" ON "grants" ("granted_client_id"); + +CREATE INDEX "account_grants_account_id_idx" ON "account_grants" ("account_id"); + +CREATE UNIQUE INDEX "account_grants_grant_id_uidx" ON "account_grants" ("grant_id"); + +CREATE INDEX "account_grants_account_credentials_id_idx" ON "account_grants" ("account_credentials_id"); + +CREATE UNIQUE INDEX "account_grants_account_id_granted_client_id_uidx" ON "account_grants" ("account_id", "granted_client_id"); + +CREATE INDEX "user_grants_user_id_idx" ON "user_grants" ("user_id"); + +CREATE UNIQUE INDEX "user_grants_grant_id_uidx" ON "user_grants" ("grant_id"); + +CREATE INDEX "user_grants_app_id_idx" ON "user_grants" ("app_id"); + +CREATE INDEX "user_grants_account_id_idx" ON "user_grants" ("account_id"); + +CREATE UNIQUE INDEX "user_grants_user_id_granted_client_id_uidx" ON "user_grants" ("user_id", "granted_client_id"); + +CREATE INDEX "sessions_account_id_idx" ON "sessions" ("account_id"); + +CREATE INDEX "sessions_grant_id_idx" ON "sessions" ("grant_id"); + +CREATE UNIQUE INDEX "sessions_session_id_uidx" ON "sessions" ("session_id"); + +CREATE INDEX "sessions_expires_at_idx" ON "sessions" ("expires_at"); + +CREATE INDEX "account_sessions_account_id_idx" ON "account_sessions" ("account_id"); + +CREATE UNIQUE INDEX "account_sessions_session_id_uidx" ON "account_sessions" ("session_id"); + +CREATE UNIQUE INDEX "account_sessions_account_id_session_uuid_uidx" ON "account_sessions" ("account_id", "session_uuid"); + +CREATE INDEX "account_sessions_account_credentials_id_idx" ON "account_sessions" ("account_credentials_id"); + +CREATE INDEX "user_sessions_user_id_idx" ON "user_sessions" ("user_id"); + +CREATE UNIQUE INDEX "user_sessions_session_id_uidx" ON "user_sessions" ("session_id"); + +CREATE INDEX "user_sessions_app_id_idx" ON "user_sessions" ("app_id"); + +CREATE INDEX "user_sessions_account_id_idx" ON "user_sessions" ("account_id"); + +CREATE UNIQUE INDEX "allowed_tokens_token_id_uidx" ON "session_tokens" ("token_id"); + +CREATE INDEX "allowed_tokens_account_id_idx" ON "session_tokens" ("account_id"); + +CREATE INDEX "allowed_tokens_session_id_idx" ON "session_tokens" ("session_id"); + +CREATE INDEX "allowed_tokens_grant_id_idx" ON "session_tokens" ("grant_id"); + +CREATE INDEX "allowed_tokens_expires_at_idx" ON "session_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 "token_signing_keys" ADD FOREIGN KEY ("dek_kid") REFERENCES "data_encryption_keys" ("kid") ON DELETE CASCADE ON UPDATE CASCADE; + +ALTER TABLE "account_2fa_configs" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("id") ON DELETE CASCADE; + +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 ("account_id") REFERENCES "accounts" ("id") ON DELETE CASCADE; + +ALTER TABLE "credentials_secrets" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("id") ON DELETE CASCADE; + +ALTER TABLE "credentials_secrets" ADD FOREIGN KEY ("dek_kid") REFERENCES "data_encryption_keys" ("kid") ON DELETE CASCADE ON UPDATE CASCADE; + +ALTER TABLE "credentials_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; + +ALTER TABLE "account_key_encryption_keys" ADD FOREIGN KEY ("key_encryption_key_id") REFERENCES "key_encryption_keys" ("id") ON DELETE CASCADE; + +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 ("data_encryption_key_id") REFERENCES "data_encryption_keys" ("id") ON DELETE CASCADE; + +ALTER TABLE "account_hmac_secrets" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("id") ON DELETE CASCADE; + +ALTER TABLE "account_hmac_secrets" ADD FOREIGN KEY ("dek_kid") REFERENCES "data_encryption_keys" ("kid") ON DELETE CASCADE ON UPDATE CASCADE; + +ALTER TABLE "account_totps" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("id") ON DELETE CASCADE; + +ALTER TABLE "account_totps" ADD FOREIGN KEY ("totp_id") REFERENCES "totps" ("id") ON DELETE CASCADE; + +ALTER TABLE "account_credentials" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("id") ON DELETE CASCADE; -CREATE INDEX "revoked_tokens_account_id_idx" ON "revoked_tokens" ("account_id"); +ALTER TABLE "account_credentials_secrets" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("id") ON DELETE CASCADE; -CREATE INDEX "revoked_tokens_expires_at_idx" ON "revoked_tokens" ("expires_at"); +ALTER TABLE "account_credentials_secrets" ADD FOREIGN KEY ("credentials_secret_id") REFERENCES "credentials_secrets" ("id") ON DELETE 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 "account_credentials_keys" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("id") ON DELETE 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_credentials_keys" ADD FOREIGN KEY ("account_credentials_id") REFERENCES "account_credentials" ("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 "account_credentials_keys" ADD FOREIGN KEY ("credentials_key_id") REFERENCES "credentials_keys" ("id") ON DELETE 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 "account_auth_providers" ADD FOREIGN KEY ("email") REFERENCES "accounts" ("email") ON DELETE CASCADE ON UPDATE CASCADE; -ALTER TABLE "totps" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("id") ON DELETE CASCADE DEFERRABLE INITIALLY IMMEDIATE; +ALTER TABLE "oidc_configs" 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 "account_token_signing_keys" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("id") ON DELETE 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 "account_token_signing_keys" ADD FOREIGN KEY ("token_signing_key_id") REFERENCES "token_signing_keys" ("id") ON DELETE CASCADE; -ALTER TABLE "credentials_keys" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("id") ON DELETE CASCADE DEFERRABLE INITIALLY IMMEDIATE; +ALTER TABLE "users" 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 "user_2fa_configs" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("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 "user_2fa_configs" ADD FOREIGN KEY ("user_id") REFERENCES "users" ("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 "user_data_encryption_keys" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("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 "user_data_encryption_keys" ADD FOREIGN KEY ("user_id") REFERENCES "users" ("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 "user_data_encryption_keys" ADD FOREIGN KEY ("data_encryption_key_id") REFERENCES "data_encryption_keys" ("id") ON DELETE 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 "user_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 "user_totps" ADD FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE; -ALTER TABLE "account_totps" ADD FOREIGN KEY ("totp_id") REFERENCES "totps" ("id") ON DELETE CASCADE DEFERRABLE INITIALLY IMMEDIATE; +ALTER TABLE "user_totps" ADD FOREIGN KEY ("totp_id") REFERENCES "totps" ("id") ON DELETE CASCADE; -ALTER TABLE "account_credentials" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("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 "account_credentials_secrets" 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 "account_credentials_secrets" ADD FOREIGN KEY ("credentials_secret_id") REFERENCES "credentials_secrets" ("id") ON DELETE CASCADE DEFERRABLE INITIALLY IMMEDIATE; +ALTER TABLE "user_credentials" ADD FOREIGN KEY ("user_id") REFERENCES "users" ("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 "user_credentials" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("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 "user_credentials" ADD FOREIGN KEY ("app_id") REFERENCES "apps" ("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 "user_credentials_secrets" ADD FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE; -ALTER TABLE "account_auth_providers" ADD FOREIGN KEY ("email") REFERENCES "accounts" ("email") ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE INITIALLY IMMEDIATE; +ALTER TABLE "user_credentials_secrets" ADD FOREIGN KEY ("user_credential_id") REFERENCES "user_credentials" ("id") ON DELETE CASCADE; -ALTER TABLE "oidc_configs" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("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 "account_token_signing_keys" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("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 "account_token_signing_keys" ADD FOREIGN KEY ("token_signing_key_id") REFERENCES "token_signing_keys" ("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 "users" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("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_2fa_configs" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("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_2fa_configs" ADD FOREIGN KEY ("user_id") REFERENCES "users" ("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_data_encryption_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 "user_data_encryption_keys" ADD FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE DEFERRABLE INITIALLY IMMEDIATE; +ALTER TABLE "app_secrets" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("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 "app_secrets" ADD FOREIGN KEY ("app_id") REFERENCES "apps" ("id") ON DELETE CASCADE; -ALTER TABLE "user_totps" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("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 "user_totps" ADD FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE DEFERRABLE INITIALLY IMMEDIATE; +ALTER TABLE "app_keys" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("id") ON DELETE CASCADE; -ALTER TABLE "user_totps" ADD FOREIGN KEY ("totp_id") REFERENCES "totps" ("id") ON DELETE CASCADE DEFERRABLE INITIALLY IMMEDIATE; +ALTER TABLE "app_keys" ADD FOREIGN KEY ("app_id") REFERENCES "apps" ("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 "app_keys" ADD FOREIGN KEY ("credentials_key_id") REFERENCES "credentials_keys" ("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 "app_related_apps" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("id") ON DELETE CASCADE; -ALTER TABLE "user_credentials" ADD FOREIGN KEY ("user_id") REFERENCES "users" ("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 "user_credentials" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("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 "user_credentials" ADD FOREIGN KEY ("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 "user_credentials_secrets" ADD FOREIGN KEY ("user_id") REFERENCES "users" ("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 "user_credentials_secrets" ADD FOREIGN KEY ("user_credential_id") REFERENCES "user_credentials" ("id") ON DELETE CASCADE DEFERRABLE INITIALLY IMMEDIATE; +ALTER TABLE "app_designs" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("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 "app_designs" ADD FOREIGN KEY ("app_id") REFERENCES "apps" ("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 "account_dynamic_registration_configs" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("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 "app_dynamic_registration_configs" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("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 "dynamic_registration_domains" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("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 "dynamic_registration_domain_codes" 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 "dynamic_registration_domain_codes" ADD FOREIGN KEY ("dynamic_registration_domain_id") REFERENCES "dynamic_registration_domains" ("id") ON DELETE CASCADE; -ALTER TABLE "apps" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("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 "app_secrets" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("id") ON DELETE CASCADE DEFERRABLE INITIALLY IMMEDIATE; +ALTER TABLE "dynamic_registration_software_statement_keys" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("id") ON DELETE CASCADE; -ALTER TABLE "app_secrets" ADD FOREIGN KEY ("app_id") REFERENCES "apps" ("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 "app_secrets" ADD FOREIGN KEY ("credentials_secret_id") REFERENCES "credentials_secrets" ("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_keys" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("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_keys" ADD FOREIGN KEY ("app_id") REFERENCES "apps" ("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_keys" ADD FOREIGN KEY ("credentials_key_id") REFERENCES "credentials_keys" ("id") ON DELETE CASCADE DEFERRABLE INITIALLY IMMEDIATE; +ALTER TABLE "grants" 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 "account_grants" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("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 "account_grants" ADD FOREIGN KEY ("grant_id") REFERENCES "grants" ("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 "account_grants" ADD FOREIGN KEY ("account_credentials_id") REFERENCES "account_credentials" ("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 "user_grants" ADD FOREIGN KEY ("user_id") REFERENCES "users" ("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 "user_grants" ADD FOREIGN KEY ("grant_id") REFERENCES "grants" ("id") ON DELETE CASCADE; -ALTER TABLE "app_designs" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("id") ON DELETE CASCADE DEFERRABLE INITIALLY IMMEDIATE; +ALTER TABLE "user_grants" 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 "user_grants" 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 "sessions" 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 "sessions" ADD FOREIGN KEY ("grant_id") REFERENCES "grants" ("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 "account_sessions" 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 "account_sessions" ADD FOREIGN KEY ("session_id") REFERENCES "sessions" ("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 "account_sessions" ADD FOREIGN KEY ("account_credentials_id") REFERENCES "account_credentials" ("id") ON DELETE 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 "user_sessions" ADD FOREIGN KEY ("user_id") REFERENCES "users" ("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 "user_sessions" ADD FOREIGN KEY ("session_id") REFERENCES "sessions" ("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 "user_sessions" 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 "user_sessions" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("id") ON DELETE CASCADE; -ALTER TABLE "app_profiles" ADD FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE DEFERRABLE INITIALLY IMMEDIATE; +ALTER TABLE "session_tokens" 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 "session_tokens" ADD FOREIGN KEY ("grant_id") REFERENCES "grants" ("id") ON DELETE CASCADE; -ALTER TABLE "revoked_tokens" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("id") ON DELETE CASCADE DEFERRABLE INITIALLY IMMEDIATE; +ALTER TABLE "session_tokens" ADD FOREIGN KEY ("session_id") REFERENCES "sessions" ("id") ON DELETE CASCADE; diff --git a/idp/internal/providers/database/models.go b/idp/internal/providers/database/models.go index a7f8194..ba0ec81 100644 --- a/idp/internal/providers/database/models.go +++ b/idp/internal/providers/database/models.go @@ -949,6 +949,48 @@ func (ns NullSecretStorageMode) Value() (driver.Value, error) { return string(ns.SecretStorageMode), nil } +type SessionType string + +const ( + SessionTypeSliding SessionType = "sliding" + SessionTypeFixed SessionType = "fixed" +) + +func (e *SessionType) Scan(src interface{}) error { + switch s := src.(type) { + case []byte: + *e = SessionType(s) + case string: + *e = SessionType(s) + default: + return fmt.Errorf("unsupported scan type for SessionType: %T", src) + } + return nil +} + +type NullSessionType struct { + SessionType SessionType + Valid bool // Valid is true if SessionType is not NULL +} + +// Scan implements the Scanner interface. +func (ns *NullSessionType) Scan(value interface{}) error { + if value == nil { + ns.SessionType, ns.Valid = "", false + return nil + } + ns.Valid = true + return ns.SessionType.Scan(value) +} + +// Value implements the driver Valuer interface. +func (ns NullSessionType) Value() (driver.Value, error) { + if !ns.Valid { + return nil, nil + } + return string(ns.SessionType), nil +} + type SoftwareStatementVerificationMethod string const ( @@ -1502,6 +1544,18 @@ type AccountDynamicRegistrationConfig struct { UpdatedAt time.Time } +type AccountGrant struct { + AccountID int32 + AccountVersion int32 + GrantID int32 + AccountCredentialsID pgtype.Int4 + GrantedClientID string + IsRevoked bool + RevokedAt pgtype.Timestamptz + ExpiresAt pgtype.Timestamptz + CreatedAt time.Time +} + type AccountHmacSecret struct { ID int32 AccountID int32 @@ -1519,6 +1573,15 @@ type AccountKeyEncryptionKey struct { CreatedAt time.Time } +type AccountSession struct { + AccountID int32 + AccountVersion int32 + SessionID int32 + AccountCredentialsID pgtype.Int4 + SessionUuid uuid.UUID + CreatedAt time.Time +} + type AccountTokenSigningKey struct { AccountID int32 TokenSigningKeyID int32 @@ -1580,9 +1643,12 @@ type App struct { InitiateLoginUri pgtype.Text RequestUris []string AccessTokenSigningAlg TokenCryptoSuite - IDTokenTtl int32 - TokenTtl int32 - RefreshTokenTtl int32 + SessionType SessionType + AccessTokenTtl int32 + IDTokenTtl pgtype.Int4 + RefreshTokenIdleTtl pgtype.Int4 + RefreshTokenTtl pgtype.Int4 + GrantTtl pgtype.Int4 CreatedAt time.Time UpdatedAt time.Time } @@ -1740,6 +1806,19 @@ type DynamicRegistrationSoftwareStatementKey struct { CreatedAt time.Time } +type Grant struct { + ID int32 + AccountID int32 + GrantID uuid.UUID + GrantedClientID string + GrantedScopes []Scopes + GrantedCustomScopes []string + IssuedAt time.Time + LastActiveAt time.Time + CreatedAt time.Time + UpdatedAt time.Time +} + type KeyEncryptionKey struct { ID int32 Kid uuid.UUID @@ -1762,15 +1841,31 @@ type OidcConfig struct { UpdatedAt time.Time } -type RevokedToken struct { - ID int32 - TokenID uuid.UUID - AccountID int32 - Owner TokenOwner - OwnerPublicID uuid.UUID - IssuedAt time.Time - ExpiresAt time.Time - CreatedAt time.Time +type Session struct { + ID int32 + AccountID int32 + GrantID int32 + SessionID uuid.UUID + SessionType SessionType + SessionClientID string + IpAddress pgtype.Text + UserAgent pgtype.Text + IssuedAt time.Time + ExpiresAt time.Time + CreatedAt time.Time + UpdatedAt time.Time +} + +type SessionToken struct { + ID int32 + AccountID int32 + SessionID int32 + GrantID int32 + SessionUuid uuid.UUID + TokenID uuid.UUID + IssuedAt time.Time + ExpiresAt time.Time + CreatedAt time.Time } type TokenSigningKey struct { @@ -1872,6 +1967,29 @@ type UserDataEncryptionKey struct { CreatedAt time.Time } +type UserGrant struct { + UserID int32 + UserVersion int32 + GrantID int32 + AppID int32 + AccountID int32 + GrantedClientID string + IsRevoked bool + RevokedAt pgtype.Timestamptz + ExpiresAt pgtype.Timestamptz + CreatedAt time.Time +} + +type UserSession struct { + UserID int32 + UserVersion int32 + SessionID int32 + AppID int32 + AccountID int32 + SessionUuid uuid.UUID + CreatedAt time.Time +} + type UserTotp struct { UserID int32 TotpID int32 diff --git a/idp/internal/providers/database/queries/account_grants.sql b/idp/internal/providers/database/queries/account_grants.sql new file mode 100644 index 0000000..c368ae6 --- /dev/null +++ b/idp/internal/providers/database/queries/account_grants.sql @@ -0,0 +1,41 @@ +-- 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/. + +-- name: CreateAccountGrantWithoutAccountCredentials :exec +INSERT INTO "account_grants" ( + "account_id", + "account_version", + "grant_id", + "granted_client_id" +) VALUES ( + $1, + $2, + $3, + $4 +); + +-- name: CreateAccountGrantWithAccountCredentials :exec +INSERT INTO "account_grants" ( + "account_id", + "account_version", + "grant_id", + "granted_client_id", + "account_credentials_id" +) VALUES ( + $1, + $2, + $3, + $4, + $5 +); + +-- name: FindAccountGrantByAccountIDAndGrantedClientID :one +SELECT "a".*, "g".* +FROM "account_grants" AS "a" +LEFT JOIN "grants" AS "g" ON "a"."grant_id" = "g"."id" +WHERE "a"."account_id" = $1 +AND "a"."granted_client_id" = $2 +LIMIT 1; diff --git a/idp/internal/providers/database/queries/account_sessions.sql b/idp/internal/providers/database/queries/account_sessions.sql new file mode 100644 index 0000000..e343fbc --- /dev/null +++ b/idp/internal/providers/database/queries/account_sessions.sql @@ -0,0 +1,51 @@ +-- 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/. + +-- name: CreateAccountSessionWithoutAccountCredentials :exec +INSERT INTO "account_sessions" ( + "account_id", + "account_version", + "session_id", + "session_uuid" +) VALUES ( + $1, + $2, + $3, + $4 +); + +-- name: CreateAccountSessionWithAccountCredentials :exec +INSERT INTO "account_sessions" ( + "account_id", + "account_version", + "session_id", + "session_uuid", + "account_credentials_id" +) VALUES ( + $1, + $2, + $3, + $4, + $5 +); + +-- name: FindAccountSessionByAccountIDAndSessionUUID :one +SELECT "a".*, "s".* +FROM "account_sessions" AS "a" +LEFT JOIN "sessions" AS "s" ON "a"."session_id" = "s"."id" +WHERE "a"."account_id" = $1 +AND "a"."session_uuid" = $2 +LIMIT 1; + +-- name: DeleteAccountSessionByAccountIDAndSessionID :exec +DELETE FROM "account_sessions" +WHERE "account_id" = $1 AND "session_id" = $2; + +-- name: DeleteAllSessionsByAccountID :exec +DELETE FROM "sessions" AS "s" +USING "account_sessions" AS "a" +WHERE "a"."session_id" = "s"."id" +AND "a"."account_id" = $1; diff --git a/idp/internal/providers/database/queries/grants.sql b/idp/internal/providers/database/queries/grants.sql new file mode 100644 index 0000000..0603df1 --- /dev/null +++ b/idp/internal/providers/database/queries/grants.sql @@ -0,0 +1,20 @@ +-- 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/. + +-- name: CreateGrant :one +INSERT INTO "grants" ( + "account_id", + "grant_id", + "granted_client_id", + "granted_scopes", + "granted_custom_scopes" +) VALUES ( + $1, + $2, + $3, + $4, + $5 +) RETURNING id; diff --git a/idp/internal/providers/database/queries/revoked_tokens.sql b/idp/internal/providers/database/queries/revoked_tokens.sql deleted file mode 100644 index ba87662..0000000 --- a/idp/internal/providers/database/queries/revoked_tokens.sql +++ /dev/null @@ -1,26 +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/. - --- name: RevokeToken :exec -INSERT INTO "revoked_tokens" ( - "token_id", - "account_id", - "owner", - "owner_public_id", - "issued_at", - "expires_at" -) VALUES ( - $1, - $2, - $3, - $4, - $5, - $6 -); - --- name: GetRevokedToken :one -SELECT * FROM "revoked_tokens" -WHERE "token_id" = $1 LIMIT 1; \ No newline at end of file diff --git a/idp/internal/providers/database/queries/session_tokens.sql b/idp/internal/providers/database/queries/session_tokens.sql new file mode 100644 index 0000000..8bafa4b --- /dev/null +++ b/idp/internal/providers/database/queries/session_tokens.sql @@ -0,0 +1,30 @@ +-- 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/. + +-- name: CreateSessionToken :exec +INSERT INTO "session_tokens" ( + "token_id", + "account_id", + "session_id", + "session_uuid", + "grant_id", + "expires_at" +) VALUES ( + $1, + $2, + $3, + $4, + $5, + $6 +); + +-- name: FindSessionTokenByTokenID :one +SELECT * FROM "session_tokens" +WHERE "token_id" = $1 LIMIT 1; + +-- name: DeleteSessionToken :exec +DELETE FROM "session_tokens" +WHERE "token_id" = $1; diff --git a/idp/internal/providers/database/queries/sessions.sql b/idp/internal/providers/database/queries/sessions.sql new file mode 100644 index 0000000..97a574a --- /dev/null +++ b/idp/internal/providers/database/queries/sessions.sql @@ -0,0 +1,32 @@ +-- 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/. + +-- name: CreateSession :one +INSERT INTO "sessions" ( + "account_id", + "grant_id", + "session_id", + "session_type", + "session_client_id", + "ip_address", + "user_agent", + "expires_at" +) VALUES ( + $1, + $2, + $3, + $4, + $5, + $6, + $7, + $8 +) RETURNING "id"; + +-- name: UpdateSessionExpiresAt :exec +UPDATE "sessions" SET "expires_at" = $1 WHERE "id" = $2; + +-- name: DeleteSessionByID :exec +DELETE FROM "sessions" WHERE "id" = $1; diff --git a/idp/internal/providers/database/registered_apps.sql.go b/idp/internal/providers/database/registered_apps.sql.go index e0e01ff..10cc1f2 100644 --- a/idp/internal/providers/database/registered_apps.sql.go +++ b/idp/internal/providers/database/registered_apps.sql.go @@ -107,7 +107,7 @@ INSERT INTO "apps" ( $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 +) 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, session_type, access_token_ttl, id_token_ttl, refresh_token_idle_ttl, refresh_token_ttl, grant_ttl, created_at, updated_at ` type CreateRegisteredAppParams struct { @@ -258,9 +258,12 @@ func (q *Queries) CreateRegisteredApp(ctx context.Context, arg CreateRegisteredA &i.InitiateLoginUri, &i.RequestUris, &i.AccessTokenSigningAlg, + &i.SessionType, + &i.AccessTokenTtl, &i.IDTokenTtl, - &i.TokenTtl, + &i.RefreshTokenIdleTtl, &i.RefreshTokenTtl, + &i.GrantTtl, &i.CreatedAt, &i.UpdatedAt, ) @@ -313,7 +316,7 @@ UPDATE "apps" SET "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 +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, session_type, access_token_ttl, id_token_ttl, refresh_token_idle_ttl, refresh_token_ttl, grant_ttl, created_at, updated_at ` type UpdateRegisteredAppParams struct { @@ -456,9 +459,12 @@ func (q *Queries) UpdateRegisteredApp(ctx context.Context, arg UpdateRegisteredA &i.InitiateLoginUri, &i.RequestUris, &i.AccessTokenSigningAlg, + &i.SessionType, + &i.AccessTokenTtl, &i.IDTokenTtl, - &i.TokenTtl, + &i.RefreshTokenIdleTtl, &i.RefreshTokenTtl, + &i.GrantTtl, &i.CreatedAt, &i.UpdatedAt, ) diff --git a/idp/internal/providers/database/revoked_tokens.sql.go b/idp/internal/providers/database/revoked_tokens.sql.go deleted file mode 100644 index 031b59b..0000000 --- a/idp/internal/providers/database/revoked_tokens.sql.go +++ /dev/null @@ -1,79 +0,0 @@ -// Code generated by sqlc. DO NOT EDIT. -// versions: -// sqlc v1.31.1 -// source: revoked_tokens.sql - -package database - -import ( - "context" - "time" - - "github.com/google/uuid" -) - -const getRevokedToken = `-- name: GetRevokedToken :one -SELECT id, token_id, account_id, owner, owner_public_id, issued_at, expires_at, created_at FROM "revoked_tokens" -WHERE "token_id" = $1 LIMIT 1 -` - -func (q *Queries) GetRevokedToken(ctx context.Context, tokenID uuid.UUID) (RevokedToken, error) { - row := q.db.QueryRow(ctx, getRevokedToken, tokenID) - var i RevokedToken - err := row.Scan( - &i.ID, - &i.TokenID, - &i.AccountID, - &i.Owner, - &i.OwnerPublicID, - &i.IssuedAt, - &i.ExpiresAt, - &i.CreatedAt, - ) - return i, err -} - -const revokeToken = `-- name: RevokeToken :exec - -INSERT INTO "revoked_tokens" ( - "token_id", - "account_id", - "owner", - "owner_public_id", - "issued_at", - "expires_at" -) VALUES ( - $1, - $2, - $3, - $4, - $5, - $6 -) -` - -type RevokeTokenParams struct { - TokenID uuid.UUID - AccountID int32 - Owner TokenOwner - OwnerPublicID uuid.UUID - IssuedAt time.Time - ExpiresAt time.Time -} - -// 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/. -func (q *Queries) RevokeToken(ctx context.Context, arg RevokeTokenParams) error { - _, err := q.db.Exec(ctx, revokeToken, - arg.TokenID, - arg.AccountID, - arg.Owner, - arg.OwnerPublicID, - arg.IssuedAt, - arg.ExpiresAt, - ) - return err -} diff --git a/idp/internal/providers/database/session_tokens.sql.go b/idp/internal/providers/database/session_tokens.sql.go new file mode 100644 index 0000000..d4cd1b5 --- /dev/null +++ b/idp/internal/providers/database/session_tokens.sql.go @@ -0,0 +1,90 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 +// source: session_tokens.sql + +package database + +import ( + "context" + "time" + + "github.com/google/uuid" +) + +const createSessionToken = `-- name: CreateSessionToken :exec + +INSERT INTO "session_tokens" ( + "token_id", + "account_id", + "session_id", + "session_uuid", + "grant_id", + "expires_at" +) VALUES ( + $1, + $2, + $3, + $4, + $5, + $6 +) +` + +type CreateSessionTokenParams struct { + TokenID uuid.UUID + AccountID int32 + SessionID int32 + SessionUuid uuid.UUID + GrantID int32 + ExpiresAt time.Time +} + +// 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/. +func (q *Queries) CreateSessionToken(ctx context.Context, arg CreateSessionTokenParams) error { + _, err := q.db.Exec(ctx, createSessionToken, + arg.TokenID, + arg.AccountID, + arg.SessionID, + arg.SessionUuid, + arg.GrantID, + arg.ExpiresAt, + ) + return err +} + +const deleteSessionToken = `-- name: DeleteSessionToken :exec +DELETE FROM "session_tokens" +WHERE "token_id" = $1 +` + +func (q *Queries) DeleteSessionToken(ctx context.Context, tokenID uuid.UUID) error { + _, err := q.db.Exec(ctx, deleteSessionToken, tokenID) + return err +} + +const findSessionTokenByTokenID = `-- name: FindSessionTokenByTokenID :one +SELECT id, account_id, session_id, grant_id, session_uuid, token_id, issued_at, expires_at, created_at FROM "session_tokens" +WHERE "token_id" = $1 LIMIT 1 +` + +func (q *Queries) FindSessionTokenByTokenID(ctx context.Context, tokenID uuid.UUID) (SessionToken, error) { + row := q.db.QueryRow(ctx, findSessionTokenByTokenID, tokenID) + var i SessionToken + err := row.Scan( + &i.ID, + &i.AccountID, + &i.SessionID, + &i.GrantID, + &i.SessionUuid, + &i.TokenID, + &i.IssuedAt, + &i.ExpiresAt, + &i.CreatedAt, + ) + return i, err +} diff --git a/idp/internal/providers/database/sessions.sql.go b/idp/internal/providers/database/sessions.sql.go new file mode 100644 index 0000000..936e7d1 --- /dev/null +++ b/idp/internal/providers/database/sessions.sql.go @@ -0,0 +1,92 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 +// source: sessions.sql + +package database + +import ( + "context" + "time" + + "github.com/google/uuid" + "github.com/jackc/pgx/v5/pgtype" +) + +const createSession = `-- name: CreateSession :one + +INSERT INTO "sessions" ( + "account_id", + "grant_id", + "session_id", + "session_type", + "session_client_id", + "ip_address", + "user_agent", + "expires_at" +) VALUES ( + $1, + $2, + $3, + $4, + $5, + $6, + $7, + $8 +) RETURNING "id" +` + +type CreateSessionParams struct { + AccountID int32 + GrantID int32 + SessionID uuid.UUID + SessionType SessionType + SessionClientID string + IpAddress pgtype.Text + UserAgent pgtype.Text + ExpiresAt time.Time +} + +// 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/. +func (q *Queries) CreateSession(ctx context.Context, arg CreateSessionParams) (int32, error) { + row := q.db.QueryRow(ctx, createSession, + arg.AccountID, + arg.GrantID, + arg.SessionID, + arg.SessionType, + arg.SessionClientID, + arg.IpAddress, + arg.UserAgent, + arg.ExpiresAt, + ) + var id int32 + err := row.Scan(&id) + return id, err +} + +const deleteSessionByID = `-- name: DeleteSessionByID :exec +DELETE FROM "sessions" WHERE "id" = $1 +` + +func (q *Queries) DeleteSessionByID(ctx context.Context, id int32) error { + _, err := q.db.Exec(ctx, deleteSessionByID, id) + return err +} + +const updateSessionExpiresAt = `-- name: UpdateSessionExpiresAt :exec +UPDATE "sessions" SET "expires_at" = $1 WHERE "id" = $2 +` + +type UpdateSessionExpiresAtParams struct { + ExpiresAt time.Time + ID int32 +} + +func (q *Queries) UpdateSessionExpiresAt(ctx context.Context, arg UpdateSessionExpiresAtParams) error { + _, err := q.db.Exec(ctx, updateSessionExpiresAt, arg.ExpiresAt, arg.ID) + return err +} diff --git a/idp/internal/providers/tokens/access.go b/idp/internal/providers/tokens/access.go index fc53f7b..4670a88 100644 --- a/idp/internal/providers/tokens/access.go +++ b/idp/internal/providers/tokens/access.go @@ -32,7 +32,7 @@ func (t *Tokens) getAccessTokenTTL(tokenSubject, publicID string) int64 { } func (t *Tokens) CreateAccessToken(opts AccountAccessTokenOptions) (*jwt.Token, error) { - return t.createAuthToken(accountAuthTokenOptions{ + accesToken, _, err := t.createAuthToken(accountAuthTokenOptions{ cryptoSuite: utils.SupportedCryptoSuiteES256, ttlSec: t.getAccessTokenTTL(opts.TokenSubject, opts.PublicID.String()), accountPublicID: opts.PublicID, @@ -41,6 +41,7 @@ func (t *Tokens) CreateAccessToken(opts AccountAccessTokenOptions) (*jwt.Token, tokenSubject: opts.TokenSubject, paths: baseAccessPaths, }) + return accesToken, err } func (t *Tokens) VerifyAccessToken(token string, getPublicJWK GetPublicJWK) (AccountClaims, []AccountScope, error) { diff --git a/idp/internal/providers/tokens/accounts.go b/idp/internal/providers/tokens/accounts.go index 050e1e3..503bd1c 100644 --- a/idp/internal/providers/tokens/accounts.go +++ b/idp/internal/providers/tokens/accounts.go @@ -44,12 +44,14 @@ const baseAuthScope = AccountScopeEmail + " " + AccountScopeProfile type AccountClaims struct { AccountID uuid.UUID `json:"account_id"` - AccountVersion int32 `json:"account_version"` + AccountVersion int32 `json:"account_ver"` } type accountAuthTokenClaims struct { AccountClaims - Scope string `json:"scope"` + Scope string `json:"scope"` + AuthorizedParty string `json:"azp"` + SessionID string `json:"sid,omitempty"` jwt.RegisteredClaims } @@ -64,6 +66,8 @@ type accountAuthTokenOptions struct { ttlSec int64 accountPublicID uuid.UUID accountVersion int32 + sessionID uuid.UUID + clientID utils.Base62UUIDStr tokenSubject string scopes []AccountScope paths []string @@ -90,13 +94,25 @@ func splitAccountScopes(scope string) ([]AccountScope, error) { return strings.Split(scope, " "), nil } -func (t *Tokens) createAuthToken(opts accountAuthTokenOptions) (*jwt.Token, error) { +type JTI = uuid.UUID + +func (t *Tokens) createAuthToken(opts accountAuthTokenOptions) (*jwt.Token, JTI, error) { now := time.Now() iat := jwt.NewNumericDate(now) exp := jwt.NewNumericDate(now.Add(time.Second * time.Duration(opts.ttlSec))) method, err := getSigningMethod(opts.cryptoSuite) if err != nil { - return nil, err + return nil, uuid.Nil, err + } + + var sessionID string + if opts.sessionID != uuid.Nil { + sessionID = opts.sessionID.String() + } + + jti, err := uuid.NewV7() + if err != nil { + return nil, uuid.Nil, err } return jwt.NewWithClaims(method, accountAuthTokenClaims{ @@ -104,6 +120,7 @@ func (t *Tokens) createAuthToken(opts accountAuthTokenOptions) (*jwt.Token, erro AccountID: opts.accountPublicID, AccountVersion: opts.accountVersion, }, + AuthorizedParty: opts.clientID, RegisteredClaims: jwt.RegisteredClaims{ Issuer: fmt.Sprintf("https://%s", t.backendDomain), Audience: utils.MapSlice(opts.paths, func(path *string) string { @@ -113,11 +130,11 @@ func (t *Tokens) createAuthToken(opts accountAuthTokenOptions) (*jwt.Token, erro IssuedAt: iat, NotBefore: iat, ExpiresAt: exp, - ID: uuid.NewString(), + ID: jti.String(), }, - Scope: processAccountScopes(opts.scopes), - }), nil - + Scope: processAccountScopes(opts.scopes), + SessionID: sessionID, + }), jti, nil } func verifyAuthToken(token string, pubKeyFn func(token *jwt.Token) (any, error)) (accountAuthTokenClaims, error) { diff --git a/idp/internal/providers/tokens/refresh.go b/idp/internal/providers/tokens/refresh.go index 6496d59..7c07388 100644 --- a/idp/internal/providers/tokens/refresh.go +++ b/idp/internal/providers/tokens/refresh.go @@ -24,7 +24,7 @@ type AccountRefreshTokenOptions struct { Scopes []AccountScope } -func (t *Tokens) CreateRefreshToken(opts AccountRefreshTokenOptions) (*jwt.Token, error) { +func (t *Tokens) CreateRefreshToken(opts AccountRefreshTokenOptions) (*jwt.Token, JTI, error) { return t.createAuthToken(accountAuthTokenOptions{ cryptoSuite: utils.SupportedCryptoSuiteEd25519, ttlSec: t.refreshTTL, diff --git a/idp/internal/providers/tokens/users.go b/idp/internal/providers/tokens/users.go index 4d58cb6..a071f7b 100644 --- a/idp/internal/providers/tokens/users.go +++ b/idp/internal/providers/tokens/users.go @@ -20,15 +20,16 @@ import ( type UserAuthClaims struct { UserID uuid.UUID `json:"user_id"` - UserVersion int32 `json:"user_version"` + UserVersion int32 `json:"user_ver"` UserRoles []string `json:"user_roles"` } type userAuthTokenClaims struct { UserAuthClaims - AppClaims Scope string `json:"scope"` - AuthorizedParty string `json:"azp,omitempty"` + SessionID string `json:"sid,omitempty"` + AuthorizedParty string `json:"azp"` + AppVersion int32 `json:"app_ver"` jwt.RegisteredClaims } @@ -85,11 +86,9 @@ func (t *Tokens) CreateUserAuthToken(opts UserAuthTokenOptions) (*jwt.Token, err UserVersion: opts.UserVersion, UserRoles: opts.UserRoles, }, - AppClaims: AppClaims{ - ClientID: opts.AppClientID, - Version: opts.AppVersion, - }, - Scope: strings.Join(opts.Scopes, " "), + AuthorizedParty: opts.AppClientID, + AppVersion: opts.AppVersion, + Scope: strings.Join(opts.Scopes, " "), RegisteredClaims: jwt.RegisteredClaims{ Issuer: iss, Audience: utils.MapSlice(opts.Paths, func(path *string) string { @@ -121,7 +120,10 @@ func (t *Tokens) VerifyUserAuthToken( return UserAuthClaims{}, AppClaims{}, nil, uuid.Nil, time.Time{}, err } - return claims.UserAuthClaims, claims.AppClaims, strings.Split(claims.Scope, " "), tokenID, claims.ExpiresAt.Time, nil + return claims.UserAuthClaims, AppClaims{ + ClientID: claims.AuthorizedParty, + Version: claims.AppVersion, + }, strings.Split(claims.Scope, " "), tokenID, claims.ExpiresAt.Time, nil } type UserPurposeClaims struct { diff --git a/idp/internal/services/account_2fa_configs.go b/idp/internal/services/account_2fa_configs.go index 14b05bb..d15796b 100644 --- a/idp/internal/services/account_2fa_configs.go +++ b/idp/internal/services/account_2fa_configs.go @@ -20,6 +20,7 @@ import ( "github.com/tugascript/devlogs/idp/internal/providers/mailer" "github.com/tugascript/devlogs/idp/internal/providers/tokens" "github.com/tugascript/devlogs/idp/internal/services/dtos" + "github.com/tugascript/devlogs/idp/internal/utils" ) const ( @@ -697,6 +698,8 @@ type ConfirmDeleteAccount2FAConfigOptions struct { Version int32 TwoFAType string Code string + IPAddress string + UserAgent string } func (s *Services) ConfirmDeleteAccount2FAConfig( @@ -780,14 +783,25 @@ func (s *Services) ConfirmDeleteAccount2FAConfig( } accountDTO = dtos.MapAccountToDTO(&account) - return s.GenerateFullAuthDTO( + sessionID, err := uuid.NewV7() + if err != nil { + logger.ErrorContext(ctx, "Failed to generate session ID", "error", err) + return dtos.AuthDTO{}, exceptions.NewInternalServerError() + } + + return s.generateFullAuthDTO( ctx, - logger, - qrs, - opts.RequestID, - &accountDTO, - []tokens.AccountScope{tokens.AccountScopeAdmin}, - "Account 2FA config deleted successfully", + generateFullAuthDTOOptions{ + requestID: opts.RequestID, + accountID: accountDTO.ID(), + accountPublicID: accountDTO.PublicID, + accountVersion: accountDTO.Version(), + sessionID: sessionID, + scopes: []tokens.AccountScope{tokens.AccountScopeAdmin}, + clientID: utils.NilBase62UUID, + ipAddress: opts.IPAddress, + userAgent: opts.UserAgent, + }, ) } @@ -821,13 +835,24 @@ func (s *Services) ConfirmDeleteAccount2FAConfig( } } - return s.GenerateFullAuthDTO( + sessionID, err := uuid.NewV7() + if err != nil { + logger.ErrorContext(ctx, "Failed to generate session ID", "error", err) + return dtos.AuthDTO{}, exceptions.NewInternalServerError() + } + + return s.generateFullAuthDTO( ctx, - logger, - qrs, - opts.RequestID, - &accountDTO, - []tokens.AccountScope{tokens.AccountScopeAdmin}, - "Account 2FA config deleted successfully", + generateFullAuthDTOOptions{ + requestID: opts.RequestID, + accountID: accountDTO.ID(), + accountPublicID: accountDTO.PublicID, + accountVersion: accountDTO.Version(), + sessionID: sessionID, + scopes: []tokens.AccountScope{tokens.AccountScopeAdmin}, + clientID: utils.NilBase62UUID, + ipAddress: opts.IPAddress, + userAgent: opts.UserAgent, + }, ) } diff --git a/idp/internal/services/account_credentials.go b/idp/internal/services/account_credentials.go index 62115b0..0b1258b 100644 --- a/idp/internal/services/account_credentials.go +++ b/idp/internal/services/account_credentials.go @@ -503,6 +503,11 @@ func (s *Services) GetAccountCredentialsByClientIDAndAccountPublicID( ) logger.InfoContext(ctx, "Getting account keys by client id and account public id...") + if opts.ClientID == utils.NilBase62UUID { + logger.DebugContext(ctx, "ClientID is nil, returning not found") + return dtos.AccountCredentialsDTO{}, exceptions.NewNotFoundError() + } + accountCredentials, err := s.database.FindAccountCredentialsByAccountPublicIDAndClientID( ctx, database.FindAccountCredentialsByAccountPublicIDAndClientIDParams{ diff --git a/idp/internal/services/accounts.go b/idp/internal/services/accounts.go index 90d3aeb..c272f55 100644 --- a/idp/internal/services/accounts.go +++ b/idp/internal/services/accounts.go @@ -127,10 +127,11 @@ func (s *Services) CreateAccount( s.database.FinalizeTx(ctx, txn, err, serviceErr) }() - publicID, err := uuid.NewRandom() + publicID, err := uuid.NewV7() if err != nil { logger.ErrorContext(ctx, "Failed to generate public ID", "error", err) - return dtos.AccountDTO{}, exceptions.NewInternalServerError() + serviceErr = exceptions.NewInternalServerError() + return dtos.AccountDTO{}, serviceErr } var account database.Account @@ -313,8 +314,7 @@ func (s *Services) updateAccountEmailInDB( }() // Delete external auth providers since they won't be valid anymore - err = qrs.DeleteExternalAccountAuthProviders(ctx, oldEmail) - if err != nil { + if err = qrs.DeleteExternalAccountAuthProviders(ctx, oldEmail); err != nil { return database.Account{}, err } @@ -336,6 +336,8 @@ type UpdateAccountEmailOptions struct { Version int32 Email string Password string + IPAddress string + UserAgent string } func (s *Services) UpdateAccountEmail( @@ -417,14 +419,26 @@ func (s *Services) UpdateAccountEmail( logger.InfoContext(ctx, "Updated account email successfully") accountDTO = dtos.MapAccountToDTO(&account) - return s.GenerateFullAuthDTO( + + sessionUUID, err := uuid.NewV7() + if err != nil { + logger.ErrorContext(ctx, "Failed to generate new session UUID", "error", err) + return dtos.AuthDTO{}, exceptions.NewInternalServerError() + } + + return s.generateFullAuthDTO( ctx, - logger, - s.database.Queries, - opts.RequestID, - &accountDTO, - []tokens.AccountScope{tokens.AccountScopeAdmin}, - "Email updated successfully", + generateFullAuthDTOOptions{ + requestID: opts.RequestID, + accountID: accountDTO.ID(), + accountVersion: accountDTO.Version(), + accountPublicID: accountDTO.PublicID, + scopes: []tokens.AccountScope{tokens.AccountScopeAdmin}, + sessionID: sessionUUID, + clientID: utils.NilBase62UUID, + ipAddress: opts.IPAddress, + userAgent: opts.UserAgent, + }, ) } @@ -463,6 +477,8 @@ type ConfirmUpdateAccountEmailOptions struct { Version int32 TwoFAType tokens.TwoFAType Code string + IPAddress string + UserAgent string } func (s *Services) ConfirmUpdateAccountEmail( @@ -518,14 +534,26 @@ func (s *Services) ConfirmUpdateAccountEmail( logger.InfoContext(ctx, "Confirmed account email update successfully") accountDTO = dtos.MapAccountToDTO(&account) - return s.GenerateFullAuthDTO( + + sessionID, err := uuid.NewV7() + if err != nil { + logger.ErrorContext(ctx, "Failed to generate session ID", "error", err) + return dtos.AuthDTO{}, exceptions.NewInternalServerError() + } + + return s.generateFullAuthDTO( ctx, - logger, - s.database.Queries, - opts.RequestID, - &accountDTO, - []tokens.AccountScope{tokens.AccountScopeAdmin}, - "Email updated successfully", + generateFullAuthDTOOptions{ + requestID: opts.RequestID, + accountID: accountDTO.ID(), + accountPublicID: accountDTO.PublicID, + accountVersion: accountDTO.Version(), + sessionID: sessionID, + scopes: []tokens.AccountScope{tokens.AccountScopeAdmin}, + clientID: utils.NilBase62UUID, + ipAddress: opts.IPAddress, + userAgent: opts.UserAgent, + }, ) } @@ -558,6 +586,8 @@ type UpdateAccountPasswordOptions struct { Version int32 Password string NewPassword string + IPAddress string + UserAgent string } func (s *Services) UpdateAccountPassword( @@ -656,14 +686,26 @@ func (s *Services) UpdateAccountPassword( } accountDTO = dtos.MapAccountToDTO(&account) - return s.GenerateFullAuthDTO( + + sessionID, err := uuid.NewV7() + if err != nil { + logger.ErrorContext(ctx, "Failed to generate session ID", "error", err) + return dtos.AuthDTO{}, exceptions.NewInternalServerError() + } + + return s.generateFullAuthDTO( ctx, - logger, - s.database.Queries, - opts.RequestID, - &accountDTO, - []tokens.AccountScope{tokens.AccountScopeAdmin}, - "Password updated successfully", + generateFullAuthDTOOptions{ + requestID: opts.RequestID, + accountID: accountDTO.ID(), + accountPublicID: accountDTO.PublicID, + accountVersion: accountDTO.Version(), + sessionID: sessionID, + scopes: []tokens.AccountScope{tokens.AccountScopeAdmin}, + clientID: utils.NilBase62UUID, + ipAddress: opts.IPAddress, + userAgent: opts.UserAgent, + }, ) } @@ -673,6 +715,8 @@ type ConfirmUpdateAccountPasswordOptions struct { Version int32 TwoFAType tokens.TwoFAType Code string + IPAddress string + UserAgent string } func (s *Services) ConfirmUpdateAccountPassword( @@ -728,14 +772,26 @@ func (s *Services) ConfirmUpdateAccountPassword( accountDTO = dtos.MapAccountToDTO(&account) logger.InfoContext(ctx, "Confirmed account password update successfully") - return s.GenerateFullAuthDTO( + + sessionID, err := uuid.NewV7() + if err != nil { + logger.ErrorContext(ctx, "Failed to generate session ID", "error", err) + return dtos.AuthDTO{}, exceptions.NewInternalServerError() + } + + return s.generateFullAuthDTO( ctx, - logger, - s.database.Queries, - opts.RequestID, - &accountDTO, - []tokens.AccountScope{tokens.AccountScopeAdmin}, - "Password updated successfully", + generateFullAuthDTOOptions{ + requestID: opts.RequestID, + accountID: accountDTO.ID(), + accountPublicID: accountDTO.PublicID, + accountVersion: accountDTO.Version(), + sessionID: sessionID, + scopes: []tokens.AccountScope{tokens.AccountScopeAdmin}, + clientID: utils.NilBase62UUID, + ipAddress: opts.IPAddress, + userAgent: opts.UserAgent, + }, ) } @@ -744,6 +800,8 @@ type CreateAccountPasswordOptions struct { PublicID uuid.UUID Version int32 Password string + IPAddress string + UserAgent string } func (s *Services) CreateAccountPassword( @@ -824,14 +882,26 @@ func (s *Services) CreateAccountPassword( accountDTO = dtos.MapAccountToDTO(&account) logger.InfoContext(ctx, "Account password created successfully") - return s.GenerateFullAuthDTO( + + sessionID, err := uuid.NewV7() + if err != nil { + logger.ErrorContext(ctx, "Failed to generate session ID", "error", err) + return dtos.AuthDTO{}, exceptions.NewInternalServerError() + } + + return s.generateFullAuthDTO( ctx, - logger, - s.database.Queries, - opts.RequestID, - &accountDTO, - []tokens.AccountScope{tokens.AccountScopeAdmin}, - "Password created successfully", + generateFullAuthDTOOptions{ + requestID: opts.RequestID, + accountID: accountDTO.ID(), + accountPublicID: accountDTO.PublicID, + accountVersion: accountDTO.Version(), + sessionID: sessionID, + scopes: []tokens.AccountScope{tokens.AccountScopeAdmin}, + clientID: utils.NilBase62UUID, + ipAddress: opts.IPAddress, + userAgent: opts.UserAgent, + }, ) } @@ -882,6 +952,8 @@ type UpdateAccountUsernameOptions struct { Version int32 Username string Password string + IPAddress string + UserAgent string } func (s *Services) UpdateAccountUsername( @@ -996,14 +1068,26 @@ func (s *Services) UpdateAccountUsername( accountDTO = dtos.MapAccountToDTO(&account) logger.InfoContext(ctx, "Updated account username successfully") - return s.GenerateFullAuthDTO( + + sessionID, err := uuid.NewV7() + if err != nil { + logger.ErrorContext(ctx, "Failed to generate session ID", "error", err) + return dtos.AuthDTO{}, exceptions.NewInternalServerError() + } + + return s.generateFullAuthDTO( ctx, - logger, - s.database.Queries, - opts.RequestID, - &accountDTO, - []tokens.AccountScope{tokens.AccountScopeAdmin}, - "Username updated successfully", + generateFullAuthDTOOptions{ + requestID: opts.RequestID, + accountID: accountDTO.ID(), + accountPublicID: accountDTO.PublicID, + accountVersion: accountDTO.Version(), + sessionID: sessionID, + scopes: []tokens.AccountScope{tokens.AccountScopeAdmin}, + clientID: utils.NilBase62UUID, + ipAddress: opts.IPAddress, + userAgent: opts.UserAgent, + }, ) } @@ -1013,6 +1097,8 @@ type ConfirmUpdateAccountUsernameOptions struct { Version int32 TwoFAType tokens.TwoFAType Code string + IPAddress string + UserAgent string } func (s *Services) ConfirmUpdateAccountUsername( @@ -1071,14 +1157,26 @@ func (s *Services) ConfirmUpdateAccountUsername( accountDTO = dtos.MapAccountToDTO(&account) logger.InfoContext(ctx, "Updated account username successfully") - return s.GenerateFullAuthDTO( + + sessionID, err := uuid.NewV7() + if err != nil { + logger.ErrorContext(ctx, "Failed to generate session ID", "error", err) + return dtos.AuthDTO{}, exceptions.NewInternalServerError() + } + + return s.generateFullAuthDTO( ctx, - logger, - s.database.Queries, - opts.RequestID, - &accountDTO, - []tokens.AccountScope{tokens.AccountScopeAdmin}, - "Username updated successfully", + generateFullAuthDTOOptions{ + requestID: opts.RequestID, + accountID: accountDTO.ID(), + accountPublicID: accountDTO.PublicID, + accountVersion: accountDTO.Version(), + sessionID: sessionID, + scopes: []tokens.AccountScope{tokens.AccountScopeAdmin}, + clientID: utils.NilBase62UUID, + ipAddress: opts.IPAddress, + userAgent: opts.UserAgent, + }, ) } diff --git a/idp/internal/services/auth.go b/idp/internal/services/auth.go index 3765b6b..900f515 100644 --- a/idp/internal/services/auth.go +++ b/idp/internal/services/auth.go @@ -12,6 +12,7 @@ import ( "log/slog" "regexp" "strconv" + "time" "github.com/google/uuid" "github.com/jackc/pgx/v5/pgtype" @@ -197,46 +198,460 @@ func (s *Services) RegisterAccount( return dtos.NewMessageDTO("Account registered successfully. Confirmation email has been sent."), nil } -func (s *Services) GenerateFullAuthDTO( - ctx context.Context, - logger *slog.Logger, - qrs *database.Queries, - requestID string, - accountDTO *dtos.AccountDTO, +func mapAccountScopes(scopes []tokens.AccountScope) ([]database.Scopes, []string) { + dbScopes := make([]database.Scopes, 0) + customScopes := make([]string, 0) + + for _, scope := range scopes { + switch scope { + case tokens.AccountScopeEmail: + dbScopes = append(dbScopes, database.ScopesEmail) + case tokens.AccountScopeProfile: + dbScopes = append(dbScopes, database.ScopesProfile) + default: + customScopes = append(customScopes, scope) + } + } + + return dbScopes, customScopes +} + +func validateScopes( scopes []tokens.AccountScope, - logSuccessMessage string, + grantedScopes []database.Scopes, + grantedCustomScopes []string, +) *exceptions.ServiceError { + scopesSet := utils.SliceToHashSet(scopes) + + for _, grantedScope := range grantedScopes { + if !scopesSet.Contains(tokens.AccountScope(grantedScope)) { + return exceptions.NewForbiddenError() + } + } + + for _, grantedCustomScope := range grantedCustomScopes { + if !scopesSet.Contains(grantedCustomScope) { + return exceptions.NewForbiddenError() + } + } + + return nil +} + +type upsertAccountGrantSessionAndTokenOptions struct { + requestID string + accountID int32 + accountVersion int32 + accountPublicID uuid.UUID + scopes []tokens.AccountScope + sessionID uuid.UUID + tokenID uuid.UUID + clientID utils.Base62UUIDStr + ipAddress string + userAgent string +} + +func (s *Services) createAccountGrantSessionAndToken( + ctx context.Context, + opts upsertAccountGrantSessionAndTokenOptions, +) *exceptions.ServiceError { + logger := s.buildLogger(opts.requestID, authLocation, "createAccountGrantSessionAndToken").With( + "accountID", opts.accountID, + "accountVersion", opts.accountVersion, + "accountPublicID", opts.accountPublicID, + "sessionID", opts.sessionID, + "clientID", opts.clientID, + ) + logger.InfoContext(ctx, "Creating account grant session and token...") + + accountCredentialsDTO, serviceErr := s.GetAccountCredentialsByClientIDAndAccountPublicID(ctx, GetAccountCredentialsByClientIDAndAccountPublicIDOptions{ + RequestID: opts.requestID, + AccountPublicID: opts.accountPublicID, + ClientID: opts.clientID, + }) + if serviceErr != nil { + if serviceErr.Code != exceptions.CodeNotFound { + logger.ErrorContext(ctx, "Failed to get account credentials", "error", serviceErr) + return serviceErr + } + serviceErr = nil + } + + grantUUID, err := uuid.NewV7() + if err != nil { + logger.ErrorContext(ctx, "Failed to generate grant UUID", "error", err) + return exceptions.NewInternalServerError() + } + + grantedScopes, grantedCustomScopes := mapAccountScopes(opts.scopes) + expiresAt := time.Now().Add(time.Duration(s.jwt.GetRefreshTTL()) * time.Second) + var ipAddress pgtype.Text + if err := ipAddress.Scan(opts.ipAddress); err != nil { + logger.ErrorContext(ctx, "Failed to scan IP address", "error", err) + return exceptions.NewInternalServerError() + } + + var userAgent pgtype.Text + if err := userAgent.Scan(opts.userAgent); err != nil { + logger.ErrorContext(ctx, "Failed to scan user agent", "error", err) + return exceptions.NewInternalServerError() + } + + qrs, txn, err := s.database.BeginTx(ctx) + if err != nil { + logger.ErrorContext(ctx, "Failed to start transaction", "error", err) + return exceptions.FromDBError(err) + } + defer func() { + logger.DebugContext(ctx, "Finalizing transaction") + s.database.FinalizeTx(ctx, txn, err, serviceErr) + }() + + grantID, err := qrs.CreateGrant(ctx, database.CreateGrantParams{ + AccountID: opts.accountID, + GrantID: grantUUID, + GrantedClientID: opts.clientID, + GrantedScopes: grantedScopes, + GrantedCustomScopes: grantedCustomScopes, + }) + if err != nil { + logger.ErrorContext(ctx, "Failed to create grant", "error", err) + serviceErr = exceptions.FromDBError(err) + return serviceErr + } + + sessionID, err := qrs.CreateSession(ctx, database.CreateSessionParams{ + AccountID: opts.accountID, + GrantID: grantID, + SessionID: opts.sessionID, + SessionType: database.SessionTypeSliding, + SessionClientID: opts.clientID, + IpAddress: ipAddress, + UserAgent: userAgent, + ExpiresAt: expiresAt, + }) + if err != nil { + logger.ErrorContext(ctx, "Failed to create session", "error", err) + serviceErr = exceptions.FromDBError(err) + return serviceErr + } + + if accountCredentialsDTO.ID() > 0 { + var accountCredentialsID pgtype.Int4 + if err = accountCredentialsID.Scan(accountCredentialsDTO.ID()); err != nil { + logger.ErrorContext(ctx, "Failed to scan account credentials ID", "error", err) + return exceptions.NewInternalServerError() + } + + if err = qrs.CreateAccountSessionWithAccountCredentials(ctx, database.CreateAccountSessionWithAccountCredentialsParams{ + AccountID: opts.accountID, + AccountVersion: opts.accountVersion, + AccountCredentialsID: accountCredentialsID, + SessionID: sessionID, + SessionUuid: opts.sessionID, + }); err != nil { + logger.ErrorContext(ctx, "Failed to create account session with account credentials", "error", err) + serviceErr = exceptions.FromDBError(err) + return serviceErr + } + } else { + if err = qrs.CreateAccountSessionWithoutAccountCredentials(ctx, database.CreateAccountSessionWithoutAccountCredentialsParams{ + AccountID: opts.accountID, + AccountVersion: opts.accountVersion, + SessionID: sessionID, + SessionUuid: opts.sessionID, + }); err != nil { + logger.ErrorContext(ctx, "Failed to create account session without account credentials", "error", err) + serviceErr = exceptions.FromDBError(err) + return serviceErr + } + } + + if err = qrs.CreateSessionToken(ctx, database.CreateSessionTokenParams{ + SessionID: sessionID, + TokenID: opts.tokenID, + AccountID: opts.accountID, + GrantID: grantID, + ExpiresAt: expiresAt, + }); err != nil { + logger.ErrorContext(ctx, "Failed to create session token", "error", err) + serviceErr = exceptions.FromDBError(err) + return serviceErr + } + + logger.InfoContext(ctx, "Created account grant, session and token successfully") + return nil +} + +type createAccountSessionAndTokenOptions struct { + requestID string + accountID int32 + accountVersion int32 + accountPublicID uuid.UUID + scopes []tokens.AccountScope + sessionID uuid.UUID + tokenID uuid.UUID + clientID utils.Base62UUIDStr + ipAddress string + userAgent string + grantID int32 +} + +func (s *Services) createAccountSessionAndToken( + ctx context.Context, + opts createAccountSessionAndTokenOptions, +) *exceptions.ServiceError { + logger := s.buildLogger(opts.requestID, authLocation, "createAccountSessionAndToken").With( + "accountID", opts.accountID, + "accountVersion", opts.accountVersion, + "accountPublicID", opts.accountPublicID, + "sessionID", opts.sessionID, + "clientID", opts.clientID, + ) + logger.InfoContext(ctx, "Creating account session and token...") + + accountCredentialsDTO, serviceErr := s.GetAccountCredentialsByClientIDAndAccountPublicID(ctx, GetAccountCredentialsByClientIDAndAccountPublicIDOptions{ + RequestID: opts.requestID, + AccountPublicID: opts.accountPublicID, + ClientID: opts.clientID, + }) + if serviceErr != nil { + if serviceErr.Code != exceptions.CodeNotFound { + logger.ErrorContext(ctx, "Failed to get account credentials", "error", serviceErr) + return serviceErr + } + serviceErr = nil + } + + expiresAt := time.Now().Add(time.Duration(s.jwt.GetRefreshTTL()) * time.Second) + var ipAddress pgtype.Text + if err := ipAddress.Scan(opts.ipAddress); err != nil { + logger.ErrorContext(ctx, "Failed to scan IP address", "error", err) + return exceptions.NewInternalServerError() + } + + var userAgent pgtype.Text + if err := userAgent.Scan(opts.userAgent); err != nil { + logger.ErrorContext(ctx, "Failed to scan user agent", "error", err) + return exceptions.NewInternalServerError() + } + + qrs, txn, err := s.database.BeginTx(ctx) + if err != nil { + logger.ErrorContext(ctx, "Failed to start transaction", "error", err) + return exceptions.FromDBError(err) + } + defer func() { + logger.DebugContext(ctx, "Finalizing transaction") + s.database.FinalizeTx(ctx, txn, err, serviceErr) + }() + + sessionID, err := qrs.CreateSession(ctx, database.CreateSessionParams{ + AccountID: opts.accountID, + GrantID: opts.grantID, + SessionID: opts.sessionID, + SessionType: database.SessionTypeSliding, + SessionClientID: opts.clientID, + IpAddress: ipAddress, + UserAgent: userAgent, + ExpiresAt: expiresAt, + }) + if err != nil { + logger.ErrorContext(ctx, "Failed to create session", "error", err) + serviceErr = exceptions.FromDBError(err) + return serviceErr + } + + if accountCredentialsDTO.ID() > 0 { + var accountCredentialsID pgtype.Int4 + if err = accountCredentialsID.Scan(accountCredentialsDTO.ID()); err != nil { + logger.ErrorContext(ctx, "Failed to scan account credentials ID", "error", err) + return exceptions.NewInternalServerError() + } + + if err = qrs.CreateAccountSessionWithAccountCredentials(ctx, database.CreateAccountSessionWithAccountCredentialsParams{ + AccountID: opts.accountID, + AccountVersion: opts.accountVersion, + AccountCredentialsID: accountCredentialsID, + SessionID: sessionID, + SessionUuid: opts.sessionID, + }); err != nil { + logger.ErrorContext(ctx, "Failed to create account session with account credentials", "error", err) + serviceErr = exceptions.FromDBError(err) + return serviceErr + } + } else { + if err = qrs.CreateAccountSessionWithoutAccountCredentials(ctx, database.CreateAccountSessionWithoutAccountCredentialsParams{ + AccountID: opts.accountID, + AccountVersion: opts.accountVersion, + SessionID: sessionID, + SessionUuid: opts.sessionID, + }); err != nil { + logger.ErrorContext(ctx, "Failed to create account session without account credentials", "error", err) + serviceErr = exceptions.FromDBError(err) + return serviceErr + } + } + + if err = qrs.CreateSessionToken(ctx, database.CreateSessionTokenParams{ + SessionID: sessionID, + TokenID: opts.tokenID, + AccountID: opts.accountID, + GrantID: opts.grantID, + ExpiresAt: expiresAt, + }); err != nil { + logger.ErrorContext(ctx, "Failed to create session token", "error", err) + serviceErr = exceptions.FromDBError(err) + return serviceErr + } + + logger.InfoContext(ctx, "Created account session and token successfully") + return nil +} + +func (s *Services) upsertAccountGrantSessionAndToken( + ctx context.Context, + opts upsertAccountGrantSessionAndTokenOptions, +) *exceptions.ServiceError { + logger := s.buildLogger(opts.requestID, authLocation, "upsertAccountGrantSessionAndToken").With( + "accountID", opts.accountID, + "accountVersion", opts.accountVersion, + "accountPublicID", opts.accountPublicID, + "sessionID", opts.sessionID, + "clientID", opts.clientID, + ) + logger.InfoContext(ctx, "Upserting account grant session and token...") + + grant, err := s.database.FindAccountGrantByAccountIDAndGrantedClientID(ctx, database.FindAccountGrantByAccountIDAndGrantedClientIDParams{ + AccountID: opts.accountID, + GrantedClientID: opts.clientID, + }) + if err != nil { + serviceErr := exceptions.FromDBError(err) + if serviceErr.Code != exceptions.CodeNotFound { + logger.ErrorContext(ctx, "Failed to fetch grant", "error", err) + return serviceErr + } + + return s.createAccountGrantSessionAndToken(ctx, opts) + } + + session, err := s.database.FindAccountSessionByAccountIDAndSessionUUID(ctx, database.FindAccountSessionByAccountIDAndSessionUUIDParams{ + AccountID: opts.accountID, + SessionUuid: opts.sessionID, + }) + if err != nil { + serviceErr := exceptions.FromDBError(err) + if serviceErr.Code != exceptions.CodeNotFound { + logger.ErrorContext(ctx, "Failed to fetch account session", "error", err) + return serviceErr + } + + return s.createAccountSessionAndToken(ctx, createAccountSessionAndTokenOptions{ + requestID: opts.requestID, + accountID: opts.accountID, + accountVersion: opts.accountVersion, + accountPublicID: opts.accountPublicID, + scopes: opts.scopes, + sessionID: opts.sessionID, + tokenID: opts.tokenID, + clientID: opts.clientID, + ipAddress: opts.ipAddress, + userAgent: opts.userAgent, + grantID: grant.GrantID, + }) + } + + expiresAt := time.Now().Add(time.Duration(s.jwt.GetRefreshTTL()) * time.Second) + var serviceErr *exceptions.ServiceError + qrs, txn, err := s.database.BeginTx(ctx) + if err != nil { + logger.ErrorContext(ctx, "Failed to start transaction", "error", err) + return exceptions.FromDBError(err) + } + defer func() { + logger.DebugContext(ctx, "Finalizing transaction") + s.database.FinalizeTx(ctx, txn, err, serviceErr) + }() + + if err = qrs.UpdateSessionExpiresAt(ctx, database.UpdateSessionExpiresAtParams{ + ExpiresAt: expiresAt, + ID: session.SessionID, + }); err != nil { + logger.ErrorContext(ctx, "Failed to update session expires at", "error", err) + serviceErr = exceptions.FromDBError(err) + return serviceErr + } + + if err = qrs.CreateSessionToken(ctx, database.CreateSessionTokenParams{ + SessionID: session.SessionID, + TokenID: opts.tokenID, + AccountID: opts.accountID, + GrantID: grant.GrantID, + ExpiresAt: expiresAt, + }); err != nil { + logger.ErrorContext(ctx, "Failed to create session token", "error", err) + serviceErr = exceptions.FromDBError(err) + return serviceErr + } + + logger.InfoContext(ctx, "Upserted account grant session and token successfully") + return nil +} + +type generateFullAuthDTOOptions struct { + requestID string + accountID int32 + accountPublicID uuid.UUID + accountVersion int32 + sessionID uuid.UUID + ipAddress string + userAgent string + clientID utils.Base62UUIDStr + scopes []tokens.AccountScope +} + +func (s *Services) generateFullAuthDTO( + ctx context.Context, + opts generateFullAuthDTOOptions, ) (dtos.AuthDTO, *exceptions.ServiceError) { + logger := s.buildLogger(opts.requestID, authLocation, "generateFullAuthDTO").With( + "accountID", opts.accountID, + "accountPublicID", opts.accountPublicID, + "accountVersion", opts.accountVersion, + "sessionID", opts.sessionID, + "scopes", opts.scopes, + ) + logger.InfoContext(ctx, "Generating full auth DTO...") + accessToken, err := s.jwt.CreateAccessToken(tokens.AccountAccessTokenOptions{ - PublicID: accountDTO.PublicID, - Version: accountDTO.Version(), - Scopes: scopes, - TokenSubject: accountDTO.PublicID.String(), + PublicID: opts.accountPublicID, + Version: opts.accountVersion, + Scopes: opts.scopes, + TokenSubject: opts.accountPublicID.String(), }) if err != nil { logger.ErrorContext(ctx, "Failed to generate access token", "error", err) return dtos.AuthDTO{}, exceptions.NewInternalServerError() } + accessTTL := s.jwt.GetAccessTTL() signedAccessToken, serviceErr := s.crypto.SignToken(ctx, crypto.SignTokenOptions{ - RequestID: requestID, + RequestID: opts.requestID, Token: accessToken, GetJWKfn: s.BuildGetGlobalEncryptedJWKFn(ctx, BuildEncryptedJWKFnOptions{ - RequestID: requestID, + RequestID: opts.requestID, KeyType: database.TokenKeyTypeAccess, - TTL: s.jwt.GetAccessTTL(), - Queries: qrs, + TTL: accessTTL, }), GetDecryptDEKfn: s.BuildGetGlobalDecDEKFn(ctx, BuildGetGlobalDEKFnOptions{ - RequestID: requestID, - Queries: qrs, + RequestID: opts.requestID, }), GetEncryptDEKfn: s.BuildGetEncGlobalDEKFn(ctx, BuildGetGlobalDEKFnOptions{ - RequestID: requestID, - Queries: qrs, + RequestID: opts.requestID, }), StoreFN: s.BuildUpdateJWKDEKFn(ctx, BuildUpdateJWKDEKFnOptions{ - RequestID: requestID, - Queries: qrs, + RequestID: opts.requestID, }), }) if serviceErr != nil { @@ -244,36 +659,33 @@ func (s *Services) GenerateFullAuthDTO( return dtos.AuthDTO{}, exceptions.NewInternalServerError() } - refreshToken, err := s.jwt.CreateRefreshToken(tokens.AccountRefreshTokenOptions{ - PublicID: accountDTO.PublicID, - Version: accountDTO.Version(), - Scopes: scopes, + refreshToken, refreshJTI, err := s.jwt.CreateRefreshToken(tokens.AccountRefreshTokenOptions{ + PublicID: opts.accountPublicID, + Version: opts.accountVersion, + Scopes: opts.scopes, }) if err != nil { logger.ErrorContext(ctx, "Failed to generate refresh token", "error", err) return dtos.AuthDTO{}, exceptions.NewInternalServerError() } + refreshTTL := s.jwt.GetRefreshTTL() signedRefreshToken, serviceErr := s.crypto.SignToken(ctx, crypto.SignTokenOptions{ - RequestID: requestID, + RequestID: opts.requestID, Token: refreshToken, GetJWKfn: s.BuildGetGlobalEncryptedJWKFn(ctx, BuildEncryptedJWKFnOptions{ - RequestID: requestID, + RequestID: opts.requestID, KeyType: database.TokenKeyTypeRefresh, - TTL: s.jwt.GetRefreshTTL(), - Queries: qrs, + TTL: refreshTTL, }), GetDecryptDEKfn: s.BuildGetGlobalDecDEKFn(ctx, BuildGetGlobalDEKFnOptions{ - RequestID: requestID, - Queries: qrs, + RequestID: opts.requestID, }), GetEncryptDEKfn: s.BuildGetEncGlobalDEKFn(ctx, BuildGetGlobalDEKFnOptions{ - RequestID: requestID, - Queries: qrs, + RequestID: opts.requestID, }), StoreFN: s.BuildUpdateJWKDEKFn(ctx, BuildUpdateJWKDEKFnOptions{ - RequestID: requestID, - Queries: qrs, + RequestID: opts.requestID, }), }) if serviceErr != nil { @@ -281,13 +693,31 @@ func (s *Services) GenerateFullAuthDTO( return dtos.AuthDTO{}, exceptions.NewInternalServerError() } - logger.InfoContext(ctx, logSuccessMessage) - return dtos.NewFullAuthDTO(signedAccessToken, signedRefreshToken, s.jwt.GetAccessTTL()), nil + if serviceErr := s.upsertAccountGrantSessionAndToken(ctx, upsertAccountGrantSessionAndTokenOptions{ + requestID: opts.requestID, + accountID: opts.accountID, + accountVersion: opts.accountVersion, + accountPublicID: opts.accountPublicID, + scopes: opts.scopes, + sessionID: opts.sessionID, + tokenID: refreshJTI, + clientID: opts.clientID, + ipAddress: opts.ipAddress, + userAgent: opts.userAgent, + }); serviceErr != nil { + logger.ErrorContext(ctx, "Failed to upsert account grant session and token", "serviceError", serviceErr) + return dtos.AuthDTO{}, serviceErr + } + + logger.InfoContext(ctx, "Generated full auth DTO successfully") + return dtos.NewFullAuthDTO(signedAccessToken, signedRefreshToken, accessTTL), nil } type ConfirmAccountOptions struct { RequestID string ConfirmationToken string + IPAddress string + UserAgent string } func (s *Services) ConfirmAccount( @@ -333,14 +763,25 @@ func (s *Services) ConfirmAccount( return dtos.AuthDTO{}, exceptions.NewForbiddenError() } - return s.GenerateFullAuthDTO( + sessionID, err := uuid.NewV7() + if err != nil { + logger.ErrorContext(ctx, "Failed to generate session ID", "error", err) + return dtos.AuthDTO{}, exceptions.NewInternalServerError() + } + + return s.generateFullAuthDTO( ctx, - logger, - s.database.Queries, - opts.RequestID, - &accountDTO, - []tokens.AccountScope{tokens.AccountScopeAdmin}, - "Confirmed Account successfully", + generateFullAuthDTOOptions{ + requestID: opts.RequestID, + accountID: accountDTO.ID(), + accountPublicID: accountDTO.PublicID, + accountVersion: accountDTO.Version(), + sessionID: sessionID, + scopes: []tokens.AccountScope{tokens.AccountScopeAdmin}, + clientID: utils.NilBase62UUID, + ipAddress: opts.IPAddress, + userAgent: opts.UserAgent, + }, ) } @@ -412,6 +853,8 @@ type LoginAccountOptions struct { RequestID string Email string Password string + IPAddress string + UserAgent string } func (s *Services) LoginAccount( @@ -491,14 +934,25 @@ func (s *Services) LoginAccount( return authDTO, nil } - return s.GenerateFullAuthDTO( + sessionID, err := uuid.NewV7() + if err != nil { + logger.ErrorContext(ctx, "Failed to generate session ID", "error", err) + return dtos.AuthDTO{}, exceptions.NewInternalServerError() + } + + return s.generateFullAuthDTO( ctx, - logger, - s.database.Queries, - opts.RequestID, - &accountDTO, - []tokens.AccountScope{tokens.AccountScopeAdmin}, - "Logged in account successfully", + generateFullAuthDTOOptions{ + requestID: opts.RequestID, + accountID: accountDTO.ID(), + accountPublicID: accountDTO.PublicID, + accountVersion: accountDTO.Version(), + sessionID: sessionID, + scopes: []tokens.AccountScope{tokens.AccountScopeAdmin}, + clientID: utils.NilBase62UUID, + ipAddress: opts.IPAddress, + userAgent: opts.UserAgent, + }, ) } @@ -699,6 +1153,8 @@ type VerifyAccount2FAOptions struct { AccountVersion int32 TwoFAType tokens.TwoFAType Code string + IPAddress string + UserAgent string } func (s *Services) VerifyAccount2FA( @@ -733,14 +1189,25 @@ func (s *Services) VerifyAccount2FA( return dtos.AuthDTO{}, serviceErr } - return s.GenerateFullAuthDTO( + sessionID, err := uuid.NewV7() + if err != nil { + logger.ErrorContext(ctx, "Failed to generate session ID", "error", err) + return dtos.AuthDTO{}, exceptions.NewInternalServerError() + } + + return s.generateFullAuthDTO( ctx, - logger, - s.database.Queries, - opts.RequestID, - &accountDTO, - []tokens.AccountScope{tokens.AccountScopeAdmin}, - "2FA Logged in successfully", + generateFullAuthDTOOptions{ + requestID: opts.RequestID, + accountID: accountDTO.ID(), + accountPublicID: accountDTO.PublicID, + accountVersion: accountDTO.Version(), + sessionID: sessionID, + scopes: []tokens.AccountScope{tokens.AccountScopeAdmin}, + clientID: utils.NilBase62UUID, + ipAddress: opts.IPAddress, + userAgent: opts.UserAgent, + }, ) } @@ -786,27 +1253,43 @@ func (s *Services) LogoutAccount( return exceptions.NewUnauthorizedError() } - blt, err := s.database.GetRevokedToken(ctx, data.TokenID) + sessionToken, err := s.database.FindSessionTokenByTokenID(ctx, data.TokenID) if err != nil { - if exceptions.FromDBError(err).Code != exceptions.CodeNotFound { - logger.ErrorContext(ctx, "Failed to fetch revoked token", "error", err) + serviceErr = exceptions.FromDBError(err) + if serviceErr.Code != exceptions.CodeNotFound { + logger.ErrorContext(ctx, "Failed to fetch session token", "error", err) return exceptions.NewInternalServerError() } - } else { - logger.WarnContext(ctx, "Token is revoked", "revokedAt", blt.CreatedAt) + + logger.WarnContext(ctx, "Session token was not found in the DB, it is probably revoked") + return exceptions.NewUnauthorizedError() + } + if sessionToken.ExpiresAt.Before(time.Now()) { + logger.WarnContext(ctx, "Session token is expired") return exceptions.NewUnauthorizedError() } - if err := s.database.RevokeToken(ctx, database.RevokeTokenParams{ - TokenID: data.TokenID, - AccountID: accountDTO.ID(), - Owner: database.TokenOwnerAccount, - OwnerPublicID: accountDTO.PublicID, - ExpiresAt: data.ExpiresAt, - IssuedAt: data.IssuedAt, - }); err != nil { - logger.ErrorContext(ctx, "Failed to revoke the token", "error", err) - return exceptions.NewInternalServerError() + accountSession, err := s.database.FindAccountSessionByAccountIDAndSessionUUID( + ctx, + database.FindAccountSessionByAccountIDAndSessionUUIDParams{ + AccountID: accountDTO.ID(), + SessionUuid: sessionToken.SessionUuid, + }, + ) + if err != nil { + serviceErr = exceptions.FromDBError(err) + if serviceErr.Code != exceptions.CodeNotFound { + logger.ErrorContext(ctx, "Failed to fetch account session", "error", err) + return exceptions.NewInternalServerError() + } + + logger.WarnContext(ctx, "Account session was not found in the DB") + return exceptions.NewUnauthorizedError() + } + + if err := s.database.DeleteSessionByID(ctx, accountSession.SessionID); err != nil { + logger.ErrorContext(ctx, "Failed to delete session", "error", err) + return exceptions.FromDBError(err) } logger.InfoContext(ctx, "Logged out account successfully") @@ -816,6 +1299,8 @@ func (s *Services) LogoutAccount( type RefreshTokenAccountOptions struct { RequestID string RefreshToken string + IPAddress string + UserAgent string } func (s *Services) RefreshTokenAccount( @@ -837,14 +1322,24 @@ func (s *Services) RefreshTokenAccount( return dtos.AuthDTO{}, exceptions.NewUnauthorizedError() } - blt, err := s.database.GetRevokedToken(ctx, data.TokenID) + sessionToken, err := s.database.FindSessionTokenByTokenID(ctx, data.TokenID) if err != nil { - if exceptions.FromDBError(err).Code != exceptions.CodeNotFound { - logger.ErrorContext(ctx, "Failed to get blacklisted token", "error", err) + serviceErr := exceptions.FromDBError(err) + if serviceErr.Code != exceptions.CodeNotFound { + logger.ErrorContext(ctx, "Failed to fetch session token", "error", err) return dtos.AuthDTO{}, exceptions.NewInternalServerError() } - } else { - logger.WarnContext(ctx, "Token is revoked", "revokedAt", blt.CreatedAt) + + logger.WarnContext(ctx, "Token was not found in the DB, it is probably revoked") + return dtos.AuthDTO{}, exceptions.NewUnauthorizedError() + } + if sessionToken.ExpiresAt.Before(time.Now()) { + if err := s.database.DeleteSessionToken(ctx, data.TokenID); err != nil { + logger.ErrorContext(ctx, "Failed to delete session token", "error", err) + return dtos.AuthDTO{}, exceptions.NewInternalServerError() + } + + logger.WarnContext(ctx, "Session token is expired") return dtos.AuthDTO{}, exceptions.NewUnauthorizedError() } @@ -858,26 +1353,30 @@ func (s *Services) RefreshTokenAccount( return dtos.AuthDTO{}, serviceErr } - if err := s.database.RevokeToken(ctx, database.RevokeTokenParams{ - TokenID: data.TokenID, - AccountID: accountDTO.ID(), - Owner: database.TokenOwnerAccount, - OwnerPublicID: accountDTO.PublicID, - ExpiresAt: data.ExpiresAt, - IssuedAt: data.IssuedAt, - }); err != nil { - logger.ErrorContext(ctx, "Failed to blacklist previous refresh token", "error", err) + if err := s.database.DeleteSessionToken(ctx, data.TokenID); err != nil { + logger.ErrorContext(ctx, "Failed to delete session token", "error", err) return dtos.AuthDTO{}, exceptions.NewInternalServerError() } - return s.GenerateFullAuthDTO( + sessionID, err := uuid.NewV7() + if err != nil { + logger.ErrorContext(ctx, "Failed to generate session ID", "error", err) + return dtos.AuthDTO{}, exceptions.NewInternalServerError() + } + + return s.generateFullAuthDTO( ctx, - logger, - s.database.Queries, - opts.RequestID, - &accountDTO, - data.Scopes, - "Refreshed access token successfully", + generateFullAuthDTOOptions{ + requestID: opts.RequestID, + accountID: accountDTO.ID(), + accountPublicID: accountDTO.PublicID, + accountVersion: accountDTO.Version(), + sessionID: sessionID, + scopes: data.Scopes, + clientID: utils.NilBase62UUID, + ipAddress: opts.IPAddress, + userAgent: opts.UserAgent, + }, ) } diff --git a/idp/internal/services/dtos/app.go b/idp/internal/services/dtos/app.go index f6fa7ed..6adf43f 100644 --- a/idp/internal/services/dtos/app.go +++ b/idp/internal/services/dtos/app.go @@ -65,9 +65,10 @@ type AppDTO struct { RedirectURIs []string `json:"redirect_uris,omitempty"` ResponseTypes []database.ResponseType `json:"response_types,omitempty"` - IDTokenTTL int32 `json:"id_token_ttl"` - TokenTTL int32 `json:"token_ttl"` - RefreshTokenTTL int32 `json:"refresh_token_ttl,omitempty"` + AccessTokenTTL int32 `json:"access_token_ttl"` + IDTokenTTL int32 `json:"id_token_ttl,omitempty"` + RefreshTokenIdleTTL int32 `json:"refresh_token_idle_ttl,omitempty"` + RefreshTokenTTL int32 `json:"refresh_token_ttl,omitempty"` ClientSecretID string `json:"client_secret_id,omitempty"` ClientSecret string `json:"client_secret,omitempty"` @@ -150,9 +151,10 @@ func MapAppToDTO(app *database.App) AppDTO { AuthProviders: app.AuthProviders, RedirectURIs: app.RedirectUris, ResponseTypes: app.ResponseTypes, - IDTokenTTL: app.IDTokenTtl, - TokenTTL: app.TokenTtl, - RefreshTokenTTL: app.RefreshTokenTtl, + AccessTokenTTL: app.AccessTokenTtl, + IDTokenTTL: app.IDTokenTtl.Int32, + RefreshTokenIdleTTL: app.RefreshTokenIdleTtl.Int32, + RefreshTokenTTL: app.RefreshTokenTtl.Int32, } } @@ -181,9 +183,10 @@ func MapWebNativeSPAMCPAppToDTO(app *database.App) AppDTO { AuthProviders: app.AuthProviders, RedirectURIs: app.RedirectUris, ResponseTypes: app.ResponseTypes, - IDTokenTTL: app.IDTokenTtl, - TokenTTL: app.TokenTtl, - RefreshTokenTTL: app.RefreshTokenTtl, + AccessTokenTTL: app.AccessTokenTtl, + IDTokenTTL: app.IDTokenTtl.Int32, + RefreshTokenIdleTTL: app.RefreshTokenIdleTtl.Int32, + RefreshTokenTTL: app.RefreshTokenTtl.Int32, } } @@ -217,9 +220,10 @@ func MapWebAppWithSecretToDTO( AuthProviders: app.AuthProviders, RedirectURIs: app.RedirectUris, ResponseTypes: app.ResponseTypes, - IDTokenTTL: app.IDTokenTtl, - TokenTTL: app.TokenTtl, - RefreshTokenTTL: app.RefreshTokenTtl, + AccessTokenTTL: app.AccessTokenTtl, + IDTokenTTL: app.IDTokenTtl.Int32, + RefreshTokenIdleTTL: app.RefreshTokenIdleTtl.Int32, + RefreshTokenTTL: app.RefreshTokenTtl.Int32, ClientSecretID: secretID, ClientSecret: fmt.Sprintf("%s.%s", secretID, secret), ClientSecretExp: expiresAt.Unix(), @@ -251,9 +255,10 @@ func MapWebAppWithJWKToDTO(app *database.App, jwk utils.JWK, exp time.Time) AppD AuthProviders: app.AuthProviders, RedirectURIs: app.RedirectUris, ResponseTypes: app.ResponseTypes, - IDTokenTTL: app.IDTokenTtl, - TokenTTL: app.TokenTtl, - RefreshTokenTTL: app.RefreshTokenTtl, + AccessTokenTTL: app.AccessTokenTtl, + IDTokenTTL: app.IDTokenTtl.Int32, + RefreshTokenIdleTTL: app.RefreshTokenIdleTtl.Int32, + RefreshTokenTTL: app.RefreshTokenTtl.Int32, ClientSecretID: jwk.GetKeyID(), ClientSecretJWK: jwk, ClientSecretExp: exp.Unix(), @@ -283,9 +288,10 @@ func MapBackendAppWithJWKToDTO(app *database.App, jwk utils.JWK, exp time.Time) Scopes: mapScopes(app.DefaultScopes, app.DefaultCustomScopes), UsernameColumn: app.UsernameColumn, AuthProviders: app.AuthProviders, - IDTokenTTL: app.IDTokenTtl, - TokenTTL: app.TokenTtl, - RefreshTokenTTL: app.RefreshTokenTtl, + AccessTokenTTL: app.AccessTokenTtl, + IDTokenTTL: app.IDTokenTtl.Int32, + RefreshTokenIdleTTL: app.RefreshTokenIdleTtl.Int32, + RefreshTokenTTL: app.RefreshTokenTtl.Int32, ClientSecretID: jwk.GetKeyID(), ClientSecretJWK: jwk, ClientSecretExp: exp.Unix(), @@ -315,9 +321,10 @@ func MapBackendAppWithSecretToDTO(app *database.App, secretID string, secret str Scopes: mapScopes(app.DefaultScopes, app.DefaultCustomScopes), UsernameColumn: app.UsernameColumn, AuthProviders: app.AuthProviders, - IDTokenTTL: app.IDTokenTtl, - TokenTTL: app.TokenTtl, - RefreshTokenTTL: app.RefreshTokenTtl, + AccessTokenTTL: app.AccessTokenTtl, + IDTokenTTL: app.IDTokenTtl.Int32, + RefreshTokenIdleTTL: app.RefreshTokenIdleTtl.Int32, + RefreshTokenTTL: app.RefreshTokenTtl.Int32, ClientSecretID: secretID, ClientSecret: fmt.Sprintf("%s.%s", secretID, secret), ClientSecretExp: expiresAt.Unix(), @@ -347,9 +354,10 @@ func MapDeviceAppToDTO(app *database.App, relatedApps []database.App, backendDom Scopes: mapScopes(app.Scopes, app.CustomScopes), UsernameColumn: app.UsernameColumn, AuthProviders: app.AuthProviders, - IDTokenTTL: app.IDTokenTtl, - TokenTTL: app.TokenTtl, - RefreshTokenTTL: app.RefreshTokenTtl, + AccessTokenTTL: app.AccessTokenTtl, + IDTokenTTL: app.IDTokenTtl.Int32, + RefreshTokenIdleTTL: app.RefreshTokenIdleTtl.Int32, + RefreshTokenTTL: app.RefreshTokenTtl.Int32, RelatedApps: utils.MapSlice(relatedApps, func(ra *database.App) RelatedAppDTO { return newRelatedAppDTO(ra, backendDomain, paths.AppsBase) }), @@ -384,9 +392,10 @@ func MapServiceAppWithJWKToDTO( Scopes: mapScopes(app.Scopes, app.CustomScopes), UsernameColumn: app.UsernameColumn, AuthProviders: app.AuthProviders, - IDTokenTTL: app.IDTokenTtl, - TokenTTL: app.TokenTtl, - RefreshTokenTTL: app.RefreshTokenTtl, + AccessTokenTTL: app.AccessTokenTtl, + IDTokenTTL: app.IDTokenTtl.Int32, + RefreshTokenIdleTTL: app.RefreshTokenIdleTtl.Int32, + RefreshTokenTTL: app.RefreshTokenTtl.Int32, ClientSecretID: jwk.GetKeyID(), ClientSecretJWK: jwk, ClientSecretExp: exp.Unix(), @@ -425,9 +434,10 @@ func MapServiceAppWithSecretToDTO( Scopes: mapScopes(app.Scopes, app.CustomScopes), UsernameColumn: app.UsernameColumn, AuthProviders: app.AuthProviders, - IDTokenTTL: app.IDTokenTtl, - TokenTTL: app.TokenTtl, - RefreshTokenTTL: app.RefreshTokenTtl, + AccessTokenTTL: app.AccessTokenTtl, + IDTokenTTL: app.IDTokenTtl.Int32, + RefreshTokenIdleTTL: app.RefreshTokenIdleTtl.Int32, + RefreshTokenTTL: app.RefreshTokenTtl.Int32, ClientSecretID: secretID, ClientSecret: fmt.Sprintf("%s.%s", secretID, secret), ClientSecretExp: expiresAt.Unix(), @@ -460,9 +470,10 @@ func MapBackendAppToDTO(app *database.App) AppDTO { Scopes: mapScopes(app.DefaultScopes, app.DefaultCustomScopes), UsernameColumn: app.UsernameColumn, AuthProviders: app.AuthProviders, - IDTokenTTL: app.IDTokenTtl, - TokenTTL: app.TokenTtl, - RefreshTokenTTL: app.RefreshTokenTtl, + AccessTokenTTL: app.AccessTokenTtl, + IDTokenTTL: app.IDTokenTtl.Int32, + RefreshTokenIdleTTL: app.RefreshTokenIdleTtl.Int32, + RefreshTokenTTL: app.RefreshTokenTtl.Int32, } } @@ -492,9 +503,10 @@ func MapServiceAppToDTO( Scopes: mapScopes(app.Scopes, app.CustomScopes), UsernameColumn: app.UsernameColumn, AuthProviders: app.AuthProviders, - IDTokenTTL: app.IDTokenTtl, - TokenTTL: app.TokenTtl, - RefreshTokenTTL: app.RefreshTokenTtl, + AccessTokenTTL: app.AccessTokenTtl, + IDTokenTTL: app.IDTokenTtl.Int32, + RefreshTokenIdleTTL: app.RefreshTokenIdleTtl.Int32, + RefreshTokenTTL: app.RefreshTokenTtl.Int32, AllowedDomains: serviceCfg.AllowedDomains, UsersAuthMethod: serviceCfg.UserAuthMethod, UsersGrantTypes: serviceCfg.UserGrantTypes, @@ -524,9 +536,10 @@ func MapMCPAppWithJWKToDTO(app *database.App, jwk utils.JWK, exp time.Time) AppD Scopes: mapScopes(app.Scopes, app.CustomScopes), UsernameColumn: app.UsernameColumn, AuthProviders: app.AuthProviders, - IDTokenTTL: app.IDTokenTtl, - TokenTTL: app.TokenTtl, - RefreshTokenTTL: app.RefreshTokenTtl, + AccessTokenTTL: app.AccessTokenTtl, + IDTokenTTL: app.IDTokenTtl.Int32, + RefreshTokenIdleTTL: app.RefreshTokenIdleTtl.Int32, + RefreshTokenTTL: app.RefreshTokenTtl.Int32, ClientSecretID: jwk.GetKeyID(), ClientSecretExp: exp.Unix(), } @@ -560,9 +573,10 @@ func MapMCPAppWithSecretToDTO( Scopes: mapScopes(app.Scopes, app.CustomScopes), UsernameColumn: app.UsernameColumn, AuthProviders: app.AuthProviders, - IDTokenTTL: app.IDTokenTtl, - TokenTTL: app.TokenTtl, - RefreshTokenTTL: app.RefreshTokenTtl, + AccessTokenTTL: app.AccessTokenTtl, + IDTokenTTL: app.IDTokenTtl.Int32, + RefreshTokenIdleTTL: app.RefreshTokenIdleTtl.Int32, + RefreshTokenTTL: app.RefreshTokenTtl.Int32, ClientSecretID: secretID, ClientSecret: fmt.Sprintf("%s.%s", secretID, secret), ClientSecretExp: expiresAt.Unix(), diff --git a/idp/internal/services/oauth.go b/idp/internal/services/oauth.go index d5badef..b9cdb34 100644 --- a/idp/internal/services/oauth.go +++ b/idp/internal/services/oauth.go @@ -415,6 +415,8 @@ type OAuthLoginAccountOptions struct { Provider string Code string ChallengeVerifier string + IPAddress string + UserAgent string } func (s *Services) OAuthLoginAccount( @@ -466,14 +468,25 @@ func (s *Services) OAuthLoginAccount( return dtos.AuthDTO{}, serviceErr } - return s.GenerateFullAuthDTO( + sessionID, err := uuid.NewV7() + if err != nil { + logger.ErrorContext(ctx, "Failed to generate session ID", "error", err) + return dtos.AuthDTO{}, exceptions.NewInternalServerError() + } + + return s.generateFullAuthDTO( ctx, - logger, - s.database.Queries, - opts.RequestID, - &accountDTO, - []tokens.AccountScope{tokens.AccountScopeAdmin}, - "OAuth logged in successfully", + generateFullAuthDTOOptions{ + requestID: opts.RequestID, + accountID: accountDTO.ID(), + accountPublicID: accountDTO.PublicID, + accountVersion: accountDTO.Version(), + sessionID: sessionID, + scopes: []tokens.AccountScope{tokens.AccountScopeAdmin}, + clientID: utils.NilBase62UUID, + ipAddress: opts.IPAddress, + userAgent: opts.UserAgent, + }, ) } diff --git a/idp/internal/services/oauth_dynamic_registration.go b/idp/internal/services/oauth_dynamic_registration.go index 29df0c4..7ec5930 100644 --- a/idp/internal/services/oauth_dynamic_registration.go +++ b/idp/internal/services/oauth_dynamic_registration.go @@ -9,7 +9,6 @@ package services import ( "context" "net/url" - "slices" "github.com/google/uuid" @@ -229,7 +228,6 @@ type oauthDynamicRegistrationIATAuthOptions struct { domain string redirectURI string state string - hostUsername string } func (s *Services) oauthDynamicRegistrationIATAuth( @@ -243,7 +241,6 @@ func (s *Services) oauthDynamicRegistrationIATAuth( ).With( "domain", opts.domain, "redirectUri", opts.redirectURI, - "hostUsername", opts.hostUsername, ) logger.InfoContext(ctx, "Handling OAuth dynamic registration IAT auth...") @@ -291,146 +288,9 @@ func (s *Services) oauthDynamicRegistrationIATAuth( }), nil } -type refreshTokenOAuthDynamicRegistrationIATLoginOptions struct { - hostUsername string - requestID string - refreshToken string - challenge string - challengeMethod string - domain string - redirectURI string - state string - backendDomain string -} - -func (s *Services) refreshTokenOAuthDynamicRegistrationIATLogin( - ctx context.Context, - opts refreshTokenOAuthDynamicRegistrationIATLoginOptions, -) (string, *exceptions.ServiceError) { - logger := s.buildLogger( - opts.requestID, - oauthDynamicRegistrationLocation, - "refreshTokenOAuthDynamicRegistrationIATLogin", - ).With( - "domain", opts.domain, - "redirectUri", opts.redirectURI, - ) - logger.InfoContext(ctx, "Refreshing OAuth dynamic registration IAT callback...") - - data, err := s.jwt.VerifyRefreshToken( - opts.refreshToken, - s.BuildGetGlobalPublicKeyFn(ctx, BuildGetGlobalVerifyKeyFnOptions{ - RequestID: opts.requestID, - KeyType: database.TokenKeyTypeRefresh, - }), - ) - if err != nil { - logger.WarnContext(ctx, "Invalid refresh token", "error", err) - return buildOAuthDynamicRegistrationIATLoginURL(buildOAuthDynamicRegistrationIATLoginURLOptions{ - domain: opts.domain, - state: opts.state, - challenge: opts.challenge, - challengeMethod: opts.challengeMethod, - redirectURI: opts.redirectURI, - }), nil - } - - if !slices.ContainsFunc(data.Scopes, func(s string) bool { - return s == tokens.AccountScopeAdmin || s == tokens.AccountScopeCredentialsWrite - }) { - logger.WarnContext(ctx, "Refresh token missing offline_access scope") - return buildOAuthDynamicRegistrationIATLoginURL(buildOAuthDynamicRegistrationIATLoginURLOptions{ - domain: opts.domain, - state: opts.state, - challenge: opts.challenge, - challengeMethod: opts.challengeMethod, - redirectURI: opts.redirectURI, - }), nil - } - - blt, err := s.database.GetRevokedToken(ctx, data.TokenID) - if err != nil { - if exceptions.FromDBError(err).Code != exceptions.CodeNotFound { - logger.ErrorContext(ctx, "Failed to get blacklisted token", "error", err) - return "", exceptions.NewInternalServerError() - } - } else { - logger.WarnContext(ctx, "Token is revoked", "revokedAt", blt.CreatedAt) - return buildOAuthDynamicRegistrationIATLoginURL(buildOAuthDynamicRegistrationIATLoginURLOptions{ - domain: opts.domain, - state: opts.state, - challenge: opts.challenge, - challengeMethod: opts.challengeMethod, - redirectURI: opts.redirectURI, - }), nil - } - - accountDTO, serviceErr := s.GetAccountByPublicIDAndVersion(ctx, GetAccountByPublicIDAndVersionOptions{ - RequestID: opts.requestID, - PublicID: data.AccountClaims.AccountID, - Version: data.AccountClaims.AccountVersion, - }) - if serviceErr != nil { - if serviceErr.Code != exceptions.CodeNotFound && serviceErr.Code != exceptions.CodeUnauthorized { - logger.ErrorContext(ctx, "Failed to get account by public ID and version", "serviceError", serviceErr) - return "", serviceErr - } - - 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, - domain: opts.domain, - redirectURI: opts.redirectURI, - state: opts.state, - }) - } - - hashedChallenge, serviceErr := hashChallenge(opts.challenge, opts.challengeMethod) - if serviceErr != nil { - logger.ErrorContext(ctx, "Invalid code challenge", "serviceError", serviceErr) - return "", serviceErr - } - - cbURL, serviceErr := s.generateOAuthDynamicRegistrationIATCallback( - ctx, - generateOAuthDynamicRegistrationIATCallbackOptions{ - hostUsername: opts.hostUsername, - requestID: opts.requestID, - clientID: utils.Base62UUID(), - accountPublicID: accountDTO.PublicID, - accountVersion: accountDTO.Version(), - challenge: hashedChallenge, - domain: opts.domain, - redirectURI: opts.redirectURI, - state: opts.state, - backendDomain: opts.backendDomain, - }, - ) - if serviceErr != nil { - logger.ErrorContext(ctx, "Failed to generate OAuth dynamic registration IAT callback", "serviceErr", serviceErr) - return "", serviceErr - } - - return cbURL, nil -} - type InitiateOAuthDynamicRegistrationIATAuthOptions struct { RequestID string + Origin string Domain string State string SessionKey string @@ -455,6 +315,21 @@ func (s *Services) InitiateOAuthDynamicRegistrationIATAuth( ) logger.InfoContext(ctx, "Starting OAuth dynamic registration IAT authorization...") + if opts.Origin == "" { + logger.WarnContext(ctx, "Origin header is missing") + return "", exceptions.NewUnauthorizedError() + } + + parsedOrigin, err := url.Parse(opts.Origin) + if err != nil { + logger.WarnContext(ctx, "Invalid origin header", "error", err) + return "", exceptions.NewUnauthorizedError() + } + if parsedOrigin.Host != opts.Domain { + logger.WarnContext(ctx, "Origin header does not match domain", "originHost", parsedOrigin.Host) + return "", exceptions.NewUnauthorizedError() + } + if opts.SessionKey == "" { if opts.RefreshToken == "" { logger.InfoContext(ctx, "No session key or refresh token provided, redirecting to login") diff --git a/idp/internal/services/oauth_dynamic_registration_accounts.go b/idp/internal/services/oauth_dynamic_registration_accounts.go new file mode 100644 index 0000000..5a41b61 --- /dev/null +++ b/idp/internal/services/oauth_dynamic_registration_accounts.go @@ -0,0 +1,212 @@ +// 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 services + +import ( + "context" + "slices" + + "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/providers/tokens" + "github.com/tugascript/devlogs/idp/internal/utils" +) + +const oauthDynamicRegistrationAccountsLocation string = "oauth_dynamic_registration_accounts" + +type refreshTokenOAuthDynamicRegistrationIATLoginOptions struct { + requestID string + refreshToken string + challenge string + challengeMethod string + domain string + redirectURI string + state string + backendDomain string +} + +func (s *Services) refreshTokenOAuthDynamicRegistrationIATLogin( + ctx context.Context, + opts refreshTokenOAuthDynamicRegistrationIATLoginOptions, +) (string, *exceptions.ServiceError) { + logger := s.buildLogger( + opts.requestID, + oauthDynamicRegistrationLocation, + "refreshTokenOAuthDynamicRegistrationIATLogin", + ).With( + "domain", opts.domain, + "redirectUri", opts.redirectURI, + ) + logger.InfoContext(ctx, "Refreshing OAuth dynamic registration IAT callback...") + + data, err := s.jwt.VerifyRefreshToken( + opts.refreshToken, + s.BuildGetGlobalPublicKeyFn(ctx, BuildGetGlobalVerifyKeyFnOptions{ + RequestID: opts.requestID, + KeyType: database.TokenKeyTypeRefresh, + }), + ) + if err != nil { + logger.WarnContext(ctx, "Invalid refresh token", "error", err) + return buildOAuthDynamicRegistrationIATLoginURL(buildOAuthDynamicRegistrationIATLoginURLOptions{ + domain: opts.domain, + state: opts.state, + challenge: opts.challenge, + challengeMethod: opts.challengeMethod, + redirectURI: opts.redirectURI, + }), nil + } + + if !slices.ContainsFunc(data.Scopes, func(s string) bool { + return s == tokens.AccountScopeAdmin || s == tokens.AccountScopeCredentialsWrite + }) { + logger.WarnContext(ctx, "Refresh token missing offline_access scope") + return buildOAuthDynamicRegistrationIATLoginURL(buildOAuthDynamicRegistrationIATLoginURLOptions{ + domain: opts.domain, + state: opts.state, + challenge: opts.challenge, + challengeMethod: opts.challengeMethod, + redirectURI: opts.redirectURI, + }), nil + } + + blt, err := s.database.GetRevokedToken(ctx, data.TokenID) + if err != nil { + if exceptions.FromDBError(err).Code != exceptions.CodeNotFound { + logger.ErrorContext(ctx, "Failed to get blacklisted token", "error", err) + return "", exceptions.NewInternalServerError() + } + } else { + logger.WarnContext(ctx, "Token is revoked", "revokedAt", blt.CreatedAt) + return buildOAuthDynamicRegistrationIATLoginURL(buildOAuthDynamicRegistrationIATLoginURLOptions{ + domain: opts.domain, + state: opts.state, + challenge: opts.challenge, + challengeMethod: opts.challengeMethod, + redirectURI: opts.redirectURI, + }), nil + } + + accountDTO, serviceErr := s.GetAccountByPublicIDAndVersion(ctx, GetAccountByPublicIDAndVersionOptions{ + RequestID: opts.requestID, + PublicID: data.AccountClaims.AccountID, + Version: data.AccountClaims.AccountVersion, + }) + if serviceErr != nil { + if serviceErr.Code != exceptions.CodeNotFound && serviceErr.Code != exceptions.CodeUnauthorized { + logger.ErrorContext(ctx, "Failed to get account by public ID and version", "serviceError", serviceErr) + return "", serviceErr + } + + 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, + domain: opts.domain, + redirectURI: opts.redirectURI, + state: opts.state, + }) + } + + hashedChallenge, serviceErr := hashChallenge(opts.challenge, opts.challengeMethod) + if serviceErr != nil { + logger.ErrorContext(ctx, "Invalid code challenge", "serviceError", serviceErr) + return "", serviceErr + } + + cbURL, serviceErr := s.generateOAuthDynamicRegistrationIATCallback( + ctx, + generateOAuthDynamicRegistrationIATCallbackOptions{ + hostUsername: opts.hostUsername, + requestID: opts.requestID, + clientID: utils.Base62UUID(), + accountPublicID: accountDTO.PublicID, + accountVersion: accountDTO.Version(), + challenge: hashedChallenge, + domain: opts.domain, + redirectURI: opts.redirectURI, + state: opts.state, + backendDomain: opts.backendDomain, + }, + ) + if serviceErr != nil { + logger.ErrorContext(ctx, "Failed to generate OAuth dynamic registration IAT callback", "serviceErr", serviceErr) + return "", serviceErr + } + + return cbURL, nil +} + +type initiateOAuthDynamicRegistrationIATAuthAccountsOptions struct { + requestID string + sessionKey string + refreshToken string + domain string +} + +func (s *Services) initiateOAuthDynamicRegistrationIATAuthAccounts( + ctx context.Context, + opts initiateOAuthDynamicRegistrationIATAuthAccountsOptions, +) (string, *exceptions.ServiceError) { + logger := s.buildLogger(opts.requestID, oauthDynamicRegistrationAccountsLocation, "CreateOAuthDynamicRegistrationAccount") + logger.InfoContext(ctx, "Creating OAuth dynamic registration account...") + + if opts.sessionKey == "" { + logger.DebugContext(ctx, "No session key provided") + + if opts.refreshToken != "" { + logger.DebugContext(ctx, "Refresh token provided, verifying...") + } + } + + data, credsClientID, verified, found, err := s.cache.VerifyAccountCredentialsRegistrationSessionKey( + ctx, + cache.VerifyAccountCredentialsRegistrationSessionKeyOptions{ + RequestID: opts.requestID, + SessionKey: opts.sessionKey, + Domain: opts.domain, + }, + ) + if err != nil { + logger.ErrorContext(ctx, "Failed to verify account credentials registration session key", "error", err) + return "", exceptions.NewInternalServerError() + } + if !found { + logger.WarnContext(ctx, "Account credentials registration session key not found") + return "", exceptions.NewUnauthorizedError() + } + if !verified { + logger.WarnContext(ctx, "Account credentials registration session key is not verified") + return "", exceptions.NewUnauthorizedError() + } + + if err := s.cache.DeleteAccountCredentialsRegistrationSessionKey( + ctx, + cache.DeleteAccountCredentialsRegistrationSessionKeyOptions{ + RequestID: opts.requestID, + ClientID: credsClientID, + }, + ); err != nil { + logger.ErrorContext(ctx, "Failed to delete account credentials registration session key", "error", err) + return "", exceptions.NewInternalServerError() + } +} diff --git a/idp/internal/services/oauth_dynamic_registration_apps.go b/idp/internal/services/oauth_dynamic_registration_apps.go new file mode 100644 index 0000000..34ea668 --- /dev/null +++ b/idp/internal/services/oauth_dynamic_registration_apps.go @@ -0,0 +1,7 @@ +// 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 services diff --git a/idp/internal/utils/ids.go b/idp/internal/utils/ids.go index 16f5398..f338361 100644 --- a/idp/internal/utils/ids.go +++ b/idp/internal/utils/ids.go @@ -14,7 +14,11 @@ import ( "github.com/google/uuid" ) -func Base62UUID() string { +type Base62UUIDStr = string + +const NilBase62UUID Base62UUIDStr = "0000000000000000000000" + +func Base62UUID() Base62UUIDStr { id := uuid.New() return fmt.Sprintf("%022s", Base62Encode(id[:])) } From 3979f1526e0673a309718f3aafca501b4c7d37f8 Mon Sep 17 00:00:00 2001 From: Afonso Barracha Date: Tue, 15 Sep 2026 18:47:57 +1200 Subject: [PATCH 06/16] fix: remove host check from accounts dynamic registration --- ... => oauth_dynamic_registration_account.go} | 0 ...auth_dynamic_registration_iat_accounts.go} | 27 +- .../oauth_dynamic_registration_iat_apps.go | 7 + ...ccount_credentials_dynamic_registration.go | 13 +- .../database/queries/user_sessions.sql | 5 + .../services/oauth_dynamic_registration.go | 1536 ----------------- .../oauth_dynamic_registration_accounts.go | 1474 +++++++++++++++- idp/internal/services/users_auth.go | 24 +- idp/tests/dynamic_registration_test.go | 93 +- project.md | 36 +- 10 files changed, 1533 insertions(+), 1682 deletions(-) rename idp/internal/controllers/{oauth_dynamic_registration.go => oauth_dynamic_registration_account.go} (100%) rename idp/internal/controllers/{oauth_dynamic_registration_iat.go => oauth_dynamic_registration_iat_accounts.go} (93%) create mode 100644 idp/internal/controllers/oauth_dynamic_registration_iat_apps.go create mode 100644 idp/internal/providers/database/queries/user_sessions.sql delete mode 100644 idp/internal/services/oauth_dynamic_registration.go diff --git a/idp/internal/controllers/oauth_dynamic_registration.go b/idp/internal/controllers/oauth_dynamic_registration_account.go similarity index 100% rename from idp/internal/controllers/oauth_dynamic_registration.go rename to idp/internal/controllers/oauth_dynamic_registration_account.go diff --git a/idp/internal/controllers/oauth_dynamic_registration_iat.go b/idp/internal/controllers/oauth_dynamic_registration_iat_accounts.go similarity index 93% rename from idp/internal/controllers/oauth_dynamic_registration_iat.go rename to idp/internal/controllers/oauth_dynamic_registration_iat_accounts.go index 078423d..7de2fc7 100644 --- a/idp/internal/controllers/oauth_dynamic_registration_iat.go +++ b/idp/internal/controllers/oauth_dynamic_registration_iat_accounts.go @@ -31,14 +31,6 @@ 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") @@ -77,7 +69,6 @@ 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, Origin: ctx.Get(fiber.HeaderOrigin), @@ -275,7 +266,6 @@ func (c *Controllers) OAuthDynamicRegistrationIATLoginPost(ctx fiber.Ctx) error Email: loginBody.Email, Password: loginBody.Password, BackendDomain: c.backendDomain, - HostUsername: registrationHostUsername(ctx), }, ) if serviceErr != nil { @@ -452,7 +442,6 @@ func (c *Controllers) OAuthDynamicRegistrationIAT2FAPost(ctx fiber.Ctx) error { CSRFToken: hiddenFields.CSRFToken, Code: twoFABody.Code, BackendDomain: c.backendDomain, - HostUsername: registrationHostUsername(ctx), }, ) if serviceErr != nil { @@ -536,11 +525,10 @@ func (c *Controllers) OAuthDynamicRegistrationIATExtAuthGet(ctx fiber.Ctx) error ACCClientID: uPrms.ACCClientID, Provider: uPrms.Provider, Domain: baseQPrms.ClientID, - CallbackURL: "https://" + c.registrationIssuerDomain(ctx) + paths.V1 + paths.AuthBase + paths.OAuthBase + paths.InitialAccessToken + "/" + uPrms.ACCClientID + paths.InitialAccessTokenAuthEXT + "/" + uPrms.Provider + paths.InitialAccessTokenCallback, + CallbackURL: "https://" + c.backendDomain + 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 { @@ -580,9 +568,8 @@ func (c *Controllers) OAuthDynamicRegistrationIATExtCB(ctx fiber.Ctx) error { 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, + RedirectURL: "https://" + c.backendDomain + 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 { @@ -634,9 +621,8 @@ func (c *Controllers) OAuthDynamicRegistrationIATExtAppleCB(ctx fiber.Ctx) error 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, + RedirectURL: "https://" + c.backendDomain + 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 { @@ -675,7 +661,6 @@ func (c *Controllers) OAuthDynamicRegistrationIATToken(ctx fiber.Ctx) error { ctx.Context(), services.VerifyOAuthDynamicRegistrationIATCodeOptions{ BackendDomain: c.backendDomain, - HostUsername: registrationHostUsername(ctx), RequestID: requestID, Code: body.Code, CodeVerifier: body.CodeVerifier, @@ -689,9 +674,3 @@ 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/oauth_dynamic_registration_iat_apps.go b/idp/internal/controllers/oauth_dynamic_registration_iat_apps.go new file mode 100644 index 0000000..745a673 --- /dev/null +++ b/idp/internal/controllers/oauth_dynamic_registration_iat_apps.go @@ -0,0 +1,7 @@ +// 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 diff --git a/idp/internal/providers/cache/account_credentials_dynamic_registration.go b/idp/internal/providers/cache/account_credentials_dynamic_registration.go index b49c98e..2548d29 100644 --- a/idp/internal/providers/cache/account_credentials_dynamic_registration.go +++ b/idp/internal/providers/cache/account_credentials_dynamic_registration.go @@ -228,7 +228,6 @@ 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"` @@ -243,7 +242,6 @@ func buildAccountCredentialsDynamicRegistrationIAT2FACacheKey(sessionID string) } type SaveAccountCredentialsDynamicRegistrationIAT2FAOptions struct { - Username string RequestID string AccountPublicID uuid.UUID AccountVersion int32 @@ -271,7 +269,6 @@ func (c *Cache) SaveAccountCredentialsDynamicRegistrationIAT2FA( sessionId := utils.Base64UUID() data := AccountCredentialsDynamicRegistrationIAT2FAData{ - Username: opts.Username, AccountPublicID: opts.AccountPublicID, AccountVersion: opts.AccountVersion, RedirectURI: opts.RedirectURI, @@ -443,7 +440,6 @@ 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"` @@ -453,7 +449,6 @@ type AccountCredentialsDynamicRegistrationIATCodeData struct { } type GenerateAccountCredentialsRegistrationIATCodeOptions struct { - HostUsername string RequestID string ClientID string AccountPublicID uuid.UUID @@ -485,7 +480,6 @@ func (c *Cache) GenerateAccountCredentialsRegistrationIATCode( } data := AccountCredentialsDynamicRegistrationIATCodeData{ - HostUsername: opts.HostUsername, AccountPublicID: opts.AccountPublicID, AccountVersion: opts.AccountVersion, Domain: opts.Domain, @@ -572,7 +566,6 @@ 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"` @@ -595,7 +588,6 @@ func parseSessionKey(sessionKey string) (string, string, bool) { } type CreateAccountCredentialsRegistrationSessionKeyOptions struct { - Username string RequestID string ClientID string Domain string @@ -625,7 +617,6 @@ func (c *Cache) CreateAccountCredentialsRegistrationSessionKey( } data := AccountCredentialsDynamicRegistrationSessionData{ - Username: opts.Username, AccountPublicID: opts.AccountPublicID, AccountVersion: opts.AccountVersion, SessionKey: utils.Sha256HashHex(sessionKey), @@ -671,7 +662,7 @@ func (c *Cache) VerifyAccountCredentialsRegistrationSessionKey( clientID, sessionKey, ok := parseSessionKey(opts.SessionKey) if !ok { logger.DebugContext(ctx, "Invalid account credentials registration session key format") - return AccountCredentialsDynamicRegistrationSessionData{}, "", false, true, nil + return AccountCredentialsDynamicRegistrationSessionData{}, "", false, false, nil } data, err := c.storage.GetWithContext(ctx, buildAccountCredentialsDynamicRegistrationSessionCacheKey(opts.Domain, clientID)) @@ -693,7 +684,7 @@ func (c *Cache) VerifyAccountCredentialsRegistrationSessionKey( ok, err = utils.CompareShaHex(sessionKey, sessionData.SessionKey) if err != nil { logger.ErrorContext(ctx, "Error comparing session key", "error", err) - return AccountCredentialsDynamicRegistrationSessionData{}, "", false, false, err + return AccountCredentialsDynamicRegistrationSessionData{}, "", false, true, err } if !ok { logger.DebugContext(ctx, "Invalid session key") diff --git a/idp/internal/providers/database/queries/user_sessions.sql b/idp/internal/providers/database/queries/user_sessions.sql new file mode 100644 index 0000000..891f7f7 --- /dev/null +++ b/idp/internal/providers/database/queries/user_sessions.sql @@ -0,0 +1,5 @@ +-- 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/. \ No newline at end of file diff --git a/idp/internal/services/oauth_dynamic_registration.go b/idp/internal/services/oauth_dynamic_registration.go deleted file mode 100644 index 7ec5930..0000000 --- a/idp/internal/services/oauth_dynamic_registration.go +++ /dev/null @@ -1,1536 +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 - -import ( - "context" - "net/url" - - "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/providers/mailer" - "github.com/tugascript/devlogs/idp/internal/providers/oauth" - "github.com/tugascript/devlogs/idp/internal/providers/tokens" - "github.com/tugascript/devlogs/idp/internal/services/dtos" - "github.com/tugascript/devlogs/idp/internal/services/templates" - "github.com/tugascript/devlogs/idp/internal/utils" -) - -const oauthDynamicRegistrationLocation string = "oauth_dynamic_registration" - -const ( - oauthDynamicRegistrationIATPath string = paths.V1 + paths.AuthBase + paths.OAuthBase + paths.InitialAccessToken - 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 - state string - challenge string - challengeMethod string - redirectURI string -} - -func buildOAuthDynamicRegistrationIATLoginURL(opts buildOAuthDynamicRegistrationIATLoginURLOptions) string { - 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 != "" { - queryParams.Add("code_challenge_method", opts.challengeMethod) - } - return oauthDynamicRegistrationIATPath + "/" + opts.accClientID + paths.OAuthAuth + paths.AuthLogin + "?" + queryParams.Encode() -} - -type buildOAuthDynamicRegistrationIATCallbackURLOptions struct { - 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.issuerDomain) - return opts.redirectURI + "?" + queryParams.Encode() -} - -type generateOAuthDynamicRegistrationIATCallbackOptions struct { - hostUsername string - requestID string - clientID string - accountPublicID uuid.UUID - accountVersion int32 - challenge string - domain string - redirectURI string - state string - backendDomain string -} - -func (s *Services) generateOAuthDynamicRegistrationIATCallback( - ctx context.Context, - opts generateOAuthDynamicRegistrationIATCallbackOptions, -) (string, *exceptions.ServiceError) { - logger := s.buildLogger( - opts.requestID, - oauthDynamicRegistrationLocation, - "generateOAuthDynamicRegistrationIATCallback", - ).With( - "clientId", opts.clientID, - "accountPublicId", opts.accountPublicID, - ) - logger.InfoContext(ctx, "Generating OAuth dynamic registration IAT callback...") - - code, err := s.cache.GenerateAccountCredentialsRegistrationIATCode( - ctx, - cache.GenerateAccountCredentialsRegistrationIATCodeOptions{ - HostUsername: opts.hostUsername, - RequestID: opts.requestID, - ClientID: opts.clientID, - AccountPublicID: opts.accountPublicID, - AccountVersion: opts.accountVersion, - Challenge: opts.challenge, - Domain: opts.domain, - }, - ) - if err != nil { - logger.ErrorContext(ctx, "Failed to generate account credentials registration IAT code", "error", err) - return "", exceptions.NewInternalServerError() - } - - return buildOAuthDynamicRegistrationIATCallbackURL(buildOAuthDynamicRegistrationIATCallbackURLOptions{ - redirectURI: opts.redirectURI, - code: code, - state: opts.state, - issuerDomain: dynamicRegistrationIssuerDomain(opts.hostUsername, opts.backendDomain), - }), nil -} - -func mapDomainUsageFromHostExistence(host string) database.DynamicRegistrationUsage { - if host != "" { - return database.DynamicRegistrationUsageApp - } - - return database.DynamicRegistrationUsageAccount -} - -type checkDynamicClientRegistrationDomainUsabilityOptions struct { - requestID string - accountUsername string - domain string -} - -func (s *Services) checkDynamicRegistrationDomainUsability( - ctx context.Context, - opts checkDynamicClientRegistrationDomainUsabilityOptions, -) *exceptions.ServiceError { - logger := s.buildLogger(opts.requestID, dynamicRegistrationDomainsLocation, "checkDynamicRegistrationDomainUsability").With( - "accountUsername", opts.accountUsername, - ) - logger.InfoContext(ctx, "Checking dynamic registration domain usability") - - usage := mapDomainUsageFromHostExistence(opts.accountUsername) - domains := breakDomainIntoAllSubdomains(opts.domain) - var count int64 - var err error - if len(domains) > 1 { - count, err = s.database.CountDynamicRegistrationDomainsByDomainsAndUsages( - ctx, - database.CountDynamicRegistrationDomainsByDomainsAndUsagesParams{ - Domains: domains, - Usages: []database.DynamicRegistrationUsage{usage}, - }, - ) - } else { - count, err = s.database.CountDynamicRegistrationDomainsByDomainAndUsages( - ctx, - database.CountDynamicRegistrationDomainsByDomainAndUsagesParams{ - Domain: opts.domain, - Usages: []database.DynamicRegistrationUsage{usage}, - }, - ) - } - if err != nil { - logger.ErrorContext(ctx, "Failed to count dynamic registration domains by domain and usages", "error", err) - return exceptions.FromDBError(err) - } - if count == 0 { - logger.WarnContext(ctx, "Domain not registered for dynamic registration") - return exceptions.NewForbiddenError() - } - - return nil -} - -type oauthDynamicRegistrationIATAuthOptions struct { - requestID string - challenge string - challengeMethod string - domain string - redirectURI string - state string -} - -func (s *Services) oauthDynamicRegistrationIATAuth( - ctx context.Context, - opts oauthDynamicRegistrationIATAuthOptions, -) (string, *exceptions.ServiceError) { - logger := s.buildLogger( - opts.requestID, - oauthDynamicRegistrationLocation, - "oauthDynamicRegistrationIATAuth", - ).With( - "domain", opts.domain, - "redirectUri", opts.redirectURI, - ) - logger.InfoContext(ctx, "Handling OAuth dynamic registration IAT auth...") - - if serviceErr := s.checkDynamicRegistrationDomainUsability( - ctx, - checkDynamicClientRegistrationDomainUsabilityOptions{ - requestID: opts.requestID, - accountUsername: opts.hostUsername, - domain: opts.domain, - }, - ); serviceErr != nil { - logger.InfoContext(ctx, "Dynamic registration domain not usable", "serviceError", serviceErr) - return "", serviceErr - } - - hashedChallenge, serviceErr := hashChallenge(opts.challenge, opts.challengeMethod) - if serviceErr != nil { - logger.ErrorContext(ctx, "Invalid code challenge", "serviceError", serviceErr) - return "", serviceErr - } - - clientID, err := s.cache.SaveAccountCredentialsDynamicRegistrationIATAuth( - ctx, - cache.SaveAccountCredentialsDynamicRegistrationIATAuthOptions{ - Domain: opts.domain, - RequestID: opts.requestID, - State: opts.state, - RedirectURI: opts.redirectURI, - Challenge: hashedChallenge, - Username: opts.hostUsername, - }, - ) - if err != nil { - logger.ErrorContext(ctx, "Failed to save account credentials dynamic registration IAT auth", "error", err) - return "", exceptions.NewInternalServerError() - } - - return buildOAuthDynamicRegistrationIATLoginURL(buildOAuthDynamicRegistrationIATLoginURLOptions{ - accClientID: clientID, - domain: opts.domain, - state: opts.state, - challenge: opts.challenge, - challengeMethod: opts.challengeMethod, - redirectURI: opts.redirectURI, - }), nil -} - -type InitiateOAuthDynamicRegistrationIATAuthOptions struct { - RequestID string - Origin string - Domain string - State string - SessionKey string - RefreshToken string - Challenge string - ChallengeMethod string - RedirectURI string - BackendDomain string - HostUsername string -} - -func (s *Services) InitiateOAuthDynamicRegistrationIATAuth( - ctx context.Context, - opts InitiateOAuthDynamicRegistrationIATAuthOptions, -) (string, *exceptions.ServiceError) { - logger := s.buildLogger( - opts.RequestID, - oauthDynamicRegistrationLocation, - "InitiateOAuthDynamicRegistrationIATAuth", - ).With( - "redirectUri", opts.RedirectURI, - ) - logger.InfoContext(ctx, "Starting OAuth dynamic registration IAT authorization...") - - if opts.Origin == "" { - logger.WarnContext(ctx, "Origin header is missing") - return "", exceptions.NewUnauthorizedError() - } - - parsedOrigin, err := url.Parse(opts.Origin) - if err != nil { - logger.WarnContext(ctx, "Invalid origin header", "error", err) - return "", exceptions.NewUnauthorizedError() - } - if parsedOrigin.Host != opts.Domain { - logger.WarnContext(ctx, "Origin header does not match domain", "originHost", parsedOrigin.Host) - return "", exceptions.NewUnauthorizedError() - } - - if opts.SessionKey == "" { - if opts.RefreshToken == "" { - logger.InfoContext(ctx, "No session key or refresh token provided, redirecting to login") - return s.oauthDynamicRegistrationIATAuth(ctx, oauthDynamicRegistrationIATAuthOptions{ - requestID: opts.RequestID, - challenge: opts.Challenge, - challengeMethod: opts.ChallengeMethod, - domain: opts.Domain, - redirectURI: opts.RedirectURI, - state: opts.State, - hostUsername: opts.HostUsername, - }) - } - - logger.InfoContext(ctx, "No session key provided, attempting to refresh with refresh token") - return s.refreshTokenOAuthDynamicRegistrationIATLogin( - ctx, - refreshTokenOAuthDynamicRegistrationIATLoginOptions{ - hostUsername: opts.HostUsername, - requestID: opts.RequestID, - refreshToken: opts.RefreshToken, - challenge: opts.Challenge, - challengeMethod: opts.ChallengeMethod, - domain: opts.Domain, - redirectURI: opts.RedirectURI, - state: opts.State, - backendDomain: opts.BackendDomain, - }, - ) - } - - data, credsClientID, verified, found, err := s.cache.VerifyAccountCredentialsRegistrationSessionKey( - ctx, - cache.VerifyAccountCredentialsRegistrationSessionKeyOptions{ - RequestID: opts.RequestID, - SessionKey: opts.SessionKey, - Domain: opts.Domain, - }, - ) - if err != nil { - logger.ErrorContext(ctx, "Failed to verify account credentials registration session key", "error", err) - return "", exceptions.NewInternalServerError() - } - if !found { - logger.WarnContext(ctx, "Account credentials registration session key not found") - 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() - } - - if err := s.cache.DeleteAccountCredentialsRegistrationSessionKey( - ctx, - cache.DeleteAccountCredentialsRegistrationSessionKeyOptions{ - RequestID: opts.RequestID, - ClientID: credsClientID, - }, - ); err != nil { - logger.ErrorContext(ctx, "Failed to delete account credentials registration session key", "error", err) - return "", exceptions.NewInternalServerError() - } - - accountDTO, serviceErr := s.GetAccountByPublicIDAndVersion(ctx, GetAccountByPublicIDAndVersionOptions{ - RequestID: opts.RequestID, - PublicID: data.AccountPublicID, - Version: data.AccountVersion, - }) - if serviceErr != nil { - logger.WarnContext(ctx, "Failed to get account by public ID and version", "serviceError", serviceErr) - return "", serviceErr - } - - hashedChallenge, serviceErr := hashChallenge(opts.Challenge, opts.ChallengeMethod) - if serviceErr != nil { - logger.ErrorContext(ctx, "Invalid code challenge", "serviceError", serviceErr) - return "", serviceErr - } - - logger.InfoContext(ctx, "Successfully verified account credentials registration IAT session key, creating code...") - cbURL, serviceErr := s.generateOAuthDynamicRegistrationIATCallback( - ctx, - generateOAuthDynamicRegistrationIATCallbackOptions{ - hostUsername: opts.HostUsername, - requestID: opts.RequestID, - clientID: credsClientID, - accountPublicID: accountDTO.PublicID, - accountVersion: accountDTO.Version(), - challenge: hashedChallenge, - domain: opts.Domain, - redirectURI: opts.RedirectURI, - state: opts.State, - backendDomain: opts.BackendDomain, - }, - ) - if serviceErr != nil { - logger.ErrorContext(ctx, "Failed to generate OAuth dynamic registration IAT callback", "serviceErr", serviceErr) - return "", serviceErr - } - - return cbURL, nil -} - -type OAuthDynamicRegistrationIATAuthRenderOptions struct { - RequestID string - ACCClientID string - State string - Domain string - CodeChallenge string - CodeChallengeMethod string - RedirectURI string -} - -func (s *Services) OAuthDynamicRegistrationIATAuthRender( - ctx context.Context, - opts OAuthDynamicRegistrationIATAuthRenderOptions, -) (string, *exceptions.ServiceError) { - logger := s.buildLogger( - opts.RequestID, - oauthDynamicRegistrationLocation, - "OAuthDynamicRegistrationIATAuthRender", - ).With( - "redirectUri", opts.RedirectURI, - ) - logger.InfoContext(ctx, "Starting OAuth dynamic registration IAT authorization html render...") - - data, found, err := s.cache.GetAccountCredentialsDynamicRegistrationAuthIAT( - ctx, - cache.GetAccountCredentialsDynamicRegistrationIATAuthOptions{ - RequestID: opts.RequestID, - ClientID: opts.ACCClientID, - }, - ) - if err != nil { - logger.ErrorContext(ctx, "Failed to get account credentials dynamic registration IAT", "error", err) - return "", exceptions.NewInternalServerError() - } - if !found { - logger.WarnContext(ctx, "Account credentials dynamic registration IAT not found") - return "", exceptions.NewForbiddenError() - } - - if data.Domain != opts.Domain { - logger.WarnContext(ctx, "OAuth Domain does not match", "dataDomain", data.Domain) - return "", exceptions.NewUnauthorizedError() - } - if data.State != opts.State { - logger.WarnContext(ctx, "OAuth State does not match") - return "", exceptions.NewUnauthorizedError() - } - if data.RedirectURI != opts.RedirectURI { - logger.WarnContext(ctx, "OAuth Redirect URI does not match") - return "", exceptions.NewUnauthorizedError() - } - - csrfToken, err := s.cache.SaveAccountCredentialsDynamicRegistrationIATLoginCSRF( - ctx, - cache.SaveAccountCredentialsDynamicRegistrationIATLoginCSRFOptions{ - RequestID: opts.RequestID, - ClientID: opts.ACCClientID, - Domain: opts.Domain, - }, - ) - if err != nil { - logger.ErrorContext(ctx, "Failed to save account credentials dynamic registration IAT auth CSRF token", "error", err) - return "", exceptions.NewInternalServerError() - } - - loginHTML, err := templates.BuildAccountDynamicRegistrationIATAuthTemplate( - templates.AccountDynamicRegistrationIATAuthOptions{ - ACCClientID: opts.ACCClientID, - CSRFToken: csrfToken, - Domain: opts.Domain, - State: opts.State, - CodeChallenge: opts.CodeChallenge, - CodeChallengeMethod: opts.CodeChallengeMethod, - RedirectURI: opts.RedirectURI, - AppleEnabled: s.oauthProviders.IsAppleEnabled(), - FacebookEnabled: s.oauthProviders.IsFacebookEnabled(), - GitHubEnabled: s.oauthProviders.IsGitHubEnabled(), - GoogleEnabled: s.oauthProviders.IsGoogleEnabled(), - MicrosoftEnabled: s.oauthProviders.IsMicrosoftEnabled(), - }, - ) - if err != nil { - logger.ErrorContext(ctx, "Failed to build account dynamic registration IAT auth template", "error", err) - return "", exceptions.NewInternalServerError() - } - - return loginHTML, nil -} - -type OAuthDynamicRegistrationIATAuthReRenderOptions struct { - RequestID string - Errors []string - CSRFToken string - ACCClientID string - State string - Domain string - CodeChallenge string - CodeChallengeMethod string - RedirectURI string -} - -func (s *Services) OAuthDynamicRegistrationIATAuthReRender( - ctx context.Context, - opts OAuthDynamicRegistrationIATAuthReRenderOptions, -) (string, *exceptions.ServiceError) { - logger := s.buildLogger( - opts.RequestID, - oauthDynamicRegistrationLocation, - "OAuthDynamicRegistrationIATAuthReRender", - ).With( - "clientId", opts.ACCClientID, - "redirectUri", opts.RedirectURI, - ) - logger.InfoContext(ctx, "Re-rendering OAuth dynamic registration IAT authorization html...") - - loginHTML, err := templates.BuildAccountDynamicRegistrationIATAuthTemplate( - templates.AccountDynamicRegistrationIATAuthOptions{ - Errors: opts.Errors, - ACCClientID: opts.ACCClientID, - CSRFToken: opts.CSRFToken, - Domain: opts.Domain, - State: opts.State, - CodeChallenge: opts.CodeChallenge, - CodeChallengeMethod: opts.CodeChallengeMethod, - RedirectURI: opts.RedirectURI, - AppleEnabled: s.oauthProviders.IsAppleEnabled(), - FacebookEnabled: s.oauthProviders.IsFacebookEnabled(), - GitHubEnabled: s.oauthProviders.IsGitHubEnabled(), - GoogleEnabled: s.oauthProviders.IsGoogleEnabled(), - MicrosoftEnabled: s.oauthProviders.IsMicrosoftEnabled(), - }, - ) - if err != nil { - logger.ErrorContext(ctx, "Failed to build account dynamic registration IAT auth template", "error", err) - return "", exceptions.NewInternalServerError() - } - - return loginHTML, nil -} - -type OAuthDynamicRegistrationIATLoginOptions struct { - RequestID string - ACCClientID string - Domain string - CSRFToken string - CodeChallenge string - CodeChallengeMethod string - State string - RedirectURI string - Email string - Password string - BackendDomain string - HostUsername string -} - -func (s *Services) OAuthDynamicRegistrationIATLogin( - ctx context.Context, - opts OAuthDynamicRegistrationIATLoginOptions, -) (string, string, bool, *exceptions.ServiceError) { - logger := s.buildLogger(opts.RequestID, oauthDynamicRegistrationLocation, "OAuthDynamicRegistrationIATLoginPost").With( - "clientId", opts.ACCClientID, - "domain", opts.Domain, - ) - logger.InfoContext(ctx, "Logging in with OAuth dynamic registration IAT...") - - validCSRF, err := s.cache.VerifyAccountCredentialsDynamicRegistrationIATLoginCSRF( - ctx, - cache.VerifyAccountCredentialsDynamicRegistrationIATLoginCSRFOptions{ - RequestID: opts.RequestID, - ClientID: opts.ACCClientID, - Domain: opts.Domain, - CSRFToken: opts.CSRFToken, - }, - ) - if err != nil { - logger.ErrorContext(ctx, "Failed to verify account credentials dynamic registration IAT auth CSRF token", "error", err) - return "", "", false, exceptions.NewInternalServerError() - } - if !validCSRF { - logger.WarnContext(ctx, "Invalid CSRF token") - return "", "", false, exceptions.NewForbiddenError() - } - - data, found, err := s.cache.GetAccountCredentialsDynamicRegistrationAuthIAT(ctx, cache.GetAccountCredentialsDynamicRegistrationIATAuthOptions{ - RequestID: opts.RequestID, - ClientID: opts.ACCClientID, - }) - if err != nil { - logger.ErrorContext(ctx, "Failed to get account credentials dynamic registration IAT", "error", err) - return "", "", false, exceptions.NewInternalServerError() - } - if !found { - logger.ErrorContext(ctx, "Account credentials dynamic registration IAT not found") - return "", "", false, exceptions.NewNotFoundError() - } - - if data.Domain != opts.Domain { - logger.WarnContext(ctx, "OAuth Domain does not match", "dataDomain", data.Domain) - return "", "", false, exceptions.NewUnauthorizedError() - } - if data.State != opts.State { - logger.WarnContext(ctx, "OAuth State does not match") - return "", "", false, exceptions.NewUnauthorizedError() - } - if data.RedirectURI != opts.RedirectURI { - 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, - Email: opts.Email, - }) - if serviceErr != nil { - if serviceErr.Code != exceptions.CodeNotFound { - return "", "", false, serviceErr - } - - logger.WarnContext(ctx, "Account was not found", "error", serviceErr) - return "", "", false, exceptions.NewUnauthorizedError() - } - if _, err := s.database.FindAccountAuthProviderByAccountPublicIdAndProvider( - ctx, - database.FindAccountAuthProviderByAccountPublicIdAndProviderParams{ - AccountPublicID: accountDTO.PublicID, - Provider: database.AuthProviderLocal, - }, - ); err != nil { - serviceErr := exceptions.FromDBError(err) - if serviceErr.Code != exceptions.CodeNotFound { - logger.ErrorContext(ctx, "Failed to find account auth provider", "error", err) - return "", "", false, serviceErr - } - - logger.WarnContext(ctx, "Account auth provider not found", "error", err) - return "", "", false, exceptions.NewUnauthorizedError() - } - - passwordVerified, err := utils.Argon2CompareHash(opts.Password, accountDTO.Password()) - if err != nil { - logger.ErrorContext(ctx, "Failed to verify password", "error", err) - return "", "", false, exceptions.NewInternalServerError() - } - if !passwordVerified { - logger.WarnContext(ctx, "Passwords do not match") - return "", "", false, exceptions.NewUnauthorizedError() - } - if !accountDTO.EmailVerified() { - 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, - accountPublicID: accountDTO.PublicID, - }) - if serviceErr != nil { - logger.ErrorContext(ctx, "Failed to get default 2FA config", "serviceError", serviceErr) - return "", "", false, serviceErr - } - if default2FAConfig != nil { - logger.InfoContext(ctx, "Two-Factor is enabled, proceeding to 2FA step") - sessionID, err := s.cache.SaveAccountCredentialsDynamicRegistrationIAT2FA( - ctx, - cache.SaveAccountCredentialsDynamicRegistrationIAT2FAOptions{ - Username: data.Username, - RequestID: opts.RequestID, - AccountPublicID: accountDTO.PublicID, - AccountVersion: accountDTO.Version(), - RedirectURI: opts.RedirectURI, - Domain: data.Domain, - ClientID: opts.ACCClientID, - State: data.State, - TwoFAType: string(default2FAConfig.TwoFactorType), - TwoFATTL: s.jwt.Get2FATTL(), - }, - ) - if err != nil { - logger.ErrorContext(ctx, "Failed to save account credentials dynamic registration IAT 2FA", "error", err) - return "", "", false, exceptions.NewInternalServerError() - } - - if default2FAConfig.TwoFactorType == database.TwoFactorTypeEmail { - code, err := s.cache.AddTwoFactorCode(ctx, cache.AddTwoFactorCodeOptions{ - RequestID: opts.RequestID, - AccountID: accountDTO.ID(), - TTL: s.jwt.Get2FATTL(), - }) - if err != nil { - logger.ErrorContext(ctx, "Failed to add two factor code", "error", err) - return "", "", false, exceptions.NewInternalServerError() - } - - if err := s.mail.Publish2FAEmail(ctx, mailer.TwoFactorEmailOptions{ - RequestID: opts.RequestID, - Email: accountDTO.Email, - Name: accountDTO.GivenName, - Code: code, - }); err != nil { - logger.ErrorContext(ctx, "Failed to send two factor code email", "error", err) - return "", "", false, exceptions.NewInternalServerError() - } - - logger.InfoContext(ctx, "Sent two factor code email successfully") - } - - if err := s.cache.DeleteAccountCredentialsDynamicRegistrationIATAuth( - ctx, - cache.DeleteAccountCredentialsDynamicRegistrationIATAuthOptions{ - RequestID: opts.RequestID, - ClientID: opts.ACCClientID, - }, - ); err != nil { - logger.ErrorContext(ctx, "Failed to delete account credentials dynamic registration IAT auth", "error", err) - return "", "", false, exceptions.NewInternalServerError() - } - - queryParams := make(url.Values) - queryParams.Add("client_id", data.Domain) - queryParams.Add("redirect_uri", opts.RedirectURI) - queryParams.Add("state", data.State) - queryParams.Add("code_challenge", opts.CodeChallenge) - if opts.CodeChallengeMethod != "" { - queryParams.Add("code_challenge_method", opts.CodeChallengeMethod) - } - return oauthDynamicRegistrationIATPath + "/" + opts.ACCClientID + paths.OAuthAuth + - paths.AuthLogin + paths.Auth2FA + "?" + queryParams.Encode(), sessionID, false, 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 registration domain", "serviceError", serviceErr) - return "", "", false, serviceErr - } - - if err := s.cache.DeleteAccountCredentialsDynamicRegistrationIATAuth( - ctx, - cache.DeleteAccountCredentialsDynamicRegistrationIATAuthOptions{ - RequestID: opts.RequestID, - ClientID: opts.ACCClientID, - }, - ); err != nil { - logger.ErrorContext(ctx, "Failed to delete account credentials dynamic registration IAT auth", "error", err) - return "", "", false, exceptions.NewInternalServerError() - } - - 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, - AccountPublicID: accountDTO.PublicID, - AccountVersion: accountDTO.Version(), - }, - ) - if err != nil { - logger.ErrorContext(ctx, "Failed to create account credentials registration session", "error", err) - return "", "", false, exceptions.NewInternalServerError() - } - - return oauthDynamicRegistrationIATAuthPath, sessionKey, true, nil -} - -type OAuthDynamicRegistrationIAT2FARenderOptions struct { - RequestID string - Domain string - ACCClientID string - SessionID string - Challenge string - ChallengeMethod string - State string - RedirectURI string -} - -func (s *Services) OAuthDynamicRegistrationIAT2FARender( - ctx context.Context, - opts OAuthDynamicRegistrationIAT2FARenderOptions, -) (string, *exceptions.ServiceError) { - logger := s.buildLogger(opts.RequestID, oauthDynamicRegistrationLocation, "OAuthDynamicRegistrationIAT2FARender").With( - "clientId", opts.ACCClientID, - ) - logger.InfoContext(ctx, "Handling OAuth dynamic registration IAT 2FA...") - - data, found, err := s.cache.GetAccountCredentialsDynamicRegistrationIAT2FA(ctx, cache.GetAccountCredentialsDynamicRegistrationIAT2FAOptions{ - RequestID: opts.RequestID, - SessionID: opts.SessionID, - }) - if err != nil { - logger.ErrorContext(ctx, "Failed to get account credentials dynamic registration IAT 2FA", "error", err) - return "", exceptions.NewInternalServerError() - } - if !found { - logger.WarnContext(ctx, "Failed to get account credentials dynamic registration IAT 2FA") - return "", exceptions.NewUnauthorizedError() - } - - if opts.ACCClientID != data.ClientID { - logger.WarnContext(ctx, "Client IDs do not match", "sessionClientId", data.ClientID) - return "", exceptions.NewUnauthorizedError() - } - if data.Domain != opts.Domain { - logger.WarnContext(ctx, "OAuth Domain does not match", "dataDomain", data.Domain) - return "", exceptions.NewUnauthorizedError() - } - if data.State != opts.State { - logger.WarnContext(ctx, "OAuth State does not match") - return "", exceptions.NewUnauthorizedError() - } - if data.RedirectURI != opts.RedirectURI { - logger.WarnContext(ctx, "OAuth Redirect URI does not match") - return "", exceptions.NewUnauthorizedError() - } - - csrfToken, err := s.cache.SaveAccountCredentialsDynamicRegistrationIAT2FACSRFToken( - ctx, - cache.SaveAccountCredentialsDynamicRegistrationIAT2FACSRFTokenOptions{ - RequestID: opts.RequestID, - SessionID: opts.SessionID, - TwoFATTL: s.jwt.Get2FATTL(), - }, - ) - if err != nil { - logger.ErrorContext(ctx, "Failed to save account credentials dynamic registration IAT 2FA CSRF token", "error", err) - return "", exceptions.NewInternalServerError() - } - - twoFAhtml, err := templates.BuildAccountDynamicRegistrationIAT2FATemplate( - templates.AccountDynamicRegistrationIAT2FAOptions{ - ACCClientID: opts.ACCClientID, - Domain: opts.Domain, - SessionID: opts.SessionID, - CSRFToken: csrfToken, - State: data.State, - CodeChallenge: opts.Challenge, - CodeChallengeMethod: opts.ChallengeMethod, - RedirectURI: data.RedirectURI, - }, - ) - if err != nil { - logger.ErrorContext(ctx, "Failed to build account dynamic registration IAT 2FA template", "error", err) - return "", exceptions.NewInternalServerError() - } - - return twoFAhtml, nil -} - -type OAuthDynamicRegistrationIAT2FAReRenderOptions struct { - RequestID string - Domain string - ACCClientID string - SessionID string - Errors []string - CSRFToken string - Challenge string - ChallengeMethod string - State string - RedirectURI string -} - -func (s *Services) OAuthDynamicRegistrationIAT2FAReRender( - ctx context.Context, - opts OAuthDynamicRegistrationIAT2FAReRenderOptions, -) (string, *exceptions.ServiceError) { - logger := s.buildLogger(opts.RequestID, oauthDynamicRegistrationLocation, "OAuthDynamicRegistrationIAT2FAReRender").With( - "clientId", opts.ACCClientID, - ) - logger.InfoContext(ctx, "Re-rendering OAuth dynamic registration IAT 2FA...") - - twoFAhtml, err := templates.BuildAccountDynamicRegistrationIAT2FATemplate( - templates.AccountDynamicRegistrationIAT2FAOptions{ - Errors: opts.Errors, - ACCClientID: opts.ACCClientID, - Domain: opts.Domain, - SessionID: opts.SessionID, - CSRFToken: opts.CSRFToken, - State: opts.State, - CodeChallenge: opts.Challenge, - CodeChallengeMethod: opts.ChallengeMethod, - RedirectURI: opts.RedirectURI, - }, - ) - if err != nil { - logger.ErrorContext(ctx, "Failed to build account dynamic registration IAT 2FA template", "error", err) - return "", exceptions.NewInternalServerError() - } - - return twoFAhtml, nil -} - -func map2FATypeTokens(twoFAType string) (tokens.TwoFAType, *exceptions.ServiceError) { - switch twoFAType { - case TwoFactorTypeEmail: - return tokens.TwoFATypeEmail, nil - case TwoFactorTypeTotp: - return tokens.TwoFATypeTOTP, nil - default: - return "", exceptions.NewValidationError("invalid two factor type") - } -} - -type OAuthDynamicRegistrationIATVerify2FACodeOptions struct { - RequestID string - ACCClientID string - Domain string - SessionID string - CSRFToken string - Code string - BackendDomain string - HostUsername string -} - -func (s *Services) OAuthDynamicRegistrationIATVerify2FACode( - ctx context.Context, - opts OAuthDynamicRegistrationIATVerify2FACodeOptions, -) (string, string, *exceptions.ServiceError) { - logger := s.buildLogger(opts.RequestID, oauthDynamicRegistrationLocation, "OAuthDynamicRegistrationIATVerify2FACode").With( - "clientId", opts.ACCClientID, - "sessionId", opts.SessionID, - ) - logger.InfoContext(ctx, "Verifying OAuth dynamic registration IAT 2FA...") - - verifiedCSRF, err := s.cache.VerifyAccountCredentialsDynamicRegistrationIAT2FACSRFToken( - ctx, - cache.VerifyAccountCredentialsDynamicRegistrationIAT2FACSRFTokenOptions{ - RequestID: opts.RequestID, - SessionID: opts.SessionID, - CSRFToken: opts.CSRFToken, - }, - ) - if err != nil { - logger.ErrorContext(ctx, "Failed to verify account credentials dynamic registration IAT 2FA CSRF token", "error", err) - return "", "", exceptions.NewInternalServerError() - } - if !verifiedCSRF { - logger.WarnContext(ctx, "Invalid CSRF token") - return "", "", exceptions.NewForbiddenError() - } - - data, found, err := s.cache.GetAccountCredentialsDynamicRegistrationIAT2FA(ctx, cache.GetAccountCredentialsDynamicRegistrationIAT2FAOptions{ - RequestID: opts.RequestID, - SessionID: opts.SessionID, - }) - if err != nil { - logger.ErrorContext(ctx, "Failed to get account credentials dynamic registration IAT 2FA", "error", err) - return "", "", exceptions.NewInternalServerError() - } - if !found { - logger.WarnContext(ctx, "Failed to get account credentials dynamic registration IAT 2FA") - return "", "", exceptions.NewUnauthorizedError() - } - if opts.ACCClientID != data.ClientID { - 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) - return "", "", serviceErr - } - - accountDTO, serviceErr := s.GetAccountByPublicIDAndVersion(ctx, GetAccountByPublicIDAndVersionOptions{ - RequestID: opts.RequestID, - PublicID: data.AccountPublicID, - Version: data.AccountVersion, - }) - if serviceErr != nil { - 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(), - accountPublicID: accountDTO.PublicID, - accountVersion: accountDTO.Version(), - twoFAType: twoFAType, - code: opts.Code, - }); serviceErr != nil { - logger.WarnContext(ctx, "Failed to verify account two factor", "serviceError", serviceErr) - return "", "", serviceErr - } - - 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 registration domain", "serviceError", serviceErr) - return "", "", serviceErr - } - - if err := s.cache.DeleteAccountCredentialsDynamicRegistrationIATAuth( - ctx, - cache.DeleteAccountCredentialsDynamicRegistrationIATAuthOptions{ - RequestID: opts.RequestID, - ClientID: opts.ACCClientID, - }, - ); err != nil { - logger.ErrorContext(ctx, "Failed to delete account credentials dynamic registration IAT auth", "error", err) - return "", "", exceptions.NewInternalServerError() - } - - 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, - AccountPublicID: accountDTO.PublicID, - AccountVersion: accountDTO.Version(), - }, - ) - if err != nil { - logger.ErrorContext(ctx, "Failed to create account credentials registration session", "error", err) - return "", "", exceptions.NewInternalServerError() - } - - return oauthDynamicRegistrationIATAuthPath, sessionKey, nil -} - -// TODO: add external callbacks - -type VerifyOAuthDynamicRegistrationIATCodeOptions struct { - BackendDomain string - HostUsername string - RequestID string - Code string - CodeVerifier string - Domain string -} - -func (s *Services) VerifyOAuthDynamicRegistrationIATCode( - ctx context.Context, - opts VerifyOAuthDynamicRegistrationIATCodeOptions, -) (dtos.AuthDTO, *exceptions.ServiceError) { - logger := s.buildLogger(opts.RequestID, accountCredentialsRegistrationIATLocation, "VerifyOAuthDynamicRegistrationIATCode") - logger.InfoContext(ctx, "Verifying account credentials registration IAT code...") - - data, found, err := s.cache.VerifyAccountCredentialsRegistrationIATCode(ctx, cache.VerifyAccountCredentialsRegistrationIATCodeOptions{ - RequestID: opts.RequestID, - Code: opts.Code, - }) - if err != nil { - logger.ErrorContext(ctx, "Failed to verify account credentials registration IAT code", "error", err) - return dtos.AuthDTO{}, exceptions.NewInternalServerError() - } - if !found { - logger.DebugContext(ctx, "Account credentials registration IAT code not found or invalid") - return dtos.AuthDTO{}, exceptions.NewUnauthorizedError() - } - - if data.Domain != opts.Domain { - logger.WarnContext(ctx, "OAuth Domain does not match", "dataDomain", data.Domain) - return dtos.AuthDTO{}, exceptions.NewUnauthorizedError() - } - - ok, err := utils.CompareShaBase64(opts.CodeVerifier, data.Challenge) - if err != nil { - logger.ErrorContext(ctx, "Failed to compare challenge", "error", err) - return dtos.AuthDTO{}, exceptions.NewInternalServerError() - } - if !ok { - logger.WarnContext(ctx, "OAuth Code challenge verification failed") - return dtos.AuthDTO{}, exceptions.NewUnauthorizedError() - } - - accountDTO, serviceErr := s.GetAccountByPublicIDAndVersion(ctx, GetAccountByPublicIDAndVersionOptions{ - RequestID: opts.RequestID, - PublicID: data.AccountPublicID, - Version: data.AccountVersion, - }) - if serviceErr != nil { - logger.ErrorContext(ctx, "Failed to get account", "serviceError", serviceErr) - return dtos.AuthDTO{}, serviceErr - } - - if data.HostUsername != opts.HostUsername { - return dtos.AuthDTO{}, exceptions.NewUnauthorizedError() - } - 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, - }) - } - signedToken, serviceErr := s.CreateAccountCredentialsRegistrationIAT(ctx, CreateAccountCredentialsRegistrationIATOptions{ - RequestID: opts.RequestID, AccountPublicID: accountDTO.PublicID, - AccountVersion: accountDTO.Version(), Domain: data.Domain, BackendDomain: opts.BackendDomain, - }) - if serviceErr != nil { - return dtos.AuthDTO{}, serviceErr - } - return dtos.NewAuthDTO(signedToken, s.jwt.GetDynamicRegistrationTTL()), nil - -} - -type OAuthDynamicRegistrationIATExtGetOptions struct { - RequestID string - ACCClientID string - Domain string - Provider string - CallbackURL string - RedirectURI string - State string - BackendDomain string - HostUsername string -} - -func (s *Services) OAuthDynamicRegistrationIATExtGet( - ctx context.Context, - opts OAuthDynamicRegistrationIATExtGetOptions, -) (string, *exceptions.ServiceError) { - logger := s.buildLogger(opts.RequestID, oauthLocation, "OAuthDynamicRegistrationIATExtGet").With( - "Provider", opts.Provider, - ) - logger.InfoContext(ctx, "External logging in account...") - - authUrlOpts := oauth.AuthorizationURLOptions{ - RequestID: opts.RequestID, - Scopes: make([]oauth.Scope, 0), - RedirectURL: opts.CallbackURL, - } - var oauthUrl, state string - var serviceErr *exceptions.ServiceError - switch opts.Provider { - case AuthProviderApple: - oauthUrl, state, serviceErr = s.oauthProviders.GetAppleAuthorizationURL(ctx, authUrlOpts) - case AuthProviderFacebook: - oauthUrl, state, serviceErr = s.oauthProviders.GetFacebookAuthorizationURL(ctx, authUrlOpts) - case AuthProviderGitHub: - oauthUrl, state, serviceErr = s.oauthProviders.GetGithubAuthorizationURL(ctx, authUrlOpts) - case AuthProviderGoogle: - oauthUrl, state, serviceErr = s.oauthProviders.GetGoogleAuthorizationURL(ctx, authUrlOpts) - case AuthProviderMicrosoft: - oauthUrl, state, serviceErr = s.oauthProviders.GetMicrosoftAuthorizationURL(ctx, authUrlOpts) - default: - logger.ErrorContext(ctx, "Provider must be 'apple', 'facebook', 'github', 'google' and 'microsoft'") - return "", exceptions.NewInternalServerError() - } - if serviceErr != nil { - logger.ErrorContext(ctx, "Failed to get authorization url or State", "error", serviceErr) - return "", serviceErr - } - - data, found, err := s.cache.GetAccountCredentialsDynamicRegistrationAuthIAT(ctx, cache.GetAccountCredentialsDynamicRegistrationIATAuthOptions{ - RequestID: opts.RequestID, - ClientID: opts.ACCClientID, - }) - if err != nil { - logger.ErrorContext(ctx, "Failed to get account credentials dynamic registration IAT", "error", err) - return "", exceptions.NewInternalServerError() - } - if !found { - logger.ErrorContext(ctx, "Account credentials dynamic registration IAT not found") - return "", exceptions.NewNotFoundError() - } - - if data.Domain != opts.Domain { - logger.WarnContext(ctx, "OAuth Domain does not match", "dataDomain", data.Domain) - return "", exceptions.NewUnauthorizedError() - } - if data.State != opts.State { - logger.WarnContext(ctx, "OAuth State does not match") - return "", exceptions.NewUnauthorizedError() - } - if data.RedirectURI != opts.RedirectURI { - 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, - ClientID: opts.ACCClientID, - Domain: opts.Domain, - Provider: opts.Provider, - State: state, - RequestState: opts.State, - }); err != nil { - logger.ErrorContext(ctx, "Failed to save account credentials dynamic registration IAT external auth", "error", err) - return "", exceptions.NewInternalServerError() - } - - logger.InfoContext(ctx, "Saved account credentials dynamic registration IAT external auth successfully") - return oauthUrl, nil -} - -type OAuthDynamicRegistrationIATExtCBOptions struct { - RequestID string - ACCClientID string - Provider string - State string - Code string - RedirectURL string - BackendDomain string - HostUsername string -} - -func (s *Services) OAuthDynamicRegistrationIATExtCB( - ctx context.Context, - opts OAuthDynamicRegistrationIATExtCBOptions, -) (string, *exceptions.ServiceError) { - logger := s.buildLogger(opts.RequestID, oauthLocation, "OAuthDynamicRegistrationIATExtCB") - logger.InfoContext(ctx, "External callback for account...") - - data, found, err := s.cache.GetAccountCredentialsDynamicRegistrationIATExtAuth(ctx, cache.GetAccountCredentialsDynamicRegistrationIATExtAuthOptions{ - RequestID: opts.RequestID, - Provider: opts.Provider, - State: opts.State, - }) - if err != nil { - logger.ErrorContext(ctx, "Failed to get account credentials dynamic registration IAT external auth", "error", err) - return "", exceptions.NewInternalServerError() - } - if !found { - logger.ErrorContext(ctx, "Account credentials dynamic registration IAT external auth not found") - return "", exceptions.NewNotFoundError() - } - - if data.ClientID != opts.ACCClientID { - logger.WarnContext(ctx, "Client IDs do not match", "dataClientId", data.ClientID) - return "", exceptions.NewUnauthorizedError() - } - - accessTokenOpts := oauth.AccessTokenOptions{ - RequestID: opts.RequestID, - Code: opts.Code, - Scopes: oauthScopes, - RedirectURL: opts.RedirectURL, - } - var token string - var serviceErr *exceptions.ServiceError - switch opts.Provider { - case AuthProviderFacebook: - token, serviceErr = s.oauthProviders.GetFacebookAccessToken(ctx, accessTokenOpts) - case AuthProviderGitHub: - token, serviceErr = s.oauthProviders.GetGithubAccessToken(ctx, accessTokenOpts) - case AuthProviderGoogle: - token, serviceErr = s.oauthProviders.GetGoogleAccessToken(ctx, accessTokenOpts) - case AuthProviderMicrosoft: - token, serviceErr = s.oauthProviders.GetMicrosoftAccessToken(ctx, accessTokenOpts) - default: - logger.ErrorContext(ctx, "Provider must be 'facebook', 'github', 'google' and 'microsoft'") - return "", exceptions.NewInternalServerError() - } - if serviceErr != nil { - logger.WarnContext(ctx, "Failed to get oauth access token", "error", serviceErr) - return "", serviceErr - } - - authData, found, err := s.cache.GetAccountCredentialsDynamicRegistrationAuthIAT(ctx, cache.GetAccountCredentialsDynamicRegistrationIATAuthOptions{ - RequestID: opts.RequestID, - ClientID: opts.ACCClientID, - }) - if err != nil { - logger.ErrorContext(ctx, "Failed to get account credentials dynamic registration IAT", "error", err) - return "", exceptions.NewInternalServerError() - } - if !found { - logger.ErrorContext(ctx, "Account credentials dynamic registration IAT not found") - return "", exceptions.NewNotFoundError() - } - - if authData.Domain != data.Domain { - logger.WarnContext(ctx, "OAuth Domain does not match", "dataDomain", authData.Domain) - return "", exceptions.NewUnauthorizedError() - } - if authData.State != data.RequestState { - 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, - provider: opts.Provider, - token: token, - }) - if serviceErr != nil { - return "", serviceErr - } - - accountDTO, serviceErr := s.GetAccountByEmail(ctx, GetAccountByEmailOptions{ - RequestID: opts.RequestID, - Email: userData.Email, - }) - 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, - Provider: opts.Provider, - }); serviceErr != nil { - if serviceErr.Code != exceptions.CodeNotFound { - logger.ErrorContext(ctx, "Failed to get account auth provider", "serviceError", serviceErr) - return "", serviceErr - } - - logger.WarnContext(ctx, "Account auth provider not found", "serviceError", serviceErr) - return "", exceptions.NewUnauthorizedError() - } - - cbURL, serviceErr := s.generateOAuthDynamicRegistrationIATCallback( - ctx, - generateOAuthDynamicRegistrationIATCallbackOptions{ - hostUsername: authData.Username, - requestID: opts.RequestID, - clientID: opts.ACCClientID, - accountPublicID: accountDTO.PublicID, - accountVersion: accountDTO.Version(), - challenge: authData.Challenge, - domain: authData.Domain, - redirectURI: authData.RedirectURI, - state: authData.State, - backendDomain: opts.BackendDomain, - }, - ) - if serviceErr != nil { - logger.ErrorContext(ctx, "Failed to generate OAuth dynamic registration IAT callback", "serviceError", serviceErr) - return "", serviceErr - } - - return cbURL, nil -} - -type OAuthDynamicRegistrationIATExtAppleCBOptions struct { - RequestID string - ACCClientID string - Email string - Code string - State string - RedirectURL string - BackendDomain string - HostUsername string -} - -func (s *Services) OAuthDynamicRegistrationIATExtAppleCB( - ctx context.Context, - opts OAuthDynamicRegistrationIATExtAppleCBOptions, -) (string, *exceptions.ServiceError) { - logger := s.buildLogger(opts.RequestID, oauthLocation, "OAuthDynamicRegistrationIATExtAppleCB") - logger.InfoContext(ctx, "External callback for account...") - - data, found, err := s.cache.GetAccountCredentialsDynamicRegistrationIATExtAuth(ctx, cache.GetAccountCredentialsDynamicRegistrationIATExtAuthOptions{ - RequestID: opts.RequestID, - Provider: AuthProviderApple, - State: opts.State, - }) - if err != nil { - logger.ErrorContext(ctx, "Failed to get account credentials dynamic registration IAT external auth", "error", err) - return "", exceptions.NewInternalServerError() - } - if !found { - logger.ErrorContext(ctx, "Account credentials dynamic registration IAT external auth not found") - return "", exceptions.NewNotFoundError() - } - - if data.ClientID != opts.ACCClientID { - logger.WarnContext(ctx, "Client IDs do not match", "dataClientId", data.ClientID) - return "", exceptions.NewUnauthorizedError() - } - - idToken, serviceErr := s.oauthProviders.GetAppleIDToken(ctx, oauth.AccessTokenOptions{ - RequestID: opts.RequestID, - Code: opts.Code, - Scopes: oauthScopes, - }) - if serviceErr != nil { - logger.WarnContext(ctx, "Failed to get apple AccountID token", "error", serviceErr) - return "", serviceErr - } - - authData, found, err := s.cache.GetAccountCredentialsDynamicRegistrationAuthIAT(ctx, cache.GetAccountCredentialsDynamicRegistrationIATAuthOptions{ - RequestID: opts.RequestID, - ClientID: opts.ACCClientID, - }) - if err != nil { - logger.ErrorContext(ctx, "Failed to get account credentials dynamic registration IAT", "error", err) - return "", exceptions.NewInternalServerError() - } - if !found { - logger.ErrorContext(ctx, "Account credentials dynamic registration IAT not found") - return "", exceptions.NewNotFoundError() - } - - if authData.Domain != data.Domain { - logger.WarnContext(ctx, "OAuth Domain does not match", "dataDomain", authData.Domain) - return "", exceptions.NewUnauthorizedError() - } - if authData.State != data.RequestState { - 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, - Token: idToken, - Email: opts.Email, - }) - if serviceErr != nil { - logger.ErrorContext(ctx, "Failed to validate apple AccountID token", "error", serviceErr) - return "", serviceErr - } - if !ok { - logger.WarnContext(ctx, "Apple account is not verified") - return "", exceptions.NewUnauthorizedError() - } - - accountDTO, serviceErr := s.GetAccountByEmail(ctx, GetAccountByEmailOptions{ - RequestID: opts.RequestID, - Email: opts.Email, - }) - 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, - Provider: AuthProviderApple, - }); serviceErr != nil { - if serviceErr.Code != exceptions.CodeNotFound { - logger.ErrorContext(ctx, "Failed to get account auth provider", "serviceError", serviceErr) - return "", serviceErr - } - - logger.WarnContext(ctx, "Account auth provider not found", "serviceError", serviceErr) - return "", exceptions.NewUnauthorizedError() - } - - cbURL, serviceErr := s.generateOAuthDynamicRegistrationIATCallback( - ctx, - generateOAuthDynamicRegistrationIATCallbackOptions{ - hostUsername: authData.Username, - requestID: opts.RequestID, - clientID: opts.ACCClientID, - accountPublicID: accountDTO.PublicID, - accountVersion: accountDTO.Version(), - challenge: authData.Challenge, - domain: authData.Domain, - redirectURI: authData.RedirectURI, - state: authData.State, - backendDomain: opts.BackendDomain, - }, - ) - if serviceErr != nil { - logger.ErrorContext(ctx, "Failed to generate OAuth dynamic registration IAT callback", "serviceError", serviceErr) - return "", serviceErr - } - - return cbURL, nil -} diff --git a/idp/internal/services/oauth_dynamic_registration_accounts.go b/idp/internal/services/oauth_dynamic_registration_accounts.go index 5a41b61..62acd24 100644 --- a/idp/internal/services/oauth_dynamic_registration_accounts.go +++ b/idp/internal/services/oauth_dynamic_registration_accounts.go @@ -1,4 +1,4 @@ -// Copyright (c) 2026 Afonso Barracha +// 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 @@ -8,16 +8,30 @@ package services import ( "context" + "net/url" "slices" + "time" + "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/providers/mailer" + "github.com/tugascript/devlogs/idp/internal/providers/oauth" "github.com/tugascript/devlogs/idp/internal/providers/tokens" + "github.com/tugascript/devlogs/idp/internal/services/dtos" + "github.com/tugascript/devlogs/idp/internal/services/templates" "github.com/tugascript/devlogs/idp/internal/utils" ) -const oauthDynamicRegistrationAccountsLocation string = "oauth_dynamic_registration_accounts" +const oauthDynamicRegistrationLocation string = "oauth_dynamic_registration_accounts" + +const ( + oauthDynamicRegistrationIATPath string = paths.V1 + paths.AuthBase + paths.OAuthBase + paths.InitialAccessToken + oauthDynamicRegistrationIATAuthPath string = oauthDynamicRegistrationIATPath + paths.OAuthAuth +) type refreshTokenOAuthDynamicRegistrationIATLoginOptions struct { requestID string @@ -75,21 +89,20 @@ func (s *Services) refreshTokenOAuthDynamicRegistrationIATLogin( }), nil } - blt, err := s.database.GetRevokedToken(ctx, data.TokenID) + stn, err := s.database.FindSessionTokenByTokenID(ctx, data.TokenID) if err != nil { - if exceptions.FromDBError(err).Code != exceptions.CodeNotFound { - logger.ErrorContext(ctx, "Failed to get blacklisted token", "error", err) + serviceErr := exceptions.FromDBError(err) + if serviceErr.Code != exceptions.CodeNotFound { + logger.ErrorContext(ctx, "Failed to find session token by token ID", "error", err) return "", exceptions.NewInternalServerError() } - } else { - logger.WarnContext(ctx, "Token is revoked", "revokedAt", blt.CreatedAt) - return buildOAuthDynamicRegistrationIATLoginURL(buildOAuthDynamicRegistrationIATLoginURLOptions{ - domain: opts.domain, - state: opts.state, - challenge: opts.challenge, - challengeMethod: opts.challengeMethod, - redirectURI: opts.redirectURI, - }), nil + logger.WarnContext(ctx, "Session token not found", "serviceError", serviceErr) + return "", exceptions.NewUnauthorizedError() + } + + if stn.ExpiresAt.Before(time.Now()) { + logger.WarnContext(ctx, "Session token expired", "expiresAt", stn.ExpiresAt) + return "", exceptions.NewUnauthorizedError() } accountDTO, serviceErr := s.GetAccountByPublicIDAndVersion(ctx, GetAccountByPublicIDAndVersionOptions{ @@ -105,19 +118,6 @@ 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, @@ -136,7 +136,6 @@ func (s *Services) refreshTokenOAuthDynamicRegistrationIATLogin( cbURL, serviceErr := s.generateOAuthDynamicRegistrationIATCallback( ctx, generateOAuthDynamicRegistrationIATCallbackOptions{ - hostUsername: opts.hostUsername, requestID: opts.requestID, clientID: utils.Base62UUID(), accountPublicID: accountDTO.PublicID, @@ -167,8 +166,8 @@ func (s *Services) initiateOAuthDynamicRegistrationIATAuthAccounts( ctx context.Context, opts initiateOAuthDynamicRegistrationIATAuthAccountsOptions, ) (string, *exceptions.ServiceError) { - logger := s.buildLogger(opts.requestID, oauthDynamicRegistrationAccountsLocation, "CreateOAuthDynamicRegistrationAccount") - logger.InfoContext(ctx, "Creating OAuth dynamic registration account...") + logger := s.buildLogger(opts.requestID, oauthDynamicRegistrationLocation, "initiateOAuthDynamicRegistrationIATAuthAccounts") + logger.InfoContext(ctx, "Initiating OAuth dynamic registration IAT authorization for accounts...") if opts.sessionKey == "" { logger.DebugContext(ctx, "No session key provided") @@ -178,7 +177,7 @@ func (s *Services) initiateOAuthDynamicRegistrationIATAuthAccounts( } } - data, credsClientID, verified, found, err := s.cache.VerifyAccountCredentialsRegistrationSessionKey( + _, credsClientID, verified, found, err := s.cache.VerifyAccountCredentialsRegistrationSessionKey( ctx, cache.VerifyAccountCredentialsRegistrationSessionKeyOptions{ RequestID: opts.requestID, @@ -209,4 +208,1417 @@ func (s *Services) initiateOAuthDynamicRegistrationIATAuthAccounts( logger.ErrorContext(ctx, "Failed to delete account credentials registration session key", "error", err) return "", exceptions.NewInternalServerError() } + + return "", nil +} + +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, + accountPublicID uuid.UUID, + domain string, +) *exceptions.ServiceError { + 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 +} + +type buildOAuthDynamicRegistrationIATLoginURLOptions struct { + accClientID string + domain string + state string + challenge string + challengeMethod string + redirectURI string +} + +func buildOAuthDynamicRegistrationIATLoginURL(opts buildOAuthDynamicRegistrationIATLoginURLOptions) string { + 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 != "" { + queryParams.Add("code_challenge_method", opts.challengeMethod) + } + return oauthDynamicRegistrationIATPath + "/" + opts.accClientID + paths.OAuthAuth + paths.AuthLogin + "?" + queryParams.Encode() +} + +type buildOAuthDynamicRegistrationIATCallbackURLOptions struct { + 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.issuerDomain) + return opts.redirectURI + "?" + queryParams.Encode() +} + +type generateOAuthDynamicRegistrationIATCallbackOptions struct { + requestID string + clientID string + accountPublicID uuid.UUID + accountVersion int32 + challenge string + domain string + redirectURI string + state string + backendDomain string +} + +func (s *Services) generateOAuthDynamicRegistrationIATCallback( + ctx context.Context, + opts generateOAuthDynamicRegistrationIATCallbackOptions, +) (string, *exceptions.ServiceError) { + logger := s.buildLogger( + opts.requestID, + oauthDynamicRegistrationLocation, + "generateOAuthDynamicRegistrationIATCallback", + ).With( + "clientId", opts.clientID, + "accountPublicId", opts.accountPublicID, + ) + logger.InfoContext(ctx, "Generating OAuth dynamic registration IAT callback...") + + code, err := s.cache.GenerateAccountCredentialsRegistrationIATCode( + ctx, + cache.GenerateAccountCredentialsRegistrationIATCodeOptions{ + RequestID: opts.requestID, + ClientID: opts.clientID, + AccountPublicID: opts.accountPublicID, + AccountVersion: opts.accountVersion, + Challenge: opts.challenge, + Domain: opts.domain, + }, + ) + if err != nil { + logger.ErrorContext(ctx, "Failed to generate account credentials registration IAT code", "error", err) + return "", exceptions.NewInternalServerError() + } + + return buildOAuthDynamicRegistrationIATCallbackURL(buildOAuthDynamicRegistrationIATCallbackURLOptions{ + redirectURI: opts.redirectURI, + code: code, + state: opts.state, + issuerDomain: opts.backendDomain, + }), nil +} + +type checkDynamicClientRegistrationDomainUsabilityOptions struct { + requestID string + domain string +} + +func (s *Services) checkDynamicRegistrationDomainUsability( + ctx context.Context, + opts checkDynamicClientRegistrationDomainUsabilityOptions, +) *exceptions.ServiceError { + logger := s.buildLogger(opts.requestID, dynamicRegistrationDomainsLocation, "checkDynamicRegistrationDomainUsability").With( + "domain", opts.domain, + ) + logger.InfoContext(ctx, "Checking dynamic registration domain usability") + + usage := database.DynamicRegistrationUsageAccount + domains := breakDomainIntoAllSubdomains(opts.domain) + var count int64 + var err error + if len(domains) > 1 { + count, err = s.database.CountDynamicRegistrationDomainsByDomainsAndUsages( + ctx, + database.CountDynamicRegistrationDomainsByDomainsAndUsagesParams{ + Domains: domains, + Usages: []database.DynamicRegistrationUsage{usage}, + }, + ) + } else { + count, err = s.database.CountDynamicRegistrationDomainsByDomainAndUsages( + ctx, + database.CountDynamicRegistrationDomainsByDomainAndUsagesParams{ + Domain: opts.domain, + Usages: []database.DynamicRegistrationUsage{usage}, + }, + ) + } + if err != nil { + logger.ErrorContext(ctx, "Failed to count dynamic registration domains by domain and usages", "error", err) + return exceptions.FromDBError(err) + } + if count == 0 { + logger.WarnContext(ctx, "Domain not registered for dynamic registration") + return exceptions.NewForbiddenError() + } + + return nil +} + +type oauthDynamicRegistrationIATAuthOptions struct { + requestID string + challenge string + challengeMethod string + domain string + redirectURI string + state string +} + +func (s *Services) oauthDynamicRegistrationIATAuth( + ctx context.Context, + opts oauthDynamicRegistrationIATAuthOptions, +) (string, *exceptions.ServiceError) { + logger := s.buildLogger( + opts.requestID, + oauthDynamicRegistrationLocation, + "oauthDynamicRegistrationIATAuth", + ).With( + "domain", opts.domain, + "redirectUri", opts.redirectURI, + ) + logger.InfoContext(ctx, "Handling OAuth dynamic registration IAT auth...") + + if serviceErr := s.checkDynamicRegistrationDomainUsability( + ctx, + checkDynamicClientRegistrationDomainUsabilityOptions{ + requestID: opts.requestID, + domain: opts.domain, + }, + ); serviceErr != nil { + logger.InfoContext(ctx, "Dynamic registration domain not usable", "serviceError", serviceErr) + return "", serviceErr + } + + hashedChallenge, serviceErr := hashChallenge(opts.challenge, opts.challengeMethod) + if serviceErr != nil { + logger.ErrorContext(ctx, "Invalid code challenge", "serviceError", serviceErr) + return "", serviceErr + } + + clientID, err := s.cache.SaveAccountCredentialsDynamicRegistrationIATAuth( + ctx, + cache.SaveAccountCredentialsDynamicRegistrationIATAuthOptions{ + Domain: opts.domain, + RequestID: opts.requestID, + State: opts.state, + RedirectURI: opts.redirectURI, + Challenge: hashedChallenge, + }, + ) + if err != nil { + logger.ErrorContext(ctx, "Failed to save account credentials dynamic registration IAT auth", "error", err) + return "", exceptions.NewInternalServerError() + } + + return buildOAuthDynamicRegistrationIATLoginURL(buildOAuthDynamicRegistrationIATLoginURLOptions{ + accClientID: clientID, + domain: opts.domain, + state: opts.state, + challenge: opts.challenge, + challengeMethod: opts.challengeMethod, + redirectURI: opts.redirectURI, + }), nil +} + +type InitiateOAuthDynamicRegistrationIATAuthOptions struct { + RequestID string + Origin string + Domain string + State string + SessionKey string + RefreshToken string + Challenge string + ChallengeMethod string + RedirectURI string + BackendDomain string +} + +func (s *Services) InitiateOAuthDynamicRegistrationIATAuth( + ctx context.Context, + opts InitiateOAuthDynamicRegistrationIATAuthOptions, +) (string, *exceptions.ServiceError) { + logger := s.buildLogger( + opts.RequestID, + oauthDynamicRegistrationLocation, + "InitiateOAuthDynamicRegistrationIATAuth", + ).With( + "redirectUri", opts.RedirectURI, + ) + logger.InfoContext(ctx, "Starting OAuth dynamic registration IAT authorization...") + + if opts.Origin == "" { + logger.WarnContext(ctx, "Origin header is missing") + return "", exceptions.NewUnauthorizedError() + } + + parsedOrigin, err := url.Parse(opts.Origin) + if err != nil { + logger.WarnContext(ctx, "Invalid origin header", "error", err) + return "", exceptions.NewUnauthorizedError() + } + if parsedOrigin.Host != opts.Domain { + logger.WarnContext(ctx, "Origin header does not match domain", "originHost", parsedOrigin.Host) + return "", exceptions.NewUnauthorizedError() + } + + if opts.SessionKey == "" { + if opts.RefreshToken == "" { + logger.InfoContext(ctx, "No session key or refresh token provided, redirecting to login") + return s.oauthDynamicRegistrationIATAuth(ctx, oauthDynamicRegistrationIATAuthOptions{ + requestID: opts.RequestID, + challenge: opts.Challenge, + challengeMethod: opts.ChallengeMethod, + domain: opts.Domain, + redirectURI: opts.RedirectURI, + state: opts.State, + }) + } + + logger.InfoContext(ctx, "No session key provided, attempting to refresh with refresh token") + return s.refreshTokenOAuthDynamicRegistrationIATLogin( + ctx, + refreshTokenOAuthDynamicRegistrationIATLoginOptions{ + requestID: opts.RequestID, + refreshToken: opts.RefreshToken, + challenge: opts.Challenge, + challengeMethod: opts.ChallengeMethod, + domain: opts.Domain, + redirectURI: opts.RedirectURI, + state: opts.State, + backendDomain: opts.BackendDomain, + }, + ) + } + + data, credsClientID, verified, found, err := s.cache.VerifyAccountCredentialsRegistrationSessionKey( + ctx, + cache.VerifyAccountCredentialsRegistrationSessionKeyOptions{ + RequestID: opts.RequestID, + SessionKey: opts.SessionKey, + Domain: opts.Domain, + }, + ) + if err != nil { + logger.ErrorContext(ctx, "Failed to verify account credentials registration session key", "error", err) + return "", exceptions.NewInternalServerError() + } + if !found { + logger.WarnContext(ctx, "Account credentials registration session key not found") + return "", exceptions.NewUnauthorizedError() + } + + if !verified { + logger.WarnContext(ctx, "Account credentials registration session key is not verified") + return "", exceptions.NewUnauthorizedError() + } + + if err := s.cache.DeleteAccountCredentialsRegistrationSessionKey( + ctx, + cache.DeleteAccountCredentialsRegistrationSessionKeyOptions{ + RequestID: opts.RequestID, + ClientID: credsClientID, + }, + ); err != nil { + logger.ErrorContext(ctx, "Failed to delete account credentials registration session key", "error", err) + return "", exceptions.NewInternalServerError() + } + + accountDTO, serviceErr := s.GetAccountByPublicIDAndVersion(ctx, GetAccountByPublicIDAndVersionOptions{ + RequestID: opts.RequestID, + PublicID: data.AccountPublicID, + Version: data.AccountVersion, + }) + if serviceErr != nil { + logger.WarnContext(ctx, "Failed to get account by public ID and version", "serviceError", serviceErr) + return "", serviceErr + } + + hashedChallenge, serviceErr := hashChallenge(opts.Challenge, opts.ChallengeMethod) + if serviceErr != nil { + logger.ErrorContext(ctx, "Invalid code challenge", "serviceError", serviceErr) + return "", serviceErr + } + + logger.InfoContext(ctx, "Successfully verified account credentials registration IAT session key, creating code...") + cbURL, serviceErr := s.generateOAuthDynamicRegistrationIATCallback( + ctx, + generateOAuthDynamicRegistrationIATCallbackOptions{ + requestID: opts.RequestID, + clientID: credsClientID, + accountPublicID: accountDTO.PublicID, + accountVersion: accountDTO.Version(), + challenge: hashedChallenge, + domain: opts.Domain, + redirectURI: opts.RedirectURI, + state: opts.State, + backendDomain: opts.BackendDomain, + }, + ) + if serviceErr != nil { + logger.ErrorContext(ctx, "Failed to generate OAuth dynamic registration IAT callback", "serviceErr", serviceErr) + return "", serviceErr + } + + return cbURL, nil +} + +type OAuthDynamicRegistrationIATAuthRenderOptions struct { + RequestID string + ACCClientID string + State string + Domain string + CodeChallenge string + CodeChallengeMethod string + RedirectURI string +} + +func (s *Services) OAuthDynamicRegistrationIATAuthRender( + ctx context.Context, + opts OAuthDynamicRegistrationIATAuthRenderOptions, +) (string, *exceptions.ServiceError) { + logger := s.buildLogger( + opts.RequestID, + oauthDynamicRegistrationLocation, + "OAuthDynamicRegistrationIATAuthRender", + ).With( + "redirectUri", opts.RedirectURI, + ) + logger.InfoContext(ctx, "Starting OAuth dynamic registration IAT authorization html render...") + + data, found, err := s.cache.GetAccountCredentialsDynamicRegistrationAuthIAT( + ctx, + cache.GetAccountCredentialsDynamicRegistrationIATAuthOptions{ + RequestID: opts.RequestID, + ClientID: opts.ACCClientID, + }, + ) + if err != nil { + logger.ErrorContext(ctx, "Failed to get account credentials dynamic registration IAT", "error", err) + return "", exceptions.NewInternalServerError() + } + if !found { + logger.WarnContext(ctx, "Account credentials dynamic registration IAT not found") + return "", exceptions.NewForbiddenError() + } + + if data.Domain != opts.Domain { + logger.WarnContext(ctx, "OAuth Domain does not match", "dataDomain", data.Domain) + return "", exceptions.NewUnauthorizedError() + } + if data.State != opts.State { + logger.WarnContext(ctx, "OAuth State does not match") + return "", exceptions.NewUnauthorizedError() + } + if data.RedirectURI != opts.RedirectURI { + logger.WarnContext(ctx, "OAuth Redirect URI does not match") + return "", exceptions.NewUnauthorizedError() + } + + csrfToken, err := s.cache.SaveAccountCredentialsDynamicRegistrationIATLoginCSRF( + ctx, + cache.SaveAccountCredentialsDynamicRegistrationIATLoginCSRFOptions{ + RequestID: opts.RequestID, + ClientID: opts.ACCClientID, + Domain: opts.Domain, + }, + ) + if err != nil { + logger.ErrorContext(ctx, "Failed to save account credentials dynamic registration IAT auth CSRF token", "error", err) + return "", exceptions.NewInternalServerError() + } + + loginHTML, err := templates.BuildAccountDynamicRegistrationIATAuthTemplate( + templates.AccountDynamicRegistrationIATAuthOptions{ + ACCClientID: opts.ACCClientID, + CSRFToken: csrfToken, + Domain: opts.Domain, + State: opts.State, + CodeChallenge: opts.CodeChallenge, + CodeChallengeMethod: opts.CodeChallengeMethod, + RedirectURI: opts.RedirectURI, + AppleEnabled: s.oauthProviders.IsAppleEnabled(), + FacebookEnabled: s.oauthProviders.IsFacebookEnabled(), + GitHubEnabled: s.oauthProviders.IsGitHubEnabled(), + GoogleEnabled: s.oauthProviders.IsGoogleEnabled(), + MicrosoftEnabled: s.oauthProviders.IsMicrosoftEnabled(), + }, + ) + if err != nil { + logger.ErrorContext(ctx, "Failed to build account dynamic registration IAT auth template", "error", err) + return "", exceptions.NewInternalServerError() + } + + return loginHTML, nil +} + +type OAuthDynamicRegistrationIATAuthReRenderOptions struct { + RequestID string + Errors []string + CSRFToken string + ACCClientID string + State string + Domain string + CodeChallenge string + CodeChallengeMethod string + RedirectURI string +} + +func (s *Services) OAuthDynamicRegistrationIATAuthReRender( + ctx context.Context, + opts OAuthDynamicRegistrationIATAuthReRenderOptions, +) (string, *exceptions.ServiceError) { + logger := s.buildLogger( + opts.RequestID, + oauthDynamicRegistrationLocation, + "OAuthDynamicRegistrationIATAuthReRender", + ).With( + "clientId", opts.ACCClientID, + "redirectUri", opts.RedirectURI, + ) + logger.InfoContext(ctx, "Re-rendering OAuth dynamic registration IAT authorization html...") + + loginHTML, err := templates.BuildAccountDynamicRegistrationIATAuthTemplate( + templates.AccountDynamicRegistrationIATAuthOptions{ + Errors: opts.Errors, + ACCClientID: opts.ACCClientID, + CSRFToken: opts.CSRFToken, + Domain: opts.Domain, + State: opts.State, + CodeChallenge: opts.CodeChallenge, + CodeChallengeMethod: opts.CodeChallengeMethod, + RedirectURI: opts.RedirectURI, + AppleEnabled: s.oauthProviders.IsAppleEnabled(), + FacebookEnabled: s.oauthProviders.IsFacebookEnabled(), + GitHubEnabled: s.oauthProviders.IsGitHubEnabled(), + GoogleEnabled: s.oauthProviders.IsGoogleEnabled(), + MicrosoftEnabled: s.oauthProviders.IsMicrosoftEnabled(), + }, + ) + if err != nil { + logger.ErrorContext(ctx, "Failed to build account dynamic registration IAT auth template", "error", err) + return "", exceptions.NewInternalServerError() + } + + return loginHTML, nil +} + +type OAuthDynamicRegistrationIATLoginOptions struct { + RequestID string + ACCClientID string + Domain string + CSRFToken string + CodeChallenge string + CodeChallengeMethod string + State string + RedirectURI string + Email string + Password string + BackendDomain string +} + +func (s *Services) OAuthDynamicRegistrationIATLogin( + ctx context.Context, + opts OAuthDynamicRegistrationIATLoginOptions, +) (string, string, bool, *exceptions.ServiceError) { + logger := s.buildLogger(opts.RequestID, oauthDynamicRegistrationLocation, "OAuthDynamicRegistrationIATLoginPost").With( + "clientId", opts.ACCClientID, + "domain", opts.Domain, + ) + logger.InfoContext(ctx, "Logging in with OAuth dynamic registration IAT...") + + validCSRF, err := s.cache.VerifyAccountCredentialsDynamicRegistrationIATLoginCSRF( + ctx, + cache.VerifyAccountCredentialsDynamicRegistrationIATLoginCSRFOptions{ + RequestID: opts.RequestID, + ClientID: opts.ACCClientID, + Domain: opts.Domain, + CSRFToken: opts.CSRFToken, + }, + ) + if err != nil { + logger.ErrorContext(ctx, "Failed to verify account credentials dynamic registration IAT auth CSRF token", "error", err) + return "", "", false, exceptions.NewInternalServerError() + } + if !validCSRF { + logger.WarnContext(ctx, "Invalid CSRF token") + return "", "", false, exceptions.NewForbiddenError() + } + + data, found, err := s.cache.GetAccountCredentialsDynamicRegistrationAuthIAT(ctx, cache.GetAccountCredentialsDynamicRegistrationIATAuthOptions{ + RequestID: opts.RequestID, + ClientID: opts.ACCClientID, + }) + if err != nil { + logger.ErrorContext(ctx, "Failed to get account credentials dynamic registration IAT", "error", err) + return "", "", false, exceptions.NewInternalServerError() + } + if !found { + logger.ErrorContext(ctx, "Account credentials dynamic registration IAT not found") + return "", "", false, exceptions.NewNotFoundError() + } + + if data.Domain != opts.Domain { + logger.WarnContext(ctx, "OAuth Domain does not match", "dataDomain", data.Domain) + return "", "", false, exceptions.NewUnauthorizedError() + } + if data.State != opts.State { + logger.WarnContext(ctx, "OAuth State does not match") + return "", "", false, exceptions.NewUnauthorizedError() + } + if data.RedirectURI != opts.RedirectURI { + logger.WarnContext(ctx, "OAuth Redirect URI does not match") + return "", "", false, exceptions.NewUnauthorizedError() + } + + accountDTO, serviceErr := s.GetAccountByEmail(ctx, GetAccountByEmailOptions{ + RequestID: opts.RequestID, + Email: opts.Email, + }) + if serviceErr != nil { + if serviceErr.Code != exceptions.CodeNotFound { + return "", "", false, serviceErr + } + + logger.WarnContext(ctx, "Account was not found", "error", serviceErr) + return "", "", false, exceptions.NewUnauthorizedError() + } + if _, err := s.database.FindAccountAuthProviderByAccountPublicIdAndProvider( + ctx, + database.FindAccountAuthProviderByAccountPublicIdAndProviderParams{ + AccountPublicID: accountDTO.PublicID, + Provider: database.AuthProviderLocal, + }, + ); err != nil { + serviceErr := exceptions.FromDBError(err) + if serviceErr.Code != exceptions.CodeNotFound { + logger.ErrorContext(ctx, "Failed to find account auth provider", "error", err) + return "", "", false, serviceErr + } + + logger.WarnContext(ctx, "Account auth provider not found", "error", err) + return "", "", false, exceptions.NewUnauthorizedError() + } + + passwordVerified, err := utils.Argon2CompareHash(opts.Password, accountDTO.Password()) + if err != nil { + logger.ErrorContext(ctx, "Failed to verify password", "error", err) + return "", "", false, exceptions.NewInternalServerError() + } + if !passwordVerified { + logger.WarnContext(ctx, "Passwords do not match") + return "", "", false, exceptions.NewUnauthorizedError() + } + if !accountDTO.EmailVerified() { + logger.InfoContext(ctx, "Account is not confirmed") + return "", "", false, exceptions.NewForbiddenError() + } + + default2FAConfig, serviceErr := s.getDefaultAccount2FAConfigInternal(ctx, getDefaultAccount2FAConfigInternalOptions{ + requestID: opts.RequestID, + accountPublicID: accountDTO.PublicID, + }) + if serviceErr != nil { + logger.ErrorContext(ctx, "Failed to get default 2FA config", "serviceError", serviceErr) + return "", "", false, serviceErr + } + if default2FAConfig != nil { + logger.InfoContext(ctx, "Two-Factor is enabled, proceeding to 2FA step") + sessionID, err := s.cache.SaveAccountCredentialsDynamicRegistrationIAT2FA( + ctx, + cache.SaveAccountCredentialsDynamicRegistrationIAT2FAOptions{ + RequestID: opts.RequestID, + AccountPublicID: accountDTO.PublicID, + AccountVersion: accountDTO.Version(), + RedirectURI: opts.RedirectURI, + Domain: data.Domain, + ClientID: opts.ACCClientID, + State: data.State, + TwoFAType: string(default2FAConfig.TwoFactorType), + TwoFATTL: s.jwt.Get2FATTL(), + }, + ) + if err != nil { + logger.ErrorContext(ctx, "Failed to save account credentials dynamic registration IAT 2FA", "error", err) + return "", "", false, exceptions.NewInternalServerError() + } + + if default2FAConfig.TwoFactorType == database.TwoFactorTypeEmail { + code, err := s.cache.AddTwoFactorCode(ctx, cache.AddTwoFactorCodeOptions{ + RequestID: opts.RequestID, + AccountID: accountDTO.ID(), + TTL: s.jwt.Get2FATTL(), + }) + if err != nil { + logger.ErrorContext(ctx, "Failed to add two factor code", "error", err) + return "", "", false, exceptions.NewInternalServerError() + } + + if err := s.mail.Publish2FAEmail(ctx, mailer.TwoFactorEmailOptions{ + RequestID: opts.RequestID, + Email: accountDTO.Email, + Name: accountDTO.GivenName, + Code: code, + }); err != nil { + logger.ErrorContext(ctx, "Failed to send two factor code email", "error", err) + return "", "", false, exceptions.NewInternalServerError() + } + + logger.InfoContext(ctx, "Sent two factor code email successfully") + } + + if err := s.cache.DeleteAccountCredentialsDynamicRegistrationIATAuth( + ctx, + cache.DeleteAccountCredentialsDynamicRegistrationIATAuthOptions{ + RequestID: opts.RequestID, + ClientID: opts.ACCClientID, + }, + ); err != nil { + logger.ErrorContext(ctx, "Failed to delete account credentials dynamic registration IAT auth", "error", err) + return "", "", false, exceptions.NewInternalServerError() + } + + queryParams := make(url.Values) + queryParams.Add("client_id", data.Domain) + queryParams.Add("redirect_uri", opts.RedirectURI) + queryParams.Add("state", data.State) + queryParams.Add("code_challenge", opts.CodeChallenge) + if opts.CodeChallengeMethod != "" { + queryParams.Add("code_challenge_method", opts.CodeChallengeMethod) + } + return oauthDynamicRegistrationIATPath + "/" + opts.ACCClientID + paths.OAuthAuth + + paths.AuthLogin + paths.Auth2FA + "?" + queryParams.Encode(), sessionID, false, nil + } + + if serviceErr := s.checkIATRegistrationDomain(ctx, opts.RequestID, accountDTO.PublicID, data.Domain); serviceErr != nil { + if serviceErr.Code != exceptions.CodeNotFound { + logger.ErrorContext(ctx, "Failed to get registration domain", "serviceError", serviceErr) + return "", "", false, serviceErr + } + + if err := s.cache.DeleteAccountCredentialsDynamicRegistrationIATAuth( + ctx, + cache.DeleteAccountCredentialsDynamicRegistrationIATAuthOptions{ + RequestID: opts.RequestID, + ClientID: opts.ACCClientID, + }, + ); err != nil { + logger.ErrorContext(ctx, "Failed to delete account credentials dynamic registration IAT auth", "error", err) + return "", "", false, exceptions.NewInternalServerError() + } + + logger.WarnContext(ctx, "Registration domain not found") + return "", "", false, exceptions.NewForbiddenError() + } + + sessionKey, err := s.cache.CreateAccountCredentialsRegistrationSessionKey( + ctx, + cache.CreateAccountCredentialsRegistrationSessionKeyOptions{ + RequestID: opts.RequestID, + ClientID: opts.ACCClientID, + Domain: opts.Domain, + AccountPublicID: accountDTO.PublicID, + AccountVersion: accountDTO.Version(), + }, + ) + if err != nil { + logger.ErrorContext(ctx, "Failed to create account credentials registration session", "error", err) + return "", "", false, exceptions.NewInternalServerError() + } + + return oauthDynamicRegistrationIATAuthPath, sessionKey, true, nil +} + +type OAuthDynamicRegistrationIAT2FARenderOptions struct { + RequestID string + Domain string + ACCClientID string + SessionID string + Challenge string + ChallengeMethod string + State string + RedirectURI string +} + +func (s *Services) OAuthDynamicRegistrationIAT2FARender( + ctx context.Context, + opts OAuthDynamicRegistrationIAT2FARenderOptions, +) (string, *exceptions.ServiceError) { + logger := s.buildLogger(opts.RequestID, oauthDynamicRegistrationLocation, "OAuthDynamicRegistrationIAT2FARender").With( + "clientId", opts.ACCClientID, + ) + logger.InfoContext(ctx, "Handling OAuth dynamic registration IAT 2FA...") + + data, found, err := s.cache.GetAccountCredentialsDynamicRegistrationIAT2FA(ctx, cache.GetAccountCredentialsDynamicRegistrationIAT2FAOptions{ + RequestID: opts.RequestID, + SessionID: opts.SessionID, + }) + if err != nil { + logger.ErrorContext(ctx, "Failed to get account credentials dynamic registration IAT 2FA", "error", err) + return "", exceptions.NewInternalServerError() + } + if !found { + logger.WarnContext(ctx, "Failed to get account credentials dynamic registration IAT 2FA") + return "", exceptions.NewUnauthorizedError() + } + + if opts.ACCClientID != data.ClientID { + logger.WarnContext(ctx, "Client IDs do not match", "sessionClientId", data.ClientID) + return "", exceptions.NewUnauthorizedError() + } + if data.Domain != opts.Domain { + logger.WarnContext(ctx, "OAuth Domain does not match", "dataDomain", data.Domain) + return "", exceptions.NewUnauthorizedError() + } + if data.State != opts.State { + logger.WarnContext(ctx, "OAuth State does not match") + return "", exceptions.NewUnauthorizedError() + } + if data.RedirectURI != opts.RedirectURI { + logger.WarnContext(ctx, "OAuth Redirect URI does not match") + return "", exceptions.NewUnauthorizedError() + } + + csrfToken, err := s.cache.SaveAccountCredentialsDynamicRegistrationIAT2FACSRFToken( + ctx, + cache.SaveAccountCredentialsDynamicRegistrationIAT2FACSRFTokenOptions{ + RequestID: opts.RequestID, + SessionID: opts.SessionID, + TwoFATTL: s.jwt.Get2FATTL(), + }, + ) + if err != nil { + logger.ErrorContext(ctx, "Failed to save account credentials dynamic registration IAT 2FA CSRF token", "error", err) + return "", exceptions.NewInternalServerError() + } + + twoFAhtml, err := templates.BuildAccountDynamicRegistrationIAT2FATemplate( + templates.AccountDynamicRegistrationIAT2FAOptions{ + ACCClientID: opts.ACCClientID, + Domain: opts.Domain, + SessionID: opts.SessionID, + CSRFToken: csrfToken, + State: data.State, + CodeChallenge: opts.Challenge, + CodeChallengeMethod: opts.ChallengeMethod, + RedirectURI: data.RedirectURI, + }, + ) + if err != nil { + logger.ErrorContext(ctx, "Failed to build account dynamic registration IAT 2FA template", "error", err) + return "", exceptions.NewInternalServerError() + } + + return twoFAhtml, nil +} + +type OAuthDynamicRegistrationIAT2FAReRenderOptions struct { + RequestID string + Domain string + ACCClientID string + SessionID string + Errors []string + CSRFToken string + Challenge string + ChallengeMethod string + State string + RedirectURI string +} + +func (s *Services) OAuthDynamicRegistrationIAT2FAReRender( + ctx context.Context, + opts OAuthDynamicRegistrationIAT2FAReRenderOptions, +) (string, *exceptions.ServiceError) { + logger := s.buildLogger(opts.RequestID, oauthDynamicRegistrationLocation, "OAuthDynamicRegistrationIAT2FAReRender").With( + "clientId", opts.ACCClientID, + ) + logger.InfoContext(ctx, "Re-rendering OAuth dynamic registration IAT 2FA...") + + twoFAhtml, err := templates.BuildAccountDynamicRegistrationIAT2FATemplate( + templates.AccountDynamicRegistrationIAT2FAOptions{ + Errors: opts.Errors, + ACCClientID: opts.ACCClientID, + Domain: opts.Domain, + SessionID: opts.SessionID, + CSRFToken: opts.CSRFToken, + State: opts.State, + CodeChallenge: opts.Challenge, + CodeChallengeMethod: opts.ChallengeMethod, + RedirectURI: opts.RedirectURI, + }, + ) + if err != nil { + logger.ErrorContext(ctx, "Failed to build account dynamic registration IAT 2FA template", "error", err) + return "", exceptions.NewInternalServerError() + } + + return twoFAhtml, nil +} + +func map2FATypeTokens(twoFAType string) (tokens.TwoFAType, *exceptions.ServiceError) { + switch twoFAType { + case TwoFactorTypeEmail: + return tokens.TwoFATypeEmail, nil + case TwoFactorTypeTotp: + return tokens.TwoFATypeTOTP, nil + default: + return "", exceptions.NewValidationError("invalid two factor type") + } +} + +type OAuthDynamicRegistrationIATVerify2FACodeOptions struct { + RequestID string + ACCClientID string + Domain string + SessionID string + CSRFToken string + Code string + BackendDomain string +} + +func (s *Services) OAuthDynamicRegistrationIATVerify2FACode( + ctx context.Context, + opts OAuthDynamicRegistrationIATVerify2FACodeOptions, +) (string, string, *exceptions.ServiceError) { + logger := s.buildLogger(opts.RequestID, oauthDynamicRegistrationLocation, "OAuthDynamicRegistrationIATVerify2FACode").With( + "clientId", opts.ACCClientID, + "sessionId", opts.SessionID, + ) + logger.InfoContext(ctx, "Verifying OAuth dynamic registration IAT 2FA...") + + verifiedCSRF, err := s.cache.VerifyAccountCredentialsDynamicRegistrationIAT2FACSRFToken( + ctx, + cache.VerifyAccountCredentialsDynamicRegistrationIAT2FACSRFTokenOptions{ + RequestID: opts.RequestID, + SessionID: opts.SessionID, + CSRFToken: opts.CSRFToken, + }, + ) + if err != nil { + logger.ErrorContext(ctx, "Failed to verify account credentials dynamic registration IAT 2FA CSRF token", "error", err) + return "", "", exceptions.NewInternalServerError() + } + if !verifiedCSRF { + logger.WarnContext(ctx, "Invalid CSRF token") + return "", "", exceptions.NewForbiddenError() + } + + data, found, err := s.cache.GetAccountCredentialsDynamicRegistrationIAT2FA(ctx, cache.GetAccountCredentialsDynamicRegistrationIAT2FAOptions{ + RequestID: opts.RequestID, + SessionID: opts.SessionID, + }) + if err != nil { + logger.ErrorContext(ctx, "Failed to get account credentials dynamic registration IAT 2FA", "error", err) + return "", "", exceptions.NewInternalServerError() + } + if !found { + logger.WarnContext(ctx, "Failed to get account credentials dynamic registration IAT 2FA") + return "", "", exceptions.NewUnauthorizedError() + } + if opts.ACCClientID != data.ClientID { + logger.WarnContext(ctx, "Client IDs do not match", "sessionClientId", data.ClientID) + return "", "", exceptions.NewUnauthorizedError() + } + twoFAType, serviceErr := map2FATypeTokens(data.TwoFAType) + if serviceErr != nil { + logger.WarnContext(ctx, "Failed to map two factor type", "serviceError", serviceErr) + return "", "", serviceErr + } + + accountDTO, serviceErr := s.GetAccountByPublicIDAndVersion(ctx, GetAccountByPublicIDAndVersionOptions{ + RequestID: opts.RequestID, + PublicID: data.AccountPublicID, + Version: data.AccountVersion, + }) + if serviceErr != nil { + logger.WarnContext(ctx, "Failed to get account by public ID and version", "serviceError", serviceErr) + return "", "", serviceErr + } + if serviceErr := s.verifyAccount2FAInternal(ctx, verifyAccount2FAInternalOptions{ + requestID: opts.RequestID, + accountID: accountDTO.ID(), + accountPublicID: accountDTO.PublicID, + accountVersion: accountDTO.Version(), + twoFAType: twoFAType, + code: opts.Code, + }); serviceErr != nil { + logger.WarnContext(ctx, "Failed to verify account two factor", "serviceError", serviceErr) + return "", "", serviceErr + } + + if serviceErr := s.checkIATRegistrationDomain(ctx, opts.RequestID, accountDTO.PublicID, data.Domain); serviceErr != nil { + if serviceErr.Code != exceptions.CodeNotFound { + logger.ErrorContext(ctx, "Failed to get registration domain", "serviceError", serviceErr) + return "", "", serviceErr + } + + if err := s.cache.DeleteAccountCredentialsDynamicRegistrationIATAuth( + ctx, + cache.DeleteAccountCredentialsDynamicRegistrationIATAuthOptions{ + RequestID: opts.RequestID, + ClientID: opts.ACCClientID, + }, + ); err != nil { + logger.ErrorContext(ctx, "Failed to delete account credentials dynamic registration IAT auth", "error", err) + return "", "", exceptions.NewInternalServerError() + } + + logger.WarnContext(ctx, "Registration domain not found") + return "", "", exceptions.NewForbiddenError() + } + + sessionKey, err := s.cache.CreateAccountCredentialsRegistrationSessionKey( + ctx, + cache.CreateAccountCredentialsRegistrationSessionKeyOptions{ + RequestID: opts.RequestID, + ClientID: opts.ACCClientID, + Domain: opts.Domain, + AccountPublicID: accountDTO.PublicID, + AccountVersion: accountDTO.Version(), + }, + ) + if err != nil { + logger.ErrorContext(ctx, "Failed to create account credentials registration session", "error", err) + return "", "", exceptions.NewInternalServerError() + } + + return oauthDynamicRegistrationIATAuthPath, sessionKey, nil +} + +// TODO: add external callbacks + +type VerifyOAuthDynamicRegistrationIATCodeOptions struct { + BackendDomain string + RequestID string + Code string + CodeVerifier string + Domain string +} + +func (s *Services) VerifyOAuthDynamicRegistrationIATCode( + ctx context.Context, + opts VerifyOAuthDynamicRegistrationIATCodeOptions, +) (dtos.AuthDTO, *exceptions.ServiceError) { + logger := s.buildLogger(opts.RequestID, accountCredentialsRegistrationIATLocation, "VerifyOAuthDynamicRegistrationIATCode") + logger.InfoContext(ctx, "Verifying account credentials registration IAT code...") + + data, found, err := s.cache.VerifyAccountCredentialsRegistrationIATCode(ctx, cache.VerifyAccountCredentialsRegistrationIATCodeOptions{ + RequestID: opts.RequestID, + Code: opts.Code, + }) + if err != nil { + logger.ErrorContext(ctx, "Failed to verify account credentials registration IAT code", "error", err) + return dtos.AuthDTO{}, exceptions.NewInternalServerError() + } + if !found { + logger.DebugContext(ctx, "Account credentials registration IAT code not found or invalid") + return dtos.AuthDTO{}, exceptions.NewUnauthorizedError() + } + + if data.Domain != opts.Domain { + logger.WarnContext(ctx, "OAuth Domain does not match", "dataDomain", data.Domain) + return dtos.AuthDTO{}, exceptions.NewUnauthorizedError() + } + + ok, err := utils.CompareShaBase64(opts.CodeVerifier, data.Challenge) + if err != nil { + logger.ErrorContext(ctx, "Failed to compare challenge", "error", err) + return dtos.AuthDTO{}, exceptions.NewInternalServerError() + } + if !ok { + logger.WarnContext(ctx, "OAuth Code challenge verification failed") + return dtos.AuthDTO{}, exceptions.NewUnauthorizedError() + } + + accountDTO, serviceErr := s.GetAccountByPublicIDAndVersion(ctx, GetAccountByPublicIDAndVersionOptions{ + RequestID: opts.RequestID, + PublicID: data.AccountPublicID, + Version: data.AccountVersion, + }) + if serviceErr != nil { + logger.ErrorContext(ctx, "Failed to get account", "serviceError", serviceErr) + return dtos.AuthDTO{}, serviceErr + } + + signedToken, serviceErr := s.CreateAccountCredentialsRegistrationIAT(ctx, CreateAccountCredentialsRegistrationIATOptions{ + RequestID: opts.RequestID, AccountPublicID: accountDTO.PublicID, + AccountVersion: accountDTO.Version(), Domain: data.Domain, BackendDomain: opts.BackendDomain, + }) + if serviceErr != nil { + return dtos.AuthDTO{}, serviceErr + } + return dtos.NewAuthDTO(signedToken, s.jwt.GetDynamicRegistrationTTL()), nil + +} + +type OAuthDynamicRegistrationIATExtGetOptions struct { + RequestID string + ACCClientID string + Domain string + Provider string + CallbackURL string + RedirectURI string + State string + BackendDomain string +} + +func (s *Services) OAuthDynamicRegistrationIATExtGet( + ctx context.Context, + opts OAuthDynamicRegistrationIATExtGetOptions, +) (string, *exceptions.ServiceError) { + logger := s.buildLogger(opts.RequestID, oauthLocation, "OAuthDynamicRegistrationIATExtGet").With( + "Provider", opts.Provider, + ) + logger.InfoContext(ctx, "External logging in account...") + + authUrlOpts := oauth.AuthorizationURLOptions{ + RequestID: opts.RequestID, + Scopes: make([]oauth.Scope, 0), + RedirectURL: opts.CallbackURL, + } + var oauthUrl, state string + var serviceErr *exceptions.ServiceError + switch opts.Provider { + case AuthProviderApple: + oauthUrl, state, serviceErr = s.oauthProviders.GetAppleAuthorizationURL(ctx, authUrlOpts) + case AuthProviderFacebook: + oauthUrl, state, serviceErr = s.oauthProviders.GetFacebookAuthorizationURL(ctx, authUrlOpts) + case AuthProviderGitHub: + oauthUrl, state, serviceErr = s.oauthProviders.GetGithubAuthorizationURL(ctx, authUrlOpts) + case AuthProviderGoogle: + oauthUrl, state, serviceErr = s.oauthProviders.GetGoogleAuthorizationURL(ctx, authUrlOpts) + case AuthProviderMicrosoft: + oauthUrl, state, serviceErr = s.oauthProviders.GetMicrosoftAuthorizationURL(ctx, authUrlOpts) + default: + logger.ErrorContext(ctx, "Provider must be 'apple', 'facebook', 'github', 'google' and 'microsoft'") + return "", exceptions.NewInternalServerError() + } + if serviceErr != nil { + logger.ErrorContext(ctx, "Failed to get authorization url or State", "error", serviceErr) + return "", serviceErr + } + + data, found, err := s.cache.GetAccountCredentialsDynamicRegistrationAuthIAT(ctx, cache.GetAccountCredentialsDynamicRegistrationIATAuthOptions{ + RequestID: opts.RequestID, + ClientID: opts.ACCClientID, + }) + if err != nil { + logger.ErrorContext(ctx, "Failed to get account credentials dynamic registration IAT", "error", err) + return "", exceptions.NewInternalServerError() + } + if !found { + logger.ErrorContext(ctx, "Account credentials dynamic registration IAT not found") + return "", exceptions.NewNotFoundError() + } + + if data.Domain != opts.Domain { + logger.WarnContext(ctx, "OAuth Domain does not match", "dataDomain", data.Domain) + return "", exceptions.NewUnauthorizedError() + } + if data.State != opts.State { + logger.WarnContext(ctx, "OAuth State does not match") + return "", exceptions.NewUnauthorizedError() + } + if data.RedirectURI != opts.RedirectURI { + logger.WarnContext(ctx, "OAuth Redirect URI does not match") + return "", exceptions.NewUnauthorizedError() + } + + if err := s.cache.SaveAccountCredentialsDynamicRegistrationIATExtAuth(ctx, cache.SaveAccountCredentialsDynamicRegistrationIATExtAuthOptions{ + RequestID: opts.RequestID, + ClientID: opts.ACCClientID, + Domain: opts.Domain, + Provider: opts.Provider, + State: state, + RequestState: opts.State, + }); err != nil { + logger.ErrorContext(ctx, "Failed to save account credentials dynamic registration IAT external auth", "error", err) + return "", exceptions.NewInternalServerError() + } + + logger.InfoContext(ctx, "Saved account credentials dynamic registration IAT external auth successfully") + return oauthUrl, nil +} + +type OAuthDynamicRegistrationIATExtCBOptions struct { + RequestID string + ACCClientID string + Provider string + State string + Code string + RedirectURL string + BackendDomain string +} + +func (s *Services) OAuthDynamicRegistrationIATExtCB( + ctx context.Context, + opts OAuthDynamicRegistrationIATExtCBOptions, +) (string, *exceptions.ServiceError) { + logger := s.buildLogger(opts.RequestID, oauthLocation, "OAuthDynamicRegistrationIATExtCB") + logger.InfoContext(ctx, "External callback for account...") + + data, found, err := s.cache.GetAccountCredentialsDynamicRegistrationIATExtAuth(ctx, cache.GetAccountCredentialsDynamicRegistrationIATExtAuthOptions{ + RequestID: opts.RequestID, + Provider: opts.Provider, + State: opts.State, + }) + if err != nil { + logger.ErrorContext(ctx, "Failed to get account credentials dynamic registration IAT external auth", "error", err) + return "", exceptions.NewInternalServerError() + } + if !found { + logger.ErrorContext(ctx, "Account credentials dynamic registration IAT external auth not found") + return "", exceptions.NewNotFoundError() + } + + if data.ClientID != opts.ACCClientID { + logger.WarnContext(ctx, "Client IDs do not match", "dataClientId", data.ClientID) + return "", exceptions.NewUnauthorizedError() + } + + accessTokenOpts := oauth.AccessTokenOptions{ + RequestID: opts.RequestID, + Code: opts.Code, + Scopes: oauthScopes, + RedirectURL: opts.RedirectURL, + } + var token string + var serviceErr *exceptions.ServiceError + switch opts.Provider { + case AuthProviderFacebook: + token, serviceErr = s.oauthProviders.GetFacebookAccessToken(ctx, accessTokenOpts) + case AuthProviderGitHub: + token, serviceErr = s.oauthProviders.GetGithubAccessToken(ctx, accessTokenOpts) + case AuthProviderGoogle: + token, serviceErr = s.oauthProviders.GetGoogleAccessToken(ctx, accessTokenOpts) + case AuthProviderMicrosoft: + token, serviceErr = s.oauthProviders.GetMicrosoftAccessToken(ctx, accessTokenOpts) + default: + logger.ErrorContext(ctx, "Provider must be 'facebook', 'github', 'google' and 'microsoft'") + return "", exceptions.NewInternalServerError() + } + if serviceErr != nil { + logger.WarnContext(ctx, "Failed to get oauth access token", "error", serviceErr) + return "", serviceErr + } + + authData, found, err := s.cache.GetAccountCredentialsDynamicRegistrationAuthIAT(ctx, cache.GetAccountCredentialsDynamicRegistrationIATAuthOptions{ + RequestID: opts.RequestID, + ClientID: opts.ACCClientID, + }) + if err != nil { + logger.ErrorContext(ctx, "Failed to get account credentials dynamic registration IAT", "error", err) + return "", exceptions.NewInternalServerError() + } + if !found { + logger.ErrorContext(ctx, "Account credentials dynamic registration IAT not found") + return "", exceptions.NewNotFoundError() + } + + if authData.Domain != data.Domain { + logger.WarnContext(ctx, "OAuth Domain does not match", "dataDomain", authData.Domain) + return "", exceptions.NewUnauthorizedError() + } + if authData.State != data.RequestState { + logger.WarnContext(ctx, "OAuth State does not match") + return "", exceptions.NewUnauthorizedError() + } + + userData, serviceErr := s.extOAuthUser(ctx, logger, extOAuthUserOptions{ + requestID: opts.RequestID, + provider: opts.Provider, + token: token, + }) + if serviceErr != nil { + return "", serviceErr + } + + accountDTO, serviceErr := s.GetAccountByEmail(ctx, GetAccountByEmailOptions{ + RequestID: opts.RequestID, + Email: userData.Email, + }) + if serviceErr != nil { + return "", serviceErr + } + if _, serviceErr := s.GetAccountAuthProvider(ctx, GetAccountAuthProviderOptions{ + RequestID: opts.RequestID, + PublicID: accountDTO.PublicID, + Provider: opts.Provider, + }); serviceErr != nil { + if serviceErr.Code != exceptions.CodeNotFound { + logger.ErrorContext(ctx, "Failed to get account auth provider", "serviceError", serviceErr) + return "", serviceErr + } + + logger.WarnContext(ctx, "Account auth provider not found", "serviceError", serviceErr) + return "", exceptions.NewUnauthorizedError() + } + + cbURL, serviceErr := s.generateOAuthDynamicRegistrationIATCallback( + ctx, + generateOAuthDynamicRegistrationIATCallbackOptions{ + requestID: opts.RequestID, + clientID: opts.ACCClientID, + accountPublicID: accountDTO.PublicID, + accountVersion: accountDTO.Version(), + challenge: authData.Challenge, + domain: authData.Domain, + redirectURI: authData.RedirectURI, + state: authData.State, + backendDomain: opts.BackendDomain, + }, + ) + if serviceErr != nil { + logger.ErrorContext(ctx, "Failed to generate OAuth dynamic registration IAT callback", "serviceError", serviceErr) + return "", serviceErr + } + + return cbURL, nil +} + +type OAuthDynamicRegistrationIATExtAppleCBOptions struct { + RequestID string + ACCClientID string + Email string + Code string + State string + RedirectURL string + BackendDomain string +} + +func (s *Services) OAuthDynamicRegistrationIATExtAppleCB( + ctx context.Context, + opts OAuthDynamicRegistrationIATExtAppleCBOptions, +) (string, *exceptions.ServiceError) { + logger := s.buildLogger(opts.RequestID, oauthLocation, "OAuthDynamicRegistrationIATExtAppleCB") + logger.InfoContext(ctx, "External callback for account...") + + data, found, err := s.cache.GetAccountCredentialsDynamicRegistrationIATExtAuth(ctx, cache.GetAccountCredentialsDynamicRegistrationIATExtAuthOptions{ + RequestID: opts.RequestID, + Provider: AuthProviderApple, + State: opts.State, + }) + if err != nil { + logger.ErrorContext(ctx, "Failed to get account credentials dynamic registration IAT external auth", "error", err) + return "", exceptions.NewInternalServerError() + } + if !found { + logger.ErrorContext(ctx, "Account credentials dynamic registration IAT external auth not found") + return "", exceptions.NewNotFoundError() + } + + if data.ClientID != opts.ACCClientID { + logger.WarnContext(ctx, "Client IDs do not match", "dataClientId", data.ClientID) + return "", exceptions.NewUnauthorizedError() + } + + idToken, serviceErr := s.oauthProviders.GetAppleIDToken(ctx, oauth.AccessTokenOptions{ + RequestID: opts.RequestID, + Code: opts.Code, + Scopes: oauthScopes, + }) + if serviceErr != nil { + logger.WarnContext(ctx, "Failed to get apple AccountID token", "error", serviceErr) + return "", serviceErr + } + + authData, found, err := s.cache.GetAccountCredentialsDynamicRegistrationAuthIAT(ctx, cache.GetAccountCredentialsDynamicRegistrationIATAuthOptions{ + RequestID: opts.RequestID, + ClientID: opts.ACCClientID, + }) + if err != nil { + logger.ErrorContext(ctx, "Failed to get account credentials dynamic registration IAT", "error", err) + return "", exceptions.NewInternalServerError() + } + if !found { + logger.ErrorContext(ctx, "Account credentials dynamic registration IAT not found") + return "", exceptions.NewNotFoundError() + } + + if authData.Domain != data.Domain { + logger.WarnContext(ctx, "OAuth Domain does not match", "dataDomain", authData.Domain) + return "", exceptions.NewUnauthorizedError() + } + if authData.State != data.RequestState { + logger.WarnContext(ctx, "OAuth State does not match") + return "", exceptions.NewUnauthorizedError() + } + + ok, serviceErr := s.oauthProviders.ValidateAppleIDToken(ctx, oauth.ValidateAppleIDTokenOptions{ + RequestID: opts.RequestID, + Token: idToken, + Email: opts.Email, + }) + if serviceErr != nil { + logger.ErrorContext(ctx, "Failed to validate apple AccountID token", "error", serviceErr) + return "", serviceErr + } + if !ok { + logger.WarnContext(ctx, "Apple account is not verified") + return "", exceptions.NewUnauthorizedError() + } + + accountDTO, serviceErr := s.GetAccountByEmail(ctx, GetAccountByEmailOptions{ + RequestID: opts.RequestID, + Email: opts.Email, + }) + if serviceErr != nil { + return "", serviceErr + } + if _, serviceErr := s.GetAccountAuthProvider(ctx, GetAccountAuthProviderOptions{ + RequestID: opts.RequestID, + PublicID: accountDTO.PublicID, + Provider: AuthProviderApple, + }); serviceErr != nil { + if serviceErr.Code != exceptions.CodeNotFound { + logger.ErrorContext(ctx, "Failed to get account auth provider", "serviceError", serviceErr) + return "", serviceErr + } + + logger.WarnContext(ctx, "Account auth provider not found", "serviceError", serviceErr) + return "", exceptions.NewUnauthorizedError() + } + + cbURL, serviceErr := s.generateOAuthDynamicRegistrationIATCallback( + ctx, + generateOAuthDynamicRegistrationIATCallbackOptions{ + requestID: opts.RequestID, + clientID: opts.ACCClientID, + accountPublicID: accountDTO.PublicID, + accountVersion: accountDTO.Version(), + challenge: authData.Challenge, + domain: authData.Domain, + redirectURI: authData.RedirectURI, + state: authData.State, + backendDomain: opts.BackendDomain, + }, + ) + if serviceErr != nil { + logger.ErrorContext(ctx, "Failed to generate OAuth dynamic registration IAT callback", "serviceError", serviceErr) + return "", serviceErr + } + + return cbURL, nil } diff --git a/idp/internal/services/users_auth.go b/idp/internal/services/users_auth.go index a9b32f4..df6dfc6 100644 --- a/idp/internal/services/users_auth.go +++ b/idp/internal/services/users_auth.go @@ -12,6 +12,7 @@ import ( "log/slog" "reflect" "slices" + "time" "github.com/google/uuid" "github.com/jackc/pgx/v5/pgtype" @@ -805,23 +806,20 @@ func (s *Services) LogoutUser( return serviceErr } - blt, err := s.database.GetRevokedToken(ctx, tokenID) + stn, err := s.database.FindSessionTokenByTokenID(ctx, tokenID) if err != nil { - if exceptions.FromDBError(err).Code != exceptions.CodeNotFound { - logger.ErrorContext(ctx, "Failed to fetch revoked token", "error", err) - return exceptions.NewInternalServerError() + serviceErr := exceptions.FromDBError(err) + if serviceErr.Code != exceptions.CodeNotFound { + logger.ErrorContext(ctx, "Failed to find session token by token ID", "error", err) + return serviceErr } - } else { - logger.WarnContext(ctx, "Token is revoked", "revokedAt", blt.CreatedAt) + + logger.WarnContext(ctx, "Session token not found", "tokenID", tokenID) return exceptions.NewUnauthorizedError() } - - if err := s.database.RevokeToken(ctx, database.RevokeTokenParams{ - TokenID: tokenID, - ExpiresAt: exp, - }); err != nil { - logger.ErrorContext(ctx, "Failed to revoke token", "error", err) - return exceptions.NewInternalServerError() + if stn.ExpiresAt.Before(time.Now()) { + logger.WarnContext(ctx, "Session token expired", "expiresAt", stn.ExpiresAt) + return exceptions.NewUnauthorizedError() } logger.InfoContext(ctx, "User logged out successfully") diff --git a/idp/tests/dynamic_registration_test.go b/idp/tests/dynamic_registration_test.go index d5e6508..902b46c 100644 --- a/idp/tests/dynamic_registration_test.go +++ b/idp/tests/dynamic_registration_test.go @@ -599,71 +599,38 @@ func assertRFC7592Lifecycle(t *testing.T, setup dcrSetup, clientID, rat, backend 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") - } + setup := setupDynamicRegistration(t, false, false, false) + verifier := utils.Base62UUID() + utils.Base62UUID() + challenge := utils.Sha256HashBase64(verifier) + code, err := cacheStore.GenerateAccountCredentialsRegistrationIATCode(ctx, cache.GenerateAccountCredentialsRegistrationIATCodeOptions{ + 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) + } - 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) - } - }) + 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") } } diff --git a/project.md b/project.md index 2f89670..781d161 100644 --- a/project.md +++ b/project.md @@ -24,19 +24,18 @@ - device - service - mcp +- Add support for multiple 2FA types ### IDP On-Going - Add OAuth Dynamic Registration for: - accounts - apps -- Add support for multiple 2FA types +- Make refresh tokens whitelisted not blacklisted +- Add grants to control refresh token ### IDP Todo -- Account key generation -- Add Passkey (WebAuthn) support -- Dynamic OIDC configs - User authentication for each app type: - web - native & spa @@ -44,6 +43,11 @@ - device - service - MCP +- Custom External Providers +- Account key generation +- Add Passkey (WebAuthn) support +- Dynamic OIDC configs +- Separate signing, encryption, and decryption into a KMS service ## Mailer @@ -60,6 +64,30 @@ NONE - Change Queue from Redis to RabbitMQ - Use templates instead of full emails +## KMS + +### KMS Done + +NONE + +### KMS On-Going + +NONE + +### KMS Todo + +- Add gRPC endpoints for KMS operations +- Add KMS mTLS authentication +- Add SEK, DEK, KEK generation +- Add SEK, DEK, KEK rotation +- Add SEK, DEK, KEK revocation +- Add SEK, DEK, KEK destruction +- Add DEK encryption & decryption +- Add JWKs generation & rotation +- Add JWKs revocation & destruction +- Add JWKs signing & verification +- Add JWKs public key retrieval + ## Scripts ### Scripts Done From 870c445b3f2d37cfad29421b6440d62534feb049 Mon Sep 17 00:00:00 2001 From: Afonso Barracha Date: Tue, 15 Sep 2026 23:05:15 +1200 Subject: [PATCH 07/16] fix: remove errors from user auth --- .../database/queries/user_sessions.sql | 10 ++- .../providers/database/user_sessions.sql.go | 83 +++++++++++++++++++ idp/internal/services/client_credentials.go | 2 +- idp/internal/services/users_auth.go | 79 ++++++++++++++---- 4 files changed, 154 insertions(+), 20 deletions(-) create mode 100644 idp/internal/providers/database/user_sessions.sql.go diff --git a/idp/internal/providers/database/queries/user_sessions.sql b/idp/internal/providers/database/queries/user_sessions.sql index 891f7f7..03b4a24 100644 --- a/idp/internal/providers/database/queries/user_sessions.sql +++ b/idp/internal/providers/database/queries/user_sessions.sql @@ -2,4 +2,12 @@ -- -- 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/. \ No newline at end of file +-- file, You can obtain one at https://mozilla.org/MPL/2.0/. + +-- name: FindUserSessionByUserIDAndSessionUUID :one +SELECT "u".*, "s".* +FROM "user_sessions" AS "u" +LEFT JOIN "sessions" AS "s" ON "u"."session_id" = "s"."id" +WHERE "u"."user_id" = $1 +AND "u"."session_uuid" = $2 +LIMIT 1; diff --git a/idp/internal/providers/database/user_sessions.sql.go b/idp/internal/providers/database/user_sessions.sql.go new file mode 100644 index 0000000..a1b8585 --- /dev/null +++ b/idp/internal/providers/database/user_sessions.sql.go @@ -0,0 +1,83 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 +// source: user_sessions.sql + +package database + +import ( + "context" + "time" + + "github.com/google/uuid" + "github.com/jackc/pgx/v5/pgtype" +) + +const findUserSessionByUserIDAndSessionUUID = `-- name: FindUserSessionByUserIDAndSessionUUID :one + +SELECT u.user_id, u.user_version, u.session_id, u.app_id, u.account_id, u.session_uuid, u.created_at, s.id, s.account_id, s.grant_id, s.session_id, s.session_type, s.session_client_id, s.ip_address, s.user_agent, s.issued_at, s.expires_at, s.created_at, s.updated_at +FROM "user_sessions" AS "u" +LEFT JOIN "sessions" AS "s" ON "u"."session_id" = "s"."id" +WHERE "u"."user_id" = $1 +AND "u"."session_uuid" = $2 +LIMIT 1 +` + +type FindUserSessionByUserIDAndSessionUUIDParams struct { + UserID int32 + SessionUuid uuid.UUID +} + +type FindUserSessionByUserIDAndSessionUUIDRow struct { + UserID int32 + UserVersion int32 + SessionID int32 + AppID int32 + AccountID int32 + SessionUuid uuid.UUID + CreatedAt time.Time + ID pgtype.Int4 + AccountID_2 pgtype.Int4 + GrantID pgtype.Int4 + SessionID_2 pgtype.UUID + SessionType NullSessionType + SessionClientID pgtype.Text + IpAddress pgtype.Text + UserAgent pgtype.Text + IssuedAt pgtype.Timestamptz + ExpiresAt pgtype.Timestamptz + CreatedAt_2 pgtype.Timestamptz + UpdatedAt pgtype.Timestamptz +} + +// 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/. +func (q *Queries) FindUserSessionByUserIDAndSessionUUID(ctx context.Context, arg FindUserSessionByUserIDAndSessionUUIDParams) (FindUserSessionByUserIDAndSessionUUIDRow, error) { + row := q.db.QueryRow(ctx, findUserSessionByUserIDAndSessionUUID, arg.UserID, arg.SessionUuid) + var i FindUserSessionByUserIDAndSessionUUIDRow + err := row.Scan( + &i.UserID, + &i.UserVersion, + &i.SessionID, + &i.AppID, + &i.AccountID, + &i.SessionUuid, + &i.CreatedAt, + &i.ID, + &i.AccountID_2, + &i.GrantID, + &i.SessionID_2, + &i.SessionType, + &i.SessionClientID, + &i.IpAddress, + &i.UserAgent, + &i.IssuedAt, + &i.ExpiresAt, + &i.CreatedAt_2, + &i.UpdatedAt, + ) + return i, err +} diff --git a/idp/internal/services/client_credentials.go b/idp/internal/services/client_credentials.go index 20635aa..02dcd6d 100644 --- a/idp/internal/services/client_credentials.go +++ b/idp/internal/services/client_credentials.go @@ -372,7 +372,7 @@ func (s *Services) BuildGetAccountClientCredentialsSecretFn( } logger.InfoContext(ctx, "Successfully fetched client credentials secret") - return []byte(fmt.Sprintf("%s.%s", secretID, decryptedSecret)), nil + return fmt.Appendf([]byte{}, "%s.%s", secretID, decryptedSecret), nil } } diff --git a/idp/internal/services/users_auth.go b/idp/internal/services/users_auth.go index df6dfc6..3f3f9af 100644 --- a/idp/internal/services/users_auth.go +++ b/idp/internal/services/users_auth.go @@ -763,18 +763,7 @@ func (s *Services) LogoutUser( ) logger.InfoContext(ctx, "Logging out user...") - appDTO, serviceErr := s.GetAppByClientIDVersionAndAccountID(ctx, GetAppByClientIDVersionAndAccountIDOptions{ - RequestID: opts.RequestID, - ClientID: opts.AppClientID, - Version: opts.AppVersion, - AccountID: opts.AccountID, - }) - if serviceErr != nil { - logger.ErrorContext(ctx, "Failed to get app by ID", "error", serviceErr) - return serviceErr - } - - userClaims, appClaims, _, tokenID, exp, err := s.jwt.VerifyUserAuthToken( + userClaims, appClaims, _, tokenID, _, err := s.jwt.VerifyUserAuthToken( opts.Token, utils.SupportedCryptoSuiteEd25519, s.BuildGetAccountPublicKeyFn(ctx, BuildGetAccountPublicKeyFnOptions{ @@ -791,11 +780,33 @@ func (s *Services) LogoutUser( logger.WarnContext(ctx, "Invalid user ID", "tokenUserId", userClaims.UserID, "userPublicId", opts.UserPublicID) return exceptions.NewUnauthorizedError() } + + appDTO, serviceErr := s.GetAppByClientIDVersionAndAccountID(ctx, GetAppByClientIDVersionAndAccountIDOptions{ + RequestID: opts.RequestID, + ClientID: opts.AppClientID, + Version: opts.AppVersion, + AccountID: opts.AccountID, + }) + if serviceErr != nil { + logger.ErrorContext(ctx, "Failed to get app by ID", "error", serviceErr) + return serviceErr + } if appClaims.ClientID != appDTO.ClientID { logger.WarnContext(ctx, "Invalid app client ID", "tokenAppClientId", appClaims.ClientID, "appClientId", opts.AppClientID) return exceptions.NewUnauthorizedError() } + userDTO, serviceErr := s.GetUserByPublicIDAndVersion(ctx, GetUserByPublicIDAndVersionOptions{ + RequestID: opts.RequestID, + AccountID: opts.AccountID, + PublicID: opts.UserPublicID, + Version: opts.UserVersion, + }) + if serviceErr != nil { + logger.ErrorContext(ctx, "Failed to get user by ID", "error", serviceErr) + return serviceErr + } + if _, serviceErr := s.GetUserByPublicIDAndVersion(ctx, GetUserByPublicIDAndVersionOptions{ RequestID: opts.RequestID, AccountID: opts.AccountID, @@ -822,6 +833,25 @@ func (s *Services) LogoutUser( return exceptions.NewUnauthorizedError() } + userSession, err := s.database.FindUserSessionByUserIDAndSessionUUID(ctx, database.FindUserSessionByUserIDAndSessionUUIDParams{ + UserID: userDTO.ID(), + SessionUuid: stn.SessionUuid, + }) + if err != nil { + serviceErr := exceptions.FromDBError(err) + if serviceErr.Code != exceptions.CodeNotFound { + logger.ErrorContext(ctx, "Failed to find user session by user ID and session UUID", "error", err) + return serviceErr + } + + logger.WarnContext(ctx, "User session not found", "userID", opts.UserPublicID, "sessionUUID", tokenID) + return exceptions.NewUnauthorizedError() + } + if err := s.database.DeleteSessionByID(ctx, userSession.SessionID); err != nil { + logger.ErrorContext(ctx, "Failed to delete session", "error", err) + return exceptions.FromDBError(err) + } + logger.InfoContext(ctx, "User logged out successfully") return nil } @@ -863,13 +893,25 @@ func (s *Services) RefreshUserAccess( } // Check if token is blacklisted - blt, err := s.database.GetRevokedToken(ctx, tokenID) - if err == nil { - logger.WarnContext(ctx, "Token is revoked", "revokedAt", blt.CreatedAt) + sessionToken, err := s.database.FindSessionTokenByTokenID(ctx, tokenID) + if err != nil { + serviceErr := exceptions.FromDBError(err) + if serviceErr.Code != exceptions.CodeNotFound { + logger.ErrorContext(ctx, "Failed to fetch session token", "error", err) + return dtos.AuthDTO{}, exceptions.NewInternalServerError() + } + + logger.WarnContext(ctx, "Token was not found in the DB, it is probably revoked") + return dtos.AuthDTO{}, exceptions.NewUnauthorizedError() + } + if sessionToken.ExpiresAt.Before(time.Now()) { + if err := s.database.DeleteSessionToken(ctx, tokenID); err != nil { + logger.ErrorContext(ctx, "Failed to delete session token", "error", err) + return dtos.AuthDTO{}, exceptions.NewInternalServerError() + } + + logger.WarnContext(ctx, "Session token is expired") return dtos.AuthDTO{}, exceptions.NewUnauthorizedError() - } else if exceptions.FromDBError(err).Code != exceptions.CodeNotFound { - logger.ErrorContext(ctx, "Failed to check blacklisted token", "error", err) - return dtos.AuthDTO{}, exceptions.NewInternalServerError() } userDTO, serviceErr := s.GetUserByPublicIDAndVersion(ctx, GetUserByPublicIDAndVersionOptions{ @@ -919,6 +961,7 @@ func (s *Services) RefreshUserAccess( return dtos.AuthDTO{}, serviceErr } + // TODO: fix all user login logic return s.generateFullUserAuthDTO( ctx, logger, From b57c4252c940ed96f5c9bdfadd977c4bd5514ff4 Mon Sep 17 00:00:00 2001 From: Afonso Barracha Date: Tue, 15 Sep 2026 23:43:09 +1200 Subject: [PATCH 08/16] test: make dynamic registration tests use standard design --- ...ccount_dynamic_registration_configs.sql.go | 8 +- idp/internal/providers/database/apps.sql.go | 12 +- .../account_dynamic_registration_configs.sql | 10 +- .../providers/database/queries/apps.sql | 8 +- .../database/queries/registered_apps.sql | 8 +- .../providers/database/registered_apps.sql.go | 12 +- idp/internal/server/routes/auth.go | 56 +- idp/internal/server/routes/common.go | 30 +- idp/internal/server/routes/oauth.go | 52 +- idp/internal/server/routes/well_known.go | 4 +- .../services/app_dynamic_registration.go | 2 + idp/internal/services/apps.go | 4 + idp/internal/services/auth.go | 33 +- idp/tests/account_credentials_test.go | 32 +- idp/tests/auth_test.go | 9 +- idp/tests/common_test.go | 133 +++-- idp/tests/dynamic_registration_test.go | 488 ++++++++++-------- idp/tests/oauth_test.go | 34 +- 18 files changed, 493 insertions(+), 442 deletions(-) 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 4eed164..5ad6bff 100644 --- a/idp/internal/providers/database/account_dynamic_registration_configs.sql.go +++ b/idp/internal/providers/database/account_dynamic_registration_configs.sql.go @@ -19,18 +19,14 @@ INSERT INTO "account_dynamic_registration_configs" ( "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" + "require_verified_domains_credentials_type" ) VALUES ( $1, $2, $3, $4, $5, - '{}'::account_credentials_type[], - '{}'::account_credentials_type[], - '{}'::initial_access_token_generation_method[] + '{}'::account_credentials_type[] ) 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/apps.sql.go b/idp/internal/providers/database/apps.sql.go index 09bd928..a97b02e 100644 --- a/idp/internal/providers/database/apps.sql.go +++ b/idp/internal/providers/database/apps.sql.go @@ -165,7 +165,9 @@ INSERT INTO "apps" ( "redirect_uris", "response_types", "allow_user_registration", - "auth_providers" + "auth_providers", + "session_type", + "access_token_ttl" ) VALUES ( $1, $2, @@ -192,7 +194,9 @@ INSERT INTO "apps" ( $23, $24, $25, - $26 + $26, + $27, + $28 ) 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, session_type, access_token_ttl, id_token_ttl, refresh_token_idle_ttl, refresh_token_ttl, grant_ttl, created_at, updated_at ` @@ -223,6 +227,8 @@ type CreateAppParams struct { ResponseTypes []ResponseType AllowUserRegistration bool AuthProviders []AuthProvider + SessionType SessionType + AccessTokenTtl int32 } // Copyright (c) 2025 Afonso Barracha @@ -258,6 +264,8 @@ func (q *Queries) CreateApp(ctx context.Context, arg CreateAppParams) (App, erro arg.ResponseTypes, arg.AllowUserRegistration, arg.AuthProviders, + arg.SessionType, + arg.AccessTokenTtl, ) var i App err := row.Scan( 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 07903fc..a9329ca 100644 --- a/idp/internal/providers/database/queries/account_dynamic_registration_configs.sql +++ b/idp/internal/providers/database/queries/account_dynamic_registration_configs.sql @@ -11,18 +11,14 @@ INSERT INTO "account_dynamic_registration_configs" ( "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" + "require_verified_domains_credentials_type" ) VALUES ( $1, $2, $3, $4, $5, - '{}'::account_credentials_type[], - '{}'::account_credentials_type[], - '{}'::initial_access_token_generation_method[] + '{}'::account_credentials_type[] ) RETURNING *; -- name: UpdateAccountDynamicRegistrationConfig :one @@ -42,4 +38,4 @@ SELECT * FROM "account_dynamic_registration_configs" WHERE "account_public_id" = $1 LIMIT 1; -- name: DeleteAccountDynamicRegistrationConfig :exec -DELETE FROM "account_dynamic_registration_configs" WHERE "id" = $1; \ No newline at end of file +DELETE FROM "account_dynamic_registration_configs" WHERE "id" = $1; diff --git a/idp/internal/providers/database/queries/apps.sql b/idp/internal/providers/database/queries/apps.sql index bf69889..a7340ab 100644 --- a/idp/internal/providers/database/queries/apps.sql +++ b/idp/internal/providers/database/queries/apps.sql @@ -31,7 +31,9 @@ INSERT INTO "apps" ( "redirect_uris", "response_types", "allow_user_registration", - "auth_providers" + "auth_providers", + "session_type", + "access_token_ttl" ) VALUES ( $1, $2, @@ -58,7 +60,9 @@ INSERT INTO "apps" ( $23, $24, $25, - $26 + $26, + $27, + $28 ) RETURNING *; diff --git a/idp/internal/providers/database/queries/registered_apps.sql b/idp/internal/providers/database/queries/registered_apps.sql index ceee5c9..44c94c0 100644 --- a/idp/internal/providers/database/queries/registered_apps.sql +++ b/idp/internal/providers/database/queries/registered_apps.sql @@ -45,7 +45,9 @@ INSERT INTO "apps" ( "default_acr_values", "initiate_login_uri", "request_uris", - "access_token_signing_alg" + "access_token_signing_alg", + "session_type", + "access_token_ttl" ) VALUES ( $1, $2, @@ -92,7 +94,9 @@ INSERT INTO "apps" ( $43, $44, $45, - $46 + $46, + $47, + $48 ) RETURNING *; -- name: UpdateRegisteredApp :one diff --git a/idp/internal/providers/database/registered_apps.sql.go b/idp/internal/providers/database/registered_apps.sql.go index 10cc1f2..1bf97ea 100644 --- a/idp/internal/providers/database/registered_apps.sql.go +++ b/idp/internal/providers/database/registered_apps.sql.go @@ -59,7 +59,9 @@ INSERT INTO "apps" ( "default_acr_values", "initiate_login_uri", "request_uris", - "access_token_signing_alg" + "access_token_signing_alg", + "session_type", + "access_token_ttl" ) VALUES ( $1, $2, @@ -106,7 +108,9 @@ INSERT INTO "apps" ( $43, $44, $45, - $46 + $46, + $47, + $48 ) 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, session_type, access_token_ttl, id_token_ttl, refresh_token_idle_ttl, refresh_token_ttl, grant_ttl, created_at, updated_at ` @@ -157,6 +161,8 @@ type CreateRegisteredAppParams struct { InitiateLoginUri pgtype.Text RequestUris []string AccessTokenSigningAlg TokenCryptoSuite + SessionType SessionType + AccessTokenTtl int32 } func (q *Queries) CreateRegisteredApp(ctx context.Context, arg CreateRegisteredAppParams) (App, error) { @@ -207,6 +213,8 @@ func (q *Queries) CreateRegisteredApp(ctx context.Context, arg CreateRegisteredA arg.InitiateLoginUri, arg.RequestUris, arg.AccessTokenSigningAlg, + arg.SessionType, + arg.AccessTokenTtl, ) var i App err := row.Scan( diff --git a/idp/internal/server/routes/auth.go b/idp/internal/server/routes/auth.go index da4efd9..6c6e399 100644 --- a/idp/internal/server/routes/auth.go +++ b/idp/internal/server/routes/auth.go @@ -18,18 +18,18 @@ func (r *Routes) AuthRoutes(app *fiber.App) { authProvsReaderMW := r.controllers.ScopeMiddleware(tokens.AccountScopeAuthProvidersRead) // Custom auth paths - router.Post(paths.AuthRegister, r.controllers.HostMiddleware, HostAwareRoute( - []fiber.Handler{r.controllers.RegisterAccount}, - []fiber.Handler{r.controllers.AppAccessClaimsMiddleware, r.controllers.RegisterUser}, - )) - router.Post(paths.AuthConfirmEmail, r.controllers.HostMiddleware, HostAwareRoute( - []fiber.Handler{r.controllers.ConfirmAccount}, - []fiber.Handler{r.controllers.AppAccessClaimsMiddleware, r.controllers.ConfirmUser}, - )) - router.Post(paths.AuthLogin, r.controllers.HostMiddleware, HostAwareRoute( - []fiber.Handler{r.controllers.LoginAccount}, - []fiber.Handler{r.controllers.AppAccessClaimsMiddleware, r.controllers.LoginUser}, - )) + router.Post(paths.AuthRegister, r.controllers.HostMiddleware, + HostAwareRoute(nextHandler, r.controllers.AppAccessClaimsMiddleware), + HostAwareRoute(r.controllers.RegisterAccount, r.controllers.RegisterUser), + ) + router.Post(paths.AuthConfirmEmail, r.controllers.HostMiddleware, + HostAwareRoute(nextHandler, r.controllers.AppAccessClaimsMiddleware), + HostAwareRoute(r.controllers.ConfirmAccount, r.controllers.ConfirmUser), + ) + router.Post(paths.AuthLogin, r.controllers.HostMiddleware, + HostAwareRoute(nextHandler, r.controllers.AppAccessClaimsMiddleware), + HostAwareRoute(r.controllers.LoginAccount, r.controllers.LoginUser), + ) router.Post( paths.AuthLogin+paths.Auth2FA, r.controllers.TwoFAAccessClaimsMiddleware, @@ -40,22 +40,22 @@ func (r *Routes) AuthRoutes(app *fiber.App) { r.controllers.TwoFAAccessClaimsMiddleware, r.controllers.RecoverAccount, ) - router.Post(paths.AuthRefresh, r.controllers.HostMiddleware, HostAwareRoute( - []fiber.Handler{r.controllers.RefreshAccount}, - []fiber.Handler{r.controllers.AppAccessClaimsMiddleware, r.controllers.RefreshUser}, - )) - router.Post(paths.AuthLogout, r.controllers.HostMiddleware, HostAwareRoute( - []fiber.Handler{r.controllers.AccountAccessClaimsMiddleware, r.controllers.LogoutAccount}, - []fiber.Handler{r.controllers.UserAccessClaimsMiddleware, r.controllers.LogoutUser}, - )) - router.Post(paths.AuthForgotPassword, r.controllers.HostMiddleware, HostAwareRoute( - []fiber.Handler{r.controllers.ForgotAccountPassword}, - []fiber.Handler{r.controllers.AppAccessClaimsMiddleware, r.controllers.ForgotUserPassword}, - )) - router.Post(paths.AuthResetPassword, r.controllers.HostMiddleware, HostAwareRoute( - []fiber.Handler{r.controllers.ResetAccountPassword}, - []fiber.Handler{r.controllers.AppAccessClaimsMiddleware, r.controllers.ResetUserPassword}, - )) + router.Post(paths.AuthRefresh, r.controllers.HostMiddleware, + HostAwareRoute(nextHandler, r.controllers.AppAccessClaimsMiddleware), + HostAwareRoute(r.controllers.RefreshAccount, r.controllers.RefreshUser), + ) + router.Post(paths.AuthLogout, r.controllers.HostMiddleware, + HostAwareRoute(r.controllers.AccountAccessClaimsMiddleware, r.controllers.UserAccessClaimsMiddleware), + HostAwareRoute(r.controllers.LogoutAccount, r.controllers.LogoutUser), + ) + router.Post(paths.AuthForgotPassword, r.controllers.HostMiddleware, + HostAwareRoute(nextHandler, r.controllers.AppAccessClaimsMiddleware), + HostAwareRoute(r.controllers.ForgotAccountPassword, r.controllers.ForgotUserPassword), + ) + router.Post(paths.AuthResetPassword, r.controllers.HostMiddleware, + HostAwareRoute(nextHandler, r.controllers.AppAccessClaimsMiddleware), + HostAwareRoute(r.controllers.ResetAccountPassword, r.controllers.ResetUserPassword), + ) router.Get( paths.AuthProviders, r.controllers.AccountAccessClaimsMiddleware, diff --git a/idp/internal/server/routes/common.go b/idp/internal/server/routes/common.go index 86ad014..782a093 100644 --- a/idp/internal/server/routes/common.go +++ b/idp/internal/server/routes/common.go @@ -17,35 +17,21 @@ func V1PathRouter(app *fiber.App) fiber.Router { } func HostAwareRoute( - normalHandlers []fiber.Handler, - hostHandlers []fiber.Handler, + normalHandler fiber.Handler, + hostHandler fiber.Handler, ) fiber.Handler { return func(ctx fiber.Ctx) error { hasAccountHost, ok := ctx.Locals("hasAccountHost").(bool) ctx.Locals("hostAwareRoute", true) if !ok || !hasAccountHost { - for _, handler := range normalHandlers { - if err := handler(ctx); err != nil { - return err - } - if ctx.Response().StatusCode() >= fiber.StatusBadRequest { - return nil - } - } - - return nil + return normalHandler(ctx) } - for _, handler := range hostHandlers { - if err := handler(ctx); err != nil { - return err - } - if ctx.Response().StatusCode() >= fiber.StatusBadRequest { - return nil - } - } - - return nil + return hostHandler(ctx) } } + +func nextHandler(ctx fiber.Ctx) error { + return ctx.Next() +} diff --git a/idp/internal/server/routes/oauth.go b/idp/internal/server/routes/oauth.go index 81ac1c4..04207f3 100644 --- a/idp/internal/server/routes/oauth.go +++ b/idp/internal/server/routes/oauth.go @@ -17,8 +17,8 @@ func (r *Routes) OAuthRoutes(app *fiber.App) { // Known auth paths (oauth2) router.Post(paths.OAuthKeys, r.controllers.HostMiddleware, HostAwareRoute( - []fiber.Handler{r.controllers.GlobalOAuthPublicJWKs}, - []fiber.Handler{r.controllers.AccountDistributedOAuthPublicJWKs}, + r.controllers.GlobalOAuthPublicJWKs, + r.controllers.AccountDistributedOAuthPublicJWKs, )) router.Post(paths.OAuthToken, r.controllers.AccountOAuthToken) router.Get(paths.OAuthAuth, r.controllers.AccountOAuthURL) @@ -31,58 +31,26 @@ func (r *Routes) OAuthRoutes(app *fiber.App) { router.Post( paths.OAuthRegister, r.controllers.HostMiddleware, - HostAwareRoute( - []fiber.Handler{ - r.controllers.DynamicRegistrationIATMiddleware, - r.controllers.OAuthDynamicRegistration, - }, - []fiber.Handler{ - r.controllers.AppDynamicRegistrationIATMiddleware, - r.controllers.OAuthAppDynamicRegistration, - }, - ), + HostAwareRoute(r.controllers.DynamicRegistrationIATMiddleware, r.controllers.AppDynamicRegistrationIATMiddleware), + HostAwareRoute(r.controllers.OAuthDynamicRegistration, r.controllers.OAuthAppDynamicRegistration), ) router.Get( paths.OAuthRegisterClient, r.controllers.HostMiddleware, - HostAwareRoute( - []fiber.Handler{ - r.controllers.DynamicRegistrationAccessTokenMiddleware, - r.controllers.OAuthDynamicRegistrationGet, - }, - []fiber.Handler{ - r.controllers.AppDynamicRegistrationAccessTokenMiddleware, - r.controllers.OAuthAppDynamicRegistrationGet, - }, - ), + HostAwareRoute(r.controllers.DynamicRegistrationAccessTokenMiddleware, r.controllers.AppDynamicRegistrationAccessTokenMiddleware), + HostAwareRoute(r.controllers.OAuthDynamicRegistrationGet, 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, - }, - ), + HostAwareRoute(r.controllers.DynamicRegistrationAccessTokenMiddleware, r.controllers.AppDynamicRegistrationAccessTokenMiddleware), + HostAwareRoute(r.controllers.OAuthDynamicRegistrationUpdate, 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, - }, - ), + HostAwareRoute(r.controllers.DynamicRegistrationAccessTokenMiddleware, r.controllers.AppDynamicRegistrationAccessTokenMiddleware), + HostAwareRoute(r.controllers.OAuthDynamicRegistrationDelete, r.controllers.OAuthAppDynamicRegistrationDelete), ) // Initial Access Token (IAT) routes diff --git a/idp/internal/server/routes/well_known.go b/idp/internal/server/routes/well_known.go index 1d3edb3..a5dab0b 100644 --- a/idp/internal/server/routes/well_known.go +++ b/idp/internal/server/routes/well_known.go @@ -16,8 +16,8 @@ func (r *Routes) WellKnownRoutes(app *fiber.App) { router := app.Group(paths.WellKnownBase, r.controllers.HostMiddleware) router.Get(paths.WellKnownJWKs, HostAwareRoute( - []fiber.Handler{r.controllers.GlobalOAuthPublicJWKs}, - []fiber.Handler{r.controllers.AccountDistributedOAuthPublicJWKs}, + r.controllers.GlobalOAuthPublicJWKs, + r.controllers.AccountDistributedOAuthPublicJWKs, )) router.Get(paths.WellKnownOIDC, r.controllers.WellKnownOIDCConfiguration) } diff --git a/idp/internal/services/app_dynamic_registration.go b/idp/internal/services/app_dynamic_registration.go index c7560d4..d60a86e 100644 --- a/idp/internal/services/app_dynamic_registration.go +++ b/idp/internal/services/app_dynamic_registration.go @@ -260,6 +260,8 @@ func (s *Services) mapAppRegistrationDataToDBParams( InitiateLoginUri: mapEmptyURL(opts.data.InitiateLoginURI), RequestUris: opts.data.RequestURIs, AccessTokenSigningAlg: accessTokenSigningAlg, + SessionType: database.SessionTypeSliding, + AccessTokenTtl: int32(s.jwt.GetAccessTTL()), AccountID: opts.accountID, AccountPublicID: opts.accountPublicID, AppType: opts.appType, diff --git a/idp/internal/services/apps.go b/idp/internal/services/apps.go index 56ae76a..bf403bc 100644 --- a/idp/internal/services/apps.go +++ b/idp/internal/services/apps.go @@ -710,6 +710,8 @@ func (s *Services) createApp( ClientUri: utils.ProcessURL(opts.clientURI), AllowUserRegistration: opts.allowUserRegistration, UsernameColumn: usernameColumn, + SessionType: database.SessionTypeSliding, + AccessTokenTtl: int32(s.jwt.GetAccessTTL()), TokenEndpointAuthMethod: opts.authMethod, GrantTypes: opts.grantTypes, LogoUri: mapEmptyURL(opts.logoURI), @@ -790,6 +792,8 @@ func (s *Services) createSingleApp( ClientUri: utils.ProcessURL(opts.clientURI), AllowUserRegistration: opts.allowUserRegistration, UsernameColumn: usernameColumn, + SessionType: database.SessionTypeSliding, + AccessTokenTtl: int32(s.jwt.GetAccessTTL()), TokenEndpointAuthMethod: opts.authMethod, GrantTypes: opts.grantTypes, LogoUri: mapEmptyURL(opts.logoURI), diff --git a/idp/internal/services/auth.go b/idp/internal/services/auth.go index 900f515..ce3e8e4 100644 --- a/idp/internal/services/auth.go +++ b/idp/internal/services/auth.go @@ -368,11 +368,12 @@ func (s *Services) createAccountGrantSessionAndToken( } if err = qrs.CreateSessionToken(ctx, database.CreateSessionTokenParams{ - SessionID: sessionID, - TokenID: opts.tokenID, - AccountID: opts.accountID, - GrantID: grantID, - ExpiresAt: expiresAt, + SessionID: sessionID, + SessionUuid: opts.sessionID, + TokenID: opts.tokenID, + AccountID: opts.accountID, + GrantID: grantID, + ExpiresAt: expiresAt, }); err != nil { logger.ErrorContext(ctx, "Failed to create session token", "error", err) serviceErr = exceptions.FromDBError(err) @@ -494,11 +495,12 @@ func (s *Services) createAccountSessionAndToken( } if err = qrs.CreateSessionToken(ctx, database.CreateSessionTokenParams{ - SessionID: sessionID, - TokenID: opts.tokenID, - AccountID: opts.accountID, - GrantID: opts.grantID, - ExpiresAt: expiresAt, + SessionID: sessionID, + SessionUuid: opts.sessionID, + TokenID: opts.tokenID, + AccountID: opts.accountID, + GrantID: opts.grantID, + ExpiresAt: expiresAt, }); err != nil { logger.ErrorContext(ctx, "Failed to create session token", "error", err) serviceErr = exceptions.FromDBError(err) @@ -584,11 +586,12 @@ func (s *Services) upsertAccountGrantSessionAndToken( } if err = qrs.CreateSessionToken(ctx, database.CreateSessionTokenParams{ - SessionID: session.SessionID, - TokenID: opts.tokenID, - AccountID: opts.accountID, - GrantID: grant.GrantID, - ExpiresAt: expiresAt, + SessionID: session.SessionID, + SessionUuid: opts.sessionID, + TokenID: opts.tokenID, + AccountID: opts.accountID, + GrantID: grant.GrantID, + ExpiresAt: expiresAt, }); err != nil { logger.ErrorContext(ctx, "Failed to create session token", "error", err) serviceErr = exceptions.FromDBError(err) diff --git a/idp/tests/account_credentials_test.go b/idp/tests/account_credentials_test.go index c5626b0..47c21ee 100644 --- a/idp/tests/account_credentials_test.go +++ b/idp/tests/account_credentials_test.go @@ -1132,15 +1132,15 @@ func TestCreateAccountCredentialsSecret(t *testing.T) { return accountCredentialsPath + "/" + clientID + "/secrets" } - testCases := []TestRequestCase[any]{ + testCases := []TestRequestCase[bodies.CreateCredentialsSecretBody]{ { Name: "Should return 201 CREATED and create new secret for client_secret_post", - ReqFn: func(t *testing.T) (any, string) { + ReqFn: func(t *testing.T) (bodies.CreateCredentialsSecretBody, string) { accessToken := createAccountCredentialBeforeEach(t, "client_secret_post") - return nil, accessToken + return bodies.CreateCredentialsSecretBody{}, accessToken }, ExpStatus: http.StatusCreated, - AssertFn: func(t *testing.T, _ any, res *http.Response) { + AssertFn: func(t *testing.T, _ bodies.CreateCredentialsSecretBody, res *http.Response) { resBody := AssertTestResponseBody(t, res, dtos.ClientCredentialsSecretDTO{}) AssertNotEmpty(t, resBody.PublicID) AssertEqual(t, resBody.Status, "active") @@ -1150,12 +1150,12 @@ func TestCreateAccountCredentialsSecret(t *testing.T) { }, { Name: "Should return 201 CREATED and create new key for private_key_jwt", - ReqFn: func(t *testing.T) (any, string) { + ReqFn: func(t *testing.T) (bodies.CreateCredentialsSecretBody, string) { accessToken := createAccountCredentialBeforeEach(t, "private_key_jwt") - return nil, accessToken + return bodies.CreateCredentialsSecretBody{Algorithm: "ES256"}, accessToken }, ExpStatus: http.StatusCreated, - AssertFn: func(t *testing.T, _ any, res *http.Response) { + AssertFn: func(t *testing.T, _ bodies.CreateCredentialsSecretBody, res *http.Response) { resBody := AssertTestResponseBody(t, res, dtos.ClientCredentialsSecretDTO{}) AssertNotEmpty(t, resBody.PublicID) AssertEqual(t, resBody.Status, "active") @@ -1165,28 +1165,28 @@ func TestCreateAccountCredentialsSecret(t *testing.T) { }, { Name: "Should return 404 NOT FOUND for non-existent credential", - ReqFn: func(t *testing.T) (any, string) { + ReqFn: func(t *testing.T) (bodies.CreateCredentialsSecretBody, string) { accessToken := createAccountCredentialBeforeEach(t, "client_secret_post") clientID = utils.Base62UUID() - return nil, accessToken + return bodies.CreateCredentialsSecretBody{}, accessToken }, ExpStatus: http.StatusNotFound, - AssertFn: AssertNotFoundError[any], + AssertFn: AssertNotFoundError[bodies.CreateCredentialsSecretBody], PathFn: pathFN, }, { Name: "Should return 401 UNAUTHORIZED without access token", - ReqFn: func(t *testing.T) (any, string) { + ReqFn: func(t *testing.T) (bodies.CreateCredentialsSecretBody, string) { createAccountCredentialBeforeEach(t, "client_secret_post") - return nil, "" + return bodies.CreateCredentialsSecretBody{}, "" }, ExpStatus: http.StatusUnauthorized, - AssertFn: AssertUnauthorizedError[any], + AssertFn: AssertUnauthorizedError[bodies.CreateCredentialsSecretBody], PathFn: pathFN, }, { Name: "Should return 403 FORBIDDEN without account:credentials:write scope", - ReqFn: func(t *testing.T) (any, string) { + ReqFn: func(t *testing.T) (bodies.CreateCredentialsSecretBody, string) { account := CreateTestAccount(t, GenerateFakeAccountData(t, services.AuthProviderGoogle)) accessToken := GenerateScopedAccountAccessToken(t, &account, []string{tokens.AccountScopeCredentialsRead}) @@ -1207,10 +1207,10 @@ func TestCreateAccountCredentialsSecret(t *testing.T) { t.Fatalf("Failed to create account credentials: %v", err) } clientID = cred.ClientID - return nil, accessToken + return bodies.CreateCredentialsSecretBody{}, accessToken }, ExpStatus: http.StatusForbidden, - AssertFn: AssertForbiddenError[any], + AssertFn: AssertForbiddenError[bodies.CreateCredentialsSecretBody], PathFn: pathFN, }, } diff --git a/idp/tests/auth_test.go b/idp/tests/auth_test.go index 9112a58..aa464dc 100644 --- a/idp/tests/auth_test.go +++ b/idp/tests/auth_test.go @@ -695,14 +695,7 @@ func TestRefreshToken(t *testing.T) { if err != nil { t.Fatal("Failed to verify refresh token", err) } - if err := testDb.RevokeToken(context.Background(), database.RevokeTokenParams{ - TokenID: data.TokenID, - AccountID: account.ID(), - Owner: database.TokenOwnerAccount, - OwnerPublicID: account.PublicID, - IssuedAt: data.IssuedAt, - ExpiresAt: data.ExpiresAt, - }); err != nil { + if err := testDb.DeleteSessionToken(context.Background(), data.TokenID); err != nil { t.Fatal("Failed to blacklist token", err) } diff --git a/idp/tests/common_test.go b/idp/tests/common_test.go index c53ae6a..a41199e 100644 --- a/idp/tests/common_test.go +++ b/idp/tests/common_test.go @@ -290,8 +290,12 @@ func CreateTestJSONRequestBody(t *testing.T, reqBody any) *bytes.Reader { } func PerformTestRequest(t *testing.T, app *fiber.App, delayMs int, method, path, tokenType, accessToken, contentType string, body io.Reader) *http.Response { + return performTestRequest(t, app, delayMs, method, path, GetTestConfig(t).BackendDomain(), tokenType, accessToken, contentType, body) +} + +func performTestRequest(t *testing.T, app *fiber.App, delayMs int, method, path, host, tokenType, accessToken, contentType string, body io.Reader) *http.Response { req := httptest.NewRequest(method, path, body) - req.Host = GetTestConfig(t).BackendDomain() + req.Host = host req.Header.Set("Content-Type", contentType) req.Header.Set("Accept", "application/json") @@ -379,29 +383,49 @@ func AssertStringContains(t *testing.T, actual string, expected string) { } type TestRequestCase[R any] struct { - Name string - ReqFn func(t *testing.T) (R, string) - ExpStatus int - AssertFn func(t *testing.T, req R, res *http.Response) - DelayMs int - Path string - PathFn func() string - Method string - TokenType string + Name string + ReqFn func(t *testing.T) (R, string) + RequestBodyFn func(req R) any + HostFn func(req R) string + PathFromReqFn func(req R) string + ExpStatus int + AssertFn func(t *testing.T, req R, res *http.Response) + DelayMs int + Path string + PathFn func() string + Method string + TokenType string } func PerformTestRequestCase[R any](t *testing.T, method, path string, tc TestRequestCase[R]) { // Arrange reqBody, accessToken := tc.ReqFn(t) - jsonBody := CreateTestJSONRequestBody(t, reqBody) + body := any(reqBody) + if tc.RequestBodyFn != nil { + body = tc.RequestBodyFn(reqBody) + } + var jsonBody io.Reader + if body != nil { + jsonBody = CreateTestJSONRequestBody(t, body) + } fiberApp := GetTestServer(t).App tokenType := "Bearer" if tc.TokenType != "" { tokenType = tc.TokenType } + host := GetTestConfig(t).BackendDomain() + if tc.HostFn != nil { + host = tc.HostFn(reqBody) + } + if tc.PathFn != nil { + path = tc.PathFn() + } + if tc.PathFromReqFn != nil { + path = tc.PathFromReqFn(reqBody) + } // Act - resp := PerformTestRequest(t, fiberApp, tc.DelayMs, method, path, tokenType, accessToken, "application/json", jsonBody) + resp := performTestRequest(t, fiberApp, tc.DelayMs, method, path, host, tokenType, accessToken, "application/json", jsonBody) defer func() { if err := resp.Body.Close(); err != nil { t.Fatal(err) @@ -436,27 +460,7 @@ func PerformTestRequestCaseWihURLEncodedBody(t *testing.T, method, path string, } func PerformTestRequestCaseWithPathFn[R any](t *testing.T, method string, tc TestRequestCase[R]) { - // Arrange - reqBody, accessToken := tc.ReqFn(t) - jsonBody := CreateTestJSONRequestBody(t, reqBody) - fiberApp := GetTestServer(t).App - tokenType := "Bearer" - if tc.TokenType != "" { - tokenType = tc.TokenType - } - - // Act - path := tc.PathFn() - resp := PerformTestRequest(t, fiberApp, tc.DelayMs, method, path, tokenType, accessToken, "application/json", jsonBody) - defer func() { - if err := resp.Body.Close(); err != nil { - t.Fatal(err) - } - }() - - // Assert - AssertTestStatusCode(t, resp, tc.ExpStatus) - tc.AssertFn(t, reqBody, resp) + PerformTestRequestCase(t, method, "", tc) } type fakeAccountData struct { @@ -543,10 +547,23 @@ func GenerateTestAccountAuthTokens(t *testing.T, account *dtos.AccountDTO) (stri t.Fatal("Failed to sign access token", serviceErr) } - refreshToken, err := tks.CreateRefreshToken(tokens.AccountRefreshTokenOptions{ + sRefreshToken := GenerateTestAccountRefreshToken(t, account, []tokens.AccountScope{tokens.AccountScopeAdmin}) + + return sAccessToken, sRefreshToken +} + +func GenerateTestAccountRefreshToken(t *testing.T, account *dtos.AccountDTO, scopes []tokens.AccountScope) string { + t.Helper() + tks := GetTestTokens(t) + cpt := GetTestCrypto(t) + s := GetTestServices(t) + requestID := uuid.NewString() + ctx := context.Background() + + refreshToken, refreshJTI, err := tks.CreateRefreshToken(tokens.AccountRefreshTokenOptions{ PublicID: account.PublicID, Version: account.Version(), - Scopes: []tokens.AccountScope{tokens.AccountScopeAdmin}, + Scopes: scopes, }) if err != nil { t.Fatal("Failed to create refresh token", err) @@ -574,7 +591,51 @@ func GenerateTestAccountAuthTokens(t *testing.T, account *dtos.AccountDTO) (stri t.Fatal("Failed to sign refresh token", serviceErr) } - return sAccessToken, sRefreshToken + sessionUUID, err := uuid.NewV7() + if err != nil { + t.Fatal("Failed to create test session UUID", err) + } + expiresAt := time.Now().Add(time.Duration(tks.GetRefreshTTL()) * time.Second) + db := GetTestDatabase(t) + grantedScopes := make([]database.Scopes, 0, len(scopes)) + grantedCustomScopes := make([]string, 0, len(scopes)) + for _, scope := range scopes { + switch scope { + case tokens.AccountScopeEmail: + grantedScopes = append(grantedScopes, database.ScopesEmail) + case tokens.AccountScopeProfile: + grantedScopes = append(grantedScopes, database.ScopesProfile) + default: + grantedCustomScopes = append(grantedCustomScopes, scope) + } + } + grantID, err := db.CreateGrant(ctx, database.CreateGrantParams{ + AccountID: account.ID(), GrantedClientID: string(utils.NilBase62UUID), + GrantID: uuid.New(), GrantedScopes: grantedScopes, GrantedCustomScopes: grantedCustomScopes, + }) + if err != nil { + t.Fatal("Failed to create test grant", err) + } + sessionID, err := db.CreateSession(ctx, database.CreateSessionParams{ + AccountID: account.ID(), GrantID: grantID, SessionID: sessionUUID, + SessionType: database.SessionTypeSliding, SessionClientID: string(utils.NilBase62UUID), ExpiresAt: expiresAt, + }) + if err != nil { + t.Fatal("Failed to create test session", err) + } + if err = db.CreateAccountSessionWithoutAccountCredentials(ctx, database.CreateAccountSessionWithoutAccountCredentialsParams{ + AccountID: account.ID(), AccountVersion: account.Version(), SessionID: sessionID, SessionUuid: sessionUUID, + }); err != nil { + t.Fatal("Failed to create test account session", err) + } + if err = db.CreateSessionToken(ctx, database.CreateSessionTokenParams{ + TokenID: refreshJTI, AccountID: account.ID(), SessionID: sessionID, + SessionUuid: sessionUUID, GrantID: grantID, ExpiresAt: expiresAt, + }); err != nil { + t.Fatal("Failed to create test session token", err) + } + + return sRefreshToken } func GenerateScopedAccountAccessToken(t *testing.T, account *dtos.AccountDTO, scopes []tokens.AccountScope) string { diff --git a/idp/tests/dynamic_registration_test.go b/idp/tests/dynamic_registration_test.go index 902b46c..722260f 100644 --- a/idp/tests/dynamic_registration_test.go +++ b/idp/tests/dynamic_registration_test.go @@ -9,13 +9,11 @@ import ( "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" @@ -65,30 +63,6 @@ func jsonString(raw json.RawMessage) string { 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 { @@ -283,18 +257,9 @@ func setupDynamicRegistration(t *testing.T, appClient, bounded, statement bool) } } -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"]) @@ -357,65 +322,90 @@ func assertCreatedRegistration(t *testing.T, setup dcrSetup, res *http.Response) // Exercise the actual HTTP route, IAT verification, software-statement trust, // credential issuance and database persistence together. func TestDynamicRegistration(t *testing.T) { + testCases := make([]TestRequestCase[dcrSetup], 0, 6) 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) + appClient, bounded, statement := appClient, bounded, statement + testCases = append(testCases, TestRequestCase[dcrSetup]{ + Name: fmt.Sprintf("app=%t/iat=%t/statement=%t", appClient, bounded, statement), + ReqFn: func(t *testing.T) (dcrSetup, string) { + setup := setupDynamicRegistration(t, appClient, bounded, statement) + return setup, setup.accessToken + }, + RequestBodyFn: func(setup dcrSetup) any { return setup.body }, + HostFn: func(setup dcrSetup) string { return setup.host }, + ExpStatus: http.StatusCreated, + AssertFn: func(t *testing.T, setup dcrSetup, res *http.Response) { + assertCreatedRegistration(t, setup, res) + }, }) } } } + + for _, tc := range testCases { + t.Run(tc.Name, func(t *testing.T) { + PerformTestRequestCase(t, http.MethodPost, oauthRegisterPath(), tc) + }) + } } 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() + assertAccessDenied := func(t *testing.T, _ dcrSetup, res *http.Response) { 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"]) - } - }) + AssertEqual(t, jsonString(response["error"]), exceptions.OAuthErrorAccessDenied) + } + testCases := []TestRequestCase[dcrSetup]{ + { + Name: "account IAT cannot register an app", + ReqFn: func(t *testing.T) (dcrSetup, string) { + 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) + } + return setup, setup.accessToken + }, + RequestBodyFn: func(setup dcrSetup) any { return setup.body }, + HostFn: func(setup dcrSetup) string { return setup.host }, + ExpStatus: http.StatusUnauthorized, + AssertFn: assertAccessDenied, + }, + { + Name: "app IAT cannot register account credentials", + ReqFn: func(t *testing.T) (dcrSetup, string) { + setup := setupDynamicRegistration(t, true, true, false) + setup.host = cfg.BackendDomain() + setup.body["application_type"] = "native" + return setup, setup.accessToken + }, + RequestBodyFn: func(setup dcrSetup) any { return setup.body }, + HostFn: func(setup dcrSetup) string { return setup.host }, + ExpStatus: http.StatusUnauthorized, + AssertFn: assertAccessDenied, + }, + } - 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"]) - } - }) + for _, tc := range testCases { + t.Run(tc.Name, func(t *testing.T) { + PerformTestRequestCase(t, http.MethodPost, oauthRegisterPath(), tc) + }) + } } func TestDynamicRegistrationSoftwareStatementFailures(t *testing.T) { @@ -427,53 +417,59 @@ func TestDynamicRegistrationSoftwareStatementFailures(t *testing.T) { } approveSoftwareStatementKey(t, setup.account, goodJSON, goodKid, database.CredentialsUsageApp) - cases := []struct { - name string - statement string - wantError string - }{ + buildRequest := func(statement string) dcrSetup { + request := setup + request.body = cloneMap(setup.body) + request.body["software_statement"] = statement + return request + } + assertError := func(want string) func(*testing.T, dcrSetup, *http.Response) { + return func(t *testing.T, _ dcrSetup, res *http.Response) { + response := decodeJSONObject(t, res) + AssertEqual(t, jsonString(response["error"]), want) + } + } + + testCases := []TestRequestCase[dcrSetup]{ { - name: "bad signature", - statement: func() string { + Name: "bad signature", + ReqFn: func(t *testing.T) (dcrSetup, string) { badPrivate, _, _ := generateEd25519JWK(t) - return signSoftwareStatement(t, badPrivate, goodKid, jwt.MapClaims{ + return buildRequest(signSoftwareStatement(t, badPrivate, goodKid, jwt.MapClaims{ "iss": "https://" + setup.domain, "client_name": setup.clientName, "client_uri": "https://" + setup.domain, - }) - }(), - wantError: exceptions.OAuthErrorInvalidSoftwareStatement, + })), "" + }, + ExpStatus: http.StatusBadRequest, + AssertFn: assertError(exceptions.OAuthErrorInvalidSoftwareStatement), }, { - name: "unapproved kid", - statement: func() string { + Name: "unapproved kid", + ReqFn: func(t *testing.T) (dcrSetup, string) { private, kid, _ := generateEd25519JWK(t) - return signSoftwareStatement(t, private, kid, jwt.MapClaims{ + return buildRequest(signSoftwareStatement(t, private, kid, jwt.MapClaims{ "iss": "https://" + setup.domain, "client_name": setup.clientName, "client_uri": "https://" + setup.domain, - }) - }(), - wantError: exceptions.OAuthErrorUnapprovedSoftwareStatement, + })), "" + }, + ExpStatus: http.StatusBadRequest, + AssertFn: assertError(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, + Name: "issuer mismatch", + ReqFn: func(t *testing.T) (dcrSetup, string) { + return buildRequest(signSoftwareStatement(t, goodPrivate, goodKid, jwt.MapClaims{ + "iss": "https://unrelated.example.net", "client_name": setup.clientName, "client_uri": "https://" + setup.domain, + })), "" + }, + ExpStatus: http.StatusBadRequest, + AssertFn: assertError(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) - } + for _, tc := range testCases { + t.Run(tc.Name, func(t *testing.T) { + tc.RequestBodyFn = func(setup dcrSetup) any { return setup.body } + tc.HostFn = func(setup dcrSetup) string { return setup.host } + PerformTestRequestCase(t, http.MethodPost, oauthRegisterPath(), tc) }) } } @@ -489,111 +485,166 @@ func cloneMap(src map[string]any) map[string]any { 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()) - }) + for _, clientType := range []struct { + name string + appClient bool + bounded bool + }{ + {name: "app", appClient: true}, + {name: "account credentials", bounded: true}, + } { + clientType := clientType + t.Run(clientType.name, func(t *testing.T) { + var clientID, rat string + createCase := TestRequestCase[dcrSetup]{ + Name: "register client", + ReqFn: func(t *testing.T) (dcrSetup, string) { + setup := setupDynamicRegistration(t, clientType.appClient, clientType.bounded, false) + return setup, setup.accessToken + }, + RequestBodyFn: func(setup dcrSetup) any { return setup.body }, + HostFn: func(setup dcrSetup) string { return setup.host }, + ExpStatus: http.StatusCreated, + AssertFn: func(t *testing.T, setup dcrSetup, res *http.Response) { + var registrationURI string + clientID, rat, registrationURI = assertCreatedRegistration(t, setup, res) + AssertEqual(t, registrationURI, requestURL(setup.host, oauthRegisterClientPath(clientID))) + assertRFC7592Lifecycle(t, setup, clientID, rat, cfg.BackendDomain()) + }, + } + PerformTestRequestCase(t, http.MethodPost, oauthRegisterPath(), createCase) + }) + } 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) + testCase := TestRequestCase[string]{ + ReqFn: func(t *testing.T) (string, string) { + account := CreateTestAccount(t, GenerateFakeAccountData(t, services.AuthProviderLocal)) + cleanupAccount(t, account) + return account.Username + "." + cfg.BackendDomain(), "" + }, + RequestBodyFn: func(string) any { return nil }, + HostFn: func(host string) string { return host }, + ExpStatus: http.StatusOK, + AssertFn: func(t *testing.T, host string, res *http.Response) { + response := decodeJSONObject(t, res) + AssertEqual(t, jsonString(response["registration_endpoint"]), requestURL(host, oauthRegisterPath())) + }, } + PerformTestRequestCase(t, http.MethodGet, paths.WellKnownBase+paths.WellKnownOIDC, testCase) }) } +type rfc7592Request struct { + setup dcrSetup + host string + path string + body any +} + 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) - } + clientPath := oauthRegisterClientPath(clientID) - delRes := doJSONRequest(t, http.MethodDelete, clientURL, rat, nil) - defer delRes.Body.Close() - if delRes.StatusCode != http.StatusNoContent { - t.Fatalf("DELETE status=%d", delRes.StatusCode) + testCases := []TestRequestCase[rfc7592Request]{ + { + Name: "get registration", + ReqFn: func(t *testing.T) (rfc7592Request, string) { + return rfc7592Request{setup: setup, host: setup.host, path: clientPath}, rat + }, + ExpStatus: http.StatusOK, + AssertFn: func(t *testing.T, req rfc7592Request, res *http.Response) { + body := decodeJSONObject(t, res) + AssertEqual(t, res.Header.Get("Cache-Control"), "no-store") + AssertEqual(t, jsonString(body["client_id"]), clientID) + AssertEqual(t, jsonString(body["client_name"]), req.setup.clientName) + if _, ok := body["client_secret"]; ok { + t.Fatal("GET must not return client_secret") + } + if _, ok := body["registration_access_token"]; ok { + t.Fatal("GET must not return registration_access_token") + } + }, + Method: http.MethodGet, + }, + { + Name: "update registration", + ReqFn: func(t *testing.T) (rfc7592Request, string) { + body := cloneMap(setup.body) + body["client_name"] = updatedName + return rfc7592Request{setup: setup, host: setup.host, path: clientPath, body: body}, rat + }, + ExpStatus: http.StatusOK, + AssertFn: func(t *testing.T, _ rfc7592Request, res *http.Response) { + body := decodeJSONObject(t, res) + AssertEqual(t, jsonString(body["client_name"]), updatedName) + if _, ok := body["client_secret"]; ok { + t.Fatal("PUT must not return client_secret") + } + }, + Method: http.MethodPut, + }, + { + Name: "confirm update", + ReqFn: func(t *testing.T) (rfc7592Request, string) { + return rfc7592Request{setup: setup, host: setup.host, path: clientPath}, rat + }, + ExpStatus: http.StatusOK, + AssertFn: func(t *testing.T, _ rfc7592Request, res *http.Response) { + AssertEqual(t, jsonString(decodeJSONObject(t, res)["client_name"]), updatedName) + }, + Method: http.MethodGet, + }, + { + Name: "reject unknown client", + ReqFn: func(t *testing.T) (rfc7592Request, string) { + return rfc7592Request{host: setup.host, path: oauthRegisterClientPath("missing-client")}, rat + }, + ExpStatus: http.StatusUnauthorized, + AssertFn: func(*testing.T, rfc7592Request, *http.Response) {}, + Method: http.MethodGet, + }, + { + Name: "reject cross-host token", + ReqFn: func(t *testing.T) (rfc7592Request, string) { + return rfc7592Request{host: crossHost, path: clientPath}, rat + }, + ExpStatus: http.StatusUnauthorized, + AssertFn: func(*testing.T, rfc7592Request, *http.Response) {}, + Method: http.MethodGet, + }, + { + Name: "delete registration", + ReqFn: func(t *testing.T) (rfc7592Request, string) { + return rfc7592Request{host: setup.host, path: clientPath}, rat + }, + ExpStatus: http.StatusNoContent, + AssertFn: func(*testing.T, rfc7592Request, *http.Response) {}, + Method: http.MethodDelete, + }, + { + Name: "reject deleted client", + ReqFn: func(t *testing.T) (rfc7592Request, string) { + return rfc7592Request{host: setup.host, path: clientPath}, rat + }, + ExpStatus: http.StatusUnauthorized, + AssertFn: func(*testing.T, rfc7592Request, *http.Response) {}, + Method: http.MethodGet, + }, } - 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) + + for _, tc := range testCases { + t.Run(tc.Name, func(t *testing.T) { + tc.RequestBodyFn = func(req rfc7592Request) any { return req.body } + tc.HostFn = func(req rfc7592Request) string { return req.host } + tc.PathFromReqFn = func(req rfc7592Request) string { return req.path } + PerformTestRequestCase(t, tc.Method, "", tc) + }) } } @@ -613,24 +664,19 @@ func TestOAuthDynamicRegistrationIATTokenExchange(t *testing.T) { 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") + testCase := TestRequestCase[string]{ + ReqFn: func(t *testing.T) (string, string) { + form := url.Values{} + form.Set("grant_type", "authorization_code") + form.Set("code", code) + form.Set("client_id", setup.domain) + form.Set("code_verifier", verifier) + return form.Encode(), "" + }, + ExpStatus: http.StatusOK, + AssertFn: func(t *testing.T, _ string, res *http.Response) { + AssertNotEmpty(t, jsonString(decodeJSONObject(t, res)["access_token"])) + }, } + PerformTestRequestCaseWihURLEncodedBody(t, http.MethodPost, oauthIATTokenPath(), testCase) } diff --git a/idp/tests/oauth_test.go b/idp/tests/oauth_test.go index 063e5ee..2b7ea0e 100644 --- a/idp/tests/oauth_test.go +++ b/idp/tests/oauth_test.go @@ -707,37 +707,9 @@ func TestOAuthToken(t *testing.T) { beforeEachRefresh := func(t *testing.T) string { account := CreateTestAccount(t, GenerateFakeAccountData(t, services.AuthProviderGoogle)) - testTokens := GetTestTokens(t) - testServices := GetTestServices(t) - requestID := uuid.NewString() - ctx := context.Background() - - refreshToken, err := testTokens.CreateRefreshToken(tokens.AccountRefreshTokenOptions{ - PublicID: account.PublicID, - Version: account.Version(), - Scopes: []tokens.AccountScope{tokens.AccountScopeEmail, tokens.AccountScopeProfile, tokens.AccountScopeAdmin}, - }) - if err != nil { - t.Fatal("Failed to create refresh token", err) - } - - sRefreshToken, serviceErr := GetTestCrypto(t).SignToken(ctx, crypto.SignTokenOptions{ - RequestID: requestID, - Token: refreshToken, - GetJWKfn: testServices.BuildGetGlobalEncryptedJWKFn(ctx, services.BuildEncryptedJWKFnOptions{ - RequestID: requestID, - KeyType: database.TokenKeyTypeRefresh, - TTL: testTokens.GetRefreshTTL(), - }), - GetDecryptDEKfn: testServices.BuildGetGlobalDecDEKFn(ctx, services.BuildGetGlobalDEKFnOptions{ - RequestID: requestID, - }), + return GenerateTestAccountRefreshToken(t, &account, []tokens.AccountScope{ + tokens.AccountScopeEmail, tokens.AccountScopeProfile, tokens.AccountScopeAdmin, }) - if serviceErr != nil { - t.Fatal("Failed to sign refresh token", serviceErr) - } - - return sRefreshToken } beforeEachBearerJWT := func( @@ -1027,7 +999,7 @@ func TestOAuthToken(t *testing.T) { testS := GetTestServices(t) requestID := uuid.NewString() - refreshToken, err := testTokens.CreateRefreshToken(tokens.AccountRefreshTokenOptions{ + refreshToken, _, err := testTokens.CreateRefreshToken(tokens.AccountRefreshTokenOptions{ PublicID: account.PublicID, Version: account.Version() + 2, Scopes: []tokens.AccountScope{tokens.AccountScopeEmail, tokens.AccountScopeProfile, tokens.AccountScopeAdmin}, From c0f886386bf8e0aa382027f8614855fe02c981fc Mon Sep 17 00:00:00 2001 From: Afonso Barracha Date: Thu, 17 Sep 2026 17:15:37 +1200 Subject: [PATCH 09/16] chore: start add apps oauth dynamic iat --- .../oauth_dynamic_registration_accounts.go | 15 +------ .../oauth_dynamic_registration_apps.go | 40 +++++++++++++++++++ .../oauth_dynamic_registration_common.go | 36 +++++++++++++++++ 3 files changed, 78 insertions(+), 13 deletions(-) create mode 100644 idp/internal/services/oauth_dynamic_registration_common.go diff --git a/idp/internal/services/oauth_dynamic_registration_accounts.go b/idp/internal/services/oauth_dynamic_registration_accounts.go index 62acd24..f427a19 100644 --- a/idp/internal/services/oauth_dynamic_registration_accounts.go +++ b/idp/internal/services/oauth_dynamic_registration_accounts.go @@ -469,19 +469,8 @@ func (s *Services) InitiateOAuthDynamicRegistrationIATAuth( ) logger.InfoContext(ctx, "Starting OAuth dynamic registration IAT authorization...") - if opts.Origin == "" { - logger.WarnContext(ctx, "Origin header is missing") - return "", exceptions.NewUnauthorizedError() - } - - parsedOrigin, err := url.Parse(opts.Origin) - if err != nil { - logger.WarnContext(ctx, "Invalid origin header", "error", err) - return "", exceptions.NewUnauthorizedError() - } - if parsedOrigin.Host != opts.Domain { - logger.WarnContext(ctx, "Origin header does not match domain", "originHost", parsedOrigin.Host) - return "", exceptions.NewUnauthorizedError() + if serviceErr := validateOrigin(ctx, logger, opts.Origin, opts.Domain); serviceErr != nil { + return "", serviceErr } if opts.SessionKey == "" { diff --git a/idp/internal/services/oauth_dynamic_registration_apps.go b/idp/internal/services/oauth_dynamic_registration_apps.go index 34ea668..f9505ae 100644 --- a/idp/internal/services/oauth_dynamic_registration_apps.go +++ b/idp/internal/services/oauth_dynamic_registration_apps.go @@ -5,3 +5,43 @@ // file, You can obtain one at https://mozilla.org/MPL/2.0/. package services + +import ( + "context" + + "github.com/tugascript/devlogs/idp/internal/exceptions" +) + +const oauthDynamicRegistrationAppsLocation string = "oauth_dynamic_registration_apps" + +type InitiateAppsOAuthDynamicRegistrationIATAuthOptions struct { + RequestID string + Origin string + Domain string + State string + SessionKey string + RefreshToken string + Challenge string + ChallengeMethod string + RedirectURI string + BackendDomain string + Hostname string +} + +func (s *Services) InitiateAppsOAuthDynamicRegistrationIATAuth( + ctx context.Context, + opts InitiateAppsOAuthDynamicRegistrationIATAuthOptions, +) (string, *exceptions.ServiceError) { + logger := s.buildLogger( + opts.RequestID, + oauthDynamicRegistrationAppsLocation, + "InitiateAppsOAuthDynamicRegistrationIATAuth", + ).With("hostname", opts.Hostname) + logger.InfoContext(ctx, "Starting OAuth dynamic registration IAT authorization for Apps...") + + if serviceErr := validateOrigin(ctx, logger, opts.Origin, opts.Domain); serviceErr != nil { + return "", serviceErr + } + + return "", nil +} diff --git a/idp/internal/services/oauth_dynamic_registration_common.go b/idp/internal/services/oauth_dynamic_registration_common.go new file mode 100644 index 0000000..ac328c3 --- /dev/null +++ b/idp/internal/services/oauth_dynamic_registration_common.go @@ -0,0 +1,36 @@ +// 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 services + +import ( + "context" + "fmt" + "log/slog" + "net/url" + "strings" + + "github.com/tugascript/devlogs/idp/internal/exceptions" +) + +func validateOrigin(ctx context.Context, logger *slog.Logger, origin, domain string) *exceptions.ServiceError { + if origin == "" { + logger.WarnContext(ctx, "Origin header is missing") + return exceptions.NewUnauthorizedError() + } + + parsedOrigin, err := url.Parse(origin) + if err != nil { + logger.WarnContext(ctx, "Invalid origin header", "error", err) + return exceptions.NewUnauthorizedError() + } + if parsedOrigin.Host != domain && strings.Contains(parsedOrigin.Host, fmt.Sprintf(".%s", domain)) { + logger.WarnContext(ctx, "Origin header does not match domain", "originHost", parsedOrigin.Host) + return exceptions.NewUnauthorizedError() + } + + return nil +} From c9e95635906b104bd4456473a55d2da948c51846 Mon Sep 17 00:00:00 2001 From: Afonso Barracha Date: Mon, 21 Sep 2026 20:16:34 +1200 Subject: [PATCH 10/16] feat: add apps oauth dynamic registration iat --- .../oauth_dynamic_registration_apps.go | 770 +++++++++ .../oauth_dynamic_registration_apps_test.go | 114 ++ ...ccount_credentials_dynamic_registration.go | 644 +------ .../cache/apps_dynamic_registration.go | 399 +++++ idp/internal/providers/cache/cache.go | 7 +- .../providers/cache/dynamic_registration.go | 738 ++++++++ .../cache/dynamic_registration_test.go | 164 ++ idp/internal/server/routes/oauth.go | 82 +- idp/internal/server/routes/oauth_test.go | 49 + .../services/oauth_dynamic_registration.go | 145 ++ .../oauth_dynamic_registration_accounts.go | 173 +- .../oauth_dynamic_registration_apps.go | 1520 ++++++++++++++++- .../oauth_dynamic_registration_test.go | 82 + .../templates/account_dynamic_registration.go | 8 +- 14 files changed, 4121 insertions(+), 774 deletions(-) create mode 100644 idp/internal/controllers/oauth_dynamic_registration_apps.go create mode 100644 idp/internal/controllers/oauth_dynamic_registration_apps_test.go create mode 100644 idp/internal/providers/cache/apps_dynamic_registration.go create mode 100644 idp/internal/providers/cache/dynamic_registration.go create mode 100644 idp/internal/providers/cache/dynamic_registration_test.go create mode 100644 idp/internal/server/routes/oauth_test.go create mode 100644 idp/internal/services/oauth_dynamic_registration.go create mode 100644 idp/internal/services/oauth_dynamic_registration_test.go diff --git a/idp/internal/controllers/oauth_dynamic_registration_apps.go b/idp/internal/controllers/oauth_dynamic_registration_apps.go new file mode 100644 index 0000000..a30e6f8 --- /dev/null +++ b/idp/internal/controllers/oauth_dynamic_registration_apps.go @@ -0,0 +1,770 @@ +// 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 ( + "encoding/json" + "fmt" + "net/url" + + "github.com/gofiber/fiber/v3" + + "github.com/tugascript/devlogs/idp/internal/controllers/bodies" + "github.com/tugascript/devlogs/idp/internal/controllers/params" + "github.com/tugascript/devlogs/idp/internal/controllers/paths" + "github.com/tugascript/devlogs/idp/internal/exceptions" + "github.com/tugascript/devlogs/idp/internal/services" + "github.com/tugascript/devlogs/idp/internal/utils" +) + +func appsIATResumeURL(fields bodies.OAuthDynamicRegistrationIATAuthHiddenFieldsBody) string { + query := url.Values{ + "client_id": {fields.ClientID}, + "response_type": {fields.ResponseType}, + "redirect_uri": {fields.RedirectURI}, + "state": {fields.State}, + "code_challenge": {fields.CodeChallenge}, + } + if fields.CodeChallengeMethod != "" { + query.Set("code_challenge_method", fields.CodeChallengeMethod) + } + return oauthDynamicRegistrationIATCookiePath() + paths.OAuthAuth + "?" + query.Encode() +} + +const ( + oauthAppsDynamicRegistrationIAT string = "oauth_dynamic_registration_apps" + + appsIATCookieSuffix string = "_app_iat" + appsIAT2FACookieSuffix string = "_app_iat_2fa" +) + +func (c *Controllers) AppsOAuthDynamicRegistrationIATAuth(ctx fiber.Ctx) error { + requestID := getRequestID(ctx) + logger := c.buildLogger(requestID, oauthAppsDynamicRegistrationIAT, "AppsOAuthDynamicRegistrationIATAuth") + logRequest(logger, ctx) + + username, accountID, hostErr := getHostAccount(ctx) + if hostErr != nil { + return serviceErrorHTMLResponse(logger, ctx, hostErr) + } + hostname := username + "." + c.backendDomain + + baseQPrms := params.OAuthDynamicRegistrationIATAuthBaseQueryParams{ + ClientID: ctx.Query("client_id"), + RedirectURI: ctx.Query("redirect_uri"), + } + if err := c.validate.StructCtx(ctx.Context(), baseQPrms); err != nil { + return serviceErrorHTMLResponse(logger, ctx, exceptions.NewForbiddenError()) + } + + responseType := ctx.Query("response_type") + state := ctx.Query("state") + if responseType != "code" { + return c.redirectErrorCallback(logger, ctx, baseQPrms.RedirectURI, state, exceptions.OAuthErrorUnsupportedResponseType) + } + + qPrms := params.OAuthDynamicRegistrationIATAuthQueryParams{ + ResponseType: responseType, + Challenge: ctx.Query("code_challenge"), + ChallengeMethod: ctx.Query("code_challenge_method"), + State: state, + } + if err := c.validate.StructCtx(ctx.Context(), qPrms); err != nil { + return c.redirectErrorCallback(logger, ctx, baseQPrms.RedirectURI, state, exceptions.OAuthErrorInvalidRequest) + } + + sessionKey := ctx.Cookies(c.cookieName + appsIATCookieSuffix) + if sessionKey != "" { + // This ensures that the key is only used once + c.removeAppIATCookie(ctx) + } + + redirectURL, serviceErr := c.services.InitiateAppsOAuthDynamicRegistrationIATAuth( + ctx.Context(), + services.InitiateAppsOAuthDynamicRegistrationIATAuthOptions{ + AccountID: accountID, + Hostname: hostname, + RequestID: requestID, + Domain: baseQPrms.ClientID, + Origin: ctx.Get(fiber.HeaderOrigin), + State: qPrms.State, + SessionKey: sessionKey, + RefreshToken: ctx.Cookies(c.cookieName + refreshCookieSuffix), + Challenge: qPrms.Challenge, + ChallengeMethod: qPrms.ChallengeMethod, + RedirectURI: baseQPrms.RedirectURI, + BackendDomain: c.backendDomain, + }, + ) + if serviceErr != nil { + return c.redirectServiceErrorCallback(logger, ctx, baseQPrms.RedirectURI, state, serviceErr) + } + + logResponse(logger, ctx, fiber.StatusFound) + return ctx.Redirect().Status(fiber.StatusFound).To(redirectURL) +} + +func (c *Controllers) AppsOAuthDynamicRegistrationIATLoginGet(ctx fiber.Ctx) error { + requestID := getRequestID(ctx) + logger := c.buildLogger(requestID, oauthAppsDynamicRegistrationIAT, "AppsOAuthDynamicRegistrationIATLoginGet") + logRequest(logger, ctx) + + username, accountID, hostErr := getHostAccount(ctx) + if hostErr != nil { + return serviceErrorHTMLResponse(logger, ctx, hostErr) + } + hostname := username + "." + c.backendDomain + + uPrms := params.OAuthDynamicRegistrationIATAuthURLParams{ + ACCClientID: ctx.Params("accClientID"), + } + if err := c.validate.StructCtx(ctx.Context(), &uPrms); err != nil { + return serviceErrorHTMLResponse(logger, ctx, exceptions.NewNotFoundError()) + } + + baseQPrms := params.OAuthDynamicRegistrationIATAuthBaseQueryParams{ + ClientID: ctx.Query("client_id"), + RedirectURI: ctx.Query("redirect_uri"), + } + if err := c.validate.StructCtx(ctx.Context(), baseQPrms); err != nil { + return serviceErrorHTMLResponse(logger, ctx, exceptions.NewForbiddenError()) + } + + qPrms := params.OAuthDynamicRegistrationIATAuthQueryParams{ + ResponseType: ctx.Query("response_type"), + Challenge: ctx.Query("code_challenge"), + ChallengeMethod: ctx.Query("code_challenge_method"), + State: ctx.Query("state"), + } + if err := c.validate.StructCtx(ctx.Context(), qPrms); err != nil { + return serviceErrorHTMLResponse(logger, ctx, exceptions.NewForbiddenError()) + } + + loginHTML, serviceErr := c.services.AppsOAuthDynamicRegistrationIATAuthRender( + ctx.Context(), + services.AppsOAuthDynamicRegistrationIATAuthRenderOptions{ + AccountID: accountID, + Hostname: hostname, + RequestID: requestID, + ACCClientID: uPrms.ACCClientID, + State: qPrms.State, + Domain: baseQPrms.ClientID, + CodeChallenge: qPrms.Challenge, + CodeChallengeMethod: qPrms.ChallengeMethod, + RedirectURI: baseQPrms.RedirectURI, + }, + ) + if serviceErr != nil { + return serviceErrorHTMLResponse(logger, ctx, serviceErr) + } + + logResponse(logger, ctx, fiber.StatusOK) + return ctx.Status(fiber.StatusOK).Type("html").SendString(loginHTML) +} + +func (c *Controllers) saveAppIATCookie( + ctx fiber.Ctx, + sessionKey string, +) { + ctx.Cookie(&fiber.Cookie{ + Name: c.cookieName + appsIATCookieSuffix, + Value: sessionKey, + Path: oauthDynamicRegistrationIATCookiePath(), + HTTPOnly: true, + SameSite: fiber.CookieSameSiteLaxMode, + Secure: true, + MaxAge: int(c.services.GetOAuthCodeTTL()), + }) +} + +func (c *Controllers) removeAppIATCookie(ctx fiber.Ctx) { + ctx.Cookie(&fiber.Cookie{ + Name: c.cookieName + appsIATCookieSuffix, + Value: "", + Path: oauthDynamicRegistrationIATCookiePath(), + HTTPOnly: true, + Secure: true, + SameSite: fiber.CookieSameSiteNoneMode, + MaxAge: -1, + }) +} + +func (c *Controllers) saveAppIAT2FACookie(ctx fiber.Ctx, sessionID string) { + ctx.Cookie(&fiber.Cookie{ + Name: c.cookieName + appsIAT2FACookieSuffix, + Value: sessionID, + Path: oauthDynamicRegistrationIATCookiePath(), + HTTPOnly: true, + SameSite: fiber.CookieSameSiteLaxMode, + Secure: true, + MaxAge: int(c.services.Get2FATTL()), + }) +} + +func (c *Controllers) removeAppIAT2FACookie(ctx fiber.Ctx) { + ctx.Cookie(&fiber.Cookie{ + Name: c.cookieName + appsIAT2FACookieSuffix, + Value: "", + Path: oauthDynamicRegistrationIATCookiePath(), + HTTPOnly: true, + SameSite: fiber.CookieSameSiteLaxMode, + Secure: true, + MaxAge: -1, + }) +} + +func (c *Controllers) AppsOAuthDynamicRegistrationIATLoginPost(ctx fiber.Ctx) error { + requestID := getRequestID(ctx) + logger := c.buildLogger(requestID, oauthAppsDynamicRegistrationIAT, "AppsOAuthDynamicRegistrationIATLoginPost") + logRequest(logger, ctx) + + username, accountID, hostErr := getHostAccount(ctx) + if hostErr != nil { + return serviceErrorHTMLResponse(logger, ctx, hostErr) + } + hostname := username + "." + c.backendDomain + + uPrms := params.OAuthDynamicRegistrationIATAuthURLParams{ + ACCClientID: ctx.Params("accClientID"), + } + if err := c.validate.StructCtx(ctx.Context(), &uPrms); err != nil { + return serviceErrorHTMLResponse(logger, ctx, exceptions.NewNotFoundError()) + } + + if ctx.Get("Content-Type") != "application/x-www-form-urlencoded" { + return serviceErrorHTMLResponse(logger, ctx, exceptions.NewUnsupportedMediaTypeError("Only application/x-www-form-urlencoded is supported")) + } + + hiddenFields := bodies.OAuthDynamicRegistrationIATAuthHiddenFieldsBody{ + CSRFToken: ctx.FormValue("csrf_token"), + ClientID: ctx.FormValue("client_id"), + ResponseType: ctx.FormValue("response_type"), + CodeChallenge: ctx.FormValue("code_challenge"), + CodeChallengeMethod: ctx.FormValue("code_challenge_method"), + State: ctx.FormValue("state"), + RedirectURI: ctx.FormValue("redirect_uri"), + } + if err := c.validate.StructCtx(ctx.Context(), &hiddenFields); err != nil { + return serviceErrorHTMLResponse(logger, ctx, exceptions.NewForbiddenError()) + } + + loginBody := bodies.LoginBody{ + Email: ctx.FormValue("email"), + Password: ctx.FormValue("password"), + } + if err := c.validate.StructCtx(ctx.Context(), &loginBody); err != nil { + valErr := validationErrorException(exceptions.ValidationResponseLocationBody, err) + loginHTML, serviceErr := c.services.AppsOAuthDynamicRegistrationIATAuthReRender( + ctx.Context(), + services.AppsOAuthDynamicRegistrationIATAuthReRenderOptions{ + AccountID: accountID, + Hostname: hostname, + RequestID: requestID, + Errors: utils.MapSlice(valErr.Fields, func(t *exceptions.FieldError) string { + return fmt.Sprintf("%s %s", t.Param, t.Message) + }), + CSRFToken: hiddenFields.CSRFToken, + ACCClientID: uPrms.ACCClientID, + State: hiddenFields.State, + Domain: hiddenFields.ClientID, + CodeChallenge: hiddenFields.CodeChallenge, + CodeChallengeMethod: hiddenFields.CodeChallengeMethod, + RedirectURI: hiddenFields.RedirectURI, + }, + ) + if serviceErr != nil { + return serviceErrorHTMLResponse(logger, ctx, serviceErr) + } + + logResponse(logger, ctx, fiber.StatusOK) + return ctx. + Status(fiber.StatusOK). + Type("html"). + SendString(loginHTML) + } + + redirectURL, sessionKey, loggedIn, serviceErr := c.services.AppsOAuthDynamicRegistrationIATLogin( + ctx.Context(), + services.AppsOAuthDynamicRegistrationIATLoginOptions{ + AccountID: accountID, + Hostname: hostname, + RequestID: requestID, + ACCClientID: uPrms.ACCClientID, + Domain: hiddenFields.ClientID, + CSRFToken: hiddenFields.CSRFToken, + CodeChallenge: hiddenFields.CodeChallenge, + CodeChallengeMethod: hiddenFields.CodeChallengeMethod, + State: hiddenFields.State, + RedirectURI: hiddenFields.RedirectURI, + Email: loginBody.Email, + Password: loginBody.Password, + BackendDomain: c.backendDomain, + }, + ) + if serviceErr != nil { + if serviceErr.Code == exceptions.CodeUnauthorized { + loginHTML, serviceErr := c.services.AppsOAuthDynamicRegistrationIATAuthReRender( + ctx.Context(), + services.AppsOAuthDynamicRegistrationIATAuthReRenderOptions{ + AccountID: accountID, + Hostname: hostname, + RequestID: requestID, + Errors: []string{"Invalid credentials"}, + CSRFToken: hiddenFields.CSRFToken, + ACCClientID: uPrms.ACCClientID, + State: hiddenFields.State, + Domain: hiddenFields.ClientID, + CodeChallenge: hiddenFields.CodeChallenge, + CodeChallengeMethod: hiddenFields.CodeChallengeMethod, + RedirectURI: hiddenFields.RedirectURI, + }, + ) + if serviceErr != nil { + return serviceErrorHTMLResponse(logger, ctx, serviceErr) + } + + logResponse(logger, ctx, fiber.StatusOK) + return ctx. + Status(fiber.StatusOK). + Type("html"). + SendString(loginHTML) + } + + return serviceErrorHTMLResponse(logger, ctx, serviceErr) + } + + if !loggedIn { + c.saveAppIAT2FACookie(ctx, sessionKey) + logResponse(logger, ctx, fiber.StatusSeeOther) + return ctx.Redirect().Status(fiber.StatusSeeOther).To(redirectURL) + } + + c.saveAppIATCookie(ctx, sessionKey) + logResponse(logger, ctx, fiber.StatusSeeOther) + return ctx.Redirect().Status(fiber.StatusSeeOther).To(appsIATResumeURL(hiddenFields)) +} + +func (c *Controllers) AppsOAuthDynamicRegistrationIAT2FAGet(ctx fiber.Ctx) error { + requestID := getRequestID(ctx) + logger := c.buildLogger(requestID, oauthAppsDynamicRegistrationIAT, "AppsOAuthDynamicRegistrationIAT2FAGet") + logRequest(logger, ctx) + + username, accountID, hostErr := getHostAccount(ctx) + if hostErr != nil { + return serviceErrorHTMLResponse(logger, ctx, hostErr) + } + hostname := username + "." + c.backendDomain + + uPrms := params.OAuthDynamicRegistrationIATAuthURLParams{ + ACCClientID: ctx.Params("accClientID"), + } + if err := c.validate.StructCtx(ctx.Context(), &uPrms); err != nil { + return serviceErrorHTMLResponse(logger, ctx, exceptions.NewNotFoundError()) + } + + baseQPrms := params.OAuthDynamicRegistrationIATAuthBaseQueryParams{ + ClientID: ctx.Query("client_id"), + RedirectURI: ctx.Query("redirect_uri"), + } + if err := c.validate.StructCtx(ctx.Context(), baseQPrms); err != nil { + return serviceErrorHTMLResponse(logger, ctx, exceptions.NewForbiddenError()) + } + + qPrms := params.OAuthDynamicRegistrationIATAuthQueryParams{ + ResponseType: ctx.Query("response_type"), + Challenge: ctx.Query("code_challenge"), + ChallengeMethod: ctx.Query("code_challenge_method"), + State: ctx.Query("state"), + } + if err := c.validate.StructCtx(ctx.Context(), qPrms); err != nil { + return serviceErrorHTMLResponse(logger, ctx, exceptions.NewForbiddenError()) + } + + sessionID := ctx.Cookies(c.cookieName + appsIAT2FACookieSuffix) + if sessionID == "" { + return serviceErrorHTMLResponse(logger, ctx, exceptions.NewUnauthorizedError()) + } + + twoFAHTML, serviceErr := c.services.AppsOAuthDynamicRegistrationIAT2FARender( + ctx.Context(), + services.AppsOAuthDynamicRegistrationIAT2FARenderOptions{ + AccountID: accountID, + Hostname: hostname, + RequestID: requestID, + Domain: baseQPrms.ClientID, + ACCClientID: uPrms.ACCClientID, + SessionID: sessionID, + Challenge: qPrms.Challenge, + ChallengeMethod: qPrms.ChallengeMethod, + State: qPrms.State, + RedirectURI: baseQPrms.RedirectURI, + }, + ) + if serviceErr != nil { + return serviceErrorHTMLResponse(logger, ctx, serviceErr) + } + + logResponse(logger, ctx, fiber.StatusOK) + return ctx.Status(fiber.StatusOK).Type("html").SendString(twoFAHTML) +} + +func (c *Controllers) AppsOAuthDynamicRegistrationIAT2FAPost(ctx fiber.Ctx) error { + requestID := getRequestID(ctx) + logger := c.buildLogger(requestID, oauthAppsDynamicRegistrationIAT, "AppsOAuthDynamicRegistrationIAT2FAPost") + logRequest(logger, ctx) + + username, accountID, hostErr := getHostAccount(ctx) + if hostErr != nil { + return serviceErrorHTMLResponse(logger, ctx, hostErr) + } + hostname := username + "." + c.backendDomain + + uPrms := params.OAuthDynamicRegistrationIATAuthURLParams{ + ACCClientID: ctx.Params("accClientID"), + } + if err := c.validate.StructCtx(ctx.Context(), &uPrms); err != nil { + return serviceErrorHTMLResponse(logger, ctx, exceptions.NewNotFoundError()) + } + + sessionID := ctx.Cookies(c.cookieName + appsIAT2FACookieSuffix) + if sessionID == "" { + return serviceErrorHTMLResponse(logger, ctx, exceptions.NewUnauthorizedError()) + } + + if ctx.Get("Content-Type") != "application/x-www-form-urlencoded" { + return serviceErrorHTMLResponse(logger, ctx, exceptions.NewUnsupportedMediaTypeError("Only application/x-www-form-urlencoded is supported")) + } + + hiddenFields := bodies.OAuthDynamicRegistrationIATAuthHiddenFieldsBody{ + CSRFToken: ctx.FormValue("csrf_token"), + ClientID: ctx.FormValue("client_id"), + ResponseType: ctx.FormValue("response_type"), + CodeChallenge: ctx.FormValue("code_challenge"), + CodeChallengeMethod: ctx.FormValue("code_challenge_method"), + State: ctx.FormValue("state"), + RedirectURI: ctx.FormValue("redirect_uri"), + } + if err := c.validate.StructCtx(ctx.Context(), &hiddenFields); err != nil { + return serviceErrorHTMLResponse(logger, ctx, exceptions.NewForbiddenError()) + } + + twoFABody := bodies.TwoFactorLoginBody{ + Code: ctx.FormValue("code"), + } + if err := c.validate.StructCtx(ctx.Context(), &twoFABody); err != nil { + valErr := validationErrorException(exceptions.ValidationResponseLocationBody, err) + twoFAHTML, serviceErr := c.services.AppsOAuthDynamicRegistrationIAT2FAReRender( + ctx.Context(), + services.AppsOAuthDynamicRegistrationIAT2FAReRenderOptions{ + AccountID: accountID, + Hostname: hostname, + RequestID: requestID, + Domain: hiddenFields.ClientID, + ACCClientID: uPrms.ACCClientID, + SessionID: sessionID, + Errors: utils.MapSlice(valErr.Fields, func(t *exceptions.FieldError) string { + return fmt.Sprintf("%s %s", t.Param, t.Message) + }), + CSRFToken: hiddenFields.CSRFToken, + Challenge: hiddenFields.CodeChallenge, + ChallengeMethod: hiddenFields.CodeChallengeMethod, + State: hiddenFields.State, + RedirectURI: hiddenFields.RedirectURI, + }, + ) + if serviceErr != nil { + return serviceErrorHTMLResponse(logger, ctx, serviceErr) + } + + logResponse(logger, ctx, fiber.StatusOK) + return ctx. + Status(fiber.StatusOK). + Type("html"). + SendString(twoFAHTML) + } + + _, sessionKey, serviceErr := c.services.AppsOAuthDynamicRegistrationIATVerify2FACode( + ctx.Context(), + services.AppsOAuthDynamicRegistrationIATVerify2FACodeOptions{ + AccountID: accountID, + Hostname: hostname, + RequestID: requestID, + ACCClientID: uPrms.ACCClientID, + Domain: hiddenFields.ClientID, + SessionID: sessionID, + CSRFToken: hiddenFields.CSRFToken, + Code: twoFABody.Code, + BackendDomain: c.backendDomain, + }, + ) + if serviceErr != nil { + if serviceErr.Code == exceptions.CodeUnauthorized { + twoFAHTML, serviceErr := c.services.AppsOAuthDynamicRegistrationIAT2FAReRender( + ctx.Context(), + services.AppsOAuthDynamicRegistrationIAT2FAReRenderOptions{ + AccountID: accountID, + Hostname: hostname, + RequestID: requestID, + Domain: hiddenFields.ClientID, + ACCClientID: uPrms.ACCClientID, + SessionID: sessionID, + Errors: []string{"Invalid 2FA code"}, + CSRFToken: hiddenFields.CSRFToken, + Challenge: hiddenFields.CodeChallenge, + ChallengeMethod: hiddenFields.CodeChallengeMethod, + State: hiddenFields.State, + RedirectURI: hiddenFields.RedirectURI, + }, + ) + if serviceErr != nil { + return serviceErrorHTMLResponse(logger, ctx, serviceErr) + } + + logResponse(logger, ctx, fiber.StatusOK) + return ctx. + Status(fiber.StatusOK). + Type("html"). + SendString(twoFAHTML) + } + + return serviceErrorHTMLResponse(logger, ctx, serviceErr) + } + + c.removeAppIAT2FACookie(ctx) + c.saveAppIATCookie(ctx, sessionKey) + logResponse(logger, ctx, fiber.StatusSeeOther) + return ctx.Redirect().Status(fiber.StatusSeeOther).To(appsIATResumeURL(hiddenFields)) +} + +func (c *Controllers) AppsOAuthDynamicRegistrationIATExtAuthGet(ctx fiber.Ctx) error { + requestID := getRequestID(ctx) + logger := c.buildLogger(requestID, oauthAppsDynamicRegistrationIAT, "AppsOAuthDynamicRegistrationIATExtAuthGet") + logRequest(logger, ctx) + + username, accountID, hostErr := getHostAccount(ctx) + if hostErr != nil { + return serviceErrorHTMLResponse(logger, ctx, hostErr) + } + hostname := username + "." + c.backendDomain + + uPrms := struct { + ACCClientID string `validate:"required,min=22,max=22,alphanum"` + Provider string `validate:"required,oneof=apple facebook github google microsoft"` + }{ + ACCClientID: ctx.Params("accClientID"), + Provider: ctx.Params("provider"), + } + if err := c.validate.StructCtx(ctx.Context(), &uPrms); err != nil { + return serviceErrorHTMLResponse(logger, ctx, exceptions.NewNotFoundError()) + } + + baseQPrms := params.OAuthDynamicRegistrationIATAuthBaseQueryParams{ + ClientID: ctx.Query("client_id"), + RedirectURI: ctx.Query("redirect_uri"), + } + if err := c.validate.StructCtx(ctx.Context(), baseQPrms); err != nil { + return serviceErrorHTMLResponse(logger, ctx, exceptions.NewForbiddenError()) + } + + responseType := ctx.Query("response_type") + state := ctx.Query("state") + if responseType != "code" { + return c.redirectErrorCallback(logger, ctx, baseQPrms.RedirectURI, state, exceptions.OAuthErrorUnsupportedResponseType) + } + + qPrms := params.OAuthDynamicRegistrationIATAuthQueryParams{ + ResponseType: responseType, + Challenge: ctx.Query("code_challenge"), + ChallengeMethod: ctx.Query("code_challenge_method"), + State: state, + } + if err := c.validate.StructCtx(ctx.Context(), qPrms); err != nil { + return c.redirectErrorCallback(logger, ctx, baseQPrms.RedirectURI, state, exceptions.OAuthErrorInvalidRequest) + } + + authURL, serviceErr := c.services.AppsOAuthDynamicRegistrationIATExtGet( + ctx.Context(), + services.AppsOAuthDynamicRegistrationIATExtGetOptions{ + AccountID: accountID, + Hostname: hostname, + RequestID: requestID, + ACCClientID: uPrms.ACCClientID, + Provider: uPrms.Provider, + Domain: baseQPrms.ClientID, + CallbackURL: "https://" + hostname + 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, + }, + ) + if serviceErr != nil { + return serviceErrorHTMLResponse(logger, ctx, serviceErr) + } + + logResponse(logger, ctx, fiber.StatusFound) + return ctx.Redirect().Status(fiber.StatusFound).To(authURL) +} + +func (c *Controllers) AppsOAuthDynamicRegistrationIATExtCB(ctx fiber.Ctx) error { + requestID := getRequestID(ctx) + logger := c.buildLogger(requestID, oauthAppsDynamicRegistrationIAT, "AppsOAuthDynamicRegistrationIATExtCB") + logRequest(logger, ctx) + + username, accountID, hostErr := getHostAccount(ctx) + if hostErr != nil { + return serviceErrorHTMLResponse(logger, ctx, hostErr) + } + hostname := username + "." + c.backendDomain + + uPrms := params.OAuthDynamicRegistrationIATExtAuthURLParams{ + ACCClientID: ctx.Params("accClientID"), + Provider: ctx.Params("provider"), + } + if err := c.validate.StructCtx(ctx.Context(), &uPrms); err != nil { + return serviceErrorHTMLResponse(logger, ctx, exceptions.NewNotFoundError()) + } + + qPrms := params.OAuthCallbackQueryParams{ + Code: ctx.Query("code"), + State: ctx.Query("state"), + } + if err := c.validate.StructCtx(ctx.Context(), &qPrms); err != nil { + return serviceErrorHTMLResponse(logger, ctx, exceptions.NewForbiddenError()) + } + + cbURL, serviceErr := c.services.AppsOAuthDynamicRegistrationIATExtCB( + ctx.Context(), + services.AppsOAuthDynamicRegistrationIATExtCBOptions{ + AccountID: accountID, + Hostname: hostname, + RequestID: requestID, + ACCClientID: uPrms.ACCClientID, + Provider: uPrms.Provider, + State: qPrms.State, + Code: qPrms.Code, + RedirectURL: "https://" + hostname + paths.V1 + paths.AuthBase + paths.OAuthBase + paths.InitialAccessToken + "/" + uPrms.ACCClientID + paths.InitialAccessTokenAuthEXT + "/" + uPrms.Provider + paths.InitialAccessTokenCallback, + BackendDomain: c.backendDomain, + }, + ) + if serviceErr != nil { + return serviceErrorHTMLResponse(logger, ctx, serviceErr) + } + + logResponse(logger, ctx, fiber.StatusFound) + return ctx.Redirect().Status(fiber.StatusFound).To(cbURL) +} + +func (c *Controllers) AppsOAuthDynamicRegistrationIATExtAppleCB(ctx fiber.Ctx) error { + requestID := getRequestID(ctx) + logger := c.buildLogger(requestID, oauthAppsDynamicRegistrationIAT, "AppsOAuthDynamicRegistrationIATExtAppleCB") + logRequest(logger, ctx) + + username, accountID, hostErr := getHostAccount(ctx) + if hostErr != nil { + return serviceErrorHTMLResponse(logger, ctx, hostErr) + } + hostname := username + "." + c.backendDomain + + uPrms := params.OAuthDynamicRegistrationIATExtAppleURLParams{ + ACCClientID: ctx.Params("accClientID"), + } + if err := c.validate.StructCtx(ctx.Context(), &uPrms); err != nil { + return serviceErrorHTMLResponse(logger, ctx, exceptions.NewNotFoundError()) + } + + if ctx.Get("Content-Type") != "application/x-www-form-urlencoded" { + return serviceErrorHTMLResponse(logger, ctx, exceptions.NewUnsupportedMediaTypeError("Only application/x-www-form-urlencoded is supported")) + } + + qPrms := bodies.OAuthDynamicRegistrationIATExtAppleBody{ + Code: ctx.FormValue("code"), + State: ctx.FormValue("state"), + User: ctx.FormValue("user"), + } + if err := c.validate.StructCtx(ctx.Context(), &qPrms); err != nil { + return serviceErrorHTMLResponse(logger, ctx, exceptions.NewForbiddenError()) + } + + user := new(bodies.OAuthDynamicRegistrationIATExtAppleUserBody) + if err := json.Unmarshal([]byte(qPrms.User), user); err != nil { + return serviceErrorHTMLResponse(logger, ctx, exceptions.NewForbiddenError()) + } + if err := c.validate.StructCtx(ctx.Context(), user); err != nil { + return serviceErrorHTMLResponse(logger, ctx, exceptions.NewForbiddenError()) + } + + cbURL, serviceErr := c.services.AppsOAuthDynamicRegistrationIATExtAppleCB( + ctx.Context(), + services.AppsOAuthDynamicRegistrationIATExtAppleCBOptions{ + AccountID: accountID, + Hostname: hostname, + RequestID: requestID, + ACCClientID: uPrms.ACCClientID, + Email: user.Email, + Code: qPrms.Code, + State: qPrms.State, + RedirectURL: "https://" + hostname + paths.V1 + paths.AuthBase + paths.OAuthBase + paths.InitialAccessToken + "/" + uPrms.ACCClientID + paths.InitialAccessTokenAuthEXT + "/" + services.AuthProviderApple + paths.InitialAccessTokenCallback, + BackendDomain: c.backendDomain, + }, + ) + if serviceErr != nil { + return serviceErrorHTMLResponse(logger, ctx, serviceErr) + } + + logResponse(logger, ctx, fiber.StatusFound) + return ctx.Redirect().Status(fiber.StatusFound).To(cbURL) +} + +func (c *Controllers) AppsOAuthDynamicRegistrationIATToken(ctx fiber.Ctx) error { + requestID := getRequestID(ctx) + logger := c.buildLogger(requestID, oauthAppsDynamicRegistrationIAT, "AppsOAuthDynamicRegistrationIATToken") + logRequest(logger, ctx) + + username, accountID, hostErr := getHostAccount(ctx) + if hostErr != nil { + return oauthErrorResponse(logger, ctx, exceptions.OAuthErrorUnauthorizedClient) + } + hostname := username + "." + c.backendDomain + + if ctx.Get("Content-Type") != "application/x-www-form-urlencoded" { + return oauthErrorResponse(logger, ctx, exceptions.OAuthErrorInvalidRequest) + } + + grantType := ctx.FormValue("grant_type") + if grantType != "authorization_code" { + return oauthErrorResponse(logger, ctx, exceptions.OAuthErrorUnsupportedGrantType) + } + + body := bodies.OAuthDynamicRegistrationIATTokenBody{ + GrantType: grantType, + Code: ctx.FormValue("code"), + ClientID: ctx.FormValue("client_id"), + CodeVerifier: ctx.FormValue("code_verifier"), + } + if err := c.validate.StructCtx(ctx.Context(), &body); err != nil { + return oauthErrorResponse(logger, ctx, exceptions.OAuthErrorInvalidRequest) + } + + authDTO, serviceErr := c.services.VerifyAppsOAuthDynamicRegistrationIATCode( + ctx.Context(), + services.VerifyAppsOAuthDynamicRegistrationIATCodeOptions{ + AccountID: accountID, + Hostname: hostname, + BackendDomain: c.backendDomain, + RequestID: requestID, + Code: body.Code, + CodeVerifier: body.CodeVerifier, + Domain: body.ClientID, + }, + ) + if serviceErr != nil { + return oauthErrorResponseMapper(logger, ctx, serviceErr) + } + + logResponse(logger, ctx, fiber.StatusOK) + return ctx.Status(fiber.StatusOK).JSON(authDTO) +} diff --git a/idp/internal/controllers/oauth_dynamic_registration_apps_test.go b/idp/internal/controllers/oauth_dynamic_registration_apps_test.go new file mode 100644 index 0000000..0cdd1c8 --- /dev/null +++ b/idp/internal/controllers/oauth_dynamic_registration_apps_test.go @@ -0,0 +1,114 @@ +package controllers + +import ( + "io" + "log/slog" + "net/http/httptest" + "net/url" + "strings" + "testing" + + "github.com/go-playground/validator/v10" + "github.com/gofiber/fiber/v3" + "github.com/tugascript/devlogs/idp/internal/controllers/bodies" + "github.com/tugascript/devlogs/idp/internal/controllers/paths" + "github.com/tugascript/devlogs/idp/internal/services/templates" +) + +func TestAppsIATControllersRequireHostContext(t *testing.T) { + c := NewControllers(slog.New(slog.NewTextHandler(io.Discard, nil)), nil, validator.New(), "example.com", "id.example.com", "session") + for name, handler := range map[string]fiber.Handler{ + "auth": c.AppsOAuthDynamicRegistrationIATAuth, + "loginGet": c.AppsOAuthDynamicRegistrationIATLoginGet, + "loginPost": c.AppsOAuthDynamicRegistrationIATLoginPost, + "twoFAGet": c.AppsOAuthDynamicRegistrationIAT2FAGet, + "twoFAPost": c.AppsOAuthDynamicRegistrationIAT2FAPost, + "external": c.AppsOAuthDynamicRegistrationIATExtAuthGet, + "callback": c.AppsOAuthDynamicRegistrationIATExtCB, + "apple": c.AppsOAuthDynamicRegistrationIATExtAppleCB, + "token": c.AppsOAuthDynamicRegistrationIATToken, + } { + t.Run(name, func(t *testing.T) { + app := fiber.New() + app.Get("/", handler) + response, err := app.Test(httptest.NewRequest("GET", "https://id.example.com/", nil)) + if err != nil { + t.Fatal(err) + } + defer response.Body.Close() + if response.StatusCode < 400 || response.StatusCode >= 500 { + t.Fatalf("expected client error without tenant context, got %d", response.StatusCode) + } + }) + } +} + +func TestAppsIATTemplatesUseRegisteredRoutes(t *testing.T) { + const clientID = "abcdefghijklmnopqrstuv" + base := oauthDynamicRegistrationIATCookiePath() + "/" + clientID + login, err := templates.BuildAccountDynamicRegistrationIATAuthTemplate(templates.AccountDynamicRegistrationIATAuthOptions{ACCClientID: clientID, AppleEnabled: true, GoogleEnabled: true}) + if err != nil { + t.Fatal(err) + } + login = strings.ReplaceAll(login, `\/`, `/`) + for _, path := range []string{base + paths.AuthLogin, base + paths.InitialAccessTokenAuthEXT + "/apple", base + paths.InitialAccessTokenAuthEXT + "/google"} { + if !strings.Contains(login, path) { + t.Errorf("login template missing route %s", path) + } + } + twoFA, err := templates.BuildAccountDynamicRegistrationIAT2FATemplate(templates.AccountDynamicRegistrationIAT2FAOptions{ACCClientID: clientID}) + if err != nil { + t.Fatal(err) + } + if !strings.Contains(twoFA, base+paths.AuthLogin+paths.Auth2FA) { + t.Fatal("2FA form points to an unregistered route") + } +} + +func TestAppsIATResumePreservesAuthorization(t *testing.T) { + fields := bodies.OAuthDynamicRegistrationIATAuthHiddenFieldsBody{ + ClientID: "client.example", ResponseType: "code", State: "state & value", + RedirectURI: "https://client.example/callback?value=1", CodeChallenge: "challenge", CodeChallengeMethod: "S256", CSRFToken: "secret", + } + resume, err := url.Parse(appsIATResumeURL(fields)) + if err != nil { + t.Fatal(err) + } + if resume.IsAbs() { + t.Fatal("resume must remain on the tenant host") + } + for key, want := range map[string]string{"client_id": fields.ClientID, "response_type": "code", "state": fields.State, "redirect_uri": fields.RedirectURI, "code_challenge": fields.CodeChallenge, "code_challenge_method": "S256"} { + if got := resume.Query().Get(key); got != want { + t.Errorf("%s: got %q want %q", key, got, want) + } + } + if resume.Query().Has("csrf_token") { + t.Fatal("CSRF token leaked into redirect") + } +} + +func TestAppsIATCookieRemovalIsHostOnly(t *testing.T) { + c := &Controllers{cookieName: "session"} + app := fiber.New() + app.Get("/", func(ctx fiber.Ctx) error { + c.removeAppIATCookie(ctx) + c.removeAppIAT2FACookie(ctx) + return ctx.SendStatus(204) + }) + response, err := app.Test(httptest.NewRequest("GET", "https://tenant.example.com/", nil)) + if err != nil { + t.Fatal(err) + } + defer response.Body.Close() + if len(response.Cookies()) != 2 { + t.Fatal("expected both apps cookies to be removed") + } + for _, cookie := range response.Cookies() { + if cookie.Domain != "" || !cookie.Secure || !cookie.HttpOnly || cookie.Path != oauthDynamicRegistrationIATCookiePath() || cookie.MaxAge >= 0 { + t.Fatalf("invalid cookie scope: %+v", cookie) + } + if cookie.Name != "session_app_iat" && cookie.Name != "session_app_iat_2fa" { + t.Fatalf("unexpected cookie: %s", cookie.Name) + } + } +} diff --git a/idp/internal/providers/cache/account_credentials_dynamic_registration.go b/idp/internal/providers/cache/account_credentials_dynamic_registration.go index 2548d29..c6d3066 100644 --- a/idp/internal/providers/cache/account_credentials_dynamic_registration.go +++ b/idp/internal/providers/cache/account_credentials_dynamic_registration.go @@ -8,12 +8,7 @@ package cache import ( "context" - "encoding/json" "fmt" - "strings" - "time" - - "github.com/google/uuid" "github.com/tugascript/devlogs/idp/internal/utils" ) @@ -22,9 +17,6 @@ const ( accountCredentialsDynamicRegistrationLocation string = "account_credentials_dynamic_registration" accountCredentialsDynamicRegistrationIATPrefix string = "account_credentials_dynamic_registration_iat" - - csrfTokenByteLen int = 16 - sessionKeyByteLen int = 32 ) func buildAccountCredentialsDynamicRegistrationIATAuthCacheKey(clientID string) string { @@ -65,19 +57,7 @@ func (c *Cache) SaveAccountCredentialsDynamicRegistrationIATAuth( RedirectURI: opts.RedirectURI, Challenge: opts.Challenge, } - dataBytes, err := json.Marshal(data) - if err != nil { - logger.ErrorContext(ctx, "Failed to marshal account credentials dynamic registration IAT data", "error", err) - return "", err - } - - clientID := utils.Base62UUID() - return clientID, c.storage.SetWithContext( - ctx, - buildAccountCredentialsDynamicRegistrationIATAuthCacheKey(clientID), - dataBytes, - c.oauthStateTTL, - ) + return c.accountCredentialsDynamicRegistration().saveAuth(ctx, logger, data) } type GetAccountCredentialsDynamicRegistrationIATAuthOptions struct { @@ -98,23 +78,7 @@ func (c *Cache) GetAccountCredentialsDynamicRegistrationAuthIAT( ) logger.DebugContext(ctx, "Getting account credentials dynamic registration IAT...") - data, err := c.storage.GetWithContext(ctx, buildAccountCredentialsDynamicRegistrationIATAuthCacheKey(opts.ClientID)) - if err != nil { - logger.ErrorContext(ctx, "Failed to get account credentials dynamic registration IAT", "error", err) - return AccountCredentialsDynamicRegistrationIATAuthData{}, false, err - } - if data == nil { - logger.DebugContext(ctx, "Account credentials dynamic registration IAT not found") - return AccountCredentialsDynamicRegistrationIATAuthData{}, false, nil - } - - var authData AccountCredentialsDynamicRegistrationIATAuthData - if err := json.Unmarshal(data, &authData); err != nil { - logger.ErrorContext(ctx, "Failed to unmarshal account credentials dynamic registration IAT data", "error", err) - return AccountCredentialsDynamicRegistrationIATAuthData{}, false, err - } - - return authData, true, nil + return getDynamicRegistrationAuth[AccountCredentialsDynamicRegistrationIATAuthData](ctx, c.accountCredentialsDynamicRegistration(), logger, opts.ClientID) } type DeleteAccountCredentialsDynamicRegistrationIATAuthOptions struct { @@ -138,674 +102,140 @@ func (c *Cache) DeleteAccountCredentialsDynamicRegistrationIATAuth( return c.storage.DeleteWithContext(ctx, buildAccountCredentialsDynamicRegistrationIATAuthCacheKey(opts.ClientID)) } -func buildAccountCredentialsDynamicRegistrationIATLoginCSRFKey(domain, clientID string) string { - return fmt.Sprintf("%s:login:%s:%s", accountCredentialsDynamicRegistrationIATPrefix, domain, clientID) -} - -type SaveAccountCredentialsDynamicRegistrationIATLoginCSRFOptions struct { - RequestID string - ClientID string - Domain string -} +type SaveAccountCredentialsDynamicRegistrationIATLoginCSRFOptions = SaveDynamicRegistrationIATLoginCSRFOptions func (c *Cache) SaveAccountCredentialsDynamicRegistrationIATLoginCSRF( ctx context.Context, opts SaveAccountCredentialsDynamicRegistrationIATLoginCSRFOptions, ) (string, error) { - logger := utils.BuildLogger(c.logger, utils.LoggerOptions{ - Location: accountCredentialsDynamicRegistrationLocation, - Method: "SaveAccountCredentialsDynamicRegistrationIATLoginCSRF", - RequestID: opts.RequestID, - }).With( - "clientId", opts.ClientID, - "domain", opts.Domain, - ) - logger.DebugContext(ctx, "Saving account credentials dynamic registration IAT login CSRF token...") - - csrfToken, err := utils.GenerateBase64Secret(csrfTokenByteLen) - if err != nil { - logger.ErrorContext(ctx, "Error generating CSRF token", "error", err) - return "", err - } - - return csrfToken, c.storage.SetWithContext( - ctx, - buildAccountCredentialsDynamicRegistrationIATLoginCSRFKey(opts.Domain, opts.ClientID), - []byte(utils.Sha256HashHex(csrfToken)), - c.oauthStateTTL, - ) + return c.accountCredentialsDynamicRegistration().SaveDynamicRegistrationIATLoginCSRF(ctx, opts) } -type VerifyAccountCredentialsDynamicRegistrationIATLoginCSRFOptions struct { - RequestID string - ClientID string - Domain string - CSRFToken string -} +type VerifyAccountCredentialsDynamicRegistrationIATLoginCSRFOptions = VerifyDynamicRegistrationIATLoginCSRFOptions func (c *Cache) VerifyAccountCredentialsDynamicRegistrationIATLoginCSRF( ctx context.Context, opts VerifyAccountCredentialsDynamicRegistrationIATLoginCSRFOptions, ) (bool, error) { - logger := utils.BuildLogger(c.logger, utils.LoggerOptions{ - Location: accountCredentialsDynamicRegistrationLocation, - Method: "VerifyAccountCredentialsDynamicRegistrationIATLoginCSRF", - RequestID: opts.RequestID, - }).With( - "clientId", opts.ClientID, - "domain", opts.Domain, - ) - logger.DebugContext(ctx, "Verifying account credentials dynamic registration IAT login CSRF token...") - - hashedCSRFToken, err := c.storage.GetWithContext(ctx, buildAccountCredentialsDynamicRegistrationIATLoginCSRFKey(opts.Domain, opts.ClientID)) - if err != nil { - logger.ErrorContext(ctx, "Failed to get account credentials dynamic registration IAT login CSRF token", "error", err) - return false, err - } - if hashedCSRFToken == nil { - logger.DebugContext(ctx, "Account credentials dynamic registration IAT login CSRF token not found") - return false, nil - } - - ok, err := utils.CompareShaHex(opts.CSRFToken, string(hashedCSRFToken)) - if err != nil { - logger.ErrorContext(ctx, "Error comparing CSRF token", "error", err) - return false, err - } - if !ok { - logger.DebugContext(ctx, "Invalid CSRF token") - return false, nil - } - if err := c.storage.DeleteWithContext( - ctx, - buildAccountCredentialsDynamicRegistrationIATLoginCSRFKey(opts.Domain, opts.ClientID), - ); err != nil { - logger.ErrorContext(ctx, "Error deleting CSRF token", "error", err) - return false, err - } - - return true, nil + return c.accountCredentialsDynamicRegistration().VerifyDynamicRegistrationIATLoginCSRF(ctx, opts) } -type AccountCredentialsDynamicRegistrationIAT2FAData struct { - AccountPublicID uuid.UUID `json:"account_public_id"` - AccountVersion int32 `json:"account_version"` - RedirectURI string `json:"redirect_uri"` - ClientID string `json:"clientId"` - Domain string `json:"domain"` - State string `json:"state"` - TwoFAType string `json:"two_factor_type"` -} +type AccountCredentialsDynamicRegistrationIAT2FAData = DynamicRegistrationIAT2FAData -func buildAccountCredentialsDynamicRegistrationIAT2FACacheKey(sessionID string) string { - return fmt.Sprintf("%s:2fa:%s", accountCredentialsDynamicRegistrationIATPrefix, utils.Sha256HashHex(sessionID)) -} - -type SaveAccountCredentialsDynamicRegistrationIAT2FAOptions struct { - RequestID string - AccountPublicID uuid.UUID - AccountVersion int32 - RedirectURI string - Domain string - ClientID string - State string - TwoFAType string - TwoFATTL int64 -} +type SaveAccountCredentialsDynamicRegistrationIAT2FAOptions = SaveDynamicRegistrationIAT2FAOptions func (c *Cache) SaveAccountCredentialsDynamicRegistrationIAT2FA( ctx context.Context, opts SaveAccountCredentialsDynamicRegistrationIAT2FAOptions, ) (string, error) { - logger := utils.BuildLogger(c.logger, utils.LoggerOptions{ - Location: accountCredentialsDynamicRegistrationLocation, - Method: "SaveAccountCredentialsDynamicRegistrationIAT2FA", - RequestID: opts.RequestID, - }).With( - "accountPublicId", opts.AccountPublicID, - "domain", opts.Domain, - ) - logger.DebugContext(ctx, "Saving account credentials dynamic registration IAT2FA...") - - sessionId := utils.Base64UUID() - data := AccountCredentialsDynamicRegistrationIAT2FAData{ - AccountPublicID: opts.AccountPublicID, - AccountVersion: opts.AccountVersion, - RedirectURI: opts.RedirectURI, - Domain: opts.Domain, - ClientID: opts.ClientID, - State: opts.State, - TwoFAType: opts.TwoFAType, - } - dataBytes, err := json.Marshal(data) - if err != nil { - logger.ErrorContext(ctx, "Failed to marshal account credentials dynamic registration IAT2FA data", "error", err) - return "", err - } - - return sessionId, c.storage.SetWithContext( - ctx, - buildAccountCredentialsDynamicRegistrationIAT2FACacheKey(sessionId), - dataBytes, - time.Duration(opts.TwoFATTL)*time.Second, - ) + return c.accountCredentialsDynamicRegistration().SaveDynamicRegistrationIAT2FA(ctx, opts) } -type GetAccountCredentialsDynamicRegistrationIAT2FAOptions struct { - RequestID string - SessionID string -} +type GetAccountCredentialsDynamicRegistrationIAT2FAOptions = GetDynamicRegistrationIAT2FAOptions func (c *Cache) GetAccountCredentialsDynamicRegistrationIAT2FA( ctx context.Context, opts GetAccountCredentialsDynamicRegistrationIAT2FAOptions, ) (AccountCredentialsDynamicRegistrationIAT2FAData, bool, error) { - logger := utils.BuildLogger(c.logger, utils.LoggerOptions{ - Location: accountCredentialsDynamicRegistrationLocation, - Method: "GetAccountCredentialsDynamicRegistrationIAT2FA", - RequestID: opts.RequestID, - }).With( - "sessionId", opts.SessionID, - ) - logger.DebugContext(ctx, "Getting account credentials dynamic registration IAT2FA...") - - data, err := c.storage.GetWithContext(ctx, buildAccountCredentialsDynamicRegistrationIAT2FACacheKey(opts.SessionID)) - if err != nil { - logger.ErrorContext(ctx, "Failed to get account credentials dynamic registration IAT2FA", "error", err) - return AccountCredentialsDynamicRegistrationIAT2FAData{}, false, err - } - if data == nil { - logger.DebugContext(ctx, "Account credentials dynamic registration IAT2FA not found") - return AccountCredentialsDynamicRegistrationIAT2FAData{}, false, nil - } - - var twoFAData AccountCredentialsDynamicRegistrationIAT2FAData - if err := json.Unmarshal(data, &twoFAData); err != nil { - logger.ErrorContext(ctx, "Failed to unmarshal account credentials dynamic registration IAT2FA data", "error", err) - return AccountCredentialsDynamicRegistrationIAT2FAData{}, false, err - } - - return twoFAData, true, nil + return c.accountCredentialsDynamicRegistration().GetDynamicRegistrationIAT2FA(ctx, opts) } -func buildAccountCredentialsDynamicRegistrationIAT2FACSRFCacheKey(sessionID string) string { - return fmt.Sprintf("%s:2fa-csrf:%s", accountCredentialsDynamicRegistrationIATPrefix, utils.Sha256HashHex(sessionID)) -} - -type SaveAccountCredentialsDynamicRegistrationIAT2FACSRFTokenOptions struct { - RequestID string - SessionID string - TwoFATTL int64 -} +type SaveAccountCredentialsDynamicRegistrationIAT2FACSRFTokenOptions = SaveDynamicRegistrationIAT2FACSRFTokenOptions func (c *Cache) SaveAccountCredentialsDynamicRegistrationIAT2FACSRFToken( ctx context.Context, opts SaveAccountCredentialsDynamicRegistrationIAT2FACSRFTokenOptions, ) (string, error) { - logger := utils.BuildLogger(c.logger, utils.LoggerOptions{ - Location: accountCredentialsDynamicRegistrationLocation, - Method: "SaveAccountCredentialsDynamicRegistrationIAT2FACSRFToken", - RequestID: opts.RequestID, - }).With( - "sessionId", opts.SessionID, - ) - logger.DebugContext(ctx, "Saving account credentials dynamic registration IAT2FA CSRF token...") - - csrfToken, err := utils.GenerateBase64Secret(csrfTokenByteLen) - if err != nil { - logger.ErrorContext(ctx, "Error generating CSRF token", "error", err) - return "", err - } - - return csrfToken, c.storage.SetWithContext( - ctx, - buildAccountCredentialsDynamicRegistrationIAT2FACSRFCacheKey(opts.SessionID), - []byte(utils.Sha256HashHex(csrfToken)), - time.Duration(opts.TwoFATTL)*time.Second, - ) + return c.accountCredentialsDynamicRegistration().SaveDynamicRegistrationIAT2FACSRFToken(ctx, opts) } -type VerifyAccountCredentialsDynamicRegistrationIAT2FACSRFTokenOptions struct { - RequestID string - SessionID string - CSRFToken string -} +type VerifyAccountCredentialsDynamicRegistrationIAT2FACSRFTokenOptions = VerifyDynamicRegistrationIAT2FACSRFTokenOptions func (c *Cache) VerifyAccountCredentialsDynamicRegistrationIAT2FACSRFToken( ctx context.Context, opts VerifyAccountCredentialsDynamicRegistrationIAT2FACSRFTokenOptions, ) (bool, error) { - logger := utils.BuildLogger(c.logger, utils.LoggerOptions{ - Location: accountCredentialsDynamicRegistrationLocation, - Method: "VerifyAccountCredentialsDynamicRegistrationIAT2FACSRFToken", - RequestID: opts.RequestID, - }).With( - "sessionId", opts.SessionID, - ) - logger.DebugContext(ctx, "Verifying account credentials dynamic registration IAT2FA CSRF token...") - - hashedCSRFToken, err := c.storage.GetWithContext(ctx, buildAccountCredentialsDynamicRegistrationIAT2FACSRFCacheKey(opts.SessionID)) - if err != nil { - logger.ErrorContext(ctx, "Failed to get account credentials dynamic registration IAT2FA CSRF token", "error", err) - return false, err - } - if hashedCSRFToken == nil { - logger.DebugContext(ctx, "Account credentials dynamic registration IAT2FA CSRF token not found") - return false, nil - } - - ok, err := utils.CompareShaHex(opts.CSRFToken, string(hashedCSRFToken)) - if err != nil { - logger.ErrorContext(ctx, "Error comparing CSRF token", "error", err) - return false, err - } - if !ok { - logger.DebugContext(ctx, "Invalid CSRF token") - return false, nil - } - if err := c.storage.DeleteWithContext( - ctx, - buildAccountCredentialsDynamicRegistrationIAT2FACSRFCacheKey(opts.SessionID), - ); err != nil { - logger.ErrorContext(ctx, "Error deleting CSRF token", "error", err) - return false, err - } - - return true, nil + return c.accountCredentialsDynamicRegistration().VerifyDynamicRegistrationIAT2FACSRFToken(ctx, opts) } -type DeleteAccountCredentialsDynamicRegistrationIAT2FACSRFTokenOptions struct { - RequestID string - SessionID string -} +type DeleteAccountCredentialsDynamicRegistrationIAT2FACSRFTokenOptions = DeleteDynamicRegistrationIAT2FACSRFTokenOptions func (c *Cache) DeleteAccountCredentialsDynamicRegistrationIAT2FACSRFToken( ctx context.Context, opts DeleteAccountCredentialsDynamicRegistrationIAT2FACSRFTokenOptions, ) error { - logger := utils.BuildLogger(c.logger, utils.LoggerOptions{ - Location: accountCredentialsDynamicRegistrationLocation, - Method: "DeleteAccountCredentialsDynamicRegistrationIAT2FACSRFToken", - RequestID: opts.RequestID, - }) - logger.DebugContext(ctx, "Deleting account credentials dynamic registration IAT2FA CSRF token...") - return c.storage.DeleteWithContext( - ctx, - buildAccountCredentialsDynamicRegistrationIAT2FACSRFCacheKey(opts.SessionID), - ) -} - -func buildAccountCredentialsDynamicRegistrationIATCodeCacheKey(codeID string) string { - return fmt.Sprintf("%s:code:%s", accountCredentialsDynamicRegistrationIATPrefix, codeID) + return c.accountCredentialsDynamicRegistration().DeleteDynamicRegistrationIAT2FACSRFToken(ctx, opts) } -type AccountCredentialsDynamicRegistrationIATCodeData struct { - AccountPublicID uuid.UUID `json:"account_public_id"` - AccountVersion int32 `json:"account_version"` - Domain string `json:"domain"` - ClientID string `json:"client_id"` - Challenge string `json:"challenge"` - Code string `json:"code"` -} +type AccountCredentialsDynamicRegistrationIATCodeData = DynamicRegistrationIATCodeData -type GenerateAccountCredentialsRegistrationIATCodeOptions struct { - RequestID string - ClientID string - AccountPublicID uuid.UUID - AccountVersion int32 - Domain string - Challenge string -} +type GenerateAccountCredentialsRegistrationIATCodeOptions = GenerateDynamicRegistrationIATCodeOptions func (c *Cache) GenerateAccountCredentialsRegistrationIATCode( ctx context.Context, opts GenerateAccountCredentialsRegistrationIATCodeOptions, ) (string, error) { - logger := utils.BuildLogger(c.logger, utils.LoggerOptions{ - Location: accountCredentialsDynamicRegistrationLocation, - Method: "GenerateAccountCredentialsRegistrationIATCode", - RequestID: opts.RequestID, - }).With( - "clientId", opts.ClientID, - "accountPublicId", opts.AccountPublicID, - "domain", opts.Domain, - ) - logger.DebugContext(ctx, "Generating account credentials registration IAT code...") - - codeID := utils.Base62UUID() - code, err := utils.GenerateBase62Secret(codeByteLen) - if err != nil { - logger.ErrorContext(ctx, "Error generating OAuth code", "error", err) - return "", err - } - - data := AccountCredentialsDynamicRegistrationIATCodeData{ - AccountPublicID: opts.AccountPublicID, - AccountVersion: opts.AccountVersion, - Domain: opts.Domain, - ClientID: opts.ClientID, - Code: utils.Sha256HashHex(code), - Challenge: opts.Challenge, - } - dataBytes, err := json.Marshal(data) - if err != nil { - logger.ErrorContext(ctx, "Failed to marshal account credentials registration IAT code data", "error", err) - return "", err - } - - if err := c.storage.SetWithContext( - ctx, - buildAccountCredentialsDynamicRegistrationIATCodeCacheKey(codeID), - dataBytes, - c.oauthCodeTTL, - ); err != nil { - logger.ErrorContext(ctx, "Failed to set account credentials registration IAT code in cache", "error", err) - return "", err - } - - return fmt.Sprintf("%s-%s", codeID, code), nil + return c.accountCredentialsDynamicRegistration().GenerateDynamicRegistrationIATCode(ctx, opts) } -type VerifyAccountCredentialsRegistrationIATCodeOptions struct { - RequestID string - Code string -} +type VerifyAccountCredentialsRegistrationIATCodeOptions = VerifyDynamicRegistrationIATCodeOptions func (c *Cache) VerifyAccountCredentialsRegistrationIATCode( ctx context.Context, opts VerifyAccountCredentialsRegistrationIATCodeOptions, ) (AccountCredentialsDynamicRegistrationIATCodeData, bool, error) { - logger := utils.BuildLogger(c.logger, utils.LoggerOptions{ - Location: accountCredentialsDynamicRegistrationLocation, - Method: "VerifyOAuthDynamicRegistrationIATCode", - RequestID: opts.RequestID, - }) - logger.DebugContext(ctx, "Verifying account credentials registration IAT code...") - - // 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 || parts[0] == "" || parts[1] == "" { - logger.WarnContext(ctx, "Invalid account credentials registration IAT code format") - return AccountCredentialsDynamicRegistrationIATCodeData{}, false, nil - } - - data, err := c.storage.GetWithContext(ctx, buildAccountCredentialsDynamicRegistrationIATCodeCacheKey(parts[0])) - if err != nil { - logger.ErrorContext(ctx, "Failed to get account credentials registration IAT code from cache", "error", err) - return AccountCredentialsDynamicRegistrationIATCodeData{}, false, err - } - if data == nil { - logger.DebugContext(ctx, "Account credentials registration IAT code not found") - return AccountCredentialsDynamicRegistrationIATCodeData{}, false, nil - } - - var codeData AccountCredentialsDynamicRegistrationIATCodeData - if err := json.Unmarshal(data, &codeData); err != nil { - logger.ErrorContext(ctx, "Failed to unmarshal account credentials registration IAT code data", "error", err) - return AccountCredentialsDynamicRegistrationIATCodeData{}, false, err - } - - ok, err := utils.CompareShaHex(parts[1], codeData.Code) - if err != nil { - logger.ErrorContext(ctx, "Error comparing OAuth code", "error", err) - return AccountCredentialsDynamicRegistrationIATCodeData{}, false, err - } - if !ok { - logger.DebugContext(ctx, "Invalid OAuth code") - return AccountCredentialsDynamicRegistrationIATCodeData{}, false, nil - } - if err := c.storage.DeleteWithContext( - ctx, - buildAccountCredentialsDynamicRegistrationIATCodeCacheKey(parts[0]), - ); err != nil { - logger.ErrorContext(ctx, "Error deleting OAuth code", "error", err) - return AccountCredentialsDynamicRegistrationIATCodeData{}, false, err - } - return codeData, true, nil -} - -type AccountCredentialsDynamicRegistrationSessionData struct { - AccountPublicID uuid.UUID `json:"account_public_id"` - AccountVersion int32 `json:"account_version"` - SessionKey string `json:"session_key"` -} - -func buildAccountCredentialsDynamicRegistrationSessionCacheKey(domain string, clientID string) string { - return fmt.Sprintf("%s:session:%s:%s", accountCredentialsDynamicRegistrationIATPrefix, domain, clientID) -} - -func formatSessionKey(clientID, sessionKey string) string { - return fmt.Sprintf("%s.%s", clientID, sessionKey) + return c.accountCredentialsDynamicRegistration().VerifyDynamicRegistrationIATCode(ctx, opts) } -func parseSessionKey(sessionKey string) (string, string, bool) { - parts := strings.Split(sessionKey, ".") - if len(parts) != 2 { - return "", "", false - } - return parts[0], parts[1], true -} +type AccountCredentialsDynamicRegistrationSessionData = DynamicRegistrationSessionData -type CreateAccountCredentialsRegistrationSessionKeyOptions struct { - RequestID string - ClientID string - Domain string - AccountPublicID uuid.UUID - AccountVersion int32 -} +type CreateAccountCredentialsRegistrationSessionKeyOptions = CreateDynamicRegistrationSessionKeyOptions func (c *Cache) CreateAccountCredentialsRegistrationSessionKey( ctx context.Context, opts CreateAccountCredentialsRegistrationSessionKeyOptions, ) (string, error) { - logger := utils.BuildLogger(c.logger, utils.LoggerOptions{ - Location: accountCredentialsDynamicRegistrationLocation, - Method: "CreateAccountCredentialsRegistrationSessionKey", - RequestID: opts.RequestID, - }).With( - "clientId", opts.ClientID, - "domain", opts.Domain, - "accountPublicId", opts.AccountPublicID, - ) - logger.DebugContext(ctx, "Creating account credentials registration session key...") - - sessionKey, err := utils.GenerateBase64Secret(sessionKeyByteLen) - if err != nil { - logger.ErrorContext(ctx, "Error generating session key", "error", err) - return "", err - } - - data := AccountCredentialsDynamicRegistrationSessionData{ - AccountPublicID: opts.AccountPublicID, - AccountVersion: opts.AccountVersion, - SessionKey: utils.Sha256HashHex(sessionKey), - } - dataBytes, err := json.Marshal(data) - if err != nil { - logger.ErrorContext(ctx, "Failed to marshal account credentials registration session data", "error", err) - return "", err - } - - if err := c.storage.SetWithContext( - ctx, - buildAccountCredentialsDynamicRegistrationSessionCacheKey(opts.Domain, opts.ClientID), - dataBytes, - c.oauthCodeTTL, - ); err != nil { - logger.ErrorContext(ctx, "Failed to set account credentials registration session in cache", "error", err) - return "", err - } - - return formatSessionKey(opts.ClientID, sessionKey), nil + return c.accountCredentialsDynamicRegistration().CreateDynamicRegistrationSessionKey(ctx, opts) } -type VerifyAccountCredentialsRegistrationSessionKeyOptions struct { - RequestID string - Domain string - SessionKey string -} +type VerifyAccountCredentialsRegistrationSessionKeyOptions = VerifyDynamicRegistrationSessionKeyOptions func (c *Cache) VerifyAccountCredentialsRegistrationSessionKey( ctx context.Context, opts VerifyAccountCredentialsRegistrationSessionKeyOptions, ) (AccountCredentialsDynamicRegistrationSessionData, string, bool, bool, error) { - logger := utils.BuildLogger(c.logger, utils.LoggerOptions{ - Location: accountCredentialsDynamicRegistrationLocation, - Method: "VerifyAccountCredentialsRegistrationSessionKey", - RequestID: opts.RequestID, - }).With( - "domain", opts.Domain, - ) - logger.DebugContext(ctx, "Verifying account credentials registration session key...") - - clientID, sessionKey, ok := parseSessionKey(opts.SessionKey) - if !ok { - logger.DebugContext(ctx, "Invalid account credentials registration session key format") - return AccountCredentialsDynamicRegistrationSessionData{}, "", false, false, nil - } - - data, err := c.storage.GetWithContext(ctx, buildAccountCredentialsDynamicRegistrationSessionCacheKey(opts.Domain, clientID)) - if err != nil { - logger.ErrorContext(ctx, "Failed to get account credentials registration session from cache", "error", err) - return AccountCredentialsDynamicRegistrationSessionData{}, "", false, false, err - } - if data == nil { - logger.DebugContext(ctx, "Account credentials registration session not found") - return AccountCredentialsDynamicRegistrationSessionData{}, "", false, false, nil - } - - var sessionData AccountCredentialsDynamicRegistrationSessionData - if err := json.Unmarshal(data, &sessionData); err != nil { - logger.ErrorContext(ctx, "Failed to unmarshal account credentials registration session data", "error", err) - return AccountCredentialsDynamicRegistrationSessionData{}, "", false, false, err - } - - ok, err = utils.CompareShaHex(sessionKey, sessionData.SessionKey) - if err != nil { - logger.ErrorContext(ctx, "Error comparing session key", "error", err) - return AccountCredentialsDynamicRegistrationSessionData{}, "", false, true, err - } - if !ok { - logger.DebugContext(ctx, "Invalid session key") - return AccountCredentialsDynamicRegistrationSessionData{}, clientID, false, true, nil - } - - return sessionData, clientID, true, true, nil + return c.accountCredentialsDynamicRegistration().VerifyDynamicRegistrationSessionKey(ctx, opts) } -type DeleteAccountCredentialsRegistrationSessionKeyOptions struct { - RequestID string - Domain string - ClientID string -} +type DeleteAccountCredentialsRegistrationSessionKeyOptions = DeleteDynamicRegistrationSessionKeyOptions func (c *Cache) DeleteAccountCredentialsRegistrationSessionKey( ctx context.Context, opts DeleteAccountCredentialsRegistrationSessionKeyOptions, ) error { - logger := utils.BuildLogger(c.logger, utils.LoggerOptions{ - Location: accountCredentialsDynamicRegistrationLocation, - Method: "DeleteAccountCredentialsRegistrationSessionKey", - RequestID: opts.RequestID, - }).With( - "domain", opts.Domain, - "clientId", opts.ClientID, - ) - logger.DebugContext(ctx, "Deleting account credentials registration session key...") - - return c.storage.DeleteWithContext( - ctx, - buildAccountCredentialsDynamicRegistrationSessionCacheKey(opts.Domain, opts.ClientID), - ) + return c.accountCredentialsDynamicRegistration().DeleteDynamicRegistrationSessionKey(ctx, opts) } -func buildAccountCredentialsDynamicRegistrationIATExtAuthCacheKey(provider, state string) string { - return fmt.Sprintf("%s:ext-auth:%s:%s", accountCredentialsDynamicRegistrationIATPrefix, provider, utils.Sha256HashHex(state)) -} +type AccountCredentialsDynamicRegistrationIATExtAuthData = DynamicRegistrationIATExtAuthData -type AccountCredentialsDynamicRegistrationIATExtAuthData struct { - ClientID string `json:"client_id"` - Domain string `json:"domain"` - RequestState string `json:"request_state"` -} - -type SaveAccountCredentialsDynamicRegistrationIATExtAuthOptions struct { - RequestID string - ClientID string - Domain string - Provider string - State string - RequestState string -} +type SaveAccountCredentialsDynamicRegistrationIATExtAuthOptions = SaveDynamicRegistrationIATExtAuthOptions func (c *Cache) SaveAccountCredentialsDynamicRegistrationIATExtAuth( ctx context.Context, opts SaveAccountCredentialsDynamicRegistrationIATExtAuthOptions, ) error { - logger := utils.BuildLogger(c.logger, utils.LoggerOptions{ - Location: accountCredentialsDynamicRegistrationLocation, - Method: "SaveAccountCredentialsDynamicRegistrationIATExtAuth", - RequestID: opts.RequestID, - }).With( - "clientId", opts.ClientID, - "provider", opts.Provider, - ) - logger.DebugContext(ctx, "Saving account credentials dynamic registration IAT external auth...") - - data := AccountCredentialsDynamicRegistrationIATExtAuthData{ - ClientID: opts.ClientID, - Domain: opts.Domain, - RequestState: opts.RequestState, - } - dataBytes, err := json.Marshal(data) - if err != nil { - logger.ErrorContext(ctx, "Failed to marshal account credentials dynamic registration IAT external auth data", "error", err) - return err - } - - return c.storage.SetWithContext( - ctx, - buildAccountCredentialsDynamicRegistrationIATExtAuthCacheKey(opts.Provider, opts.State), - dataBytes, - c.oauthStateTTL, - ) + return c.accountCredentialsDynamicRegistration().SaveDynamicRegistrationIATExtAuth(ctx, opts) } -type GetAccountCredentialsDynamicRegistrationIATExtAuthOptions struct { - RequestID string - Provider string - State string -} +type GetAccountCredentialsDynamicRegistrationIATExtAuthOptions = GetDynamicRegistrationIATExtAuthOptions func (c *Cache) GetAccountCredentialsDynamicRegistrationIATExtAuth( ctx context.Context, opts GetAccountCredentialsDynamicRegistrationIATExtAuthOptions, ) (AccountCredentialsDynamicRegistrationIATExtAuthData, bool, error) { - logger := utils.BuildLogger(c.logger, utils.LoggerOptions{ - Location: accountCredentialsDynamicRegistrationLocation, - Method: "GetAccountCredentialsDynamicRegistrationIATExtAuth", - RequestID: opts.RequestID, - }).With( - "provider", opts.Provider, - ) - logger.DebugContext(ctx, "Getting account credentials dynamic registration IAT external auth...") - - data, err := c.storage.GetWithContext(ctx, buildAccountCredentialsDynamicRegistrationIATExtAuthCacheKey(opts.Provider, opts.State)) - if err != nil { - logger.ErrorContext(ctx, "Failed to get account credentials dynamic registration IAT external auth", "error", err) - return AccountCredentialsDynamicRegistrationIATExtAuthData{}, false, err - } - if data == nil { - logger.DebugContext(ctx, "Account credentials dynamic registration IAT external auth not found") - return AccountCredentialsDynamicRegistrationIATExtAuthData{}, false, nil - } - - var authData AccountCredentialsDynamicRegistrationIATExtAuthData - if err := json.Unmarshal(data, &authData); err != nil { - logger.ErrorContext(ctx, "Failed to unmarshal account credentials dynamic registration IAT external auth data", "error", err) - return AccountCredentialsDynamicRegistrationIATExtAuthData{}, false, err - } + return c.accountCredentialsDynamicRegistration().GetDynamicRegistrationIATExtAuth(ctx, opts) +} - return authData, true, nil +func (c *Cache) accountCredentialsDynamicRegistration() dynamicRegistrationCache { + return c.dynamicRegistration(accountCredentialsDynamicRegistrationIATPrefix, accountCredentialsDynamicRegistrationLocation) } diff --git a/idp/internal/providers/cache/apps_dynamic_registration.go b/idp/internal/providers/cache/apps_dynamic_registration.go new file mode 100644 index 0000000..a43c2b5 --- /dev/null +++ b/idp/internal/providers/cache/apps_dynamic_registration.go @@ -0,0 +1,399 @@ +// 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 cache + +import ( + "context" + "fmt" + + "github.com/google/uuid" + + "github.com/tugascript/devlogs/idp/internal/utils" +) + +const ( + appsDynamicRegistrationLocation string = "apps_dynamic_registration" + + appsDynamicRegistrationIATPrefix string = "apps_dynamic_registration_iat" +) + +func buildAppDynamicRegistrationIATAuthCacheKey(hostname, clientID string) string { + return fmt.Sprintf("%s:%s:auth:%s", appsDynamicRegistrationIATPrefix, hostname, clientID) +} + +type AppsDynamicRegistrationIATAuthData struct { + AccountID int32 `json:"account_id"` + Domain string `json:"domain"` + State string `json:"state"` + Challenge string `json:"challenge"` + RedirectURI string `json:"redirect_uri"` +} + +type SaveAppsDynamicRegistrationIATAuthOptions struct { + RequestID string + Hostname string + AccountID int32 + Domain string + State string + RedirectURI string + Challenge string +} + +func (c *Cache) SaveAppsDynamicRegistrationIATAuth( + ctx context.Context, + opts SaveAppsDynamicRegistrationIATAuthOptions, +) (string, error) { + logger := utils.BuildLogger(c.logger, utils.LoggerOptions{ + Location: appsDynamicRegistrationLocation, + Method: "SaveAppsDynamicRegistrationIATAuth", + RequestID: opts.RequestID, + }).With( + "redirectUri", opts.RedirectURI, + ) + logger.DebugContext(ctx, "Saving apps dynamic registration IAT sessions...") + + data := AppsDynamicRegistrationIATAuthData{ + AccountID: opts.AccountID, + State: opts.State, + Domain: opts.Domain, + RedirectURI: opts.RedirectURI, + Challenge: opts.Challenge, + } + return c.appsDynamicRegistration(opts.Hostname).saveAuth(ctx, logger, data) +} + +type GetAppsDynamicRegistrationIATAuthOptions struct { + RequestID string + Hostname string + ClientID string +} + +func (c *Cache) GetAppsDynamicRegistrationAuthIAT( + ctx context.Context, + opts GetAppsDynamicRegistrationIATAuthOptions, +) (AppsDynamicRegistrationIATAuthData, bool, error) { + logger := utils.BuildLogger(c.logger, utils.LoggerOptions{ + Location: appsDynamicRegistrationLocation, + Method: "GetAppsDynamicRegistrationAuthIAT", + RequestID: opts.RequestID, + }).With( + "clientId", opts.ClientID, + ) + logger.DebugContext(ctx, "Getting apps dynamic registration IAT...") + + return getDynamicRegistrationAuth[AppsDynamicRegistrationIATAuthData](ctx, c.appsDynamicRegistration(opts.Hostname), logger, opts.ClientID) +} + +type DeleteAppsDynamicRegistrationIATAuthOptions struct { + RequestID string + Hostname string + ClientID string +} + +func (c *Cache) DeleteAppsDynamicRegistrationIATAuth( + ctx context.Context, + opts DeleteAppsDynamicRegistrationIATAuthOptions, +) error { + logger := utils.BuildLogger(c.logger, utils.LoggerOptions{ + Location: appsDynamicRegistrationLocation, + Method: "DeleteAppsDynamicRegistrationIATAuth", + RequestID: opts.RequestID, + }).With( + "clientId", opts.ClientID, + ) + logger.DebugContext(ctx, "Deleting apps dynamic registration IAT...") + + return c.storage.DeleteWithContext(ctx, buildAppDynamicRegistrationIATAuthCacheKey(opts.Hostname, opts.ClientID)) +} + +type SaveAppsDynamicRegistrationIATLoginCSRFOptions struct { + RequestID string + Hostname string + ClientID string + Domain string +} + +func (c *Cache) SaveAppsDynamicRegistrationIATLoginCSRF( + ctx context.Context, + opts SaveAppsDynamicRegistrationIATLoginCSRFOptions, +) (string, error) { + return c.appsDynamicRegistration(opts.Hostname).SaveDynamicRegistrationIATLoginCSRF(ctx, SaveDynamicRegistrationIATLoginCSRFOptions{ + RequestID: opts.RequestID, + ClientID: opts.ClientID, + Domain: opts.Domain, + }) +} + +type VerifyAppsDynamicRegistrationIATLoginCSRFOptions struct { + RequestID string + Hostname string + ClientID string + Domain string + CSRFToken string +} + +func (c *Cache) VerifyAppsDynamicRegistrationIATLoginCSRF( + ctx context.Context, + opts VerifyAppsDynamicRegistrationIATLoginCSRFOptions, +) (bool, error) { + return c.appsDynamicRegistration(opts.Hostname).VerifyDynamicRegistrationIATLoginCSRF(ctx, VerifyDynamicRegistrationIATLoginCSRFOptions{ + RequestID: opts.RequestID, + ClientID: opts.ClientID, + Domain: opts.Domain, + CSRFToken: opts.CSRFToken, + }) +} + +type AppsDynamicRegistrationIAT2FAData = DynamicRegistrationIAT2FAData + +type SaveAppsDynamicRegistrationIAT2FAOptions struct { + RequestID string + Hostname string + AccountPublicID uuid.UUID + AccountVersion int32 + RedirectURI string + Domain string + ClientID string + State string + TwoFAType string + TwoFATTL int64 +} + +func (c *Cache) SaveAppsDynamicRegistrationIAT2FA( + ctx context.Context, + opts SaveAppsDynamicRegistrationIAT2FAOptions, +) (string, error) { + return c.appsDynamicRegistration(opts.Hostname).SaveDynamicRegistrationIAT2FA(ctx, SaveDynamicRegistrationIAT2FAOptions{ + RequestID: opts.RequestID, + AccountPublicID: opts.AccountPublicID, + AccountVersion: opts.AccountVersion, + RedirectURI: opts.RedirectURI, + Domain: opts.Domain, + ClientID: opts.ClientID, + State: opts.State, + TwoFAType: opts.TwoFAType, + TwoFATTL: opts.TwoFATTL, + }) +} + +type GetAppsDynamicRegistrationIAT2FAOptions struct { + RequestID string + Hostname string + SessionID string +} + +func (c *Cache) GetAppsDynamicRegistrationIAT2FA( + ctx context.Context, + opts GetAppsDynamicRegistrationIAT2FAOptions, +) (AppsDynamicRegistrationIAT2FAData, bool, error) { + return c.appsDynamicRegistration(opts.Hostname).GetDynamicRegistrationIAT2FA(ctx, GetDynamicRegistrationIAT2FAOptions{ + RequestID: opts.RequestID, + SessionID: opts.SessionID, + }) +} + +type SaveAppsDynamicRegistrationIAT2FACSRFTokenOptions struct { + RequestID string + Hostname string + SessionID string + TwoFATTL int64 +} + +func (c *Cache) SaveAppsDynamicRegistrationIAT2FACSRFToken( + ctx context.Context, + opts SaveAppsDynamicRegistrationIAT2FACSRFTokenOptions, +) (string, error) { + return c.appsDynamicRegistration(opts.Hostname).SaveDynamicRegistrationIAT2FACSRFToken(ctx, SaveDynamicRegistrationIAT2FACSRFTokenOptions{ + RequestID: opts.RequestID, + SessionID: opts.SessionID, + TwoFATTL: opts.TwoFATTL, + }) +} + +type VerifyAppsDynamicRegistrationIAT2FACSRFTokenOptions struct { + RequestID string + Hostname string + SessionID string + CSRFToken string +} + +func (c *Cache) VerifyAppsDynamicRegistrationIAT2FACSRFToken( + ctx context.Context, + opts VerifyAppsDynamicRegistrationIAT2FACSRFTokenOptions, +) (bool, error) { + return c.appsDynamicRegistration(opts.Hostname).VerifyDynamicRegistrationIAT2FACSRFToken(ctx, VerifyDynamicRegistrationIAT2FACSRFTokenOptions{ + RequestID: opts.RequestID, + SessionID: opts.SessionID, + CSRFToken: opts.CSRFToken, + }) +} + +type DeleteAppsDynamicRegistrationIAT2FACSRFTokenOptions struct { + RequestID string + Hostname string + SessionID string +} + +func (c *Cache) DeleteAppsDynamicRegistrationIAT2FACSRFToken( + ctx context.Context, + opts DeleteAppsDynamicRegistrationIAT2FACSRFTokenOptions, +) error { + return c.appsDynamicRegistration(opts.Hostname).DeleteDynamicRegistrationIAT2FACSRFToken(ctx, DeleteDynamicRegistrationIAT2FACSRFTokenOptions{ + RequestID: opts.RequestID, + SessionID: opts.SessionID, + }) +} + +type AppsDynamicRegistrationIATCodeData = DynamicRegistrationIATCodeData + +type GenerateAppsRegistrationIATCodeOptions struct { + RequestID string + Hostname string + ClientID string + AccountPublicID uuid.UUID + AccountVersion int32 + Domain string + Challenge string +} + +func (c *Cache) GenerateAppsRegistrationIATCode( + ctx context.Context, + opts GenerateAppsRegistrationIATCodeOptions, +) (string, error) { + return c.appsDynamicRegistration(opts.Hostname).GenerateDynamicRegistrationIATCode(ctx, GenerateDynamicRegistrationIATCodeOptions{ + RequestID: opts.RequestID, + ClientID: opts.ClientID, + AccountPublicID: opts.AccountPublicID, + AccountVersion: opts.AccountVersion, + Domain: opts.Domain, + Challenge: opts.Challenge, + }) +} + +type VerifyAppsRegistrationIATCodeOptions struct { + RequestID string + Hostname string + Code string +} + +func (c *Cache) VerifyAppsRegistrationIATCode( + ctx context.Context, + opts VerifyAppsRegistrationIATCodeOptions, +) (AppsDynamicRegistrationIATCodeData, bool, error) { + return c.appsDynamicRegistration(opts.Hostname).VerifyDynamicRegistrationIATCode(ctx, VerifyDynamicRegistrationIATCodeOptions{ + RequestID: opts.RequestID, + Code: opts.Code, + }) +} + +type AppsDynamicRegistrationSessionData = DynamicRegistrationSessionData + +type CreateAppsRegistrationSessionKeyOptions struct { + RequestID string + Hostname string + ClientID string + Domain string + AccountPublicID uuid.UUID + AccountVersion int32 +} + +func (c *Cache) CreateAppsRegistrationSessionKey( + ctx context.Context, + opts CreateAppsRegistrationSessionKeyOptions, +) (string, error) { + return c.appsDynamicRegistration(opts.Hostname).CreateDynamicRegistrationSessionKey(ctx, CreateDynamicRegistrationSessionKeyOptions{ + RequestID: opts.RequestID, + ClientID: opts.ClientID, + Domain: opts.Domain, + AccountPublicID: opts.AccountPublicID, + AccountVersion: opts.AccountVersion, + }) +} + +type VerifyAppsRegistrationSessionKeyOptions struct { + RequestID string + Hostname string + Domain string + SessionKey string +} + +func (c *Cache) VerifyAppsRegistrationSessionKey( + ctx context.Context, + opts VerifyAppsRegistrationSessionKeyOptions, +) (AppsDynamicRegistrationSessionData, string, bool, bool, error) { + return c.appsDynamicRegistration(opts.Hostname).VerifyDynamicRegistrationSessionKey(ctx, VerifyDynamicRegistrationSessionKeyOptions{ + RequestID: opts.RequestID, + Domain: opts.Domain, + SessionKey: opts.SessionKey, + }) +} + +type DeleteAppsRegistrationSessionKeyOptions struct { + RequestID string + Hostname string + Domain string + ClientID string +} + +func (c *Cache) DeleteAppsRegistrationSessionKey( + ctx context.Context, + opts DeleteAppsRegistrationSessionKeyOptions, +) error { + return c.appsDynamicRegistration(opts.Hostname).DeleteDynamicRegistrationSessionKey(ctx, DeleteDynamicRegistrationSessionKeyOptions{ + RequestID: opts.RequestID, + Domain: opts.Domain, + ClientID: opts.ClientID, + }) +} + +type AppsDynamicRegistrationIATExtAuthData = DynamicRegistrationIATExtAuthData + +type SaveAppsDynamicRegistrationIATExtAuthOptions struct { + RequestID string + Hostname string + ClientID string + Domain string + Provider string + State string + RequestState string +} + +func (c *Cache) SaveAppsDynamicRegistrationIATExtAuth( + ctx context.Context, + opts SaveAppsDynamicRegistrationIATExtAuthOptions, +) error { + return c.appsDynamicRegistration(opts.Hostname).SaveDynamicRegistrationIATExtAuth(ctx, SaveDynamicRegistrationIATExtAuthOptions{ + RequestID: opts.RequestID, + ClientID: opts.ClientID, + Domain: opts.Domain, + Provider: opts.Provider, + State: opts.State, + RequestState: opts.RequestState, + }) +} + +type GetAppsDynamicRegistrationIATExtAuthOptions struct { + RequestID string + Hostname string + Provider string + State string +} + +func (c *Cache) GetAppsDynamicRegistrationIATExtAuth( + ctx context.Context, + opts GetAppsDynamicRegistrationIATExtAuthOptions, +) (AppsDynamicRegistrationIATExtAuthData, bool, error) { + return c.appsDynamicRegistration(opts.Hostname).GetDynamicRegistrationIATExtAuth(ctx, GetDynamicRegistrationIATExtAuthOptions{ + RequestID: opts.RequestID, + Provider: opts.Provider, + State: opts.State, + }) +} + +func (c *Cache) appsDynamicRegistration(hostname string) dynamicRegistrationCache { + return c.dynamicRegistration(appsDynamicRegistrationIATPrefix+":"+hostname, appsDynamicRegistrationLocation) +} diff --git a/idp/internal/providers/cache/cache.go b/idp/internal/providers/cache/cache.go index 25b8df8..4f5d07c 100644 --- a/idp/internal/providers/cache/cache.go +++ b/idp/internal/providers/cache/cache.go @@ -17,7 +17,12 @@ import ( "github.com/tugascript/devlogs/idp/internal/utils" ) -const logLayer string = utils.ProvidersLogLayer + "/cache" +const ( + logLayer string = utils.ProvidersLogLayer + "/cache" + + csrfTokenByteLen int = 16 + sessionKeyByteLen int = 32 +) type Cache struct { logger *slog.Logger diff --git a/idp/internal/providers/cache/dynamic_registration.go b/idp/internal/providers/cache/dynamic_registration.go new file mode 100644 index 0000000..5ec5232 --- /dev/null +++ b/idp/internal/providers/cache/dynamic_registration.go @@ -0,0 +1,738 @@ +// 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 cache + +import ( + "context" + "encoding/json" + "fmt" + "log/slog" + "strings" + "time" + + "github.com/google/uuid" + + "github.com/tugascript/devlogs/idp/internal/utils" +) + +// dynamicRegistrationCache scopes shared operations to a registration flow. +type dynamicRegistrationCache struct { + *Cache + storage dynamicRegistrationStorage + prefix string + location string +} + +type dynamicRegistrationStorage interface { + GetWithContext(context.Context, string) ([]byte, error) + SetWithContext(context.Context, string, []byte, time.Duration) error + DeleteWithContext(context.Context, string) error +} + +func (c *Cache) dynamicRegistration(prefix, location string) dynamicRegistrationCache { + return dynamicRegistrationCache{Cache: c, storage: c.storage, prefix: prefix, location: location} +} + +func (c dynamicRegistrationCache) saveAuth(ctx context.Context, logger *slog.Logger, data any) (string, error) { + encoded, err := json.Marshal(data) + if err != nil { + logger.ErrorContext(ctx, "Failed to marshal dynamic registration auth data", "error", err) + return "", err + } + clientID := utils.Base62UUID() + return clientID, c.storage.SetWithContext(ctx, c.prefix+":auth:"+clientID, encoded, c.oauthStateTTL) +} + +func getDynamicRegistrationAuth[T any](ctx context.Context, c dynamicRegistrationCache, logger *slog.Logger, clientID string) (T, bool, error) { + var data T + encoded, err := c.storage.GetWithContext(ctx, c.prefix+":auth:"+clientID) + if err != nil { + logger.ErrorContext(ctx, "Failed to get dynamic registration auth data", "error", err) + return data, false, err + } + if encoded == nil { + return data, false, nil + } + if err := json.Unmarshal(encoded, &data); err != nil { + logger.ErrorContext(ctx, "Failed to unmarshal dynamic registration auth data", "error", err) + var zero T + return zero, false, err + } + return data, true, nil +} + +func buildDynamicRegistrationIATLoginCSRFKey(prefix, domain, clientID string) string { + return fmt.Sprintf("%s:login:%s:%s", prefix, domain, clientID) +} + +type SaveDynamicRegistrationIATLoginCSRFOptions struct { + RequestID string + ClientID string + Domain string +} + +func (c dynamicRegistrationCache) SaveDynamicRegistrationIATLoginCSRF( + ctx context.Context, + opts SaveDynamicRegistrationIATLoginCSRFOptions, +) (string, error) { + logger := utils.BuildLogger(c.logger, utils.LoggerOptions{ + Location: c.location, + Method: "SaveDynamicRegistrationIATLoginCSRF", + RequestID: opts.RequestID, + }).With( + "clientId", opts.ClientID, + "domain", opts.Domain, + ) + logger.DebugContext(ctx, "Saving dynamic registration IAT login CSRF token...") + + csrfToken, err := utils.GenerateBase64Secret(csrfTokenByteLen) + if err != nil { + logger.ErrorContext(ctx, "Error generating CSRF token", "error", err) + return "", err + } + + return csrfToken, c.storage.SetWithContext( + ctx, + buildDynamicRegistrationIATLoginCSRFKey(c.prefix, opts.Domain, opts.ClientID), + []byte(utils.Sha256HashHex(csrfToken)), + c.oauthStateTTL, + ) +} + +type VerifyDynamicRegistrationIATLoginCSRFOptions struct { + RequestID string + ClientID string + Domain string + CSRFToken string +} + +func (c dynamicRegistrationCache) VerifyDynamicRegistrationIATLoginCSRF( + ctx context.Context, + opts VerifyDynamicRegistrationIATLoginCSRFOptions, +) (bool, error) { + logger := utils.BuildLogger(c.logger, utils.LoggerOptions{ + Location: c.location, + Method: "VerifyDynamicRegistrationIATLoginCSRF", + RequestID: opts.RequestID, + }).With( + "clientId", opts.ClientID, + "domain", opts.Domain, + ) + logger.DebugContext(ctx, "Verifying dynamic registration IAT login CSRF token...") + + hashedCSRFToken, err := c.storage.GetWithContext(ctx, buildDynamicRegistrationIATLoginCSRFKey(c.prefix, opts.Domain, opts.ClientID)) + if err != nil { + logger.ErrorContext(ctx, "Failed to get dynamic registration IAT login CSRF token", "error", err) + return false, err + } + if hashedCSRFToken == nil { + logger.DebugContext(ctx, "dynamic registration IAT login CSRF token not found") + return false, nil + } + + ok, err := utils.CompareShaHex(opts.CSRFToken, string(hashedCSRFToken)) + if err != nil { + logger.ErrorContext(ctx, "Error comparing CSRF token", "error", err) + return false, err + } + if !ok { + logger.DebugContext(ctx, "Invalid CSRF token") + return false, nil + } + if err := c.storage.DeleteWithContext( + ctx, + buildDynamicRegistrationIATLoginCSRFKey(c.prefix, opts.Domain, opts.ClientID), + ); err != nil { + logger.ErrorContext(ctx, "Error deleting CSRF token", "error", err) + return false, err + } + + return true, nil +} + +type DynamicRegistrationIAT2FAData struct { + AccountPublicID uuid.UUID `json:"account_public_id"` + AccountVersion int32 `json:"account_version"` + RedirectURI string `json:"redirect_uri"` + ClientID string `json:"clientId"` + Domain string `json:"domain"` + State string `json:"state"` + TwoFAType string `json:"two_factor_type"` +} + +func buildDynamicRegistrationIAT2FACacheKey(prefix, sessionID string) string { + return fmt.Sprintf("%s:2fa:%s", prefix, utils.Sha256HashHex(sessionID)) +} + +type SaveDynamicRegistrationIAT2FAOptions struct { + RequestID string + AccountPublicID uuid.UUID + AccountVersion int32 + RedirectURI string + Domain string + ClientID string + State string + TwoFAType string + TwoFATTL int64 +} + +func (c dynamicRegistrationCache) SaveDynamicRegistrationIAT2FA( + ctx context.Context, + opts SaveDynamicRegistrationIAT2FAOptions, +) (string, error) { + logger := utils.BuildLogger(c.logger, utils.LoggerOptions{ + Location: c.location, + Method: "SaveDynamicRegistrationIAT2FA", + RequestID: opts.RequestID, + }).With( + "accountPublicId", opts.AccountPublicID, + "domain", opts.Domain, + ) + logger.DebugContext(ctx, "Saving dynamic registration IAT2FA...") + + sessionId := utils.Base64UUID() + data := DynamicRegistrationIAT2FAData{ + AccountPublicID: opts.AccountPublicID, + AccountVersion: opts.AccountVersion, + RedirectURI: opts.RedirectURI, + Domain: opts.Domain, + ClientID: opts.ClientID, + State: opts.State, + TwoFAType: opts.TwoFAType, + } + dataBytes, err := json.Marshal(data) + if err != nil { + logger.ErrorContext(ctx, "Failed to marshal dynamic registration IAT2FA data", "error", err) + return "", err + } + + return sessionId, c.storage.SetWithContext( + ctx, + buildDynamicRegistrationIAT2FACacheKey(c.prefix, sessionId), + dataBytes, + time.Duration(opts.TwoFATTL)*time.Second, + ) +} + +type GetDynamicRegistrationIAT2FAOptions struct { + RequestID string + SessionID string +} + +func (c dynamicRegistrationCache) GetDynamicRegistrationIAT2FA( + ctx context.Context, + opts GetDynamicRegistrationIAT2FAOptions, +) (DynamicRegistrationIAT2FAData, bool, error) { + logger := utils.BuildLogger(c.logger, utils.LoggerOptions{ + Location: c.location, + Method: "GetDynamicRegistrationIAT2FA", + RequestID: opts.RequestID, + }).With( + "sessionId", opts.SessionID, + ) + logger.DebugContext(ctx, "Getting dynamic registration IAT2FA...") + + data, err := c.storage.GetWithContext(ctx, buildDynamicRegistrationIAT2FACacheKey(c.prefix, opts.SessionID)) + if err != nil { + logger.ErrorContext(ctx, "Failed to get dynamic registration IAT2FA", "error", err) + return DynamicRegistrationIAT2FAData{}, false, err + } + if data == nil { + logger.DebugContext(ctx, "dynamic registration IAT2FA not found") + return DynamicRegistrationIAT2FAData{}, false, nil + } + + var twoFAData DynamicRegistrationIAT2FAData + if err := json.Unmarshal(data, &twoFAData); err != nil { + logger.ErrorContext(ctx, "Failed to unmarshal dynamic registration IAT2FA data", "error", err) + return DynamicRegistrationIAT2FAData{}, false, err + } + + return twoFAData, true, nil +} + +func buildDynamicRegistrationIAT2FACSRFCacheKey(prefix, sessionID string) string { + return fmt.Sprintf("%s:2fa-csrf:%s", prefix, utils.Sha256HashHex(sessionID)) +} + +type SaveDynamicRegistrationIAT2FACSRFTokenOptions struct { + RequestID string + SessionID string + TwoFATTL int64 +} + +func (c dynamicRegistrationCache) SaveDynamicRegistrationIAT2FACSRFToken( + ctx context.Context, + opts SaveDynamicRegistrationIAT2FACSRFTokenOptions, +) (string, error) { + logger := utils.BuildLogger(c.logger, utils.LoggerOptions{ + Location: c.location, + Method: "SaveDynamicRegistrationIAT2FACSRFToken", + RequestID: opts.RequestID, + }).With( + "sessionId", opts.SessionID, + ) + logger.DebugContext(ctx, "Saving dynamic registration IAT2FA CSRF token...") + + csrfToken, err := utils.GenerateBase64Secret(csrfTokenByteLen) + if err != nil { + logger.ErrorContext(ctx, "Error generating CSRF token", "error", err) + return "", err + } + + return csrfToken, c.storage.SetWithContext( + ctx, + buildDynamicRegistrationIAT2FACSRFCacheKey(c.prefix, opts.SessionID), + []byte(utils.Sha256HashHex(csrfToken)), + time.Duration(opts.TwoFATTL)*time.Second, + ) +} + +type VerifyDynamicRegistrationIAT2FACSRFTokenOptions struct { + RequestID string + SessionID string + CSRFToken string +} + +func (c dynamicRegistrationCache) VerifyDynamicRegistrationIAT2FACSRFToken( + ctx context.Context, + opts VerifyDynamicRegistrationIAT2FACSRFTokenOptions, +) (bool, error) { + logger := utils.BuildLogger(c.logger, utils.LoggerOptions{ + Location: c.location, + Method: "VerifyDynamicRegistrationIAT2FACSRFToken", + RequestID: opts.RequestID, + }).With( + "sessionId", opts.SessionID, + ) + logger.DebugContext(ctx, "Verifying dynamic registration IAT2FA CSRF token...") + + hashedCSRFToken, err := c.storage.GetWithContext(ctx, buildDynamicRegistrationIAT2FACSRFCacheKey(c.prefix, opts.SessionID)) + if err != nil { + logger.ErrorContext(ctx, "Failed to get dynamic registration IAT2FA CSRF token", "error", err) + return false, err + } + if hashedCSRFToken == nil { + logger.DebugContext(ctx, "dynamic registration IAT2FA CSRF token not found") + return false, nil + } + + ok, err := utils.CompareShaHex(opts.CSRFToken, string(hashedCSRFToken)) + if err != nil { + logger.ErrorContext(ctx, "Error comparing CSRF token", "error", err) + return false, err + } + if !ok { + logger.DebugContext(ctx, "Invalid CSRF token") + return false, nil + } + if err := c.storage.DeleteWithContext( + ctx, + buildDynamicRegistrationIAT2FACSRFCacheKey(c.prefix, opts.SessionID), + ); err != nil { + logger.ErrorContext(ctx, "Error deleting CSRF token", "error", err) + return false, err + } + + return true, nil +} + +type DeleteDynamicRegistrationIAT2FACSRFTokenOptions struct { + RequestID string + SessionID string +} + +func (c dynamicRegistrationCache) DeleteDynamicRegistrationIAT2FACSRFToken( + ctx context.Context, + opts DeleteDynamicRegistrationIAT2FACSRFTokenOptions, +) error { + logger := utils.BuildLogger(c.logger, utils.LoggerOptions{ + Location: c.location, + Method: "DeleteDynamicRegistrationIAT2FACSRFToken", + RequestID: opts.RequestID, + }) + logger.DebugContext(ctx, "Deleting dynamic registration IAT2FA CSRF token...") + return c.storage.DeleteWithContext( + ctx, + buildDynamicRegistrationIAT2FACSRFCacheKey(c.prefix, opts.SessionID), + ) +} + +func buildDynamicRegistrationIATCodeCacheKey(prefix, codeID string) string { + return fmt.Sprintf("%s:code:%s", prefix, codeID) +} + +type DynamicRegistrationIATCodeData struct { + AccountPublicID uuid.UUID `json:"account_public_id"` + AccountVersion int32 `json:"account_version"` + Domain string `json:"domain"` + ClientID string `json:"client_id"` + Challenge string `json:"challenge"` + Code string `json:"code"` +} + +type GenerateDynamicRegistrationIATCodeOptions struct { + RequestID string + ClientID string + AccountPublicID uuid.UUID + AccountVersion int32 + Domain string + Challenge string +} + +func (c dynamicRegistrationCache) GenerateDynamicRegistrationIATCode( + ctx context.Context, + opts GenerateDynamicRegistrationIATCodeOptions, +) (string, error) { + logger := utils.BuildLogger(c.logger, utils.LoggerOptions{ + Location: c.location, + Method: "GenerateDynamicRegistrationIATCode", + RequestID: opts.RequestID, + }).With( + "clientId", opts.ClientID, + "accountPublicId", opts.AccountPublicID, + "domain", opts.Domain, + ) + logger.DebugContext(ctx, "Generating registration IAT code...") + + codeID := utils.Base62UUID() + code, err := utils.GenerateBase62Secret(codeByteLen) + if err != nil { + logger.ErrorContext(ctx, "Error generating OAuth code", "error", err) + return "", err + } + + data := DynamicRegistrationIATCodeData{ + AccountPublicID: opts.AccountPublicID, + AccountVersion: opts.AccountVersion, + Domain: opts.Domain, + ClientID: opts.ClientID, + Code: utils.Sha256HashHex(code), + Challenge: opts.Challenge, + } + dataBytes, err := json.Marshal(data) + if err != nil { + logger.ErrorContext(ctx, "Failed to marshal registration IAT code data", "error", err) + return "", err + } + + if err := c.storage.SetWithContext( + ctx, + buildDynamicRegistrationIATCodeCacheKey(c.prefix, codeID), + dataBytes, + c.oauthCodeTTL, + ); err != nil { + logger.ErrorContext(ctx, "Failed to set registration IAT code in cache", "error", err) + return "", err + } + + return fmt.Sprintf("%s-%s", codeID, code), nil +} + +type VerifyDynamicRegistrationIATCodeOptions struct { + RequestID string + Code string +} + +func (c dynamicRegistrationCache) VerifyDynamicRegistrationIATCode( + ctx context.Context, + opts VerifyDynamicRegistrationIATCodeOptions, +) (DynamicRegistrationIATCodeData, bool, error) { + logger := utils.BuildLogger(c.logger, utils.LoggerOptions{ + Location: c.location, + Method: "VerifyDynamicRegistrationIATCode", + RequestID: opts.RequestID, + }) + logger.DebugContext(ctx, "Verifying registration IAT code...") + + // 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 || parts[0] == "" || parts[1] == "" { + logger.WarnContext(ctx, "Invalid registration IAT code format") + return DynamicRegistrationIATCodeData{}, false, nil + } + + data, err := c.storage.GetWithContext(ctx, buildDynamicRegistrationIATCodeCacheKey(c.prefix, parts[0])) + if err != nil { + logger.ErrorContext(ctx, "Failed to get registration IAT code from cache", "error", err) + return DynamicRegistrationIATCodeData{}, false, err + } + if data == nil { + logger.DebugContext(ctx, "registration IAT code not found") + return DynamicRegistrationIATCodeData{}, false, nil + } + + var codeData DynamicRegistrationIATCodeData + if err := json.Unmarshal(data, &codeData); err != nil { + logger.ErrorContext(ctx, "Failed to unmarshal registration IAT code data", "error", err) + return DynamicRegistrationIATCodeData{}, false, err + } + + ok, err := utils.CompareShaHex(parts[1], codeData.Code) + if err != nil { + logger.ErrorContext(ctx, "Error comparing OAuth code", "error", err) + return DynamicRegistrationIATCodeData{}, false, err + } + if !ok { + logger.DebugContext(ctx, "Invalid OAuth code") + return DynamicRegistrationIATCodeData{}, false, nil + } + if err := c.storage.DeleteWithContext( + ctx, + buildDynamicRegistrationIATCodeCacheKey(c.prefix, parts[0]), + ); err != nil { + logger.ErrorContext(ctx, "Error deleting OAuth code", "error", err) + return DynamicRegistrationIATCodeData{}, false, err + } + return codeData, true, nil +} + +type DynamicRegistrationSessionData struct { + AccountPublicID uuid.UUID `json:"account_public_id"` + AccountVersion int32 `json:"account_version"` + SessionKey string `json:"session_key"` +} + +func buildDynamicRegistrationSessionCacheKey(prefix, domain string, clientID string) string { + return fmt.Sprintf("%s:session:%s:%s", prefix, domain, clientID) +} + +func formatSessionKey(clientID, sessionKey string) string { + return fmt.Sprintf("%s.%s", clientID, sessionKey) +} + +func parseSessionKey(sessionKey string) (string, string, bool) { + parts := strings.Split(sessionKey, ".") + if len(parts) != 2 { + return "", "", false + } + return parts[0], parts[1], true +} + +type CreateDynamicRegistrationSessionKeyOptions struct { + RequestID string + ClientID string + Domain string + AccountPublicID uuid.UUID + AccountVersion int32 +} + +func (c dynamicRegistrationCache) CreateDynamicRegistrationSessionKey( + ctx context.Context, + opts CreateDynamicRegistrationSessionKeyOptions, +) (string, error) { + logger := utils.BuildLogger(c.logger, utils.LoggerOptions{ + Location: c.location, + Method: "CreateDynamicRegistrationSessionKey", + RequestID: opts.RequestID, + }).With( + "clientId", opts.ClientID, + "domain", opts.Domain, + "accountPublicId", opts.AccountPublicID, + ) + logger.DebugContext(ctx, "Creating registration session key...") + + sessionKey, err := utils.GenerateBase64Secret(sessionKeyByteLen) + if err != nil { + logger.ErrorContext(ctx, "Error generating session key", "error", err) + return "", err + } + + data := DynamicRegistrationSessionData{ + AccountPublicID: opts.AccountPublicID, + AccountVersion: opts.AccountVersion, + SessionKey: utils.Sha256HashHex(sessionKey), + } + dataBytes, err := json.Marshal(data) + if err != nil { + logger.ErrorContext(ctx, "Failed to marshal registration session data", "error", err) + return "", err + } + + if err := c.storage.SetWithContext( + ctx, + buildDynamicRegistrationSessionCacheKey(c.prefix, opts.Domain, opts.ClientID), + dataBytes, + c.oauthCodeTTL, + ); err != nil { + logger.ErrorContext(ctx, "Failed to set registration session in cache", "error", err) + return "", err + } + + return formatSessionKey(opts.ClientID, sessionKey), nil +} + +type VerifyDynamicRegistrationSessionKeyOptions struct { + RequestID string + Domain string + SessionKey string +} + +func (c dynamicRegistrationCache) VerifyDynamicRegistrationSessionKey( + ctx context.Context, + opts VerifyDynamicRegistrationSessionKeyOptions, +) (DynamicRegistrationSessionData, string, bool, bool, error) { + logger := utils.BuildLogger(c.logger, utils.LoggerOptions{ + Location: c.location, + Method: "VerifyDynamicRegistrationSessionKey", + RequestID: opts.RequestID, + }).With( + "domain", opts.Domain, + ) + logger.DebugContext(ctx, "Verifying registration session key...") + + clientID, sessionKey, ok := parseSessionKey(opts.SessionKey) + if !ok { + logger.DebugContext(ctx, "Invalid registration session key format") + return DynamicRegistrationSessionData{}, "", false, false, nil + } + + data, err := c.storage.GetWithContext(ctx, buildDynamicRegistrationSessionCacheKey(c.prefix, opts.Domain, clientID)) + if err != nil { + logger.ErrorContext(ctx, "Failed to get registration session from cache", "error", err) + return DynamicRegistrationSessionData{}, "", false, false, err + } + if data == nil { + logger.DebugContext(ctx, "registration session not found") + return DynamicRegistrationSessionData{}, "", false, false, nil + } + + var sessionData DynamicRegistrationSessionData + if err := json.Unmarshal(data, &sessionData); err != nil { + logger.ErrorContext(ctx, "Failed to unmarshal registration session data", "error", err) + return DynamicRegistrationSessionData{}, "", false, false, err + } + + ok, err = utils.CompareShaHex(sessionKey, sessionData.SessionKey) + if err != nil { + logger.ErrorContext(ctx, "Error comparing session key", "error", err) + return DynamicRegistrationSessionData{}, "", false, true, err + } + if !ok { + logger.DebugContext(ctx, "Invalid session key") + return DynamicRegistrationSessionData{}, clientID, false, true, nil + } + + return sessionData, clientID, true, true, nil +} + +type DeleteDynamicRegistrationSessionKeyOptions struct { + RequestID string + Domain string + ClientID string +} + +func (c dynamicRegistrationCache) DeleteDynamicRegistrationSessionKey( + ctx context.Context, + opts DeleteDynamicRegistrationSessionKeyOptions, +) error { + logger := utils.BuildLogger(c.logger, utils.LoggerOptions{ + Location: c.location, + Method: "DeleteDynamicRegistrationSessionKey", + RequestID: opts.RequestID, + }).With( + "domain", opts.Domain, + "clientId", opts.ClientID, + ) + logger.DebugContext(ctx, "Deleting registration session key...") + + return c.storage.DeleteWithContext( + ctx, + buildDynamicRegistrationSessionCacheKey(c.prefix, opts.Domain, opts.ClientID), + ) +} + +func buildDynamicRegistrationIATExtAuthCacheKey(prefix, provider, state string) string { + return fmt.Sprintf("%s:ext-auth:%s:%s", prefix, provider, utils.Sha256HashHex(state)) +} + +type DynamicRegistrationIATExtAuthData struct { + ClientID string `json:"client_id"` + Domain string `json:"domain"` + RequestState string `json:"request_state"` +} + +type SaveDynamicRegistrationIATExtAuthOptions struct { + RequestID string + ClientID string + Domain string + Provider string + State string + RequestState string +} + +func (c dynamicRegistrationCache) SaveDynamicRegistrationIATExtAuth( + ctx context.Context, + opts SaveDynamicRegistrationIATExtAuthOptions, +) error { + logger := utils.BuildLogger(c.logger, utils.LoggerOptions{ + Location: c.location, + Method: "SaveDynamicRegistrationIATExtAuth", + RequestID: opts.RequestID, + }).With( + "clientId", opts.ClientID, + "provider", opts.Provider, + ) + logger.DebugContext(ctx, "Saving dynamic registration IAT external auth...") + + data := DynamicRegistrationIATExtAuthData{ + ClientID: opts.ClientID, + Domain: opts.Domain, + RequestState: opts.RequestState, + } + dataBytes, err := json.Marshal(data) + if err != nil { + logger.ErrorContext(ctx, "Failed to marshal dynamic registration IAT external auth data", "error", err) + return err + } + + return c.storage.SetWithContext( + ctx, + buildDynamicRegistrationIATExtAuthCacheKey(c.prefix, opts.Provider, opts.State), + dataBytes, + c.oauthStateTTL, + ) +} + +type GetDynamicRegistrationIATExtAuthOptions struct { + RequestID string + Provider string + State string +} + +func (c dynamicRegistrationCache) GetDynamicRegistrationIATExtAuth( + ctx context.Context, + opts GetDynamicRegistrationIATExtAuthOptions, +) (DynamicRegistrationIATExtAuthData, bool, error) { + logger := utils.BuildLogger(c.logger, utils.LoggerOptions{ + Location: c.location, + Method: "GetDynamicRegistrationIATExtAuth", + RequestID: opts.RequestID, + }).With( + "provider", opts.Provider, + ) + logger.DebugContext(ctx, "Getting dynamic registration IAT external auth...") + + data, err := c.storage.GetWithContext(ctx, buildDynamicRegistrationIATExtAuthCacheKey(c.prefix, opts.Provider, opts.State)) + if err != nil { + logger.ErrorContext(ctx, "Failed to get dynamic registration IAT external auth", "error", err) + return DynamicRegistrationIATExtAuthData{}, false, err + } + if data == nil { + logger.DebugContext(ctx, "dynamic registration IAT external auth not found") + return DynamicRegistrationIATExtAuthData{}, false, nil + } + + var authData DynamicRegistrationIATExtAuthData + if err := json.Unmarshal(data, &authData); err != nil { + logger.ErrorContext(ctx, "Failed to unmarshal dynamic registration IAT external auth data", "error", err) + return DynamicRegistrationIATExtAuthData{}, false, err + } + + return authData, true, nil +} diff --git a/idp/internal/providers/cache/dynamic_registration_test.go b/idp/internal/providers/cache/dynamic_registration_test.go new file mode 100644 index 0000000..e9d717d --- /dev/null +++ b/idp/internal/providers/cache/dynamic_registration_test.go @@ -0,0 +1,164 @@ +package cache + +import ( + "context" + "errors" + "io" + "log/slog" + "strings" + "testing" + "time" + + "github.com/google/uuid" +) + +type registrationTestStorage struct { + values map[string][]byte + ttls map[string]time.Duration + deleteErr error +} + +func (s *registrationTestStorage) GetWithContext(_ context.Context, key string) ([]byte, error) { + return s.values[key], nil +} + +func (s *registrationTestStorage) SetWithContext(_ context.Context, key string, value []byte, ttl time.Duration) error { + s.values[key] = value + s.ttls[key] = ttl + return nil +} + +func (s *registrationTestStorage) DeleteWithContext(_ context.Context, key string) error { + if s.deleteErr != nil { + return s.deleteErr + } + delete(s.values, key) + return nil +} + +func TestDynamicRegistrationFlows(t *testing.T) { + ctx := context.Background() + storage := ®istrationTestStorage{values: map[string][]byte{}, ttls: map[string]time.Duration{}} + c := &Cache{logger: slog.New(slog.NewTextHandler(io.Discard, nil)), oauthStateTTL: time.Minute, oauthCodeTTL: 30 * time.Second} + flows := []dynamicRegistrationCache{c.accountCredentialsDynamicRegistration(), c.appsDynamicRegistration("one.example"), c.appsDynamicRegistration("two.example")} + for i := range flows { + flows[i].storage = storage + } + for _, flow := range flows { + t.Run(flow.prefix, func(t *testing.T) { + token, err := flow.SaveDynamicRegistrationIATLoginCSRF(ctx, SaveDynamicRegistrationIATLoginCSRFOptions{Domain: "example.com", ClientID: "client"}) + if err != nil { + t.Fatal(err) + } + key := flow.prefix + ":login:example.com:client" + if storage.ttls[key] != time.Minute || string(storage.values[key]) == token { + t.Fatal("CSRF TTL or hashing changed") + } + opts := VerifyDynamicRegistrationIATLoginCSRFOptions{Domain: "example.com", ClientID: "client", CSRFToken: "wrong"} + if ok, err := flow.VerifyDynamicRegistrationIATLoginCSRF(ctx, opts); ok || err != nil { + t.Fatalf("wrong token: %v %v", ok, err) + } + opts.CSRFToken = token + if ok, err := flow.VerifyDynamicRegistrationIATLoginCSRF(ctx, opts); !ok || err != nil { + t.Fatalf("valid token: %v %v", ok, err) + } + if ok, err := flow.VerifyDynamicRegistrationIATLoginCSRF(ctx, opts); ok || err != nil { + t.Fatalf("replayed token: %v %v", ok, err) + } + + id := uuid.New() + code, err := flow.GenerateDynamicRegistrationIATCode(ctx, GenerateDynamicRegistrationIATCodeOptions{AccountPublicID: id, AccountVersion: 3, ClientID: "client", Domain: "example.com", Challenge: "challenge"}) + if err != nil { + t.Fatal(err) + } + codeKey := flow.prefix + ":code:" + strings.Split(code, "-")[0] + if storage.ttls[codeKey] != 30*time.Second { + t.Fatal("code TTL changed") + } + for _, other := range flows { + if other.prefix == flow.prefix { + continue + } + if _, ok, err := other.VerifyDynamicRegistrationIATCode(ctx, VerifyDynamicRegistrationIATCodeOptions{Code: code}); ok || err != nil { + t.Fatal("code crossed registration scopes") + } + } + data, ok, err := flow.VerifyDynamicRegistrationIATCode(ctx, VerifyDynamicRegistrationIATCodeOptions{Code: code}) + if err != nil || !ok || data.AccountPublicID != id || data.AccountVersion != 3 || data.Challenge != "challenge" { + t.Fatalf("code round trip: %+v %v %v", data, ok, err) + } + if _, ok, err := flow.VerifyDynamicRegistrationIATCode(ctx, VerifyDynamicRegistrationIATCodeOptions{Code: code}); ok || err != nil { + t.Fatal("code replay accepted") + } + + session, err := flow.CreateDynamicRegistrationSessionKey(ctx, CreateDynamicRegistrationSessionKeyOptions{AccountPublicID: id, AccountVersion: 3, ClientID: "client", Domain: "example.com"}) + if err != nil { + t.Fatal(err) + } + for range 2 { + data, client, valid, found, err := flow.VerifyDynamicRegistrationSessionKey(ctx, VerifyDynamicRegistrationSessionKeyOptions{Domain: "example.com", SessionKey: session}) + if err != nil || !valid || !found || client != "client" || data.AccountPublicID != id { + t.Fatalf("session round trip: %+v %s %v %v %v", data, client, valid, found, err) + } + } + _, client, valid, found, err := flow.VerifyDynamicRegistrationSessionKey(ctx, VerifyDynamicRegistrationSessionKeyOptions{Domain: "example.com", SessionKey: "client.wrong"}) + if err != nil || valid || !found || client != "client" { + t.Fatal("invalid session flags changed") + } + _, _, valid, found, err = flow.VerifyDynamicRegistrationSessionKey(ctx, VerifyDynamicRegistrationSessionKeyOptions{Domain: "other.example", SessionKey: session}) + if err != nil || valid || found { + t.Fatal("session crossed domains") + } + + storage.values[flow.prefix+":code:corrupt"] = []byte("{") + if _, ok, err := flow.VerifyDynamicRegistrationIATCode(ctx, VerifyDynamicRegistrationIATCodeOptions{Code: "corrupt.secret"}); ok || err != nil { + t.Fatal("malformed code accepted") + } + if _, ok, err := flow.VerifyDynamicRegistrationIATCode(ctx, VerifyDynamicRegistrationIATCodeOptions{Code: "corrupt-secret"}); ok || err == nil { + t.Fatal("corrupt JSON error lost") + } + + csrf, err := flow.SaveDynamicRegistrationIAT2FACSRFToken(ctx, SaveDynamicRegistrationIAT2FACSRFTokenOptions{SessionID: "session", TwoFATTL: 42}) + if err != nil { + t.Fatal(err) + } + if storage.ttls[buildDynamicRegistrationIAT2FACSRFCacheKey(flow.prefix, "session")] != 42*time.Second { + t.Fatal("2FA TTL changed") + } + storage.deleteErr = errors.New("delete failed") + if ok, err := flow.VerifyDynamicRegistrationIAT2FACSRFToken(ctx, VerifyDynamicRegistrationIAT2FACSRFTokenOptions{SessionID: "session", CSRFToken: csrf}); ok || !errors.Is(err, storage.deleteErr) { + t.Fatal("delete failure lost") + } + storage.deleteErr = nil + }) + } +} + +func TestDynamicRegistrationAuthPayloads(t *testing.T) { + ctx := context.Background() + storage := ®istrationTestStorage{values: map[string][]byte{}, ttls: map[string]time.Duration{}} + c := &Cache{logger: slog.New(slog.NewTextHandler(io.Discard, nil)), oauthStateTTL: time.Minute} + flow := c.appsDynamicRegistration("example.com") + flow.storage = storage + want := AppsDynamicRegistrationIATAuthData{AccountID: 42, Domain: "domain", State: "state", Challenge: "challenge", RedirectURI: "redirect"} + client, err := flow.saveAuth(ctx, c.logger, want) + if err != nil { + t.Fatal(err) + } + got, ok, err := getDynamicRegistrationAuth[AppsDynamicRegistrationIATAuthData](ctx, flow, c.logger, client) + if err != nil || !ok || got != want { + t.Fatalf("auth round trip: %+v %v %v", got, ok, err) + } + account := c.accountCredentialsDynamicRegistration() + account.storage = storage + if _, ok, err := getDynamicRegistrationAuth[AccountCredentialsDynamicRegistrationIATAuthData](ctx, account, c.logger, client); ok || err != nil { + t.Fatal("auth crossed scopes") + } + client, err = account.saveAuth(ctx, c.logger, AccountCredentialsDynamicRegistrationIATAuthData{Domain: "domain"}) + if err != nil { + t.Fatal(err) + } + if strings.Contains(string(storage.values[account.prefix+":auth:"+client]), "account_id") { + t.Fatal("account credentials JSON schema changed") + } +} diff --git a/idp/internal/server/routes/oauth.go b/idp/internal/server/routes/oauth.go index 04207f3..ea5e5e9 100644 --- a/idp/internal/server/routes/oauth.go +++ b/idp/internal/server/routes/oauth.go @@ -61,24 +61,86 @@ func (r *Routes) OAuthRoutes(app *fiber.App) { 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) + iatRouter.Get( + paths.OAuthAuth, + r.controllers.HostMiddleware, + HostAwareRoute( + r.controllers.OAuthDynamicRegistrationIATAuth, + r.controllers.AppsOAuthDynamicRegistrationIATAuth, + ), + ) + iatRouter.Post( + paths.OAuthToken, + r.controllers.HostMiddleware, + HostAwareRoute( + r.controllers.OAuthDynamicRegistrationIATToken, + r.controllers.AppsOAuthDynamicRegistrationIATToken, + ), + ) // Dynamic Registration IAT Login flow const loginRoute = paths.InitialAccessTokenSingle + paths.AuthLogin - iatRouter.Get(loginRoute, r.controllers.OAuthDynamicRegistrationIATLoginGet) - iatRouter.Post(loginRoute, r.controllers.OAuthDynamicRegistrationIATLoginPost) + iatRouter.Get( + loginRoute, + r.controllers.HostMiddleware, + HostAwareRoute( + r.controllers.OAuthDynamicRegistrationIATLoginGet, + r.controllers.AppsOAuthDynamicRegistrationIATLoginGet, + ), + ) + iatRouter.Post( + loginRoute, + r.controllers.HostMiddleware, + HostAwareRoute( + r.controllers.OAuthDynamicRegistrationIATLoginPost, + r.controllers.AppsOAuthDynamicRegistrationIATLoginPost, + ), + ) // Dynamic Registration IAT 2FA flow const twoFAAuthRoute = loginRoute + paths.Auth2FA - iatRouter.Get(twoFAAuthRoute, r.controllers.OAuthDynamicRegistrationIAT2FAGet) - iatRouter.Post(twoFAAuthRoute, r.controllers.OAuthDynamicRegistrationIAT2FAPost) + iatRouter.Get( + twoFAAuthRoute, + r.controllers.HostMiddleware, + HostAwareRoute( + r.controllers.OAuthDynamicRegistrationIAT2FAGet, + r.controllers.AppsOAuthDynamicRegistrationIAT2FAGet, + ), + ) + iatRouter.Post( + twoFAAuthRoute, + r.controllers.HostMiddleware, + HostAwareRoute( + r.controllers.OAuthDynamicRegistrationIAT2FAPost, + r.controllers.AppsOAuthDynamicRegistrationIAT2FAPost, + ), + ) // Dynamic Registration IAT External Auth flow const extAuthRoute = paths.InitialAccessTokenSingle + paths.InitialAccessTokenAuthEXT - iatRouter.Get(extAuthRoute+paths.InitialAccessTokenProvider, r.controllers.OAuthDynamicRegistrationIATExtAuthGet) - iatRouter.Post(extAuthRoute+paths.OAuthAppleCallback, r.controllers.OAuthDynamicRegistrationIATExtAppleCB) - iatRouter.Get(extAuthRoute+paths.OAuthCallback, r.controllers.OAuthDynamicRegistrationIATExtCB) + iatRouter.Get( + extAuthRoute+paths.InitialAccessTokenProvider, + r.controllers.HostMiddleware, + HostAwareRoute( + r.controllers.OAuthDynamicRegistrationIATExtAuthGet, + r.controllers.AppsOAuthDynamicRegistrationIATExtAuthGet, + ), + ) + iatRouter.Post( + extAuthRoute+paths.OAuthAppleCallback, + r.controllers.HostMiddleware, + HostAwareRoute( + r.controllers.OAuthDynamicRegistrationIATExtAppleCB, + r.controllers.AppsOAuthDynamicRegistrationIATExtAppleCB, + ), + ) + iatRouter.Get( + extAuthRoute+paths.OAuthCallback, + r.controllers.HostMiddleware, + HostAwareRoute( + r.controllers.OAuthDynamicRegistrationIATExtCB, + r.controllers.AppsOAuthDynamicRegistrationIATExtCB, + ), + ) } diff --git a/idp/internal/server/routes/oauth_test.go b/idp/internal/server/routes/oauth_test.go new file mode 100644 index 0000000..f4cf86b --- /dev/null +++ b/idp/internal/server/routes/oauth_test.go @@ -0,0 +1,49 @@ +package routes + +import ( + "io" + "log/slog" + "net/http/httptest" + "testing" + + "github.com/go-playground/validator/v10" + "github.com/gofiber/fiber/v3" + "github.com/tugascript/devlogs/idp/internal/controllers" + "github.com/tugascript/devlogs/idp/internal/controllers/paths" +) + +func TestOAuthIATRoutesRejectUnknownHosts(t *testing.T) { + ctrl := controllers.NewControllers(slog.New(slog.NewTextHandler(io.Discard, nil)), nil, validator.New(), "example.com", "id.example.com", "session") + app := fiber.New() + NewRoutes(ctrl).OAuthRoutes(app) + base := paths.V1 + paths.AuthBase + paths.OAuthBase + paths.InitialAccessToken + client := "/abcdefghijklmnopqrstuv" + for _, tc := range []struct{ method, path string }{ + {"GET", paths.OAuthAuth}, {"POST", paths.OAuthToken}, + {"GET", client + paths.AuthLogin}, {"POST", client + paths.AuthLogin}, + {"GET", client + paths.AuthLogin + paths.Auth2FA}, {"POST", client + paths.AuthLogin + paths.Auth2FA}, + {"GET", client + paths.InitialAccessTokenAuthEXT + "/google"}, + {"GET", client + paths.InitialAccessTokenAuthEXT + "/google" + paths.InitialAccessTokenCallback}, + {"POST", client + paths.InitialAccessTokenAuthEXT + "/apple" + paths.InitialAccessTokenCallback}, + } { + t.Run(tc.method+tc.path, func(t *testing.T) { + response, err := app.Test(httptest.NewRequest(tc.method, "https://untrusted.example"+base+tc.path, nil)) + if err != nil { + t.Fatal(err) + } + defer response.Body.Close() + if response.StatusCode != 404 { + t.Fatalf("unknown host: got %d", response.StatusCode) + } + // The same route on the base host reaches request validation. + response, err = app.Test(httptest.NewRequest(tc.method, "https://id.example.com"+base+tc.path, nil)) + if err != nil { + t.Fatal(err) + } + defer response.Body.Close() + if response.StatusCode < 400 || response.StatusCode == 404 || response.StatusCode >= 500 { + t.Fatalf("base-host route not reached: got %d", response.StatusCode) + } + }) + } +} diff --git a/idp/internal/services/oauth_dynamic_registration.go b/idp/internal/services/oauth_dynamic_registration.go new file mode 100644 index 0000000..be957d2 --- /dev/null +++ b/idp/internal/services/oauth_dynamic_registration.go @@ -0,0 +1,145 @@ +// 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 + +import ( + "context" + "log/slog" + "net/url" + + "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/services/dtos" + "github.com/tugascript/devlogs/idp/internal/utils" +) + +const ( + oauthDynamicRegistrationIATPath string = paths.V1 + paths.AuthBase + paths.OAuthBase + paths.InitialAccessToken + oauthDynamicRegistrationIATAuthPath string = oauthDynamicRegistrationIATPath + paths.OAuthAuth +) + +type dynamicRegistrationRequest struct { + domain string + state string + redirectURI string +} + +func validateDynamicRegistrationRequest(saved, requested dynamicRegistrationRequest) *exceptions.ServiceError { + if saved != requested { + return exceptions.NewUnauthorizedError() + } + return nil +} + +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 +} + +type buildOAuthDynamicRegistrationIATLoginURLOptions struct { + accClientID string + domain string + state string + challenge string + challengeMethod string + redirectURI string +} + +func buildOAuthDynamicRegistrationIATLoginURL(opts buildOAuthDynamicRegistrationIATLoginURLOptions) string { + 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 != "" { + queryParams.Add("code_challenge_method", opts.challengeMethod) + } + return oauthDynamicRegistrationIATPath + "/" + opts.accClientID + paths.AuthLogin + "?" + queryParams.Encode() +} + +type buildOAuthDynamicRegistrationIATCallbackURLOptions struct { + 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.issuerDomain) + return opts.redirectURI + "?" + queryParams.Encode() +} + +func map2FATypeTokens(twoFAType string) (tokens.TwoFAType, *exceptions.ServiceError) { + switch twoFAType { + case TwoFactorTypeEmail: + return tokens.TwoFATypeEmail, nil + case TwoFactorTypeTotp: + return tokens.TwoFATypeTOTP, nil + default: + return "", exceptions.NewValidationError("invalid two factor type") + } +} + +func (s *Services) authenticateDynamicRegistrationAccount(ctx context.Context, logger *slog.Logger, requestID, email, password string) (dtos.AccountDTO, *exceptions.ServiceError) { + accountDTO, serviceErr := s.GetAccountByEmail(ctx, GetAccountByEmailOptions{ + RequestID: requestID, + Email: email, + }) + if serviceErr != nil { + if serviceErr.Code != exceptions.CodeNotFound { + return dtos.AccountDTO{}, serviceErr + } + + logger.WarnContext(ctx, "Account was not found", "error", serviceErr) + return dtos.AccountDTO{}, exceptions.NewUnauthorizedError() + } + if _, err := s.database.FindAccountAuthProviderByAccountPublicIdAndProvider( + ctx, + database.FindAccountAuthProviderByAccountPublicIdAndProviderParams{ + AccountPublicID: accountDTO.PublicID, + Provider: database.AuthProviderLocal, + }, + ); err != nil { + serviceErr := exceptions.FromDBError(err) + if serviceErr.Code != exceptions.CodeNotFound { + logger.ErrorContext(ctx, "Failed to find account auth provider", "error", err) + return dtos.AccountDTO{}, serviceErr + } + + logger.WarnContext(ctx, "Account auth provider not found", "error", err) + return dtos.AccountDTO{}, exceptions.NewUnauthorizedError() + } + + passwordVerified, err := utils.Argon2CompareHash(password, accountDTO.Password()) + if err != nil { + logger.ErrorContext(ctx, "Failed to verify password", "error", err) + return dtos.AccountDTO{}, exceptions.NewInternalServerError() + } + if !passwordVerified { + logger.WarnContext(ctx, "Passwords do not match") + return dtos.AccountDTO{}, exceptions.NewUnauthorizedError() + } + if !accountDTO.EmailVerified() { + logger.InfoContext(ctx, "Account is not confirmed") + return dtos.AccountDTO{}, exceptions.NewForbiddenError() + } + + return accountDTO, nil +} diff --git a/idp/internal/services/oauth_dynamic_registration_accounts.go b/idp/internal/services/oauth_dynamic_registration_accounts.go index f427a19..54125a1 100644 --- a/idp/internal/services/oauth_dynamic_registration_accounts.go +++ b/idp/internal/services/oauth_dynamic_registration_accounts.go @@ -28,11 +28,6 @@ import ( const oauthDynamicRegistrationLocation string = "oauth_dynamic_registration_accounts" -const ( - oauthDynamicRegistrationIATPath string = paths.V1 + paths.AuthBase + paths.OAuthBase + paths.InitialAccessToken - oauthDynamicRegistrationIATAuthPath string = oauthDynamicRegistrationIATPath + paths.OAuthAuth -) - type refreshTokenOAuthDynamicRegistrationIATLoginOptions struct { requestID string refreshToken string @@ -212,18 +207,6 @@ func (s *Services) initiateOAuthDynamicRegistrationIATAuthAccounts( return "", nil } -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, @@ -244,43 +227,6 @@ func (s *Services) checkIATRegistrationDomain( return nil } -type buildOAuthDynamicRegistrationIATLoginURLOptions struct { - accClientID string - domain string - state string - challenge string - challengeMethod string - redirectURI string -} - -func buildOAuthDynamicRegistrationIATLoginURL(opts buildOAuthDynamicRegistrationIATLoginURLOptions) string { - 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 != "" { - queryParams.Add("code_challenge_method", opts.challengeMethod) - } - return oauthDynamicRegistrationIATPath + "/" + opts.accClientID + paths.OAuthAuth + paths.AuthLogin + "?" + queryParams.Encode() -} - -type buildOAuthDynamicRegistrationIATCallbackURLOptions struct { - 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.issuerDomain) - return opts.redirectURI + "?" + queryParams.Encode() -} - type generateOAuthDynamicRegistrationIATCallbackOptions struct { requestID string clientID string @@ -613,17 +559,11 @@ func (s *Services) OAuthDynamicRegistrationIATAuthRender( return "", exceptions.NewForbiddenError() } - if data.Domain != opts.Domain { - logger.WarnContext(ctx, "OAuth Domain does not match", "dataDomain", data.Domain) - return "", exceptions.NewUnauthorizedError() - } - if data.State != opts.State { - logger.WarnContext(ctx, "OAuth State does not match") - return "", exceptions.NewUnauthorizedError() - } - if data.RedirectURI != opts.RedirectURI { - logger.WarnContext(ctx, "OAuth Redirect URI does not match") - return "", exceptions.NewUnauthorizedError() + if serviceErr := validateDynamicRegistrationRequest( + dynamicRegistrationRequest{domain: data.Domain, state: data.State, redirectURI: data.RedirectURI}, + dynamicRegistrationRequest{domain: opts.Domain, state: opts.State, redirectURI: opts.RedirectURI}, + ); serviceErr != nil { + return "", serviceErr } csrfToken, err := s.cache.SaveAccountCredentialsDynamicRegistrationIATLoginCSRF( @@ -769,60 +709,16 @@ func (s *Services) OAuthDynamicRegistrationIATLogin( return "", "", false, exceptions.NewNotFoundError() } - if data.Domain != opts.Domain { - logger.WarnContext(ctx, "OAuth Domain does not match", "dataDomain", data.Domain) - return "", "", false, exceptions.NewUnauthorizedError() - } - if data.State != opts.State { - logger.WarnContext(ctx, "OAuth State does not match") - return "", "", false, exceptions.NewUnauthorizedError() - } - if data.RedirectURI != opts.RedirectURI { - logger.WarnContext(ctx, "OAuth Redirect URI does not match") - return "", "", false, exceptions.NewUnauthorizedError() + if serviceErr := validateDynamicRegistrationRequest( + dynamicRegistrationRequest{domain: data.Domain, state: data.State, redirectURI: data.RedirectURI}, + dynamicRegistrationRequest{domain: opts.Domain, state: opts.State, redirectURI: opts.RedirectURI}, + ); serviceErr != nil { + return "", "", false, serviceErr } - accountDTO, serviceErr := s.GetAccountByEmail(ctx, GetAccountByEmailOptions{ - RequestID: opts.RequestID, - Email: opts.Email, - }) + accountDTO, serviceErr := s.authenticateDynamicRegistrationAccount(ctx, logger, opts.RequestID, opts.Email, opts.Password) if serviceErr != nil { - if serviceErr.Code != exceptions.CodeNotFound { - return "", "", false, serviceErr - } - - logger.WarnContext(ctx, "Account was not found", "error", serviceErr) - return "", "", false, exceptions.NewUnauthorizedError() - } - if _, err := s.database.FindAccountAuthProviderByAccountPublicIdAndProvider( - ctx, - database.FindAccountAuthProviderByAccountPublicIdAndProviderParams{ - AccountPublicID: accountDTO.PublicID, - Provider: database.AuthProviderLocal, - }, - ); err != nil { - serviceErr := exceptions.FromDBError(err) - if serviceErr.Code != exceptions.CodeNotFound { - logger.ErrorContext(ctx, "Failed to find account auth provider", "error", err) - return "", "", false, serviceErr - } - - logger.WarnContext(ctx, "Account auth provider not found", "error", err) - return "", "", false, exceptions.NewUnauthorizedError() - } - - passwordVerified, err := utils.Argon2CompareHash(opts.Password, accountDTO.Password()) - if err != nil { - logger.ErrorContext(ctx, "Failed to verify password", "error", err) - return "", "", false, exceptions.NewInternalServerError() - } - if !passwordVerified { - logger.WarnContext(ctx, "Passwords do not match") - return "", "", false, exceptions.NewUnauthorizedError() - } - if !accountDTO.EmailVerified() { - logger.InfoContext(ctx, "Account is not confirmed") - return "", "", false, exceptions.NewForbiddenError() + return "", "", false, serviceErr } default2FAConfig, serviceErr := s.getDefaultAccount2FAConfigInternal(ctx, getDefaultAccount2FAConfigInternalOptions{ @@ -897,7 +793,7 @@ func (s *Services) OAuthDynamicRegistrationIATLogin( if opts.CodeChallengeMethod != "" { queryParams.Add("code_challenge_method", opts.CodeChallengeMethod) } - return oauthDynamicRegistrationIATPath + "/" + opts.ACCClientID + paths.OAuthAuth + + return oauthDynamicRegistrationIATPath + "/" + opts.ACCClientID + paths.AuthLogin + paths.Auth2FA + "?" + queryParams.Encode(), sessionID, false, nil } @@ -977,17 +873,11 @@ func (s *Services) OAuthDynamicRegistrationIAT2FARender( logger.WarnContext(ctx, "Client IDs do not match", "sessionClientId", data.ClientID) return "", exceptions.NewUnauthorizedError() } - if data.Domain != opts.Domain { - logger.WarnContext(ctx, "OAuth Domain does not match", "dataDomain", data.Domain) - return "", exceptions.NewUnauthorizedError() - } - if data.State != opts.State { - logger.WarnContext(ctx, "OAuth State does not match") - return "", exceptions.NewUnauthorizedError() - } - if data.RedirectURI != opts.RedirectURI { - logger.WarnContext(ctx, "OAuth Redirect URI does not match") - return "", exceptions.NewUnauthorizedError() + if serviceErr := validateDynamicRegistrationRequest( + dynamicRegistrationRequest{domain: data.Domain, state: data.State, redirectURI: data.RedirectURI}, + dynamicRegistrationRequest{domain: opts.Domain, state: opts.State, redirectURI: opts.RedirectURI}, + ); serviceErr != nil { + return "", serviceErr } csrfToken, err := s.cache.SaveAccountCredentialsDynamicRegistrationIAT2FACSRFToken( @@ -1066,17 +956,6 @@ func (s *Services) OAuthDynamicRegistrationIAT2FAReRender( return twoFAhtml, nil } -func map2FATypeTokens(twoFAType string) (tokens.TwoFAType, *exceptions.ServiceError) { - switch twoFAType { - case TwoFactorTypeEmail: - return tokens.TwoFATypeEmail, nil - case TwoFactorTypeTotp: - return tokens.TwoFATypeTOTP, nil - default: - return "", exceptions.NewValidationError("invalid two factor type") - } -} - type OAuthDynamicRegistrationIATVerify2FACodeOptions struct { RequestID string ACCClientID string @@ -1322,17 +1201,11 @@ func (s *Services) OAuthDynamicRegistrationIATExtGet( return "", exceptions.NewNotFoundError() } - if data.Domain != opts.Domain { - logger.WarnContext(ctx, "OAuth Domain does not match", "dataDomain", data.Domain) - return "", exceptions.NewUnauthorizedError() - } - if data.State != opts.State { - logger.WarnContext(ctx, "OAuth State does not match") - return "", exceptions.NewUnauthorizedError() - } - if data.RedirectURI != opts.RedirectURI { - logger.WarnContext(ctx, "OAuth Redirect URI does not match") - return "", exceptions.NewUnauthorizedError() + if serviceErr := validateDynamicRegistrationRequest( + dynamicRegistrationRequest{domain: data.Domain, state: data.State, redirectURI: data.RedirectURI}, + dynamicRegistrationRequest{domain: opts.Domain, state: opts.State, redirectURI: opts.RedirectURI}, + ); serviceErr != nil { + return "", serviceErr } if err := s.cache.SaveAccountCredentialsDynamicRegistrationIATExtAuth(ctx, cache.SaveAccountCredentialsDynamicRegistrationIATExtAuthOptions{ diff --git a/idp/internal/services/oauth_dynamic_registration_apps.go b/idp/internal/services/oauth_dynamic_registration_apps.go index f9505ae..a3e3274 100644 --- a/idp/internal/services/oauth_dynamic_registration_apps.go +++ b/idp/internal/services/oauth_dynamic_registration_apps.go @@ -8,12 +8,329 @@ package services import ( "context" + "net/url" + "slices" + "time" + "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/providers/mailer" + "github.com/tugascript/devlogs/idp/internal/providers/oauth" + "github.com/tugascript/devlogs/idp/internal/providers/tokens" + "github.com/tugascript/devlogs/idp/internal/services/dtos" + "github.com/tugascript/devlogs/idp/internal/services/templates" + "github.com/tugascript/devlogs/idp/internal/utils" ) const oauthDynamicRegistrationAppsLocation string = "oauth_dynamic_registration_apps" +// Hostname and AccountID must come from the trusted host middleware, never from +// registration form fields. Both are required to avoid an unscoped cache flow. +func validateAppsRegistrationTenant(hostname string, accountID int32) *exceptions.ServiceError { + if hostname == "" || accountID <= 0 { + return exceptions.NewUnauthorizedError() + } + return nil +} + +type refreshTokenAppsOAuthDynamicRegistrationIATLoginOptions struct { + requestID string + refreshToken string + challenge string + challengeMethod string + domain string + redirectURI string + state string + backendDomain string + hostname string + accountID int32 +} + +func (s *Services) refreshTokenAppsOAuthDynamicRegistrationIATLogin( + ctx context.Context, + opts refreshTokenAppsOAuthDynamicRegistrationIATLoginOptions, +) (string, *exceptions.ServiceError) { + if serviceErr := validateAppsRegistrationTenant(opts.hostname, opts.accountID); serviceErr != nil { + return "", serviceErr + } + logger := s.buildLogger( + opts.requestID, + oauthDynamicRegistrationAppsLocation, + "refreshTokenAppsOAuthDynamicRegistrationIATLogin", + ).With( + "domain", opts.domain, + "redirectUri", opts.redirectURI, + "hostname", opts.hostname, + ) + logger.InfoContext(ctx, "Refreshing OAuth dynamic registration IAT callback for Apps...") + + data, err := s.jwt.VerifyRefreshToken( + opts.refreshToken, + s.BuildGetGlobalPublicKeyFn(ctx, BuildGetGlobalVerifyKeyFnOptions{ + RequestID: opts.requestID, + KeyType: database.TokenKeyTypeRefresh, + }), + ) + if err != nil { + logger.WarnContext(ctx, "Invalid refresh token", "error", err) + return s.oauthAppsDynamicRegistrationIATAuth(ctx, oauthAppsDynamicRegistrationIATAuthOptions{ + requestID: opts.requestID, domain: opts.domain, state: opts.state, + challenge: opts.challenge, challengeMethod: opts.challengeMethod, redirectURI: opts.redirectURI, + hostname: opts.hostname, accountID: opts.accountID, + }) + } + + if !slices.ContainsFunc(data.Scopes, func(s string) bool { + return s == tokens.AccountScopeAdmin || s == tokens.AccountScopeAppsWrite + }) { + logger.WarnContext(ctx, "Refresh token missing app registration scope") + return s.oauthAppsDynamicRegistrationIATAuth(ctx, oauthAppsDynamicRegistrationIATAuthOptions{ + requestID: opts.requestID, domain: opts.domain, state: opts.state, + challenge: opts.challenge, challengeMethod: opts.challengeMethod, redirectURI: opts.redirectURI, + hostname: opts.hostname, accountID: opts.accountID, + }) + } + + stn, err := s.database.FindSessionTokenByTokenID(ctx, data.TokenID) + if err != nil { + serviceErr := exceptions.FromDBError(err) + if serviceErr.Code != exceptions.CodeNotFound { + logger.ErrorContext(ctx, "Failed to find session token by token ID", "error", err) + return "", exceptions.NewInternalServerError() + } + logger.WarnContext(ctx, "Session token not found", "serviceError", serviceErr) + return "", exceptions.NewUnauthorizedError() + } + + if stn.ExpiresAt.Before(time.Now()) { + logger.WarnContext(ctx, "Session token expired", "expiresAt", stn.ExpiresAt) + return "", exceptions.NewUnauthorizedError() + } + + accountDTO, serviceErr := s.GetAccountByPublicIDAndVersion(ctx, GetAccountByPublicIDAndVersionOptions{ + RequestID: opts.requestID, + PublicID: data.AccountClaims.AccountID, + Version: data.AccountClaims.AccountVersion, + }) + if serviceErr != nil { + if serviceErr.Code != exceptions.CodeNotFound && serviceErr.Code != exceptions.CodeUnauthorized { + logger.ErrorContext(ctx, "Failed to get account by public ID and version", "serviceError", serviceErr) + return "", serviceErr + } + + logger.WarnContext(ctx, "Account not found or version mismatch", "serviceError", serviceErr) + return s.oauthAppsDynamicRegistrationIATAuth(ctx, oauthAppsDynamicRegistrationIATAuthOptions{ + requestID: opts.requestID, + challenge: opts.challenge, + challengeMethod: opts.challengeMethod, + domain: opts.domain, + redirectURI: opts.redirectURI, + state: opts.state, + hostname: opts.hostname, + accountID: opts.accountID, + }) + } + if accountDTO.ID() != opts.accountID { + return "", exceptions.NewUnauthorizedError() + } + + hashedChallenge, serviceErr := hashChallenge(opts.challenge, opts.challengeMethod) + if serviceErr != nil { + logger.ErrorContext(ctx, "Invalid code challenge", "serviceError", serviceErr) + return "", serviceErr + } + + cbURL, serviceErr := s.generateAppsOAuthDynamicRegistrationIATCallback( + ctx, + generateAppsOAuthDynamicRegistrationIATCallbackOptions{ + requestID: opts.requestID, + clientID: utils.Base62UUID(), + accountPublicID: accountDTO.PublicID, + accountVersion: accountDTO.Version(), + challenge: hashedChallenge, + domain: opts.domain, + redirectURI: opts.redirectURI, + state: opts.state, + hostname: opts.hostname, + accountID: opts.accountID, + }, + ) + if serviceErr != nil { + logger.ErrorContext(ctx, "Failed to generate OAuth dynamic registration IAT callback", "serviceErr", serviceErr) + return "", serviceErr + } + + return cbURL, nil +} + +func (s *Services) checkAppIATRegistrationDomain( + ctx context.Context, + requestID string, + accountPublicID uuid.UUID, + domain string, +) *exceptions.ServiceError { + domainDTO, serviceErr := s.GetAppCredentialsRegistrationDomain(ctx, GetAppCredentialsRegistrationDomainOptions{ + RequestID: requestID, + AccountPublicID: accountPublicID, + Domain: domain, + }) + if serviceErr != nil { + return serviceErr + } + if !domainDTO.Verified { + return exceptions.NewForbiddenError() + } + return nil +} + +type generateAppsOAuthDynamicRegistrationIATCallbackOptions struct { + requestID string + clientID string + accountPublicID uuid.UUID + accountVersion int32 + challenge string + domain string + redirectURI string + state string + hostname string + accountID int32 +} + +func (s *Services) generateAppsOAuthDynamicRegistrationIATCallback( + ctx context.Context, + opts generateAppsOAuthDynamicRegistrationIATCallbackOptions, +) (string, *exceptions.ServiceError) { + if serviceErr := validateAppsRegistrationTenant(opts.hostname, opts.accountID); serviceErr != nil { + return "", serviceErr + } + logger := s.buildLogger( + opts.requestID, + oauthDynamicRegistrationAppsLocation, + "generateAppsOAuthDynamicRegistrationIATCallback", + ).With( + "clientId", opts.clientID, + "accountPublicId", opts.accountPublicID, + "hostname", opts.hostname, + ) + logger.InfoContext(ctx, "Generating OAuth dynamic registration IAT callback for Apps...") + + account, serviceErr := s.GetAccountByPublicIDAndVersion(ctx, GetAccountByPublicIDAndVersionOptions{ + RequestID: opts.requestID, PublicID: opts.accountPublicID, Version: opts.accountVersion, + }) + if serviceErr != nil { + return "", serviceErr + } + if account.ID() != opts.accountID { + return "", exceptions.NewUnauthorizedError() + } + if serviceErr := s.checkAppIATRegistrationDomain(ctx, opts.requestID, account.PublicID, opts.domain); serviceErr != nil { + return "", serviceErr + } + + code, err := s.cache.GenerateAppsRegistrationIATCode( + ctx, + cache.GenerateAppsRegistrationIATCodeOptions{ + Hostname: opts.hostname, + RequestID: opts.requestID, + ClientID: opts.clientID, + AccountPublicID: opts.accountPublicID, + AccountVersion: opts.accountVersion, + Challenge: opts.challenge, + Domain: opts.domain, + }, + ) + if err != nil { + logger.ErrorContext(ctx, "Failed to generate apps registration IAT code", "error", err) + return "", exceptions.NewInternalServerError() + } + + return buildOAuthDynamicRegistrationIATCallbackURL(buildOAuthDynamicRegistrationIATCallbackURLOptions{ + redirectURI: opts.redirectURI, + code: code, + state: opts.state, + issuerDomain: opts.hostname, + }), nil +} + +func (s *Services) checkAppDynamicRegistrationDomainUsability(ctx context.Context, requestID string, accountID int32, domain string) *exceptions.ServiceError { + account, serviceErr := s.GetAccountByID(ctx, GetAccountByIDOptions{RequestID: requestID, ID: accountID}) + if serviceErr != nil { + return serviceErr + } + return s.checkAppIATRegistrationDomain(ctx, requestID, account.PublicID, domain) +} + +type oauthAppsDynamicRegistrationIATAuthOptions struct { + requestID string + challenge string + challengeMethod string + domain string + redirectURI string + state string + hostname string + accountID int32 +} + +func (s *Services) oauthAppsDynamicRegistrationIATAuth( + ctx context.Context, + opts oauthAppsDynamicRegistrationIATAuthOptions, +) (string, *exceptions.ServiceError) { + if serviceErr := validateAppsRegistrationTenant(opts.hostname, opts.accountID); serviceErr != nil { + return "", serviceErr + } + logger := s.buildLogger( + opts.requestID, + oauthDynamicRegistrationAppsLocation, + "oauthAppsDynamicRegistrationIATAuth", + ).With( + "domain", opts.domain, + "redirectUri", opts.redirectURI, + "hostname", opts.hostname, + ) + logger.InfoContext(ctx, "Handling OAuth dynamic registration IAT auth for Apps...") + + if serviceErr := s.checkAppDynamicRegistrationDomainUsability(ctx, opts.requestID, opts.accountID, opts.domain); serviceErr != nil { + logger.InfoContext(ctx, "App dynamic registration domain not usable", "serviceError", serviceErr) + return "", serviceErr + } + + hashedChallenge, serviceErr := hashChallenge(opts.challenge, opts.challengeMethod) + if serviceErr != nil { + logger.ErrorContext(ctx, "Invalid code challenge", "serviceError", serviceErr) + return "", serviceErr + } + + clientID, err := s.cache.SaveAppsDynamicRegistrationIATAuth( + ctx, + cache.SaveAppsDynamicRegistrationIATAuthOptions{ + AccountID: opts.accountID, + Hostname: opts.hostname, + Domain: opts.domain, + RequestID: opts.requestID, + State: opts.state, + RedirectURI: opts.redirectURI, + Challenge: hashedChallenge, + }, + ) + if err != nil { + logger.ErrorContext(ctx, "Failed to save apps dynamic registration IAT auth", "error", err) + return "", exceptions.NewInternalServerError() + } + + return buildOAuthDynamicRegistrationIATLoginURL(buildOAuthDynamicRegistrationIATLoginURLOptions{ + accClientID: clientID, + domain: opts.domain, + state: opts.state, + challenge: opts.challenge, + challengeMethod: opts.challengeMethod, + redirectURI: opts.redirectURI, + }), nil +} + type InitiateAppsOAuthDynamicRegistrationIATAuthOptions struct { RequestID string Origin string @@ -26,22 +343,1221 @@ type InitiateAppsOAuthDynamicRegistrationIATAuthOptions struct { RedirectURI string BackendDomain string Hostname string + AccountID int32 } func (s *Services) InitiateAppsOAuthDynamicRegistrationIATAuth( ctx context.Context, opts InitiateAppsOAuthDynamicRegistrationIATAuthOptions, ) (string, *exceptions.ServiceError) { + if serviceErr := validateAppsRegistrationTenant(opts.Hostname, opts.AccountID); serviceErr != nil { + return "", serviceErr + } logger := s.buildLogger( opts.RequestID, oauthDynamicRegistrationAppsLocation, "InitiateAppsOAuthDynamicRegistrationIATAuth", - ).With("hostname", opts.Hostname) + ).With( + "hostname", opts.Hostname, + "redirectUri", opts.RedirectURI, + ) logger.InfoContext(ctx, "Starting OAuth dynamic registration IAT authorization for Apps...") if serviceErr := validateOrigin(ctx, logger, opts.Origin, opts.Domain); serviceErr != nil { return "", serviceErr } - return "", nil + if opts.SessionKey == "" { + if opts.RefreshToken == "" { + logger.InfoContext(ctx, "No session key or refresh token provided, redirecting to login") + return s.oauthAppsDynamicRegistrationIATAuth(ctx, oauthAppsDynamicRegistrationIATAuthOptions{ + requestID: opts.RequestID, + challenge: opts.Challenge, + challengeMethod: opts.ChallengeMethod, + domain: opts.Domain, + redirectURI: opts.RedirectURI, + state: opts.State, + hostname: opts.Hostname, + accountID: opts.AccountID, + }) + } + + logger.InfoContext(ctx, "No session key provided, attempting to refresh with refresh token") + return s.refreshTokenAppsOAuthDynamicRegistrationIATLogin( + ctx, + refreshTokenAppsOAuthDynamicRegistrationIATLoginOptions{ + requestID: opts.RequestID, + refreshToken: opts.RefreshToken, + challenge: opts.Challenge, + challengeMethod: opts.ChallengeMethod, + domain: opts.Domain, + redirectURI: opts.RedirectURI, + state: opts.State, + backendDomain: opts.BackendDomain, + hostname: opts.Hostname, + accountID: opts.AccountID, + }, + ) + } + + data, credsClientID, verified, found, err := s.cache.VerifyAppsRegistrationSessionKey( + ctx, + cache.VerifyAppsRegistrationSessionKeyOptions{ + Hostname: opts.Hostname, + RequestID: opts.RequestID, + SessionKey: opts.SessionKey, + Domain: opts.Domain, + }, + ) + if err != nil { + logger.ErrorContext(ctx, "Failed to verify apps registration session key", "error", err) + return "", exceptions.NewInternalServerError() + } + if !found { + logger.WarnContext(ctx, "Apps registration session key not found") + return "", exceptions.NewUnauthorizedError() + } + + if !verified { + logger.WarnContext(ctx, "Apps registration session key is not verified") + return "", exceptions.NewUnauthorizedError() + } + + if err := s.cache.DeleteAppsRegistrationSessionKey( + ctx, + cache.DeleteAppsRegistrationSessionKeyOptions{ + Domain: opts.Domain, + Hostname: opts.Hostname, + RequestID: opts.RequestID, + ClientID: credsClientID, + }, + ); err != nil { + logger.ErrorContext(ctx, "Failed to delete apps registration session key", "error", err) + return "", exceptions.NewInternalServerError() + } + + accountDTO, serviceErr := s.GetAccountByPublicIDAndVersion(ctx, GetAccountByPublicIDAndVersionOptions{ + RequestID: opts.RequestID, + PublicID: data.AccountPublicID, + Version: data.AccountVersion, + }) + if serviceErr != nil { + logger.WarnContext(ctx, "Failed to get account by public ID and version", "serviceError", serviceErr) + return "", serviceErr + } + if accountDTO.ID() != opts.AccountID { + return "", exceptions.NewUnauthorizedError() + } + + hashedChallenge, serviceErr := hashChallenge(opts.Challenge, opts.ChallengeMethod) + if serviceErr != nil { + logger.ErrorContext(ctx, "Invalid code challenge", "serviceError", serviceErr) + return "", serviceErr + } + + logger.InfoContext(ctx, "Successfully verified apps registration IAT session key, creating code...") + cbURL, serviceErr := s.generateAppsOAuthDynamicRegistrationIATCallback( + ctx, + generateAppsOAuthDynamicRegistrationIATCallbackOptions{ + requestID: opts.RequestID, + clientID: credsClientID, + accountPublicID: accountDTO.PublicID, + accountVersion: accountDTO.Version(), + challenge: hashedChallenge, + domain: opts.Domain, + redirectURI: opts.RedirectURI, + state: opts.State, + hostname: opts.Hostname, + accountID: opts.AccountID, + }, + ) + if serviceErr != nil { + logger.ErrorContext(ctx, "Failed to generate OAuth dynamic registration IAT callback", "serviceErr", serviceErr) + return "", serviceErr + } + + return cbURL, nil +} + +type AppsOAuthDynamicRegistrationIATAuthRenderOptions struct { + RequestID string + ACCClientID string + State string + Domain string + CodeChallenge string + CodeChallengeMethod string + RedirectURI string + AccountID int32 + Hostname string +} + +func (s *Services) AppsOAuthDynamicRegistrationIATAuthRender( + ctx context.Context, + opts AppsOAuthDynamicRegistrationIATAuthRenderOptions, +) (string, *exceptions.ServiceError) { + if serviceErr := validateAppsRegistrationTenant(opts.Hostname, opts.AccountID); serviceErr != nil { + return "", serviceErr + } + logger := s.buildLogger( + opts.RequestID, + oauthDynamicRegistrationAppsLocation, + "AppsOAuthDynamicRegistrationIATAuthRender", + ).With( + "redirectUri", opts.RedirectURI, + ) + logger.InfoContext(ctx, "Starting OAuth dynamic registration IAT authorization html render for Apps...") + + data, found, err := s.cache.GetAppsDynamicRegistrationAuthIAT( + ctx, + cache.GetAppsDynamicRegistrationIATAuthOptions{ + Hostname: opts.Hostname, + RequestID: opts.RequestID, + ClientID: opts.ACCClientID, + }, + ) + if err != nil { + logger.ErrorContext(ctx, "Failed to get apps dynamic registration IAT", "error", err) + return "", exceptions.NewInternalServerError() + } + if !found { + logger.WarnContext(ctx, "Apps dynamic registration IAT not found") + return "", exceptions.NewForbiddenError() + } + if data.AccountID != opts.AccountID { + return "", exceptions.NewUnauthorizedError() + } + + if serviceErr := validateDynamicRegistrationRequest( + dynamicRegistrationRequest{domain: data.Domain, state: data.State, redirectURI: data.RedirectURI}, + dynamicRegistrationRequest{domain: opts.Domain, state: opts.State, redirectURI: opts.RedirectURI}, + ); serviceErr != nil { + return "", serviceErr + } + + csrfToken, err := s.cache.SaveAppsDynamicRegistrationIATLoginCSRF( + ctx, + cache.SaveAppsDynamicRegistrationIATLoginCSRFOptions{ + Hostname: opts.Hostname, + RequestID: opts.RequestID, + ClientID: opts.ACCClientID, + Domain: opts.Domain, + }, + ) + if err != nil { + logger.ErrorContext(ctx, "Failed to save apps dynamic registration IAT auth CSRF token", "error", err) + return "", exceptions.NewInternalServerError() + } + + loginHTML, err := templates.BuildAccountDynamicRegistrationIATAuthTemplate( + templates.AccountDynamicRegistrationIATAuthOptions{ + ACCClientID: opts.ACCClientID, + CSRFToken: csrfToken, + Domain: opts.Domain, + State: opts.State, + CodeChallenge: opts.CodeChallenge, + CodeChallengeMethod: opts.CodeChallengeMethod, + RedirectURI: opts.RedirectURI, + AppleEnabled: s.oauthProviders.IsAppleEnabled(), + FacebookEnabled: s.oauthProviders.IsFacebookEnabled(), + GitHubEnabled: s.oauthProviders.IsGitHubEnabled(), + GoogleEnabled: s.oauthProviders.IsGoogleEnabled(), + MicrosoftEnabled: s.oauthProviders.IsMicrosoftEnabled(), + }, + ) + if err != nil { + logger.ErrorContext(ctx, "Failed to build account dynamic registration IAT auth template", "error", err) + return "", exceptions.NewInternalServerError() + } + + return loginHTML, nil +} + +type AppsOAuthDynamicRegistrationIATAuthReRenderOptions struct { + RequestID string + Errors []string + CSRFToken string + ACCClientID string + State string + Domain string + CodeChallenge string + CodeChallengeMethod string + RedirectURI string + AccountID int32 + Hostname string +} + +func (s *Services) AppsOAuthDynamicRegistrationIATAuthReRender( + ctx context.Context, + opts AppsOAuthDynamicRegistrationIATAuthReRenderOptions, +) (string, *exceptions.ServiceError) { + if serviceErr := validateAppsRegistrationTenant(opts.Hostname, opts.AccountID); serviceErr != nil { + return "", serviceErr + } + logger := s.buildLogger( + opts.RequestID, + oauthDynamicRegistrationAppsLocation, + "AppsOAuthDynamicRegistrationIATAuthReRender", + ).With( + "clientId", opts.ACCClientID, + "redirectUri", opts.RedirectURI, + ) + logger.InfoContext(ctx, "Re-rendering OAuth dynamic registration IAT authorization html for Apps...") + + loginHTML, err := templates.BuildAccountDynamicRegistrationIATAuthTemplate( + templates.AccountDynamicRegistrationIATAuthOptions{ + Errors: opts.Errors, + ACCClientID: opts.ACCClientID, + CSRFToken: opts.CSRFToken, + Domain: opts.Domain, + State: opts.State, + CodeChallenge: opts.CodeChallenge, + CodeChallengeMethod: opts.CodeChallengeMethod, + RedirectURI: opts.RedirectURI, + AppleEnabled: s.oauthProviders.IsAppleEnabled(), + FacebookEnabled: s.oauthProviders.IsFacebookEnabled(), + GitHubEnabled: s.oauthProviders.IsGitHubEnabled(), + GoogleEnabled: s.oauthProviders.IsGoogleEnabled(), + MicrosoftEnabled: s.oauthProviders.IsMicrosoftEnabled(), + }, + ) + if err != nil { + logger.ErrorContext(ctx, "Failed to build account dynamic registration IAT auth template", "error", err) + return "", exceptions.NewInternalServerError() + } + + return loginHTML, nil +} + +type AppsOAuthDynamicRegistrationIATLoginOptions struct { + RequestID string + ACCClientID string + Domain string + CSRFToken string + CodeChallenge string + CodeChallengeMethod string + State string + RedirectURI string + Email string + Password string + BackendDomain string + Hostname string + AccountID int32 +} + +func (s *Services) AppsOAuthDynamicRegistrationIATLogin( + ctx context.Context, + opts AppsOAuthDynamicRegistrationIATLoginOptions, +) (string, string, bool, *exceptions.ServiceError) { + if serviceErr := validateAppsRegistrationTenant(opts.Hostname, opts.AccountID); serviceErr != nil { + return "", "", false, serviceErr + } + logger := s.buildLogger(opts.RequestID, oauthDynamicRegistrationAppsLocation, "AppsOAuthDynamicRegistrationIATLogin").With( + "clientId", opts.ACCClientID, + "domain", opts.Domain, + "hostname", opts.Hostname, + ) + logger.InfoContext(ctx, "Logging in with OAuth dynamic registration IAT for Apps...") + + validCSRF, err := s.cache.VerifyAppsDynamicRegistrationIATLoginCSRF( + ctx, + cache.VerifyAppsDynamicRegistrationIATLoginCSRFOptions{ + Hostname: opts.Hostname, + RequestID: opts.RequestID, + ClientID: opts.ACCClientID, + Domain: opts.Domain, + CSRFToken: opts.CSRFToken, + }, + ) + if err != nil { + logger.ErrorContext(ctx, "Failed to verify apps dynamic registration IAT auth CSRF token", "error", err) + return "", "", false, exceptions.NewInternalServerError() + } + if !validCSRF { + logger.WarnContext(ctx, "Invalid CSRF token") + return "", "", false, exceptions.NewForbiddenError() + } + + data, found, err := s.cache.GetAppsDynamicRegistrationAuthIAT(ctx, cache.GetAppsDynamicRegistrationIATAuthOptions{ + Hostname: opts.Hostname, + RequestID: opts.RequestID, + ClientID: opts.ACCClientID, + }) + if err != nil { + logger.ErrorContext(ctx, "Failed to get apps dynamic registration IAT", "error", err) + return "", "", false, exceptions.NewInternalServerError() + } + if !found { + logger.ErrorContext(ctx, "Apps dynamic registration IAT not found") + return "", "", false, exceptions.NewNotFoundError() + } + if data.AccountID != opts.AccountID { + return "", "", false, exceptions.NewUnauthorizedError() + } + + if serviceErr := validateDynamicRegistrationRequest( + dynamicRegistrationRequest{domain: data.Domain, state: data.State, redirectURI: data.RedirectURI}, + dynamicRegistrationRequest{domain: opts.Domain, state: opts.State, redirectURI: opts.RedirectURI}, + ); serviceErr != nil { + return "", "", false, serviceErr + } + + accountDTO, serviceErr := s.authenticateDynamicRegistrationAccount(ctx, logger, opts.RequestID, opts.Email, opts.Password) + if serviceErr != nil { + return "", "", false, serviceErr + } + if accountDTO.ID() != opts.AccountID { + return "", "", false, exceptions.NewUnauthorizedError() + } + + default2FAConfig, serviceErr := s.getDefaultAccount2FAConfigInternal(ctx, getDefaultAccount2FAConfigInternalOptions{ + requestID: opts.RequestID, + accountPublicID: accountDTO.PublicID, + }) + if serviceErr != nil { + logger.ErrorContext(ctx, "Failed to get default 2FA config", "serviceError", serviceErr) + return "", "", false, serviceErr + } + if default2FAConfig != nil { + logger.InfoContext(ctx, "Two-Factor is enabled, proceeding to 2FA step") + sessionID, err := s.cache.SaveAppsDynamicRegistrationIAT2FA( + ctx, + cache.SaveAppsDynamicRegistrationIAT2FAOptions{ + Hostname: opts.Hostname, + RequestID: opts.RequestID, + AccountPublicID: accountDTO.PublicID, + AccountVersion: accountDTO.Version(), + RedirectURI: opts.RedirectURI, + Domain: data.Domain, + ClientID: opts.ACCClientID, + State: data.State, + TwoFAType: string(default2FAConfig.TwoFactorType), + TwoFATTL: s.jwt.Get2FATTL(), + }, + ) + if err != nil { + logger.ErrorContext(ctx, "Failed to save apps dynamic registration IAT 2FA", "error", err) + return "", "", false, exceptions.NewInternalServerError() + } + + if default2FAConfig.TwoFactorType == database.TwoFactorTypeEmail { + code, err := s.cache.AddTwoFactorCode(ctx, cache.AddTwoFactorCodeOptions{ + RequestID: opts.RequestID, + AccountID: accountDTO.ID(), + TTL: s.jwt.Get2FATTL(), + }) + if err != nil { + logger.ErrorContext(ctx, "Failed to add two factor code", "error", err) + return "", "", false, exceptions.NewInternalServerError() + } + + if err := s.mail.Publish2FAEmail(ctx, mailer.TwoFactorEmailOptions{ + RequestID: opts.RequestID, + Email: accountDTO.Email, + Name: accountDTO.GivenName, + Code: code, + }); err != nil { + logger.ErrorContext(ctx, "Failed to send two factor code email", "error", err) + return "", "", false, exceptions.NewInternalServerError() + } + + logger.InfoContext(ctx, "Sent two factor code email successfully") + } + + if err := s.cache.DeleteAppsDynamicRegistrationIATAuth( + ctx, + cache.DeleteAppsDynamicRegistrationIATAuthOptions{ + Hostname: opts.Hostname, + RequestID: opts.RequestID, + ClientID: opts.ACCClientID, + }, + ); err != nil { + logger.ErrorContext(ctx, "Failed to delete apps dynamic registration IAT auth", "error", err) + return "", "", false, exceptions.NewInternalServerError() + } + + queryParams := make(url.Values) + queryParams.Add("response_type", "code") + queryParams.Add("client_id", data.Domain) + queryParams.Add("redirect_uri", opts.RedirectURI) + queryParams.Add("state", data.State) + queryParams.Add("code_challenge", opts.CodeChallenge) + if opts.CodeChallengeMethod != "" { + queryParams.Add("code_challenge_method", opts.CodeChallengeMethod) + } + return oauthDynamicRegistrationIATPath + "/" + opts.ACCClientID + + paths.AuthLogin + paths.Auth2FA + "?" + queryParams.Encode(), sessionID, false, nil + } + + if serviceErr := s.checkAppIATRegistrationDomain(ctx, opts.RequestID, accountDTO.PublicID, data.Domain); serviceErr != nil { + if serviceErr.Code != exceptions.CodeNotFound { + logger.ErrorContext(ctx, "Failed to get registration domain", "serviceError", serviceErr) + return "", "", false, serviceErr + } + + if err := s.cache.DeleteAppsDynamicRegistrationIATAuth( + ctx, + cache.DeleteAppsDynamicRegistrationIATAuthOptions{ + Hostname: opts.Hostname, + RequestID: opts.RequestID, + ClientID: opts.ACCClientID, + }, + ); err != nil { + logger.ErrorContext(ctx, "Failed to delete apps dynamic registration IAT auth", "error", err) + return "", "", false, exceptions.NewInternalServerError() + } + + logger.WarnContext(ctx, "Registration domain not found") + return "", "", false, exceptions.NewForbiddenError() + } + + sessionKey, err := s.cache.CreateAppsRegistrationSessionKey( + ctx, + cache.CreateAppsRegistrationSessionKeyOptions{ + Hostname: opts.Hostname, + RequestID: opts.RequestID, + ClientID: opts.ACCClientID, + Domain: opts.Domain, + AccountPublicID: accountDTO.PublicID, + AccountVersion: accountDTO.Version(), + }, + ) + if err != nil { + logger.ErrorContext(ctx, "Failed to create apps registration session", "error", err) + return "", "", false, exceptions.NewInternalServerError() + } + + return oauthDynamicRegistrationIATAuthPath, sessionKey, true, nil +} + +type AppsOAuthDynamicRegistrationIAT2FARenderOptions struct { + RequestID string + Domain string + ACCClientID string + SessionID string + Challenge string + ChallengeMethod string + State string + RedirectURI string + AccountID int32 + Hostname string +} + +func (s *Services) AppsOAuthDynamicRegistrationIAT2FARender( + ctx context.Context, + opts AppsOAuthDynamicRegistrationIAT2FARenderOptions, +) (string, *exceptions.ServiceError) { + if serviceErr := validateAppsRegistrationTenant(opts.Hostname, opts.AccountID); serviceErr != nil { + return "", serviceErr + } + logger := s.buildLogger(opts.RequestID, oauthDynamicRegistrationAppsLocation, "AppsOAuthDynamicRegistrationIAT2FARender").With( + "clientId", opts.ACCClientID, + ) + logger.InfoContext(ctx, "Handling OAuth dynamic registration IAT 2FA for Apps...") + + data, found, err := s.cache.GetAppsDynamicRegistrationIAT2FA(ctx, cache.GetAppsDynamicRegistrationIAT2FAOptions{ + Hostname: opts.Hostname, + RequestID: opts.RequestID, + SessionID: opts.SessionID, + }) + if err != nil { + logger.ErrorContext(ctx, "Failed to get apps dynamic registration IAT 2FA", "error", err) + return "", exceptions.NewInternalServerError() + } + if !found { + logger.WarnContext(ctx, "Failed to get apps dynamic registration IAT 2FA") + return "", exceptions.NewUnauthorizedError() + } + + if opts.ACCClientID != data.ClientID || opts.Domain != data.Domain { + logger.WarnContext(ctx, "Client IDs do not match", "sessionClientId", data.ClientID) + return "", exceptions.NewUnauthorizedError() + } + if serviceErr := validateDynamicRegistrationRequest( + dynamicRegistrationRequest{domain: data.Domain, state: data.State, redirectURI: data.RedirectURI}, + dynamicRegistrationRequest{domain: opts.Domain, state: opts.State, redirectURI: opts.RedirectURI}, + ); serviceErr != nil { + return "", serviceErr + } + + csrfToken, err := s.cache.SaveAppsDynamicRegistrationIAT2FACSRFToken( + ctx, + cache.SaveAppsDynamicRegistrationIAT2FACSRFTokenOptions{ + Hostname: opts.Hostname, + RequestID: opts.RequestID, + SessionID: opts.SessionID, + TwoFATTL: s.jwt.Get2FATTL(), + }, + ) + if err != nil { + logger.ErrorContext(ctx, "Failed to save apps dynamic registration IAT 2FA CSRF token", "error", err) + return "", exceptions.NewInternalServerError() + } + + twoFAhtml, err := templates.BuildAccountDynamicRegistrationIAT2FATemplate( + templates.AccountDynamicRegistrationIAT2FAOptions{ + ACCClientID: opts.ACCClientID, + Domain: opts.Domain, + SessionID: opts.SessionID, + CSRFToken: csrfToken, + State: data.State, + CodeChallenge: opts.Challenge, + CodeChallengeMethod: opts.ChallengeMethod, + RedirectURI: data.RedirectURI, + }, + ) + if err != nil { + logger.ErrorContext(ctx, "Failed to build account dynamic registration IAT 2FA template", "error", err) + return "", exceptions.NewInternalServerError() + } + + return twoFAhtml, nil +} + +type AppsOAuthDynamicRegistrationIAT2FAReRenderOptions struct { + RequestID string + Domain string + ACCClientID string + SessionID string + Errors []string + CSRFToken string + Challenge string + ChallengeMethod string + State string + RedirectURI string + AccountID int32 + Hostname string +} + +func (s *Services) AppsOAuthDynamicRegistrationIAT2FAReRender( + ctx context.Context, + opts AppsOAuthDynamicRegistrationIAT2FAReRenderOptions, +) (string, *exceptions.ServiceError) { + if serviceErr := validateAppsRegistrationTenant(opts.Hostname, opts.AccountID); serviceErr != nil { + return "", serviceErr + } + logger := s.buildLogger(opts.RequestID, oauthDynamicRegistrationAppsLocation, "AppsOAuthDynamicRegistrationIAT2FAReRender").With( + "clientId", opts.ACCClientID, + ) + logger.InfoContext(ctx, "Re-rendering OAuth dynamic registration IAT 2FA for Apps...") + + twoFAhtml, err := templates.BuildAccountDynamicRegistrationIAT2FATemplate( + templates.AccountDynamicRegistrationIAT2FAOptions{ + Errors: opts.Errors, + ACCClientID: opts.ACCClientID, + Domain: opts.Domain, + SessionID: opts.SessionID, + CSRFToken: opts.CSRFToken, + State: opts.State, + CodeChallenge: opts.Challenge, + CodeChallengeMethod: opts.ChallengeMethod, + RedirectURI: opts.RedirectURI, + }, + ) + if err != nil { + logger.ErrorContext(ctx, "Failed to build account dynamic registration IAT 2FA template", "error", err) + return "", exceptions.NewInternalServerError() + } + + return twoFAhtml, nil +} + +type AppsOAuthDynamicRegistrationIATVerify2FACodeOptions struct { + RequestID string + ACCClientID string + Domain string + SessionID string + CSRFToken string + Code string + BackendDomain string + Hostname string + AccountID int32 +} + +func (s *Services) AppsOAuthDynamicRegistrationIATVerify2FACode( + ctx context.Context, + opts AppsOAuthDynamicRegistrationIATVerify2FACodeOptions, +) (string, string, *exceptions.ServiceError) { + if serviceErr := validateAppsRegistrationTenant(opts.Hostname, opts.AccountID); serviceErr != nil { + return "", "", serviceErr + } + logger := s.buildLogger(opts.RequestID, oauthDynamicRegistrationAppsLocation, "AppsOAuthDynamicRegistrationIATVerify2FACode").With( + "clientId", opts.ACCClientID, + "sessionId", opts.SessionID, + "hostname", opts.Hostname, + ) + logger.InfoContext(ctx, "Verifying OAuth dynamic registration IAT 2FA for Apps...") + + verifiedCSRF, err := s.cache.VerifyAppsDynamicRegistrationIAT2FACSRFToken( + ctx, + cache.VerifyAppsDynamicRegistrationIAT2FACSRFTokenOptions{ + Hostname: opts.Hostname, + RequestID: opts.RequestID, + SessionID: opts.SessionID, + CSRFToken: opts.CSRFToken, + }, + ) + if err != nil { + logger.ErrorContext(ctx, "Failed to verify apps dynamic registration IAT 2FA CSRF token", "error", err) + return "", "", exceptions.NewInternalServerError() + } + if !verifiedCSRF { + logger.WarnContext(ctx, "Invalid CSRF token") + return "", "", exceptions.NewForbiddenError() + } + + data, found, err := s.cache.GetAppsDynamicRegistrationIAT2FA(ctx, cache.GetAppsDynamicRegistrationIAT2FAOptions{ + Hostname: opts.Hostname, + RequestID: opts.RequestID, + SessionID: opts.SessionID, + }) + if err != nil { + logger.ErrorContext(ctx, "Failed to get apps dynamic registration IAT 2FA", "error", err) + return "", "", exceptions.NewInternalServerError() + } + if !found { + logger.WarnContext(ctx, "Failed to get apps dynamic registration IAT 2FA") + return "", "", exceptions.NewUnauthorizedError() + } + if opts.ACCClientID != data.ClientID || opts.Domain != data.Domain { + logger.WarnContext(ctx, "Client IDs do not match", "sessionClientId", data.ClientID) + return "", "", exceptions.NewUnauthorizedError() + } + twoFAType, serviceErr := map2FATypeTokens(data.TwoFAType) + if serviceErr != nil { + logger.WarnContext(ctx, "Failed to map two factor type", "serviceError", serviceErr) + return "", "", serviceErr + } + + accountDTO, serviceErr := s.GetAccountByPublicIDAndVersion(ctx, GetAccountByPublicIDAndVersionOptions{ + RequestID: opts.RequestID, + PublicID: data.AccountPublicID, + Version: data.AccountVersion, + }) + if serviceErr != nil { + logger.WarnContext(ctx, "Failed to get account by public ID and version", "serviceError", serviceErr) + return "", "", serviceErr + } + if accountDTO.ID() != opts.AccountID { + return "", "", exceptions.NewUnauthorizedError() + } + if serviceErr := s.verifyAccount2FAInternal(ctx, verifyAccount2FAInternalOptions{ + requestID: opts.RequestID, + accountID: accountDTO.ID(), + accountPublicID: accountDTO.PublicID, + accountVersion: accountDTO.Version(), + twoFAType: twoFAType, + code: opts.Code, + }); serviceErr != nil { + logger.WarnContext(ctx, "Failed to verify account two factor", "serviceError", serviceErr) + return "", "", serviceErr + } + + if serviceErr := s.checkAppIATRegistrationDomain(ctx, opts.RequestID, accountDTO.PublicID, data.Domain); serviceErr != nil { + if serviceErr.Code != exceptions.CodeNotFound { + logger.ErrorContext(ctx, "Failed to get registration domain", "serviceError", serviceErr) + return "", "", serviceErr + } + + if err := s.cache.DeleteAppsDynamicRegistrationIATAuth( + ctx, + cache.DeleteAppsDynamicRegistrationIATAuthOptions{ + Hostname: opts.Hostname, + RequestID: opts.RequestID, + ClientID: opts.ACCClientID, + }, + ); err != nil { + logger.ErrorContext(ctx, "Failed to delete apps dynamic registration IAT auth", "error", err) + return "", "", exceptions.NewInternalServerError() + } + + logger.WarnContext(ctx, "Registration domain not found") + return "", "", exceptions.NewForbiddenError() + } + + sessionKey, err := s.cache.CreateAppsRegistrationSessionKey( + ctx, + cache.CreateAppsRegistrationSessionKeyOptions{ + Hostname: opts.Hostname, + RequestID: opts.RequestID, + ClientID: opts.ACCClientID, + Domain: opts.Domain, + AccountPublicID: accountDTO.PublicID, + AccountVersion: accountDTO.Version(), + }, + ) + if err != nil { + logger.ErrorContext(ctx, "Failed to create apps registration session", "error", err) + return "", "", exceptions.NewInternalServerError() + } + + return oauthDynamicRegistrationIATAuthPath, sessionKey, nil +} + +type VerifyAppsOAuthDynamicRegistrationIATCodeOptions struct { + BackendDomain string + RequestID string + Code string + CodeVerifier string + Domain string + AccountID int32 + Hostname string +} + +func (s *Services) VerifyAppsOAuthDynamicRegistrationIATCode( + ctx context.Context, + opts VerifyAppsOAuthDynamicRegistrationIATCodeOptions, +) (dtos.AuthDTO, *exceptions.ServiceError) { + if serviceErr := validateAppsRegistrationTenant(opts.Hostname, opts.AccountID); serviceErr != nil { + return dtos.AuthDTO{}, serviceErr + } + logger := s.buildLogger(opts.RequestID, appDynamicRegistrationIATLocation, "VerifyAppsOAuthDynamicRegistrationIATCode") + logger.InfoContext(ctx, "Verifying app credentials registration IAT code...") + + data, found, err := s.cache.VerifyAppsRegistrationIATCode(ctx, cache.VerifyAppsRegistrationIATCodeOptions{ + Hostname: opts.Hostname, + RequestID: opts.RequestID, + Code: opts.Code, + }) + if err != nil { + logger.ErrorContext(ctx, "Failed to verify app credentials registration IAT code", "error", err) + return dtos.AuthDTO{}, exceptions.NewInternalServerError() + } + if !found { + logger.DebugContext(ctx, "App credentials registration IAT code not found or invalid") + return dtos.AuthDTO{}, exceptions.NewUnauthorizedError() + } + + if data.Domain != opts.Domain { + logger.WarnContext(ctx, "OAuth Domain does not match", "dataDomain", data.Domain) + return dtos.AuthDTO{}, exceptions.NewUnauthorizedError() + } + + ok, err := utils.CompareShaBase64(opts.CodeVerifier, data.Challenge) + if err != nil { + logger.ErrorContext(ctx, "Failed to compare challenge", "error", err) + return dtos.AuthDTO{}, exceptions.NewInternalServerError() + } + if !ok { + logger.WarnContext(ctx, "OAuth Code challenge verification failed") + return dtos.AuthDTO{}, exceptions.NewUnauthorizedError() + } + + accountDTO, serviceErr := s.GetAccountByPublicIDAndVersion(ctx, GetAccountByPublicIDAndVersionOptions{ + RequestID: opts.RequestID, + PublicID: data.AccountPublicID, + Version: data.AccountVersion, + }) + if serviceErr != nil { + logger.ErrorContext(ctx, "Failed to get account", "serviceError", serviceErr) + return dtos.AuthDTO{}, serviceErr + } + if accountDTO.ID() != opts.AccountID { + return dtos.AuthDTO{}, exceptions.NewUnauthorizedError() + } + + if opts.BackendDomain == "" || opts.Hostname != dynamicRegistrationIssuerDomain(accountDTO.Username, opts.BackendDomain) { + return dtos.AuthDTO{}, exceptions.NewUnauthorizedError() + } + if serviceErr := s.checkAppIATRegistrationDomain(ctx, opts.RequestID, accountDTO.PublicID, data.Domain); serviceErr != nil { + return dtos.AuthDTO{}, serviceErr + } + + authDTO, serviceErr := s.CreateAppCredentialsRegistrationIAT(ctx, CreateAppCredentialsRegistrationIATOptions{ + RequestID: opts.RequestID, + AccountPublicID: accountDTO.PublicID, + AccountVersion: accountDTO.Version(), + Domain: data.Domain, + BackendDomain: opts.BackendDomain, + }) + if serviceErr != nil { + return dtos.AuthDTO{}, serviceErr + } + return authDTO, nil +} + +type AppsOAuthDynamicRegistrationIATExtGetOptions struct { + RequestID string + ACCClientID string + Domain string + Provider string + CallbackURL string + RedirectURI string + State string + BackendDomain string + Hostname string + AccountID int32 +} + +func (s *Services) AppsOAuthDynamicRegistrationIATExtGet( + ctx context.Context, + opts AppsOAuthDynamicRegistrationIATExtGetOptions, +) (string, *exceptions.ServiceError) { + if serviceErr := validateAppsRegistrationTenant(opts.Hostname, opts.AccountID); serviceErr != nil { + return "", serviceErr + } + logger := s.buildLogger(opts.RequestID, oauthLocation, "AppsOAuthDynamicRegistrationIATExtGet").With( + "Provider", opts.Provider, + "hostname", opts.Hostname, + ) + logger.InfoContext(ctx, "External logging in account for Apps...") + + authUrlOpts := oauth.AuthorizationURLOptions{ + RequestID: opts.RequestID, + Scopes: make([]oauth.Scope, 0), + RedirectURL: opts.CallbackURL, + } + var oauthUrl, state string + var serviceErr *exceptions.ServiceError + switch opts.Provider { + case AuthProviderApple: + oauthUrl, state, serviceErr = s.oauthProviders.GetAppleAuthorizationURL(ctx, authUrlOpts) + case AuthProviderFacebook: + oauthUrl, state, serviceErr = s.oauthProviders.GetFacebookAuthorizationURL(ctx, authUrlOpts) + case AuthProviderGitHub: + oauthUrl, state, serviceErr = s.oauthProviders.GetGithubAuthorizationURL(ctx, authUrlOpts) + case AuthProviderGoogle: + oauthUrl, state, serviceErr = s.oauthProviders.GetGoogleAuthorizationURL(ctx, authUrlOpts) + case AuthProviderMicrosoft: + oauthUrl, state, serviceErr = s.oauthProviders.GetMicrosoftAuthorizationURL(ctx, authUrlOpts) + default: + logger.ErrorContext(ctx, "Provider must be 'apple', 'facebook', 'github', 'google' and 'microsoft'") + return "", exceptions.NewInternalServerError() + } + if serviceErr != nil { + logger.ErrorContext(ctx, "Failed to get authorization url or State", "error", serviceErr) + return "", serviceErr + } + + data, found, err := s.cache.GetAppsDynamicRegistrationAuthIAT(ctx, cache.GetAppsDynamicRegistrationIATAuthOptions{ + Hostname: opts.Hostname, + RequestID: opts.RequestID, + ClientID: opts.ACCClientID, + }) + if err != nil { + logger.ErrorContext(ctx, "Failed to get apps dynamic registration IAT", "error", err) + return "", exceptions.NewInternalServerError() + } + if !found { + logger.ErrorContext(ctx, "Apps dynamic registration IAT not found") + return "", exceptions.NewNotFoundError() + } + if data.AccountID != opts.AccountID { + return "", exceptions.NewUnauthorizedError() + } + + if serviceErr := validateDynamicRegistrationRequest( + dynamicRegistrationRequest{domain: data.Domain, state: data.State, redirectURI: data.RedirectURI}, + dynamicRegistrationRequest{domain: opts.Domain, state: opts.State, redirectURI: opts.RedirectURI}, + ); serviceErr != nil { + return "", serviceErr + } + + if err := s.cache.SaveAppsDynamicRegistrationIATExtAuth(ctx, cache.SaveAppsDynamicRegistrationIATExtAuthOptions{ + Hostname: opts.Hostname, + RequestID: opts.RequestID, + ClientID: opts.ACCClientID, + Domain: opts.Domain, + Provider: opts.Provider, + State: state, + RequestState: opts.State, + }); err != nil { + logger.ErrorContext(ctx, "Failed to save apps dynamic registration IAT external auth", "error", err) + return "", exceptions.NewInternalServerError() + } + + logger.InfoContext(ctx, "Saved apps dynamic registration IAT external auth successfully") + return oauthUrl, nil +} + +type AppsOAuthDynamicRegistrationIATExtCBOptions struct { + RequestID string + ACCClientID string + Provider string + State string + Code string + RedirectURL string + BackendDomain string + Hostname string + AccountID int32 +} + +func (s *Services) AppsOAuthDynamicRegistrationIATExtCB( + ctx context.Context, + opts AppsOAuthDynamicRegistrationIATExtCBOptions, +) (string, *exceptions.ServiceError) { + if serviceErr := validateAppsRegistrationTenant(opts.Hostname, opts.AccountID); serviceErr != nil { + return "", serviceErr + } + logger := s.buildLogger(opts.RequestID, oauthLocation, "AppsOAuthDynamicRegistrationIATExtCB").With( + "hostname", opts.Hostname, + ) + logger.InfoContext(ctx, "External callback for account for Apps...") + + data, found, err := s.cache.GetAppsDynamicRegistrationIATExtAuth(ctx, cache.GetAppsDynamicRegistrationIATExtAuthOptions{ + Hostname: opts.Hostname, + RequestID: opts.RequestID, + Provider: opts.Provider, + State: opts.State, + }) + if err != nil { + logger.ErrorContext(ctx, "Failed to get apps dynamic registration IAT external auth", "error", err) + return "", exceptions.NewInternalServerError() + } + if !found { + logger.ErrorContext(ctx, "Apps dynamic registration IAT external auth not found") + return "", exceptions.NewNotFoundError() + } + + if data.ClientID != opts.ACCClientID { + logger.WarnContext(ctx, "Client IDs do not match", "dataClientId", data.ClientID) + return "", exceptions.NewUnauthorizedError() + } + + accessTokenOpts := oauth.AccessTokenOptions{ + RequestID: opts.RequestID, + Code: opts.Code, + Scopes: oauthScopes, + RedirectURL: opts.RedirectURL, + } + var token string + var serviceErr *exceptions.ServiceError + switch opts.Provider { + case AuthProviderFacebook: + token, serviceErr = s.oauthProviders.GetFacebookAccessToken(ctx, accessTokenOpts) + case AuthProviderGitHub: + token, serviceErr = s.oauthProviders.GetGithubAccessToken(ctx, accessTokenOpts) + case AuthProviderGoogle: + token, serviceErr = s.oauthProviders.GetGoogleAccessToken(ctx, accessTokenOpts) + case AuthProviderMicrosoft: + token, serviceErr = s.oauthProviders.GetMicrosoftAccessToken(ctx, accessTokenOpts) + default: + logger.ErrorContext(ctx, "Provider must be 'facebook', 'github', 'google' and 'microsoft'") + return "", exceptions.NewInternalServerError() + } + if serviceErr != nil { + logger.WarnContext(ctx, "Failed to get oauth access token", "error", serviceErr) + return "", serviceErr + } + + authData, found, err := s.cache.GetAppsDynamicRegistrationAuthIAT(ctx, cache.GetAppsDynamicRegistrationIATAuthOptions{ + Hostname: opts.Hostname, + RequestID: opts.RequestID, + ClientID: opts.ACCClientID, + }) + if err != nil { + logger.ErrorContext(ctx, "Failed to get apps dynamic registration IAT", "error", err) + return "", exceptions.NewInternalServerError() + } + if !found { + logger.ErrorContext(ctx, "Apps dynamic registration IAT not found") + return "", exceptions.NewNotFoundError() + } + if authData.AccountID != opts.AccountID { + return "", exceptions.NewUnauthorizedError() + } + + if authData.Domain != data.Domain { + logger.WarnContext(ctx, "OAuth Domain does not match", "dataDomain", authData.Domain) + return "", exceptions.NewUnauthorizedError() + } + if authData.State != data.RequestState { + logger.WarnContext(ctx, "OAuth State does not match") + return "", exceptions.NewUnauthorizedError() + } + + userData, serviceErr := s.extOAuthUser(ctx, logger, extOAuthUserOptions{ + requestID: opts.RequestID, + provider: opts.Provider, + token: token, + }) + if serviceErr != nil { + return "", serviceErr + } + + accountDTO, serviceErr := s.GetAccountByEmail(ctx, GetAccountByEmailOptions{ + RequestID: opts.RequestID, + Email: userData.Email, + }) + if serviceErr != nil { + return "", serviceErr + } + if accountDTO.ID() != opts.AccountID { + return "", exceptions.NewUnauthorizedError() + } + if _, serviceErr := s.GetAccountAuthProvider(ctx, GetAccountAuthProviderOptions{ + RequestID: opts.RequestID, + PublicID: accountDTO.PublicID, + Provider: opts.Provider, + }); serviceErr != nil { + if serviceErr.Code != exceptions.CodeNotFound { + logger.ErrorContext(ctx, "Failed to get account auth provider", "serviceError", serviceErr) + return "", serviceErr + } + + logger.WarnContext(ctx, "Account auth provider not found", "serviceError", serviceErr) + return "", exceptions.NewUnauthorizedError() + } + + cbURL, serviceErr := s.generateAppsOAuthDynamicRegistrationIATCallback( + ctx, + generateAppsOAuthDynamicRegistrationIATCallbackOptions{ + requestID: opts.RequestID, + clientID: opts.ACCClientID, + accountPublicID: accountDTO.PublicID, + accountVersion: accountDTO.Version(), + challenge: authData.Challenge, + domain: authData.Domain, + redirectURI: authData.RedirectURI, + state: authData.State, + hostname: opts.Hostname, + accountID: opts.AccountID, + }, + ) + if serviceErr != nil { + logger.ErrorContext(ctx, "Failed to generate OAuth dynamic registration IAT callback", "serviceError", serviceErr) + return "", serviceErr + } + + return cbURL, nil +} + +type AppsOAuthDynamicRegistrationIATExtAppleCBOptions struct { + RequestID string + ACCClientID string + Email string + Code string + State string + RedirectURL string + BackendDomain string + Hostname string + AccountID int32 +} + +func (s *Services) AppsOAuthDynamicRegistrationIATExtAppleCB( + ctx context.Context, + opts AppsOAuthDynamicRegistrationIATExtAppleCBOptions, +) (string, *exceptions.ServiceError) { + if serviceErr := validateAppsRegistrationTenant(opts.Hostname, opts.AccountID); serviceErr != nil { + return "", serviceErr + } + logger := s.buildLogger(opts.RequestID, oauthLocation, "AppsOAuthDynamicRegistrationIATExtAppleCB").With( + "hostname", opts.Hostname, + ) + logger.InfoContext(ctx, "External callback for account for Apps...") + + data, found, err := s.cache.GetAppsDynamicRegistrationIATExtAuth(ctx, cache.GetAppsDynamicRegistrationIATExtAuthOptions{ + Hostname: opts.Hostname, + RequestID: opts.RequestID, + Provider: AuthProviderApple, + State: opts.State, + }) + if err != nil { + logger.ErrorContext(ctx, "Failed to get apps dynamic registration IAT external auth", "error", err) + return "", exceptions.NewInternalServerError() + } + if !found { + logger.ErrorContext(ctx, "Apps dynamic registration IAT external auth not found") + return "", exceptions.NewNotFoundError() + } + + if data.ClientID != opts.ACCClientID { + logger.WarnContext(ctx, "Client IDs do not match", "dataClientId", data.ClientID) + return "", exceptions.NewUnauthorizedError() + } + + idToken, serviceErr := s.oauthProviders.GetAppleIDToken(ctx, oauth.AccessTokenOptions{ + RequestID: opts.RequestID, + Code: opts.Code, + Scopes: oauthScopes, + }) + if serviceErr != nil { + logger.WarnContext(ctx, "Failed to get apple AccountID token", "error", serviceErr) + return "", serviceErr + } + + authData, found, err := s.cache.GetAppsDynamicRegistrationAuthIAT(ctx, cache.GetAppsDynamicRegistrationIATAuthOptions{ + Hostname: opts.Hostname, + RequestID: opts.RequestID, + ClientID: opts.ACCClientID, + }) + if err != nil { + logger.ErrorContext(ctx, "Failed to get apps dynamic registration IAT", "error", err) + return "", exceptions.NewInternalServerError() + } + if !found { + logger.ErrorContext(ctx, "Apps dynamic registration IAT not found") + return "", exceptions.NewNotFoundError() + } + if authData.AccountID != opts.AccountID { + return "", exceptions.NewUnauthorizedError() + } + + if authData.Domain != data.Domain { + logger.WarnContext(ctx, "OAuth Domain does not match", "dataDomain", authData.Domain) + return "", exceptions.NewUnauthorizedError() + } + if authData.State != data.RequestState { + logger.WarnContext(ctx, "OAuth State does not match") + return "", exceptions.NewUnauthorizedError() + } + + ok, serviceErr := s.oauthProviders.ValidateAppleIDToken(ctx, oauth.ValidateAppleIDTokenOptions{ + RequestID: opts.RequestID, + Token: idToken, + Email: opts.Email, + }) + if serviceErr != nil { + logger.ErrorContext(ctx, "Failed to validate apple AccountID token", "error", serviceErr) + return "", serviceErr + } + if !ok { + logger.WarnContext(ctx, "Apple account is not verified") + return "", exceptions.NewUnauthorizedError() + } + + accountDTO, serviceErr := s.GetAccountByEmail(ctx, GetAccountByEmailOptions{ + RequestID: opts.RequestID, + Email: opts.Email, + }) + if serviceErr != nil { + return "", serviceErr + } + if accountDTO.ID() != opts.AccountID { + return "", exceptions.NewUnauthorizedError() + } + if _, serviceErr := s.GetAccountAuthProvider(ctx, GetAccountAuthProviderOptions{ + RequestID: opts.RequestID, + PublicID: accountDTO.PublicID, + Provider: AuthProviderApple, + }); serviceErr != nil { + if serviceErr.Code != exceptions.CodeNotFound { + logger.ErrorContext(ctx, "Failed to get account auth provider", "serviceError", serviceErr) + return "", serviceErr + } + + logger.WarnContext(ctx, "Account auth provider not found", "serviceError", serviceErr) + return "", exceptions.NewUnauthorizedError() + } + + cbURL, serviceErr := s.generateAppsOAuthDynamicRegistrationIATCallback( + ctx, + generateAppsOAuthDynamicRegistrationIATCallbackOptions{ + requestID: opts.RequestID, + clientID: opts.ACCClientID, + accountPublicID: accountDTO.PublicID, + accountVersion: accountDTO.Version(), + challenge: authData.Challenge, + domain: authData.Domain, + redirectURI: authData.RedirectURI, + state: authData.State, + hostname: opts.Hostname, + accountID: opts.AccountID, + }, + ) + if serviceErr != nil { + logger.ErrorContext(ctx, "Failed to generate OAuth dynamic registration IAT callback", "serviceError", serviceErr) + return "", serviceErr + } + + return cbURL, nil } diff --git a/idp/internal/services/oauth_dynamic_registration_test.go b/idp/internal/services/oauth_dynamic_registration_test.go new file mode 100644 index 0000000..3969947 --- /dev/null +++ b/idp/internal/services/oauth_dynamic_registration_test.go @@ -0,0 +1,82 @@ +package services + +import ( + "context" + "net/url" + "reflect" + "strings" + "testing" + + "github.com/tugascript/devlogs/idp/internal/exceptions" +) + +func TestAppsRegistrationRequiresTenantContext(t *testing.T) { + // All entry points must fail before touching a database, cache, or provider + // when host middleware has not supplied a complete tenant context. + s := reflect.ValueOf(&Services{}) + for i := 0; i < s.NumMethod(); i++ { + name := s.Type().Method(i).Name + if !strings.Contains(name, "AppsOAuthDynamicRegistration") { + continue + } + method := s.Method(i) + for _, missing := range []string{"Hostname", "AccountID"} { + t.Run(name+"/missing"+missing, func(t *testing.T) { + opts := reflect.New(method.Type().In(1)).Elem() + if missing != "Hostname" { + opts.FieldByName("Hostname").SetString("tenant.example.com") + } + if missing != "AccountID" { + opts.FieldByName("AccountID").SetInt(42) + } + result := method.Call([]reflect.Value{reflect.ValueOf(context.Background()), opts}) + serviceErr, ok := result[len(result)-1].Interface().(*exceptions.ServiceError) + if !ok || serviceErr == nil || serviceErr.Code != exceptions.CodeUnauthorized { + t.Fatalf("expected unauthorized, got %v", serviceErr) + } + }) + } + } +} + +func TestDynamicRegistrationRequestBinding(t *testing.T) { + saved := dynamicRegistrationRequest{domain: "client.example", state: "state", redirectURI: "https://client.example/callback"} + for _, tc := range []struct { + name string + request dynamicRegistrationRequest + valid bool + }{ + {"match", saved, true}, + {"domain", dynamicRegistrationRequest{domain: "other.example", state: saved.state, redirectURI: saved.redirectURI}, false}, + {"state", dynamicRegistrationRequest{domain: saved.domain, state: "other", redirectURI: saved.redirectURI}, false}, + {"redirect", dynamicRegistrationRequest{domain: saved.domain, state: saved.state, redirectURI: "https://other.example/callback"}, false}, + } { + t.Run(tc.name, func(t *testing.T) { + err := validateDynamicRegistrationRequest(saved, tc.request) + if (err == nil) != tc.valid { + t.Fatalf("unexpected validation result: %v", err) + } + }) + } +} + +func TestDynamicRegistrationURLs(t *testing.T) { + login, err := url.Parse(buildOAuthDynamicRegistrationIATLoginURL(buildOAuthDynamicRegistrationIATLoginURLOptions{ + accClientID: "client", domain: "client.example", state: "state & value", challenge: "challenge", challengeMethod: "S256", redirectURI: "https://client.example/callback", + })) + if err != nil { + t.Fatal(err) + } + if login.Query().Get("state") != "state & value" || login.Query().Get("code_challenge_method") != "S256" || !strings.Contains(login.Path, "/client/") { + t.Fatalf("invalid login URL: %s", login) + } + callback, err := url.Parse(buildOAuthDynamicRegistrationIATCallbackURL(buildOAuthDynamicRegistrationIATCallbackURLOptions{ + redirectURI: "https://client.example/callback", code: "code", state: "state & value", issuerDomain: "tenant.example.com", + })) + if err != nil { + t.Fatal(err) + } + if callback.Query().Get("iss") != "https://tenant.example.com" || callback.Query().Get("state") != "state & value" { + t.Fatalf("invalid callback URL: %s", callback) + } +} diff --git a/idp/internal/services/templates/account_dynamic_registration.go b/idp/internal/services/templates/account_dynamic_registration.go index 0eab5dc..0bdd9ee 100644 --- a/idp/internal/services/templates/account_dynamic_registration.go +++ b/idp/internal/services/templates/account_dynamic_registration.go @@ -448,8 +448,8 @@ func BuildAccountDynamicRegistrationIATAuthTemplate(opts AccountDynamicRegistrat baseTemplateBody += buildFormErrors(opts.Errors) } - baseURL := paths.V1 + paths.AccountsBase + paths.CredentialsBase + paths.DynamicRegistrationBase + - paths.InitialAccessToken + "/" + opts.ACCClientID + paths.OAuthAuth + baseURL := paths.V1 + paths.AuthBase + paths.OAuthBase + + paths.InitialAccessToken + "/" + opts.ACCClientID data := accountDynamicRegistrationLoginTemplateData{ Title: baseAccountLoginTitle, Header: "OAuth Dynamic Client Registration Initial Access Token Login", @@ -715,8 +715,8 @@ func BuildAccountDynamicRegistrationIAT2FATemplate(opts AccountDynamicRegistrati } data := accountDynamicRegistrationIAT2FAData{ - TwoFAURL: paths.V1 + paths.AccountsBase + paths.CredentialsBase + paths.DynamicRegistrationBase + - paths.InitialAccessToken + "/" + opts.ACCClientID + paths.OAuthAuth + paths.AuthLogin + paths.Auth2FA, + TwoFAURL: paths.V1 + paths.AuthBase + paths.OAuthBase + + paths.InitialAccessToken + "/" + opts.ACCClientID + paths.AuthLogin + paths.Auth2FA, ClientID: opts.Domain, RedirectURI: opts.RedirectURI, CodeChallenge: opts.CodeChallenge, From 4990fb75b1a23de55a41e6286b899b28e78ff19d Mon Sep 17 00:00:00 2001 From: Afonso Barracha Date: Mon, 21 Sep 2026 21:58:41 +1200 Subject: [PATCH 11/16] refactor: make registration matadata more readable --- .../services/registration_metadata.go | 137 +++++++++++++----- 1 file changed, 104 insertions(+), 33 deletions(-) diff --git a/idp/internal/services/registration_metadata.go b/idp/internal/services/registration_metadata.go index 6a67c46..634c12c 100644 --- a/idp/internal/services/registration_metadata.go +++ b/idp/internal/services/registration_metadata.go @@ -1,3 +1,9 @@ +// 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 services import ( @@ -19,25 +25,32 @@ import ( ) type prepareDynamicRegistrationOptions struct { - requestID string - accountID int32 - accountPublicID uuid.UUID - data ApplicationRegistrationData - softwareStatement, backendDomain, frontendDomain string - app bool + requestID string + accountID int32 + accountPublicID uuid.UUID + data ApplicationRegistrationData + softwareStatement string + backendDomain string + 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) { +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 } + // Preserve explicit empty arrays omitted by the claims JSON tags. if body.ResponseTypes != nil { fields["response_types"], _ = json.Marshal(body.ResponseTypes) } @@ -51,40 +64,58 @@ func mergeRegistrationMetadata(body ApplicationRegistrationData, statement token 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) { +func (s *Services) prepareDynamicRegistration( + ctx context.Context, + opts prepareDynamicRegistrationOptions, +) (ApplicationRegistrationData, *exceptions.ServiceError) { data := opts.data - var methods []database.SoftwareStatementVerificationMethod + + var verificationMethods []database.SoftwareStatementVerificationMethod allowedScopes := allowedAccountCredentialsScopes if opts.app { - account, err := s.GetAccountByID(ctx, GetAccountByIDOptions{RequestID: opts.requestID, ID: opts.accountID}) + 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}) + config, 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) }) + verificationMethods = config.SoftwareStatementVerificationMethods + if len(config.DefaultAllowedScopes) > 0 { + allowedScopes = utils.MapSlice(config.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}) + config, err := s.GetAndCacheAccountDynamicRegistrationConfig(ctx, GetAndCacheAccountDynamicRegistrationConfigOptions{ + RequestID: opts.requestID, + AccountPublicID: opts.accountPublicID, + }) if err != nil { return data, err } - methods = cfg.SoftwareStatementVerificationMethods + verificationMethods = config.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. @@ -101,15 +132,21 @@ func (s *Services) prepareDynamicRegistration(ctx context.Context, opts prepareD jwksURI = claimJWKS } domain := registrationDomain(keyURI, data.RedirectURIs) - base, err := publicsuffix.EffectiveTLDPlusOne(domain) + baseDomain, 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, + 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, + requestID: opts.requestID, + accountPublicID: opts.accountPublicID, + verificationMethods: verificationMethods, + jwksURI: jwksURI, + jwks: data.JWKs, + domain: domain, + baseDomain: baseDomain, }), }) if err != nil { @@ -119,12 +156,19 @@ func (s *Services) prepareDynamicRegistration(ctx context.Context, opts prepareD 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, + requestID: opts.requestID, + domain: domain, + baseDomain: baseDomain, + 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), + requestID: opts.requestID, + claims: &claims, + allowedScopes: utils.SliceToHashSet(allowedScopes), }); serviceErr != nil { return data, exceptions.NewInvalidTokenError("invalid software statement") } @@ -133,6 +177,7 @@ func (s *Services) prepareDynamicRegistration(ctx context.Context, opts prepareD return data, exceptions.NewInvalidTokenError("invalid software statement metadata") } } + if data.ApplicationType == "" { if opts.app { data.ApplicationType = "web" @@ -142,9 +187,11 @@ func (s *Services) prepareDynamicRegistration(ctx context.Context, opts prepareD data.ApplicationType = "native" } } + if data.ClientName == "" { data.ClientName = "Client " + utils.Base62UUID() } + if data.ClientURI == "" { domain := registrationDomain("", data.RedirectURIs) if domain == "" { @@ -152,15 +199,17 @@ func (s *Services) prepareDynamicRegistration(ctx context.Context, opts prepareD } data.ClientURI = "https://" + domain } + if data.Scope == "" && !opts.app { data.Scope = "profile" } - if err := normalizeRegistrationMetadata(&data); err != nil { - return data, err + if serviceErr := normalizeRegistrationMetadata(&data); serviceErr != nil { + return data, serviceErr } if err := s.validate.StructCtx(ctx, &data); err != nil { return data, exceptions.NewValidationError("invalid client metadata") } + return data, nil } @@ -180,36 +229,57 @@ func normalizeRegistrationMetadata(data *ApplicationRegistrationData) *exception 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") - } + hasCodeGrant := slices.Contains(data.GrantTypes, "authorization_code") + hasCodeResponse := slices.ContainsFunc(data.ResponseTypes, func(response string) bool { + return slices.Contains(strings.Fields(response), "code") + }) + if hasCodeResponse && !hasCodeGrant { + 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") }) { + if hasCodeGrant && !hasCodeResponse { return exceptions.NewValidationError("authorization_code requires a code response") } - if codeGrant && len(data.RedirectURIs) == 0 { + if hasCodeGrant && len(data.RedirectURIs) == 0 { return exceptions.NewError(exceptions.OAuthErrorInvalidRedirectURI, "redirect_uris is required for authorization_code") } - for _, raw := range data.RedirectURIs { + + if serviceErr := validateRegistrationRedirectURIs(data.RedirectURIs); serviceErr != nil { + return serviceErr + } + + return validateRegistrationKeys(data) +} + +func validateRegistrationRedirectURIs(redirectURIs []string) *exceptions.ServiceError { + for _, raw := range 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 == "") { + if err != nil || uri.Scheme == "" || uri.User != nil || strings.Contains(raw, "#") { + return exceptions.NewError(exceptions.OAuthErrorInvalidRedirectURI, "invalid redirect URI") + } + if (uri.Scheme == "https" || uri.Scheme == "http") && uri.Host == "" { return exceptions.NewError(exceptions.OAuthErrorInvalidRedirectURI, "invalid redirect URI") } } + + return nil +} + +func validateRegistrationKeys(data *ApplicationRegistrationData) *exceptions.ServiceError { 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") @@ -236,6 +306,7 @@ func normalizeRegistrationMetadata(data *ApplicationRegistrationData) *exception } } } + if data.JWKsURI != "" { uri, err := url.Parse(data.JWKsURI) if err != nil || uri.Scheme != "https" || uri.Host == "" || uri.User != nil || uri.Fragment != "" { From 2d9bb879c90f5f943b7001018c4a89fe883c77be Mon Sep 17 00:00:00 2001 From: Afonso Barracha Date: Tue, 22 Sep 2026 05:48:01 +1200 Subject: [PATCH 12/16] fix: remove bug for allowed redirects --- idp/internal/controllers/helpers.go | 11 +++ idp/internal/controllers/middleware.go | 6 +- .../oauth_dynamic_registration_account.go | 10 +++ .../controllers/registration_errors_test.go | 67 ++++++++++++++++++ .../account_credentials_registration.go | 8 ++- .../services/app_dynamic_registration.go | 10 ++- .../services/registration_metadata.go | 4 ++ .../services/registration_metadata_test.go | 62 +++++++++++++++++ idp/internal/services/software_statement.go | 4 ++ idp/tests/dynamic_registration_test.go | 68 +++++++++++++++++-- 10 files changed, 238 insertions(+), 12 deletions(-) create mode 100644 idp/internal/controllers/registration_errors_test.go diff --git a/idp/internal/controllers/helpers.go b/idp/internal/controllers/helpers.go index 7b35c08..4d8101c 100644 --- a/idp/internal/controllers/helpers.go +++ b/idp/internal/controllers/helpers.go @@ -129,8 +129,17 @@ func serviceErrorHTMLResponse(logger *slog.Logger, ctx fiber.Ctx, serviceErr *ex return ctx.Status(status).Type("html").SendString(errHtml) } +func bearerAuthenticationRequired(logger *slog.Logger, ctx fiber.Ctx) error { + ctx.Set(fiber.HeaderWWWAuthenticate, "Bearer") + logResponse(logger, ctx, fiber.StatusUnauthorized) + return ctx.Status(fiber.StatusUnauthorized).Send(nil) +} + func oauthErrorResponse(logger *slog.Logger, ctx fiber.Ctx, message string) error { resErr := exceptions.NewOAuthError(message) + if message == exceptions.OAuthErrorInvalidToken { + ctx.Set(fiber.HeaderWWWAuthenticate, `Bearer error="invalid_token"`) + } switch message { case exceptions.OAuthErrorInvalidRequest, exceptions.OAuthErrorInvalidGrant, @@ -203,6 +212,8 @@ func dynamicRegistrationServiceError( switch serviceErr.Code { case exceptions.OAuthErrorInvalidRedirectURI: return oauthErrorResponse(logger, ctx, exceptions.OAuthErrorInvalidRedirectURI) + case exceptions.OAuthErrorInvalidToken: + return oauthErrorResponse(logger, ctx, exceptions.OAuthErrorInvalidToken) 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 23a8301..e939f22 100644 --- a/idp/internal/controllers/middleware.go +++ b/idp/internal/controllers/middleware.go @@ -172,7 +172,7 @@ func (c *Controllers) DynamicRegistrationIATMiddleware(ctx fiber.Ctx) error { if authHeader == "" { logger.InfoContext(ctx.Context(), "No Authorization header found") - return oauthErrorResponse(logger, ctx, exceptions.OAuthErrorAccessDenied) + return bearerAuthenticationRequired(logger, ctx) } domain, accountClaims, serviceErr := c.services.ProcessAccountCredentialsRegistrationIATAuth( @@ -186,7 +186,7 @@ func (c *Controllers) DynamicRegistrationIATMiddleware(ctx fiber.Ctx) error { if serviceErr != nil { logger.InfoContext(ctx.Context(), "Failed to process account credentials registration IAT auth", "serviceError", serviceErr) - return oauthErrorResponse(logger, ctx, exceptions.OAuthErrorAccessDenied) + return oauthErrorResponse(logger, ctx, exceptions.OAuthErrorInvalidToken) } ctx.Locals("account", accountClaims) @@ -221,7 +221,7 @@ func (c *Controllers) AppDynamicRegistrationIATMiddleware(ctx fiber.Ctx) error { ) if serviceErr != nil { logger.InfoContext(ctx.Context(), "Failed to process app dynamic registration IAT auth", "serviceError", serviceErr) - return oauthErrorResponse(logger, ctx, exceptions.OAuthErrorAccessDenied) + return oauthErrorResponse(logger, ctx, exceptions.OAuthErrorInvalidToken) } logger.DebugContext(ctx.Context(), "Processed app dynamic registration IAT auth successfully", "domain", domain, "account", accountClaims) diff --git a/idp/internal/controllers/oauth_dynamic_registration_account.go b/idp/internal/controllers/oauth_dynamic_registration_account.go index 576800d..78d2adb 100644 --- a/idp/internal/controllers/oauth_dynamic_registration_account.go +++ b/idp/internal/controllers/oauth_dynamic_registration_account.go @@ -35,9 +35,11 @@ func (c *Controllers) OAuthDynamicRegistration(ctx fiber.Ctx) error { return oauthErrorResponse(logger, ctx, exceptions.OAuthErrorInvalidClientMetadata) } + iatDomain, _ := ctx.Locals("domain").(string) accountCredentialsDTO, serviceErr := c.services.CreateAccountCredentialsRegistration( ctx.Context(), services.CreateAccountCredentialsRegistrationOptions{ + InitialAccessTokenDomain: iatDomain, RequestID: requestID, AccountPublicID: accountClaims.AccountID, AccountVersion: accountClaims.AccountVersion, @@ -117,9 +119,14 @@ func (c *Controllers) OAuthAppDynamicRegistration(ctx fiber.Ctx) error { return oauthErrorResponse(logger, ctx, exceptions.OAuthErrorServerError) } + var iatDomain string + if isAuthenticated { + iatDomain, _ = ctx.Locals("domain").(string) + } appDTO, serviceErr := c.services.CreateAppCredentialsRegistration( ctx.Context(), services.CreateAppCredentialsRegistrationOptions{ + InitialAccessTokenDomain: iatDomain, RequestID: requestID, IsAuthenticated: isAuthenticated, AccountID: accountID, @@ -164,6 +171,9 @@ func (c *Controllers) OAuthAppDynamicRegistration(ctx fiber.Ctx) error { }, ) if serviceErr != nil { + if !isAuthenticated && serviceErr.Code == exceptions.OAuthErrorInvalidToken { + return bearerAuthenticationRequired(logger, ctx) + } return dynamicRegistrationServiceError(logger, ctx, serviceErr) } diff --git a/idp/internal/controllers/registration_errors_test.go b/idp/internal/controllers/registration_errors_test.go new file mode 100644 index 0000000..1771d86 --- /dev/null +++ b/idp/internal/controllers/registration_errors_test.go @@ -0,0 +1,67 @@ +package controllers + +import ( + "encoding/json" + "io" + "log/slog" + "net/http" + "net/http/httptest" + "testing" + + "github.com/gofiber/fiber/v3" + "github.com/tugascript/devlogs/idp/internal/exceptions" +) + +func TestRegistrationErrorResponses(t *testing.T) { + logger := slog.New(slog.NewTextHandler(io.Discard, nil)) + c := &Controllers{logger: logger} + for _, tc := range []struct { + name string + handler fiber.Handler + status int + challenge, code string + }{ + {"missing IAT", c.DynamicRegistrationIATMiddleware, http.StatusUnauthorized, "Bearer", ""}, + {"invalid IAT", func(ctx fiber.Ctx) error { + return dynamicRegistrationServiceError(logger, ctx, exceptions.NewError(exceptions.OAuthErrorInvalidToken, "invalid IAT")) + }, http.StatusUnauthorized, `Bearer error="invalid_token"`, exceptions.OAuthErrorInvalidToken}, + {"missing statement issuer", func(ctx fiber.Ctx) error { + return dynamicRegistrationServiceError(logger, ctx, exceptions.NewInvalidTokenError("issuer required")) + }, http.StatusBadRequest, "", exceptions.OAuthErrorInvalidSoftwareStatement}, + {"unapproved statement", func(ctx fiber.Ctx) error { + return dynamicRegistrationServiceError(logger, ctx, exceptions.NewUnauthorizedTokenError("unapproved issuer")) + }, http.StatusBadRequest, "", exceptions.OAuthErrorUnapprovedSoftwareStatement}, + } { + t.Run(tc.name, func(t *testing.T) { + app := fiber.New() + app.Post("/register", tc.handler) + res, err := app.Test(httptest.NewRequest(http.MethodPost, "/register", nil)) + if err != nil { + t.Fatal(err) + } + defer res.Body.Close() + if res.StatusCode != tc.status || res.Header.Get("WWW-Authenticate") != tc.challenge { + t.Fatalf("status=%d challenge=%q", res.StatusCode, res.Header.Get("WWW-Authenticate")) + } + body, err := io.ReadAll(res.Body) + if err != nil { + t.Fatal(err) + } + if tc.code == "" { + if len(body) != 0 { + t.Fatalf("missing credentials must not return an error code: %s", body) + } + return + } + var payload struct { + Error string `json:"error"` + } + if err := json.Unmarshal(body, &payload); err != nil { + t.Fatal(err) + } + if payload.Error != tc.code { + t.Fatalf("error=%q, want %q", payload.Error, tc.code) + } + }) + } +} diff --git a/idp/internal/services/account_credentials_registration.go b/idp/internal/services/account_credentials_registration.go index 953560e..75f7db9 100644 --- a/idp/internal/services/account_credentials_registration.go +++ b/idp/internal/services/account_credentials_registration.go @@ -223,6 +223,8 @@ func (s *Services) mapAccountCredentialsRegistrationDataToDBParams( } type CreateAccountCredentialsRegistrationOptions struct { + // InitialAccessTokenDomain comes only from the verified IAT, never client metadata. + InitialAccessTokenDomain string RequestID string AccountPublicID uuid.UUID AccountVersion int32 @@ -314,7 +316,9 @@ func (s *Services) CreateAccountCredentialsRegistration( TokenEndpointAuthSigningAlg: opts.TokenEndpointAuthSigningAlg, AccessTokenSigningAlg: opts.AccessTokenSigningAlg, } - iatDomain := registrationDomain(opts.ClientURI, opts.RedirectURIs) + if opts.InitialAccessTokenDomain == "" { + return dtos.AccountCredentialsDTO{}, exceptions.NewError(exceptions.OAuthErrorInvalidToken, "initial access token domain is required") + } data, preparationErr := s.prepareDynamicRegistration(ctx, prepareDynamicRegistrationOptions{ requestID: opts.RequestID, accountID: 0, accountPublicID: opts.AccountPublicID, data: data, softwareStatement: opts.SoftwareStatement, @@ -437,7 +441,7 @@ func (s *Services) CreateAccountCredentialsRegistration( _, serviceErr = s.checkClientRegistrationDomain(ctx, checkClientRegistrationDomainOptions{ requestID: opts.RequestID, accountPublicID: opts.AccountPublicID, - iatDomain: iatDomain, + iatDomain: opts.InitialAccessTokenDomain, 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 d60a86e..ce9d960 100644 --- a/idp/internal/services/app_dynamic_registration.go +++ b/idp/internal/services/app_dynamic_registration.go @@ -298,6 +298,8 @@ func (s *Services) mapAppRegistrationDataToDBParams( } type CreateAppCredentialsRegistrationOptions struct { + // InitialAccessTokenDomain comes only from the verified IAT, never client metadata. + InitialAccessTokenDomain string RequestID string AccountID int32 IsAuthenticated bool @@ -390,7 +392,9 @@ func (s *Services) CreateAppCredentialsRegistration( TokenEndpointAuthSigningAlg: opts.TokenEndpointAuthSigningAlg, AccessTokenSigningAlg: opts.AccessTokenSigningAlg, } - iatDomain := registrationDomain(opts.ClientURI, opts.RedirectURIs) + if opts.IsAuthenticated && opts.InitialAccessTokenDomain == "" { + return dtos.AppDTO{}, exceptions.NewError(exceptions.OAuthErrorInvalidToken, "initial access token domain is required") + } data, preparationErr := s.prepareDynamicRegistration(ctx, prepareDynamicRegistrationOptions{ requestID: opts.RequestID, accountID: opts.AccountID, accountPublicID: uuid.Nil, data: data, softwareStatement: opts.SoftwareStatement, @@ -490,7 +494,7 @@ func (s *Services) CreateAppCredentialsRegistration( if slices.Contains(appDRConfigDTO.RequireInitialAccessTokenAppTypes, appType) && !opts.IsAuthenticated { logger.WarnContext(ctx, "App dynamic registration configuration requires initial access token") - return dtos.AppDTO{}, exceptions.NewUnauthorizedError() + return dtos.AppDTO{}, exceptions.NewError(exceptions.OAuthErrorInvalidToken, "initial access token is required") } if !slices.Contains(appDRConfigDTO.AllowedAppTypes, appType) { @@ -533,7 +537,7 @@ func (s *Services) CreateAppCredentialsRegistration( _, serviceErr = s.checkClientRegistrationDomain(ctx, checkClientRegistrationDomainOptions{ requestID: opts.RequestID, accountPublicID: accountDTO.PublicID, - iatDomain: iatDomain, + iatDomain: opts.InitialAccessTokenDomain, 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 634c12c..5d82751 100644 --- a/idp/internal/services/registration_metadata.go +++ b/idp/internal/services/registration_metadata.go @@ -10,6 +10,7 @@ import ( "context" "encoding/json" "errors" + "net" "net/url" "slices" "strings" @@ -270,6 +271,9 @@ func validateRegistrationRedirectURIs(redirectURIs []string) *exceptions.Service if (uri.Scheme == "https" || uri.Scheme == "http") && uri.Host == "" { return exceptions.NewError(exceptions.OAuthErrorInvalidRedirectURI, "invalid redirect URI") } + if uri.Scheme == "http" && !strings.EqualFold(uri.Hostname(), "localhost") && !net.ParseIP(uri.Hostname()).IsLoopback() { + return exceptions.NewError(exceptions.OAuthErrorInvalidRedirectURI, "HTTP redirect URIs must use localhost or a loopback IP address") + } } return nil diff --git a/idp/internal/services/registration_metadata_test.go b/idp/internal/services/registration_metadata_test.go index 9708f25..67bd529 100644 --- a/idp/internal/services/registration_metadata_test.go +++ b/idp/internal/services/registration_metadata_test.go @@ -1,10 +1,15 @@ package services import ( + "context" "encoding/json" + "io" + "log/slog" "reflect" "testing" + "github.com/golang-jwt/jwt/v5" + "github.com/tugascript/devlogs/idp/internal/exceptions" "github.com/tugascript/devlogs/idp/internal/providers/tokens" "github.com/tugascript/devlogs/idp/internal/utils" @@ -26,6 +31,13 @@ func TestRegistrationMetadataDefaultsAndValidation(t *testing.T) { {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"}}}, + {name: "remote HTTP", data: ApplicationRegistrationData{RedirectURIs: []string{"http://example.com/callback"}}, errorCode: exceptions.OAuthErrorInvalidRedirectURI}, + {name: "uppercase remote HTTP", data: ApplicationRegistrationData{RedirectURIs: []string{"HTTP://example.com/callback"}}, errorCode: exceptions.OAuthErrorInvalidRedirectURI}, + {name: "localhost lookalike", data: ApplicationRegistrationData{RedirectURIs: []string{"http://localhost.example.com/callback"}}, errorCode: exceptions.OAuthErrorInvalidRedirectURI}, + {name: "private LAN HTTP", data: ApplicationRegistrationData{RedirectURIs: []string{"http://192.168.1.1/callback"}}, errorCode: exceptions.OAuthErrorInvalidRedirectURI}, + {name: "localhost HTTP", data: ApplicationRegistrationData{RedirectURIs: []string{"http://localhost:8080/callback"}}}, + {name: "IPv4 loopback HTTP", data: ApplicationRegistrationData{RedirectURIs: []string{"http://127.0.0.1:8080/callback"}}}, + {name: "IPv6 loopback HTTP", data: ApplicationRegistrationData{RedirectURIs: []string{"http://[::1]:8080/callback"}}}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { @@ -54,6 +66,56 @@ func TestRegistrationMetadataDefaultsAndValidation(t *testing.T) { } } +func TestSoftwareStatementIssuerClassification(t *testing.T) { + s := &Services{logger: slog.New(slog.NewTextHandler(io.Discard, nil))} + for _, tc := range []struct{ name, issuer, want string }{ + {"missing", "", exceptions.CodeInvalidToken}, + {"unapproved", "https://other.example.net", exceptions.CodeUnauthorizedToken}, + {"approved", "https://client.example.com", ""}, + } { + t.Run(tc.name, func(t *testing.T) { + err := s.verifySoftwareStatementSTDClaims(context.Background(), verifySoftwareStatementSTDClaimsOptions{ + domain: "client.example.com", baseDomain: "example.com", claims: &jwt.RegisteredClaims{Issuer: tc.issuer}, + }) + if tc.want == "" { + if err != nil { + t.Fatal(err) + } + } else if err == nil || err.Code != tc.want { + t.Fatalf("error=%v, want %s", err, tc.want) + } + }) + } +} + +func TestRegistrationRequiresVerifiedIATDomain(t *testing.T) { + s := &Services{logger: slog.New(slog.NewTextHandler(io.Discard, nil))} + _, err := s.CreateAccountCredentialsRegistration(context.Background(), CreateAccountCredentialsRegistrationOptions{ClientURI: "https://client.example.com"}) + if err == nil || err.Code != exceptions.OAuthErrorInvalidToken { + t.Fatalf("account error=%v", err) + } + _, err = s.CreateAppCredentialsRegistration(context.Background(), CreateAppCredentialsRegistrationOptions{IsAuthenticated: true, ClientURI: "https://client.example.com"}) + if err == nil || err.Code != exceptions.OAuthErrorInvalidToken { + t.Fatalf("app error=%v", err) + } +} + +func TestRegistrationRejectsOtherIATDomains(t *testing.T) { + s := &Services{logger: slog.New(slog.NewTextHandler(io.Discard, nil))} + for _, domain := range []string{"sibling.example.com", "other.example.net", "client.example.com.attacker.net"} { + t.Run(domain, func(t *testing.T) { + // Reject before looking up domain approval in the database, even when + // both domains might otherwise be approved for this account. + _, err := s.checkClientRegistrationDomain(context.Background(), checkClientRegistrationDomainOptions{ + iatDomain: "client.example.com", domain: domain, + }) + if err == nil || err.Code != exceptions.CodeUnauthorized { + t.Fatalf("error=%v, want unauthorized", err) + } + }) + } +} + 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{ diff --git a/idp/internal/services/software_statement.go b/idp/internal/services/software_statement.go index 7c85b55..a70e927 100644 --- a/idp/internal/services/software_statement.go +++ b/idp/internal/services/software_statement.go @@ -49,6 +49,10 @@ func (s *Services) verifySoftwareStatementSTDClaims( ) logger.InfoContext(ctx, "Verifying software statement standard claims") + if opts.claims.Issuer == "" { + return exceptions.NewInvalidTokenError("software statement issuer is required") + } + if opts.claims.Issuer != fmt.Sprintf("https://%s", opts.baseDomain) && opts.claims.Issuer != fmt.Sprintf("https://%s", opts.domain) { logger.WarnContext(ctx, "Software statement issuer does not match client URI domain or base domain", diff --git a/idp/tests/dynamic_registration_test.go b/idp/tests/dynamic_registration_test.go index 722260f..b768816 100644 --- a/idp/tests/dynamic_registration_test.go +++ b/idp/tests/dynamic_registration_test.go @@ -357,9 +357,10 @@ func TestDynamicRegistration(t *testing.T) { func TestDynamicRegistrationIATHostIsolation(t *testing.T) { cfg := GetTestConfig(t) - assertAccessDenied := func(t *testing.T, _ dcrSetup, res *http.Response) { + assertInvalidToken := func(t *testing.T, _ dcrSetup, res *http.Response) { response := decodeJSONObject(t, res) - AssertEqual(t, jsonString(response["error"]), exceptions.OAuthErrorAccessDenied) + AssertEqual(t, jsonString(response["error"]), exceptions.OAuthErrorInvalidToken) + AssertEqual(t, res.Header.Get("WWW-Authenticate"), `Bearer error="invalid_token"`) } testCases := []TestRequestCase[dcrSetup]{ { @@ -384,7 +385,7 @@ func TestDynamicRegistrationIATHostIsolation(t *testing.T) { RequestBodyFn: func(setup dcrSetup) any { return setup.body }, HostFn: func(setup dcrSetup) string { return setup.host }, ExpStatus: http.StatusUnauthorized, - AssertFn: assertAccessDenied, + AssertFn: assertInvalidToken, }, { Name: "app IAT cannot register account credentials", @@ -397,7 +398,7 @@ func TestDynamicRegistrationIATHostIsolation(t *testing.T) { RequestBodyFn: func(setup dcrSetup) any { return setup.body }, HostFn: func(setup dcrSetup) string { return setup.host }, ExpStatus: http.StatusUnauthorized, - AssertFn: assertAccessDenied, + AssertFn: assertInvalidToken, }, } @@ -408,6 +409,55 @@ func TestDynamicRegistrationIATHostIsolation(t *testing.T) { } } +func TestDynamicRegistrationIATDomainBinding(t *testing.T) { + for _, appClient := range []bool{false, true} { + for _, statement := range []bool{false, true} { + t.Run(fmt.Sprintf("app=%t/statement=%t", appClient, statement), func(t *testing.T) { + PerformTestRequestCase(t, http.MethodPost, oauthRegisterPath(), TestRequestCase[dcrSetup]{ + ReqFn: func(t *testing.T) (dcrSetup, string) { + setup := setupDynamicRegistration(t, appClient, true, statement) + if statement { + // The signed client_uri must override this conflicting unsigned value. + setup.body["client_uri"] = "https://unsigned.example.net" + } else { + // Approve a second domain for this same account: the IAT must still + // authorize only its own domain, not every approved domain. + other := utils.Base62UUID() + ".example.com" + _, err := GetTestDatabase(t).CreateDynamicRegistrationDomain(context.Background(), database.CreateDynamicRegistrationDomainParams{ + AccountID: setup.account.ID(), AccountPublicID: setup.account.PublicID, Domain: other, + VerificationMethod: database.DomainVerificationMethodDnsTxtRecord, + Usages: []database.DynamicRegistrationUsage{database.DynamicRegistrationUsageApp, database.DynamicRegistrationUsageAccount}, + }) + if err != nil { + t.Fatal(err) + } + setup.body["client_uri"] = "https://" + other + setup.body["redirect_uris"] = []string{"https://" + other + "/callback/"} + } + return setup, setup.accessToken + }, + RequestBodyFn: func(setup dcrSetup) any { return setup.body }, + HostFn: func(setup dcrSetup) string { return setup.host }, + ExpStatus: func() int { + if statement { + return http.StatusCreated + } + return http.StatusUnauthorized + }(), + AssertFn: func(t *testing.T, setup dcrSetup, res *http.Response) { + if statement { + assertCreatedRegistration(t, setup, res) + return + } + response := decodeJSONObject(t, res) + AssertEqual(t, jsonString(response["error"]), exceptions.OAuthErrorUnauthorizedClient) + }, + }) + }) + } + } +} + func TestDynamicRegistrationSoftwareStatementFailures(t *testing.T) { setup := setupDynamicRegistration(t, true, false, false) goodPrivate, goodKid, goodJWK := generateEd25519JWK(t) @@ -453,6 +503,16 @@ func TestDynamicRegistrationSoftwareStatementFailures(t *testing.T) { ExpStatus: http.StatusBadRequest, AssertFn: assertError(exceptions.OAuthErrorUnapprovedSoftwareStatement), }, + { + Name: "missing issuer", + ReqFn: func(t *testing.T) (dcrSetup, string) { + return buildRequest(signSoftwareStatement(t, goodPrivate, goodKid, jwt.MapClaims{ + "client_name": setup.clientName, "client_uri": "https://" + setup.domain, + })), "" + }, + ExpStatus: http.StatusBadRequest, + AssertFn: assertError(exceptions.OAuthErrorInvalidSoftwareStatement), + }, { Name: "issuer mismatch", ReqFn: func(t *testing.T) (dcrSetup, string) { From 06eaf0574bf64700ea147dc04f5d975d62ed826e Mon Sep 17 00:00:00 2001 From: Afonso Barracha Date: Tue, 22 Sep 2026 13:00:16 +1200 Subject: [PATCH 13/16] fix: store all secrets encrypted for RFC-7592 --- idp/initial_schema.dbml | 12 +- .../bodies/oauth_dynamic_registration.go | 2 + idp/internal/controllers/helpers.go | 10 +- idp/internal/controllers/oauth.go | 2 +- .../oauth_dynamic_registration_account.go | 12 +- .../controllers/registration_errors_test.go | 8 +- idp/internal/exceptions/controllers.go | 1 + idp/internal/providers/crypto/kek.go | 5 +- .../account_credential_secrets.sql.go | 12 +- .../database/account_credentials_keys.sql.go | 16 +- .../providers/database/app_keys.sql.go | 8 +- .../providers/database/app_secrets.sql.go | 6 +- .../database/credentials_keys.sql.go | 12 +- .../database/credentials_secrets.sql.go | 18 +- ...egistration_software_statement_keys.sql.go | 4 +- ...0241213231542_create_initial_schema.up.sql | 16 +- idp/internal/providers/database/models.go | 47 +-- .../database/queries/credentials_secrets.sql | 4 +- idp/internal/services/account_credentials.go | 37 ++- .../account_credentials_registration.go | 13 +- .../services/app_dynamic_registration.go | 9 +- idp/internal/services/apps.go | 45 ++- idp/internal/services/client_credentials.go | 135 ++++++--- idp/internal/services/helpers.go | 8 - idp/internal/services/oauth.go | 85 +++--- .../oauth_dynamic_registration_config.go | 286 ++++++++++++++++-- 26 files changed, 533 insertions(+), 280 deletions(-) diff --git a/idp/initial_schema.dbml b/idp/initial_schema.dbml index c7beaf4..aaaad96 100644 --- a/idp/initial_schema.dbml +++ b/idp/initial_schema.dbml @@ -215,18 +215,12 @@ Enum credentials_usage { "user" } -Enum secret_storage_mode { - "hashed" // Store secrets as hashes - "encrypted" // Store secrets encrypted with a data encryption key -} - Table credentials_secrets as CS { id serial [pk] secret_id varchar(22) [not null] client_secret text [not null] - storage_mode secret_storage_mode [not null] - dek_kid varchar(22) [null] + dek_kid varchar(22) [not null] is_revoked boolean [not null, default: false] usage credentials_usage [not null] @@ -253,6 +247,8 @@ Table credentials_keys as CK { public_kid varchar(22) [not null] public_key jsonb [not null] + private_key text [not null] + dek_kid varchar(22) [not null] crypto_suite token_crypto_suite [not null] is_revoked boolean [not null, default: false] is_external boolean [not null, default: false] @@ -268,10 +264,12 @@ Table credentials_keys as CK { (public_kid) [unique, name: 'credential_keys_public_kid_uidx'] (expires_at) [name: 'credential_keys_expires_at_idx'] (is_revoked, usage, expires_at) [name: 'credential_keys_is_revoked_usage_expires_at_idx'] + (dek_kid) [name: 'credential_keys_dek_kid_idx'] (public_kid, crypto_suite, usage, is_revoked, expires_at) [name: 'credential_keys_public_kid_crypto_suite_usage_is_revoked_expires_at_idx'] } } Ref: CK.account_id > A.id [delete: cascade] +Ref: CK.dek_kid > DEK.kid [delete: cascade, update: cascade] Table account_key_encryption_keys as AKEK { account_id integer [not null] diff --git a/idp/internal/controllers/bodies/oauth_dynamic_registration.go b/idp/internal/controllers/bodies/oauth_dynamic_registration.go index 27e1086..dc35b60 100644 --- a/idp/internal/controllers/bodies/oauth_dynamic_registration.go +++ b/idp/internal/controllers/bodies/oauth_dynamic_registration.go @@ -9,6 +9,8 @@ package bodies import "github.com/tugascript/devlogs/idp/internal/utils" type OAuthDynamicClientRegistrationBody struct { + ClientID string `json:"client_id,omitempty" validate:"omitempty"` + ClientSecret string `json:"client_secret,omitempty" validate:"omitempty"` 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'"` diff --git a/idp/internal/controllers/helpers.go b/idp/internal/controllers/helpers.go index 4d8101c..a37345b 100644 --- a/idp/internal/controllers/helpers.go +++ b/idp/internal/controllers/helpers.go @@ -2,8 +2,7 @@ // // 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/. - +// file, You can obtain one at https://mozilla.org/MPL/2.0/.\n package controllers import ( @@ -149,7 +148,8 @@ func oauthErrorResponse(logger *slog.Logger, ctx fiber.Ctx, message string) erro exceptions.OAuthErrorUnsupportedResponseType: logResponse(logger, ctx, fiber.StatusBadRequest) return ctx.Status(fiber.StatusBadRequest).JSON(&resErr) - case exceptions.OAuthErrorUnauthorizedClient, exceptions.OAuthErrorAccessDenied, exceptions.OAuthErrorInvalidToken: + case exceptions.OAuthErrorUnauthorizedClient, exceptions.OAuthErrorAccessDenied, + exceptions.OAuthErrorInvalidToken, exceptions.OAuthErrorInvalidClient: logResponse(logger, ctx, fiber.StatusUnauthorized) return ctx.Status(fiber.StatusUnauthorized).JSON(&resErr) case exceptions.OAuthErrorServerError: @@ -210,6 +210,10 @@ func dynamicRegistrationServiceError( serviceErr *exceptions.ServiceError, ) error { switch serviceErr.Code { + case exceptions.OAuthErrorInvalidRequest: + return oauthErrorResponse(logger, ctx, exceptions.OAuthErrorInvalidRequest) + case exceptions.OAuthErrorInvalidClient: + return oauthErrorResponse(logger, ctx, exceptions.OAuthErrorInvalidClient) case exceptions.OAuthErrorInvalidRedirectURI: return oauthErrorResponse(logger, ctx, exceptions.OAuthErrorInvalidRedirectURI) case exceptions.OAuthErrorInvalidToken: diff --git a/idp/internal/controllers/oauth.go b/idp/internal/controllers/oauth.go index b9e942c..9323f4c 100644 --- a/idp/internal/controllers/oauth.go +++ b/idp/internal/controllers/oauth.go @@ -404,7 +404,7 @@ func (c *Controllers) AccountOAuthToken(ctx fiber.Ctx) error { logger := c.buildLogger(requestID, oauthLocation, "AccountOAuthToken") logRequest(logger, ctx) - if ctx.Get("Content-Type") != "application/x-www-form-urlencoded" { + if ctx.Get(fiber.HeaderContentType) != "application/x-www-form-urlencoded" { return oauthErrorResponse(logger, ctx, exceptions.OAuthErrorInvalidRequest) } diff --git a/idp/internal/controllers/oauth_dynamic_registration_account.go b/idp/internal/controllers/oauth_dynamic_registration_account.go index 78d2adb..2f8bee5 100644 --- a/idp/internal/controllers/oauth_dynamic_registration_account.go +++ b/idp/internal/controllers/oauth_dynamic_registration_account.go @@ -208,7 +208,8 @@ func (c *Controllers) OAuthDynamicRegistrationGet(ctx fiber.Ctx) error { } dto, serviceErr := c.services.GetRegisteredAccountCredentials(ctx.Context(), services.GetRegisteredClientOptions{ - RequestID: requestID, AccountPublicID: accountClaims.AccountID, ClientID: tokenClientID, BackendDomain: c.backendDomain, + RequestID: requestID, AccountPublicID: accountClaims.AccountID, ClientID: tokenClientID, + BackendDomain: c.backendDomain, }) if serviceErr != nil { return oauthErrorResponse(logger, ctx, exceptions.OAuthErrorInvalidToken) @@ -260,7 +261,9 @@ func (c *Controllers) OAuthDynamicRegistrationUpdate(ctx fiber.Ctx) error { } dto, serviceErr := c.services.UpdateRegisteredAccountCredentials(ctx.Context(), services.UpdateRegisteredClientOptions{ - ClientID: tokenClientID, + ClientID: tokenClientID, + SubmittedClientID: body.ClientID, + SubmittedClientSecret: body.ClientSecret, CreateAccountCredentialsRegistrationOptions: services.CreateAccountCredentialsRegistrationOptions{ RequestID: requestID, AccountPublicID: accountClaims.AccountID, AccountVersion: accountClaims.AccountVersion, ApplicationType: body.ApplicationType, RedirectURIs: body.RedirectURIs, TokenEndpointAuthMethod: body.TokenEndpointAuthMethod, @@ -303,7 +306,10 @@ func (c *Controllers) OAuthAppDynamicRegistrationUpdate(ctx fiber.Ctx) error { } dto, serviceErr := c.services.UpdateRegisteredApp(ctx.Context(), services.UpdateRegisteredAppOptions{ - ClientID: tokenClientID, HostUsername: username, + ClientID: tokenClientID, + SubmittedClientID: body.ClientID, + SubmittedClientSecret: body.ClientSecret, + HostUsername: username, CreateAppCredentialsRegistrationOptions: services.CreateAppCredentialsRegistrationOptions{ RequestID: requestID, AccountID: accountID, ApplicationType: body.ApplicationType, RedirectURIs: body.RedirectURIs, TokenEndpointAuthMethod: body.TokenEndpointAuthMethod, GrantTypes: body.GrantTypes, ResponseTypes: body.ResponseTypes, diff --git a/idp/internal/controllers/registration_errors_test.go b/idp/internal/controllers/registration_errors_test.go index 1771d86..28f7217 100644 --- a/idp/internal/controllers/registration_errors_test.go +++ b/idp/internal/controllers/registration_errors_test.go @@ -31,6 +31,12 @@ func TestRegistrationErrorResponses(t *testing.T) { {"unapproved statement", func(ctx fiber.Ctx) error { return dynamicRegistrationServiceError(logger, ctx, exceptions.NewUnauthorizedTokenError("unapproved issuer")) }, http.StatusBadRequest, "", exceptions.OAuthErrorUnapprovedSoftwareStatement}, + {"invalid request metadata", func(ctx fiber.Ctx) error { + return dynamicRegistrationServiceError(logger, ctx, exceptions.NewError(exceptions.OAuthErrorInvalidRequest, "invalid request")) + }, http.StatusBadRequest, "", exceptions.OAuthErrorInvalidRequest}, + {"invalid client authentication", func(ctx fiber.Ctx) error { + return dynamicRegistrationServiceError(logger, ctx, exceptions.NewError(exceptions.OAuthErrorInvalidClient, "invalid client")) + }, http.StatusUnauthorized, "", exceptions.OAuthErrorInvalidClient}, } { t.Run(tc.name, func(t *testing.T) { app := fiber.New() @@ -41,7 +47,7 @@ func TestRegistrationErrorResponses(t *testing.T) { } defer res.Body.Close() if res.StatusCode != tc.status || res.Header.Get("WWW-Authenticate") != tc.challenge { - t.Fatalf("status=%d challenge=%q", res.StatusCode, res.Header.Get("WWW-Authenticate")) + t.Fatalf("status=%d (want %d) challenge=%q (want %q)", res.StatusCode, tc.status, res.Header.Get("WWW-Authenticate"), tc.challenge) } body, err := io.ReadAll(res.Body) if err != nil { diff --git a/idp/internal/exceptions/controllers.go b/idp/internal/exceptions/controllers.go index a67da73..614646e 100644 --- a/idp/internal/exceptions/controllers.go +++ b/idp/internal/exceptions/controllers.go @@ -26,6 +26,7 @@ const ( OAuthErrorInvalidRequest string = "invalid_request" OAuthErrorInvalidGrant string = "invalid_grant" + OAuthErrorInvalidClient string = "invalid_client" OAuthErrorUnauthorizedClient string = "unauthorized_client" OAuthErrorAccessDenied string = "access_denied" OAuthErrorServerError string = "server_error" diff --git a/idp/internal/providers/crypto/kek.go b/idp/internal/providers/crypto/kek.go index 96e1d53..26a4926 100644 --- a/idp/internal/providers/crypto/kek.go +++ b/idp/internal/providers/crypto/kek.go @@ -32,7 +32,10 @@ func buildKEKPath(kekPath string, path string, keyID uuid.UUID) string { } func (e *Crypto) createTransitKey(ctx context.Context) (uuid.UUID, error) { - keyID := uuid.New() + keyID, err := uuid.NewV7() + if err != nil { + return uuid.Nil, fmt.Errorf("failed to generate UUID for transit key: %w", err) + } if _, err := e.opLogical.WriteWithContext(ctx, buildKEKPath(e.kekPath, "keys", keyID), kekKeyConfig); err != nil { return uuid.Nil, fmt.Errorf("failed to create transit key: %w", err) } diff --git a/idp/internal/providers/database/account_credential_secrets.sql.go b/idp/internal/providers/database/account_credential_secrets.sql.go index aad0c68..babed30 100644 --- a/idp/internal/providers/database/account_credential_secrets.sql.go +++ b/idp/internal/providers/database/account_credential_secrets.sql.go @@ -67,7 +67,7 @@ func (q *Queries) CreateAccountCredentialSecret(ctx context.Context, arg CreateA } const findAccountCredentialSecretByAccountCredentialIDAndCredentialsSecretID = `-- name: FindAccountCredentialSecretByAccountCredentialIDAndCredentialsSecretID :one -SELECT csr.id, csr.secret_id, csr.client_secret, csr.storage_mode, csr.dek_kid, csr.is_revoked, csr.usage, csr.account_id, csr.expires_at, csr.created_at, csr.updated_at FROM "credentials_secrets" "csr" +SELECT csr.id, csr.secret_id, csr.client_secret, csr.dek_kid, csr.is_revoked, csr.usage, csr.account_id, csr.expires_at, csr.created_at, csr.updated_at FROM "credentials_secrets" "csr" LEFT JOIN "account_credentials_secrets" "acs" ON "acs"."credentials_secret_id" = "csr"."id" WHERE "acs"."account_credentials_id" = $1 AND @@ -87,7 +87,6 @@ func (q *Queries) FindAccountCredentialSecretByAccountCredentialIDAndCredentials &i.ID, &i.SecretID, &i.ClientSecret, - &i.StorageMode, &i.DekKid, &i.IsRevoked, &i.Usage, @@ -135,7 +134,7 @@ func (q *Queries) FindAccountCredentialsSecretAccountByAccountCredentialIDAndSec } const findCurrentAccountCredentialSecretByAccountCredentialID = `-- name: FindCurrentAccountCredentialSecretByAccountCredentialID :one -SELECT csr.id, csr.secret_id, csr.client_secret, csr.storage_mode, csr.dek_kid, csr.is_revoked, csr.usage, csr.account_id, csr.expires_at, csr.created_at, csr.updated_at FROM "credentials_secrets" "csr" +SELECT csr.id, csr.secret_id, csr.client_secret, csr.dek_kid, csr.is_revoked, csr.usage, csr.account_id, csr.expires_at, csr.created_at, csr.updated_at FROM "credentials_secrets" "csr" LEFT JOIN "account_credentials_secrets" "acs" ON "acs"."credentials_secret_id" = "csr"."id" WHERE "acs"."account_credentials_id" = $1 AND @@ -151,7 +150,6 @@ func (q *Queries) FindCurrentAccountCredentialSecretByAccountCredentialID(ctx co &i.ID, &i.SecretID, &i.ClientSecret, - &i.StorageMode, &i.DekKid, &i.IsRevoked, &i.Usage, @@ -164,7 +162,7 @@ func (q *Queries) FindCurrentAccountCredentialSecretByAccountCredentialID(ctx co } const findPaginatedAccountCredentialSecretsByAccountCredentialID = `-- name: FindPaginatedAccountCredentialSecretsByAccountCredentialID :many -SELECT csr.id, csr.secret_id, csr.client_secret, csr.storage_mode, csr.dek_kid, csr.is_revoked, csr.usage, csr.account_id, csr.expires_at, csr.created_at, csr.updated_at FROM "credentials_secrets" "csr" +SELECT csr.id, csr.secret_id, csr.client_secret, csr.dek_kid, csr.is_revoked, csr.usage, csr.account_id, csr.expires_at, csr.created_at, csr.updated_at FROM "credentials_secrets" "csr" LEFT JOIN "account_credentials_secrets" "acs" ON "acs"."credentials_secret_id" = "csr"."id" WHERE "acs"."account_credentials_id" = $1 ORDER BY "csr"."expires_at" DESC @@ -190,7 +188,6 @@ func (q *Queries) FindPaginatedAccountCredentialSecretsByAccountCredentialID(ctx &i.ID, &i.SecretID, &i.ClientSecret, - &i.StorageMode, &i.DekKid, &i.IsRevoked, &i.Usage, @@ -210,7 +207,7 @@ func (q *Queries) FindPaginatedAccountCredentialSecretsByAccountCredentialID(ctx } const findValidAccountCredentialSecretByAccountCredentialIDAndCredentialsSecretID = `-- name: FindValidAccountCredentialSecretByAccountCredentialIDAndCredentialsSecretID :one -SELECT csr.id, csr.secret_id, csr.client_secret, csr.storage_mode, csr.dek_kid, csr.is_revoked, csr.usage, csr.account_id, csr.expires_at, csr.created_at, csr.updated_at FROM "credentials_secrets" "csr" +SELECT csr.id, csr.secret_id, csr.client_secret, csr.dek_kid, csr.is_revoked, csr.usage, csr.account_id, csr.expires_at, csr.created_at, csr.updated_at FROM "credentials_secrets" "csr" LEFT JOIN "account_credentials_secrets" "acs" ON "acs"."credentials_secret_id" = "csr"."id" WHERE "acs"."account_credentials_id" = $1 AND @@ -232,7 +229,6 @@ func (q *Queries) FindValidAccountCredentialSecretByAccountCredentialIDAndCreden &i.ID, &i.SecretID, &i.ClientSecret, - &i.StorageMode, &i.DekKid, &i.IsRevoked, &i.Usage, diff --git a/idp/internal/providers/database/account_credentials_keys.sql.go b/idp/internal/providers/database/account_credentials_keys.sql.go index 8896737..b376726 100644 --- a/idp/internal/providers/database/account_credentials_keys.sql.go +++ b/idp/internal/providers/database/account_credentials_keys.sql.go @@ -67,7 +67,7 @@ func (q *Queries) CreateAccountCredentialKey(ctx context.Context, arg CreateAcco } const findAccountCredentialKeyByAccountCredentialIDAndPublicKID = `-- name: FindAccountCredentialKeyByAccountCredentialIDAndPublicKID :one -SELECT ckr.id, ckr.public_kid, ckr.public_key, ckr.crypto_suite, ckr.is_revoked, ckr.is_external, ckr.usage, ckr.account_id, ckr.expires_at, ckr.created_at, ckr.updated_at FROM "credentials_keys" "ckr" +SELECT ckr.id, ckr.public_kid, ckr.public_key, ckr.private_key, ckr.dek_kid, ckr.crypto_suite, ckr.is_revoked, ckr.is_external, ckr.usage, ckr.account_id, ckr.expires_at, ckr.created_at, ckr.updated_at FROM "credentials_keys" "ckr" LEFT JOIN "account_credentials_keys" "ack" ON "ack"."credentials_key_id" = "ckr"."id" WHERE "ack"."account_credentials_id" = $1 AND @@ -87,6 +87,8 @@ func (q *Queries) FindAccountCredentialKeyByAccountCredentialIDAndPublicKID(ctx &i.ID, &i.PublicKid, &i.PublicKey, + &i.PrivateKey, + &i.DekKid, &i.CryptoSuite, &i.IsRevoked, &i.IsExternal, @@ -135,7 +137,7 @@ func (q *Queries) FindAccountCredentialsKeyAccountByAccountCredentialIDAndJWKKID } const findActiveAccountCredentialKeysByAccountPublicID = `-- name: FindActiveAccountCredentialKeysByAccountPublicID :many -SELECT ckr.id, ckr.public_kid, ckr.public_key, ckr.crypto_suite, ckr.is_revoked, ckr.is_external, ckr.usage, ckr.account_id, ckr.expires_at, ckr.created_at, ckr.updated_at FROM "credentials_keys" "ckr" +SELECT ckr.id, ckr.public_kid, ckr.public_key, ckr.private_key, ckr.dek_kid, ckr.crypto_suite, ckr.is_revoked, ckr.is_external, ckr.usage, ckr.account_id, ckr.expires_at, ckr.created_at, ckr.updated_at FROM "credentials_keys" "ckr" LEFT JOIN "account_credentials_keys" "ack" ON "ack"."credentials_key_id" = "ckr"."id" WHERE "ack"."account_public_id" = $1 AND @@ -157,6 +159,8 @@ func (q *Queries) FindActiveAccountCredentialKeysByAccountPublicID(ctx context.C &i.ID, &i.PublicKid, &i.PublicKey, + &i.PrivateKey, + &i.DekKid, &i.CryptoSuite, &i.IsRevoked, &i.IsExternal, @@ -177,7 +181,7 @@ func (q *Queries) FindActiveAccountCredentialKeysByAccountPublicID(ctx context.C } const findCurrentAccountCredentialKeyByAccountCredentialID = `-- name: FindCurrentAccountCredentialKeyByAccountCredentialID :one -SELECT ckr.id, ckr.public_kid, ckr.public_key, ckr.crypto_suite, ckr.is_revoked, ckr.is_external, ckr.usage, ckr.account_id, ckr.expires_at, ckr.created_at, ckr.updated_at FROM "credentials_keys" "ckr" +SELECT ckr.id, ckr.public_kid, ckr.public_key, ckr.private_key, ckr.dek_kid, ckr.crypto_suite, ckr.is_revoked, ckr.is_external, ckr.usage, ckr.account_id, ckr.expires_at, ckr.created_at, ckr.updated_at FROM "credentials_keys" "ckr" LEFT JOIN "account_credentials_keys" "ack" ON "ack"."credentials_key_id" = "ckr"."id" WHERE "ack"."account_credentials_id" = $1 AND @@ -193,6 +197,8 @@ func (q *Queries) FindCurrentAccountCredentialKeyByAccountCredentialID(ctx conte &i.ID, &i.PublicKid, &i.PublicKey, + &i.PrivateKey, + &i.DekKid, &i.CryptoSuite, &i.IsRevoked, &i.IsExternal, @@ -206,7 +212,7 @@ func (q *Queries) FindCurrentAccountCredentialKeyByAccountCredentialID(ctx conte } const findPaginatedAccountCredentialKeysByAccountCredentialID = `-- name: FindPaginatedAccountCredentialKeysByAccountCredentialID :many -SELECT ckr.id, ckr.public_kid, ckr.public_key, ckr.crypto_suite, ckr.is_revoked, ckr.is_external, ckr.usage, ckr.account_id, ckr.expires_at, ckr.created_at, ckr.updated_at FROM "credentials_keys" "ckr" +SELECT ckr.id, ckr.public_kid, ckr.public_key, ckr.private_key, ckr.dek_kid, ckr.crypto_suite, ckr.is_revoked, ckr.is_external, ckr.usage, ckr.account_id, ckr.expires_at, ckr.created_at, ckr.updated_at FROM "credentials_keys" "ckr" LEFT JOIN "account_credentials_keys" "ack" ON "ack"."credentials_key_id" = "ckr"."id" WHERE "ack"."account_credentials_id" = $1 ORDER BY "ckr"."expires_at" DESC @@ -232,6 +238,8 @@ func (q *Queries) FindPaginatedAccountCredentialKeysByAccountCredentialID(ctx co &i.ID, &i.PublicKid, &i.PublicKey, + &i.PrivateKey, + &i.DekKid, &i.CryptoSuite, &i.IsRevoked, &i.IsExternal, diff --git a/idp/internal/providers/database/app_keys.sql.go b/idp/internal/providers/database/app_keys.sql.go index e9d37ef..8d8ef76 100644 --- a/idp/internal/providers/database/app_keys.sql.go +++ b/idp/internal/providers/database/app_keys.sql.go @@ -53,7 +53,7 @@ func (q *Queries) CreateAppKey(ctx context.Context, arg CreateAppKeyParams) erro } const findAppKeyByAppIDAndPublicKID = `-- name: FindAppKeyByAppIDAndPublicKID :one -SELECT ckr.id, ckr.public_kid, ckr.public_key, ckr.crypto_suite, ckr.is_revoked, ckr.is_external, ckr.usage, ckr.account_id, ckr.expires_at, ckr.created_at, ckr.updated_at FROM "credentials_keys" "ckr" +SELECT ckr.id, ckr.public_kid, ckr.public_key, ckr.private_key, ckr.dek_kid, ckr.crypto_suite, ckr.is_revoked, ckr.is_external, ckr.usage, ckr.account_id, ckr.expires_at, ckr.created_at, ckr.updated_at FROM "credentials_keys" "ckr" LEFT JOIN "app_keys" "ak" ON "ak"."credentials_key_id" = "ckr"."id" WHERE "ak"."app_id" = $1 AND @@ -73,6 +73,8 @@ func (q *Queries) FindAppKeyByAppIDAndPublicKID(ctx context.Context, arg FindApp &i.ID, &i.PublicKid, &i.PublicKey, + &i.PrivateKey, + &i.DekKid, &i.CryptoSuite, &i.IsRevoked, &i.IsExternal, @@ -86,7 +88,7 @@ func (q *Queries) FindAppKeyByAppIDAndPublicKID(ctx context.Context, arg FindApp } const findPaginatedAppKeysByAppID = `-- name: FindPaginatedAppKeysByAppID :many -SELECT ckr.id, ckr.public_kid, ckr.public_key, ckr.crypto_suite, ckr.is_revoked, ckr.is_external, ckr.usage, ckr.account_id, ckr.expires_at, ckr.created_at, ckr.updated_at FROM "credentials_keys" "ckr" +SELECT ckr.id, ckr.public_kid, ckr.public_key, ckr.private_key, ckr.dek_kid, ckr.crypto_suite, ckr.is_revoked, ckr.is_external, ckr.usage, ckr.account_id, ckr.expires_at, ckr.created_at, ckr.updated_at FROM "credentials_keys" "ckr" LEFT JOIN "app_keys" "ak" ON "ak"."credentials_key_id" = "ckr"."id" WHERE "ak"."app_id" = $1 ORDER BY "ckr"."expires_at" DESC @@ -112,6 +114,8 @@ func (q *Queries) FindPaginatedAppKeysByAppID(ctx context.Context, arg FindPagin &i.ID, &i.PublicKid, &i.PublicKey, + &i.PrivateKey, + &i.DekKid, &i.CryptoSuite, &i.IsRevoked, &i.IsExternal, diff --git a/idp/internal/providers/database/app_secrets.sql.go b/idp/internal/providers/database/app_secrets.sql.go index f83e7e1..ffd1491 100644 --- a/idp/internal/providers/database/app_secrets.sql.go +++ b/idp/internal/providers/database/app_secrets.sql.go @@ -53,7 +53,7 @@ func (q *Queries) CreateAppSecret(ctx context.Context, arg CreateAppSecretParams } const findAppSecretByAppIDAndSecretID = `-- name: FindAppSecretByAppIDAndSecretID :one -SELECT csr.id, csr.secret_id, csr.client_secret, csr.storage_mode, csr.dek_kid, csr.is_revoked, csr.usage, csr.account_id, csr.expires_at, csr.created_at, csr.updated_at FROM "credentials_secrets" "csr" +SELECT csr.id, csr.secret_id, csr.client_secret, csr.dek_kid, csr.is_revoked, csr.usage, csr.account_id, csr.expires_at, csr.created_at, csr.updated_at FROM "credentials_secrets" "csr" LEFT JOIN "app_secrets" "as" ON "as"."credentials_secret_id" = "csr"."id" WHERE "as"."app_id" = $1 AND @@ -73,7 +73,6 @@ func (q *Queries) FindAppSecretByAppIDAndSecretID(ctx context.Context, arg FindA &i.ID, &i.SecretID, &i.ClientSecret, - &i.StorageMode, &i.DekKid, &i.IsRevoked, &i.Usage, @@ -86,7 +85,7 @@ func (q *Queries) FindAppSecretByAppIDAndSecretID(ctx context.Context, arg FindA } const findPaginatedAppSecretsByAppID = `-- name: FindPaginatedAppSecretsByAppID :many -SELECT csr.id, csr.secret_id, csr.client_secret, csr.storage_mode, csr.dek_kid, csr.is_revoked, csr.usage, csr.account_id, csr.expires_at, csr.created_at, csr.updated_at FROM "credentials_secrets" "csr" +SELECT csr.id, csr.secret_id, csr.client_secret, csr.dek_kid, csr.is_revoked, csr.usage, csr.account_id, csr.expires_at, csr.created_at, csr.updated_at FROM "credentials_secrets" "csr" LEFT JOIN "app_secrets" "as" ON "as"."credentials_secret_id" = "csr"."id" WHERE "as"."app_id" = $1 ORDER BY "csr"."expires_at" DESC @@ -112,7 +111,6 @@ func (q *Queries) FindPaginatedAppSecretsByAppID(ctx context.Context, arg FindPa &i.ID, &i.SecretID, &i.ClientSecret, - &i.StorageMode, &i.DekKid, &i.IsRevoked, &i.Usage, diff --git a/idp/internal/providers/database/credentials_keys.sql.go b/idp/internal/providers/database/credentials_keys.sql.go index 0cf0540..7833392 100644 --- a/idp/internal/providers/database/credentials_keys.sql.go +++ b/idp/internal/providers/database/credentials_keys.sql.go @@ -26,7 +26,7 @@ INSERT INTO "credentials_keys" ( $4, $5, $6 -) RETURNING id, public_kid, public_key, crypto_suite, is_revoked, is_external, usage, account_id, expires_at, created_at, updated_at +) RETURNING id, public_kid, public_key, private_key, dek_kid, crypto_suite, is_revoked, is_external, usage, account_id, expires_at, created_at, updated_at ` type CreateCredentialsKeyParams struct { @@ -57,6 +57,8 @@ func (q *Queries) CreateCredentialsKey(ctx context.Context, arg CreateCredential &i.ID, &i.PublicKid, &i.PublicKey, + &i.PrivateKey, + &i.DekKid, &i.CryptoSuite, &i.IsRevoked, &i.IsExternal, @@ -79,7 +81,7 @@ func (q *Queries) DeleteAllCredentialsKeys(ctx context.Context) error { } const findCredentialsKeyByID = `-- name: FindCredentialsKeyByID :one -SELECT id, public_kid, public_key, crypto_suite, is_revoked, is_external, usage, account_id, expires_at, created_at, updated_at FROM "credentials_keys" +SELECT id, public_kid, public_key, private_key, dek_kid, crypto_suite, is_revoked, is_external, usage, account_id, expires_at, created_at, updated_at FROM "credentials_keys" WHERE "id" = $1 LIMIT 1 ` @@ -91,6 +93,8 @@ func (q *Queries) FindCredentialsKeyByID(ctx context.Context, id int32) (Credent &i.ID, &i.PublicKid, &i.PublicKey, + &i.PrivateKey, + &i.DekKid, &i.CryptoSuite, &i.IsRevoked, &i.IsExternal, @@ -132,7 +136,7 @@ UPDATE "credentials_keys" SET "is_revoked" = true, "updated_at" = now() WHERE "id" = $1 -RETURNING id, public_kid, public_key, crypto_suite, is_revoked, is_external, usage, account_id, expires_at, created_at, updated_at +RETURNING id, public_kid, public_key, private_key, dek_kid, crypto_suite, is_revoked, is_external, usage, account_id, expires_at, created_at, updated_at ` func (q *Queries) RevokeCredentialsKey(ctx context.Context, id int32) (CredentialsKey, error) { @@ -142,6 +146,8 @@ func (q *Queries) RevokeCredentialsKey(ctx context.Context, id int32) (Credentia &i.ID, &i.PublicKid, &i.PublicKey, + &i.PrivateKey, + &i.DekKid, &i.CryptoSuite, &i.IsRevoked, &i.IsExternal, diff --git a/idp/internal/providers/database/credentials_secrets.sql.go b/idp/internal/providers/database/credentials_secrets.sql.go index 7b52fe8..2d42378 100644 --- a/idp/internal/providers/database/credentials_secrets.sql.go +++ b/idp/internal/providers/database/credentials_secrets.sql.go @@ -8,8 +8,6 @@ package database import ( "context" "time" - - "github.com/jackc/pgx/v5/pgtype" ) const createCredentialsSecret = `-- name: CreateCredentialsSecret :one @@ -18,7 +16,6 @@ INSERT INTO "credentials_secrets" ( "account_id", "secret_id", "client_secret", - "storage_mode", "dek_kid", "expires_at", "usage" @@ -28,8 +25,7 @@ INSERT INTO "credentials_secrets" ( $3, $4, $5, - $6, - $7 + $6 ) RETURNING "id" ` @@ -37,8 +33,7 @@ type CreateCredentialsSecretParams struct { AccountID int32 SecretID string ClientSecret string - StorageMode SecretStorageMode - DekKid pgtype.Text + DekKid string ExpiresAt time.Time Usage CredentialsUsage } @@ -53,7 +48,6 @@ func (q *Queries) CreateCredentialsSecret(ctx context.Context, arg CreateCredent arg.AccountID, arg.SecretID, arg.ClientSecret, - arg.StorageMode, arg.DekKid, arg.ExpiresAt, arg.Usage, @@ -73,7 +67,7 @@ func (q *Queries) DeleteAllCredentialsSecrets(ctx context.Context) error { } const findValidCredentialsSecretBySecretID = `-- name: FindValidCredentialsSecretBySecretID :one -SELECT id, secret_id, client_secret, storage_mode, dek_kid, is_revoked, usage, account_id, expires_at, created_at, updated_at FROM "credentials_secrets" +SELECT id, secret_id, client_secret, dek_kid, is_revoked, usage, account_id, expires_at, created_at, updated_at FROM "credentials_secrets" WHERE "secret_id" = $1 AND "is_revoked" = false AND @@ -88,7 +82,6 @@ func (q *Queries) FindValidCredentialsSecretBySecretID(ctx context.Context, secr &i.ID, &i.SecretID, &i.ClientSecret, - &i.StorageMode, &i.DekKid, &i.IsRevoked, &i.Usage, @@ -105,7 +98,7 @@ UPDATE "credentials_secrets" SET "is_revoked" = true, "updated_at" = now() WHERE "id" = $1 -RETURNING id, secret_id, client_secret, storage_mode, dek_kid, is_revoked, usage, account_id, expires_at, created_at, updated_at +RETURNING id, secret_id, client_secret, dek_kid, is_revoked, usage, account_id, expires_at, created_at, updated_at ` func (q *Queries) RevokeCredentialsSecret(ctx context.Context, id int32) (CredentialsSecret, error) { @@ -115,7 +108,6 @@ func (q *Queries) RevokeCredentialsSecret(ctx context.Context, id int32) (Creden &i.ID, &i.SecretID, &i.ClientSecret, - &i.StorageMode, &i.DekKid, &i.IsRevoked, &i.Usage, @@ -138,7 +130,7 @@ WHERE "id" = $1 type UpdateCredentialsSecretClientSecretParams struct { ID int32 ClientSecret string - DekKid pgtype.Text + DekKid string } func (q *Queries) UpdateCredentialsSecretClientSecret(ctx context.Context, arg UpdateCredentialsSecretClientSecretParams) error { 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 f48cb5b..93605f2 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 @@ -39,7 +39,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" +SELECT c.id, c.public_kid, c.public_key, c.private_key, c.dek_kid, 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"."credentials_key_id" WHERE "d"."root_domain" = $1 AND "d"."account_public_id" = $2 LIMIT 1 @@ -62,6 +62,8 @@ func (q *Queries) FindDynamicRegistrationSoftwareStatementKeysByRootDomainAndAcc &i.ID, &i.PublicKid, &i.PublicKey, + &i.PrivateKey, + &i.DekKid, &i.CryptoSuite, &i.IsRevoked, &i.IsExternal, 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 7f0e932..3383431 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: 2026-09-14T01:24:14.048Z +-- Generated at: 2026-09-21T22:02:21.631Z CREATE TYPE "kek_usage" AS ENUM ( 'global', @@ -72,11 +72,6 @@ CREATE TYPE "credentials_usage" AS ENUM ( 'user' ); -CREATE TYPE "secret_storage_mode" AS ENUM ( - 'hashed', - 'encrypted' -); - CREATE TYPE "auth_method" AS ENUM ( 'none', 'client_secret_basic', @@ -312,8 +307,7 @@ CREATE TABLE "credentials_secrets" ( "id" serial PRIMARY KEY, "secret_id" varchar(22) NOT NULL, "client_secret" text NOT NULL, - "storage_mode" secret_storage_mode NOT NULL, - "dek_kid" varchar(22), + "dek_kid" varchar(22) NOT NULL, "is_revoked" boolean NOT NULL DEFAULT false, "usage" credentials_usage NOT NULL, "account_id" integer NOT NULL, @@ -326,6 +320,8 @@ CREATE TABLE "credentials_keys" ( "id" serial PRIMARY KEY, "public_kid" varchar(22) NOT NULL, "public_key" jsonb NOT NULL, + "private_key" text NOT NULL, + "dek_kid" varchar(22) NOT NULL, "crypto_suite" token_crypto_suite NOT NULL, "is_revoked" boolean NOT NULL DEFAULT false, "is_external" boolean NOT NULL DEFAULT false, @@ -886,6 +882,8 @@ CREATE INDEX "credential_keys_expires_at_idx" ON "credentials_keys" ("expires_at CREATE INDEX "credential_keys_is_revoked_usage_expires_at_idx" ON "credentials_keys" ("is_revoked", "usage", "expires_at"); +CREATE INDEX "credential_keys_dek_kid_idx" ON "credentials_keys" ("dek_kid"); + CREATE INDEX "credential_keys_public_kid_crypto_suite_usage_is_revoked_expires_at_idx" ON "credentials_keys" ("public_kid", "crypto_suite", "usage", "is_revoked", "expires_at"); CREATE INDEX "account_key_encryption_keys_account_id_idx" ON "account_key_encryption_keys" ("account_id"); @@ -1200,6 +1198,8 @@ ALTER TABLE "credentials_secrets" ADD FOREIGN KEY ("dek_kid") REFERENCES "data_e ALTER TABLE "credentials_keys" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("id") ON DELETE CASCADE; +ALTER TABLE "credentials_keys" ADD FOREIGN KEY ("dek_kid") REFERENCES "data_encryption_keys" ("kid") ON DELETE CASCADE ON UPDATE CASCADE; + 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 ("key_encryption_key_id") REFERENCES "key_encryption_keys" ("id") ON DELETE CASCADE; diff --git a/idp/internal/providers/database/models.go b/idp/internal/providers/database/models.go index ba0ec81..e1c4034 100644 --- a/idp/internal/providers/database/models.go +++ b/idp/internal/providers/database/models.go @@ -907,48 +907,6 @@ func (ns NullScopes) Value() (driver.Value, error) { return string(ns.Scopes), nil } -type SecretStorageMode string - -const ( - SecretStorageModeHashed SecretStorageMode = "hashed" - SecretStorageModeEncrypted SecretStorageMode = "encrypted" -) - -func (e *SecretStorageMode) Scan(src interface{}) error { - switch s := src.(type) { - case []byte: - *e = SecretStorageMode(s) - case string: - *e = SecretStorageMode(s) - default: - return fmt.Errorf("unsupported scan type for SecretStorageMode: %T", src) - } - return nil -} - -type NullSecretStorageMode struct { - SecretStorageMode SecretStorageMode - Valid bool // Valid is true if SecretStorageMode is not NULL -} - -// Scan implements the Scanner interface. -func (ns *NullSecretStorageMode) Scan(value interface{}) error { - if value == nil { - ns.SecretStorageMode, ns.Valid = "", false - return nil - } - ns.Valid = true - return ns.SecretStorageMode.Scan(value) -} - -// Value implements the driver Valuer interface. -func (ns NullSecretStorageMode) Value() (driver.Value, error) { - if !ns.Valid { - return nil, nil - } - return string(ns.SecretStorageMode), nil -} - type SessionType string const ( @@ -1735,6 +1693,8 @@ type CredentialsKey struct { ID int32 PublicKid string PublicKey []byte + PrivateKey string + DekKid string CryptoSuite TokenCryptoSuite IsRevoked bool IsExternal bool @@ -1749,8 +1709,7 @@ type CredentialsSecret struct { ID int32 SecretID string ClientSecret string - StorageMode SecretStorageMode - DekKid pgtype.Text + DekKid string IsRevoked bool Usage CredentialsUsage AccountID int32 diff --git a/idp/internal/providers/database/queries/credentials_secrets.sql b/idp/internal/providers/database/queries/credentials_secrets.sql index f264bbd..eb0de7a 100644 --- a/idp/internal/providers/database/queries/credentials_secrets.sql +++ b/idp/internal/providers/database/queries/credentials_secrets.sql @@ -9,7 +9,6 @@ INSERT INTO "credentials_secrets" ( "account_id", "secret_id", "client_secret", - "storage_mode", "dek_kid", "expires_at", "usage" @@ -19,8 +18,7 @@ INSERT INTO "credentials_secrets" ( $3, $4, $5, - $6, - $7 + $6 ) RETURNING "id"; -- name: RevokeCredentialsSecret :one diff --git a/idp/internal/services/account_credentials.go b/idp/internal/services/account_credentials.go index 0b1258b..e3f8865 100644 --- a/idp/internal/services/account_credentials.go +++ b/idp/internal/services/account_credentials.go @@ -277,7 +277,7 @@ func (s *Services) CreateAccountCredentials( return dtos.AccountCredentialsDTO{}, serviceErr } responseTypes := make([]database.ResponseType, 0) - if credentialsType == database.AccountCredentialsTypeMcp && transport != database.TransportStdio { + if credentialsType == database.AccountCredentialsTypeMcp { responseTypes, serviceErr = mapResponseTypesWithDefault(nil) if serviceErr != nil { return dtos.AccountCredentialsDTO{}, serviceErr @@ -452,14 +452,13 @@ func (s *Services) CreateAccountCredentials( var secretID, secret string var exp time.Time ccID, secretID, secret, exp, serviceErr = s.clientCredentialsSecret(ctx, qrs, clientCredentialsSecretOptions{ - requestID: opts.RequestID, - accountID: accountID, - storageMode: mapCCSecretStorageMode(opts.AuthMethod), - expiresIn: s.appCCExpDays, - usage: database.CredentialsUsageAccount, - dekFN: s.BuildGetEncAccountDEKfn(ctx, BuildGetEncAccountDEKOptions{ + requestID: opts.RequestID, + accountID: accountID, + expiresIn: s.appCCExpDays, + usage: database.CredentialsUsageAccount, + dekFN: s.BuildGetEncGlobalDEKFn(ctx, BuildGetGlobalDEKFnOptions{ RequestID: opts.RequestID, - AccountID: accountID, + Queries: qrs, }), }) if serviceErr != nil { @@ -905,7 +904,6 @@ type createAccountCredentialsSecretOptions struct { requestID string accountID int32 accountPublicID uuid.UUID - storageMode database.SecretStorageMode accountCredentialsID int32 } @@ -931,14 +929,13 @@ func (s *Services) createAccountCredentialsSecret( }() ccID, secretID, secret, exp, serviceErr := s.clientCredentialsSecret(ctx, qrs, clientCredentialsSecretOptions{ - requestID: opts.requestID, - accountID: opts.accountID, - storageMode: opts.storageMode, - expiresIn: s.appCCExpDays, - usage: database.CredentialsUsageAccount, - dekFN: s.BuildGetEncAccountDEKfn(ctx, BuildGetEncAccountDEKOptions{ + requestID: opts.requestID, + accountID: opts.accountID, + expiresIn: s.appCCExpDays, + usage: database.CredentialsUsageAccount, + dekFN: s.BuildGetEncGlobalDEKFn(ctx, BuildGetGlobalDEKFnOptions{ RequestID: opts.requestID, - AccountID: opts.accountID, + Queries: qrs, }), }) if serviceErr != nil { @@ -994,7 +991,6 @@ func (s *Services) rotateAccountCredentialsSecret( requestID: opts.requestID, accountID: opts.accountID, accountPublicID: opts.accountPublicID, - storageMode: mapCCSecretStorageMode(string(opts.authMethod)), accountCredentialsID: opts.accountCredentialsID, }) } @@ -1005,7 +1001,6 @@ func (s *Services) rotateAccountCredentialsSecret( requestID: opts.requestID, accountID: opts.accountID, accountPublicID: opts.accountPublicID, - storageMode: currentSecret.StorageMode, accountCredentialsID: opts.accountCredentialsID, }) } @@ -1201,7 +1196,8 @@ func (s *Services) ListAccountCredentialsSecretsOrKeys( }) } if accountCredentialsDTO.TokenEndpointAuthMethod == database.AuthMethodClientSecretBasic || - accountCredentialsDTO.TokenEndpointAuthMethod == database.AuthMethodClientSecretPost { + accountCredentialsDTO.TokenEndpointAuthMethod == database.AuthMethodClientSecretPost || + accountCredentialsDTO.TokenEndpointAuthMethod == database.AuthMethodClientSecretJwt { return s.listAccountCredentialsSecrets(ctx, listAccountCredentialsSecretsOptions{ requestID: opts.RequestID, accountCredentialsID: accountCredentialsDTO.ID(), @@ -1326,7 +1322,8 @@ func (s *Services) GetAccountCredentialsSecretOrKey( }) } if accountCredentialsDTO.TokenEndpointAuthMethod == database.AuthMethodClientSecretBasic || - accountCredentialsDTO.TokenEndpointAuthMethod == database.AuthMethodClientSecretPost { + accountCredentialsDTO.TokenEndpointAuthMethod == database.AuthMethodClientSecretPost || + accountCredentialsDTO.TokenEndpointAuthMethod == database.AuthMethodClientSecretJwt { return s.getAccountCredentialsSecretByID(ctx, getAccountCredentialsSecretByIDOptions{ requestID: opts.RequestID, accountCredentialsID: accountCredentialsDTO.ID(), diff --git a/idp/internal/services/account_credentials_registration.go b/idp/internal/services/account_credentials_registration.go index 75f7db9..fff2219 100644 --- a/idp/internal/services/account_credentials_registration.go +++ b/idp/internal/services/account_credentials_registration.go @@ -537,14 +537,13 @@ func (s *Services) CreateAccountCredentialsRegistration( var secretID, secret string var exp time.Time ccID, secretID, secret, exp, serviceErr = s.clientCredentialsSecret(ctx, qrs, clientCredentialsSecretOptions{ - requestID: opts.RequestID, - accountID: accountID, - storageMode: mapCCSecretStorageMode(string(tokenEndpointAuthMethod)), - expiresIn: s.appCCExpDays, - usage: database.CredentialsUsageAccount, - dekFN: s.BuildGetEncAccountDEKfn(ctx, BuildGetEncAccountDEKOptions{ + requestID: opts.RequestID, + accountID: accountID, + expiresIn: s.appCCExpDays, + usage: database.CredentialsUsageAccount, + dekFN: s.BuildGetEncGlobalDEKFn(ctx, BuildGetGlobalDEKFnOptions{ RequestID: opts.RequestID, - AccountID: accountID, + Queries: qrs, }), }) if serviceErr != nil { diff --git a/idp/internal/services/app_dynamic_registration.go b/idp/internal/services/app_dynamic_registration.go index ce9d960..c980004 100644 --- a/idp/internal/services/app_dynamic_registration.go +++ b/idp/internal/services/app_dynamic_registration.go @@ -668,11 +668,10 @@ func (s *Services) CreateAppCredentialsRegistration( var secret string var exp time.Time ccID, _, secret, exp, serviceErr = s.clientCredentialsSecret(ctx, qrs, clientCredentialsSecretOptions{ - requestID: opts.RequestID, - accountID: opts.AccountID, - storageMode: mapCCSecretStorageMode(string(tokenEndpointAuthMethod)), - expiresIn: s.appCCExpDays, - usage: database.CredentialsUsageApp, + requestID: opts.RequestID, + accountID: opts.AccountID, + expiresIn: s.appCCExpDays, + usage: database.CredentialsUsageApp, dekFN: s.BuildGetEncAccountDEKfn(ctx, BuildGetEncAccountDEKOptions{ RequestID: opts.RequestID, AccountID: opts.AccountID, diff --git a/idp/internal/services/apps.go b/idp/internal/services/apps.go index bf403bc..c7fb548 100644 --- a/idp/internal/services/apps.go +++ b/idp/internal/services/apps.go @@ -987,11 +987,10 @@ func (s *Services) CreateWebApp( var secretID, secret string var exp time.Time ccID, secretID, secret, exp, serviceErr = s.clientCredentialsSecret(ctx, qrs, clientCredentialsSecretOptions{ - requestID: opts.RequestID, - accountID: accountID, - storageMode: mapCCSecretStorageMode(opts.AuthMethod), - expiresIn: s.appCCExpDays, - usage: database.CredentialsUsageApp, + requestID: opts.RequestID, + accountID: accountID, + expiresIn: s.appCCExpDays, + usage: database.CredentialsUsageApp, dekFN: s.BuildGetEncAccountDEKfn(ctx, BuildGetEncAccountDEKOptions{ RequestID: opts.RequestID, AccountID: accountID, @@ -1271,11 +1270,10 @@ func (s *Services) CreateBackendApp( var secretID, secret string var exp time.Time ccID, secretID, secret, exp, serviceErr = s.clientCredentialsSecret(ctx, qrs, clientCredentialsSecretOptions{ - requestID: opts.RequestID, - accountID: accountID, - storageMode: mapCCSecretStorageMode(opts.AuthMethod), - expiresIn: s.appCCExpDays, - usage: database.CredentialsUsageApp, + requestID: opts.RequestID, + accountID: accountID, + expiresIn: s.appCCExpDays, + usage: database.CredentialsUsageApp, dekFN: s.BuildGetEncAccountDEKfn(ctx, BuildGetEncAccountDEKOptions{ RequestID: opts.RequestID, AccountID: accountID, @@ -1679,11 +1677,10 @@ func (s *Services) CreateServiceApp( var secretID, secret string var exp time.Time ccID, secretID, secret, exp, serviceErr = s.clientCredentialsSecret(ctx, qrs, clientCredentialsSecretOptions{ - requestID: opts.RequestID, - accountID: accountID, - storageMode: mapCCSecretStorageMode(opts.AuthMethod), - expiresIn: s.appCCExpDays, - usage: database.CredentialsUsageApp, + requestID: opts.RequestID, + accountID: accountID, + expiresIn: s.appCCExpDays, + usage: database.CredentialsUsageApp, dekFN: s.BuildGetEncAccountDEKfn(ctx, BuildGetEncAccountDEKOptions{ RequestID: opts.RequestID, AccountID: accountID, @@ -1965,11 +1962,10 @@ func (s *Services) CreateMCPApp( var secretID, secret string var exp time.Time ccID, secretID, secret, exp, serviceErr = s.clientCredentialsSecret(ctx, qrs, clientCredentialsSecretOptions{ - requestID: opts.RequestID, - accountID: accountID, - storageMode: mapCCSecretStorageMode(opts.AuthMethod), - expiresIn: s.appCCExpDays, - usage: database.CredentialsUsageApp, + requestID: opts.RequestID, + accountID: accountID, + expiresIn: s.appCCExpDays, + usage: database.CredentialsUsageApp, dekFN: s.BuildGetEncAccountDEKfn(ctx, BuildGetEncAccountDEKOptions{ RequestID: opts.RequestID, AccountID: accountID, @@ -3280,11 +3276,10 @@ func (s *Services) rotateAppSecret( }() id, secretID, secret, exp, serviceErr := s.clientCredentialsSecret(ctx, qrs, clientCredentialsSecretOptions{ - requestID: opts.requestID, - accountID: opts.accountID, - storageMode: mapCCSecretStorageMode(string(opts.authMethod)), - expiresIn: s.accountCCExpDays, - usage: database.CredentialsUsageApp, + requestID: opts.requestID, + accountID: opts.accountID, + expiresIn: s.accountCCExpDays, + usage: database.CredentialsUsageApp, dekFN: s.BuildGetEncAccountDEKfn(ctx, BuildGetEncAccountDEKOptions{ RequestID: opts.requestID, AccountID: opts.accountID, diff --git a/idp/internal/services/client_credentials.go b/idp/internal/services/client_credentials.go index 02dcd6d..1582954 100644 --- a/idp/internal/services/client_credentials.go +++ b/idp/internal/services/client_credentials.go @@ -12,17 +12,15 @@ import ( "crypto/ed25519" "crypto/elliptic" "crypto/rand" + "crypto/subtle" "encoding/base64" "encoding/json" - "errors" "fmt" "log/slog" "strings" "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/crypto" "github.com/tugascript/devlogs/idp/internal/providers/database" @@ -42,12 +40,11 @@ const ( ) type clientCredentialsSecretOptions struct { - requestID string - accountID int32 - storageMode database.SecretStorageMode - expiresIn time.Duration - usage database.CredentialsUsage - dekFN crypto.GetDEKtoEncrypt + requestID string + accountID int32 + expiresIn time.Duration + usage database.CredentialsUsage + dekFN crypto.GetDEKtoEncrypt } func (s *Services) clientCredentialsSecret( @@ -58,7 +55,6 @@ func (s *Services) clientCredentialsSecret( logger := s.buildLogger(opts.requestID, clientCredentialsLocation, "clientCredentialsSecret").With( "accountId", opts.accountID, "usage", opts.usage, - "storageMode", opts.storageMode, ) logger.InfoContext(ctx, "Generating client credentials secret...") @@ -70,31 +66,6 @@ func (s *Services) clientCredentialsSecret( } exp := time.Now().Add(opts.expiresIn) - if opts.storageMode == database.SecretStorageModeHashed { - hashedSecret, err := utils.Argon2HashString(secret) - if err != nil { - logger.ErrorContext(ctx, "Failed to hash secret", "error", err) - return 0, "", "", time.Time{}, exceptions.NewInternalServerError() - } - - id, err := qrs.CreateCredentialsSecret(ctx, database.CreateCredentialsSecretParams{ - AccountID: opts.accountID, - SecretID: secretID, - ClientSecret: hashedSecret, - StorageMode: opts.storageMode, - DekKid: pgtype.Text{Valid: false}, - ExpiresAt: exp, - Usage: opts.usage, - }) - if err != nil { - logger.ErrorContext(ctx, "Failed to create credentials secret", "error", err) - return 0, "", "", time.Time{}, exceptions.FromDBError(err) - } - - logger.InfoContext(ctx, "Created credentials secret with hashed storage mode", "secretId", secretID) - return id, secretID, secret, exp, nil - } - dekID, encryptedSecret, serviceErr := s.crypto.EncryptWithDEK(ctx, crypto.EncryptWithDEKOptions{ RequestID: opts.requestID, GetDEKfn: opts.dekFN, @@ -109,8 +80,7 @@ func (s *Services) clientCredentialsSecret( AccountID: opts.accountID, SecretID: secretID, ClientSecret: encryptedSecret, - StorageMode: opts.storageMode, - DekKid: pgtype.Text{String: dekID, Valid: true}, + DekKid: dekID, ExpiresAt: exp, Usage: opts.usage, }) @@ -326,20 +296,14 @@ func (s *Services) BuildGetAccountClientCredentialsSecretFn( logger.ErrorContext(ctx, "Failed to find client credentials secret", "error", err) return nil, err } - if secret.StorageMode != database.SecretStorageModeEncrypted { - logger.WarnContext(ctx, "Client credentials secret is not encrypted", "storageMode", secret.StorageMode) - return nil, errors.New("client credentials secret is not encrypted") - } decryptedSecret, serviceErr := s.crypto.DecryptWithDEK(ctx, crypto.DecryptWithDEKOptions{ RequestID: requestID, - GetDecryptDEKfn: s.BuildGetDecAccountDEKFn(ctx, BuildGetDecAccountDEKFnOptions{ + GetDecryptDEKfn: s.BuildGetGlobalDecDEKFn(ctx, BuildGetGlobalDEKFnOptions{ RequestID: requestID, - AccountID: secret.AccountID, }), - GetEncryptDEKfn: s.BuildGetEncAccountDEKfn(ctx, BuildGetEncAccountDEKOptions{ + GetEncryptDEKfn: s.BuildGetEncGlobalDEKFn(ctx, BuildGetGlobalDEKFnOptions{ RequestID: requestID, - AccountID: secret.AccountID, }), StoreReEncryptedDataFn: func( _ crypto.EntityID, @@ -351,7 +315,7 @@ func (s *Services) BuildGetAccountClientCredentialsSecretFn( database.UpdateCredentialsSecretClientSecretParams{ ID: secret.ID, ClientSecret: ciphertext, - DekKid: pgtype.Text{Valid: true, String: dekID}, + DekKid: dekID, }, ); err != nil { logger.ErrorContext(ctx, "Failed to update client credentials secret with re-encrypted DEK", @@ -448,3 +412,82 @@ func (s *Services) ProcessClientCredentialsLoginData( ) return clientID, decodedSlice[1], database.AuthMethodClientSecretBasic, nil } + +type processClientCredentialsSecretOptions struct { + requestID string + clientSecret string +} + +func (s *Services) processClientCredentialsSecret( + ctx context.Context, + opts processClientCredentialsSecretOptions, +) (string, []byte, *exceptions.ServiceError) { + logger := s.buildLogger(opts.requestID, oauthLocation, "processClientCredentialsSecret") + logger.InfoContext(ctx, "Processing client credentials secret...") + secret := strings.TrimSpace(opts.clientSecret) + if secret == "" { + logger.WarnContext(ctx, "Client secret is empty") + return "", nil, exceptions.NewUnauthorizedError() + } + + parts := strings.Split(secret, ".") + if len(parts) != 2 { + logger.WarnContext(ctx, "Client secret must be in the format 'secretID.secretValue'") + return "", nil, exceptions.NewUnauthorizedError() + } + + idLen := len(parts[0]) + if idLen != 22 { + logger.WarnContext(ctx, "Client secret ID must be 22 characters long", "secretIdLength", idLen) + return "", nil, exceptions.NewUnauthorizedError() + } + + b64Secret := parts[1] + secretLen := len(b64Secret) + if secretLen < 40 { + logger.WarnContext(ctx, "Client secret value must be at least 40 characters long", "secretValueLength", secretLen) + return "", nil, exceptions.NewUnauthorizedError() + } + + secretBytes, err := utils.DecodeBase64Secret(b64Secret) + if err != nil { + logger.WarnContext(ctx, "Client secret value is not valid base64", "secretValue", b64Secret) + return "", nil, exceptions.NewUnauthorizedError() + } + + logger.InfoContext(ctx, "Successfully processed client credentials secret", "secretID", parts[0]) + return parts[0], secretBytes, nil +} + +type verifyClientCredentialsSecretOptions struct { + requestID string + secret []byte + decryptWithDekOpts crypto.DecryptWithDEKOptions +} + +func (s *Services) verifyClientCredentialsSecret( + ctx context.Context, + opts verifyClientCredentialsSecretOptions, +) *exceptions.ServiceError { + logger := s.buildLogger(opts.requestID, oauthLocation, "verifyClientCredentialsSecret") + logger.InfoContext(ctx, "Verifying client credentials secret...") + + decryptedSecret, serviceErr := s.crypto.DecryptWithDEK(ctx, opts.decryptWithDekOpts) + if serviceErr != nil { + logger.ErrorContext(ctx, "Failed to decrypt client credentials secret", "serviceError", serviceErr) + return serviceErr + } + + decodedSecret, err := utils.DecodeBase64Secret(decryptedSecret) + if err != nil { + logger.ErrorContext(ctx, "Failed to decode base64 encoded decrypted secret", "error", err) + return exceptions.NewInternalServerError() + } + + if subtle.ConstantTimeCompare(opts.secret, decodedSecret) != 1 { + logger.WarnContext(ctx, "Client Credentials secrets do not match") + return exceptions.NewUnauthorizedError() + } + + return nil +} diff --git a/idp/internal/services/helpers.go b/idp/internal/services/helpers.go index 39d7de9..e069b59 100644 --- a/idp/internal/services/helpers.go +++ b/idp/internal/services/helpers.go @@ -214,14 +214,6 @@ func mapDomain(baseURI string, domain string) (string, *exceptions.ServiceError) return host, nil } -func mapCCSecretStorageMode(authMethod string) database.SecretStorageMode { - if authMethod == AuthMethodClientSecretJWT { - return database.SecretStorageModeEncrypted - } - - return database.SecretStorageModeHashed -} - func hashChallenge(challenge, challengeMethod string) (string, *exceptions.ServiceError) { if challengeMethod == "" { return utils.Sha256HashBase64(challenge), nil diff --git a/idp/internal/services/oauth.go b/idp/internal/services/oauth.go index b9cdb34..c2e84cd 100644 --- a/idp/internal/services/oauth.go +++ b/idp/internal/services/oauth.go @@ -16,7 +16,6 @@ import ( "github.com/golang-jwt/jwt/v5" "github.com/google/uuid" - "github.com/tugascript/devlogs/idp/internal/exceptions" "github.com/tugascript/devlogs/idp/internal/providers/cache" "github.com/tugascript/devlogs/idp/internal/providers/crypto" @@ -835,39 +834,6 @@ func (s *Services) JWTBearerAccountLogin( }) } -type processAccountClientSecretOptions struct { - requestID string - clientSecret string -} - -func (s *Services) processAccountClientSecret( - ctx context.Context, - opts processAccountClientSecretOptions, -) (string, string, *exceptions.ServiceError) { - logger := s.buildLogger(opts.requestID, oauthLocation, "processAccountClientSecret") - logger.InfoContext(ctx, "Processing account client secret...") - secret := strings.TrimSpace(opts.clientSecret) - if secret == "" { - logger.WarnContext(ctx, "Client secret is empty") - return "", "", exceptions.NewUnauthorizedError() - } - - parts := strings.Split(secret, ".") - if len(parts) != 2 { - logger.WarnContext(ctx, "Client secret must be in the format 'secretID.secretValue'") - return "", "", exceptions.NewUnauthorizedError() - } - - idLen := len(parts[0]) - if idLen != 22 { - logger.WarnContext(ctx, "Client secret ID must be 22 characters long", "secretIdLength", idLen) - return "", "", exceptions.NewUnauthorizedError() - } - - logger.InfoContext(ctx, "Successfully processed account client secret", "secretID", parts[0]) - return parts[0], parts[1], nil -} - type ClientCredentialsAccountLoginOptions struct { RequestID string ClientID string @@ -886,12 +852,12 @@ func (s *Services) ClientCredentialsAccountLogin( ) logger.InfoContext(ctx, "Client credentials account logging in...") - secretID, secret, serviceErr := s.processAccountClientSecret(ctx, processAccountClientSecretOptions{ + secretID, secret, serviceErr := s.processClientCredentialsSecret(ctx, processClientCredentialsSecretOptions{ requestID: opts.RequestID, clientSecret: opts.ClientSecret, }) if serviceErr != nil { - logger.WarnContext(ctx, "Failed to process account client secret", "serviceError", serviceErr) + logger.WarnContext(ctx, "Failed to process client credentials secret", "serviceError", serviceErr) return dtos.AuthDTO{}, serviceErr } @@ -932,14 +898,45 @@ func (s *Services) ClientCredentialsAccountLogin( return dtos.AuthDTO{}, serviceErr } - verified, err := utils.Argon2CompareHash(secret, secretEnt.ClientSecret) - if err != nil { - logger.ErrorContext(ctx, "Failed to compare account credentials secret", "error", err) - return dtos.AuthDTO{}, exceptions.NewInternalServerError() - } - if !verified { - logger.WarnContext(ctx, "Account credentials secret is invalid") - return dtos.AuthDTO{}, exceptions.NewUnauthorizedError() + if serviceErr := s.verifyClientCredentialsSecret(ctx, verifyClientCredentialsSecretOptions{ + requestID: opts.RequestID, + secret: secret, + decryptWithDekOpts: crypto.DecryptWithDEKOptions{ + RequestID: opts.RequestID, + GetDecryptDEKfn: s.BuildGetGlobalDecDEKFn(ctx, BuildGetGlobalDEKFnOptions{ + RequestID: opts.RequestID, + }), + GetEncryptDEKfn: s.BuildGetEncGlobalDEKFn(ctx, BuildGetGlobalDEKFnOptions{ + RequestID: opts.RequestID, + }), + StoreReEncryptedDataFn: func( + _ crypto.EntityID, + dekID crypto.DEKID, + ciphertext crypto.DEKCiphertext, + ) *exceptions.ServiceError { + if err := s.database.UpdateCredentialsSecretClientSecret( + ctx, + database.UpdateCredentialsSecretClientSecretParams{ + ID: secretEnt.ID, + ClientSecret: ciphertext, + DekKid: dekID, + }, + ); err != nil { + logger.ErrorContext(ctx, "Failed to update client credentials secret with re-encrypted DEK", + "error", err, + ) + return exceptions.FromDBError(err) + } + + logger.InfoContext(ctx, "Successfully updated client credentials secret with re-encrypted DEK") + return nil + }, + EntityID: secretID, + Ciphertext: secretEnt.ClientSecret, + }, + }); serviceErr != nil { + logger.WarnContext(ctx, "Failed to verify client credentials secret", "serviceError", serviceErr) + return dtos.AuthDTO{}, serviceErr } accountDTO, serviceErr := s.GetAccountByID(ctx, GetAccountByIDOptions{ diff --git a/idp/internal/services/oauth_dynamic_registration_config.go b/idp/internal/services/oauth_dynamic_registration_config.go index 25619f9..4bafeca 100644 --- a/idp/internal/services/oauth_dynamic_registration_config.go +++ b/idp/internal/services/oauth_dynamic_registration_config.go @@ -2,14 +2,18 @@ package services import ( "context" + "crypto/subtle" + "fmt" "net/url" "strings" + "time" "github.com/google/uuid" - "github.com/tugascript/devlogs/idp/internal/exceptions" + "github.com/tugascript/devlogs/idp/internal/providers/crypto" "github.com/tugascript/devlogs/idp/internal/providers/database" "github.com/tugascript/devlogs/idp/internal/services/dtos" + "github.com/tugascript/devlogs/idp/internal/utils" ) const oauthDynamicRegistrationConfigLocation = "oauth_dynamic_registration_config" @@ -22,6 +26,146 @@ type GetRegisteredClientOptions struct { HostUsername string } +func (s *Services) getActiveAccountCredentialSecretAndKey( + ctx context.Context, + requestID string, + accountCredID int32, + authMethod database.AuthMethod, +) (string, time.Time, utils.JWK, *exceptions.ServiceError) { + switch authMethod { + case database.AuthMethodClientSecretBasic, database.AuthMethodClientSecretPost, database.AuthMethodClientSecretJwt: + secretEnt, err := s.database.FindCurrentAccountCredentialSecretByAccountCredentialID(ctx, accountCredID) + if err != nil { + return "", time.Time{}, nil, nil + } + decryptedSecret, decryptErr := s.crypto.DecryptWithDEK(ctx, crypto.DecryptWithDEKOptions{ + RequestID: requestID, + GetDecryptDEKfn: s.BuildGetGlobalDecDEKFn(ctx, BuildGetGlobalDEKFnOptions{ + RequestID: requestID, + }), + GetEncryptDEKfn: s.BuildGetEncGlobalDEKFn(ctx, BuildGetGlobalDEKFnOptions{ + RequestID: requestID, + }), + StoreReEncryptedDataFn: func( + _ crypto.EntityID, + dekID crypto.DEKID, + ciphertext crypto.DEKCiphertext, + ) *exceptions.ServiceError { + if err := s.database.UpdateCredentialsSecretClientSecret( + ctx, + database.UpdateCredentialsSecretClientSecretParams{ + ID: secretEnt.ID, + ClientSecret: ciphertext, + DekKid: dekID, + }, + ); err != nil { + return exceptions.FromDBError(err) + } + return nil + }, + EntityID: secretEnt.SecretID, + Ciphertext: secretEnt.ClientSecret, + }) + if decryptErr != nil { + return "", time.Time{}, nil, decryptErr + } + return fmt.Sprintf("%s.%s", secretEnt.SecretID, decryptedSecret), secretEnt.ExpiresAt, nil, nil + case database.AuthMethodPrivateKeyJwt: + keyEnt, err := s.database.FindCurrentAccountCredentialKeyByAccountCredentialID(ctx, accountCredID) + if err != nil { + return "", time.Time{}, nil, nil + } + jwk, _ := utils.JsonToJWK(keyEnt.PublicKey) + return "", keyEnt.ExpiresAt, jwk, nil + default: + return "", time.Time{}, nil, nil + } +} + +func (s *Services) getActiveAppSecretAndKey( + ctx context.Context, + requestID string, + accountID int32, + appID int32, + authMethod database.AuthMethod, +) (string, time.Time, utils.JWK, *exceptions.ServiceError) { + switch authMethod { + case database.AuthMethodClientSecretBasic, database.AuthMethodClientSecretPost, database.AuthMethodClientSecretJwt: + secrets, err := s.database.FindPaginatedAppSecretsByAppID(ctx, database.FindPaginatedAppSecretsByAppIDParams{ + AppID: appID, + Offset: 0, + Limit: 10, + }) + if err != nil || len(secrets) == 0 { + return "", time.Time{}, nil, nil + } + var activeSecret *database.CredentialsSecret + now := time.Now() + for i := range secrets { + if !secrets[i].IsRevoked && secrets[i].ExpiresAt.After(now) { + activeSecret = &secrets[i] + break + } + } + if activeSecret == nil { + return "", time.Time{}, nil, nil + } + decryptedSecret, decryptErr := s.crypto.DecryptWithDEK(ctx, crypto.DecryptWithDEKOptions{ + RequestID: requestID, + GetDecryptDEKfn: s.BuildGetDecAccountDEKFn(ctx, BuildGetDecAccountDEKFnOptions{ + RequestID: requestID, + AccountID: accountID, + }), + GetEncryptDEKfn: s.BuildGetEncAccountDEKfn(ctx, BuildGetEncAccountDEKOptions{ + RequestID: requestID, + AccountID: accountID, + }), + StoreReEncryptedDataFn: func( + _ crypto.EntityID, + dekID crypto.DEKID, + ciphertext crypto.DEKCiphertext, + ) *exceptions.ServiceError { + if err := s.database.UpdateCredentialsSecretClientSecret( + ctx, + database.UpdateCredentialsSecretClientSecretParams{ + ID: activeSecret.ID, + ClientSecret: ciphertext, + DekKid: dekID, + }, + ); err != nil { + return exceptions.FromDBError(err) + } + return nil + }, + EntityID: activeSecret.SecretID, + Ciphertext: activeSecret.ClientSecret, + }) + if decryptErr != nil { + return "", time.Time{}, nil, decryptErr + } + return fmt.Sprintf("%s.%s", activeSecret.SecretID, decryptedSecret), activeSecret.ExpiresAt, nil, nil + case database.AuthMethodPrivateKeyJwt: + keys, err := s.database.FindPaginatedAppKeysByAppID(ctx, database.FindPaginatedAppKeysByAppIDParams{ + AppID: appID, + Offset: 0, + Limit: 10, + }) + if err != nil || len(keys) == 0 { + return "", time.Time{}, nil, nil + } + now := time.Now() + for i := range keys { + if !keys[i].IsRevoked && keys[i].ExpiresAt.After(now) { + jwk, _ := utils.JsonToJWK(keys[i].PublicKey) + return "", keys[i].ExpiresAt, jwk, nil + } + } + return "", time.Time{}, nil, nil + default: + return "", time.Time{}, nil, nil + } +} + func (s *Services) GetRegisteredAccountCredentials( ctx context.Context, opts GetRegisteredClientOptions, @@ -30,12 +174,38 @@ func (s *Services) GetRegisteredAccountCredentials( if serviceErr != nil { return nil, serviceErr } - dto, serviceErr := dtos.MapRegisteredAccountCredentials(row, "", "", row.CreatedAt, nil) + secret, exp, key, serviceErr := s.getActiveAccountCredentialSecretAndKey( + ctx, + opts.RequestID, + row.ID, + row.TokenEndpointAuthMethod, + ) + if serviceErr != nil { + return nil, serviceErr + } + account, serviceErr := s.GetAccountByPublicID(ctx, GetAccountByPublicIDOptions{ + RequestID: opts.RequestID, + PublicID: opts.AccountPublicID, + }) + if serviceErr != nil { + return nil, serviceErr + } + token, serviceErr := s.CreateAccountCredentialsRegistrationAccessToken(ctx, CreateAccountCredentialsRegistrationAccessTokenOptions{ + RequestID: opts.RequestID, + AccountPublicID: opts.AccountPublicID, + AccountVersion: account.Version(), + ClientID: row.ClientID, + BackendDomain: opts.BackendDomain, + }) + if serviceErr != nil { + return nil, serviceErr + } + dto, serviceErr := dtos.MapRegisteredAccountCredentials(row, "", secret, exp, key) if serviceErr != nil { return nil, exceptions.NewUnauthorizedError() } - dto.Registration.WithRegistrationAccess("", dtos.RegistrationClientURI(opts.BackendDomain, row.ClientID)) - return dto.Registration.WithoutSecrets(), nil + dto.Registration.WithRegistrationAccess(token, dtos.RegistrationClientURI(opts.BackendDomain, row.ClientID)) + return dto.Registration, nil } func (s *Services) GetRegisteredApp( @@ -46,10 +216,33 @@ func (s *Services) GetRegisteredApp( if serviceErr != nil { return nil, serviceErr } - dto := dtos.MapRegisteredApp(row, "", "", row.CreatedAt, nil) + secret, exp, key, serviceErr := s.getActiveAppSecretAndKey( + ctx, opts.RequestID, row.AccountID, row.ID, row.TokenEndpointAuthMethod, + ) + if serviceErr != nil { + return nil, serviceErr + } + account, serviceErr := s.GetAccountByPublicID(ctx, GetAccountByPublicIDOptions{ + RequestID: opts.RequestID, + PublicID: opts.AccountPublicID, + }) + if serviceErr != nil { + return nil, serviceErr + } + token, serviceErr := s.CreateAppCredentialsRegistrationAccessToken(ctx, CreateAppCredentialsRegistrationAccessTokenOptions{ + RequestID: opts.RequestID, + AccountPublicID: opts.AccountPublicID, + AccountVersion: account.Version(), + ClientID: row.ClientID, + BackendDomain: opts.BackendDomain, + }) + if serviceErr != nil { + return nil, serviceErr + } + dto := dtos.MapRegisteredApp(row, "", secret, exp, key) issuer := dynamicRegistrationIssuerDomain(opts.HostUsername, opts.BackendDomain) - dto.Registration.WithRegistrationAccess("", dtos.RegistrationClientURI(issuer, row.ClientID)) - return dto.Registration.WithoutSecrets(), nil + dto.Registration.WithRegistrationAccess(token, dtos.RegistrationClientURI(issuer, row.ClientID)) + return dto.Registration, nil } func (s *Services) DeleteRegisteredAccountCredentials( @@ -88,7 +281,7 @@ func (s *Services) findRegisteredAccountCredential( ClientID: opts.ClientID, }) if err != nil { - return nil, exceptions.NewUnauthorizedError() + return nil, exceptions.NewError(exceptions.OAuthErrorInvalidToken, "client not found") } return &row, nil } @@ -102,14 +295,16 @@ func (s *Services) findRegisteredApp( ClientID: opts.ClientID, }) if err != nil { - return nil, exceptions.NewUnauthorizedError() + return nil, exceptions.NewError(exceptions.OAuthErrorInvalidToken, "client not found") } return &row, nil } type UpdateRegisteredClientOptions struct { CreateAccountCredentialsRegistrationOptions - ClientID string + ClientID string + SubmittedClientID string + SubmittedClientSecret string } func (s *Services) UpdateRegisteredAccountCredentials( @@ -117,12 +312,29 @@ func (s *Services) UpdateRegisteredAccountCredentials( opts UpdateRegisteredClientOptions, ) (*dtos.ClientRegistrationDTO, *exceptions.ServiceError) { logger := s.buildLogger(opts.RequestID, oauthDynamicRegistrationConfigLocation, "UpdateRegisteredAccountCredentials") + if opts.SubmittedClientID == "" || opts.SubmittedClientID != opts.ClientID { + return nil, exceptions.NewError(exceptions.OAuthErrorInvalidRequest, "client_id is required and must match the client making the request") + } existing, serviceErr := s.findRegisteredAccountCredential(ctx, GetRegisteredClientOptions{ RequestID: opts.RequestID, AccountPublicID: opts.AccountPublicID, ClientID: opts.ClientID, }) if serviceErr != nil { return nil, serviceErr } + currentSecret, exp, key, serviceErr := s.getActiveAccountCredentialSecretAndKey( + ctx, + opts.RequestID, + existing.ID, + existing.TokenEndpointAuthMethod, + ) + if serviceErr != nil { + return nil, serviceErr + } + if opts.SubmittedClientSecret != "" { + if currentSecret == "" || subtle.ConstantTimeCompare([]byte(opts.SubmittedClientSecret), []byte(currentSecret)) != 1 { + return nil, exceptions.NewError(exceptions.OAuthErrorInvalidClient, "invalid client secret") + } + } data := registrationDataFromAccountOptions(opts.CreateAccountCredentialsRegistrationOptions) data.ApplicationType = string(existing.CredentialsType) data, preparationErr := s.prepareDynamicRegistration(ctx, prepareDynamicRegistrationOptions{ @@ -181,18 +393,30 @@ func (s *Services) UpdateRegisteredAccountCredentials( 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) + dto, serviceErr := dtos.MapRegisteredAccountCredentials(&updated, opts.SoftwareStatement, currentSecret, exp, key) if serviceErr != nil { return nil, serviceErr } - dto.Registration.WithRegistrationAccess("", dtos.RegistrationClientURI(opts.BackendDomain, updated.ClientID)) - return dto.Registration.WithoutSecrets(), nil + token, serviceErr := s.CreateAccountCredentialsRegistrationAccessToken(ctx, CreateAccountCredentialsRegistrationAccessTokenOptions{ + RequestID: opts.RequestID, + AccountPublicID: opts.AccountPublicID, + AccountVersion: opts.AccountVersion, + ClientID: updated.ClientID, + BackendDomain: opts.BackendDomain, + }) + if serviceErr != nil { + return nil, serviceErr + } + dto.Registration.WithRegistrationAccess(token, dtos.RegistrationClientURI(opts.BackendDomain, updated.ClientID)) + return dto.Registration, nil } type UpdateRegisteredAppOptions struct { CreateAppCredentialsRegistrationOptions - ClientID string - HostUsername string + ClientID string + SubmittedClientID string + SubmittedClientSecret string + HostUsername string } func (s *Services) UpdateRegisteredApp( @@ -200,9 +424,12 @@ func (s *Services) UpdateRegisteredApp( opts UpdateRegisteredAppOptions, ) (*dtos.ClientRegistrationDTO, *exceptions.ServiceError) { logger := s.buildLogger(opts.RequestID, oauthDynamicRegistrationConfigLocation, "UpdateRegisteredApp") + if opts.SubmittedClientID == "" || opts.SubmittedClientID != opts.ClientID { + return nil, exceptions.NewError(exceptions.OAuthErrorInvalidRequest, "client_id is required and must match the client making the request") + } account, serviceErr := s.GetAccountByID(ctx, GetAccountByIDOptions{RequestID: opts.RequestID, ID: opts.AccountID}) if serviceErr != nil { - return nil, exceptions.NewUnauthorizedError() + return nil, exceptions.NewError(exceptions.OAuthErrorInvalidToken, "account not found") } existing, serviceErr := s.findRegisteredApp(ctx, GetRegisteredClientOptions{ RequestID: opts.RequestID, AccountPublicID: account.PublicID, ClientID: opts.ClientID, @@ -210,6 +437,17 @@ func (s *Services) UpdateRegisteredApp( if serviceErr != nil { return nil, serviceErr } + currentSecret, exp, key, serviceErr := s.getActiveAppSecretAndKey( + ctx, opts.RequestID, existing.AccountID, existing.ID, existing.TokenEndpointAuthMethod, + ) + if serviceErr != nil { + return nil, serviceErr + } + if opts.SubmittedClientSecret != "" { + if currentSecret == "" || subtle.ConstantTimeCompare([]byte(opts.SubmittedClientSecret), []byte(currentSecret)) != 1 { + return nil, exceptions.NewError(exceptions.OAuthErrorInvalidClient, "invalid client secret") + } + } data := registrationDataFromAppOptions(opts.CreateAppCredentialsRegistrationOptions) data.ApplicationType = string(existing.AppType) data, preparationErr := s.prepareDynamicRegistration(ctx, prepareDynamicRegistrationOptions{ @@ -267,10 +505,20 @@ func (s *Services) UpdateRegisteredApp( logger.ErrorContext(ctx, "Failed to update app", "error", err) return nil, exceptions.FromDBError(err) } - dto := dtos.MapRegisteredApp(&updated, opts.SoftwareStatement, "", updated.CreatedAt, nil) + dto := dtos.MapRegisteredApp(&updated, opts.SoftwareStatement, currentSecret, exp, key) + token, serviceErr := s.CreateAppCredentialsRegistrationAccessToken(ctx, CreateAppCredentialsRegistrationAccessTokenOptions{ + RequestID: opts.RequestID, + AccountPublicID: account.PublicID, + AccountVersion: account.Version(), + ClientID: updated.ClientID, + BackendDomain: opts.BackendDomain, + }) + if serviceErr != nil { + return nil, serviceErr + } issuer := dynamicRegistrationIssuerDomain(opts.HostUsername, opts.BackendDomain) - dto.Registration.WithRegistrationAccess("", dtos.RegistrationClientURI(issuer, updated.ClientID)) - return dto.Registration.WithoutSecrets(), nil + dto.Registration.WithRegistrationAccess(token, dtos.RegistrationClientURI(issuer, updated.ClientID)) + return dto.Registration, nil } func registrationDataFromAccountOptions(opts CreateAccountCredentialsRegistrationOptions) ApplicationRegistrationData { From 39f3c5a440bc0521ce0b15c64feed4bfd811391d Mon Sep 17 00:00:00 2001 From: Afonso Barracha Date: Tue, 22 Sep 2026 13:00:38 +1200 Subject: [PATCH 14/16] chore: fix contributing error --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3beb92d..8604edd 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -28,7 +28,7 @@ pull request: Please update the file headers for new files with your information, adhering to the Mozilla Public License 2.0 license: -```go +```md // Copyright (c) // // This Source Code Form is subject to the terms of the Mozilla Public From ecd0f8238a482f2004169bf1f064f66f76326169 Mon Sep 17 00:00:00 2001 From: Afonso Barracha Date: Tue, 22 Sep 2026 14:20:41 +1200 Subject: [PATCH 15/16] feat: save encrypted JWK private key --- idp/go.mod | 55 ++++----- idp/go.sum | 58 +++++++++ idp/internal/providers/crypto/jwk.go | 108 ++++++----------- idp/internal/providers/crypto/jwk_test.go | 114 ++++++++++++++++++ .../database/credentials_keys.sql.go | 10 +- .../database/queries/credentials_keys.sql | 6 +- idp/internal/services/client_credentials.go | 21 ++++ idp/internal/utils/jwk.go | 81 ++++--------- 8 files changed, 297 insertions(+), 156 deletions(-) create mode 100644 idp/internal/providers/crypto/jwk_test.go diff --git a/idp/go.mod b/idp/go.mod index eda413a..c7fde93 100644 --- a/idp/go.mod +++ b/idp/go.mod @@ -4,47 +4,47 @@ go 1.26.7 require ( github.com/biter777/countries v1.7.5 - github.com/go-faker/faker/v4 v4.6.1 - github.com/go-playground/validator/v10 v10.30.3 + github.com/go-faker/faker/v4 v4.12.0 + github.com/go-playground/validator/v10 v10.30.5 github.com/gofiber/fiber/v3 v3.5.0 github.com/gofiber/storage/redis/v3 v3.6.0 github.com/golang-jwt/jwt/v5 v5.3.1 github.com/google/uuid v1.6.0 github.com/h2non/gock v1.2.0 - github.com/jackc/pgx/v5 v5.10.0 + github.com/jackc/pgx/v5 v5.11.0 github.com/joho/godotenv v1.5.1 - github.com/openbao/openbao/api/auth/approle/v2 v2.6.0 - github.com/openbao/openbao/api/v2 v2.6.0 + github.com/openbao/openbao/api/auth/approle/v2 v2.7.0 + github.com/openbao/openbao/api/v2 v2.7.0 github.com/pquerna/otp v1.5.0 github.com/redis/go-redis/v9 v9.22.0 - golang.org/x/crypto v0.56.0 - golang.org/x/net v0.58.0 - golang.org/x/oauth2 v0.36.0 - golang.org/x/text v0.41.0 - google.golang.org/api v0.297.0 + golang.org/x/crypto v0.57.0 + golang.org/x/net v0.59.0 + golang.org/x/oauth2 v0.37.0 + golang.org/x/text v0.42.0 + google.golang.org/api v0.299.0 ) require ( - cloud.google.com/go/auth v0.23.2 // indirect + cloud.google.com/go/auth v0.23.3 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect - cloud.google.com/go/compute/metadata v0.9.0 // indirect - github.com/andybalholm/brotli v1.2.3 // indirect + cloud.google.com/go/compute/metadata v0.9.1 // indirect + github.com/andybalholm/brotli v1.2.4 // indirect github.com/boombuler/barcode v1.1.0 // indirect github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/felixge/httpsnoop v1.1.0 // indirect github.com/gabriel-vasile/mimetype v1.4.15 // indirect - github.com/go-jose/go-jose/v4 v4.1.4 // indirect + github.com/go-jose/go-jose/v4 v4.1.5 // indirect github.com/go-logr/logr v1.4.4 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-playground/locales v0.14.1 // indirect - github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/universal-translator v0.18.2 // indirect github.com/go-viper/mapstructure/v2 v2.5.0 // indirect - github.com/gofiber/schema v1.8.3 // indirect - github.com/gofiber/utils/v2 v2.4.1 // indirect - github.com/google/s2a-go v0.1.9 // indirect - github.com/googleapis/enterprise-certificate-proxy v0.3.21 // indirect - github.com/googleapis/gax-go/v2 v2.24.0 // indirect + github.com/gofiber/schema v1.8.7 // indirect + github.com/gofiber/utils/v2 v2.5.3 // indirect + github.com/google/s2a-go v0.1.10 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.22 // indirect + github.com/googleapis/gax-go/v2 v2.25.0 // indirect github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect @@ -62,21 +62,22 @@ require ( github.com/mattn/go-colorable v0.1.15 // indirect github.com/mattn/go-isatty v0.0.24 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/molecule-man/go-brrr v1.1.1 // indirect github.com/philhofer/fwd v1.2.0 // indirect github.com/ryanuber/go-glob v1.0.0 // indirect github.com/tinylib/msgp v1.6.4 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect - github.com/valyala/fasthttp v1.73.0 // indirect + github.com/valyala/fasthttp v1.74.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.71.0 // indirect go.opentelemetry.io/otel v1.46.0 // indirect go.opentelemetry.io/otel/metric v1.46.0 // indirect go.opentelemetry.io/otel/trace v1.46.0 // indirect - go.uber.org/atomic v1.11.0 // indirect - golang.org/x/sync v0.22.0 // indirect - golang.org/x/sys v0.47.0 // indirect - golang.org/x/time v0.15.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260831171406-18b4a7587f8a // indirect - google.golang.org/grpc v1.83.2 // indirect + go.uber.org/atomic v1.12.0 // indirect + golang.org/x/sync v0.23.0 // indirect + golang.org/x/sys v0.48.0 // indirect + golang.org/x/time v0.16.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260921155816-b14227669459 // indirect + google.golang.org/grpc v1.84.0 // indirect google.golang.org/protobuf v1.36.12 // indirect ) diff --git a/idp/go.sum b/idp/go.sum index 9b8f698..c846f11 100644 --- a/idp/go.sum +++ b/idp/go.sum @@ -1,9 +1,13 @@ cloud.google.com/go/auth v0.23.2 h1:pxSCpfiji41hpzpPdMCftEUCezpgpqmmDdYiAjCKXxo= cloud.google.com/go/auth v0.23.2/go.mod h1:4DhBRcqvtljQN3dJ57qtqbib5ZGCYE5f2crfiiC2EM0= +cloud.google.com/go/auth v0.23.3 h1:UMK+oBtuNGMCR/6i6mmySUItqjOazpJrbmZyhGbGBWo= +cloud.google.com/go/auth v0.23.3/go.mod h1:fClbry28fo7XkxhSeT6AQtAVAp6Jy0fW9N99PoPNPFM= cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= +cloud.google.com/go/compute/metadata v0.9.1 h1:CTE1OWBQ0vnF5uHwdFAQJvMQ0Fi/KRcqqKTo9V0F8Ik= +cloud.google.com/go/compute/metadata v0.9.1/go.mod h1:NtnlvB6X3t4R6xSWyVX/ZWk493PCxGQlhI/iqxh4M8I= dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= @@ -12,6 +16,8 @@ github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERo github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/andybalholm/brotli v1.2.3 h1:8H1qwOkl2LPfjf3YezB90JnCliZb6SInJ/OJkEbA5NQ= github.com/andybalholm/brotli v1.2.3/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= +github.com/andybalholm/brotli v1.2.4 h1:649LN6qF+/7JJYNN3Wdu4Dt5PS6eC/yP/Go3Bb0/AHI= +github.com/andybalholm/brotli v1.2.4/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/biter777/countries v1.7.5 h1:MJ+n3+rSxWQdqVJU8eBy9RqcdH6ePPn4PJHocVWUa+Q= github.com/biter777/countries v1.7.5/go.mod h1:1HSpZ526mYqKJcpT5Ti1kcGQ0L0SrXWIaptUWjFfv2E= github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= @@ -55,12 +61,17 @@ github.com/felixge/httpsnoop v1.1.0 h1:3YtUj32ZZkqZtt3sZZsClsymw/QDuVfpNhoA31zeO github.com/felixge/httpsnoop v1.1.0/go.mod h1:Zqxgdd+1Rkcz8euOqdr7lqgCRJztwr5hp9vDSi5UZCE= github.com/fxamacker/cbor/v2 v2.9.2 h1:X4Ksno9+x3cz0TZv69ec1hxP/+tymuR8PXQJyDwfh78= github.com/fxamacker/cbor/v2 v2.9.2/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= +github.com/fxamacker/cbor/v2 v2.9.4 h1:xwjVlxEMR3S605oUlgBjKLTTeGFciYPGYCtF/35LKGo= github.com/gabriel-vasile/mimetype v1.4.15 h1:05iP/CYtZ/w455R/KZM6rZ5ieAdh99UPtd+d3YzLmaI= github.com/gabriel-vasile/mimetype v1.4.15/go.mod h1:azpTcoLcDZRNgFou5j+APrqQx9HqVPWa6ijYQIIVswQ= github.com/go-faker/faker/v4 v4.6.1 h1:xUyVpAjEtB04l6XFY0V/29oR332rOSPWV4lU8RwDt4k= github.com/go-faker/faker/v4 v4.6.1/go.mod h1:arSdxNCSt7mOhdk8tEolvHeIJ7eX4OX80wXjKKvkKBY= +github.com/go-faker/faker/v4 v4.12.0 h1:yZXxuoQjxN+C2PVgYoDSHGiD9wj6dX1/Ful4p7QQV0k= +github.com/go-faker/faker/v4 v4.12.0/go.mod h1:VFIEwWDd16EdYDLF6NJ5gAAzEp7vz5LgKgJ2iZ17Tdg= github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA= github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= +github.com/go-jose/go-jose/v4 v4.1.5 h1:RjgjO2LOtWOJKUC5wpwY9LR3B3vwVAz6JS2YHfYU6eA= +github.com/go-jose/go-jose/v4 v4.1.5/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8= github.com/go-logr/logr v1.4.4/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -74,8 +85,12 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/universal-translator v0.18.2 h1:LCsMLC9RzmbUMNUPVYD15dmcjwYAJhmX8mPZRW4rAVU= +github.com/go-playground/universal-translator v0.18.2/go.mod h1:67VZIMp5lQpDWlnStOct22q1bkdJGJqHghbOtmkawxk= github.com/go-playground/validator/v10 v10.30.3 h1:4MU6YkEwx7GbcPJOZxrtbu+QfF3pJLJuaYTeAH0DYy8= github.com/go-playground/validator/v10 v10.30.3/go.mod h1:4Axh7oCNGcoGkqLoE4YWt6n20mcEIsPRlB7vPk3lpyc= +github.com/go-playground/validator/v10 v10.30.5 h1:YyCXvVShZbs2Sm3Mb53eNOlhRXctSOzW5QJAouCTZL4= +github.com/go-playground/validator/v10 v10.30.5/go.mod h1:wEqiaov48pXX1kjhc3Da8y0M0Dtg/BK7gurFBLgwFrQ= github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U= github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= @@ -84,12 +99,16 @@ github.com/gofiber/fiber/v3 v3.5.0 h1:dk7TOUH6DXJGtOLsN2XEG+0ZML7cznzHILTVozbNEK github.com/gofiber/fiber/v3 v3.5.0/go.mod h1:GOVDTW+gjJvfe0iJyVujbQ1Lnx+JUjFySJRI/9/xX/w= github.com/gofiber/schema v1.8.3 h1:06ZedxIYjngzc0095PYy7uWnFnbRflWFpikvZH61fDc= github.com/gofiber/schema v1.8.3/go.mod h1:jWnnZdhcW1mHyV+VnfRxKJDPNcepJsTZ9RIWxrr32Ng= +github.com/gofiber/schema v1.8.7 h1:7cSagr2ObiUVFuitCOqjKMqIhV+oVbkanP4Ge/wHsnI= +github.com/gofiber/schema v1.8.7/go.mod h1:fBz4T6uOepH3L+dYlz/yzQ0bFHPnfExkp/B3Qrk2VBI= github.com/gofiber/storage/redis/v3 v3.6.0 h1:ryGdJNq5OTcC/VV7vO07obDG5qmjmhwYkkFo6jAaJbg= github.com/gofiber/storage/redis/v3 v3.6.0/go.mod h1:InnkIWanfBw4dX6FjTIHMW3/PRO9azi5lhzKoNVnpUY= github.com/gofiber/storage/testhelpers/redis v0.1.0 h1:lDUwtanDf3f5YwlDwhbqnqCtj9Y/xc8ctxRE6HpQcws= github.com/gofiber/storage/testhelpers/redis v0.1.0/go.mod h1:Y1UccxbGVL04+TF5RuyCsksX+76hu6nJIWjPukBBgJ4= github.com/gofiber/utils/v2 v2.4.1 h1:E2X9G8O5Mn7b2GDb0JU3IUk42Rw2npuhhepIbuJQ2po= github.com/gofiber/utils/v2 v2.4.1/go.mod h1:I+RTsgMUdzFuifVc3LOEkfh32wQW9BfRl7l5RYjamW4= +github.com/gofiber/utils/v2 v2.5.3 h1:8bOn+DSKeANT8R/eqm+g2FHU/8uuE4Ed2IXOrijS0yw= +github.com/gofiber/utils/v2 v2.5.3/go.mod h1:tc9KqdRAQZRD/u0mN2d1ZHROGheSZv+0FcC9cg5OEdw= github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= @@ -98,12 +117,18 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= +github.com/google/s2a-go v0.1.10 h1:EMp+aOuXN6l8cE/gjF5Bt+vyZxsUuyCWe9chDWR/+uU= +github.com/google/s2a-go v0.1.10/go.mod h1:pz4tyvwXvJLLbyrkh6FW1eS2zPUXMaTmyNhYtyP2tNw= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/enterprise-certificate-proxy v0.3.21 h1:OFdQ3tnCX/zaQ0Cedur3D3z7kI6HiLX9g3TiAN4/DFU= github.com/googleapis/enterprise-certificate-proxy v0.3.21/go.mod h1:L3D/IQExI6LqEjBdXcZQ1WluSgigQmSwBboFstVPM4w= +github.com/googleapis/enterprise-certificate-proxy v0.3.22 h1:NU4XpII6jD+Dxcot94fqjE+AfJoE/lQP9q3faYGzC/c= +github.com/googleapis/enterprise-certificate-proxy v0.3.22/go.mod h1:L3D/IQExI6LqEjBdXcZQ1WluSgigQmSwBboFstVPM4w= github.com/googleapis/gax-go/v2 v2.24.0 h1:myMaPYyF9MecEmvQqMqomIwn9t/4KCZN9qnwsS76wlg= github.com/googleapis/gax-go/v2 v2.24.0/go.mod h1:IaTHBDd7NHxSCiu0vEs8pQZu4dGZrWwuSoxCnk16OFM= +github.com/googleapis/gax-go/v2 v2.25.0 h1:77gyzCOrTyzPKfzGZJG/V5DO4/Kuamek8W+cYJQ4U8k= +github.com/googleapis/gax-go/v2 v2.25.0/go.mod h1:sMKqnMesnKH+3wiRJROcttA+cJoZoGbZl1vDQ8XYtGk= github.com/h2non/gock v1.2.0 h1:K6ol8rfrRkUOefooBC8elXoaNGYkpp7y2qcxGG6BzUE= github.com/h2non/gock v1.2.0/go.mod h1:tNhoxHYW2W42cYkYb1WqzdbYIieALC99kpYr7rH/BQk= github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw= @@ -133,6 +158,8 @@ github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7Ulw github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= github.com/jackc/pgx/v5 v5.10.0 h1:VhSvgU2jSli8o3AqIEOTJr7rZwAEUVo4E4XhR94Zfr0= github.com/jackc/pgx/v5 v5.10.0/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4= +github.com/jackc/pgx/v5 v5.11.0 h1:IzBBtyK9AHqf98cctWFifYSci2hgQR/cd56wB4p+ogg= +github.com/jackc/pgx/v5 v5.11.0/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= @@ -173,12 +200,18 @@ github.com/moby/sys/userns v0.2.0 h1:nEtDtp7NCV/6dutSklNe8FrENPwFdc4mXnZqC/JWgXM github.com/moby/sys/userns v0.2.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= +github.com/molecule-man/go-brrr v1.1.1 h1:KYwusQhjtV3smALnhmafZf4OBrh7vP4p8O47DyUmmuY= +github.com/molecule-man/go-brrr v1.1.1/go.mod h1:7ybW6/7gA3oKY45jOfVNjSJDtrr6ea4tzbsTkjmQDC4= github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32 h1:W6apQkHrMkS0Muv8G/TipAy/FJl/rCYT0+EuS8+Z0z4= github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms= github.com/openbao/openbao/api/auth/approle/v2 v2.6.0 h1:b24RZeBFDUyQX4liW07KMczDj2R9R/CsZgCKifSyOcs= github.com/openbao/openbao/api/auth/approle/v2 v2.6.0/go.mod h1:bR50ddAcm6UWz1k13ZOtCOC90vqfCmA2V+lKa7sfoBU= +github.com/openbao/openbao/api/auth/approle/v2 v2.7.0 h1:Z3sEM22/Nr5IrKmBXoR+b3okH/A5JjyiGOW74PKRscg= +github.com/openbao/openbao/api/auth/approle/v2 v2.7.0/go.mod h1:OoMbWNiRtOdkxEMyzBTDyCzVv2uM3xzE4YMhwMOnux0= github.com/openbao/openbao/api/v2 v2.6.0 h1:KvfspAaL9bab9hI8jFYkV2cgtSrwWtaG+k9AUTHWU4M= github.com/openbao/openbao/api/v2 v2.6.0/go.mod h1:H4IWiH+2rgF/TbrsUbsfrMyGoqojkLqxPCRLENSMnSo= +github.com/openbao/openbao/api/v2 v2.7.0 h1:3CD1l3tr39nQraCgFGAWA5vYvPFzZoZrt3NL7DMQKAc= +github.com/openbao/openbao/api/v2 v2.7.0/go.mod h1:uXbMoyH2pjSvNyTepinUvLde8pOJB82EuhUCfOKnKbo= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= @@ -196,6 +229,7 @@ github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkB github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= github.com/shamaton/msgpack/v3 v3.2.0 h1:1q2Ms+MWmuRju+PuDMSFDB7p7621npeX4zprJN5Zck8= github.com/shamaton/msgpack/v3 v3.2.0/go.mod h1:sgBYvEiyz8JR1NC3yGRoPVME9xXovpnh3l/plW1nfRo= +github.com/shamaton/msgpack/v3 v3.2.3 h1:0r0aZtrXDFOPpwWJTEOpFq+Ih/JmMLm0pqLN5mSilrw= github.com/shirou/gopsutil/v4 v4.26.8 h1:YQMTF/1J50B5+Y0vlo1eDRf5DoR7Gk69hY+8wjYkQeo= github.com/shirou/gopsutil/v4 v4.26.8/go.mod h1:5O9FjBiXoTDFatIWjZZosqj4pV0DRtLx598xGbBehzM= github.com/sirupsen/logrus v1.10.2 h1:G2SED73/qrAu6YwbdxOD6peLkCBI3z7L+ykJFTXJBBo= @@ -219,6 +253,8 @@ github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6Kllzaw github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasthttp v1.73.0 h1:ocTOORnBWtJ+P8t/6wAjdkchMzdfHmWx2VD/DPbgZ7s= github.com/valyala/fasthttp v1.73.0/go.mod h1:EtXQDHaR+5P18p8wqDRFpUhxr108Ga9mXvVJXHRrN2k= +github.com/valyala/fasthttp v1.74.0 h1:wMS9fnO2QTALozYx5pId2Vi7ZwU/epUkY8i/KPWCHoU= +github.com/valyala/fasthttp v1.74.0/go.mod h1:3ARmLamUcw7ElxVtC8PXaGzQ6VEuvnetlkrwIklQBSE= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= @@ -243,34 +279,56 @@ go.opentelemetry.io/otel/trace v1.46.0 h1:OULy7ccdJnZtJ0UDYFOIGaCmiWzJ8Vi2G/Rsu6 go.opentelemetry.io/otel/trace v1.46.0/go.mod h1:J7GAXweO77XSFkB/rmAqk9D6ihszhFjLU+d9WuUxDLI= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +go.uber.org/atomic v1.12.0 h1:BvcXdFKuviU4fTL/f+SxdQ5qJX/Jix8pAkgdUcb3XOE= +go.uber.org/atomic v1.12.0/go.mod h1:I6c4cg+6HCxRjfjSsYtApoFILnpc0CGUdGkXVqbYVNk= go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= golang.org/x/crypto v0.56.0 h1:GUh5Ii4J5jtcseSMiRqr1jXCNHoxjeV9Fmekc2oLy6Y= golang.org/x/crypto v0.56.0/go.mod h1:OMW5y6CY9l38uPLmxU6l6pwcXp1obtLo3e6gT7gQR2I= +golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M= +golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA= golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To= golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU= +golang.org/x/net v0.59.0 h1:5zfYln+w5XCxwrnMMJPufRgNoXEaGxl0wo5GqPXyues= +golang.org/x/net v0.59.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= +golang.org/x/oauth2 v0.37.0 h1:JUlcxA8oAtauLfiH8FX2/FkAWHAdi0QtGCGc+hofE98= +golang.org/x/oauth2 v0.37.0/go.mod h1:IxwZNxUULJmpBFf9K/9NTMSIfZZuvuTy1gGxhigP/58= golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk= +golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0= golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= +golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8= golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M= +golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI= +golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= +golang.org/x/time v0.16.0 h1:vMb6ptszcQMkcwiRTAuNNU50gom6++Q/6gY2hDM6VDE= +golang.org/x/time v0.16.0/go.mod h1:rVKOqvZeKvrDKTQiAHJ7wmwP0RzleSphoEA9RcdLA0s= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= google.golang.org/api v0.297.0 h1:WktxTsnnx0yZNnsR6j0q6hR21RnnK81FHTOPy/ux4OE= google.golang.org/api v0.297.0/go.mod h1:S4m8x0M6OkQpkOzGk1y9JG2sm4fFQrMh6dxzjCTszhE= +google.golang.org/api v0.299.0 h1:b3K+ydSMd0kh6TQI6bJyApRQfqQX2MfSOaVkpM59mJw= +google.golang.org/api v0.299.0/go.mod h1:zlR3GVA8b2R5nv5Ij9UWe37StVB3cxDD7DBFi4ZFsHw= google.golang.org/genproto v0.0.0-20260715232425-e75dac1f907d h1:C9v1o0/4quuhOAfmRXA2j+we0PqZIp8traLdeogF3Ms= google.golang.org/genproto v0.0.0-20260715232425-e75dac1f907d/go.mod h1:Wz2wFJntZFmLGo7pLDXZ3wYk5hyc0Mb+SkHhDDXT+lU= google.golang.org/genproto/googleapis/api v0.0.0-20260715232425-e75dac1f907d h1:QwnJwPte4XXAkhPu26LTDIahnsMSUV0kK8HkxbC+Pc4= google.golang.org/genproto/googleapis/api v0.0.0-20260715232425-e75dac1f907d/go.mod h1:WRrQ7/7N19PypuT0fxLOL5Lq0waoiRri4FbtHDEKrGE= google.golang.org/genproto/googleapis/rpc v0.0.0-20260831171406-18b4a7587f8a h1:3Dnd1cDaZlB68lziofO+bJXpjOy8UfRv8Unt+yH8tQ4= google.golang.org/genproto/googleapis/rpc v0.0.0-20260831171406-18b4a7587f8a/go.mod h1:DjtHYE8FKJLivXcBEjGwndXfIC23G0VpXiXKqG179uA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260921155816-b14227669459 h1:b0xCahf3FK2m2Cv0p4vTozGPWncCvLfwV86UNg8xWU8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260921155816-b14227669459/go.mod h1:OaIUM3+LpYcK2GXM4FTmhWoIq371Owdr+Cc7/BsYHHc= google.golang.org/grpc v1.83.2 h1:EManeRomTObA0BU7I8vXgg/78uE5MJ9M8B39EX2WscU= google.golang.org/grpc v1.83.2/go.mod h1:YPI1hK3kDked6iHvgX3tR0y+nX/qpMFKhPgFsokw1S8= +google.golang.org/grpc v1.84.0 h1:soMyaPJ8pAak5PIQ0DGBUir0XRo2fRoMqhNWMLlLxO0= +google.golang.org/grpc v1.84.0/go.mod h1:ljCht0DrxQrXBDRTZp52Qxh3Ffk8CdYm2sj4O2QN2C0= google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc= google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/idp/internal/providers/crypto/jwk.go b/idp/internal/providers/crypto/jwk.go index 7fba577..c04cb1c 100644 --- a/idp/internal/providers/crypto/jwk.go +++ b/idp/internal/providers/crypto/jwk.go @@ -12,11 +12,7 @@ import ( "crypto/ed25519" "crypto/elliptic" "crypto/rand" - "encoding/base64" "errors" - "fmt" - "math/big" - "strings" "github.com/golang-jwt/jwt/v5" @@ -49,10 +45,6 @@ type getDecryptedPrivateKeyOptions struct { storeReEncDataFn StoreReEncryptedData } -func encodeEd25519PrivateKeyBytes(privKey ed25519.PrivateKey) string { - return base64.RawURLEncoding.EncodeToString(privKey) -} - func (e *Crypto) GenerateEd25519KeyPair( ctx context.Context, opts GenerateKeyPairOptions, @@ -71,12 +63,20 @@ func (e *Crypto) GenerateEd25519KeyPair( } defer utils.WipeBytes(ctx, logger, priv) + kid := utils.ExtractEd25519KeyID(pub) + privateJWK := utils.EncodeEd25519JwkPrivate(priv, pub, kid) + privateJSON, err := privateJWK.MarshalJSON() + if err != nil { + return KeyPair{}, exceptions.NewInternalServerError() + } + defer utils.WipeBytes(ctx, logger, privateJSON) + dekID, encryptedKey, serviceErr := e.EncryptWithDEK( ctx, EncryptWithDEKOptions{ RequestID: opts.RequestID, GetDEKfn: opts.GetDEKfn, - PlainText: encodeEd25519PrivateKeyBytes(priv), + PlainText: string(privateJSON), }, ) if serviceErr != nil { @@ -84,7 +84,6 @@ func (e *Crypto) GenerateEd25519KeyPair( return KeyPair{}, serviceErr } - kid := utils.ExtractEd25519KeyID(pub) publicJwk := utils.EncodeEd25519Jwk(pub, kid) if _, err := opts.StoreFN(dekID, utils.SupportedCryptoSuiteEd25519, kid, encryptedKey, &publicJwk); err != nil { logger.ErrorContext(ctx, "Failed to store private key", "error", err) @@ -100,20 +99,16 @@ func (e *Crypto) GenerateEd25519KeyPair( type GetEncryptedJWKPrivKey = func(kid string) (string, utils.SupportedCryptoSuite, error) -func decodeEd25519PrivateKeyBytes( - bytes string, -) (ed25519.PrivateKey, error) { - decodedBytes, err := base64.RawURLEncoding.DecodeString(bytes) +func decodeEd25519PrivateKeyJSON(data string) (ed25519.PrivateKey, error) { + jwk, err := utils.JsonToJWK([]byte(data)) if err != nil { - return nil, fmt.Errorf("failed to decode Ed25519 private key: %w", err) + return nil, err } - - if len(decodedBytes) != ed25519.PrivateKeySize { - return nil, fmt.Errorf("invalid Ed25519 private key size: expected %d, got %d", - ed25519.PrivateKeySize, len(decodedBytes)) + key, ok := jwk.(*utils.Ed25519JWK) + if !ok { + return nil, errors.New("expected an Ed25519 JWK") } - - return decodedBytes, nil + return utils.DecodeEd25519JwkPrivate(key) } func (e *Crypto) getDecryptedEd25519PrivateKey( @@ -127,7 +122,7 @@ func (e *Crypto) getDecryptedEd25519PrivateKey( }).With("jwkKid", opts.jwkKID) logger.DebugContext(ctx, "Getting decrypted Ed25519 private key...") - base64PrivKey, serviceErr := e.DecryptWithDEK( + privateKeyData, serviceErr := e.DecryptWithDEK( ctx, DecryptWithDEKOptions{ RequestID: opts.requestID, @@ -143,7 +138,7 @@ func (e *Crypto) getDecryptedEd25519PrivateKey( return nil, serviceErr } - privKey, err := decodeEd25519PrivateKeyBytes(base64PrivKey) + privKey, err := decodeEd25519PrivateKeyJSON(privateKeyData) if err != nil { logger.ErrorContext(ctx, "Failed to decode private key", "error", err) return nil, exceptions.NewInternalServerError() @@ -153,15 +148,6 @@ func (e *Crypto) getDecryptedEd25519PrivateKey( return privKey, nil } -func encodeES256PrivateKeyBytes(privKey *ecdsa.PrivateKey) string { - pubKey := privKey.Public().(*ecdsa.PublicKey) - return fmt.Sprintf("%s.%s.%s", - base64.RawURLEncoding.EncodeToString(privKey.D.Bytes()), - base64.RawURLEncoding.EncodeToString(pubKey.X.Bytes()), - base64.RawURLEncoding.EncodeToString(pubKey.Y.Bytes()), - ) -} - func (e *Crypto) GenerateES256KeyPair( ctx context.Context, opts GenerateKeyPairOptions, @@ -180,13 +166,22 @@ func (e *Crypto) GenerateES256KeyPair( } defer utils.WipeES256PrivateKey(ctx, logger, priv) - encodedPrivateKey := encodeES256PrivateKeyBytes(priv) + kid := utils.ExtractECDSAKeyID(&priv.PublicKey) + privateJWK, err := utils.EncodeP256JwkPrivate(priv, kid) + if err != nil { + return KeyPair{}, exceptions.NewInternalServerError() + } + privateJSON, err := privateJWK.MarshalJSON() + if err != nil { + return KeyPair{}, exceptions.NewInternalServerError() + } + defer utils.WipeBytes(ctx, logger, privateJSON) dekID, encryptedPrivateKey, serviceErr := e.EncryptWithDEK( ctx, EncryptWithDEKOptions{ RequestID: opts.RequestID, GetDEKfn: opts.GetDEKfn, - PlainText: encodedPrivateKey, + PlainText: string(privateJSON), }, ) if serviceErr != nil { @@ -194,7 +189,6 @@ func (e *Crypto) GenerateES256KeyPair( return KeyPair{}, serviceErr } - kid := utils.ExtractECDSAKeyID(priv.Public().(*ecdsa.PublicKey)) publicJwk, err := utils.EncodeP256Jwk(&priv.PublicKey, kid) if err != nil { logger.ErrorContext(ctx, "Failed to encode JWK", "error", err) @@ -213,44 +207,16 @@ func (e *Crypto) GenerateES256KeyPair( }, nil } -func decodeES256PrivateKeyBytes(bytes string) (*ecdsa.PrivateKey, error) { - parts := strings.Split(bytes, ".") - if len(parts) != 3 { - return nil, errors.New("invalid key format") - } - - d, err := base64.RawURLEncoding.DecodeString(parts[0]) - if err != nil { - return nil, err - } - if len(d) == 0 { - return nil, errors.New("invalid D value in key") - } - - x, err := base64.RawURLEncoding.DecodeString(parts[1]) +func decodeES256PrivateKeyJSON(data string) (*ecdsa.PrivateKey, error) { + jwk, err := utils.JsonToJWK([]byte(data)) if err != nil { return nil, err } - if len(x) == 0 { - return nil, errors.New("invalid X value in key") + key, ok := jwk.(*utils.ES256JWK) + if !ok { + return nil, errors.New("expected an ES256 JWK") } - - y, err := base64.RawURLEncoding.DecodeString(parts[2]) - if err != nil { - return nil, err - } - if len(y) == 0 { - return nil, errors.New("invalid Y value in key") - } - - return &ecdsa.PrivateKey{ - D: new(big.Int).SetBytes(d), - PublicKey: ecdsa.PublicKey{ - Curve: elliptic.P256(), - X: new(big.Int).SetBytes(x), - Y: new(big.Int).SetBytes(y), - }, - }, nil + return utils.DecodeP256JwkPrivate(key) } func (e *Crypto) getDecryptedES256PrivateKey( @@ -264,7 +230,7 @@ func (e *Crypto) getDecryptedES256PrivateKey( }).With("jwkKID", opts.jwkKID) logger.DebugContext(ctx, "Getting decrypted ES256 private key...") - base64PrivKey, serviceErr := e.DecryptWithDEK( + privateKeyData, serviceErr := e.DecryptWithDEK( ctx, DecryptWithDEKOptions{ RequestID: opts.requestID, @@ -280,7 +246,7 @@ func (e *Crypto) getDecryptedES256PrivateKey( return nil, serviceErr } - privKey, err := decodeES256PrivateKeyBytes(base64PrivKey) + privKey, err := decodeES256PrivateKeyJSON(privateKeyData) if err != nil { logger.ErrorContext(ctx, "Failed to decode private key", "error", err) return nil, exceptions.NewInternalServerError() diff --git a/idp/internal/providers/crypto/jwk_test.go b/idp/internal/providers/crypto/jwk_test.go new file mode 100644 index 0000000..f33604b --- /dev/null +++ b/idp/internal/providers/crypto/jwk_test.go @@ -0,0 +1,114 @@ +package crypto + +import ( + "crypto/ecdsa" + "crypto/ed25519" + "crypto/elliptic" + "crypto/rand" + "crypto/sha256" + "encoding/base64" + "testing" + + "github.com/tugascript/devlogs/idp/internal/utils" +) + +func TestPrivateJWKJSONRoundTrip(t *testing.T) { + t.Run("ES256", func(t *testing.T) { + // Scalar 1 exercises fixed-width encoding with leading zeroes. + scalar := make([]byte, 32) + scalar[31] = 1 + original, err := ecdsa.ParseRawPrivateKey(elliptic.P256(), scalar) + if err != nil { + t.Fatal(err) + } + kid := utils.ExtractECDSAKeyID(&original.PublicKey) + jwk, err := utils.EncodeP256JwkPrivate(original, kid) + if err != nil { + t.Fatal(err) + } + if err := jwk.Validate(); err != nil { + t.Fatal(err) + } + data, err := jwk.MarshalJSON() + if err != nil { + t.Fatal(err) + } + key, err := decodeES256PrivateKeyJSON(string(data)) + if err != nil { + t.Fatal(err) + } + if !key.Equal(original) { + t.Fatal("private key changed") + } + digest := sha256.Sum256([]byte("round trip")) + signature, err := ecdsa.SignASN1(rand.Reader, key, digest[:]) + if err != nil { + t.Fatal(err) + } + if !ecdsa.VerifyASN1(&original.PublicKey, digest[:], signature) { + t.Fatal("signature verification failed") + } + other, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatal(err) + } + otherJWK, err := utils.EncodeP256JwkPrivate(other, kid) + if err != nil { + t.Fatal(err) + } + jwk.D = otherJWK.D + bad, _ := jwk.MarshalJSON() + if _, err := decodeES256PrivateKeyJSON(string(bad)); err == nil { + t.Fatal("accepted mismatched public and private keys") + } + }) + t.Run("Ed25519", func(t *testing.T) { + pub, original, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + t.Fatal(err) + } + jwk := utils.EncodeEd25519JwkPrivate(original, pub, utils.ExtractEd25519KeyID(pub)) + seed, err := base64.RawURLEncoding.DecodeString(jwk.D) + if err != nil || len(seed) != ed25519.SeedSize { + t.Fatal("JWK must contain a 32-byte seed") + } + if err := jwk.Validate(); err != nil { + t.Fatal(err) + } + data, err := jwk.MarshalJSON() + if err != nil { + t.Fatal(err) + } + key, err := decodeEd25519PrivateKeyJSON(string(data)) + if err != nil { + t.Fatal(err) + } + if !key.Equal(original) { + t.Fatal("private key changed") + } + message := []byte("round trip") + if !ed25519.Verify(pub, message, ed25519.Sign(key, message)) { + t.Fatal("signature verification failed") + } + other, _, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + t.Fatal(err) + } + jwk.X = base64.RawURLEncoding.EncodeToString(other) + bad, _ := jwk.MarshalJSON() + if _, err := decodeEd25519PrivateKeyJSON(string(bad)); err == nil { + t.Fatal("accepted mismatched public and private keys") + } + }) +} + +func TestPrivateJWKRejectsInvalidData(t *testing.T) { + for _, data := range []string{"", "{}", "{", "a.b.c", "AA.AA.AA"} { + if _, err := decodeES256PrivateKeyJSON(data); err == nil { + t.Errorf("ES256 accepted %q", data) + } + if _, err := decodeEd25519PrivateKeyJSON(data); err == nil { + t.Errorf("Ed25519 accepted %q", data) + } + } +} diff --git a/idp/internal/providers/database/credentials_keys.sql.go b/idp/internal/providers/database/credentials_keys.sql.go index 7833392..b3916d8 100644 --- a/idp/internal/providers/database/credentials_keys.sql.go +++ b/idp/internal/providers/database/credentials_keys.sql.go @@ -16,6 +16,8 @@ INSERT INTO "credentials_keys" ( "account_id", "public_kid", "public_key", + "private_key", + "dek_kid", "crypto_suite", "expires_at", "usage" @@ -25,7 +27,9 @@ INSERT INTO "credentials_keys" ( $3, $4, $5, - $6 + $6, + $7, + $8 ) RETURNING id, public_kid, public_key, private_key, dek_kid, crypto_suite, is_revoked, is_external, usage, account_id, expires_at, created_at, updated_at ` @@ -33,6 +37,8 @@ type CreateCredentialsKeyParams struct { AccountID int32 PublicKid string PublicKey []byte + PrivateKey string + DekKid string CryptoSuite TokenCryptoSuite ExpiresAt time.Time Usage CredentialsUsage @@ -48,6 +54,8 @@ func (q *Queries) CreateCredentialsKey(ctx context.Context, arg CreateCredential arg.AccountID, arg.PublicKid, arg.PublicKey, + arg.PrivateKey, + arg.DekKid, arg.CryptoSuite, arg.ExpiresAt, arg.Usage, diff --git a/idp/internal/providers/database/queries/credentials_keys.sql b/idp/internal/providers/database/queries/credentials_keys.sql index bdbaea0..3e7e63a 100644 --- a/idp/internal/providers/database/queries/credentials_keys.sql +++ b/idp/internal/providers/database/queries/credentials_keys.sql @@ -9,6 +9,8 @@ INSERT INTO "credentials_keys" ( "account_id", "public_kid", "public_key", + "private_key", + "dek_kid", "crypto_suite", "expires_at", "usage" @@ -18,7 +20,9 @@ INSERT INTO "credentials_keys" ( $3, $4, $5, - $6 + $6, + $7, + $8 ) RETURNING *; -- name: RevokeCredentialsKey :one diff --git a/idp/internal/services/client_credentials.go b/idp/internal/services/client_credentials.go index 1582954..49624b3 100644 --- a/idp/internal/services/client_credentials.go +++ b/idp/internal/services/client_credentials.go @@ -218,10 +218,31 @@ func (s *Services) clientCredentialsKey( return database.CreateCredentialsKeyParams{}, nil, serviceErr } + privateKeyJSON, err := privJwk.MarshalJSON() + if err != nil { + logger.ErrorContext(ctx, "Failed to marshal client credentials private key", "error", err) + return database.CreateCredentialsKeyParams{}, nil, exceptions.NewInternalServerError() + } + defer utils.WipeBytes(ctx, logger, privateKeyJSON) + + dekID, encryptedPrivateKey, serviceErr := s.crypto.EncryptWithDEK(ctx, crypto.EncryptWithDEKOptions{ + RequestID: opts.requestID, + GetDEKfn: s.BuildGetEncGlobalDEKFn(ctx, BuildGetGlobalDEKFnOptions{ + RequestID: opts.requestID, + }), + PlainText: string(privateKeyJSON), + }) + if serviceErr != nil { + logger.ErrorContext(ctx, "Failed to encrypt client credentials private key", "serviceError", serviceErr) + return database.CreateCredentialsKeyParams{}, nil, serviceErr + } + return database.CreateCredentialsKeyParams{ AccountID: opts.accountID, PublicKid: kid, PublicKey: jsonJwk, + PrivateKey: encryptedPrivateKey, + DekKid: dekID, ExpiresAt: time.Now().Add(opts.expiresIn), Usage: opts.usage, CryptoSuite: cryptoSuite, diff --git a/idp/internal/utils/jwk.go b/idp/internal/utils/jwk.go index 0aa4df1..a5d861f 100644 --- a/idp/internal/utils/jwk.go +++ b/idp/internal/utils/jwk.go @@ -19,7 +19,6 @@ import ( "log/slog" "math/big" "slices" - "unsafe" ) type SupportedCryptoSuite string @@ -132,7 +131,7 @@ func validateCommonJWKFields(kid, use string, keyOps []string) error { 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 keyOp != sign && keyOp != verify })) { return fmt.Errorf("key operation should be sign or verify") } @@ -341,26 +340,13 @@ func (j *RS256JWK) Validate() error { return nil } -func bigIntToPaddedBytes(n *big.Int, length int) []byte { - bytes := n.Bytes() - if len(bytes) >= length { - return bytes - } - - paddedBytes := make([]byte, length) - copy(paddedBytes[length-len(bytes):], bytes) - return paddedBytes -} - func ExtractECDSAKeyID(pub *ecdsa.PublicKey) string { - curveBits := pub.Curve.Params().BitSize - byteLen := (curveBits + 7) / 8 - - xBytes := bigIntToPaddedBytes(pub.X, byteLen) - yBytes := bigIntToPaddedBytes(pub.Y, byteLen) - keyBytes := append(xBytes, yBytes...) - - return extractKeyID(keyBytes) + raw, err := pub.Bytes() + if err != nil { + return "" + } + // Preserve the existing KID: hash the coordinates without the SEC1 prefix. + return extractKeyID(raw[1:]) } func ExtractEd25519KeyID(pub ed25519.PublicKey) string { @@ -391,7 +377,7 @@ func EncodeEd25519JwkPrivate( Use: useSig, Alg: algEdDSA, Kid: kid, - D: base64.RawURLEncoding.EncodeToString(privateKey), + D: base64.RawURLEncoding.EncodeToString(privateKey.Seed()), KeyOps: []string{sign, verify}, } } @@ -406,19 +392,24 @@ func DecodeEd25519Jwk(jwk *Ed25519JWK) (ed25519.PublicKey, error) { } func DecodeEd25519JwkPrivate(jwk *Ed25519JWK) (ed25519.PrivateKey, error) { - if jwk.D == "" { - return nil, fmt.Errorf("private key not available in JWK") + if jwk == nil || jwk.Kty != okpKty || jwk.Crv != ed25519Crv { + return nil, fmt.Errorf("expected an Ed25519 JWK") } - - privateKey, 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(privateKey) != ed25519.PrivateKeySize { - return nil, fmt.Errorf("invalid private key size") + if len(d) != ed25519.SeedSize { + return nil, fmt.Errorf("Ed25519 private key seed must be 32 bytes") + } + privateKey := ed25519.NewKeyFromSeed(d) + publicKey, err := DecodeEd25519Jwk(jwk) + if err != nil { + return nil, err + } + if !privateKey.Public().(ed25519.PublicKey).Equal(publicKey) { + return nil, fmt.Errorf("JWK public key does not match private key") } - return privateKey, nil } @@ -582,8 +573,6 @@ 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 { @@ -597,30 +586,10 @@ func WipeBytes(ctx context.Context, logger *slog.Logger, data []byte) { } } -func WipeBigInt(ctx context.Context, logger *slog.Logger, bi *big.Int) { - if bi == nil { - return - } - - words := bi.Bits() - if len(words) == 0 { - return - } - - byteSlice := (*[1 << 30]byte)(unsafe.Pointer(&words[0]))[:len(words)*int(unsafe.Sizeof(words[0]))] - WipeBytes(ctx, logger, byteSlice) - bi.SetInt64(0) -} - -func WipeES256PrivateKey(ctx context.Context, logger *slog.Logger, privKey *ecdsa.PrivateKey) { +// WipeES256PrivateKey releases this key's references. Go does not expose a +// supported API to zero the private scalar or its internal cached copies. +func WipeES256PrivateKey(_ context.Context, _ *slog.Logger, privKey *ecdsa.PrivateKey) { if privKey != nil { - WipeBigInt(ctx, logger, privKey.D) - WipeBigInt(ctx, logger, privKey.X) - WipeBigInt(ctx, logger, privKey.Y) - privKey.PublicKey = ecdsa.PublicKey{} - privKey.Curve = nil - privKey.X = nil - privKey.Y = nil - privKey.D = nil + *privKey = ecdsa.PrivateKey{} } } From 0d540ac55433ad18aa0ec121319011290a57e6ab Mon Sep 17 00:00:00 2001 From: Afonso Barracha Date: Wed, 23 Sep 2026 14:51:26 +1200 Subject: [PATCH 16/16] feat: add session to dynamic registration --- idp/initial_schema.dbml | 5 + .../bodies/oauth_dynamic_registration.go | 1 + idp/internal/controllers/helpers.go | 8 +- idp/internal/controllers/middleware.go | 6 +- .../oauth_dynamic_registration_account.go | 71 ++++- .../controllers/registration_errors_test.go | 9 + .../controllers/registration_update_test.go | 50 +++ .../account_credential_secrets.sql.go | 1 + .../database/account_credentials.sql.go | 136 ++++++++- .../database/account_credentials_keys.sql.go | 1 + .../providers/database/app_keys.sql.go | 27 ++ .../database/app_related_apps.sql.go | 4 +- .../providers/database/app_secrets.sql.go | 24 ++ idp/internal/providers/database/apps.sql.go | 192 ++++++++++-- .../database/credentials_keys.sql.go | 15 + idp/internal/providers/database/database.go | 19 +- ...0241213231542_create_initial_schema.up.sql | 8 +- idp/internal/providers/database/models.go | 4 + .../queries/account_credential_secrets.sql | 1 + .../database/queries/account_credentials.sql | 17 ++ .../queries/account_credentials_keys.sql | 1 + .../providers/database/queries/app_keys.sql | 5 + .../database/queries/app_secrets.sql | 5 + .../providers/database/queries/apps.sql | 17 ++ .../database/queries/credentials_keys.sql | 3 + .../providers/database/registered_apps.sql.go | 8 +- idp/internal/providers/tokens/access.go | 7 +- .../tokens/dynamic_registration_iat.go | 45 +-- .../tokens/dynamic_registration_iat_test.go | 51 ++++ idp/internal/server/routes/oauth.go | 5 + idp/internal/server/routes/oauth_test.go | 16 + .../account_credentials_registration.go | 50 +-- .../account_credentials_registration_iat.go | 27 +- .../services/app_dynamic_registration.go | 39 ++- .../services/app_dynamic_registration_iat.go | 29 +- idp/internal/services/apps_auth.go | 3 + idp/internal/services/auth.go | 7 + idp/internal/services/client_credentials.go | 2 +- idp/internal/services/deks.go | 10 + idp/internal/services/jwks.go | 8 + .../oauth_dynamic_registration_config.go | 289 +++++++++++------- .../services/registration_credentials.go | 66 ++++ .../services/registration_management.go | 182 +++++++++++ .../services/registration_management_test.go | 116 +++++++ idp/internal/services/services.go | 2 + idp/internal/services/users_auth.go | 3 + idp/tests/dynamic_registration_test.go | 53 +++- 47 files changed, 1384 insertions(+), 264 deletions(-) create mode 100644 idp/internal/controllers/registration_update_test.go create mode 100644 idp/internal/services/registration_credentials.go create mode 100644 idp/internal/services/registration_management.go create mode 100644 idp/internal/services/registration_management_test.go diff --git a/idp/initial_schema.dbml b/idp/initial_schema.dbml index aaaad96..067713c 100644 --- a/idp/initial_schema.dbml +++ b/idp/initial_schema.dbml @@ -397,6 +397,8 @@ Enum creation_method { } Table account_credentials as AC { + registration_token_jti uuid [unique] + software_statement text [not null, default: ''] id serial [pk] account_id integer [not null] @@ -482,6 +484,7 @@ Table account_credentials_secrets as ACS { (account_credentials_id, secret_id) [name: 'account_credential_secrets_account_credentials_id_secret_id_idx'] } } +Ref: ACS.account_credentials_id > AC.id [delete: cascade] Ref: ACS.account_id > A.id [delete: cascade] Ref: ACS.credentials_secret_id > CS.id [delete: cascade] @@ -817,6 +820,8 @@ Enum session_type { } Table apps as APP { + registration_token_jti uuid [unique] + software_statement text [not null, default: ''] id serial [pk] account_id integer [not null] diff --git a/idp/internal/controllers/bodies/oauth_dynamic_registration.go b/idp/internal/controllers/bodies/oauth_dynamic_registration.go index dc35b60..f08734b 100644 --- a/idp/internal/controllers/bodies/oauth_dynamic_registration.go +++ b/idp/internal/controllers/bodies/oauth_dynamic_registration.go @@ -9,6 +9,7 @@ package bodies import "github.com/tugascript/devlogs/idp/internal/utils" type OAuthDynamicClientRegistrationBody struct { + ClientSecretPresent bool `json:"-"` ClientID string `json:"client_id,omitempty" validate:"omitempty"` ClientSecret string `json:"client_secret,omitempty" validate:"omitempty"` RedirectURIs []string `json:"redirect_uris,omitempty" validate:"omitempty,min=1,dive,uri"` diff --git a/idp/internal/controllers/helpers.go b/idp/internal/controllers/helpers.go index a37345b..58ddf8f 100644 --- a/idp/internal/controllers/helpers.go +++ b/idp/internal/controllers/helpers.go @@ -210,6 +210,8 @@ func dynamicRegistrationServiceError( serviceErr *exceptions.ServiceError, ) error { switch serviceErr.Code { + case exceptions.OAuthErrorInvalidClientMetadata: + return oauthErrorResponse(logger, ctx, exceptions.OAuthErrorInvalidClientMetadata) case exceptions.OAuthErrorInvalidRequest: return oauthErrorResponse(logger, ctx, exceptions.OAuthErrorInvalidRequest) case exceptions.OAuthErrorInvalidClient: @@ -218,7 +220,11 @@ func dynamicRegistrationServiceError( return oauthErrorResponse(logger, ctx, exceptions.OAuthErrorInvalidRedirectURI) case exceptions.OAuthErrorInvalidToken: return oauthErrorResponse(logger, ctx, exceptions.OAuthErrorInvalidToken) - case exceptions.CodeUnauthorized, exceptions.CodeForbidden: + case exceptions.CodeForbidden: + return ctx.Status(fiber.StatusForbidden).JSON(exceptions.NewOAuthError(exceptions.OAuthErrorAccessDenied)) + case exceptions.CodeUnauthorized: + return oauthErrorResponse(logger, ctx, exceptions.OAuthErrorInvalidToken) + case exceptions.OAuthErrorUnauthorizedClient: return oauthErrorResponse(logger, ctx, exceptions.OAuthErrorUnauthorizedClient) case exceptions.CodeNotFound, exceptions.CodeValidation: return oauthErrorResponse(logger, ctx, exceptions.OAuthErrorInvalidClientMetadata) diff --git a/idp/internal/controllers/middleware.go b/idp/internal/controllers/middleware.go index e939f22..78f7d6f 100644 --- a/idp/internal/controllers/middleware.go +++ b/idp/internal/controllers/middleware.go @@ -248,7 +248,7 @@ func (c *Controllers) DynamicRegistrationAccessTokenMiddleware(ctx fiber.Ctx) er }, ) if serviceErr != nil { - return oauthErrorResponse(logger, ctx, exceptions.OAuthErrorInvalidToken) + return dynamicRegistrationServiceError(logger, ctx, serviceErr) } ctx.Locals("account", accountClaims) @@ -261,7 +261,7 @@ func (c *Controllers) AppDynamicRegistrationAccessTokenMiddleware(ctx fiber.Ctx) logger := c.buildLogger(requestID, middlewareLocation, "AppDynamicRegistrationAccessTokenMiddleware") username, accountID, serviceErr := getHostAccount(ctx) if serviceErr != nil { - return oauthErrorResponse(logger, ctx, exceptions.OAuthErrorInvalidToken) + return dynamicRegistrationServiceError(logger, ctx, serviceErr) } authHeader := ctx.Get("Authorization") @@ -279,7 +279,7 @@ func (c *Controllers) AppDynamicRegistrationAccessTokenMiddleware(ctx fiber.Ctx) }, ) if serviceErr != nil { - return oauthErrorResponse(logger, ctx, exceptions.OAuthErrorInvalidToken) + return dynamicRegistrationServiceError(logger, ctx, serviceErr) } ctx.Locals("account", accountClaims) diff --git a/idp/internal/controllers/oauth_dynamic_registration_account.go b/idp/internal/controllers/oauth_dynamic_registration_account.go index 2f8bee5..0fbd507 100644 --- a/idp/internal/controllers/oauth_dynamic_registration_account.go +++ b/idp/internal/controllers/oauth_dynamic_registration_account.go @@ -7,7 +7,10 @@ package controllers import ( + "encoding/json" + "errors" "github.com/gofiber/fiber/v3" + "strings" "github.com/tugascript/devlogs/idp/internal/controllers/bodies" "github.com/tugascript/devlogs/idp/internal/exceptions" @@ -186,11 +189,39 @@ func registrationClientIDFromContext(ctx fiber.Ctx) (string, bool) { return clientID, ok && clientID != "" } +func registrationBearer(ctx fiber.Ctx) string { + parts := strings.Fields(ctx.Get("Authorization")) + if len(parts) != 2 { + return "" + } + return parts[1] +} + func (c *Controllers) bindRegistrationBody(ctx fiber.Ctx) (*bodies.OAuthDynamicClientRegistrationBody, error) { + if !strings.EqualFold(strings.TrimSpace(strings.Split(ctx.Get("Content-Type"), ";")[0]), "application/json") { + return nil, errors.New("JSON body required") + } + var fields map[string]json.RawMessage + if err := json.Unmarshal(ctx.Body(), &fields); err != nil { + return nil, err + } + if fields == nil { + return nil, errors.New("JSON object required") + } + for _, name := range []string{"registration_access_token", "registration_client_uri", "client_secret_expires_at", "client_id_issued_at"} { + if _, present := fields[name]; present { + return nil, errors.New("server-managed field in update") + } + } body := new(bodies.OAuthDynamicClientRegistrationBody) if err := ctx.Bind().Body(body); err != nil { return nil, err } + raw, present := fields["client_secret"] + body.ClientSecretPresent = present + if present && (string(raw) == "null" || body.ClientSecret == "") { + return nil, errors.New("empty client_secret") + } return body, nil } @@ -208,11 +239,12 @@ func (c *Controllers) OAuthDynamicRegistrationGet(ctx fiber.Ctx) error { } dto, serviceErr := c.services.GetRegisteredAccountCredentials(ctx.Context(), services.GetRegisteredClientOptions{ - RequestID: requestID, AccountPublicID: accountClaims.AccountID, ClientID: tokenClientID, + RegistrationToken: registrationBearer(ctx), + RequestID: requestID, AccountPublicID: accountClaims.AccountID, ClientID: tokenClientID, BackendDomain: c.backendDomain, }) if serviceErr != nil { - return oauthErrorResponse(logger, ctx, exceptions.OAuthErrorInvalidToken) + return dynamicRegistrationServiceError(logger, ctx, serviceErr) } logResponse(logger, ctx, fiber.StatusOK) return ctx.Status(fiber.StatusOK).JSON(dto) @@ -233,11 +265,12 @@ func (c *Controllers) OAuthAppDynamicRegistrationGet(ctx fiber.Ctx) error { } dto, serviceErr := c.services.GetRegisteredApp(ctx.Context(), services.GetRegisteredClientOptions{ - RequestID: requestID, AccountPublicID: accountClaims.AccountID, ClientID: tokenClientID, + RegistrationToken: registrationBearer(ctx), + RequestID: requestID, AccountPublicID: accountClaims.AccountID, ClientID: tokenClientID, BackendDomain: c.backendDomain, HostUsername: username, }) if serviceErr != nil { - return oauthErrorResponse(logger, ctx, exceptions.OAuthErrorInvalidToken) + return dynamicRegistrationServiceError(logger, ctx, serviceErr) } logResponse(logger, ctx, fiber.StatusOK) return ctx.Status(fiber.StatusOK).JSON(dto) @@ -261,9 +294,11 @@ func (c *Controllers) OAuthDynamicRegistrationUpdate(ctx fiber.Ctx) error { } dto, serviceErr := c.services.UpdateRegisteredAccountCredentials(ctx.Context(), services.UpdateRegisteredClientOptions{ - ClientID: tokenClientID, - SubmittedClientID: body.ClientID, - SubmittedClientSecret: body.ClientSecret, + ClientID: tokenClientID, + SubmittedClientID: body.ClientID, + SubmittedClientSecret: body.ClientSecret, + SubmittedClientSecretPresent: body.ClientSecretPresent, + RegistrationToken: registrationBearer(ctx), CreateAccountCredentialsRegistrationOptions: services.CreateAccountCredentialsRegistrationOptions{ RequestID: requestID, AccountPublicID: accountClaims.AccountID, AccountVersion: accountClaims.AccountVersion, ApplicationType: body.ApplicationType, RedirectURIs: body.RedirectURIs, TokenEndpointAuthMethod: body.TokenEndpointAuthMethod, @@ -306,10 +341,12 @@ func (c *Controllers) OAuthAppDynamicRegistrationUpdate(ctx fiber.Ctx) error { } dto, serviceErr := c.services.UpdateRegisteredApp(ctx.Context(), services.UpdateRegisteredAppOptions{ - ClientID: tokenClientID, - SubmittedClientID: body.ClientID, - SubmittedClientSecret: body.ClientSecret, - HostUsername: username, + ClientID: tokenClientID, + SubmittedClientID: body.ClientID, + SubmittedClientSecret: body.ClientSecret, + SubmittedClientSecretPresent: body.ClientSecretPresent, + RegistrationToken: registrationBearer(ctx), + HostUsername: username, CreateAppCredentialsRegistrationOptions: services.CreateAppCredentialsRegistrationOptions{ RequestID: requestID, AccountID: accountID, ApplicationType: body.ApplicationType, RedirectURIs: body.RedirectURIs, TokenEndpointAuthMethod: body.TokenEndpointAuthMethod, GrantTypes: body.GrantTypes, ResponseTypes: body.ResponseTypes, @@ -344,9 +381,11 @@ func (c *Controllers) OAuthDynamicRegistrationDelete(ctx fiber.Ctx) error { return oauthErrorResponse(logger, ctx, exceptions.OAuthErrorInvalidToken) } if serviceErr := c.services.DeleteRegisteredAccountCredentials(ctx.Context(), services.GetRegisteredClientOptions{ - RequestID: requestID, AccountPublicID: accountClaims.AccountID, ClientID: tokenClientID, + RegistrationToken: registrationBearer(ctx), + BackendDomain: c.backendDomain, + RequestID: requestID, AccountPublicID: accountClaims.AccountID, ClientID: tokenClientID, }); serviceErr != nil { - return oauthErrorResponse(logger, ctx, exceptions.OAuthErrorInvalidToken) + return dynamicRegistrationServiceError(logger, ctx, serviceErr) } logResponse(logger, ctx, fiber.StatusNoContent) return ctx.SendStatus(fiber.StatusNoContent) @@ -357,16 +396,18 @@ func (c *Controllers) OAuthAppDynamicRegistrationDelete(ctx fiber.Ctx) error { logger := c.buildLogger(requestID, oauthDynamicRegistration, "OAuthAppDynamicRegistrationDelete") logRequest(logger, ctx) - _, _, serviceErr := getHostAccount(ctx) + 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) } if serviceErr := c.services.DeleteRegisteredApp(ctx.Context(), services.GetRegisteredClientOptions{ + RegistrationToken: registrationBearer(ctx), + BackendDomain: c.backendDomain, HostUsername: username, RequestID: requestID, AccountPublicID: accountClaims.AccountID, ClientID: tokenClientID, }); serviceErr != nil { - return oauthErrorResponse(logger, ctx, exceptions.OAuthErrorInvalidToken) + return dynamicRegistrationServiceError(logger, ctx, serviceErr) } logResponse(logger, ctx, fiber.StatusNoContent) return ctx.SendStatus(fiber.StatusNoContent) diff --git a/idp/internal/controllers/registration_errors_test.go b/idp/internal/controllers/registration_errors_test.go index 28f7217..120834a 100644 --- a/idp/internal/controllers/registration_errors_test.go +++ b/idp/internal/controllers/registration_errors_test.go @@ -21,6 +21,15 @@ func TestRegistrationErrorResponses(t *testing.T) { status int challenge, code string }{ + {"forbidden management", func(ctx fiber.Ctx) error { + return dynamicRegistrationServiceError(logger, ctx, exceptions.NewError(exceptions.CodeForbidden, "denied")) + }, http.StatusForbidden, "", exceptions.OAuthErrorAccessDenied}, + {"infrastructure failure", func(ctx fiber.Ctx) error { + return dynamicRegistrationServiceError(logger, ctx, exceptions.NewInternalServerError()) + }, http.StatusInternalServerError, "", exceptions.OAuthErrorServerError}, + {"invalid secret metadata", func(ctx fiber.Ctx) error { + return dynamicRegistrationServiceError(logger, ctx, exceptions.NewError(exceptions.OAuthErrorInvalidClientMetadata, "invalid secret")) + }, http.StatusBadRequest, "", exceptions.OAuthErrorInvalidClientMetadata}, {"missing IAT", c.DynamicRegistrationIATMiddleware, http.StatusUnauthorized, "Bearer", ""}, {"invalid IAT", func(ctx fiber.Ctx) error { return dynamicRegistrationServiceError(logger, ctx, exceptions.NewError(exceptions.OAuthErrorInvalidToken, "invalid IAT")) diff --git a/idp/internal/controllers/registration_update_test.go b/idp/internal/controllers/registration_update_test.go new file mode 100644 index 0000000..f3ceeba --- /dev/null +++ b/idp/internal/controllers/registration_update_test.go @@ -0,0 +1,50 @@ +package controllers + +import ( + "github.com/gofiber/fiber/v3" + "net/http/httptest" + "strings" + "testing" +) + +func TestRegistrationUpdateBody(t *testing.T) { + for _, tc := range []struct { + name, body, contentType string + valid, present bool + }{ + {"omitted", `{"client_id":"client"}`, "application/json", true, false}, + {"present", `{"client_id":"client","client_secret":"secret"}`, "application/json", true, true}, + {"empty", `{"client_secret":""}`, "application/json", false, false}, + {"null secret", `{"client_secret":null}`, "application/json", false, false}, + {"null body", `null`, "application/json", false, false}, + {"array", `[]`, "application/json", false, false}, + {"form", `client_id=client`, "application/x-www-form-urlencoded", false, false}, + {"token", `{"registration_access_token":"x"}`, "application/json", false, false}, + {"uri", `{"registration_client_uri":null}`, "application/json", false, false}, + {"expiry", `{"client_secret_expires_at":0}`, "application/json", false, false}, + {"issued at", `{"client_id_issued_at":0}`, "application/json", false, false}, + {"extension", `{"custom_extension":true}`, "application/json", true, false}, + } { + t.Run(tc.name, func(t *testing.T) { + app := fiber.New() + c := new(Controllers) + app.Put("/register", func(ctx fiber.Ctx) error { + body, err := c.bindRegistrationBody(ctx) + if (err == nil) != tc.valid { + t.Errorf("valid=%v error=%v", tc.valid, err) + } + if err == nil && body.ClientSecretPresent != tc.present { + t.Errorf("secret presence=%v", body.ClientSecretPresent) + } + return ctx.SendStatus(204) + }) + req := httptest.NewRequest("PUT", "/register", strings.NewReader(tc.body)) + req.Header.Set("Content-Type", tc.contentType) + res, err := app.Test(req) + if err != nil { + t.Fatal(err) + } + res.Body.Close() + }) + } +} diff --git a/idp/internal/providers/database/account_credential_secrets.sql.go b/idp/internal/providers/database/account_credential_secrets.sql.go index babed30..f4a5e09 100644 --- a/idp/internal/providers/database/account_credential_secrets.sql.go +++ b/idp/internal/providers/database/account_credential_secrets.sql.go @@ -140,6 +140,7 @@ WHERE "acs"."account_credentials_id" = $1 AND "csr"."is_revoked" = false AND "csr"."expires_at" > now() +ORDER BY "csr"."created_at" DESC, "csr"."id" DESC LIMIT 1 ` diff --git a/idp/internal/providers/database/account_credentials.sql.go b/idp/internal/providers/database/account_credentials.sql.go index bfe2b89..35c4e42 100644 --- a/idp/internal/providers/database/account_credentials.sql.go +++ b/idp/internal/providers/database/account_credentials.sql.go @@ -143,7 +143,7 @@ INSERT INTO "account_credentials" ( $38, $39, $40 -) 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 +) RETURNING registration_token_jti, software_statement, 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 CreateAccountCredentialsParams struct { @@ -234,6 +234,8 @@ func (q *Queries) CreateAccountCredentials(ctx context.Context, arg CreateAccoun ) var i AccountCredential err := row.Scan( + &i.RegistrationTokenJti, + &i.SoftwareStatement, &i.ID, &i.AccountID, &i.AccountPublicID, @@ -301,8 +303,22 @@ func (q *Queries) DeleteAllAccountCredentials(ctx context.Context) error { return err } +const deleteRegisteredAccountCredentialsGrants = `-- name: DeleteRegisteredAccountCredentialsGrants :exec +DELETE FROM grants WHERE account_id = $1 AND granted_client_id = $2 +` + +type DeleteRegisteredAccountCredentialsGrantsParams struct { + AccountID int32 + GrantedClientID string +} + +func (q *Queries) DeleteRegisteredAccountCredentialsGrants(ctx context.Context, arg DeleteRegisteredAccountCredentialsGrantsParams) error { + _, err := q.db.Exec(ctx, deleteRegisteredAccountCredentialsGrants, arg.AccountID, arg.GrantedClientID) + return err +} + const findAccountCredentialsByAccountPublicIDAndClientID = `-- name: FindAccountCredentialsByAccountPublicIDAndClientID :one -SELECT 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 FROM "account_credentials" +SELECT registration_token_jti, software_statement, 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 FROM "account_credentials" WHERE "account_public_id" = $1 AND "client_id" = $2 LIMIT 1 ` @@ -316,6 +332,8 @@ func (q *Queries) FindAccountCredentialsByAccountPublicIDAndClientID(ctx context row := q.db.QueryRow(ctx, findAccountCredentialsByAccountPublicIDAndClientID, arg.AccountPublicID, arg.ClientID) var i AccountCredential err := row.Scan( + &i.RegistrationTokenJti, + &i.SoftwareStatement, &i.ID, &i.AccountID, &i.AccountPublicID, @@ -366,7 +384,7 @@ func (q *Queries) FindAccountCredentialsByAccountPublicIDAndClientID(ctx context const findAccountCredentialsByClientID = `-- name: FindAccountCredentialsByClientID :one -SELECT 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 FROM "account_credentials" +SELECT registration_token_jti, software_statement, 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 FROM "account_credentials" WHERE "client_id" = $1 LIMIT 1 ` @@ -380,6 +398,8 @@ func (q *Queries) FindAccountCredentialsByClientID(ctx context.Context, clientID row := q.db.QueryRow(ctx, findAccountCredentialsByClientID, clientID) var i AccountCredential err := row.Scan( + &i.RegistrationTokenJti, + &i.SoftwareStatement, &i.ID, &i.AccountID, &i.AccountPublicID, @@ -429,7 +449,7 @@ func (q *Queries) FindAccountCredentialsByClientID(ctx context.Context, clientID } const findPaginatedAccountCredentialsByAccountPublicID = `-- name: FindPaginatedAccountCredentialsByAccountPublicID :many -SELECT 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 FROM "account_credentials" +SELECT registration_token_jti, software_statement, 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 FROM "account_credentials" WHERE "account_public_id" = $1 ORDER BY "id" DESC OFFSET $2 LIMIT $3 @@ -451,6 +471,8 @@ func (q *Queries) FindPaginatedAccountCredentialsByAccountPublicID(ctx context.C for rows.Next() { var i AccountCredential if err := rows.Scan( + &i.RegistrationTokenJti, + &i.SoftwareStatement, &i.ID, &i.AccountID, &i.AccountPublicID, @@ -506,6 +528,104 @@ func (q *Queries) FindPaginatedAccountCredentialsByAccountPublicID(ctx context.C return items, nil } +const lockRegisteredAccountCredentials = `-- name: LockRegisteredAccountCredentials :one +SELECT registration_token_jti, software_statement, 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 FROM account_credentials WHERE client_id = $1 AND account_public_id = $2 FOR UPDATE +` + +type LockRegisteredAccountCredentialsParams struct { + ClientID string + AccountPublicID uuid.UUID +} + +func (q *Queries) LockRegisteredAccountCredentials(ctx context.Context, arg LockRegisteredAccountCredentialsParams) (AccountCredential, error) { + row := q.db.QueryRow(ctx, lockRegisteredAccountCredentials, arg.ClientID, arg.AccountPublicID) + var i AccountCredential + err := row.Scan( + &i.RegistrationTokenJti, + &i.SoftwareStatement, + &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 +} + +const revokeRegisteredAccountCredentialsKeys = `-- name: RevokeRegisteredAccountCredentialsKeys :exec +UPDATE credentials_keys SET is_revoked = true, updated_at = now() +WHERE id IN (SELECT credentials_key_id FROM account_credentials_keys WHERE account_credentials_id = $1) +` + +func (q *Queries) RevokeRegisteredAccountCredentialsKeys(ctx context.Context, accountCredentialsID int32) error { + _, err := q.db.Exec(ctx, revokeRegisteredAccountCredentialsKeys, accountCredentialsID) + return err +} + +const revokeRegisteredAccountCredentialsSecrets = `-- name: RevokeRegisteredAccountCredentialsSecrets :exec +UPDATE credentials_secrets SET is_revoked = true, updated_at = now() +WHERE id IN (SELECT credentials_secret_id FROM account_credentials_secrets WHERE account_credentials_id = $1) +` + +func (q *Queries) RevokeRegisteredAccountCredentialsSecrets(ctx context.Context, accountCredentialsID int32) error { + _, err := q.db.Exec(ctx, revokeRegisteredAccountCredentialsSecrets, accountCredentialsID) + return err +} + +const setAccountCredentialsRegistrationState = `-- name: SetAccountCredentialsRegistrationState :exec +UPDATE account_credentials SET registration_token_jti = $2, software_statement = $3 WHERE id = $1 +` + +type SetAccountCredentialsRegistrationStateParams struct { + ID int32 + RegistrationTokenJti pgtype.UUID + SoftwareStatement string +} + +func (q *Queries) SetAccountCredentialsRegistrationState(ctx context.Context, arg SetAccountCredentialsRegistrationStateParams) error { + _, err := q.db.Exec(ctx, setAccountCredentialsRegistrationState, arg.ID, arg.RegistrationTokenJti, arg.SoftwareStatement) + return err +} + const updateAccountCredentials = `-- name: UpdateAccountCredentials :one UPDATE "account_credentials" SET "scopes" = $2, @@ -522,7 +642,7 @@ UPDATE "account_credentials" SET "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 +RETURNING registration_token_jti, software_statement, 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 UpdateAccountCredentialsParams struct { @@ -557,6 +677,8 @@ func (q *Queries) UpdateAccountCredentials(ctx context.Context, arg UpdateAccoun ) var i AccountCredential err := row.Scan( + &i.RegistrationTokenJti, + &i.SoftwareStatement, &i.ID, &i.AccountID, &i.AccountPublicID, @@ -645,7 +767,7 @@ UPDATE "account_credentials" SET "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 +RETURNING registration_token_jti, software_statement, 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 { @@ -728,6 +850,8 @@ func (q *Queries) UpdateRegisteredAccountCredentials(ctx context.Context, arg Up ) var i AccountCredential err := row.Scan( + &i.RegistrationTokenJti, + &i.SoftwareStatement, &i.ID, &i.AccountID, &i.AccountPublicID, diff --git a/idp/internal/providers/database/account_credentials_keys.sql.go b/idp/internal/providers/database/account_credentials_keys.sql.go index b376726..4d3ef43 100644 --- a/idp/internal/providers/database/account_credentials_keys.sql.go +++ b/idp/internal/providers/database/account_credentials_keys.sql.go @@ -187,6 +187,7 @@ WHERE "ack"."account_credentials_id" = $1 AND "ckr"."is_revoked" = false AND "ckr"."expires_at" > now() +ORDER BY "ckr"."created_at" DESC, "ckr"."id" DESC LIMIT 1 ` diff --git a/idp/internal/providers/database/app_keys.sql.go b/idp/internal/providers/database/app_keys.sql.go index 8d8ef76..07c9085 100644 --- a/idp/internal/providers/database/app_keys.sql.go +++ b/idp/internal/providers/database/app_keys.sql.go @@ -87,6 +87,33 @@ func (q *Queries) FindAppKeyByAppIDAndPublicKID(ctx context.Context, arg FindApp return i, err } +const findCurrentAppKey = `-- name: FindCurrentAppKey :one +SELECT c.id, c.public_kid, c.public_key, c.private_key, c.dek_kid, 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 c JOIN app_keys a ON a.credentials_key_id = c.id +WHERE a.app_id = $1 AND NOT c.is_revoked AND c.expires_at > now() +ORDER BY c.created_at DESC, c.id DESC LIMIT 1 +` + +func (q *Queries) FindCurrentAppKey(ctx context.Context, appID int32) (CredentialsKey, error) { + row := q.db.QueryRow(ctx, findCurrentAppKey, appID) + var i CredentialsKey + err := row.Scan( + &i.ID, + &i.PublicKid, + &i.PublicKey, + &i.PrivateKey, + &i.DekKid, + &i.CryptoSuite, + &i.IsRevoked, + &i.IsExternal, + &i.Usage, + &i.AccountID, + &i.ExpiresAt, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + const findPaginatedAppKeysByAppID = `-- name: FindPaginatedAppKeysByAppID :many SELECT ckr.id, ckr.public_kid, ckr.public_key, ckr.private_key, ckr.dek_kid, ckr.crypto_suite, ckr.is_revoked, ckr.is_external, ckr.usage, ckr.account_id, ckr.expires_at, ckr.created_at, ckr.updated_at FROM "credentials_keys" "ckr" LEFT JOIN "app_keys" "ak" ON "ak"."credentials_key_id" = "ckr"."id" diff --git a/idp/internal/providers/database/app_related_apps.sql.go b/idp/internal/providers/database/app_related_apps.sql.go index a663a37..5cab0b3 100644 --- a/idp/internal/providers/database/app_related_apps.sql.go +++ b/idp/internal/providers/database/app_related_apps.sql.go @@ -54,7 +54,7 @@ func (q *Queries) DeleteAppRelatedAppsByAppIDAndRelatedAppIDs(ctx context.Contex } const findRelatedAppsByAppID = `-- name: FindRelatedAppsByAppID :many -SELECT a.id, a.account_id, a.account_public_id, a.client_id, a.version, a.creation_method, a.redirect_uris, a.token_endpoint_auth_method, a.grant_types, a.response_types, a.client_name, a.client_uri, a.logo_uri, a.scopes, a.custom_scopes, a.contacts, a.tos_uri, a.policy_uri, a.jwks_uri, a.jwks, a.software_id, a.software_version, a.domain, a.transport, a.allow_user_registration, a.auth_providers, a.username_column, a.default_scopes, a.default_custom_scopes, a.app_type, a.sector_identifier_uri, a.subject_type, a.id_token_signed_response_alg, a.id_token_encrypted_response_alg, a.id_token_encrypted_response_enc, a.userinfo_signed_response_alg, a.userinfo_encrypted_response_alg, a.userinfo_encrypted_response_enc, a.request_object_signing_alg, a.request_object_encryption_alg, a.request_object_encryption_enc, a.token_endpoint_auth_signing_alg, a.default_max_age, a.require_auth_time, a.default_acr_values, a.initiate_login_uri, a.request_uris, a.access_token_signing_alg, a.session_type, a.access_token_ttl, a.id_token_ttl, a.refresh_token_idle_ttl, a.refresh_token_ttl, a.grant_ttl, a.created_at, a.updated_at FROM "apps" a +SELECT a.registration_token_jti, a.software_statement, a.id, a.account_id, a.account_public_id, a.client_id, a.version, a.creation_method, a.redirect_uris, a.token_endpoint_auth_method, a.grant_types, a.response_types, a.client_name, a.client_uri, a.logo_uri, a.scopes, a.custom_scopes, a.contacts, a.tos_uri, a.policy_uri, a.jwks_uri, a.jwks, a.software_id, a.software_version, a.domain, a.transport, a.allow_user_registration, a.auth_providers, a.username_column, a.default_scopes, a.default_custom_scopes, a.app_type, a.sector_identifier_uri, a.subject_type, a.id_token_signed_response_alg, a.id_token_encrypted_response_alg, a.id_token_encrypted_response_enc, a.userinfo_signed_response_alg, a.userinfo_encrypted_response_alg, a.userinfo_encrypted_response_enc, a.request_object_signing_alg, a.request_object_encryption_alg, a.request_object_encryption_enc, a.token_endpoint_auth_signing_alg, a.default_max_age, a.require_auth_time, a.default_acr_values, a.initiate_login_uri, a.request_uris, a.access_token_signing_alg, a.session_type, a.access_token_ttl, a.id_token_ttl, a.refresh_token_idle_ttl, a.refresh_token_ttl, a.grant_ttl, a.created_at, a.updated_at FROM "apps" a INNER JOIN "app_related_apps" ara ON a.id = ara.related_app_id WHERE ara.app_id = $1 ORDER BY a.client_name ASC @@ -70,6 +70,8 @@ func (q *Queries) FindRelatedAppsByAppID(ctx context.Context, appID int32) ([]Ap for rows.Next() { var i App if err := rows.Scan( + &i.RegistrationTokenJti, + &i.SoftwareStatement, &i.ID, &i.AccountID, &i.AccountPublicID, diff --git a/idp/internal/providers/database/app_secrets.sql.go b/idp/internal/providers/database/app_secrets.sql.go index ffd1491..4c8befe 100644 --- a/idp/internal/providers/database/app_secrets.sql.go +++ b/idp/internal/providers/database/app_secrets.sql.go @@ -84,6 +84,30 @@ func (q *Queries) FindAppSecretByAppIDAndSecretID(ctx context.Context, arg FindA return i, err } +const findCurrentAppSecret = `-- name: FindCurrentAppSecret :one +SELECT c.id, c.secret_id, c.client_secret, c.dek_kid, c.is_revoked, c.usage, c.account_id, c.expires_at, c.created_at, c.updated_at FROM credentials_secrets c JOIN app_secrets a ON a.credentials_secret_id = c.id +WHERE a.app_id = $1 AND NOT c.is_revoked AND c.expires_at > now() +ORDER BY c.created_at DESC, c.id DESC LIMIT 1 +` + +func (q *Queries) FindCurrentAppSecret(ctx context.Context, appID int32) (CredentialsSecret, error) { + row := q.db.QueryRow(ctx, findCurrentAppSecret, appID) + var i CredentialsSecret + err := row.Scan( + &i.ID, + &i.SecretID, + &i.ClientSecret, + &i.DekKid, + &i.IsRevoked, + &i.Usage, + &i.AccountID, + &i.ExpiresAt, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + const findPaginatedAppSecretsByAppID = `-- name: FindPaginatedAppSecretsByAppID :many SELECT csr.id, csr.secret_id, csr.client_secret, csr.dek_kid, csr.is_revoked, csr.usage, csr.account_id, csr.expires_at, csr.created_at, csr.updated_at FROM "credentials_secrets" "csr" LEFT JOIN "app_secrets" "as" ON "as"."credentials_secret_id" = "csr"."id" diff --git a/idp/internal/providers/database/apps.sql.go b/idp/internal/providers/database/apps.sql.go index a97b02e..d0db776 100644 --- a/idp/internal/providers/database/apps.sql.go +++ b/idp/internal/providers/database/apps.sql.go @@ -197,7 +197,7 @@ INSERT INTO "apps" ( $26, $27, $28 -) 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, session_type, access_token_ttl, id_token_ttl, refresh_token_idle_ttl, refresh_token_ttl, grant_ttl, created_at, updated_at +) RETURNING registration_token_jti, software_statement, 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, session_type, access_token_ttl, id_token_ttl, refresh_token_idle_ttl, refresh_token_ttl, grant_ttl, created_at, updated_at ` type CreateAppParams struct { @@ -269,6 +269,8 @@ func (q *Queries) CreateApp(ctx context.Context, arg CreateAppParams) (App, erro ) var i App err := row.Scan( + &i.RegistrationTokenJti, + &i.SoftwareStatement, &i.ID, &i.AccountID, &i.AccountPublicID, @@ -348,8 +350,22 @@ func (q *Queries) DeleteApp(ctx context.Context, id int32) error { return err } +const deleteRegisteredAppGrants = `-- name: DeleteRegisteredAppGrants :exec +DELETE FROM grants WHERE account_id = $1 AND granted_client_id = $2 +` + +type DeleteRegisteredAppGrantsParams struct { + AccountID int32 + GrantedClientID string +} + +func (q *Queries) DeleteRegisteredAppGrants(ctx context.Context, arg DeleteRegisteredAppGrantsParams) error { + _, err := q.db.Exec(ctx, deleteRegisteredAppGrants, arg.AccountID, arg.GrantedClientID) + return err +} + const filterAppsByNameAndByAccountPublicIDOrderedByID = `-- name: FilterAppsByNameAndByAccountPublicIDOrderedByID :many -SELECT 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, session_type, access_token_ttl, id_token_ttl, refresh_token_idle_ttl, refresh_token_ttl, grant_ttl, created_at, updated_at FROM "apps" +SELECT registration_token_jti, software_statement, 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, session_type, access_token_ttl, id_token_ttl, refresh_token_idle_ttl, refresh_token_ttl, grant_ttl, created_at, updated_at FROM "apps" WHERE "account_public_id" = $1 AND "client_name" ILIKE $2 ORDER BY "id" DESC OFFSET $3 LIMIT $4 @@ -377,6 +393,8 @@ func (q *Queries) FilterAppsByNameAndByAccountPublicIDOrderedByID(ctx context.Co for rows.Next() { var i App if err := rows.Scan( + &i.RegistrationTokenJti, + &i.SoftwareStatement, &i.ID, &i.AccountID, &i.AccountPublicID, @@ -445,7 +463,7 @@ func (q *Queries) FilterAppsByNameAndByAccountPublicIDOrderedByID(ctx context.Co } const filterAppsByNameAndByAccountPublicIDOrderedByName = `-- name: FilterAppsByNameAndByAccountPublicIDOrderedByName :many -SELECT 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, session_type, access_token_ttl, id_token_ttl, refresh_token_idle_ttl, refresh_token_ttl, grant_ttl, created_at, updated_at FROM "apps" +SELECT registration_token_jti, software_statement, 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, session_type, access_token_ttl, id_token_ttl, refresh_token_idle_ttl, refresh_token_ttl, grant_ttl, created_at, updated_at FROM "apps" WHERE "account_public_id" = $1 AND "client_name" ILIKE $2 ORDER BY "client_name" ASC OFFSET $3 LIMIT $4 @@ -473,6 +491,8 @@ func (q *Queries) FilterAppsByNameAndByAccountPublicIDOrderedByName(ctx context. for rows.Next() { var i App if err := rows.Scan( + &i.RegistrationTokenJti, + &i.SoftwareStatement, &i.ID, &i.AccountID, &i.AccountPublicID, @@ -541,7 +561,7 @@ func (q *Queries) FilterAppsByNameAndByAccountPublicIDOrderedByName(ctx context. } const filterAppsByNameAndTypeAndByAccountPublicIDOrderedByID = `-- name: FilterAppsByNameAndTypeAndByAccountPublicIDOrderedByID :many -SELECT 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, session_type, access_token_ttl, id_token_ttl, refresh_token_idle_ttl, refresh_token_ttl, grant_ttl, created_at, updated_at FROM "apps" +SELECT registration_token_jti, software_statement, 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, session_type, access_token_ttl, id_token_ttl, refresh_token_idle_ttl, refresh_token_ttl, grant_ttl, created_at, updated_at FROM "apps" WHERE "account_public_id" = $1 AND "client_name" ILIKE $2 AND "app_type" = $3 @@ -573,6 +593,8 @@ func (q *Queries) FilterAppsByNameAndTypeAndByAccountPublicIDOrderedByID(ctx con for rows.Next() { var i App if err := rows.Scan( + &i.RegistrationTokenJti, + &i.SoftwareStatement, &i.ID, &i.AccountID, &i.AccountPublicID, @@ -641,7 +663,7 @@ func (q *Queries) FilterAppsByNameAndTypeAndByAccountPublicIDOrderedByID(ctx con } const filterAppsByNameAndTypeAndByAccountPublicIDOrderedByName = `-- name: FilterAppsByNameAndTypeAndByAccountPublicIDOrderedByName :many -SELECT 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, session_type, access_token_ttl, id_token_ttl, refresh_token_idle_ttl, refresh_token_ttl, grant_ttl, created_at, updated_at FROM "apps" +SELECT registration_token_jti, software_statement, 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, session_type, access_token_ttl, id_token_ttl, refresh_token_idle_ttl, refresh_token_ttl, grant_ttl, created_at, updated_at FROM "apps" WHERE "account_public_id" = $1 AND "client_name" ILIKE $2 AND "app_type" = $3 @@ -673,6 +695,8 @@ func (q *Queries) FilterAppsByNameAndTypeAndByAccountPublicIDOrderedByName(ctx c for rows.Next() { var i App if err := rows.Scan( + &i.RegistrationTokenJti, + &i.SoftwareStatement, &i.ID, &i.AccountID, &i.AccountPublicID, @@ -741,7 +765,7 @@ func (q *Queries) FilterAppsByNameAndTypeAndByAccountPublicIDOrderedByName(ctx c } const filterAppsByTypeAndByAccountPublicIDOrderedByID = `-- name: FilterAppsByTypeAndByAccountPublicIDOrderedByID :many -SELECT 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, session_type, access_token_ttl, id_token_ttl, refresh_token_idle_ttl, refresh_token_ttl, grant_ttl, created_at, updated_at FROM "apps" +SELECT registration_token_jti, software_statement, 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, session_type, access_token_ttl, id_token_ttl, refresh_token_idle_ttl, refresh_token_ttl, grant_ttl, created_at, updated_at FROM "apps" WHERE "account_public_id" = $1 AND "app_type" = $2 ORDER BY "id" DESC OFFSET $3 LIMIT $4 @@ -769,6 +793,8 @@ func (q *Queries) FilterAppsByTypeAndByAccountPublicIDOrderedByID(ctx context.Co for rows.Next() { var i App if err := rows.Scan( + &i.RegistrationTokenJti, + &i.SoftwareStatement, &i.ID, &i.AccountID, &i.AccountPublicID, @@ -837,7 +863,7 @@ func (q *Queries) FilterAppsByTypeAndByAccountPublicIDOrderedByID(ctx context.Co } const filterAppsByTypeAndByAccountPublicIDOrderedByName = `-- name: FilterAppsByTypeAndByAccountPublicIDOrderedByName :many -SELECT 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, session_type, access_token_ttl, id_token_ttl, refresh_token_idle_ttl, refresh_token_ttl, grant_ttl, created_at, updated_at FROM "apps" +SELECT registration_token_jti, software_statement, 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, session_type, access_token_ttl, id_token_ttl, refresh_token_idle_ttl, refresh_token_ttl, grant_ttl, created_at, updated_at FROM "apps" WHERE "account_public_id" = $1 AND "app_type" = $2 ORDER BY "client_name" ASC OFFSET $3 LIMIT $4 @@ -865,6 +891,8 @@ func (q *Queries) FilterAppsByTypeAndByAccountPublicIDOrderedByName(ctx context. for rows.Next() { var i App if err := rows.Scan( + &i.RegistrationTokenJti, + &i.SoftwareStatement, &i.ID, &i.AccountID, &i.AccountPublicID, @@ -933,7 +961,7 @@ func (q *Queries) FilterAppsByTypeAndByAccountPublicIDOrderedByName(ctx context. } const findAppByClientID = `-- name: FindAppByClientID :one -SELECT 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, session_type, access_token_ttl, id_token_ttl, refresh_token_idle_ttl, refresh_token_ttl, grant_ttl, created_at, updated_at FROM "apps" +SELECT registration_token_jti, software_statement, 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, session_type, access_token_ttl, id_token_ttl, refresh_token_idle_ttl, refresh_token_ttl, grant_ttl, created_at, updated_at FROM "apps" WHERE "client_id" = $1 LIMIT 1 ` @@ -941,6 +969,8 @@ func (q *Queries) FindAppByClientID(ctx context.Context, clientID string) (App, row := q.db.QueryRow(ctx, findAppByClientID, clientID) var i App err := row.Scan( + &i.RegistrationTokenJti, + &i.SoftwareStatement, &i.ID, &i.AccountID, &i.AccountPublicID, @@ -1002,7 +1032,7 @@ func (q *Queries) FindAppByClientID(ctx context.Context, clientID string) (App, } const findAppByClientIDAndAccountPublicID = `-- name: FindAppByClientIDAndAccountPublicID :one -SELECT 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, session_type, access_token_ttl, id_token_ttl, refresh_token_idle_ttl, refresh_token_ttl, grant_ttl, created_at, updated_at FROM "apps" +SELECT registration_token_jti, software_statement, 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, session_type, access_token_ttl, id_token_ttl, refresh_token_idle_ttl, refresh_token_ttl, grant_ttl, created_at, updated_at FROM "apps" WHERE "client_id" = $1 AND "account_public_id" = $2 LIMIT 1 ` @@ -1016,6 +1046,8 @@ func (q *Queries) FindAppByClientIDAndAccountPublicID(ctx context.Context, arg F row := q.db.QueryRow(ctx, findAppByClientIDAndAccountPublicID, arg.ClientID, arg.AccountPublicID) var i App err := row.Scan( + &i.RegistrationTokenJti, + &i.SoftwareStatement, &i.ID, &i.AccountID, &i.AccountPublicID, @@ -1077,7 +1109,7 @@ func (q *Queries) FindAppByClientIDAndAccountPublicID(ctx context.Context, arg F } const findAppByClientIDAndVersion = `-- name: FindAppByClientIDAndVersion :one -SELECT 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, session_type, access_token_ttl, id_token_ttl, refresh_token_idle_ttl, refresh_token_ttl, grant_ttl, created_at, updated_at FROM "apps" +SELECT registration_token_jti, software_statement, 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, session_type, access_token_ttl, id_token_ttl, refresh_token_idle_ttl, refresh_token_ttl, grant_ttl, created_at, updated_at FROM "apps" WHERE "client_id" = $1 AND "version" = $2 LIMIT 1 ` @@ -1090,6 +1122,8 @@ func (q *Queries) FindAppByClientIDAndVersion(ctx context.Context, arg FindAppBy row := q.db.QueryRow(ctx, findAppByClientIDAndVersion, arg.ClientID, arg.Version) var i App err := row.Scan( + &i.RegistrationTokenJti, + &i.SoftwareStatement, &i.ID, &i.AccountID, &i.AccountPublicID, @@ -1151,7 +1185,7 @@ func (q *Queries) FindAppByClientIDAndVersion(ctx context.Context, arg FindAppBy } const findAppByID = `-- name: FindAppByID :one -SELECT 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, session_type, access_token_ttl, id_token_ttl, refresh_token_idle_ttl, refresh_token_ttl, grant_ttl, created_at, updated_at FROM "apps" +SELECT registration_token_jti, software_statement, 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, session_type, access_token_ttl, id_token_ttl, refresh_token_idle_ttl, refresh_token_ttl, grant_ttl, created_at, updated_at FROM "apps" WHERE "id" = $1 LIMIT 1 ` @@ -1159,6 +1193,8 @@ func (q *Queries) FindAppByID(ctx context.Context, id int32) (App, error) { row := q.db.QueryRow(ctx, findAppByID, id) var i App err := row.Scan( + &i.RegistrationTokenJti, + &i.SoftwareStatement, &i.ID, &i.AccountID, &i.AccountPublicID, @@ -1220,7 +1256,7 @@ func (q *Queries) FindAppByID(ctx context.Context, id int32) (App, error) { } const findAppsByClientIDsAndAccountID = `-- name: FindAppsByClientIDsAndAccountID :many -SELECT 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, session_type, access_token_ttl, id_token_ttl, refresh_token_idle_ttl, refresh_token_ttl, grant_ttl, created_at, updated_at FROM "apps" +SELECT registration_token_jti, software_statement, 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, session_type, access_token_ttl, id_token_ttl, refresh_token_idle_ttl, refresh_token_ttl, grant_ttl, created_at, updated_at FROM "apps" WHERE "client_id" IN ($3) AND "account_id" = $1 ORDER BY "client_name" ASC LIMIT $2 ` @@ -1241,6 +1277,8 @@ func (q *Queries) FindAppsByClientIDsAndAccountID(ctx context.Context, arg FindA for rows.Next() { var i App if err := rows.Scan( + &i.RegistrationTokenJti, + &i.SoftwareStatement, &i.ID, &i.AccountID, &i.AccountPublicID, @@ -1309,7 +1347,7 @@ func (q *Queries) FindAppsByClientIDsAndAccountID(ctx context.Context, arg FindA } const findPaginatedAppsByAccountPublicIDOrderedByID = `-- name: FindPaginatedAppsByAccountPublicIDOrderedByID :many -SELECT 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, session_type, access_token_ttl, id_token_ttl, refresh_token_idle_ttl, refresh_token_ttl, grant_ttl, created_at, updated_at FROM "apps" +SELECT registration_token_jti, software_statement, 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, session_type, access_token_ttl, id_token_ttl, refresh_token_idle_ttl, refresh_token_ttl, grant_ttl, created_at, updated_at FROM "apps" WHERE "account_public_id" = $1 ORDER BY "id" DESC OFFSET $2 LIMIT $3 @@ -1331,6 +1369,8 @@ func (q *Queries) FindPaginatedAppsByAccountPublicIDOrderedByID(ctx context.Cont for rows.Next() { var i App if err := rows.Scan( + &i.RegistrationTokenJti, + &i.SoftwareStatement, &i.ID, &i.AccountID, &i.AccountPublicID, @@ -1399,7 +1439,7 @@ func (q *Queries) FindPaginatedAppsByAccountPublicIDOrderedByID(ctx context.Cont } const findPaginatedAppsByAccountPublicIDOrderedByName = `-- name: FindPaginatedAppsByAccountPublicIDOrderedByName :many -SELECT 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, session_type, access_token_ttl, id_token_ttl, refresh_token_idle_ttl, refresh_token_ttl, grant_ttl, created_at, updated_at FROM "apps" +SELECT registration_token_jti, software_statement, 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, session_type, access_token_ttl, id_token_ttl, refresh_token_idle_ttl, refresh_token_ttl, grant_ttl, created_at, updated_at FROM "apps" WHERE "account_public_id" = $1 ORDER BY "client_name" ASC OFFSET $2 LIMIT $3 @@ -1421,6 +1461,8 @@ func (q *Queries) FindPaginatedAppsByAccountPublicIDOrderedByName(ctx context.Co for rows.Next() { var i App if err := rows.Scan( + &i.RegistrationTokenJti, + &i.SoftwareStatement, &i.ID, &i.AccountID, &i.AccountPublicID, @@ -1488,6 +1530,116 @@ func (q *Queries) FindPaginatedAppsByAccountPublicIDOrderedByName(ctx context.Co return items, nil } +const lockRegisteredApp = `-- name: LockRegisteredApp :one +SELECT registration_token_jti, software_statement, 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, session_type, access_token_ttl, id_token_ttl, refresh_token_idle_ttl, refresh_token_ttl, grant_ttl, created_at, updated_at FROM apps WHERE client_id = $1 AND account_public_id = $2 FOR UPDATE +` + +type LockRegisteredAppParams struct { + ClientID string + AccountPublicID uuid.UUID +} + +func (q *Queries) LockRegisteredApp(ctx context.Context, arg LockRegisteredAppParams) (App, error) { + row := q.db.QueryRow(ctx, lockRegisteredApp, arg.ClientID, arg.AccountPublicID) + var i App + err := row.Scan( + &i.RegistrationTokenJti, + &i.SoftwareStatement, + &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.SessionType, + &i.AccessTokenTtl, + &i.IDTokenTtl, + &i.RefreshTokenIdleTtl, + &i.RefreshTokenTtl, + &i.GrantTtl, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const revokeRegisteredAppKeys = `-- name: RevokeRegisteredAppKeys :exec +UPDATE credentials_keys SET is_revoked = true, updated_at = now() +WHERE id IN (SELECT credentials_key_id FROM app_keys WHERE app_id = $1) +` + +func (q *Queries) RevokeRegisteredAppKeys(ctx context.Context, appID int32) error { + _, err := q.db.Exec(ctx, revokeRegisteredAppKeys, appID) + return err +} + +const revokeRegisteredAppSecrets = `-- name: RevokeRegisteredAppSecrets :exec +UPDATE credentials_secrets SET is_revoked = true, updated_at = now() +WHERE id IN (SELECT credentials_secret_id FROM app_secrets WHERE app_id = $1) +` + +func (q *Queries) RevokeRegisteredAppSecrets(ctx context.Context, appID int32) error { + _, err := q.db.Exec(ctx, revokeRegisteredAppSecrets, appID) + return err +} + +const setAppRegistrationState = `-- name: SetAppRegistrationState :exec +UPDATE apps SET registration_token_jti = $2, software_statement = $3 WHERE id = $1 +` + +type SetAppRegistrationStateParams struct { + ID int32 + RegistrationTokenJti pgtype.UUID + SoftwareStatement string +} + +func (q *Queries) SetAppRegistrationState(ctx context.Context, arg SetAppRegistrationStateParams) error { + _, err := q.db.Exec(ctx, setAppRegistrationState, arg.ID, arg.RegistrationTokenJti, arg.SoftwareStatement) + return err +} + const updateApp = `-- name: UpdateApp :one UPDATE "apps" SET "client_name" = $2, @@ -1507,7 +1659,7 @@ SET "client_name" = $2, "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, session_type, access_token_ttl, id_token_ttl, refresh_token_idle_ttl, refresh_token_ttl, grant_ttl, created_at, updated_at +RETURNING registration_token_jti, software_statement, 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, session_type, access_token_ttl, id_token_ttl, refresh_token_idle_ttl, refresh_token_ttl, grant_ttl, created_at, updated_at ` type UpdateAppParams struct { @@ -1548,6 +1700,8 @@ func (q *Queries) UpdateApp(ctx context.Context, arg UpdateAppParams) (App, erro ) var i App err := row.Scan( + &i.RegistrationTokenJti, + &i.SoftwareStatement, &i.ID, &i.AccountID, &i.AccountPublicID, @@ -1617,7 +1771,7 @@ SET "scopes" = $2, "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, session_type, access_token_ttl, id_token_ttl, refresh_token_idle_ttl, refresh_token_ttl, grant_ttl, created_at, updated_at +RETURNING registration_token_jti, software_statement, 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, session_type, access_token_ttl, id_token_ttl, refresh_token_idle_ttl, refresh_token_ttl, grant_ttl, created_at, updated_at ` type UpdateAppScopesParams struct { @@ -1638,6 +1792,8 @@ func (q *Queries) UpdateAppScopes(ctx context.Context, arg UpdateAppScopesParams ) var i App err := row.Scan( + &i.RegistrationTokenJti, + &i.SoftwareStatement, &i.ID, &i.AccountID, &i.AccountPublicID, @@ -1703,13 +1859,15 @@ UPDATE "apps" SET "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, session_type, access_token_ttl, id_token_ttl, refresh_token_idle_ttl, refresh_token_ttl, grant_ttl, created_at, updated_at +RETURNING registration_token_jti, software_statement, 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, session_type, access_token_ttl, id_token_ttl, refresh_token_idle_ttl, refresh_token_ttl, grant_ttl, created_at, updated_at ` func (q *Queries) UpdateAppVersion(ctx context.Context, id int32) (App, error) { row := q.db.QueryRow(ctx, updateAppVersion, id) var i App err := row.Scan( + &i.RegistrationTokenJti, + &i.SoftwareStatement, &i.ID, &i.AccountID, &i.AccountPublicID, diff --git a/idp/internal/providers/database/credentials_keys.sql.go b/idp/internal/providers/database/credentials_keys.sql.go index b3916d8..948631e 100644 --- a/idp/internal/providers/database/credentials_keys.sql.go +++ b/idp/internal/providers/database/credentials_keys.sql.go @@ -186,3 +186,18 @@ func (q *Queries) UpdateCredentialsKeyExpiresAtAndCreatedAt(ctx context.Context, _, err := q.db.Exec(ctx, updateCredentialsKeyExpiresAtAndCreatedAt, arg.PublicKid, arg.ExpiresAt, arg.CreatedAt) return err } + +const updateCredentialsKeyPrivateKey = `-- name: UpdateCredentialsKeyPrivateKey :exec +UPDATE credentials_keys SET private_key = $2, dek_kid = $3 WHERE id = $1 +` + +type UpdateCredentialsKeyPrivateKeyParams struct { + ID int32 + PrivateKey string + DekKid string +} + +func (q *Queries) UpdateCredentialsKeyPrivateKey(ctx context.Context, arg UpdateCredentialsKeyPrivateKeyParams) error { + _, err := q.db.Exec(ctx, updateCredentialsKeyPrivateKey, arg.ID, arg.PrivateKey, arg.DekKid) + return err +} diff --git a/idp/internal/providers/database/database.go b/idp/internal/providers/database/database.go index a67bff7..2a8318d 100644 --- a/idp/internal/providers/database/database.go +++ b/idp/internal/providers/database/database.go @@ -16,7 +16,8 @@ import ( ) type Database struct { - connPool *pgxpool.Pool + transaction pgx.Tx + connPool *pgxpool.Pool *Queries } @@ -27,7 +28,23 @@ func NewDatabase(connPool *pgxpool.Pool) *Database { } } +// InTransaction returns a copy bound to tx without changing the shared database. +func (d *Database) InTransaction(tx pgx.Tx) *Database { + return &Database{ + transaction: tx, + connPool: d.connPool, + Queries: New(tx), + } +} + func (d *Database) BeginTx(ctx context.Context) (*Queries, pgx.Tx, error) { + if d.transaction != nil { + tx, err := d.transaction.Begin(ctx) + if err != nil { + return nil, nil, err + } + return d.WithTx(tx), tx, nil + } txn, err := d.connPool.BeginTx(ctx, pgx.TxOptions{ DeferrableMode: pgx.Deferrable, IsoLevel: pgx.ReadCommitted, 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 3383431..ced9eee 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: 2026-09-21T22:02:21.631Z +-- Generated at: 2026-09-22T07:32:32.147Z CREATE TYPE "kek_usage" AS ENUM ( 'global', @@ -365,6 +365,8 @@ CREATE TABLE "account_totps" ( ); CREATE TABLE "account_credentials" ( + "registration_token_jti" uuid UNIQUE, + "software_statement" text NOT NULL DEFAULT '', "id" serial PRIMARY KEY, "account_id" integer NOT NULL, "account_public_id" uuid NOT NULL, @@ -541,6 +543,8 @@ CREATE TABLE "user_credentials_keys" ( ); CREATE TABLE "apps" ( + "registration_token_jti" uuid UNIQUE, + "software_statement" text NOT NULL DEFAULT '', "id" serial PRIMARY KEY, "account_id" integer NOT NULL, "account_public_id" uuid NOT NULL, @@ -1218,6 +1222,8 @@ ALTER TABLE "account_totps" ADD FOREIGN KEY ("totp_id") REFERENCES "totps" ("id" ALTER TABLE "account_credentials" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("id") ON DELETE CASCADE; +ALTER TABLE "account_credentials_secrets" ADD FOREIGN KEY ("account_credentials_id") REFERENCES "account_credentials" ("id") ON DELETE CASCADE; + ALTER TABLE "account_credentials_secrets" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("id") ON DELETE CASCADE; ALTER TABLE "account_credentials_secrets" ADD FOREIGN KEY ("credentials_secret_id") REFERENCES "credentials_secrets" ("id") ON DELETE CASCADE; diff --git a/idp/internal/providers/database/models.go b/idp/internal/providers/database/models.go index e1c4034..e59a00f 100644 --- a/idp/internal/providers/database/models.go +++ b/idp/internal/providers/database/models.go @@ -1420,6 +1420,8 @@ type AccountAuthProvider struct { } type AccountCredential struct { + RegistrationTokenJti pgtype.UUID + SoftwareStatement string ID int32 AccountID int32 AccountPublicID uuid.UUID @@ -1553,6 +1555,8 @@ type AccountTotp struct { } type App struct { + RegistrationTokenJti pgtype.UUID + SoftwareStatement string ID int32 AccountID int32 AccountPublicID uuid.UUID diff --git a/idp/internal/providers/database/queries/account_credential_secrets.sql b/idp/internal/providers/database/queries/account_credential_secrets.sql index bb213db..50ae8fe 100644 --- a/idp/internal/providers/database/queries/account_credential_secrets.sql +++ b/idp/internal/providers/database/queries/account_credential_secrets.sql @@ -47,6 +47,7 @@ WHERE "acs"."account_credentials_id" = $1 AND "csr"."is_revoked" = false AND "csr"."expires_at" > now() +ORDER BY "csr"."created_at" DESC, "csr"."id" DESC LIMIT 1; -- name: FindAccountCredentialSecretByAccountCredentialIDAndCredentialsSecretID :one diff --git a/idp/internal/providers/database/queries/account_credentials.sql b/idp/internal/providers/database/queries/account_credentials.sql index f1ca418..58fbef0 100644 --- a/idp/internal/providers/database/queries/account_credentials.sql +++ b/idp/internal/providers/database/queries/account_credentials.sql @@ -185,3 +185,20 @@ LIMIT 1; -- name: DeleteAllAccountCredentials :exec DELETE FROM "account_credentials"; + +-- name: LockRegisteredAccountCredentials :one +SELECT * FROM account_credentials WHERE client_id = $1 AND account_public_id = $2 FOR UPDATE; + +-- name: SetAccountCredentialsRegistrationState :exec +UPDATE account_credentials SET registration_token_jti = $2, software_statement = $3 WHERE id = $1; + +-- name: RevokeRegisteredAccountCredentialsSecrets :exec +UPDATE credentials_secrets SET is_revoked = true, updated_at = now() +WHERE id IN (SELECT credentials_secret_id FROM account_credentials_secrets WHERE account_credentials_id = $1); + +-- name: RevokeRegisteredAccountCredentialsKeys :exec +UPDATE credentials_keys SET is_revoked = true, updated_at = now() +WHERE id IN (SELECT credentials_key_id FROM account_credentials_keys WHERE account_credentials_id = $1); + +-- name: DeleteRegisteredAccountCredentialsGrants :exec +DELETE FROM grants WHERE account_id = $1 AND granted_client_id = $2; diff --git a/idp/internal/providers/database/queries/account_credentials_keys.sql b/idp/internal/providers/database/queries/account_credentials_keys.sql index 8a63c46..385d36d 100644 --- a/idp/internal/providers/database/queries/account_credentials_keys.sql +++ b/idp/internal/providers/database/queries/account_credentials_keys.sql @@ -47,6 +47,7 @@ WHERE "ack"."account_credentials_id" = $1 AND "ckr"."is_revoked" = false AND "ckr"."expires_at" > now() +ORDER BY "ckr"."created_at" DESC, "ckr"."id" DESC LIMIT 1; -- name: FindAccountCredentialKeyByAccountCredentialIDAndPublicKID :one diff --git a/idp/internal/providers/database/queries/app_keys.sql b/idp/internal/providers/database/queries/app_keys.sql index ae4b34b..795e97b 100644 --- a/idp/internal/providers/database/queries/app_keys.sql +++ b/idp/internal/providers/database/queries/app_keys.sql @@ -35,3 +35,8 @@ WHERE "ak"."app_id" = $1 AND "ckr"."public_kid" = $2 LIMIT 1; + +-- name: FindCurrentAppKey :one +SELECT c.* FROM credentials_keys c JOIN app_keys a ON a.credentials_key_id = c.id +WHERE a.app_id = $1 AND NOT c.is_revoked AND c.expires_at > now() +ORDER BY c.created_at DESC, c.id DESC LIMIT 1; diff --git a/idp/internal/providers/database/queries/app_secrets.sql b/idp/internal/providers/database/queries/app_secrets.sql index fc6547a..c82ed89 100644 --- a/idp/internal/providers/database/queries/app_secrets.sql +++ b/idp/internal/providers/database/queries/app_secrets.sql @@ -35,3 +35,8 @@ WHERE "as"."app_id" = $1 AND "csr"."secret_id" = $2 LIMIT 1; + +-- name: FindCurrentAppSecret :one +SELECT c.* FROM credentials_secrets c JOIN app_secrets a ON a.credentials_secret_id = c.id +WHERE a.app_id = $1 AND NOT c.is_revoked AND c.expires_at > now() +ORDER BY c.created_at DESC, c.id DESC LIMIT 1; diff --git a/idp/internal/providers/database/queries/apps.sql b/idp/internal/providers/database/queries/apps.sql index a7340ab..d305a4d 100644 --- a/idp/internal/providers/database/queries/apps.sql +++ b/idp/internal/providers/database/queries/apps.sql @@ -222,3 +222,20 @@ LIMIT 1; -- name: DeleteAllApps :exec DELETE FROM "apps"; + +-- name: LockRegisteredApp :one +SELECT * FROM apps WHERE client_id = $1 AND account_public_id = $2 FOR UPDATE; + +-- name: SetAppRegistrationState :exec +UPDATE apps SET registration_token_jti = $2, software_statement = $3 WHERE id = $1; + +-- name: RevokeRegisteredAppSecrets :exec +UPDATE credentials_secrets SET is_revoked = true, updated_at = now() +WHERE id IN (SELECT credentials_secret_id FROM app_secrets WHERE app_id = $1); + +-- name: RevokeRegisteredAppKeys :exec +UPDATE credentials_keys SET is_revoked = true, updated_at = now() +WHERE id IN (SELECT credentials_key_id FROM app_keys WHERE app_id = $1); + +-- name: DeleteRegisteredAppGrants :exec +DELETE FROM grants WHERE account_id = $1 AND granted_client_id = $2; diff --git a/idp/internal/providers/database/queries/credentials_keys.sql b/idp/internal/providers/database/queries/credentials_keys.sql index 3e7e63a..b38d292 100644 --- a/idp/internal/providers/database/queries/credentials_keys.sql +++ b/idp/internal/providers/database/queries/credentials_keys.sql @@ -56,3 +56,6 @@ DELETE FROM "credentials_keys"; SELECT * FROM "credentials_keys" WHERE "id" = $1 LIMIT 1; + +-- name: UpdateCredentialsKeyPrivateKey :exec +UPDATE credentials_keys SET private_key = $2, dek_kid = $3 WHERE id = $1; diff --git a/idp/internal/providers/database/registered_apps.sql.go b/idp/internal/providers/database/registered_apps.sql.go index 1bf97ea..883670b 100644 --- a/idp/internal/providers/database/registered_apps.sql.go +++ b/idp/internal/providers/database/registered_apps.sql.go @@ -111,7 +111,7 @@ INSERT INTO "apps" ( $46, $47, $48 -) 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, session_type, access_token_ttl, id_token_ttl, refresh_token_idle_ttl, refresh_token_ttl, grant_ttl, created_at, updated_at +) RETURNING registration_token_jti, software_statement, 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, session_type, access_token_ttl, id_token_ttl, refresh_token_idle_ttl, refresh_token_ttl, grant_ttl, created_at, updated_at ` type CreateRegisteredAppParams struct { @@ -218,6 +218,8 @@ func (q *Queries) CreateRegisteredApp(ctx context.Context, arg CreateRegisteredA ) var i App err := row.Scan( + &i.RegistrationTokenJti, + &i.SoftwareStatement, &i.ID, &i.AccountID, &i.AccountPublicID, @@ -324,7 +326,7 @@ UPDATE "apps" SET "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, session_type, access_token_ttl, id_token_ttl, refresh_token_idle_ttl, refresh_token_ttl, grant_ttl, created_at, updated_at +RETURNING registration_token_jti, software_statement, 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, session_type, access_token_ttl, id_token_ttl, refresh_token_idle_ttl, refresh_token_ttl, grant_ttl, created_at, updated_at ` type UpdateRegisteredAppParams struct { @@ -419,6 +421,8 @@ func (q *Queries) UpdateRegisteredApp(ctx context.Context, arg UpdateRegisteredA ) var i App err := row.Scan( + &i.RegistrationTokenJti, + &i.SoftwareStatement, &i.ID, &i.AccountID, &i.AccountPublicID, diff --git a/idp/internal/providers/tokens/access.go b/idp/internal/providers/tokens/access.go index 4670a88..6b8f964 100644 --- a/idp/internal/providers/tokens/access.go +++ b/idp/internal/providers/tokens/access.go @@ -44,7 +44,7 @@ func (t *Tokens) CreateAccessToken(opts AccountAccessTokenOptions) (*jwt.Token, return accesToken, err } -func (t *Tokens) VerifyAccessToken(token string, getPublicJWK GetPublicJWK) (AccountClaims, []AccountScope, error) { +func (t *Tokens) VerifyAccessToken(token string, getPublicJWK GetPublicJWK, validateClient ...func(string, AccountClaims) error) (AccountClaims, []AccountScope, error) { claims, err := verifyAuthToken(token, buildVerifyKey(utils.SupportedCryptoSuiteES256, getPublicJWK)) if err != nil { return AccountClaims{}, nil, err @@ -55,6 +55,11 @@ func (t *Tokens) VerifyAccessToken(token string, getPublicJWK GetPublicJWK) (Acc return AccountClaims{}, nil, err } + for _, validate := range validateClient { + if err := validate(claims.Subject, claims.AccountClaims); err != nil { + return AccountClaims{}, nil, err + } + } return claims.AccountClaims, scopes, nil } diff --git a/idp/internal/providers/tokens/dynamic_registration_iat.go b/idp/internal/providers/tokens/dynamic_registration_iat.go index 34e053d..0f8f49d 100644 --- a/idp/internal/providers/tokens/dynamic_registration_iat.go +++ b/idp/internal/providers/tokens/dynamic_registration_iat.go @@ -19,8 +19,6 @@ import ( const dynamicRegistrationIATLocation = "dynamic_registration_iat" -const registrationAccessTokenTTLSeconds int64 = 10 * 365 * 24 * 60 * 60 - type DynamicRegistrationUsage string const ( @@ -90,19 +88,21 @@ func (t *Tokens) DynamicRegistrationIAT( func (t *Tokens) DynamicRegistrationAccessToken(opts DynamicRegistrationIATOptions) *jwt.Token { opts.TokenUse = DynamicRegistrationTokenUseRegistration - if opts.TTL == 0 { - opts.TTL = registrationAccessTokenTTLSeconds - } - return t.DynamicRegistrationIAT(opts) + token := t.DynamicRegistrationIAT(opts) + claims := token.Claims.(dynamicRegistrationTokenClaims) + claims.ExpiresAt = nil + token.Claims = claims + return token } type VerifyDynamicRegistrationIATOptions struct { - RequestID string - IAT string - IssuerDomain string - Usage DynamicRegistrationUsage - TokenUse DynamicRegistrationTokenUse - GetPublicJWK GetPublicJWK + RequestID string + IAT string + IssuerDomain string + Usage DynamicRegistrationUsage + TokenUse DynamicRegistrationTokenUse + GetPublicJWK GetPublicJWK + ValidateRegistration func(clientID string, account AccountClaims, jti string, hasExpiry bool) error } func (t *Tokens) VerifyDynamicRegistrationIAT( @@ -120,6 +120,10 @@ func (t *Tokens) VerifyDynamicRegistrationIAT( opts.TokenUse = DynamicRegistrationTokenUseInitialAccess } + parserOptions := []jwt.ParserOption{jwt.WithValidMethods([]string{jwt.SigningMethodEdDSA.Alg()}), jwt.WithIssuer("https://" + opts.IssuerDomain), jwt.WithAudience("https://" + opts.IssuerDomain), jwt.WithIssuedAt()} + if opts.TokenUse != DynamicRegistrationTokenUseRegistration { + parserOptions = append(parserOptions, jwt.WithExpirationRequired()) + } claims := new(dynamicRegistrationTokenClaims) if _, err := jwt.ParseWithClaims(opts.IAT, claims, func(token *jwt.Token) (interface{}, error) { kid, err := extractTokenKID(token) @@ -135,10 +139,7 @@ func (t *Tokens) VerifyDynamicRegistrationIAT( } return jwk.ToUsableKey() - }, jwt.WithValidMethods([]string{jwt.SigningMethodEdDSA.Alg()}), - jwt.WithIssuer("https://"+opts.IssuerDomain), - jwt.WithAudience("https://"+opts.IssuerDomain), - jwt.WithExpirationRequired(), jwt.WithIssuedAt()); err != nil { + }, parserOptions...); err != nil { logger.WarnContext(ctx, "Failed to verify dynamic registration token", "error", err) return "", AccountClaims{}, err } @@ -153,6 +154,14 @@ func (t *Tokens) VerifyDynamicRegistrationIAT( return "", AccountClaims{}, errors.New("registration token use mismatch") } + if opts.TokenUse == DynamicRegistrationTokenUseRegistration { + if opts.ValidateRegistration == nil { + return "", AccountClaims{}, errors.New("registration token state validator required") + } + if err := opts.ValidateRegistration(claims.Subject, claims.AccountClaims, claims.ID, claims.ExpiresAt != nil); err != nil { + return "", AccountClaims{}, err + } + } logger.InfoContext(ctx, "Verified dynamic registration token successfully") return claims.Subject, claims.AccountClaims, nil } @@ -160,7 +169,3 @@ func (t *Tokens) VerifyDynamicRegistrationIAT( 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 index 1433cf6..c317d8c 100644 --- a/idp/internal/providers/tokens/dynamic_registration_iat_test.go +++ b/idp/internal/providers/tokens/dynamic_registration_iat_test.go @@ -4,6 +4,7 @@ import ( "context" "crypto/ed25519" "crypto/rand" + "fmt" "io" "log/slog" "testing" @@ -85,3 +86,53 @@ func TestDynamicRegistrationIATBindings(t *testing.T) { }) } } + +func TestRegistrationAccessTokenHasNoExpiryAndRequiresStateValidation(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} + id, err := uuid.NewV7() + if err != nil { + t.Fatal(err) + } + account := uuid.New() + token := provider.DynamicRegistrationAccessToken(DynamicRegistrationIATOptions{AccountPublicID: account, AccountVersion: 1, IssuerDomain: "id.example.com", Subject: "client", JTI: id.String(), Usage: DynamicRegistrationUsageAccount}) + claims := token.Claims.(dynamicRegistrationTokenClaims) + if claims.ExpiresAt != nil { + t.Fatal("registration token must not expire") + } + token.Header["kid"] = "test" + signed, err := token.SignedString(private) + if err != nil { + t.Fatal(err) + } + opts := VerifyDynamicRegistrationIATOptions{IAT: signed, IssuerDomain: "id.example.com", Usage: DynamicRegistrationUsageAccount, TokenUse: DynamicRegistrationTokenUseRegistration, GetPublicJWK: func(string, utils.SupportedCryptoSuite) (utils.JWK, error) { return &jwk, nil }} + if _, _, err := provider.VerifyDynamicRegistrationIAT(context.Background(), opts); err == nil { + t.Fatal("missing state validator accepted") + } + called := false + opts.ValidateRegistration = func(client string, a AccountClaims, jti string, expiry bool) error { + called = true + if client != "client" || a.AccountID != account || jti != id.String() || expiry { + t.Fatal("incorrect state validation bindings") + } + return nil + } + if _, _, err := provider.VerifyDynamicRegistrationIAT(context.Background(), opts); err != nil { + t.Fatal(err) + } + if !called { + t.Fatal("state validator not called") + } + opts.ValidateRegistration = func(string, AccountClaims, string, bool) error { return fmt.Errorf("revoked") } + if _, _, err := provider.VerifyDynamicRegistrationIAT(context.Background(), opts); err == nil { + t.Fatal("revoked token accepted") + } + opts.TokenUse = DynamicRegistrationTokenUseInitialAccess + if _, _, err := provider.VerifyDynamicRegistrationIAT(context.Background(), opts); err == nil { + t.Fatal("registration token accepted as initial-access token") + } +} diff --git a/idp/internal/server/routes/oauth.go b/idp/internal/server/routes/oauth.go index ea5e5e9..c50a8d0 100644 --- a/idp/internal/server/routes/oauth.go +++ b/idp/internal/server/routes/oauth.go @@ -53,6 +53,11 @@ func (r *Routes) OAuthRoutes(app *fiber.App) { HostAwareRoute(r.controllers.OAuthDynamicRegistrationDelete, r.controllers.OAuthAppDynamicRegistrationDelete), ) + router.All(paths.OAuthRegisterClient, func(ctx fiber.Ctx) error { + ctx.Set(fiber.HeaderAllow, "GET, PUT, DELETE") + return ctx.SendStatus(fiber.StatusMethodNotAllowed) + }) + // Initial Access Token (IAT) routes iatRouter := router.Group(paths.InitialAccessToken, r.controllers.HostMiddleware) iatRouter.Post( diff --git a/idp/internal/server/routes/oauth_test.go b/idp/internal/server/routes/oauth_test.go index f4cf86b..24e161e 100644 --- a/idp/internal/server/routes/oauth_test.go +++ b/idp/internal/server/routes/oauth_test.go @@ -47,3 +47,19 @@ func TestOAuthIATRoutesRejectUnknownHosts(t *testing.T) { }) } } + +func TestRegistrationUnsupportedMethods(t *testing.T) { + ctrl := controllers.NewControllers(slog.New(slog.NewTextHandler(io.Discard, nil)), nil, validator.New(), "example.com", "id.example.com", "session") + app := fiber.New() + NewRoutes(ctrl).OAuthRoutes(app) + for _, method := range []string{"POST", "PATCH"} { + res, err := app.Test(httptest.NewRequest(method, "https://id.example.com"+paths.V1+paths.AuthBase+paths.OAuthBase+paths.OAuthRegister+"/client", nil)) + if err != nil { + t.Fatal(err) + } + res.Body.Close() + if res.StatusCode != 405 || res.Header.Get("Allow") != "GET, PUT, DELETE" { + t.Fatalf("%s: status=%d allow=%q", method, res.StatusCode, res.Header.Get("Allow")) + } + } +} diff --git a/idp/internal/services/account_credentials_registration.go b/idp/internal/services/account_credentials_registration.go index fff2219..970635c 100644 --- a/idp/internal/services/account_credentials_registration.go +++ b/idp/internal/services/account_credentials_registration.go @@ -267,8 +267,18 @@ type CreateAccountCredentialsRegistrationOptions struct { AccessTokenSigningAlg string } -func (s *Services) CreateAccountCredentialsRegistration( +func (s *Services) CreateAccountCredentialsRegistration(ctx context.Context, opts CreateAccountCredentialsRegistrationOptions) (dtos.AccountCredentialsDTO, *exceptions.ServiceError) { + if opts.InitialAccessTokenDomain == "" { + return dtos.AccountCredentialsDTO{}, exceptions.NewError(exceptions.OAuthErrorInvalidToken, "initial access token domain is required") + } + return registrationTransaction(s, ctx, opts.RequestID, func(qrs *database.Queries) (dtos.AccountCredentialsDTO, *exceptions.ServiceError) { + return s.createAccountCredentialsRegistration(ctx, qrs, opts) + }) +} + +func (s *Services) createAccountCredentialsRegistration( ctx context.Context, + qrs *database.Queries, opts CreateAccountCredentialsRegistrationOptions, ) (dtos.AccountCredentialsDTO, *exceptions.ServiceError) { logger := s.buildLogger( @@ -319,11 +329,18 @@ func (s *Services) CreateAccountCredentialsRegistration( if opts.InitialAccessTokenDomain == "" { return dtos.AccountCredentialsDTO{}, exceptions.NewError(exceptions.OAuthErrorInvalidToken, "initial access token domain is required") } - data, preparationErr := s.prepareDynamicRegistration(ctx, prepareDynamicRegistrationOptions{ - requestID: opts.RequestID, accountID: 0, accountPublicID: opts.AccountPublicID, - data: data, softwareStatement: opts.SoftwareStatement, - backendDomain: opts.BackendDomain, frontendDomain: opts.FrontendDomain, app: false, - }) + data, preparationErr := s.prepareDynamicRegistration(ctx, + prepareDynamicRegistrationOptions{ + requestID: opts.RequestID, + accountID: 0, + accountPublicID: opts.AccountPublicID, + data: data, + softwareStatement: opts.SoftwareStatement, + backendDomain: opts.BackendDomain, + frontendDomain: opts.FrontendDomain, + app: false, + }, + ) if preparationErr != nil { return dtos.AccountCredentialsDTO{}, preparationErr } @@ -478,16 +495,6 @@ func (s *Services) CreateAccountCredentialsRegistration( return s.finalizeAccountCredentialsRegistration(ctx, opts, &accountCredentials, "", time.Time{}, nil) } - qrs, txn, err := s.database.BeginTx(ctx) - if err != nil { - logger.ErrorContext(ctx, "Failed to start transaction", "error", err) - return dtos.AccountCredentialsDTO{}, exceptions.FromDBError(err) - } - defer func() { - logger.DebugContext(ctx, "Finalizing transaction") - s.database.FinalizeTx(ctx, txn, err, serviceErr) - }() - accountCredentials, err := qrs.CreateAccountCredentials(ctx, params) if err != nil { logger.ErrorContext(ctx, "Failed to create account credentials", "error", err) @@ -563,7 +570,7 @@ func (s *Services) CreateAccountCredentialsRegistration( return dtos.AccountCredentialsDTO{}, serviceErr } - return s.finalizeAccountCredentialsRegistration(ctx, opts, &accountCredentials, secret, exp, nil) + return s.finalizeAccountCredentialsRegistration(ctx, opts, &accountCredentials, secretID+"."+secret, exp, nil) default: logger.ErrorContext(ctx, "Invalid token endpoint auth method", "tokenEndpointAuthMethod", tokenEndpointAuthMethod) serviceErr = exceptions.NewInternalServerError() @@ -583,12 +590,9 @@ func (s *Services) finalizeAccountCredentialsRegistration( 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, + token, serviceErr := s.registrationResponseToken(ctx, registrationStateOptions{ + RequestID: opts.RequestID, AccountPublicID: opts.AccountPublicID, AccountVersion: opts.AccountVersion, + ClientID: row.ClientID, BackendDomain: opts.BackendDomain, ID: row.ID, Statement: opts.SoftwareStatement, }) if serviceErr != nil { return dtos.AccountCredentialsDTO{}, serviceErr diff --git a/idp/internal/services/account_credentials_registration_iat.go b/idp/internal/services/account_credentials_registration_iat.go index 25df703..d6c9e7a 100644 --- a/idp/internal/services/account_credentials_registration_iat.go +++ b/idp/internal/services/account_credentials_registration_iat.go @@ -134,6 +134,7 @@ func (s *Services) ProcessAccountCredentialsRegistrationIATAuth( } type CreateAccountCredentialsRegistrationAccessTokenOptions struct { + JTI uuid.UUID RequestID string AccountPublicID uuid.UUID AccountVersion int32 @@ -145,6 +146,9 @@ func (s *Services) CreateAccountCredentialsRegistrationAccessToken( ctx context.Context, opts CreateAccountCredentialsRegistrationAccessTokenOptions, ) (string, *exceptions.ServiceError) { + if opts.JTI.Version() != 7 { + return "", exceptions.NewInternalServerError() + } logger := s.buildLogger(opts.RequestID, accountCredentialsRegistrationIATLocation, "CreateAccountCredentialsRegistrationAccessToken").With( "accountPublicId", opts.AccountPublicID, "clientId", opts.ClientID, @@ -158,15 +162,14 @@ func (s *Services) CreateAccountCredentialsRegistrationAccessToken( AccountVersion: opts.AccountVersion, IssuerDomain: opts.BackendDomain, Subject: opts.ClientID, - JTI: utils.Base62UUID(), + JTI: opts.JTI.String(), 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(), + TTL: s.jwt.GetDynamicRegistrationTTL(), }), GetDecryptDEKfn: s.BuildGetGlobalDecDEKFn(ctx, BuildGetGlobalDEKFnOptions{ RequestID: opts.RequestID, @@ -208,20 +211,18 @@ func (s *Services) ProcessAccountCredentialsRegistrationAccessToken( 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, - }), + RequestID: opts.RequestID, + IAT: token, + IssuerDomain: opts.IssuerDomain, + Usage: tokens.DynamicRegistrationUsageAccount, + TokenUse: tokens.DynamicRegistrationTokenUseRegistration, + ValidateRegistration: s.registrationTokenValidator(ctx, false), + GetPublicJWK: s.registrationPublicKey(ctx, 0), }, ) if err != nil { logger.WarnContext(ctx, "Failed to verify account credentials registration access token", "error", err) - return "", tokens.AccountClaims{}, exceptions.NewUnauthorizedError() + return "", tokens.AccountClaims{}, registrationVerificationError(err) } return clientID, accountClaims, nil diff --git a/idp/internal/services/app_dynamic_registration.go b/idp/internal/services/app_dynamic_registration.go index c980004..968053d 100644 --- a/idp/internal/services/app_dynamic_registration.go +++ b/idp/internal/services/app_dynamic_registration.go @@ -343,8 +343,18 @@ type CreateAppCredentialsRegistrationOptions struct { AccessTokenSigningAlg string } -func (s *Services) CreateAppCredentialsRegistration( +func (s *Services) CreateAppCredentialsRegistration(ctx context.Context, opts CreateAppCredentialsRegistrationOptions) (dtos.AppDTO, *exceptions.ServiceError) { + if opts.IsAuthenticated && opts.InitialAccessTokenDomain == "" { + return dtos.AppDTO{}, exceptions.NewError(exceptions.OAuthErrorInvalidToken, "initial access token domain is required") + } + return registrationTransaction(s, ctx, opts.RequestID, func(qrs *database.Queries) (dtos.AppDTO, *exceptions.ServiceError) { + return s.createAppCredentialsRegistration(ctx, qrs, opts) + }) +} + +func (s *Services) createAppCredentialsRegistration( ctx context.Context, + qrs *database.Queries, opts CreateAppCredentialsRegistrationOptions, ) (dtos.AppDTO, *exceptions.ServiceError) { logger := s.buildLogger( @@ -607,16 +617,6 @@ func (s *Services) CreateAppCredentialsRegistration( return s.finalizeRegisteredApp(ctx, opts, accountDTO, &app, "", time.Time{}, nil) } - qrs, txn, err := s.database.BeginTx(ctx) - if err != nil { - logger.ErrorContext(ctx, "Failed to start transaction", "error", err) - return dtos.AppDTO{}, exceptions.FromDBError(err) - } - defer func() { - logger.DebugContext(ctx, "Finalizing transaction") - s.database.FinalizeTx(ctx, txn, err, serviceErr) - }() - app, err := qrs.CreateRegisteredApp(ctx, params) if err != nil { logger.ErrorContext(ctx, "Failed to create app", "error", err) @@ -665,9 +665,9 @@ func (s *Services) CreateAppCredentialsRegistration( return s.finalizeRegisteredApp(ctx, opts, accountDTO, &app, "", dbPrms.ExpiresAt, jwk) case database.AuthMethodClientSecretBasic, database.AuthMethodClientSecretPost, database.AuthMethodClientSecretJwt: var ccID int32 - var secret string + var secretID, secret string var exp time.Time - ccID, _, secret, exp, serviceErr = s.clientCredentialsSecret(ctx, qrs, clientCredentialsSecretOptions{ + ccID, secretID, secret, exp, serviceErr = s.clientCredentialsSecret(ctx, qrs, clientCredentialsSecretOptions{ requestID: opts.RequestID, accountID: opts.AccountID, expiresIn: s.appCCExpDays, @@ -693,10 +693,10 @@ func (s *Services) CreateAppCredentialsRegistration( } if appType == database.AppTypeBackend || appType == database.AppTypeService { - return s.finalizeRegisteredApp(ctx, opts, accountDTO, &app, secret, exp, nil) + return s.finalizeRegisteredApp(ctx, opts, accountDTO, &app, secretID+"."+secret, exp, nil) } - return s.finalizeRegisteredApp(ctx, opts, accountDTO, &app, secret, exp, nil) + return s.finalizeRegisteredApp(ctx, opts, accountDTO, &app, secretID+"."+secret, exp, nil) default: logger.ErrorContext(ctx, "Invalid token endpoint auth method", "tokenEndpointAuthMethod", tokenEndpointAuthMethod) serviceErr = exceptions.NewInternalServerError() @@ -714,12 +714,9 @@ func (s *Services) finalizeRegisteredApp( 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, + token, serviceErr := s.registrationResponseToken(ctx, registrationStateOptions{ + RequestID: opts.RequestID, AccountPublicID: accountDTO.PublicID, AccountVersion: accountDTO.Version(), + ClientID: app.ClientID, BackendDomain: opts.BackendDomain, ID: app.ID, Statement: opts.SoftwareStatement, App: true, }) if serviceErr != nil { return dtos.AppDTO{}, serviceErr diff --git a/idp/internal/services/app_dynamic_registration_iat.go b/idp/internal/services/app_dynamic_registration_iat.go index bd24463..19064b2 100644 --- a/idp/internal/services/app_dynamic_registration_iat.go +++ b/idp/internal/services/app_dynamic_registration_iat.go @@ -147,6 +147,7 @@ func (s *Services) ProcessAppDynamicRegistrationIATAuth( } type CreateAppCredentialsRegistrationAccessTokenOptions struct { + JTI uuid.UUID RequestID string AccountPublicID uuid.UUID AccountVersion int32 @@ -158,6 +159,9 @@ func (s *Services) CreateAppCredentialsRegistrationAccessToken( ctx context.Context, opts CreateAppCredentialsRegistrationAccessTokenOptions, ) (string, *exceptions.ServiceError) { + if opts.JTI.Version() != 7 { + return "", exceptions.NewInternalServerError() + } logger := s.buildLogger(opts.RequestID, appDynamicRegistrationIATLocation, "CreateAppCredentialsRegistrationAccessToken").With( "accountPublicId", opts.AccountPublicID, "clientId", opts.ClientID, @@ -182,10 +186,9 @@ func (s *Services) CreateAppCredentialsRegistrationAccessToken( AccountVersion: opts.AccountVersion, IssuerDomain: fmt.Sprintf("%s.%s", accountDTO.Username, opts.BackendDomain), Subject: opts.ClientID, - JTI: utils.Base62UUID(), + JTI: opts.JTI.String(), Usage: tokens.DynamicRegistrationUsageApp, TokenUse: tokens.DynamicRegistrationTokenUseRegistration, - TTL: s.jwt.GetRegistrationAccessTokenTTL(), }), GetJWKfn: s.BuildGetEncryptedAccountJWKFn(ctx, BuildGetEncryptedAccountJWKFnOptions{ RequestID: opts.RequestID, @@ -235,24 +238,18 @@ func (s *Services) ProcessAppDynamicRegistrationAccessToken( 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, - }, - ), + RequestID: opts.RequestID, + IAT: token, + IssuerDomain: opts.IssuerDomain, + Usage: tokens.DynamicRegistrationUsageApp, + TokenUse: tokens.DynamicRegistrationTokenUseRegistration, + ValidateRegistration: s.registrationTokenValidator(ctx, true), + GetPublicJWK: s.registrationPublicKey(ctx, opts.AccountID), }, ) if err != nil { logger.WarnContext(ctx, "Failed to verify app registration access token", "error", err) - return "", tokens.AccountClaims{}, exceptions.NewUnauthorizedError() + return "", tokens.AccountClaims{}, registrationVerificationError(err) } return clientID, accountClaims, nil diff --git a/idp/internal/services/apps_auth.go b/idp/internal/services/apps_auth.go index de9f9f5..b2c72d0 100644 --- a/idp/internal/services/apps_auth.go +++ b/idp/internal/services/apps_auth.go @@ -49,5 +49,8 @@ func (s *Services) ProcessAppAuthHeader( return tokens.AppClaims{}, exceptions.NewUnauthorizedError() } + if _, serviceErr := s.GetAppByClientIDVersionAndAccountID(ctx, GetAppByClientIDVersionAndAccountIDOptions{RequestID: opts.RequestID, ClientID: appClaims.ClientID, Version: appClaims.Version, AccountID: opts.AccountID}); serviceErr != nil { + return tokens.AppClaims{}, serviceErr + } return appClaims, nil } diff --git a/idp/internal/services/auth.go b/idp/internal/services/auth.go index ce3e8e4..97333f2 100644 --- a/idp/internal/services/auth.go +++ b/idp/internal/services/auth.go @@ -58,6 +58,13 @@ func (s *Services) ProcessAccountAuthHeader( RequestID: opts.RequestID, KeyType: database.TokenKeyTypeAccess, }), + func(subject string, account tokens.AccountClaims) error { + if subject == account.AccountID.String() { + return nil + } + _, err := s.database.FindAccountCredentialsByAccountPublicIDAndClientID(ctx, database.FindAccountCredentialsByAccountPublicIDAndClientIDParams{ClientID: subject, AccountPublicID: account.AccountID}) + return err + }, ) if err != nil { logger.WarnContext(ctx, "Failed to verify access token", "error", err) diff --git a/idp/internal/services/client_credentials.go b/idp/internal/services/client_credentials.go index 49624b3..b37765b 100644 --- a/idp/internal/services/client_credentials.go +++ b/idp/internal/services/client_credentials.go @@ -472,7 +472,7 @@ func (s *Services) processClientCredentialsSecret( secretBytes, err := utils.DecodeBase64Secret(b64Secret) if err != nil { - logger.WarnContext(ctx, "Client secret value is not valid base64", "secretValue", b64Secret) + logger.WarnContext(ctx, "Client secret value is not valid base64") return "", nil, exceptions.NewUnauthorizedError() } diff --git a/idp/internal/services/deks.go b/idp/internal/services/deks.go index a5411d3..301e102 100644 --- a/idp/internal/services/deks.go +++ b/idp/internal/services/deks.go @@ -98,6 +98,11 @@ func (s *Services) BuildGetEncGlobalDEKFn( ctx context.Context, opts BuildGetGlobalDEKFnOptions, ) crypto.GetDEKtoEncrypt { + if s.registrationKeyServices != nil { + opts.Queries = nil + return s.registrationKeyServices.BuildGetEncGlobalDEKFn(ctx, opts) + } + logger := s.buildLogger(opts.RequestID, deksLocation, "BuildGetEncGlobalDEKFn") logger.InfoContext(ctx, "Build GetDEKtoEncrypt function...") return func() (crypto.DEKID, crypto.EncryptedDEK, uuid.UUID, *exceptions.ServiceError) { @@ -291,6 +296,11 @@ func (s *Services) BuildGetEncAccountDEKfn( ctx context.Context, opts BuildGetEncAccountDEKOptions, ) crypto.GetDEKtoEncrypt { + if s.registrationKeyServices != nil { + opts.Queries = nil + return s.registrationKeyServices.BuildGetEncAccountDEKfn(ctx, opts) + } + logger := s.buildLogger(opts.RequestID, deksLocation, "BuildGetEncAccountDEKfn").With( "accountId", opts.AccountID, ) diff --git a/idp/internal/services/jwks.go b/idp/internal/services/jwks.go index c6388ea..047e337 100644 --- a/idp/internal/services/jwks.go +++ b/idp/internal/services/jwks.go @@ -163,6 +163,10 @@ func (s *Services) BuildGetGlobalEncryptedJWKFn( ctx context.Context, opts BuildEncryptedJWKFnOptions, ) crypto.GetEncryptedJWK { + if s.registrationKeyServices != nil { + return s.registrationKeyServices.BuildGetGlobalEncryptedJWKFn(ctx, opts) + } + logger := s.buildLogger(opts.RequestID, jwkLocation, "BuildGetGlobalEncryptedJWKFn").With( "keyType", opts.KeyType, ) @@ -494,6 +498,10 @@ func (s *Services) BuildGetEncryptedAccountJWKFn( ctx context.Context, opts BuildGetEncryptedAccountJWKFnOptions, ) crypto.GetEncryptedJWK { + if s.registrationKeyServices != nil { + return s.registrationKeyServices.BuildGetEncryptedAccountJWKFn(ctx, opts) + } + logger := s.buildLogger(opts.RequestID, jwkLocation, "BuildGetEncryptedAccountJWKFn").With( "keyType", opts.KeyType, "AccountID", opts.AccountID, ) diff --git a/idp/internal/services/oauth_dynamic_registration_config.go b/idp/internal/services/oauth_dynamic_registration_config.go index 4bafeca..33302bd 100644 --- a/idp/internal/services/oauth_dynamic_registration_config.go +++ b/idp/internal/services/oauth_dynamic_registration_config.go @@ -3,13 +3,17 @@ package services import ( "context" "crypto/subtle" + "errors" "fmt" "net/url" "strings" "time" + "github.com/jackc/pgx/v5" + "github.com/google/uuid" "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/services/dtos" @@ -19,24 +23,27 @@ import ( const oauthDynamicRegistrationConfigLocation = "oauth_dynamic_registration_config" type GetRegisteredClientOptions struct { - RequestID string - AccountPublicID uuid.UUID - ClientID string - BackendDomain string - HostUsername string + RegistrationToken string + RequestID string + AccountPublicID uuid.UUID + ClientID string + BackendDomain string + HostUsername string } func (s *Services) getActiveAccountCredentialSecretAndKey( ctx context.Context, requestID string, - accountCredID int32, - authMethod database.AuthMethod, + row *database.AccountCredential, ) (string, time.Time, utils.JWK, *exceptions.ServiceError) { - switch authMethod { + switch row.TokenEndpointAuthMethod { case database.AuthMethodClientSecretBasic, database.AuthMethodClientSecretPost, database.AuthMethodClientSecretJwt: - secretEnt, err := s.database.FindCurrentAccountCredentialSecretByAccountCredentialID(ctx, accountCredID) + secretEnt, err := s.database.FindCurrentAccountCredentialSecretByAccountCredentialID(ctx, row.ID) + if errors.Is(err, pgx.ErrNoRows) { + return s.replaceRegistrationSecret(ctx, requestID, row.AccountID, row.AccountPublicID, row.ID, false) + } if err != nil { - return "", time.Time{}, nil, nil + return "", time.Time{}, nil, exceptions.FromDBError(err) } decryptedSecret, decryptErr := s.crypto.DecryptWithDEK(ctx, crypto.DecryptWithDEKOptions{ RequestID: requestID, @@ -71,12 +78,15 @@ func (s *Services) getActiveAccountCredentialSecretAndKey( } return fmt.Sprintf("%s.%s", secretEnt.SecretID, decryptedSecret), secretEnt.ExpiresAt, nil, nil case database.AuthMethodPrivateKeyJwt: - keyEnt, err := s.database.FindCurrentAccountCredentialKeyByAccountCredentialID(ctx, accountCredID) - if err != nil { + if row.JwksUri.Valid || len(row.Jwks) > 0 { return "", time.Time{}, nil, nil } - jwk, _ := utils.JsonToJWK(keyEnt.PublicKey) - return "", keyEnt.ExpiresAt, jwk, nil + keyEnt, err := s.database.FindCurrentAccountCredentialKeyByAccountCredentialID(ctx, row.ID) + if err != nil { + return "", time.Time{}, nil, exceptions.FromDBError(err) + } + key, serviceErr := s.registrationPrivateKey(ctx, requestID, keyEnt) + return "", keyEnt.ExpiresAt, key, serviceErr default: return "", time.Time{}, nil, nil } @@ -85,40 +95,26 @@ func (s *Services) getActiveAccountCredentialSecretAndKey( func (s *Services) getActiveAppSecretAndKey( ctx context.Context, requestID string, - accountID int32, - appID int32, - authMethod database.AuthMethod, + row *database.App, ) (string, time.Time, utils.JWK, *exceptions.ServiceError) { - switch authMethod { + switch row.TokenEndpointAuthMethod { case database.AuthMethodClientSecretBasic, database.AuthMethodClientSecretPost, database.AuthMethodClientSecretJwt: - secrets, err := s.database.FindPaginatedAppSecretsByAppID(ctx, database.FindPaginatedAppSecretsByAppIDParams{ - AppID: appID, - Offset: 0, - Limit: 10, - }) - if err != nil || len(secrets) == 0 { - return "", time.Time{}, nil, nil - } - var activeSecret *database.CredentialsSecret - now := time.Now() - for i := range secrets { - if !secrets[i].IsRevoked && secrets[i].ExpiresAt.After(now) { - activeSecret = &secrets[i] - break - } + activeSecret, err := s.database.FindCurrentAppSecret(ctx, row.ID) + if errors.Is(err, pgx.ErrNoRows) { + return s.replaceRegistrationSecret(ctx, requestID, row.AccountID, row.AccountPublicID, row.ID, true) } - if activeSecret == nil { - return "", time.Time{}, nil, nil + if err != nil { + return "", time.Time{}, nil, exceptions.FromDBError(err) } decryptedSecret, decryptErr := s.crypto.DecryptWithDEK(ctx, crypto.DecryptWithDEKOptions{ RequestID: requestID, GetDecryptDEKfn: s.BuildGetDecAccountDEKFn(ctx, BuildGetDecAccountDEKFnOptions{ RequestID: requestID, - AccountID: accountID, + AccountID: row.AccountID, }), GetEncryptDEKfn: s.BuildGetEncAccountDEKfn(ctx, BuildGetEncAccountDEKOptions{ RequestID: requestID, - AccountID: accountID, + AccountID: row.AccountID, }), StoreReEncryptedDataFn: func( _ crypto.EntityID, @@ -145,28 +141,25 @@ func (s *Services) getActiveAppSecretAndKey( } return fmt.Sprintf("%s.%s", activeSecret.SecretID, decryptedSecret), activeSecret.ExpiresAt, nil, nil case database.AuthMethodPrivateKeyJwt: - keys, err := s.database.FindPaginatedAppKeysByAppID(ctx, database.FindPaginatedAppKeysByAppIDParams{ - AppID: appID, - Offset: 0, - Limit: 10, - }) - if err != nil || len(keys) == 0 { + if row.JwksUri.Valid || len(row.Jwks) > 0 { return "", time.Time{}, nil, nil } - now := time.Now() - for i := range keys { - if !keys[i].IsRevoked && keys[i].ExpiresAt.After(now) { - jwk, _ := utils.JsonToJWK(keys[i].PublicKey) - return "", keys[i].ExpiresAt, jwk, nil - } + keyEnt, err := s.database.FindCurrentAppKey(ctx, row.ID) + if err != nil { + return "", time.Time{}, nil, exceptions.FromDBError(err) } - return "", time.Time{}, nil, nil + key, serviceErr := s.registrationPrivateKey(ctx, requestID, keyEnt) + return "", keyEnt.ExpiresAt, key, serviceErr default: return "", time.Time{}, nil, nil } } -func (s *Services) GetRegisteredAccountCredentials( +func (s *Services) GetRegisteredAccountCredentials(ctx context.Context, opts GetRegisteredClientOptions) (*dtos.ClientRegistrationDTO, *exceptions.ServiceError) { + return s.getRegisteredAccountCredentials(ctx, opts) +} + +func (s *Services) getRegisteredAccountCredentials( ctx context.Context, opts GetRegisteredClientOptions, ) (*dtos.ClientRegistrationDTO, *exceptions.ServiceError) { @@ -177,8 +170,7 @@ func (s *Services) GetRegisteredAccountCredentials( secret, exp, key, serviceErr := s.getActiveAccountCredentialSecretAndKey( ctx, opts.RequestID, - row.ID, - row.TokenEndpointAuthMethod, + row, ) if serviceErr != nil { return nil, serviceErr @@ -190,17 +182,15 @@ func (s *Services) GetRegisteredAccountCredentials( if serviceErr != nil { return nil, serviceErr } - token, serviceErr := s.CreateAccountCredentialsRegistrationAccessToken(ctx, CreateAccountCredentialsRegistrationAccessTokenOptions{ - RequestID: opts.RequestID, - AccountPublicID: opts.AccountPublicID, - AccountVersion: account.Version(), - ClientID: row.ClientID, - BackendDomain: opts.BackendDomain, + token, serviceErr := s.registrationResponseToken(ctx, registrationStateOptions{ + RequestID: opts.RequestID, AccountPublicID: opts.AccountPublicID, AccountVersion: account.Version(), + ClientID: row.ClientID, BackendDomain: opts.BackendDomain, ID: row.ID, AccountID: row.AccountID, + Statement: row.SoftwareStatement, Stored: row.RegistrationTokenJti, Token: opts.RegistrationToken, App: false, }) if serviceErr != nil { return nil, serviceErr } - dto, serviceErr := dtos.MapRegisteredAccountCredentials(row, "", secret, exp, key) + dto, serviceErr := dtos.MapRegisteredAccountCredentials(row, row.SoftwareStatement, secret, exp, key) if serviceErr != nil { return nil, exceptions.NewUnauthorizedError() } @@ -208,7 +198,11 @@ func (s *Services) GetRegisteredAccountCredentials( return dto.Registration, nil } -func (s *Services) GetRegisteredApp( +func (s *Services) GetRegisteredApp(ctx context.Context, opts GetRegisteredClientOptions) (*dtos.ClientRegistrationDTO, *exceptions.ServiceError) { + return s.getRegisteredApp(ctx, opts) +} + +func (s *Services) getRegisteredApp( ctx context.Context, opts GetRegisteredClientOptions, ) (*dtos.ClientRegistrationDTO, *exceptions.ServiceError) { @@ -217,7 +211,7 @@ func (s *Services) GetRegisteredApp( return nil, serviceErr } secret, exp, key, serviceErr := s.getActiveAppSecretAndKey( - ctx, opts.RequestID, row.AccountID, row.ID, row.TokenEndpointAuthMethod, + ctx, opts.RequestID, row, ) if serviceErr != nil { return nil, serviceErr @@ -229,44 +223,95 @@ func (s *Services) GetRegisteredApp( if serviceErr != nil { return nil, serviceErr } - token, serviceErr := s.CreateAppCredentialsRegistrationAccessToken(ctx, CreateAppCredentialsRegistrationAccessTokenOptions{ - RequestID: opts.RequestID, - AccountPublicID: opts.AccountPublicID, - AccountVersion: account.Version(), - ClientID: row.ClientID, - BackendDomain: opts.BackendDomain, + token, serviceErr := s.registrationResponseToken(ctx, registrationStateOptions{ + RequestID: opts.RequestID, AccountPublicID: opts.AccountPublicID, AccountVersion: account.Version(), + ClientID: row.ClientID, BackendDomain: opts.BackendDomain, ID: row.ID, AccountID: row.AccountID, + Statement: row.SoftwareStatement, Stored: row.RegistrationTokenJti, Token: opts.RegistrationToken, App: true, + HostUsername: opts.HostUsername, }) if serviceErr != nil { return nil, serviceErr } - dto := dtos.MapRegisteredApp(row, "", secret, exp, key) + dto := dtos.MapRegisteredApp(row, row.SoftwareStatement, secret, exp, key) issuer := dynamicRegistrationIssuerDomain(opts.HostUsername, opts.BackendDomain) dto.Registration.WithRegistrationAccess(token, dtos.RegistrationClientURI(issuer, row.ClientID)) return dto.Registration, nil } -func (s *Services) DeleteRegisteredAccountCredentials( +func (s *Services) DeleteRegisteredAccountCredentials(ctx context.Context, opts GetRegisteredClientOptions) *exceptions.ServiceError { + _, err := registrationTransaction(s, ctx, opts.RequestID, func(qrs *database.Queries) (struct{}, *exceptions.ServiceError) { + return struct{}{}, s.deleteRegisteredAccountCredentials(ctx, qrs, opts) + }) + if err == nil { + if cacheErr := s.cache.DeleteResponse(ctx, cache.DeleteResponseOptions{RequestID: opts.RequestID, Key: fmt.Sprintf("%s:%s", accountCredentialsKeysCacheKeyPrefix, opts.AccountPublicID)}); cacheErr != nil { + s.logger.WarnContext(ctx, "Failed to invalidate deleted client key discovery cache", "error", cacheErr) + } + } + return err +} +func (s *Services) deleteRegisteredAccountCredentials( ctx context.Context, + qrs *database.Queries, opts GetRegisteredClientOptions, ) *exceptions.ServiceError { - if _, serviceErr := s.findRegisteredAccountCredential(ctx, opts); serviceErr != nil { + row, serviceErr := s.findRegisteredAccountCredential(ctx, opts) + if serviceErr != nil { return serviceErr } - if err := s.database.DeleteAccountCredentials(ctx, opts.ClientID); err != nil { + // The middleware authenticated before the lock; check state again under it. + var authErr *exceptions.ServiceError + _, _, authErr = s.ProcessAccountCredentialsRegistrationAccessToken(ctx, ProcessAccountCredentialsRegistrationAccessTokenOptions{AuthHeader: "Bearer " + opts.RegistrationToken, IssuerDomain: opts.BackendDomain, RequestID: opts.RequestID}) + if authErr != nil { + return authErr + } + if err := qrs.RevokeRegisteredAccountCredentialsSecrets(ctx, row.ID); err != nil { + return exceptions.FromDBError(err) + } + if err := qrs.RevokeRegisteredAccountCredentialsKeys(ctx, row.ID); err != nil { + return exceptions.FromDBError(err) + } + if err := qrs.DeleteRegisteredAccountCredentialsGrants(ctx, database.DeleteRegisteredAccountCredentialsGrantsParams{AccountID: row.AccountID, GrantedClientID: row.ClientID}); err != nil { + return exceptions.FromDBError(err) + } + + if err := qrs.DeleteAccountCredentials(ctx, opts.ClientID); err != nil { return exceptions.FromDBError(err) } return nil } -func (s *Services) DeleteRegisteredApp( +func (s *Services) DeleteRegisteredApp(ctx context.Context, opts GetRegisteredClientOptions) *exceptions.ServiceError { + _, err := registrationTransaction(s, ctx, opts.RequestID, func(qrs *database.Queries) (struct{}, *exceptions.ServiceError) { + return struct{}{}, s.deleteRegisteredApp(ctx, qrs, opts) + }) + return err +} +func (s *Services) deleteRegisteredApp( ctx context.Context, + qrs *database.Queries, 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 { + // The middleware authenticated before the lock; check state again under it. + var authErr *exceptions.ServiceError + _, _, authErr = s.ProcessAppDynamicRegistrationAccessToken(ctx, ProcessAppDynamicRegistrationAccessTokenOptions{AuthHeader: "Bearer " + opts.RegistrationToken, AccountID: row.AccountID, IssuerDomain: dynamicRegistrationIssuerDomain(opts.HostUsername, opts.BackendDomain), RequestID: opts.RequestID}) + if authErr != nil { + return authErr + } + if err := qrs.RevokeRegisteredAppSecrets(ctx, row.ID); err != nil { + return exceptions.FromDBError(err) + } + if err := qrs.RevokeRegisteredAppKeys(ctx, row.ID); err != nil { + return exceptions.FromDBError(err) + } + if err := qrs.DeleteRegisteredAppGrants(ctx, database.DeleteRegisteredAppGrantsParams{AccountID: row.AccountID, GrantedClientID: row.ClientID}); err != nil { + return exceptions.FromDBError(err) + } + + if err := qrs.DeleteApp(ctx, row.ID); err != nil { return exceptions.FromDBError(err) } return nil @@ -276,12 +321,12 @@ func (s *Services) findRegisteredAccountCredential( ctx context.Context, opts GetRegisteredClientOptions, ) (*database.AccountCredential, *exceptions.ServiceError) { - row, err := s.database.FindAccountCredentialsByAccountPublicIDAndClientID(ctx, database.FindAccountCredentialsByAccountPublicIDAndClientIDParams{ + row, err := s.database.LockRegisteredAccountCredentials(ctx, database.LockRegisteredAccountCredentialsParams{ AccountPublicID: opts.AccountPublicID, ClientID: opts.ClientID, }) if err != nil { - return nil, exceptions.NewError(exceptions.OAuthErrorInvalidToken, "client not found") + return nil, registrationLookupError(err) } return &row, nil } @@ -290,25 +335,34 @@ func (s *Services) findRegisteredApp( ctx context.Context, opts GetRegisteredClientOptions, ) (*database.App, *exceptions.ServiceError) { - row, err := s.database.FindAppByClientIDAndAccountPublicID(ctx, database.FindAppByClientIDAndAccountPublicIDParams{ + row, err := s.database.LockRegisteredApp(ctx, database.LockRegisteredAppParams{ AccountPublicID: opts.AccountPublicID, ClientID: opts.ClientID, }) if err != nil { - return nil, exceptions.NewError(exceptions.OAuthErrorInvalidToken, "client not found") + return nil, registrationLookupError(err) } return &row, nil } type UpdateRegisteredClientOptions struct { CreateAccountCredentialsRegistrationOptions - ClientID string - SubmittedClientID string - SubmittedClientSecret string + ClientID string + SubmittedClientID string + SubmittedClientSecret string + SubmittedClientSecretPresent bool + RegistrationToken string } -func (s *Services) UpdateRegisteredAccountCredentials( +func (s *Services) UpdateRegisteredAccountCredentials(ctx context.Context, opts UpdateRegisteredClientOptions) (*dtos.ClientRegistrationDTO, *exceptions.ServiceError) { + return registrationTransaction(s, ctx, opts.RequestID, func(qrs *database.Queries) (*dtos.ClientRegistrationDTO, *exceptions.ServiceError) { + return s.updateRegisteredAccountCredentials(ctx, qrs, opts) + }) +} + +func (s *Services) updateRegisteredAccountCredentials( ctx context.Context, + qrs *database.Queries, opts UpdateRegisteredClientOptions, ) (*dtos.ClientRegistrationDTO, *exceptions.ServiceError) { logger := s.buildLogger(opts.RequestID, oauthDynamicRegistrationConfigLocation, "UpdateRegisteredAccountCredentials") @@ -324,18 +378,21 @@ func (s *Services) UpdateRegisteredAccountCredentials( currentSecret, exp, key, serviceErr := s.getActiveAccountCredentialSecretAndKey( ctx, opts.RequestID, - existing.ID, - existing.TokenEndpointAuthMethod, + existing, ) if serviceErr != nil { return nil, serviceErr } - if opts.SubmittedClientSecret != "" { + if opts.SubmittedClientSecretPresent || opts.SubmittedClientSecret != "" { if currentSecret == "" || subtle.ConstantTimeCompare([]byte(opts.SubmittedClientSecret), []byte(currentSecret)) != 1 { - return nil, exceptions.NewError(exceptions.OAuthErrorInvalidClient, "invalid client secret") + return nil, exceptions.NewError(exceptions.OAuthErrorInvalidClientMetadata, "invalid client secret") } } data := registrationDataFromAccountOptions(opts.CreateAccountCredentialsRegistrationOptions) + if data.TokenEndpointAuthMethod != "" && data.TokenEndpointAuthMethod != string(existing.TokenEndpointAuthMethod) { + return nil, exceptions.NewValidationError("token_endpoint_auth_method cannot be changed") + } + data.TokenEndpointAuthMethod = string(existing.TokenEndpointAuthMethod) data.ApplicationType = string(existing.CredentialsType) data, preparationErr := s.prepareDynamicRegistration(ctx, prepareDynamicRegistrationOptions{ requestID: opts.RequestID, accountPublicID: opts.AccountPublicID, data: data, @@ -345,6 +402,9 @@ func (s *Services) UpdateRegisteredAccountCredentials( if preparationErr != nil { return nil, preparationErr } + if data.TokenEndpointAuthMethod != string(existing.TokenEndpointAuthMethod) { + return nil, exceptions.NewValidationError("token_endpoint_auth_method cannot be changed") + } if data.ApplicationType != string(existing.CredentialsType) { return nil, exceptions.NewValidationError("application_type cannot be changed") } @@ -374,7 +434,7 @@ func (s *Services) UpdateRegisteredAccountCredentials( if serviceErr != nil { return nil, serviceErr } - updated, err := s.database.UpdateRegisteredAccountCredentials(ctx, database.UpdateRegisteredAccountCredentialsParams{ + updated, err := qrs.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, @@ -397,12 +457,10 @@ func (s *Services) UpdateRegisteredAccountCredentials( if serviceErr != nil { return nil, serviceErr } - token, serviceErr := s.CreateAccountCredentialsRegistrationAccessToken(ctx, CreateAccountCredentialsRegistrationAccessTokenOptions{ - RequestID: opts.RequestID, - AccountPublicID: opts.AccountPublicID, - AccountVersion: opts.AccountVersion, - ClientID: updated.ClientID, - BackendDomain: opts.BackendDomain, + token, serviceErr := s.registrationResponseToken(ctx, registrationStateOptions{ + RequestID: opts.RequestID, AccountPublicID: opts.AccountPublicID, AccountVersion: opts.AccountVersion, + ClientID: updated.ClientID, BackendDomain: opts.BackendDomain, ID: updated.ID, AccountID: updated.AccountID, + Statement: opts.SoftwareStatement, Stored: existing.RegistrationTokenJti, Token: opts.RegistrationToken, App: false, }) if serviceErr != nil { return nil, serviceErr @@ -413,14 +471,23 @@ func (s *Services) UpdateRegisteredAccountCredentials( type UpdateRegisteredAppOptions struct { CreateAppCredentialsRegistrationOptions - ClientID string - SubmittedClientID string - SubmittedClientSecret string - HostUsername string + ClientID string + SubmittedClientID string + SubmittedClientSecret string + SubmittedClientSecretPresent bool + RegistrationToken string + HostUsername string +} + +func (s *Services) UpdateRegisteredApp(ctx context.Context, opts UpdateRegisteredAppOptions) (*dtos.ClientRegistrationDTO, *exceptions.ServiceError) { + return registrationTransaction(s, ctx, opts.RequestID, func(qrs *database.Queries) (*dtos.ClientRegistrationDTO, *exceptions.ServiceError) { + return s.updateRegisteredApp(ctx, qrs, opts) + }) } -func (s *Services) UpdateRegisteredApp( +func (s *Services) updateRegisteredApp( ctx context.Context, + qrs *database.Queries, opts UpdateRegisteredAppOptions, ) (*dtos.ClientRegistrationDTO, *exceptions.ServiceError) { logger := s.buildLogger(opts.RequestID, oauthDynamicRegistrationConfigLocation, "UpdateRegisteredApp") @@ -438,17 +505,21 @@ func (s *Services) UpdateRegisteredApp( return nil, serviceErr } currentSecret, exp, key, serviceErr := s.getActiveAppSecretAndKey( - ctx, opts.RequestID, existing.AccountID, existing.ID, existing.TokenEndpointAuthMethod, + ctx, opts.RequestID, existing, ) if serviceErr != nil { return nil, serviceErr } - if opts.SubmittedClientSecret != "" { + if opts.SubmittedClientSecretPresent || opts.SubmittedClientSecret != "" { if currentSecret == "" || subtle.ConstantTimeCompare([]byte(opts.SubmittedClientSecret), []byte(currentSecret)) != 1 { - return nil, exceptions.NewError(exceptions.OAuthErrorInvalidClient, "invalid client secret") + return nil, exceptions.NewError(exceptions.OAuthErrorInvalidClientMetadata, "invalid client secret") } } data := registrationDataFromAppOptions(opts.CreateAppCredentialsRegistrationOptions) + if data.TokenEndpointAuthMethod != "" && data.TokenEndpointAuthMethod != string(existing.TokenEndpointAuthMethod) { + return nil, exceptions.NewValidationError("token_endpoint_auth_method cannot be changed") + } + data.TokenEndpointAuthMethod = string(existing.TokenEndpointAuthMethod) data.ApplicationType = string(existing.AppType) data, preparationErr := s.prepareDynamicRegistration(ctx, prepareDynamicRegistrationOptions{ requestID: opts.RequestID, accountID: opts.AccountID, data: data, @@ -458,6 +529,9 @@ func (s *Services) UpdateRegisteredApp( if preparationErr != nil { return nil, preparationErr } + if data.TokenEndpointAuthMethod != string(existing.TokenEndpointAuthMethod) { + return nil, exceptions.NewValidationError("token_endpoint_auth_method cannot be changed") + } if data.ApplicationType != string(existing.AppType) { return nil, exceptions.NewValidationError("application_type cannot be changed") } @@ -484,7 +558,7 @@ func (s *Services) UpdateRegisteredApp( if serviceErr != nil { return nil, serviceErr } - updated, err := s.database.UpdateRegisteredApp(ctx, database.UpdateRegisteredAppParams{ + updated, err := qrs.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, @@ -506,12 +580,11 @@ func (s *Services) UpdateRegisteredApp( return nil, exceptions.FromDBError(err) } dto := dtos.MapRegisteredApp(&updated, opts.SoftwareStatement, currentSecret, exp, key) - token, serviceErr := s.CreateAppCredentialsRegistrationAccessToken(ctx, CreateAppCredentialsRegistrationAccessTokenOptions{ - RequestID: opts.RequestID, - AccountPublicID: account.PublicID, - AccountVersion: account.Version(), - ClientID: updated.ClientID, - BackendDomain: opts.BackendDomain, + token, serviceErr := s.registrationResponseToken(ctx, registrationStateOptions{ + RequestID: opts.RequestID, AccountPublicID: account.PublicID, AccountVersion: account.Version(), + ClientID: updated.ClientID, BackendDomain: opts.BackendDomain, ID: updated.ID, AccountID: updated.AccountID, + Statement: opts.SoftwareStatement, Stored: existing.RegistrationTokenJti, Token: opts.RegistrationToken, App: true, + HostUsername: opts.HostUsername, }) if serviceErr != nil { return nil, serviceErr diff --git a/idp/internal/services/registration_credentials.go b/idp/internal/services/registration_credentials.go new file mode 100644 index 0000000..4fa06f7 --- /dev/null +++ b/idp/internal/services/registration_credentials.go @@ -0,0 +1,66 @@ +package services + +import ( + "context" + "fmt" + "github.com/google/uuid" + "github.com/tugascript/devlogs/idp/internal/exceptions" + "github.com/tugascript/devlogs/idp/internal/providers/crypto" + "github.com/tugascript/devlogs/idp/internal/providers/database" + "github.com/tugascript/devlogs/idp/internal/utils" + "time" +) + +func (s *Services) replaceRegistrationSecret(ctx context.Context, requestID string, accountID int32, publicID uuid.UUID, clientID int32, app bool) (string, time.Time, utils.JWK, *exceptions.ServiceError) { + usage := database.CredentialsUsageAccount + ttl := s.accountCCExpDays + dek := s.BuildGetEncGlobalDEKFn(ctx, BuildGetGlobalDEKFnOptions{RequestID: requestID, Queries: s.database.Queries}) + if app { + usage = database.CredentialsUsageApp + ttl = s.appCCExpDays + dek = s.BuildGetEncAccountDEKfn(ctx, BuildGetEncAccountDEKOptions{RequestID: requestID, AccountID: accountID}) + } + id, secretID, secret, exp, serviceErr := s.clientCredentialsSecret(ctx, s.database.Queries, clientCredentialsSecretOptions{requestID: requestID, accountID: accountID, expiresIn: ttl, usage: usage, dekFN: dek}) + if serviceErr != nil { + return "", time.Time{}, nil, serviceErr + } + var err error + if app { + err = s.database.CreateAppSecret(ctx, database.CreateAppSecretParams{AppID: clientID, AccountID: accountID, CredentialsSecretID: id}) + } else { + err = s.database.CreateAccountCredentialSecret(ctx, database.CreateAccountCredentialSecretParams{AccountCredentialsID: clientID, AccountID: accountID, AccountPublicID: publicID, CredentialsSecretID: id, SecretID: secretID}) + } + if err != nil { + return "", time.Time{}, nil, exceptions.FromDBError(err) + } + return fmt.Sprintf("%s.%s", secretID, secret), exp, nil, nil +} + +// Server-generated credential keys use the global DEK in clientCredentialsKey. +func (s *Services) registrationPrivateKey(ctx context.Context, requestID string, key database.CredentialsKey) (utils.JWK, *exceptions.ServiceError) { + if key.IsExternal { + return nil, nil + } + plaintext, serviceErr := s.crypto.DecryptWithDEK(ctx, crypto.DecryptWithDEKOptions{ + RequestID: requestID, + GetDecryptDEKfn: s.BuildGetGlobalDecDEKFn(ctx, BuildGetGlobalDEKFnOptions{RequestID: requestID}), + GetEncryptDEKfn: s.BuildGetEncGlobalDEKFn(ctx, BuildGetGlobalDEKFnOptions{RequestID: requestID, Queries: s.database.Queries}), + StoreReEncryptedDataFn: func(_ crypto.EntityID, dek crypto.DEKID, ciphertext crypto.DEKCiphertext) *exceptions.ServiceError { + if err := s.database.UpdateCredentialsKeyPrivateKey(ctx, database.UpdateCredentialsKeyPrivateKeyParams{ID: key.ID, PrivateKey: ciphertext, DekKid: dek}); err != nil { + return exceptions.FromDBError(err) + } + return nil + }, EntityID: key.PublicKid, Ciphertext: key.PrivateKey, + }) + if serviceErr != nil { + return nil, serviceErr + } + jwk, err := utils.JsonToJWK([]byte(plaintext)) + if err != nil { + return nil, exceptions.NewInternalServerError() + } + if _, err = jwk.ToPrivateKey(); err != nil { + return nil, exceptions.NewInternalServerError() + } + return jwk, nil +} diff --git a/idp/internal/services/registration_management.go b/idp/internal/services/registration_management.go new file mode 100644 index 0000000..f9b3823 --- /dev/null +++ b/idp/internal/services/registration_management.go @@ -0,0 +1,182 @@ +package services + +import ( + "context" + "errors" + + "github.com/google/uuid" + "github.com/jackc/pgx/v5" + "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/utils" +) + +// Keep every client mutation and its registration state in the same transaction. +func registrationTransaction[T any]( + s *Services, + ctx context.Context, + requestID string, + fn func(queries *database.Queries) (T, *exceptions.ServiceError), +) (T, *exceptions.ServiceError) { + logger := s.buildLogger(requestID, "registration", "registrationTransaction") + logger.InfoContext(ctx, "Starting registration transaction") + + var zero T + var serviceErr *exceptions.ServiceError + + qrs, txn, err := s.database.BeginTx(ctx) + if err != nil { + logger.ErrorContext(ctx, "Failed to start transaction", "error", err) + return zero, exceptions.FromDBError(err) + } + defer func() { + logger.DebugContext(ctx, "Finalizing transaction") + s.database.FinalizeTx(ctx, txn, err, serviceErr) + }() + + result, serviceErr := fn(qrs) + if serviceErr != nil { + logger.WarnContext(ctx, "Failed to execute transaction function", "serviceError", serviceErr) + return zero, serviceErr + } + + logger.DebugContext(ctx, "Transaction function executed successfully") + return result, nil +} + +func registrationLookupError(err error) *exceptions.ServiceError { + if errors.Is(err, pgx.ErrNoRows) { + return exceptions.NewError(exceptions.OAuthErrorInvalidToken, "client not found") + } + return exceptions.FromDBError(err) +} + +func registrationVerificationError(err error) *exceptions.ServiceError { + var serviceErr *exceptions.ServiceError + if errors.As(err, &serviceErr) { + return serviceErr + } + return exceptions.NewError(exceptions.OAuthErrorInvalidToken, "invalid registration token") +} + +func validateRegistrationID(stored pgtype.UUID, presented string, hasExpiry bool) error { + if !stored.Valid { + if !hasExpiry { + return exceptions.NewError(exceptions.OAuthErrorInvalidToken, "legacy token requires expiration") + } + return nil + } + id, err := uuid.Parse(presented) + if err != nil || id.Version() != 7 || id != uuid.UUID(stored.Bytes) { + return exceptions.NewError(exceptions.OAuthErrorInvalidToken, "registration token has been superseded") + } + return nil +} + +func (s *Services) registrationTokenValidator(ctx context.Context, app bool) func(string, tokens.AccountClaims, string, bool) error { + return func(clientID string, account tokens.AccountClaims, jti string, hasExpiry bool) error { + var stored pgtype.UUID + if app { + row, err := s.database.FindAppByClientIDAndAccountPublicID(ctx, database.FindAppByClientIDAndAccountPublicIDParams{ClientID: clientID, AccountPublicID: account.AccountID}) + if err != nil { + return registrationLookupError(err) + } + stored = row.RegistrationTokenJti + } else { + row, err := s.database.FindAccountCredentialsByAccountPublicIDAndClientID(ctx, database.FindAccountCredentialsByAccountPublicIDAndClientIDParams{ClientID: clientID, AccountPublicID: account.AccountID}) + if err != nil { + return registrationLookupError(err) + } + stored = row.RegistrationTokenJti + } + return validateRegistrationID(stored, jti, hasExpiry) + } +} + +// Bypass the JWK cache for registration authentication: explicit revocation must +// take effect immediately. Signing-key expiry does not retire verification keys. +func (s *Services) registrationPublicKey(ctx context.Context, accountID int32) tokens.GetPublicJWK { + return func(kid string, suite utils.SupportedCryptoSuite) (utils.JWK, error) { + var row database.TokenSigningKey + var err error + usage := database.TokenKeyUsageGlobal + if accountID == 0 { + row, err = s.database.FindTokenSigningKeyByKID(ctx, kid) + } else { + usage = database.TokenKeyUsageAccount + row, err = s.database.FindAccountTokenSigningKeyByAccountIDAndKID(ctx, database.FindAccountTokenSigningKeyByAccountIDAndKIDParams{AccountID: accountID, Kid: kid}) + } + if err != nil { + return nil, registrationLookupError(err) + } + if row.IsRevoked || row.Usage != usage || row.KeyType != database.TokenKeyTypeDynamicRegistration || string(row.CryptoSuite) != string(suite) { + return nil, exceptions.NewUnauthorizedError() + } + key, err := utils.JsonToJWK(row.PublicKey) + if err != nil { + return nil, exceptions.NewInternalServerError() + } + return key, nil + } +} + +type registrationStateOptions struct { + RequestID, ClientID, BackendDomain, HostUsername, Token, Statement string + AccountPublicID uuid.UUID + AccountID, AccountVersion, ID int32 + Stored pgtype.UUID + App bool +} + +// Call with the client row locked (or freshly inserted). Reauthenticate after +// locking so concurrent legacy upgrades cannot both succeed. +func (s *Services) registrationResponseToken(ctx context.Context, o registrationStateOptions) (string, *exceptions.ServiceError) { + if o.Token != "" { + var serviceErr *exceptions.ServiceError + var clientID string + var account tokens.AccountClaims + if o.App { + clientID, account, serviceErr = s.ProcessAppDynamicRegistrationAccessToken(ctx, ProcessAppDynamicRegistrationAccessTokenOptions{RequestID: o.RequestID, AuthHeader: "Bearer " + o.Token, AccountID: o.AccountID, IssuerDomain: dynamicRegistrationIssuerDomain(o.HostUsername, o.BackendDomain)}) + } else { + clientID, account, serviceErr = s.ProcessAccountCredentialsRegistrationAccessToken(ctx, ProcessAccountCredentialsRegistrationAccessTokenOptions{RequestID: o.RequestID, AuthHeader: "Bearer " + o.Token, IssuerDomain: o.BackendDomain}) + } + if serviceErr != nil { + return "", serviceErr + } + if clientID != o.ClientID || account.AccountID != o.AccountPublicID { + return "", exceptions.NewError(exceptions.OAuthErrorInvalidToken, "registration token binding mismatch") + } + } else if o.Stored.Valid { + return "", exceptions.NewError(exceptions.OAuthErrorInvalidToken, "missing registration token") + } + token := o.Token + jti := o.Stored + if !jti.Valid { + id, err := uuid.NewV7() + if err != nil { + return "", exceptions.NewInternalServerError() + } + jti = pgtype.UUID{Bytes: id, Valid: true} + var serviceErr *exceptions.ServiceError + if o.App { + token, serviceErr = s.CreateAppCredentialsRegistrationAccessToken(ctx, CreateAppCredentialsRegistrationAccessTokenOptions{RequestID: o.RequestID, AccountPublicID: o.AccountPublicID, AccountVersion: o.AccountVersion, ClientID: o.ClientID, BackendDomain: o.BackendDomain, JTI: id}) + } else { + token, serviceErr = s.CreateAccountCredentialsRegistrationAccessToken(ctx, CreateAccountCredentialsRegistrationAccessTokenOptions{RequestID: o.RequestID, AccountPublicID: o.AccountPublicID, AccountVersion: o.AccountVersion, ClientID: o.ClientID, BackendDomain: o.BackendDomain, JTI: id}) + } + if serviceErr != nil { + return "", serviceErr + } + } + var err error + if o.App { + err = s.database.SetAppRegistrationState(ctx, database.SetAppRegistrationStateParams{ID: o.ID, RegistrationTokenJti: jti, SoftwareStatement: o.Statement}) + } else { + err = s.database.SetAccountCredentialsRegistrationState(ctx, database.SetAccountCredentialsRegistrationStateParams{ID: o.ID, RegistrationTokenJti: jti, SoftwareStatement: o.Statement}) + } + if err != nil { + return "", exceptions.FromDBError(err) + } + return token, nil +} diff --git a/idp/internal/services/registration_management_test.go b/idp/internal/services/registration_management_test.go new file mode 100644 index 0000000..1fb43d5 --- /dev/null +++ b/idp/internal/services/registration_management_test.go @@ -0,0 +1,116 @@ +package services + +import ( + "context" + "errors" + "io" + "log/slog" + "testing" + + "github.com/google/uuid" + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgtype" + "github.com/tugascript/devlogs/idp/internal/exceptions" + "github.com/tugascript/devlogs/idp/internal/providers/database" +) + +func TestRegistrationTokenState(t *testing.T) { + id, err := uuid.NewV7() + if err != nil { + t.Fatal(err) + } + stored := pgtype.UUID{Bytes: id, Valid: true} + for _, tc := range []struct { + name string + stored pgtype.UUID + presented string + expiry, valid bool + }{ + {"current", stored, id.String(), false, true}, + {"wrong identifier", stored, uuid.NewString(), false, false}, + {"missing identifier", stored, "", false, false}, + {"legacy allowed before upgrade", pgtype.UUID{}, "legacy-base62-id", true, true}, + {"legacy needs expiry", pgtype.UUID{}, "legacy-base62-id", false, false}, + {"legacy denied after upgrade", stored, "legacy-base62-id", true, false}, + {"v4 is not v7", pgtype.UUID{Bytes: uuid.Nil, Valid: true}, uuid.Nil.String(), false, false}, + } { + t.Run(tc.name, func(t *testing.T) { + err := validateRegistrationID(tc.stored, tc.presented, tc.expiry) + if (err == nil) != tc.valid { + t.Fatalf("valid=%v, error=%v", tc.valid, err) + } + }) + } +} + +// Embedding pgx.Tx leaves unused methods unavailable: any unexpected query in +// these transaction-boundary tests fails immediately. +type registrationTestTx struct { + pgx.Tx + committed, rolledBack bool + commitErr error +} + +func (tx *registrationTestTx) Begin(context.Context) (pgx.Tx, error) { return tx, nil } +func (tx *registrationTestTx) Commit(context.Context) error { tx.committed = true; return tx.commitErr } +func (tx *registrationTestTx) Rollback(context.Context) error { tx.rolledBack = true; return nil } + +func TestRegistrationTransactionDoesNotReturnSuccessBeforeCommit(t *testing.T) { + for _, tc := range []struct { + name string + operationErr, commitErr bool + }{ + {"success", false, false}, {"operation rollback", true, false}, {"commit failure", false, true}, + } { + t.Run(tc.name, func(t *testing.T) { + tx := ®istrationTestTx{} + if tc.commitErr { + tx.commitErr = errors.New("commit failed") + } + db := &database.Database{} + s := &Services{ + database: db.InTransaction(tx), + logger: slog.New(slog.NewTextHandler(io.Discard, nil)), + } + if db.Queries != nil { + t.Fatal("transaction setup mutated shared database") + } + var result string + var err *exceptions.ServiceError + var panicValue any + returned := false + func() { + defer func() { panicValue = recover() }() + result, err = registrationTransaction(s, context.Background(), "test-request", func(qrs *database.Queries) (string, *exceptions.ServiceError) { + if qrs == s.database.Queries { + t.Fatal("callback received shared queries") + } + if tc.operationErr { + return "uncommitted", exceptions.NewValidationError("invalid metadata") + } + return "committed", nil + }) + returned = true + }() + if tc.commitErr { + if panicValue != tx.commitErr || returned { + t.Fatalf("expected commit failure panic, got %v (returned=%v)", panicValue, returned) + } + } else if panicValue != nil || !returned { + t.Fatalf("unexpected panic: %v", panicValue) + } else if tc.operationErr { + if err == nil || result != "" { + t.Fatal("returned an uncommitted response") + } + } else if err != nil || result != "committed" { + t.Fatalf("result=%q error=%v", result, err) + } + if tx.committed == tc.operationErr { + t.Fatal("incorrect commit behavior") + } + if tx.rolledBack != tc.operationErr { + t.Fatal("incorrect rollback behavior") + } + }) + } +} diff --git a/idp/internal/services/services.go b/idp/internal/services/services.go index b11d4d0..a7c152c 100644 --- a/idp/internal/services/services.go +++ b/idp/internal/services/services.go @@ -22,6 +22,8 @@ import ( ) type Services struct { + // Key material is committed independently so a registration rollback never leaves cached, uncommitted keys. + registrationKeyServices *Services logger *slog.Logger database *database.Database cache *cache.Cache diff --git a/idp/internal/services/users_auth.go b/idp/internal/services/users_auth.go index 3f3f9af..8c7d4a0 100644 --- a/idp/internal/services/users_auth.go +++ b/idp/internal/services/users_auth.go @@ -102,6 +102,9 @@ func (s *Services) ProcessUserAuthHeader( return tokens.UserAuthClaims{}, tokens.AppClaims{}, nil, exceptions.NewUnauthorizedError() } + if _, serviceErr := s.GetAppByClientIDVersionAndAccountID(ctx, GetAppByClientIDVersionAndAccountIDOptions{RequestID: opts.RequestID, ClientID: appClaims.ClientID, Version: appClaims.Version, AccountID: opts.AccountID}); serviceErr != nil { + return tokens.UserAuthClaims{}, tokens.AppClaims{}, nil, serviceErr + } return userClaims, appClaims, scopes, nil } diff --git a/idp/tests/dynamic_registration_test.go b/idp/tests/dynamic_registration_test.go index b768816..c3d12aa 100644 --- a/idp/tests/dynamic_registration_test.go +++ b/idp/tests/dynamic_registration_test.go @@ -257,7 +257,7 @@ func setupDynamicRegistration(t *testing.T, appClient, bounded, statement bool) } } -func assertCreatedRegistration(t *testing.T, setup dcrSetup, res *http.Response) (clientID, rat, registrationURI string) { +func assertCreatedRegistration(t *testing.T, setup dcrSetup, res *http.Response) (clientID, rat, registrationURI, secret string) { t.Helper() response := decodeJSONObject(t, res) clientID = jsonString(response["client_id"]) @@ -265,6 +265,10 @@ func assertCreatedRegistration(t *testing.T, setup dcrSetup, res *http.Response) scope := jsonString(response["scope"]) version := jsonString(response["software_version"]) returnedStatement := jsonString(response["software_statement"]) + secret = jsonString(response["client_secret"]) + if parts := strings.Split(secret, "."); len(parts) != 2 || len(parts[0]) != 22 { + t.Fatal("registration did not return a usable secretID.secretValue credential") + } rat = jsonString(response["registration_access_token"]) registrationURI = jsonString(response["registration_client_uri"]) if clientID == "" || returnedName != setup.clientName || returnedStatement != setup.statement { @@ -316,7 +320,7 @@ func assertCreatedRegistration(t *testing.T, setup dcrSetup, res *http.Response) t.Fatalf("creation_method=%q", row.CreationMethod) } } - return clientID, rat, registrationURI + return clientID, rat, registrationURI, secret } // Exercise the actual HTTP route, IAT verification, software-statement trust, @@ -566,10 +570,10 @@ func TestRFC7592ClientConfiguration(t *testing.T) { HostFn: func(setup dcrSetup) string { return setup.host }, ExpStatus: http.StatusCreated, AssertFn: func(t *testing.T, setup dcrSetup, res *http.Response) { - var registrationURI string - clientID, rat, registrationURI = assertCreatedRegistration(t, setup, res) + var registrationURI, secret string + clientID, rat, registrationURI, secret = assertCreatedRegistration(t, setup, res) AssertEqual(t, registrationURI, requestURL(setup.host, oauthRegisterClientPath(clientID))) - assertRFC7592Lifecycle(t, setup, clientID, rat, cfg.BackendDomain()) + assertRFC7592Lifecycle(t, setup, clientID, rat, secret, cfg.BackendDomain()) }, } PerformTestRequestCase(t, http.MethodPost, oauthRegisterPath(), createCase) @@ -602,7 +606,7 @@ type rfc7592Request struct { body any } -func assertRFC7592Lifecycle(t *testing.T, setup dcrSetup, clientID, rat, backendDomain string) { +func assertRFC7592Lifecycle(t *testing.T, setup dcrSetup, clientID, rat, secret, backendDomain string) { t.Helper() updatedName := "Updated " + setup.clientName crossHost := backendDomain @@ -611,6 +615,25 @@ func assertRFC7592Lifecycle(t *testing.T, setup dcrSetup, clientID, rat, backend } clientPath := oauthRegisterClientPath(clientID) + for _, tc := range []struct { + name string + field string + value any + }{ + {"empty secret", "client_secret", ""}, {"null secret", "client_secret", nil}, {"wrong secret", "client_secret", "not-the-secret"}, + {"auth transition", "token_endpoint_auth_method", "none"}, {"wrong ID", "client_id", "another-client"}, + {"server token field", "registration_access_token", rat}, + } { + t.Run(tc.name, func(t *testing.T) { + body := cloneMap(setup.body) + body["client_id"] = clientID + body[tc.field] = tc.value + res := performTestRequest(t, GetTestServer(t).App, 0, http.MethodPut, clientPath, setup.host, "Bearer", rat, "application/json", CreateTestJSONRequestBody(t, body)) + defer res.Body.Close() + AssertTestStatusCode(t, res, http.StatusBadRequest) + }) + } + testCases := []TestRequestCase[rfc7592Request]{ { Name: "get registration", @@ -623,12 +646,9 @@ func assertRFC7592Lifecycle(t *testing.T, setup dcrSetup, clientID, rat, backend AssertEqual(t, res.Header.Get("Cache-Control"), "no-store") AssertEqual(t, jsonString(body["client_id"]), clientID) AssertEqual(t, jsonString(body["client_name"]), req.setup.clientName) - if _, ok := body["client_secret"]; ok { - t.Fatal("GET must not return client_secret") - } - if _, ok := body["registration_access_token"]; ok { - t.Fatal("GET must not return registration_access_token") - } + AssertEqual(t, jsonString(body["client_secret"]), secret) + AssertEqual(t, jsonString(body["registration_access_token"]), rat) + AssertEqual(t, jsonString(body["registration_client_uri"]), requestURL(setup.host, clientPath)) }, Method: http.MethodGet, }, @@ -637,14 +657,19 @@ func assertRFC7592Lifecycle(t *testing.T, setup dcrSetup, clientID, rat, backend ReqFn: func(t *testing.T) (rfc7592Request, string) { body := cloneMap(setup.body) body["client_name"] = updatedName + body["client_id"] = clientID + body["client_secret"] = secret + delete(body, "logo_uri") return rfc7592Request{setup: setup, host: setup.host, path: clientPath, body: body}, rat }, ExpStatus: http.StatusOK, AssertFn: func(t *testing.T, _ rfc7592Request, res *http.Response) { body := decodeJSONObject(t, res) AssertEqual(t, jsonString(body["client_name"]), updatedName) - if _, ok := body["client_secret"]; ok { - t.Fatal("PUT must not return client_secret") + AssertEqual(t, jsonString(body["client_secret"]), secret) + AssertEqual(t, jsonString(body["registration_access_token"]), rat) + if jsonString(body["logo_uri"]) != "" { + t.Fatal("omitted metadata was not cleared") } }, Method: http.MethodPut,