diff --git a/Sources/AuthFoundation/OAuth2/ClientAuthentication.swift b/Sources/AuthFoundation/OAuth2/ClientAuthentication.swift index ecd69a388..cdd058cd9 100644 --- a/Sources/AuthFoundation/OAuth2/ClientAuthentication.swift +++ b/Sources/AuthFoundation/OAuth2/ClientAuthentication.swift @@ -14,14 +14,14 @@ import Foundation extension OAuth2Client { /// Defines the types of authentication the client may use when interacting with the authorization server. - public enum ClientAuthentication: Codable, Equatable, Hashable { + public enum ClientAuthentication: Codable, Equatable, Hashable, ProvidesOAuth2Parameters { /// No client authentication will be made when interacting with the authorization server. case none /// A client secret will be supplied when interacting with the authorization server. case clientSecret(String) - /// The additional parameters this authentication type will contribute to outgoing API requests when needed. + @_documentation(visibility: private) public var additionalParameters: [String: APIRequestArgument]? { switch self { case .none: diff --git a/Sources/AuthFoundation/OAuth2/Internal/ProvidesOAuth2Parameters+Extensions.swift b/Sources/AuthFoundation/OAuth2/Internal/ProvidesOAuth2Parameters+Extensions.swift new file mode 100644 index 000000000..30d5967ae --- /dev/null +++ b/Sources/AuthFoundation/OAuth2/Internal/ProvidesOAuth2Parameters+Extensions.swift @@ -0,0 +1,47 @@ +// +// Copyright (c) 2024-Present, Okta, Inc. and/or its affiliates. All rights reserved. +// The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.") +// +// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and limitations under the License. +// + +import Foundation + +extension ProvidesOAuth2Parameters { + @_documentation(visibility: private) + public var shouldOverride: Bool { true } +} + +extension Dictionary { + @_documentation(visibility: private) + @inlinable + public mutating func merge(_ oauth2Parameters: ProvidesOAuth2Parameters?) { + guard let oauth2Parameters = oauth2Parameters, + let additionalParameters = oauth2Parameters.additionalParameters + else { + return + } + + merge(additionalParameters) { oauth2Parameters.shouldOverride ? $1 : $0 } + } + + @_documentation(visibility: private) + @inlinable + public func merging(_ oauth2Parameters: ProvidesOAuth2Parameters?) -> [Key: Value] { + var result = self + result.merge(oauth2Parameters) + return result + } +} + +extension Dictionary: ProvidesOAuth2Parameters { + @_documentation(visibility: private) + public var additionalParameters: [String: any APIRequestArgument]? { + self + } +} diff --git a/Sources/AuthFoundation/OAuth2/OAuth2ClientConfiguration.swift b/Sources/AuthFoundation/OAuth2/OAuth2ClientConfiguration.swift index 34ef7e2f3..8439cec31 100644 --- a/Sources/AuthFoundation/OAuth2/OAuth2ClientConfiguration.swift +++ b/Sources/AuthFoundation/OAuth2/OAuth2ClientConfiguration.swift @@ -24,7 +24,7 @@ extension OAuth2Client { /// Utility struct used internally to process `Okta.plist` and other similar client configuration files. /// /// > Important: This struct is intended for internal use, and may be subject to change. - public struct PropertyListConfiguration { + public struct PropertyListConfiguration: ProvidesOAuth2Parameters { /// The client issuer URL, defined in the "issuer" key. public let issuer: URL @@ -41,7 +41,7 @@ extension OAuth2Client { public let logoutRedirectUri: URL? /// Additional parameters defined by the developer within the property list. - public let additionalParameters: [String: String]? + public let additionalParameters: [String: APIRequestArgument]? /// Default initializer that reads the `Okta.plist` file from the application's main bundle. public init() throws { diff --git a/Sources/AuthFoundation/OAuth2/ProvidesOAuth2Parameters.swift b/Sources/AuthFoundation/OAuth2/ProvidesOAuth2Parameters.swift new file mode 100644 index 000000000..b885e91d3 --- /dev/null +++ b/Sources/AuthFoundation/OAuth2/ProvidesOAuth2Parameters.swift @@ -0,0 +1,22 @@ +// +// Copyright (c) 2024-Present, Okta, Inc. and/or its affiliates. All rights reserved. +// The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.") +// +// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and limitations under the License. +// + +import Foundation + +/// Used by types that are capable of providing parameters to OAuth2 API requests. +public protocol ProvidesOAuth2Parameters { + /// The additional parameters this authentication type will contribute to outgoing API requests when needed. + var additionalParameters: [String: APIRequestArgument]? { get } + + /// Indicates if the parameters included in the result should override those previously declared. + var shouldOverride: Bool { get } +} diff --git a/Sources/AuthFoundation/Requests/Token+Requests.swift b/Sources/AuthFoundation/Requests/Token+Requests.swift index b61f12de9..265f40041 100644 --- a/Sources/AuthFoundation/Requests/Token+Requests.swift +++ b/Sources/AuthFoundation/Requests/Token+Requests.swift @@ -98,9 +98,7 @@ extension Token.RevokeRequest: OAuth2APIRequest, APIRequestBody { result["token_type_hint"] = hint } - if let parameters = clientAuthentication.additionalParameters { - result.merge(parameters, uniquingKeysWith: { $1 }) - } + result.merge(clientAuthentication) return result } @@ -120,9 +118,7 @@ extension Token.IntrospectRequest: OAuth2APIRequest, APIRequestBody { "token_type_hint": type ] - if let parameters = clientConfiguration.authentication.additionalParameters { - result.merge(parameters, uniquingKeysWith: { $1 }) - } + result.merge(clientConfiguration.authentication) return result } @@ -141,9 +137,7 @@ extension Token.RefreshRequest: OAuth2APIRequest, APIRequestBody, APIParsingCont result["grant_type"] = "refresh_token" result["refresh_token"] = refreshToken - if let parameters = clientConfiguration.authentication.additionalParameters { - result.merge(parameters, uniquingKeysWith: { $1 }) - } + result.merge(clientConfiguration.authentication) return result } diff --git a/Sources/OktaDirectAuth/DirectAuthFlow.swift b/Sources/OktaDirectAuth/DirectAuthFlow.swift index 16c991ec6..4f4e0660d 100644 --- a/Sources/OktaDirectAuth/DirectAuthFlow.swift +++ b/Sources/OktaDirectAuth/DirectAuthFlow.swift @@ -244,12 +244,26 @@ public class DirectAuthenticationFlow: AuthenticationFlow { } } + /// Indicates the intent for the user authentication operation. + /// + /// This value is used to toggle behavior to distinguish between sign-in authentication, password recovery / reset operations, etc. + public enum Intent: String, Codable { + /// The user intends to sign in. + case signIn + + /// The user intends to recover / reset their password, or some other authentication factor. + case recovery + } + /// The OAuth2Client this authentication flow will use. public let client: OAuth2Client /// The list of grant types the application supports. public let supportedGrantTypes: [GrantType] + /// The intent of the current flow. + public private(set) var intent: Intent = .signIn + /// Indicates whether or not this flow is currently in the process of authenticating a user. public private(set) var isAuthenticating: Bool = false { didSet { @@ -311,7 +325,7 @@ public class DirectAuthenticationFlow: AuthenticationFlow { private convenience init(_ config: OAuth2Client.PropertyListConfiguration) throws { let supportedGrantTypes: [GrantType] - if let supportedGrants = config.additionalParameters?["supportedGrants"] { + if let supportedGrants = config.additionalParameters?["supportedGrants"] as? String { supportedGrantTypes = try .from(string: supportedGrants) } else { supportedGrantTypes = .directAuth @@ -329,12 +343,15 @@ public class DirectAuthenticationFlow: AuthenticationFlow { /// - Parameters: /// - loginHint: The login hint, or username, to authenticate. /// - factor: The primary factor to use when authenticating the user. + /// - intent: The intent behind this authentication (default: `signIn`) /// - completion: Completion block called when the operation completes. public func start(_ loginHint: String, with factor: PrimaryFactor, + intent: Intent = .signIn, completion: @escaping (Result) -> Void) { reset() + self.intent = intent runStep(loginHint: loginHint, with: factor, completion: completion) } @@ -404,6 +421,7 @@ public class DirectAuthenticationFlow: AuthenticationFlow { /// Resets the authentication session. public func reset() { isAuthenticating = false + intent = .signIn } // MARK: Private properties / methods @@ -416,10 +434,14 @@ extension DirectAuthenticationFlow { /// - Parameters: /// - loginHint: The login hint, or username, to authenticate. /// - factor: The primary factor to use when authenticating the user. + /// - intent: The intent behind this authentication (default: `signIn`) /// - Returns: Status returned when the operation completes. - public func start(_ loginHint: String, with factor: PrimaryFactor) async throws -> DirectAuthenticationFlow.Status { + public func start(_ loginHint: String, + with factor: PrimaryFactor, + intent: DirectAuthenticationFlow.Intent = .signIn) async throws -> DirectAuthenticationFlow.Status + { try await withCheckedThrowingContinuation { continuation in - start(loginHint, with: factor) { result in + start(loginHint, with: factor, intent: intent) { result in continuation.resume(with: result) } } diff --git a/Sources/OktaDirectAuth/Internal/Authentication Factors/ContinuationFactor.swift b/Sources/OktaDirectAuth/Internal/Authentication Factors/ContinuationFactor.swift index 343a1e2f4..e2cb24b8f 100644 --- a/Sources/OktaDirectAuth/Internal/Authentication Factors/ContinuationFactor.swift +++ b/Sources/OktaDirectAuth/Internal/Authentication Factors/ContinuationFactor.swift @@ -46,6 +46,7 @@ extension DirectAuthenticationFlow.ContinuationFactor: AuthenticationFactor { clientConfiguration: flow.client.configuration, currentStatus: currentStatus, factor: factor, + intent: flow.intent, parameters: bindingContext.oobResponse, grantTypesSupported: flow.supportedGrantTypes) return TokenStepHandler(flow: flow, request: request) @@ -56,6 +57,7 @@ extension DirectAuthenticationFlow.ContinuationFactor: AuthenticationFactor { currentStatus: currentStatus, loginHint: loginHint, factor: factor, + intent: flow.intent, parameters: response, grantTypesSupported: flow.supportedGrantTypes) return TokenStepHandler(flow: flow, request: request) diff --git a/Sources/OktaDirectAuth/Internal/Authentication Factors/PrimaryFactor.swift b/Sources/OktaDirectAuth/Internal/Authentication Factors/PrimaryFactor.swift index bf101af0e..b2ecafdc9 100644 --- a/Sources/OktaDirectAuth/Internal/Authentication Factors/PrimaryFactor.swift +++ b/Sources/OktaDirectAuth/Internal/Authentication Factors/PrimaryFactor.swift @@ -39,6 +39,7 @@ extension DirectAuthenticationFlow.PrimaryFactor: AuthenticationFactor { currentStatus: currentStatus, loginHint: loginHint, factor: factor, + intent: flow.intent, grantTypesSupported: flow.supportedGrantTypes) return TokenStepHandler(flow: flow, request: request) case .oob(channel: let channel): diff --git a/Sources/OktaDirectAuth/Internal/Authentication Factors/SecondaryFactor.swift b/Sources/OktaDirectAuth/Internal/Authentication Factors/SecondaryFactor.swift index 49dfd93a8..4e81b8505 100644 --- a/Sources/OktaDirectAuth/Internal/Authentication Factors/SecondaryFactor.swift +++ b/Sources/OktaDirectAuth/Internal/Authentication Factors/SecondaryFactor.swift @@ -27,6 +27,7 @@ extension DirectAuthenticationFlow.SecondaryFactor: AuthenticationFactor { currentStatus: currentStatus, loginHint: loginHint, factor: factor, + intent: flow.intent, grantTypesSupported: flow.supportedGrantTypes) return TokenStepHandler(flow: flow, request: request) case .oob(channel: let channel): diff --git a/Sources/OktaDirectAuth/Internal/Extensions/Intent+Extensions.swift b/Sources/OktaDirectAuth/Internal/Extensions/Intent+Extensions.swift new file mode 100644 index 000000000..df4e215f3 --- /dev/null +++ b/Sources/OktaDirectAuth/Internal/Extensions/Intent+Extensions.swift @@ -0,0 +1,33 @@ +// +// Copyright (c) 2024-Present, Okta, Inc. and/or its affiliates. All rights reserved. +// The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.") +// +// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and limitations under the License. +// + +import Foundation + +extension DirectAuthenticationFlow.Intent: ProvidesOAuth2Parameters { + @_documentation(visibility: private) + public var overrideParameters: Bool { + true + } + + @_documentation(visibility: private) + public var additionalParameters: [String: any AuthFoundation.APIRequestArgument]? { + switch self { + case .signIn: + return nil + case .recovery: + return [ + "scope": "okta.myAccount.password.manage", + "intent": "recovery", + ] + } + } +} diff --git a/Sources/OktaDirectAuth/Internal/Requests/ChallengeRequest.swift b/Sources/OktaDirectAuth/Internal/Requests/ChallengeRequest.swift index 01c5b5f2c..5f044230b 100644 --- a/Sources/OktaDirectAuth/Internal/Requests/ChallengeRequest.swift +++ b/Sources/OktaDirectAuth/Internal/Requests/ChallengeRequest.swift @@ -84,9 +84,7 @@ extension ChallengeRequest: APIRequest, APIRequestBody { .joined(separator: " ") ] - if let parameters = clientConfiguration.authentication.additionalParameters { - result.merge(parameters, uniquingKeysWith: { $1 }) - } + result.merge(clientConfiguration.authentication) return result } diff --git a/Sources/OktaDirectAuth/Internal/Requests/OOBAuthenticateRequest.swift b/Sources/OktaDirectAuth/Internal/Requests/OOBAuthenticateRequest.swift index 5e9fb9708..5ef864847 100644 --- a/Sources/OktaDirectAuth/Internal/Requests/OOBAuthenticateRequest.swift +++ b/Sources/OktaDirectAuth/Internal/Requests/OOBAuthenticateRequest.swift @@ -86,9 +86,7 @@ extension OOBAuthenticateRequest: APIRequest, APIRequestBody { "challenge_hint": challengeHint, ] - if let parameters = clientConfiguration.authentication.additionalParameters { - result.merge(parameters, uniquingKeysWith: { $1 }) - } + result.merge(clientConfiguration.authentication) return result } diff --git a/Sources/OktaDirectAuth/Internal/Requests/TokenRequest.swift b/Sources/OktaDirectAuth/Internal/Requests/TokenRequest.swift index 575764727..49e07ff78 100644 --- a/Sources/OktaDirectAuth/Internal/Requests/TokenRequest.swift +++ b/Sources/OktaDirectAuth/Internal/Requests/TokenRequest.swift @@ -19,6 +19,7 @@ struct TokenRequest { let currentStatus: DirectAuthenticationFlow.Status? let loginHint: String? let factor: any AuthenticationFactor + let intent: DirectAuthenticationFlow.Intent let parameters: (any HasTokenParameters)? let grantTypesSupported: [GrantType]? @@ -27,6 +28,7 @@ struct TokenRequest { currentStatus: DirectAuthenticationFlow.Status?, loginHint: String? = nil, factor: any AuthenticationFactor, + intent: DirectAuthenticationFlow.Intent, parameters: (any HasTokenParameters)? = nil, grantTypesSupported: [GrantType]? = nil) { @@ -35,6 +37,7 @@ struct TokenRequest { self.currentStatus = currentStatus self.loginHint = loginHint self.factor = factor + self.intent = intent self.parameters = parameters self.grantTypesSupported = grantTypesSupported } @@ -47,9 +50,7 @@ extension TokenRequest: OAuth2TokenRequest, OAuth2APIRequest, APIRequestBody { result["client_id"] = clientConfiguration.clientId result["scope"] = clientConfiguration.scopes - if let tokenParameters = parameters?.tokenParameters(currentStatus: currentStatus) { - result.merge(tokenParameters, uniquingKeysWith: { $1 }) - } + result.merge(parameters?.tokenParameters(currentStatus: currentStatus)) if let loginHint = loginHint { let key: String @@ -65,9 +66,8 @@ extension TokenRequest: OAuth2TokenRequest, OAuth2APIRequest, APIRequestBody { result["grant_types_supported"] = grantTypesSupported.joined(separator: " ") } - if let parameters = clientConfiguration.authentication.additionalParameters { - result.merge(parameters, uniquingKeysWith: { $1 }) - } + result.merge(clientConfiguration.authentication) + result.merge(intent) return result } diff --git a/Sources/OktaDirectAuth/Internal/Requests/WebAuthnRequest.swift b/Sources/OktaDirectAuth/Internal/Requests/WebAuthnRequest.swift index ab57f501c..08475bc6b 100644 --- a/Sources/OktaDirectAuth/Internal/Requests/WebAuthnRequest.swift +++ b/Sources/OktaDirectAuth/Internal/Requests/WebAuthnRequest.swift @@ -55,9 +55,7 @@ extension WebAuthnChallengeRequest: APIRequest, APIRequestBody { result["mfa_token"] = mfaToken } - if let parameters = clientConfiguration.authentication.additionalParameters { - result.merge(parameters, uniquingKeysWith: { $1 }) - } + result.merge(clientConfiguration.authentication) return result } diff --git a/Sources/OktaDirectAuth/Internal/Step Handlers/OOBStepHandler.swift b/Sources/OktaDirectAuth/Internal/Step Handlers/OOBStepHandler.swift index 233e8c050..c540852dc 100644 --- a/Sources/OktaDirectAuth/Internal/Step Handlers/OOBStepHandler.swift +++ b/Sources/OktaDirectAuth/Internal/Step Handlers/OOBStepHandler.swift @@ -152,6 +152,7 @@ class OOBStepHandler: StepHandler { clientConfiguration: flow.client.configuration, currentStatus: currentStatus, factor: factor, + intent: flow.intent, parameters: response, grantTypesSupported: flow.supportedGrantTypes) self.poll = PollingHandler(client: flow.client, diff --git a/Sources/OktaOAuth2/Authentication/AuthorizationCodeFlow.swift b/Sources/OktaOAuth2/Authentication/AuthorizationCodeFlow.swift index cf15eb7cc..45f04d9e3 100644 --- a/Sources/OktaOAuth2/Authentication/AuthorizationCodeFlow.swift +++ b/Sources/OktaOAuth2/Authentication/AuthorizationCodeFlow.swift @@ -65,7 +65,7 @@ public protocol AuthorizationCodeFlowDelegate: AuthenticationDelegate { /// let redirectUri: URL /// let token = try await flow.resume(with: redirectUri) /// ``` -public class AuthorizationCodeFlow: AuthenticationFlow { +public class AuthorizationCodeFlow: AuthenticationFlow, ProvidesOAuth2Parameters { /// A model representing the context and current state for an authorization session. public struct Context: Equatable { /// The `PKCE` credentials to use in the authorization request. @@ -122,7 +122,7 @@ public class AuthorizationCodeFlow: AuthenticationFlow { public let redirectUri: URL /// Any additional query string parameters you would like to supply to the authorization server. - public let additionalParameters: [String: String]? + public let additionalParameters: [String: APIRequestArgument]? /// Indicates whether or not this flow is currently in the process of authenticating a user. public private(set) var isAuthenticating: Bool = false { @@ -161,7 +161,7 @@ public class AuthorizationCodeFlow: AuthenticationFlow { clientId: String, scopes: String, redirectUri: URL, - additionalParameters: [String: String]? = nil) + additionalParameters: [String: APIRequestArgument]? = nil) { self.init(redirectUri: redirectUri, additionalParameters: additionalParameters, @@ -176,7 +176,7 @@ public class AuthorizationCodeFlow: AuthenticationFlow { /// - additionalParameters: Optional additional query string parameters you would like to supply to the authorization server. /// - client: The `OAuth2Client` to use with this flow. public init(redirectUri: URL, - additionalParameters: [String: String]? = nil, + additionalParameters: [String: APIRequestArgument]? = nil, client: OAuth2Client) { // Ensure this SDK's static version is included in the user agent. diff --git a/Sources/OktaOAuth2/Authentication/SessionTokenFlow.swift b/Sources/OktaOAuth2/Authentication/SessionTokenFlow.swift index 4edab0680..f01cbcd4f 100644 --- a/Sources/OktaOAuth2/Authentication/SessionTokenFlow.swift +++ b/Sources/OktaOAuth2/Authentication/SessionTokenFlow.swift @@ -20,7 +20,7 @@ import FoundationNetworking /// An authentication flow class that exchanges a Session Token for access tokens. /// /// This flow is typically used in conjunction with the [classic Okta native authentication library](https://github.com/okta/okta-auth-swift). For native authentication using the Okta Identity Engine (OIE), please use the [Okta IDX library](https://github.com/okta/okta-idx-swift). -public class SessionTokenFlow: AuthenticationFlow { +public class SessionTokenFlow: AuthenticationFlow, ProvidesOAuth2Parameters { /// The OAuth2Client this authentication flow will use. public let client: OAuth2Client @@ -28,7 +28,7 @@ public class SessionTokenFlow: AuthenticationFlow { public let redirectUri: URL /// Any additional query string parameters you would like to supply to the authorization server. - public let additionalParameters: [String: String]? + public let additionalParameters: [String: APIRequestArgument]? /// Indicates whether or not this flow is currently in the process of authenticating a user. public private(set) var isAuthenticating: Bool = false { @@ -54,7 +54,7 @@ public class SessionTokenFlow: AuthenticationFlow { clientId: String, scopes: String, redirectUri: URL, - additionalParameters: [String: String]? = nil) + additionalParameters: [String: APIRequestArgument]? = nil) { self.init(redirectUri: redirectUri, additionalParameters: additionalParameters, @@ -64,7 +64,7 @@ public class SessionTokenFlow: AuthenticationFlow { } public init(redirectUri: URL, - additionalParameters: [String: String]? = nil, + additionalParameters: [String: APIRequestArgument]? = nil, client: OAuth2Client) { // Ensure this SDK's static version is included in the user agent. diff --git a/Sources/OktaOAuth2/Internal/Requests/AuthorizationCodeFlow+Extensions.swift b/Sources/OktaOAuth2/Internal/Requests/AuthorizationCodeFlow+Extensions.swift index 28c0d1c17..0eba2562b 100644 --- a/Sources/OktaOAuth2/Internal/Requests/AuthorizationCodeFlow+Extensions.swift +++ b/Sources/OktaOAuth2/Internal/Requests/AuthorizationCodeFlow+Extensions.swift @@ -16,7 +16,7 @@ import AuthFoundation extension AuthorizationCodeFlow { func authenticationUrlComponents(from authenticationUrl: URL, using context: AuthorizationCodeFlow.Context, - additionalParameters: [String: String]?) throws -> URLComponents + additionalParameters: [String: APIRequestArgument]?) throws -> URLComponents { guard var components = URLComponents(url: authenticationUrl, resolvingAgainstBaseURL: true) else { @@ -30,16 +30,10 @@ extension AuthorizationCodeFlow { } private func queryParameters(using context: AuthorizationCodeFlow.Context, - additionalParameters: [String: String]?) -> [String: String] + additionalParameters: [String: APIRequestArgument]?) -> [String: String] { - var parameters = [String: String]() - if let additional = self.additionalParameters { - parameters.merge(additional, uniquingKeysWith: { $1 }) - } - - if let additional = additionalParameters { - parameters.merge(additional, uniquingKeysWith: { $1 }) - } + var parameters = self.additionalParameters ?? [:] + parameters.merge(additionalParameters) parameters["client_id"] = client.configuration.clientId parameters["scope"] = client.configuration.scopes @@ -53,7 +47,7 @@ extension AuthorizationCodeFlow { parameters["code_challenge_method"] = pkce.method.rawValue } - return parameters + return parameters.mapValues(\.stringValue) } func createAuthenticationURL(from authenticationUrl: URL, diff --git a/Sources/OktaOAuth2/Logout/SessionLogoutFlow.swift b/Sources/OktaOAuth2/Logout/SessionLogoutFlow.swift index f8c038c03..945fc7777 100644 --- a/Sources/OktaOAuth2/Logout/SessionLogoutFlow.swift +++ b/Sources/OktaOAuth2/Logout/SessionLogoutFlow.swift @@ -54,7 +54,7 @@ public protocol SessionLogoutFlowDelegate: LogoutFlowDelegate { /// // Create the logout URL. Open this in a browser. /// let authorizeUrl = try await flow.start() /// ``` -public class SessionLogoutFlow: LogoutFlow { +public class SessionLogoutFlow: LogoutFlow, ProvidesOAuth2Parameters { /// A model representing the context and current state for a logout session. public struct Context: Codable, Equatable { /// The ID token string used for log-out. @@ -83,7 +83,7 @@ public class SessionLogoutFlow: LogoutFlow { public let logoutRedirectUri: URL /// Any additional query string parameters you would like to supply to the authorization server. - public let additionalParameters: [String: String]? + public let additionalParameters: [String: APIRequestArgument]? /// Indicates if this flow is currently in progress. public private(set) var inProgress: Bool = false @@ -127,7 +127,7 @@ public class SessionLogoutFlow: LogoutFlow { /// - logoutRedirectUri: The logout redirect URI. /// - client: The `OAuth2Client` to use with this flow. public init(logoutRedirectUri: URL, - additionalParameters: [String: String]? = nil, + additionalParameters: [String: APIRequestArgument]? = nil, client: OAuth2Client) { // Ensure this SDK's static version is included in the user agent. @@ -281,28 +281,23 @@ private extension SessionLogoutFlow { } func queryParameters(using context: SessionLogoutFlow.Context, - additionalParameters: [String: String]?) -> [String: String] { - var result = [String: String]() - if let additional = self.additionalParameters { - result.merge(additional, uniquingKeysWith: { $1 }) - } - - if let additional = additionalParameters { - result.merge(additional, uniquingKeysWith: { $1 }) - } + additionalParameters: [String: APIRequestArgument]?) -> [String: String] + { + var result = self.additionalParameters ?? [:] + result.merge(additionalParameters) result["id_token_hint"] = context.idToken result["post_logout_redirect_uri"] = logoutRedirectUri.absoluteString result["state"] = context.state // If requesting a login prompt, the post_logout_redirect_uri should be omitted. - if let prompt = additionalParameters?["prompt"]?.lowercased(), - ["login", "consent", "login consent", "consent login"].contains(prompt) + if let prompt = additionalParameters?["prompt"] as? String, + ["login", "consent", "login consent", "consent login"].contains(prompt.lowercased()) { result.removeValue(forKey: "post_logout_redirect_uri") } - return result + return result.mapValues(\.stringValue) } func createLogoutURL(from url: URL, diff --git a/Sources/WebAuthenticationUI/WebAuthentication.swift b/Sources/WebAuthenticationUI/WebAuthentication.swift index 61d114316..564c75bdb 100644 --- a/Sources/WebAuthenticationUI/WebAuthentication.swift +++ b/Sources/WebAuthenticationUI/WebAuthentication.swift @@ -306,7 +306,7 @@ public class WebAuthentication { scopes: String, redirectUri: URL, logoutRedirectUri: URL? = nil, - additionalParameters: [String: String]? = nil) + additionalParameters: [String: APIRequestArgument]? = nil) { let client = OAuth2Client(baseURL: issuer, clientId: clientId, diff --git a/Tests/OktaDirectAuthTests/RequestTests.swift b/Tests/OktaDirectAuthTests/RequestTests.swift index 6987368de..3e733685d 100644 --- a/Tests/OktaDirectAuthTests/RequestTests.swift +++ b/Tests/OktaDirectAuthTests/RequestTests.swift @@ -27,13 +27,14 @@ final class RequestTests: XCTestCase { func testTokenRequestParameters() throws { var request: TokenRequest - // No authentication + // No authentication, sign-in intent request = .init(openIdConfiguration: openIdConfiguration, clientConfiguration: try .init(domain: "example.com", clientId: "theClientId", scopes: "openid profile"), currentStatus: nil, - factor: DirectAuthenticationFlow.PrimaryFactor.password("password123")) + factor: DirectAuthenticationFlow.PrimaryFactor.password("password123"), + intent: .signIn) XCTAssertEqual(request.bodyParameters?.stringComponents, [ "client_id": "theClientId", @@ -42,14 +43,15 @@ final class RequestTests: XCTestCase { "password": "password123" ]) - // Client Secret authentication + // Client Secret authentication, sign-in intent request = .init(openIdConfiguration: openIdConfiguration, clientConfiguration: try .init(domain: "example.com", clientId: "theClientId", scopes: "openid profile", authentication: .clientSecret("supersecret")), currentStatus: nil, - factor: DirectAuthenticationFlow.PrimaryFactor.password("password123")) + factor: DirectAuthenticationFlow.PrimaryFactor.password("password123"), + intent: .signIn) XCTAssertEqual(request.bodyParameters?.stringComponents, [ "client_id": "theClientId", @@ -58,6 +60,23 @@ final class RequestTests: XCTestCase { "grant_type": "password", "password": "password123" ]) + + // No authentication, recovery intent + request = .init(openIdConfiguration: openIdConfiguration, + clientConfiguration: try .init(domain: "example.com", + clientId: "theClientId", + scopes: "openid profile"), + currentStatus: nil, + factor: DirectAuthenticationFlow.PrimaryFactor.otp(code: "123456"), + intent: .recovery) + XCTAssertEqual(request.bodyParameters?.stringComponents, + [ + "client_id": "theClientId", + "scope": "okta.myAccount.password.manage", + "grant_type": "urn:okta:params:oauth:grant-type:otp", + "intent": "recovery", + "otp": "123456" + ]) } func testOOBAuthenticateRequestParameters() throws { diff --git a/Tests/WebAuthenticationUITests/WebAuthenticationInitializerTests.swift b/Tests/WebAuthenticationUITests/WebAuthenticationInitializerTests.swift index 31accba82..afe521f89 100644 --- a/Tests/WebAuthenticationUITests/WebAuthenticationInitializerTests.swift +++ b/Tests/WebAuthenticationUITests/WebAuthenticationInitializerTests.swift @@ -33,8 +33,8 @@ class WebAuthenticationInitializerTests: XCTestCase { XCTAssertEqual(auth.signInFlow.client.configuration.clientId, "client_id") XCTAssertEqual(auth.signInFlow.client.configuration.scopes, "openid profile") XCTAssertTrue(auth.signInFlow.client === auth.signOutFlow?.client) - XCTAssertEqual(auth.signInFlow.additionalParameters, ["foo": "bar"]) - XCTAssertEqual(auth.signOutFlow?.additionalParameters, ["foo": "bar"]) + XCTAssertEqual(auth.signInFlow.additionalParameters?.stringComponents, ["foo": "bar"]) + XCTAssertEqual(auth.signOutFlow?.additionalParameters?.stringComponents, ["foo": "bar"]) } }