Skip to content

Commit

Permalink
Merge pull request #596 from supabase-community/auth-exceptions
Browse files Browse the repository at this point in the history
Handle `weak_password` and `session_not_found` auth error codes
  • Loading branch information
jan-tennert authored May 21, 2024
2 parents f57f9cc + f269c50 commit 0ce064c
Show file tree
Hide file tree
Showing 7 changed files with 98 additions and 12 deletions.
14 changes: 10 additions & 4 deletions GoTrue/src/commonMain/kotlin/io/github/jan/supabase/gotrue/Auth.kt
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

0 comments on commit 0ce064c

Please sign in to comment.