Skip to content
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

Handle weak_password and session_not_found auth error codes #596

Merged
merged 4 commits into from
May 21, 2024
Merged
Show file tree
Hide file tree
Changes from all 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 @@ -4,6 +4,7 @@ import io.github.jan.supabase.SupabaseClient
import io.github.jan.supabase.exceptions.HttpRequestException
import io.github.jan.supabase.exceptions.RestException
import io.github.jan.supabase.gotrue.admin.AdminApi
import io.github.jan.supabase.gotrue.exception.AuthWeakPasswordException
import io.github.jan.supabase.gotrue.mfa.MfaApi
import io.github.jan.supabase.gotrue.providers.AuthProvider
import io.github.jan.supabase.gotrue.providers.ExternalAuthConfigDefaults
Expand Down Expand Up @@ -79,12 +80,14 @@ sealed interface Auth : MainPlugin<AuthConfig>, CustomSerializationPlugin {
*
* Example:
* ```kotlin
* val result = gotrue.signUpWith(Email) {
* val result = auth.signUpWith(Email) {
* email = "[email protected]"
* password = "password"
* }
* ```
* or
* gotrue.signUpWith(Google) // Opens the browser to login with google
* ```kotlin
* auth.signUpWith(Google) // Opens the browser to login with google
* ```
*
* @param provider the provider to use for signing up. E.g. [Email], [Phone] or [Google]
Expand All @@ -94,6 +97,7 @@ sealed interface Auth : MainPlugin<AuthConfig>, CustomSerializationPlugin {
* @throws RestException or one of its subclasses if receiving an error response
* @throws HttpRequestTimeoutException if the request timed out
* @throws HttpRequestException on network related issues
* @throws AuthWeakPasswordException if using the [Email] or [Phone] provider and the password is too weak. You can get the reasons via [AuthWeakPasswordException.reasons]
*/
suspend fun <C, R, Provider : AuthProvider<C, R>> signUpWith(
provider: Provider,
Expand All @@ -106,12 +110,14 @@ sealed interface Auth : MainPlugin<AuthConfig>, CustomSerializationPlugin {
*
* Example:
* ```kotlin
* val result = gotrue.signInWith(Email) {
* val result = auth.signInWith(Email) {
* email = "[email protected]"
* password = "password"
* }
* ```
* or
* gotrue.signInWith(Google) // Opens the browser to login with google
* ```kotlin
* auth.signInWith(Google) // Opens the browser to login with google
* ```
*
* @param provider the provider to use for signing in. E.g. [Email], [Phone] or [Google]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import io.github.jan.supabase.exceptions.UnauthorizedRestException
import io.github.jan.supabase.exceptions.UnknownRestException
import io.github.jan.supabase.gotrue.admin.AdminApi
import io.github.jan.supabase.gotrue.admin.AdminApiImpl
import io.github.jan.supabase.gotrue.exception.AuthSessionMissingException
import io.github.jan.supabase.gotrue.exception.AuthWeakPasswordException
import io.github.jan.supabase.gotrue.mfa.MfaApi
import io.github.jan.supabase.gotrue.mfa.MfaApiImpl
import io.github.jan.supabase.gotrue.providers.AuthProvider
Expand Down Expand Up @@ -460,6 +462,7 @@ internal class AuthImpl(
override suspend fun parseErrorResponse(response: HttpResponse): RestException {
val errorBody =
response.bodyOrNull<GoTrueErrorResponse>() ?: GoTrueErrorResponse("Unknown error", "")
checkErrorCodes(errorBody)?.let { return it }
return when (response.status) {
HttpStatusCode.Unauthorized -> UnauthorizedRestException(
errorBody.error,
Expand All @@ -471,7 +474,6 @@ internal class AuthImpl(
response,
errorBody.description
)

HttpStatusCode.UnprocessableEntity -> BadRequestRestException(
errorBody.error,
response,
Expand All @@ -481,6 +483,20 @@ internal class AuthImpl(
}
}

private fun checkErrorCodes(error: GoTrueErrorResponse): RestException? {
return when (error.error) {
AuthWeakPasswordException.CODE -> AuthWeakPasswordException(error.error, error.weakPassword?.reasons ?: emptyList())
AuthSessionMissingException.CODE -> {
authScope.launch {
Auth.logger.e { "Received session not found api error. Clearing session..." }
clearSession()
}
AuthSessionMissingException()
}
else -> null
}
}

@OptIn(SupabaseExperimental::class)
override fun getOAuthUrl(
provider: OAuthProvider,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,24 @@ import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.descriptors.buildClassSerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonDecoder
import kotlinx.serialization.json.decodeFromJsonElement
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive

@Serializable(with = GoTrueErrorResponse.Companion::class)
internal data class GoTrueErrorResponse(
val error: String,
val description: String?
val description: String? = null,
val weakPassword: WeakPassword? = null
) {

@Serializable
data class WeakPassword(
val reasons: List<String>
)

companion object : KSerializer<GoTrueErrorResponse> {

override val descriptor = buildClassSerialDescriptor("GoTrueErrorResponse") {
Expand All @@ -25,9 +33,12 @@ internal data class GoTrueErrorResponse(
override fun deserialize(decoder: Decoder): GoTrueErrorResponse {
decoder as JsonDecoder
val json = decoder.decodeJsonElement()
val error = json.jsonObject["error"]?.jsonPrimitive?.content ?: json.jsonObject["msg"]?.jsonPrimitive?.content ?: json.toString()
val description = json.jsonObject["error_description"]?.jsonPrimitive?.content
return GoTrueErrorResponse(error, description)
val error = json.jsonObject["error_code"]?.jsonPrimitive?.content ?: "unknown_error"
val description = json.jsonObject["error_description"]?.jsonPrimitive?.content ?: json.jsonObject["msg"]?.jsonPrimitive?.content ?: json.jsonObject["message"]?.jsonPrimitive?.content ?: json.toString()
val weakPassword = if(json.jsonObject.containsKey("weak_password")) {
Json.decodeFromJsonElement<WeakPassword>(json.jsonObject["weak_password"]!!)
} else null
return GoTrueErrorResponse(error, description, weakPassword)
}

override fun serialize(encoder: Encoder, value: GoTrueErrorResponse) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package io.github.jan.supabase.gotrue.exception

import io.github.jan.supabase.exceptions.RestException

/**
* Base class for rest exceptions thrown by the Auth API.
*/
open class AuthRestException(errorCode: String, message: String): RestException(
error = errorCode,
description = null,
message = message
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package io.github.jan.supabase.gotrue.exception

/**
* Exception thrown when a session is not found.
*/
class AuthSessionMissingException: AuthRestException(
errorCode = CODE,
message = "Session not found. This can happen if the user was logged out or deleted."
) {

internal companion object {
const val CODE = "session_not_found"
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package io.github.jan.supabase.gotrue.exception

/**
* Exception thrown when a session is not found.
* @param description The description of the exception.
* @param reasons The reasons why the password is weak.
*/
class AuthWeakPasswordException(
description: String,
val reasons: List<String>
) : AuthRestException(
CODE,
description,
) {

internal companion object {
const val CODE = "weak_password"
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,16 @@ import io.ktor.client.statement.request

/**
* Base class for all response-related exceptions
* @param error The error returned by supabase
* @param description The error description returned by supabase
*
* Plugins may extend this class to provide more specific exceptions
* @param error The error returned by Supabase
* @param description The error description returned by Supabase
* @see UnauthorizedRestException
* @see BadRequestRestException
* @see NotFoundRestException
* @see UnknownRestException
*/
sealed class RestException(val error: String, val description: String?, message: String): Exception(message) {
open class RestException(val error: String, val description: String?, message: String): Exception(message) {

constructor(error: String, response: HttpResponse, message: String? = null): this(error, message, """
$error${message?.let { " ($it)" } ?: ""}
Expand Down