From 4ab4c99d13355489394a48c36994ef733d8b9ae2 Mon Sep 17 00:00:00 2001 From: Harsh <6162866+harsh62@users.noreply.github.com> Date: Wed, 7 Feb 2024 20:21:26 -0500 Subject: [PATCH] feat(auth): Removed dependency on oauth2 token for refreshToken and move to initiateAuth for hostedUI (#3497) --- .../UserPool/RefreshHostedUITokens.swift | 139 -------- .../RefreshSessionState+Resolver.swift | 28 +- .../HostedUI/HostedUIRequestHelper.swift | 30 -- .../RefreshHostedUITokensTests.swift | 310 ------------------ .../Support/HostedUIRequestHelperTests.swift | 27 -- 5 files changed, 7 insertions(+), 527 deletions(-) delete mode 100644 AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Actions/RefreshAuthorizationSession/UserPool/RefreshHostedUITokens.swift delete mode 100644 AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/ActionTests/FetchAuthSession/FetchUserPoolTokens/RefreshHostedUITokensTests.swift diff --git a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Actions/RefreshAuthorizationSession/UserPool/RefreshHostedUITokens.swift b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Actions/RefreshAuthorizationSession/UserPool/RefreshHostedUITokens.swift deleted file mode 100644 index 367b9eb222..0000000000 --- a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Actions/RefreshAuthorizationSession/UserPool/RefreshHostedUITokens.swift +++ /dev/null @@ -1,139 +0,0 @@ -// -// Copyright Amazon.com Inc. or its affiliates. -// All Rights Reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// - -import Amplify -import Foundation - -struct RefreshHostedUITokens: Action { - - let identifier = "RefreshHostedUITokens" - - let existingSignedIndata: SignedInData - - func execute(withDispatcher dispatcher: EventDispatcher, environment: Environment) { - - Task { - await refresh( - withDispatcher: dispatcher, - environment: environment - ) - } - } - - func refresh(withDispatcher dispatcher: EventDispatcher, environment: Environment) async { - - do { - logVerbose("\(#fileID) Starting execution", environment: environment) - guard let environment = environment as? AuthEnvironment, - let hostedUIEnvironment = environment.hostedUIEnvironment else { - let event = RefreshSessionEvent.init(eventType: .throwError(.noUserPool)) - await dispatcher.send(event) - return - } - let configuration = environment.userPoolConfiguration - let existingTokens = existingSignedIndata.cognitoUserPoolTokens - - let request = try HostedUIRequestHelper.createRefreshTokenRequest( - refreshToken: existingTokens.refreshToken, - configuration: hostedUIEnvironment.configuration) - - let data = try await withCheckedThrowingContinuation { - (continuation: CheckedContinuation) in - let task = hostedUIEnvironment.urlSessionFactory().dataTask(with: request) { - data, _, error in - if let error = error { - continuation.resume(with: .failure(FetchSessionError.service(error))) - } else if let data = data { - continuation.resume(with: .success(data)) - } else { - continuation.resume(with: .failure(FetchSessionError.invalidTokens)) - } - } - task.resume() - } - let signedInData = try await handleData(data, - configuration: configuration) - let event: RefreshSessionEvent - - if (environment.identityPoolConfigData) != nil { - let provider = CognitoUserPoolLoginsMap( - idToken: signedInData.cognitoUserPoolTokens.idToken, - region: configuration.region, - poolId: configuration.poolId) - event = .init(eventType: .refreshIdentityInfo(signedInData, provider)) - } else { - event = .init(eventType: .refreshedCognitoUserPool(signedInData)) - } - logVerbose("\(#fileID) Sending event \(event.type)", environment: environment) - await dispatcher.send(event) - - } catch let fetchError as FetchSessionError { - let event = RefreshSessionEvent(eventType: .throwError(fetchError)) - logVerbose("\(#fileID) Sending event \(event.type)", environment: environment) - await dispatcher.send(event) - } catch { - let event = RefreshSessionEvent(eventType: .throwError(.service(error))) - logVerbose("\(#fileID) Sending event \(event.type)", environment: environment) - await dispatcher.send(event) - } - logVerbose("\(#fileID) Refresh hostedUI token complete", environment: environment) - } - - func handleData(_ data: Data, - configuration: UserPoolConfigurationData) async throws -> SignedInData { - guard let json = try JSONSerialization.jsonObject( - with: data, - options: []) as? [String: Any] else { - throw FetchSessionError.invalidTokens - } - - if let errorString = json["error"] as? String { - let description = json["error_description"] as? String ?? "" - let error = HostedUIError.serviceMessage("\(errorString) \(description)") - throw FetchSessionError.service(error) - - } else if let idToken = json["id_token"] as? String, - let accessToken = json["access_token"] as? String { - let userPoolTokens = AWSCognitoUserPoolTokens( - idToken: idToken, - accessToken: accessToken, - refreshToken: existingSignedIndata.cognitoUserPoolTokens.refreshToken, - expiresIn: json["expires_in"] as? Int) - return SignedInData( - signedInDate: existingSignedIndata.signedInDate, - signInMethod: existingSignedIndata.signInMethod, - cognitoUserPoolTokens: userPoolTokens) - } else { - throw FetchSessionError.invalidTokens - } - } -} - -extension RefreshHostedUITokens: DefaultLogger { - public static var log: Logger { - Amplify.Logging.logger(forCategory: CategoryType.auth.displayName, forNamespace: String(describing: self)) - } - - public var log: Logger { - Self.log - } -} - -extension RefreshHostedUITokens: CustomDebugDictionaryConvertible { - var debugDictionary: [String: Any] { - [ - "identifier": identifier, - "existingSignedInData": existingSignedIndata - ] - } -} - -extension RefreshHostedUITokens: CustomDebugStringConvertible { - var debugDescription: String { - debugDictionary.debugDescription - } -} diff --git a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/StateMachine/Resolvers/RefreshSession/RefreshSessionState+Resolver.swift b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/StateMachine/Resolvers/RefreshSession/RefreshSessionState+Resolver.swift index 94e09d0412..3a0828f867 100644 --- a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/StateMachine/Resolvers/RefreshSession/RefreshSessionState+Resolver.swift +++ b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/StateMachine/Resolvers/RefreshSession/RefreshSessionState+Resolver.swift @@ -23,33 +23,19 @@ extension RefreshSessionState { case .notStarted: if case .refreshCognitoUserPool(let signedInData) = event.isRefreshSessionEvent { - if case .hostedUI = signedInData.signInMethod { - let action = RefreshHostedUITokens(existingSignedIndata: signedInData) - return .init(newState: .refreshingUserPoolToken(signedInData), - actions: [action]) - } else { - let action = RefreshUserPoolTokens(existingSignedIndata: signedInData) - return .init(newState: .refreshingUserPoolToken(signedInData), - actions: [action]) - } + let action = RefreshUserPoolTokens(existingSignedIndata: signedInData) + return .init(newState: .refreshingUserPoolToken(signedInData), + actions: [action]) } if case .refreshCognitoUserPoolWithIdentityId( let signedInData, let identityID) = event.isRefreshSessionEvent { - if case .hostedUI = signedInData.signInMethod { - let action = RefreshHostedUITokens(existingSignedIndata: signedInData) - return .init( - newState: .refreshingUserPoolTokenWithIdentity(signedInData, - identityID), - actions: [action]) - } else { - let action = RefreshUserPoolTokens(existingSignedIndata: signedInData) - return .init(newState: - .refreshingUserPoolTokenWithIdentity(signedInData, identityID), - actions: [action]) - } + let action = RefreshUserPoolTokens(existingSignedIndata: signedInData) + return .init(newState: + .refreshingUserPoolTokenWithIdentity(signedInData, identityID), + actions: [action]) } if case .refreshUnAuthAWSCredentials(let identityID) = event.isRefreshSessionEvent { diff --git a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Support/HostedUI/HostedUIRequestHelper.swift b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Support/HostedUI/HostedUIRequestHelper.swift index 7e06e9ac77..8976659779 100644 --- a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Support/HostedUI/HostedUIRequestHelper.swift +++ b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Support/HostedUI/HostedUIRequestHelper.swift @@ -120,36 +120,6 @@ struct HostedUIRequestHelper { return urlRequest } - static func createRefreshTokenRequest( - refreshToken: String, - configuration: HostedUIConfigurationData) throws -> URLRequest { - - var components = URLComponents() - components.scheme = "https" - components.path = "/oauth2/token" - components.host = configuration.oauth.domain - - guard let url = components.url else { - throw HostedUIError.tokenURI - } - - var queryComponents = URLComponents() - queryComponents.queryItems = [ - .init(name: "grant_type", value: "refresh_token"), - .init(name: "refresh_token", value: refreshToken), - .init(name: "client_id", value: configuration.clientId)] - - guard let body = queryComponents.query else { - throw HostedUIError.tokenURI - } - - var urlRequest = URLRequest(url: url) - urlRequest.httpMethod = "POST" - urlRequest.httpBody = Data(body.utf8) - urlRequest.addHeaders(using: configuration) - return urlRequest - } - static func urlSafeBase64(_ content: String) -> String { return content.replacingOccurrences(of: "/", with: "_") .replacingOccurrences(of: "+", with: "-") diff --git a/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/ActionTests/FetchAuthSession/FetchUserPoolTokens/RefreshHostedUITokensTests.swift b/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/ActionTests/FetchAuthSession/FetchUserPoolTokens/RefreshHostedUITokensTests.swift deleted file mode 100644 index 2bc60245eb..0000000000 --- a/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/ActionTests/FetchAuthSession/FetchUserPoolTokens/RefreshHostedUITokensTests.swift +++ /dev/null @@ -1,310 +0,0 @@ -// -// Copyright Amazon.com Inc. or its affiliates. -// All Rights Reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// - -#if os(iOS) || os(macOS) - -@testable import AWSCognitoAuthPlugin -import AWSCognitoIdentityProvider -import AWSPluginsCore -import XCTest - -class RefreshHostedUITokensTests: XCTestCase { - private let tokenResult: [String: Any] = [ - "id_token": AWSCognitoUserPoolTokens.testData.idToken, - "access_token": AWSCognitoUserPoolTokens.testData.accessToken, - "refresh_token": AWSCognitoUserPoolTokens.testData.refreshToken, - "expires_in": 10 - ] - - private var hostedUIEnvironment: HostedUIEnvironment { - BasicHostedUIEnvironment( - configuration: .init( - clientId: "clientId", - oauth: .init( - domain: "cognitodomain", - scopes: ["name"], - signInRedirectURI: "myapp://", - signOutRedirectURI: "myapp://" - ) - ), - hostedUISessionFactory: sessionFactory, - urlSessionFactory: urlSessionMock, - randomStringFactory: mockRandomString - ) - } - - override func setUp() { - let result = try! JSONSerialization.data(withJSONObject: tokenResult) - MockURLProtocol.requestHandler = { _ in - return (HTTPURLResponse(), result) - } - } - - override func tearDown() { - MockURLProtocol.requestHandler = nil - } - - /// Given: A RefreshHostedUITokens action - /// When: execute is invoked with a valid response - /// Then: A RefreshSessionEvent.refreshIdentityInfo is dispatched - func testExecute_withValidResponse_shouldDispatchRefreshEvent() async { - let expectation = expectation(description: "refreshHostedUITokens") - let action = RefreshHostedUITokens(existingSignedIndata: .testData) - action.execute( - withDispatcher: MockDispatcher { event in - guard let event = event as? RefreshSessionEvent, - case .refreshIdentityInfo(let data, _) = event.eventType else { - XCTFail("Failed to refresh tokens") - expectation.fulfill() - return - } - - XCTAssertEqual(data.cognitoUserPoolTokens.idToken, self.tokenResult["id_token"] as? String) - XCTAssertEqual(data.cognitoUserPoolTokens.accessToken, self.tokenResult["access_token"] as? String) - XCTAssertEqual(data.cognitoUserPoolTokens.refreshToken, self.tokenResult["refresh_token"] as? String) - expectation.fulfill() - }, - environment: Defaults.makeDefaultAuthEnvironment( - userPoolFactory: identityProviderFactory, - hostedUIEnvironment: hostedUIEnvironment - ) - ) - - await fulfillment(of: [expectation], timeout: 1) - } - - /// Given: A RefreshHostedUITokens action - /// When: execute is invoked and throws a HostedUIError - /// Then: A RefreshSessionEvent.throwError is dispatched with .service - func testExecute_withHostedUIError_shouldDispatchErrorEvent() async { - let expectedError = HostedUIError.serviceMessage("Something went wrong") - MockURLProtocol.requestHandler = { _ in - throw expectedError - } - - let expectation = expectation(description: "refreshHostedUITokens") - let action = RefreshHostedUITokens(existingSignedIndata: .testData) - action.execute( - withDispatcher: MockDispatcher { event in - guard let event = event as? RefreshSessionEvent, - case let .throwError(error) = event.eventType else { - XCTFail("Expected failure due to Service Error") - expectation.fulfill() - return - } - - XCTAssertEqual(error, .service(expectedError)) - expectation.fulfill() - }, - environment: Defaults.makeDefaultAuthEnvironment( - userPoolFactory: identityProviderFactory, - hostedUIEnvironment: hostedUIEnvironment - ) - ) - - await fulfillment(of: [expectation], timeout: 1) - } - - /// Given: A RefreshHostedUITokens action - /// When: execute is invoked and returns empty data - /// Then: A RefreshSessionEvent.throwError is dispatched with .service - func testExecute_withEmptyData_shouldDispatchErrorEvent() async { - MockURLProtocol.requestHandler = { _ in - return (HTTPURLResponse(), Data()) - } - - let expectation = expectation(description: "refreshHostedUITokens") - let action = RefreshHostedUITokens(existingSignedIndata: .testData) - action.execute( - withDispatcher: MockDispatcher { event in - guard let event = event as? RefreshSessionEvent, - case let .throwError(error) = event.eventType else { - XCTFail("Expected failure due to Invalid Tokens") - expectation.fulfill() - return - } - - guard case .service(let serviceError) = error else { - XCTFail("Expected FetchSessionError.service, got \(error)") - expectation.fulfill() - return - } - - - XCTAssertEqual((serviceError as NSError).code, NSPropertyListReadCorruptError) - expectation.fulfill() - }, - environment: Defaults.makeDefaultAuthEnvironment( - userPoolFactory: identityProviderFactory, - hostedUIEnvironment: hostedUIEnvironment - ) - ) - - await fulfillment(of: [expectation], timeout: 1) - } - - /// Given: A RefreshHostedUITokens action - /// When: execute is invoked and returns data that is invalid for tokens - /// Then: A RefreshSessionEvent.throwError is dispatched with .invalidTokens - func testExecute_withInvalidTokens_shouldDispatchErrorEvent() async { - let result: [String: Any] = [ - "key": "value" - ] - MockURLProtocol.requestHandler = { _ in - return (HTTPURLResponse(), try! JSONSerialization.data(withJSONObject: result)) - } - - let expectation = expectation(description: "refreshHostedUITokens") - let action = RefreshHostedUITokens(existingSignedIndata: .testData) - action.execute( - withDispatcher: MockDispatcher { event in - guard let event = event as? RefreshSessionEvent, - case let .throwError(error) = event.eventType else { - XCTFail("Expected failure due to Invalid Tokens") - expectation.fulfill() - return - } - - - XCTAssertEqual(error, .invalidTokens) - expectation.fulfill() - }, - environment: Defaults.makeDefaultAuthEnvironment( - userPoolFactory: identityProviderFactory, - hostedUIEnvironment: hostedUIEnvironment - ) - ) - - await fulfillment(of: [expectation], timeout: 1) - } - - /// Given: A RefreshHostedUITokens action - /// When: execute is invoked and returns data representing an error - /// Then: A RefreshSessionEvent.throwError is dispatched with .service - func testExecute_withErrorResponse_shouldDispatchErrorEvent() async { - let result: [String: Any] = [ - "error": "Error.", - "error_description": "Something went wrong" - ] - MockURLProtocol.requestHandler = { _ in - return (HTTPURLResponse(), try! JSONSerialization.data(withJSONObject: result)) - } - - let expectation = expectation(description: "refreshHostedUITokens") - let action = RefreshHostedUITokens(existingSignedIndata: .testData) - action.execute( - withDispatcher: MockDispatcher { event in - guard let event = event as? RefreshSessionEvent, - case let .throwError(error) = event.eventType else { - XCTFail("Expected failure due to Invalid Tokens") - expectation.fulfill() - return - } - - guard case .service(let serviceError) = error, - case .serviceMessage(let errorMessage) = serviceError as? HostedUIError else { - XCTFail("Expected HostedUIError.serviceMessage, got \(error)") - expectation.fulfill() - return - } - - - XCTAssertEqual(errorMessage, "Error. Something went wrong") - expectation.fulfill() - }, - environment: Defaults.makeDefaultAuthEnvironment( - userPoolFactory: identityProviderFactory, - hostedUIEnvironment: hostedUIEnvironment - ) - ) - - await fulfillment(of: [expectation], timeout: 1) - } - - /// Given: A RefreshHostedUITokens action - /// When: execute is invoked without a HostedUIEnvironment - /// Then: A RefreshSessionEvent.throwError is dispatched with .noUserPool - func testExecute_withoutHostedUIEnvironment_shouldDispatchErrorEvent() async { - let expectation = expectation(description: "noHostedUIEnvironment") - let action = RefreshHostedUITokens(existingSignedIndata: .testData) - action.execute( - withDispatcher: MockDispatcher { event in - guard let event = event as? RefreshSessionEvent, - case let .throwError(error) = event.eventType else { - XCTFail("Expected failure due to no HostedUIEnvironment") - expectation.fulfill() - return - } - - XCTAssertEqual(error, .noUserPool) - expectation.fulfill() - }, - environment: Defaults.makeDefaultAuthEnvironment( - userPoolFactory: identityProviderFactory, - hostedUIEnvironment: nil - ) - ) - - await fulfillment(of: [expectation], timeout: 1) - } - - /// Given: A RefreshHostedUITokens action - /// When: execute is invoked without a UserPoolEnvironment - /// Then: A RefreshSessionEvent.throwError is dispatched with .noUserPool - func testExecute_withoutUserPoolEnvironment_shouldDispatchErrorEvent() async { - let expectation = expectation(description: "noUserPoolEnvironment") - let action = RefreshHostedUITokens(existingSignedIndata: .testData) - action.execute( - withDispatcher: MockDispatcher { event in - guard let event = event as? RefreshSessionEvent, - case let .throwError(error) = event.eventType else { - XCTFail("Expected failure due to no UserPoolEnvironment") - expectation.fulfill() - return - } - - XCTAssertEqual(error, .noUserPool) - expectation.fulfill() - }, - environment: MockInvalidEnvironment() - ) - - await fulfillment(of: [expectation], timeout: 1) - } - - private func identityProviderFactory() throws -> CognitoUserPoolBehavior { - return MockIdentityProvider( - mockInitiateAuthResponse: { _ in - return InitiateAuthOutput( - authenticationResult: .init( - accessToken: "accessTokenNew", - expiresIn: 100, - idToken: "idTokenNew", - refreshToken: "refreshTokenNew") - ) - } - ) - } - - private func urlSessionMock() -> URLSession { - let configuration = URLSessionConfiguration.ephemeral - configuration.protocolClasses = [MockURLProtocol.self] - return URLSession(configuration: configuration) - } - - private func sessionFactory() -> HostedUISessionBehavior { - MockHostedUISession(result: .failure(.cancelled)) - } - - private func mockRandomString() -> RandomStringBehavior { - return MockRandomStringGenerator( - mockString: "mockString", - mockUUID: "mockUUID" - ) - } -} -#endif diff --git a/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/Support/HostedUIRequestHelperTests.swift b/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/Support/HostedUIRequestHelperTests.swift index 374a53371d..69f8ae19a0 100644 --- a/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/Support/HostedUIRequestHelperTests.swift +++ b/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/Support/HostedUIRequestHelperTests.swift @@ -82,31 +82,4 @@ class HostedUIRequestHelperTests: XCTestCase { let encodedSecret = try XCTUnwrap(encodedSecret) XCTAssertEqual("Basic \(encodedSecret)", header) } - - /// Given: A HostedUI configuration without a client secret - /// When: HostedUIRequestHelper.createRefreshTokenRequest is invoked with said configuration - /// Then: A request is generated that does not include an Authorization header - func testCreateRefreshTokenRequest_withoutClientSecret_shouldNotAddAuthorizationHeader() throws { - let request = try HostedUIRequestHelper.createRefreshTokenRequest( - refreshToken: "refreshToken", - configuration: configuration - ) - - XCTAssertNil(request.value(forHTTPHeaderField: "Authorization")) - } - - /// Given: A HostedUI configuration that defines a client secret - /// When: HostedUIRequestHelper.createRefreshTokenRequest is invoked with said configuration - /// Then: A request is generated that includes an Authorization header and its value has an encoded version of the secret - func testCreateRefreshTokenRequest_withClientSecret_shouldEncodeSecretAndAddAuthorizationHeader() throws { - createConfiguration(clientSecret: "clientSecret") - let request = try HostedUIRequestHelper.createRefreshTokenRequest( - refreshToken: "refreshToken", - configuration: configuration - ) - - let header = try XCTUnwrap(request.value(forHTTPHeaderField: "Authorization")) - let encodedSecret = try XCTUnwrap(encodedSecret) - XCTAssertEqual("Basic \(encodedSecret)", header) - } }