Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Accept ACR Values to authentication flows, Part 1 #214

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 90 additions & 30 deletions Samples/DirectAuthSignIn/DirectAuthSignIn/ContinuationView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
//

import SwiftUI
import OktaDirectAuth
@testable import OktaDirectAuth

extension SignInView {
struct ContinuationView: View {
Expand All @@ -30,40 +30,41 @@
}
}

var continuationType: DirectAuthenticationFlow.ContinuationType? {
guard case let .continuation(continuationType) = status else {
return nil
}

return continuationType
}

@Binding var error: Error?
@Binding var hasError: Bool

var body: some View {
VStack {
Text("Please continue authenticating.")
.padding(25)

VStack(alignment: .leading, spacing: 1) {
Picker(selection: $selectedFactor, label: EmptyView()) {
ForEach(SignInView.Factor.continuationFactors, id: \.self) {
Text($0.title)
}
}.pickerStyle(.menu)
.accessibilityIdentifier("factor_type_button")
.padding(.horizontal, -10)
.padding(.vertical, -4)

if selectedFactor == .code {
TextField("123456", text: $verificationCode)
.textContentType(.oneTimeCode)
.accessibilityIdentifier("verification_code_button")
.padding(10)
.overlay {
RoundedRectangle(cornerRadius: 6)
.stroke(.secondary, lineWidth: 1)
}
switch continuationType {
case .webAuthn(_):
Text("Ignoring WebAuthn type")
.padding()
case .transfer(_, let code):
VStack(alignment: .center, spacing: 8) {
Text("Use the verification code")
.font(.headline)
.multilineTextAlignment(.center)
if #available(iOS 16.0, *) {
Text(code)
.font(.system(.largeTitle, design: .monospaced, weight: .black))
.multilineTextAlignment(.center)
} else {
Text(code)
.font(.largeTitle)
.multilineTextAlignment(.center)
}

if let factor = factor {
Button("Continue") {
ProgressView()
.onAppear {
Task {
do {
status = try await flow.resume(status, with: factor)
status = try await flow.resume(status, with: .transfer)
if case let .success(token) = status {
Credential.default = try Credential.store(token)
}
Expand All @@ -73,12 +74,71 @@
}
}
}
.accessibilityIdentifier("signin_button")
}
case .prompt(_):
VStack(alignment: .leading, spacing: 8) {
Text("Verification code:")
.font(.headline)
.buttonStyle(.borderedProminent)
.multilineTextAlignment(.center)
TextField("123456", text: $verificationCode)
.textContentType(.oneTimeCode)
.accessibilityIdentifier("verification_code_button")
.padding(10)
.overlay {
RoundedRectangle(cornerRadius: 6)
.stroke(.secondary, lineWidth: 1)
}

Button("Continue") {
Task {
do {
status = try await flow.resume(status, with: .prompt(code: verificationCode))
if case let .success(token) = status {
Credential.default = try Credential.store(token)
}
} catch {
self.error = error
self.hasError = true
}
}
}
.accessibilityIdentifier("signin_button")
.font(.headline)
.buttonStyle(.borderedProminent)
}.padding()
case nil:
Text("Invalid status type")
.padding()
}
}
}
}

extension DirectAuthenticationFlow.ContinuationType {
static let previewTransfer: Self = .transfer(
.init(oobResponse: .init(oobCode: "OOBCODE", expiresIn: 600, interval: 10, channel: .push, bindingMethod: .transfer), mfaContext: nil),
code: "73")
static let previewPrompt: Self = .prompt(.init(oobResponse: .init(oobCode: "OOBCODE", expiresIn: 600, interval: 10, channel: .push, bindingMethod: .transfer), mfaContext: nil))
}

#Preview {
struct Preview: View {
var flow = DirectAuthenticationFlow(
issuer: URL(string: "https://example.com/")!,

Check warning on line 127 in Samples/DirectAuthSignIn/DirectAuthSignIn/ContinuationView.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Force unwrapping should be avoided (force_unwrapping)
clientId: "clientid",
scopes: "scopes")
@State var error: Error?
@State var hasError: Bool = false
@State var continuationType: DirectAuthenticationFlow.ContinuationType = .previewPrompt

var body: some View {
SignInView.ContinuationView(
flow: flow,
status: .continuation(continuationType),
error: $error,
hasError: $hasError)
}
}

return Preview()
}
96 changes: 96 additions & 0 deletions Sources/AuthFoundation/Debugging/APIRequestObserver.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
//
// 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
import OSLog

// **Note:** It would be preferable to use `Logger` for this, but this would mean setting the minimum OS version to iOS 14.
//
// Since this is a debug feature, It isn't worthwhile to update the minimum supported OS version for just this.
//
// If the minimum supported version of this SDK is to increase in the future, this class should be updated to use the modern Logger struct.

/// Convenience class used for debugging SDK network operations.
public class DebugAPIRequestObserver: OAuth2ClientDelegate {
/// Shared convenience instance to use.
public static var shared: DebugAPIRequestObserver = {
DebugAPIRequestObserver()
}()

/// Indicates if HTTP request and response headers should be logged.
public var showHeaders = false

public func api(client: any APIClient, willSend request: inout URLRequest) {
var headers = "<omitted>"
if showHeaders {
dump(request.allHTTPHeaderFields ?? [:], to: &headers)
}

if let bodyData = request.httpBody,
let body = String(data: bodyData, encoding: .utf8)
{
os_log(.debug, log: Self.log, "Sending HTTP Request\nEndpoint: %{public}s %s\nHeaders: %s\nBody: %d bytes\n\n%s",
request.httpMethod ?? "<null>",
request.url?.absoluteString ?? "<null>",
headers,
bodyData.count,
body)
} else {
os_log(.debug, log: Self.log, "Sending HTTP Request\nEndpoint: %{public}s %s\nHeaders: %s",
request.httpMethod ?? "<null>",
request.url?.absoluteString ?? "<null>",
headers)
}
}

public func api(client: any APIClient, didSend request: URLRequest, received response: HTTPURLResponse) {
var headers = "<omitted>"
if showHeaders {
dump(response.allHeaderFields, to: &headers)
}

os_log(.debug, log: Self.log, "Received HTTP Response %{public}s\nStatus Code: %d\nHeaders: %s",
requestId(from: response.allHeaderFields, using: client.requestIdHeader),
response.statusCode,
headers)
}

public func api(client: any APIClient,
didSend request: URLRequest,
received error: APIClientError,
requestId: String?,
rateLimit: APIRateLimit?)
{
var result = ""
dump(error, to: &result)
os_log(.debug, log: Self.log, "Error:\n%{public}s", result)
}

public func api<T>(client: any APIClient,
didSend request: URLRequest,
received response: APIResponse<T>) where T: Decodable
{
var result = ""
dump(response.result, to: &result)

os_log(.debug, log: Self.log, "Response:\n\n%s", result)
}

private static var log = OSLog(subsystem: "com.okta.client.network", category: "Debugging")
private func requestId(from headers: [AnyHashable: Any]?, using name: String?) -> String {
headers?.first(where: { (key, _) in
(key as? String)?.lowercased() == name?.lowercased()
})?.value as? String ?? "<unknown>"
}
}


32 changes: 32 additions & 0 deletions Sources/AuthFoundation/OAuth2/Authentication.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,38 @@ public protocol AuthenticationFlow: AnyObject, UsesDelegateCollection {
var delegateCollection: DelegateCollection<Delegate> { get }
}

/// Optional configuration settings that can be used to customize an authentication flow.
public protocol AuthenticationFlowConfiguration: Equatable, ProvidesOAuth2Parameters {
/// The "nonce" value to send with this authorization request.
var nonce: String? { get }

/// The maximum age an ID token can be when authenticating.
var maxAge: TimeInterval? { get }

/// The ACR values, if any, which should be requested by the client.
var acrValues: [String]? { get }
}

extension AuthenticationFlowConfiguration {
public var additionalParameters: [String: any APIRequestArgument]? {
var result = [String: any APIRequestArgument]()

if let nonce = nonce {
result["nonce"] = nonce
}

if let maxAge = maxAge {
result["max_age"] = Int(maxAge).stringValue
}

if let acrValues = acrValues {
result["acr_values"] = acrValues.joined(separator: " ")
}

return result
}

}
/// Errors that may be generated during the process of authenticating with a variety of authentication flows.
public enum AuthenticationError: Error {
case flowNotReady
Expand Down
3 changes: 3 additions & 0 deletions Sources/AuthFoundation/OAuth2/OAuth2TokenRequest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ public protocol OAuth2TokenRequest: APIRequest, APIRequestBody where ResponseTyp

/// The client's Open ID Configuration object defining the settings and endpoints used to interact with this Authorization Server.
var openIdConfiguration: OpenIdConfiguration { get }

/// The flow's configuration settings used to customize the token request.
var authenticationFlowConfiguration: (any AuthenticationFlowConfiguration)? { get }
}

extension OAuth2TokenRequest {
Expand Down
4 changes: 4 additions & 0 deletions Sources/AuthFoundation/Requests/Token+Requests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,10 @@ extension Token.IntrospectRequest: OAuth2APIRequest, APIRequestBody {
}

extension Token.RefreshRequest: OAuth2APIRequest, APIRequestBody, APIParsingContext, OAuth2TokenRequest {
var authenticationFlowConfiguration: (any AuthenticationFlowConfiguration)? {
nil
}

typealias ResponseType = Token

var httpMethod: APIRequestMethod { .post }
Expand Down
Loading
Loading