Skip to content

Commit

Permalink
Refactor authentication flows for consistency and customization.
Browse files Browse the repository at this point in the history
- Introduces the concept of an Authentication Context, which is used to supply flow-specific capabilities to adapt the flows to specific use-cases.
- Normalize the coalescence of values supplied to OAuth2Client configuration, auth flow context, and user-supplied information.
- Introduce support for ACR Values
- Pave the way for refactoring these objects and types to support actors and @sendable
  • Loading branch information
mikenachbaur-okta committed Jan 31, 2025
1 parent 0046217 commit 1758ef0
Show file tree
Hide file tree
Showing 157 changed files with 3,216 additions and 2,123 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,14 @@ class SignInViewController: UIHostingController<SignInView> {

// Workaround to remove the `device_sso` scope, when included in the property list.
if let configuration = try? OAuth2Client.PropertyListConfiguration(),
let ssoRange = configuration.scopes.range(of: "device_sso")
let ssoRange = configuration.scope.range(of: "device_sso")
{
var scopes = configuration.scopes
scopes.removeSubrange(ssoRange)
var scope = configuration.scope
scope.removeSubrange(ssoRange)

flow = DirectAuthenticationFlow(issuer: configuration.issuer,
flow = DirectAuthenticationFlow(issuerURL: configuration.issuerURL,
clientId: configuration.clientId,
scopes: scopes)
scope: scope)
} else {
flow = try? DirectAuthenticationFlow()
}
Expand Down
4 changes: 0 additions & 4 deletions Sources/AuthFoundation/AuthFoundation.docc/APIClient.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
# ``AuthFoundation/APIClient``

@Metadata {
@DocumentationExtension(mergeBehavior: append)
}

## Subclassing Notes

Many features of the APIClient protocol have default implementations that should serve most purposes, but there are some methods that either must be implemented in concrete instances of APIClient, or may need to be customized for special behavior.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,6 @@ You can use AuthFoundation when you want to:
### Error Types

- ``APIClientError``
- ``AuthenticationError``
- ``ClaimError``
- ``CredentialError``
- ``JSONError``
Expand Down
4 changes: 0 additions & 4 deletions Sources/AuthFoundation/AuthFoundation.docc/Credential.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
# ``AuthFoundation/Credential``

@Metadata {
@DocumentationExtension(mergeBehavior: append)
}

## Storing Credentials

The Credential class fundamentally is used as a convenience to simplify access and storage of ``Token``s. Regardless of how the token is created, it can be securely stored by using ``store(_:tags:security:)``. This saves the token for later use.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,4 @@ When you add your delegate (e.g. using ``OAuth2Client/add(delegate:)``), your cl

### Monitoring outgoing network requests

Building upon the previous section, another method ``APIClientDelegate`` supports is handling responses to requests, through the use of ``APIClientDelegate/api(client:didSend:received:)-4mcbm``. Information about the raw response, including ``APIResponse/RateLimit``, associated links (e.g. next, previous, and current pagination results), and information about the request ID (which can be used for debugging purposes).
Building upon the previous section, another method ``APIClientDelegate`` supports is handling responses to requests, through the use of ``APIClientDelegate/api(client:didSend:received:)-4mcbm``. Information about the raw response, including ``APIResponse/APIRateLimit``, associated links (e.g. next, previous, and current pagination results), and information about the request ID (which can be used for debugging purposes).
4 changes: 2 additions & 2 deletions Sources/AuthFoundation/AuthFoundation.docc/JWT.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ These objects conform to both the ``HasClaims`` and ``Expires`` protocols, to pr

Reading information, or "Claims", from a JWT token can be done in three different ways:

1. Accessing values using convenience properties
2. Using keyed subscripting of common claims using the ``Claim`` enum
1. Accessing values using convenience properties.
2. Using keyed subscripting of common claims using the ``ClaimType`` enum corresponding to the claim container.
3. Using keyed subscripting of custom claims using the claim's string name.

Some common properties, such as ``HasClaims/subject`` or ``issuer``, are defined as properties on the JWT object, to simplify access to these values. Additionally, some claims that return dates or time intervals have conveniences such as ``issuedAt``, ``expirationTime``, ``expiresIn``, or ``scope``, that returns their values in the expected type.
Expand Down
4 changes: 0 additions & 4 deletions Sources/AuthFoundation/AuthFoundation.docc/Keychain.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
# ``AuthFoundation/Keychain``

@Metadata {
@DocumentationExtension(mergeBehavior: append)
}

## Creating Keychain Items

The ``Keychain/Item`` struct represents individual items in the keychain. It can be used to create new items, and is returned as a result when getting an existing item.
Expand Down
4 changes: 0 additions & 4 deletions Sources/AuthFoundation/AuthFoundation.docc/OAuth2Client.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
# ``AuthFoundation/OAuth2Client``

@Metadata {
@DocumentationExtension(mergeBehavior: append)
}

This class serves two purposes:
1. Expose high-level actions a client can perform against an OAuth2 service.
2. Connect authentication flows to the OAuth2 servers they intend to authenticate against.
Expand Down
66 changes: 63 additions & 3 deletions Sources/AuthFoundation/Debugging/APIRequestObserver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,65 @@ import OSLog
//
// 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.

#if DEBUG
/// Convenience class used for debugging SDK network operations.
public class DebugAPIRequestObserver: OAuth2ClientDelegate {
///
/// Developers can use this to assist in debugging interactions with the Client SDK, and any network operations that are performed on behalf of the user via this SDK.
///
/// > Important: This is only available in `DEBUG` builds, and should not be used within production applications.
///
/// To use this debug tool, you can either add the shared observer as a delegate to the ``OAuth2Client`` instance you would like to observe:
///
/// ```swift
/// let flow = try AuthorizationCodeFlow()
/// flow.client.add(delegate: DebugAPIRequestObserver.shared)
/// ```
///
/// Or alternatively you can use the ``observeAllOAuth2Clients`` convenience property to automatically bind to ``OAuth2Client`` instances as they are initialized.
///
/// ```swift
/// func application(
/// _ application: UIApplication,
/// didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool
/// {
/// // Application setup
/// #if DEBUG
/// DebugAPIRequestObserver.shared.observeAllOAuth2Clients = true
/// #endif
/// }
/// ```
public final 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

/// Convenience flag that automatically binds newly-created ``OAuth2Client`` instances to the debug observer.
public var observeAllOAuth2Clients: Bool = false {
didSet {
if observeAllOAuth2Clients {
oauth2Observer = NotificationCenter.default.addObserver(
forName: .oauth2ClientCreated,
object: nil,
queue: nil,
using: { [weak self] notification in
guard let self = self,
let client = notification.object as? OAuth2Client
else {
return
}
client.add(delegate: self)
})
} else {
oauth2Observer = nil
}
}
}

@_documentation(visibility: private)
public func api(client: any APIClient, willSend request: inout URLRequest) {
var headers = "<omitted>"
if showHeaders {
Expand All @@ -52,6 +101,7 @@ public class DebugAPIRequestObserver: OAuth2ClientDelegate {
}
}

@_documentation(visibility: private)
public func api(client: any APIClient, didSend request: URLRequest, received response: HTTPURLResponse) {
var headers = "<omitted>"
if showHeaders {
Expand All @@ -64,6 +114,7 @@ public class DebugAPIRequestObserver: OAuth2ClientDelegate {
headers)
}

@_documentation(visibility: private)
public func api(client: any APIClient,
didSend request: URLRequest,
received error: APIClientError,
Expand All @@ -75,6 +126,7 @@ public class DebugAPIRequestObserver: OAuth2ClientDelegate {
os_log(.debug, log: Self.log, "Error:\n%{public}s", result)
}

@_documentation(visibility: private)
public func api<T>(client: any APIClient,
didSend request: URLRequest,
received response: APIResponse<T>) where T: Decodable
Expand All @@ -86,11 +138,19 @@ public class DebugAPIRequestObserver: OAuth2ClientDelegate {
}

private static var log = OSLog(subsystem: "com.okta.client.network", category: "Debugging")
private var oauth2Observer: NSObjectProtocol? {
didSet {
if let oldValue = oldValue,
oauth2Observer == nil
{
NotificationCenter.default.removeObserver(oldValue)
}
}
}
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>"
}
}


#endif
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
//
// 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 OAuth2Client {
@_documentation(visibility: private)
@available(*, deprecated, renamed: "init(domain:clientId:scope:redirectUri:logoutRedirectUri:authentication:session:)")
public convenience init(domain: String,
clientId: String,
scopes: String,
authentication: ClientAuthentication = .none,
session: URLSessionProtocol? = nil) throws
{
try self.init(domain: domain,
clientId: clientId,
scope: scopes,
authentication: authentication,
session: session)
}

@_documentation(visibility: private)
@available(*, deprecated, renamed: "init(issuerURL:clientId:scope:redirectUri:logoutRedirectUri:authentication:session:)")
public convenience init(baseURL: URL,
clientId: String,
scopes: String,
authentication: ClientAuthentication = .none,
session: URLSessionProtocol? = nil)
{
self.init(issuerURL: baseURL,
clientId: clientId,
scope: scopes,
authentication: authentication,
session: session)
}
}
3 changes: 0 additions & 3 deletions Sources/AuthFoundation/JWT/Enums/JWTClaim.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,6 @@

import Foundation

@available(*, deprecated, renamed: "JWTClaim")
public typealias Claim = JWTClaim

/// List of registered and public claims.
public enum JWTClaim: Codable, IsClaim {
/// Issuer
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ public extension HasClaims {
}

/// Returns the value for the given key as an array of values converted using a``ClaimConvertable`` type.
/// - Parameter key: String payload key name.
/// - Parameter claim: The claim type to retrieve.
/// - Returns: Value converted to an array of the requested type.
func value<T: ClaimConvertable>(for claim: ClaimType) throws -> [T] {
try value(for: claim.rawValue)
Expand All @@ -81,7 +81,7 @@ public extension HasClaims {
}

/// Returns the optional value for the given key as an array of values converted using a``ClaimConvertable`` type.
/// - Parameter key: String payload key name.
/// - Parameter claim: The claim type to retrieve.
/// - Returns: Optional value converted to an array of the requested type.
func value<T: ClaimConvertable>(for claim: ClaimType) -> [T]? {
value(for: claim.rawValue)
Expand All @@ -103,7 +103,7 @@ public extension HasClaims {
}

/// Returns the value for the given key as an array of values converted using a``ClaimConvertable`` type.
/// - Parameter key: String payload key name.
/// - Parameter claim: The claim type to retrieve.
/// - Returns: Value converted to an array of the requested type.
func value<T: ClaimConvertable>(for claim: ClaimType) throws -> [String: T] {
try value(for: claim.rawValue)
Expand All @@ -118,7 +118,7 @@ public extension HasClaims {
}

/// Returns the optional value for the given key as an array of values converted using a``ClaimConvertable`` type.
/// - Parameter key: String payload key name.
/// - Parameter claim: Payload claim to retrieve.
/// - Returns: Optional value converted to an array of the requested type.
func value<T: ClaimConvertable>(for claim: ClaimType) -> [String: T]? {
value(for: claim.rawValue)
Expand Down
2 changes: 2 additions & 0 deletions Sources/AuthFoundation/JWT/JWK.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ public struct JWK: Codable, Equatable, Identifiable, Hashable {
/// A default implementation of ``JWKValidator`` is provided and will be used if this value is not changed.
public static var validator: JWKValidator = DefaultJWKValidator()

@_documentation(visibility: internal)
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
type = try container.decode(KeyType.self, forKey: .keyType)
Expand All @@ -56,6 +57,7 @@ public struct JWK: Codable, Equatable, Identifiable, Hashable {
}
}

@_documentation(visibility: internal)
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(type, forKey: .keyType)
Expand Down
3 changes: 2 additions & 1 deletion Sources/AuthFoundation/JWT/JWT.swift
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ public struct JWT: RawRepresentable, Codable, HasClaims, Expires {
case algorithm = "alg"
}

@_documentation(visibility: internal)
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
keyId = try container.decode(String.self, forKey: .keyId)
Expand All @@ -72,7 +73,7 @@ public struct JWT: RawRepresentable, Codable, HasClaims, Expires {
}

/// Verifies the JWT token using the given ``JWK`` key.
/// - Parameter key: JWK key to use to verify this token.
/// - Parameter keySet: JWK keyset which should be used to verify this token.
/// - Returns: Returns whether or not signing passes for this token/key combination.
/// - Throws: ``JWTError``
public func validate(using keySet: JWKS) throws -> Bool {
Expand Down
2 changes: 1 addition & 1 deletion Sources/AuthFoundation/JWT/Protocols/Claim.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public protocol HasClaims {
public extension HasClaims {
/// Returns the collection of claims this object contains.
///
/// > Note: This will only return the list of official claims defined in the ``Claim`` enum. For custom claims, please see the ``customClaims`` property.
/// > Note: This will only return the list of official claims defined in the ``ClaimType`` enum corresponding to this claim container. For custom claims, please see the ``customClaims`` property.
var claims: [ClaimType] {
payload.keys.compactMap { ClaimType(rawValue: $0) }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,15 +42,6 @@ extension SDKVersion.Migration {
try register(try OAuth2Client.PropertyListConfiguration(plist: fileURL))
}

@available(*, deprecated, renamed: "register(clientId:)")
public static func register(issuer: URL,
clientId: String,
redirectUri: URL,
scopes: String)
{
register(clientId: clientId)
}

/// Registers the legacy OIDC migration using the supplied configuration values.
/// - Parameters:
/// - clientId: Client ID for your application.
Expand Down Expand Up @@ -164,9 +155,10 @@ extension SDKVersion.Migration {
idToken = nil
}

let configuration = OAuth2Client.Configuration(baseURL: issuer,
let configuration = OAuth2Client.Configuration(issuerURL: issuer,
clientId: clientId,
scopes: scope)
scope: scope,
redirectUri: redirectURL)
var clientSettings: [String: String] = [
"client_id": clientId,
"redirect_uri": redirectURL.absoluteString,
Expand Down
7 changes: 4 additions & 3 deletions Sources/AuthFoundation/Network/APIClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import Foundation
import FoundationNetworking
#endif

public protocol APIClientConfiguration: AnyObject {
public protocol APIClientConfiguration {
var baseURL: URL { get }
}

Expand Down Expand Up @@ -45,7 +45,7 @@ public protocol APIClient {
///
/// The userInfo property may be included, which can include contextual information that can help decoders formulate objects.
/// - Returns: Decoded object.
func decode<T: Decodable>(_ type: T.Type, from data: Data, userInfo: [CodingUserInfoKey: Any]?) throws -> T
func decode<T: Decodable>(_ type: T.Type, from data: Data, parsing context: APIParsingContext?) throws -> T

/// Parses HTTP response body data when a request fails.
/// - Returns: Error instance, if any, described within the data.
Expand Down Expand Up @@ -102,6 +102,7 @@ extension APIClientDelegate {
public enum APIRetry {
/// Indicates the APIRequest should not be retried.
case doNotRetry

/// The APIRequest should be retried, up to the given maximum number of times.
case retry(maximumCount: Int)

Expand Down Expand Up @@ -315,7 +316,7 @@ extension APIClient {

return APIResponse(result: try decode(T.self,
from: jsonData,
userInfo: context?.codingUserInfo),
parsing: context),
date: date ?? Date(),
statusCode: response.statusCode,
links: relatedLinks(from: response.allHeaderFields["Link"] as? String),
Expand Down
Loading

0 comments on commit 1758ef0

Please sign in to comment.