从 React 调用受 Azure AD 保护的 API

Posted

技术标签:

【中文标题】从 React 调用受 Azure AD 保护的 API【英文标题】:Call Azure AD protected API from React 【发布时间】:2020-02-21 13:45:37 【问题描述】:

我有一个需要访问托管在 Azure 中的 ASP.NET Core API 的反应网络应用程序。这两个应用程序都受 Azure AD 保护。我已在 Azure AD 应用注册中单独注册了这些应用。我已经公开了给我这个范围的 API:api://api_azure_client_id/user_impersonation - 这个范围允许用户和管理员都同意。我单独注册了 react 应用,并授予它访问暴露的 API 范围的权限。

我正在使用 MSAL 获取令牌。我可以成功签名,但我不知道如何获取 access_token 来调用 API。我正在关注the azure ad samples for react here。使用钩子的决定受到this pull request on the sample.

的启发

MSAL 配置:

export const GRAPH_SCOPES = 
 OPENID: "openid",
 PROFILE: "profile",
 USER_READ: "User.Read",
 API_CALL: "api://api_azure_client_id/user_impersonation"
;

       export const GRAPH_ENDPOINTS = 
        ME: "https://graph.microsoft.com/v1.0/me"
      ;

   export const GRAPH_REQUESTS = 
     LOGIN: 
      scopes: [
        GRAPH_SCOPES.OPENID,
        GRAPH_SCOPES.PROFILE,
        GRAPH_SCOPES.USER_READ,
        GRAPH_SCOPES.API_CALL
      ]
    
  ;

    export const msalApp = new UserAgentApplication(
      auth: 
        clientId: "react_app_azure_client_id",
        authority: "https://login.microsoftonline.com/common",
        validateAuthority: true,
        postLogoutRedirectUri: "http://localhost:3000",
        navigateToLoginRequestUrl: false
      ,
      cache: 
        cacheLocation: "localStorage",
        storeAuthStateInCookie: isIE()
      
    );

useAuthProvider 钩子:

    import  useState, useEffect  from "react";
    import  msalApp, requiresInteraction, fetchMsGraph, isIE, GRAPH_ENDPOINTS, GRAPH_REQUESTS
            from "./../utils/auth-util";

     // If you support IE, our recommendation is that you sign-in using Redirect APIs
    const useRedirectFlow = isIE();
    // const useRedirectFlow = true;

    export default function useAuthProvider() 
     const [account, setAccount] = useState(null);
     const [accessToken, setAccessToken] = useState(null);
     const [error, setError] = useState(null);
     const [graphprofile, setGraphProfile] = useState(null);

      useEffect(() => 
        async function doAuth() 
         msalApp.handleRedirectCallback(error => 
           if (error) 
      const errorMessage = error.errorMessage
        ? error.errorMessage
        : "Unable to acquire access token.";
      // setState works as long as navigateToLoginRequestUrl: false
      setError(errorMessage);
    
  );

  const account = msalApp.getAccount();

  setAccount(account);

  if (account) 
    const tokenResponse = await acquireToken(
      GRAPH_REQUESTS.LOGIN,
      useRedirectFlow
    );

    if (tokenResponse) 
      setAccessToken(tokenResponse.accessToken);
      const graphProfile = await fetchMsGraph(
        GRAPH_ENDPOINTS.ME,
        tokenResponse.accessToken
      ).catch(() => 
        setError("Unable to fetch Graph profile.");
      );

      if (graphProfile) 
        setGraphProfile(graphProfile);
      
    
  


   doAuth();
   , []);

     async function acquireToken(request, redirect) 
       return msalApp.acquireTokenSilent(request).catch(error => 
         // Call acquireTokenPopup (popup window) in case of acquireTokenSilent failure
         // due to consent or interaction required ONLY
         if (requiresInteraction(error.errorCode)) 
         return redirect ? msalApp.acquireTokenRedirect(request) : 
                msalApp.acquireTokenPopup(request);
          else 
          console.error("Non-interactive error:", error.errorCode);
        
      );
    

      async function onSignIn(redirect) 
       if (redirect) 
         return msalApp.loginRedirect(GRAPH_REQUESTS.LOGIN);
       

const loginResponse = await msalApp
  .loginPopup(GRAPH_REQUESTS.LOGIN)
  .catch(error => 
    setError(error.message);
  );

if (loginResponse) 
  setAccount(loginResponse.account);
  setError(null);

  const tokenResponse = await acquireToken(GRAPH_REQUESTS.LOGIN).catch(
    error => 
      setError(error.message);
    
  );

  if (tokenResponse) 
    setAccessToken(tokenResponse.accessToken);
    const graphProfile = await fetchMsGraph(
      GRAPH_ENDPOINTS.ME,
      tokenResponse.accessToken
    ).catch(() => 
      setError("Unable to fetch Graph profile.");
    );

        if (graphProfile) 
           setGraphProfile(graphProfile);
          
        
      
    

     function onSignOut() 
        return msalApp.logout();
     

     return 
       account,
       accessToken,
       error,
       graphProfile,
       onSignIn: () => onSignIn(useRedirectFlow),
       onSignOut: () => onSignOut()
     ;
    

我创建了一个 httpService,它只是 axios 的一个包装器。我正在使用此服务调用api,httpService的配置如下,是我需要访问令牌的地方。我在下面尝试过,但我得到的钩子只能在函数体内调用。

import axios from "axios";
import useAuthProvider from "./useAuthProvider";

   axios.interceptors.request.use(
     config => 
     const  accessToken  = useAuthProvider();

   if (accessToken) 
      config.headers["Authorization"] = "Bearer " + accessToken;
           
      return config;
 ,
   error => 
     Promise.reject(error);
   
  );

   export default 
      get: axios.get,
      post: axios.post,
      put: axios.put,
      delete: axios.delete
   ;

如果用户未登录,我想显示一个带有登录按钮的组件,该按钮会提示用户输入凭据。我在 App 组件中执行此操作,并且运行良好。

App.js

   import React from "react";
   import useAuthProvider from "./services/useAuthProvider";
   import NavBar from "./components/navigation/navBar";
   import "./App.css";
   import Welcome from "./components/welcome";

   function App() 
    const  account, accessToken, onSignIn  = useAuthProvider();

    if (account === null)
      return <Welcome authButtonMethod=onSignIn.bind(this) />;
    return <NavBar />;
  

    export default App;

我可以在本地存储中使用令牌,然后在 httpService 中从那里获取它。根据我上面的代码,我在哪里可以访问令牌并将其保存在本地存储中?

【问题讨论】:

【参考方案1】:

我可以成功签名,但我不知道如何获取 access_token 调用 API。

由于您可以成功签名,您已经在本地存储中缓存了一个令牌(cacheLocation:“localStorage”)。如果令牌还没有过期,可以静默获取。

var accessToken = await this.userAgentApplication.acquireTokenSilent(
        scopes: config.scopes
      );

如果您想访问自己的 webapi,范围应该类似于 api://api_azure_client_id/user_impersonation

【讨论】:

我在控制台中看到:令牌不在缓存范围内:api://api_azure_client_id/user_impersonation。然后它似乎卡在 Renew token for scope api://api_azure_client_id/user_impersonation 正在进行中。注册呼叫球。 @Munhu 你不是配置了同样的登录使用范围吗? 是的,我做到了。我的配置如上。【参考方案2】:

在Sign方法中,可以通过添加window.localStorage.setItem('accessToken',tokenResponse.accessToken);将访问令牌保存到本地存储中

async function onSignIn(redirect) 
       if (redirect) 
         return msalApp.loginRedirect(GRAPH_REQUESTS.LOGIN);
       

const loginResponse = await msalApp
  .loginPopup(GRAPH_REQUESTS.LOGIN)
  .catch(error => 
    setError(error.message);
  );

if (loginResponse) 
  setAccount(loginResponse.account);
  setError(null);

  const tokenResponse = await acquireToken(GRAPH_REQUESTS.LOGIN).catch(
    error => 
      setError(error.message);
    
  );

  if (tokenResponse) 
    setAccessToken(tokenResponse.accessToken);
    window.localStorage.setItem('accessToken',tokenResponse.accessToken);
    const graphProfile = await fetchMsGraph(
      GRAPH_ENDPOINTS.ME,
      tokenResponse.accessToken
    ).catch(() => 
      setError("Unable to fetch Graph profile.");
    );

        if (graphProfile) 
           setGraphProfile(graphProfile);
          
        
      
    

在httpService中从本地存储中获取token添加window.localStorage.getItem('accessToken');然后传递给axios header

【讨论】:

以上是关于从 React 调用受 Azure AD 保护的 API的主要内容,如果未能解决你的问题,请参考以下文章

从 PowerApp(或逻辑应用)调用受 AAD 保护的 Azure 函数

使用 Azure AD 和本地帐户保护 API

iframe 中的 Azure AD MSAL

如何使用 Azure AD B2C 保护 Blazor Wasm 应用访问的 Azure 函数?

Azure 应用服务能够使用托管标识(无应用角色)调用另一个受 AAD 保护的应用服务,为啥?

如何在 Azure Ad 中实现代流?