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

[RKOTLIN-1091] Decouple authentication errors from server error messages #1762

Merged
merged 2 commits into from
May 23, 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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ This release will bump the Realm file format 24. Opening a file with an older fo
* Removed `LogConfiguration`. Log levels and custom loggers can be set with `RealmLog`. (Issue [#1691](https://github.com/realm/realm-kotlin/issues/1691) [JIRA](https://jira.mongodb.org/browse/RKOTLIN-1038))
* Removed deprecated `io.realm.kotlin.types.ObjectId`. Use `org.mongodb.kbson.BsonObjectId` or its type alias `org.mongodb.kbson.ObjectId` instead. (Issue [#1749](https://github.com/realm/realm-kotlin/issues/1749) [JIRA](https://jira.mongodb.org/browse/RKOTLIN-1082))
* Removed deprecated `RealmClass.isEmbedded`. Class embeddeness can be check with `RealmClassKind.EMBEDDED`. (Issue [#1753](https://github.com/realm/realm-kotlin/issues/1753) [JIRA](https://jira.mongodb.org/browse/RKOTLIN-1080))
* Some authentication related operations will no longer throw specialized `InvalidCredentialsException` and `CredentialsCannotBeLinkedException` but the more general `AuthException` and `ServiceException`. (Issue [#1763](https://github.com/realm/realm-kotlin/issues/1763)/[RKOTLIN-1091](https://jira.mongodb.org/browse/RKOTLIN-1091))
* [Sync] Removed deprecated methods `User.identity` and `User.provider`, user identities can be accessed with the already existing `User.identities`. (Issue [#1751](https://github.com/realm/realm-kotlin/issues/1751) [JIRA](https://jira.mongodb.org/browse/RKOTLIN-1083))
* [Sync] `App.allUsers` does no longer return a map, but only a list of users known locally. (Issue [#1751](https://github.com/realm/realm-kotlin/issues/1751) [JIRA](https://jira.mongodb.org/browse/RKOTLIN-1083))
* [Sync ]Removed deprecated `DiscardUnsyncedChangesStrategy.onError`. (Issue [#1755](https://github.com/realm/realm-kotlin/issues/1755) [JIRA](https://jira.mongodb.org/browse/RKOTLIN-1085))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import io.realm.kotlin.mongodb.annotations.ExperimentalEdgeServerApi
import io.realm.kotlin.mongodb.auth.EmailPasswordAuth
import io.realm.kotlin.mongodb.exceptions.AppException
import io.realm.kotlin.mongodb.exceptions.AuthException
import io.realm.kotlin.mongodb.exceptions.InvalidCredentialsException
import io.realm.kotlin.mongodb.internal.AppConfigurationImpl
import io.realm.kotlin.mongodb.internal.AppImpl
import io.realm.kotlin.mongodb.sync.Sync
Expand Down Expand Up @@ -115,10 +114,6 @@ public interface App {
*
* @param credentials the credentials representing the type of login.
* @return the logged in [User].
* @throws InvalidCredentialsException if the provided credentials were not correct. Note, only
* [AuthenticationProvider.EMAIL_PASSWORD], [AuthenticationProvider.API_KEY] and
* [AuthenticationProvider.JWT] can throw this exception. Other authentication providers throw
* an [AuthException] instead.
* @throws AuthException if a problem occurred when logging in. See the exception message for
* further details.
* @throws io.realm.kotlin.mongodb.exceptions.ServiceException for other failures that can happen when
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,8 @@ public class BadRequestException internal constructor(message: String) : Service
* This exception is considered the top-level or "catch-all" for problems related to user account
* actions. The exact reason for the error can be found in [Throwable.message].
*
* Generally, this exception does not need to be caught as more specific subtypes are available.
* These will be documented for the relevant API methods.
* For some error scenarios there are more specific and descriptive subtypes available.
* These are documented for the relevant API methods where they can be thrown.
*
* @see UserAlreadyConfirmedException
* @see UserNotFoundException
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -172,39 +172,14 @@ internal fun convertAppError(appError: AppError): Throwable {
// generic `ServiceException`'s.
when (appError.code) {
ErrorCode.RLM_ERR_INTERNAL_SERVER_ERROR -> {
if (msg.contains("linking an anonymous identity is not allowed") || // Trying to link an anonymous account to a named one.
msg.contains("linking a local-userpass identity is not allowed") // Trying to link two email logins with each other
) {
CredentialsCannotBeLinkedException(msg)
} else {
ServiceException(msg)
}
ServiceException(msg)
}
ErrorCode.RLM_ERR_INVALID_SESSION -> {
if (msg.contains("a user already exists with the specified provider")) {
CredentialsCannotBeLinkedException(msg)
} else {
ServiceException(msg)
}
ServiceException(msg)
}
ErrorCode.RLM_ERR_USER_DISABLED,
ErrorCode.RLM_ERR_AUTH_ERROR -> {
// Some auth providers return a generic AuthError when
// invalid credentials are presented. We make a best effort
// to map these to a more sensible `InvalidCredentialsExceptions`
if (msg.contains("invalid API key")) {
// API Key
// See https://github.com/10gen/baas/blob/master/authprovider/providers/apikey/provider.go
InvalidCredentialsException(msg)
} else if (msg.contains("invalid custom auth token:")) {
// Custom JWT
// See https://github.com/10gen/baas/blob/master/authprovider/providers/custom/provider.go
InvalidCredentialsException(msg)
} else {
// It does not look possible to reliably detect Facebook, Google and Apple
// invalid tokens: https://github.com/10gen/baas/blob/master/authprovider/providers/oauth2/oauth.go#L139
AuthException(msg)
}
AuthException(msg)
}
ErrorCode.RLM_ERR_USER_NOT_FOUND -> {
UserNotFoundException(msg)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,6 @@ import io.realm.kotlin.mongodb.Functions
import io.realm.kotlin.mongodb.User
import io.realm.kotlin.mongodb.UserIdentity
import io.realm.kotlin.mongodb.auth.ApiKeyAuth
import io.realm.kotlin.mongodb.exceptions.CredentialsCannotBeLinkedException
import io.realm.kotlin.mongodb.exceptions.ServiceException
import io.realm.kotlin.mongodb.mongo.MongoClient
import kotlinx.coroutines.channels.Channel
import org.mongodb.kbson.ExperimentalKBsonSerializerApi
Expand Down Expand Up @@ -153,29 +151,17 @@ public class UserImpl(
if (state != User.State.LOGGED_IN) {
throw IllegalStateException("User must be logged in, in order to link credentials to it.")
}
try {
Channel<Result<User>>(1).use { channel ->
RealmInterop.realm_app_link_credentials(
app.nativePointer,
nativePointer,
(credentials as CredentialsImpl).nativePointer,
channelResultCallback<RealmUserPointer, User>(channel) { userPointer ->
UserImpl(userPointer, app)
}
)
channel.receive().getOrThrow()
return this
}
} catch (ex: ServiceException) {
// Linking an account with itself throws a different error code than other linking errors:
// It is unclear if this error is shared between other error scenarios, so for now,
// we remap the exception type here instead of in the generic handler in
// `RealmSyncUtils.kt`.
if (ex.message?.contains("[Service][InvalidSession(2)] a user already exists with the specified provider.") == true) {
throw CredentialsCannotBeLinkedException(ex.message!!)
} else {
throw ex
}
Channel<Result<User>>(1).use { channel ->
RealmInterop.realm_app_link_credentials(
app.nativePointer,
nativePointer,
(credentials as CredentialsImpl).nativePointer,
channelResultCallback<RealmUserPointer, User>(channel) { userPointer ->
UserImpl(userPointer, app)
}
)
channel.receive().getOrThrow()
return this
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import io.realm.kotlin.mongodb.LoggedOut
import io.realm.kotlin.mongodb.Removed
import io.realm.kotlin.mongodb.User
import io.realm.kotlin.mongodb.annotations.ExperimentalEdgeServerApi
import io.realm.kotlin.mongodb.exceptions.InvalidCredentialsException
import io.realm.kotlin.mongodb.exceptions.AuthException
import io.realm.kotlin.mongodb.exceptions.ServiceException
import io.realm.kotlin.mongodb.sync.SyncConfiguration
import io.realm.kotlin.test.mongodb.SyncServerConfig
Expand Down Expand Up @@ -144,7 +144,7 @@ class AppTests {
null
}
}?.let { credentials: Credentials ->
assertFailsWith<InvalidCredentialsException> {
assertFailsWith<AuthException> {
app.login(credentials)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -369,7 +369,7 @@ class CredentialsTests {
payload = mapOf("mail" to TestHelper.randomEmail(), "id" to 0)
)

assertFailsWithMessage<AuthException>("Error: Authentication failed.") {
assertFailsWithMessage<AuthException>("unauthorized") {
runBlocking {
app.login(credentials)
}
Expand All @@ -383,7 +383,7 @@ class CredentialsTests {
}
fail()
} catch (error: AppException) {
assertTrue(error.message!!.contains("authentication via"), error.message)
assertTrue(error.message!!.contains("unauthorized"), error.message)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -459,11 +459,11 @@ class UserTests {
app.emailPasswordAuth.registerUser(otherEmail, otherPassword)
val credentials = Credentials.emailPassword(otherEmail, otherPassword)

assertFailsWith<CredentialsCannotBeLinkedException> {
assertFailsWith<ServiceException> {
anonUser.linkCredentials(credentials)
}.let {
assertTrue(
it.message!!.contains("linking a local-userpass identity is not allowed when one is already linked"),
it.message!!.contains("unauthorized"),
it.message
)
}
Expand All @@ -478,11 +478,11 @@ class UserTests {
val (email2, password2) = randomEmail() to "123456"
app.emailPasswordAuth.registerUser(email2, password2)
val credentials2 = Credentials.emailPassword(email2, password2)
assertFailsWith<CredentialsCannotBeLinkedException> {
assertFailsWith<ServiceException> {
emailUser1.linkCredentials(credentials2)
}.let {
assertTrue(
it.message!!.contains("linking a local-userpass identity is not allowed when one is already linked"),
it.message!!.contains("unauthorized"),
it.message
)
}
Expand Down Expand Up @@ -512,11 +512,11 @@ class UserTests {
app.emailPasswordAuth.registerUser(email, password)
val creds = Credentials.emailPassword(email, password)
app.login(creds)
assertFailsWith<CredentialsCannotBeLinkedException> {
assertFailsWith<ServiceException> {
anonUser.linkCredentials(creds)
}.let {
assertTrue(
it.message!!.contains("a user already exists with the specified provider"),
it.message!!.contains("unauthorized"),
it.message
)
}
Expand Down
Loading