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