diff --git a/Package.swift b/Package.swift index 483e7cf..ee733c0 100644 --- a/Package.swift +++ b/Package.swift @@ -30,7 +30,7 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/apple/swift-crypto.git", "1.0.0"..<"5.0.0"), - .package(url: "https://github.com/soto-project/soto.git", from: "7.0.0"), + .package(url: "https://github.com/soto-project/soto.git", from: "7.1.0"), .package(url: "https://github.com/swift-server/async-http-client.git", from: "1.10.0"), .package(url: "https://github.com/vapor/jwt-kit.git", from: "5.0.0"), // for SRP @@ -54,7 +54,6 @@ let package = Package( .target(name: "SotoCognitoAuthenticationKit"), ] ), - .testTarget(name: "SotoCognitoAuthenticationKitTests", dependencies: ["SotoCognitoAuthenticationKit"]), - .testTarget(name: "SotoCognitoAuthenticationSRPTests", dependencies: ["SotoCognitoAuthenticationSRP"]), + .testTarget(name: "SotoCognitoAuthenticationKitTests", dependencies: ["SotoCognitoAuthenticationKit", "SotoCognitoAuthenticationSRP"]), ] ) diff --git a/Tests/SotoCognitoAuthenticationKitTests/CognitoTests.swift b/Tests/SotoCognitoAuthenticationKitTests/CognitoTests.swift index 800b145..ee006f6 100644 --- a/Tests/SotoCognitoAuthenticationKitTests/CognitoTests.swift +++ b/Tests/SotoCognitoAuthenticationKitTests/CognitoTests.swift @@ -2,7 +2,7 @@ // // This source file is part of the Soto for AWS open source project // -// Copyright (c) 2020-2021 the Soto project authors +// Copyright (c) 2020-2024 the Soto project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information @@ -17,24 +17,11 @@ import Crypto import Foundation import NIO @testable import SotoCognitoAuthenticationKit +@testable import SotoCognitoAuthenticationSRP import SotoCognitoIdentity import SotoCognitoIdentityProvider import SotoCore -import XCTest - -public func XCTRunAsyncAndBlock(_ closure: @Sendable @escaping () async throws -> Void) { - let dg = DispatchGroup() - dg.enter() - Task { - do { - try await closure() - } catch { - XCTFail("\(error)") - } - dg.leave() - } - dg.wait() -} +import Testing enum AWSCognitoTestError: Error { case unrecognisedChallenge @@ -49,121 +36,132 @@ struct AWSCognitoContextTest: CognitoContextData { } } -final class CognitoTests: XCTestCase { - var middleware: AWSMiddlewareProtocol? { - ProcessInfo.processInfo.environment["CI"] == "true" ? nil : AWSLoggingMiddleware() +struct EmptyMiddleware: AWSMiddlewareProtocol { + func handle(_ request: AWSHTTPRequest, context: AWSMiddlewareContext, next: AWSMiddlewareNextHandler) async throws -> AWSHTTPResponse { + return try await next(request, context) } +} - var awsClient: AWSClient! - var region: Region = .useast1 - var cognitoIdentity: CognitoIdentity! - var cognitoIDP: CognitoIdentityProvider! - let userPoolName: String = "aws-cognito-authentication-tests" - let userPoolClientName: String = UUID().uuidString - var authenticatable: CognitoAuthenticatable! - var userPoolId: String! - var clientId: String! - var clientSecret: String! - let identityPoolName: String = UUID().uuidString - var identityPoolId: String! - var identifiable: CognitoIdentifiable! - - var setUpFailure: String? - - override func setUp() async throws { - if ProcessInfo.processInfo.environment["CI"] == "true" { - self.awsClient = AWSClient() - } else { - self.awsClient = AWSClient(middleware: AWSLoggingMiddleware()) - } +final class CognitoTests { + let region: Region = .useast1 - self.cognitoIDP = CognitoIdentityProvider(client: self.awsClient, region: self.region) - self.cognitoIdentity = CognitoIdentity(client: self.awsClient, region: self.region) + func withAWSClient( + credentialProvider: CredentialProviderFactory = .default, + middleware: some AWSMiddlewareProtocol = EmptyMiddleware(), + process: (AWSClient) async throws -> Value + ) async throws -> Value { + let awsClient = AWSClient(credentialProvider: credentialProvider, middleware: middleware) + let value: Value do { - try await self.setupUserpool() + value = try await process(awsClient) + } catch { + try? await awsClient.shutdown() + throw error + } + try await awsClient.shutdown() + return value + } + + func withUserPool( + awsClient: AWSClient, + explicitAuthFlows: [CognitoIdentityProvider.ExplicitAuthFlowsType] = [.allowAdminUserPasswordAuth, .allowUserPasswordAuth, .allowRefreshTokenAuth], + process: (CognitoAuthenticatable) async throws -> Value + ) async throws -> Value { + let cognitoIDP = CognitoIdentityProvider(client: awsClient, region: self.region) + let ids = try await self.setupUserpool(cognitoIDP: cognitoIDP, explicitAuthFlows: explicitAuthFlows) + let value: Value + do { let configuration = CognitoConfiguration( - userPoolId: userPoolId, - clientId: clientId, - clientSecret: clientSecret, - cognitoIDP: self.cognitoIDP, + userPoolId: ids.userPoolId, + clientId: ids.clientId, + clientSecret: ids.clientSecret, + cognitoIDP: cognitoIDP, adminClient: true ) - self.authenticatable = CognitoAuthenticatable(configuration: configuration) + let authenticatable = CognitoAuthenticatable(configuration: configuration) + value = try await process(authenticatable) + } catch { + try await cognitoIDP.deleteUserPoolClient(clientId: ids.clientId, userPoolId: ids.userPoolId) + try await cognitoIDP.deleteUserPool(userPoolId: ids.userPoolId) + throw error + } + try await cognitoIDP.deleteUserPoolClient(clientId: ids.clientId, userPoolId: ids.userPoolId) + try await cognitoIDP.deleteUserPool(userPoolId: ids.userPoolId) + return value + } - try await self.setupIdentityPool() + func withIdentityPool( + authenticatable: CognitoAuthenticatable, + awsClient: AWSClient? = nil, + process: (CognitoIdentifiable) async throws -> Value + ) async throws -> Value { + let awsClient = awsClient ?? authenticatable.configuration.cognitoIDP.client + let cognitoIdentity = CognitoIdentity(client: awsClient, region: self.region) + let identityPoolId = try await self.setupIdentityPool( + cognitoIdentity: cognitoIdentity, + userPoolId: authenticatable.configuration.userPoolId, + clientId: authenticatable.configuration.clientId + ) + let value: Value + do { let identityConfiguration = CognitoIdentityConfiguration( - identityPoolId: self.identityPoolId, - userPoolId: self.userPoolId, + identityPoolId: identityPoolId, + userPoolId: authenticatable.configuration.userPoolId, region: self.region, - cognitoIdentity: self.cognitoIdentity + cognitoIdentity: cognitoIdentity ) - self.identifiable = CognitoIdentifiable(configuration: identityConfiguration) - } catch let error as AWSErrorType { - setUpFailure = error.description + let identifiable = CognitoIdentifiable(configuration: identityConfiguration) + value = try await process(identifiable) } catch { - self.setUpFailure = error.localizedDescription + try await cognitoIdentity.deleteIdentityPool(identityPoolId: identityPoolId) + throw error } + try await cognitoIdentity.deleteIdentityPool(identityPoolId: identityPoolId) + return value } - override func tearDown() async throws { - // delete client so we need to re-generate - let deleteClientRequest = CognitoIdentityProvider.DeleteUserPoolClientRequest(clientId: self.clientId, userPoolId: self.userPoolId) - try await self.cognitoIDP.deleteUserPoolClient(deleteClientRequest) - let deleteIdentityPool = CognitoIdentity.DeleteIdentityPoolInput(identityPoolId: self.identityPoolId) - try await self.cognitoIdentity.deleteIdentityPool(deleteIdentityPool) - try await self.awsClient.shutdown() - } - - func setupUserpool() async throws { + func setupUserpool( + cognitoIDP: CognitoIdentityProvider, + explicitAuthFlows: [CognitoIdentityProvider.ExplicitAuthFlowsType] + ) async throws -> (userPoolId: String, clientId: String, clientSecret: String) { // does userpool exist - let listRequest = CognitoIdentityProvider.ListUserPoolsRequest(maxResults: 60) - let userPools = try await cognitoIDP.listUserPools(listRequest).userPools - if let userPool = userPools?.first(where: { $0.name == userPoolName }) { - self.userPoolId = userPool.id! - } else { - // create userpool - let createRequest = CognitoIdentityProvider.CreateUserPoolRequest( - adminCreateUserConfig: CognitoIdentityProvider.AdminCreateUserConfigType(allowAdminCreateUserOnly: true), - poolName: self.userPoolName - ) - let createResponse = try await cognitoIDP.createUserPool(createRequest) - self.userPoolId = createResponse.userPool!.id! - } + let userPoolName = "aws-cognito-authentication-tests-\(UUID().uuidString)" + // create userpool + let createRequest = CognitoIdentityProvider.CreateUserPoolRequest( + adminCreateUserConfig: CognitoIdentityProvider.AdminCreateUserConfigType(allowAdminCreateUserOnly: true), + poolName: userPoolName + ) + let createResponse = try await cognitoIDP.createUserPool(createRequest) + let userPoolId = createResponse.userPool!.id! + let userPoolClientName = UUID().uuidString // does userpool client exist - let listClientRequest = CognitoIdentityProvider.ListUserPoolClientsRequest(maxResults: 60, userPoolId: self.userPoolId) - let clients = try await cognitoIDP.listUserPoolClients(listClientRequest).userPoolClients - if let client = clients?.first(where: { $0.clientName == userPoolClientName }) { - self.clientId = client.clientId! - let describeRequest = CognitoIdentityProvider.DescribeUserPoolClientRequest(clientId: self.clientId, userPoolId: self.userPoolId) - let describeResponse = try await cognitoIDP.describeUserPoolClient(describeRequest) - self.clientSecret = describeResponse.userPoolClient!.clientSecret - } else { - // create userpool client - let createClientRequest = CognitoIdentityProvider.CreateUserPoolClientRequest( - clientName: self.userPoolClientName, - explicitAuthFlows: [.allowAdminUserPasswordAuth, .allowUserPasswordAuth, .allowRefreshTokenAuth], - generateSecret: true, - userPoolId: self.userPoolId - ) - let createClientResponse = try await cognitoIDP.createUserPoolClient(createClientRequest) - self.clientId = createClientResponse.userPoolClient!.clientId! - self.clientSecret = createClientResponse.userPoolClient!.clientSecret - } + // create userpool client + let createClientRequest = CognitoIdentityProvider.CreateUserPoolClientRequest( + clientName: userPoolClientName, + explicitAuthFlows: explicitAuthFlows, + generateSecret: true, + userPoolId: userPoolId + ) + let createClientResponse = try await cognitoIDP.createUserPoolClient(createClientRequest) + let clientId = createClientResponse.userPoolClient!.clientId! + let clientSecret = createClientResponse.userPoolClient!.clientSecret! + return (userPoolId: userPoolId, clientId: clientId, clientSecret: clientSecret) } - func setupIdentityPool() async throws { + func setupIdentityPool(cognitoIdentity: CognitoIdentity, userPoolId: String, clientId: String) async throws -> String { // create identity pool - let providerName = "cognito-idp.\(self.region.rawValue).amazonaws.com/\(self.userPoolId!)" + let identityPoolName = UUID().uuidString + let providerName = "cognito-idp.\(self.region.rawValue).amazonaws.com/\(userPoolId)" let createRequest = CognitoIdentity.CreateIdentityPoolInput( allowUnauthenticatedIdentities: false, - cognitoIdentityProviders: [.init(clientId: self.clientId, providerName: providerName)], - identityPoolName: self.identityPoolName + cognitoIdentityProviders: [.init(clientId: clientId, providerName: providerName)], + identityPoolName: identityPoolName ) let createResponse = try await cognitoIdentity.createIdentityPool(createRequest) - self.identityPoolId = createResponse.identityPoolId + return createResponse.identityPoolId } func login(username: String, password: String, authenticatable: CognitoAuthenticatable) async throws -> CognitoAuthenticateResponse { @@ -191,31 +189,56 @@ final class CognitoTests: XCTestCase { /// create new user for test, run test and delete user func test( _ testName: String, + adminClient: Bool = true, attributes: [String: String] = [:], - _ work: @escaping (String, String) async throws -> Void + explicitAuthFlows: [CognitoIdentityProvider.ExplicitAuthFlowsType] = [.allowAdminUserPasswordAuth, .allowUserPasswordAuth, .allowRefreshTokenAuth], + _ process: @escaping (CognitoAuthenticatable, String, String) async throws -> Void ) async throws { - let username = testName + self.randomString() - let messageHmac: HashedAuthenticationCode = HMAC.authenticationCode( - for: Data(testName.utf8), - using: SymmetricKey(data: Data(self.authenticatable.configuration.userPoolId.utf8)) - ) - let password = String(messageHmac.flatMap { String(format: "%x", $0) }) + "1!A" + try await self.withAWSClient { client in + try await self.withUserPool(awsClient: client, explicitAuthFlows: explicitAuthFlows) { authenticatable in + let cognitoIDP = authenticatable.configuration.cognitoIDP + let username = testName + self.randomString() + let messageHmac: HashedAuthenticationCode = HMAC.authenticationCode( + for: Data(testName.utf8), + using: SymmetricKey(data: Data(authenticatable.configuration.userPoolId.utf8)) + ) + let password = String(messageHmac.flatMap { String(format: "%x", $0) }) + "1!A" + + do { + _ = try await authenticatable.createUser( + username: username, + attributes: attributes, + temporaryPassword: password, + messageAction: .suppress + ) + } catch let error as CognitoIdentityProviderErrorType where error == .usernameExistsException { + return + } - do { - _ = try await self.authenticatable.createUser( - username: username, - attributes: attributes, - temporaryPassword: password, - messageAction: .suppress - ) - } catch let error as CognitoIdentityProviderErrorType where error == .usernameExistsException { - return + do { + if adminClient { + try await process(authenticatable, username, password) + } else { + try await self.withAWSClient(credentialProvider: .empty) { awsClient in + let cognitoIdentityProvider = CognitoIdentityProvider(client: awsClient, region: self.region) + let configuration = CognitoConfiguration( + userPoolId: authenticatable.configuration.userPoolId, + clientId: authenticatable.configuration.clientId, + clientSecret: authenticatable.configuration.clientSecret, + cognitoIDP: cognitoIdentityProvider, + adminClient: false + ) + let authenticatable = CognitoAuthenticatable(configuration: configuration) + try await process(authenticatable, username, password) + } + } + } catch { + try? await cognitoIDP.adminDeleteUser(username: username, userPoolId: authenticatable.configuration.userPoolId) + throw error + } + try? await cognitoIDP.adminDeleteUser(username: username, userPoolId: authenticatable.configuration.userPoolId) + } } - - try await work(username, password) - - let deleteUserRequest = CognitoIdentityProvider.AdminDeleteUserRequest(username: username, userPoolId: self.authenticatable.configuration.userPoolId) - try? await self.cognitoIDP.adminDeleteUser(deleteUserRequest) } func randomString() -> String { @@ -224,20 +247,20 @@ final class CognitoTests: XCTestCase { // MARK: Tests - func testAccessToken() async throws { - XCTAssertNil(self.setUpFailure) - try await self.test(#function) { username, password in - let response = try await self.login(username: username, password: password, authenticatable: self.authenticatable) + @Test(arguments: [true, false]) + func testAccessToken(adminClient: Bool) async throws { + try await self.test(#function, adminClient: adminClient) { authenticatable, username, password in + let response = try await self.login(username: username, password: password, authenticatable: authenticatable) guard case .authenticated(let authenticated) = response else { throw AWSCognitoTestError.notAuthenticated } guard let accessToken = authenticated.accessToken else { throw AWSCognitoTestError.missingToken } - let result = try await self.authenticatable.authenticate(accessToken: accessToken) - XCTAssertEqual(result.username, username) + let result = try await authenticatable.authenticate(accessToken: accessToken) + #expect(result.username == username) } } - func testIdToken() async throws { - XCTAssertNil(self.setUpFailure) + @Test(arguments: [true, false]) + func testIdToken(adminClient: Bool) async throws { struct User: Codable { let email: String let givenName: String @@ -251,191 +274,194 @@ final class CognitoTests: XCTestCase { } let attributes = ["given_name": "John", "family_name": "Smith", "email": "johnsmith@email.com"] - try await self.test(#function, attributes: attributes) { username, password in - let response = try await self.login(username: username, password: password, authenticatable: self.authenticatable) + try await self.test(#function, adminClient: adminClient, attributes: attributes) { authenticatable, username, password in + let response = try await self.login(username: username, password: password, authenticatable: authenticatable) guard case .authenticated(let authenticated) = response else { throw AWSCognitoTestError.notAuthenticated } guard let idToken = authenticated.idToken else { throw AWSCognitoTestError.missingToken } - let result: User = try await self.authenticatable.authenticate(idToken: idToken) + let result: User = try await authenticatable.authenticate(idToken: idToken) - XCTAssertEqual(result.email, attributes["email"]) - XCTAssertEqual(result.givenName, attributes["given_name"]) - XCTAssertEqual(result.familyName, attributes["family_name"]) + #expect(result.email == attributes["email"]) + #expect(result.givenName == attributes["given_name"]) + #expect(result.familyName == attributes["family_name"]) } } - func testRefreshToken() async throws { - XCTAssertNil(self.setUpFailure) - try await self.test(#function) { username, password in - let response = try await self.login(username: username, password: password, authenticatable: self.authenticatable) + @Test(arguments: [true, false]) + func testRefreshToken(adminClient: Bool) async throws { + try await self.test(#function, adminClient: adminClient) { authenticatable, username, password in + let response = try await self.login(username: username, password: password, authenticatable: authenticatable) guard case .authenticated(let authenticated) = response else { throw AWSCognitoTestError.notAuthenticated } guard let refreshToken = authenticated.refreshToken else { throw AWSCognitoTestError.missingToken } - let response2 = try await self.authenticatable.refresh(username: username, refreshToken: refreshToken) + let response2 = try await authenticatable.refresh(username: username, refreshToken: refreshToken) guard case .authenticated(let authenticated) = response2 else { throw AWSCognitoTestError.notAuthenticated } guard let accessToken = authenticated.accessToken else { throw AWSCognitoTestError.missingToken } - _ = try await self.authenticatable.authenticate(accessToken: accessToken) + _ = try await authenticatable.authenticate(accessToken: accessToken) } } + @Test func testAdminUpdateUserAttributes() async throws { - XCTAssertNil(self.setUpFailure) struct User: Codable { let email: String } let attributes = ["email": "test@test.com"] let attributes2 = ["email": "test2@test2.com"] - try await self.test(#function, attributes: attributes) { username, password in - _ = try await self.authenticatable.updateUserAttributes(username: username, attributes: attributes2) - let response = try await self.login(username: username, password: password, authenticatable: self.authenticatable) + try await self.test(#function, attributes: attributes) { authenticatable, username, password in + try await authenticatable.updateUserAttributes(username: username, attributes: attributes2) + let response = try await self.login(username: username, password: password, authenticatable: authenticatable) guard case .authenticated(let authenticated) = response else { throw AWSCognitoTestError.notAuthenticated } guard let idToken = authenticated.idToken else { throw AWSCognitoTestError.missingToken } - let result: User = try await self.authenticatable.authenticate(idToken: idToken) - XCTAssertEqual(result.email, attributes2["email"]) + let result: User = try await authenticatable.authenticate(idToken: idToken) + #expect(result.email == attributes2["email"]) } } + @Test func testNonAdminUpdateUserAttributes() async throws { - XCTAssertNil(self.setUpFailure) struct User: Codable { let email: String } - let attributes = ["email": "test@test.com"] let attributes2 = ["email": "test2@test2.com"] - try await self.test(#function, attributes: attributes) { username, password in - let response = try await self.login(username: username, password: password, authenticatable: self.authenticatable) + try await self.test(#function, adminClient: false, attributes: attributes) { authenticatable, username, password in + let response = try await self.login(username: username, password: password, authenticatable: authenticatable) guard case .authenticated(let authenticated) = response else { throw AWSCognitoTestError.notAuthenticated } guard let accessToken = authenticated.accessToken else { throw AWSCognitoTestError.missingToken } guard let idToken = authenticated.idToken else { throw AWSCognitoTestError.missingToken } - let user: User = try await self.authenticatable.authenticate(idToken: idToken) + let user: User = try await authenticatable.authenticate(idToken: idToken) - XCTAssertEqual(user.email, attributes["email"]) - _ = try await self.authenticatable.updateUserAttributes( + #expect(user.email == attributes["email"]) + _ = try await authenticatable.updateUserAttributes( accessToken: accessToken, attributes: attributes2 ) - let response2 = try await self.login(username: username, password: password, authenticatable: self.authenticatable) + let response2 = try await self.login(username: username, password: password, authenticatable: authenticatable) guard case .authenticated(let authenticated) = response2 else { throw AWSCognitoTestError.notAuthenticated } guard let idToken = authenticated.idToken else { throw AWSCognitoTestError.missingToken } - let user2: User = try await self.authenticatable.authenticate(idToken: idToken) - - XCTAssertEqual(user2.email, attributes2["email"]) - } - } - - func testUnauthenticatdClient() async throws { - XCTAssertNil(self.setUpFailure) - try await self.test(#function) { username, password in - let awsClient = AWSClient(credentialProvider: .empty, httpClient: self.awsClient.httpClient) - defer { XCTAssertNoThrow(try awsClient.syncShutdown()) } - let cognitoIdentityProvider = CognitoIdentityProvider(client: awsClient, region: self.cognitoIDP.region) - let configuration = CognitoConfiguration( - userPoolId: self.authenticatable.configuration.userPoolId, - clientId: self.authenticatable.configuration.clientId, - clientSecret: self.authenticatable.configuration.clientSecret, - cognitoIDP: cognitoIdentityProvider, - adminClient: false - ) - let authenticatable = CognitoAuthenticatable(configuration: configuration) - - let response = try await self.login(username: username, password: password, authenticatable: self.authenticatable) - guard case .authenticated(let authenticated) = response else { throw AWSCognitoTestError.notAuthenticated } - guard let accessToken = authenticated.accessToken else { throw AWSCognitoTestError.missingToken } + let user2: User = try await authenticatable.authenticate(idToken: idToken) - let result = try await authenticatable.authenticate(accessToken: accessToken) - XCTAssertEqual(result.username, username) + #expect(user2.email == attributes2["email"]) } } - func testRequireAuthenticatedClient() async throws { - XCTAssertNil(self.setUpFailure) - try await self.test(#function) { username, password in - let awsClient = AWSClient(credentialProvider: .empty, httpClient: self.awsClient.httpClient) - defer { XCTAssertNoThrow(try awsClient.syncShutdown()) } - let cognitoIdentityProvider = CognitoIdentityProvider(client: awsClient, region: self.cognitoIDP.region) - let configuration = CognitoConfiguration( - userPoolId: self.authenticatable.configuration.userPoolId, - clientId: self.authenticatable.configuration.clientId, - clientSecret: self.authenticatable.configuration.clientSecret, - cognitoIDP: cognitoIdentityProvider, - adminClient: true - ) - let authenticatable = CognitoAuthenticatable(configuration: configuration) + @Test + func testAdminClientRequiresCredentials() async throws { + try await self.test(#function) { authenticatable, username, password in + try await self.withAWSClient(credentialProvider: .empty) { awsClient in + let cognitoIdentityProvider = CognitoIdentityProvider(client: awsClient, region: self.region) + let configuration = CognitoConfiguration( + userPoolId: authenticatable.configuration.userPoolId, + clientId: authenticatable.configuration.clientId, + clientSecret: authenticatable.configuration.clientSecret, + cognitoIDP: cognitoIdentityProvider, + adminClient: true + ) + let authenticatable = CognitoAuthenticatable(configuration: configuration) - do { - _ = try await self.login(username: username, password: password, authenticatable: authenticatable) - XCTFail("Login should fail") - } catch SotoCognitoError.unauthorized {} + do { + _ = try await self.login(username: username, password: password, authenticatable: authenticatable) + Issue.record("Login should fail") + } catch SotoCognitoError.unauthorized {} + } } } + @Test func testAuthenticateFail() async throws { - XCTAssertNil(self.setUpFailure) - try await self.test(#function) { username, password in + try await self.test(#function) { authenticatable, username, password in do { - _ = try await self.authenticatable.authenticate( + _ = try await authenticatable.authenticate( username: username, password: password + "!" ) - XCTFail("Login should fail") + Issue.record("Login should fail") } catch SotoCognitoError.unauthorized {} } } + @Test + func testAuthenticateSRP() async throws { + try await self.test(#function, explicitAuthFlows: [.allowUserSrpAuth, .allowRefreshTokenAuth]) { authenticatable, username, password in + try await self.withAWSClient(credentialProvider: .empty) { awsClient in + let cognitoIDPUnauthenticated = CognitoIdentityProvider(client: awsClient, region: .useast1) + let configuration = CognitoConfiguration( + userPoolId: authenticatable.configuration.userPoolId, + clientId: authenticatable.configuration.clientId, + clientSecret: authenticatable.configuration.clientSecret, + cognitoIDP: cognitoIDPUnauthenticated, + adminClient: false + ) + let authenticatable = CognitoAuthenticatable(configuration: configuration) + let context = AWSCognitoContextTest() + _ = try await authenticatable.authenticateSRP(username: username, password: password, context: context) + } + } + } + + @Test func testIdentity() async throws { - XCTAssertNil(self.setUpFailure) - try await self.test(#function) { username, password in - let response = try await self.login(username: username, password: password, authenticatable: self.authenticatable) + try await self.test(#function) { authenticatable, username, password in + let response = try await self.login(username: username, password: password, authenticatable: authenticatable) guard case .authenticated(let authenticated) = response else { throw AWSCognitoTestError.notAuthenticated } guard let idToken = authenticated.idToken else { throw AWSCognitoTestError.missingToken } - let id = try await self.identifiable.getIdentityId(idToken: idToken) - do { - _ = try await self.identifiable.getCredentialForIdentity(identityId: id, idToken: idToken) - XCTFail("getCredentialForIdentity should fail") - } catch let error as CognitoIdentityErrorType where error == .invalidIdentityPoolConfigurationException { - // should get an invalid identity pool configuration error as the identity pool authentication provider - // is setup as cognito userpools, but we havent set up a role to return + try await self.withIdentityPool(authenticatable: authenticatable) { identifiable in + let id = try await identifiable.getIdentityId(idToken: idToken) + do { + _ = try await identifiable.getCredentialForIdentity(identityId: id, idToken: idToken) + #expect(Bool(false), "getCredentialForIdentity should fail") + } catch let error as CognitoIdentityErrorType where error == .invalidIdentityPoolConfigurationException { + // should get an invalid identity pool configuration error as the identity pool authentication provider + // is setup as cognito userpools, but we havent set up a role to return + } } } } - func testCredentialProvider() async throws { - XCTAssertNil(self.setUpFailure) - try await self.test(#function) { username, password in - let credentialProvider: CredentialProviderFactory = .cognitoUserPool( - userName: username, - authentication: .password(password), - userPoolId: self.userPoolId, - clientId: self.clientId, - clientSecret: self.clientSecret, - identityPoolId: self.identityPoolId, - region: self.region, - respondToChallenge: { challenge, _, error in - switch challenge { - case .newPasswordRequired: - if error == nil { - return ["NEW_PASSWORD": "NewPassword123"] - } else { - return ["NEW_PASSWORD": "NewPassword123!"] + @Test(arguments: [ + [CognitoIdentityProvider.ExplicitAuthFlowsType.allowAdminUserPasswordAuth, .allowUserPasswordAuth, .allowRefreshTokenAuth], + [.allowUserSrpAuth, .allowRefreshTokenAuth], + ]) + func testCredentialProvider(explicitAuthFlows: [CognitoIdentityProvider.ExplicitAuthFlowsType]) async throws { + try await self.test(#function, explicitAuthFlows: explicitAuthFlows) { authenticatable, username, password in + try await self.withIdentityPool(authenticatable: authenticatable) { identifiable in + let authenticationMethod = if explicitAuthFlows.first(where: { $0 == .allowUserSrpAuth }) != nil { + CognitoAuthenticationMethod.srp(password) + } else { + CognitoAuthenticationMethod.password(password) + } + let credentialProvider: CredentialProviderFactory = .cognitoUserPool( + userName: username, + authentication: authenticationMethod, + userPoolId: authenticatable.configuration.userPoolId, + clientId: authenticatable.configuration.clientId, + clientSecret: authenticatable.configuration.clientSecret, + identityPoolId: identifiable.configuration.identityPoolId, + region: self.region, + respondToChallenge: { challenge, _, error in + switch challenge { + case .newPasswordRequired: + if error == nil { + return ["NEW_PASSWORD": "NewPassword123"] + } else { + return ["NEW_PASSWORD": "NewPassword123!"] + } + default: + return nil } - default: - return nil } + ) + try await self.withAWSClient(credentialProvider: credentialProvider) { client in + do { + _ = try await client.credentialProvider.getCredential(logger: AWSClient.loggingDisabled) + } catch let error as CognitoIdentityErrorType where error == .invalidIdentityPoolConfigurationException {} } - ) - let client = AWSClient(credentialProvider: credentialProvider) - do { - _ = try await client.credentialProvider.getCredential(logger: AWSClient.loggingDisabled) - } catch let error as CognitoIdentityErrorType where error == .invalidIdentityPoolConfigurationException { - } catch { - XCTFail() } - try await client.shutdown() } } } diff --git a/Tests/SotoCognitoAuthenticationKitTests/SRPTests.swift b/Tests/SotoCognitoAuthenticationKitTests/SRPTests.swift new file mode 100644 index 0000000..13ffb79 --- /dev/null +++ b/Tests/SotoCognitoAuthenticationKitTests/SRPTests.swift @@ -0,0 +1,90 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Soto for AWS open source project +// +// Copyright (c) 2020-2024 the Soto project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Soto project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import BigNum +import Crypto +@testable import SotoCognitoAuthenticationSRP +@_spi(SotoInternal) import SotoSignerV4 +import Testing + +struct SRPTests { + /// create SRP for testing + func createTestSRP() -> SRP { + let a = BigNum(hex: + "37981750af33fdf93fc6dce831fe794aba312572e7c33472528" + + "54e5ce7c7f40343f5ad82f9ad3585c8cb1184c54c562f8317fc" + + "2924c6c7ade72f1e8d964f606e040c8f9f0b12e3fe6b6828202" + + "a5e40ca0103e880531921e56de3acc410331b8e5ddcd0dbf249" + + "bfa104f6b797719613aa50eabcdb40fd5457f64861dd71890eba") + return SRP(a: a) + } + + @Test + func testSRPAValue() { + let expectedA = BigNum(hex: + "f93b917abccc667f4fac29d1e4c111bcd37d2c37577e7f113ad85030ec6" + + "157c70dfee728ac4aee9a7631d85a68aec3ef72864b6e8a134f5c5eef89" + + "40b93bb1db1ada9c1de770db282d644eeb3c551d35ce8de4d2cf98d0d79" + + "9b6a7f1fe51568d11162ce0cded8246b630169dcfc2d5a43817d52f121b" + + "3d75ab1a43dc30b7cec02e42e332d5fd781023d9c1fd44f3d1129d21155" + + "0ce57c004aca95a367592705b517298f724e6314ffbac2425b2beb5095f" + + "23b75dd3dd232adda700080d7a22a87383d3746d39f6427b7daf2a00683" + + "038ff7dc099081b2bf43eb5e2e30465487dafb3cc875fdd9b475d46a0ac" + + "1d07cf928fd11e06c5999596160168fc31228f7f3329d4b873acbf1540a" + + "16418a3ee5a0a5070a3db558f5cf8cf15388ff0a6e4234bf1de3e5bade8" + + "e4aa607d633a94a06bee4386c7444e06fd584282b9d576be318f0f20305" + + "7e80996f79a2bb0a63ad4786d5cc12b1321bd6644e001cee194171f5b04" + + "fcd65f3f280b6dadabae0401a9ae557ad27939730ce146319aa7f08d1e33") + let srp = self.createTestSRP() + #expect(expectedA == srp.A) + } + + @Test + func testSRPKey() { + let B = BigNum(hex: + "a0812a0ee3fa8484a73addeb6a9afa145cff1eca2a6b86537a5d15132d" + + "5811dd088d16e7d581b2798229350e6e473503cebddf19cabd3f14fb34" + + "50a6858bafc972a29702d8772a22b000a160812a7fe29bcac2c36d43b9" + + "1c118224626c2f0782d70f79c82ac5183e0d7d8c7b23ad0bda1f4fba94" + + "1998bfc82e46415e49026bb33f8271cb9a56e69f518e90bc2f4c42c7bb" + + "27720e25a14dcfbb5176effb3069a2bc627f18ec07a3e4118f61402dda" + + "56a6da3f331d8c2cf78513d767b2bf040809e5a334c7bb98cb720ef565" + + "4100cfa57d21155fc7630654964370fd512b30febc6c61bfa3415c7266" + + "0c5dad3444881d272c3abd7ecec0e483493b1491391bef4348d1c27be7" + + "00e443301fc856a9d1b6ca36fdc46eec9f3c51f0ea566f5a85c87d395d" + + "3d9fc2a594945a860841d5b328f1910058b2bb822ac976d961736fac42" + + "e84b46074762de8b254f37260e3b1da88529dd1060ca52b2dc9de5d773" + + "72b1d74ea111de406aac964993133a6f172e8fae54eb885e6a3cd774f1" + + "ca6be98b6ddc35")! + let salt = BigNum(hex: "8dbcb21f18ae3216")!.bytes + let expectedKey = BigNum(hex: "b70fad71e9658b24b0ec678774ecca30")!.bytes + + let srp = self.createTestSRP() + let key = srp.getPasswordAuthenticationKey(username: "poolidtestuser", password: "testpassword", B: B, salt: salt) + + #expect(key == expectedKey) + } + + @Test + func testHKDF() { + let password = [UInt8]("password".utf8) + let salt = [UInt8]("salt".utf8) + let info = [UInt8]("HKDF key derivation".utf8) + + let sha1Result = SRP.HKDF(seed: password, info: info, salt: salt, count: Insecure.SHA1.Digest.byteCount) + #expect(sha1Result.hexDigest().uppercased() == "9912F20853DFF1AFA944E9B88CA63C410CBB1938") + let sha256Result = SRP.HKDF(seed: password, info: info, salt: salt, count: 16) + #expect(sha256Result.hexDigest().uppercased() == "398F838A6019FC27D99D90009A1FE0BF") + } +} diff --git a/Tests/SotoCognitoAuthenticationSRPTests/CognitoSRPTests.swift b/Tests/SotoCognitoAuthenticationSRPTests/CognitoSRPTests.swift deleted file mode 100644 index 665bc21..0000000 --- a/Tests/SotoCognitoAuthenticationSRPTests/CognitoSRPTests.swift +++ /dev/null @@ -1,321 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Soto for AWS open source project -// -// Copyright (c) 2020-2021 the Soto project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of Soto project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -import BigNum -import Crypto -import Foundation -import NIO -import SotoCognitoAuthenticationKit -@testable import SotoCognitoAuthenticationSRP -import SotoCognitoIdentity -import SotoCognitoIdentityProvider -import SotoCore -@_spi(SotoInternal) import SotoSignerV4 -import XCTest - -public func XCTRunAsyncAndBlock(_ closure: @Sendable @escaping () async throws -> Void) { - let dg = DispatchGroup() - dg.enter() - Task { - do { - try await closure() - } catch { - XCTFail("\(error)") - } - dg.leave() - } - dg.wait() -} - -func attempt(function: () throws -> Void) { - do { - try function() - } catch { - XCTFail("\(error)") - } -} - -enum AWSCognitoTestError: Error { - case unrecognisedChallenge - case notAuthenticated - case missingToken -} - -/// context object used for tests -public class AWSCognitoContextTest: CognitoContextData { - public var contextData: CognitoIdentityProvider.ContextDataType? { - return CognitoIdentityProvider.ContextDataType(httpHeaders: [], ipAddress: "127.0.0.1", serverName: "127.0.0.1", serverPath: "/") - } -} - -final class CognitoSRPTests: XCTestCase { - var awsClient: AWSClient! - var region: Region = .useast1 - var cognitoIDP: CognitoIdentityProvider! - var cognitoIdentity: CognitoIdentity! - let userPoolName: String = "aws-cognito-authentication-tests" - let userPoolClientName: String = UUID().uuidString - var authenticatable: CognitoAuthenticatable! - var userPoolId: String! - var clientId: String! - var clientSecret: String! - let identityPoolName: String = UUID().uuidString - var identityPoolId: String! - var identifiable: CognitoIdentifiable! - - var setUpFailure: String? - - override func setUp() async throws { - self.awsClient = AWSClient(middleware: AWSLoggingMiddleware()) - self.cognitoIDP = CognitoIdentityProvider(client: self.awsClient, region: .useast1) - self.cognitoIdentity = CognitoIdentity(client: self.awsClient, region: .useast1) - do { - try await self.setupUserPool() - - let configuration = CognitoConfiguration( - userPoolId: userPoolId, - clientId: clientId, - clientSecret: self.clientSecret, - cognitoIDP: self.cognitoIDP, - adminClient: true - ) - self.authenticatable = CognitoAuthenticatable(configuration: configuration) - - try await self.setupIdentityPool() - - let identityConfiguration = CognitoIdentityConfiguration( - identityPoolId: self.identityPoolId, - userPoolId: self.userPoolId, - region: self.region, - cognitoIdentity: self.cognitoIdentity - ) - self.identifiable = CognitoIdentifiable(configuration: identityConfiguration) - } catch { - self.setUpFailure = "\(error)" - } - } - - func setupUserPool() async throws { - // does userpool exist - let listRequest = CognitoIdentityProvider.ListUserPoolsRequest(maxResults: 60) - let userPools = try await cognitoIDP.listUserPools(listRequest).userPools - if let userPool = userPools?.first(where: { $0.name == userPoolName }) { - self.userPoolId = userPool.id! - } else { - // create userpool - let createRequest = CognitoIdentityProvider.CreateUserPoolRequest( - adminCreateUserConfig: CognitoIdentityProvider.AdminCreateUserConfigType(allowAdminCreateUserOnly: true), - poolName: self.userPoolName - ) - let createResponse = try await cognitoIDP.createUserPool(createRequest) - self.userPoolId = createResponse.userPool!.id! - } - - // does userpool client exist - let listClientRequest = CognitoIdentityProvider.ListUserPoolClientsRequest(maxResults: 60, userPoolId: self.userPoolId) - let clients = try await cognitoIDP.listUserPoolClients(listClientRequest).userPoolClients - if let client = clients?.first(where: { $0.clientName == userPoolClientName }) { - self.clientId = client.clientId! - let describeRequest = CognitoIdentityProvider.DescribeUserPoolClientRequest(clientId: self.clientId, userPoolId: self.userPoolId) - let describeResponse = try await cognitoIDP.describeUserPoolClient(describeRequest) - self.clientSecret = describeResponse.userPoolClient!.clientSecret - } else { - // create userpool client - let createClientRequest = CognitoIdentityProvider.CreateUserPoolClientRequest( - clientName: self.userPoolClientName, - explicitAuthFlows: [.allowUserSrpAuth, .allowRefreshTokenAuth], - generateSecret: true, - userPoolId: self.userPoolId - ) - let createClientResponse = try await cognitoIDP.createUserPoolClient(createClientRequest) - self.clientId = createClientResponse.userPoolClient!.clientId! - self.clientSecret = createClientResponse.userPoolClient!.clientSecret - } - } - - func setupIdentityPool() async throws { - // create identity pool - let providerName = "cognito-idp.\(self.region.rawValue).amazonaws.com/\(self.userPoolId!)" - let createRequest = CognitoIdentity.CreateIdentityPoolInput( - allowUnauthenticatedIdentities: false, - cognitoIdentityProviders: [.init(clientId: self.clientId, providerName: providerName)], - identityPoolName: self.identityPoolName - ) - let createResponse = try await cognitoIdentity.createIdentityPool(createRequest) - self.identityPoolId = createResponse.identityPoolId - } - - override func tearDown() async throws { - // delete client so we need to re-generate - let deleteClientRequest = CognitoIdentityProvider.DeleteUserPoolClientRequest(clientId: self.clientId, userPoolId: self.userPoolId) - try await self.cognitoIDP.deleteUserPoolClient(deleteClientRequest) - try await self.awsClient.shutdown() - } - - /// create new user for test, run test and delete user - func test( - _ testName: String, - attributes: [String: String] = [:], - _ work: @escaping (String, String) async throws -> Void - ) async throws { - let username = testName + self.randomString() - let messageHmac: HashedAuthenticationCode = HMAC.authenticationCode( - for: Data(testName.utf8), - using: SymmetricKey(data: Data(self.authenticatable.configuration.userPoolId.utf8)) - ) - let password = String(messageHmac.flatMap { String(format: "%x", $0) }) + "1!A" - - do { - _ = try await self.authenticatable.createUser( - username: username, - attributes: attributes, - temporaryPassword: password, - messageAction: .suppress - ) - } catch let error as CognitoIdentityProviderErrorType where error == .usernameExistsException { - return - } - - try await work(username, password) - - let deleteUserRequest = CognitoIdentityProvider.AdminDeleteUserRequest(username: username, userPoolId: self.authenticatable.configuration.userPoolId) - try? await self.cognitoIDP.adminDeleteUser(deleteUserRequest) - } - - func randomString() -> String { - return String((0...7).map { _ in "abcdefghijklmnopqrstuvwxyz".randomElement()! }) - } - - func testAuthenticateSRP() async throws { - XCTAssertNil(self.setUpFailure) - - let awsClient = AWSClient(credentialProvider: .empty, middleware: AWSLoggingMiddleware()) - defer { XCTAssertNoThrow(try awsClient.syncShutdown()) } - let cognitoIDPUnauthenticated = CognitoIdentityProvider(client: awsClient, region: .useast1) - let configuration = CognitoConfiguration( - userPoolId: self.authenticatable.configuration.userPoolId, - clientId: self.authenticatable.configuration.clientId, - clientSecret: self.authenticatable.configuration.clientSecret, - cognitoIDP: cognitoIDPUnauthenticated, - adminClient: false - ) - let authenticatable = CognitoAuthenticatable(configuration: configuration) - - try await test(#function) { username, password in - let context = AWSCognitoContextTest() - _ = try await authenticatable.authenticateSRP(username: username, password: password, context: context) - } - } - - /// create SRP for testing - func createTestSRP() -> SRP { - let a = BigNum(hex: - "37981750af33fdf93fc6dce831fe794aba312572e7c33472528" + - "54e5ce7c7f40343f5ad82f9ad3585c8cb1184c54c562f8317fc" + - "2924c6c7ade72f1e8d964f606e040c8f9f0b12e3fe6b6828202" + - "a5e40ca0103e880531921e56de3acc410331b8e5ddcd0dbf249" + - "bfa104f6b797719613aa50eabcdb40fd5457f64861dd71890eba") - return SRP(a: a) - } - - func testSRPAValue() { - let expectedA = BigNum(hex: - "f93b917abccc667f4fac29d1e4c111bcd37d2c37577e7f113ad85030ec6" + - "157c70dfee728ac4aee9a7631d85a68aec3ef72864b6e8a134f5c5eef89" + - "40b93bb1db1ada9c1de770db282d644eeb3c551d35ce8de4d2cf98d0d79" + - "9b6a7f1fe51568d11162ce0cded8246b630169dcfc2d5a43817d52f121b" + - "3d75ab1a43dc30b7cec02e42e332d5fd781023d9c1fd44f3d1129d21155" + - "0ce57c004aca95a367592705b517298f724e6314ffbac2425b2beb5095f" + - "23b75dd3dd232adda700080d7a22a87383d3746d39f6427b7daf2a00683" + - "038ff7dc099081b2bf43eb5e2e30465487dafb3cc875fdd9b475d46a0ac" + - "1d07cf928fd11e06c5999596160168fc31228f7f3329d4b873acbf1540a" + - "16418a3ee5a0a5070a3db558f5cf8cf15388ff0a6e4234bf1de3e5bade8" + - "e4aa607d633a94a06bee4386c7444e06fd584282b9d576be318f0f20305" + - "7e80996f79a2bb0a63ad4786d5cc12b1321bd6644e001cee194171f5b04" + - "fcd65f3f280b6dadabae0401a9ae557ad27939730ce146319aa7f08d1e33") - let srp = self.createTestSRP() - XCTAssertEqual(expectedA, srp.A) - } - - func testSRPKey() { - let B = BigNum(hex: - "a0812a0ee3fa8484a73addeb6a9afa145cff1eca2a6b86537a5d15132d" + - "5811dd088d16e7d581b2798229350e6e473503cebddf19cabd3f14fb34" + - "50a6858bafc972a29702d8772a22b000a160812a7fe29bcac2c36d43b9" + - "1c118224626c2f0782d70f79c82ac5183e0d7d8c7b23ad0bda1f4fba94" + - "1998bfc82e46415e49026bb33f8271cb9a56e69f518e90bc2f4c42c7bb" + - "27720e25a14dcfbb5176effb3069a2bc627f18ec07a3e4118f61402dda" + - "56a6da3f331d8c2cf78513d767b2bf040809e5a334c7bb98cb720ef565" + - "4100cfa57d21155fc7630654964370fd512b30febc6c61bfa3415c7266" + - "0c5dad3444881d272c3abd7ecec0e483493b1491391bef4348d1c27be7" + - "00e443301fc856a9d1b6ca36fdc46eec9f3c51f0ea566f5a85c87d395d" + - "3d9fc2a594945a860841d5b328f1910058b2bb822ac976d961736fac42" + - "e84b46074762de8b254f37260e3b1da88529dd1060ca52b2dc9de5d773" + - "72b1d74ea111de406aac964993133a6f172e8fae54eb885e6a3cd774f1" + - "ca6be98b6ddc35")! - let salt = BigNum(hex: "8dbcb21f18ae3216")!.bytes - let expectedKey = BigNum(hex: "b70fad71e9658b24b0ec678774ecca30")!.bytes - - let srp = self.createTestSRP() - let key = srp.getPasswordAuthenticationKey(username: "poolidtestuser", password: "testpassword", B: B, salt: salt) - - XCTAssertEqual(key, expectedKey) - } - - func testHKDF() { - let password = [UInt8]("password".utf8) - let salt = [UInt8]("salt".utf8) - let info = [UInt8]("HKDF key derivation".utf8) - - let sha1Result = SRP.HKDF(seed: password, info: info, salt: salt, count: Insecure.SHA1.Digest.byteCount) - XCTAssertEqual(sha1Result.hexDigest().uppercased(), "9912F20853DFF1AFA944E9B88CA63C410CBB1938") - let sha256Result = SRP.HKDF(seed: password, info: info, salt: salt, count: 16) - XCTAssertEqual(sha256Result.hexDigest().uppercased(), "398F838A6019FC27D99D90009A1FE0BF") - } - - func testCredentialProvider() async throws { - XCTAssertNil(self.setUpFailure) - try await self.test(#function) { username, password in - let credentialProvider: CredentialProviderFactory = .cognitoUserPool( - userName: username, - authentication: .srp(password), - userPoolId: self.userPoolId, - clientId: self.clientId, - clientSecret: self.clientSecret, - identityPoolId: self.identityPoolId, - region: self.region, - respondToChallenge: { challenge, _, error in - switch challenge { - case .newPasswordRequired: - if error == nil { - return ["NEW_PASSWORD": "NewPassword123"] - } else { - return ["NEW_PASSWORD": "NewPassword123!"] - } - default: - return nil - } - } - ) - let client = AWSClient(credentialProvider: credentialProvider) - do { - _ = try await client.credentialProvider.getCredential(logger: AWSClient.loggingDisabled) - } catch let error as CognitoIdentityErrorType where error == .invalidIdentityPoolConfigurationException { - } catch { - XCTFail("Error: \(error)") - } - try await client.shutdown() - } - } -}