S'authentifier par code auprès d'Immersive : mot de passe, MFA et clés API

Documentation

Ce guide explique comment une application appelle directement les interfaces d'authentification de RestFrontage, obtient un token Immersive et l'utilise avec les WebAPI et OData. Les exemples ne nécessitent aucune DLL Immersive.

Les variantes HTTP, C#, JavaScript, TypeScript, Python et PowerShell couvrent les applications .NET, les interfaces Web, les backends et les scripts d'exploitation. Les exemples HTTP montrent le contrat ; les autres variantes réalisent les mêmes échanges.

Dans cet article

    Afficher plus
    Réduire

    1. Choisir le mode adapté

    Besoin Mode Résultat
    Connecter un utilisateur lorsque son mot de passe suffit selon la politique du serveur. POST /api/Security/Users/Authenticate Un objet contenant Token, ou un refus si la MFA est requise.
    Connecter un utilisateur en prenant en charge la MFA et sa première configuration. POST /api/Security/Mfa/VerifyCredentials, puis VerifyTotp si demandé. Soit Token immédiatement, soit une étape CeremonyToken à terminer avant d'obtenir Token.
    Appeler une API depuis un service ou une automatisation. En-têtes x-app-name et x-api-key. L'appel est authentifié directement ; la réponse reste celle de l'API appelée.
    Faire afficher le login et la MFA par RestFrontage, sans construire ces écrans. Browser/Authorize et Browser/Token, avec PKCE. Un token après le parcours dans le navigateur ; ce mode fait l'objet de l'article dédié à l'interface de connexion hébergée.

    Une clé API représente l'application avec les droits du compte qui lui est associé. Elle s'utilise sans mot de passe ni TOTP. Elle ne donne pas automatiquement des droits administrateur.

    Pour une application utilisée par une personne, le parcours hébergé évite d'implémenter le formulaire de mot de passe, le QR code et la saisie MFA. Le présent guide couvre les appels directs lorsque votre application prend elle-même en charge ces interactions.

    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. Préparer les appels

    2.1 Adresse, HTTPS et identité

    Reposez sur l'origine HTTPS de RestFrontage, par exemple https://localhost:44333 ou https://rest.exemple.fr sur votre réseau. Le certificat doit être reconnu par le client.

    Le mot de passe et la clé API viennent d'une saisie utilisateur ou du stockage de secrets de votre application. Ils ne doivent pas figurer dans le code publié, les captures, les journaux ou les URL.

    Le client conserve le token en mémoire. Il l'envoie uniquement au serveur qui l'a délivré. Les fonctions ci-dessous refusent les chemins vers un autre serveur et les redirections HTTP.

    2.2 Respecter les noms JSON

    Champ Rôle
    AccountName, Password Identifiants envoyés au serveur.
    Token Token final à utiliser dans Authorization: Bearer.
    Status Étape du parcours MFA.
    CeremonyToken Référence temporaire de la vérification en cours ; ce n'est pas un token d'accès.
    ProvisioningUri, ManualKey Informations confidentielles de configuration TOTP, présentes à l'inscription.
    Code, Message dans une erreur Identifiant technique de l'erreur et texte explicatif.
    Code dans la requête VerifyTotp Code TOTP à six chiffres, transmis comme chaîne de caractères.

    Les réponses de ces API utilisent le PascalCase. Lisez Token, pas token, et CeremonyToken, pas ceremonyToken. Certaines réponses classiques peuvent contenir des métadonnées telles que $type : les clients ci-dessous les ignorent.

    Le contrat de la connexion hébergée constitue une exception : Browser/Token renvoie access_token, token_type et expires_in. Ne mélangez pas les deux formats.

    2.3 L'en-tête Origin pour la MFA

    Les routes Mfa/VerifyCredentials, Mfa/VerifyTotp et Mfa/Cancel exigent un corps JSON et une origine figurant dans Mfa:Origins.

    Client Valeur d'Origin
    Fichier .http, C#, Python, PowerShell, JavaScript dans Node.js L'envoyer explicitement. Dans un test local, https://localhost:44333 convient si cette origine est autorisée par le serveur.
    JavaScript ou TypeScript dans un navigateur Le navigateur l'envoie automatiquement : c'est l'origine du site qui exécute le code. Le script ne peut pas la remplacer par celle de RestFrontage.

    Pour une application Web hébergée sur https://application.exemple.fr qui appelle https://rest.exemple.fr, faites autoriser https://application.exemple.fr dans Mfa:Origins et dans la politique CORS de RestFrontage. Les deux contrôles ont des rôles distincts. Une origine ne comporte ni chemin ni barre finale.

    Origin n'est ni une clé API ni une preuve d'identité. Dans un client hors navigateur, cet en-tête peut être renseigné par le programme ; le serveur vérifie toujours le mot de passe et la MFA. Voir Origin dans le navigateur et CORS.

    Pour plus d'informations sur la MFA dans Immersive reportez vous à la documentation sur l'authentification multifacteur

    2.4 Fonctions communes aux exemples

    Chaque variante prépare le client HTTP utilisé dans les sections suivantes. SendAsync, send, send_request et Send-Immersive sont des utilitaires de cet article, pas des fonctions fournies par une DLL Immersive.

    Pour C#, créez une application console .NET 8 ou ultérieur ; les extraits s'utilisent dans le même fichier à instructions de premier niveau, avec les usings implicites activés. Pour JavaScript, utilisez un module dans un navigateur HTTPS ou Node.js 22 ou ultérieur. Les appels JavaScript suivants sont aussi utilisables en TypeScript ; l'onglet TypeScript donne une variante typée de la préparation.

    Python nécessite Python 3.10 ou ultérieur et requests. La conversion facultative d'une clé API en token utilise aussi cryptography :

    python -m pip install requests cryptography
    
    

    Les exemples PowerShell ciblent PowerShell 7, notamment pour RSA-OAEP-SHA256 ; ils ne ciblent pas Windows PowerShell 5.1.

    C#
    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 };
    
    JavaScript
    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;
    }
    
    Python
    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
    
    PowerShell
    $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
    }
    
    TypeScript
    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)
    

    Ces utilitaires lèvent une erreur pour toute réponse hors 2xx et n'affichent pas les corps sensibles. Dans votre interface, traitez le statut HTTP et le champ Message des erreurs JSON pour afficher un texte adapté. Certains refus sont du texte ou n'ont pas de corps.

    Python utilise le magasin de certificats reconnu par requests. Si votre entreprise fournit une autorité interne, configurez son certificat avec REQUESTS_CA_BUNDLE. Ne désactivez pas la vérification TLS. Voir la documentation requests.

    2.5 Fournir les valeurs propres à votre application

    Les extraits de chaque mode complètent la préparation ci-dessus. Ils ne constituent pas un programme unique à concaténer intégralement : choisissez un mode de connexion, puis l'appel authentifié.

    Valeur C# / JavaScript / TypeScript Python PowerShell
    Identifiant saisi accountName account_name $accountName
    Mot de passe saisi password password $password
    Code TOTP saisi, sous forme de chaîne code code $code
    Nom d'application fourni avec la clé API appName app_name $appName
    Clé API fournie par l'administrateur apiKey api_key $apiKey

    Ces variables doivent être renseignées par votre interface ou votre stockage de secrets avant d'exécuter l'extrait. Les exemples .http contiennent des valeurs à remplacer. Gardez ces fichiers de test hors du dépôt s'ils contiennent des secrets réels.

    3. Login et mot de passe classiques

    3.1 Requête et réponse

    L'appel POST /api/Security/Users/Authenticate reçoit un objet JSON contenant AccountName et Password. Il ne s'agit pas d'une authentification HTTP Basic : aucun en-tête Basic n'est nécessaire.

    HTTP
    @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}}"
    }
    
    C#
    var authentication = await SendAsync(
        HttpMethod.Post, "/api/Security/Users/Authenticate",
        new { AccountName = accountName, Password = password });
    string token = ReadToken(authentication);
    
    JavaScript
    const authentication = await send(
        "POST", "/api/Security/Users/Authenticate",
        { AccountName: accountName, Password: password }
    );
    const token = readToken(authentication);
    
    Python
    authentication = send_request(
        "POST", "/api/Security/Users/Authenticate",
        {"AccountName": account_name, "Password": password}
    )
    token = read_token(authentication)
    
    PowerShell
    $body = @{ AccountName = $accountName; Password = $password }
    $authentication = Send-Immersive -Method POST -Path '/api/Security/Users/Authenticate' -Body $body
    $token = Read-ImmersiveToken $authentication
    

    La réponse réussie contient Token et les informations de l'utilisateur dans AuthenticatedUser. Voici uniquement les champs utiles à cet exemple :

    {
      "Token": "JWT_IMMERSIVE",
      "AuthenticatedUser": {
        "AccountName": "compte.exemple"
      }
    }
    
    

    N'exigez pas de champ Status sur cette réponse classique. Ne supposez pas non plus que toutes les propriétés de l'utilisateur sont présentes dans tous les environnements.

    Dans un fichier .http, les caractères spéciaux du mot de passe doivent être échappés pour former un JSON valide. Dans un programme, laissez le sérialiseur JSON encoder la valeur ; ne construisez pas le corps par concaténation.

    3.2 Lorsque la MFA est obligatoire

    Avec des identifiants corrects, cette route renvoie HTTP 403 lorsque la MFA est requise pour le compte :

    {
      "Code": "mfa_required",
      "Message": "Utilisez un client compatible avec la MFA."
    }
    
    

    Cela ne signifie pas qu'OData est indisponible : il faut terminer une connexion compatible MFA avant de l'appeler.

    Le mot de passe seul reste utilisable lorsque la MFA est désactivée ou lorsque le compte est effectivement exempté dans le mode avec exemptions. La présence d'une application TOTP déjà configurée ne rend pas à elle seule la MFA obligatoire : c'est la politique du serveur qui décide.

    Un mot de passe vide suit le réglage existant du serveur. Le client ne doit pas modifier ce réglage ni considérer une valeur vide comme un autre mode de connexion.

    4. Login avec MFA TOTP

    4.1 Choisir le Purpose

    Pour un nouveau client, envoyez login-with-totp-or-enroll. Ce parcours convient aussi bien à un utilisateur déjà inscrit qu'à un nouvel utilisateur.

    Purpose Utilisation
    login-with-totp-or-enroll Connexion complète : token immédiat si autorisé, TOTP si nécessaire, inscription initiale ou réenregistrement autorisé si nécessaire.
    login-with-totp Connexion sans écran d'inscription. Si la MFA est requise, le compte doit déjà disposer d'une vérification configurée.

    Ces valeurs désignent la capacité du parcours client ; elles ne permettent pas de désactiver la MFA choisie par l'administrateur. login-with-totp peut donc renvoyer un token immédiatement si la politique n'impose pas de MFA pour ce compte.

    4.2 Vérifier les identifiants

    HTTP
    @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"
    }
    
    C#
    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()!;
    
    JavaScript
    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;
    
    Python
    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"]
    
    PowerShell
    $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
    

    Pour plus d'informations sur la MFA dans Immersive reportez vous à la documentation sur l'authentification multifacteur

    4.3 Traiter les trois réponses possibles

    Cas 1 — Connexion terminée. Si le serveur autorise la connexion sans étape supplémentaire :

    {
      "Status": "authenticated",
      "Token": "JWT_IMMERSIVE"
    }
    
    

    Utilisez directement Token : ReadToken(step) en C#, readToken(step) en JavaScript/TypeScript, read_token(step) en Python, Read-ImmersiveToken $step en PowerShell. N'appelez pas VerifyTotp : aucun CeremonyToken n'a été émis.

    Cas 2 — TOTP demandé. Le compte dispose déjà d'une vérification :

    {
      "Status": "totp_required",
      "CeremonyToken": "REFERENCE_TEMPORAIRE",
      "ProvisioningUri": null,
      "ManualKey": null
    }
    
    

    Conservez CeremonyToken en mémoire et affichez la saisie du code du téléphone. Aucun token d'accès n'a encore été délivré.

    Cas 3 — Inscription demandée. Le compte doit configurer sa vérification :

    {
      "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"
    }
    
    

    L'URI et le secret ci-dessus sont illustratifs. Dans votre interface :

    1. Générez localement le QR code représentant exactement ProvisioningUri.
    2. Proposez ManualKey comme solution de saisie manuelle si le scan est impossible.
    3. L'utilisateur ajoute le compte dans Microsoft Authenticator ou une autre application compatible TOTP.
    4. Il saisit le premier code à six chiffres.
    5. Envoyez ce code avec le même CeremonyToken à VerifyTotp.

    Ne transmettez pas ProvisioningUri à un service Web tiers de génération de QR code. Cette URI contient le secret permettant de calculer les futurs codes. Effacez-la, ainsi que ManualKey et la référence temporaire, après succès ou abandon.

    Un TOTP est un mot de passe à usage unique calculé à partir d'un secret et de l'heure. Immersive utilise ici six chiffres, SHA-1 et une période de 30 secondes. Le téléphone peut calculer le code sans Internet ; il faut toujours pouvoir joindre RestFrontage pour le vérifier. Le standard est la 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 Confirmer le code et récupérer le token

    Cette étape s'exécute uniquement après totp_required ou totp_enrollment_required. La variable code est une chaîne issue de la saisie utilisateur, par exemple "012345". Ne la convertissez pas en nombre : le zéro initial fait partie du code.

    HTTP
    @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}}"
    }
    
    C#
    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);
    
    JavaScript
    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);
    
    Python
    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)
    
    PowerShell
    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
    

    Pour les deux Purpose de connexion, la réponse réussie est :

    {
      "Status": "authenticated",
      "Token": "JWT_IMMERSIVE"
    }
    
    

    Cela vaut aussi après la confirmation d'une inscription initiée avec login-with-totp-or-enroll : l'inscription réussie termine la connexion et fournit le token. Il n'est pas nécessaire de recommencer immédiatement avec le mot de passe.

    4.5 Expiration, nouvel essai et annulation

    Une vérification est valable cinq minutes. Sa référence n'est utilisable qu'une fois avec succès. Un code déjà accepté ne peut pas être réutilisé pour une autre vérification.

    Après un code refusé, affichez l'erreur et permettez une nouvelle saisie avec le même CeremonyToken tant que la vérification reste valable. Ne relancez pas automatiquement des essais. Après cinq codes incorrects, la vérification du compte est bloquée pendant quinze minutes ; recommencer avec le mot de passe ne remet pas ce compteur à zéro.

    Si la référence a expiré, si la politique a changé ou si l'utilisateur a annulé, recommencez à VerifyCredentials. Après la réinitialisation d'un téléphone par l'administrateur, login-with-totp-or-enroll présente la nouvelle inscription tant que l'autorisation de réenregistrement est valide. Aucun code supplémentaire de récupération n'est à recopier.

    Pour abandonner explicitement une vérification en attente :

    POST {{server}}/api/Security/Mfa/Cancel
    Content-Type: application/json
    Origin: {{origin}}
    
    {
      "CeremonyToken": "{{verifyCredentials.response.body.$.CeremonyToken}}"
    }
    
    

    La réponse est { "Status": "cancelled" }. Effacez ensuite les données temporaires de votre interface. Cancel n'est pas une déconnexion et ne révoque pas un token final déjà délivré.

    4.6 Les autres Purpose ne sont pas des modes de connexion

    Valeur Finalité Résultat possible après vérification
    enroll Configurer la vérification du compte dans un parcours de gestion autorisé. Status: registered ; pas de token de connexion.
    admin Confirmer une opération de gestion protégée. Status: authorized et ProofToken ; cette preuve n'est pas un Bearer.
    manage Consulter le moyen de vérification du compte. Status: credentials et Credentials ; pas de token de connexion.
    delete Retirer le moyen de vérification lorsque la politique le permet. Status: deleted ; opération refusée si la MFA reste obligatoire.
    credentialsonly Nom utilisé par le parcours classique mot de passe. Utilisez Users/Authenticate pour ce parcours ; n'en faites pas un Purpose de votre client MFA.

    Pour obtenir un token avec MFA, votre intégration utilise les deux Purpose de connexion décrits plus haut. Elle n'a pas besoin du champ Name, qui ne fait plus partie de la requête.

    5. Authentification par clé API

    5.1 Appeler directement les API

    Obtenez auprès de l'administrateur une clé valide et son nom d'application. La clé doit être dans sa période de validité et son compte associé doit être autorisé à se connecter. Le nom d'application est comparé sans distinction de casse ; la valeur de la clé est sensible à la casse.

    Pour plus d'informations reportez vous à notre documentation sur la gestion des clés d’API dans le Rest Frontage

    HTTP
    @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}}
    
    C#
    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();
    
    JavaScript
    // 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;
    
    Python
    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"]
    
    PowerShell
    $keyHeaders = @{ 'x-app-name' = $appName; 'x-api-key' = $apiKey }
    $account = Send-Immersive -Method GET -Path '/api/Security/Mfa/Context' -Headers $keyHeaders
    $name = $account.AccountName
    

    La réponse de cet exemple contient AccountName, pas un token. La clé a authentifié directement la requête. Vous pouvez envoyer les mêmes deux en-têtes à votre route WebAPI ou OData.

    Le chemin Mfa/Context sert ici seulement à vérifier le compte connecté. Ce GET ne demande pas de code MFA.

    N'envoyez pas simultanément un ancien Authorization: Bearer ou un cookie d'authentification : utilisez clairement soit les en-têtes de clé API, soit le Bearer obtenu. Une clé API ne se transmet pas en écrivant Authorization: Bearer CLE_API.

    6. Utiliser le token avec les WebAPI et OData

    6.1 Appel authentifié

    Qu'il provienne du mot de passe, de la MFA ou de la clé API, le token final est envoyé dans Authorization: Bearer.

    HTTP
    ### 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}}
    
    C#
    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();
    
    JavaScript
    const result = await send(
        "GET", "/api/Security/Mfa/Context", undefined,
        { Authorization: "Bearer " + token }
    );
    const accountNameFromToken = result.AccountName;
    
    Python
    result = send_request(
        "GET", "/api/Security/Mfa/Context",
        headers={"Authorization": "Bearer " + token}
    )
    account_name_from_token = result["AccountName"]
    
    PowerShell
    $bearerHeaders = @{ Authorization = 'Bearer ' + $token }
    $result = Send-Immersive -Method GET -Path '/api/Security/Mfa/Context' -Headers $bearerHeaders
    $accountNameFromToken = $result.AccountName
    

    Pour OData, gardez le même en-tête et remplacez le chemin par une collection publiée par votre serveur. NomDeCollection est un emplacement à remplacer, pas une route fournie par cet article :

    GET {{server}}/odata/NomDeCollection?$top=10
    Accept: application/json
    Authorization: Bearer {{authenticateMfa.response.body.$.Token}}
    
    

    Un token valide prouve l'authentification. Les droits du compte, les filtres d'organisation et les autorisations métier restent applicables. La MFA ne transforme pas un utilisateur ordinaire en administrateur.

    6.2 Valider, expirer et renouveler

    GET /api/Security/Token/Validate, avec le Bearer, permet de vérifier la session. Le succès est HTTP 202 Accepted, avec un objet comprenant Subject et IsAdministrator. Ne testez pas uniquement HTTP 200.

    GET {{server}}/api/Security/Token/Validate
    Accept: application/json
    Authorization: Bearer {{authenticateMfa.response.body.$.Token}}
    
    

    La date d'expiration d'un JWT figure dans son champ exp. La lire côté client sert à organiser une reconnexion ; cela ne remplace jamais la validation du token par le serveur.

    Les routes classiques et MFA de ce guide ne renvoient pas de refresh token. Lorsqu'une session n'est plus acceptée, recommencez le parcours correspondant au mode d'authentification.

    La route historique Token/Renew peut renouveler une session encore valide, avec le même corps chiffré que précédemment, mais en envoyant le Bearer courant à la place des en-têtes de clé API. Elle ne permet pas de récupérer une session expirée ni de contourner une exigence MFA. La nouvelle réponse doit remplacer le token conservé par le client.

    Un changement de politique MFA ou une réinitialisation peut rendre une session inutilisable avant son expiration. Traitez donc toujours les refus du serveur, même si exp indique encore une date future.

    7. Utiliser une clé d'API avec les WebAPI et OData

    Une clé d'API authentifie directement chaque requête auprès de RestFrontage. Votre application transmet systématiquement les deux en-têtes suivants :

    • x-app-name : le nom de l'application associé à la clé.
    • x-api-key : la valeur de la clé.

    Aucune connexion préalable ni récupération de token n'est nécessaire. Vous n'appelez pas les routes de connexion par mot de passe ou de vérification TOTP.

    7.1 Effectuer les appels

    Les exemples ci-dessous effectuent deux requêtes :

    1. Un appel WebAPI à /api/Security/Mfa/Context, pour vérifier le nom du compte associé à la clé. Malgré son nom, cette route ne demande aucun code MFA.
    2. Un appel OData à /odata/EquipmentInstances?$top=10, pour demander jusqu'à dix équipements accessibles à ce compte.

    Les variantes programmées réutilisent les fonctions communes présentées au début de cet article. Renseignez le nom de l'application et sa clé avec les valeurs fournies par votre administrateur.

    HTTP
    @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}}
    
    C#
    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);
    
    JavaScript
    // 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
    );
    
    Python
    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
    )
    
    PowerShell
    $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
    

    Chaque réponse contient le résultat de l'opération demandée : les informations du compte pour le premier appel, les données OData pour le second. Il n'y a pas de token à extraire de ces réponses.

    Pour appeler une autre route, conservez les deux en-têtes et adaptez le chemin, la méthode HTTP et le corps selon l'opération. Les paramètres OData, comme $filter, $select ou $orderby, restent utilisables lorsque la collection les prend en charge.

    7.2 Conserver les en-têtes sur chaque requête

    L'authentification par clé API s'effectue à chaque appel. Une première requête réussie ne dispense pas de transmettre les en-têtes lors des suivantes.

    Cela s'applique également aux requêtes de pagination et aux appels vers d'autres collections du même serveur. Vérifiez qu'une adresse de pagination appartient bien à votre serveur RestFrontage avant d'y transmettre la clé.

    N'ajoutez pas d'en-tête Authorization: Bearer à ces appels et n'envoyez pas de cookie correspondant à une autre session utilisateur.

    7.3 Les droits restent ceux du compte associé

    La clé API donne accès aux opérations autorisées pour son compte associé. Les restrictions d'organisation et les autorisations métier restent applicables.

    Une clé valide ne garantit donc pas l'accès à toutes les données ni à toutes les opérations. Par exemple, un compte autorisé à consulter des équipements ne dispose pas automatiquement du droit de les modifier.

    La clé doit également rester valide, et son compte associé doit être autorisé à se connecter. Une clé expirée, désactivée ou supprimée ne permet plus d'authentifier les appels.

    8. Lire les erreurs sans bloquer l'utilisateur

    Réponse Interprétation et action
    400 sur Users/Authenticate Requête invalide ; vérifier le corps et le réglage éventuel de mot de passe vide.
    401 sur Users/Authenticate Identifiants refusés ou compte indisponible. Présenter une erreur sans relancer automatiquement des tentatives.
    403, Code: mfa_required sur le login classique Utiliser le parcours MFA ou le login hébergé.
    403 sur une route MFA, parfois sans corps JSON Vérifier Origin et Mfa:Origins. Dans le navigateur, vérifier également CORS.
    415 sur une route MFA Envoyer Content-Type: application/json.
    400, Code: mfa_verification_failed Lire Message : code incorrect, déjà utilisé, référence expirée, verrouillage ou configuration indisponible.
    400, Code: mfa_enrollment_unavailable L'autorisation de réenregistrement n'est plus utilisable ; demander à l'administrateur de renouveler l'action.
    401 avec une clé API Clé invalide, hors période de validité, ou token dérivé révoqué ; vérifier aussi x-app-name.
    403 avec une clé API valide Son compte associé peut être absent, désactivé, expiré ou verrouillé.
    401 sur une WebAPI ou OData avec Bearer Token absent, expiré, révoqué ou devenu incompatible avec la politique ; reconnecter le client.
    403 sur une opération métier L'identité est reconnue mais l'opération n'est pas autorisée ; vérifier les droits.
    503 Une vérification de sécurité est temporairement indisponible ; afficher l'indisponibilité et prévoir un nouvel essai différé.

    Tous les refus n'ont pas le même format. Votre client doit vérifier le statut HTTP avant de tenter de lire un JSON.

    En cas d'échec réseau pendant la confirmation MFA, ne supposez pas que la requête n'a pas été exécutée : le serveur peut avoir consommé la référence sans que la réponse soit parvenue au client. Recommencez la connexion si nécessaire et attendez le prochain code du téléphone.

    9. Parcours historiques et connexion hébergée

    9.1 Security/Authenticate et SSO

    POST /api/Security/Authenticate est une ancienne entrée qui reçoit une chaîne JSON chiffrée, pas l'objet AccountName/Password en clair présenté ici. Son résultat reste soumis à la politique MFA. Pour une nouvelle intégration directe par mot de passe, utilisez Users/Authenticate ou les routes MFA.

    La connexion SSO est un parcours distinct, réalisé avec le fournisseur d'identité configuré chez le client. Envoyer un login et un mot de passe à Users/Authenticate ne transforme pas l'appel en SSO. Un token SSO valide conserve les droits du compte ; la MFA de ce parcours relève du fournisseur configuré.

    9.2 Réutiliser les écrans du serveur

    Si vous ne souhaitez pas développer les interactions décrites dans la section TOTP, utilisez le parcours présenté dans l'article Intégrer l'authentification Immersive et la MFA dans vos applications Web et .NET.

    Le client ouvre Browser/Authorize avec une adresse de retour déclarée, state et PKCE. Il reçoit un code temporaire, puis l'échange via Browser/Token. Il n'appelle pas lui-même VerifyCredentials ou VerifyTotp. Le WPF peut afficher cette connexion dans le navigateur externe ou dans sa fenêtre WebView2.

    Télécharger les exemples WPF et Web de connexion hébergée

    Ce téléchargement concerne le parcours avec l'interface du serveur. Les exemples d'appels directs par mot de passe, MFA et clé API figurent dans les onglets du présent article.

    Intégrer l’authentification Immersive

    10. Vérifier votre intégration

    Vérifiez les cas qui correspondent à votre application :

    • Un compte autorisé à se connecter par mot de passe seul.
    • Un compte déjà inscrit et soumis à la MFA.
    • Un compte soumis à la MFA qui configure sa vérification pour la première fois.
    • Un réenregistrement autorisé après réinitialisation.
    • Un code TOTP incorrect, un code déjà utilisé, une annulation et une référence expirée.
    • Une clé API valide, une clé expirée et un compte associé désactivé.
    • Un appel réussi, un token refusé et une opération interdite par les droits.

    Le client doit distinguer une étape MFA en cours, une connexion terminée et une opération métier autorisée. Ce sont trois résultats différents.