From 34ff5763639f2f5c1e49c9ef0bbf1a6892378dc0 Mon Sep 17 00:00:00 2001 From: George Bafaloukas Date: Mon, 18 Mar 2024 14:46:04 +0000 Subject: [PATCH 1/2] Moving the node and authservice common code to the nextprotocol class --- FRAuth/FRAuth.xcodeproj/project.pbxproj | 6 +- .../FRAuth/Authentication/AuthService.swift | 192 ++------------ .../FRAuth/Authentication/NextProtocol.swift | 224 +++++++++++++++++ FRAuth/FRAuth/Authentication/Node.swift | 238 ++---------------- .../FRAuth/Auth/AuthServiceTests.swift | 28 +-- .../FRTestHost.xcodeproj/project.pbxproj | 10 +- 6 files changed, 289 insertions(+), 409 deletions(-) create mode 100644 FRAuth/FRAuth/Authentication/NextProtocol.swift diff --git a/FRAuth/FRAuth.xcodeproj/project.pbxproj b/FRAuth/FRAuth.xcodeproj/project.pbxproj index c5d94deb..751d5326 100644 --- a/FRAuth/FRAuth.xcodeproj/project.pbxproj +++ b/FRAuth/FRAuth.xcodeproj/project.pbxproj @@ -283,6 +283,7 @@ D5F3E10324D20FBF00536EA0 /* SuspendedTextOutputCallback.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5F3E10224D20FBF00536EA0 /* SuspendedTextOutputCallback.swift */; }; D5F8F9EE24B7D50600EC9AEB /* BooleanAttributeInputCallback.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5F8F9ED24B7D50600EC9AEB /* BooleanAttributeInputCallback.swift */; }; D5F8F9F024B7D85600EC9AEB /* NumberAttributeInputCallback.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5F8F9EF24B7D85600EC9AEB /* NumberAttributeInputCallback.swift */; }; + EC03A7902BA1F8AE00BF9711 /* NextProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC03A78F2BA1F8AE00BF9711 /* NextProtocol.swift */; }; EC0BA2E7285B8F8F00F8326E /* FROptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC0BA2E6285B8F8F00F8326E /* FROptions.swift */; }; EC0BA2FA2863325B00F8326E /* FROptionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC0BA2F92863325B00F8326E /* FROptionsTests.swift */; }; EC13ABAA29A380920069AC41 /* FRWebAuthnManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC13ABA929A380920069AC41 /* FRWebAuthnManager.swift */; }; @@ -620,6 +621,7 @@ D5F8F9EF24B7D85600EC9AEB /* NumberAttributeInputCallback.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NumberAttributeInputCallback.swift; sourceTree = ""; }; D5FBD8A224B930E30005DD0F /* BooleanAttributeInputCallbackTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BooleanAttributeInputCallbackTests.swift; sourceTree = ""; }; D5FBD8A524B930F00005DD0F /* NumberAttributeInputCallbackTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NumberAttributeInputCallbackTests.swift; sourceTree = ""; }; + EC03A78F2BA1F8AE00BF9711 /* NextProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NextProtocol.swift; sourceTree = ""; }; EC0BA2E6285B8F8F00F8326E /* FROptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FROptions.swift; sourceTree = ""; }; EC0BA2F92863325B00F8326E /* FROptionsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FROptionsTests.swift; sourceTree = ""; }; EC13ABA929A380920069AC41 /* FRWebAuthnManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FRWebAuthnManager.swift; sourceTree = ""; }; @@ -1002,6 +1004,7 @@ children = ( D5723C3923F4C65800557AA8 /* AuthService.swift */, D5723C3A23F4C65800557AA8 /* Node.swift */, + EC03A78F2BA1F8AE00BF9711 /* NextProtocol.swift */, ); path = Authentication; sourceTree = ""; @@ -1202,7 +1205,7 @@ 959D7D97290B4B9200A1F22F /* AA-05-DeviceBindingCallbackTest.swift */, 95E180B32992A6F20087457D /* AA-06-DeviceSigningVerifierCallbackTest.swift */, 3A4B46E32AB95B3D009E7171 /* AA_07_AppIntegrityTest.swift */, - 9536C56B2B865DD600B2DFDD /* AA_08_TextInputCallbackTest.swift */, + 9536C56B2B865DD600B2DFDD /* AA_08_TextInputCallbackTest.swift */, 95EB7E4C2B8D010B00B59CD6 /* AA_09_PingOneProtectInitializeCallbackTest.swift */, 95EB7E522B8D5F6100B59CD6 /* AA_10_PingOneProtectEvaluateCallbackTest.swift */, ); @@ -1771,6 +1774,7 @@ D53A8044262789BD0093B1CA /* PlatformAuthenticatorConfig.swift in Sources */, EC7EA0F427D10FA70003F878 /* AppleSignInUserStructs.swift in Sources */, D533270B24806665002FF207 /* TokenManagementPolicy.swift in Sources */, + EC03A7902BA1F8AE00BF9711 /* NextProtocol.swift in Sources */, D586CF9D23358EE0007A2194 /* AbstractValidatedCallback.swift in Sources */, D586CFBD23358EE0007A2194 /* FRDeviceIdentifier.swift in Sources */, D586CF9723358EE0007A2194 /* SingleValueCallback.swift in Sources */, diff --git a/FRAuth/FRAuth/Authentication/AuthService.swift b/FRAuth/FRAuth/Authentication/AuthService.swift index fffd0c5c..e7ad560a 100644 --- a/FRAuth/FRAuth/Authentication/AuthService.swift +++ b/FRAuth/FRAuth/Authentication/AuthService.swift @@ -22,25 +22,7 @@ import FRCore * Any custom Callback must be implemented by inheriting Callback class, and be registered through CallbackFactory.shared.registerCallback(callbackType:callbackClass:). */ @objc(FRAuthService) -public class AuthService: NSObject { - - // MARK: - Property - - /// String value of AuthService name registered in AM - @objc public internal(set) var serviceName: String - /// Unique UUID String value of initiated AuthService flow - @objc public internal(set) var authServiceId: String - - /// authIndexType value in AM - var authIndexType: String - /// ServerConfig that contains OpenAM server information - var serverConfig: ServerConfig - /// OAuth2CLient that contains OpenAM's OAuth2 client information - var oAuth2Config: OAuth2Client? - /// TokenManager instance to manage, and persist authenticated session - var tokenManager: TokenManager? - /// KeychainManager instance to persist, and retrieve credentials from storage - var keychainManager: KeychainManager? +public class AuthService: NextProtocol { // MARK: - Init @@ -52,6 +34,8 @@ public class AuthService: NSObject { /// - serverConfig: ServerConfig object for AuthService server communication @objc public init(name: String, serverConfig: ServerConfig) { + super.init() + FRLog.v("AuthService init - service: \(name), ServerConfig: \(serverConfig)") self.serviceName = name self.authIndexType = OpenAM.service @@ -68,6 +52,8 @@ public class AuthService: NSObject { /// - keychainManager: KeychainManager instance to persist, and retrieve credentials from secure storage /// - tokenManager: TokenManager instance to manage and persist authenticated session init(suspendedId: String, serverConfig: ServerConfig, oAuth2Config: OAuth2Client?, keychainManager: KeychainManager? = nil, tokenManager: TokenManager? = nil) { + super.init() + FRLog.v("AuthService init - suspendedId: \(suspendedId), ServerConfig: \(serverConfig), OAuth2Client: \(String(describing: oAuth2Config)), KeychainManager: \(String(describing: keychainManager)), TokenManager: \(String(describing: tokenManager))") self.serviceName = suspendedId self.authIndexType = OpenAM.suspendedId @@ -90,6 +76,8 @@ public class AuthService: NSObject { /// - tokenManager: TokenManager instance to manage and persist authenticated session /// - authIndexType: String value of Authentication Tree type init(authIndexValue: String, serverConfig: ServerConfig, oAuth2Config: OAuth2Client?, keychainManager: KeychainManager? = nil, tokenManager: TokenManager? = nil, authIndexType: String = OpenAM.service) { + super.init() + FRLog.v("AuthService init - service: \(authIndexValue), serviceType: \(authIndexType) ServerConfig: \(serverConfig), OAuth2Client: \(String(describing: oAuth2Config)), KeychainManager: \(String(describing: keychainManager)), TokenManager: \(String(describing: tokenManager))") self.serviceName = authIndexValue self.authIndexType = authIndexType @@ -101,168 +89,27 @@ public class AuthService: NSObject { } - // MARK: Public - - /// Submits current Node object with Callback(s) and its given value(s) to OpenAM to proceed on authentication flow. - /// - /// - Parameter completion: NodeCompletion callback which returns the result of Node submission. - public func next(completion: @escaping NodeCompletion) { - - if T.self as AnyObject? === Token.self { - next { (token: Token?, node, error) in - completion(token as? T, node, error) - } - } - else if T.self as AnyObject? === AccessToken.self { - next { (token: AccessToken?, node, error) in - completion(token as? T, node, error) - } - } - else if T.self as AnyObject? === FRUser.self { - next { (user: FRUser?, node, error) in - completion(user as? T, node, error) - } - } - else { - completion(nil, nil, AuthError.invalidGenericType) - } - } - - - // MARK: Private/internal methods to handle different expected type of result - - fileprivate func next(completion: @escaping NodeCompletion) { - if let currentUser = FRUser.currentUser, currentUser.token != nil { - FRLog.i("FRUser.currentUser retrieved from SessionManager; ignoring AuthService submit") - completion(currentUser, nil, nil) - } - else { - self.next { (accessToken: AccessToken?, node, error) in - if let token = accessToken { - let user = FRUser(token: token) - - completion(user, nil, nil) - } - else { - completion(nil, node, error) - } - } - } - } - - - fileprivate func next(completion: @escaping NodeCompletion) { - - if let accessToken = try? self.keychainManager?.getAccessToken() { - FRLog.i("access_token retrieved from SessionManager; ignoring AuthService submit") - completion(accessToken, nil, nil) - } - else { - self.next { (token: Token?, node, error) in - - if let tokenId = token { - // If OAuth2Client is provided (for abstraction layer) - if let oAuth2Client = self.oAuth2Config { - // Exchange 'tokenId' (SSOToken) to OAuth2 token set - oAuth2Client.exchangeToken(token: tokenId, completion: { (accessToken, error) in - // Return an error if failed - if let error = error { - completion(nil, nil, error) - } - else { - - if let token = accessToken { - do { - try self.keychainManager?.setAccessToken(token: token) - } - catch { - FRLog.e("Unexpected error while storing AccessToken: \(error.localizedDescription)") - } - } - - // Return AccessToken - completion(accessToken, nil, nil) - } - }) - } - else { - completion(nil, nil, AuthError.invalidOAuth2Client) - } - } - else { - completion(nil, node, error) - } - } - } - } - - - fileprivate func next(completion: @escaping NodeCompletion) { + override func next(completion: @escaping NodeCompletion) { // Construct Request object for AuthService flow with given serviceName - let request = self.buildAuthServiceRequest() + guard let request = try? self.buildAuthServiceRequest(), + let authServiceId = self.authServiceId, + let serverConfig = self.serverConfig, + let serviceName = self.serviceName, + let authIndexType = self.authIndexType + else { return } - var action: Action? + let action: Action // For /authenticate request with suspendedId, return .RESUME_AUTHENTICATE type if self.authIndexType == OpenAM.suspendedId { action = Action(type: .RESUME_AUTHENTICATE) } // Otherwise, regular .START_AUTHENTICATE Action type else { - action = Action(type: .START_AUTHENTICATE, payload: ["tree": self.serviceName, "type": self.authIndexType]) + action = Action(type: .START_AUTHENTICATE, payload: ["tree": serviceName, "type": authIndexType]) } - // Invoke request - FRRestClient.invoke(request: request, action: action) { (result) in - switch result { - case .success(let response, _): - - // If authId received - if let _ = response[OpenAM.authId] { - do { - let node = try Node(self.authServiceId, response, self.serverConfig, self.serviceName, self.authIndexType, self.oAuth2Config, self.keychainManager, self.tokenManager) - completion(nil, node, nil) - } catch let authError as AuthError { - completion(nil, nil, authError) - } catch { - completion(nil, nil, error) - } - } - else if let tokenId = response[OpenAM.tokenId] as? String { - let token = Token(tokenId) - if let keychainManager = self.keychainManager { - let currentSessionToken = keychainManager.getSSOToken() - if let _ = try? keychainManager.getAccessToken(), token.value != currentSessionToken?.value { - FRLog.w("SDK identified existing Session Token (\(currentSessionToken?.value ?? "nil")) and received Session Token (\(token.value))'s mismatch; to avoid misled information, SDK automatically revokes OAuth2 token set issued with existing Session Token.") - if let tokenManager = self.tokenManager { - tokenManager.revokeAndEndSession { (error) in - FRLog.i("OAuth2 token set was revoked due to mismatch of Session Tokens; \(error?.localizedDescription ?? "")") - } - } - else { - FRLog.i("TokenManager is not found; OAuth2 token set was removed from the storage") - do { - try keychainManager.setAccessToken(token: nil) - } - catch { - FRLog.e("Unexpected error while removing AccessToken: \(error.localizedDescription)") - } - } - } - keychainManager.setSSOToken(ssoToken: token) - } - - completion(token, nil, nil) - } - else { - completion(nil, nil, nil) - } - break - case .failure(let error): - completion(nil, nil, error) - break - } - } + self.invoke(authServiceId: authServiceId, serverConfig: serverConfig, serviceName: serviceName, authIndexType: authIndexType, request: request, action: action, completion: completion) } @@ -271,12 +118,13 @@ public class AuthService: NSObject { /// Builds Request object for current Node /// /// - Returns: Request object for OpenAM AuthTree submit - func buildAuthServiceRequest() -> Request { + override func buildAuthServiceRequest() throws -> Request { // AM 6.5.2 - 7.0.0 // // Endpoint: /json/realms/authenticate // API Version: resource=2.1,protocol=1.0 + guard let serverConfig = self.serverConfig else { throw ConfigError.invalidSDKState } var header: [String: String] = [:] header[OpenAM.acceptAPIVersion] = OpenAM.apiResource21 + "," + OpenAM.apiProtocol10 @@ -292,7 +140,7 @@ public class AuthService: NSObject { parameter[OpenAM.authIndexValue] = self.serviceName } - return Request(url: self.serverConfig.authenticateURL, method: .POST, headers: header, urlParams: parameter, requestType: .json, responseType: .json, timeoutInterval: self.serverConfig.timeout) + return Request(url: serverConfig.authenticateURL, method: .POST, headers: header, urlParams: parameter, requestType: .json, responseType: .json, timeoutInterval: serverConfig.timeout) } diff --git a/FRAuth/FRAuth/Authentication/NextProtocol.swift b/FRAuth/FRAuth/Authentication/NextProtocol.swift new file mode 100644 index 00000000..51c1c346 --- /dev/null +++ b/FRAuth/FRAuth/Authentication/NextProtocol.swift @@ -0,0 +1,224 @@ +// +// NextProtocol.swift +// FRAuth +// +// Copyright (c) 2024 ForgeRock. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + + +import Foundation +import FRCore + +public class NextProtocol: NSObject { + + /// A list of Callback for the state + @objc public var callbacks: [Callback] = [] + /// authId for the authentication flow + @objc public var authId: String? + /// Unique UUID String value of initiated AuthService flow + @objc public var authServiceId: String? + /// Designated AuthService name defined in OpenAM + var serviceName: String? + /// authIndexType value in AM + var authIndexType: String? + /// ServerConfig information for AuthService/Node API communication + var serverConfig: ServerConfig? + /// OAuth2Client information for AuthService/Node API communication + var oAuth2Config: OAuth2Client? + /// TokenManager instance to manage, and persist authenticated session + var tokenManager: TokenManager? + /// KeychainManager instance to persist, and retrieve credentials from storage + var keychainManager: KeychainManager? + + public func next(completion:@escaping NodeCompletion) { + + if T.self as AnyObject? === Token.self { + next { (token: Token?, node, error) in + completion(token as? T, node, error) + } + } + else if T.self as AnyObject? === AccessToken.self { + next { (token: AccessToken?, node, error) in + completion(token as? T, node, error) + } + } + else if T.self as AnyObject? === FRUser.self { + next { (user: FRUser?, node, error) in + completion(user as? T, node, error) + } + } + else { + completion(nil, nil, AuthError.invalidGenericType) + } + } + + func next(completion:@escaping NodeCompletion) { + FRLog.v("Called") + if let currentUser = FRUser.currentUser { + FRLog.i("FRUser.currentUser retrieved from SessionManager; ignoring Node submit") + completion(currentUser, nil, nil) + } + else { + self.next { (accessToken: AccessToken?, node, error) in + if let token = accessToken { + let user = FRUser(token: token) + + completion(user, nil, nil) + } + else { + completion(nil, node, error) + } + } + } + } + + func next(completion:@escaping NodeCompletion) { + FRLog.v("Called") + if let accessToken = try? self.keychainManager?.getAccessToken() { + FRLog.i("access_token retrieved from SessionManager; ignoring Node submit") + completion(accessToken, nil, nil) + } + else { + self.next { (token: Token?, node, error) in + + if let tokenId = token { + // If OAuth2Client is provided (for abstraction layer) + if let oAuth2Client = self.oAuth2Config { + // Exchange 'tokenId' (SSOToken) to OAuth2 token set + oAuth2Client.exchangeToken(token: tokenId, completion: { (accessToken, error) in + // Return an error if failed + if let error = error { + completion(nil, nil, error) + } + else { + if let token = accessToken { + do { + try self.keychainManager?.setAccessToken(token: token) + } + catch { + FRLog.e("Unexpected error while storing AccessToken: \(error.localizedDescription)") + } + } + + // Return AccessToken + completion(accessToken, nil, nil) + } + }) + } + else { + completion(nil, nil, AuthError.invalidOAuth2Client) + } + } + else { + completion(nil, node, error) + } + } + } + } + + func next(completion:@escaping NodeCompletion) { + + guard let thisRequest = try? self.buildAuthServiceRequest(), + let authServiceId = self.authServiceId, + let serverConfig = self.serverConfig, + let serviceName = self.serviceName, + let authIndexType = self.authIndexType + else { return } + + let thisAction = Action(type: .AUTHENTICATE, payload: ["tree": serviceName, "type": authIndexType]) + + self.invoke(authServiceId: authServiceId, serverConfig: serverConfig, serviceName: serviceName, authIndexType: authIndexType, request: thisRequest, action: thisAction, completion: completion) + } + + func invoke(authServiceId: String, serverConfig: ServerConfig, serviceName: String, authIndexType: String, request: Request, action: Action, completion: @escaping NodeCompletion) { + + FRRestClient.invoke(request: request, action: action) { (result) in + switch result { + case .success(let response, _): + + // If authId received + if let _ = response[OpenAM.authId] { + do { + let node = try Node(authServiceId, response, serverConfig, serviceName, authIndexType, self.oAuth2Config, self.keychainManager, self.tokenManager) + completion(nil, node, nil) + } catch let authError as AuthError { + completion(nil, nil, authError) + } catch { + completion(nil, nil, error) + } + } + else if let tokenId = response[OpenAM.tokenId] as? String { + let token = Token(tokenId) + if let keychainManager = self.keychainManager { + let currentSessionToken = keychainManager.getSSOToken() + //If the `currentSessionToken` is nil and have OAuth2.0 tokens it means the user did a Centralized Login flow to authenticate initially. The token is now returned from either a Policy Advice and should be the same one as originaly created, or from a new authentication + if ((currentSessionToken == nil) && (try? keychainManager.getAccessToken()) != nil) && authIndexType == "composite_advice" { + // In this case we are running a transactional authorization flow, the new SSO Token is the same as the originally created one. When running Centralised login, this lived in the browser cookie storage and is unaccesssible from the app + // Save the SSO Token in the storage and return + keychainManager.setSSOToken(ssoToken: token) + } + else if let _ = try? keychainManager.getAccessToken(), token.value != currentSessionToken?.value { + FRLog.w("SDK identified existing Session Token (\(currentSessionToken?.value ?? "nil")) and received Session Token (\(token.value))'s mismatch; to avoid misled information, SDK automatically revokes OAuth2 token set issued with existing Session Token.") + if let tokenManager = self.tokenManager { + tokenManager.revokeAndEndSession { (error) in + FRLog.i("OAuth2 token set was revoked due to mismatch of Session Tokens; \(error?.localizedDescription ?? "")") + } + } + else { + FRLog.i("TokenManager is not found; OAuth2 token set was removed from the storage") + do { + try keychainManager.setAccessToken(token: nil) + } + catch { + FRLog.e("Unexpected error while removing AccessToken: \(error.localizedDescription)") + } + } + } + keychainManager.setSSOToken(ssoToken: token) + } + + completion(token, nil, nil) + } + else { + completion(nil, nil, nil) + } + break + case .failure(let error): + completion(nil, nil, error) + break + } + } + } + + func buildRequestPayload() -> [String:Any] { + + var payload: [String: Any] = [:] + + payload[OpenAM.authId] = self.authId + var callbacks: [Any] = [] + + for callback:Callback in self.callbacks { + callbacks.append(callback.buildResponse()) + } + + payload[OpenAM.callbacks] = callbacks + + return payload + } + + func buildAuthServiceRequest() throws -> Request { + + // AM 6.5.2 - 7.0.0 + // + // Endpoint: /json/realms/authenticate + // API Version: resource=2.1,protocol=1.0 + guard let serverConfig = self.serverConfig else { throw ConfigError.invalidSDKState } + + var header: [String: String] = [:] + header[OpenAM.acceptAPIVersion] = OpenAM.apiResource21 + "," + OpenAM.apiProtocol10 + return Request(url: serverConfig.authenticateURL, method: .POST, headers: header, bodyParams: self.buildRequestPayload(), urlParams: [:], requestType: .json, responseType: .json, timeoutInterval: serverConfig.timeout) + } +} diff --git a/FRAuth/FRAuth/Authentication/Node.swift b/FRAuth/FRAuth/Authentication/Node.swift index abf9d120..dbccd68c 100644 --- a/FRAuth/FRAuth/Authentication/Node.swift +++ b/FRAuth/FRAuth/Authentication/Node.swift @@ -18,16 +18,16 @@ import FRCore * An error, if occurred during the authentication flow */ @objc(FRNode) -public class Node: NSObject { +public class Node: NextProtocol { // MARK: - Public properties - /// A list of Callback for the state - @objc public var callbacks: [Callback] = [] - /// authId for the authentication flow - @objc public var authId: String - /// Unique UUID String value of initiated AuthService flow - @objc public var authServiceId: String +// /// A list of Callback for the state +// @objc public var callbacks: [Callback] = [] +// /// authId for the authentication flow +// @objc public var authId: String +// /// Unique UUID String value of initiated AuthService flow +// @objc public var authServiceId: String /// Stage attribute in Page Node @objc public var stage: String? /// Header attribute in Page Node @@ -35,17 +35,17 @@ public class Node: NSObject { /// Description attribute in Page Node @objc public var pageDescription: String? /// Designated AuthService name defined in OpenAM - var serviceName: String - /// authIndexType value in AM - var authIndexType: String - /// ServerConfig information for AuthService/Node API communication - var serverConfig: ServerConfig - /// OAuth2Client information for AuthService/Node API communication - var oAuth2Config: OAuth2Client? - /// TokenManager instance to manage, and persist authenticated session - var tokenManager: TokenManager? - /// KeychainManager instance to persist, and retrieve credentials from storage - var keychainManager: KeychainManager? +// var serviceName: String +// /// authIndexType value in AM +// var authIndexType: String +// /// ServerConfig information for AuthService/Node API communication +// var serverConfig: ServerConfig +// /// OAuth2Client information for AuthService/Node API communication +// var oAuth2Config: OAuth2Client? +// /// TokenManager instance to manage, and persist authenticated session +// var tokenManager: TokenManager? +// /// KeychainManager instance to persist, and retrieve credentials from storage +// var keychainManager: KeychainManager? @@ -64,6 +64,8 @@ public class Node: NSObject { /// - Throws: `AuthError` init?(_ authServiceId: String, _ authServiceResponse: [String: Any], _ serverConfig: ServerConfig, _ serviceName: String, _ authIndexType: String, _ oAuth2Config: OAuth2Client? = nil, _ keychainManager: KeychainManager? = nil, _ tokenManager: TokenManager? = nil) throws { + super.init() + guard let authId = authServiceResponse[OpenAM.authId] as? String else { FRLog.e("Invalid response: missing 'authId'") throw AuthError.invalidAuthServiceResponse("missing or invalid 'authId'") @@ -146,206 +148,6 @@ public class Node: NSObject { return callback } - - // MARK: - Public methods - - /// Submits current Node object with Callback(s) and its given value(s) to OpenAM to proceed on authentication flow. - /// - /// - Parameter completion: NodeCompletion callback which returns the result of Node submission. - public func next(completion:@escaping NodeCompletion) { - - if T.self as AnyObject? === Token.self { - next { (token: Token?, node, error) in - completion(token as? T, node, error) - } - } - else if T.self as AnyObject? === AccessToken.self { - next { (token: AccessToken?, node, error) in - completion(token as? T, node, error) - } - } - else if T.self as AnyObject? === FRUser.self { - next { (user: FRUser?, node, error) in - completion(user as? T, node, error) - } - } - else { - completion(nil, nil, AuthError.invalidGenericType) - } - } - - - // MARK: Private/internal methods to handle different expected type of result - - /// Submits current node, and returns FRUser instance if result of node returns SSO TOken - /// - /// - Parameter completion: NodeCompletion callback that returns FRUser upon completion - fileprivate func next(completion:@escaping NodeCompletion) { - FRLog.v("Called") - if let currentUser = FRUser.currentUser { - FRLog.i("FRUser.currentUser retrieved from SessionManager; ignoring Node submit") - completion(currentUser, nil, nil) - } - else { - self.next { (accessToken: AccessToken?, node, error) in - if let token = accessToken { - let user = FRUser(token: token) - - completion(user, nil, nil) - } - else { - completion(nil, node, error) - } - } - } - } - - - /// Submits current node, and returns AccessToken instance if result of node returns SSO TOken - /// - /// - Parameter completion: NodeCompletion callback that returns AccessToken upon completion - fileprivate func next(completion:@escaping NodeCompletion) { - FRLog.v("Called") - if let accessToken = try? self.keychainManager?.getAccessToken() { - FRLog.i("access_token retrieved from SessionManager; ignoring Node submit") - completion(accessToken, nil, nil) - } - else { - self.next { (token: Token?, node, error) in - - if let tokenId = token { - // If OAuth2Client is provided (for abstraction layer) - if let oAuth2Client = self.oAuth2Config { - // Exchange 'tokenId' (SSOToken) to OAuth2 token set - oAuth2Client.exchangeToken(token: tokenId, completion: { (accessToken, error) in - // Return an error if failed - if let error = error { - completion(nil, nil, error) - } - else { - if let token = accessToken { - do { - try self.keychainManager?.setAccessToken(token: token) - } - catch { - FRLog.e("Unexpected error while storing AccessToken: \(error.localizedDescription)") - } - } - - // Return AccessToken - completion(accessToken, nil, nil) - } - }) - } - else { - completion(nil, nil, AuthError.invalidOAuth2Client) - } - } - else { - completion(nil, node, error) - } - } - } - } - - - /// Submits current node, and returns Token instance if result of node returns SSO TOken - /// - /// - Parameter completion: NodeCompletion callback that returns Token upon completion - fileprivate func next(completion:@escaping NodeCompletion) { - - let thisRequest = self.buildAuthServiceRequest() - FRRestClient.invoke(request: thisRequest, action: Action(type: .AUTHENTICATE, payload: ["tree": self.serviceName, "type": self.authIndexType])) { (result) in - switch result { - case .success(let response, _): - - // If authId received - if let _ = response[OpenAM.authId] { - do { - let node = try Node(self.authServiceId, response, self.serverConfig, self.serviceName, self.authIndexType, self.oAuth2Config, self.keychainManager, self.tokenManager) - completion(nil, node, nil) - } catch let authError as AuthError { - completion(nil, nil, authError) - } catch { - completion(nil, nil, error) - } - } - else if let tokenId = response[OpenAM.tokenId] as? String { - let token = Token(tokenId) - if let keychainManager = self.keychainManager { - let currentSessionToken = keychainManager.getSSOToken() - if let _ = try? keychainManager.getAccessToken(), token.value != currentSessionToken?.value { - FRLog.w("SDK identified existing Session Token (\(currentSessionToken?.value ?? "nil")) and received Session Token (\(token.value))'s mismatch; to avoid misled information, SDK automatically revokes OAuth2 token set issued with existing Session Token.") - if let tokenManager = self.tokenManager { - tokenManager.revokeAndEndSession { (error) in - FRLog.i("OAuth2 token set was revoked due to mismatch of Session Tokens; \(error?.localizedDescription ?? "")") - } - } - else { - FRLog.i("TokenManager is not found; OAuth2 token set was removed from the storage") - do { - try keychainManager.setAccessToken(token: nil) - } - catch { - FRLog.e("Unexpected error while removing AccessToken: \(error.localizedDescription)") - } - } - } - keychainManager.setSSOToken(ssoToken: token) - } - - completion(token, nil, nil) - } - else { - completion(nil, nil, nil) - } - break - case .failure(let error): - completion(nil, nil, error) - break - } - } - } - - - // - MARK: Private request build helper methods - - /// Builds Dictionary object for request parameter with given list of Callback(s) - /// - /// - Returns: Dictionary object containing all values of Callback(s), and AuthService information - @objc - func buildRequestPayload() -> [String:Any] { - - var payload: [String: Any] = [:] - - payload[OpenAM.authId] = self.authId - var callbacks: [Any] = [] - - for callback:Callback in self.callbacks { - callbacks.append(callback.buildResponse()) - } - - payload[OpenAM.callbacks] = callbacks - - return payload - } - - /// Builds Request object for current Node - /// - /// - Returns: Request object for OpenAM AuthTree submit - func buildAuthServiceRequest() -> Request { - - // AM 6.5.2 - 7.0.0 - // - // Endpoint: /json/realms/authenticate - // API Version: resource=2.1,protocol=1.0 - - var header: [String: String] = [:] - header[OpenAM.acceptAPIVersion] = OpenAM.apiResource21 + "," + OpenAM.apiProtocol10 - return Request(url: self.serverConfig.authenticateURL, method: .POST, headers: header, bodyParams: self.buildRequestPayload(), urlParams: [:], requestType: .json, responseType: .json, timeoutInterval: self.serverConfig.timeout) - } - - // - MARK: Objective-C Compatibility @objc(nextWithUserCompletion:) diff --git a/FRAuth/FRAuthTests/FRAuthSwiftTests/FRAuth/Auth/AuthServiceTests.swift b/FRAuth/FRAuthTests/FRAuthSwiftTests/FRAuth/Auth/AuthServiceTests.swift index 61036b76..33769468 100644 --- a/FRAuth/FRAuthTests/FRAuthSwiftTests/FRAuth/Auth/AuthServiceTests.swift +++ b/FRAuth/FRAuthTests/FRAuthSwiftTests/FRAuth/Auth/AuthServiceTests.swift @@ -35,12 +35,12 @@ class AuthServiceTests: FRAuthBaseTest { // Then XCTAssertEqual(authService.serviceName, self.authServiceName) - XCTAssertEqual(authService.serverConfig.baseURL.absoluteString, self.serverURL) - XCTAssertEqual(authService.serverConfig.authenticateURL, self.serverURL + "/json/realms/\(self.realm)/authenticate") - XCTAssertEqual(authService.serverConfig.tokenURL, self.serverURL + "/oauth2/realms/\(self.realm)/access_token") - XCTAssertEqual(authService.serverConfig.authorizeURL, self.serverURL + "/oauth2/realms/\(self.realm)/authorize") - XCTAssertEqual(authService.serverConfig.timeout, 90) - XCTAssertEqual(authService.serverConfig.realm, self.realm) + XCTAssertEqual(authService.serverConfig?.baseURL.absoluteString, self.serverURL) + XCTAssertEqual(authService.serverConfig?.authenticateURL, self.serverURL + "/json/realms/\(self.realm)/authenticate") + XCTAssertEqual(authService.serverConfig?.tokenURL, self.serverURL + "/oauth2/realms/\(self.realm)/access_token") + XCTAssertEqual(authService.serverConfig?.authorizeURL, self.serverURL + "/oauth2/realms/\(self.realm)/authorize") + XCTAssertEqual(authService.serverConfig?.timeout, 90) + XCTAssertEqual(authService.serverConfig?.realm, self.realm) XCTAssertNil(authService.oAuth2Config) } @@ -52,12 +52,12 @@ class AuthServiceTests: FRAuthBaseTest { // Then XCTAssertEqual(authService.serviceName, self.authServiceName) - XCTAssertEqual(authService.serverConfig.baseURL.absoluteString, self.serverURL) - XCTAssertEqual(authService.serverConfig.authenticateURL, self.serverURL + "/json/realms/\(self.realm)/authenticate") - XCTAssertEqual(authService.serverConfig.tokenURL, self.serverURL + "/oauth2/realms/\(self.realm)/access_token") - XCTAssertEqual(authService.serverConfig.authorizeURL, self.serverURL + "/oauth2/realms/\(self.realm)/authorize") - XCTAssertEqual(authService.serverConfig.timeout, 90) - XCTAssertEqual(authService.serverConfig.realm, "customRealm") + XCTAssertEqual(authService.serverConfig?.baseURL.absoluteString, self.serverURL) + XCTAssertEqual(authService.serverConfig?.authenticateURL, self.serverURL + "/json/realms/\(self.realm)/authenticate") + XCTAssertEqual(authService.serverConfig?.tokenURL, self.serverURL + "/oauth2/realms/\(self.realm)/access_token") + XCTAssertEqual(authService.serverConfig?.authorizeURL, self.serverURL + "/oauth2/realms/\(self.realm)/authorize") + XCTAssertEqual(authService.serverConfig?.timeout, 90) + XCTAssertEqual(authService.serverConfig?.realm, "customRealm") XCTAssertNotNil(authService.oAuth2Config) XCTAssertEqual(authService.oAuth2Config?.clientId, self.clientId) XCTAssertEqual(authService.oAuth2Config?.redirectUri.absoluteString, self.redirectUri) @@ -135,8 +135,8 @@ class AuthServiceTests: FRAuthBaseTest { XCTAssertEqual(authService.authIndexType, "suspendedId") XCTAssertEqual(authService.serviceName, "6IIIUln3ajONR4ySwZt15qzh8X4") - let request = authService.buildAuthServiceRequest() - let urlRequest = request.build() + let request = try? authService.buildAuthServiceRequest() + let urlRequest = request?.build() let urlStr = urlRequest?.url?.absoluteString XCTAssertNotNil(urlRequest) XCTAssertNotNil(urlStr) diff --git a/FRTestHost/FRTestHost.xcodeproj/project.pbxproj b/FRTestHost/FRTestHost.xcodeproj/project.pbxproj index 07986cc1..cddc731f 100644 --- a/FRTestHost/FRTestHost.xcodeproj/project.pbxproj +++ b/FRTestHost/FRTestHost.xcodeproj/project.pbxproj @@ -9,8 +9,6 @@ /* Begin PBXBuildFile section */ A589EC8829E6430700399B64 /* FRDeviceBinding.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A589EC8729E6430700399B64 /* FRDeviceBinding.framework */; }; A589EC8929E6430F00399B64 /* FRDeviceBinding.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = A589EC8729E6430700399B64 /* FRDeviceBinding.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - A59508352B630FB400E366F9 /* PingProtect.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A59508342B630FB400E366F9 /* PingProtect.framework */; }; - A59508362B630FCE00E366F9 /* PingProtect.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = A59508342B630FB400E366F9 /* PingProtect.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; A5950A2C27EA2C6D00EDEFE4 /* FRAuthConfigPKHash.plist in Resources */ = {isa = PBXBuildFile; fileRef = A5950A2B27EA2C6D00EDEFE4 /* FRAuthConfigPKHash.plist */; }; A5950A2E27EA2F7100EDEFE4 /* FRAuthConfigEmptyPKHash.plist in Resources */ = {isa = PBXBuildFile; fileRef = A5950A2D27EA2F7100EDEFE4 /* FRAuthConfigEmptyPKHash.plist */; }; D5A7A1E9248CD88C00E30CFE /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5A7A1E8248CD88C00E30CFE /* AppDelegate.swift */; }; @@ -31,6 +29,8 @@ D5D5DC1225E76C8100A8AF0E /* FRProximity.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = D5D5DC0925E76C8100A8AF0E /* FRProximity.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; D5D5DC1325E76C8100A8AF0E /* FRUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D5D5DC0A25E76C8100A8AF0E /* FRUI.framework */; }; D5D5DC1425E76C8100A8AF0E /* FRUI.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = D5D5DC0A25E76C8100A8AF0E /* FRUI.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + EC03A79F2BA4702500BF9711 /* PingProtect.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EC03A79E2BA4702500BF9711 /* PingProtect.framework */; }; + EC03A7A02BA4702500BF9711 /* PingProtect.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = EC03A79E2BA4702500BF9711 /* PingProtect.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -40,13 +40,13 @@ dstPath = ""; dstSubfolderSpec = 10; files = ( - A59508362B630FCE00E366F9 /* PingProtect.framework in Embed Frameworks */, A589EC8929E6430F00399B64 /* FRDeviceBinding.framework in Embed Frameworks */, D5D5DC0E25E76C8100A8AF0E /* FRAuthenticator.framework in Embed Frameworks */, D5D5DC0C25E76C8100A8AF0E /* FRAuth.framework in Embed Frameworks */, D5D5DC1425E76C8100A8AF0E /* FRUI.framework in Embed Frameworks */, D5D5DC1225E76C8100A8AF0E /* FRProximity.framework in Embed Frameworks */, D5D5DC1025E76C8100A8AF0E /* FRCore.framework in Embed Frameworks */, + EC03A7A02BA4702500BF9711 /* PingProtect.framework in Embed Frameworks */, ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; @@ -76,6 +76,7 @@ D5D5DC0825E76C8100A8AF0E /* FRCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = FRCore.framework; sourceTree = BUILT_PRODUCTS_DIR; }; D5D5DC0925E76C8100A8AF0E /* FRProximity.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = FRProximity.framework; sourceTree = BUILT_PRODUCTS_DIR; }; D5D5DC0A25E76C8100A8AF0E /* FRUI.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = FRUI.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + EC03A79E2BA4702500BF9711 /* PingProtect.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = PingProtect.framework; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -83,13 +84,13 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - A59508352B630FB400E366F9 /* PingProtect.framework in Frameworks */, A589EC8829E6430700399B64 /* FRDeviceBinding.framework in Frameworks */, D5D5DC0D25E76C8100A8AF0E /* FRAuthenticator.framework in Frameworks */, D5D5DC0B25E76C8100A8AF0E /* FRAuth.framework in Frameworks */, D5D5DC1325E76C8100A8AF0E /* FRUI.framework in Frameworks */, D5D5DC1125E76C8100A8AF0E /* FRProximity.framework in Frameworks */, D5D5DC0F25E76C8100A8AF0E /* FRCore.framework in Frameworks */, + EC03A79F2BA4702500BF9711 /* PingProtect.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -145,6 +146,7 @@ D5D5DC0525E76C8100A8AF0E /* Frameworks */ = { isa = PBXGroup; children = ( + EC03A79E2BA4702500BF9711 /* PingProtect.framework */, A59508342B630FB400E366F9 /* PingProtect.framework */, A589EC8729E6430700399B64 /* FRDeviceBinding.framework */, D5D5DC0625E76C8100A8AF0E /* FRAuth.framework */, From e366b9e767102cd2b65f346566efd6a25d8ca9f5 Mon Sep 17 00:00:00 2001 From: George Bafaloukas Date: Wed, 20 Mar 2024 16:45:55 +0000 Subject: [PATCH 2/2] Added NodeNext parent class that AuthService and Node classes subclass to do the Node.next calls. Added tests that cover policy auths for both Central and Embedded use cases Fixed an issue that if Central login happens the Policy Authentications with OAuth2,0 tokens, revoke the OAuth2.0 tokens --- FRAuth/FRAuth.xcodeproj/project.pbxproj | 12 +- .../FRAuth/Authentication/AuthService.swift | 4 +- FRAuth/FRAuth/Authentication/Node.swift | 4 +- .../{NextProtocol.swift => NodeNext.swift} | 6 +- .../FRAuth/FRSession/FRSessionTests.swift | 231 +++++++++++++++++- .../AuthTree/PolicyAdviceUsernameNode.json | 43 ++++ 6 files changed, 288 insertions(+), 12 deletions(-) rename FRAuth/FRAuth/Authentication/{NextProtocol.swift => NodeNext.swift} (98%) create mode 100644 FRTestHost/FRTestHost/SharedTestFiles/TestData/MockResponseData/AuthTree/PolicyAdviceUsernameNode.json diff --git a/FRAuth/FRAuth.xcodeproj/project.pbxproj b/FRAuth/FRAuth.xcodeproj/project.pbxproj index 751d5326..85d2856a 100644 --- a/FRAuth/FRAuth.xcodeproj/project.pbxproj +++ b/FRAuth/FRAuth.xcodeproj/project.pbxproj @@ -283,7 +283,8 @@ D5F3E10324D20FBF00536EA0 /* SuspendedTextOutputCallback.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5F3E10224D20FBF00536EA0 /* SuspendedTextOutputCallback.swift */; }; D5F8F9EE24B7D50600EC9AEB /* BooleanAttributeInputCallback.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5F8F9ED24B7D50600EC9AEB /* BooleanAttributeInputCallback.swift */; }; D5F8F9F024B7D85600EC9AEB /* NumberAttributeInputCallback.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5F8F9EF24B7D85600EC9AEB /* NumberAttributeInputCallback.swift */; }; - EC03A7902BA1F8AE00BF9711 /* NextProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC03A78F2BA1F8AE00BF9711 /* NextProtocol.swift */; }; + EC03A7902BA1F8AE00BF9711 /* NodeNext.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC03A78F2BA1F8AE00BF9711 /* NodeNext.swift */; }; + EC03A7CB2BAB1C8200BF9711 /* PolicyAdviceUsernameNode.json in Resources */ = {isa = PBXBuildFile; fileRef = EC03A7C92BAB1C8200BF9711 /* PolicyAdviceUsernameNode.json */; }; EC0BA2E7285B8F8F00F8326E /* FROptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC0BA2E6285B8F8F00F8326E /* FROptions.swift */; }; EC0BA2FA2863325B00F8326E /* FROptionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC0BA2F92863325B00F8326E /* FROptionsTests.swift */; }; EC13ABAA29A380920069AC41 /* FRWebAuthnManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC13ABA929A380920069AC41 /* FRWebAuthnManager.swift */; }; @@ -621,7 +622,8 @@ D5F8F9EF24B7D85600EC9AEB /* NumberAttributeInputCallback.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NumberAttributeInputCallback.swift; sourceTree = ""; }; D5FBD8A224B930E30005DD0F /* BooleanAttributeInputCallbackTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BooleanAttributeInputCallbackTests.swift; sourceTree = ""; }; D5FBD8A524B930F00005DD0F /* NumberAttributeInputCallbackTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NumberAttributeInputCallbackTests.swift; sourceTree = ""; }; - EC03A78F2BA1F8AE00BF9711 /* NextProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NextProtocol.swift; sourceTree = ""; }; + EC03A78F2BA1F8AE00BF9711 /* NodeNext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeNext.swift; sourceTree = ""; }; + EC03A7C92BAB1C8200BF9711 /* PolicyAdviceUsernameNode.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = PolicyAdviceUsernameNode.json; sourceTree = ""; }; EC0BA2E6285B8F8F00F8326E /* FROptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FROptions.swift; sourceTree = ""; }; EC0BA2F92863325B00F8326E /* FROptionsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FROptionsTests.swift; sourceTree = ""; }; EC13ABA929A380920069AC41 /* FRWebAuthnManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FRWebAuthnManager.swift; sourceTree = ""; }; @@ -1004,7 +1006,7 @@ children = ( D5723C3923F4C65800557AA8 /* AuthService.swift */, D5723C3A23F4C65800557AA8 /* Node.swift */, - EC03A78F2BA1F8AE00BF9711 /* NextProtocol.swift */, + EC03A78F2BA1F8AE00BF9711 /* NodeNext.swift */, ); path = Authentication; sourceTree = ""; @@ -1348,6 +1350,7 @@ D5888C6C25664EDF0041FD94 /* AuthTree_DeviceCollectorNodeLocationOnly.json */, D5888C6D25664EDF0041FD94 /* AuthTree_LoginNode.json */, D5888C6E25664EDF0041FD94 /* AuthTree_UsernamePasswordNode.json */, + EC03A7C92BAB1C8200BF9711 /* PolicyAdviceUsernameNode.json */, D5888C6F25664EDF0041FD94 /* AM_Push_Authentication_Successful.json */, D5888C7025664EDF0041FD94 /* AuthTree_DeviceCollectorNodeWithMessage.json */, D5888C7125664EDF0041FD94 /* AuthTree_RegistrationNode.json */, @@ -1689,6 +1692,7 @@ D5888CA425664EDF0041FD94 /* AuthTree_UsernamePasswordNodeWithCustomCallback.json in Resources */, D5888C9E25664EDF0041FD94 /* AuthTree_SSOToken_Success2.json in Resources */, D5888C9B25664EDF0041FD94 /* OAuth2_EndSession_Failure.json in Resources */, + EC03A7CB2BAB1C8200BF9711 /* PolicyAdviceUsernameNode.json in Resources */, D5888C9725664EDF0041FD94 /* OAuth2_UserInfo_Failure.json in Resources */, D5888CAB25664EDF0041FD94 /* AuthTree_LoginNode.json in Resources */, D5888CA225664EDF0041FD94 /* AM_Push_Authentication_Fail.json in Resources */, @@ -1774,7 +1778,7 @@ D53A8044262789BD0093B1CA /* PlatformAuthenticatorConfig.swift in Sources */, EC7EA0F427D10FA70003F878 /* AppleSignInUserStructs.swift in Sources */, D533270B24806665002FF207 /* TokenManagementPolicy.swift in Sources */, - EC03A7902BA1F8AE00BF9711 /* NextProtocol.swift in Sources */, + EC03A7902BA1F8AE00BF9711 /* NodeNext.swift in Sources */, D586CF9D23358EE0007A2194 /* AbstractValidatedCallback.swift in Sources */, D586CFBD23358EE0007A2194 /* FRDeviceIdentifier.swift in Sources */, D586CF9723358EE0007A2194 /* SingleValueCallback.swift in Sources */, diff --git a/FRAuth/FRAuth/Authentication/AuthService.swift b/FRAuth/FRAuth/Authentication/AuthService.swift index e7ad560a..bbfa9416 100644 --- a/FRAuth/FRAuth/Authentication/AuthService.swift +++ b/FRAuth/FRAuth/Authentication/AuthService.swift @@ -2,7 +2,7 @@ // AuthService.swift // FRAuth // -// Copyright (c) 2019-2021 ForgeRock. All rights reserved. +// Copyright (c) 2019-2024 ForgeRock. All rights reserved. // // This software may be modified and distributed under the terms // of the MIT license. See the LICENSE file for details. @@ -22,7 +22,7 @@ import FRCore * Any custom Callback must be implemented by inheriting Callback class, and be registered through CallbackFactory.shared.registerCallback(callbackType:callbackClass:). */ @objc(FRAuthService) -public class AuthService: NextProtocol { +public class AuthService: NodeNext { // MARK: - Init diff --git a/FRAuth/FRAuth/Authentication/Node.swift b/FRAuth/FRAuth/Authentication/Node.swift index dbccd68c..658d7321 100644 --- a/FRAuth/FRAuth/Authentication/Node.swift +++ b/FRAuth/FRAuth/Authentication/Node.swift @@ -2,7 +2,7 @@ // Node.swift // FRAuth // -// Copyright (c) 2019-2021 ForgeRock. All rights reserved. +// Copyright (c) 2019-2024 ForgeRock. All rights reserved. // // This software may be modified and distributed under the terms // of the MIT license. See the LICENSE file for details. @@ -18,7 +18,7 @@ import FRCore * An error, if occurred during the authentication flow */ @objc(FRNode) -public class Node: NextProtocol { +public class Node: NodeNext { // MARK: - Public properties diff --git a/FRAuth/FRAuth/Authentication/NextProtocol.swift b/FRAuth/FRAuth/Authentication/NodeNext.swift similarity index 98% rename from FRAuth/FRAuth/Authentication/NextProtocol.swift rename to FRAuth/FRAuth/Authentication/NodeNext.swift index 51c1c346..dd81d833 100644 --- a/FRAuth/FRAuth/Authentication/NextProtocol.swift +++ b/FRAuth/FRAuth/Authentication/NodeNext.swift @@ -12,7 +12,7 @@ import Foundation import FRCore -public class NextProtocol: NSObject { +public class NodeNext: NSObject { /// A list of Callback for the state @objc public var callbacks: [Callback] = [] @@ -157,8 +157,8 @@ public class NextProtocol: NSObject { //If the `currentSessionToken` is nil and have OAuth2.0 tokens it means the user did a Centralized Login flow to authenticate initially. The token is now returned from either a Policy Advice and should be the same one as originaly created, or from a new authentication if ((currentSessionToken == nil) && (try? keychainManager.getAccessToken()) != nil) && authIndexType == "composite_advice" { // In this case we are running a transactional authorization flow, the new SSO Token is the same as the originally created one. When running Centralised login, this lived in the browser cookie storage and is unaccesssible from the app - // Save the SSO Token in the storage and return - keychainManager.setSSOToken(ssoToken: token) + completion(token, nil, nil) + return } else if let _ = try? keychainManager.getAccessToken(), token.value != currentSessionToken?.value { FRLog.w("SDK identified existing Session Token (\(currentSessionToken?.value ?? "nil")) and received Session Token (\(token.value))'s mismatch; to avoid misled information, SDK automatically revokes OAuth2 token set issued with existing Session Token.") diff --git a/FRAuth/FRAuthTests/FRAuthSwiftTests/FRAuth/FRSession/FRSessionTests.swift b/FRAuth/FRAuthTests/FRAuthSwiftTests/FRAuth/FRSession/FRSessionTests.swift index 4a4eb836..a6ca1295 100644 --- a/FRAuth/FRAuthTests/FRAuthSwiftTests/FRAuth/FRSession/FRSessionTests.swift +++ b/FRAuth/FRAuthTests/FRAuthSwiftTests/FRAuth/FRSession/FRSessionTests.swift @@ -2,7 +2,7 @@ // FRSessionTests.swift // FRAuthTests // -// Copyright (c) 2020-2022 ForgeRock. All rights reserved. +// Copyright (c) 2020-2024 ForgeRock. All rights reserved. // // This software may be modified and distributed under the terms // of the MIT license. See the LICENSE file for details. @@ -519,6 +519,215 @@ class FRSessionTests: FRAuthBaseTest { XCTAssertNotNil(FRSession.currentSession?.sessionToken) XCTAssertNotNil(FRSession.currentSession?.sessionToken?.value) } + + func test_11_frsession_authenticate_with_policyAdvice_with_session() { + + // Start SDK + self.startSDK() + super.setUp() + + // Authenticate user and get a session + self.authenticateUser() + XCTAssertNotNil(FRSession.currentSession) + XCTAssertNotNil(FRSession.currentSession?.sessionToken) + + // Set mock responses + self.loadMockResponses(["PolicyAdviceUsernameNode", + "AuthTree_SSOToken_Success"]) + + + + let policyAdvice = PolicyAdvice(type: "TransactionConditionAdvice", value: "5afff42a-2715-40c8-98e7-919abc1b2dfc") + var currentNode: Node? + + var ex = self.expectation(description: "First Node submit - again") + FRSession.authenticate(policyAdvice: policyAdvice!) { token, node, error in + // Validate result + XCTAssertNil(token) + XCTAssertNil(error) + XCTAssertNotNil(node) + currentNode = node + ex.fulfill() + } + waitForExpectations(timeout: 60, handler: nil) + + guard let node = currentNode else { + XCTFail("Failed to get Node from the first request") + return + } + + // Provide input value for callbacks + for callback in node.callbacks { + if callback is NameCallback, let nameCallback = callback as? NameCallback { + nameCallback.setValue(config.username) + } + else if callback is PasswordCallback, let passwordCallback = callback as? PasswordCallback { + passwordCallback.setValue(config.password) + } + else { + XCTFail("Received unexpected callback \(callback)") + } + } + + ex = self.expectation(description: "Second Node submit") + node.next { (token: Token?, node, error) in + // Validate result + XCTAssertNil(node) + XCTAssertNil(error) + XCTAssertNotNil(token) + ex.fulfill() + } + waitForExpectations(timeout: 60, handler: nil) + + //Check that the Session is still there + XCTAssertNotNil(FRSession.currentSession) + XCTAssertNotNil(FRSession.currentSession?.sessionToken) + } + + func test_12_frsession_authenticate_with_policyAdvice_with_IDToken() { + + // Start SDK + self.startSDK() + super.setUp() + + // Authenticate user and get a session + self.authenticateUser() + XCTAssertNotNil(FRSession.currentSession) + XCTAssertNotNil(FRSession.currentSession?.sessionToken) + FRRequestInterceptorRegistry.shared.registerInterceptors(interceptors: [IDTokenForSessionInterceptor()]) + //Get access token + // Set mock responses + self.loadMockResponses(["OAuth2_AuthorizeRedirect_Success", + "OAuth2_Token_Success"]) + + // Get AccessToken with newly grnated Session Token for next test + var ex = self.expectation(description: "Get Access Token") + FRUser.currentUser?.getAccessToken() { (user, error) in + XCTAssertNil(error) + XCTAssertNotNil(user) + ex.fulfill() + } + waitForExpectations(timeout: 60, handler: nil) + + XCTAssertNotNil(FRUser.currentUser?.token) + let currentAccessToken = FRUser.currentUser?.token + // Simulate Centralized Login state by removing SSO Token from storage and the Token Object + // Deletes SSO token from Keychain Service + let keychainManager = FRAuth.shared?.keychainManager + keychainManager?.setSSOToken(ssoToken: nil) + let newAccessToken = AccessToken(token: currentAccessToken?.value, expiresIn: currentAccessToken?.expiresIn, scope: currentAccessToken?.scope, tokenType: currentAccessToken?.tokenType, refreshToken: currentAccessToken?.refreshToken, idToken: currentAccessToken?.idToken, authenticatedTimestamp: currentAccessToken?.authenticatedTimestamp.timeIntervalSince1970) + try? keychainManager?.setAccessToken(token: newAccessToken) + XCTAssertNotNil(FRSession.currentSession) + XCTAssertNil(FRSession.currentSession?.sessionToken) + + + // Set mock responses + self.loadMockResponses(["PolicyAdviceUsernameNode", + "AuthTree_SSOToken_Success"]) + + + + let policyAdvice = PolicyAdvice(type: "TransactionConditionAdvice", value: "5afff42a-2715-40c8-98e7-919abc1b2dfc") + var currentNode: Node? + + + ex = self.expectation(description: "First Node submit - again") + FRSession.authenticate(policyAdvice: policyAdvice!) { token, node, error in + // Validate result + XCTAssertNil(token) + XCTAssertNil(error) + XCTAssertNotNil(node) + currentNode = node + ex.fulfill() + } + waitForExpectations(timeout: 60, handler: nil) + XCTAssertNotNil(FRSession.currentSession) + XCTAssertNil(FRSession.currentSession?.sessionToken) + guard let node = currentNode else { + XCTFail("Failed to get Node from the first request") + return + } + + // Provide input value for callbacks + for callback in node.callbacks { + if callback is NameCallback, let nameCallback = callback as? NameCallback { + nameCallback.setValue(config.username) + } + else if callback is PasswordCallback, let passwordCallback = callback as? PasswordCallback { + passwordCallback.setValue(config.password) + } + else { + XCTFail("Received unexpected callback \(callback)") + } + } + + ex = self.expectation(description: "Second Node submit") + node.next { (token: Token?, node, error) in + // Validate result + XCTAssertNil(node) + XCTAssertNil(error) + XCTAssertNotNil(token) + ex.fulfill() + } + waitForExpectations(timeout: 60, handler: nil) + + //Check that the Session is still there + XCTAssertNotNil(FRSession.currentSession) + XCTAssertNil(FRSession.currentSession?.sessionToken) + } + + fileprivate func authenticateUser() { + self.config.authServiceName = "UsernamePassword" + + // Set mock responses + self.loadMockResponses(["AuthTree_UsernamePasswordNode", + "AuthTree_SSOToken_Success"]) + + var currentNode: Node? + + var ex = self.expectation(description: "First Node submit") + FRSession.authenticate(authIndexValue: self.config.authServiceName!) { (token: Token?, node, error) in + + // Validate result + XCTAssertNil(token) + XCTAssertNil(error) + XCTAssertNotNil(node) + currentNode = node + ex.fulfill() + } + waitForExpectations(timeout: 60, handler: nil) + + guard let node = currentNode else { + XCTFail("Failed to get Node from the first request") + return + } + + // Provide input value for callbacks + for callback in node.callbacks { + if callback is NameCallback, let nameCallback = callback as? NameCallback { + nameCallback.setValue(config.username) + } + else if callback is PasswordCallback, let passwordCallback = callback as? PasswordCallback { + passwordCallback.setValue(config.password) + } + else { + XCTFail("Received unexpected callback \(callback)") + } + } + + ex = self.expectation(description: "Second Node submit") + node.next { (token: Token?, node, error) in + // Validate result + XCTAssertNil(node) + XCTAssertNil(error) + XCTAssertNotNil(token) + ex.fulfill() + } + waitForExpectations(timeout: 60, handler: nil) + + XCTAssertNotNil(FRSession.currentSession) + XCTAssertNotNil(FRSession.currentSession?.sessionToken) + } } @@ -533,3 +742,23 @@ class SuspendedRequestInterceptor: RequestInterceptor { static var requests: [Request] = [] static var actions: [Action] = [] } + +class IDTokenForSessionInterceptor: RequestInterceptor { + func intercept(request: Request, action: Action) -> Request { + if (action.type == "START_AUTHENTICATE" || action.type == "AUTHENTICATE"), + let payload = action.payload, + let type = payload["type"] as? String, + type == "composite_advice", + let token = FRUser.currentUser?.token, + let idToken = token.idToken + { + var headers = request.headers + headers["Cookie"] = "iPlanetDirectoryPro=\(idToken)" + let newRequest = Request(url: request.url, method: request.method, headers: headers, bodyParams: request.bodyParams, urlParams: request.urlParams, requestType: request.requestType, responseType: request.responseType, timeoutInterval: request.timeoutInterval) + return newRequest + } + else { + return request + } + } +} diff --git a/FRTestHost/FRTestHost/SharedTestFiles/TestData/MockResponseData/AuthTree/PolicyAdviceUsernameNode.json b/FRTestHost/FRTestHost/SharedTestFiles/TestData/MockResponseData/AuthTree/PolicyAdviceUsernameNode.json new file mode 100644 index 00000000..b2b6c495 --- /dev/null +++ b/FRTestHost/FRTestHost/SharedTestFiles/TestData/MockResponseData/AuthTree/PolicyAdviceUsernameNode.json @@ -0,0 +1,43 @@ +{ + "responsePayload": { + "authId": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdXRoSW5kZXhWYWx1ZSI6IkV4YW1wbGUiLCJvdGsiOiJkamwzZjVsbDk4MzM3aHFwMXZvYm5qcWFyZiIsImF1dGhJbmRleFR5cGUiOiJzZXJ2aWNlIiwicmVhbG0iOiIvIiwic2Vzc2lvbklkIjoiKkFBSlRTUUFDTURFQUJIUjVjR1VBQ0VwWFZGOUJWVlJJQUFKVE1RQUEqZXlKMGVYQWlPaUpLVjFRaUxDSmpkSGtpT2lKS1YxUWlMQ0poYkdjaU9pSklVekkxTmlKOS5aWGxLTUdWWVFXbFBhVXBMVmpGUmFVeERTalpoV0VGcFQybEtUMVF3TlVaSmFYZHBXbGMxYWtscWIybFJWRVY1VDBWT1ExRjVNVWxWZWtreFRtbEpjMGx0Um5OYWVVazJTVzFTY0dOcFNqa3VMa2hNVlZnMGRtbEtjbTVWVFhoalpYZGxlRWR2U1hjdVEzRlpURlJtYzJadmIzSktZUzFsUzJaRU4zSm9lVTR0Wm1sU1dVdElNRTFyWmtkTGRrUnBUV0p1V0d0eWMwUTRSR05MWjFWSGJqSmZUbWx0YkVSUFdIQjZXVEpaWWpoR2JFUnZTMnBHV1MxcmRuRnRiRlpvWDFaWWNUTjFOM1ZqUlVkb1JXbHdPWGxNZG5oaGRuQnJlaTF4VG5aWGFFMDBORWRRWVhsVE5qaDZiM0I1U21wcFJVaENZa3ROUms0M1EzazFTbkZKUms1VGFqQXdiMkZVUjJnMk5sTkZka1JHZG5wSVRIRjBORFYyZUVKeE5FaERWVmxQVW5GVmVFNVlSRGMxWDJoMVFucEhRMGR0ZURONE0xZFdVbFpOZEdKTFJ6SlBXVmxXZFZCVVVHeDVTRVJRYkVsNWQxbFlObkJ6ZW5KdU9YZHFRa3hQYnpNemFGaE1NRXRGUVhRdFpFaGpVSEp2Wlhoa2F6azFNMFprZG1wV2N6RkhWM2RSWm5SaFZtSkxUSG80WjJVNU1FeHhVMDFUTlVWaGFEQkZVRVJGWm5FNVJVcFJka1kyZUVORFgzVllhMGs1VDFWUlNEQjJkV2c1WjNaMFdWRm1jVFExV1drNWNFbE1abXN0UTJSSGNFbHNhRlJVZDB4NWJWSkVhMUZpWlZkYWEwcDBNa2N0Y201ZlNtMWhUVlJmV0VrNVkya3lOMnRUY1ZKSGFETkxZWFp2ZVhob05VWlVSWGxhZG1VdE9FRmhORm96YnpKclEzTldUbGQwWVdGNVF5MU9XVzR5VGxKVmVGcENSMXBsY1RCUVVUVmpZbEY2Y0ZGM09GVldabTVSV2tkRFpYQkRhMWhqU1c5VU1HOURjazVaUVdaeFZFVllhM05YY2tOVVRXcFVUblU1VEdGM0xXVXlhVTA1WXpJeE1EaHBXVzFJVVVWNFh5MVdVMUpKYlZSU2FEbFFTR1IwY2tOSVdUZHdRbUZOWTJ4RFRIWnJjSGx2Y1VaMk5rUm5XblI2Y2tWaE1VaFRWWEZFUlhsS1RsVmhaV05YWm10cmFrOTFlSEpEUTI5Uk9WRllWbkpFYUVWQmJEZDBialpqTW5ScmNIUjZiRFoyYm5kc1JXSlFRMVI1ZFY5cVdpNDJkRFJ4WVZKTU1XVlJabWhLVnpsbVZtMUJUamxCLlVOdnBiaVR4OTNVYzRRQmV5WFNQckpHS0NHMWNXcXMwbWJaVGM2Y1RrN3ciLCJleHAiOjE1NjUyNDQ2MDgsImlhdCI6MTU2NTI0NDMwOH0._oubvoLcba5d4RmlZk1s55MUE2ImbMhpnoeXGO2F1zQ", + "callbacks": [{ + "type": "NameCallback", + "output": [{ + "name": "prompt", + "value": "User Name" + }], + "input": [{ + "name": "IDToken1", + "value": "" + }], + "_id": 0 + }, + { + "type": "PasswordCallback", + "output": [{ + "name": "prompt", + "value": "Password" + }], + "input": [{ + "name": "IDToken2", + "value": "" + }], + "_id": 1 + } + ], + "stage": "UsernamePassword" + }, + "response": { + "statusCode": 200, + "url": "https://localhost/json/realms/root/authenticate?authenticate?authIndexType=composite_advice&authIndexValue=5afff42a-2715-40c8-98e7-919abc1b2dfc", + "httpVersion": "HTTP/1.1", + "headerFields": { + "Content-Length": "2150", + "Content-Type": "application/json;charset=UTF-8", + "Date": "Thu, 08 Aug 2019 06:12:31 GMT", + "X-Frame-Options": "SAMEORIGIN", + "content-api-version": "resource=2.1" + } + } +}