Skip to content
Open
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
4 changes: 3 additions & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: |
Expand Down
9 changes: 7 additions & 2 deletions Sources/Logto/Core/LogtoCore+Generate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -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
Expand Down
8 changes: 6 additions & 2 deletions Sources/LogtoClient/LogtoAuthSession/LogtoAuthSession.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ class LogtoAuthSession {
let oidcConfig: LogtoCore.OidcConfigResponse
let redirectUri: URL
let socialPlugins: [LogtoSocialPlugin]
let extraParams: [String: String]?

internal var callbackUri: URL?

Expand All @@ -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()
Expand All @@ -42,6 +44,7 @@ class LogtoAuthSession {
self.oidcConfig = oidcConfig
self.redirectUri = redirectUri
self.socialPlugins = socialPlugins
self.extraParams = extraParams
}

func start() async throws -> LogtoCore.CodeTokenResponse {
Expand All @@ -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)
Expand Down
25 changes: 23 additions & 2 deletions Sources/LogtoClient/LogtoClient/LogtoClient+SignIn.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ import Logto
extension LogtoClient {
func signInWithBrowser<AuthSession: LogtoAuthSession>(
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))
Expand All @@ -24,7 +25,8 @@ extension LogtoClient {
logtoConfig: logtoConfig,
oidcConfig: oidcConfig,
redirectUri: redirectUri,
socialPlugins: socialPlugins
socialPlugins: socialPlugins,
extraParams: extraParams
)

let response = try await session.start()
Expand Down Expand Up @@ -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
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
27 changes: 27 additions & 0 deletions Tests/LogtoTests/LogtoCoreTests+Generate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down