Skip to content

Commit 202383d

Browse files
authored
feat(auth): add getLinkIdentityURL (#342)
* feat: add `getLinkIdentityURL` * test: add test for getLinkIdentityURL method
1 parent 8843529 commit 202383d

File tree

8 files changed

+143
-47
lines changed

8 files changed

+143
-47
lines changed

Examples/Examples/Profile/UserIdentityList.swift

Lines changed: 4 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import SwiftUI
1010

1111
struct UserIdentityList: View {
1212
@Environment(\.webAuthenticationSession) private var webAuthenticationSession
13+
@Environment(\.openURL) private var openURL
1314

1415
@State private var identities = ActionState<[UserIdentity], any Error>.idle
1516
@State private var error: (any Error)?
@@ -61,22 +62,9 @@ struct UserIdentityList: View {
6162
Button(provider.rawValue) {
6263
Task {
6364
do {
64-
if #available(iOS 17.4, *) {
65-
let url = try await supabase.auth._getURLForLinkIdentity(provider: provider)
66-
let accessToken = try await supabase.auth.session.accessToken
67-
68-
let callbackURL = try await webAuthenticationSession.authenticate(
69-
using: url,
70-
callback: .customScheme(Constants.redirectToURL.scheme!),
71-
preferredBrowserSession: .shared,
72-
additionalHeaderFields: ["Authorization": "Bearer \(accessToken)"]
73-
)
74-
75-
debug("\(callbackURL)")
76-
} else {
77-
// Fallback on earlier versions
78-
}
79-
65+
let response = try await supabase.auth.getLinkIdentityURL(provider: provider)
66+
openURL(response.url)
67+
debug("getLinkIdentityURL: \(response.url) opened for provider \(response.provider)")
8068
} catch {
8169
self.error = error
8270
}

Examples/supabase/config.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ jwt_expiry = 3600
5353
enable_signup = true
5454
# Allow/disallow testing manual linking of accounts
5555
enable_manual_linking = true
56+
enable_anonymous_sign_ins = true
5657

5758
[auth.email]
5859
# Allow/disallow new user signups via email to your project.

Sources/Auth/AuthClient.swift

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1068,28 +1068,43 @@ public final class AuthClient: @unchecked Sendable {
10681068
try await user().identities ?? []
10691069
}
10701070

1071-
/// Gets an URL that can be used for manual linking identity.
1071+
/// Returns the URL to link the user's identity with an OAuth provider.
1072+
///
1073+
/// This method supports the PKCE flow.
1074+
///
10721075
/// - Parameters:
10731076
/// - provider: The provider you want to link the user with.
10741077
/// - scopes: The scopes to request from the OAuth provider.
10751078
/// - redirectTo: The redirect URL to use, specify a configured deep link.
10761079
/// - queryParams: Additional query parameters to use.
1077-
/// - Returns: A URL that you can use to initiate the OAuth flow.
1078-
///
1079-
/// - Warning: This method is experimental and is expected to change.
1080-
public func _getURLForLinkIdentity(
1080+
public func getLinkIdentityURL(
10811081
provider: Provider,
10821082
scopes: String? = nil,
10831083
redirectTo: URL? = nil,
10841084
queryParams: [(name: String, value: String?)] = []
1085-
) throws -> URL {
1086-
try getURLForProvider(
1085+
) async throws -> OAuthResponse {
1086+
let url = try getURLForProvider(
10871087
url: configuration.url.appendingPathComponent("user/identities/authorize"),
10881088
provider: provider,
10891089
scopes: scopes,
10901090
redirectTo: redirectTo,
1091-
queryParams: queryParams
1091+
queryParams: queryParams,
1092+
skipBrowserRedirect: true
1093+
)
1094+
1095+
struct Response: Codable {
1096+
let url: URL
1097+
}
1098+
1099+
let response = try await api.authorizedExecute(
1100+
Request(
1101+
url: url,
1102+
method: .get
1103+
)
10921104
)
1105+
.decoded(as: Response.self, decoder: configuration.decoder)
1106+
1107+
return OAuthResponse(provider: provider, url: response.url)
10931108
}
10941109

10951110
/// Unlinks an identity from a user by deleting it. The user will no longer be able to sign in
@@ -1202,7 +1217,8 @@ public final class AuthClient: @unchecked Sendable {
12021217
provider: Provider,
12031218
scopes: String? = nil,
12041219
redirectTo: URL? = nil,
1205-
queryParams: [(name: String, value: String?)] = []
1220+
queryParams: [(name: String, value: String?)] = [],
1221+
skipBrowserRedirect: Bool? = nil
12061222
) throws -> URL {
12071223
guard
12081224
var components = URLComponents(
@@ -1234,6 +1250,10 @@ public final class AuthClient: @unchecked Sendable {
12341250
queryItems.append(URLQueryItem(name: "code_challenge_method", value: codeChallengeMethod))
12351251
}
12361252

1253+
if let skipBrowserRedirect {
1254+
queryItems.append(URLQueryItem(name: "skip_http_redirect", value: "\(skipBrowserRedirect)"))
1255+
}
1256+
12371257
queryItems.append(contentsOf: queryParams.map(URLQueryItem.init))
12381258

12391259
components.queryItems = queryItems

Sources/Auth/Types.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -717,3 +717,8 @@ public struct SSOResponse: Codable, Hashable, Sendable {
717717
/// identity provider's authentication flow.
718718
public let url: URL
719719
}
720+
721+
public struct OAuthResponse: Codable, Hashable, Sendable {
722+
public let provider: Provider
723+
public let url: URL
724+
}

Sources/_Helpers/Request.swift

Lines changed: 56 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -78,11 +78,27 @@ package struct HTTPClient: Sendable {
7878
}
7979

8080
package struct Request: Sendable {
81-
public var path: String
82-
public var method: Method
83-
public var query: [URLQueryItem]
84-
public var headers: [String: String]
85-
public var body: Data?
81+
enum _URL {
82+
case absolute(url: URL)
83+
case relative(path: String)
84+
85+
func resolve(withBaseURL baseURL: URL) -> URL {
86+
switch self {
87+
case let .absolute(url): url
88+
case let .relative(path): baseURL.appendingPathComponent(path)
89+
}
90+
}
91+
}
92+
93+
var _url: _URL
94+
package var method: Method
95+
package var query: [URLQueryItem]
96+
package var headers: [String: String]
97+
package var body: Data?
98+
99+
package func url(withBaseURL baseURL: URL) -> URL {
100+
_url.resolve(withBaseURL: baseURL)
101+
}
86102

87103
package enum Method: String, Sendable {
88104
case get = "GET"
@@ -93,22 +109,8 @@ package struct Request: Sendable {
93109
case head = "HEAD"
94110
}
95111

96-
package init(
97-
path: String,
98-
method: Method,
99-
query: [URLQueryItem] = [],
100-
headers: [String: String] = [:],
101-
body: Data? = nil
102-
) {
103-
self.path = path
104-
self.method = method
105-
self.query = query
106-
self.headers = headers
107-
self.body = body
108-
}
109-
110112
package func urlRequest(withBaseURL baseURL: URL) throws -> URLRequest {
111-
var url = baseURL.appendingPathComponent(path)
113+
var url = url(withBaseURL: baseURL)
112114
if !query.isEmpty {
113115
guard var components = URLComponents(url: url, resolvingAgainstBaseURL: false) else {
114116
throw URLError(.badURL)
@@ -158,6 +160,40 @@ package struct Request: Sendable {
158160
}
159161
}
160162

163+
extension Request {
164+
package init(
165+
path: String,
166+
method: Method,
167+
query: [URLQueryItem] = [],
168+
headers: [String: String] = [:],
169+
body: Data? = nil
170+
) {
171+
self.init(
172+
_url: .relative(path: path),
173+
method: method,
174+
query: query,
175+
headers: headers,
176+
body: body
177+
)
178+
}
179+
180+
package init(
181+
url: URL,
182+
method: Method,
183+
query: [URLQueryItem] = [],
184+
headers: [String: String] = [:],
185+
body: Data? = nil
186+
) {
187+
self.init(
188+
_url: .absolute(url: url),
189+
method: method,
190+
query: query,
191+
headers: headers,
192+
body: body
193+
)
194+
}
195+
}
196+
161197
extension CharacterSet {
162198
/// Creates a CharacterSet from RFC 3986 allowed characters.
163199
///

Tests/AuthTests/AuthClientTests.swift

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@
66
//
77

88
@testable import _Helpers
9+
@testable import Auth
910
import ConcurrencyExtras
11+
import CustomDump
1012
import TestHelpers
1113
import XCTest
1214

13-
@testable import Auth
14-
1515
#if canImport(FoundationNetworking)
1616
import FoundationNetworking
1717
#endif
@@ -342,6 +342,32 @@ final class AuthClientTests: XCTestCase {
342342
}
343343
}
344344

345+
func testGetLinkIdentityURL() async throws {
346+
api.execute = { @Sendable _ in
347+
.stub(
348+
"""
349+
{
350+
"url" : "https://github.com/login/oauth/authorize?client_id=1234&redirect_to=com.supabase.swift-examples://&redirect_uri=http://127.0.0.1:54321/auth/v1/callback&response_type=code&scope=user:email&skip_http_redirect=true&state=jwt"
351+
}
352+
"""
353+
)
354+
}
355+
356+
sessionManager.session = { @Sendable _ in .validSession }
357+
codeVerifierStorage = .live
358+
let sut = makeSUT()
359+
360+
let response = try await sut.getLinkIdentityURL(provider: .github)
361+
362+
XCTAssertNoDifference(
363+
response,
364+
OAuthResponse(
365+
provider: .github,
366+
url: URL(string: "https://github.com/login/oauth/authorize?client_id=1234&redirect_to=com.supabase.swift-examples://&redirect_uri=http://127.0.0.1:54321/auth/v1/callback&response_type=code&scope=user:email&skip_http_redirect=true&state=jwt")!
367+
)
368+
)
369+
}
370+
345371
private func makeSUT() -> AuthClient {
346372
let configuration = AuthClient.Configuration(
347373
url: clientURL,

Tests/AuthTests/RequestsTests.swift

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -406,6 +406,21 @@ final class RequestsTests: XCTestCase {
406406
}
407407
}
408408

409+
func testGetLinkIdentityURL() async {
410+
sessionManager.session = { @Sendable _ in .validSession }
411+
412+
let sut = makeSUT()
413+
414+
await assert {
415+
_ = try await sut.getLinkIdentityURL(
416+
provider: .github,
417+
scopes: "user:email",
418+
redirectTo: URL(string: "https://supabase.com"),
419+
queryParams: [("extra_key", "extra_value")]
420+
)
421+
}
422+
}
423+
409424
private func assert(_ block: () async throws -> Void) async {
410425
do {
411426
try await block()
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
curl \
2+
--header "Apikey: dummy.api.key" \
3+
--header "Authorization: Bearer accesstoken" \
4+
--header "X-Client-Info: gotrue-swift/x.y.z" \
5+
"http://localhost:54321/auth/v1/user/identities/authorize?extra_key=extra_value&provider=github&redirect_to=https://supabase.com&scopes=user:email&skip_http_redirect=true"

0 commit comments

Comments
 (0)