Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jackawatts/5ec20bed7aa3bdf10615e945e79097df to your computer and use it in GitHub Desktop.
Save jackawatts/5ec20bed7aa3bdf10615e945e79097df to your computer and use it in GitHub Desktop.
AAD v 2.0 auth protected Web API with REACT SPA end-to-end

A WIP (work in progreess), collected from a variety of sources...

Securing the Web API

Create an App registration

eg. my-app-api

We will refer to it as {nameOfAppRegistration} from this point forward. (This can be pretty much anything but adding the suffix -api may help distinguish it from the spa registration at a later stage.)

  1. no redirect is required as there is no interactive user sign-in
  2. Expose an API:
    • use the default resource URI api://{clientId} (this is the default when Azure AD v2.0)
    • Add a scope (scope name, the display name and description can vary, these are just some reasonable default values that can be used)
      • Scope name: access_as_user
      • Who can consent?: Admins and users
      • Admin consent display name: {nameOfAppRegistration}
      • Admin consent description: Allows the app to access {nameOfAppRegistration} as the signed-in user.
      • User consent display name: Access {nameOfAppRegistration}
      • User consent description: Allow the application to access {nameOfAppRegistration} on your behalf.
  3. Manifest
    • set accessTokenAcceptedVersion to 2
  4. Copy the Application (Client) Id, we will refer to it as {apiClientId} from this point forward.

Configure the code

  1. In Startup.cs

    • ConfigureServices
    // The following 2 options are equivalent
    services.AddMicrosoftIdentityWebApiAuthentication(Configuration, "AzureAd");
    // or
    services.AddAuthentication(AzureADDefaults.JwtBearerAuthenticationScheme)
        .AddMicrosoftIdentityWebApi(Configuration, "AzureAd");
    • Configure
    // Ordering is important here, UseCors **must** appear before UseAuthentication and UseAuthorization
    app.UseCors();
    app.UseAuthentication();
    app.UseAuthorization();
    
  2. Configure appsettings.json

    Audience is only necessary when not using the default Resource Uri generated in the step above

    "AzureAd": {
        "Instance": "https://login.microsoftonline.com/",
        "Domain": "qualified.domain.name", // the directory name
        "ClientId": "{apiClientId}",
        "TenantId": "Enter_the_Tenant_Info_Here" // Tenant ID
    },
    

Securing the Client

Create an App registration

eg. my-app-spa

(This can be pretty much anything but adding the suffix -spa may help distinguish it from the api registration above.)

  1. Authentication: Add a platform, Single-page application, enter a Redirect URI
  2. API permissions: Add a permission, My APIs, select the API app registration and select the scope created above eg. access_as_user
  3. Copy the Application (Client) Id, we will refer to it as {spaClientId} from this point forward.

Setup the project

  1. Install react-msal

    npm install react react-dom
    npm install @azure/msal-react @azure/msal-browser
    
  2. Configure the environment (envirtonment.ts)

    export const environment = {
      authConfig: {
        apiClientId: "{apiClientId}",
        spaClientId: "{spaClientId}",
        tenantId: "{tenantId}"
      },
      apiConfig: {
        baseUrl: "{apiBaseUrl}"
      }
    };
  3. Add an authConfig.ts

    import { environment } from './environments/environment';
    import { AccountInfo, AuthenticationResult, Configuration, EventType, EventMessage, PublicClientApplication, RedirectRequest, SilentRequest } from "@azure/msal-browser";
    
    const msalConfig: Configuration = {
      "auth": {
        "authority": `https://login.microsoftonline.com/${environment.authConfig.tenantId}/`,
        "clientId": environment.authConfig.spaClientId,
        "postLogoutRedirectUri": "/",
        "redirectUri": "/"
      },
      "cache": {
        "cacheLocation": "localStorage",
        "storeAuthStateInCookie": true /* setting this to false breaks in IE 11 and Edge */
      }
    }
    export const pca = new PublicClientApplication(msalConfig);
    
    const accounts = pca.getAllAccounts();
    if (accounts.length > 0) {
      pca.setActiveAccount(accounts[0]);
    }
    
    pca.addEventCallback((event: EventMessage) => {
        if (event.eventType === EventType.LOGIN_SUCCESS && event.payload) {
            const payload = event.payload as AuthenticationResult;
            const account = payload.account;
            pca.setActiveAccount(account);
        }
    });
    
    export const loginRequest: RedirectRequest = {
      scopes: [`api://${environment.authConfig.apiClientId}/.default`] /* request all supported claims */
    }

    Optional: Add a rudimentary msalApiFetch, call this to have the bearer added by default or refreshed as needed for API calls

    const baseUrl = environment.apiConfig.baseUrl;
    const authService = {
      GetToken(): Promise<AuthenticationResult> {
        let tokenRequest: SilentRequest = {
          account: pca.getActiveAccount() as AccountInfo,
          scopes: [`api://${environment.authConfig.apiClientId}/.default`]
        }
        return pca.acquireTokenSilent(tokenRequest);
      }
    }
    
    export const msalApiFetch = async (relativeUrl: string, options: any = {}) => {
      if (options && options.method !== 'post')
          options.headers = { ...options.headers, ...{ 'pragma': 'no-cache', 'cache-control': 'no-cache' } }
      if (options && options.method !== 'get')
          options.headers = { ...options.headers, 'Content-Type': 'application/json' }
      const url = `${baseUrl}${relativeUrl}`;
      return await authService.GetToken()
          .then((r: AuthenticationResult) => { return { ...options, headers: { ...options.headers, 'Authorization': `Bearer ${r.accessToken}` }}})
          .then((opt: any) => fetch(url, opt))
          .then((response: Response) => {
              if (!response.ok) {
                  response.json().then((problem: any) => {
                      console.error(JSON.stringify(problem))
                  }, (error: Error) => {
                      console.error(error)
                  })
              }
              return response;
          });
    }
  4. Modify index.tsx

    import { MsalProvider } from "@azure/msal-react";
    import { pca } from "./authConfig";
    
    ReactDOM.render(
       <MsalProvider instance={pca}>
          <App />
       </MsalProvider>,
      document.getElementById('root'),
    );
  5. Modify App.tsx

    import { MsalAuthenticationTemplate } from "@azure/msal-react";
    import { InteractionType } from "@azure/msal-browser";
    import { loginRequest } from "./authConfig"
    
    <MsalAuthenticationTemplate
       authenticationRequest={loginRequest} /* ensure we request all scopes outlined above */
       interactionType={InteractionType.Redirect} /* avoids blocked pop-ups */
    >
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment