mirror of
https://github.com/stack-auth/stack.git
synced 2026-06-13 21:01:21 +08:00
### Summary of Changes Previously, on the Swift SDK, the `signInWithOAuth` function wasn't working. In this PR, we fix it by having the `getOAuthUrl` function to actually redirect correctly. Note that to do so, we updated the `validRedirectUrl` check on the backend to accept app native redirects (from our new trusted url scheme). Another thing to note is that we added functionality to the `TokenStore` abstraction to conditionally refresh the access token that the user is trying to fetch if it is expired/close to expiring if possible. `getOAuthUrl` will attempt to get a valid access token, and thus will rely on our algorithm documented in `utilities.md`. The specs serve as the source of truth. We go further and implement Apple Native sign in. To do so, we have it hit a new route on the backend and verify the `jwtToken` retrieved by the sdk against an Apple-provided set of `jwks`. We use jose to do so, in line with the rest of the codebase. We take this opportunity to refactor the oauth provider route owing to the amount of duplicated logic. Additionally, to enable the apple sign in, users will have to update the Apple authentication method modal on the dashboard and add accepted bundle ids. These are identifiers for projects, and we will check the `JWT` on the backend to make sure the audience is set to an accepted bundleId. We also update the Apple modal to be more informative. ### Using the new Features To use the Apple native sign in, users will have to 1) sign up with an apple developer account, 2) set up their bundleids for their projects by connecting them to the apple developer account, 3) update the Stack-Auth Authentication Methods dashboard apple modal with the relevant fields. Then, trying to sign in with apple with our Swift SDK will use the apple native sign in. ### UI Changes Renamed the fields in the apple modal. Added a new field for bundle ids. See below. https://github.com/user-attachments/assets/0e760c0e-3198-4818-ac7f-4900d7a125bb Co-authored-by: Konstantin Wohlwend <n2d4xc@gmail.com>
179 lines
7.0 KiB
Swift
179 lines
7.0 KiB
Swift
import Foundation
|
|
|
|
/// Base protocol for all Stack Auth errors
|
|
public protocol StackAuthErrorProtocol: Error, CustomStringConvertible {
|
|
var code: String { get }
|
|
var message: String { get }
|
|
var details: [String: Any]? { get }
|
|
}
|
|
|
|
/// Standard Stack Auth API error
|
|
public struct StackAuthError: StackAuthErrorProtocol {
|
|
public let code: String
|
|
public let message: String
|
|
public let details: [String: Any]?
|
|
|
|
public var description: String {
|
|
"StackAuthError(\(code)): \(message)"
|
|
}
|
|
|
|
public init(code: String, message: String, details: [String: Any]? = nil) {
|
|
self.code = code
|
|
self.message = message
|
|
self.details = details
|
|
}
|
|
}
|
|
|
|
// MARK: - Specific Error Types
|
|
|
|
public struct EmailPasswordMismatchError: StackAuthErrorProtocol {
|
|
public let code = "EMAIL_PASSWORD_MISMATCH"
|
|
public let message = "The email and password combination is incorrect."
|
|
public let details: [String: Any]? = nil
|
|
public var description: String { "EmailPasswordMismatchError: \(message)" }
|
|
}
|
|
|
|
public struct UserWithEmailAlreadyExistsError: StackAuthErrorProtocol {
|
|
public let code = "USER_EMAIL_ALREADY_EXISTS"
|
|
public let message = "A user with this email address already exists."
|
|
public let details: [String: Any]? = nil
|
|
public var description: String { "UserWithEmailAlreadyExistsError: \(message)" }
|
|
}
|
|
|
|
public struct PasswordRequirementsNotMetError: StackAuthErrorProtocol {
|
|
public let code = "PASSWORD_REQUIREMENTS_NOT_MET"
|
|
public let message = "The password does not meet the project's requirements."
|
|
public let details: [String: Any]? = nil
|
|
public var description: String { "PasswordRequirementsNotMetError: \(message)" }
|
|
}
|
|
|
|
public struct UserNotFoundError: StackAuthErrorProtocol {
|
|
public let code = "USER_NOT_FOUND"
|
|
public let message = "No user with this email address was found."
|
|
public let details: [String: Any]? = nil
|
|
public var description: String { "UserNotFoundError: \(message)" }
|
|
}
|
|
|
|
public struct VerificationCodeError: StackAuthErrorProtocol {
|
|
public let code = "VERIFICATION_CODE_ERROR"
|
|
public let message = "The verification code is invalid or expired."
|
|
public let details: [String: Any]? = nil
|
|
public var description: String { "VerificationCodeError: \(message)" }
|
|
}
|
|
|
|
public struct InvalidTotpCodeError: StackAuthErrorProtocol {
|
|
public let code = "INVALID_TOTP_CODE"
|
|
public let message = "The MFA code is incorrect."
|
|
public let details: [String: Any]? = nil
|
|
public var description: String { "InvalidTotpCodeError: \(message)" }
|
|
}
|
|
|
|
public struct RedirectUrlNotWhitelistedError: StackAuthErrorProtocol {
|
|
public let code = "REDIRECT_URL_NOT_WHITELISTED"
|
|
public let message = "The callback URL is not in the project's trusted domains list."
|
|
public let details: [String: Any]? = nil
|
|
public var description: String { "RedirectUrlNotWhitelistedError: \(message)" }
|
|
}
|
|
|
|
public struct PasskeyAuthenticationFailedError: StackAuthErrorProtocol {
|
|
public let code = "PASSKEY_AUTHENTICATION_FAILED"
|
|
public let message = "Passkey authentication failed. Please try again."
|
|
public let details: [String: Any]? = nil
|
|
public var description: String { "PasskeyAuthenticationFailedError: \(message)" }
|
|
}
|
|
|
|
public struct PasskeyWebAuthnError: StackAuthErrorProtocol {
|
|
public let code = "PASSKEY_WEBAUTHN_ERROR"
|
|
public let message: String
|
|
public let details: [String: Any]? = nil
|
|
public var description: String { "PasskeyWebAuthnError: \(message)" }
|
|
|
|
public init(errorName: String) {
|
|
self.message = "WebAuthn error: \(errorName)."
|
|
}
|
|
}
|
|
|
|
public struct MultiFactorAuthenticationRequiredError: StackAuthErrorProtocol {
|
|
public let code = "MULTI_FACTOR_AUTHENTICATION_REQUIRED"
|
|
public let message = "Multi-factor authentication is required."
|
|
public let attemptCode: String
|
|
public var details: [String: Any]? { ["attempt_code": attemptCode] }
|
|
public var description: String { "MultiFactorAuthenticationRequiredError: \(message)" }
|
|
|
|
public init(attemptCode: String) {
|
|
self.attemptCode = attemptCode
|
|
}
|
|
}
|
|
|
|
public struct UserNotSignedInError: StackAuthErrorProtocol {
|
|
public let code = "USER_NOT_SIGNED_IN"
|
|
public let message = "User is not signed in."
|
|
public let details: [String: Any]? = nil
|
|
public var description: String { "UserNotSignedInError: \(message)" }
|
|
}
|
|
|
|
public struct OAuthError: StackAuthErrorProtocol {
|
|
public let code: String
|
|
public let message: String
|
|
public let details: [String: Any]?
|
|
public var description: String { "OAuthError(\(code)): \(message)" }
|
|
|
|
public init(code: String, message: String, details: [String: Any]? = nil) {
|
|
self.code = code
|
|
self.message = message
|
|
self.details = details
|
|
}
|
|
}
|
|
|
|
public struct PasswordConfirmationMismatchError: StackAuthErrorProtocol {
|
|
public let code = "PASSWORD_CONFIRMATION_MISMATCH"
|
|
public let message = "The current password is incorrect."
|
|
public let details: [String: Any]? = nil
|
|
public var description: String { "PasswordConfirmationMismatchError: \(message)" }
|
|
}
|
|
|
|
public struct OAuthProviderAccountIdAlreadyUsedError: StackAuthErrorProtocol {
|
|
public let code = "OAUTH_PROVIDER_ACCOUNT_ID_ALREADY_USED_FOR_SIGN_IN"
|
|
public let message = "This OAuth account is already linked to another user for sign-in."
|
|
public let details: [String: Any]? = nil
|
|
public var description: String { "OAuthProviderAccountIdAlreadyUsedError: \(message)" }
|
|
}
|
|
|
|
// MARK: - Error Parsing
|
|
|
|
extension StackAuthError {
|
|
/// Parse error from API response
|
|
/// Error codes from the API are UPPERCASE_WITH_UNDERSCORES
|
|
static func from(code: String, message: String, details: [String: Any]? = nil) -> any StackAuthErrorProtocol {
|
|
switch code {
|
|
case "EMAIL_PASSWORD_MISMATCH":
|
|
return EmailPasswordMismatchError()
|
|
case "USER_EMAIL_ALREADY_EXISTS":
|
|
return UserWithEmailAlreadyExistsError()
|
|
case "PASSWORD_REQUIREMENTS_NOT_MET":
|
|
return PasswordRequirementsNotMetError()
|
|
case "USER_NOT_FOUND":
|
|
return UserNotFoundError()
|
|
case "VERIFICATION_CODE_ERROR":
|
|
return VerificationCodeError()
|
|
case "INVALID_TOTP_CODE":
|
|
return InvalidTotpCodeError()
|
|
case "REDIRECT_URL_NOT_WHITELISTED":
|
|
return RedirectUrlNotWhitelistedError()
|
|
case "PASSKEY_AUTHENTICATION_FAILED":
|
|
return PasskeyAuthenticationFailedError()
|
|
case "MULTI_FACTOR_AUTHENTICATION_REQUIRED":
|
|
if let attemptCode = details?["attempt_code"] as? String {
|
|
return MultiFactorAuthenticationRequiredError(attemptCode: attemptCode)
|
|
}
|
|
return StackAuthError(code: code, message: message, details: details)
|
|
case "PASSWORD_CONFIRMATION_MISMATCH":
|
|
return PasswordConfirmationMismatchError()
|
|
case "OAUTH_PROVIDER_ACCOUNT_ID_ALREADY_USED_FOR_SIGN_IN":
|
|
return OAuthProviderAccountIdAlreadyUsedError()
|
|
default:
|
|
return StackAuthError(code: code, message: message, details: details)
|
|
}
|
|
}
|
|
}
|