Patient sign-in
A patient signs in to the tenant they belong to. The flow has up to three steps: start, an optional multi-factor step, and a token exchange.
| Step | Method | Path |
|---|---|---|
| 1 | POST | /auth/tenant/Patient/login/start |
| 2, only with MFA | POST | /auth/tenant/Patient/login/mfa |
| 3 | POST | /auth/tenant/Patient/login/token |
None of the three needs a bearer token.
Before you start
| You need | Where it comes from |
|---|---|
| The tenant code | Find your tenant code. |
| A patient account in that project | Patient registration or a patient invitation. |
| Patient sign-in switched on | PATIENT_LOGIN_ENABLED must not be false. It is on when unset. |
The account must be a Patient of that tenant's project. A practitioner with the same email cannot sign in here; use practitioner sign-in.
Step 0: Create a code verifier and challenge
Create these in your app for every sign-in attempt, and keep the verifier until step 3. The verifier is a random string of 43 to 128 URL-safe characters; the challenge is its SHA-256 hash, base64url-encoded without padding.
const base64url = (bytes) =>
btoa(String.fromCharCode(...new Uint8Array(bytes)))
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
const codeVerifier = base64url(crypto.getRandomValues(new Uint8Array(48)));
const codeChallenge = base64url(
await crypto.subtle.digest('SHA-256', new TextEncoder().encode(codeVerifier)),
);
The same in a shell:
VERIFIER=$(openssl rand -base64 48 | tr '+/' '-_' | tr -d '=\n')
CHALLENGE=$(printf %s "$VERIFIER" | openssl dgst -sha256 -binary | openssl base64 -A | tr '+/' '-_' | tr -d '=')
Step 1: Start sign-in
Checks the email and password for one tenant.
curl --request POST \
--url 'https://api.sandbox.ovok.com/auth/tenant/Patient/login/start' \
--header 'Content-Type: application/json' \
--data '{
"email": "patient@example.com",
"password": "your-password",
"tenantCode": "big-health-company",
"codeChallenge": "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM"
}'
| Body field | Description |
|---|---|
email | The patient's email. Ovok lowercases it. |
password | The patient's password. |
tenantCode | The tenant to sign in to. |
codeChallenge | The hash from step 0. |
The answer tells you what to do next:
| Response field | Example | Meaning |
|---|---|---|
nextStep | token or mfa | Tells the app which sign-in step to show next. |
sessionCode | Temporary code | Present when nextStep is token; send it with the verifier in step 3. |
loginId | UUID | Present when nextStep is mfa; send it with the one-time code in step 2. |
| Status | Message | Cause |
|---|---|---|
404 | Tenant not found. | The tenant code is wrong. Checked first. |
403 | Login is not enabled. | PATIENT_LOGIN_ENABLED is false. |
400 | Username or password is incorrect. | A wrong email, an unknown account, an account without a password, a wrong password, or an account that is not a patient of this tenant. All give the same answer on purpose. |
422 | A list of path and message | A body field is missing or invalid. |
429 | More than 10 requests in a minute. |
Step 2: Verify the one-time code (only with MFA)
curl --request POST \
--url 'https://api.sandbox.ovok.com/auth/tenant/Patient/login/mfa' \
--header 'Content-Type: application/json' \
--data '{"loginId":"8c1e5a52-6f0b-4d9e-9a41-0b2f7c3d9e10","mfaToken":"123456"}'
| Body field | Description |
|---|---|
loginId | The loginId from step 1. A UUID. |
mfaToken | The current one-time code from the patient's authenticator app, at least 6 characters. |
On success the answer is { "nextStep": "token", "sessionCode": "…" }. Use the sessionCode in step 3.
| Status | Message | Cause |
|---|---|---|
400 | Invalid MFA token. | The code is wrong or expired, or the loginId is unknown. |
422 | A list of path and message | loginId is not a UUID, or mfaToken is shorter than 6 characters. |
429 | More than 5 requests in a minute. Few guesses are allowed on purpose. |
Step 3: Exchange the session code for tokens
curl --request POST \
--url 'https://api.sandbox.ovok.com/auth/tenant/Patient/login/token' \
--header 'Content-Type: application/json' \
--data '{"sessionCode":"<sessionCode from step 1 or 2>","codeVerifier":"<your code verifier>"}'
This step takes no tenant code: the session code already belongs to the tenant you started in.
| Response field | Example | Meaning |
|---|---|---|
accessToken | Token string | Bearer token for API calls. |
refreshToken | Token string | Token used to renew the session. |
expiresIn | 3600 | Access-token lifetime in seconds. |
project.reference, project.display | Project/<id>, Big Health Company | Project the tokens are for. |
profile.reference, profile.display | Patient/<id>, Example Patient | Patient profile that signed in. |
| Status | Message | Cause |
|---|---|---|
400 | Code verification failed. | The sessionCode or the codeVerifier is wrong. |
400 | Invalid user profile. | The session belongs to something other than a patient. |
400 | Account not found. | The patient account could not be found. |
429 | More than 30 requests in a minute. |
If the patient had scheduled their account for deletion, signing in cancels the deletion.
End to end in a shell
This signs in a patient who has no multi-factor step. It needs jq.
VERIFIER=$(openssl rand -base64 48 | tr '+/' '-_' | tr -d '=\n')
CHALLENGE=$(printf %s "$VERIFIER" | openssl dgst -sha256 -binary | openssl base64 -A | tr '+/' '-_' | tr -d '=')
SESSION=$(curl --silent --request POST \
--url 'https://api.sandbox.ovok.com/auth/tenant/Patient/login/start' \
--header 'Content-Type: application/json' \
--data "{\"email\":\"patient@example.com\",\"password\":\"${PASSWORD}\",\"tenantCode\":\"big-health-company\",\"codeChallenge\":\"${CHALLENGE}\"}" \
| jq -r '.sessionCode')
curl --request POST \
--url 'https://api.sandbox.ovok.com/auth/tenant/Patient/login/token' \
--header 'Content-Type: application/json' \
--data "{\"sessionCode\":\"${SESSION}\",\"codeVerifier\":\"${VERIFIER}\"}"
Gotchas
- The setting is checked at step 1 only. A session code issued before you turned
PATIENT_LOGIN_ENABLEDoff can still be exchanged at step 3. - Turning the switch off does not end sessions. A patient who is already signed in keeps working until their token expires, up to an hour.
- Registration returns tokens too. A patient who registers is signed in even if sign-in is off. Use
PATIENT_REGISTRATION_ENABLEDto stop registration. - Projects created with
POST /v1/slim/project/childstart with patient sign-in off. Turn it on before you launch. - A failed read of the project looks like "off". If the project cannot be read for a moment, callers see the
403. - Keep the verifier private. Do not send it anywhere before step 3, and do not reuse it across sign-in attempts.
- Every error takes at least a second. Show a spinner, not an outage message.