Skip to content

Commit

Permalink
Support ASWebAuthenticationSession (#335)
Browse files Browse the repository at this point in the history
* support multimedia

* try finalizeUpload until succeeded

* MultipartMediaType -> MediaType

* modifer finalizeUpload

* fixed SwifterMedia appendUpload

* fixed length of appendUpload

* Followed the official Twitter API procedure

* separate private helper method

* add authorize fucntion

* update SwifterDemoMac

* increased Swifter version of podspec

* typo

* Support ASWebAuthenticationSession to iOS

- Removed code duplication as much as possible.
- Improved Demo App and made the structure of the iOS demo and the macOS demo similar.

* Put at return statement in the first `if`

* Set nil to `session` after callback to avoid a retain cycle

* Modified `let session`

* Corrected `available` attributes

* Fixed s.version
  • Loading branch information
Kyome22 authored Dec 23, 2020
1 parent 7906c94 commit 3c818bb
Show file tree
Hide file tree
Showing 6 changed files with 220 additions and 119 deletions.
9 changes: 9 additions & 0 deletions Sources/Swifter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@

import Foundation
import Dispatch
import AuthenticationServices

#if os(macOS) || os(iOS)
import Accounts
Expand Down Expand Up @@ -114,6 +115,14 @@ public class Swifter {
NotificationCenter.default.removeObserver(token)
}
}

private var storedSession: Any?
@available(macOS 10.15, *)
@available(iOS 13.0, *)
internal var session: ASWebAuthenticationSession? {
get { return storedSession as? ASWebAuthenticationSession }
set { storedSession = newValue as Any }
}

// MARK: - Initializers

Expand Down
108 changes: 72 additions & 36 deletions Sources/SwifterAuth.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
//

import Foundation
import AuthenticationServices

#if os(iOS)
import UIKit
Expand All @@ -36,7 +37,39 @@ public extension Swifter {

typealias TokenSuccessHandler = (Credential.OAuthAccessToken?, URLResponse) -> Void
typealias SSOTokenSuccessHandler = (Credential.OAuthAccessToken) -> Void


/**
Begin Authorization with a Callback URL
- for macOS and iOS
*/
#if os(macOS) || os(iOS)
@available(macOS 10.15, *)
@available(iOS 13.0, *)
func authorize(withProvider provider: ASWebAuthenticationPresentationContextProviding,
ephemeralSession: Bool = false,
callbackURL: URL,
forceLogin: Bool = false,
success: TokenSuccessHandler?,
failure: FailureHandler? = nil) {
let callbackURLScheme = callbackURL.absoluteString.components(separatedBy: "://").first
self.postOAuthRequestToken(with: callbackURL, success: { token, response in
let queryURL = self.makeQueryURL(tokenKey: token!.key, forceLogin: forceLogin)
let session = ASWebAuthenticationSession(url: queryURL, callbackURLScheme: callbackURLScheme) { (url, error) in
self.session = nil
if let error = error {
failure?(error)
return
}
self.postOAuthAccessTokenHelper(requestToken: token!, responseURL: url!, success: success, failure: failure)
}
session.presentationContextProvider = provider
session.prefersEphemeralWebBrowserSession = ephemeralSession
session.start()
self.session = session
}, failure: failure)
}
#endif

/**
Begin Authorization with a Callback URL.
- OS X only
Expand All @@ -47,24 +80,13 @@ public extension Swifter {
success: TokenSuccessHandler?,
failure: FailureHandler? = nil) {
self.postOAuthRequestToken(with: callbackURL, success: { token, response in
var requestToken = token!

NotificationCenter.default.addObserver(forName: .swifterCallback, object: nil, queue: .main) { notification in
NotificationCenter.default.removeObserver(self)
let url = notification.userInfo![CallbackNotification.optionsURLKey] as! URL
let parameters = url.query!.queryStringParameters
requestToken.verifier = parameters["oauth_verifier"]

self.postOAuthAccessToken(with: requestToken, success: { accessToken, response in
self.client.credential = Credential(accessToken: accessToken!)
success?(accessToken!, response)
}, failure: failure)
self.postOAuthAccessTokenHelper(requestToken: token!, responseURL: url, success: success, failure: failure)
}

let forceLogin = forceLogin ? "&force_login=true" : ""
let query = "oauth/authorize?oauth_token=\(token!.key)\(forceLogin)"
let queryUrl = URL(string: query, relativeTo: TwitterURL.oauth.url)!.absoluteURL
NSWorkspace.shared.open(queryUrl)
let queryURL = self.makeQueryURL(tokenKey: token!.key, forceLogin: forceLogin)
NSWorkspace.shared.open(queryURL)
}, failure: failure)
}
#endif
Expand All @@ -76,46 +98,36 @@ public extension Swifter {
The UIViewController must inherit SFSafariViewControllerDelegate

*/

#if os(iOS)
@available(iOS, deprecated: 13.0)
func authorize(withCallback callbackURL: URL,
presentingFrom presenting: UIViewController?,
forceLogin: Bool = false,
safariDelegate: SFSafariViewControllerDelegate? = nil,
success: TokenSuccessHandler?,
failure: FailureHandler? = nil) {
self.postOAuthRequestToken(with: callbackURL, success: { token, response in
var requestToken = token!
self.swifterCallbackToken = NotificationCenter.default.addObserver(forName: .swifterCallback, object: nil, queue: .main) { notification in
self.swifterCallbackToken = nil
presenting?.presentedViewController?.dismiss(animated: true, completion: nil)
let url = notification.userInfo![CallbackNotification.optionsURLKey] as! URL

let parameters = url.query!.queryStringParameters
requestToken.verifier = parameters["oauth_verifier"]

self.postOAuthAccessToken(with: requestToken, success: { accessToken, response in
self.client.credential = Credential(accessToken: accessToken!)
success?(accessToken!, response)
}, failure: failure)
self.postOAuthAccessTokenHelper(requestToken: token!, responseURL: url, success: success, failure: failure)
}

let forceLogin = forceLogin ? "&force_login=true" : ""
let query = "oauth/authorize?oauth_token=\(token!.key)\(forceLogin)"
let queryUrl = URL(string: query, relativeTo: TwitterURL.oauth.url)!.absoluteURL

let queryURL = self.makeQueryURL(tokenKey: token!.key, forceLogin: forceLogin)

if let delegate = safariDelegate ?? (presenting as? SFSafariViewControllerDelegate) {
let safariView = SFSafariViewController(url: queryUrl)
let safariView = SFSafariViewController(url: queryURL)
safariView.delegate = delegate
safariView.modalTransitionStyle = .coverVertical
safariView.modalPresentationStyle = .overFullScreen
presenting?.present(safariView, animated: true, completion: nil)
} else {
UIApplication.shared.open(queryUrl, options: [:], completionHandler: nil)
UIApplication.shared.open(queryURL, options: [:], completionHandler: nil)
}
}, failure: failure)
}

func authorizeSSO(success: SSOTokenSuccessHandler?, failure: FailureHandler? = nil) {
guard let client = client as? SwifterAppProtocol else {
let error = SwifterError(message: "SSO not supported AppOnly client",
Expand Down Expand Up @@ -149,15 +161,19 @@ public extension Swifter {
let url = URL(string: "twitterauth://authorize?consumer_key=\(client.consumerKey)&consumer_secret=\(client.consumerSecret)&oauth_callback=\(urlScheme)")!
UIApplication.shared.open(url, options: [:], completionHandler: { (success) in
if !success {
let error = SwifterError(message: "Cannot open twitter app",
kind: .noTwitterApp)
let error = SwifterError(message: "Cannot open twitter app", kind: .noTwitterApp)
failure?(error)
}
})
}

#endif

func makeQueryURL(tokenKey: String, forceLogin: Bool) -> URL {
let forceLogin = forceLogin ? "&force_login=true" : ""
let query = "oauth/authorize?oauth_token=\(tokenKey)\(forceLogin)"
return URL(string: query, relativeTo: TwitterURL.oauth.url)!.absoluteURL
}

@discardableResult
class func handleOpenURL(_ url: URL, callbackURL: URL, isSSO: Bool = false) -> Bool {
guard url.hasSameUrlScheme(as: callbackURL) else {
Expand Down Expand Up @@ -252,5 +268,25 @@ public extension Swifter {
failure?(error)
}
}


private func postOAuthAccessTokenHelper(
requestToken token: Credential.OAuthAccessToken,
responseURL: URL,
success: TokenSuccessHandler?,
failure: FailureHandler? = nil
) {
let parameters = responseURL.query!.queryStringParameters
guard let verifier = parameters["oauth_verifier"] else {
let error = SwifterError(message: "User cancelled login from Twitter App", kind: .cancelled)
failure?(error)
return
}
var requestToken = token
requestToken.verifier = verifier
self.postOAuthAccessToken(with: requestToken, success: { accessToken, response in
self.client.credential = Credential(accessToken: accessToken!)
success?(accessToken!, response)
}, failure: failure)
}

}
6 changes: 2 additions & 4 deletions Swifter.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,6 @@
A198590F233737C400B63092 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A198590E233737C400B63092 /* SceneDelegate.swift */; };
A1AA8B001D5C514E00CE7F17 /* SwifterError.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1AA8AFE1D5C50AE00CE7F17 /* SwifterError.swift */; };
A1AA8B011D5C515000CE7F17 /* SwifterError.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1AA8AFE1D5C50AE00CE7F17 /* SwifterError.swift */; };
A1AA8B031D5C59D200CE7F17 /* TweetCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1AA8B021D5C59D200CE7F17 /* TweetCell.swift */; };
A1BDB38C1D4C864200B0080F /* SwifterTag.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1BDB38A1D4C859200B0080F /* SwifterTag.swift */; };
A1BDB38D1D4C864200B0080F /* SwifterTag.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1BDB38A1D4C859200B0080F /* SwifterTag.swift */; };
A1C7F0A51C7845FD00955ADB /* SwifterMac.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8B9F51131944A61D00894629 /* SwifterMac.framework */; };
Expand Down Expand Up @@ -202,7 +201,6 @@
A17A694D1C78333400063DFD /* Operator++.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Operator++.swift"; sourceTree = "<group>"; };
A198590E233737C400B63092 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
A1AA8AFE1D5C50AE00CE7F17 /* SwifterError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwifterError.swift; sourceTree = "<group>"; };
A1AA8B021D5C59D200CE7F17 /* TweetCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TweetCell.swift; sourceTree = "<group>"; };
A1BDB38A1D4C859200B0080F /* SwifterTag.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwifterTag.swift; sourceTree = "<group>"; };
A1E207A61D4B85D90093E498 /* HMAC.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HMAC.swift; sourceTree = "<group>"; };
A1E207A71D4B85D90093E498 /* SHA1.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SHA1.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -265,7 +263,6 @@
A198590E233737C400B63092 /* SceneDelegate.swift */,
8B5EFF04195F064400B354F9 /* AuthViewController.swift */,
8B5EFF05195F064400B354F9 /* TweetsViewController.swift */,
A1AA8B021D5C59D200CE7F17 /* TweetCell.swift */,
8B300C211944AA1A00993175 /* Supporting Files */,
);
path = SwifterDemoiOS;
Expand Down Expand Up @@ -626,7 +623,6 @@
8B5EFF08195F064400B354F9 /* AuthViewController.swift in Sources */,
8B5EFF07195F064400B354F9 /* AppDelegate.swift in Sources */,
8B5EFF09195F064400B354F9 /* TweetsViewController.swift in Sources */,
A1AA8B031D5C59D200CE7F17 /* TweetCell.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down Expand Up @@ -893,6 +889,7 @@
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
MACOSX_DEPLOYMENT_TARGET = 10.10;
METAL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
Expand Down Expand Up @@ -946,6 +943,7 @@
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
MACOSX_DEPLOYMENT_TARGET = 10.10;
METAL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SWIFT_VERSION = 5.0;
Expand Down
103 changes: 69 additions & 34 deletions SwifterDemoMac/ViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,64 +26,99 @@
import Cocoa
import Accounts
import SwifterMac
import AuthenticationServices

enum AuthorizationMode {
@available(macOS, deprecated: 10.13)
case account
case browser
}

let authorizationMode: AuthorizationMode = .browser

class ViewController: NSViewController {
let useACAccount = false

private var swifter = Swifter(
consumerKey: "nLl1mNYc25avPPF4oIzMyQzft",
consumerSecret: "Qm3e5JTXDhbbLl44cq6WdK00tSUwa17tWlO8Bf70douE4dcJe2"
)
@objc dynamic var tweets: [Tweet] = []

override func viewDidLoad() {
super.viewDidLoad()

if #available(macOS 10.13, *) {
authorizeWithWebLogin()
} else if useACAccount {
switch authorizationMode {
case .account:
authorizeWithACAccountStore()
} else {
case .browser:
authorizeWithWebLogin()
}
}

@available(macOS, deprecated: 10.13)
private func authorizeWithACAccountStore() {
let accountStore = ACAccountStore()
let accountType = accountStore.accountType(withAccountTypeIdentifier: ACAccountTypeIdentifierTwitter)

accountStore.requestAccessToAccounts(with: accountType, options: nil) { granted, error in
guard granted else {
print("There are no Twitter accounts configured. You can add or create a Twitter account in Settings.")
if #available(macOS 10.13, *) {
self.alert(title: "Deprecated",
message: "ACAccountStore was deprecated on macOS 10.13, please use the OAuth flow instead")
return
}
let store = ACAccountStore()
let type = store.accountType(withAccountTypeIdentifier: ACAccountTypeIdentifierTwitter)
store.requestAccessToAccounts(with: type, options: nil) { granted, error in
guard let twitterAccounts = store.accounts(with: type), granted else {
self.alert(error: error)
return
}

guard let twitterAccount = accountStore.accounts(with: accountType).first as? ACAccount else {
print("There are no Twitter accounts configured. You can add or create a Twitter account in Settings.")
if twitterAccounts.isEmpty {
self.alert(title: "Error", message: "There are no Twitter accounts configured. You can add or create a Twitter account in Settings.")
return
} else {
let twitterAccount = twitterAccounts[0] as! ACAccount
self.swifter = Swifter(account: twitterAccount)
self.fetchTwitterHomeStream()
}

let swifter = Swifter(account: twitterAccount)
swifter.getHomeTimeline(count: 100, success: { statuses in
self.processTweets(result: statuses)
}) { print($0.localizedDescription) }
}
}

private func authorizeWithWebLogin() {
let swifter = Swifter(
consumerKey: "nLl1mNYc25avPPF4oIzMyQzft",
consumerSecret: "Qm3e5JTXDhbbLl44cq6WdK00tSUwa17tWlO8Bf70douE4dcJe2"
)
let callbackUrl = URL(string: "swifter://success")!
swifter.authorize(withCallback: callbackUrl, success: { _, _ in
swifter.getHomeTimeline(count: 100, success: { statuses in
self.processTweets(result: statuses)
}) { print($0.localizedDescription) }
}) { print($0.localizedDescription) }
}

private func processTweets(result: JSON) {
guard let tweets = result.array else { return }
self.tweets = tweets.map {
return Tweet(name: $0["user"]["name"].string!, text: $0["text"].string!)
if #available(macOS 10.15, *) {
swifter.authorize(withProvider: self, callbackURL: callbackUrl) { _, _ in
self.fetchTwitterHomeStream()
} failure: { self.alert(error: $0) }
} else {
swifter.authorize(withCallback: callbackUrl) { _, _ in
self.fetchTwitterHomeStream()
} failure: { self.alert(error: $0) }
}
}

private func fetchTwitterHomeStream() {
swifter.getHomeTimeline(count: 100) { json in
guard let tweets = json.array else { return }
self.tweets = tweets.map {
return Tweet(name: $0["user"]["name"].string!, text: $0["text"].string!)
}
} failure: { self.alert(error: $0) }
}

private func alert(title: String, message: String) {
let alert = NSAlert()
alert.alertStyle = .warning
alert.messageText = title
alert.informativeText = message
alert.runModal()
}

private func alert(error: Error) {
NSAlert(error: error).runModal()
}
}

// This is need for ASWebAuthenticationSession
@available(macOS 10.15, *)
extension ViewController: ASWebAuthenticationPresentationContextProviding {
func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
return self.view.window!
}
}
Loading

0 comments on commit 3c818bb

Please sign in to comment.