Instantly share code, notes, and snippets.
Last active
September 28, 2020 15:04
-
Star
0
(0)
You must be signed in to star a gist -
Fork
0
(0)
You must be signed in to fork a gist
-
-
Save KaQuMiQ/9fb9961b82779c950116f9fbb76478b0 to your computer and use it in GitHub Desktop.
Easy auth for Google, Apple, Facebook
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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)) } | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | |
| } | |
| } |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Example: