基于MSAL的msgraph-training-reactspa React SPA获取刷新令牌并用于CRON任务调用Microsoft Graph API的实现方法咨询
Great question! Let's walk through how to make this work—since by default, SPAs using MSAL (like the msgraph-training-reactspa SDK) don't get refresh tokens because of Azure AD's security defaults for public clients. Here's what you need to do step by step:
1. Update Your Azure AD App Registration
First, you need to configure your app registration to allow refresh tokens for your SPA:
- Log into the Azure Portal, navigate to your app registration.
- Go to Authentication > Under "Advanced settings", set Allow public client flows to Yes. This enables Azure AD to issue refresh tokens to your SPA (a public client).
- Ensure you've added your SPA's redirect URI under the "Single-page application" platform section.
2. Modify Your React SPA's MSAL Configuration
Next, update your MSAL setup to request the offline_access scope (required to get a refresh token) and extract the refresh token to send to your backend:
Initialize MSAL with the Right Scopes
import { PublicClientApplication } from '@azure/msal-browser'; // MSAL configuration const msalConfig = { auth: { clientId: "YOUR_CLIENT_ID", authority: "https://login.microsoftonline.com/YOUR_TENANT_ID", redirectUri: window.location.origin, // Match your Azure AD redirect URI }, cache: { cacheLocation: "sessionStorage", // Avoid localStorage for better security storeAuthStateInCookie: false, } }; const msalInstance = new PublicClientApplication(msalConfig); // Login request with offline_access scope const loginRequest = { scopes: ["user.read", "mail.read", "offline_access"] // Include offline_access here };
Handle Login and Extract the Refresh Token
After the user logs in, you can pull the refresh token from MSAL's token cache and send it to your backend for storage:
async function handleLogin() { try { const loginResponse = await msalInstance.loginPopup(loginRequest); const account = msalInstance.getAccountByUsername(loginResponse.account.username); // Get the refresh token from MSAL's cache const tokenCache = msalInstance.getTokenCache(); const allRefreshTokens = await tokenCache.getAllRefreshTokens(); const userRefreshToken = allRefreshTokens.find(rt => rt.account.homeAccountId === account.homeAccountId); if (userRefreshToken) { // Send refresh token to your backend API for database storage await fetch("/api/save-refresh-token", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ refreshToken: userRefreshToken.secret, userId: account.localAccountId }) }); } } catch (err) { console.error("Login failed:", err); } }
Important: Never store the refresh token in the frontend long-term—send it to your backend immediately, as frontend storage is insecure for sensitive tokens.
3. Use the Refresh Token in Your Backend Cron Job
For your cron task, you'll need to use MSAL for Node.js to exchange the stored refresh token for a new access token, then call Microsoft Graph. Here's a sample Node.js backend implementation:
MSAL Node Setup
const msal = require('@azure/msal-node'); // Confidential client config (your backend is a confidential client) const msalConfig = { auth: { clientId: "YOUR_CLIENT_ID", authority: "https://login.microsoftonline.com/YOUR_TENANT_ID", clientSecret: "YOUR_CLIENT_SECRET" // Generate this in your Azure AD app registration under "Certificates & secrets" } }; const cca = new msal.ConfidentialClientApplication(msalConfig);
Exchange Refresh Token for Access Token
async function getAccessToken(refreshToken) { const tokenRequest = { refreshToken: refreshToken, scopes: ["user.read", "mail.read"] // Match the scopes used in the SPA login }; try { const response = await cca.acquireTokenByRefreshToken(tokenRequest); // Optionally, update the stored refresh token with the new one returned (MSAL may issue a new refresh token) await updateRefreshTokenInDB(response.refreshToken); return response.accessToken; } catch (err) { console.error("Failed to get access token:", err); // Handle refresh token expiration (e.g., notify user to re-login) throw err; } }
Cron Job Implementation
const cron = require('node-cron'); // Schedule your cron job (example: runs daily at 2 AM) cron.schedule('0 2 * * *', async () => { try { // Fetch the stored refresh token from your database const { refreshToken } = await getStoredRefreshTokenFromDB(); const accessToken = await getAccessToken(refreshToken); // Call Microsoft Graph API const graphResponse = await fetch("https://graph.microsoft.com/v1.0/me/messages", { headers: { Authorization: `Bearer ${accessToken}` } }); const data = await graphResponse.json(); // Process and update your data as needed console.log("Fetched data from Graph:", data); } catch (err) { console.error("Cron job failed:", err); } });
Key Security & Implementation Notes
- Encrypt Refresh Tokens: Always encrypt refresh tokens when storing them in your database—never store them as plain text.
- Handle Token Expiry: Refresh tokens do expire (and can be revoked), so your backend should handle cases where
acquireTokenByRefreshTokenfails (e.g., trigger a user re-login flow or alert an admin). - Permission Consent: Ensure the user has consented to all scopes you're requesting (including
offline_access)—you may need to prompt for consent on login if permissions haven't been granted yet.
内容的提问来源于stack exchange,提问作者Navdeep Singh

