Authentication with a secret provider
Overview​
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
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.) thegetSecret
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 theAuthProcessApi
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.
- Kotlin
- Typescript
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)
}
}
}
}
import {
AuthenticationClass,
AuthenticationProcessApi, AuthenticationProcessCaptchaType,
AuthenticationProcessRequest, AuthenticationProcessTelecomType,
AuthSecretDetails,
ThirdPartyProvider
} from "@icure/cardinal-sdk";
const promptInvalidPasswordMessage = async () => {
// This is a mock method, you will have to implement this according to your logic
console.log("The provided password was wrong")
}
const loadRememberMe = async (): Promise<AuthSecretDetails.LongLivedTokenDetails> => {
// This is a mock method, you will have to implement this according to your logic
return new AuthSecretDetails.LongLivedTokenDetails("a_secret_token")
}
const askPreferredMethodToUser = async (acceptedSecrets: AuthenticationClass[]): Promise<AuthenticationClass> => {
// This is a mock method, you will have to implement this according to your logic
const randomIndex = Math.floor(Math.random() * acceptedSecrets.length)
return acceptedSecrets[randomIndex]
}
const authenticateUsingThirdParty = async (): Promise<AuthSecretDetails.ExternalAuthenticationDetails> => {
// This is a mock method, you will have to implement this according to your logic
return new AuthSecretDetails.ExternalAuthenticationDetails(
"token_received_from_oauth_flow",
ThirdPartyProvider.GOOGLE // Currently, only Google is supported
)
}
const askForUserPassword = async (): Promise<AuthSecretDetails.PasswordDetails> => {
// This is a mock method, you will have to implement this according to your logic
return new AuthSecretDetails.PasswordDetails("correct horse battery staple")
}
const askForTotp = async (): Promise<AuthSecretDetails.TwoFactorAuthTokenDetails> => {
// This is a mock method, you will have to implement this according to your logic
return new AuthSecretDetails.TwoFactorAuthTokenDetails("424242")
}
const askForShortTokenReceivedByMail = async (process: AuthenticationProcessRequest): Promise<AuthSecretDetails.ShortLivedTokenDetails> => {
// This is a mock method, you will have to implement this according to your logic
return new AuthSecretDetails.ShortLivedTokenDetails("424242", process)
}
const secretProvider = {
getSecret: async (
acceptedSecrets: Array<AuthenticationClass>,
previousAttempts: Array<AuthSecretDetails>,
authProcessApi: AuthenticationProcessApi
): Promise<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[previousAttempts.length-1] instanceof AuthSecretDetails.PasswordDetails) {
await 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.every(it => !(it instanceof AuthSecretDetails.LongLivedTokenDetails))) {
return loadRememberMe()
}
// Then, the user is asked for the preferred authentication method.
const choice = await askPreferredMethodToUser(acceptedSecrets)
if(choice === AuthenticationClass.DigitalId) {
throw new Error("Digital id is not supported")
} else if (choice === AuthenticationClass.ExternalAuthentication) {
return await authenticateUsingThirdParty()
} else if (choice === AuthenticationClass.Password) {
return await askForUserPassword()
} else if (choice === AuthenticationClass.TwoFactorAuthentication) {
return await askForTotp()
} else if (choice === AuthenticationClass.ShortLivedToken) {
// This implementation creates a short token for the user and sends it to them via email or SMS
const process = await authProcessApi.executeProcess(
"https://msg-gw.icure.cloud",
"/*You can find the spec id in the Cockpit*/",
"/*You can find the process id in the Cockpit*/",
AuthenticationProcessTelecomType.Email, // You can also use MobilePhone
"email.of.your.user@email.com",
AuthenticationProcessCaptchaType.FriendlyCaptcha, // You can also use Google ReCaptcha
"/*The captcha key returned upon captcha completion*/"
)
return await askForShortTokenReceivedByMail(process)
} else {
throw new Error(`Invalid option: ${choice}`)
}
}
}
Instantiate the SDK with the secret provider​
Once your secret provider is created, you can use it to instantiate the Cardinal SDK:
- Kotlin
- Typescript
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")
)
import {
AuthenticationMethod,
CardinalSdk,
SecretProviderAuthenticationOptions,
StorageFacade
} from "@icure/cardinal-sdk";
const sdk = await CardinalSdk.initialize(
undefined,
"https://api.icure.cloud",
new AuthenticationMethod.UsingSecretProvider(
secretProvider,
{
loginUsername: "", /* The username for the login */
initialSecret: new SecretProviderAuthenticationOptions.InitialSecret.Password("A password"),
cacheSecrets: true /* Whether to try and cache credentials when possible */,
}
),
StorageFacade.usingBrowserLocalStorage()
)
Along with the secret provider, you can also specify additional options to further customize the behaviour of your secret provider:
loginUsername
: thelogin
,email
, ormobilePhone
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
andExternalAuthentication
) 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:
- Kotlin
- Typescript
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")
)
import {
AuthenticationMethod,
CardinalSdk,
SecretProviderAuthenticationOptions,
StorageFacade
} from "@icure/cardinal-sdk";
const sdk = await CardinalSdk.initialize(
undefined,
"https://api.icure.cloud",
new AuthenticationMethod.UsingSecretProvider(
secretProvider,
{
loginUsername: /*The username*/,
initialSecret: new SecretProviderAuthenticationOptions.InitialSecret.LongLivedToken(/*Your existing long token*/),
cacheSecrets: true,
}
),
StorageFacade.usingBrowserLocalStorage()
)
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:
- Kotlin
- Typescript
val user = sdk.user.getCurrentUser()
val token = sdk.user.getToken(user.id, "test token", 600)
const user = await sdk.user.getCurrentUser()
const token = await 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.