mirror of
https://github.com/pocket-id/pocket-id.git
synced 2026-05-04 18:00:38 +03:00
feat: add support for "select_account" prompt (#1453)
Co-authored-by: Elias Schneider <login@eliasschneider.com>
This commit is contained in:
committed by
GitHub
parent
e33a9b8c88
commit
f4706cd6cc
@@ -8,21 +8,28 @@ import (
|
||||
|
||||
type AppError interface {
|
||||
error
|
||||
|
||||
HttpStatusCode() int
|
||||
}
|
||||
|
||||
type AppErrorDescription interface {
|
||||
AppError
|
||||
|
||||
Description() string
|
||||
}
|
||||
|
||||
// Custom error types for various conditions
|
||||
|
||||
type AlreadyInUseError struct {
|
||||
Property string
|
||||
}
|
||||
|
||||
func (e *AlreadyInUseError) Error() string {
|
||||
func (e AlreadyInUseError) Error() string {
|
||||
return e.Property + " is already in use"
|
||||
}
|
||||
func (e *AlreadyInUseError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
func (e AlreadyInUseError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
|
||||
func (e *AlreadyInUseError) Is(target error) bool {
|
||||
func (e AlreadyInUseError) Is(target error) bool {
|
||||
// Ignore the field property when checking if an error is of the type AlreadyInUseError
|
||||
x := &AlreadyInUseError{}
|
||||
return errors.As(target, &x)
|
||||
@@ -30,445 +37,357 @@ func (e *AlreadyInUseError) Is(target error) bool {
|
||||
|
||||
type SetupNotAvailableError struct{}
|
||||
|
||||
func (e *SetupNotAvailableError) Error() string { return "not found" }
|
||||
func (e *SetupNotAvailableError) HttpStatusCode() int { return http.StatusNotFound }
|
||||
func (e SetupNotAvailableError) Error() string { return "not found" }
|
||||
func (e SetupNotAvailableError) HttpStatusCode() int { return http.StatusNotFound }
|
||||
|
||||
type TokenInvalidOrExpiredError struct{}
|
||||
|
||||
func (e *TokenInvalidOrExpiredError) Error() string { return "token is invalid or expired" }
|
||||
func (e *TokenInvalidOrExpiredError) HttpStatusCode() int { return http.StatusUnauthorized }
|
||||
func (e TokenInvalidOrExpiredError) Error() string { return "token is invalid or expired" }
|
||||
func (e TokenInvalidOrExpiredError) HttpStatusCode() int { return http.StatusUnauthorized }
|
||||
|
||||
type DeviceCodeInvalid struct{}
|
||||
|
||||
func (e *DeviceCodeInvalid) Error() string {
|
||||
func (e DeviceCodeInvalid) Error() string {
|
||||
return "one time access code must be used on the device it was generated for"
|
||||
}
|
||||
func (e *DeviceCodeInvalid) HttpStatusCode() int { return http.StatusUnauthorized }
|
||||
func (e DeviceCodeInvalid) HttpStatusCode() int { return http.StatusUnauthorized }
|
||||
|
||||
type TokenInvalidError struct{}
|
||||
|
||||
func (e *TokenInvalidError) Error() string {
|
||||
return "Token is invalid"
|
||||
}
|
||||
func (e *TokenInvalidError) HttpStatusCode() int { return http.StatusUnauthorized }
|
||||
func (e TokenInvalidError) Error() string { return "Token is invalid" }
|
||||
func (e TokenInvalidError) HttpStatusCode() int { return http.StatusUnauthorized }
|
||||
|
||||
type OidcMissingAuthorizationError struct{}
|
||||
|
||||
func (e *OidcMissingAuthorizationError) Error() string { return "missing authorization" }
|
||||
func (e *OidcMissingAuthorizationError) HttpStatusCode() int { return http.StatusForbidden }
|
||||
func (e OidcMissingAuthorizationError) Error() string { return "missing authorization" }
|
||||
func (e OidcMissingAuthorizationError) HttpStatusCode() int { return http.StatusForbidden }
|
||||
|
||||
type OidcGrantTypeNotSupportedError struct{}
|
||||
|
||||
func (e *OidcGrantTypeNotSupportedError) Error() string { return "grant type not supported" }
|
||||
func (e *OidcGrantTypeNotSupportedError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
func (e OidcGrantTypeNotSupportedError) Error() string { return "grant type not supported" }
|
||||
func (e OidcGrantTypeNotSupportedError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
|
||||
type OidcMissingClientCredentialsError struct{}
|
||||
|
||||
func (e *OidcMissingClientCredentialsError) Error() string { return "client id or secret not provided" }
|
||||
func (e *OidcMissingClientCredentialsError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
func (e OidcMissingClientCredentialsError) Error() string { return "client id or secret not provided" }
|
||||
func (e OidcMissingClientCredentialsError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
|
||||
type OidcClientSecretInvalidError struct{}
|
||||
|
||||
func (e *OidcClientSecretInvalidError) Error() string { return "invalid client secret" }
|
||||
func (e *OidcClientSecretInvalidError) HttpStatusCode() int { return http.StatusUnauthorized }
|
||||
func (e OidcClientSecretInvalidError) Error() string { return "invalid client secret" }
|
||||
func (e OidcClientSecretInvalidError) HttpStatusCode() int { return http.StatusUnauthorized }
|
||||
|
||||
type OidcClientAssertionInvalidError struct{}
|
||||
|
||||
func (e *OidcClientAssertionInvalidError) Error() string { return "invalid client assertion" }
|
||||
func (e *OidcClientAssertionInvalidError) HttpStatusCode() int { return http.StatusUnauthorized }
|
||||
func (e OidcClientAssertionInvalidError) Error() string { return "invalid client assertion" }
|
||||
func (e OidcClientAssertionInvalidError) HttpStatusCode() int { return http.StatusUnauthorized }
|
||||
|
||||
type OidcInvalidAuthorizationCodeError struct{}
|
||||
|
||||
func (e *OidcInvalidAuthorizationCodeError) Error() string { return "invalid authorization code" }
|
||||
func (e *OidcInvalidAuthorizationCodeError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
func (e OidcInvalidAuthorizationCodeError) Error() string { return "invalid authorization code" }
|
||||
func (e OidcInvalidAuthorizationCodeError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
|
||||
type OidcClientNotFoundError struct{}
|
||||
|
||||
func (e *OidcClientNotFoundError) Error() string { return "client not found" }
|
||||
func (e *OidcClientNotFoundError) HttpStatusCode() int { return http.StatusNotFound }
|
||||
func (e OidcClientNotFoundError) Error() string { return "client not found" }
|
||||
func (e OidcClientNotFoundError) HttpStatusCode() int { return http.StatusNotFound }
|
||||
|
||||
type OidcMissingCallbackURLError struct{}
|
||||
|
||||
func (e *OidcMissingCallbackURLError) Error() string {
|
||||
func (e OidcMissingCallbackURLError) Error() string {
|
||||
return "unable to detect callback url, it might be necessary for an admin to fix this"
|
||||
}
|
||||
func (e *OidcMissingCallbackURLError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
func (e OidcMissingCallbackURLError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
|
||||
type OidcInvalidCallbackURLError struct{}
|
||||
|
||||
func (e *OidcInvalidCallbackURLError) Error() string {
|
||||
func (e OidcInvalidCallbackURLError) Error() string {
|
||||
return "invalid callback URL, it might be necessary for an admin to fix this"
|
||||
}
|
||||
func (e *OidcInvalidCallbackURLError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
func (e OidcInvalidCallbackURLError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
|
||||
type FileTypeNotSupportedError struct{}
|
||||
|
||||
func (e *FileTypeNotSupportedError) Error() string { return "file type not supported" }
|
||||
func (e *FileTypeNotSupportedError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
func (e FileTypeNotSupportedError) Error() string { return "file type not supported" }
|
||||
func (e FileTypeNotSupportedError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
|
||||
type FileTooLargeError struct {
|
||||
MaxSize string
|
||||
}
|
||||
|
||||
func (e *FileTooLargeError) Error() string {
|
||||
func (e FileTooLargeError) Error() string {
|
||||
return fmt.Sprintf("The file can't be larger than %s", e.MaxSize)
|
||||
}
|
||||
func (e *FileTooLargeError) HttpStatusCode() int { return http.StatusRequestEntityTooLarge }
|
||||
func (e FileTooLargeError) HttpStatusCode() int { return http.StatusRequestEntityTooLarge }
|
||||
|
||||
type NotSignedInError struct{}
|
||||
|
||||
func (e *NotSignedInError) Error() string { return "You are not signed in" }
|
||||
func (e *NotSignedInError) HttpStatusCode() int { return http.StatusUnauthorized }
|
||||
func (e NotSignedInError) Error() string { return "You are not signed in" }
|
||||
func (e NotSignedInError) HttpStatusCode() int { return http.StatusUnauthorized }
|
||||
|
||||
type MissingAccessToken struct{}
|
||||
|
||||
func (e *MissingAccessToken) Error() string { return "Missing access token" }
|
||||
func (e *MissingAccessToken) HttpStatusCode() int { return http.StatusUnauthorized }
|
||||
func (e MissingAccessToken) Error() string { return "Missing access token" }
|
||||
func (e MissingAccessToken) HttpStatusCode() int { return http.StatusUnauthorized }
|
||||
|
||||
type MissingPermissionError struct{}
|
||||
|
||||
func (e *MissingPermissionError) Error() string {
|
||||
func (e MissingPermissionError) Error() string {
|
||||
return "You don't have permission to perform this action"
|
||||
}
|
||||
func (e *MissingPermissionError) HttpStatusCode() int { return http.StatusForbidden }
|
||||
func (e MissingPermissionError) HttpStatusCode() int { return http.StatusForbidden }
|
||||
|
||||
type TooManyRequestsError struct{}
|
||||
|
||||
func (e *TooManyRequestsError) Error() string {
|
||||
return "Too many requests"
|
||||
}
|
||||
func (e *TooManyRequestsError) HttpStatusCode() int { return http.StatusTooManyRequests }
|
||||
func (e TooManyRequestsError) Error() string { return "Too many requests" }
|
||||
func (e TooManyRequestsError) HttpStatusCode() int { return http.StatusTooManyRequests }
|
||||
|
||||
type UserIdNotProvidedError struct{}
|
||||
|
||||
func (e *UserIdNotProvidedError) Error() string {
|
||||
return "User id not provided"
|
||||
}
|
||||
func (e *UserIdNotProvidedError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
func (e UserIdNotProvidedError) Error() string { return "User id not provided" }
|
||||
func (e UserIdNotProvidedError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
|
||||
type UserNotFoundError struct{}
|
||||
|
||||
func (e *UserNotFoundError) Error() string {
|
||||
return "User not found"
|
||||
}
|
||||
func (e *UserNotFoundError) HttpStatusCode() int { return http.StatusNotFound }
|
||||
func (e UserNotFoundError) Error() string { return "User not found" }
|
||||
func (e UserNotFoundError) HttpStatusCode() int { return http.StatusNotFound }
|
||||
|
||||
type ClientIdOrSecretNotProvidedError struct{}
|
||||
|
||||
func (e *ClientIdOrSecretNotProvidedError) Error() string {
|
||||
return "Client id or secret not provided"
|
||||
}
|
||||
func (e *ClientIdOrSecretNotProvidedError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
func (e ClientIdOrSecretNotProvidedError) Error() string { return "Client id or secret not provided" }
|
||||
func (e ClientIdOrSecretNotProvidedError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
|
||||
type WrongFileTypeError struct {
|
||||
ExpectedFileType string
|
||||
}
|
||||
|
||||
func (e *WrongFileTypeError) Error() string {
|
||||
func (e WrongFileTypeError) Error() string {
|
||||
return fmt.Sprintf("File must be of type %s", e.ExpectedFileType)
|
||||
}
|
||||
func (e *WrongFileTypeError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
func (e WrongFileTypeError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
|
||||
type MissingSessionIdError struct{}
|
||||
|
||||
func (e *MissingSessionIdError) Error() string {
|
||||
return "Missing session id"
|
||||
}
|
||||
func (e *MissingSessionIdError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
func (e MissingSessionIdError) Error() string { return "Missing session id" }
|
||||
func (e MissingSessionIdError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
|
||||
type ReservedClaimError struct {
|
||||
Key string
|
||||
}
|
||||
|
||||
func (e *ReservedClaimError) Error() string {
|
||||
func (e ReservedClaimError) Error() string {
|
||||
return fmt.Sprintf("Claim %s is reserved and can't be used", e.Key)
|
||||
}
|
||||
func (e *ReservedClaimError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
func (e ReservedClaimError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
|
||||
type DuplicateClaimError struct {
|
||||
Key string
|
||||
}
|
||||
|
||||
func (e *DuplicateClaimError) Error() string {
|
||||
func (e DuplicateClaimError) Error() string {
|
||||
return fmt.Sprintf("Claim %s is already defined", e.Key)
|
||||
}
|
||||
func (e *DuplicateClaimError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
func (e DuplicateClaimError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
|
||||
type OidcInvalidCodeVerifierError struct{}
|
||||
|
||||
func (e *OidcInvalidCodeVerifierError) Error() string {
|
||||
return "Invalid code verifier"
|
||||
}
|
||||
func (e *OidcInvalidCodeVerifierError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
func (e OidcInvalidCodeVerifierError) Error() string { return "Invalid code verifier" }
|
||||
func (e OidcInvalidCodeVerifierError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
|
||||
type OidcMissingCodeChallengeError struct{}
|
||||
|
||||
func (e *OidcMissingCodeChallengeError) Error() string {
|
||||
return "Missing code challenge"
|
||||
}
|
||||
func (e *OidcMissingCodeChallengeError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
func (e OidcMissingCodeChallengeError) Error() string { return "Missing code challenge" }
|
||||
func (e OidcMissingCodeChallengeError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
|
||||
type LdapUserUpdateError struct{}
|
||||
|
||||
func (e *LdapUserUpdateError) Error() string {
|
||||
return "LDAP users can't be updated"
|
||||
}
|
||||
func (e *LdapUserUpdateError) HttpStatusCode() int { return http.StatusForbidden }
|
||||
func (e LdapUserUpdateError) Error() string { return "LDAP users can't be updated" }
|
||||
func (e LdapUserUpdateError) HttpStatusCode() int { return http.StatusForbidden }
|
||||
|
||||
type LdapUserGroupUpdateError struct{}
|
||||
|
||||
func (e *LdapUserGroupUpdateError) Error() string {
|
||||
return "LDAP user groups can't be updated"
|
||||
}
|
||||
func (e *LdapUserGroupUpdateError) HttpStatusCode() int { return http.StatusForbidden }
|
||||
func (e LdapUserGroupUpdateError) Error() string { return "LDAP user groups can't be updated" }
|
||||
func (e LdapUserGroupUpdateError) HttpStatusCode() int { return http.StatusForbidden }
|
||||
|
||||
type OidcAccessDeniedError struct{}
|
||||
|
||||
func (e *OidcAccessDeniedError) Error() string {
|
||||
return "You're not allowed to access this service"
|
||||
}
|
||||
func (e *OidcAccessDeniedError) HttpStatusCode() int { return http.StatusForbidden }
|
||||
func (e OidcAccessDeniedError) Error() string { return "You're not allowed to access this service" }
|
||||
func (e OidcAccessDeniedError) HttpStatusCode() int { return http.StatusForbidden }
|
||||
|
||||
type OidcClientIdNotMatchingError struct{}
|
||||
|
||||
func (e *OidcClientIdNotMatchingError) Error() string {
|
||||
func (e OidcClientIdNotMatchingError) Error() string {
|
||||
return "Client id in request doesn't match client id in token"
|
||||
}
|
||||
func (e *OidcClientIdNotMatchingError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
func (e OidcClientIdNotMatchingError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
|
||||
type OidcNoCallbackURLError struct{}
|
||||
|
||||
func (e *OidcNoCallbackURLError) Error() string {
|
||||
func (e OidcNoCallbackURLError) Error() string {
|
||||
return "No callback URL provided"
|
||||
}
|
||||
func (e *OidcNoCallbackURLError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
func (e OidcNoCallbackURLError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
|
||||
type UiConfigDisabledError struct{}
|
||||
|
||||
func (e *UiConfigDisabledError) Error() string {
|
||||
func (e UiConfigDisabledError) Error() string {
|
||||
return "The configuration can't be changed since the UI configuration is disabled"
|
||||
}
|
||||
func (e *UiConfigDisabledError) HttpStatusCode() int { return http.StatusForbidden }
|
||||
func (e UiConfigDisabledError) HttpStatusCode() int { return http.StatusForbidden }
|
||||
|
||||
type InvalidUUIDError struct{}
|
||||
|
||||
func (e *InvalidUUIDError) Error() string {
|
||||
return "Invalid UUID"
|
||||
}
|
||||
func (e *InvalidUUIDError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
func (e InvalidUUIDError) Error() string { return "Invalid UUID" }
|
||||
func (e InvalidUUIDError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
|
||||
type OneTimeAccessDisabledError struct{}
|
||||
|
||||
func (e *OneTimeAccessDisabledError) Error() string {
|
||||
return "One-time access is disabled"
|
||||
}
|
||||
func (e *OneTimeAccessDisabledError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
func (e OneTimeAccessDisabledError) Error() string { return "One-time access is disabled" }
|
||||
func (e OneTimeAccessDisabledError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
|
||||
type InvalidAPIKeyError struct{}
|
||||
|
||||
func (e *InvalidAPIKeyError) Error() string {
|
||||
return "Invalid Api Key"
|
||||
}
|
||||
func (e *InvalidAPIKeyError) HttpStatusCode() int { return http.StatusUnauthorized }
|
||||
func (e InvalidAPIKeyError) Error() string { return "Invalid Api Key" }
|
||||
func (e InvalidAPIKeyError) HttpStatusCode() int { return http.StatusUnauthorized }
|
||||
|
||||
type NoAPIKeyProvidedError struct{}
|
||||
|
||||
func (e *NoAPIKeyProvidedError) Error() string {
|
||||
return "No API Key Provided"
|
||||
}
|
||||
func (e *NoAPIKeyProvidedError) HttpStatusCode() int { return http.StatusUnauthorized }
|
||||
func (e NoAPIKeyProvidedError) Error() string { return "No API Key Provided" }
|
||||
func (e NoAPIKeyProvidedError) HttpStatusCode() int { return http.StatusUnauthorized }
|
||||
|
||||
type APIKeyNotFoundError struct{}
|
||||
|
||||
func (e *APIKeyNotFoundError) Error() string {
|
||||
return "API Key Not Found"
|
||||
}
|
||||
func (e *APIKeyNotFoundError) HttpStatusCode() int { return http.StatusUnauthorized }
|
||||
func (e APIKeyNotFoundError) Error() string { return "API Key Not Found" }
|
||||
func (e APIKeyNotFoundError) HttpStatusCode() int { return http.StatusUnauthorized }
|
||||
|
||||
type APIKeyNotExpiredError struct{}
|
||||
|
||||
func (e *APIKeyNotExpiredError) Error() string {
|
||||
return "API Key is not expired yet"
|
||||
}
|
||||
func (e *APIKeyNotExpiredError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
func (e APIKeyNotExpiredError) Error() string { return "API Key is not expired yet" }
|
||||
func (e APIKeyNotExpiredError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
|
||||
type APIKeyExpirationDateError struct{}
|
||||
|
||||
func (e *APIKeyExpirationDateError) Error() string {
|
||||
func (e APIKeyExpirationDateError) Error() string {
|
||||
return "API Key expiration time must be in the future"
|
||||
}
|
||||
func (e *APIKeyExpirationDateError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
func (e APIKeyExpirationDateError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
|
||||
type APIKeyAuthNotAllowedError struct{}
|
||||
|
||||
func (e *APIKeyAuthNotAllowedError) Error() string {
|
||||
func (e APIKeyAuthNotAllowedError) Error() string {
|
||||
return "API key authentication is not allowed for this endpoint"
|
||||
}
|
||||
func (e *APIKeyAuthNotAllowedError) HttpStatusCode() int { return http.StatusForbidden }
|
||||
func (e APIKeyAuthNotAllowedError) HttpStatusCode() int { return http.StatusForbidden }
|
||||
|
||||
type OidcInvalidRefreshTokenError struct{}
|
||||
|
||||
func (e *OidcInvalidRefreshTokenError) Error() string {
|
||||
return "refresh token is invalid or expired"
|
||||
}
|
||||
func (e *OidcInvalidRefreshTokenError) HttpStatusCode() int {
|
||||
return http.StatusBadRequest
|
||||
}
|
||||
func (e OidcInvalidRefreshTokenError) Error() string { return "refresh token is invalid or expired" }
|
||||
func (e OidcInvalidRefreshTokenError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
|
||||
type OidcMissingRefreshTokenError struct{}
|
||||
|
||||
func (e *OidcMissingRefreshTokenError) Error() string {
|
||||
return "refresh token is required"
|
||||
}
|
||||
func (e *OidcMissingRefreshTokenError) HttpStatusCode() int {
|
||||
return http.StatusBadRequest
|
||||
}
|
||||
func (e OidcMissingRefreshTokenError) Error() string { return "refresh token is required" }
|
||||
func (e OidcMissingRefreshTokenError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
|
||||
type OidcMissingAuthorizationCodeError struct{}
|
||||
|
||||
func (e *OidcMissingAuthorizationCodeError) Error() string {
|
||||
return "authorization code is required"
|
||||
}
|
||||
func (e *OidcMissingAuthorizationCodeError) HttpStatusCode() int {
|
||||
return http.StatusBadRequest
|
||||
}
|
||||
func (e OidcMissingAuthorizationCodeError) Error() string { return "authorization code is required" }
|
||||
func (e OidcMissingAuthorizationCodeError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
|
||||
type UserDisabledError struct{}
|
||||
|
||||
func (e *UserDisabledError) Error() string {
|
||||
return "User account is disabled"
|
||||
}
|
||||
func (e *UserDisabledError) HttpStatusCode() int {
|
||||
return http.StatusForbidden
|
||||
}
|
||||
func (e UserDisabledError) Error() string { return "User account is disabled" }
|
||||
func (e UserDisabledError) HttpStatusCode() int { return http.StatusForbidden }
|
||||
|
||||
type ValidationError struct {
|
||||
Message string
|
||||
}
|
||||
type ValidationError struct{ Message string }
|
||||
|
||||
func (e *ValidationError) Error() string {
|
||||
return e.Message
|
||||
}
|
||||
func (e ValidationError) Error() string { return e.Message }
|
||||
|
||||
func (e *ValidationError) HttpStatusCode() int {
|
||||
return http.StatusBadRequest
|
||||
}
|
||||
func (e ValidationError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
|
||||
type OidcDeviceCodeExpiredError struct{}
|
||||
|
||||
func (e *OidcDeviceCodeExpiredError) Error() string {
|
||||
return "device code has expired"
|
||||
}
|
||||
func (e *OidcDeviceCodeExpiredError) HttpStatusCode() int {
|
||||
return http.StatusBadRequest
|
||||
}
|
||||
func (e OidcDeviceCodeExpiredError) Error() string { return "device code has expired" }
|
||||
func (e OidcDeviceCodeExpiredError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
|
||||
type OidcInvalidDeviceCodeError struct{}
|
||||
|
||||
func (e *OidcInvalidDeviceCodeError) Error() string {
|
||||
return "invalid device code"
|
||||
}
|
||||
func (e *OidcInvalidDeviceCodeError) HttpStatusCode() int {
|
||||
return http.StatusBadRequest
|
||||
}
|
||||
func (e OidcInvalidDeviceCodeError) Error() string { return "invalid device code" }
|
||||
func (e OidcInvalidDeviceCodeError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
|
||||
type OidcSlowDownError struct{}
|
||||
|
||||
func (e *OidcSlowDownError) Error() string {
|
||||
return "polling too frequently"
|
||||
}
|
||||
func (e *OidcSlowDownError) HttpStatusCode() int {
|
||||
return http.StatusTooManyRequests
|
||||
}
|
||||
func (e OidcSlowDownError) Error() string { return "polling too frequently" }
|
||||
func (e OidcSlowDownError) HttpStatusCode() int { return http.StatusTooManyRequests }
|
||||
|
||||
type OidcAuthorizationPendingError struct{}
|
||||
|
||||
func (e *OidcAuthorizationPendingError) Error() string {
|
||||
return "authorization is still pending"
|
||||
}
|
||||
func (e *OidcAuthorizationPendingError) HttpStatusCode() int {
|
||||
return http.StatusBadRequest
|
||||
}
|
||||
func (e OidcAuthorizationPendingError) Error() string { return "authorization is still pending" }
|
||||
func (e OidcAuthorizationPendingError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
|
||||
type ReauthenticationRequiredError struct{}
|
||||
|
||||
func (e *ReauthenticationRequiredError) Error() string {
|
||||
return "reauthentication required"
|
||||
}
|
||||
func (e *ReauthenticationRequiredError) HttpStatusCode() int {
|
||||
return http.StatusUnauthorized
|
||||
}
|
||||
func (e ReauthenticationRequiredError) Error() string { return "reauthentication required" }
|
||||
func (e ReauthenticationRequiredError) HttpStatusCode() int { return http.StatusUnauthorized }
|
||||
|
||||
type OpenSignupDisabledError struct{}
|
||||
|
||||
func (e *OpenSignupDisabledError) Error() string {
|
||||
return "Open user signup is not enabled"
|
||||
}
|
||||
func (e OpenSignupDisabledError) Error() string { return "Open user signup is not enabled" }
|
||||
|
||||
func (e *OpenSignupDisabledError) HttpStatusCode() int {
|
||||
return http.StatusForbidden
|
||||
}
|
||||
func (e OpenSignupDisabledError) HttpStatusCode() int { return http.StatusForbidden }
|
||||
|
||||
type ClientIdAlreadyExistsError struct{}
|
||||
|
||||
func (e *ClientIdAlreadyExistsError) Error() string {
|
||||
return "Client ID already in use"
|
||||
}
|
||||
func (e ClientIdAlreadyExistsError) Error() string { return "Client ID already in use" }
|
||||
|
||||
func (e *ClientIdAlreadyExistsError) HttpStatusCode() int {
|
||||
return http.StatusBadRequest
|
||||
}
|
||||
func (e ClientIdAlreadyExistsError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
|
||||
type UserEmailNotSetError struct{}
|
||||
|
||||
func (e *UserEmailNotSetError) Error() string {
|
||||
return "The user does not have an email address set"
|
||||
}
|
||||
func (e UserEmailNotSetError) Error() string { return "The user does not have an email address set" }
|
||||
|
||||
func (e *UserEmailNotSetError) HttpStatusCode() int {
|
||||
return http.StatusBadRequest
|
||||
}
|
||||
func (e UserEmailNotSetError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
|
||||
type ImageNotFoundError struct{}
|
||||
|
||||
func (e *ImageNotFoundError) Error() string {
|
||||
return "Image not found"
|
||||
}
|
||||
func (e ImageNotFoundError) Error() string { return "Image not found" }
|
||||
|
||||
func (e *ImageNotFoundError) HttpStatusCode() int {
|
||||
return http.StatusNotFound
|
||||
}
|
||||
func (e ImageNotFoundError) HttpStatusCode() int { return http.StatusNotFound }
|
||||
|
||||
type InvalidEmailVerificationTokenError struct{}
|
||||
|
||||
func (e *InvalidEmailVerificationTokenError) Error() string {
|
||||
return "Invalid email verification token"
|
||||
}
|
||||
func (e InvalidEmailVerificationTokenError) Error() string { return "Invalid email verification token" }
|
||||
|
||||
func (e *InvalidEmailVerificationTokenError) HttpStatusCode() int {
|
||||
return http.StatusBadRequest
|
||||
}
|
||||
func (e InvalidEmailVerificationTokenError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
|
||||
// OIDC prompt parameter errors - used for redirect error responses
|
||||
|
||||
type OidcLoginRequiredError struct{}
|
||||
|
||||
func (e *OidcLoginRequiredError) Error() string { return "login_required" }
|
||||
func (e *OidcLoginRequiredError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
func (e OidcLoginRequiredError) Error() string { return "login_required" }
|
||||
func (e OidcLoginRequiredError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
|
||||
type OidcConsentRequiredError struct{}
|
||||
|
||||
func (e *OidcConsentRequiredError) Error() string { return "consent_required" }
|
||||
func (e *OidcConsentRequiredError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
func (e OidcConsentRequiredError) Error() string { return "consent_required" }
|
||||
func (e OidcConsentRequiredError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
|
||||
type OidcInteractionRequiredError struct{}
|
||||
|
||||
func (e *OidcInteractionRequiredError) Error() string { return "interaction_required" }
|
||||
func (e *OidcInteractionRequiredError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
func (e OidcInteractionRequiredError) Error() string { return "interaction_required" }
|
||||
func (e OidcInteractionRequiredError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
|
||||
type OidcInvalidRequestError struct{ description string }
|
||||
|
||||
func NewOidcInvalidRequestError(description string) *OidcInvalidRequestError {
|
||||
return &OidcInvalidRequestError{description: description}
|
||||
}
|
||||
|
||||
func (e OidcInvalidRequestError) Error() string { return "invalid_request" }
|
||||
func (e OidcInvalidRequestError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
func (e OidcInvalidRequestError) Description() string { return e.description }
|
||||
|
||||
type OidcAccountSelectionRequiredError struct{}
|
||||
|
||||
func (e *OidcAccountSelectionRequiredError) Error() string {
|
||||
return "account_selection_required"
|
||||
}
|
||||
func (e *OidcAccountSelectionRequiredError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
func (e OidcAccountSelectionRequiredError) Error() string { return "account_selection_required" }
|
||||
func (e OidcAccountSelectionRequiredError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||
|
||||
@@ -91,7 +91,7 @@ func (wkc *WellKnownController) computeOIDCConfiguration() ([]byte, error) {
|
||||
"id_token_signing_alg_values_supported": []string{alg.String()},
|
||||
"authorization_response_iss_parameter_supported": true,
|
||||
"code_challenge_methods_supported": []string{"plain", "S256"},
|
||||
"prompt_values_supported": []string{"none", "login", "consent"},
|
||||
"prompt_values_supported": []string{"none", "login", "consent", "select_account"},
|
||||
"token_endpoint_auth_methods_supported": []string{"client_secret_basic", "client_secret_post", "none"},
|
||||
}
|
||||
return json.Marshal(config)
|
||||
|
||||
@@ -23,7 +23,6 @@ func (m *ErrorHandlerMiddleware) Add() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
c.Next()
|
||||
for _, err := range c.Errors {
|
||||
|
||||
// Check for record not found errors
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
errorResponse(c, http.StatusNotFound, "Record not found")
|
||||
@@ -39,30 +38,56 @@ func (m *ErrorHandlerMiddleware) Add() gin.HandlerFunc {
|
||||
}
|
||||
|
||||
// Check for slice validation errors
|
||||
var sliceValidationErrors binding.SliceValidationError
|
||||
if errors.As(err, &sliceValidationErrors) {
|
||||
if errors.As(sliceValidationErrors[0], &validationErrors) {
|
||||
svErr, ok := errors.AsType[binding.SliceValidationError](err)
|
||||
if ok {
|
||||
if errors.As(svErr[0], &validationErrors) {
|
||||
message := handleValidationError(validationErrors)
|
||||
errorResponse(c, http.StatusBadRequest, message)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var appErr common.AppError
|
||||
if errors.As(err, &appErr) {
|
||||
// AppError with description
|
||||
appDescErr, ok := errors.AsType[common.AppErrorDescription](err)
|
||||
if ok {
|
||||
errorResponseWithDescription(c, appDescErr.HttpStatusCode(), appDescErr.Error(), appDescErr.Description())
|
||||
return
|
||||
}
|
||||
|
||||
// AppError (without description)
|
||||
appErr, ok := errors.AsType[common.AppError](err)
|
||||
if ok {
|
||||
errorResponse(c, appErr.HttpStatusCode(), appErr.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Something went wrong"})
|
||||
c.JSON(http.StatusInternalServerError, errorResponseBody{
|
||||
Error: "Something went wrong",
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type errorResponseBody struct {
|
||||
Error string `json:"error"`
|
||||
ErrorDescription string `json:"error_description,omitempty"`
|
||||
}
|
||||
|
||||
func errorResponse(c *gin.Context, statusCode int, message string) {
|
||||
// Capitalize the first letter of the message
|
||||
message = strings.ToUpper(message[:1]) + message[1:]
|
||||
c.JSON(statusCode, gin.H{"error": message})
|
||||
c.JSON(statusCode, errorResponseBody{
|
||||
Error: message,
|
||||
})
|
||||
}
|
||||
|
||||
func errorResponseWithDescription(c *gin.Context, statusCode int, message string, description string) {
|
||||
// Capitalize the first letter of the message
|
||||
message = strings.ToUpper(message[:1]) + message[1:]
|
||||
c.JSON(statusCode, errorResponseBody{
|
||||
Error: message,
|
||||
ErrorDescription: description,
|
||||
})
|
||||
}
|
||||
|
||||
func handleValidationError(validationErrors validator.ValidationErrors) string {
|
||||
|
||||
@@ -151,22 +151,20 @@ func (s *OidcService) Authorize(ctx context.Context, input dto.AuthorizeOidcClie
|
||||
|
||||
// Parse prompt parameter (space-delimited list per OIDC spec)
|
||||
promptValues := parsePromptParameter(input.Prompt)
|
||||
hasPromptNone := contains(promptValues, "none")
|
||||
hasPromptLogin := contains(promptValues, "login")
|
||||
hasPromptConsent := contains(promptValues, "consent")
|
||||
hasPromptSelectAccount := contains(promptValues, "select_account")
|
||||
hasPromptNone := slices.Contains(promptValues, "none")
|
||||
hasPromptLogin := slices.Contains(promptValues, "login")
|
||||
hasPromptConsent := slices.Contains(promptValues, "consent")
|
||||
hasPromptSelectAccount := slices.Contains(promptValues, "select_account")
|
||||
|
||||
// Validate prompt parameter conflicts early.
|
||||
// Per OIDC Core §3.1.2.6, prompt=none must not be combined with any
|
||||
// value that requires user interaction.
|
||||
if hasPromptNone && (hasPromptConsent || hasPromptLogin || hasPromptSelectAccount) {
|
||||
return "", "", &common.OidcInteractionRequiredError{}
|
||||
return "", "", common.NewOidcInvalidRequestError("prompt type 'none' cannot be combined with others")
|
||||
}
|
||||
|
||||
// Handle prompt=select_account early (not supported)
|
||||
if hasPromptSelectAccount {
|
||||
return "", "", &common.OidcInteractionRequiredError{}
|
||||
}
|
||||
// prompt=select_account is handled entirely in the UI
|
||||
// Pocket ID holds one session per browser, so the frontend renders the current user as the sole selectable account and then calls Authorize normally.
|
||||
|
||||
// If prompt=login is specified or the client requires reauthentication, check the reauthentication token
|
||||
if hasPromptLogin || client.RequiresReauthentication {
|
||||
@@ -219,14 +217,24 @@ func (s *OidcService) Authorize(ctx context.Context, input dto.AuthorizeOidcClie
|
||||
|
||||
// Log the authorization event
|
||||
if hasAlreadyAuthorizedClient {
|
||||
s.auditLogService.Create(ctx, model.AuditLogEventClientAuthorization, ipAddress, userAgent, userID, model.AuditLogData{"clientName": client.Name}, tx)
|
||||
s.auditLogService.Create(
|
||||
ctx, model.AuditLogEventClientAuthorization,
|
||||
ipAddress, userAgent, userID,
|
||||
model.AuditLogData{"clientName": client.Name},
|
||||
tx,
|
||||
)
|
||||
} else {
|
||||
s.auditLogService.Create(ctx, model.AuditLogEventNewClientAuthorization, ipAddress, userAgent, userID, model.AuditLogData{"clientName": client.Name}, tx)
|
||||
s.auditLogService.Create(
|
||||
ctx, model.AuditLogEventNewClientAuthorization,
|
||||
ipAddress, userAgent, userID,
|
||||
model.AuditLogData{"clientName": client.Name},
|
||||
tx,
|
||||
)
|
||||
}
|
||||
|
||||
err = tx.Commit().Error
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
return "", "", fmt.Errorf("failed to commit transaction: %w", err)
|
||||
}
|
||||
|
||||
return code, callbackURL, nil
|
||||
@@ -2219,13 +2227,3 @@ func parsePromptParameter(prompt string) []string {
|
||||
}
|
||||
return strings.Fields(prompt)
|
||||
}
|
||||
|
||||
// contains checks if a string slice contains a specific value
|
||||
func contains(slice []string, value string) bool {
|
||||
for _, item := range slice {
|
||||
if item == value {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -1238,30 +1239,13 @@ func TestPromptParameterConflicts(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
values := parsePromptParameter(tt.prompt)
|
||||
hasNone := contains(values, "none")
|
||||
hasConsent := contains(values, "consent")
|
||||
hasLogin := contains(values, "login")
|
||||
hasSelectAccount := contains(values, "select_account")
|
||||
hasNone := slices.Contains(values, "none")
|
||||
hasConsent := slices.Contains(values, "consent")
|
||||
hasLogin := slices.Contains(values, "login")
|
||||
hasSelectAccount := slices.Contains(values, "select_account")
|
||||
|
||||
conflict := hasNone && (hasConsent || hasLogin || hasSelectAccount)
|
||||
assert.Equal(t, tt.expectConflict, conflict)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestContains(t *testing.T) {
|
||||
t.Run("finds value in slice", func(t *testing.T) {
|
||||
slice := []string{"none", "login", "consent"}
|
||||
assert.True(t, contains(slice, "login"))
|
||||
})
|
||||
|
||||
t.Run("returns false for missing value", func(t *testing.T) {
|
||||
slice := []string{"none", "login"}
|
||||
assert.False(t, contains(slice, "consent"))
|
||||
})
|
||||
|
||||
t.Run("returns false for empty slice", func(t *testing.T) {
|
||||
slice := []string{}
|
||||
assert.False(t, contains(slice, "none"))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -53,6 +53,8 @@
|
||||
"webauthn_not_supported_by_browser": "Passkeys are not supported by this browser. Please use an alternative sign in method.",
|
||||
"critical_error_occurred_contact_administrator": "A critical error occurred. Please contact your administrator.",
|
||||
"sign_in_to": "Sign in to {name}",
|
||||
"account_selection_signin_confirmation": "Do you want to use the following account to continue to <b>{name}</b>?",
|
||||
"use_a_different_account": "Use a different account",
|
||||
"client_not_found": "Client not found",
|
||||
"client_wants_to_access_the_following_information": "<b>{client}</b> wants to access the following information:",
|
||||
"do_you_want_to_sign_in_to_client_with_your_app_name_account": "Do you want to sign in to <b>{client}</b> with your {appName} account?",
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
<div class=" w-full {isAuthPage ? 'absolute top-0 z-10 mt-3 lg:mt-8 pr-2 lg:pr-3' : 'border-b'}">
|
||||
<div
|
||||
class="{!isAuthPage
|
||||
? 'max-w-[1640px]'
|
||||
? 'max-w-410'
|
||||
: ''} mx-auto flex w-full items-center justify-between px-4 md:px-10"
|
||||
>
|
||||
<div class="flex h-16 items-center">
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { invalidateAll } from '$app/navigation';
|
||||
import FormattedMessage from '$lib/components/formatted-message.svelte';
|
||||
import SignInWrapper from '$lib/components/login-wrapper.svelte';
|
||||
import ScopeList from '$lib/components/scope-list.svelte';
|
||||
import * as Avatar from '$lib/components/ui/avatar';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
@@ -9,6 +11,7 @@
|
||||
import WebAuthnService from '$lib/services/webauthn-service';
|
||||
import appConfigStore from '$lib/stores/application-configuration-store';
|
||||
import userStore from '$lib/stores/user-store';
|
||||
import { cachedProfilePicture } from '$lib/utils/cached-image-util';
|
||||
import { getWebauthnErrorMessage } from '$lib/utils/error-util';
|
||||
import { startAuthentication, type AuthenticationResponseJSON } from '@simplewebauthn/browser';
|
||||
import { onMount } from 'svelte';
|
||||
@@ -37,8 +40,22 @@
|
||||
let errorMessage: string | null = $state(null);
|
||||
let authorizationRequired = $state(false);
|
||||
let authorizationConfirmed = $state(false);
|
||||
let accountSelectionRequired = $state(false);
|
||||
let userSignedInAt: Date | undefined;
|
||||
|
||||
const fullName = $derived.by(() => {
|
||||
if (!$userStore) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if ($userStore.displayName) {
|
||||
return $userStore.displayName;
|
||||
}
|
||||
|
||||
return [$userStore.firstName, $userStore.lastName].filter(Boolean).join(' ').trim();
|
||||
});
|
||||
const primaryName = $derived(fullName || $userStore?.email || '');
|
||||
|
||||
// Parse prompt parameter once (space-delimited per OIDC spec)
|
||||
const promptValues = prompt ? prompt.split(' ') : [];
|
||||
const hasPromptNone = promptValues.includes('none');
|
||||
@@ -59,11 +76,27 @@
|
||||
return;
|
||||
}
|
||||
|
||||
// prompt=select_account: if the user is already signed in, pause so they can
|
||||
// confirm the current account before proceeding. If they're not signed in,
|
||||
// the normal login flow below is selection enough.
|
||||
if (hasPromptSelectAccount && $userStore) {
|
||||
accountSelectionRequired = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if ($userStore) {
|
||||
authorize();
|
||||
}
|
||||
});
|
||||
|
||||
async function useDifferentAccount() {
|
||||
try {
|
||||
await webauthnService.logout();
|
||||
} finally {
|
||||
await invalidateAll();
|
||||
}
|
||||
}
|
||||
|
||||
async function authorize() {
|
||||
isLoading = true;
|
||||
|
||||
@@ -227,7 +260,39 @@
|
||||
{errorMessage}.
|
||||
</p>
|
||||
{/if}
|
||||
{#if !authorizationRequired && !errorMessage}
|
||||
{#if accountSelectionRequired && $userStore && !errorMessage}
|
||||
<div transition:slide={{ duration: 300 }} class="flex flex-col items-center">
|
||||
<p class="text-muted-foreground mt-2 mb-8">
|
||||
<FormattedMessage m={m.account_selection_signin_confirmation({ name: client.name })} />
|
||||
</p>
|
||||
<Card.Root class="mb-2 py-4 w-sm" data-testid="account-selection">
|
||||
<Card.Content class="flex items-center gap-4">
|
||||
<Avatar.Root class="size-11 shrink-0">
|
||||
<Avatar.Image src={cachedProfilePicture.getUrl($userStore.id)} />
|
||||
</Avatar.Root>
|
||||
<div class="flex min-w-0 flex-col text-start">
|
||||
<p class="truncate text-base leading-tight font-medium">
|
||||
{primaryName}
|
||||
</p>
|
||||
{#if fullName && $userStore.email}
|
||||
<p class="text-muted-foreground mt-1 truncate text-sm leading-tight">
|
||||
{$userStore.email}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
<div class="mb-10 flex justify-center">
|
||||
<button
|
||||
type="button"
|
||||
class="text-muted-foreground text-xs transition-colors hover:underline"
|
||||
onclick={useDifferentAccount}
|
||||
>
|
||||
{m.use_a_different_account()}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{:else if !authorizationRequired && !errorMessage}
|
||||
<p class="text-muted-foreground mt-2 mb-10">
|
||||
<FormattedMessage
|
||||
m={m.do_you_want_to_sign_in_to_client_with_your_app_name_account({
|
||||
|
||||
@@ -14,7 +14,10 @@ test('Authorize existing client', async ({ page }) => {
|
||||
|
||||
// Ignore DNS resolution error as the callback URL is not reachable
|
||||
await page.waitForURL(oidcClient.callbackUrl).catch((e) => {
|
||||
if (!e.message.includes('net::ERR_NAME_NOT_RESOLVED') && !e.message.includes('net::ERR_CERT_AUTHORITY_INVALID')) {
|
||||
if (
|
||||
!e.message.includes('net::ERR_NAME_NOT_RESOLVED') &&
|
||||
!e.message.includes('net::ERR_CERT_AUTHORITY_INVALID')
|
||||
) {
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
@@ -31,7 +34,10 @@ test('Authorize existing client while not signed in', async ({ page }) => {
|
||||
|
||||
// Ignore DNS resolution error as the callback URL is not reachable
|
||||
await page.waitForURL(oidcClient.callbackUrl).catch((e) => {
|
||||
if (!e.message.includes('net::ERR_NAME_NOT_RESOLVED') && !e.message.includes('net::ERR_CERT_AUTHORITY_INVALID')) {
|
||||
if (
|
||||
!e.message.includes('net::ERR_NAME_NOT_RESOLVED') &&
|
||||
!e.message.includes('net::ERR_CERT_AUTHORITY_INVALID')
|
||||
) {
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
@@ -49,7 +55,10 @@ test('Authorize new client', async ({ page }) => {
|
||||
|
||||
// Ignore DNS resolution error as the callback URL is not reachable
|
||||
await page.waitForURL(oidcClient.callbackUrl).catch((e) => {
|
||||
if (!e.message.includes('net::ERR_NAME_NOT_RESOLVED') && !e.message.includes('net::ERR_CERT_AUTHORITY_INVALID')) {
|
||||
if (
|
||||
!e.message.includes('net::ERR_NAME_NOT_RESOLVED') &&
|
||||
!e.message.includes('net::ERR_CERT_AUTHORITY_INVALID')
|
||||
) {
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
@@ -71,7 +80,10 @@ test('Authorize new client while not signed in', async ({ page }) => {
|
||||
|
||||
// Ignore DNS resolution error as the callback URL is not reachable
|
||||
await page.waitForURL(oidcClient.callbackUrl).catch((e) => {
|
||||
if (!e.message.includes('net::ERR_NAME_NOT_RESOLVED') && !e.message.includes('net::ERR_CERT_AUTHORITY_INVALID')) {
|
||||
if (
|
||||
!e.message.includes('net::ERR_NAME_NOT_RESOLVED') &&
|
||||
!e.message.includes('net::ERR_CERT_AUTHORITY_INVALID')
|
||||
) {
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
@@ -125,7 +137,10 @@ test('End session with id token hint redirects to callback URL', async ({ page }
|
||||
`/api/oidc/end-session?id_token_hint=${idToken}&post_logout_redirect_uri=${client.logoutCallbackUrl}`
|
||||
)
|
||||
.catch((e) => {
|
||||
if (e.message.includes('net::ERR_NAME_NOT_RESOLVED') || e.message.includes('net::ERR_CERT_AUTHORITY_INVALID')) {
|
||||
if (
|
||||
e.message.includes('net::ERR_NAME_NOT_RESOLVED') ||
|
||||
e.message.includes('net::ERR_CERT_AUTHORITY_INVALID')
|
||||
) {
|
||||
redirectedCorrectly = true;
|
||||
} else {
|
||||
throw e;
|
||||
@@ -617,7 +632,10 @@ test('Forces reauthentication when client requires it', async ({ page, request }
|
||||
await expect(page.getByTestId('scopes')).not.toBeVisible();
|
||||
|
||||
await page.waitForURL(oidcClients.nextcloud.callbackUrl).catch((e) => {
|
||||
if (!e.message.includes('net::ERR_NAME_NOT_RESOLVED') && !e.message.includes('net::ERR_CERT_AUTHORITY_INVALID')) {
|
||||
if (
|
||||
!e.message.includes('net::ERR_NAME_NOT_RESOLVED') &&
|
||||
!e.message.includes('net::ERR_CERT_AUTHORITY_INVALID')
|
||||
) {
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
@@ -678,10 +696,8 @@ test.describe('OIDC prompt parameter', () => {
|
||||
urlParams.set('prompt', 'none');
|
||||
|
||||
// Should redirect to callback URL with error
|
||||
const redirectUrl = await oidcUtil.interceptCallbackRedirect(
|
||||
page,
|
||||
'/auth/callback',
|
||||
() => page.goto(`/authorize?${urlParams.toString()}`).then(() => {})
|
||||
const redirectUrl = await oidcUtil.interceptCallbackRedirect(page, '/auth/callback', () =>
|
||||
page.goto(`/authorize?${urlParams.toString()}`).then(() => {})
|
||||
);
|
||||
|
||||
expect(redirectUrl.searchParams.get('error')).toBe('login_required');
|
||||
@@ -696,10 +712,8 @@ test.describe('OIDC prompt parameter', () => {
|
||||
urlParams.set('prompt', 'none');
|
||||
|
||||
// Should redirect to callback URL with error
|
||||
const redirectUrl = await oidcUtil.interceptCallbackRedirect(
|
||||
page,
|
||||
'/auth/callback',
|
||||
() => page.goto(`/authorize?${urlParams.toString()}`).then(() => {})
|
||||
const redirectUrl = await oidcUtil.interceptCallbackRedirect(page, '/auth/callback', () =>
|
||||
page.goto(`/authorize?${urlParams.toString()}`).then(() => {})
|
||||
);
|
||||
|
||||
expect(redirectUrl.searchParams.get('error')).toBe('consent_required');
|
||||
@@ -715,7 +729,10 @@ test.describe('OIDC prompt parameter', () => {
|
||||
|
||||
// Should redirect successfully to callback URL with code
|
||||
await page.waitForURL(oidcClient.callbackUrl).catch((e) => {
|
||||
if (!e.message.includes('net::ERR_NAME_NOT_RESOLVED') && !e.message.includes('net::ERR_CERT_AUTHORITY_INVALID')) {
|
||||
if (
|
||||
!e.message.includes('net::ERR_NAME_NOT_RESOLVED') &&
|
||||
!e.message.includes('net::ERR_CERT_AUTHORITY_INVALID')
|
||||
) {
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
@@ -729,14 +746,19 @@ test.describe('OIDC prompt parameter', () => {
|
||||
await page.goto(`/authorize?${urlParams.toString()}`);
|
||||
|
||||
// Should show consent UI even though client was already authorized
|
||||
await expect(page.getByTestId('scopes').getByRole('heading', { name: 'Profile' })).toBeVisible();
|
||||
await expect(
|
||||
page.getByTestId('scopes').getByRole('heading', { name: 'Profile' })
|
||||
).toBeVisible();
|
||||
await expect(page.getByTestId('scopes').getByRole('heading', { name: 'Email' })).toBeVisible();
|
||||
|
||||
await page.getByRole('button', { name: 'Sign in' }).click();
|
||||
|
||||
// Should redirect successfully after consent
|
||||
await page.waitForURL(oidcClient.callbackUrl).catch((e) => {
|
||||
if (!e.message.includes('net::ERR_NAME_NOT_RESOLVED') && !e.message.includes('net::ERR_CERT_AUTHORITY_INVALID')) {
|
||||
if (
|
||||
!e.message.includes('net::ERR_NAME_NOT_RESOLVED') &&
|
||||
!e.message.includes('net::ERR_CERT_AUTHORITY_INVALID')
|
||||
) {
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
@@ -758,7 +780,10 @@ test.describe('OIDC prompt parameter', () => {
|
||||
|
||||
// Should require reauthentication even though user is signed in
|
||||
await page.waitForURL(oidcClient.callbackUrl).catch((e) => {
|
||||
if (!e.message.includes('net::ERR_NAME_NOT_RESOLVED') && !e.message.includes('net::ERR_CERT_AUTHORITY_INVALID')) {
|
||||
if (
|
||||
!e.message.includes('net::ERR_NAME_NOT_RESOLVED') &&
|
||||
!e.message.includes('net::ERR_CERT_AUTHORITY_INVALID')
|
||||
) {
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
@@ -766,17 +791,56 @@ test.describe('OIDC prompt parameter', () => {
|
||||
expect(reauthCalled).toBe(true);
|
||||
});
|
||||
|
||||
test('prompt=select_account returns interaction_required error', async ({ page }) => {
|
||||
test('prompt=select_account shows current user and continues on confirm', async ({ page }) => {
|
||||
const oidcClient = oidcClients.nextcloud;
|
||||
const urlParams = createUrlParams(oidcClient);
|
||||
urlParams.set('prompt', 'select_account');
|
||||
|
||||
await page.goto(`/authorize?${urlParams.toString()}`);
|
||||
|
||||
// Should show error since account selection is not supported
|
||||
await expect(
|
||||
page.getByRole('paragraph').filter({ hasText: 'interaction_required' })
|
||||
).toBeVisible();
|
||||
// Account selection card with the signed-in user should appear
|
||||
const selectionCard = page.getByTestId('account-selection');
|
||||
await expect(selectionCard).toBeVisible();
|
||||
await expect(selectionCard).toContainText('Tim Cook');
|
||||
|
||||
await page.getByRole('button', { name: 'Sign In' }).click();
|
||||
|
||||
// Should redirect successfully to callback URL with code
|
||||
await page.waitForURL(oidcClient.callbackUrl).catch((e) => {
|
||||
if (
|
||||
!e.message.includes('net::ERR_NAME_NOT_RESOLVED') &&
|
||||
!e.message.includes('net::ERR_CERT_AUTHORITY_INVALID')
|
||||
) {
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test('prompt=select_account account can be changed', async ({ page }) => {
|
||||
const oidcClient = oidcClients.nextcloud;
|
||||
const urlParams = createUrlParams(oidcClient);
|
||||
urlParams.set('prompt', 'select_account');
|
||||
|
||||
await page.goto(`/authorize?${urlParams.toString()}`);
|
||||
|
||||
await page.getByRole('button', { name: 'Use a different account' }).click();
|
||||
await expect(page.getByText('Do you want to sign in to Nextcloud')).toBeVisible();
|
||||
|
||||
(await passkeyUtil.init(page)).addPasskey('craig');
|
||||
|
||||
await page.getByRole('button', { name: 'Sign In' }).click();
|
||||
|
||||
await page.getByRole('button', { name: 'Sign In' }).click();
|
||||
|
||||
// Should redirect successfully to callback URL with code
|
||||
await page.waitForURL(oidcClient.callbackUrl).catch((e) => {
|
||||
if (
|
||||
!e.message.includes('net::ERR_NAME_NOT_RESOLVED') &&
|
||||
!e.message.includes('net::ERR_CERT_AUTHORITY_INVALID')
|
||||
) {
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test('prompt=none with prompt=consent returns interaction_required', async ({ page }) => {
|
||||
@@ -785,10 +849,8 @@ test.describe('OIDC prompt parameter', () => {
|
||||
urlParams.set('prompt', 'none consent');
|
||||
|
||||
// Should redirect with error since both can't be satisfied
|
||||
const redirectUrl = await oidcUtil.interceptCallbackRedirect(
|
||||
page,
|
||||
'/auth/callback',
|
||||
() => page.goto(`/authorize?${urlParams.toString()}`).then(() => {})
|
||||
const redirectUrl = await oidcUtil.interceptCallbackRedirect(page, '/auth/callback', () =>
|
||||
page.goto(`/authorize?${urlParams.toString()}`).then(() => {})
|
||||
);
|
||||
|
||||
expect(redirectUrl.searchParams.get('error')).toBe('interaction_required');
|
||||
@@ -801,10 +863,8 @@ test.describe('OIDC prompt parameter', () => {
|
||||
urlParams.set('prompt', 'none login');
|
||||
|
||||
// Should redirect with error since both can't be satisfied
|
||||
const redirectUrl = await oidcUtil.interceptCallbackRedirect(
|
||||
page,
|
||||
'/auth/callback',
|
||||
() => page.goto(`/authorize?${urlParams.toString()}`).then(() => {})
|
||||
const redirectUrl = await oidcUtil.interceptCallbackRedirect(page, '/auth/callback', () =>
|
||||
page.goto(`/authorize?${urlParams.toString()}`).then(() => {})
|
||||
);
|
||||
|
||||
expect(redirectUrl.searchParams.get('error')).toBe('interaction_required');
|
||||
@@ -817,13 +877,11 @@ test.describe('OIDC prompt parameter', () => {
|
||||
urlParams.set('prompt', 'none select_account');
|
||||
|
||||
// Should redirect with error since both can't be satisfied
|
||||
const redirectUrl = await oidcUtil.interceptCallbackRedirect(
|
||||
page,
|
||||
'/auth/callback',
|
||||
() => page.goto(`/authorize?${urlParams.toString()}`).then(() => {})
|
||||
const redirectUrl = await oidcUtil.interceptCallbackRedirect(page, '/auth/callback', () =>
|
||||
page.goto(`/authorize?${urlParams.toString()}`).then(() => {})
|
||||
);
|
||||
|
||||
expect(redirectUrl.searchParams.get('error')).toBe('interaction_required');
|
||||
expect(redirectUrl.searchParams.get('state')).toBe('nXx-6Qr-owc1SHBa');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user