Skip to content

Commit

Permalink
Implement WebAuthn authenticator support in DirectAuth (#172)
Browse files Browse the repository at this point in the history
Includes the following updates and improvements:

* Implement WebAuthn authenticator support in DirectAuth
* Update linting rules
* Update TimeCoordinator to use a lock, now that ThreadSafe is deleted
* Handle MFA scenarios in WebAuthn properly
  • Loading branch information
mikenachbaur-okta authored Feb 22, 2024
1 parent c78705b commit 01bbaa4
Show file tree
Hide file tree
Showing 39 changed files with 1,089 additions and 233 deletions.
1 change: 1 addition & 0 deletions .swiftlint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ only_rules:
- operator_usage_whitespace
- return_arrow_whitespace
- trailing_whitespace
- attributes

# Empty
- empty_collection_literal
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<dict>
<key>FILEHEADER</key>
<string>
// Copyright (c) 2023-Present, Okta, Inc. and/or its affiliates. All rights reserved.
// 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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ class OIDCSignInViewController: UIViewController {
object: nil)
}

@objc func dismissProfile() {
@objc
func dismissProfile() {
guard presentedViewController != nil else { return }

self.presentedViewController?.dismiss(animated: true, completion: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,8 @@ extension SDKVersion.Migration {
NotificationCenter.default.post(name: .credentialMigrated, object: credential)
}

@objc(_OIDCLegacyStateManager) class StateManager: NSObject, NSCoding {
@objc(_OIDCLegacyStateManager)
class StateManager: NSObject, NSCoding {
@objc let authState: AuthState?
@objc let accessibility: String?

Expand All @@ -222,7 +223,8 @@ extension SDKVersion.Migration {
accessibility = coder.decodeObject(forKey: "accessibility") as? String
}

@objc(_OIDCLegacyAuthState) class AuthState: NSObject, NSCoding {
@objc(_OIDCLegacyAuthState)
class AuthState: NSObject, NSCoding {
@objc let refreshToken: String?
@objc let scope: String?
@objc let lastTokenResponse: TokenResponse?
Expand All @@ -238,7 +240,8 @@ extension SDKVersion.Migration {
}
}

@objc(_OIDCLegacyTokenResponse) class TokenResponse: NSObject, NSCoding {
@objc(_OIDCLegacyTokenResponse)
class TokenResponse: NSObject, NSCoding {
@objc let accessToken: String?
@objc let accessTokenExpirationDate: Date?
@objc let tokenType: String?
Expand All @@ -260,7 +263,8 @@ extension SDKVersion.Migration {
}
}

@objc(_OIDCLegacyAuthorizationResponse) class AuthorizationResponse: NSObject, NSCoding {
@objc(_OIDCLegacyAuthorizationResponse)
class AuthorizationResponse: NSObject, NSCoding {
@objc let authorizationCode: String?
@objc let state: String?

Expand Down
11 changes: 9 additions & 2 deletions Sources/AuthFoundation/Responses/GrantType.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ public enum GrantType: Codable, Hashable {
case oob
case otpMFA
case oobMFA
case webAuthn
case webAuthnMFA
case other(_ type: String)
}

Expand All @@ -36,8 +38,9 @@ private let grantTypeMapping: [String: GrantType] = [
"urn:okta:params:oauth:grant-type:otp": .otp,
"urn:okta:params:oauth:grant-type:oob": .oob,
"http://auth0.com/oauth/grant-type/mfa-otp": .otpMFA,
"http://auth0.com/oauth/grant-type/mfa-oob": .oobMFA

"http://auth0.com/oauth/grant-type/mfa-oob": .oobMFA,
"urn:okta:params:oauth:grant-type:webauthn": .webAuthn,
"urn:okta:params:oauth:grant-type:mfa-webauthn": .webAuthnMFA,
]

extension GrantType: RawRepresentable {
Expand Down Expand Up @@ -75,6 +78,10 @@ extension GrantType: RawRepresentable {
return "http://auth0.com/oauth/grant-type/mfa-otp"
case .oobMFA:
return "http://auth0.com/oauth/grant-type/mfa-oob"
case .webAuthn:
return "urn:okta:params:oauth:grant-type:webauthn"
case .webAuthnMFA:
return "urn:okta:params:oauth:grant-type:mfa-webauthn"
}
}
}
3 changes: 1 addition & 2 deletions Sources/AuthFoundation/Utilities/DelegateCollection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,7 @@ extension UsesDelegateCollection {
}

public final class DelegateCollection<D> {
@WeakCollection
private var delegates: [AnyObject?]
@WeakCollection private var delegates: [AnyObject?]

public init() {
delegates = []
Expand Down
172 changes: 172 additions & 0 deletions Sources/AuthFoundation/Utilities/JSONValue.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
//
// Copyright (c) 2023-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

public enum JSONValueError: Error {
case cannotDecode(value: Any)
}

/// Represent mixed JSON values as instances of `Any`. This is used to expose API response values to Swift native types
/// where Swift enums are not supported.
public enum JSONValue: Equatable {
case string(String)
case number(Double)
case bool(Bool)
case dictionary([String: JSONValue])
case array([JSONValue])
case object(Any)
case null

public init(_ value: Any?) throws {
if let value = value as? String {
self = .string(value)
} else if let value = value as? NSNumber {
self = .number(value.doubleValue)
} else if let value = value as? Bool {
self = .bool(value)
} else if let value = value as? [String: Any] {
self = .dictionary(try value.mapValues({ try JSONValue($0) }))
} else if let value = value as? [Any] {
self = .array(try value.map({ try JSONValue($0) }))
} else if value == nil {
self = .null
} else {
throw JSONValueError.cannotDecode(value: value as Any)
}
}

/// Returns the value as an instance of `Any`.
public var anyValue: Any? {
switch self {
case let .string(value):
return value
case let .number(value):
return value
case let .bool(value):
return value
case let .dictionary(value):
return value.reduce(into: [String: Any?]()) {
$0[$1.key] = $1.value.anyValue
}
case let .array(value):
return value.map { $0.anyValue }
case let .object(value):
return value
case .null:
return nil
}
}

public static func == (lhs: JSONValue, rhs: JSONValue) -> Bool {
switch (lhs, rhs) {
case (.string(let lhsValue), .string(let rhsValue)):
return lhsValue == rhsValue
case (.number(let lhsValue), .number(let rhsValue)):
return lhsValue == rhsValue
case (.bool(let lhsValue), .bool(let rhsValue)):
return lhsValue == rhsValue
case (.dictionary(let lhsValue), .dictionary(let rhsValue)):
return lhsValue == rhsValue
case (.array(let lhsValue), .array(let rhsValue)):
return lhsValue == rhsValue
case (.object(let lhsValue), .object(let rhsValue)):
if let lhsValue = lhsValue as? AnyHashable,
let rhsValue = rhsValue as? AnyHashable
{
return lhsValue == rhsValue
} else {
return false
}
case (.null, .null):
return true
default:
return false
}
}
}

extension JSONValue: Codable {
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let value = try? container.decode(String.self) {
self = .string(value)
} else if let value = try? container.decode(Double.self) {
self = .number(value)
} else if let value = try? container.decode(Bool.self) {
self = .bool(value)
} else if let value = try? container.decode([String: JSONValue].self) {
self = .dictionary(value)
} else if let value = try? container.decode([JSONValue].self) {
self = .array(value)
} else if container.decodeNil() {
self = .null
} else {
throw DecodingError.dataCorrupted(.init(codingPath: decoder.codingPath,
debugDescription: "Invalid JSON value \(decoder.codingPath)"))
}
}

public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case let .string(value):
try container.encode(value)
case let .number(value):
try container.encode(value)
case let .bool(value):
try container.encode(value)
case let .dictionary(value):
try container.encode(value)
case let .array(value):
try container.encode(value)
case let .object(value):
if let value = value as? Codable {
try container.encode(value)
} else {
throw EncodingError.invalidValue(value, .init(codingPath: encoder.codingPath,
debugDescription: "Value is not encodable at \(encoder.codingPath)"))
}
case .null:
try container.encodeNil()
}
}
}

extension JSONValue: CustomDebugStringConvertible {
public var debugDescription: String {
switch self {
case .string(let str):
return str.debugDescription
case .number(let num):
return num.debugDescription
case .bool(let bool):
return bool ? "true" : "false"
case .null:
return "null"
case .object(let obj):
if let obj = obj as? CustomDebugStringConvertible {
return obj.debugDescription
} else {
return "Custom object \(String(describing: obj))"
}
default:
let encoder = JSONEncoder()
encoder.outputFormatting = [.prettyPrinted]
// swiftlint:disable force_unwrapping
// swiftlint:disable force_try
return try! String(data: encoder.encode(self), encoding: .utf8)!
// swiftlint:enable force_try
// swiftlint:enable force_unwrapping
}
}
}

This file was deleted.

10 changes: 7 additions & 3 deletions Sources/AuthFoundation/Utilities/TimeCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,17 @@ class DefaultTimeCoordinator: TimeCoordinator, OAuth2ClientDelegate {
Date.coordinator = DefaultTimeCoordinator()
}

@ThreadSafe
private(set) var offset: TimeInterval
private let lock = UnfairLock()
private var _offset: TimeInterval
private(set) var offset: TimeInterval {
get { lock.withLock { _offset } }
set { lock.withLock { _offset = newValue } }
}

private var observer: NSObjectProtocol?

init() {
self.offset = 0
self._offset = 0
self.observer = NotificationCenter.default.addObserver(forName: .oauth2ClientCreated,
object: nil,
queue: nil,
Expand Down
Loading

0 comments on commit 01bbaa4

Please sign in to comment.