Skip to content

Commit

Permalink
Add support for HTTPS callbacks [SDK-4749] (#832)
Browse files Browse the repository at this point in the history
  • Loading branch information
Widcket committed Feb 29, 2024
1 parent eb9eea5 commit f051007
Show file tree
Hide file tree
Showing 7 changed files with 191 additions and 82 deletions.
58 changes: 44 additions & 14 deletions Auth0/ASProvider.swift
Original file line number Diff line number Diff line change
@@ -1,38 +1,68 @@
#if WEB_AUTH_PLATFORM
import AuthenticationServices

typealias ASHandler = ASWebAuthenticationSession.CompletionHandler

extension WebAuthentication {

static func asProvider(urlScheme: String, ephemeralSession: Bool = false) -> WebAuthProvider {
static func asProvider(redirectURL: URL, ephemeralSession: Bool = false) -> WebAuthProvider {
return { url, callback in
let session = ASWebAuthenticationSession(url: url, callbackURLScheme: urlScheme) {
guard let callbackURL = $0, $1 == nil else {
if let error = $1, case ASWebAuthenticationSessionError.canceledLogin = error {
return callback(.failure(WebAuthError(code: .userCancelled)))
} else if let error = $1 {
return callback(.failure(WebAuthError(code: .other, cause: error)))
}

return callback(.failure(WebAuthError(code: .unknown("ASWebAuthenticationSession failed"))))
}
let session: ASWebAuthenticationSession

_ = TransactionStore.shared.resume(callbackURL)
#if compiler(>=5.10)
if #available(iOS 17.4, macOS 14.4, *) {
if redirectURL.scheme == "https" {
session = ASWebAuthenticationSession(url: url,
callback: .https(host: redirectURL.host!,
path: redirectURL.path),
completionHandler: completionHandler(callback))
} else {
session = ASWebAuthenticationSession(url: url,
callback: .customScheme(redirectURL.scheme!),
completionHandler: completionHandler(callback))
}
} else {
session = ASWebAuthenticationSession(url: url,
callbackURLScheme: redirectURL.scheme,
completionHandler: completionHandler(callback))
}
#else
session = ASWebAuthenticationSession(url: url,
callbackURLScheme: redirectURL.scheme,
completionHandler: completionHandler(callback))
#endif

session.prefersEphemeralWebBrowserSession = ephemeralSession

return ASUserAgent(session: session, callback: callback)
}
}

static let completionHandler: (_ callback: @escaping WebAuthProviderCallback) -> ASHandler = { callback in
return {
guard let callbackURL = $0, $1 == nil else {
if let error = $1 as? NSError,
error.userInfo.isEmpty,
case ASWebAuthenticationSessionError.canceledLogin = error {
return callback(.failure(WebAuthError(code: .userCancelled)))
} else if let error = $1 {
return callback(.failure(WebAuthError(code: .other, cause: error)))
}

return callback(.failure(WebAuthError(code: .unknown("ASWebAuthenticationSession failed"))))
}

_ = TransactionStore.shared.resume(callbackURL)
}
}
}

class ASUserAgent: NSObject, WebAuthUserAgent {

let session: ASWebAuthenticationSession
let callback: (WebAuthResult<Void>) -> Void
let callback: WebAuthProviderCallback

init(session: ASWebAuthenticationSession, callback: @escaping (WebAuthResult<Void>) -> Void) {
init(session: ASWebAuthenticationSession, callback: @escaping WebAuthProviderCallback) {
self.session = session
self.callback = callback
super.init()
Expand Down
35 changes: 25 additions & 10 deletions Auth0/Auth0WebAuth.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ final class Auth0WebAuth: WebAuth {
private let responseType = "code"

private(set) var parameters: [String: String] = [:]
private(set) var https = false
private(set) var ephemeralSession = false
private(set) var issuer: String
private(set) var leeway: Int = 60 * 1000 // Default leeway is 60 seconds
Expand All @@ -35,15 +36,26 @@ final class Auth0WebAuth: WebAuth {
}

lazy var redirectURL: URL? = {
guard let bundleIdentifier = Bundle.main.bundleIdentifier,
let domain = self.url.host,
let baseURL = URL(string: "\(bundleIdentifier)://\(domain)") else { return nil }
guard let bundleID = Bundle.main.bundleIdentifier, let domain = self.url.host else { return nil }
let scheme: String

#if compiler(>=5.10)
if #available(iOS 17.4, macOS 14.4, *) {
scheme = https ? "https" : bundleID
} else {
scheme = bundleID
}
#else
scheme = bundleID
#endif

guard let baseURL = URL(string: "\(scheme)://\(domain)") else { return nil }
var components = URLComponents(url: baseURL, resolvingAgainstBaseURL: true)

return components?.url?
.appendingPathComponent(self.url.path)
.appendingPathComponent(self.platform)
.appendingPathComponent(bundleIdentifier)
.appendingPathComponent(bundleID)
.appendingPathComponent("callback")
}()

Expand Down Expand Up @@ -115,6 +127,11 @@ final class Auth0WebAuth: WebAuth {
return self
}

func useHTTPS() -> Self {
self.https = true
return self
}

func useEphemeralSession() -> Self {
self.ephemeralSession = true
return self
Expand All @@ -141,7 +158,7 @@ final class Auth0WebAuth: WebAuth {
}

func start(_ callback: @escaping (WebAuthResult<Credentials>) -> Void) {
guard let redirectURL = self.redirectURL, let urlScheme = redirectURL.scheme else {
guard let redirectURL = self.redirectURL else {
return callback(.failure(WebAuthError(code: .noBundleIdentifier)))
}

Expand All @@ -166,7 +183,7 @@ final class Auth0WebAuth: WebAuth {
state: state,
organization: organization,
invitation: invitation)
let provider = self.provider ?? WebAuthentication.asProvider(urlScheme: urlScheme,
let provider = self.provider ?? WebAuthentication.asProvider(redirectURL: redirectURL,
ephemeralSession: ephemeralSession)
let userAgent = provider(authorizeURL) { [storage, onCloseCallback] result in
storage.clear()
Expand Down Expand Up @@ -199,13 +216,11 @@ final class Auth0WebAuth: WebAuth {
let queryItems = components?.queryItems ?? []
components?.queryItems = queryItems + [returnTo, clientId]

guard let logoutURL = components?.url,
let redirectURL = self.redirectURL,
let urlScheme = redirectURL.scheme else {
guard let logoutURL = components?.url, let redirectURL = self.redirectURL else {
return callback(.failure(WebAuthError(code: .noBundleIdentifier)))
}

let provider = self.provider ?? WebAuthentication.asProvider(urlScheme: urlScheme)
let provider = self.provider ?? WebAuthentication.asProvider(redirectURL: redirectURL)
let userAgent = provider(logoutURL) { [storage] result in
storage.clear()
callback(result)
Expand Down
4 changes: 2 additions & 2 deletions Auth0/SafariProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,9 @@ extension SFSafariViewController {
class SafariUserAgent: NSObject, WebAuthUserAgent {

let controller: SFSafariViewController
let callback: ((WebAuthResult<Void>) -> Void)
let callback: WebAuthProviderCallback

init(controller: SFSafariViewController, callback: @escaping (WebAuthResult<Void>) -> Void) {
init(controller: SFSafariViewController, callback: @escaping WebAuthProviderCallback) {
self.controller = controller
self.callback = callback
super.init()
Expand Down
18 changes: 17 additions & 1 deletion Auth0/WebAuth.swift
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
// swiftlint:disable file_length

#if WEB_AUTH_PLATFORM
import Foundation
import Combine

/// Callback invoked by the ``WebAuthUserAgent`` when the web-based operation concludes.
public typealias WebAuthProviderCallback = (WebAuthResult<Void>) -> Void

/// Thunk that returns a function that creates and returns a ``WebAuthUserAgent`` to perform a web-based operation.
/// The ``WebAuthUserAgent`` opens the URL in an external user agent and then invokes the callback when done.
///
/// ## See Also
///
/// - [Example](https://github.com/auth0/Auth0.swift/blob/master/Auth0/SafariProvider.swift)
public typealias WebAuthProvider = (_ url: URL, _ callback: @escaping (WebAuthResult<Void>) -> Void) -> WebAuthUserAgent
public typealias WebAuthProvider = (_ url: URL, _ callback: @escaping WebAuthProviderCallback) -> WebAuthUserAgent

/// Web-based authentication using Auth0.
///
Expand Down Expand Up @@ -127,6 +132,17 @@ public protocol WebAuth: Trackable, Loggable {
/// - Returns: The same `WebAuth` instance to allow method chaining.
func maxAge(_ maxAge: Int) -> Self

/// Use `https` as the scheme for the redirect URL on iOS 17.4+ and macOS 14.4+. On older versions of iOS and
/// macOS, the bundle identifier of the app will be used as a custom scheme.
///
/// - Returns: The same `WebAuth` instance to allow method chaining.
/// - Requires: An Associated Domain configured with the `webcredentials` service type. For example,
/// `webcredentials:example.com`. If you're using a custom domain on your Auth0 tenant, use this domain as the
/// Associated Domain. Otherwise, use the domain of your Auth0 tenant.
/// - Note: Don't use this method along with ``provider(_:)``. Use either one or the other, because this
/// method will only work with the default `ASWebAuthenticationSession` implementation.
func useHTTPS() -> Self

/// Use a private browser session to avoid storing the session cookie in the shared cookie jar.
/// Using this method will disable single sign-on (SSO).
///
Expand Down
18 changes: 11 additions & 7 deletions Auth0Tests/ASProviderSpec.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import Nimble

@testable import Auth0

private let Url = URL(string: "https://auth0.com")!
private let AuthorizeURL = URL(string: "https://auth0.com")!
private let HTTPSRedirectURL = URL(string: "https://auth0.com/callback")!
private let CustomSchemeRedirectURL = URL(string: "com.auth0.example://samples.auth0.com/callback")!
private let Timeout: NimbleTimeInterval = .seconds(2)

class ASProviderSpec: QuickSpec {
Expand All @@ -16,7 +18,7 @@ class ASProviderSpec: QuickSpec {
var userAgent: ASUserAgent!

beforeEach {
session = ASWebAuthenticationSession(url: Url, callbackURLScheme: nil, completionHandler: { _, _ in })
session = ASWebAuthenticationSession(url: AuthorizeURL, callbackURLScheme: nil, completionHandler: { _, _ in })
userAgent = ASUserAgent(session: session, callback: { _ in })
}

Expand All @@ -27,20 +29,22 @@ class ASProviderSpec: QuickSpec {
describe("WebAuthentication extension") {

it("should create a web authentication session provider") {
let provider = WebAuthentication.asProvider(urlScheme: Url.scheme!)
expect(provider(Url, {_ in })).to(beAKindOf(ASUserAgent.self))
let provider = WebAuthentication.asProvider(redirectURL: HTTPSRedirectURL)
expect(provider(AuthorizeURL, {_ in })).to(beAKindOf(ASUserAgent.self))
}

it("should not use an ephemeral session by default") {
userAgent = WebAuthentication.asProvider(urlScheme: Url.scheme!)(Url, { _ in }) as? ASUserAgent
let provider = WebAuthentication.asProvider(redirectURL: CustomSchemeRedirectURL)
userAgent = provider(AuthorizeURL, { _ in }) as? ASUserAgent
expect(userAgent.session.prefersEphemeralWebBrowserSession) == false
}

it("should use an ephemeral session") {
userAgent = WebAuthentication.asProvider(urlScheme: Url.scheme!,
ephemeralSession: true)(Url, { _ in }) as? ASUserAgent
let provider = WebAuthentication.asProvider(redirectURL: CustomSchemeRedirectURL, ephemeralSession: true)
userAgent = provider(AuthorizeURL, { _ in }) as? ASUserAgent
expect(userAgent.session.prefersEphemeralWebBrowserSession) == true
}

}

describe("user agent") {
Expand Down

0 comments on commit f051007

Please sign in to comment.