Home > Mobile >  @azure/msal-node -> User auth to access Azure Vault
@azure/msal-node -> User auth to access Azure Vault

Time:07-29

So,

I've got a test azure instance and I'm trying to get access to a secret in a vault.

I've created the vault and now I want to access it with an electron application. I do not wish to have any client secrets anywhere on the application. So I am trying to get a bearer token to present to the vault using an authorized user and only the registered application's ID

Image of Vault

In this case the user in question is part of a group that in the vault's access policies has Get and List for secrets.

I have an application with a MSAL authentication added.

Image of application registration

I'm attempting to make use of some very slightly modified versions of this code base example: https://docs.microsoft.com/en-us/azure/active-directory/develop/tutorial-v2-nodejs-desktop

global.api.azure

// https://docs.microsoft.com/en-us/rest/api/keyvault/
// Azure key Vault (API Key Storage / etc.) 

"use strict";
const {BrowserWindow} = require("electron")
const {
    PublicClientApplication,
    CryptoProvider
} = require("@azure/msal-node")
const api = require("../api/api")

// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

const { protocol } = require("electron");

/**
 * CustomProtocolListener can be instantiated in order
 * to register and unregister a custom typed protocol on which
 * MSAL can listen for Auth Code reponses.
 *
 * For information on available protocol types, check the Electron
 * protcol docs: https://www.electronjs.org/docs/latest/api/protocol/
 */
class CustomProtocolListener {
  hostName;
  /**
   * Constructor
   * @param hostName - A string that represents the host name that should be listened on (i.e. 'msal' or '127.0.0.1')
   */
  constructor(hostName) {
    this.hostName = hostName; //A string that represents the host name that should be listened on (i.e. 'msal' or '127.0.0.1')
  }

  get host() {
    return this.hostName;
  }

  /**
   * Registers a custom string protocol on which the library will
   * listen for Auth Code response.
   */
  start() {
    const codePromise = new Promise((resolve, reject) => {
      protocol.registerStringProtocol(this.host, (req, callback) => {
        const requestUrl = new URL(req.url);
        const authCode = requestUrl.searchParams.get("code");
        if (authCode) {
          resolve(authCode);
        } else {
          protocol.unregisterProtocol(this.host);
          reject(new Error("No code found in URL"));
        }
      });
    });

    return codePromise;
  }

  /**
   * Unregisters a custom string protocol to stop listening for Auth Code response.
   */
  close() {
    protocol.unregisterProtocol(this.host);
  }
}

const REDIRECT_URI = "msald3e41458-b256-4b01-87c2-54c9a4db1eef://auth";

const msalConfig = {
    auth: {
      clientId: "d3e41458-b256-4b01-87c2-54c9a4db1eef",
      authority: `https://login.microsoftonline.com/fullphaseroutlook.onmicrosoft.com`,
    },
  };

class azure { 
    constructor() {
        /**
         * Initialize a public client application. For more information, visit:
         * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-node/docs/initialize-public-client-application.md
         */
        this.clientApplication = new PublicClientApplication(msalConfig);
        this.account = null;
    
        // Initialize CryptoProvider instance
        this.cryptoProvider = new CryptoProvider();
    
        /**
         * To demonstrate best security practices, this Electron sample application makes use of
         * a custom file protocol instead of a regular web (https://) redirect URI in order to
         * handle the redirection step of the authorization flow, as suggested in the OAuth2.0 specification for Native Apps.
         */
        this.customFileProtocolName = REDIRECT_URI.split(":")[0];
    
        this.setRequestObjects();
      }
    
      // Creates a "popup" window for interactive authentication
      static createAuthWindow() {
        return new BrowserWindow({
          width: 400,
          height: 600,
        });
      }
    
      /**
       * Initialize request objects used by this AuthModule.
       */
      setRequestObjects() {
        const requestScopes = ["User.Read"];
    
        this.authCodeUrlParams = {
          scopes: requestScopes,
          redirectUri: REDIRECT_URI,
        };
    
        this.authCodeRequest = {
          scopes: requestScopes,
          redirectUri: REDIRECT_URI,
          code: null,
        };
    
        this.pkceCodes = {
          challengeMethod: "S256", // Use SHA256 Algorithm
          verifier: "", // Generate a code verifier for the Auth Code Request first
          challenge: "", // Generate a code challenge from the previously generated code verifier
        };
      }
    
      async login() {
        const authResult = await this.getTokenInteractive(this.authCodeUrlParams);
        return this.handleResponse(authResult);
      }
    
      async logout() {
        if (this.account) {
          await this.clientApplication.getTokenCache().removeAccount(this.account);
          this.account = null;
        }
      }
    
      async getToken(tokenRequest) {
        let authResponse;
        const account = this.account || (await this.getAccount());
        if (account) {
          tokenRequest.account = account;
          authResponse = await this.getTokenSilent(tokenRequest);
        } else {
          const authCodeRequest = {
            ...this.authCodeUrlParams,
            ...tokenRequest,
          };
    
          authResponse = await this.getTokenInteractive(authCodeRequest);
        }
    
        return authResponse.accessToken || null;
      }
    
      async getTokenSilent(tokenRequest) {
        try {
          var data = await this.clientApplication.acquireTokenSilent(tokenRequest);
          console.log(data)
          return data 
        } catch (error) {
          console.log(
            "Silent token acquisition failed, acquiring token using pop up"
          );
          const authCodeRequest = {
            ...this.authCodeUrlParams,
            ...tokenRequest,
          };
          return await this.getTokenInteractive(authCodeRequest);
        }
      }
    
      async getTokenInteractive(tokenRequest) {
        /**
         * Proof Key for Code Exchange (PKCE) Setup
         *
         * MSAL enables PKCE in the Authorization Code Grant Flow by including the codeChallenge and codeChallengeMethod parameters
         * in the request passed into getAuthCodeUrl() API, as well as the codeVerifier parameter in the
         * second leg (acquireTokenByCode() API).
         *
         * MSAL Node provides PKCE Generation tools through the CryptoProvider class, which exposes
         * the generatePkceCodes() asynchronous API. As illustrated in the example below, the verifier
         * and challenge values should be generated previous to the authorization flow initiation.
         *
         * For details on PKCE code generation logic, consult the
         * PKCE specification https://tools.ietf.org/html/rfc7636#section-4
         */
    
        const { verifier, challenge } = await this.cryptoProvider.generatePkceCodes();
        this.pkceCodes.verifier = verifier;
        this.pkceCodes.challenge = challenge;
        const popupWindow = azure.createAuthWindow();
    
        // Add PKCE params to Auth Code URL request
        const authCodeUrlParams = {
          ...this.authCodeUrlParams,
          scopes: tokenRequest.scopes,
          codeChallenge: this.pkceCodes.challenge, // PKCE Code Challenge
          codeChallengeMethod: this.pkceCodes.challengeMethod, // PKCE Code Challenge Method
        };
    
        try {
          // Get Auth Code URL
          const authCodeUrl = await this.clientApplication.getAuthCodeUrl(
            authCodeUrlParams
          );
          const authCode = await this.listenForAuthCode(authCodeUrl, popupWindow);
          // Use Authorization Code and PKCE Code verifier to make token request
          const authResult = await this.clientApplication.acquireTokenByCode({
            ...this.authCodeRequest,
            code: authCode,
            codeVerifier: verifier,
          });
    
          popupWindow.close();
          return authResult;
        } catch (error) {
          popupWindow.close();
          throw error;
        }
      }
    
      async listenForAuthCode(navigateUrl, authWindow) {
        // Set up custom file protocol to listen for redirect response
        const authCodeListener = new CustomProtocolListener(
          this.customFileProtocolName
        );
        const codePromise = authCodeListener.start();
        authWindow.loadURL(navigateUrl);
        const code = await codePromise;
        authCodeListener.close();
        return code;
      }
    
      /**
       * Handles the response from a popup or redirect. If response is null, will check if we have any accounts and attempt to sign in.
       * @param response
       */
      async handleResponse(response) {
        if (response !== null) {
          this.account = response.account;
        } else {
          this.account = await this.getAccount();
        }
        return response;
      }
    
      /**
       * Calls getAllAccounts and determines the correct account to sign into, currently defaults to first account found in cache.
       * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-common/docs/Accounts.md
       */
      async getAccount() {
        // need to call getAccount here?
        const cache = this.clientApplication.getTokenCache();
        const currentAccounts = await cache.getAllAccounts();
    
        if (currentAccounts === null) {
          console.log("No accounts detected");
          return null;
        }
    
        if (currentAccounts.length > 1) {
          // Add choose account code here
          console.log(
            "Multiple accounts detected, need to add choose account code."
          );
          return currentAccounts[0];
        } else if (currentAccounts.length === 1) {
          return currentAccounts[0];
        } else {
          return null;
        }
      }
}

const vaultURL = "https://vault-test.vault.azure.net/"
class vault extends api{
    constructor(config){
        super(config)
        this.base = vaultURL
        this.headers = { 
            'Authorization': `Bearer ${config.token}`,
        }
    }
    async getSecret(secretName){
        return await this._req(`secrets/${secretName}/`,{
            method: "GET",
        })
    }
}

module.exports = { azure, vault } 
async function go(){
    var login = new global.api.azure.azure()
    var resp = await login.login() 
    var vault = new global.api.azure.vault({
        token: resp.accessToken
    })
    var secret = await vault.getSecret("test")
}
go() 

The "extends API" is little more then a wrapper for the axios npm package. Everything for the most part works fine. Login Prompt in Electron

The login prompt is presented, the user logs in and the correct account is retrieved along with a bearer token

Bearer Token

However the vault specifies that the Audience is invalid

Vault Response

I know I'm not presenting the correct audience in the scope, but I was curious how I would go about doing that. I tried adding the Azure Key Vault / User Impersonation API permission under the registered app; however, when changing

const requestScopes = ["User.Read"];

To something like user_impersonation, the CustomProtocolListener returns an error that the code was undefined / null. Any advice would be greatly appreciated.

CodePudding user response:

For KV, you need to define the entire URL of the scope, you cannot pass only user_impersonation. The short version of the scopes can only be used with MS Graph. For example, the below are equivalent:

const requestScopes = ["User.Read"];
const requestScopes = ["https://graph.microsoft.com/User.Read"];

Try changing your scopes array to the following, that should do the trick:

const requestScopes = ["https://vault.azure.net/user_impersonation"];
  • Related