Web Native
Web-only banking scenarios can't rely on mobile app SDK for cryptography and signing. Instead, you can use the web native solution to generate SCA proofs within a web browser.
Treezor built its web-native technology by relying on the Web Authentication standard (WebAuthn). It leverages public key cryptography for user authentication, achieving a security level comparable to native apps.
WebAuthn addresses the SCA elements as follows.
Element | Description |
---|---|
Passkeys | Represents "something the user has". |
Passcode | Represents "something the user knows". The passcode is unique by user for all their devices, and is encrypted on the user's device before transmission to Treezor. |
Why is the passcode necessary?
You might wonder why Treezor requires the passcode, when a smartphone might already mandate biometry to unlock the passkeys, satisfying both "something the user has" and "something the user is".
Although the conditions may be satisfied in this very situation, due to the multiplicity of authenticators supported by WebAuthn the passkeys may be accessed with or without biometry.
Therefore, to be SCA-compliant with every WebAuthn authenticator, Treezor requires the encrypted passcode in addition to the WebAuthn generated signature.
Note – Authentication type != None
only applies to the SDK.
When using WebAuthn, the authentication is always stronger than None
as we use "something the user has" (trusted browser/device) and require "something the user knows" (passcode).
Requirements
Web Authentication API usage requires hardware capabilities to handle cryptographic operations and keys. Unauthorized access protection can be ensured using either:
TEEs and TPMs are available on modern computers and smartphones. They can also be found in dedicated roaming hardware authenticators.
When first generating their passkey, the browser prompts the User to select a storage location. The passkey storage location depends on the device capabilities and how the WebAuthn API is called. They can be one of the following:
The device itself
Requires the device to possess necessary cryptographic capabilities (TEE, TPM, or other hardware-based security features).
Roaming hardware authenticators
Such as YubiKey, SoloKey, or Google Titan for instance.
Smartphones
Utilizing a QR code displayed during the WebAuthn enrollment process, a smartphone can scan the code to assist in the key pair generation and storage process.
This last method leverages the smartphone security features to extend WebAuthn reach beyond the first two options. The smartphone can either store the key pair or assist in the authentication process by verifying the user's identity before the key pair is generated, stored, or accessed securely. Both the computer and the smartphone must have Bluetooth enabled and be connected to the internet (although they do not necessarily need to be on the same network).
Reading – Matrix of device support
A comprehensive matrix detailing device support and key pair synchronization features is available at https://passkeys.dev/device-support/, offering developers insight into the wide array of compatible devices and their specific capabilities.
First device enrollment
The device enrollment process pairs the User with their WebAuthn secured passkeys and passcode. This must happen when first creating the User in the Treezor API as the User can't use the Treezor API without an enrolled device.
The following steps are required:
- Generation of the passkeys using WebAuthn on the User's device
- Creation of a passcode and its encryption on the User's device
- Forwarding of the public key and encrypted passcode to Treezor to finalize the device enrollment
The following snippet triggers the passkeys creation process using Web Authentication API on the User's device.
js
const publicKeyCredentialCreationOptions = {
// Mandatory, must be set to "device-enrollment"
challenge: convertToArrayBuffer('device-enrollment'),
user: {
// Mandatory, the User's login (this name will be displayed on WebAuthn key selection screen during the authentication process)
name: 'foobar',
// Optional, the User's full name (Firstname LASTNAME)
displayName: '',
// Mandatory, a random ID for each User's device, this is never displayed to the user and doesn't need to be stored anywhere
id: convertToArrayBuffer(randomId),
},
pubKeyCredParams: [{ alg: -7, type: "public-key" }], // Mandatory, as specified
authenticatorSelection: {
// Optional, can be set to "platform", "cross-platform" or omited to allow all authenticators types
authenticatorAttachment: '',
},
// Mandatory, timeout to complete the enrollment in milliseconds. We recommend keeping a fairly high value, as user might be unfamiliar with the process it could take them some time
600000,
// Mandatory, this value must be set to "direct"
"direct",
{
name: "Your website name",
// FQDN, Must be the same at the current domain where your trigger WebAuthn
id: 'www.example.com',
},
};
// trigger Web Authn to register the current device
const credential = await navigator.credentials.create({
publicKey: publicKeyCredentialCreationOptions,
});
const response = credential.response;
const rawId = arrayBufferToBase64Url(credential.rawId).toString();
const clientDataJSON = arrayBufferToBase64Url(
credential.response.clientDataJSON
).toString();
const attestationObject = arrayBufferToBase64Url(
(credential.response).attestationObject
).toString();
// this base 64 encoded string will be required at a later stage
let encodedResponse = btoa(
JSON.stringify({
response: {
attestationObject,
clientDataJSON,
},
id: credential.id,
rawId,
type: "public-key",
authenticatorAttachment:"platform" // platform or cross-platform
})
);
// helper function
function convertToArrayBuffer(input) {
return Uint8Array.from(input, (c) => c.charCodeAt(0));
}
// helper function
function arrayBufferToBase64Url(arrayBuffer) {
const bytes = new Uint8Array(arrayBuffer);
let str = "";
for (const charCode of bytes) {
str += String.fromCharCode(charCode);
}
const base64String = btoa(str);
return base64String.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
}
Note – Platform vs. Roaming authenticator (authenticatorAttachment
)
- Platform – Attached with client device-specific transport (
platform
attachment). Usually not removable from the client device. - Roaming – Attached with cross-platform transports (
cross-platform
attachment). Removable and can "roam" between client devices.
If the keys are created successfully, you can now ask the User to define a passcode and encrypt it.
js
const encryptedPassCode = encryptPasscode(passCode)
// see Passcode encryption section below for how to encrypt the passcode
At this point, you may use the User creation endpoint (/v1/users
) with:
- A
legal
scope, - The
passcode
andwebauthn
attributes to the usual payload.
bash
curl -X POST {baseUrl}/v1/users \
--header 'Authorization: Bearer {accessToken}' \
--header 'Content-Type: application/json' \
-d '{payload}'
Here is an example of {payload}
:
json
{
"userTypeId":1,
"specifiedUSPerson":0,
"firstname":"Alex",
"lastname":"Oak",
// [...] other User attributes hidden for brevity
"passcode": "2kdqUTkb3MF[...]My+VIBZB6Xm/tECw+RE9L02/K3x97cvwFXKDTIEZzzwAv1OYbS0w==",
"webauthn":"eyJyZXNwbrRG[...]9CejhlWWNxem9mUSIsInR5cGUiOiJwdWJsaWMta2V5In0="
}
Tip – Creating the first device of an existing User
To enroll a device of an existing User, you may use the POST /core-connect/sca/scawallets
request as described in the additional devices section.
Obtain JWT
Once the browser has undergone the enrollment, it is able to produce SCA signatures.
SCA enforcement requires that all request to the Treezor API be made in the name of the User. Therefore when obtaining a JWT, you must now provide an SCA proof.
The following code snippet shows how to generate an SCA proof that can then be used to obtain a JWT.
js
const publicKeyCredentialRequestOptions = {
challenge: convertToArrayBuffer(JSON.stringify({ iat: Date.now() })),
// for the login step, you must strictly provide the challenge above
allowCredentials: [],
timeout: 60000,
// time in miliseconds for the end user to complete the WebAuthn flow
rpId: 'www.example.com',
// the current FQDN, must be the same used during enrollment flow
};
const assertion = await navigator.credentials.get({
publicKey: publicKeyCredentialRequestOptions,
});
const response = assertion.response;
const rawId = arrayBufferToBase64Url(assertion.rawId).toString();
const clientDataJSON = arrayBufferToBase64Url(
assertion.response.clientDataJSON
).toString();
const authenticatorData = arrayBufferToBase64Url(
response.authenticatorData
).toString();
const signature = arrayBufferToBase64Url(response.signature).toString();
const userHandle = arrayBufferToBase64Url(response.userHandle).toString();
let encodedResponse = btoa(
JSON.stringify({
response: {
authenticatorData,
clientDataJSON,
signature,
userHandle,
},
id: assertion.id,
rawId,
type: "public-key",
})
);
Request the User to enter their passcode and encrypt it.
js
const encryptedPassCode = encryptPasscode(passCode)
// see Passcode encryption section below regarding how to encrypt the passcode
The two strings from the previous steps should now be concatenated with a dot as follows:
js
const sca = `${encryptedPassCode}.${encodedResponse}` // concatenate these values with a dot
You are now able to obtain a JWT by calling /oauth/token
with the following payload.
json
{
"grant_type": "delegated_end_user",
"client_id": "string",
"client_secret": "string",
"username": "string",
"password": "string",
"sca": "{scaProof}"
}
This JWT can be used for per-session operations.
Per-Operation signature
To sign an operation, you may use the same code snippet as for the JWT, only the challenge
differs.
We are expecting the following challenge
:
js
convertToArrayBuffer(JSON.stringify({
iat: Date.now(),
url: "string",
body: {}
}))
Attributes
url
andbody
vary by endpoint and are available in the dedicated article. Payload examples are also available.iat
is a timestamp is millisecond. It is mandatory when using WebAuthn to ensure uniqueness of the signature and prevent replay attacks.
Additional devices enrollement
By default, 5 devices may be enrolled with WebAuthn.
When a new device must be enrolled for an existing User, two options are available to you:
- Using an already enrolled device to ensure that the request is legitimate Preferred
- Validating the User's identity using questions that only they can answer (i.e., birthdate, email, OTP via email, etc.)
Both methods aim at ensuring that the User is who they claim to be before allowing the new device to be enrolled and generate SCA signatures.
Using an already enrolled device
If the User already has a device enrolled and accessible, you may use the cross-device method, requesting that the User approves the new device from their already enrolled device.
The passkeys generation process should be carried out on the non-enrolled device (without carrying out last User creation step).
Once the encodedResponse
from the non-enrolled device available, you can finalize the new device enrollment using the following request.
bash
curl -X POST {baseUrl}/core-connect/sca/scawallets \
--header 'Authorization: Bearer {accessToken}' \
--header 'Content-Type: application/json' \
-d '{payload}'
Here is an example of {payload}
.
json
{
"userId":"{userId}",
"webauthn":"{encryptedPublicKey}",
"sca":"{scaProofObtainedAtThePreviousStep}"
}
Using questions
This method can typically be used when the User:
- Has lost or cannot access their already enrolled device
- Was created in the Treezor API before the enforcement of SCA regulations
Security – Verify the User's identity
In this situation, you are sole responsible for verifying the User's identity.
In addition to authenticating the User using your usual authentication mechanism, you should ensure the User is able to provide personal information. Use as much relevant information as necessary, such as:
- Their birthdate, current password, email address, mobile phone number
- A one-time-password that you sent to their email address
- A one-time-password that you sent to their mobile phone
If enough conditions are met, you may contact Treezor endpoint and enroll the new device with the following request.
bash
curl -X POST {baseUrl}/core-connect/sca/scawallets \
--header 'Authorization: Bearer {accessToken}' \
--header 'Content-Type: application/json' \
-d '{payload}'
Treezor expects the following {payload}
:
json
{
"userId":"",
"authMethod":["OTP SMS", "OTP EMAIL", "ID", "OTHER"], // List methods used to ensure the user's identity
"passcode":"{theUsersCurrentPasscodeInEncryptedForm}",
"webauthn":"{newDevicesPublicKey}"
}
SCA Wallet structure
The SCA Wallet returned by /core-connect/sca/scawallets/{scaWalletId}
differs from the one returned by the Mobile SDK. You can identify whether the SCA Wallet is from WebAuthn or the Mobile SDK by having a look at the settingsProfile
value.
Below an example of a SCA Wallet object with a settingsProfile
attribute set to webauthn
.
json
{
"scaWallets": [
{
"id": "a[...]]9kjw_k", // Soon to be UUID V4
"status": "ACTIVE",
"subStatus": null, // Always null
"passcodeStatus": null, // "SET" if user has a defined passcode (same passcode for all webauthn wallets)
"locked": false, // false by default
"lockReasons": [], // empty array by default, see list of values
"lockMessage": null, // null or string
"settingsProfile": "webauthn", // "webauthn" (Web Native) or "default" (Mobile SDK)
"mobileWallet": null, // Always null for webauthn
"activationCode": null, // Always null for webauthn
"creationDate": "2024-03-18T10:44:49+01:00",
"deletionDate": null,
"activationCodeExpiryDate": null, // Always null for webauthn
"authenticationMethods": [ // array of objects, PublicKeyCredentialInterface is a webauthn-only parameter
{
"userHandle": "c3R2NUBnbWFpbC5jb20", // base64 encoded id of the user you've given during credentials.create for webauthn. No specific usage on Treezor side.
"publicKeyCredentialId": "alPXdC5x26rRlpCbBy-ic96q4XLRSmqWVZU209kjw_k", // Unique identifier of the webauthn public key
"aaguid": "adce0002-35bc-c60a-648b-0b25f1f05503",
"uvInitialized": true,
"attestationType": "self",
"backupEligible": false,
"counter": 0,
"otherUI": null,
"type": "public-key",
"transports": [],
"backupStatus": false,
"credentialPublicKey": "pQECAyYgASFYICt865X_cTGaGHTaI-braQi3QbqEcto3lwBQpwbfGC30IlggIedbBvSMh43ANnrDWlrdEriikhP9pgyfwvzYoa6ee9w",
"trustPath": {
"type": "Webauthn\\TrustPath\\EmptyTrustPath"
}
}
],
"invalidActivationAttempts": null, // Always null for webauthn
"userId": "100218468", // The user id for the webauthn scaWallet
"scaWalletTag": null,
"clientId": "http://example", // URL on which the webauthn public key can be used
"activationDate": null // Identical to creationDate
}
],
"cursor": null
}
Passcode encryption
Anytime Treezor requires the User's passcode to be provided, you must use Treezor public key to encrypt the passcode.
Security – Passcode privacy
The User's passcode must only be known to the User. It must never be stored on your infrastructure in any form.
The following code snippet includes all the necessary code for client-side encryption of the passcode.
js
(async () => {
const str2ab = (str) => {
const buf = new ArrayBuffer(str.length);
const bufView = new Uint8Array(buf);
for (let i = 0, strLen = str.length; i < strLen; i++) {
bufView[i] = str.charCodeAt(i);
}
return buf;
}
const pemEncodedKey = `-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA7rpmH1tnPWJ3iqHg/87S
GV1TA/3fgkz7Q36MqovyzjXTeOynMAh35FpxCaI0W4UQDQDfgBL92Z0/7Sm67ubn
uJZoVSvb8P0OCm1c7b2qGDnmhaxftR516chnrfYxUYJDwNmqFVlUziyCoFVnWgI1
bwTxQkqa2YEbh5LQOtNmr63KU0SrzzBEkKSprKEIAjuyiHIutwsD3Y1syv/s9NCC
5+sU1LPHvmn+IKnjPxKwjP8IrNPAJX8EWIOQD6i9WmUYMURzLU5cIYCtry58ytWd
2Xp8XoJrNmoMhvQ0wm49J5EFcBzO/LrVxV/c/BOfAvQqbJqJN+mLkD/k6ZE9YjMZ
8QIDAQAB
-----END PUBLIC KEY-----`;
const importRsaKey = async (pem) => {
// fetch the part of the PEM string between header and footer
const pemHeader = "-----BEGIN PUBLIC KEY-----";
const pemFooter = "-----END PUBLIC KEY-----";
const pemContents = pem.substring(
pemHeader.length,
pem.length - pemFooter.length - 1,
);
// base64 decode the string to get the binary data
const binaryDerString = window.atob(pemContents);
// convert from a binary string to an ArrayBuffer
const binaryDer = str2ab(binaryDerString);
return await window.crypto.subtle.importKey(
"spki",
binaryDer,
{
name: "RSA-OAEP",
hash: "SHA-256",
},
true,
["encrypt"],
);
}
let key = await importRsaKey(pemEncodedKey)
const encryptMessage = async (publicKey) => {
let message = 'nicolas'
let enc = new TextEncoder();
let encoded = enc.encode(message)
return window.crypto.subtle.encrypt(
{
name: "RSA-OAEP",
},
publicKey,
encoded,
);
}
let result = await encryptMessage(key)
_arrayBufferToBase64 = ( buffer ) => {
var binary = '';
var bytes = new Uint8Array( buffer );
var len = bytes.byteLength;
for (var i = 0; i < len; i++) {
binary += String.fromCharCode( bytes[ i ] );
}
return window.btoa( binary );
}
let enc = new TextDecoder();
// enc.encode(enc.decode(result))
console.log(_arrayBufferToBase64(result))
})()
Passcode change
This is considered a sensitive action, so users must be strongly authenticated before setting a new passcode. There are two use cases depending on whether the user knows their current passcode:
- With SCA proof – If the user knows their current passcode.
- With 2-factor authentication – If the user doesn't know their current passcode.
Tip – Passcode is shared between devices
Contrary to device-specific SDK passcodes, the passcode used with WebAuthn is not tied to the device passkeys and technicaly not used to access the passkeys. Therefore the passcode is not device-specific; if a User has multiple WebAuthn devices enrolled, a single and unique passcode is used for all of them.
With SCA proof
If your user alrady knows their passcodes, use the following request with a delegated_end_user
JWT.
bash
curl -X PUT {baseUrl}/core-connect/sca/setPasscode \
--header 'Authorization: Bearer {accessToken}' \
--header 'Content-Type: application/json' \
-d '{payload}'
Here is an example of {payload}
:
json
{
"userId": "123456",
"newPasscode": "",
"confirmPasscode": "",
"sca": "{scaProof}"
}
Answers with 204 Accepted
HTTP status code.
With 2-factor authentication
If your user don't knwo their passcodes, use the following request with a client_credentials
JWT while ensuring the user is who they claim to be with relevant information as necessary, such as:
- Their birthdate, email address, mobile phone number
- A one-time-password that you sent to their email address
- A one-time-password that you sent to their mobile phone
If enough conditions are met, you may contact Treezor endpoint for the user to set their new passcode with the following request.
bash
curl -X PUT {baseUrl}/core-connect/sca/setPasscode \
--header 'Authorization: Bearer {accessToken}' \
--header 'Content-Type: application/json' \
-d '{payload}'
Here is an example of {payload}
:
json
{
"userId": "123456",
"newPasscode": "",
"confirmPasscode": "",
"authMethod":["OTP SMS", "OTP EMAIL", "ID", "OTHER"], // List methods used to ensure the user's identity
}
Answers with 204 Accepted
HTTP status code.
Endpoints
Endpoint | Scope |
---|---|
/v1/users Create an SCA Wallet automatically when onboarding the user (app). | legal |
/core-connect/sca/scawallets Create an SCA Wallet manually (app or end-user if cross-device). | read_write |
/core-connect/sca/scawallets/{scaWalletId} Retrieve an SCA Wallet. | read_only |
/core-connect/sca/setPasscode Change the passcode | read_write |