Skip to content

Commit 618875a

Browse files
authored
Merge pull request #912 from supabase-community/not-authenticated-reason
Add new event system, add support for error code in OTP links
2 parents d559d20 + 69aaee4 commit 618875a

File tree

17 files changed

+431
-57
lines changed

17 files changed

+431
-57
lines changed

Auth/src/androidMain/kotlin/io/github/jan/supabase/auth/Android.kt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,14 @@ fun SupabaseClient.handleDeeplinks(intent: Intent, onSessionSuccess: (UserSessio
3939
when(this.auth.config.flowType) {
4040
FlowType.IMPLICIT -> {
4141
val fragment = data.fragment ?: return
42-
auth.parseFragmentAndImportSession(fragment, onSessionSuccess)
42+
auth.parseFragmentAndImportSession(fragment) {
43+
it?.let(onSessionSuccess)
44+
}
4345
}
4446
FlowType.PKCE -> {
47+
if(auth.handledUrlParameterError { data.getQueryParameter(it) }) {
48+
return
49+
}
4550
val code = data.getQueryParameter("code") ?: return
4651
(auth as AuthImpl).authScope.launch {
4752
this@handleDeeplinks.auth.exchangeCodeForSession(code)

Auth/src/androidMain/kotlin/io/github/jan/supabase/auth/setupPlatform.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import androidx.lifecycle.LifecycleOwner
66
import androidx.lifecycle.ProcessLifecycleOwner
77
import androidx.startup.Initializer
88
import io.github.jan.supabase.annotations.SupabaseInternal
9+
import io.github.jan.supabase.auth.status.SessionStatus
910
import io.github.jan.supabase.logging.d
1011
import kotlinx.coroutines.Dispatchers
1112
import kotlinx.coroutines.launch
@@ -59,7 +60,7 @@ private fun addLifecycleCallbacks(gotrue: Auth) {
5960
Auth.logger.d { "Cancelling auto refresh because app is switching to the background" }
6061
scope.launch {
6162
gotrue.stopAutoRefreshForCurrentSession()
62-
gotrue.resetLoadingState()
63+
gotrue.setSessionStatus(SessionStatus.Initializing)
6364
}
6465
}
6566
}

Auth/src/appleMain/kotlin/io/github/jan/supabase/auth/Apple.kt

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,16 +26,25 @@ fun SupabaseClient.handleDeeplinks(url: NSURL, onSessionSuccess: (UserSession) -
2626
Auth.logger.d { "No fragment for deeplink" }
2727
return
2828
}
29-
auth.parseFragmentAndImportSession(fragment, onSessionSuccess)
29+
auth.parseFragmentAndImportSession(fragment) {
30+
it?.let(onSessionSuccess)
31+
}
3032
}
3133
FlowType.PKCE -> {
3234
val components = NSURLComponents(url, false)
33-
val code = (components.queryItems?.firstOrNull { it is NSURLQueryItem && it.name == "code" } as? NSURLQueryItem)?.value ?: return
35+
if (auth.handledUrlParameterError{ key -> getQueryItem(components, key) }) {
36+
return
37+
}
38+
val code = getQueryItem(components, "code") ?: return
3439
val scope = (auth as AuthImpl).authScope
3540
scope.launch {
3641
auth.exchangeCodeForSession(code)
3742
onSessionSuccess(auth.currentSessionOrNull() ?: error("No session available"))
3843
}
3944
}
4045
}
46+
}
47+
48+
private fun getQueryItem(components: NSURLComponents, key: String): String? {
49+
return (components.queryItems?.firstOrNull { it is NSURLQueryItem && it.name == key } as? NSURLQueryItem)?.value
4150
}

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
@@ -1,7 +1,10 @@
11
package io.github.jan.supabase.auth
22

33
import io.github.jan.supabase.SupabaseClient
4+
import io.github.jan.supabase.annotations.SupabaseExperimental
5+
import io.github.jan.supabase.annotations.SupabaseInternal
46
import io.github.jan.supabase.auth.admin.AdminApi
7+
import io.github.jan.supabase.auth.event.AuthEvent
58
import io.github.jan.supabase.auth.exception.AuthRestException
69
import io.github.jan.supabase.auth.exception.AuthWeakPasswordException
710
import io.github.jan.supabase.auth.mfa.MfaApi
@@ -26,6 +29,7 @@ import io.github.jan.supabase.plugins.MainPlugin
2629
import io.github.jan.supabase.plugins.SupabasePluginProvider
2730
import io.ktor.client.plugins.HttpRequestTimeoutException
2831
import kotlinx.coroutines.ensureActive
32+
import kotlinx.coroutines.flow.SharedFlow
2933
import kotlinx.coroutines.flow.StateFlow
3034
import kotlinx.serialization.json.JsonObject
3135
import kotlin.coroutines.coroutineContext
@@ -55,6 +59,12 @@ interface Auth : MainPlugin<AuthConfig>, CustomSerializationPlugin {
5559
*/
5660
val sessionStatus: StateFlow<SessionStatus>
5761

62+
/**
63+
* Events emitted by the auth plugin
64+
*/
65+
@SupabaseExperimental
66+
val events: SharedFlow<AuthEvent>
67+
5868
/**
5969
* Whether the [sessionStatus] session is getting refreshed automatically
6070
*/
@@ -347,6 +357,18 @@ interface Auth : MainPlugin<AuthConfig>, CustomSerializationPlugin {
347357
*/
348358
suspend fun clearSession()
349359

360+
/**
361+
* Sets the session status to the specified [status]
362+
*/
363+
@SupabaseInternal
364+
fun setSessionStatus(status: SessionStatus)
365+
366+
/**
367+
* Emits an event to the [events] flow
368+
*/
369+
@SupabaseInternal
370+
fun emitEvent(event: AuthEvent)
371+
350372
/**
351373
* Exchanges a code for a session. Used when using the [FlowType.PKCE] flow
352374
* @param code The code to exchange
@@ -419,7 +441,17 @@ interface Auth : MainPlugin<AuthConfig>, CustomSerializationPlugin {
419441
"token_type",
420442
"type",
421443
"provider_refresh_token",
422-
"provider_token"
444+
"provider_token",
445+
"error",
446+
"error_code",
447+
"error_description",
448+
)
449+
450+
internal val QUERY_PARAMETERS = listOf(
451+
"code",
452+
"error_code",
453+
"error",
454+
"error_description",
423455
)
424456

425457
override val key = "auth"

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

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,7 @@ internal fun noDeeplinkError(arg: String): Nothing = error("""
2121
* @return The parsed session. Note that the user will be null, but you can retrieve it using [Auth.retrieveUser]
2222
*/
2323
fun Auth.parseSessionFromFragment(fragment: String): UserSession {
24-
val sessionParts = fragment.split("&").associate {
25-
it.split("=").let { pair ->
26-
pair[0] to pair[1]
27-
}
28-
}
24+
val sessionParts = getFragmentParts(fragment)
2925

3026
Auth.logger.d { "Fragment parts: $sessionParts" }
3127

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

Lines changed: 41 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import io.github.jan.supabase.annotations.SupabaseExperimental
55
import io.github.jan.supabase.annotations.SupabaseInternal
66
import io.github.jan.supabase.auth.admin.AdminApi
77
import io.github.jan.supabase.auth.admin.AdminApiImpl
8+
import io.github.jan.supabase.auth.event.AuthEvent
89
import io.github.jan.supabase.auth.exception.AuthRestException
910
import io.github.jan.supabase.auth.exception.AuthSessionMissingException
1011
import io.github.jan.supabase.auth.exception.AuthWeakPasswordException
@@ -44,8 +45,11 @@ import kotlinx.coroutines.SupervisorJob
4445
import kotlinx.coroutines.cancel
4546
import kotlinx.coroutines.delay
4647
import kotlinx.coroutines.ensureActive
48+
import kotlinx.coroutines.flow.MutableSharedFlow
4749
import kotlinx.coroutines.flow.MutableStateFlow
50+
import kotlinx.coroutines.flow.SharedFlow
4851
import kotlinx.coroutines.flow.StateFlow
52+
import kotlinx.coroutines.flow.asSharedFlow
4953
import kotlinx.coroutines.flow.asStateFlow
5054
import kotlinx.coroutines.flow.first
5155
import kotlinx.coroutines.launch
@@ -73,6 +77,9 @@ internal class AuthImpl(
7377

7478
private val _sessionStatus = MutableStateFlow<SessionStatus>(SessionStatus.Initializing)
7579
override val sessionStatus: StateFlow<SessionStatus> = _sessionStatus.asStateFlow()
80+
private val _events = MutableSharedFlow<AuthEvent>(replay = 1)
81+
override val events: SharedFlow<AuthEvent> = _events.asSharedFlow()
82+
@Suppress("DEPRECATION")
7683
internal val authScope = CoroutineScope((config.coroutineDispatcher ?: supabaseClient.coroutineDispatcher) + SupervisorJob())
7784
override val sessionManager = config.sessionManager ?: createDefaultSessionManager()
7885
override val codeVerifierCache = config.codeVerifierCache ?: createDefaultCodeVerifierCache()
@@ -99,7 +106,6 @@ internal class AuthImpl(
99106

100107
override fun init() {
101108
Auth.logger.d { "Initializing Auth plugin..." }
102-
setupPlatform()
103109
if (config.autoLoadFromStorage) {
104110
authScope.launch {
105111
Auth.logger.i {
@@ -112,15 +118,16 @@ internal class AuthImpl(
112118
}
113119
} else {
114120
Auth.logger.i {
115-
"No session found. Setting session status to NotAuthenticated."
121+
"No session found in storage."
116122
}
117-
_sessionStatus.value = SessionStatus.NotAuthenticated(false)
123+
setSessionStatus(SessionStatus.NotAuthenticated())
118124
}
119125
}
120126
} else {
121127
Auth.logger.d { "Skipping loading from storage (autoLoadFromStorage is set to false)" }
122-
_sessionStatus.value = SessionStatus.NotAuthenticated(false)
128+
setSessionStatus(SessionStatus.NotAuthenticated())
123129
}
130+
setupPlatform()
124131
Auth.logger.d { "Initialized Auth plugin" }
125132
}
126133

@@ -184,7 +191,7 @@ internal class AuthImpl(
184191
val session = currentSessionOrNull() ?: return
185192
val newUser = session.user?.copy(identities = session.user.identities?.filter { it.identityId != identityId })
186193
val newSession = session.copy(user = newUser)
187-
_sessionStatus.value = SessionStatus.Authenticated(newSession, SessionSource.UserIdentitiesChanged(session))
194+
setSessionStatus(SessionStatus.Authenticated(newSession, SessionSource.UserIdentitiesChanged(session)))
188195
}
189196
}
190197

@@ -237,7 +244,7 @@ internal class AuthImpl(
237244
if (this.config.autoSaveToStorage) {
238245
sessionManager.saveSession(newSession)
239246
}
240-
_sessionStatus.value = SessionStatus.Authenticated(newSession, SessionSource.UserChanged(newSession))
247+
setSessionStatus(SessionStatus.Authenticated(newSession, SessionSource.UserChanged(newSession)))
241248
}
242249
return userInfo
243250
}
@@ -364,7 +371,7 @@ internal class AuthImpl(
364371
if (updateSession) {
365372
val session = currentSessionOrNull() ?: error("No session found")
366373
val newStatus = SessionStatus.Authenticated(session.copy(user = user), SessionSource.UserChanged(currentSessionOrNull() ?: error("Session shouldn't be null")))
367-
_sessionStatus.value = newStatus
374+
setSessionStatus(newStatus)
368375
if (config.autoSaveToStorage) sessionManager.saveSession(newStatus.session)
369376
}
370377
return user
@@ -420,7 +427,7 @@ internal class AuthImpl(
420427
sessionManager.saveSession(session)
421428
Auth.logger.d { "Session saved to storage (no auto refresh)" }
422429
}
423-
_sessionStatus.value = SessionStatus.Authenticated(session, source)
430+
setSessionStatus(SessionStatus.Authenticated(session, source))
424431
Auth.logger.d { "Session imported successfully." }
425432
return
426433
}
@@ -429,22 +436,24 @@ internal class AuthImpl(
429436
Auth.logger.d { "Session is under the threshold date. Refreshing session..." }
430437
tryImportingSession(
431438
{ handleExpiredSession(session, config.alwaysAutoRefresh) },
432-
{ importSession(session) }
439+
{ importSession(session) },
440+
{ updateStatusIfExpired(session, it) }
433441
)
434442
} else {
435443
if (config.autoSaveToStorage) {
436444
sessionManager.saveSession(session)
437445
Auth.logger.d { "Session saved to storage (auto refresh enabled)" }
438446
}
439-
_sessionStatus.value = SessionStatus.Authenticated(session, source)
447+
setSessionStatus(SessionStatus.Authenticated(session, source))
440448
Auth.logger.d { "Session imported successfully. Starting auto refresh..." }
441449
sessionJob?.cancel()
442450
sessionJob = authScope.launch {
443451
delayBeforeExpiry(session)
444452
launch {
445453
tryImportingSession(
446454
{ handleExpiredSession(session) },
447-
{ importSession(session, source = source) }
455+
{ importSession(session, source = source) },
456+
{ updateStatusIfExpired(session, it) }
448457
)
449458
}
450459
}
@@ -455,14 +464,15 @@ internal class AuthImpl(
455464
@Suppress("MagicNumber")
456465
private suspend fun tryImportingSession(
457466
importRefreshedSession: suspend () -> Unit,
458-
retry: suspend () -> Unit
467+
retry: suspend () -> Unit,
468+
updateStatus: suspend (RefreshFailureCause) -> Unit
459469
) {
460470
try {
461471
importRefreshedSession()
462472
} catch (e: RestException) {
463473
if (e.statusCode in 500..599) {
464474
Auth.logger.e(e) { "Couldn't refresh session due to an internal server error. Retrying in ${config.retryDelay} (Status code ${e.statusCode})..." }
465-
_sessionStatus.value = SessionStatus.RefreshFailure(RefreshFailureCause.InternalServerError(e))
475+
updateStatus(RefreshFailureCause.InternalServerError(e))
466476
delay(config.retryDelay)
467477
retry()
468478
} else {
@@ -472,12 +482,20 @@ internal class AuthImpl(
472482
} catch (e: Exception) {
473483
coroutineContext.ensureActive()
474484
Auth.logger.e(e) { "Couldn't reach Supabase. Either the address doesn't exist or the network might not be on. Retrying in ${config.retryDelay}..." }
475-
_sessionStatus.value = SessionStatus.RefreshFailure(RefreshFailureCause.NetworkError(e))
485+
updateStatus(RefreshFailureCause.NetworkError(e))
476486
delay(config.retryDelay)
477487
retry()
478488
}
479489
}
480490

491+
private fun updateStatusIfExpired(session: UserSession, reason: RefreshFailureCause) {
492+
if (session.expiresAt <= Clock.System.now()) {
493+
Auth.logger.d { "Session expired while trying to refresh the session. Updating status..." }
494+
setSessionStatus(SessionStatus.RefreshFailure(reason))
495+
}
496+
emitEvent(AuthEvent.RefreshFailure(reason))
497+
}
498+
481499
private suspend fun delayBeforeExpiry(session: UserSession) {
482500
val timeAtBeginningOfSession = session.expiresAt - session.expiresIn.seconds
483501

@@ -590,16 +608,22 @@ internal class AuthImpl(
590608
codeVerifierCache.deleteCodeVerifier()
591609
sessionManager.deleteSession()
592610
sessionJob?.cancel()
593-
_sessionStatus.value = SessionStatus.NotAuthenticated(true)
611+
setSessionStatus(SessionStatus.NotAuthenticated(true))
594612
sessionJob = null
595613
}
596614

597615
override suspend fun awaitInitialization() {
598616
sessionStatus.first { it !is SessionStatus.Initializing }
599617
}
600618

601-
fun resetLoadingState() {
602-
_sessionStatus.value = SessionStatus.Initializing
619+
override fun setSessionStatus(status: SessionStatus) {
620+
Auth.logger.d { "Setting session status to $status" }
621+
_sessionStatus.value = status
622+
}
623+
624+
override fun emitEvent(event: AuthEvent) {
625+
Auth.logger.d { "Emitting event $event" }
626+
_events.tryEmit(event)
603627
}
604628

605629
/**

0 commit comments

Comments
 (0)