Skip to content

Commit

Permalink
Merge pull request #289 from ForgeRock/develop
Browse files Browse the repository at this point in the history
ForgeRock iOS SDK 4.5.0 Release
  • Loading branch information
spetrov authored Jul 8, 2024
2 parents 94dd1b2 + b954e2b commit a0ec7cc
Show file tree
Hide file tree
Showing 44 changed files with 1,643 additions and 844 deletions.
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
## [4.5.0]
#### Added
- Added SDK support for deleting registered WebAuthn devices from the server. [SDKS-1753]
- Added support for signing off from PingOne to the centralized login flow. [SDKS-3021]
- Added the ability to dynamically configure the SDK by collecting values from the server's OpenID Connect `.well-known` endpoint. [SDKS-3023]

#### Fixed
- SSL pinning configuration was ignored in `FRURLProtocol` class. [SDKS-3239]
- Removed scope validation from `AccessToken` initialization. [SDKS-3305]

## [4.4.1]
#### Added
- Added privacy manifest files to the SDK's modules [SDKS-3086]
Expand Down
4 changes: 2 additions & 2 deletions FRAuth.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

Pod::Spec.new do |s|
s.name = 'FRAuth'
s.version = '4.4.1'
s.version = '4.5.0'
s.summary = 'ForgeRock Auth SDK for iOS'
s.description = <<-DESC
FRAuth is a SDK that allows you easily and quickly develop an application with ForgeRock Platform or ForgeRock Identity Cloud. FRAuth SDK provides interfaces and functionalities of user authentication, registration, and identity and access management against ForgeRock solutions.
Expand All @@ -32,5 +32,5 @@ Pod::Spec.new do |s|
s.resource_bundles = {
'FRAuth' => [base_dir + '/*.xcprivacy']
}
s.ios.dependency 'FRCore', '~> 4.4.1'
s.ios.dependency 'FRCore', '~> 4.5.0'
end
8 changes: 6 additions & 2 deletions FRAuth/FRAuth.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
objects = {

/* Begin PBXBuildFile section */
1B5DD69B2BF599F400EE0C8B /* RemoteWebAuthnRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B5DD69A2BF599F400EE0C8B /* RemoteWebAuthnRepository.swift */; };
1B6037E62B505924009090DF /* TextInputCallback.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B6037E52B505924009090DF /* TextInputCallback.swift */; };
1B85DA132B85208C0023E953 /* TextInputCallbackTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B85DA122B85208C0023E953 /* TextInputCallbackTests.swift */; };
3A1B43D0284510B700EAFC9D /* AtomicDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A1B43CF284510B700EAFC9D /* AtomicDictionary.swift */; };
Expand Down Expand Up @@ -339,6 +340,7 @@
/* End PBXContainerItemProxy section */

/* Begin PBXFileReference section */
1B5DD69A2BF599F400EE0C8B /* RemoteWebAuthnRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteWebAuthnRepository.swift; sourceTree = "<group>"; };
1B6037E52B505924009090DF /* TextInputCallback.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextInputCallback.swift; sourceTree = "<group>"; };
1B85DA122B85208C0023E953 /* TextInputCallbackTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextInputCallbackTests.swift; sourceTree = "<group>"; };
3A1B43CF284510B700EAFC9D /* AtomicDictionary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AtomicDictionary.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -852,6 +854,7 @@
D53A8038262789BD0093B1CA /* WAKTypes.swift */,
ECDF5F4829674E9E007BB721 /* FRWebAuthn.swift */,
EC13ABA929A380920069AC41 /* FRWebAuthnManager.swift */,
1B5DD69A2BF599F400EE0C8B /* RemoteWebAuthnRepository.swift */,
);
path = WebAuthn;
sourceTree = "<group>";
Expand Down Expand Up @@ -1825,6 +1828,7 @@
D586CFB423358EE0007A2194 /* FRDeviceCollector.swift in Sources */,
D53A804C262789BD0093B1CA /* ClientGetOperation.swift in Sources */,
D586CF9423358EE0007A2194 /* PasswordCallback.swift in Sources */,
1B5DD69B2BF599F400EE0C8B /* RemoteWebAuthnRepository.swift in Sources */,
D586CFBF23358EE0007A2194 /* TokenManager.swift in Sources */,
D586CFA923358EE0007A2194 /* OAuth2Client.swift in Sources */,
EC0BA2E7285B8F8F00F8326E /* FROptions.swift in Sources */,
Expand Down Expand Up @@ -2146,7 +2150,7 @@
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
MARKETING_VERSION = 4.4.1;
MARKETING_VERSION = 4.5.0;
MODULEMAP_FILE = "";
OTHER_CFLAGS = "-DXCODE_FRAMEWORK=1";
PRODUCT_BUNDLE_IDENTIFIER = com.forgerock.ios.FRAuth;
Expand Down Expand Up @@ -2183,7 +2187,7 @@
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
MARKETING_VERSION = 4.4.1;
MARKETING_VERSION = 4.5.0;
MODULEMAP_FILE = "";
OTHER_CFLAGS = "-DXCODE_FRAMEWORK=1";
PRODUCT_BUNDLE_IDENTIFIER = com.forgerock.ios.FRAuth;
Expand Down
58 changes: 55 additions & 3 deletions FRAuth/FRAuth/Config/FROptions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// FROptions.swift
// FRAuth
//
// Copyright (c) 2022 ForgeRock. All rights reserved.
// Copyright (c) 2022-2024 ForgeRock. All rights reserved.
//
// This software may be modified and distributed under the terms
// of the MIT license. See the LICENSE file for details.
Expand All @@ -13,7 +13,7 @@ import Foundation
/// FROptions represents a configuration object for the SDK. It can be used for passing configuration options in the FRAuth.start() method.
///
@objc
public class FROptions: NSObject, Codable {
open class FROptions: NSObject, Codable {
/// String constant for FROptions storage key
internal static let frOptionsStorageKey: String = "FROptions"

Expand All @@ -37,6 +37,7 @@ public class FROptions: NSObject, Codable {
public var oauthThreshold: String?
public var oauthClientId: String?
public var oauthRedirectUri: String?
public var oauthSignoutRedirectUri: String?
public var oauthScope: String?
public var keychainAccessGroup: String?
public var sslPinningPublicKeyHashes: [String]?
Expand All @@ -59,6 +60,7 @@ public class FROptions: NSObject, Codable {
case oauthThreshold = "forgerock_oauth_threshold"
case oauthClientId = "forgerock_oauth_client_id"
case oauthRedirectUri = "forgerock_oauth_redirect_uri"
case oauthSignoutRedirectUri = "forgerock_oauth_sign_out_redirect_uri"
case oauthScope = "forgerock_oauth_scope"
case keychainAccessGroup = "forgerock_keychain_access_group"
case sslPinningPublicKeyHashes = "forgerock_ssl_pinning_public_key_hashes"
Expand All @@ -84,6 +86,7 @@ public class FROptions: NSObject, Codable {
/// - oauthThreshold: OAuth Client timeout threshold
/// - oauthClientId: OAuth Client name
/// - oauthRedirectUri: OAuth Client redirectURI
/// - oauthSignoutRedirectUri: OAuth Client signout redirectURI
/// - oauthScope: OAuth Client scopes
/// - keychainAccessGroup: Keychain access group for shared keychain
/// - sslPinningPublicKeyHashes: SSL Pinning hashes
Expand All @@ -105,6 +108,7 @@ public class FROptions: NSObject, Codable {
oauthThreshold: String? = nil,
oauthClientId: String? = nil,
oauthRedirectUri: String? = nil,
oauthSignoutRedirectUri: String? = nil,
oauthScope: String? = nil,
keychainAccessGroup: String? = nil,
sslPinningPublicKeyHashes: [String]? = nil) {
Expand All @@ -125,6 +129,7 @@ public class FROptions: NSObject, Codable {
self.oauthClientId = oauthClientId
self.oauthThreshold = oauthThreshold
self.oauthRedirectUri = oauthRedirectUri
self.oauthSignoutRedirectUri = oauthSignoutRedirectUri
self.oauthScope = oauthScope
self.keychainAccessGroup = keychainAccessGroup
self.sslPinningPublicKeyHashes = sslPinningPublicKeyHashes
Expand Down Expand Up @@ -154,6 +159,7 @@ public class FROptions: NSObject, Codable {
self.oauthClientId = config[FROptions.CodingKeys.oauthClientId.rawValue] as? String
self.oauthThreshold = config[FROptions.CodingKeys.oauthThreshold.rawValue] as? String
self.oauthRedirectUri = config[FROptions.CodingKeys.oauthRedirectUri.rawValue] as? String
self.oauthSignoutRedirectUri = config[FROptions.CodingKeys.oauthSignoutRedirectUri.rawValue] as? String
self.oauthScope = config[FROptions.CodingKeys.oauthScope.rawValue] as? String
self.keychainAccessGroup = config[FROptions.CodingKeys.keychainAccessGroup.rawValue] as? String
self.sslPinningPublicKeyHashes = config[FROptions.CodingKeys.sslPinningPublicKeyHashes.rawValue] as? [String]
Expand Down Expand Up @@ -195,7 +201,32 @@ public class FROptions: NSObject, Codable {
public func getEndSessionEndpoint() -> String {
return self.endSessionEndpoint ?? "/oauth2/realms/\(self.realm)/connect/endSession"
}


/// Asynchronously discovers configuration options based on a provided discovery URL.
///
/// - Parameter discoveryURL: The URL string from which to discover configuration options. This URL should point to a well-known configuration endpoint that returns the necessary configuration settings in a JSON format.
/// - Returns: An instance of `FROptions` populated with the configuration settings fetched from the discovery URL.
@available(iOS 13.0.0, *)
open func discover(discoveryURL: String) async throws -> FROptions {
guard let discoveryURL = URL(string: discoveryURL) else {
throw OAuth2Error.other("Invalid discovery URL")
}
let data = try await URLSession.shared.data(from: discoveryURL)
let config = try JSONDecoder().decode(OpenIdConfiguration.self, from: data.0)

guard let baseUrl = self.url.isEmpty ? config.issuer : self.url else {
throw OAuth2Error.other("Missing base URL")
}
self.url = baseUrl
self.authorizeEndpoint = config.authorizationEndpoint
self.tokenEndpoint = config.tokenEndpoint
self.userinfoEndpoint = config.userinfoEndpoint
self.endSessionEndpoint = config.endSessionEndpoint
self.revokeEndpoint = config.revocationEndpoint

return self
}

// - MARK: Private

/// Equatable comparison method. Comparing the realm, cookie and oauthClientId values
Expand All @@ -206,6 +237,7 @@ public class FROptions: NSObject, Codable {
lhs.oauthClientId == rhs.oauthClientId &&
lhs.oauthScope == rhs.oauthScope &&
lhs.oauthRedirectUri == rhs.oauthRedirectUri &&
lhs.oauthSignoutRedirectUri == rhs.oauthSignoutRedirectUri &&
lhs.keychainAccessGroup == rhs.keychainAccessGroup)
}
}
Expand All @@ -219,3 +251,23 @@ extension Encodable {
return dictionary
}
}

private struct OpenIdConfiguration: Codable {
public let issuer: String?
public let authorizationEndpoint: String?
public let tokenEndpoint: String?
public let userinfoEndpoint: String?
public let endSessionEndpoint: String?
public let revocationEndpoint: String?


private enum CodingKeys: String, CodingKey {
case issuer = "issuer"
case authorizationEndpoint = "authorization_endpoint"
case tokenEndpoint = "token_endpoint"
case userinfoEndpoint = "userinfo_endpoint"
case endSessionEndpoint = "end_session_endpoint"
case revocationEndpoint = "revocation_endpoint"
}
}

36 changes: 33 additions & 3 deletions FRAuth/FRAuth/Config/OAuth2Client.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// OAuth2Client.swift
// FRAuth
//
// Copyright (c) 2019-2023 ForgeRock. All rights reserved.
// Copyright (c) 2019-2024 ForgeRock. All rights reserved.
//
// This software may be modified and distributed under the terms
// of the MIT license. See the LICENSE file for details.
Expand All @@ -23,6 +23,8 @@ public class OAuth2Client: NSObject, Codable {
let scope: String
/// OAuth2 redirect_uri for the client
let redirectUri: URL
/// OAuth2 signout_redirect_uri for the client
let signoutRedirectUri: URL?
/// ServerConfig which OAuth2 client will communicate to
let serverConfig: ServerConfig
/// Threshold to refresh access_token in advance
Expand All @@ -37,13 +39,15 @@ public class OAuth2Client: NSObject, Codable {
/// - clientId: client_id of the client
/// - scope: set of scope(s) separated by space to request for the client; requesting scope set must be registered in the OAuth2 client
/// - redirectUri: redirect_uri in URL object as registered in the client
/// - signoutRedirectUri: optional signout_redirect_uri in URL object as registered in the client
/// - serverConfig: ServerConfig that OAuth2 Client will communicate to
/// - threshold: threshold in seconds to refresh access_token before it actually expires
@objc
public init (clientId: String, scope: String, redirectUri: URL, serverConfig: ServerConfig, threshold: Int = 60) {
public init (clientId: String, scope: String, redirectUri: URL, signoutRedirectUri: URL? = nil, serverConfig: ServerConfig, threshold: Int = 60) {

self.clientId = clientId
self.redirectUri = redirectUri
self.signoutRedirectUri = signoutRedirectUri
self.scope = scope
self.serverConfig = serverConfig
self.threshold = threshold
Expand Down Expand Up @@ -458,4 +462,30 @@ public class OAuth2Client: NSObject, Codable {
// Call /token service to exchange auth code to OAuth token set
return Request(url: self.serverConfig.tokenURL, method: .POST, headers: header, bodyParams: parameter, requestType: .urlEncoded, responseType: .json, timeoutInterval: self.serverConfig.timeout)
}

/// Builds /endSession request for an external user-agent based on given OAuth2 client information
/// - Parameters:
/// - idToken: OIDC id_token
/// - Returns: Request object
func buildEndSessionRequestForExternalAgent(idToken: String?) -> Request {
// Construct parameter for the request
var parameter: [String: String] = [:]
if let signoutRedirectUri = self.signoutRedirectUri {
parameter[OAuth2.postLogoutRedirectUri] = signoutRedirectUri.absoluteString
}
if let idToken, !idToken.isEmpty {
parameter[OAuth2.idTokenHint] = idToken
}

// AM 6.5.2 - 7.0.0
//
// Endpoint: /oauth2/realms/endSession
// API Version: resource=2.1,protocol=1.0

var header: [String: String] = [:]
header[OpenAM.acceptAPIVersion] = OpenAM.apiResource21 + "," + OpenAM.apiProtocol10

return Request(url: self.serverConfig.endSessionURL, method: .GET, headers: header, urlParams:parameter, requestType: .urlEncoded, responseType: .urlEncoded, timeoutInterval: self.serverConfig.timeout)
}

}
16 changes: 8 additions & 8 deletions FRAuth/FRAuth/Config/ServerConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// ServerConfig.swift
// FRAuth
//
// Copyright (c) 2019-2020 ForgeRock. All rights reserved.
// Copyright (c) 2019-2024 ForgeRock. All rights reserved.
//
// This software may be modified and distributed under the terms
// of the MIT license. See the LICENSE file for details.
Expand Down Expand Up @@ -95,43 +95,43 @@ public class ServerConfigBuilder: NSObject {

@objc
@discardableResult public func set(authenticatePath: String) -> ServerConfigBuilder {
self.config.authenticateURL = self.config.baseURL.absoluteString + authenticatePath
self.config.authenticateURL = authenticatePath.isValidUrl ? authenticatePath : self.config.baseURL.absoluteString + authenticatePath
return self
}

@objc
@discardableResult public func set(tokenPath: String) -> ServerConfigBuilder {
self.config.tokenURL = self.config.baseURL.absoluteString + tokenPath
self.config.tokenURL = tokenPath.isValidUrl ? tokenPath : self.config.baseURL.absoluteString + tokenPath
return self
}

@objc
@discardableResult public func set(authorizePath: String) -> ServerConfigBuilder {
self.config.authorizeURL = self.config.baseURL.absoluteString + authorizePath
self.config.authorizeURL = authorizePath.isValidUrl ? authorizePath : self.config.baseURL.absoluteString + authorizePath
return self
}

@objc
@discardableResult public func set(userInfoPath: String) -> ServerConfigBuilder {
self.config.userInfoURL = self.config.baseURL.absoluteString + userInfoPath
self.config.userInfoURL = userInfoPath.isValidUrl ? userInfoPath : self.config.baseURL.absoluteString + userInfoPath
return self
}

@objc
@discardableResult public func set(revokePath: String) -> ServerConfigBuilder {
self.config.tokenRevokeURL = self.config.baseURL.absoluteString + revokePath
self.config.tokenRevokeURL = revokePath.isValidUrl ? revokePath : self.config.baseURL.absoluteString + revokePath
return self
}

@objc
@discardableResult public func set(sessionPath: String) -> ServerConfigBuilder {
self.config.sessionURL = self.config.baseURL.absoluteString + sessionPath
self.config.sessionURL = sessionPath.isValidUrl ? sessionPath : self.config.baseURL.absoluteString + sessionPath
return self
}

@objc
@discardableResult public func set(endSessionPath: String) -> ServerConfigBuilder {
self.config.endSessionURL = self.config.baseURL.absoluteString + endSessionPath
self.config.endSessionURL = endSessionPath.isValidUrl ? endSessionPath : self.config.baseURL.absoluteString + endSessionPath
return self
}

Expand Down
5 changes: 3 additions & 2 deletions FRAuth/FRAuth/Constants/OAuth2.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// OAuth2.swift
// FRAuth
//
// Copyright (c) 2019-2020 ForgeRock. All rights reserved.
// Copyright (c) 2019-2024 ForgeRock. All rights reserved.
//
// This software may be modified and distributed under the terms
// of the MIT license. See the LICENSE file for details.
Expand All @@ -13,7 +13,8 @@ struct OAuth2 {
static let clientId = "client_id"
static let scope = "scope"
static let redirecUri = "redirect_uri"

static let postLogoutRedirectUri = "post_logout_redirect_uri"

static let csrf = "csrf"
static let decision = "decision"

Expand Down
8 changes: 5 additions & 3 deletions FRAuth/FRAuth/FRAuth.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// FRAuth.swift
// FRAuth
//
// Copyright (c) 2019-2023 ForgeRock. All rights reserved.
// Copyright (c) 2019-2024 ForgeRock. All rights reserved.
//
// This software may be modified and distributed under the terms
// of the MIT license. See the LICENSE file for details.
Expand Down Expand Up @@ -224,7 +224,9 @@ public final class FRAuth: NSObject {
if let thresholdConfigStr = config[FROptions.CodingKeys.oauthThreshold.rawValue] as? String, let timeOutConfigInt = Int(thresholdConfigStr) {
threshold = timeOutConfigInt
}


let signoutRedirectUri = URL(string: config[FROptions.CodingKeys.oauthSignoutRedirectUri.rawValue] as? String ?? "")

let serverConfig = configBuilder.build()
FRLog.v("ServerConfig created: \(serverConfig)")
var oAuth2Client: OAuth2Client?
Expand All @@ -235,7 +237,7 @@ public final class FRAuth: NSObject {
redirectUri.absoluteString.isValidUrl,
let scope = config[FROptions.CodingKeys.oauthScope.rawValue] as? String
{
oAuth2Client = OAuth2Client(clientId: clientId, scope: scope, redirectUri: redirectUri, serverConfig: serverConfig, threshold: threshold)
oAuth2Client = OAuth2Client(clientId: clientId, scope: scope, redirectUri: redirectUri, signoutRedirectUri: signoutRedirectUri, serverConfig: serverConfig, threshold: threshold)
FRLog.v("OAuth2Client created: \(String(describing: oAuth2Client))")
}
else {
Expand Down
Loading

0 comments on commit a0ec7cc

Please sign in to comment.