我有一个适用于 Microsoft 团队的应用程序,它使用 Azure SSO 登录。
认证流程如下:
sso-token
并将其发送到后端。/token
和 sso-token
(包括 scopes
)调用 Microsoft graph API 的 offline_access
路由来获取 access_token
和 refresh_token
。 refresh_token
和 access_token
并且后端返回 200
状态。
但如果需要同意,则后端会将 403
状态发送回前端。403
状态后,前端现在启动同意流程,并使用 /authorize
和 client-id
(包括 scopes
)调用 offline_access
路由。在这种情况下,在用户同意后,我不会从 Microsoft 获得 refresh_token
,而只返回 access_token
。现在,我有一个功能可以从应用程序向用户发送 Microsoft 团队活动通知,为此,我需要不时刷新访问令牌,而无需用户交互,因为发送活动通知的过程是从后端发生的。并且由于在需要用户同意的情况下后端无法获取
refresh_token
,一段时间后后端保存的 access_token
就会过期,并且所有发送活动通知的 API 调用都会导致错误.
access_token
获取 refresh_token
和 sso-token
的实用函数。// other import statements
//...
const {
MS_CLIENT_ID,
MS_CLIENT_SECRET,
MS_GRAPH_SCOPES,
} = require('../config/env');
const graphScopes = `https://graph.microsoft.com/User.Read https://graph.microsoft.com/${MS_GRAPH_SCOPES} offline_access`;
// receives the sso-token as parameter from front-end
const getAccessToken = async (ssoToken) => {
try {
const { tid } = jwt_decode(ssoToken);
if (!tid) throw new Error('sso token is malformed');
const accessTokenQueryParams = new URLSearchParams({
grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
client_id: MS_CLIENT_ID,
client_secret: MS_CLIENT_SECRET,
assertion: ssoToken,
scope: graphScopes,
requested_token_use: 'on_behalf_of',
}).toString();
const { data, status } = await axios({
method: 'POST',
url: `https://login.microsoftonline.com/${tid}/oauth2/v2.0/token`,
headers: {
Accept: 'application/json',
'Content-Type': 'application/x-www-form-urlencoded',
},
data: accessTokenQueryParams,
});
if (status != 200) throw new Error('could not exchange access token');
return data;
} catch (error) {
console.error({
function: 'getAccessToken',
dir: __dirname,
file: __filename,
error:
error.response && error.response.data ? error.response.data : error,
});
return null;
}
};
sso-token
来调用实用程序函数。router.get('/auth', async (req, res) => {
try {
const {
ssoToken,
teamId,
teamName = '',
channelId,
channelName = '',
} = req.query;
const { tid } = jwt_decode(ssoToken);
if (!tid || !teamId)
return res
.status(500)
.json({ errors: [{ msg: 'Could not exchange access token' }] });
// CALLING THE UTILITY FUNCSTION WITH SSO TOKEN
const data = await getAccessToken(ssoToken);
if (!data || !data.access_token)
res
.status(403)
.json({ errors: [{ msg: 'User must consent or perform MFA' }] });
const { access_token: accessToken, refresh_token: refreshToken } = data;
console.log({ accessToken, refreshToken });
if (!accessToken)
return res
.status(500)
.json({ errors: [{ msg: 'Could not exchange access token' }] });
// SUCCESS
// return 200 status
//...
} catch (error) {
console.error({
error:
error.response && error.response.data ? error.response.data : error,
person: req.person,
company: req.company,
headers: req.headers,
params: req.params,
url: req.originalUrl,
});
// this error should trigger the consent flow in the client.
res
.status(403)
.json({ errors: [{ msg: 'User must consent or perform MFA' }] });
}
});
ConsentPopup.js
(负责启动用户同意流程的组件)class ConsentPopup extends React.Component {
componentDidMount() {
console.log("consentPopUp initialized");
microsoftTeams.initialize();
// Get the user context in order to extract the tenant ID
microsoftTeams.getContext((context, error) => {
let tenant = context["tid"]; //Tenant ID of the logged in user
let client_id = env.REACT_APP_AZURE_APP_REGISTRATION_ID;
let queryParams = {
tenant: `${tenant}`,
client_id: `${client_id}`,
response_type: "token", //token_id in other samples is only needed if using open ID
// offline_access is Added for RefreshToken
scope: "https://graph.microsoft.com/User.Read https://graph.microsoft.com/TeamsActivity.Send offline_access",
redirect_uri: window.location.origin + "/auth-end",
nonce: crypto.randomBytes(16).toString("base64"),
};
let url = `https://login.microsoftonline.com/${tenant}/oauth2/v2.0/authorize?`;
queryParams = new URLSearchParams(queryParams).toString();
let authorizeEndpoint = url + queryParams;
//Redirect to the Azure authorization endpoint. When that flow completes, the user will be directed to auth-end
// GO TO ClosePopup.js
console.log(authorizeEndpoint);
window.location.assign(authorizeEndpoint);
});
}
render() {
return (
<div>
<h1>Please wait...</h1>
</div>
);
}
}
ClosePopup.js
(负责完成用户同意流程的组件)class ClosePopup extends React.Component {
componentDidMount(){
microsoftTeams.initialize();
//The Azure implicit grant flow injects the result into the window.location.hash object. Parse it to find the results.
let hashParams = this.getHashParameters();
console.log({hashParams});
//If consent has been successfully granted, the Graph ACCESS TOKEN and REFRESH TOKEN should be present as a field in the dictionary.
if (hashParams["access_token"]){
//Notifify the showConsentDialogue function in Tab.js that authorization succeeded. The success callback should fire.
//SENDING BOTH REFRESH TOKEN AND ACCESS TOKEN
microsoftTeams.authentication.notifySuccess(hashParams);
} else {
microsoftTeams.authentication.notifyFailure("Consent failed");
}
}
getHashParameters() {
let hashParams = {};
console.log({hash: window.location.hash, hashSubStr: window.location.hash.substr(1)});
window.location.hash.substr(1).split("&").forEach(function(item) {
let [key,value] = item.split('=');
hashParams[key] = decodeURIComponent(value);
});
console.log('Get Hash Params');
console.log({hashParams});
return hashParams;
}
render() {
return (
<div>
<h1>Consent flow complete.</h1>
</div>
);
}
}
export default ClosePopup;
拨款可能不会立即完成。根据我的经验,最长可达 30 秒。 IE。授权调用返回,但后端仍然无法“工作”。
在第二种情况下,您可以尝试继续查询,直到获得refresh_token。 IE。只需执行多次(在后端)。 IE。尝试多次交换 sso 令牌。我想几秒钟后,refresh_token 就会开始出现。