Reputation: 601
I am trying to create an outlook email automation tool. In order for it to do things such as send a email in a folder for the user at a given time. Microsoft allows third party apis to trigger microsoft's apis on behalf of a user through auth2. I am able to retrieve the necessary access token, but I need a refresh token in order to call code on behalf of the user without forcing him to login every hour.
I am currently using the microsoft's javascript authentication library in order to receive tokens. From what I have read, it seems refresh token must have the offline scope in the token request. With code below, I am able to a response with the access token, but I am still unable to get the access token.
const tokenRequest = {
scopes: [
"https://graph.microsoft.com/Mail.ReadWrite",
"https://graph.microsoft.com/mail.send",
"https://graph.microsoft.com/offline_access"
]
};
const hostname = "http://localhost:5000";
const backendServerAdress = "http://localhost:8050";
var xhr = new XMLHttpRequest();
xhr.open("POST", backendServerAdress, true);
xhr.setRequestHeader("Content-Type", "application/json");
const msalConfig = {
auth: {
clientId: "something",
redirectUri: hostname + "/homepage/index.html"
}
};
var loginRequest = {
scopes: ["Mail.ReadWrite", "mail.send", "offline_access"] // optional Array<string>
};
const TOKEN_ID = "token_id";
const msalInstance = new Msal.UserAgentApplication(msalConfig);
msalInstance.handleRedirectCallback((error, response) => {
console.log("redirect callback done");
});
async function redirectToDashboard() {
console.log("redirect to dashboard");
// var response = await requestTokenSilent();
var response;
if (!response || !response.status == 200) {
response = await requestTokenPopup();
}
if (response && response.status == 200) {
xhr.send(
JSON.stringify({
firstname: "something",
lastname: "something",
accessToken: "something"
})
);
location.href = hostname;
} else {
console.log("Unable to acquire token");
}
}
function redirectLogin() {
console.log("redirect called");
if (!msalInstance.getAccount()) {
return msalInstance
.loginRedirect(loginRequest)
.then(response => {
console.log(response);
return response;
})
.catch(err => {
console.log("Authentication error: ", err);
});
}
if (msalInstance.getAccount()) {
redirectToDashboard();
}
}
async function requestTokenSilent() {
console.log("requestTokenSilent");
if (msalInstance.getAccount()) {
return msalInstance
.acquireTokenSilent(tokenRequest)
.then(response => {
localStorage.setItem(TOKEN_ID, response.accessToken);
console.log("response reached: ", response);
resolve(response);
})
.catch(err => {
if (err.name === "InteractionRequiredAuthError") {
alert("Authentication failed try again");
}
});
}
}
async function requestTokenPopup() {
console.log("requestTokenPopup");
if (msalInstance.getAccount()) {
return msalInstance
.acquireTokenPopup(tokenRequest)
.then(response => {
localStorage.setItem(TOKEN_ID, response.accessToken);
return response;
})
.catch(err => {
console.log(err);
if (err.name === "InteractionRequiredAuthError") {
alert("Authentication failed try again");
}
});
}
}
Upvotes: 1
Views: 2291
Reputation: 175
Not sure if it helps, but I stumbled upon this post when looking for a solution and I ended up with this working typescript implementation:
export async function renewAccessToken(refreshTokenFromUser: string): Promise<string> {
const data =
"grant_type=refresh_token" +
"&refresh_token=" +
refreshTokenFromUser +
"&client_id=" +
config.creds.clientID +
"&client_secret=" +
encodeURIComponent(config.creds.clientSecret) +
"&scope=" +
config.creds.scope;
const response = await axios({
method: "POST",
url: config.creds.tokenEndpoint,
headers: { "Content-Type": "application/x-www-form-urlencoded" },
data: data,
});
let refreshedToken;
if (response && response.data) {
const tokenAnswer = response.data;
if (tokenAnswer.hasOwnProperty("access_token")) {
refreshedToken = tokenAnswer.access_token;
} else {
console.warn("error in refresh token");
}
} else {
console.warn("error in refresh token");
refreshedToken = null;
}
return refreshedToken;
}
with the attributes in the config file being:
exports.creds = {
tokenEndpoint: "https://login.microsoftonline.com/common/oauth2/token",
// The client ID of your app in AzureActiveDirectory (required)
clientID: "insert your ID",
clientSecret: "insert your secret",
scope: ["profile", "offline_access", "https://graph.microsoft.com/calendars.read", "https://graph.microsoft.com/calendars.read.shared"],
};
Upvotes: 0
Reputation: 17692
MSAL.js does the implicit flow to get access tokens. This flow does not return refresh tokens at all, because refresh tokens don't have a purpose in the implicit flow. Refresh is accomplished with a hidden request. From the link above:
The implicit grant does not provide refresh tokens. Both id_tokens and access_tokens will expire after a short period of time, so your app must be prepared to refresh these tokens periodically. To refresh either type of token, you can perform the same hidden iframe request from above using the prompt=none parameter to control the identity platform's behavior. If you want to receive a new id_token, be sure to use id_token in the response_type and scope=openid, as well as a nonce parameter.
MSAL.js will do this automatically for you if the current access token is expired when you call requestTokenSilent
.
You want a refresh token because your real goal is to get your backend server process access to the Graph. Implicit won't work for that. Instead, you need to use the on-behalf-of flow. You can read all the specifics there, but the high-level summary is:
Authorization
header in a call to your backend.Upvotes: 1
Reputation: 601
I was using v1 version of msal. V1 version does not support refresh tokens any more. I was told msal v2 supports refresh tokens, but it it is currently in beta.
Upvotes: 2
Reputation: 637
First, I am not good at JavaScript, but it seems to me that you do not take out the refresh_token. Refresh_tokens are long-lived, however when requesting a new one you must update the refresh_token when you recieve a new access_token, because it the authorization server MAY issue a new one.
I found a lot of help in this article when I struggeled with this. Microsoft docs v2-oauth2-auth-code-flow
Upvotes: 0