Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for HTTPS callbacks [SDK-4749] #832

Merged
merged 3 commits into from
Feb 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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)))

Check warning on line 49 in Auth0/ASProvider.swift

View check run for this annotation

Codecov / codecov/patch

Auth0/ASProvider.swift#L49

Added line #L49 was not covered by tests
}

Check warning on line 51 in Auth0/ASProvider.swift

View check run for this annotation

Codecov / codecov/patch

Auth0/ASProvider.swift#L51

Added line #L51 was not covered by tests

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these bogus code coverage problems? It seems to be complaining about a blank line not being covered.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is due to partial coverage in this line:

Screenshot 2024-02-29 at 11 06 46

Which isn't even relevant anymore, as with Xcode 15 URLComponents and URL now have the same parsing behavior: https://forums.swift.org/t/url-string-behavior-changed-with-xcode-15-0-beta-5/66570

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

_ = TransactionStore.shared.resume(callbackURL)

Check warning on line 55 in Auth0/ASProvider.swift

View check run for this annotation

Codecov / codecov/patch

Auth0/ASProvider.swift#L54-L55

Added lines #L54 - L55 were not covered by tests
}
}
}

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