Authenticate with Immersive by code: password, MFA and API keys
- Undefined type
Documentation
This guide explains how an application directly calls the RestFrontage authentication interfaces, gets a Immersive token and uses it with WebAPIs and OData. The examples do not require any Immersive DLLs.
Variants HTTP, C#, JavaScript, TypeScript, Python, and PowerShell cover .NET applications, web interfaces, backends, and exploit scripts. HTTP examples show the contract; other variants perform the same exchanges.
In this article
1. Choose the right mode
| Need | Fashion | Result |
|---|---|---|
| Log in a user when their password is sufficient according to server policy. | POST /api/Security/Users/Authenticate | An object containing Token, or a denial if MFA is required. |
| Sign in a user with MFA support and their first configuration. | POST /api/Security/Mfa/VerifyCredentials, then VerifyTotp if requested. | So be it Token immediately, a step CeremonyToken to be completed before obtaining Token. |
| Call an API from a service or automation. | Headings x-app-name and x-api-key. | The call is authenticated directly; the response remains that of the called API. |
| Have RestFrontage display login and MFA, without building these screens. | Browser/Authorize and Browser/Token, with PKCE. | A token after the journey in the browser; this mode is the subject of the article dedicated to the hosted connection interface. |
An API key represents the application with the rights of the account associated with it. It can be used without a password or TOTP. It does not automatically give administrator rights.
For an app used by a person, the hosted journey avoids implementing the password form, QR code, and MFA input. This guide covers direct calls when your app supports these interactions itself.
flowchart TD
A{"Type de connexion ?"}
A -->|Clé API| B["Envoyer x-app-name et x-api-key à chaque appel"]
B --> C["Appeler les WebAPI ou OData"]
A -->|Compte utilisateur| D["VerifyCredentials : vérifier l'identifiant et le mot de passe"]
D --> E{"Réponse du serveur"}
E -->|authenticated| F["Token disponible"]
E -->|totp_required| G["Saisir le code du téléphone"]
E -->|totp_enrollment_required| H["Configurer l'application TOTP avec le QR code"]
H --> G
G --> I["VerifyTotp : vérifier le code"]
I -->|Code accepté| F
I -->|Code incorrect, nouvel essai autorisé| G
F --> J["Envoyer Authorization: Bearer TOKEN à chaque appel"]
J --> C
2. Preparing for calls
2.1 Address, HTTPS and identity
Rely on the HTTPS origin of RestFrontage, for example https://localhost:44333 or https://rest.exemple.fr on your network. The certificate must be recognized by the customer.
The password and API key come from user input or your app's secret storage. They shouldn't be in published code, captures, logs, or URLs.
The client keeps the token in memory. It sends it only to the server that issued it. The functions below deny paths to another server and HTTP redirects.
2.2 Respect JSON names
| Field | Role |
|---|---|
| AccountName, Password | Credentials sent to the server. |
| Token | Final token to be used in Authorization: Bearer. |
| Status | Stage of the MFA journey. |
| CeremonyToken | Temporary reference of the current verification; it is not an access token. |
| ProvisioningUri, ManualKey | Confidential TOTP configuration information, present at registration. |
| Code, Message in an error | Technical identifier of the error and explanatory text. |
| Code in the VerifyTotp query | A six-digit TOTP code, transmitted as a string. |
Responses from these APIs use the PascalCase. Read Token, not Token, and CeremonyToken, not ceremonyToken. Some classic responses may contain metadata such as $type : The clients below ignore them.
The Hosted Connection Contract is an Exception: Browser/Token Returns access_token, token_type and expires_in. Do not mix the two formats.
2.3 The Origin Header for MFA
Roads Mfa/VerifyCredentials, Mfa/VerifyTotp and Mfa/Cancel require a JSON body and origin that is in Mfa:Origins.
| Customer | Value of Origin |
|---|---|
| .http, C#, Python, PowerShell, JavaScript file in Node.js | Send it explicitly. In a local test, https://localhost:44333 Suitable if this origin is allowed by the server. |
| JavaScript or TypeScript in a browser | The browser sends it automatically: it is the origin of the site that executes the code. The script cannot replace it with that of RestFrontage. |
For a web application hosted on https://application.exemple.fr who calls https://rest.exemple.fr, have https://application.exemple.fr in Mfa:Origins and in the CORS policy of RestFrontage. The two controls have separate roles. An origin has no path or end bar.
Origin is neither an API key nor proof of identity. In a non-browser client, this header can be filled in by the program; the server always verifies the password and MFA. See Origin in the browser and CORS.
For more information about MFA in Immersive, see the Multi-Factor Authentication documentation
2.4 Common Functions of Examples
Each variant prepares the HTTP client used in the following sections. SendAsync, send, send_request and Send-Immersive are utilities in this article, not functions provided by an Immersive DLL.
For C#, create a console app .NET 8 or later ; Snippets are used in the same top-level instruction file, with implicit usings enabled. For JavaScript, use a module in an HTTPS browser or Node.js 22 or later. The following JavaScript calls can also be used in TypeScript; the TypeScript tab gives a typed variant of the preparation.
Python Requires Python 3.10 or later and requests. The optional conversion of an API key to a token also uses Cryptography :
python -m pip install requests cryptography
PowerShell samples target PowerShell 7, including RSA-OAEP-SHA256. They don't target Windows PowerShell 5.1.
using System.Net.Http;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
var server = new Uri("https://localhost:44333");
var origin = server.GetLeftPart(UriPartial.Authority);
using var http = new HttpClient(new HttpClientHandler
{
AllowAutoRedirect = false,
UseCookies = false
}) { Timeout = TimeSpan.FromSeconds(30) };
async Task<JsonElement> SendAsync(
HttpMethod method, string path, object? body = null,
Dictionary<string, string>? headers = null)
{
if (server.Scheme != "https" || !string.IsNullOrEmpty(server.UserInfo)
|| !path.StartsWith('/') || path.StartsWith("//") || path.Contains('\\'))
throw new ArgumentException("Adresse HTTPS et chemin local attendus.");
var uri = new Uri(server, path);
if (uri.GetLeftPart(UriPartial.Authority) != origin)
throw new ArgumentException("Changement de serveur interdit.");
using var request = new HttpRequestMessage(method, uri);
request.Headers.Accept.ParseAdd("application/json");
if (headers != null)
foreach (var pair in headers) request.Headers.Add(pair.Key, pair.Value);
if (body != null)
request.Content = new StringContent(
JsonSerializer.Serialize(body), Encoding.UTF8, "application/json");
using var response = await http.SendAsync(request);
if (!response.IsSuccessStatusCode)
throw new HttpRequestException(
"Appel refusé : HTTP " + (int)response.StatusCode,
null, response.StatusCode);
using var json = JsonDocument.Parse(await response.Content.ReadAsStringAsync());
return json.RootElement.Clone();
}
string ReadToken(JsonElement response)
{
if (!response.TryGetProperty("Token", out var value)
|| value.ValueKind != JsonValueKind.String
|| string.IsNullOrWhiteSpace(value.GetString()))
throw new InvalidOperationException("Aucun token d'accès dans cette réponse.");
return value.GetString()!;
}
var mfaHeaders = new Dictionary<string, string> { ["Origin"] = origin };
const server = new URL("https://localhost:44333");
const origin = server.origin;
// Hors navigateur seulement ; dans le navigateur, Origin est automatique.
const mfaHeaders = typeof window === "undefined" ? { Origin: origin } : {};
async function send(method, path, body = undefined, headers = {}) {
if (server.protocol !== "https:" || server.username || server.password
|| !path.startsWith("/") || path.startsWith("//") || path.includes("\\"))
throw new Error("Adresse HTTPS et chemin local attendus.");
const url = new URL(path, server);
if (url.origin !== origin) throw new Error("Changement de serveur interdit.");
const response = await fetch(url, {
method, credentials: "omit", redirect: "error", cache: "no-store",
signal: AbortSignal.timeout(30000),
headers: {
Accept: "application/json",
...(body === undefined ? {} : { "Content-Type": "application/json" }),
...headers
},
...(body === undefined ? {} : { body: JSON.stringify(body) })
});
if (!response.ok) throw new Error("Appel refusé : HTTP " + response.status);
return await response.json();
}
function readToken(response) {
if (typeof response.Token !== "string" || !response.Token)
throw new Error("Aucun token d'accès dans cette réponse.");
return response.Token;
}
import requests
from urllib.parse import urljoin, urlsplit
server = "https://localhost:44333"
origin = server
mfa_headers = {"Origin": origin}
def send_request(method, path, body=None, headers=None):
base = urlsplit(server)
if (base.scheme != "https" or base.username or base.password
or not path.startswith("/") or path.startswith("//") or "\\" in path):
raise ValueError("Adresse HTTPS et chemin local attendus.")
url = urljoin(server, path)
if urlsplit(url).netloc != base.netloc:
raise ValueError("Changement de serveur interdit.")
response = requests.request(
method, url, json=body,
headers={"Accept": "application/json", **(headers or {})},
timeout=30, allow_redirects=False
)
if not 200 <= response.status_code < 300:
raise RuntimeError("Appel refusé : HTTP " + str(response.status_code))
return response.json()
def read_token(response):
token = response.get("Token")
if not isinstance(token, str) or not token:
raise RuntimeError("Aucun token d'accès dans cette réponse.")
return token
$server = [Uri]'https://localhost:44333'
$origin = $server.GetLeftPart([UriPartial]::Authority)
$mfaHeaders = @{ Origin = $origin }
function Send-Immersive {
param(
[string]$Method, [string]$Path,
[object]$Body = $null, [hashtable]$Headers = @{}
)
if ($server.Scheme -ne 'https' -or $server.UserInfo -or
-not $Path.StartsWith('/') -or $Path.StartsWith('//') -or $Path.Contains('\')) {
throw 'Adresse HTTPS et chemin local attendus.'
}
$uri = [Uri]::new($server, $Path)
if ($uri.GetLeftPart([UriPartial]::Authority) -ne $origin) {
throw 'Changement de serveur interdit.'
}
$allHeaders = @{ Accept = 'application/json' }
foreach ($key in $Headers.Keys) { $allHeaders[$key] = $Headers[$key] }
$arguments = @{
Uri = $uri; Method = $Method; Headers = $allHeaders
TimeoutSec = 30; MaximumRedirection = 0; ErrorAction = 'Stop'
}
if ($null -ne $Body) {
$arguments.ContentType = 'application/json; charset=utf-8'
$arguments.Body = ConvertTo-Json -InputObject $Body -Depth 8 -Compress
}
Invoke-RestMethod @arguments
}
function Read-ImmersiveToken {
param([object]$Response)
if ([string]::IsNullOrWhiteSpace($Response.Token)) {
throw "Aucun token d'accès dans cette réponse."
}
$Response.Token
}
const server = new URL("https://localhost:44333");
const origin = server.origin;
// Hors navigateur seulement ; dans le navigateur, Origin est automatique.
const mfaHeaders: Record<string, string> = typeof window === "undefined" ? { Origin: origin } : {};
async function send<T = any>(
method: string, path: string, body?: unknown,
headers: Record<string, string> = {}
): Promise<T> {
if (server.protocol !== "https:" || server.username || server.password
|| !path.startsWith("/") || path.startsWith("//") || path.includes("\\"))
throw new Error("Adresse HTTPS et chemin local attendus.");
const url = new URL(path, server);
if (url.origin !== origin) throw new Error("Changement de serveur interdit.");
const response = await fetch(url, {
method, credentials: "omit", redirect: "error", cache: "no-store",
signal: AbortSignal.timeout(30000),
headers: {
Accept: "application/json",
...(body === undefined ? {} : { "Content-Type": "application/json" }),
...headers
},
...(body === undefined ? {} : { body: JSON.stringify(body) })
});
if (!response.ok) throw new Error("Appel refusé : HTTP " + response.status);
return await response.json() as T;
}
function readToken(response: { Token?: unknown }): string {
if (typeof response.Token !== "string" || !response.Token)
throw new Error("Aucun token d'accès dans cette réponse.");
return response.Token;
}
type MfaReply =
| { Status: "authenticated"; Token: string }
| { Status: "totp_required"; CeremonyToken: string;
ProvisioningUri: null; ManualKey: null }
| { Status: "totp_enrollment_required"; CeremonyToken: string;
ProvisioningUri: string; ManualKey: string };
// Pour typer la réponse : await send<MfaReply>("POST", chemin, corps, mfaHeaders)
These utilities throw an error for any non-2xx response and do not display sensitive bodies. In your interface, process the HTTP status and the Message JSON errors to display appropriate text. Some rejections are text or have no body.
Python uses the certificate store recognized by requests. If your company provides internal authority, configure its certificate with REQUESTS_CA_BUNDLE. Do not disable TLS checking. See Documentation Requests.
2.5 Provide values specific to your application
The snippets for each mode complete the above preparation. They are not a single program to be fully concatenated: choose a connection mode and then the authenticated call.
| Value | C# / JavaScript / TypeScript | Python | PowerShell |
|---|---|---|---|
| Login entered | accountName | account_name | $accountName |
| Password entered | password | password | $password |
| TOTP code entered, as a string | Code | Code | $code |
| Application name provided with the API key | appName | app_name | $appName |
| API key provided by the administrator | apiKey | api_key | $apiKey |
These variables must be populated by your interface or secret store before running the snippet. The .http examples contain values to override. Keep these test files out of the repository if they contain actual secrets.
3. Classic login and password
3.1 Request and response
The appeal POST /api/Security/Users/Authenticate receives a JSON object containing AccountName and Password. This is not HTTP Basic authentication: no Basic header is required.
@server = https://localhost:44333
@username = compte.exemple
@password = REMPLACER_PAR_LE_MOT_DE_PASSE
### Mot de passe seul, si la politique l'autorise
# @name authenticate
POST {{server}}/api/Security/Users/Authenticate
Content-Type: application/json
Accept: application/json
{
"AccountName": "{{username}}",
"Password": "{{password}}"
}
var authentication = await SendAsync(
HttpMethod.Post, "/api/Security/Users/Authenticate",
new { AccountName = accountName, Password = password });
string token = ReadToken(authentication);
const authentication = await send(
"POST", "/api/Security/Users/Authenticate",
{ AccountName: accountName, Password: password }
);
const token = readToken(authentication);
authentication = send_request(
"POST", "/api/Security/Users/Authenticate",
{"AccountName": account_name, "Password": password}
)
token = read_token(authentication)
$body = @{ AccountName = $accountName; Password = $password }
$authentication = Send-Immersive -Method POST -Path '/api/Security/Users/Authenticate' -Body $body
$token = Read-ImmersiveToken $authentication
The successful response contains Token and user information in AuthenticatedUser. Here are only the fields that are useful for this example:
{
"Token": "JWT_IMMERSIVE",
"AuthenticatedUser": {
"AccountName": "compte.exemple"
}
}
Don't require a field Status on this classic answer. Also, don't assume that all of the user's properties are present in all environments.
In an .http file, special characters in the password must be escaped to form a valid JSON. In a program, let the JSON serializer encode the value; do not construct the body by concatenation.
3.2 When MFA is mandatory
With correct identifiers, this route returns HTTP 403 When MFA is required for the account:
{
"Code": "mfa_required",
"Message": "Utilisez un client compatible avec la MFA."
}
This doesn't mean that OData is unavailable: you must complete an MFA-enabled connection before calling it.
The password alone remains usable when MFA is disabled or when the account is effectively exempted in the mode with exemptions. The presence of a TOTP application already configured does not in itself make MFA mandatory: it is the policy of the server that decides.
A blank password follows the existing server setting. The client should not change this setting or consider an empty value as an alternative way of connecting.
4. Login with MFA TOTP
4.1 Choose Purpose
For a new customer, send login-with-totp-or-enroll. This course is suitable both to an already registered user and to a new user.
| Purpose | Usage |
|---|---|
| login-with-totp-or-enroll | Full login: immediate token if allowed, TOTP if needed, initial registration or re-registration allowed if needed. |
| login-with-totp | Sign in without a sign-up screen. If MFA is required, the account must already have verification set up. |
These values denote the capacity of the customer journey; they do not allow you to disable the MFA chosen by the administrator. login-with-totp can therefore return a token immediately if the policy does not require MFA for that account.
4.2 Verify credentials
@server = https://localhost:44333
@origin = https://localhost:44333
@username = compte.exemple
@password = REMPLACER_PAR_LE_MOT_DE_PASSE
### Étape 1 : lire Status avant de poursuivre
# @name verifyCredentials
POST {{server}}/api/Security/Mfa/VerifyCredentials
Content-Type: application/json
Accept: application/json
Origin: {{origin}}
{
"Credentials": {
"AccountName": "{{username}}",
"Password": "{{password}}"
},
"Purpose": "login-with-totp-or-enroll"
}
var step = await SendAsync(
HttpMethod.Post, "/api/Security/Mfa/VerifyCredentials",
new {
Credentials = new { AccountName = accountName, Password = password },
Purpose = "login-with-totp-or-enroll"
}, mfaHeaders);
string status = step.GetProperty("Status").GetString()!;
const step = await send(
"POST", "/api/Security/Mfa/VerifyCredentials",
{
Credentials: { AccountName: accountName, Password: password },
Purpose: "login-with-totp-or-enroll"
}, mfaHeaders
);
const status = step.Status;
step = send_request(
"POST", "/api/Security/Mfa/VerifyCredentials",
{
"Credentials": {"AccountName": account_name, "Password": password},
"Purpose": "login-with-totp-or-enroll"
}, mfa_headers
)
status = step["Status"]
$body = @{
Credentials = @{ AccountName = $accountName; Password = $password }
Purpose = 'login-with-totp-or-enroll'
}
$step = Send-Immersive -Method POST -Path '/api/Security/Mfa/VerifyCredentials' -Body $body -Headers $mfaHeaders
$status = $step.Status
For more information about MFA in Immersive, see the Multi-Factor Authentication documentation
4.3 Dealing with the three possible responses
Case 1 — Connection completed. If the server allows the connection without an additional step:
{
"Status": "authenticated",
"Token": "JWT_IMMERSIVE"
}
Use directly Token : ReadToken(step) in C#, readToken(step) in JavaScript/TypeScript, read_token(step) in Python, Read-ImmersiveToken $step in PowerShell. Don't call VerifyTotp : No CeremonyToken was issued.
Case 2 — TOTP requested. The account already has verification:
{
"Status": "totp_required",
"CeremonyToken": "REFERENCE_TEMPORAIRE",
"ProvisioningUri": null,
"ManualKey": null
}
Keep CeremonyToken in memory and display the entry of the phone code. No access tokens have yet been issued.
Case 3 — Registration requested. The account must set up its verification:
{
"Status": "totp_enrollment_required",
"CeremonyToken": "REFERENCE_TEMPORAIRE",
"ProvisioningUri": "otpauth://totp/Immersive:compte.exemple?secret=SECRET_FICTIF&issuer=Immersive&algorithm=SHA1&digits=6&period=30",
"ManualKey": "SECRET_FICTIF"
}
The URI and secret above are illustrative. In your interface:
- Generate locally the QR code representing exactly ProvisioningUri.
- Propose ManualKey as a manual input solution if scanning is not possible.
- The user adds the account in Microsoft Authenticator or another TOTP-enabled app.
- He enters the first six-digit code.
- Send this code with the same CeremonyToken to VerifyTotp.
Don't pass ProvisioningUri to a third-party QR code generation web service. This URI contains the secret to calculating future codes. Clear it, along with ManualKey and the temporary reference, after success or abandonment.
A TOTP is a one-time password calculated from a secret and the time. Immersive uses here Six figures, SHA-1 and a period of 30 seconds. The phone can calculate the code without the Internet; you always need to be able to reach RestFrontage to verify it. The switchboard is the RFC 6238.
sequenceDiagram
participant U as Utilisateur
participant A as Application cliente
participant R as RestFrontage
participant T as Application TOTP
U->>A: Identifiant et mot de passe
A->>R: VerifyCredentials
alt Connexion immédiatement autorisée
R-->>A: authenticated et Token
else Vérification supplémentaire
R-->>A: CeremonyToken et étape attendue
opt Inscription ou réenregistrement autorisé
A-->>U: QR code et clé manuelle
U->>T: Ajouter le compte
end
T-->>U: Code à six chiffres
U->>A: Saisir le code
A->>R: VerifyTotp avec CeremonyToken et Code
alt Code accepté
R-->>A: authenticated et Token
else Code refusé
R-->>A: HTTP 400 et Message
A-->>U: Corriger la saisie ou attendre le prochain code
end
end
4.4 Confirm the code and retrieve the token
This step runs only after totp_required or totp_enrollment_required. The variable Code is a string derived from user input, for example "012345". Don't convert it to a number: the initial zero is part of the code.
@totp = 012345
### Étape 2 : seulement si l'étape 1 fournit CeremonyToken
# @name authenticateMfa
POST {{server}}/api/Security/Mfa/VerifyTotp
Content-Type: application/json
Accept: application/json
Origin: {{origin}}
{
"CeremonyToken": "{{verifyCredentials.response.body.$.CeremonyToken}}",
"Code": "{{totp}}"
}
if (status is not ("totp_required" or "totp_enrollment_required"))
throw new InvalidOperationException("Aucune vérification TOTP en attente.");
if (code.Length != 6 || code.Any(c => c is < '0' or > '9'))
throw new ArgumentException("Saisissez six chiffres.");
var authentication = await SendAsync(
HttpMethod.Post, "/api/Security/Mfa/VerifyTotp",
new { CeremonyToken = step.GetProperty("CeremonyToken").GetString(), Code = code },
mfaHeaders);
string token = ReadToken(authentication);
if (!["totp_required", "totp_enrollment_required"].includes(status))
throw new Error("Aucune vérification TOTP en attente.");
if (!/^[0-9]{6}$/.test(code)) throw new Error("Saisissez six chiffres.");
const authentication = await send(
"POST", "/api/Security/Mfa/VerifyTotp",
{ CeremonyToken: step.CeremonyToken, Code: code }, mfaHeaders
);
const token = readToken(authentication);
if status not in ("totp_required", "totp_enrollment_required"):
raise ValueError("Aucune vérification TOTP en attente.")
if len(code) != 6 or any(c not in "0123456789" for c in code):
raise ValueError("Saisissez six chiffres.")
authentication = send_request(
"POST", "/api/Security/Mfa/VerifyTotp",
{"CeremonyToken": step["CeremonyToken"], "Code": code}, mfa_headers
)
token = read_token(authentication)
if ($status -notin @('totp_required', 'totp_enrollment_required')) {
throw 'Aucune vérification TOTP en attente.'
}
if ($code -cnotmatch '^[0-9]{6}$') { throw 'Saisissez six chiffres.' }
$body = @{ CeremonyToken = $step.CeremonyToken; Code = $code }
$authentication = Send-Immersive -Method POST -Path '/api/Security/Mfa/VerifyTotp' -Body $body -Headers $mfaHeaders
$token = Read-ImmersiveToken $authentication
For both Purpose connections, the successful response is:
{
"Status": "authenticated",
"Token": "JWT_IMMERSIVE"
}
This also applies after the confirmation of a registration initiated with login-with-totp-or-enroll : Successful registration completes the login and provides the token. There is no need to start over immediately with the password.
An HTTP 200 status is not sufficient to conclude that the user is logged in.
VerifyCredentials can respond HTTP 200 with a pending MFA step. The connection is completed only when Status is authenticated and a Token is present.
4.5 Expiry, retry and cancel
A check is valid Five minutes. Its reference can only be used once successfully. A code that has already been accepted cannot be reused for another verification.
After a declined code, display the error and allow a new entry with the same CeremonyToken as long as the verification remains valid. Do not automatically retry attempts. After Five incorrect codes, account verification is blocked for fifteen minutes ; Starting over with the password does not reset the timer.
If the reference has expired, the policy has changed, or the user has canceled, start again at VerifyCredentials. After the administrator resets a phone, login-with-totp-or-enroll presents the new registration as long as the re-registration authorization is valid. No additional recovery codes are required.
To explicitly abort a pending check:
POST {{server}}/api/Security/Mfa/Cancel
Content-Type: application/json
Origin: {{origin}}
{
"CeremonyToken": "{{verifyCredentials.response.body.$.CeremonyToken}}"
}
The answer is { "Status": "cancelled" }. Then clear the temporary data from your interface. Cancel is not a disconnection and does not revoke a final token that has already been issued.
4.6 Other Purpose are not connection modes
| Value | Purpose | Possible result after verification |
|---|---|---|
| enroll | Configure account verification in an authorized management path. | Status: registered ; No login token. |
| admin | Confirm a protected management operation. | Status: authorized and ProofToken ; this proof is not a Bearer. |
| manage | View the account verification method. | Status: credentials and Credentials ; No login token. |
| delete | Remove the means of verification when permitted by policy. | Status: deleted ; Declined if MFA is still required. |
| credentialsonly | The name used by the classic password journey. | Use Users/Authenticate for that journey; don't make it a Purpose of your MFA customer. |
To get a token with MFA, your integration uses the two connection Purposes described above. It doesn't need the Name, which is no longer part of the query.
5. API key authentication
5.1 Call APIs directly
Get a valid key and its Application Name. The key must be within its validity period, and its associated account must be authorized to log in. The application name is compared on a case-insensitive basis; the value of the key is case-sensitive.
An API key for an application should remain in a backend, service, or controlled environment. Don't embed it in JavaScript files downloaded by site visitors. The JavaScript examples in this section run server-side, in Node.js.
For more information, refer to our documentation on managing API keys in Rest Frontage
@server = https://localhost:44333
@appName = Supervision atelier
@apiKey = REMPLACER_PAR_LA_CLE_API
### Authentification directe par clé API
GET {{server}}/api/Security/Mfa/Context
Accept: application/json
x-app-name: {{appName}}
x-api-key: {{apiKey}}
var keyHeaders = new Dictionary<string, string>
{
["x-app-name"] = appName,
["x-api-key"] = apiKey
};
var account = await SendAsync(
HttpMethod.Get, "/api/Security/Mfa/Context", headers: keyHeaders);
string? name = account.GetProperty("AccountName").GetString();
// Node.js uniquement : la clé reste côté serveur.
const keyHeaders = { "x-app-name": appName, "x-api-key": apiKey };
const account = await send(
"GET", "/api/Security/Mfa/Context", undefined, keyHeaders
);
const name = account.AccountName;
key_headers = {"x-app-name": app_name, "x-api-key": api_key}
account = send_request(
"GET", "/api/Security/Mfa/Context", headers=key_headers
)
name = account["AccountName"]
$keyHeaders = @{ 'x-app-name' = $appName; 'x-api-key' = $apiKey }
$account = Send-Immersive -Method GET -Path '/api/Security/Mfa/Context' -Headers $keyHeaders
$name = $account.AccountName
The response in this example contains AccountName, not a token. The key directly authenticated the request. You can send the same two headers to your WebAPI or OData route.
The Path Mfa/Context This is only used here to verify the connected account. This GET does not ask for an MFA code.
Don't send an old one at the same time Authorization: Bearer or an authentication cookie: clearly use either the API key headers or the resulting Bearer. An API key is not transmitted by writing Permission: Bearer CLE_API.
6. Using the token with WebAPIs and OData
6.1 Authenticated call
Whether it's from the password, MFA, or API key, the final token is sent to Authorization: Bearer.
### Après Users/Authenticate
GET {{server}}/api/Security/Mfa/Context
Accept: application/json
Authorization: Bearer {{authenticate.response.body.$.Token}}
### Après VerifyTotp
GET {{server}}/api/Security/Mfa/Context
Accept: application/json
Authorization: Bearer {{authenticateMfa.response.body.$.Token}}
### Si VerifyCredentials a répondu authenticated directement
GET {{server}}/api/Security/Mfa/Context
Accept: application/json
Authorization: Bearer {{verifyCredentials.response.body.$.Token}}
### Après Token/Renew avec une clé API
GET {{server}}/api/Security/Mfa/Context
Accept: application/json
Authorization: Bearer {{authenticateApiKey.response.body.$.Token}}
var bearerHeaders = new Dictionary<string, string>
{
["Authorization"] = "Bearer " + token
};
var result = await SendAsync(
HttpMethod.Get, "/api/Security/Mfa/Context", headers: bearerHeaders);
string? accountNameFromToken = result.GetProperty("AccountName").GetString();
const result = await send(
"GET", "/api/Security/Mfa/Context", undefined,
{ Authorization: "Bearer " + token }
);
const accountNameFromToken = result.AccountName;
result = send_request(
"GET", "/api/Security/Mfa/Context",
headers={"Authorization": "Bearer " + token}
)
account_name_from_token = result["AccountName"]
$bearerHeaders = @{ Authorization = 'Bearer ' + $token }
$result = Send-Immersive -Method GET -Path '/api/Security/Mfa/Context' -Headers $bearerHeaders
$accountNameFromToken = $result.AccountName
For OData, keep the same header and replace the path with a collection published by your server. CollectionName is a location to be replaced, not a road provided by this article:
GET {{server}}/odata/NomDeCollection?$top=10
Accept: application/json
Authorization: Bearer {{authenticateMfa.response.body.$.Token}}
A valid token proves authentication. Account rights, organization filters, and business permissions remain applicable. MFA doesn't turn an ordinary user into an administrator.
6.2 Validate, expire and renew
GET /api/Security/Token/Validate, with the Bearer, allows you to check the session. Success is HTTP 202 Accepted, with an object that includes Subject and IsAdministrator. Don't just test HTTP 200.
GET {{server}}/api/Security/Token/Validate
Accept: application/json
Authorization: Bearer {{authenticateMfa.response.body.$.Token}}
The expiration date of a JWT is listed in its field exp. Reading it on the client side is used to arrange a reconnection; it never replaces the validation of the token by the server.
The classic and MFA routes in this guide don't return a refresh token. When a session is no longer accepted, restart the journey for the authentication mode.
The Historic Route Token/Renew can renew a session that is still valid, with the same encrypted body as before, but by sending the Common Bearer instead of API key headers. It doesn't allow you to recover an expired session or bypass an MFA requirement. The new response must replace the token that the client holds.
A change in MFA policy or a reset can render a session unusable before it expires. So always handle server rejections, even if exp still indicates a future date.
7. Use an API key with WebAPIs and OData
An API key directly authenticates each request to RestFrontage. Your application always passes the following two headers:
- x-app-name : The name of the application associated with the key.
- x-api-key : The value of the key.
No prior login or token retrieval is required. You don't call the password login or TOTP verification routes.
7.1 Make calls
The examples below perform two queries:
- A WebAPI call to /api/Security/Mfa/Context, to verify the name of the account associated with the key. Despite its name, this route does not require any MFA code.
- An OData call to /odata/EquipmentInstances?$top=10, to request up to ten pieces of equipment accessible to this account.
Programmed variants reuse the common functions presented at the beginning of this article. Fill in the name of the application and its key with the values provided by your administrator.
@server = https://localhost:44333
@appName = MonApplication
@apiKey = REMPLACER_PAR_LA_CLE_API
### WebAPI : vérifier le compte connecté
GET {{server}}/api/Security/Mfa/Context
Accept: application/json
x-app-name: {{appName}}
x-api-key: {{apiKey}}
### OData : lire jusqu'à dix équipements
GET {{server}}/odata/EquipmentInstances?$top=10
Accept: application/json
x-app-name: {{appName}}
x-api-key: {{apiKey}}
var keyHeaders = new Dictionary<string, string>
{
["x-app-name"] = appName,
["x-api-key"] = apiKey
};
// WebAPI
var account = await SendAsync(
HttpMethod.Get,
"/api/Security/Mfa/Context",
headers: keyHeaders);
// OData
var equipments = await SendAsync(
HttpMethod.Get,
"/odata/EquipmentInstances?$top=10",
headers: keyHeaders);
// Node.js uniquement : conserver la clé côté serveur.
// Cet extrait s'utilise également en TypeScript.
const keyHeaders = {
"x-app-name": appName,
"x-api-key": apiKey
};
// WebAPI
const account = await send(
"GET",
"/api/Security/Mfa/Context",
undefined,
keyHeaders
);
// OData
const equipments = await send(
"GET",
"/odata/EquipmentInstances?$top=10",
undefined,
keyHeaders
);
key_headers = {
"x-app-name": app_name,
"x-api-key": api_key
}
# WebAPI
account = send_request(
"GET",
"/api/Security/Mfa/Context",
headers=key_headers
)
# OData
equipments = send_request(
"GET",
"/odata/EquipmentInstances?$top=10",
headers=key_headers
)
$keyHeaders = @{
'x-app-name' = $appName
'x-api-key' = $apiKey
}
# WebAPI
$account = Send-Immersive -Method GET -Path '/api/Security/Mfa/Context' -Headers $keyHeaders
# OData
$equipments = Send-Immersive -Method GET -Path '/odata/EquipmentInstances?$top=10' -Headers $keyHeaders
Each response contains the result of the requested operation: account information for the first call, OData data for the second. There is no token to be extracted from these answers.
To call a different route, keep both headers and adapt the path, HTTP method, and body as per the operation. OData parameters, such as $filter, $select or $orderby, remain usable when the collection supports them.
7.2 Keep headers on every request
API key authentication is performed on each call. A successful first request does not exempt you from passing the headers on subsequent calls.
This also applies to paging requests and calls to other collections on the same server. Verify that a paging address belongs to your RestFrontage server before passing the key to it.
Don't add a header Authorization: Bearer to these calls and do not send a cookie corresponding to another user session.
7.3 The rights remain those of the associated account
The API key provides access to the operations that are allowed for its associated account. Organization restrictions and business permissions remain applicable.
A valid key does not guarantee access to all data or operations. For example, an account that is authorized to view equipment does not automatically have the right to modify it.
The key must also remain valid, and its associated account must be allowed to sign in. An expired, disabled, or deleted key can no longer authenticate calls.
Keep the key in a secret store that is appropriate for your application. Don't put it in a URL, log, or JavaScript files distributed to users of a website.
For a web application, calls using this key must be made by the backend.
8. Read errors without blocking the user
| Answer | Interpretation and action |
|---|---|
| 400 on Users/Authenticate | Invalid request; check the body and any blank password setting. |
| 401 on Users/Authenticate | Declined credentials or account unavailable. Presenting an error without automatically retrying attempts. |
| 403, Code: mfa_required on the classic login | Use the MFA journey or hosted login. |
| 403 on an MFA route, sometimes without a JSON body | Check Origin and Mfa:Origins. In the browser, also check CORS. |
| 415 on an MFA route | Send Content-Type: application/json. |
| 400, Code: mfa_verification_failed | Read Message: Incorrect code, already used, expired reference, lock, or configuration unavailable. |
| 400, Code: mfa_enrollment_unavailable | The re-registration authorization is no longer usable; ask the administrator to renew the action. |
| 401 with an API key | Invalid key, out of validity, or revoked derived token; also check x-app-name. |
| 403 with a valid API key | Its linked account can be absent, deactivated, expired, or locked. |
| 401 on a WebAPI or OData with Bearer | Token that is absent, expired, revoked, or has become incompatible with the policy; reconnect the client. |
| 403 on a business operation | The identity is recognized but the operation is not authorized; verify the rights. |
| 503 | A security check is temporarily unavailable; show the unavailability and schedule a new deferred trial. |
Not all rejections have the same format. Your client should check the HTTP status before attempting to read a JSON.
If there is a network failure during MFA confirmation, don't assume that the request was not executed: the server may have consumed the referral without the response reaching the client. Retry the connection if necessary and wait for the next code from the phone.
9. Historical tours and hosted connection
9.1 Security/Authenticate and SSO
POST /api/Security/Authenticate is an old entrance that receives a encrypted JSON string, not the plaintext AccountName/Password object shown here. The result of this is still subject to MFA policy. For a new direct password integration, use Users/Authenticate or MFA routes.
The SSO login is a separate journey, carried out with the identity provider configured at the customer's premises. Sending a login and password to Users/Authenticate does not turn the call into an SSO. A valid SSO token retains the rights of the account; the MFA of this journey is the responsibility of the configured provider.
9.2 Reuse server screens
If you don't want to expand on the interactions described in the TOTP section, use the path presented in the article Integrate Immersive Authentication and MFA into Your Web and .NET Applications.
Customer opens Browser/Authorize with a declared return address, state, and PKCE. It receives a temporary code and then redeems it via Browser/Token. It does not call VerifyCredentials or VerifyTotp itself. The WPF can display this connection in the external browser or in its WebView2 window.
Download WPF and Hosted Login Web Samples
This download is for the journey with the server interface. Examples of password, MFA, and API key direct calls can be found in the tabs in this article.
Integrate Immersive Authentication
10. Verify your integration
Check the cases that match your application:
- An account that is authorized to log in by password only.
- An account that is already registered and subject to MFA.
- An account that is subject to MFA that sets up its verification for the first time.
- A re-registration allowed after reset.
- An incorrect TOTP code, a code already used, a cancellation, and an expired reference.
- A valid API key, an expired key, and a disabled associated account.
- A successful call, a token refused, and an operation prohibited by rights.
The customer must distinguish between an ongoing MFA stage, A connection terminated and An authorized business operation. These are three different outcomes.