From 83bc8eeaad7f49ebdca8be99389e66b558ed1ae4 Mon Sep 17 00:00:00 2001 From: Ethella Date: Thu, 20 Jan 2022 19:44:02 -0800 Subject: [PATCH] Migrated from fortmatic org --- .gitignore | 8 + LICENSE | 12 +- MagicExt-OAuth.podspec | 27 ++++ Package.swift | 30 ++++ README.md | 44 +++++- .../MagicExt-OAuth/Core/OAuthExtension.swift | 141 ++++++++++++++++++ .../Core/ShimASViewController.swift | 60 ++++++++ .../Core/ShimSFASViewController.swift | 48 ++++++ .../Types/OAuthConfiguration.swift | 37 +++++ .../MagicExt-OAuth/Types/OAuthMethod.swift | 12 ++ .../Types/OAuthRedirectError.swift | 16 ++ .../MagicExt-OAuth/Types/OAuthResponse.swift | 27 ++++ Sources/MagicExt-OAuth/Types/OIDType.swift | 47 ++++++ .../MagicExt-OAuth/Utils/OAuthChallenge.swift | 51 +++++++ .../MagicExt_OAuthTests.swift | 7 + 15 files changed, 558 insertions(+), 9 deletions(-) create mode 100644 MagicExt-OAuth.podspec create mode 100644 Package.swift create mode 100644 Sources/MagicExt-OAuth/Core/OAuthExtension.swift create mode 100644 Sources/MagicExt-OAuth/Core/ShimASViewController.swift create mode 100644 Sources/MagicExt-OAuth/Core/ShimSFASViewController.swift create mode 100644 Sources/MagicExt-OAuth/Types/OAuthConfiguration.swift create mode 100644 Sources/MagicExt-OAuth/Types/OAuthMethod.swift create mode 100644 Sources/MagicExt-OAuth/Types/OAuthRedirectError.swift create mode 100644 Sources/MagicExt-OAuth/Types/OAuthResponse.swift create mode 100644 Sources/MagicExt-OAuth/Types/OIDType.swift create mode 100644 Sources/MagicExt-OAuth/Utils/OAuthChallenge.swift create mode 100644 Tests/MagicExt-OAuthTests/MagicExt_OAuthTests.swift diff --git a/.gitignore b/.gitignore index 330d167..3e5c3a1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,10 @@ +.DS_Store +/.build +/Packages +/*.xcodeproj +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata + +Package.resolved # Xcode # # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore @@ -88,3 +95,4 @@ fastlane/test_output # https://github.com/johnno1962/injectionforxcode iOSInjectionProject/ + diff --git a/LICENSE b/LICENSE index 5798434..c586215 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,4 @@ -MIT License - -Copyright (c) 2022 Magic +Copyright (c) 2020 Magic Labs Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -9,13 +7,13 @@ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/MagicExt-OAuth.podspec b/MagicExt-OAuth.podspec new file mode 100644 index 0000000..53705f5 --- /dev/null +++ b/MagicExt-OAuth.podspec @@ -0,0 +1,27 @@ +# +# Local Podspec for building local target +# + +Pod::Spec.new do |s| + s.name = 'MagicExt-OAuth' + s.version = '1.0.0' + s.summary = 'Magic IOS Extension - OAuth' + + s.description = <<-DESC +TODO: Add long description of the pod here. + DESC + + s.homepage = 'https://github.com/magicLabs/magic-ios-ext' + s.license = { :type => 'MIT', :file => 'LICENSE' } + s.author = { 'Jerry Liu' => 'jerry@magic.link' } + s.source = { :git => 'https://github.com/magicLabs/magic-ios-ext.git', :tag => s.version.to_s } + s.swift_version = '5.0' + s.ios.deployment_target = '10.0' +# s.osx.deployment_target = '10.12' + + s.source_files = 'Sources/MagicExt-OAuth/**/*' + + s.dependency 'MagicSDK', '~> 3.0' + + s.pod_target_xcconfig = { 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'arm64' } +end diff --git a/Package.swift b/Package.swift new file mode 100644 index 0000000..833ce92 --- /dev/null +++ b/Package.swift @@ -0,0 +1,30 @@ +// swift-tools-version:5.5 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "MagicExt-OAuth", + + products: [ + // Products define the executables and libraries a package produces, and make them visible to other packages. + .library( + name: "MagicExt-OAuth", + targets: ["MagicExt-OAuth"]), + ], + dependencies: [ + .package(url: "https://github.com/magiclabs/magic-ios.git", from:"3.0.0"), + ], + targets: [ + // Targets are the basic building blocks of a package. A target can define a module or a test suite. + // Targets can depend on other targets in this package, and on products in packages this package depends on. + .target( + name: "MagicExt-OAuth", + dependencies: [ + .product(name: "MagicSDK", package: "magic-ios"), + ]), + .testTarget( + name: "MagicExt-OAuthTests", + dependencies: ["MagicExt-OAuth"]), + ] +) diff --git a/README.md b/README.md index c164dc3..9e7032c 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,42 @@ -# magic-ios-ext -Magic IOS Extension libraries +# MagicExt-OAuth +[![CI Status](https://img.shields.io/travis/Ethella/MagicExt-OAuth.svg?style=flat)](https://travis-ci.org/Ethella/MagicExt-OAuth) +[![Version](https://img.shields.io/cocoapods/v/MagicExt-OAuth.svg?style=flat)](https://cocoapods.org/pods/MagicExt-OAuth) +[![License](https://img.shields.io/cocoapods/l/MagicExt-OAuth.svg?style=flat)](https://cocoapods.org/pods/MagicExt-OAuth) +[![Platform](https://img.shields.io/cocoapods/p/MagicExt-OAuth.svg?style=flat)](https://cocoapods.org/pods/MagicExt-OAuth) + +Cocoapods +--- +## Set up the local development env +1. To start the demo app with local development SDK, download following projects +```bash +# demo app +$ git clone https://github.com/magiclabs/magic-ios-demo +# ios SDK +$ git clone https://github.com/magiclabs/magic-ios +$ git clone https://github.com/magiclabs/magic-ios-ext +``` + +2. To enable the demo use the local development SDK. Navigate to `magic-ios-demo/Podfile` and edit the following lines. + This will make pod file install local dependencies instead of the ones distributed. + +```ruby +# Distributed Library on Cocoapods +# pod 'MagicSDK', '~> 3.0' +# pod 'MagicExt-OAuth', '~> 1.0' + +# Local development library +pod 'MagicSDK', :path => '../magic-ios/MagicSDK.podspec' +pod 'MagicExt-OAuth', :path => '../magic-ios-ext/MagicExt-OAuth.podspec' +``` + +```bash +$ cd /YOUR/PATH/TO/magic-ios-demo + +# Install dependencies +$ pod install +``` + +3. Open `/YOUR/PATH/TO/magic-ios-demo/magic-ios-demo.xcworkspace` with XCode and try it out! + +--- + diff --git a/Sources/MagicExt-OAuth/Core/OAuthExtension.swift b/Sources/MagicExt-OAuth/Core/OAuthExtension.swift new file mode 100644 index 0000000..633a5d2 --- /dev/null +++ b/Sources/MagicExt-OAuth/Core/OAuthExtension.swift @@ -0,0 +1,141 @@ +// +// OauthModule.swift +// MagicSDK +// +// Created by Wentao Liu on 9/16/20. +// + +import Foundation +import AuthenticationServices +import SafariServices +import MagicSDK_Web3 +import MagicSDK +import PromiseKit + +public class OAuthExtension: BaseModule { + + public enum OAuthExtensionError: Swift.Error { + case parseSuccessURLError(url: String) + case unsupportedVersions + case userDeniedAccess(Swift.Error) + case unableToStartPopup + } + + public func loginWithPopup (_ configuration: OAuthConfiguration) -> Promise { + return Promise { resolver in + loginWithPopup(configuration, response: promiseResolver(resolver)) + } + } + + public func loginWithPopup (_ configuration: OAuthConfiguration, response: @escaping Web3ResponseCompletion) { + let oauthChallenge = OAuthChallenge() + + // Construct OAuth URL + var components = URLComponents() + components.scheme = "https" + components.host = "auth.magic.link" +// components.scheme = "http" +// components.host = "192.168.0.106" +// components.port = 3014 + components.path = "/v1/oauth2/\(configuration.provider.rawValue.lowercased())/start" + + components.queryItems = [ + URLQueryItem(name: "magic_api_key", value: self.provider.urlBuilder.apiKey), + URLQueryItem(name: "magic_challenge", value: oauthChallenge.challenge), + URLQueryItem(name: "state", value: oauthChallenge.state), + URLQueryItem(name: "redirect_uri", value: configuration.redirectURI), + URLQueryItem(name: "platform", value: "rn") + ] + + if let scope = configuration.scope { + if scope.count > 0 { + components.queryItems?.append(URLQueryItem(name: "scope", value: scope.joined(separator: " "))) + } + } + + if let loginHint = configuration.loginHint { + components.queryItems?.append(URLQueryItem(name: "login_hint", value: loginHint)) + } + + let authURL = components.url + + + + firstly { + // Pop Authentication Session + createAuthenticationSession(authURL: authURL, configuration: configuration) + }.done {successURL -> Void in + + // Remove Percentage Encode to prevent double encoding + guard let query = URL(string:successURL)?.query?.removingPercentEncoding else { + throw OAuthExtensionError.parseSuccessURLError(url: successURL) + } + + // send credential to auth relayer to authenticate + let request = RPCRequest<[String]>(method: OAuthMethod.magic_oauth_parse_redirect_result.rawValue, params: [ "?\(query)", oauthChallenge.verifier, oauthChallenge.state]) + self.provider.send(request: request, response: response) + }.catch { error in + let errResponse = Web3Response(error: OAuthExtensionError.userDeniedAccess(error)) + response(errResponse) +// handleRollbarError(error, log: false) + } + } + + private func createAuthenticationSession(authURL: URL?, configuration: OAuthConfiguration) -> Promise { + + // Remove "://" from app schemes to prevent error + let callbackURLScheme = configuration.redirectURI.replacingOccurrences(of: "://", with: "", options: NSString.CompareOptions.literal, range: nil) + + return Promise { resolver in + + // find topmost view controller from the hierarchy and attach modal Controller to it + guard let keyWindow = UIApplication.shared.windows.filter({$0.isKeyWindow}).first else { + return resolver.reject(OAuthExtensionError.unableToStartPopup) + } + + if var topController = keyWindow.rootViewController { + while let presentedVC = topController.presentedViewController { + topController = presentedVC + } + if #available(iOS 12, *) { + let shimVC = ShimASViewController() + shimVC.source = authURL + shimVC.callbackURL = callbackURLScheme + shimVC.resolver = resolver + topController.present(shimVC, animated: true) + } else if #available(iOS 11.0, *) { + let shimVC = ShimSFASViewController() + shimVC.source = authURL + shimVC.callbackURL = callbackURLScheme + shimVC.resolver = resolver + topController.present(shimVC, animated: true) + } else { + resolver.reject(OAuthExtensionError.unsupportedVersions) + } + } else { + return resolver.reject(OAuthExtensionError.unableToStartPopup) + } + + } + } +} + +extension Magic { + public var oauth: OAuthExtension { + return OAuthExtension(rpcProvider: self.rpcProvider) + } +} + +// Handles Specific OAuthError +extension Web3Response { + public var magicExtOAuthError: OAuthExtension.OAuthExtensionError? { + switch self.status { + case .failure(let error): + return error as? OAuthExtension.OAuthExtensionError + case .success: + return nil + @unknown default: + return nil + } + } +} diff --git a/Sources/MagicExt-OAuth/Core/ShimASViewController.swift b/Sources/MagicExt-OAuth/Core/ShimASViewController.swift new file mode 100644 index 0000000..884648f --- /dev/null +++ b/Sources/MagicExt-OAuth/Core/ShimASViewController.swift @@ -0,0 +1,60 @@ +// +// FortmaticShimViewController.swift +// Fortmatic +// +// Created by Wentao Liu on 2/3/20. +// + +import Foundation +import AuthenticationServices +import SafariServices +import MagicSDK +import PromiseKit + +@available(iOS 12.0, *) +class ShimASViewController: UIViewController, ASWebAuthenticationPresentationContextProviding +{ + var authSession: ASWebAuthenticationSession? + + /// X source url + var source:URL? + + /// callback URL scheme + var callbackURL:String! + + /// resolver + var resolver:Resolver? + + func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor { + // Perhaps I don't need the window object at all, and can just use: + // return ASPresentationAnchor() + return UIApplication.shared.keyWindow ?? ASPresentationAnchor() + } + + override func viewDidLoad() { + + + //tries ASWebAuthenticationSession + authSession = ASWebAuthenticationSession.init(url: source!, callbackURLScheme: callbackURL, completionHandler: { (callBack:URL?, error:Error?) in + + //auto close VC after popup is closed + DispatchQueue.main.async { + self.dismiss(animated: true) + } + + // handle response + guard error == nil, let successURL = callBack else { + self.resolver?.reject(error!) + return + } + + self.resolver?.fulfill(successURL.absoluteString) + }) + + if #available(iOS 13, *){ + authSession?.presentationContextProvider = self + } + + authSession?.start() + } +} diff --git a/Sources/MagicExt-OAuth/Core/ShimSFASViewController.swift b/Sources/MagicExt-OAuth/Core/ShimSFASViewController.swift new file mode 100644 index 0000000..b517344 --- /dev/null +++ b/Sources/MagicExt-OAuth/Core/ShimSFASViewController.swift @@ -0,0 +1,48 @@ +// +// ShimSFViewController.swift +// MagicExt-OAuth +// +// Created by Wentao Liu on 9/28/20. +// + +import Foundation +import SafariServices +import MagicSDK +import PromiseKit + +@available(iOS 11.0, *) +class ShimSFASViewController: UIViewController +{ + var authSession: SFAuthenticationSession? + + /// X source url + var source:URL? + + /// callback URL scheme + var callbackURL:String! + + /// resolver + var resolver: Resolver? + + override func viewDidLoad() { + + + //tries ASWebAuthenticationSession + authSession = SFAuthenticationSession.init(url: source!, callbackURLScheme: callbackURL, completionHandler: { (callBack:URL?, error:Error?) in + + //auto close VC after popup is closed + self.dismiss(animated: true) + + // handle response + guard error == nil, let successURL = callBack else { + self.resolver?.reject(error!) + return + } + + // Resolve data back to send + self.resolver?.fulfill(successURL.absoluteString) + }) + + authSession?.start() + } +} diff --git a/Sources/MagicExt-OAuth/Types/OAuthConfiguration.swift b/Sources/MagicExt-OAuth/Types/OAuthConfiguration.swift new file mode 100644 index 0000000..00b5fec --- /dev/null +++ b/Sources/MagicExt-OAuth/Types/OAuthConfiguration.swift @@ -0,0 +1,37 @@ +// +// OAuthConfiguration.swift +// MagicExt-OAuth +// +// Created by Wentao Liu on 9/23/20. +// + +import Foundation +import MagicSDK + +public struct OAuthConfiguration: BaseConfiguration { + public var provider: OAuthProvider + public var redirectURI: String + public var scope: [String]? + public var loginHint: String? + + public init(provider: OAuthProvider, redirectURI: String, scope: [String]? = nil, loginHint: String? = nil) { + self.provider = provider + self.redirectURI = redirectURI + self.scope = scope + self.loginHint = loginHint + } +} + +public enum OAuthProvider: String, CaseIterable, Codable { + case GOOGLE + case FACEBOOK + case GITHUB + case APPLE + case LINKEDIN + case BITBUCKET + case GITLAB + case TWITTER + case DISCORD + case TWITCH + case MICROSOFT +} diff --git a/Sources/MagicExt-OAuth/Types/OAuthMethod.swift b/Sources/MagicExt-OAuth/Types/OAuthMethod.swift new file mode 100644 index 0000000..5d24dc2 --- /dev/null +++ b/Sources/MagicExt-OAuth/Types/OAuthMethod.swift @@ -0,0 +1,12 @@ +// +// OAuthMethod.swift +// MagicExt-OAuth +// +// Created by Wentao Liu on 9/29/20. +// + +import Foundation + +internal enum OAuthMethod: String, CaseIterable { + case magic_oauth_parse_redirect_result +} diff --git a/Sources/MagicExt-OAuth/Types/OAuthRedirectError.swift b/Sources/MagicExt-OAuth/Types/OAuthRedirectError.swift new file mode 100644 index 0000000..4f39f20 --- /dev/null +++ b/Sources/MagicExt-OAuth/Types/OAuthRedirectError.swift @@ -0,0 +1,16 @@ +// +// OAuthRedirectError.swift +// MagicExt-OAuth +// +// Created by Wentao Liu on 6/25/21. +// + +import Foundation +import MagicSDK + +public struct OAuthRedirectError: MagicResponse { + public let provider: OAuthProvider.RawValue + public let error: String + public let error_description: String + public let error_uri: String? +} diff --git a/Sources/MagicExt-OAuth/Types/OAuthResponse.swift b/Sources/MagicExt-OAuth/Types/OAuthResponse.swift new file mode 100644 index 0000000..1f6fbf4 --- /dev/null +++ b/Sources/MagicExt-OAuth/Types/OAuthResponse.swift @@ -0,0 +1,27 @@ +// +// OAuthResult.swift +// MagicExt-OAuth +// +// Created by Wentao Liu on 9/29/20. +// + +import Foundation +import MagicSDK + +public struct OAuthResponse: MagicResponse { + public let oauth: OauthPartialResult + public let magic: MagicPartialResult +} + +public struct OauthPartialResult: Codable { + public let provider: String; + public let scope: [String]; + public let accessToken: String; + public let userHandle: String; + public let userInfo: OpenIDConnectProfile; + +} +public struct MagicPartialResult: Codable { + public let idToken: String; + public let userMetadata: UserMetadata; +} diff --git a/Sources/MagicExt-OAuth/Types/OIDType.swift b/Sources/MagicExt-OAuth/Types/OIDType.swift new file mode 100644 index 0000000..c86393a --- /dev/null +++ b/Sources/MagicExt-OAuth/Types/OIDType.swift @@ -0,0 +1,47 @@ +// +// File.swift +// MagicExt-OAuth +// +// Created by Wentao Liu on 9/29/20. +// + +import Foundation +import MagicSDK + +public struct OpenIDConnectProfile: Codable { + public let name: String? + public let familyName: String? + public let givenName: String? + public let middleName: String? + public let nickname: String? + public let preferredUsername: String? + public let profile: String? + public let picture: String? + public let website: String? + public let gender: String? + public let birthdate: String? + public let zoneinfo: String? + public let locale: String? + public let updatedAt: Int? + + // OpenIDConnectEmail + public let email: String? + public let emailVerified: Bool? + + // OpenIDConnectPhone + public let phoneNumber: String? + public let phoneNumberVerified: Bool? + + // OpenIDConnectAddress + public let address: OIDAddress? + + // OIDAddress + public struct OIDAddress: Codable { + let formatted: String; + let streetAddress: String; + let locality: String; + let region: String; + let postalCode: String; + let country: String; + } +} diff --git a/Sources/MagicExt-OAuth/Utils/OAuthChallenge.swift b/Sources/MagicExt-OAuth/Utils/OAuthChallenge.swift new file mode 100644 index 0000000..cbdba1a --- /dev/null +++ b/Sources/MagicExt-OAuth/Utils/OAuthChallenge.swift @@ -0,0 +1,51 @@ +// +// OAuthChallenge.swift +// MagicExt-OAuth +// +// Created by Wentao Liu on 9/24/20. +// + +import Foundation +import MagicSDK +import CryptoSwift + +internal class OAuthChallenge { + var state: String + let verifier: String + let challenge: String + + init() { + self.state = createRandomString(size: 128) + self.verifier = createRandomString(size: 128) + self.challenge = hexToBase64URLSafe(self.verifier.sha256()) + } +} + +func createRandomString(size: Int) -> String { + let letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~" + return String((0.. String { + + /// Create `Data` from hexadecimal string representation + /// + /// This creates a `Data` object from hex string. Note, if the string has any spaces or non-hex characters (e.g. starts with '<' and with a '>'), those are ignored and only hex characters are processed. + /// + /// - returns: Data represented by this hexadecimal string. + + var data = Data(capacity: hexString.count / 2) + + let regex = try! NSRegularExpression(pattern: "[0-9a-f]{1,2}", options: .caseInsensitive) + regex.enumerateMatches(in: hexString, range: NSRange(hexString.startIndex..., in: hexString)) { match, _, _ in + let byteString = (hexString as NSString).substring(with: match!.range) + let num = UInt8(byteString, radix: 16)! + data.append(num) + } + + guard data.count > 0 else { return "" } + + /// Remove or replace +, /, = + let base64String = data.base64EncodedString() + return base64String.replacingOccurrences(of: "+", with: "-").replacingOccurrences(of: "/", with: "_").replacingOccurrences(of: "=", with: "") +} diff --git a/Tests/MagicExt-OAuthTests/MagicExt_OAuthTests.swift b/Tests/MagicExt-OAuthTests/MagicExt_OAuthTests.swift new file mode 100644 index 0000000..fe9d077 --- /dev/null +++ b/Tests/MagicExt-OAuthTests/MagicExt_OAuthTests.swift @@ -0,0 +1,7 @@ +import XCTest +@testable import MagicExt_OAuth + +final class MagicExt_OAuthTests: XCTestCase { + func testExample() throws { + } +}