Skip to content

Add new event system, add support for error code in OTP links #912

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 15 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,14 @@ fun SupabaseClient.handleDeeplinks(intent: Intent, onSessionSuccess: (UserSessio
when(this.auth.config.flowType) {
FlowType.IMPLICIT -> {
val fragment = data.fragment ?: return
auth.parseFragmentAndImportSession(fragment, onSessionSuccess)
auth.parseFragmentAndImportSession(fragment) {
it?.let(onSessionSuccess)
}
}
FlowType.PKCE -> {
if(auth.handledUrlParameterError { data.getQueryParameter(it) }) {
return
}
val code = data.getQueryParameter("code") ?: return
(auth as AuthImpl).authScope.launch {
[email protected](code)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ProcessLifecycleOwner
import androidx.startup.Initializer
import io.github.jan.supabase.annotations.SupabaseInternal
import io.github.jan.supabase.auth.status.SessionStatus
import io.github.jan.supabase.logging.d
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
Expand Down Expand Up @@ -59,7 +60,7 @@ private fun addLifecycleCallbacks(gotrue: Auth) {
Auth.logger.d { "Cancelling auto refresh because app is switching to the background" }
scope.launch {
gotrue.stopAutoRefreshForCurrentSession()
gotrue.resetLoadingState()
gotrue.setSessionStatus(SessionStatus.Initializing)
}
}
}
Expand Down
13 changes: 11 additions & 2 deletions Auth/src/appleMain/kotlin/io/github/jan/supabase/auth/Apple.kt
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,25 @@ fun SupabaseClient.handleDeeplinks(url: NSURL, onSessionSuccess: (UserSession) -
Auth.logger.d { "No fragment for deeplink" }
return
}
auth.parseFragmentAndImportSession(fragment, onSessionSuccess)
auth.parseFragmentAndImportSession(fragment) {
it?.let(onSessionSuccess)
}
}
FlowType.PKCE -> {
val components = NSURLComponents(url, false)
val code = (components.queryItems?.firstOrNull { it is NSURLQueryItem && it.name == "code" } as? NSURLQueryItem)?.value ?: return
if (auth.handledUrlParameterError{ key -> getQueryItem(components, key) }) {
return
}
val code = getQueryItem(components, "code") ?: return
val scope = (auth as AuthImpl).authScope
scope.launch {
auth.exchangeCodeForSession(code)
onSessionSuccess(auth.currentSessionOrNull() ?: error("No session available"))
}
}
}
}

private fun getQueryItem(components: NSURLComponents, key: String): String? {
return (components.queryItems?.firstOrNull { it is NSURLQueryItem && it.name == key } as? NSURLQueryItem)?.value
}
34 changes: 33 additions & 1 deletion Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/Auth.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package io.github.jan.supabase.auth

import io.github.jan.supabase.SupabaseClient
import io.github.jan.supabase.annotations.SupabaseExperimental
import io.github.jan.supabase.annotations.SupabaseInternal
import io.github.jan.supabase.auth.admin.AdminApi
import io.github.jan.supabase.auth.event.AuthEvent
import io.github.jan.supabase.auth.exception.AuthRestException
import io.github.jan.supabase.auth.exception.AuthWeakPasswordException
import io.github.jan.supabase.auth.mfa.MfaApi
Expand All @@ -26,6 +29,7 @@ import io.github.jan.supabase.plugins.MainPlugin
import io.github.jan.supabase.plugins.SupabasePluginProvider
import io.ktor.client.plugins.HttpRequestTimeoutException
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.serialization.json.JsonObject
import kotlin.coroutines.coroutineContext
Expand Down Expand Up @@ -55,6 +59,12 @@ interface Auth : MainPlugin<AuthConfig>, CustomSerializationPlugin {
*/
val sessionStatus: StateFlow<SessionStatus>

/**
* Events emitted by the auth plugin
*/
@SupabaseExperimental
val events: SharedFlow<AuthEvent>

/**
* Whether the [sessionStatus] session is getting refreshed automatically
*/
Expand Down Expand Up @@ -347,6 +357,18 @@ interface Auth : MainPlugin<AuthConfig>, CustomSerializationPlugin {
*/
suspend fun clearSession()

/**
* Sets the session status to the specified [status]
*/
@SupabaseInternal
fun setSessionStatus(status: SessionStatus)

/**
* Emits an event to the [events] flow
*/
@SupabaseInternal
fun emitEvent(event: AuthEvent)

/**
* Exchanges a code for a session. Used when using the [FlowType.PKCE] flow
* @param code The code to exchange
Expand Down Expand Up @@ -419,7 +441,17 @@ interface Auth : MainPlugin<AuthConfig>, CustomSerializationPlugin {
"token_type",
"type",
"provider_refresh_token",
"provider_token"
"provider_token",
"error",
"error_code",
"error_description",
)

internal val QUERY_PARAMETERS = listOf(
"code",
"error_code",
"error",
"error_description",
)

override val key = "auth"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,7 @@ internal fun noDeeplinkError(arg: String): Nothing = error("""
* @return The parsed session. Note that the user will be null, but you can retrieve it using [Auth.retrieveUser]
*/
fun Auth.parseSessionFromFragment(fragment: String): UserSession {
val sessionParts = fragment.split("&").associate {
it.split("=").let { pair ->
pair[0] to pair[1]
}
}
val sessionParts = getFragmentParts(fragment)

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import io.github.jan.supabase.annotations.SupabaseExperimental
import io.github.jan.supabase.annotations.SupabaseInternal
import io.github.jan.supabase.auth.admin.AdminApi
import io.github.jan.supabase.auth.admin.AdminApiImpl
import io.github.jan.supabase.auth.event.AuthEvent
import io.github.jan.supabase.auth.exception.AuthRestException
import io.github.jan.supabase.auth.exception.AuthSessionMissingException
import io.github.jan.supabase.auth.exception.AuthWeakPasswordException
Expand Down Expand Up @@ -44,8 +45,11 @@ import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
Expand Down Expand Up @@ -73,6 +77,9 @@ internal class AuthImpl(

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

override fun init() {
Auth.logger.d { "Initializing Auth plugin..." }
setupPlatform()
if (config.autoLoadFromStorage) {
authScope.launch {
Auth.logger.i {
Expand All @@ -112,15 +118,16 @@ internal class AuthImpl(
}
} else {
Auth.logger.i {
"No session found. Setting session status to NotAuthenticated."
"No session found in storage."
}
_sessionStatus.value = SessionStatus.NotAuthenticated(false)
setSessionStatus(SessionStatus.NotAuthenticated())
}
}
} else {
Auth.logger.d { "Skipping loading from storage (autoLoadFromStorage is set to false)" }
_sessionStatus.value = SessionStatus.NotAuthenticated(false)
setSessionStatus(SessionStatus.NotAuthenticated())
}
setupPlatform()
Auth.logger.d { "Initialized Auth plugin" }
}

Expand Down Expand Up @@ -184,7 +191,7 @@ internal class AuthImpl(
val session = currentSessionOrNull() ?: return
val newUser = session.user?.copy(identities = session.user.identities?.filter { it.identityId != identityId })
val newSession = session.copy(user = newUser)
_sessionStatus.value = SessionStatus.Authenticated(newSession, SessionSource.UserIdentitiesChanged(session))
setSessionStatus(SessionStatus.Authenticated(newSession, SessionSource.UserIdentitiesChanged(session)))
}
}

Expand Down Expand Up @@ -237,7 +244,7 @@ internal class AuthImpl(
if (this.config.autoSaveToStorage) {
sessionManager.saveSession(newSession)
}
_sessionStatus.value = SessionStatus.Authenticated(newSession, SessionSource.UserChanged(newSession))
setSessionStatus(SessionStatus.Authenticated(newSession, SessionSource.UserChanged(newSession)))
}
return userInfo
}
Expand Down Expand Up @@ -364,7 +371,7 @@ internal class AuthImpl(
if (updateSession) {
val session = currentSessionOrNull() ?: error("No session found")
val newStatus = SessionStatus.Authenticated(session.copy(user = user), SessionSource.UserChanged(currentSessionOrNull() ?: error("Session shouldn't be null")))
_sessionStatus.value = newStatus
setSessionStatus(newStatus)
if (config.autoSaveToStorage) sessionManager.saveSession(newStatus.session)
}
return user
Expand Down Expand Up @@ -420,7 +427,7 @@ internal class AuthImpl(
sessionManager.saveSession(session)
Auth.logger.d { "Session saved to storage (no auto refresh)" }
}
_sessionStatus.value = SessionStatus.Authenticated(session, source)
setSessionStatus(SessionStatus.Authenticated(session, source))
Auth.logger.d { "Session imported successfully." }
return
}
Expand All @@ -429,22 +436,24 @@ internal class AuthImpl(
Auth.logger.d { "Session is under the threshold date. Refreshing session..." }
tryImportingSession(
{ handleExpiredSession(session, config.alwaysAutoRefresh) },
{ importSession(session) }
{ importSession(session) },
{ updateStatusIfExpired(session, it) }
)
} else {
if (config.autoSaveToStorage) {
sessionManager.saveSession(session)
Auth.logger.d { "Session saved to storage (auto refresh enabled)" }
}
_sessionStatus.value = SessionStatus.Authenticated(session, source)
setSessionStatus(SessionStatus.Authenticated(session, source))
Auth.logger.d { "Session imported successfully. Starting auto refresh..." }
sessionJob?.cancel()
sessionJob = authScope.launch {
delayBeforeExpiry(session)
launch {
tryImportingSession(
{ handleExpiredSession(session) },
{ importSession(session, source = source) }
{ importSession(session, source = source) },
{ updateStatusIfExpired(session, it) }
)
}
}
Expand All @@ -455,14 +464,15 @@ internal class AuthImpl(
@Suppress("MagicNumber")
private suspend fun tryImportingSession(
importRefreshedSession: suspend () -> Unit,
retry: suspend () -> Unit
retry: suspend () -> Unit,
updateStatus: suspend (RefreshFailureCause) -> Unit
) {
try {
importRefreshedSession()
} catch (e: RestException) {
if (e.statusCode in 500..599) {
Auth.logger.e(e) { "Couldn't refresh session due to an internal server error. Retrying in ${config.retryDelay} (Status code ${e.statusCode})..." }
_sessionStatus.value = SessionStatus.RefreshFailure(RefreshFailureCause.InternalServerError(e))
updateStatus(RefreshFailureCause.InternalServerError(e))
delay(config.retryDelay)
retry()
} else {
Expand All @@ -472,12 +482,20 @@ internal class AuthImpl(
} catch (e: Exception) {
coroutineContext.ensureActive()
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}..." }
_sessionStatus.value = SessionStatus.RefreshFailure(RefreshFailureCause.NetworkError(e))
updateStatus(RefreshFailureCause.NetworkError(e))
delay(config.retryDelay)
retry()
}
}

private fun updateStatusIfExpired(session: UserSession, reason: RefreshFailureCause) {
if (session.expiresAt <= Clock.System.now()) {
Auth.logger.d { "Session expired while trying to refresh the session. Updating status..." }
setSessionStatus(SessionStatus.RefreshFailure(reason))
}
emitEvent(AuthEvent.RefreshFailure(reason))
}

private suspend fun delayBeforeExpiry(session: UserSession) {
val timeAtBeginningOfSession = session.expiresAt - session.expiresIn.seconds

Expand Down Expand Up @@ -590,16 +608,22 @@ internal class AuthImpl(
codeVerifierCache.deleteCodeVerifier()
sessionManager.deleteSession()
sessionJob?.cancel()
_sessionStatus.value = SessionStatus.NotAuthenticated(true)
setSessionStatus(SessionStatus.NotAuthenticated(true))
sessionJob = null
}

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

fun resetLoadingState() {
_sessionStatus.value = SessionStatus.Initializing
override fun setSessionStatus(status: SessionStatus) {
Auth.logger.d { "Setting session status to $status" }
_sessionStatus.value = status
}

override fun emitEvent(event: AuthEvent) {
Auth.logger.d { "Emitting event $event" }
_events.tryEmit(event)
}

/**
Expand Down
Loading
Loading