From 8f00d6fe31ffab04155c76912433098247e90634 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Everl=C3=B6f?= Date: Fri, 18 Nov 2016 15:11:50 +0100 Subject: [PATCH 1/4] Add workaround for issue in Safari There's an issue with Safari, references in some websites, where you cant open the url again if you press 'Cancel' once when asking to go back to original app. For the user to workaround this issue it has to restart Safari, which is not really nice. This fix instead gives the option to append a parameter to the url, which will make it unique, thus the browser till prompt the user again next time the app authenticates. --- Sources/Base/OAuth2ClientConfig.swift | 14 +++++++++++++- Sources/Flows/OAuth2.swift | 3 +++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/Sources/Base/OAuth2ClientConfig.swift b/Sources/Base/OAuth2ClientConfig.swift index f175443a..5ccbe8c9 100644 --- a/Sources/Base/OAuth2ClientConfig.swift +++ b/Sources/Base/OAuth2ClientConfig.swift @@ -68,7 +68,19 @@ open class OAuth2ClientConfig { /// Custom request parameters to be added during authorization. open var authParameters: OAuth2StringDict? - + + /// There's an issue with authenticating through 'system browser', where safari says: + /// "Safari cannot open the page because the address is invalid." if you first selects 'Cancel' + /// when asked to switch back to "your" app, and then you try authenticating again. + /// To get rid of it you must restart Safari. + /// + /// Read more about it here: + /// http://stackoverflow.com/questions/27739442/ios-safari-does-not-recognize-url-schemes-after-user-cancels + /// https://community.fitbit.com/t5/Web-API/oAuth2-authentication-page-gives-me-a-quot-Cannot-Open-Page-quot-error/td-p/1150391 + /// + /// Toggling `safariCancelWorkaround` to true will send an extra get-paramter to make the url unique, + /// thus it will ask again for the new url. + open var safariCancelWorkaround = false /** Initializer to initialize properties from a settings dictionary. diff --git a/Sources/Flows/OAuth2.swift b/Sources/Flows/OAuth2.swift index 9b5b8064..80c3b4d8 100644 --- a/Sources/Flows/OAuth2.swift +++ b/Sources/Flows/OAuth2.swift @@ -282,6 +282,9 @@ open class OAuth2: OAuth2Base { req.params["redirect_uri"] = redirect req.params["client_id"] = clientId req.params["state"] = context.state + if clientConfig.safariCancelWorkaround { + req.params["swa"] = "\(Date.timeIntervalSinceReferenceDate)" // Safari issue workaround + } if let scope = scope ?? clientConfig.scope { req.params["scope"] = scope } From 41378f32474049126757c5461527d4a00bbf5c58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Everl=C3=B6f?= Date: Mon, 21 Nov 2016 10:17:59 +0100 Subject: [PATCH 2/4] Add OAuth2CodeGrantAzure to authenticate against Microsoft Azure OAuth2CodeGrantAzure requires the client to indicate which resource are requested. This required the addition of `customParameters` in `OAuth2AuthConfig` which adds custom parameter to the body of the request --- OAuth2.xcodeproj/project.pbxproj | 4 +++ Sources/Base/OAuth2AuthConfig.swift | 5 ++- Sources/Base/OAuth2AuthRequest.swift | 6 +++- Sources/Flows/OAuth2CodeGrantAzure.swift | 40 ++++++++++++++++++++++ Tests/FlowTests/OAuth2CodeGrantTests.swift | 20 +++++++++++ 5 files changed, 73 insertions(+), 2 deletions(-) create mode 100644 Sources/Flows/OAuth2CodeGrantAzure.swift diff --git a/OAuth2.xcodeproj/project.pbxproj b/OAuth2.xcodeproj/project.pbxproj index 57766cf3..72af7709 100644 --- a/OAuth2.xcodeproj/project.pbxproj +++ b/OAuth2.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 0C2F5E5B1DE2DB8500F621E0 /* OAuth2CodeGrantAzure.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C2F5E5A1DE2DB8500F621E0 /* OAuth2CodeGrantAzure.swift */; }; 6598544E1C5B3C9500237D39 /* OAuth2Authorizer+tvOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6598543F1C5B3B4000237D39 /* OAuth2Authorizer+tvOS.swift */; }; 6598544F1C5B3C9C00237D39 /* OAuth2Base.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEDB8640193FAB9200C4EEA1 /* OAuth2Base.swift */; }; 659854501C5B3C9C00237D39 /* OAuth2Requestable.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEF47D2A1B1E3FDD0057D838 /* OAuth2Requestable.swift */; }; @@ -154,6 +155,7 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 0C2F5E5A1DE2DB8500F621E0 /* OAuth2CodeGrantAzure.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OAuth2CodeGrantAzure.swift; sourceTree = ""; }; 6598543F1C5B3B4000237D39 /* OAuth2Authorizer+tvOS.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "OAuth2Authorizer+tvOS.swift"; path = "Sources/tvOS/OAuth2Authorizer+tvOS.swift"; sourceTree = SOURCE_ROOT; }; 659854461C5B3BEA00237D39 /* OAuth2.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = OAuth2.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 65EC05DF1C9050CB00DE9186 /* OAuth2KeychainAccount.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OAuth2KeychainAccount.swift; sourceTree = ""; }; @@ -313,6 +315,7 @@ EE29836F1D40B83600933CDD /* OAuth2.swift */, EE3174EB1945E83100210E62 /* OAuth2ImplicitGrant.swift */, EE44F691194F2C7D0094AB8B /* OAuth2CodeGrant.swift */, + 0C2F5E5A1DE2DB8500F621E0 /* OAuth2CodeGrantAzure.swift */, EEACE1DE1A7E8FC1009BF3A7 /* OAuth2CodeGrantFacebook.swift */, EEC6D57B1C2837EA00FA9B1C /* OAuth2CodeGrantLinkedIn.swift */, EE1391D91AC5B41A002C7B18 /* OAuth2CodeGrantBasicAuth.swift */, @@ -698,6 +701,7 @@ EEC7A8D81AE4851E008C30E7 /* Keychain.swift in Sources */, EEAEF10B1CDBCF28001A1C6F /* OAuth2Logger.swift in Sources */, 65EC05E01C9050CB00DE9186 /* OAuth2KeychainAccount.swift in Sources */, + 0C2F5E5B1DE2DB8500F621E0 /* OAuth2CodeGrantAzure.swift in Sources */, DD0CCBAD1C4DC83A0044C4E3 /* OAuth2WebViewController.swift in Sources */, EE9EBF1B1D775F74003263FC /* OAuth2Securable.swift in Sources */, EE79F65A1BFAA36900746243 /* OAuth2Error.swift in Sources */, diff --git a/Sources/Base/OAuth2AuthConfig.swift b/Sources/Base/OAuth2AuthConfig.swift index 33bdac3f..1e5078df 100644 --- a/Sources/Base/OAuth2AuthConfig.swift +++ b/Sources/Base/OAuth2AuthConfig.swift @@ -45,7 +45,10 @@ public struct OAuth2AuthConfig { /// Whether to automatically dismiss the auto-presented authorization screen. public var authorizeEmbeddedAutoDismiss = true - + + /// Add custom parameters to the request + public var customParameters: [String: String]? = nil + /// Context information for the authorization flow: /// - iOS: The parent view controller to present from /// - macOS: An NSWindow from which to present a modal sheet _or_ `nil` to present in a new window diff --git a/Sources/Base/OAuth2AuthRequest.swift b/Sources/Base/OAuth2AuthRequest.swift index 187a9d5b..a59292fc 100644 --- a/Sources/Base/OAuth2AuthRequest.swift +++ b/Sources/Base/OAuth2AuthRequest.swift @@ -220,7 +220,11 @@ open class OAuth2AuthRequest { req.setValue(val, forHTTPHeaderField: key) } } - + if let customParameters = oauth2.authConfig.customParameters { + for (k, v) in customParameters { + finalParams[k] = v + } + } // add a body to POST requests if .POST == method && finalParams.count > 0 { req.httpBody = try finalParams.utf8EncodedData() diff --git a/Sources/Flows/OAuth2CodeGrantAzure.swift b/Sources/Flows/OAuth2CodeGrantAzure.swift new file mode 100644 index 00000000..77ee1a07 --- /dev/null +++ b/Sources/Flows/OAuth2CodeGrantAzure.swift @@ -0,0 +1,40 @@ +// +// OAuth2CodeGrantAzure.swift +// OAuth2 +// +// Created by Pascal Pfiffner on 2/1/15. +// Copyright 2016 David Everlöf +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +#if !NO_MODULE_IMPORT + import Base +#endif + + +/** + Azure requires a `resource`, hence our `init` requires it as well + */ +public class OAuth2CodeGrantAzure: OAuth2CodeGrant { + + public init(settings: OAuth2JSON, resource: String) { + super.init(settings: settings) + authConfig.secretInBody = true + authConfig.customParameters = [ + "resource": resource + ] + } +} + diff --git a/Tests/FlowTests/OAuth2CodeGrantTests.swift b/Tests/FlowTests/OAuth2CodeGrantTests.swift index ef8f2ea5..c3098081 100644 --- a/Tests/FlowTests/OAuth2CodeGrantTests.swift +++ b/Tests/FlowTests/OAuth2CodeGrantTests.swift @@ -253,6 +253,26 @@ class OAuth2CodeGrantTests: XCTestCase { XCTAssertEqual(query2["redirect_uri"]!, "oauth2://callback", "Expecting correct `redirect_uri`") XCTAssertNil(query2["state"], "`state` must be empty") } + + func testCustomAuthParameters() { + let oauth = OAuth2CodeGrant(settings: baseSettings) + oauth.redirect = "oauth2://callback" + oauth.context.redirectURL = "oauth2://callback" + + // not in body + let req = try! oauth.accessTokenRequest(with: "pp").asURLRequest(for: oauth) + let body = String(data: req.httpBody!, encoding: String.Encoding.utf8) + let query = OAuth2CodeGrant.params(fromQuery: body!) + XCTAssertNil(query["foo"], "`foo` should be nil") + + oauth.authConfig.customParameters = [ "foo": "bar" ] + + // in body + let req2 = try! oauth.accessTokenRequest(with: "pp").asURLRequest(for: oauth) + let body2 = String(data: req2.httpBody!, encoding: String.Encoding.utf8) + let query2 = OAuth2CodeGrant.params(fromQuery: body2!) + XCTAssertEqual(query2["foo"]!, "bar", "Expecting key `foo` to be `bar`") + } func testTokenRequestAgainstAuthURL() { From 068cf29adcf40f18d00d244670bab076eaf09ac3 Mon Sep 17 00:00:00 2001 From: Pascal Pfiffner Date: Thu, 24 Nov 2016 12:27:48 +0100 Subject: [PATCH 3/4] Move all extra auth parameters to authConfig This commit addresses the discrepancy introduced by the last merge. --- OAuth2.xcodeproj/project.pbxproj | 12 +++-- .../xcshareddata/xcschemes/OAuth2iOS.xcscheme | 2 +- .../xcschemes/OAuth2macOS.xcscheme | 2 +- .../xcschemes/OAuth2tvOS.xcscheme | 2 +- README.md | 2 +- Sources/Base/OAuth2Base.swift | 44 ++++++++++--------- Sources/Base/OAuth2ClientConfig.swift | 15 ++----- Sources/Flows/OAuth2.swift | 21 ++++----- Tests/FlowTests/OAuth2CodeGrantTests.swift | 25 ++++++++++- 9 files changed, 72 insertions(+), 53 deletions(-) diff --git a/OAuth2.xcodeproj/project.pbxproj b/OAuth2.xcodeproj/project.pbxproj index 72af7709..57d29154 100644 --- a/OAuth2.xcodeproj/project.pbxproj +++ b/OAuth2.xcodeproj/project.pbxproj @@ -548,7 +548,7 @@ attributes = { LastSwiftMigration = 0700; LastSwiftUpdateCheck = 0700; - LastUpgradeCheck = 0800; + LastUpgradeCheck = 0810; ORGANIZATIONNAME = "Pascal Pfiffner"; TargetAttributes = { 659854451C5B3BEA00237D39 = { @@ -787,6 +787,7 @@ isa = XCBuildConfiguration; buildSettings = { APPLICATION_EXTENSION_API_ONLY = YES; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; DEBUG_INFORMATION_FORMAT = dwarf; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; @@ -812,6 +813,7 @@ isa = XCBuildConfiguration; buildSettings = { APPLICATION_EXTENSION_API_ONLY = YES; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; @@ -848,8 +850,10 @@ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; @@ -898,8 +902,10 @@ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; @@ -932,7 +938,7 @@ buildSettings = { CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = "iPhone Developer"; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; @@ -954,7 +960,7 @@ buildSettings = { CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = "iPhone Developer"; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; diff --git a/OAuth2.xcodeproj/xcshareddata/xcschemes/OAuth2iOS.xcscheme b/OAuth2.xcodeproj/xcshareddata/xcschemes/OAuth2iOS.xcscheme index 11e8cb58..947f86aa 100644 --- a/OAuth2.xcodeproj/xcshareddata/xcschemes/OAuth2iOS.xcscheme +++ b/OAuth2.xcodeproj/xcshareddata/xcschemes/OAuth2iOS.xcscheme @@ -1,6 +1,6 @@ Void)) { if isAuthorizing { callback(nil, OAuth2Error.alreadyAuthorizing) return } - var prms = authParameters - if nil != prms, let params = params { - params.forEach() { prms![$0] = $1 } - } - let useParams = prms ?? params didAuthorizeOrFail = callback logger?.debug("OAuth2", msg: "Starting authorization") - tryToObtainAccessTokenIfNeeded(params: useParams) { successParams in + tryToObtainAccessTokenIfNeeded(params: params) { successParams in if let successParams = successParams { self.didAuthorize(withParameters: successParams) } @@ -114,7 +109,7 @@ open class OAuth2: OAuth2Base { else { do { assert(Thread.isMainThread) - try self.doAuthorize(params: useParams) + try self.doAuthorize(params: params) } catch let error { self.didFail(with: error.asOAuth2Error) @@ -143,8 +138,8 @@ open class OAuth2: OAuth2Base { - parameter from: The context to start authorization from, depends on platform (UIViewController or NSWindow, see `authorizeContext`) - parameter params: Optional key/value pairs to pass during authorization - - parameter callback: The callback to call when authorization finishes (parameters will be non-nil but may be an empty dict), fails or is - cancelled (error will be non-nil, e.g. `.requestCancelled` if auth was aborted) + - parameter callback: The callback to call when authorization finishes (parameters will be non-nil but may be an empty dict), fails or + is cancelled (error will be non-nil, e.g. `.requestCancelled` if auth was aborted) */ open func authorizeEmbedded(from context: AnyObject, params: OAuth2StringDict? = nil, callback: @escaping ((_ authParameters: OAuth2JSON?, _ error: OAuth2Error?) -> Void)) { if isAuthorizing { // `authorize()` will check this, but we want to exit before changing `authConfig` @@ -268,7 +263,7 @@ open class OAuth2: OAuth2Base { Method that creates the OAuth2AuthRequest instance used to create the authorize URL - parameter redirect: The redirect URI string to supply. If it is nil, the first value of the settings' `redirect_uris` entries is - used. Must be present in the end! + used. Must be present in the end! - parameter scope: The scope to request - parameter params: Any additional parameters as dictionary with string keys and values that will be added to the query part - returns: OAuth2AuthRequest to be used to call to the authorize endpoint @@ -310,7 +305,7 @@ open class OAuth2: OAuth2Base { Convenience method to be overridden by and used from subclasses. - parameter redirect: The redirect URI string to supply. If it is nil, the first value of the settings' `redirect_uris` entries is - used. Must be present in the end! + used. Must be present in the end! - parameter scope: The scope to request - parameter params: Any additional parameters as dictionary with string keys and values that will be added to the query part - returns: NSURL to be used to start the OAuth dance diff --git a/Tests/FlowTests/OAuth2CodeGrantTests.swift b/Tests/FlowTests/OAuth2CodeGrantTests.swift index c3098081..6e8fa77e 100644 --- a/Tests/FlowTests/OAuth2CodeGrantTests.swift +++ b/Tests/FlowTests/OAuth2CodeGrantTests.swift @@ -271,8 +271,29 @@ class OAuth2CodeGrantTests: XCTestCase { let req2 = try! oauth.accessTokenRequest(with: "pp").asURLRequest(for: oauth) let body2 = String(data: req2.httpBody!, encoding: String.Encoding.utf8) let query2 = OAuth2CodeGrant.params(fromQuery: body2!) - XCTAssertEqual(query2["foo"]!, "bar", "Expecting key `foo` to be `bar`") - } + XCTAssertEqual(query2["foo"], "bar", "Expecting key `foo` to be `bar`") + + oauth.authParameters = ["bar": "hat"] + + // in body + let req3 = try! oauth.accessTokenRequest(with: "pp").asURLRequest(for: oauth) + let body3 = String(data: req3.httpBody!, encoding: String.Encoding.utf8) + let query3 = OAuth2CodeGrant.params(fromQuery: body3!) + XCTAssertEqual(query3["bar"], "hat", "Expecting key `bar` to be `hat`") + } + + func testCustomAuthParametersInit() { + var settings = baseSettings + settings["parameters"] = ["foo": "bar"] + let oauth = OAuth2CodeGrant(settings: settings) + oauth.redirect = "oauth2://callback" + oauth.context.redirectURL = "oauth2://callback" + + let req = try! oauth.accessTokenRequest(with: "pp").asURLRequest(for: oauth) + let body = String(data: req.httpBody!, encoding: String.Encoding.utf8) + let query = OAuth2CodeGrant.params(fromQuery: body!) + XCTAssertEqual(query["foo"], "bar", "Expecting key `foo` to be `bar`") + } func testTokenRequestAgainstAuthURL() { From e088f8e8806e292deedb3c7c5a844bccca9e3f10 Mon Sep 17 00:00:00 2001 From: Pascal Pfiffner Date: Thu, 24 Nov 2016 12:45:12 +0100 Subject: [PATCH 4/4] Prepare for 3.0.1 --- CHANGELOG.md | 7 +++++++ Info.plist | 2 +- generate-docs.sh | 2 +- p2.OAuth2.podspec | 2 +- 4 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index effb0d13..4a4e2c9f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ Version numbering represents the Swift version, plus a running number representi You can also refer to commit logs to get details on what was implemented, fixed and improved. +### 3.0.1 + +- Add Azure flow (thanks @everlof) +- Add `keychain_account_*` settings (thanks @aidzz) +- Workaround for Safari issue (thanks @everlof) + + ### 3.0.0 - Rewrite in Swift 3 diff --git a/Info.plist b/Info.plist index 1d9dbbe0..11061937 100644 --- a/Info.plist +++ b/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 3.0.0 + 3.0.1 CFBundleSignature ???? CFBundleVersion diff --git a/generate-docs.sh b/generate-docs.sh index e0314b6a..779010b1 100755 --- a/generate-docs.sh +++ b/generate-docs.sh @@ -6,7 +6,7 @@ jazzy \ -o "docs" \ --min-acl "internal" \ - --module-version "3.0.0" + --module-version "3.0.1" mkdir docs/assets 2>/dev/null cp assets/* docs/assets/ diff --git a/p2.OAuth2.podspec b/p2.OAuth2.podspec index 45138aff..7c36172e 100644 --- a/p2.OAuth2.podspec +++ b/p2.OAuth2.podspec @@ -7,7 +7,7 @@ Pod::Spec.new do |s| s.name = "p2.OAuth2" - s.version = "3.0.0" + s.version = "3.0.1" s.summary = "OAuth2 framework for macOS, iOS and tvOS, written in Swift." s.description = <<-DESC OAuth2 frameworks for macOS, iOS and tvOS written in Swift.