Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for Google optional authorization parameters. #15

Merged
merged 3 commits into from
Nov 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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: "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,
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 == "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,
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) }))
}

}
Loading