将 Node.js 应用程序从 ADAL 迁移到 MSAL

用于 Node 的 Microsoft 身份验证库(MSAL 节点)是推荐的 SDK,用于为在Microsoft 标识平台上注册的应用程序启用身份验证和授权。 本文介绍了将应用从适用于 Node 的 Active Directory 身份验证库 (ADAL Node) 迁移到 MSAL Node 时所需完成的重要步骤。

先决条件

将应用程序从 ADAL 节点迁移到 MSAL 节点

有关 ADAL 与 MSAL 之间的差异以及重要日期和常见问题的一般说明,请参阅:将应用程序迁移到Microsoft 身份验证库(MSAL)

更新应用注册设置

使用 ADAL Node 时,你有可能会用到 Azure AD v1.0 终结点。 将应用程序从 ADAL 迁移到 MSAL,以使用 Microsoft 标识平台终结点

安装并导入 MSAL

  1. 通过 NPM 安装 MSAL Node 程序包:
  npm install @azure/msal-node
  1. 然后,在代码中导入 MSAL Node:
  const msal = require('@azure/msal-node');
  1. 最后,卸载 ADAL Node 程序包并删除代码中的任何引用:
  npm uninstall adal-node

初始化 MSAL

在 ADAL Node 中,你可以初始化 AuthenticationContext 对象,该对象之后会为你显示可以在不同身份验证流中使用的方法(例如适用于 Web 应用的 acquireTokenWithAuthorizationCode)。 初始化时,唯一必需的参数是 颁发机构 URI

var adal = require('adal-node');

var authorityURI = "https://login.microsoftonline.com/common";
var authenticationContex = new adal.AuthenticationContext(authorityURI);

在 MSAL Node 中,你有两种替代方案可以使用:若要生成移动应用或桌面应用,请实例化 PublicClientApplication 对象。 构造函数需要用到配置对象,而且其中至少要包含参数 clientId。 如果未指定颁发机构 URI,则 MSAL 会默认将其设置为 https://login.microsoftonline.com/common

const msal = require('@azure/msal-node');

const pca = new msal.PublicClientApplication({
        auth: {
            clientId: "YOUR_CLIENT_ID"
        }
    });

注释

如果您在 v2.0 中使用 https://login.microsoftonline.com/common 权限,则可允许用户使用任何 Microsoft Entra 组织或个人 Microsoft 帐户 (MSA) 登录。 在 MSAL Node 中,如果你想要限制用户登录到任何 Microsoft Entra 帐户(该行为同样适用于 ADAL Node),则应改为使用 https://login.microsoftonline.com/organizations

另一方面,如果要生成 Web 应用或守护程序应用,则实例化对象 ConfidentialClientApplication 。 使用此类应用时,还需要提供 客户端凭据,例如客户端机密或证书:

const msal = require('@azure/msal-node');

const cca = new msal.ConfidentialClientApplication({
        auth: {
            clientId: "YOUR_CLIENT_ID",
            clientSecret: "YOUR_CLIENT_SECRET"
        }
    });

与 ADAL 的 PublicClientApplication 不同,ConfidentialClientApplicationAuthenticationContext 都要与客户端 ID 绑定。 这意味着,如果你要在应用程序中使用不同的客户端 ID,则需要为每个 ID 实例化一个新的 MSAL 实例。 有关详细信息,请参阅 MSAL Node 的初始化

配置 MSAL

在 Microsoft 身份识别平台上构建应用时,应用中将包含许多与身份验证相关的参数。 在 ADAL Node 中,对象 AuthenticationContext 具有有限数量的配置参数,可以使用它实例化它,而其余参数在代码中自由挂起(例如 clientSecret):

var adal = require('adal-node');

var authority = "https://login.microsoftonline.com/YOUR_TENANT_ID"
var validateAuthority = true,
var cache = null;

var authenticationContext = new adal.AuthenticationContext(authority, validateAuthority, cache);
  • authority:可识别令牌颁发机构的 URL
  • validateAuthority:防止你的代码从潜在恶意颁发机构请求令牌的功能
  • cache:设置此 AuthenticationContext 实例使用的令牌缓存。 如果未设置此参数,则使用内存缓存中的默认值

另一方面,MSAL Node 使用配置类型的配置对象。 其中包含以下属性:

const msal = require('@azure/msal-node');

const msalConfig = {
    auth: {
        clientId: "YOUR_CLIENT_ID",
        authority: "https://login.microsoftonline.com/YOUR_TENANT_ID",
        clientSecret: "YOUR_CLIENT_SECRET",
        knownAuthorities: [],
    },
    cache: {
        // your implementation of caching
    },
    system: {
        loggerOptions: { /** logging related options */ }
    }
}


const cca = new msal.ConfidentialClientApplication(msalConfig);

有一个显著的区别是,MSAL 不含用于禁用机构/授权验证的标志,而且默认情况下一律要对机构/授权进行验证。 MSAL 会将你请求的颁发机构与 Microsoft 已知的一系列颁发机构或你已经在配置中指定的一系列颁发机构进行比较。 有关详细信息,请参阅: 配置选项

切换到 MSAL API

ADAL Node 中的大多数公共方法都可在 MSAL Node 中找到同等方法:

ADAL MSAL 备注
acquireToken acquireTokenSilent 已重命名,现在需要 帐户 对象
acquireTokenWithAuthorizationCode acquireTokenByCode
acquireTokenWithClientCredentials acquireTokenByClientCredential
acquireTokenWithRefreshToken acquireTokenByRefreshToken 可用于迁移有效的 刷新令牌
acquireTokenWithDeviceCode acquireTokenByDeviceCode 现在介绍抽象用户代码的获取方法(见下文)
acquireTokenWithUsernamePassword acquireTokenByUsernamePassword

但是,ADAL Node 中的某些方法已经弃用,而 MSAL Node 提供了新的方法:

ADAL MSAL 备注
acquireUserCode N/A acquireTokeByDeviceCode 合并(见上文)
N/A acquireTokenOnBehalfOf 一种用于抽象化 OBO 流的新方法
acquireTokenWithClientCertificate N/A 不再需要,因为现在系统会在初始化期间分配证书(请参阅配置选项
N/A getAuthCodeUrl 一种新方法用于抽象化授权终结点的 URL 构造

使用作用域而不是资源

v1.0 和 v2.0 终结点之间的一个重要区别就在于资源的访问方式。 在 ADAL Node 中,您首先需要在应用注册门户上注册权限,然后为某个资源(如 Microsoft Graph)请求访问令牌,如下所示:

authenticationContext.acquireTokenWithAuthorizationCode(
    req.query.code,
    redirectUri,
    resource, // e.g. 'https://graph.microsoft.com'
    clientId,
    clientSecret,
    function (err, response) {
        // do something with the authentication response
    }
);

MSAL 节点仅支持 v2.0 终结点。 v2.0 终结点采用 以范围为中心的 模型来访问资源。 因此,你在为资源请求访问令牌时,还需要指定该资源的作用域:

const tokenRequest = {
    code: req.query.code,
    scopes: ["https://graph.microsoft.com/User.Read"],
    redirectUri: REDIRECT_URI,
};

pca.acquireTokenByCode(tokenRequest).then((response) => {
    // do something with the authentication response
}).catch((error) => {
    console.log(error);
});

以范围为中心的模型的优点之一是能够使用 动态范围。 使用 v1.0 生成应用程序时,需要注册应用程序所需的完整权限集(称为 静态作用域),以便用户在登录时同意。 在 v2.0 中,你可以随时按需使用作用域参数来请求相关权限(因此称为“动态作用域”)。 这样,用户就可以向范围提供 增量同意 。 因此,如果你最初只是希望用户登录到你的应用程序,而不需要任何类型的访问权限,则可以这样做。 如果后来需要读取用户的日历,则可以在 acquireToken 方法中请求日历范围,并获取用户的许可。 有关详细信息,请参阅: 资源和范围

使用承诺而不是回叫

在 ADAL Node 中,成功完成身份验证后,你可使用回叫执行任何操作并获得响应:

var context = new AuthenticationContext(authorityUrl, validateAuthority);

context.acquireTokenWithClientCredentials(resource, clientId, clientSecret, function(err, response) {
    if (err) {
        console.log(err);
    } else {
        // do something with the authentication response
    }
});

在 MSAL Node 中,则需改为使用承诺:

    const cca = new msal.ConfidentialClientApplication(msalConfig);

    cca.acquireTokenByClientCredential(tokenRequest).then((response) => {
        // do something with the authentication response
    }).catch((error) => {
        console.log(error);
    });

还可以使用 ES8 附带的 async/await 语法:

    try {
        const authResponse = await cca.acquireTokenByCode(tokenRequest);
    } catch (error) {
        console.log(error);
    }

启用日志记录

在 ADAL Node 中,你可以在代码中的任何位置单独配置日志记录:

var adal = require('adal-node');

//PII or OII logging disabled. Default Logger does not capture any PII or OII.
adal.logging.setLoggingOptions({
  log: function (level, message, error) {
    console.log(message);

    if (error) {
        console.log(error);
    }
  },
  level: logging.LOGGING_LEVEL.VERBOSE, // provide the logging level
  loggingWithPII: false  // Determine if you want to log personal identification information. The default value is false.
});

在 MSAL Node 中,日志记录是配置选项的一部分,可通过初始化 MSAL Node 实例进行创建:

const msal = require('@azure/msal-node');

const msalConfig = {
    auth: {
        // authentication related parameters
    },
    cache: {
        // cache related parameters
    },
    system: {
        loggerOptions: {
            loggerCallback(loglevel, message, containsPii) {
                console.log(message);
            },
            piiLoggingEnabled: false,
            logLevel: msal.LogLevel.Verbose,
        }
    }
}

const cca = new msal.ConfidentialClientApplication(msalConfig);

启用令牌缓存

在 ADAL Node 中,你可以选择导入内存中的令牌缓存。 初始化 AuthenticationContext 对象时,令牌缓存可用作参数:

var MemoryCache = require('adal-node/lib/memory-cache');

var cache = new MemoryCache();
var authorityURI = "https://login.microsoftonline.com/common";

var context = new AuthenticationContext(authorityURI, true, cache);

MSAL Node 默认使用内存中的令牌缓存。 无需显式导入它;内存中的令牌缓存会作为 ConfidentialClientApplicationPublicClientApplication 类的一部分予以公开。

const msalTokenCache = publicClientApplication.getTokenCache();

重要的是,以前在 ADAL Node 上的令牌缓存无法转移到 MSAL Node,因为缓存架构不兼容。 但是,你可以使用你的应用程序先前通过 ADAL Node 在 MSAL Node 上获取的有效刷新令牌。 有关详细信息,请参阅刷新令牌部分。

还可以通过提供自己的 缓存插件将缓存写入磁盘。 缓存插件必须实现接口 ICachePlugin。 与日志记录一样,高速缓存是配置选项的一部分,它在初始化 MSAL Node 实例时被创建。

const msal = require('@azure/msal-node');

const msalConfig = {
    auth: {
        // authentication related parameters
    },
    cache: {
        cachePlugin // your implementation of cache plugin
    },
    system: {
        // logging related options
    }
}

const msalInstance = new ConfidentialClientApplication(msalConfig);

可以按如下所示实现缓存插件示例:

const fs = require('fs');

// Call back APIs which automatically write and read into a .json file - example implementation
const beforeCacheAccess = async (cacheContext) => {
    cacheContext.tokenCache.deserialize(await fs.readFile(cachePath, "utf-8"));
};

const afterCacheAccess = async (cacheContext) => {
    if(cacheContext.cacheHasChanged) {
        await fs.writeFile(cachePath, cacheContext.tokenCache.serialize());
    }
};

// Cache Plugin
const cachePlugin = {
    beforeCacheAccess,
    afterCacheAccess
};

若要开发公共客户端应用程序(如桌面应用),适用于 Node 的 Microsoft 身份验证扩展可提供安全机制,以使客户端应用程序能够跨平台执行令牌缓存的序列化和暂留操作。 支持的平台包括 Windows、Mac 和 Linux。

注释

不建议在 Web 应用程序中使用Microsoft 身份验证扩展 for Node,因为这可能会导致扩展性和性能问题。 相反,我们建议使用 Web 应用将缓存保存到会话中。

删除有关刷新令牌的逻辑

在 ADAL Node 中,刷新令牌(RT)已公开,允许你通过缓存这些令牌并使用 acquireTokenWithRefreshToken 该方法来开发有关使用这些令牌的解决方案。 与 RT 尤其相关的典型场景:

  • 持续运行的服务,可以在用户不再连接时,为用户执行刷新仪表板及其他操作。
  • WebFarm 方案,允许客户端将 RT 带入 Web 服务(高速缓存是在客户端完成,而不是服务器端,而且 cookie 经加密)。

出于安全原因,MSAL Node 和其他 MSAL 一样,不会公开刷新令牌。 而是由 MSAL 代你处理令牌刷新。 因此,你无需再为此生成逻辑。 但是, 可以使用 以前从 ADAL Node 缓存获取的(仍然有效)刷新令牌来获取 MSAL Node 的新令牌集。 为此,MSAL Node 提供 acquireTokenByRefreshToken,它等同于 ADAL Node 的 acquireTokenWithRefreshToken 方法:

var msal = require('@azure/msal-node');

const config = {
    auth: {
        clientId: "ENTER_CLIENT_ID",
        authority: "https://login.microsoftonline.com/ENTER_TENANT_ID",
        clientSecret: "ENTER_CLIENT_SECRET"
    }
};

const cca = new msal.ConfidentialClientApplication(config);

const refreshTokenRequest = {
    refreshToken: "", // your previous refresh token here
    scopes: ["https://graph.microsoft.com/.default"],
    forceCache: true,
};

cca.acquireTokenByRefreshToken(refreshTokenRequest).then((response) => {
    console.log(response);
}).catch((error) => {
    console.log(error);
});

有关详细信息,请参阅 ADAL 节点到 MSAL 节点迁移示例

注释

我们建议您在使用 MSAL Node 的 acquireTokenByRefreshToken 方法如上所示,通过仍然有效的刷新令牌获取新令牌集后,销毁较旧的 ADAL Node 令牌缓存。

处理错误和异常

使用 MSAL Node 时,最常出现的错误类型是 interaction_required 错误。 通常通过启动交互式令牌获取提示,就能解决此错误。 例如,使用 acquireTokenSilent 时,如果没有缓存的刷新令牌,则 MSAL Node 将无法以静默方式获取访问令牌。 同样,尝试访问的 Web API 可能有条件 访问 策略到位,要求用户执行 多重身份验证 (MFA)。 在这种情况下,处理interaction_required错误时触发acquireTokenByCode将要求用户进行 MFA,从而允许他们完成该操作。

然而,你可能面临的另一个常见错误是 consent_required,当用户未同意获取受保护资源的访问令牌所需的权限时,会发生此错误。 像在 interaction_required 中那样,对于 consent_required 错误的解决方案通常是使用 acquireTokenByCode 方法触发交互式令牌获取提示。

运行应用

完成更改后,运行应用并测试身份验证方案:

npm start

示例:通过 ADAL Node 和 MSAL Node 获取令牌

下方片段演示了 Express.js 框架中的一款机密客户端 Web 应用。 当用户访问认证路由/auth时,它会执行登录,通过/redirect路由获取 Microsoft Graph 的访问令牌,然后显示该令牌的内容。

使用 ADAL 节点 使用 MSAL 节点
// Import dependencies
var express = require('express');
var crypto = require('crypto');
var adal = require('adal-node');

// Authentication parameters
var clientId = 'Enter_the_Application_Id_Here';
var clientSecret = 'Enter_the_Client_Secret_Here';
var tenant = 'Enter_the_Tenant_Info_Here';
var authorityUrl = 'https://login.microsoftonline.com/' + tenant;
var redirectUri = 'http://localhost:3000/redirect';
var resource = 'https://graph.microsoft.com';

// Configure logging
adal.Logging.setLoggingOptions({
    log: function (level, message, error) {
        console.log(message);
    },
    level: adal.Logging.LOGGING_LEVEL.VERBOSE,
    loggingWithPII: false
});

// Auth code request URL template
var templateAuthzUrl = 'https://login.microsoftonline.com/'
    + tenant + '/oauth2/authorize?response_type=code&client_id='
    + clientId + '&redirect_uri=' + redirectUri
    + '&state=<state>&resource=' + resource;

// Initialize express
var app = express();

// State variable persists throughout the app lifetime
app.locals.state = "";

app.get('/auth', function(req, res) {

    // Create a random string to use against XSRF
    crypto.randomBytes(48, function(ex, buf) {
        app.locals.state = buf.toString('base64')
            .replace(/\//g, '_')
            .replace(/\+/g, '-');

        // Construct auth code request URL
        var authorizationUrl = templateAuthzUrl
            .replace('<state>', app.locals.state);

        res.redirect(authorizationUrl);
    });
});

app.get('/redirect', function(req, res) {
    // Compare state parameter against XSRF
    if (app.locals.state !== req.query.state) {
        res.send('error: state does not match');
    }

    // Initialize an AuthenticationContext object
    var authenticationContext =
        new adal.AuthenticationContext(authorityUrl);

    // Exchange auth code for tokens
    authenticationContext.acquireTokenWithAuthorizationCode(
        req.query.code,
        redirectUri,
        resource,
        clientId,
        clientSecret,
        function(err, response) {
            res.send(response);
        }
    );
});

app.listen(3000, function() {
    console.log(`listening on port 3000!`);
});
// Import dependencies
const express = require("express");
const msal = require('@azure/msal-node');

// Authentication parameters
const config = {
    auth: {
        clientId: "Enter_the_Application_Id_Here",
        authority: "https://login.microsoftonline.com/Enter_the_Tenant_Info_Here",
        clientSecret: "Enter_the_Client_Secret_Here"
    },
    system: {
        loggerOptions: {
            loggerCallback(loglevel, message, containsPii) {
                console.log(message);
            },
            piiLoggingEnabled: false,
            logLevel: msal.LogLevel.Verbose,
        }
    }
};

const REDIRECT_URI = "http://localhost:3000/redirect";

// Initialize MSAL Node object using authentication parameters
const cca = new msal.ConfidentialClientApplication(config);

// Initialize express
const app = express();

app.get('/auth', (req, res) => {

    // Construct a request object for auth code
    const authCodeUrlParameters = {
        scopes: ["user.read"],
        redirectUri: REDIRECT_URI,
    };

    // Request auth code, then redirect
    cca.getAuthCodeUrl(authCodeUrlParameters)
        .then((response) => {
            res.redirect(response);
        }).catch((error) => res.send(error));
});

app.get('/redirect', (req, res) => {

    // Use the auth code in redirect request to construct
    // a token request object
    const tokenRequest = {
        code: req.query.code,
        scopes: ["user.read"],
        redirectUri: REDIRECT_URI,
    };

    // Exchange the auth code for tokens
    cca.acquireTokenByCode(tokenRequest)
        .then((response) => {
            res.send(response);
        }).catch((error) => res.status(500).send(error));
});

app.listen(3000, () =>
    console.log(`listening on port 3000!`));

后续步骤