diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d528eec..13e0fc7 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -26,7 +26,9 @@ jobs: path: BuildTools/.build key: ${{ runner.os }}-spm-v1-${{ hashFiles('BuildTools/Package.resolved') }} - - run: sudo xcode-select -s /Applications/Xcode_14.1.app + - uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: latest-stable - name: SwiftFormat run: | diff --git a/Sources/Logto/Core/LogtoCore+Generate.swift b/Sources/Logto/Core/LogtoCore+Generate.swift index fc42246..7a6e4b1 100644 --- a/Sources/Logto/Core/LogtoCore+Generate.swift +++ b/Sources/Logto/Core/LogtoCore+Generate.swift @@ -24,7 +24,8 @@ public extension LogtoCore { state: String, scopes: [String] = [], resources: [String] = [], - prompt: Prompt = .consent + prompt: Prompt = .consent, + extraParams: [String: String]? = nil ) throws -> URL { guard var components = URLComponents(string: authorizationEndpoint), @@ -51,7 +52,11 @@ public extension LogtoCore { URLQueryItem(name: "resource", value: $0) } - components.queryItems = (baseQueryItems + resourceQueryItems).filter { $0.value != "" } + let extraParamsQueryItems = extraParams?.map { + URLQueryItem(name: $0.key, value: $0.value) + } ?? [] + + components.queryItems = (baseQueryItems + resourceQueryItems + extraParamsQueryItems).filter { $0.value != "" } guard let url = components.url else { throw LogtoErrors.UrlConstruction.unableToConstructUrl diff --git a/Sources/LogtoClient/LogtoAuthSession/LogtoAuthSession.swift b/Sources/LogtoClient/LogtoAuthSession/LogtoAuthSession.swift index f75562b..daf4b00 100644 --- a/Sources/LogtoClient/LogtoAuthSession/LogtoAuthSession.swift +++ b/Sources/LogtoClient/LogtoAuthSession/LogtoAuthSession.swift @@ -22,6 +22,7 @@ class LogtoAuthSession { let oidcConfig: LogtoCore.OidcConfigResponse let redirectUri: URL let socialPlugins: [LogtoSocialPlugin] + let extraParams: [String: String]? internal var callbackUri: URL? @@ -30,7 +31,8 @@ class LogtoAuthSession { logtoConfig: LogtoConfig, oidcConfig: LogtoCore.OidcConfigResponse, redirectUri: URL, - socialPlugins: [LogtoSocialPlugin] + socialPlugins: [LogtoSocialPlugin], + extraParams: [String: String]? = nil ) { authContext = LogtoAuthContext() state = LogtoUtilities.generateState() @@ -42,6 +44,7 @@ class LogtoAuthSession { self.oidcConfig = oidcConfig self.redirectUri = redirectUri self.socialPlugins = socialPlugins + self.extraParams = extraParams } func start() async throws -> LogtoCore.CodeTokenResponse { @@ -54,7 +57,8 @@ class LogtoAuthSession { state: state, scopes: logtoConfig.scopes, resources: logtoConfig.resources, - prompt: logtoConfig.prompt + prompt: logtoConfig.prompt, + extraParams: extraParams ) #if !os(macOS) diff --git a/Sources/LogtoClient/LogtoClient/LogtoClient+SignIn.swift b/Sources/LogtoClient/LogtoClient/LogtoClient+SignIn.swift index 57c37de..c9752d7 100644 --- a/Sources/LogtoClient/LogtoClient/LogtoClient+SignIn.swift +++ b/Sources/LogtoClient/LogtoClient/LogtoClient+SignIn.swift @@ -12,7 +12,8 @@ import Logto extension LogtoClient { func signInWithBrowser( authSessionType _: AuthSession.Type, - redirectUri: String + redirectUri: String, + extraParams: [String: String]? = nil ) async throws { guard let redirectUri = URL(string: redirectUri) else { throw (LogtoClientErrors.SignIn(type: .unableToConstructRedirectUri, innerError: nil)) @@ -24,7 +25,8 @@ extension LogtoClient { logtoConfig: logtoConfig, oidcConfig: oidcConfig, redirectUri: redirectUri, - socialPlugins: socialPlugins + socialPlugins: socialPlugins, + extraParams: extraParams ) let response = try await session.start() @@ -63,4 +65,23 @@ extension LogtoClient { redirectUri: redirectUri ) } + + /** + Start a sign in session with WKWebView with additional parameters. If the function returns with no error threw, it means the user has signed in successfully. + + - Parameters: + - redirectUri: One of Redirect URIs of this application. + - extraParams: Additional parameters to be passed to the authorization endpoint. + - Throws: An error if the session failed to complete. + */ + public func signInWithBrowser( + redirectUri: String, + extraParams: [String: String] + ) async throws { + try await signInWithBrowser( + authSessionType: LogtoAuthSession.self, + redirectUri: redirectUri, + extraParams: extraParams + ) + } } diff --git a/Tests/LogtoClientTests/LogtoClient/LogtoClientTests+SignIn.swift b/Tests/LogtoClientTests/LogtoClient/LogtoClientTests+SignIn.swift index 802b182..b36fe9b 100644 --- a/Tests/LogtoClientTests/LogtoClient/LogtoClientTests+SignIn.swift +++ b/Tests/LogtoClientTests/LogtoClient/LogtoClientTests+SignIn.swift @@ -23,7 +23,44 @@ class LogtoAuthSessionFailureMock: LogtoAuthSession { } } +class LogtoAuthSessionExtraParamsMock: LogtoAuthSession { + override func start() async throws -> LogtoCore.CodeTokenResponse { + // Verify that extraParams were set correctly + XCTAssertNotNil(extraParams) + XCTAssertEqual(extraParams?["tenant_id"], "test_tenant") + XCTAssertEqual(extraParams?["ui_locales"], "en-US") + + return try! JSONDecoder().decode(LogtoCore.CodeTokenResponse.self, from: Data(""" + { + "accessToken": "foo", + "refreshToken": "bar", + "idToken": "baz", + "scope": "openid offline_access", + "expiresIn": 300 + } + """.utf8)) + } +} + extension LogtoClientTests { + func testSignInWithExtraParams() async throws { + let client = buildClient(withToken: true) + + do { + try await client.signInWithBrowser( + authSessionType: LogtoAuthSessionExtraParamsMock.self, + redirectUri: "io.logto.dev://callback", + extraParams: ["tenant_id": "test_tenant", "ui_locales": "en-US"] + ) + } catch let error as LogtoClientErrors.JwkSet { + // Expected error since we don't have JWKS endpoint in the mock + XCTAssertEqual(error.type, .unableToFetchJwkSet) + return + } + + XCTFail() + } + func testSignInUnableToFetchJwkSet() async throws { let client = buildClient(withToken: true) diff --git a/Tests/LogtoTests/LogtoCoreTests+Generate.swift b/Tests/LogtoTests/LogtoCoreTests+Generate.swift index bb71c04..3afff07 100644 --- a/Tests/LogtoTests/LogtoCoreTests+Generate.swift +++ b/Tests/LogtoTests/LogtoCoreTests+Generate.swift @@ -160,6 +160,33 @@ extension LogtoCoreTests { ])) } + func testGenerateSignInUriWithExtraParams() throws { + let codeChallenge = LogtoUtilities.generateCodeChallenge(codeVerifier: codeVerifier) + + let url = try LogtoCore.generateSignInUri( + authorizationEndpoint: authorizationEndpoint, + clientId: clientId, + redirectUri: URL(string: "logto://sign-in/redirect")!, + codeChallenge: codeChallenge, + state: state, + extraParams: ["tenant_id": "my_tenant", "ui_locales": "en-US"] + ) + try validateBaseInformation(url: url) + + XCTAssertTrue(validate(url: url, queryItems: [ + URLQueryItem(name: "client_id", value: "foo"), + URLQueryItem(name: "redirect_uri", value: "logto://sign-in/redirect"), + URLQueryItem(name: "code_challenge", value: codeChallenge), + URLQueryItem(name: "code_challenge_method", value: "S256"), + URLQueryItem(name: "state", value: state), + URLQueryItem(name: "scope", value: "offline_access openid profile"), + URLQueryItem(name: "response_type", value: "code"), + URLQueryItem(name: "prompt", value: "consent"), + URLQueryItem(name: "tenant_id", value: "my_tenant"), + URLQueryItem(name: "ui_locales", value: "en-US"), + ])) + } + func testGenerateSignOutUri() throws { XCTAssertThrowsError(try LogtoCore .generateSignOutUri(endSessionEndpoint: "???", idToken: "", postLogoutRedirectUri: nil)) {