Skip to content

Instantly share code, notes, and snippets.

@KaQuMiQ
Last active September 28, 2020 15:04
Show Gist options
  • Select an option

  • Save KaQuMiQ/9fb9961b82779c950116f9fbb76478b0 to your computer and use it in GitHub Desktop.

Select an option

Save KaQuMiQ/9fb9961b82779c950116f9fbb76478b0 to your computer and use it in GitHub Desktop.
Easy auth for Google, Apple, Facebook
import AuthenticationServices
/// Container for function allowing third party authentication.
public struct Authorization {
private let authorization: (ASPresentationAnchor, @escaping (Result<AuthorizationCredential, AuthorizationError>) -> Void) -> Void
/// - parameter authorization: Function executed for authorization.
public init(_ authorization: @escaping (ASPresentationAnchor, @escaping (Result<AuthorizationCredential, AuthorizationError>) -> Void) -> Void) {
self.authorization = authorization
}
}
/// Credential returned from successfull authorization process.
public struct AuthorizationCredential {
public let token: String
}
/// Error returned from unsuccessfull authorization process.
public enum AuthorizationError: Error {
case missingCredential
case canceled
case undefined(Error)
}
public extension Authorization {
/// Execute authorization.
/// - parameter presentationAnchor: Anchor used to present required views.
/// - parameter callback: Callback executed on authorization process completion containing its result.
func authorize(using presentationAnchor: ASPresentationAnchor, _ callback: @escaping (Result<AuthorizationCredential, AuthorizationError>) -> Void) {
authorization(presentationAnchor, callback)
}
}
// MARK: - Apple
public extension Authorization {
/// You have to add reversed clientID (`clientID.split(separator: ".").reversed().joined(separator: ".")`)
/// to your app URL types for this to work.
/// - parameter scopes: Authorization scopes for service.
static func apple(
scopes: Set<ASAuthorization.Scope>
) -> Self {
Self { presentationAnchor, callback in
let authorizationController = AppleAuthorizationController(scopes: scopes, callback)
authorizationController.presentationContextProvider = presentationAnchor
authorizationController.start()
}
}
}
private final class AppleAuthorizationController: NSObject, ASAuthorizationControllerDelegate {
fileprivate let authorizationController: ASAuthorizationController
fileprivate let completion: (Result<AuthorizationCredential, AuthorizationError>) -> Void
fileprivate var presentationContextProvider: ASAuthorizationControllerPresentationContextProviding? {
get { authorizationController.presentationContextProvider }
set { authorizationController.presentationContextProvider = newValue }
}
fileprivate init(scopes: Set<ASAuthorization.Scope>, _ completion: @escaping (Result<AuthorizationCredential, AuthorizationError>) -> Void) {
self.completion = completion
let request = ASAuthorizationAppleIDProvider().createRequest()
request.requestedScopes = Array(scopes)
self.authorizationController = ASAuthorizationController(authorizationRequests: [request])
super.init()
self.authorizationController.delegate = self
}
fileprivate func start() {
authorizationController.performRequests()
}
fileprivate func authorizationController(
controller: ASAuthorizationController,
didCompleteWithAuthorization authorization: ASAuthorization
) {
if
let appleIDCredential = authorization.credential as? ASAuthorizationAppleIDCredential,
let token = appleIDCredential.identityToken.flatMap({ String.init(data: $0, encoding: .utf8) })
{
completion(.success(AuthorizationCredential(token: token)))
} else {
completion(.failure(.missingCredential))
}
}
fileprivate func authorizationController(
controller: ASAuthorizationController,
didCompleteWithError error: Error
) {
if case ASAuthorizationError.canceled = error {
completion(.failure(.canceled))
} else {
completion(.failure(.undefined(error)))
}
}
}
// MARK: - Google
public extension Authorization {
/// Register your app in google developers console in order to use google authorization.
/// You might have to add reversed clientID (`clientID.split(separator: ".").reversed().joined(separator: ".")`)
/// to your app URL types for this to work.
/// - warning: In order to get full google JWT you might have to continue the process.
/// - parameter clientID: Google client ID.
/// - parameter scopes: Authorization scopes for service.
static func google(
clientID: String,
scopes: Set<ASAuthorization.Scope>
) -> Self {
guard var urlComponents = URLComponents(string: "https://accounts.google.com/o/oauth2/v2/auth")
else { fatalError("Impossible") }
let scopes = scopes.map(\.rawValue).joined(separator: " ")
let callbackScheme = clientID.split(separator: ".").reversed().joined(separator: ".")
urlComponents.queryItems = [
URLQueryItem(name: "client_id", value: clientID),
URLQueryItem(name: "redirect_uri", value: "\(callbackScheme)://"),
URLQueryItem(name: "response_type", value: "code"),
URLQueryItem(name: "scope", value: scopes)
]
guard let url = urlComponents.url
else { fatalError("Invalid google auth url components: \(urlComponents)") }
return Self { presentationAnchor, callback in
let session = ASWebAuthenticationSession(
url: url,
callbackURLScheme: callbackScheme
) { url, error in
if case .some(ASWebAuthenticationSessionError.canceledLogin) = error {
callback(.failure(.canceled))
} else if let error = error {
callback(.failure(.undefined(error)))
} else if
let urlComponents = (url?.absoluteString).flatMap(URLComponents.init(string:)),
let code = urlComponents.queryItems?.first(where: { $0.name == "code" })?.value
{
callback(.success(AuthorizationCredential(token: code)))
} else {
callback(.failure(.missingCredential))
}
}
session.presentationContextProvider = presentationAnchor
session.start()
}
}
}
// MARK: - Facebook
public extension Authorization {
/// Register your app in facebook developers console in order to use facebook authorization.
/// You might have to add your clientID with "fb" prefix (`"fb\(clientID)"`) to your app URL types for this to work.
/// - warning: Facebook requires using their SDK by its platform policy, see: https://developers.facebook.com/policy/
/// - parameter clientID: Facebook client ID.
/// - parameter scopes: Authorization scopes for service.
static func facebook(
clientID: String,
scopes: Set<ASAuthorization.Scope>
) -> Self {
guard var urlComponents = URLComponents(string: "https://www.facebook.com/v7.0/dialog/oauth")
else { fatalError("Impossible") }
let scopes = scopes.map(\.rawValue).joined(separator: ",")
let callbackScheme = "fb\(clientID)"
urlComponents.queryItems = [
URLQueryItem(name: "client_id", value: clientID),
URLQueryItem(name: "redirect_uri", value: "\(callbackScheme)://authorize"),
URLQueryItem(name: "response_type", value: "token"),
URLQueryItem(name: "scope", value: scopes)
]
guard let url = urlComponents.url
else { fatalError("Invalid facebook auth url components: \(urlComponents)") }
return Self { presentationAnchor, callback in
let session = ASWebAuthenticationSession(
url: url,
callbackURLScheme: callbackScheme
) { url, error in
if case .some(ASWebAuthenticationSessionError.canceledLogin) = error {
callback(.failure(.canceled))
} else if let error = error {
callback(.failure(.undefined(error)))
} else if
let urlComponents = (url?.absoluteString).flatMap(URLComponents.init(string:)),
let token = urlComponents.queryItems?.first(where: { $0.name == "access_token" })?.value
{
callback(.success(AuthorizationCredential(token: token)))
} else {
callback(.failure(.missingCredential))
}
}
session.presentationContextProvider = presentationAnchor
session.start()
}
}
}
// MARK: - Mock
public extension Authorization {
/// Mock authorization which always succeeds with provided credential.
/// Callback is executed immediatetly.
static func always(_ credential: AuthorizationCredential) -> Self {
Self { _, callback in callback(.success(credential)) }
}
/// Mock authorization which always fails with provided error.
/// Callback is executed immediatetly.
static func always(_ error: AuthorizationError) -> Self {
Self { _, callback in callback(.failure(error)) }
}
}
import SwiftUI
import UIKit
import AuthenticationServices
/// Presentation anchor for authorization process in SwiftUI.
/// You can initialize it as your view's private property to and embed it anywhere inside that view.
/// Best way to do so is to use `ZStack` along button (or other control) used to trigger the action.
/// It is not visible, it only provides required context for presenting authorization.
/// Use `authorize(_:)` method to execute authorization process.
public struct AuthorizationPresentationAnchor: UIViewRepresentable {
public typealias UIViewType = UIView
private let view: UIViewType = {
let view = UIView(frame: .zero)
view.backgroundColor = .clear
view.alpha = 0
view.isUserInteractionEnabled = false
return view
}()
private let authorization: Authorization
private var presentationAnchor: ASPresentationAnchor { view.window ?? UIWindow() }
public init(authorization: Authorization) {
self.authorization = authorization
}
public func makeUIView(context _: Context) -> UIViewType { view }
public func updateUIView(_: UIViewType, context _: Context) {}
/// Begin authorization process.
/// - parameter callback: Callback executed when authorization finishes.
public func authorize(_ callback: @escaping (Result<AuthorizationCredential, AuthorizationError>) -> Void) {
authorization.authorize(using: presentationAnchor, callback)
}
}
// MARK: - Anchor
extension ASPresentationAnchor: ASWebAuthenticationPresentationContextProviding {
public func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
self
}
}
extension ASPresentationAnchor: ASAuthorizationControllerPresentationContextProviding {
public func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor {
self
}
}
@KaQuMiQ
Copy link
Author

KaQuMiQ commented Sep 23, 2020

Example:

Authentication
    .google(
        clientID: "yourID_123123.apps.googleusercontent.com",
        scope: [.email]
    )
    .authenticate(using: UIApplication.shared.keyWindow!) { result in
        print(result)
    }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment