如何从 Microsoft Graph 获取刷新令牌

How do I get refresh token from microsoft graph

我正在尝试创建一个 outlook 电子邮件自动化工具。为了让它做一些事情,例如在给定时间为用户发送文件夹中的电子邮件。微软允许第三方api通过auth2代表用户触发微软的api。我能够检索必要的访问令牌,但我需要一个刷新令牌才能代表用户调用代码,而不是强迫他每小时登录一次。

我目前正在使用微软的 javascript 身份验证库来接收令牌。据我所知,刷新令牌似乎必须在令牌请求中具有离线范围。使用下面的代码,我可以使用访问令牌进行响应,但我仍然无法获取访问令牌。

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");
        }
      });
  }
}

首先,我不擅长JavaScript,但在我看来,你没有拿出refresh_token。 Refresh_tokens 是长期存在的,但是在请求新的时,您必须在收到新的 access_token 时更新 refresh_token,因为授权服务器可能会发布一个新的。

当我为此苦恼时,我在这篇文章中找到了很多帮助。 Microsoft docs v2-oauth2-auth-code-flow

我使用的是 v1 版本的 msal。 V1 版本不再支持刷新令牌。有人告诉我 msal v2 支持刷新令牌,但它目前处于测试阶段。

MSAL.js does the implicit flow 获取访问令牌。此流程根本没有 return 刷新令牌,因为刷新令牌在隐式流程中没有用途。刷新是通过隐藏请求完成的。来自上面的link:

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.

如果当前访问令牌在您调用 requestTokenSilent.

时已过期,

MSAL.js 将自动为您执行此操作

您需要一个刷新令牌,因为您的真正目标是让您的后端服务器进程访问 Graph。隐式不会为此工作。相反,您需要使用 on-behalf-of flow。您可以在那里阅读所有细节,但 high-level 摘要是:

  • 您的 JavaScript front-end 通过使用 MSAL.js 的隐式流为您的后端 Web API 获取令牌。它会在 Authorization header 中向您的后端发送此令牌。
  • 您的后端使用 on-behalf-of 流将该令牌交换为访问令牌并为 Graph 刷新令牌。

不确定它是否有帮助,但我在寻找解决方案时偶然发现了这个 post,我最终得到了这个有效的打字稿实现:

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;
}

配置文件中的属性为:

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"],
};