Skip to content

Commit

Permalink
Add support for Google optional authorization parameters. (ChimeHQ#15)
Browse files Browse the repository at this point in the history
* Update GoogleAPI to support optional authentication parameters
Cleanup functions to remove unneeded parameters

* Add new tests to ensure AuthorizationURL provider is properly including the optional parameters in the constructed URL.

* Specify userAuthenticator for platforms that need it

---------

Co-authored-by: Matt <[email protected]>
  • Loading branch information
martindufort and mattmassicotte authored Nov 10, 2023
1 parent 7f4c920 commit b33e2e0
Show file tree
Hide file tree
Showing 2 changed files with 94 additions and 8 deletions.
38 changes: 30 additions & 8 deletions Sources/OAuthenticator/Services/GoogleAPI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -58,16 +59,32 @@ 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() {
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 = .init()) -> 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()

Expand All @@ -79,8 +96,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
Expand Down Expand Up @@ -139,7 +161,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)

Expand Down Expand Up @@ -192,7 +214,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)
Expand Down
64 changes: 64 additions & 0 deletions Tests/OAuthenticatorTests/GoogleTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,68 @@ final class GoogleTests: XCTestCase {
sleep(5)
XCTAssert(!login.accessToken.valid)
}

func testSuppliedParameters() throws {
let googleParameters = GoogleAPI.GoogleAPIParameters(includeGrantedScopes: true, loginHint: "[email protected]")

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,
userAuthenticator: Authenticator.failingUserAuthenticator
)

// 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 == "[email protected]" }))
}

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,
userAuthenticator: Authenticator.failingUserAuthenticator
)

// 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) }))
}

}

0 comments on commit b33e2e0

Please sign in to comment.