Skip to content

Commit

Permalink
Introduce a sign-in "intent" to enable SSPR (#209)
Browse files Browse the repository at this point in the history
  • Loading branch information
mikenachbaur-okta authored Sep 9, 2024
1 parent 34ca926 commit 3ca3e3b
Show file tree
Hide file tree
Showing 22 changed files with 197 additions and 72 deletions.
4 changes: 2 additions & 2 deletions Sources/AuthFoundation/OAuth2/ClientAuthentication.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String, APIRequestArgument> {
@_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<String, APIRequestArgument>: ProvidesOAuth2Parameters {
@_documentation(visibility: private)
public var additionalParameters: [String: any APIRequestArgument]? {
self
}
}
4 changes: 2 additions & 2 deletions Sources/AuthFoundation/OAuth2/OAuth2ClientConfiguration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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 {
Expand Down
22 changes: 22 additions & 0 deletions Sources/AuthFoundation/OAuth2/ProvidesOAuth2Parameters.swift
Original file line number Diff line number Diff line change
@@ -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 }
}
12 changes: 3 additions & 9 deletions Sources/AuthFoundation/Requests/Token+Requests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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
}
Expand All @@ -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
}
Expand Down
28 changes: 25 additions & 3 deletions Sources/OktaDirectAuth/DirectAuthFlow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand All @@ -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<Status, DirectAuthenticationFlowError>) -> Void)
{
reset()
self.intent = intent
runStep(loginHint: loginHint, with: factor, completion: completion)
}

Expand Down Expand Up @@ -404,6 +421,7 @@ public class DirectAuthenticationFlow: AuthenticationFlow {
/// Resets the authentication session.
public func reset() {
isAuthenticating = false
intent = .signIn
}

// MARK: Private properties / methods
Expand All @@ -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)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
33 changes: 33 additions & 0 deletions Sources/OktaDirectAuth/Internal/Extensions/Intent+Extensions.swift
Original file line number Diff line number Diff line change
@@ -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",
]
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
12 changes: 6 additions & 6 deletions Sources/OktaDirectAuth/Internal/Requests/TokenRequest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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]?

Expand All @@ -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)
{
Expand All @@ -35,6 +37,7 @@ struct TokenRequest {
self.currentStatus = currentStatus
self.loginHint = loginHint
self.factor = factor
self.intent = intent
self.parameters = parameters
self.grantTypesSupported = grantTypesSupported
}
Expand All @@ -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
Expand All @@ -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
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ class OOBStepHandler<Factor: AuthenticationFactor>: StepHandler {
clientConfiguration: flow.client.configuration,
currentStatus: currentStatus,
factor: factor,
intent: flow.intent,
parameters: response,
grantTypesSupported: flow.supportedGrantTypes)
self.poll = PollingHandler(client: flow.client,
Expand Down
Loading

0 comments on commit 3ca3e3b

Please sign in to comment.