From d0f5bdccdba096a23a2cbec870b6e87d7aa836ad Mon Sep 17 00:00:00 2001 From: Edward Jiang Date: Tue, 10 May 2016 17:02:21 -0700 Subject: [PATCH] added files from previous project, added podspec --- Simplicity.xcodeproj/project.pbxproj | 35 +++++++++++++ Simplicity/Helpers.swift | 57 +++++++++++++++++++++ Simplicity/LoginProvider.swift | 17 +++++++ Simplicity/LoginProviders/Facebook.swift | 64 ++++++++++++++++++++++++ Simplicity/OAuth2LoginProvider.swift | 30 +++++++++++ Simplicity/Simplicity.swift | 49 ++++++++++++++++++ Stormpath.podspec | 42 ++++++++++++++++ 7 files changed, 294 insertions(+) create mode 100644 Simplicity/Helpers.swift create mode 100644 Simplicity/LoginProvider.swift create mode 100644 Simplicity/LoginProviders/Facebook.swift create mode 100644 Simplicity/OAuth2LoginProvider.swift create mode 100644 Simplicity/Simplicity.swift create mode 100644 Stormpath.podspec diff --git a/Simplicity.xcodeproj/project.pbxproj b/Simplicity.xcodeproj/project.pbxproj index 9c744c9..49a63c9 100644 --- a/Simplicity.xcodeproj/project.pbxproj +++ b/Simplicity.xcodeproj/project.pbxproj @@ -9,6 +9,12 @@ /* Begin PBXBuildFile section */ DF74EC341CE2A8BB008F16BF /* Simplicity.h in Headers */ = {isa = PBXBuildFile; fileRef = DF74EC331CE2A8BB008F16BF /* Simplicity.h */; settings = {ATTRIBUTES = (Public, ); }; }; DF74EC3F1CE2A943008F16BF /* LICENSE in Resources */ = {isa = PBXBuildFile; fileRef = DF74EC3E1CE2A943008F16BF /* LICENSE */; }; + DF74EC411CE2AC2F008F16BF /* LoginProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF74EC401CE2AC2F008F16BF /* LoginProvider.swift */; }; + DF74EC431CE2AC45008F16BF /* OAuth2LoginProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF74EC421CE2AC45008F16BF /* OAuth2LoginProvider.swift */; }; + DF74EC451CE2AC54008F16BF /* Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF74EC441CE2AC54008F16BF /* Helpers.swift */; }; + DF74EC471CE2AC6F008F16BF /* Simplicity.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF74EC461CE2AC6F008F16BF /* Simplicity.swift */; }; + DF74EC4A1CE2ACF0008F16BF /* Facebook.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF74EC491CE2ACF0008F16BF /* Facebook.swift */; }; + DF74EC4C1CE2AD19008F16BF /* Stormpath.podspec in Resources */ = {isa = PBXBuildFile; fileRef = DF74EC4B1CE2AD19008F16BF /* Stormpath.podspec */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -18,6 +24,12 @@ DF74EC3B1CE2A919008F16BF /* .gitignore */ = {isa = PBXFileReference; lastKnownFileType = text; path = .gitignore; sourceTree = ""; }; DF74EC3C1CE2A936008F16BF /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; DF74EC3E1CE2A943008F16BF /* LICENSE */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = LICENSE; sourceTree = ""; }; + DF74EC401CE2AC2F008F16BF /* LoginProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoginProvider.swift; sourceTree = ""; }; + DF74EC421CE2AC45008F16BF /* OAuth2LoginProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OAuth2LoginProvider.swift; sourceTree = ""; }; + DF74EC441CE2AC54008F16BF /* Helpers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Helpers.swift; sourceTree = ""; }; + DF74EC461CE2AC6F008F16BF /* Simplicity.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Simplicity.swift; sourceTree = ""; }; + DF74EC491CE2ACF0008F16BF /* Facebook.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Facebook.swift; path = LoginProviders/Facebook.swift; sourceTree = ""; }; + DF74EC4B1CE2AD19008F16BF /* Stormpath.podspec */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = Stormpath.podspec; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -34,6 +46,7 @@ DF74EC261CE2A8BB008F16BF = { isa = PBXGroup; children = ( + DF74EC4B1CE2AD19008F16BF /* Stormpath.podspec */, DF74EC3E1CE2A943008F16BF /* LICENSE */, DF74EC3C1CE2A936008F16BF /* README.md */, DF74EC3B1CE2A919008F16BF /* .gitignore */, @@ -53,12 +66,25 @@ DF74EC321CE2A8BB008F16BF /* Simplicity */ = { isa = PBXGroup; children = ( + DF74EC481CE2ACB8008F16BF /* LoginProviders */, DF74EC331CE2A8BB008F16BF /* Simplicity.h */, DF74EC351CE2A8BB008F16BF /* Info.plist */, + DF74EC401CE2AC2F008F16BF /* LoginProvider.swift */, + DF74EC421CE2AC45008F16BF /* OAuth2LoginProvider.swift */, + DF74EC441CE2AC54008F16BF /* Helpers.swift */, + DF74EC461CE2AC6F008F16BF /* Simplicity.swift */, ); path = Simplicity; sourceTree = ""; }; + DF74EC481CE2ACB8008F16BF /* LoginProviders */ = { + isa = PBXGroup; + children = ( + DF74EC491CE2ACF0008F16BF /* Facebook.swift */, + ); + name = LoginProviders; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ @@ -128,6 +154,7 @@ buildActionMask = 2147483647; files = ( DF74EC3F1CE2A943008F16BF /* LICENSE in Resources */, + DF74EC4C1CE2AD19008F16BF /* Stormpath.podspec in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -138,6 +165,11 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + DF74EC411CE2AC2F008F16BF /* LoginProvider.swift in Sources */, + DF74EC451CE2AC54008F16BF /* Helpers.swift in Sources */, + DF74EC4A1CE2ACF0008F16BF /* Facebook.swift in Sources */, + DF74EC471CE2AC6F008F16BF /* Simplicity.swift in Sources */, + DF74EC431CE2AC45008F16BF /* OAuth2LoginProvider.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -238,6 +270,7 @@ DF74EC391CE2A8BB008F16BF /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + CLANG_ENABLE_MODULES = YES; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; @@ -248,12 +281,14 @@ PRODUCT_BUNDLE_IDENTIFIER = com.stormpath.Simplicity; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; }; name = Debug; }; DF74EC3A1CE2A8BB008F16BF /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + CLANG_ENABLE_MODULES = YES; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; diff --git a/Simplicity/Helpers.swift b/Simplicity/Helpers.swift new file mode 100644 index 0000000..d434ef4 --- /dev/null +++ b/Simplicity/Helpers.swift @@ -0,0 +1,57 @@ +// +// Helpers.swift +// Simplicity +// +// Created by Edward Jiang on 5/10/16. +// Copyright © 2016 Stormpath. All rights reserved. +// + +import Foundation + +class Helpers { + static func registeredURLSchemes(matching closure: String -> Bool) -> [String] { + guard let urlTypes = NSBundle.mainBundle().infoDictionary?["CFBundleURLTypes"] as? [[String: AnyObject]] else { + return [String]() + } + + // Convert the complex dictionary into an array of URL schemes + let urlSchemes = urlTypes.flatMap({($0["CFBundleURLSchemes"] as? [String])?.first }) + + return urlSchemes.flatMap({closure($0) ? $0 : nil}) + } + + static func queryString(parts: [String: String]) -> String? { + return parts.map { $0 + "=" + $1 }.joinWithSeparator("&").stringByAddingPercentEncodingWithAllowedCharacters(NSCharacterSet.URLQueryAllowedCharacterSet()) + } +} + +extension NSURL { + /// Dictionary with key/value pairs from the URL fragment + var fragmentDictionary: [String: String] { + return dictionaryFromFormEncodedString(fragment) + } + + /// Dictionary with key/value pairs from the URL query string + var queryDictionary: [String: String] { + return dictionaryFromFormEncodedString(query) + } + + private func dictionaryFromFormEncodedString(input: String?) -> [String: String] { + var result = [String: String]() + + guard let input = input else { + return result + } + let inputPairs = input.componentsSeparatedByString("&") + + for pair in inputPairs { + let split = pair.componentsSeparatedByString("=") + if split.count == 2 { + if let key = split[0].stringByRemovingPercentEncoding, value = split[1].stringByRemovingPercentEncoding { + result[key] = value + } + } + } + return result + } +} \ No newline at end of file diff --git a/Simplicity/LoginProvider.swift b/Simplicity/LoginProvider.swift new file mode 100644 index 0000000..acd2011 --- /dev/null +++ b/Simplicity/LoginProvider.swift @@ -0,0 +1,17 @@ +// +// LoginProvider.swift +// Simplicity +// +// Created by Edward Jiang on 5/10/16. +// Copyright © 2016 Stormpath. All rights reserved. +// + +import Foundation + +public protocol LoginProvider { + var authorizationURL: NSURL { get } + var urlScheme: String { get } + + func linkHandler(url: NSURL, callback: ExternalLoginCallback?) + +} \ No newline at end of file diff --git a/Simplicity/LoginProviders/Facebook.swift b/Simplicity/LoginProviders/Facebook.swift new file mode 100644 index 0000000..a62ff0b --- /dev/null +++ b/Simplicity/LoginProviders/Facebook.swift @@ -0,0 +1,64 @@ +// +// Facebook.swift +// Simplicity +// +// Created by Edward Jiang on 5/10/16. +// Copyright © 2016 Stormpath. All rights reserved. +// + +import Foundation + +public class FacebookLoginProvider: OAuth2LoginProvider { + public var scopes = Set() + public var urlScheme: String + + public var state = arc4random_uniform(10000000) + public var clientId: String + public var grantType: OAuth2GrantType = .Custom + + + public var authorizationURL: NSURL { + // Auth_type is re-request since we need to ask for email scope again if + // people decline the email permission. If it gets annoying because + // people keep asking for more scopes, we can change this. + + let query = ["client_id": clientId, + "redirect_uri": urlScheme + "://authorize", + "response_type": "token", + "scope": scopes.joinWithSeparator(" "), + "auth_type": "rerequest", + "state": String(state)] + + let queryString = Helpers.queryString(query)! + + return NSURL(string: "https://www.facebook.com/dialog/oauth?\(queryString)")! + } + + public func linkHandler(url: NSURL, callback: ExternalLoginCallback?) { + if(url.queryDictionary["error"] != nil) { + // We are not even going to callback, because the user never started + // the login process in the first place. Error is always because + // people cancelled the FB login according to https://developers.facebook.com/docs/facebook-login/manually-build-a-login-flow + return + } + + // Get the access token, and check that the state is the same + guard let accessToken = url.fragmentDictionary["access_token"] where url.fragmentDictionary["state"] == "\(state)" else { + callback?(authToken: nil, error: nil) + return + } + + callback?(authToken: accessToken, error: nil) + } + + public init?() { + // Search for URL Scheme, error if not there + + guard let urlScheme = Helpers.registeredURLSchemes(matching: {$0.hasPrefix("fb")}).first, + range = urlScheme.rangeOfString("\\d+", options: .RegularExpressionSearch) else { + return nil + } + self.urlScheme = urlScheme + self.clientId = urlScheme.substringWithRange(range) + } +} diff --git a/Simplicity/OAuth2LoginProvider.swift b/Simplicity/OAuth2LoginProvider.swift new file mode 100644 index 0000000..4751438 --- /dev/null +++ b/Simplicity/OAuth2LoginProvider.swift @@ -0,0 +1,30 @@ +// +// OAuth2LoginProvider.swift +// Simplicity +// +// Created by Edward Jiang on 5/10/16. +// Copyright © 2016 Stormpath. All rights reserved. +// + +import Foundation + +public protocol OAuth2LoginProvider: LoginProvider { + var clientId: String { get } + var scopes: Set { get set } + var grantType: OAuth2GrantType { get } +} + +public protocol OAuth2Scopes { + var set: Set { get set } + var string: String { get } +} + +public extension OAuth2Scopes { + public var string: String { + return set.joinWithSeparator(" ") + } +} + +public enum OAuth2GrantType { + case AuthorizationCode, Implicit, Custom +} \ No newline at end of file diff --git a/Simplicity/Simplicity.swift b/Simplicity/Simplicity.swift new file mode 100644 index 0000000..6a6144e --- /dev/null +++ b/Simplicity/Simplicity.swift @@ -0,0 +1,49 @@ +// +// Simplicity.swift +// Simplicity +// +// Created by Edward Jiang on 5/10/16. +// Copyright © 2016 Stormpath. All rights reserved. +// + +import UIKit +import SafariServices + +public typealias ExternalLoginCallback = (authToken: String?, error: NSError?) -> Void + +public class LoginManager: NSObject { + static var currentLoginProvider: LoginProvider? + static var callback: ExternalLoginCallback? + static var safari: UIViewController? + + public static func login(loginProvider: LoginProvider, callback: ExternalLoginCallback? = nil) { + self.currentLoginProvider = loginProvider + self.callback = callback + + presentSafariView(loginProvider.authorizationURL) + } + + /// Deep link handler (iOS9) + public static func application(app: UIApplication, openURL url: NSURL, options: [String : AnyObject]) -> Bool { + if url.scheme != currentLoginProvider?.urlScheme { + return false + } + currentLoginProvider?.linkHandler(url, callback: callback) + + return true + } + + /// Deep link handler ( Bool { + return self.application(application, openURL: url, options: [String: AnyObject]()) + } + + static func presentSafariView(url: NSURL) { + if #available(iOS 9, *) { + safari = SFSafariViewController(URL: url) + UIApplication.sharedApplication().delegate?.window??.rootViewController?.presentViewController(safari!, animated: true, completion: nil) + } else { + UIApplication.sharedApplication().openURL(url) + } + } +} \ No newline at end of file diff --git a/Stormpath.podspec b/Stormpath.podspec new file mode 100644 index 0000000..7d2a524 --- /dev/null +++ b/Stormpath.podspec @@ -0,0 +1,42 @@ +# +# Be sure to run `pod lib lint Simplicity.podspec' to ensure this is a +# valid spec before submitting. +# +# Any lines starting with a # are optional, but their use is encouraged +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html +# + +Pod::Spec.new do |s| +s.name = "Simplicity" +s.version = "0.1.0" +s.summary = "A framework for authenticating with external providers on iOS" + +# This description is used to generate tags and improve search results. +# * Think: What does it do? Why did you write it? What is the focus? +# * Try to keep it short, snappy and to the point. +# * Write the description between the DESC delimiters below. +# * Finally, don't worry about the indent, CocoaPods strips it! + +s.description = <<-DESC +A framework for authenticating with external providers on iOS +DESC + +s.homepage = "https://github.com/SimplicityMobile/Simplicity" +# s.screenshots = "www.example.com/screenshots_1", "www.example.com/screenshots_2" +s.license = 'Apache2' +s.author = { "Edward Jiang" => "edward@stormpath.com" } +s.source = { :git => "https://github.com/SimplicityMobile/Simplicity.git", :tag => s.version.to_s } +# s.social_media_url = 'https://twitter.com/' + +s.ios.deployment_target = '8.0' + +s.source_files = 'Simplicity/**/*.swift' + +# s.resource_bundles = { +# 'Simplicity' => ['Simplicity/Assets/*.png'] +# } + +s.public_header_files = 'Simplicity/**/*.h' +# s.frameworks = 'UIKit', 'MapKit' +# s.dependency 'AFNetworking', '~> 2.3' +end \ No newline at end of file