Authenticating a user
This tutorial only applies to the Cloud version: you can't register new users in the free version of iCure.
When using your solution, your users will need to be authenticated to iCure in order to access their data. Therefore, you will need to integrate iCure's user authentication process into your product.
When starting your app, the users may be in different situations:
- They start it for the first time and need to register
- They already registered and need to login
- Their latest login session is still valid and you can reuse the corresponding authentication token
At the end of this guide, you will be able to implement authentication for those 3 use cases using the iCure MedTech SDK.
Pre-requisites
Make sure to have the following elements in your possession:
- The iCure reCAPTCHA v3 SiteKey
- Your
msgGtwSpecId
- Your
patientAuthProcessByEmailId
andpatientAuthProcessBySmsId
identifiers to authenticate your patient users - Your
hcpAuthProcessByEmailId
andhcpAuthProcessBySmsId
identifiers to authenticate your healthcare professionals users
Currently, you need to contact us at support@icure.com to get this information. However, you you will be able to retrieve it autonomously from the Cockpit in a future release.
Register a user
Let's say your patient Daenaerys uses your app for the first time. You will ask her to sign up.
During this procedure, Daenaerys is not known by iCure system yet. Therefore, you can't use the MedTechApi directly.
You will have to create an AnonymousMedTechApi
instead.
Init AnonymousMedTechApi
const iCureUrl = process.env.ICURE_URL
const msgGtwUrl = process.env.ICURE_MSG_GTW_URL
const specId = process.env.SPEC_ID
const authProcessByEmailId = process.env.AUTH_BY_EMAIL_PROCESS_ID
const authProcessBySmsId = process.env.AUTH_BY_SMS_PROCESS_ID
const recaptcha = process.env.RECAPTCHA
const anonymousApi = await new AnonymousMedTechApiBuilder()
.withICureBaseUrl(iCureUrl)
.withCrypto(webcrypto as any)
.withMsgGwUrl(msgGtwUrl)
.withMsgGwSpecId(specId)
.withAuthProcessByEmailId(authProcessByEmailId)
.withAuthProcessBySmsId(authProcessBySmsId)
.build()
The AnonymousMedTechApi asks you to provide multiple information. Here are their details :
Argument | Description |
---|---|
iCureUrlPath | The URL to contact the iCure API. By default, https://api.icure.cloud is used |
msgGtwUrl | The URL to contact the iCure Message Gateway API. By default, https://msg-gw.icure.cloud is used |
msgGtwSpecId | Your iCure Message Gateway Identifier. See next section to know more about it |
authProcessByEmailId | Identifier of the authentication by email process. See next section to know more about it |
authProcessBySmsId | Identifier of the authentication by SMS process. See next section to know more about it |
Since Daenaerys is a patient, you will have to provide the patientAuthProcessByEmailId
as a
authProcessByEmailId and patientAuthProcessBySmsId
as a authProcessBySmsId.
If Daenaerys was a doctor, you would instead provide the hcpAuthProcessByEmailId
as
authProcessByEmailId and hcpAuthProcessByEmailId
as authProcessBySmsId.
Starting the authentication process
The registration process of iCure uses a one-time password (OTP) sent by email or sms. Therefore, Daenaerys will need to provide at least an email or mobile phone number to register or login.
You will also have to implement the ReCAPTCHA mechanism and provide us the computed score during the startAuthentication process.
Check the official reCAPTCHA v3 documentation for more information. Also, do not forget to contact the iCure team to get our ReCAPTCHA SiteKey that you will need to implement the reCAPTCHA
const authProcess = await anonymousApi.authenticationApi.startAuthentication(
recaptcha,
userEmail, // Email address of the user who wants to register
undefined,
'Daenerys',
'Targaryen',
masterHcpId,
)
As an output, you receive an AuthenticationProcess
object, which you will need for next steps of the procedure.
The masterHcpId
represents the identifier of the dataOwner that will be responsible of Daenaerys user creation.
This masterHcpId
is optional for healthcare professionals registration but mandatory for patients.
It's good to know that after their registration, user will share all their future data with this responsible. The user may decide to stop
sharing their data with this responsible by using the userApi.stopSharingDataWith
service. For more information,
go to the How-to: Automatically share data with other data owners.
Getting the validation code (OTP)
The iCure Message Gateway will send the validation code to the user. Since Daenaerys decided
to authenticate by email, she can now check her emails to get this code.
In a future version of Cockpit, you will be able to edit the email and SMS templating for the authentication process. For now, these have all a default template.
Once Daenaerys retrieves her validation code, she can come back to your app and continue the process.
Completing the authentication process
To complete Daenaerys registration, you will have to call the authenticationApi.completeAuthentication
service,
by providing three arguments:
- The previous
AuthenticationProcess
- The validation code Daenaerys received by email
- A lambda providing the RSA Keypair Daenaerys should use. For this last point, you may use the dedicated
service
anonymousMedTechApi.generateRSAKeypair()
const authenticationResult = await anonymousApi.authenticationApi.completeAuthentication(
authProcess!,
validationCode,
() => anonymousApi.generateRSAKeypair(), // Generate an RSA Keypair for the user
)
const authenticatedApi = authenticationResult.medTechApi
console.log(`Your new user id: ${authenticationResult.userId}`)
console.log(`Database id where new user was created: ${authenticationResult.groupId}`)
console.log(`Your initialised MedTechAPI: ***\${authenticatedApi}***`)
console.log(`RSA keypair of your new user: ***\${authenticationResult.keyPair}***`)
console.log(`Token created to authenticate your new user: ***\${authenticationResult.token}***`)
As a result, you receive :
- The MedTechApi instance to use for Daenaerys (properly initialised);
- The
userId
, identifying Daenaerys user uniquely; - The
groupId
, identifying the database in which Daenaerys was created; - The
keyPair
, the RSA keypair you provided for Daenaerys through the lambda; - The
token
, the time-limited token created for Daenaerys, to authenticate her;
Make sure to save these elements to be able to authenticate Daenaerys again when she'll come back on your app.
// saveSecurely does not exist: Use your own way of storing the following data securely
// One option is to put these elements into the localStorage
saveSecurely(
userEmail,
authenticationResult.token,
authenticationResult.userId,
authenticationResult.groupId,
authenticationResult.keyPair,
)
Now that her authentication is completed, Daenaerys may manage data with iCure.
const createdDataSample = await authenticatedApi.dataSampleApi.createOrModifyDataSampleFor(
loggedUser.patientId,
new DataSample({
labels: new Set([new CodingReference({ type: 'IC-TEST', code: 'TEST' })]),
content: { en: { stringValue: 'Hello world' } },
openingDate: 20220929083400,
comment: 'This is a comment',
}),
)
Output
{
"id": "48e571a0-ac5f-47b3-8e25-16f5e78b50c9",
"identifiers": [],
"labels": {},
"codes": {},
"names": [
{
"firstNames": [
"John"
],
"prefix": [],
"suffix": [],
"lastName": "Snow",
"text": "Snow John",
"use": "official"
}
],
"languages": [],
"addresses": [],
"mergedIds": {},
"active": true,
"deactivationReason": "none",
"partnerships": [],
"patientHealthCareParties": [],
"patientProfessions": [],
"parameters": {},
"properties": {},
"rev": "1-8e3ad0d7e3179188dcd95f186f78b68d",
"created": 1664552695128,
"modified": 1664552695128,
"author": "3363719b-579e-4640-ac62-13e608e69395",
"responsible": "d6c8dbc7-eaa8-4c95-b9b3-920fb70ce59b",
"firstName": "John",
"lastName": "Snow",
"gender": "male",
"birthSex": "unknown",
"personalStatus": "unknown",
"note": "Winter is coming",
"systemMetaData": {
"hcPartyKeys": {},
"privateKeyShamirPartitions": {},
"secretForeignKeys": [],
"cryptedForeignKeys": {},
"delegations": {
"d6c8dbc7-eaa8-4c95-b9b3-920fb70ce59b": {}
},
"encryptionKeys": {
"d6c8dbc7-eaa8-4c95-b9b3-920fb70ce59b": {}
},
"aesExchangeKeys": {},
"transferKeys": {}
}
}
But what do you have to do when the authentication token of Daenaerys expires and she needs to login again?
Login a user
In iCure, the login flow is similar to the registration. Once Daenaerys's token is expired, she will need to authenticate again to iCure by starting the login process.
As Daenaerys is not authenticated anymore, you have to create a new AnonymousMedTechApi instance.
const anonymousApiForLogin = await new AnonymousMedTechApiBuilder()
.withICureBaseUrl(iCureUrl)
.withCrypto(webcrypto as any)
.withMsgGwUrl(msgGtwUrl)
.withMsgGwSpecId(specId)
.withAuthProcessByEmailId(authProcessByEmailId)
.withAuthProcessBySmsId(authProcessBySmsId)
.build()
const authProcessLogin = await anonymousApiForLogin.authenticationApi.startAuthentication(
recaptcha,
userEmail, // The email address used for user registration
)
Output
Daenaerys then receives a new validation code by email.
Since you already created an RSA keypair for her, you just need to retrieve it from where you stored it previously
and provide it to the completeAuthentication
method.
const loginResult = await anonymousApiForLogin.authenticationApi.completeAuthentication(
authProcessLogin!,
validationCodeForLogin,
() => {
const userInfo = getBackCredentials()
if (userInfo.pubKey != undefined && userInfo.privKey != undefined) {
return Promise.resolve({ privateKey: userInfo.privKey, publicKey: userInfo.pubKey })
} else {
// You can't find back the user's RSA Keypair: You need to generate a new one
return anonymousApiForLogin.generateRSAKeypair()
}
},
)
console.log(`Your new user id: ${loginResult.userId}`)
console.log(`Database id where new user was created: ${loginResult.groupId}`)
console.log(`Your new initialised MedTechAPI: ***\${loginResult.medTechApi}***`)
console.log(`RSA keypair of your user stays the same: ***\${loginResult.keyPair}***`)
console.log(`The token of your user will change: ***\${loginResult.token}***`)
Do not forget to save these new credentials :
// saveSecurely does not exist: Use your own way of storing the following data securely
// One option is to put these elements into the localStorage
saveSecurely(
userEmail,
authenticationResult.token,
authenticationResult.userId,
authenticationResult.groupId,
authenticationResult.keyPair,
)
If you are building a web app and store the private key only in your user's browser local storage, you should consider that if the user deletes their browser data, they will lose access to the data they created in iCure. After completing their registration, it might be a good idea to ask your user to store their private key in a safe place in their filesystem, possibly encrypting it with a password.
Make sure your users understand they should never share this file with anyone.
For more information check the In-Depth Explanation What happens if my user loses his private key ?
And Daenaerys may manage her data again :
const loggedUserApi = loginResult.medTechApi
const foundDataSampleAfterLogin = await loggedUserApi.dataSampleApi.getDataSample(createdDataSample.id)
Output
{
"id": "48e571a0-ac5f-47b3-8e25-16f5e78b50c9",
"identifiers": [],
"labels": {},
"codes": {},
"names": [
{
"firstNames": [
"John"
],
"prefix": [],
"suffix": [],
"lastName": "Snow",
"text": "Snow John",
"use": "official"
}
],
"languages": [],
"addresses": [],
"mergedIds": {},
"active": true,
"deactivationReason": "none",
"partnerships": [],
"patientHealthCareParties": [],
"patientProfessions": [],
"parameters": {},
"properties": {},
"rev": "1-8e3ad0d7e3179188dcd95f186f78b68d",
"created": 1664552695128,
"modified": 1664552695128,
"author": "3363719b-579e-4640-ac62-13e608e69395",
"responsible": "d6c8dbc7-eaa8-4c95-b9b3-920fb70ce59b",
"firstName": "John",
"lastName": "Snow",
"gender": "male",
"birthSex": "unknown",
"personalStatus": "unknown",
"note": "Winter is coming",
"systemMetaData": {
"hcPartyKeys": {},
"privateKeyShamirPartitions": {},
"secretForeignKeys": [],
"cryptedForeignKeys": {},
"delegations": {
"d6c8dbc7-eaa8-4c95-b9b3-920fb70ce59b": {}
},
"encryptionKeys": {
"d6c8dbc7-eaa8-4c95-b9b3-920fb70ce59b": {}
},
"aesExchangeKeys": {},
"transferKeys": {}
}
}
The last thing you need to know is what to do when Daenaerys credentials are still valid: if you saved the result from the login (or registration) process, the token may still be valid the next time Daenaerys accesses your application. In this case, you can reuse the existing token and avoid having Daenaerys go through the login process with email or SMS OTP every time she opens your application.
Reusing existing credentials
Each time you complete the registration or login process, you can save the credentials you receive
in a secured place.
We symbolised it through the saveSecurely
method.
// saveSecurely does not exist: Use your own way of storing the following data securely
// One option is to put these elements into the localStorage
saveSecurely(
userEmail,
authenticationResult.token,
authenticationResult.userId,
authenticationResult.groupId,
authenticationResult.keyPair,
)
The first thing you have to do is to retrieve Daenaerys credentials and her RSA Keypair
// getBackCredentials does not exist: Use your own way of storing the following data securely
// One option is to get them back from the localStorage
const { login, token, pubKey, privKey } = getBackCredentials()
And then, initialise a MedTechApi, authenticating Daenaerys directly.
const reInstantiatedApi = await new MedTechApiBuilder()
.withICureBaseUrl(iCureUrl)
.withUserName(login)
.withPassword(token)
.withCrypto(webcrypto as any)
.build()
await reInstantiatedApi.initUserCrypto(false, { publicKey: pubKey, privateKey: privKey })
Daenaerys can finally manage her data again.
const foundDataSampleAfterInstantiatingApi = await reInstantiatedApi.dataSampleApi.getDataSample(createdDataSample.id)
Output
{
"id": "48e571a0-ac5f-47b3-8e25-16f5e78b50c9",
"identifiers": [],
"labels": {},
"codes": {},
"names": [
{
"firstNames": [
"John"
],
"prefix": [],
"suffix": [],
"lastName": "Snow",
"text": "Snow John",
"use": "official"
}
],
"languages": [],
"addresses": [],
"mergedIds": {},
"active": true,
"deactivationReason": "none",
"partnerships": [],
"patientHealthCareParties": [],
"patientProfessions": [],
"parameters": {},
"properties": {},
"rev": "1-8e3ad0d7e3179188dcd95f186f78b68d",
"created": 1664552695128,
"modified": 1664552695128,
"author": "3363719b-579e-4640-ac62-13e608e69395",
"responsible": "d6c8dbc7-eaa8-4c95-b9b3-920fb70ce59b",
"firstName": "John",
"lastName": "Snow",
"gender": "male",
"birthSex": "unknown",
"personalStatus": "unknown",
"note": "Winter is coming",
"systemMetaData": {
"hcPartyKeys": {},
"privateKeyShamirPartitions": {},
"secretForeignKeys": [],
"cryptedForeignKeys": {},
"delegations": {
"d6c8dbc7-eaa8-4c95-b9b3-920fb70ce59b": {}
},
"encryptionKeys": {
"d6c8dbc7-eaa8-4c95-b9b3-920fb70ce59b": {}
},
"aesExchangeKeys": {},
"transferKeys": {}
}
}
What's next?
Some specific use cases can bring you some questions: what happens if Daenaerys lost her RSA Keypair? What happens if Daenaerys would like to start your app on another device?
All those questions are answered in the children pages of this tutorial.