From 7065ee215649db076358d994761c8dcd19b66f04 Mon Sep 17 00:00:00 2001 From: Martin Dufort Date: Wed, 8 Nov 2023 16:51:08 -0500 Subject: [PATCH 1/3] Update GoogleAPI to support optional authentication parameters Cleanup functions to remove unneeded parameters --- .../OAuthenticator/Services/GoogleAPI.swift | 33 ++++++++++++++----- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/Sources/OAuthenticator/Services/GoogleAPI.swift b/Sources/OAuthenticator/Services/GoogleAPI.swift index 3a29c2e..cad7d0c 100644 --- a/Sources/OAuthenticator/Services/GoogleAPI.swift +++ b/Sources/OAuthenticator/Services/GoogleAPI.swift @@ -18,6 +18,7 @@ public struct GoogleAPI { static let scopeKey: String = "scope" static let includeGrantedScopeKey: String = "include_granted_scopes" + static let loginHint: String = "login_hint" static let codeKey: String = "code" static let refreshTokenKey: String = "refresh_token" @@ -58,16 +59,27 @@ public struct GoogleAPI { } } - public static func googleAPITokenHandling(with parameters: AppCredentials) -> TokenHandling { + /// Optional Google API Parameters for authorization request + public struct GoogleAPIParameters: Sendable { + public var includeGrantedScopes: Bool + public var loginHint: String? + + public init(includeGrantedScopes: Bool, loginHint: String?) { + self.includeGrantedScopes = includeGrantedScopes + self.loginHint = loginHint + } + } + + public static func googleAPITokenHandling(with parameters: GoogleAPIParameters) -> TokenHandling { TokenHandling(authorizationURLProvider: Self.authorizationURLProvider(with: parameters), - loginProvider: Self.loginProvider(with: parameters), - refreshProvider: Self.refreshProvider(with: parameters)) + loginProvider: Self.loginProvider(), + refreshProvider: Self.refreshProvider()) } /// This is part 1 of the OAuth process /// /// Will request an authentication `code` based on the acceptance by the user - public static func authorizationURLProvider(with parameters: AppCredentials) -> TokenHandling.AuthorizationURLProvider { + public static func authorizationURLProvider(with parameters: GoogleAPIParameters) -> TokenHandling.AuthorizationURLProvider { return { credentials in var urlBuilder = URLComponents() @@ -79,8 +91,13 @@ public struct GoogleAPI { URLQueryItem(name: GoogleAPI.redirectURIKey, value: credentials.callbackURL.absoluteString), URLQueryItem(name: GoogleAPI.responseTypeKey, value: GoogleAPI.responseTypeCode), URLQueryItem(name: GoogleAPI.scopeKey, value: credentials.scopeString), - URLQueryItem(name: GoogleAPI.includeGrantedScopeKey, value: "true") // Will include previously granted scoped for this user - ] + URLQueryItem(name: GoogleAPI.includeGrantedScopeKey, value: String(parameters.includeGrantedScopes)) + ] + + // Add login hint if provided + if let loginHint = parameters.loginHint { + urlBuilder.queryItems?.append(URLQueryItem(name: GoogleAPI.loginHint, value: loginHint)) + } guard let url = urlBuilder.url else { throw AuthenticatorError.missingAuthorizationURL @@ -139,7 +156,7 @@ public struct GoogleAPI { return request } - static func loginProvider(with parameters: AppCredentials) -> TokenHandling.LoginProvider { + static func loginProvider() -> TokenHandling.LoginProvider { return { url, appCredentials, tokenURL, urlLoader in let request = try authenticationRequest(url: url, appCredentials: appCredentials) @@ -192,7 +209,7 @@ public struct GoogleAPI { return request } - static func refreshProvider(with parameters: AppCredentials) -> TokenHandling.RefreshProvider { + static func refreshProvider() -> TokenHandling.RefreshProvider { return { login, appCredentials, urlLoader in let request = try authenticationRefreshRequest(login: login, appCredentials: appCredentials) let (data, _) = try await urlLoader(request) From 831f70031a5c4fdee2093f38de44f13808647f1c Mon Sep 17 00:00:00 2001 From: Martin Dufort Date: Fri, 10 Nov 2023 10:21:04 -0500 Subject: [PATCH 2/3] Add new tests to ensure AuthorizationURL provider is properly including the optional parameters in the constructed URL. --- .../OAuthenticator/Services/GoogleAPI.swift | 7 ++- Tests/OAuthenticatorTests/GoogleTests.swift | 56 +++++++++++++++++++ 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/Sources/OAuthenticator/Services/GoogleAPI.swift b/Sources/OAuthenticator/Services/GoogleAPI.swift index cad7d0c..742fdef 100644 --- a/Sources/OAuthenticator/Services/GoogleAPI.swift +++ b/Sources/OAuthenticator/Services/GoogleAPI.swift @@ -64,13 +64,18 @@ public struct GoogleAPI { public var includeGrantedScopes: Bool public var loginHint: String? + public init() { + self.includeGrantedScopes = true + self.loginHint = nil + } + public init(includeGrantedScopes: Bool, loginHint: String?) { self.includeGrantedScopes = includeGrantedScopes self.loginHint = loginHint } } - public static func googleAPITokenHandling(with parameters: GoogleAPIParameters) -> TokenHandling { + public static func googleAPITokenHandling(with parameters: GoogleAPIParameters = .init()) -> TokenHandling { TokenHandling(authorizationURLProvider: Self.authorizationURLProvider(with: parameters), loginProvider: Self.loginProvider(), refreshProvider: Self.refreshProvider()) diff --git a/Tests/OAuthenticatorTests/GoogleTests.swift b/Tests/OAuthenticatorTests/GoogleTests.swift index 153e3cd..c9e9eeb 100644 --- a/Tests/OAuthenticatorTests/GoogleTests.swift +++ b/Tests/OAuthenticatorTests/GoogleTests.swift @@ -32,4 +32,60 @@ final class GoogleTests: XCTestCase { sleep(5) XCTAssert(!login.accessToken.valid) } + + func testSuppliedParameters() throws { + let googleParameters = GoogleAPI.GoogleAPIParameters(includeGrantedScopes: true, loginHint: "john@doe.com") + + XCTAssertNotNil(googleParameters.loginHint) + XCTAssertTrue(googleParameters.includeGrantedScopes) + + let callback = URL(string: "callback://google_api") + XCTAssertNotNil(callback) + + let creds = AppCredentials(clientId: "client_id", clientPassword: "client_pwd", scopes: ["scope1", "scope2"], callbackURL: callback!) + let tokenHandling = GoogleAPI.googleAPITokenHandling(with: googleParameters) + let config = Authenticator.Configuration(appCredentials: creds, tokenHandling: tokenHandling) + + // Validate URL is properly constructed + let googleURLProvider = try config.tokenHandling.authorizationURLProvider(creds) + + let urlComponent = URLComponents(url: googleURLProvider, resolvingAgainstBaseURL: true) + XCTAssertNotNil(urlComponent) + XCTAssertEqual(urlComponent!.scheme, GoogleAPI.scheme) + + // Validate query items inclusion and value + XCTAssertNotNil(urlComponent!.queryItems) + XCTAssertTrue(urlComponent!.queryItems!.contains(where: { $0.name == GoogleAPI.includeGrantedScopeKey })) + XCTAssertTrue(urlComponent!.queryItems!.contains(where: { $0.name == GoogleAPI.loginHint })) + XCTAssertTrue(urlComponent!.queryItems!.contains(where: { $0.value == String(true) })) + XCTAssertTrue(urlComponent!.queryItems!.contains(where: { $0.value == "john@doe.com" })) + } + + func testDefaultParameters() throws { + let googleParameters = GoogleAPI.GoogleAPIParameters() + + XCTAssertNil(googleParameters.loginHint) + XCTAssertTrue(googleParameters.includeGrantedScopes) + + let callback = URL(string: "callback://google_api") + XCTAssertNotNil(callback) + + let creds = AppCredentials(clientId: "client_id", clientPassword: "client_pwd", scopes: ["scope1", "scope2"], callbackURL: callback!) + let tokenHandling = GoogleAPI.googleAPITokenHandling(with: googleParameters) + let config = Authenticator.Configuration(appCredentials: creds, tokenHandling: tokenHandling) + + // Validate URL is properly constructed + let googleURLProvider = try config.tokenHandling.authorizationURLProvider(creds) + + let urlComponent = URLComponents(url: googleURLProvider, resolvingAgainstBaseURL: true) + XCTAssertNotNil(urlComponent) + XCTAssertEqual(urlComponent!.scheme, GoogleAPI.scheme) + + // Validate query items inclusion and value + XCTAssertNotNil(urlComponent!.queryItems) + XCTAssertTrue(urlComponent!.queryItems!.contains(where: { $0.name == GoogleAPI.includeGrantedScopeKey })) + XCTAssertFalse(urlComponent!.queryItems!.contains(where: { $0.name == GoogleAPI.loginHint })) + XCTAssertTrue(urlComponent!.queryItems!.contains(where: { $0.value == String(true) })) + } + } From e002130a89e96ea8e0766140fb49f2939eeae5d1 Mon Sep 17 00:00:00 2001 From: Matt <85322+mattmassicotte@users.noreply.github.com> Date: Fri, 10 Nov 2023 11:10:31 -0500 Subject: [PATCH 3/3] Specify userAuthenticator for platforms that need it --- Tests/OAuthenticatorTests/GoogleTests.swift | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/Tests/OAuthenticatorTests/GoogleTests.swift b/Tests/OAuthenticatorTests/GoogleTests.swift index c9e9eeb..0211823 100644 --- a/Tests/OAuthenticatorTests/GoogleTests.swift +++ b/Tests/OAuthenticatorTests/GoogleTests.swift @@ -44,8 +44,12 @@ final class GoogleTests: XCTestCase { let creds = AppCredentials(clientId: "client_id", clientPassword: "client_pwd", scopes: ["scope1", "scope2"], callbackURL: callback!) let tokenHandling = GoogleAPI.googleAPITokenHandling(with: googleParameters) - let config = Authenticator.Configuration(appCredentials: creds, tokenHandling: tokenHandling) - + let config = Authenticator.Configuration( + appCredentials: creds, + tokenHandling: tokenHandling, + userAuthenticator: Authenticator.failingUserAuthenticator + ) + // Validate URL is properly constructed let googleURLProvider = try config.tokenHandling.authorizationURLProvider(creds) @@ -72,8 +76,12 @@ final class GoogleTests: XCTestCase { let creds = AppCredentials(clientId: "client_id", clientPassword: "client_pwd", scopes: ["scope1", "scope2"], callbackURL: callback!) let tokenHandling = GoogleAPI.googleAPITokenHandling(with: googleParameters) - let config = Authenticator.Configuration(appCredentials: creds, tokenHandling: tokenHandling) - + let config = Authenticator.Configuration( + appCredentials: creds, + tokenHandling: tokenHandling, + userAuthenticator: Authenticator.failingUserAuthenticator + ) + // Validate URL is properly constructed let googleURLProvider = try config.tokenHandling.authorizationURLProvider(creds)