Skip to main content

Authentication with a secret provider

Overview​

note

This feature is unavailable on the Cardinal Python SDK

When you authenticate a user using the Cardinal SDK, you can choose among several types of credentials:

  • Username + long token (i.e. an authentication token with a duration greater than 5 minutes, like the one used to implement a remember me functionality
  • Username + short token (i.e. an authentication token with a duration less than or equal to 5 minutes)
  • Username + password
  • Username + OTP from two-factor authentication
  • OAuth
info

To use the two-factor authentication to authorize a user, you need first to enable it as explained in this how-to.

The secret provider is a callback that you can define to ask your user their credentials among one of those types. The Cardinal SDK will then call it whenever it needs to get credentials from the user.

Implement the secret provider​

To implement a secret provider, you need to define a class that implements the AuthSecretProvider interface., that defines a single method: getSecret. This is the method that will be called by the SDK whenever new credentials are needed; it has 3 parameters that can be used in its implementation:

  • acceptedSecrets: the types of credentials that will be accepted by the provider for that specific call. On certain operations (e.g. create a short token, enabling or disabling the 2FA) only the safest credentials option can be used. For more details, check the sensitive operations section.
  • previousAttempts: if the credentials returned by the method are invalid (the token expired, the user typed the password wrong, etc.) the getSecret method will be called again and this parameter will contain all the previous credentials type that were not valid. The first element of this array correspond to the oldest attempts, while the last element corresponds to the latest failed attempt.
  • authProcessApi: an instance of the AuthProcessApi that can be used to create a short token for the user when needed.

Below you will find an example implementation of a secret provider.

import com.icure.cardinal.sdk.auth.AuthSecretDetails
import com.icure.cardinal.sdk.auth.AuthSecretProvider
import com.icure.cardinal.sdk.auth.AuthenticationProcessApi
import com.icure.cardinal.sdk.auth.AuthenticationProcessCaptchaType
import com.icure.cardinal.sdk.auth.AuthenticationProcessRequest
import com.icure.cardinal.sdk.auth.AuthenticationProcessTelecomType
import com.icure.cardinal.sdk.auth.ThirdPartyProvider
import com.icure.cardinal.sdk.model.embed.AuthenticationClass

fun promptInvalidPasswordMessage() {
// This is a mock method, you will have to implement this according to your logic
println("The provided password was wrong")
}

fun loadRememberMe(): AuthSecretDetails.LongLivedTokenDetails {
// This is a mock method, you will have to implement this according to your logic
return AuthSecretDetails.LongLivedTokenDetails("a_secret_token")
}

fun askPreferredMethodToUser(acceptedSecrets: Set<AuthenticationClass>): AuthenticationClass {
// This is a mock method, you will have to implement this according to your logic
return acceptedSecrets.random()
}

fun authenticateUsingThirdParty(): AuthSecretDetails.ExternalAuthenticationDetails {
// This is a mock method, you will have to implement this according to your logic
return AuthSecretDetails.ExternalAuthenticationDetails(
secret = "token_received_from_oauth_flow",
oauthType = ThirdPartyProvider.GOOGLE // Currently, only Google is supported
)
}

fun askForUserPassword(): AuthSecretDetails.PasswordDetails {
// This is a mock method, you will have to implement this according to your logic
return AuthSecretDetails.PasswordDetails("correct horse battery staple")
}

fun askForTotp(): AuthSecretDetails.TwoFactorAuthTokenDetails {
// This is a mock method, you will have to implement this according to your logic
return AuthSecretDetails.TwoFactorAuthTokenDetails("424242")
}

fun askForShortTokenReceivedByMail(process: AuthenticationProcessRequest): AuthSecretDetails.ShortLivedTokenDetails {
// This is a mock method, you will have to implement this according to your logic
return AuthSecretDetails.ShortLivedTokenDetails("424242", process)
}


val secretProvider = object : AuthSecretProvider {
override suspend fun getSecret(
acceptedSecrets: Set<AuthenticationClass>,
previousAttempts: List<AuthSecretDetails>,
authProcessApi: AuthenticationProcessApi
): AuthSecretDetails {
// First, it checks if the last failed attempt was made with a password. In this case, it will prompt a
// message saying that the password was wrong.
if(previousAttempts.last() is AuthSecretDetails.PasswordDetails) {
promptInvalidPasswordMessage()
}

// Then, if a remember-me token (long token) can be used and was not already tried, it loads that
if(AuthenticationClass.LongLivedToken in acceptedSecrets
&& previousAttempts.none { it.type == AuthenticationClass.LongLivedToken }) {
return loadRememberMe()
}

// Then, the user is asked for the preferred authentication method.
val choice = askPreferredMethodToUser(acceptedSecrets)

return when(choice) {
AuthenticationClass.DigitalId -> {
throw UnsupportedOperationException("Digital id is not supported")
}
AuthenticationClass.ExternalAuthentication -> {
authenticateUsingThirdParty()
}
AuthenticationClass.Password -> {
askForUserPassword()
}
AuthenticationClass.TwoFactorAuthentication -> {
askForTotp()
}
AuthenticationClass.LongLivedToken -> {
throw IllegalStateException("If a long token was available, it would have been used")
}
AuthenticationClass.ShortLivedToken -> {
// This implementation creates a short token for the user and sends it to them via email or SMS
val process = authProcessApi.executeProcess(
messageGatewayUrl = "https://msg-gw.icure.cloud",
externalServicesSpecId = "/*You can find it in the Cockpit*/",
processId = "/*You can find it in the Cockpit*/",
captchaType = AuthenticationProcessCaptchaType.FriendlyCaptcha, // You can also use Google ReCaptcha
captchaKey = "/*The captcha key returned upon captcha completion*/",
userTelecomType = AuthenticationProcessTelecomType.Email, // You can also use MobilePhone
userTelecom = "email.of.your.user@email.com"
)

askForShortTokenReceivedByMail(process)
}
}

}

}

Instantiate the SDK with the secret provider​

Once your secret provider is created, you can use it to instantiate the Cardinal SDK:

import com.icure.cardinal.sdk.CardinalSdk
import com.icure.cardinal.sdk.options.AuthenticationMethod
import com.icure.cardinal.sdk.storage.impl.FileStorageFacade

val sdk = CardinalSdk.initialize(
applicationId = null,
baseUrl = "https://api.icure.cloud",
authenticationMethod = AuthenticationMethod.UsingSecretProvider(
secretProvider = secretProvider,
loginUsername = ""/* The username for the login */,
initialSecret = AuthenticationMethod.UsingSecretProvider.InitialSecret.Password("A password"),
cacheSecrets = true /* Whether to try and cache credentials when possible */,
),
baseStorage = FileStorageFacade("./scratch/storage")
)

Along with the secret provider, you can also specify additional options to further customize the behaviour of your secret provider:

  • loginUsername: the login, email, or mobilePhone of your user to per form the login. If you don't provide this field, only the authentication methods that do not require a username (i.e. DigitalId and ExternalAuthentication) can be used.
  • initialSecret: an existing, valid secret for the user that can be used to login. You can use either the password, a long-lived token or the token from an OAuth authentication flow. If the initial secret is not passed, the SDK will immediately ask for credentials using the secret provider.
  • cacheSecret: if true, the SDK instance will cache the secrets provided by the user in the volatile memory, to try and minimize the secrets asked to the user.

Sensitive operations​

The different types of credentials that the secret provider can ask and use are not deemed equal in terms of security. In fact, they are ranked as follows (from the more secure to the least secure):

  • DigitalId
  • TwoFactorAuthentication
  • ShortLivedToken
  • ExternalAuthentication
  • Password
  • LongLivedToken

In the SDK, some operations are deemed safety critical and can only be executed if a user logged in with credentials considered safe enough, even if it has the appropriate permission to do so. In those cases, the SDK will use the secret provider to ask new safe credentials to the user.

The minimum required credentials for all the safe operations can be modifying the minimumAuthenticationClassForElevatedPrivileges property through the GroupApi. By default, this value is set to Password.

Example: creating a short-lived token​

As short-lived token are considered safe credentials, creating one requires an elevated security context.

Let's take into account a use case where the sdk was initialized with a secret provider and an initial long token:

import com.icure.cardinal.sdk.CardinalSdk
import com.icure.cardinal.sdk.options.AuthenticationMethod
import com.icure.cardinal.sdk.storage.impl.FileStorageFacade

val sdk = CardinalSdk.initialize(
applicationId = null,
baseUrl = "https://api.icure.cloud",
authenticationMethod = AuthenticationMethod.UsingSecretProvider(
secretProvider = secretProvider,
loginUsername = /*The username*/,
initialSecret = AuthenticationMethod.UsingSecretProvider.InitialSecret.LongLivedToken(/*Your existing long token*/),
cacheSecrets = true,
),
baseStorage = FileStorageFacade("./scratch/storage")
)

Since the loginUsername to long token combination is valid for most operations, the user will not be asked additional credentials for their initial operations.

However, if they try to create a short token:

val user = sdk.user.getCurrentUser()
val token = sdk.user.getToken(user.id, "test token", 600)

Then the User will be asked for more secure credentials through the secret provider. In this case, the acceptedSecret parameter of the getSecret function will only contain the credential type that are as safe or more safe as the threshold specified in the group (so Password or higher in the default case).

If the user provides valid credentials, it will be responsibility of the SDK to ensure that the original operation completes successfully without any additional step.

Sensitive operations list​

The following operations are considered safety critical and will require stronger credentials compared to the rest:

  • Create a short token
  • Enable or disable the two-factor authentication for a user.
  • Modify the password or the roles for a user.
  • Hard-delete a group.