Azure OIDC 的 Node.js/Express 配置未返回请求的刷新令牌

问题描述 投票:0回答:1

我的任务是将现有应用程序迁移到使用 Azure OIDC 进行身份验证。我有基本的登录流程,使用下面的代码作为测试,但是身份验证完成后的重定向不会发送刷新令牌,尽管 - 根据我发现配置的所有内容 - 它应该是。

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

const ACCOUNTS_COOKIE_NAME = 'test-account';
const CREDENTIALS_COOKIE_NAME = 'test-credentials';
const TEST_GROUPS_COOKIE_NAME = 'test-groups';

const COOKIE_OPTIONS = {
  path: '/',
  httpOnly: true,
  secure: true,
  sameSite: 'strict',
  ...config.get('cookies')
}

const MSAL_CONFIG = {
  auth: {
    clientId: config.get("azure.auth.clientId"),
    authority: `https://login.microsoftonline.com/${config.get("azure.auth.tenantId")}/v2.0`,
    clientSecret: config.get("azure.auth.clientSecret")
  },
  system: {
    loggerOptions: {
      loggerCallback: (level, message, containsPii) => {
        if (!containsPii) {
          console.log(message);
        }
      },
      piiLoggingEnabled: false,
      logLevel: msal.LogLevel.Verbose,
    }
  }
}

const REDIRECT_URI = config.get("azure.redirectUri");
const GRAPH_ME_ENDPOINT = 'https://graph.microsoft.com/v1.0/me';

const generateCredentialsCookiePayload = (tokenResponse) => {
  const { tokenType, accessToken, refreshToken, account } = tokenResponse;
  return {
    account_id: account.localAccountId,
    token_type: tokenType,
    access_token: accessToken,
    refresh_token: refreshToken,
    expires: account.idTokenClaims.exp * 1000
  }
}

const generateAccountCookiePayload = (account) => {
  const { homeAccountId, tenantId, localAccountId, username, name } = account;
  return {
    homeAccountId,
    tenantId,
    localAccountId,
    username,
    name
  }
}

module.exports = (router) => {
  const msalClient = new msal.ConfidentialClientApplication(MSAL_CONFIG);

  router.get('/auth/login', (req, res) => {
    const authCodeUrlParams = {
      scopes: [ 'https://graph.microsoft.com/.default', 'offline_access' ],
      redirectUri: REDIRECT_URI,
    };

    msalClient.getAuthCodeUrl(authCodeUrlParams)
      .then((authCodeUrl) => {
        res.redirect(authCodeUrl);
      })
      .catch((error) => {
        console.error(error);
        res.status(500).send('Error during authentication');
      });
  });

  router.get('/auth/acquireToken', (req, res, next) => {

    // TODO: Update this to use a refresh token to get a new access token
    return res.redirect('/auth/login');
  });

  router.get('/auth/redirect', (req, res) => {
    console.log(`req.query is`, req.query);

    const tokenRequest = {
      code: req.query.code,
      scopes: [ 'https://graph.microsoft.com/.default', 'offline_access' ],
      redirectUri: REDIRECT_URI,
    };

    msalClient.acquireTokenByCode(tokenRequest)
      .then((response) => {
        console.log(response);

        console.log(`token response is ${JSON.stringify(response)}`);

        res.cookie(CREDENTIALS_COOKIE_NAME, generateCredentialsCookiePayload(response), COOKIE_OPTIONS);
        res.cookie(ACCOUNTS_COOKIE_NAME, generateAccountCookiePayload(response.account), COOKIE_OPTIONS);
        res.cookie(TEST_GROUPS_COOKIE_NAME, response.account.idTokenClaims.groups, COOKIE_OPTIONS);

        res.redirect('/testpage');     // TODO: update this
      })
      .catch((error) => {
        console.error(error);
        res.status(500).send('Error acquiring token');
      });
  });

  router.get('/auth/logout', (req, res) => {
    res.clearCookie(CREDENTIALS_COOKIE_NAME, { path: COOKIE_OPTIONS.path });
    res.clearCookie(ACCOUNTS_COOKIE_NAME, { path: COOKIE_OPTIONS.path });
    res.clearCookie(TEST_GROUPS_COOKIE_NAME, { path: COOKIE_OPTIONS.path });
    res.redirect('/');
  });

  router.get('/auth/me', (req, res, next) => {
    const authorization = req.header('authorization');
    const token = authorization.split(' ')[1];
    const endpoint = `${GRAPH_ME_ENDPOINT}?$select=id,employeeId,userPrincipalName,displayName,givenName,surname,jobTitle,mail,officeLocation,businessPhones,mobilePhone`;

    axios.get(endpoint, { headers: { authorization: `Bearer ${token}` }})
      .then((response) => {
        res.json(response.data);
      })
      .catch((error) => {
        next(error);
      });
  });

  return router;
};

我注意到在令牌响应负载中,列出的范围不包括

offline_access

我还从

https://login.microsoftonline.com/TENANT_ID/v2.0/.well-known/openid-configuration
端点请求了我们的配置,它表明支持
offline_access
范围。

我在这里缺少什么?

node.js azure express openid-connect
1个回答
0
投票

注意:刷新令牌在响应中不直接公开。相反,它存储在 MSAL 令牌缓存中,用于跟踪令牌以供将来使用。 MSAL 在内部管理缓存,您可以在需要时使用

getTokenCache()
方法检索它。

  • 序列化令牌缓存以检索刷新令牌:首次成功获取令牌后,可以通过序列化令牌缓存来检索刷新令牌。

要获取访问权限、ID 和刷新令牌并调用用户信息端点,请修改如下代码:

const express = require('express');
const msal = require('@azure/msal-node');
const axios = require('axios'); 
const cookieParser = require('cookie-parser');

// Constants for cookies
const COOKIE_OPTIONS = {
  path: '/',
  httpOnly: true,
  secure: false, // Set to false for local dev over HTTP
  sameSite: 'strict',
};

const app = express();
const port = 3000;

// MSAL Config
const msalConfig = {
  auth: {
    clientId: "ClientID", 
    authority: "https://login.microsoftonline.com/TenantID", 
    redirectUri: "http://localhost:3000/auth/redirect",
  },
  system: {
    loggerOptions: {
      loggerCallback: (level, message, containsPii) => {
        if (!containsPii) {
          console.log(message);
        }
      },
      logLevel: msal.LogLevel.Verbose,
    },
    cache: {
      cacheLocation: 'memoryStorage', // Use in-memory cache for Node.js
      storeAuthStateInCookie: true, // Store the auth state in cookies for persistence
    }
  }
};

// MSAL instance
const msalClient = new msal.PublicClientApplication(msalConfig);

// Root route
app.get('/', (req, res) => {
  res.send('<h1>Welcome! <a href="/auth/login">Login with Microsoft</a></h1>');
});

// Login route to initiate MSAL authentication
app.get('/auth/login', (req, res) => {
  const authCodeUrlParams = {
    scopes: ['user.read', 'offline_access'], // Include offline_access to get refresh token
    redirectUri: msalConfig.auth.redirectUri,
  };

  msalClient.getAuthCodeUrl(authCodeUrlParams)
    .then(authCodeUrl => {
      res.redirect(authCodeUrl); // Redirect the user to the Microsoft login page
    })
    .catch(error => {
      console.error('Error while getting auth code URL:', error);
      res.status(500).send('Error while redirecting to Microsoft login');
    });
});

// Redirect route after successful login
app.get('/auth/redirect', (req, res) => {
  const tokenRequest = {
    code: req.query.code, // Code received in the redirect
    scopes: ['user.read', 'offline_access'], // Include offline_access to get refresh token
    redirectUri: msalConfig.auth.redirectUri,
    accessType: 'offline', // Ensure offline access to get the refresh token
  };

  msalClient.acquireTokenByCode(tokenRequest)
    .then(response => {
      console.log("Access Token:", response.accessToken);  // Log access token
      console.log("ID Token:", response.idToken);  // Log ID token

      // Extract the refresh token from the token cache synchronously
      try {
        const tokenCache = msalClient.getTokenCache().serialize(); // Synchronously serialize token cache
        const parsedCache = JSON.parse(tokenCache);
        const refreshTokenObject = parsedCache.RefreshToken;

        const refreshToken = refreshTokenObject
          ? refreshTokenObject[Object.keys(refreshTokenObject)[0]].secret
          : 'No refresh token found';

        // Store tokens in cookies
        res.cookie('access_token', response.accessToken, COOKIE_OPTIONS);
        res.cookie('id_token', response.idToken, COOKIE_OPTIONS);
        res.cookie('refresh_token', refreshToken, COOKIE_OPTIONS);

        // Call Microsoft Graph API to get user info
        const GRAPH_ME_ENDPOINT = 'https://graph.microsoft.com/v1.0/me';

        axios.get(GRAPH_ME_ENDPOINT, {
          headers: {
            Authorization: `Bearer ${response.accessToken}`, // Set the access token in Authorization header
          }
        })
        .then(graphResponse => {
          // Handle successful response from Graph API
          console.log('User Info:', graphResponse.data);
          res.send(`
            <h1>Logged in successfully!</h1>
            <p>Access Token: ${response.accessToken}</p>
            <p>ID Token: ${response.idToken}</p>
            <p>Refresh Token: ${refreshToken}</p>
            <h2>Graph API User Info:</h2>
            <pre>${JSON.stringify(graphResponse.data, null, 2)}</pre>
          `);
        })
        .catch(error => {
          console.error('Error fetching user data from Graph API:', error);
          res.status(500).send('Error fetching user data from Microsoft Graph API');
        });
      } catch (error) {
        console.error('Error while serializing token cache:', error);
        res.status(500).send('Error while retrieving refresh token');
      }
    })
    .catch(error => {
      console.error('Error while acquiring token:', error);
      res.status(500).send('Error while acquiring token');
    });
});

// Logout route to clear cookies
app.get('/auth/logout', (req, res) => {
  res.clearCookie('access_token', { path: COOKIE_OPTIONS.path });
  res.clearCookie('id_token', { path: COOKIE_OPTIONS.path });
  res.clearCookie('refresh_token', { path: COOKIE_OPTIONS.path });
  res.redirect('/');
});

// Start the server
app.listen(port, () => {
  console.log(`Server running at http://localhost:${port}`);
});

enter image description here

enter image description here

单击 Login with Microsoft 时,生成 访问、ID 和刷新令牌,也称为 Microsoft Graph API 端点

enter image description here

授予 Microsoft Entra ID 应用程序的 API 权限:

enter image description here

参考:

node.js - 处理Azure(Microsoft graph)委托流程中的刷新令牌 - 堆栈内存溢出

最新问题
© www.soinside.com 2019 - 2024. All rights reserved.