diff --git a/Sources/Auth/AuthClient.swift b/Sources/Auth/AuthClient.swift index 9948ea1f..22d674ca 100644 --- a/Sources/Auth/AuthClient.swift +++ b/Sources/Auth/AuthClient.swift @@ -380,7 +380,15 @@ public actor AuthClient { /// The ID token is verified for validity and a new session is established. @discardableResult public func signInWithIdToken(credentials: OpenIDConnectCredentials) async throws -> Session { - try await _signIn( + try await _signInWithIdToken(credentials: credentials, linkIdentity: false) + } + + private func _signInWithIdToken(credentials: OpenIDConnectCredentials, linkIdentity: Bool) + async throws -> Session + { + var credentials = credentials + credentials.linkIdentity = linkIdentity + return try await _signIn( request: .init( url: configuration.url.appendingPathComponent("token"), method: .post, @@ -578,7 +586,8 @@ public actor AuthClient { if codeVerifier == nil { logger?.error( - "code verifier not found, a code verifier should exist when calling this method.") + "code verifier not found, a code verifier should exist when calling this method." + ) } let session: Session = try await api.execute( @@ -804,7 +813,8 @@ public actor AuthClient { case .implicit: guard isImplicitGrantFlow(params: params) else { throw AuthError.implicitGrantRedirect( - message: "Not a valid implicit grant flow URL: \(url)") + message: "Not a valid implicit grant flow URL: \(url)" + ) } return try await handleImplicitGrantFlow(params: params) @@ -821,7 +831,8 @@ public actor AuthClient { if let errorDescription = params["error_description"] { throw AuthError.implicitGrantRedirect( - message: errorDescription.replacingOccurrences(of: "+", with: " ")) + message: errorDescription.replacingOccurrences(of: "+", with: " ") + ) } guard @@ -1177,6 +1188,14 @@ public actor AuthClient { try await user().identities ?? [] } + /// Link an identity to the current user using an ID token. + @discardableResult + public func linkIdentityWithIdToken( + credentials: OpenIDConnectCredentials + ) async throws -> Session { + try await _signInWithIdToken(credentials: credentials, linkIdentity: true) + } + /// Links an OAuth identity to an existing user. /// /// This method supports the PKCE flow. @@ -1378,7 +1397,8 @@ public actor AuthClient { ) throws -> URL { guard var components = URLComponents( - url: url, resolvingAgainstBaseURL: false + url: url, + resolvingAgainstBaseURL: false ) else { throw URLError(.badURL) diff --git a/Sources/Auth/Types.swift b/Sources/Auth/Types.swift index 3aca69ca..d03cf8a2 100644 --- a/Sources/Auth/Types.swift +++ b/Sources/Auth/Types.swift @@ -336,6 +336,8 @@ public struct OpenIDConnectCredentials: Codable, Hashable, Sendable { /// Verification token received when the user completes the captcha on the site. public var gotrueMetaSecurity: AuthMetaSecurity? + var linkIdentity: Bool = false + public init( provider: Provider, idToken: String, diff --git a/Tests/AuthTests/AuthClientTests.swift b/Tests/AuthTests/AuthClientTests.swift index 2a364045..477f06d8 100644 --- a/Tests/AuthTests/AuthClientTests.swift +++ b/Tests/AuthTests/AuthClientTests.swift @@ -484,6 +484,43 @@ final class AuthClientTests: XCTestCase { expectNoDifference(receivedURL.value?.absoluteString, url) } + func testLinkIdentityWithIdToken() async throws { + Mock( + url: clientURL.appendingPathComponent("token"), + ignoreQuery: true, + statusCode: 200, + data: [.post: MockData.session] + ) + .snapshotRequest { + #""" + curl \ + --request POST \ + --header "Content-Length: 166" \ + --header "Content-Type: application/json" \ + --header "X-Client-Info: auth-swift/0.0.0" \ + --header "X-Supabase-Api-Version: 2024-01-01" \ + --header "apikey: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0" \ + --data "{\"access_token\":\"access-token\",\"gotrue_meta_security\":{\"captcha_token\":\"captcha-token\"},\"id_token\":\"id-token\",\"link_identity\":true,\"nonce\":\"nonce\",\"provider\":\"apple\"}" \ + "http://localhost:54321/auth/v1/token?grant_type=id_token" + """# + } + .register() + + let sut = makeSUT() + + try await sut.linkIdentityWithIdToken( + credentials: OpenIDConnectCredentials( + provider: .apple, + idToken: "id-token", + accessToken: "access-token", + nonce: "nonce", + gotrueMetaSecurity: AuthMetaSecurity( + captchaToken: "captcha-token" + ) + ) + ) + } + func testAdminListUsers() async throws { Mock( url: clientURL.appendingPathComponent("admin/users"), @@ -712,12 +749,12 @@ final class AuthClientTests: XCTestCase { #""" curl \ --request POST \ - --header "Content-Length: 145" \ + --header "Content-Length: 167" \ --header "Content-Type: application/json" \ --header "X-Client-Info: auth-swift/0.0.0" \ --header "X-Supabase-Api-Version: 2024-01-01" \ --header "apikey: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0" \ - --data "{\"access_token\":\"access-token\",\"gotrue_meta_security\":{\"captcha_token\":\"captcha-token\"},\"id_token\":\"id-token\",\"nonce\":\"nonce\",\"provider\":\"apple\"}" \ + --data "{\"access_token\":\"access-token\",\"gotrue_meta_security\":{\"captcha_token\":\"captcha-token\"},\"id_token\":\"id-token\",\"link_identity\":false,\"nonce\":\"nonce\",\"provider\":\"apple\"}" \ "http://localhost:54321/auth/v1/token?grant_type=id_token" """# } diff --git a/Tests/AuthTests/__Snapshots__/RequestsTests/testSignInWithIdToken.1.txt b/Tests/AuthTests/__Snapshots__/RequestsTests/testSignInWithIdToken.1.txt index 37477c44..5c0100f9 100644 --- a/Tests/AuthTests/__Snapshots__/RequestsTests/testSignInWithIdToken.1.txt +++ b/Tests/AuthTests/__Snapshots__/RequestsTests/testSignInWithIdToken.1.txt @@ -4,5 +4,5 @@ curl \ --header "Content-Type: application/json" \ --header "X-Client-Info: gotrue-swift/x.y.z" \ --header "X-Supabase-Api-Version: 2024-01-01" \ - --data "{\"access_token\":\"access-token\",\"gotrue_meta_security\":{\"captcha_token\":\"captcha-token\"},\"id_token\":\"id-token\",\"nonce\":\"nonce\",\"provider\":\"apple\"}" \ + --data "{\"access_token\":\"access-token\",\"gotrue_meta_security\":{\"captcha_token\":\"captcha-token\"},\"id_token\":\"id-token\",\"link_identity\":false,\"nonce\":\"nonce\",\"provider\":\"apple\"}" \ "http://localhost:54321/auth/v1/token?grant_type=id_token" \ No newline at end of file