SHR Server Nonce

Sebagai penyempurnaan dari Access Token Proof-of-Possession, MSAL Browser menyediakan cara untuk menyisipkan stempel waktu bertanda tangan yang dihasilkan server (alias server nonce) ke dalam Signed HTTP Request, yang juga dikenal sebagai PoP Token. Nonce yang dihasilkan server ini dapat ditambahkan ke permintaan token apa pun yang menggunakan skema autentikasi POP.

Mengingat bahwa MSAL tidak menyimpan cacheSigned HTTP Requests, nonce server juga tidak akan di-cache. Ini berarti bahwa nonce server harus diteruskan ke setiap panggilan acquireTokenSilent agar ditambahkan ke objek SignedHttpRequest yang dihasilkan.

Signed HTTP Request Setelah dikirim ke server sumber daya sebagai PoP Token, server sumber daya bertanggung jawab untuk memvalidasi payload yang ditandatangani serta mengekstrak dan memvalidasi shrNonce.

Penggunaan

Setelah diperoleh, nonce yang dihasilkan server untuk SHR dapat diteruskan ke objek permintaan token apa pun sebagai shrNonce. Penting untuk dicatat bahwa nonce server SHR adalah ekstensi dari skema Bukti Kepemilikan Token Akses, sehingga hanya akan digunakan ketika permintaan authenticationScheme token diatur ke POP.

Contoh Permintaan untuk Mendapatkan Token

const popTokenRequest = {
    scopes: ["User.Read"],
    authenticationScheme: msal.AuthenticationScheme.POP,
    resourceRequestMethod: "POST",
    resourceRequestUri: "YOUR_RESOURCE_ENDPOINT",
    shrNonce: "eyJhbGciOiJIUzI1NiIsImtpZCI6IktJRCIsInR5cCI6IkpXVCJ9.eyJ0cyI6IjE2MjU2NzI1MjkifQ.rA5ho63Lbdwo8eqZ_gUtQxY3HaseL0InIVwdgf7L_fc" // Sample Base64URL encoded server nonce value
}

Setelah permintaan dikonfigurasi dan POP ditetapkan sebagai authenticationScheme, permintaan dapat diteruskan ke api loginXXXX atau acquireTokenXXX:

const response = await myMSALObj.acquireTokenSilent(popTokenRequest);

Respons akan berisi properti yang disebut accessToken, yang akan berisi Signed HTTP Request. Saat diverifikasi menggunakan kunci publik, payload JWT SHR akan terlihat seperti berikut:

{
    at: ...,
    ts: ...,
    m: "POST",
    u: "YOUR_RESOURCE_ENDPOINT",
    nonce: "eyJhbGciOiJIUzI1NiIsImtpZCI6IktJRCIsInR5cCI6IkpXVCJ9.eyJ0cyI6IjE2MjU2NzI1MjkifQ.rA5ho63Lbdwo8eqZ_gUtQxY3HaseL0InIVwdgf7L_fc",
    p: ...,
    q: ...,
    client_claims: "{\"nonce\": \"AQAA123456\",\"local_nonce\": \"AQAA7890\"}"
}

Atribut Nonce DitandatanganiHttpRequest

Nilai shrNonce yang dapat dikonfigurasi dalam permintaan token PoP akan ditetapkan ke atribut nonce dalam SignedHttpRequest yang dikembalikan dalam AuthenticationResult. Namun, atribut nonce di SHR tidak akan kosong jika tidak dikonfigurasi secara manual melalui nilai shrNonce. Daftar berikut menjelaskan logika (berdasarkan urutan prioritas) yang digunakan untuk menetapkan nilai nonce pada SignedHttpRequest:

  1. Jika shrNonce dalam permintaan autentikasi bukan null atau undefined, MSAL menetapkan nilai tersebut ke nonce properti di SHR
  2. Jika shrNonce adalah null atau undefined, MSAL menghasilkan string GUID acak dan menetapkannya ke nonce properti di SHR

Mendapatkan nilai nonce server

Metode di mana aplikasi klien awalnya memperoleh dan kemudian memperbarui nonce yang dihasilkan server mungkin berbeda tergantung pada server sumber daya mana yang berinteraksi dengan aplikasi klien. Berikut adalah gambaran umum alur perolehan dan pembaruan nonce server yang dioptimalkan untuk MSAL Browser. Perlu diingat bahwa bahkan jika alur akuisisi nonce berbeda, nonce server masih selalu ditambahkan secara manual ke dalam permintaan token oleh aplikasi klien seperti yang dijelaskan di bagian Penggunaan di bawah ini.

Memperoleh nonce awal

  1. Langkah pertama untuk memperoleh nonce yang dihasilkan oleh server adalah membuat permintaan yang diautorisasi ke sumber daya. Dalam permintaan yang diotorisasi ini, PoP Token akan ditambahkan ke header Authorization, tetapi PoP Token tersebut tidak akan menyertakan nonce yang valid.

let shrNonce = null; // Globally scoped variable

// 1. Configure PoP Token Request without a valid SHR Nonce
const popTokenRequest = {
    scopes: ["User.Read"],
    authenticationScheme: msal.AuthenticationScheme.POP,
    resourceRequestMethod: "POST",
    resourceRequestUri: "YOUR_RESOURCE_ENDPOINT"
    shrNonce: shrNonce // SHR Nonce is invalid as null string at this point
};


 // Get PoP token to make authenticated request
const shr = await publicClientApplication.acquireTokenSilent(popTokenRequest);

// Set up PoP Resource request
const reqHeaders = new Headers();
const authorizationHeader = `PoP ${shr}`;

headers.append("Authorization", authorizationHeader);

const options = {
    method: method,
    headers: headers
};
  1. Setelah permintaan disiapkan dan SHR telah ditambahkan ke header otorisasi, permintaan POSTdikirim ke sumber daya. Pada tahap ini, tanpa nonce server yang valid dalam PoP Token/SHR, resource harus merespons dengan kesalahan HTTP 401 Unauthorized, yang akan memiliki header WWW-Authenticate yang berisi nonce server valid pertama sebagai salah satu challenge-nya.
// Make call to resource with SHR
return fetch(resourceEndpointData.endpoint, options)
    .then(response => response.json())
    .then(response => {
        // At this point, the response will be a 401 Error, so ignore the success case for now
    })
    .catch(error => {
        // This error will be a `401 Unauthorized` error, containing a WWW-Authenticate header with an error message such as "nonce_missing" or "nonce_malformed"
        // The correct way to handle this scenario is shown in the following step.
});
  1. Untuk mengekstrak server nonce dari header WWW-Authenticate tersebut, MSAL menyediakan kelas AuthenticationHeaderParser, yang mencakup API getShrNonce yang akan menguraikan server nonce dari header autentikasi yang memuatnya:
import { PublicClientApplication, AuthenticationHeaderParser } from "@azure/msal-browser";

...

// Make call to resource with SHR
return fetch(resourceEndpointData.endpoint, options)
    .then(response => response.json())
    .then(response => {
        if (response.status === 200 && response.headers.get("Authentication-Info")) {
            // At this point, the response will be a 401 Error, so ignore the success case for now
        }
        // Check if error is 401 unauthorized and WWW-Authenticate header is included
        else if (response.status === 401 && response.headers.get("WWW-Authenticate")) {
            lastResponseHeaders = response.headers;
            const authHeaderParser = new AuthenticationHeaderParser(response.headers);
            shrNonce = authHeaderParser.getShrNonce(); // Null is replaced with valid nonce from WWW-Authenticate header
        } else {
            // Deal with other errors as necessary
        }
    }); 
});

Menggunakan dan memperbarui nonce yang valid

  1. Sekarang, setelah shrNonce diperoleh untuk pertama kalinya, PoP Token dapat diminta kembali dengan menyertakan nonce yang valid, dan permintaan sumber daya yang diotorisasi dapat berhasil diselesaikan. Respons 200 OK yang berhasil kini akan menyertakan Authentication-Info header yang berisi nextnonce challenge, yang dapat diuraikan oleh MSAL dengan cara yang sama seperti WWW-Authenticate nonce:
import { PublicClientApplication, AuthenticationHeaderParser } from "@azure/msal-browser";

// 1. Configure PoP Token Request without a valid SHR Nonce
const popTokenRequest = {
    scopes: ["User.Read"],
    authenticationScheme: msal.AuthenticationScheme.POP,
    resourceRequestMethod: "POST",
    resourceRequestUri: "YOUR_RESOURCE_ENDPOINT"
    shrNonce: shrNonce // SHR Nonce is now a valid server-generated nonce
};


 // Get PoP token to make authenticated request
const shr = await publicClientApplication.acquireTokenSilent(popTokenRequest);

// Set up PoP Resource request
const reqHeaders = new Headers();
const authorizationHeader = `PoP ${shr}`;

headers.append("Authorization", authorizationHeader);

const options = {
    method: method,
    headers: headers
};

// Make call to resource with SHR
return fetch(resourceEndpointData.endpoint, options)
    .then(response => response.json())
    .then(response => {
         if (response.status === 200 && response.headers.get("Authentication-Info")) {
             /** NEW **/
            // 200 OK if nonce was valid
            lastResponseHeaders = response.headers;
            const authHeaderParser = new AuthenticationHeaderParser(response.headers);
            shrNonce = authHeaderParser.getShrNonce(); // Previous nonce (possibly expired) is replaced with the nextnonce generated by the server
        }
        // Check if error is 401 unauthorized and WWW-Authenticate header is included
        else if (response.status === 401 && response.headers.get("WWW-Authenticate")) {
           /** SAME AS BEFORE **/
            lastResponseHeaders = response.headers;
            const authHeaderParser = new AuthenticationHeaderParser(response.headers);
            shrNonce = authHeaderParser.getShrNonce(); // Null is replaced with valid nonce from WWW-Authenticate header
        } else {
            // Deal with other errors as necessary
        }
    });
});

Siklus Akuisisi Nonce Terintegrasi

Skrip berikut mengusulkan cara yang direkomendasikan untuk menangani PoP Token Permintaan yang mengharuskan server nonce diperoleh dan diperbarui terus menerus:

/**
 * Application script
 */

import { PublicClientApplication, AuthenticationHeaderParser } from "@azure/msal-browser";
const publicClientApplication = new PublicClientApplication(msalConfig);

// Initialize header map to keep track of the "last" response's headers.
let lastResponseHeaders: HttpHeaders = null;
// Call the PoP API endpoint 
const { responseBody, lastResponseHeaders } = await callPopResource(publicClientApplication, resourceEndpointData, lastResponseHeaders);

/**
 * End Application script
 */


/**
 * Source Code: 
 * This method is responsible for getting data from a PoP-protected API. It is called at the bottom of the 
 * demo code in the application script.
 */
const async callPopResource(
    publicClientApplication: PublicClientApplication,
    resourceEndpointData: ResourceEndpointData,
    lastResponseHeaders: HttpHeaders): ResponseBody {
    
    // Get headers from last response's headers
    const headerParser = new AuthenticationHeaderParser(lastResponseHeaders);
    let shrNonce: string | null;
    
    try {
        shrNonce = headerParser.getShrNonce(); // Will return 
    } catch (e) {
        // If the lastResponse headers are null, .getShrNonce will throw (by design)
        shrNonce = null;
    }

    // Build PoP request as usual, adding the server nonce
    const popTokenRequest = {
        account: CURRENT_ACCOUNT,
        scopes: resourceEndpointData.POP_RESOURCE_SCOPES,
        authenticationScheme: AuthenticationScheme.POP,
        resourceRequestUri: resourceEndpointData.RESOURCE_URI,
        resourceRequestMethod: resourceEndpointData.METHOD,
        shrClaims: resourceEndpointData.CUSTOM_CLAIMS_STRING,
        shrNonce: shrNonce || undefined // Will be undefined on the first call, shrNonce should be valid on subsequent calls
    }

    // Get pop token to make authenticated request
    const shr = await publicClientApplication.acquireTokenSilent(popTokenRequest);

    // PoP Resource request
    const reqHeaders = new Headers();
    const authorizationHeader = `PoP ${shr}`; //Create Authorization header

    headers.append("Authorization", authorizationHeader); // Add Authorization header to request headers

    const options = {
        method: method,
        headers: headers
    };

    // Make call to resource with SHR
    return fetch(resourceEndpointData.endpoint, options)
        .then(response => response.json())
        .then(response => {
            if (response.status === 200 && response.headers.get("Authentication-Info")) {
                lastResponseHeaders = response.headers;
                const authHeaderParser = new AuthenticationHeaderParser(response.headers);
                shrNonce = authHeaderParser.getShrNonce(); // Previous nonce (possibly expired) is replaced with the nextnonce generated by the server
            }
            // Check if error is 401 unauthorized and WWW-Authenticate header is included
            else if (response.status === 401 && response.headers.get("WWW-Authenticate")) {
            /** SAME AS BEFORE **/
                lastResponseHeaders = response.headers;
                const authHeaderParser = new AuthenticationHeaderParser(response.headers);
                shrNonce = authHeaderParser.getShrNonce(); // Null is replaced with valid nonce from WWW-Authenticate header
            } else {
                // Deal with other errors as necessary
            }
        });
    });
}