diff --git a/AeroGearOAuth2.xcodeproj/project.pbxproj b/AeroGearOAuth2.xcodeproj/project.pbxproj index 307c0c0..fb3258a 100644 --- a/AeroGearOAuth2.xcodeproj/project.pbxproj +++ b/AeroGearOAuth2.xcodeproj/project.pbxproj @@ -31,6 +31,8 @@ 48F79D9D1ABC191E00D8B712 /* OAuth2WebViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48F79D9C1ABC191E00D8B712 /* OAuth2WebViewController.swift */; }; 508544F2B38CDEDC0A4CE6C9 /* Pods_AeroGearOAuth2.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 425AC596F1B7BEC7C7342EC5 /* Pods_AeroGearOAuth2.framework */; }; 6F1432B81A168594003BEE5B /* AeroGearOAuth2.framework in Copy Frameworks */ = {isa = PBXBuildFile; fileRef = 4833046219AF1635002F8DA9 /* AeroGearOAuth2.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 830CCE1824ED89F4006CAAD2 /* DictionaryUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 830CCE1724ED89F4006CAAD2 /* DictionaryUtils.swift */; }; + 830CCE1A24ED8E09006CAAD2 /* DictionaryUtilsTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 830CCE1924ED8E09006CAAD2 /* DictionaryUtilsTest.swift */; }; B18D33472E86C2B2AA07D19D /* Pods_AeroGearOAuth2Tests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2917739A0BF73285D969FA56 /* Pods_AeroGearOAuth2Tests.framework */; }; /* End PBXBuildFile section */ @@ -89,6 +91,8 @@ 48F79D9C1ABC191E00D8B712 /* OAuth2WebViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OAuth2WebViewController.swift; sourceTree = ""; }; 4BEBEA405587A6D46CBF5CBC /* Pods-AeroGearOAuth2.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AeroGearOAuth2.release.xcconfig"; path = "Pods/Target Support Files/Pods-AeroGearOAuth2/Pods-AeroGearOAuth2.release.xcconfig"; sourceTree = ""; }; 6CB5D8DB5479CD05722EB5A2 /* Pods-AeroGearOAuth2Tests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AeroGearOAuth2Tests.release.xcconfig"; path = "Pods/Target Support Files/Pods-AeroGearOAuth2Tests/Pods-AeroGearOAuth2Tests.release.xcconfig"; sourceTree = ""; }; + 830CCE1724ED89F4006CAAD2 /* DictionaryUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DictionaryUtils.swift; sourceTree = ""; }; + 830CCE1924ED8E09006CAAD2 /* DictionaryUtilsTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DictionaryUtilsTest.swift; sourceTree = ""; }; 9ADB5FF0CB048392A40B2D60 /* Pods.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods.framework; sourceTree = BUILT_PRODUCTS_DIR; }; A0589EF0A39385A17C91B99C /* Pods-AeroGearOAuth2Tests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AeroGearOAuth2Tests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-AeroGearOAuth2Tests/Pods-AeroGearOAuth2Tests.debug.xcconfig"; sourceTree = ""; }; D75BD934F346EB2E7AB2F18D /* Pods-AeroGearOAuth2.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AeroGearOAuth2.debug.xcconfig"; path = "Pods/Target Support Files/Pods-AeroGearOAuth2/Pods-AeroGearOAuth2.debug.xcconfig"; sourceTree = ""; }; @@ -154,6 +158,7 @@ 48CD52D619B0C1CB008D0694 /* OAuth2Module.swift */, 485D73801A35BCBC0007D9EC /* OpenIdClaim.swift */, 48CD52DC19B0C22A008D0694 /* OAuth2Session.swift */, + 830CCE1724ED89F4006CAAD2 /* DictionaryUtils.swift */, 4872F71519EFE7D100F3FDE8 /* DateUtils.swift */, 4814EF4919C1C0FA008BAC4D /* TrustedPersistantOAuth2Session.swift */, 4814EF4A19C1C0FA008BAC4D /* UntrustedMemoryOAuth2Session.swift */, @@ -181,6 +186,7 @@ 485D73791A35B6240007D9EC /* OpenIDConnectGoogleOAuth2ModuleTest.swift */, 485D737A1A35B6240007D9EC /* OpenIDConnectKeycloakOAuth2ModuleTest.swift */, 48CD52DE19B0CB5A008D0694 /* OAuth2SessionTest.swift */, + 830CCE1924ED8E09006CAAD2 /* DictionaryUtilsTest.swift */, 4872F71919EFE80F00F3FDE8 /* DateUtilsTest.swift */, 48C94D7F19C1F0970000ABBB /* OAuth2ModuleTest.swift */, 485AAB821A37259E00ABEBB2 /* KeycloakOAuth2ModuleTest.swift */, @@ -400,6 +406,7 @@ 4814EF4E19C1C10C008BAC4D /* FacebookOAuth2Module.swift in Sources */, 483304A519AF44DF002F8DA9 /* Config.swift in Sources */, 4814EF4C19C1C0FA008BAC4D /* UntrustedMemoryOAuth2Session.swift in Sources */, + 830CCE1824ED89F4006CAAD2 /* DictionaryUtils.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -409,6 +416,7 @@ files = ( 4872F71A19EFE80F00F3FDE8 /* DateUtilsTest.swift in Sources */, 485D737B1A35B6240007D9EC /* OpenIDConnectFacebookOAuth2ModuleTest.swift in Sources */, + 830CCE1A24ED8E09006CAAD2 /* DictionaryUtilsTest.swift in Sources */, 485AAB941A383FB500ABEBB2 /* OAuth2ModuleMock.swift in Sources */, 48432BE91A2F727C00F7A0FF /* ConfigTest.swift in Sources */, 48C94D8019C1F0970000ABBB /* OAuth2ModuleTest.swift in Sources */, diff --git a/AeroGearOAuth2/AccountManager.swift b/AeroGearOAuth2/AccountManager.swift index ccf95d0..e808198 100644 --- a/AeroGearOAuth2/AccountManager.swift +++ b/AeroGearOAuth2/AccountManager.swift @@ -29,7 +29,7 @@ open class FacebookConfig: Config { :param: accountId this unique id is used by AccountManager to identify the OAuth2 client. :param: isOpenIDConnect to identify if fetching id information is required. */ - public init(clientId: String, clientSecret: String, scopes: [String], accountId: String? = nil, isOpenIDConnect: Bool = false) { + public init(clientId: String, clientSecret: String, scopes: [String], accountId: String? = nil, isOpenIDConnect: Bool = false, additionalParameters: [String: String] = [:]) { super.init(base: "", authzEndpoint: "https://www.facebook.com/dialog/oauth", redirectURL: "fb\(clientId)://authorize/", @@ -41,7 +41,8 @@ open class FacebookConfig: Config { userInfoEndpoint: isOpenIDConnect ? "https://graph.facebook.com/v2.10/me" : nil, scopes: scopes, clientSecret: clientSecret, - accountId: accountId) + accountId: accountId, + additionalParameters: additionalParameters) // Add openIdConnect scope if self.isOpenIDConnect { if self.scopes[0].range(of: "public_profile") == nil { @@ -62,7 +63,7 @@ open class GoogleConfig: Config { :param: accountId this unique id is used by AccountManager to identify the OAuth2 client. :param: isOpenIDConnect to identify if fetching id information is required. */ - public init(clientId: String, scopes: [String], audienceId: String? = nil, accountId: String? = nil, isOpenIDConnect: Bool = false) { + public init(clientId: String, scopes: [String], audienceId: String? = nil, accountId: String? = nil, isOpenIDConnect: Bool = false, additionalParameters: [String: String] = [:]) { let bundleString = Bundle.main.bundleIdentifier ?? "google" super.init(base: "https://accounts.google.com", authzEndpoint: "o/oauth2/v2/auth", @@ -75,7 +76,8 @@ open class GoogleConfig: Config { isOpenIDConnect: isOpenIDConnect, userInfoEndpoint: isOpenIDConnect ? "https://www.googleapis.com/plus/v1/people/me/openIdConnect" : nil, scopes: scopes, - accountId: accountId + accountId: accountId, + additionalParameters: additionalParameters ) // Add openIdConnect scope @@ -95,7 +97,7 @@ open class KeycloakConfig: Config { :param: realm to identify which realm to use. A realm group a set of application/OAuth2 client together. :param: isOpenIDConnect to identify if fetching id information is required. */ - public init(clientId: String, host: String, realm: String? = nil, isOpenIDConnect: Bool = false) { + public init(clientId: String, host: String, realm: String? = nil, isOpenIDConnect: Bool = false, additionalParameters: [String: String] = [:]) { let bundleString = Bundle.main.bundleIdentifier ?? "keycloak" let defaulRealmName = String(format: "%@-realm", clientId) let realm = realm ?? defaulRealmName @@ -107,7 +109,8 @@ open class KeycloakConfig: Config { clientId: clientId, refreshTokenEndpoint: "realms/\(realm)/protocol/openid-connect/token", revokeTokenEndpoint: "realms/\(realm)/protocol/openid-connect/logout", - isOpenIDConnect: isOpenIDConnect + isOpenIDConnect: isOpenIDConnect, + additionalParameters: additionalParameters ) // Add openIdConnect scope if self.isOpenIDConnect { diff --git a/AeroGearOAuth2/Config.swift b/AeroGearOAuth2/Config.swift index 6557a9f..1a6a920 100644 --- a/AeroGearOAuth2/Config.swift +++ b/AeroGearOAuth2/Config.swift @@ -115,6 +115,12 @@ open class Config { Set type of webView to use during OAuth flow. */ open var webView: WebViewType = WebViewType.externalSafari + + /** + Additional parameters is used within the oAuthModule and will concatenate query string parameters + to the request URL + */ + open var additionalParameters: [String: String]? /** A handler to allow the webview to be pushed onto the navigation controller @@ -124,7 +130,7 @@ open class Config { UIApplication.shared.keyWindow?.rootViewController?.present(webView, animated: true, completion: nil) } - public init(base: String, authzEndpoint: String, redirectURL: String, accessTokenEndpoint: String, clientId: String, audienceId: String? = nil, refreshTokenEndpoint: String? = nil, revokeTokenEndpoint: String? = nil, isOpenIDConnect: Bool = false, userInfoEndpoint: String? = nil, scopes: [String] = [], clientSecret: String? = nil, accountId: String? = nil, webView: WebViewType = WebViewType.externalSafari) { + public init(base: String, authzEndpoint: String, redirectURL: String, accessTokenEndpoint: String, clientId: String, audienceId: String? = nil, refreshTokenEndpoint: String? = nil, revokeTokenEndpoint: String? = nil, isOpenIDConnect: Bool = false, userInfoEndpoint: String? = nil, scopes: [String] = [], clientSecret: String? = nil, accountId: String? = nil, webView: WebViewType = WebViewType.externalSafari, additionalParameters: [String: String] = [:]) { self.baseURL = base self.authzEndpoint = authzEndpoint self.redirectURL = redirectURL @@ -139,5 +145,6 @@ open class Config { self.audienceId = audienceId self.accountId = accountId self.webView = webView + self.additionalParameters = additionalParameters } } diff --git a/AeroGearOAuth2/DictionaryUtils.swift b/AeroGearOAuth2/DictionaryUtils.swift new file mode 100644 index 0000000..e4a3fb6 --- /dev/null +++ b/AeroGearOAuth2/DictionaryUtils.swift @@ -0,0 +1,39 @@ +/* +* JBoss, Home of Professional Open Source. +* Copyright Red Hat, Inc., and individual contributors +* +* 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 + +/** +Handy extensions for Dictionary +*/ + +extension Dictionary { + + /** + Returns a formatted URL query string from a Dictionary of [String: String]. + + :returns: a formatted string object. + */ + + public func toQueryString() -> String { + let queryString = self.compactMap({ (key, value) -> String in + return "\(key)=\(value)" + }).joined(separator: "&") + return queryString + } + +} diff --git a/AeroGearOAuth2/OAuth2Module.swift b/AeroGearOAuth2/OAuth2Module.swift index fb15ad5..8901018 100644 --- a/AeroGearOAuth2/OAuth2Module.swift +++ b/AeroGearOAuth2/OAuth2Module.swift @@ -137,6 +137,9 @@ open class OAuth2Module: AuthzModule { if let audienceId = config.audienceId { params = "\(params)&audience=\(audienceId)" } + + // add additional parameters + params.append("&\(config.additionalParameters?.toQueryString() ?? "")") guard let computedUrl = http.calculateURL(baseURL: config.baseURL, url: config.authzEndpoint) else { let error = NSError(domain:AGAuthzErrorDomain, code:0, userInfo:["NSLocalizedDescriptionKey": "Malformatted URL."]) diff --git a/AeroGearOAuth2Tests/DictionaryUtilsTest.swift b/AeroGearOAuth2Tests/DictionaryUtilsTest.swift new file mode 100644 index 0000000..16b928a --- /dev/null +++ b/AeroGearOAuth2Tests/DictionaryUtilsTest.swift @@ -0,0 +1,36 @@ +/* +* JBoss, Home of Professional Open Source. +* Copyright Red Hat, Inc., and individual contributors +* +* 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 XCTest +import AeroGearOAuth2 + +class DictionaryUtilsTest: XCTestCase { + + override func setUp() { + super.setUp() + } + + override func tearDown() { + // Put teardown code here. This method is called after the invocation of each test method in the class. + super.tearDown() + } + + func testQueryParsing() { + let queryDict = ["site": "mySite", "key": "12345"] + XCTAssertEqual(queryDict.toQueryString(), "site=mySite&key=12345") + } + +}