diff --git a/Sources/Auth/AuthClient.swift b/Sources/Auth/AuthClient.swift index 5e14aa02..2217b088 100644 --- a/Sources/Auth/AuthClient.swift +++ b/Sources/Auth/AuthClient.swift @@ -457,6 +457,76 @@ public actor AuthClient { ) } + /// Attempts a single-sign on using an enterprise Identity Provider. + /// - Parameters: + /// - domain: The email domain to use for signing in. + /// - redirectTo: The URL to redirect the user to after they sign in with the third-party + /// provider. + /// - captchaToken: The captcha token to be used for captcha verification. + /// - Returns: A URL that you can use to initiate the provider's authentication flow. + public func signInWithSSO( + domain: String, + redirectTo: URL? = nil, + captchaToken: String? = nil + ) async throws -> SSOResponse { + await sessionManager.remove() + + let (codeChallenge, codeChallengeMethod) = prepareForPKCE() + + return try await api.execute( + Request( + path: "/sso", + method: .post, + body: configuration.encoder.encode( + SignInWithSSORequest( + providerId: nil, + domain: domain, + redirectTo: redirectTo, + gotrueMetaSecurity: captchaToken.map { AuthMetaSecurity(captchaToken: $0) }, + codeChallenge: codeChallenge, + codeChallengeMethod: codeChallengeMethod + ) + ) + ) + ) + .decoded(decoder: configuration.decoder) + } + + /// Attempts a single-sign on using an enterprise Identity Provider. + /// - Parameters: + /// - providerId: The ID of the SSO provider to use for signing in. + /// - redirectTo: The URL to redirect the user to after they sign in with the third-party + /// provider. + /// - captchaToken: The captcha token to be used for captcha verification. + /// - Returns: A URL that you can use to initiate the provider's authentication flow. + public func signInWithSSO( + providerId: String, + redirectTo: URL? = nil, + captchaToken: String? = nil + ) async throws -> SSOResponse { + await sessionManager.remove() + + let (codeChallenge, codeChallengeMethod) = prepareForPKCE() + + return try await api.execute( + Request( + path: "/sso", + method: .post, + body: configuration.encoder.encode( + SignInWithSSORequest( + providerId: providerId, + domain: nil, + redirectTo: redirectTo, + gotrueMetaSecurity: captchaToken.map { AuthMetaSecurity(captchaToken: $0) }, + codeChallenge: codeChallenge, + codeChallengeMethod: codeChallengeMethod + ) + ) + ) + ) + .decoded(decoder: configuration.decoder) + } + /// Log in an existing user by exchanging an Auth Code issued during the PKCE flow. public func exchangeCodeForSession(authCode: String) async throws -> Session { guard let codeVerifier = try codeVerifierStorage.getCodeVerifier() else { @@ -945,29 +1015,29 @@ public actor AuthClient { } private func prepareForPKCE() -> (codeChallenge: String?, codeChallengeMethod: String?) { - if configuration.flowType == .pkce { - let codeVerifier = PKCE.generateCodeVerifier() - - do { - try codeVerifierStorage.storeCodeVerifier(codeVerifier) - } catch { - assertionFailure( - """ - An error occurred while storing the code verifier, - PKCE flow may not work as expected. - - Error: \(error.localizedDescription) - """ - ) - } + guard configuration.flowType == .pkce else { + return (nil, nil) + } + + let codeVerifier = PKCE.generateCodeVerifier() - let codeChallenge = PKCE.generateCodeChallenge(from: codeVerifier) - let codeChallengeMethod = codeVerifier == codeChallenge ? "plain" : "s256" + do { + try codeVerifierStorage.storeCodeVerifier(codeVerifier) + } catch { + assertionFailure( + """ + An error occurred while storing the code verifier, + PKCE flow may not work as expected. - return (codeChallenge, codeChallengeMethod) + Error: \(error.localizedDescription) + """ + ) } - return (nil, nil) + let codeChallenge = PKCE.generateCodeChallenge(from: codeVerifier) + let codeChallengeMethod = codeVerifier == codeChallenge ? "plain" : "s256" + + return (codeChallenge, codeChallengeMethod) } private func isImplicitGrantFlow(url: URL) -> Bool { diff --git a/Sources/Auth/Types.swift b/Sources/Auth/Types.swift index 9da70cbf..a6ea0e7e 100644 --- a/Sources/Auth/Types.swift +++ b/Sources/Auth/Types.swift @@ -664,3 +664,18 @@ public enum MessagingChannel: String, Codable, Sendable { case sms case whatsapp } + +struct SignInWithSSORequest: Encodable { + let providerId: String? + let domain: String? + let redirectTo: URL? + let gotrueMetaSecurity: AuthMetaSecurity? + let codeChallenge: String? + let codeChallengeMethod: String? +} + +public struct SSOResponse: Codable, Hashable, Sendable { + /// URL to open in a browser which will complete the sign-in flow by taking the user to the + /// identity provider's authentication flow. + public let url: URL +} diff --git a/Tests/AuthTests/RequestsTests.swift b/Tests/AuthTests/RequestsTests.swift index 4c5851b4..13c7a383 100644 --- a/Tests/AuthTests/RequestsTests.swift +++ b/Tests/AuthTests/RequestsTests.swift @@ -375,6 +375,30 @@ final class RequestsTests: XCTestCase { } } + func testSignInWithSSOUsingDomain() async { + let sut = makeSUT() + + await assert { + _ = try await sut.signInWithSSO( + domain: "supabase.com", + redirectTo: URL(string: "https://supabase.com"), + captchaToken: "captcha-token" + ) + } + } + + func testSignInWithSSOUsingProviderId() async { + let sut = makeSUT() + + await assert { + _ = try await sut.signInWithSSO( + providerId: "E621E1F8-C36C-495A-93FC-0C247A3E6E5F", + redirectTo: URL(string: "https://supabase.com"), + captchaToken: "captcha-token" + ) + } + } + private func assert(_ block: () async throws -> Void) async { do { try await block() diff --git a/Tests/AuthTests/__Snapshots__/RequestsTests/testSignInWithSSOUsingDomain.1.txt b/Tests/AuthTests/__Snapshots__/RequestsTests/testSignInWithSSOUsingDomain.1.txt new file mode 100644 index 00000000..5c0e1139 --- /dev/null +++ b/Tests/AuthTests/__Snapshots__/RequestsTests/testSignInWithSSOUsingDomain.1.txt @@ -0,0 +1,7 @@ +curl \ + --request POST \ + --header "Apikey: dummy.api.key" \ + --header "Content-Type: application/json" \ + --header "X-Client-Info: gotrue-swift/x.y.z" \ + --data "{\"domain\":\"supabase.com\",\"gotrue_meta_security\":{\"captcha_token\":\"captcha-token\"},\"redirect_to\":\"https:\/\/supabase.com\"}" \ + "http://localhost:54321/auth/v1/sso" \ No newline at end of file diff --git a/Tests/AuthTests/__Snapshots__/RequestsTests/testSignInWithSSOUsingProviderId.1.txt b/Tests/AuthTests/__Snapshots__/RequestsTests/testSignInWithSSOUsingProviderId.1.txt new file mode 100644 index 00000000..8fbbf196 --- /dev/null +++ b/Tests/AuthTests/__Snapshots__/RequestsTests/testSignInWithSSOUsingProviderId.1.txt @@ -0,0 +1,7 @@ +curl \ + --request POST \ + --header "Apikey: dummy.api.key" \ + --header "Content-Type: application/json" \ + --header "X-Client-Info: gotrue-swift/x.y.z" \ + --data "{\"gotrue_meta_security\":{\"captcha_token\":\"captcha-token\"},\"provider_id\":\"E621E1F8-C36C-495A-93FC-0C247A3E6E5F\",\"redirect_to\":\"https:\/\/supabase.com\"}" \ + "http://localhost:54321/auth/v1/sso" \ No newline at end of file