Skip to content

Commit 1c7d384

Browse files
authored
Merge pull request #1051 from supabase-community/oidc-linkidentity
Add support for linkIdentity with OIDC
2 parents 19d3365 + c0e2f86 commit 1c7d384

File tree

4 files changed

+79
-2
lines changed

4 files changed

+79
-2
lines changed

Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/Auth.kt

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@ import io.github.jan.supabase.auth.mfa.MfaApi
1111
import io.github.jan.supabase.auth.providers.AuthProvider
1212
import io.github.jan.supabase.auth.providers.ExternalAuthConfigDefaults
1313
import io.github.jan.supabase.auth.providers.Google
14+
import io.github.jan.supabase.auth.providers.IDTokenProvider
1415
import io.github.jan.supabase.auth.providers.OAuthProvider
1516
import io.github.jan.supabase.auth.providers.builtin.Email
17+
import io.github.jan.supabase.auth.providers.builtin.IDToken
1618
import io.github.jan.supabase.auth.providers.builtin.Phone
1719
import io.github.jan.supabase.auth.providers.builtin.SSO
1820
import io.github.jan.supabase.auth.status.SessionSource
@@ -164,7 +166,13 @@ interface Auth : MainPlugin<AuthConfig>, CustomSerializationPlugin {
164166
/**
165167
* Links an OAuth Identity to an existing user.
166168
*
167-
* This methods works similar to signing in with OAuth providers. Refer to the [documentation](https://supabase.com/docs/reference/kotlin/initializing) to learn how to handle OAuth and OTP links.
169+
* Example:
170+
* ```kotlin
171+
* val url = supabase.auth.linkIdentity(Google)
172+
* // Open the url in the browser, but this will happen automatically if [ExternalAuthConfigDefaults.automaticallyOpenUrl] is true (which it is by default)
173+
* ```
174+
*
175+
* This method works similar to signing in with OAuth providers. Refer to the [documentation](https://supabase.com/docs/reference/kotlin/initializing) to learn how to handle OAuth and OTP links.
168176
* @param provider The OAuth provider
169177
* @param redirectUrl The redirect url to use. If you don't specify this, the platform specific will be used, like deeplinks on android.
170178
* @param config Extra configuration
@@ -179,6 +187,30 @@ interface Auth : MainPlugin<AuthConfig>, CustomSerializationPlugin {
179187
config: ExternalAuthConfigDefaults.() -> Unit = {}
180188
): String?
181189

190+
/**
191+
* Links an identity to the current user using an ID token.
192+
*
193+
* Example:
194+
* ```kotlin
195+
* supabase.auth.linkIdentityWithIdToken(provider = Google, idToken = "idToken") {
196+
* // Optional nonce
197+
* nonce = "nonce"
198+
* }
199+
* ```
200+
*
201+
* @param provider One of the [IDTokenProvider] providers.
202+
* @param idToken The ID token to use
203+
* @param config Extra configuration
204+
* @throws RestException or one of its subclasses if receiving an error response. If the error response contains a error code, an [AuthRestException] will be thrown which can be used to easier identify the problem.
205+
* @throws HttpRequestTimeoutException if the request timed out
206+
* @throws HttpRequestException on network related issues
207+
*/
208+
suspend fun linkIdentityWithIdToken(
209+
provider: IDTokenProvider,
210+
idToken: String,
211+
config: (IDToken.Config).() -> Unit = {}
212+
)
213+
182214
/**
183215
* Unlinks an OAuth Identity from an existing user.
184216
* @param identityId The id of the OAuth identity

Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/AuthImpl.kt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ import io.github.jan.supabase.auth.mfa.MfaApi
1313
import io.github.jan.supabase.auth.mfa.MfaApiImpl
1414
import io.github.jan.supabase.auth.providers.AuthProvider
1515
import io.github.jan.supabase.auth.providers.ExternalAuthConfigDefaults
16+
import io.github.jan.supabase.auth.providers.IDTokenProvider
1617
import io.github.jan.supabase.auth.providers.OAuthProvider
18+
import io.github.jan.supabase.auth.providers.builtin.IDToken
1719
import io.github.jan.supabase.auth.providers.builtin.OTP
1820
import io.github.jan.supabase.auth.providers.builtin.SSO
1921
import io.github.jan.supabase.auth.status.RefreshFailureCause
@@ -185,6 +187,16 @@ internal class AuthImpl(
185187
return null
186188
}
187189

190+
override suspend fun linkIdentityWithIdToken(
191+
provider: IDTokenProvider,
192+
idToken: String,
193+
config: (IDToken.Config) -> Unit
194+
) {
195+
val body = IDToken.Config(idToken = idToken, provider = provider, linkIdentity = true).apply(config)
196+
val result = api.postJson("token?grant_type=id_token", body)
197+
importSession(result.safeBody(), source = SessionSource.UserIdentitiesChanged(result.safeBody()))
198+
}
199+
188200
override suspend fun unlinkIdentity(identityId: String, updateLocalUser: Boolean) {
189201
api.delete("user/identities/$identityId")
190202
if (updateLocalUser) {

Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/providers/builtin/IDToken.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package io.github.jan.supabase.auth.providers.builtin
22

3+
import io.github.jan.supabase.annotations.SupabaseInternal
34
import io.github.jan.supabase.auth.providers.Apple
45
import io.github.jan.supabase.auth.providers.Azure
56
import io.github.jan.supabase.auth.providers.Facebook
@@ -39,7 +40,8 @@ data object IDToken : DefaultAuthProvider<IDToken.Config, UserInfo> {
3940
@SerialName("id_token") var idToken: String = "",
4041
var provider: IDTokenProvider? = null,
4142
@SerialName("access_token") var accessToken: String? = null,
42-
var nonce: String? = null
43+
var nonce: String? = null,
44+
@property:SupabaseInternal var linkIdentity: Boolean = false
4345
) : DefaultAuthProvider.Config()
4446

4547
@OptIn(ExperimentalSerializationApi::class)

Auth/src/commonTest/kotlin/AuthApiTest.kt

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -385,6 +385,37 @@ class AuthRequestTest {
385385
}
386386
}
387387

388+
@Test
389+
fun testLinkIdentityWithIdToken() {
390+
runTest {
391+
val expectedProvider = Google
392+
val expectedIdToken = "idToken"
393+
val expectedAccessToken = "accessToken"
394+
val expectedNonce = "nonce"
395+
val client = createMockedSupabaseClient(configuration = configuration) {
396+
val body = it.body.toJsonElement().jsonObject
397+
val params = it.url.parameters
398+
assertMethodIs(HttpMethod.Post, it.method)
399+
assertPathIs("/token", it.url.pathAfterVersion())
400+
assertEquals("id_token", params["grant_type"])
401+
assertEquals(expectedIdToken, body["id_token"]?.jsonPrimitive?.content)
402+
assertEquals(expectedProvider.name, body["provider"]?.jsonPrimitive?.content)
403+
assertEquals(expectedAccessToken, body["access_token"]?.jsonPrimitive?.content)
404+
assertEquals(expectedNonce, body["nonce"]?.jsonPrimitive?.content)
405+
// ensure we signal linking
406+
assertEquals("true", body["linkIdentity"]?.jsonPrimitive?.content)
407+
respondJson(
408+
sampleUserSession()
409+
)
410+
}
411+
client.auth.linkIdentityWithIdToken(expectedProvider, expectedIdToken) {
412+
accessToken = expectedAccessToken
413+
nonce = expectedNonce
414+
}
415+
assertNotNull(client.auth.currentSessionOrNull(), "Session should not be null")
416+
}
417+
}
418+
388419
@Test
389420
fun testUnlinkIdentity() {
390421
runTest {

0 commit comments

Comments
 (0)