通过


如何将 Node.js 应用从 ADAL 迁移到 MSAL

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

先决条件

更新应用注册设置

使用 ADAL Node 时,您可能会使用 Azure AD v1.0 终结点。 从 ADAL 迁移到 MSAL 的应用应切换到 Azure AD v2.0 终结点

安装并导入 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 对象,然后公开可在不同身份验证流中使用的方法(例如, acquireTokenWithAuthorizationCode 对于 Web 应用)。 初始化时,唯一必需的参数是 颁发机构 URI

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

var authorityURI = "https://login.microsoftonline.com/common";
var authenticationContext = 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"
        }
    });

PublicClientApplicationConfidentialClientApplication 都绑定到客户端 ID,而不是像 ADAL 的 AuthenticationContext。 这意味着,如果你要在应用程序中使用不同的客户端 ID,则需要为每个 ID 实例化一个新的 MSAL 实例。 有关详细信息,请参阅: MSAL 节点的初始化

配置 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 节点使用 配置类型的配置对象。 其中包含以下属性:

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 不适用 acquireTokeByDeviceCode (请参阅上图)
不适用 acquireTokenOnBehalfOf 抽象 OBO 流的新方法
acquireTokenWithClientCertificate 不适用 由于证书在初始化过程中已经分配,现在不再需要(请参阅配置选项
不适用 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 中,可以使用 scope 参数在您想要的时间请求权限(因此,动态作用域)。 这样,用户就可以向范围提供 增量同意 。 因此,如果你最初只是希望用户登录到你的应用程序,而不需要任何类型的访问权限,则可以这样做。 如果后来需要读取用户的日历,则可以在 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,因为缓存架构不兼容。 但是,你可以使用应用先前通过 MSAL Node 中的 ADAL 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
};

如果要开发 公共客户端应用程序 (如桌面应用), Microsoft Node 的身份验证扩展 为客户端应用程序提供安全机制来执行跨平台令牌缓存序列化和持久性。 支持的平台是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);
});

有关详细信息,请参阅 MSAL 节点示例

注意

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

处理错误和异常

使用 MSAL 节点时,可能遇到的最常见的错误类型是 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!`));

后续步骤