Skip to content

Commit

Permalink
Implement RFC7523 with JWTAuthorizationFlow
Browse files Browse the repository at this point in the history
  • Loading branch information
Alex Nachbaur committed Aug 7, 2024
1 parent 367f2e3 commit 6476c00
Show file tree
Hide file tree
Showing 10 changed files with 408 additions and 1 deletion.
4 changes: 4 additions & 0 deletions Sources/AuthFoundation/Responses/GrantType.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ public enum GrantType: Codable, Hashable, IsClaim {
case password
case deviceCode
case tokenExchange
case jwtBearer
case otp
case oob
case otpMFA
Expand All @@ -35,6 +36,7 @@ private let grantTypeMapping: [String: GrantType] = [
"refresh_token": .refreshToken,
"password": .password,
"urn:ietf:params:oauth:grant-type:token-exchange": .tokenExchange,
"urn:ietf:params:oauth:grant-type:jwt-bearer": .jwtBearer,
"urn:ietf:params:oauth:grant-type:device_code": .deviceCode,
"urn:okta:params:oauth:grant-type:otp": .otp,
"urn:okta:params:oauth:grant-type:oob": .oob,
Expand Down Expand Up @@ -69,6 +71,8 @@ extension GrantType: RawRepresentable {
return "password"
case .tokenExchange:
return "urn:ietf:params:oauth:grant-type:token-exchange"
case .jwtBearer:
return "urn:ietf:params:oauth:grant-type:jwt-bearer"
case .deviceCode:
return "urn:ietf:params:oauth:grant-type:device_code"
case .otp:
Expand Down
5 changes: 5 additions & 0 deletions Sources/OktaOAuth2/Authentication/AuthorizationCodeFlow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,11 @@ extension AuthorizationCodeFlow: OAuth2ClientDelegate {
}

extension OAuth2Client {
/// Creates a new Authorization Code flow configured to use this OAuth2Client.
/// - Parameters:
/// - redirectUri: Redirect URI
/// - additionalParameters: Additional parameters to pass to the flow
/// - Returns: Initialized authorization flow.
public func authorizationCodeFlow(
redirectUri: URL,
additionalParameters: [String: String]? = nil) -> AuthorizationCodeFlow
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,8 @@ extension DeviceAuthorizationFlow: OAuth2ClientDelegate {
}

extension OAuth2Client {
/// Creates a new Device Authorization flow configured to use this OAuth2Client.
/// - Returns: Initialized authorization flow.
public func deviceAuthorizationFlow() -> DeviceAuthorizationFlow {
DeviceAuthorizationFlow(client: self)
}
Expand Down
151 changes: 151 additions & 0 deletions Sources/OktaOAuth2/Authentication/JWTAuthorizationFlow.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
//
// 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 AuthFoundation

/// An authentication flow class that implements the JWT Authorization Bearer Flow, for authenticating users using JWTs signed by a trusted key.
public class JWTAuthorizationFlow: AuthenticationFlow {
/// The OAuth2Client this authentication flow will use.
public let client: OAuth2Client

/// Indicates whether or not this flow is currently in the process of authenticating a user.
/// ``JWTAuthorizationFlow/init(issuer:clientId:scopes:)``
public private(set) var isAuthenticating: Bool = false {
didSet {
guard oldValue != isAuthenticating else {
return
}

if isAuthenticating {
delegateCollection.invoke { $0.authenticationStarted(flow: self) }
} else {
delegateCollection.invoke { $0.authenticationFinished(flow: self) }
}
}
}

/// Convenience initializer to construct an authentication flow from variables.
/// - Parameters:
/// - issuer: The issuer URL.
/// - clientId: The client ID
/// - scopes: The scopes to request
public convenience init(issuer: URL,
clientId: String,
scopes: String)
{
self.init(client: OAuth2Client(baseURL: issuer,
clientId: clientId,
scopes: scopes))
}

/// Initializer to construct an authentication flow from an OAuth2Client.
/// - Parameter client: `OAuth2Client` instance to authenticate with.
public init(client: OAuth2Client) {
// Ensure this SDK's static version is included in the user agent.
SDKVersion.register(sdk: Version)

self.client = client

client.add(delegate: self)
}

/// Initializer that uses the configuration defined within the application's `Okta.plist` file.
public convenience init() throws {
self.init(try OAuth2Client.PropertyListConfiguration())
}

/// Initializer that uses the configuration defined within the given file URL.
/// - Parameter fileURL: File URL to a `plist` containing client configuration.
public convenience init(plist fileURL: URL) throws {
self.init(try OAuth2Client.PropertyListConfiguration(plist: fileURL))
}

private convenience init(_ config: OAuth2Client.PropertyListConfiguration) {
self.init(issuer: config.issuer,
clientId: config.clientId,
scopes: config.scopes)
}

/// Authenticates using the supplied JWT bearer assertion.
/// - Parameters:
/// - assertion: JWT Assertion
/// - completion: Completion invoked when a response is received.
public func start(with assertion: JWT, completion: @escaping (Result<Token, OAuth2Error>) -> Void) {
isAuthenticating = true

client.openIdConfiguration { result in
switch result {
case .success(let configuration):
let request = TokenRequest(openIdConfiguration: configuration,
clientId: self.client.configuration.clientId,
scope: self.client.configuration.scopes,
assertion: assertion)
self.client.exchange(token: request) { result in
self.reset()

switch result {
case .failure(let error):
self.delegateCollection.invoke { $0.authentication(flow: self, received: .network(error: error)) }
completion(.failure(.network(error: error)))
case .success(let response):
self.delegateCollection.invoke { $0.authentication(flow: self, received: response.result) }
completion(.success(response.result))
}
}

case .failure(let error):
self.delegateCollection.invoke { $0.authentication(flow: self, received: error) }
completion(.failure(error))
}
}
}

/// Resets the flow for later reuse.
public func reset() {
isAuthenticating = false
}

// MARK: Private properties / methods
public let delegateCollection = DelegateCollection<AuthenticationDelegate>()
}

#if swift(>=5.5.1)
@available(iOS 13.0, tvOS 13.0, macOS 10.15, watchOS 6, *)
extension JWTAuthorizationFlow {
/// Asynchronously authenticates with a JWT bearer assertion.
///
/// - Parameter jwt: JWT Assertion
/// - Returns: The token resulting from signing in.
public func start(with assertion: JWT) async throws -> Token {
try await withCheckedThrowingContinuation { continuation in
start(with: assertion) { result in
continuation.resume(with: result)
}
}
}
}
#endif

extension JWTAuthorizationFlow: UsesDelegateCollection {
public typealias Delegate = AuthenticationDelegate
}

extension JWTAuthorizationFlow: OAuth2ClientDelegate {}

extension OAuth2Client {
/// Creates a new JWT Authorization flow configured to use this OAuth2Client.
/// - Returns: Initialized authorization flow.
public func jwtAuthorizationFlow() -> JWTAuthorizationFlow {
JWTAuthorizationFlow(client: self)
}
}
2 changes: 2 additions & 0 deletions Sources/OktaOAuth2/Authentication/ResourceOwnerFlow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,8 @@ extension ResourceOwnerFlow: OAuth2ClientDelegate {
}

extension OAuth2Client {
/// Creates a new Resource Owner flow configured to use this OAuth2Client.
/// - Returns: Initialized authorization flow.
public func resourceOwnerFlow() -> ResourceOwnerFlow {
ResourceOwnerFlow(client: self)
}
Expand Down
5 changes: 5 additions & 0 deletions Sources/OktaOAuth2/Authentication/SessionTokenFlow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,11 @@ extension SessionTokenFlow: OAuth2ClientDelegate {
}

extension OAuth2Client {
/// Creates a new Session Token flow configured to use this OAuth2Client.
/// - Parameters:
/// - redirectUri: Redirect URI
/// - additionalParameters: Additional parameters to pass to the flow
/// - Returns: Initialized authorization flow.
public func sessionTokenFlow(redirectUri: URL,
additionalParameters: [String: String]? = nil) -> SessionTokenFlow
{
Expand Down
3 changes: 3 additions & 0 deletions Sources/OktaOAuth2/Authentication/TokenExchangeFlow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,9 @@ extension TokenExchangeFlow {
#endif

extension OAuth2Client {
/// Creates a new Token Exchange flow configured to use this OAuth2Client, using the supplied arguments.
/// - Parameter audience: Audience to configure the flow to use
/// - Returns: Initialized authorization flow.
public func tokenExchangeFlow(audience: TokenExchangeFlow.Audience = .default) -> TokenExchangeFlow {
TokenExchangeFlow(audience: audience, client: self)
}
Expand Down
43 changes: 43 additions & 0 deletions Sources/OktaOAuth2/Requests/JWTAuthorizationFlow+Requests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
//
// 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 AuthFoundation

extension JWTAuthorizationFlow {
struct TokenRequest {
let openIdConfiguration: OpenIdConfiguration
let clientId: String
let scope: String
let assertion: JWT
}
}

extension JWTAuthorizationFlow.TokenRequest: OAuth2TokenRequest, OAuth2APIRequest, APIRequestBody, APIParsingContext {
var bodyParameters: [String: APIRequestArgument]? {
[
"client_id": clientId,
"scope": scope,
"grant_type": GrantType.jwtBearer,
"assertion": assertion,
]
}

var codingUserInfo: [CodingUserInfoKey: Any]? {
[
.clientSettings: [
"client_id": clientId,
"scope": scope,
]
]
}
}
Loading

0 comments on commit 6476c00

Please sign in to comment.