mirror of
https://github.com/stack-auth/stack.git
synced 2026-06-21 21:09:49 +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>
285 lines
9.8 KiB
Swift
285 lines
9.8 KiB
Swift
import Testing
|
|
import Foundation
|
|
@testable import StackAuth
|
|
|
|
@Suite("Authentication Tests")
|
|
struct AuthenticationTests {
|
|
|
|
// MARK: - Sign Up Tests
|
|
|
|
@Test("Should sign up with valid credentials")
|
|
func signUpWithValidCredentials() async throws {
|
|
let app = TestConfig.createClientApp()
|
|
let email = TestConfig.uniqueEmail()
|
|
|
|
try await app.signUpWithCredential(email: email, password: TestConfig.testPassword)
|
|
|
|
let user = try await app.getUser()
|
|
#expect(user != nil)
|
|
|
|
let primaryEmail = await user?.primaryEmail
|
|
#expect(primaryEmail == email)
|
|
}
|
|
|
|
@Test("Should fail sign up with duplicate email")
|
|
func signUpWithDuplicateEmail() async throws {
|
|
let app = TestConfig.createClientApp()
|
|
let email = TestConfig.uniqueEmail()
|
|
|
|
// First sign up
|
|
try await app.signUpWithCredential(email: email, password: TestConfig.testPassword)
|
|
try await app.signOut()
|
|
|
|
// Second sign up with same email should fail
|
|
do {
|
|
try await app.signUpWithCredential(email: email, password: TestConfig.testPassword)
|
|
Issue.record("Expected UserWithEmailAlreadyExistsError")
|
|
} catch is UserWithEmailAlreadyExistsError {
|
|
// Expected
|
|
} catch let error as StackAuthErrorProtocol where error.code == "USER_EMAIL_ALREADY_EXISTS" {
|
|
// Also acceptable
|
|
}
|
|
}
|
|
|
|
@Test("Should fail sign up with weak password")
|
|
func signUpWithWeakPassword() async throws {
|
|
let app = TestConfig.createClientApp()
|
|
let email = TestConfig.uniqueEmail()
|
|
|
|
do {
|
|
try await app.signUpWithCredential(email: email, password: TestConfig.weakPassword)
|
|
Issue.record("Expected password error")
|
|
} catch is PasswordRequirementsNotMetError {
|
|
// Expected
|
|
} catch let error as StackAuthErrorProtocol where error.code == "PASSWORD_REQUIREMENTS_NOT_MET" || error.code == "PASSWORD_TOO_SHORT" {
|
|
// Also acceptable - different error codes for password issues
|
|
}
|
|
}
|
|
|
|
@Test("Should fail sign up with invalid email format")
|
|
func signUpWithInvalidEmail() async throws {
|
|
let app = TestConfig.createClientApp()
|
|
|
|
do {
|
|
try await app.signUpWithCredential(email: "not-an-email", password: TestConfig.testPassword)
|
|
Issue.record("Expected error for invalid email")
|
|
} catch {
|
|
// Expected - any error is acceptable for invalid email
|
|
}
|
|
}
|
|
|
|
// MARK: - Sign In Tests
|
|
|
|
@Test("Should sign in with valid credentials")
|
|
func signInWithValidCredentials() async throws {
|
|
let app = TestConfig.createClientApp()
|
|
let email = TestConfig.uniqueEmail()
|
|
|
|
// First sign up
|
|
try await app.signUpWithCredential(email: email, password: TestConfig.testPassword)
|
|
try await app.signOut()
|
|
|
|
// Then sign in
|
|
try await app.signInWithCredential(email: email, password: TestConfig.testPassword)
|
|
|
|
let user = try await app.getUser()
|
|
#expect(user != nil)
|
|
|
|
let userEmail = await user?.primaryEmail
|
|
#expect(userEmail == email)
|
|
}
|
|
|
|
@Test("Should fail sign in with wrong password")
|
|
func signInWithWrongPassword() async throws {
|
|
let app = TestConfig.createClientApp()
|
|
let email = TestConfig.uniqueEmail()
|
|
|
|
// First sign up
|
|
try await app.signUpWithCredential(email: email, password: TestConfig.testPassword)
|
|
try await app.signOut()
|
|
|
|
// Try sign in with wrong password
|
|
do {
|
|
try await app.signInWithCredential(email: email, password: "WrongPassword123!")
|
|
Issue.record("Expected EmailPasswordMismatchError")
|
|
} catch is EmailPasswordMismatchError {
|
|
// Expected
|
|
}
|
|
}
|
|
|
|
@Test("Should fail sign in with non-existent user")
|
|
func signInWithNonExistentUser() async throws {
|
|
let app = TestConfig.createClientApp()
|
|
|
|
do {
|
|
try await app.signInWithCredential(email: "nonexistent-\(UUID().uuidString)@example.com", password: TestConfig.testPassword)
|
|
Issue.record("Expected EmailPasswordMismatchError")
|
|
} catch is EmailPasswordMismatchError {
|
|
// Expected - returns same error as wrong password for security
|
|
}
|
|
}
|
|
|
|
@Test("Should fail sign in with empty password")
|
|
func signInWithEmptyPassword() async throws {
|
|
let app = TestConfig.createClientApp()
|
|
let email = TestConfig.uniqueEmail()
|
|
|
|
try await app.signUpWithCredential(email: email, password: TestConfig.testPassword)
|
|
try await app.signOut()
|
|
|
|
do {
|
|
try await app.signInWithCredential(email: email, password: "")
|
|
Issue.record("Expected error for empty password")
|
|
} catch {
|
|
// Expected - any error is acceptable for empty password
|
|
}
|
|
}
|
|
|
|
// MARK: - Sign Out Tests
|
|
|
|
@Test("Should sign out successfully")
|
|
func signOutSuccessfully() async throws {
|
|
let app = TestConfig.createClientApp()
|
|
let email = TestConfig.uniqueEmail()
|
|
|
|
try await app.signUpWithCredential(email: email, password: TestConfig.testPassword)
|
|
|
|
let userBefore = try await app.getUser()
|
|
#expect(userBefore != nil)
|
|
|
|
try await app.signOut()
|
|
|
|
let userAfter = try await app.getUser()
|
|
#expect(userAfter == nil)
|
|
}
|
|
|
|
@Test("Should be able to sign out when not signed in")
|
|
func signOutWhenNotSignedIn() async throws {
|
|
let app = TestConfig.createClientApp()
|
|
|
|
// Should not throw even when not signed in
|
|
try await app.signOut()
|
|
|
|
let user = try await app.getUser()
|
|
#expect(user == nil)
|
|
}
|
|
|
|
@Test("Should clear tokens after sign out")
|
|
func clearTokensAfterSignOut() async throws {
|
|
let app = TestConfig.createClientApp()
|
|
let email = TestConfig.uniqueEmail()
|
|
|
|
try await app.signUpWithCredential(email: email, password: TestConfig.testPassword)
|
|
|
|
let tokenBefore = await app.getAccessToken()
|
|
#expect(tokenBefore != nil)
|
|
|
|
try await app.signOut()
|
|
|
|
let tokenAfter = await app.getAccessToken()
|
|
#expect(tokenAfter == nil)
|
|
}
|
|
|
|
// MARK: - Multiple Auth Cycles
|
|
|
|
@Test("Should handle multiple sign in/out cycles")
|
|
func multipleAuthCycles() async throws {
|
|
let app = TestConfig.createClientApp()
|
|
let email = TestConfig.uniqueEmail()
|
|
|
|
// Sign up
|
|
try await app.signUpWithCredential(email: email, password: TestConfig.testPassword)
|
|
var user = try await app.getUser()
|
|
#expect(user != nil)
|
|
|
|
// Sign out and in again (3 cycles)
|
|
for _ in 1...3 {
|
|
try await app.signOut()
|
|
user = try await app.getUser()
|
|
#expect(user == nil)
|
|
|
|
try await app.signInWithCredential(email: email, password: TestConfig.testPassword)
|
|
user = try await app.getUser()
|
|
#expect(user != nil)
|
|
}
|
|
}
|
|
|
|
// MARK: - Password Management
|
|
|
|
@Test("Should update password for authenticated user")
|
|
func updatePassword() async throws {
|
|
let app = TestConfig.createClientApp()
|
|
let email = TestConfig.uniqueEmail()
|
|
let newPassword = "NewPassword456!"
|
|
|
|
try await app.signUpWithCredential(email: email, password: TestConfig.testPassword)
|
|
|
|
let user = try await app.getUser()
|
|
#expect(user != nil)
|
|
|
|
try await user?.updatePassword(
|
|
oldPassword: TestConfig.testPassword,
|
|
newPassword: newPassword
|
|
)
|
|
|
|
// Sign out and sign in with new password
|
|
try await app.signOut()
|
|
try await app.signInWithCredential(email: email, password: newPassword)
|
|
|
|
let userAfter = try await app.getUser()
|
|
#expect(userAfter != nil)
|
|
}
|
|
|
|
@Test("Should fail password update with wrong old password")
|
|
func updatePasswordWithWrongOldPassword() async throws {
|
|
let app = TestConfig.createClientApp()
|
|
let email = TestConfig.uniqueEmail()
|
|
|
|
try await app.signUpWithCredential(email: email, password: TestConfig.testPassword)
|
|
|
|
let user = try await app.getUser()
|
|
#expect(user != nil)
|
|
|
|
do {
|
|
try await user?.updatePassword(
|
|
oldPassword: "WrongOldPassword!",
|
|
newPassword: "NewPassword456!"
|
|
)
|
|
Issue.record("Expected PasswordConfirmationMismatchError")
|
|
} catch is PasswordConfirmationMismatchError {
|
|
// Expected
|
|
} catch let error as StackAuthErrorProtocol where error.code == "PASSWORD_CONFIRMATION_MISMATCH" {
|
|
// Also acceptable
|
|
}
|
|
}
|
|
|
|
// MARK: - Unauthenticated User Tests
|
|
|
|
@Test("Should return nil for unauthenticated user")
|
|
func unauthenticatedUserReturnsNil() async throws {
|
|
let app = TestConfig.createClientApp()
|
|
|
|
let user = try await app.getUser()
|
|
|
|
#expect(user == nil)
|
|
}
|
|
|
|
@Test("Should throw for unauthenticated user with or: throw")
|
|
func unauthenticatedUserThrows() async throws {
|
|
let app = TestConfig.createClientApp()
|
|
|
|
await #expect(throws: UserNotSignedInError.self) {
|
|
_ = try await app.getUser(or: .throw)
|
|
}
|
|
}
|
|
|
|
@Test("Should return nil for partial user when unauthenticated")
|
|
func unauthenticatedPartialUserReturnsNil() async throws {
|
|
let app = TestConfig.createClientApp()
|
|
|
|
let partialUser = await app.getPartialUser()
|
|
|
|
#expect(partialUser == nil)
|
|
}
|
|
}
|