我的任务是将现有应用程序迁移到使用 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
范围。
我在这里缺少什么?
注意:刷新令牌在响应中不直接公开。相反,它存储在 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}`);
});
单击 Login with Microsoft 时,生成 访问、ID 和刷新令牌,也称为 Microsoft Graph API 端点:
授予 Microsoft Entra ID 应用程序的 API 权限:
参考: