Skip to content

Commit

Permalink
Add support for returning tokens while using MFA Attestation (#191)
Browse files Browse the repository at this point in the history
  • Loading branch information
mikenachbaur-okta authored May 14, 2024
1 parent d0ff813 commit 1a533d8
Show file tree
Hide file tree
Showing 8 changed files with 105 additions and 5 deletions.
3 changes: 3 additions & 0 deletions Sources/AuthFoundation/OAuth2/Authentication.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ public protocol AuthenticationFlow: AnyObject, UsesDelegateCollection {

/// Resets the authentication session.
func reset()

/// The collection of delegates this flow notifies for key authentication events.
var delegateCollection: DelegateCollection<Delegate> { get }
}

/// Errors that may be generated during the process of authenticating with a variety of authentication flows.
Expand Down
6 changes: 3 additions & 3 deletions Sources/AuthFoundation/OAuth2/OAuth2Client.swift
Original file line number Diff line number Diff line change
Expand Up @@ -435,9 +435,9 @@ public final class OAuth2Client {
group.notify(queue: DispatchQueue.global()) {
// Perform idToken/accessToken validation
self.validateToken(request: request,
keySet: keySet,
oauthTokenResponse: result,
completion: completion)
keySet: keySet,
oauthTokenResponse: result,
completion: completion)
}
}
}
Expand Down
16 changes: 15 additions & 1 deletion Sources/AuthFoundation/OAuth2/OAuth2TokenRequest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,20 @@
import Foundation

/// Protocol that represents a type of ``APIRequest`` that can be used to exchange a token.
public protocol OAuth2TokenRequest: APIRequest where ResponseType == Token {
///
/// Many different OAuth2 authentication flows can issue tokens, but the types of arguments and their particular workflow can differ. This protocol abstracts the necessary interface for requests that are capable of returning tokens, while allowing the specific arguments and validation steps to be implemented for each unique type of flow.
public protocol OAuth2TokenRequest: APIRequest, APIRequestBody where ResponseType == Token {
/// The application's OAuth2 `client_id` value for this token request.
var clientId: String { get }
}

extension OAuth2TokenRequest {
private var bodyParameterStrings: [String: String]? {
bodyParameters?.compactMapValues({ $0 as? String })
}

/// Convenience function that exposes an array of ACR Values requested by this OAuth2 token request, if applicable.
public var acrValues: [String]? {
bodyParameterStrings?["acr_values"]?.components(separatedBy: .whitespaces)
}
}
19 changes: 18 additions & 1 deletion Sources/AuthFoundation/Token Management/Token.swift
Original file line number Diff line number Diff line change
Expand Up @@ -200,11 +200,27 @@ public final class Token: Codable, Equatable, Hashable, Expires {
idToken = try JWT(idTokenString)
}

// There are some conditions where a missing or null access_token is acceptable.
// Detect this condition, and assign an empty string where necessary.
var accessToken: String
do {
accessToken = try container.decode(String.self, forKey: .accessToken)
} catch {
if let request = decoder.userInfo[.request] as? (any OAuth2TokenRequest),
let acrValues = request.acrValues,
acrValues.contains("urn:okta:app:mfa:attestation")
{
accessToken = ""
} else {
throw error
}
}

self.init(id: id,
issuedAt: try container.decodeIfPresent(Date.self, forKey: .issuedAt) ?? Date.nowCoordinated,
tokenType: try container.decode(String.self, forKey: .tokenType),
expiresIn: try container.decode(TimeInterval.self, forKey: .expiresIn),
accessToken: try container.decode(String.self, forKey: .accessToken),
accessToken: accessToken,
scope: try container.decodeIfPresent(String.self, forKey: .scope),
refreshToken: try container.decodeIfPresent(String.self, forKey: .refreshToken),
idToken: idToken,
Expand Down Expand Up @@ -251,5 +267,6 @@ extension CodingUserInfoKey {
public static let tokenId = CodingUserInfoKey(rawValue: "tokenId")!
public static let apiClientConfiguration = CodingUserInfoKey(rawValue: "apiClientConfiguration")!
public static let clientSettings = CodingUserInfoKey(rawValue: "clientSettings")!
public static let request = CodingUserInfoKey(rawValue: "request")!
// swiftlint:enable force_unwrapping
}
17 changes: 17 additions & 0 deletions Sources/AuthFoundation/Utilities/DelegateCollection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,19 @@

import Foundation

/// Indicates the class contains a collection of delegates, and the necessary convenience functions to add and remove delegates from the collection.
public protocol UsesDelegateCollection {
associatedtype Delegate

/// Adds the given argument as a delegate.
/// - Parameter delegate: Delegate to add to the collection.
func add(delegate: Delegate)

/// Removes the given argument from the collection of delegates.
/// - Parameter delegate: Delegate to remove from the collection.
func remove(delegate: Delegate)

/// The collection of delegates this flow notifies for key authentication events.
var delegateCollection: DelegateCollection<Delegate> { get }
}

Expand All @@ -34,24 +42,33 @@ public final class DelegateCollection<D> {
}

extension DelegateCollection {
/// Adds the given argument as a delegate.
/// - Parameter delegate: Delegate to add to the collection.
public func add(_ delegate: D) {
delegates.append(delegate as AnyObject)
}

/// Removes the given argument from the collection of delegates.
/// - Parameter delegate: Delegate to remove from the collection.
public func remove(_ delegate: D) {
let delegateObject = delegate as AnyObject
delegates.removeAll { object in
object === delegateObject
}
}

/// Performs the given block against each delegate within the collection.
/// - Parameter block: Block to invoke for each delegate instance.
public func invoke(_ block: (D) -> Void) {
delegates.forEach {
guard let delegate = $0 as? D else { return }
block(delegate)
}
}

/// Performs the given block for each delegate within the collection, coalescing the results into the returned array.
/// - Parameter block: Block to invoke for each delegate in the collection.
/// - Returns: Resulting array of returned values from the delegates in the collection.
public func call<T>(_ block: (D) -> T) -> [T] {
delegates.compactMap {
guard let delegate = $0 as? D else { return nil }
Expand Down
1 change: 1 addition & 0 deletions Sources/OktaDirectAuth/DirectAuthFlow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,7 @@ public class DirectAuthenticationFlow: AuthenticationFlow {
with factor: PrimaryFactor,
completion: @escaping (Result<Status, DirectAuthenticationFlowError>) -> Void)
{
reset()
runStep(loginHint: loginHint, with: factor, completion: completion)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"token_type": "Bearer",
"expires_in": 0,
"access_token": null,
"scope": "openid",
"id_token": "eyJraWQiOiI5RmR5LXYxOXVRM2E5TTlzV1lNeGNoblNMckFkMjZWWFhIdkZVZXpsTWtNIiwiYWxnIjoiUlMyNTYifQ.eyJzdWIiOiIwMHViNDF6N21nek5xcnlNdjY5NiIsInZlciI6MSwiaXNzIjoiaHR0cHM6Ly9leGFtcGxlLmNvbSIsImF1ZCI6InVuaXRfdGVzdF9jbGllbnRfaWQiLCJpYXQiOjE3MTUyNzQ4NzYsImV4cCI6MTcxNTI3ODQ3NiwianRpIjoiSUQuQVItU2s4bkR1clk0ZTJ2RW1ORm9iQmFFZHZIYXNaNVQyUGVmZ2RSWXdGQSIsImFtciI6WyJvdHAiXSwiaWRwIjoiMDBvOGZvdTdzUmFHR3dkbjQ2OTYiLCJhdXRoX3RpbWUiOjE3MTUyNzQ4NzZ9.eQkqeSMDu8CVYhusfixh1ZfcrmXe03PJfHEcJDwMG1vSrTGR5wyMY1TmlCzQ1WaQ7LI8HrBJfBbh2_keYn7j5qYp_K0zV7Y9HNA2An8hCBfeELFdtcWrTxUmjgNTAa7iuTCPWma8lyYOD3Q89mdMloTYDFAic7ZDIVGTBDoyYJR5OaG-LpnNTSCebhRFnqfWoyIThnTTv0VIwZJ2qiGbCZdnwoZzEisNZcpjOmT4ML_dtA9S2n1j2ZJBwsT2mvmAPmYMFJDLUXWEsOrEJ475RsnQMkN7wzL4931gUU-crzCl7WigCSdTzah1BsKWI6Ya_9qzde0iOD9Of59Y6HBPRw"
}
41 changes: 41 additions & 0 deletions Tests/AuthFoundationTests/TokenTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ import XCTest
@testable import AuthFoundation
@testable import TestCommon

fileprivate struct MockTokenRequest: OAuth2TokenRequest {
let clientId: String
let url: URL
var bodyParameters: [String: Any]?
}

final class TokenTests: XCTestCase {
let configuration = OAuth2Client.Configuration(baseURL: URL(string: "https://example.com")!,
clientId: "clientid",
Expand Down Expand Up @@ -96,6 +102,41 @@ final class TokenTests: XCTestCase {
let decodedToken = try JSONDecoder().decode(Token.self, from: data)
XCTAssertEqual(token, decodedToken)
}

func testMFAAttestationToken() throws {
let request = MockTokenRequest(clientId: configuration.clientId,
url: configuration.baseURL,
bodyParameters: [
"acr_values": "urn:okta:app:mfa:attestation"
])

let decoder = defaultJSONDecoder
decoder.userInfo = [
.apiClientConfiguration: configuration,
.request: request,
]

let token = try decoder.decode(Token.self,
from: try data(from: .module,
for: "token-mfa_attestation",
in: "MockResponses"))
XCTAssertTrue(token.accessToken.isEmpty)
}


func testMFAAttestationTokenFailed() throws {
let request = MockTokenRequest(clientId: configuration.clientId,
url: configuration.baseURL)
let decoder = defaultJSONDecoder
decoder.userInfo = [
.apiClientConfiguration: configuration,
]

XCTAssertThrowsError(try decoder.decode(Token.self,
from: try data(from: .module,
for: "token-mfa_attestation",
in: "MockResponses")))
}

func testTokenEquality() throws {
var token1 = Token.mockToken()
Expand Down

0 comments on commit 1a533d8

Please sign in to comment.