kristin1111
kristin1111

Reputation: 61

How to use MSAL to authenticate APIs from front end without user log in

I have an application that has a user login, but using the app doesn't require a log in. My app is registered in Azure. For the user account, we're using Microsoft Entra and MSAL react package to handle the user login/logout and account.

We need to authenticate the app itself and provide a Bearer token with each API call from our front end to our .NET backend.

I tried using Auth code flow to accomplish this, but to get the auth code to include in the /authorize endpoint that gets the access token, the code is sent as a parameter in the redirect URI, requiring user interaction to extract it and make the call to the second endpoint for the actual token to secure API requests.

Does MSAL support what I'm trying to accomplish? Do I need to be using client credentials (auth flow recommended for Daemon apps), or is this something the back end should be implementing?

import { PublicClientApplication } from '@azure/msal-browser';

export const msalConfig = {

  auth: {
    authority: "https://login.microsoftonline.com/TenantId",
    clientId: "ClientId",
    postLogoutRedirectUri: 'http://localhost:3000',
    redirectUri: 'http://localhost:3000',
    validateAuthority: true,
    navigateToLoginRequestUrl: true,
  },
  cache:
  {
    cacheLocation: 'sessionStorage',
    storeAuthStateInCookie: true,
  }
}

export const msalInstance = new PublicClientApplication(msalConfig);


nterface AuthProviderProps {
    children : React.ReactNode
}

 function AuthProvider ({ children } : AuthProviderProps) {

    return <MsalProvider instance={msalInstance}>
        {children}
    </MsalProvider>
}

export default AuthProvider;

// layout.tsx

  <AuthProvider>
        <ThemeProvider theme={theme}>
          <Providers>
            <Header />
            <main className="lg:!pb-10 sm:pb-2 lg:pb-5 sm:pt-[4rem] lg:pt-0 sm:min-h-[calc(100%-6%)] lg:min-h-auto">
              {children}
              <MuiXLicense />
            </main>
            <Footer />
            <CustomToast />
            <Loader />
          </Providers>
        </ThemeProvider>
        </AuthProvider>

// /api/secure-api
export async function POST() {
  try {
    const params = new URLSearchParams({
      client_id: 'ClientId',
      scope: 'api://TenantId/.default',
      response_type: "code",
      client_secret: 'ClientSecret',
      code_challenge_method: 'S256',
      code_challenge: 'Nj9Youq443xUOCe_HsmBXJy5dKC8YsqlUdn1sga3CR0',
      redirect_uri: "http://localhost:3000",
      response_mode: "query",
      state: '12345'
    })
    const authorize = await fetch(`https://login.microsoftonline.com/TenantId/oauth2/v2.0/authorize?${params.toString()}`, {
      method: 'GET',
      headers: {
        'Content-Type': 'application/json',
        'Access-Control-Allow-Origin': '*',
        'Access-Control-Allow-Headers': 'Origin, X-Requested-With, Content-Type, Accept',
        'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE',
      },
    });
    console.log('authorize: ,', authorize, authorize.body);
    const data = await authorize.json();
    const code = data.data.refresh_token;
    if (!authorize.ok) {
      return NextResponse.json(data, { status: authorize.status });
    }
    const accessTokenParams = new URLSearchParams({
      grant_type: "client_credentials",
      redirect_uri: "https://dev-travelinsured.azurewebsites.net",
      client_id: 'ClientID',
      scope: 'api://Instance/.default',
      client_secret: 'ClientSecret',
      code_verifier: 'Nj9Youq443xUOCe_HsmBXJy5dKC8YsqlUdn1sga3CR0',
      code: code,
    })
    const response = await fetch(
      `https://login.microsoftonline.com/tenantId/oauth2/v2.0/token?${accessTokenParams.toString()}`, {
        method: 'GET',
        headers: {
          'Content-Type': 'application/json',
          'Access-Control-Allow-Origin': '*',
          'Access-Control-Allow-Headers': 'Origin, X-Requested-With, Content-Type, Accept',
          'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE',
        },
      });
    console.log(response);

    const accessToken = await response.json();
    if (!response.ok) {
      return NextResponse.json(accessToken, { status: response.status });
    }

    return NextResponse.json(accessToken, { status: 200 });
  } catch (error) {
    console.error("Error in POST /api/secure-api", error);
    return NextResponse.json({ error: error }, { status: 500 });
  }
}


//generateSecureApiToken

export const generateSecureApiToken = createApi({
  reducerPath: "generateSecureApiToken",
  baseQuery: fetchBaseQuery({ baseUrl: "/api/secure-api" }),
  endpoints: (builder) => ({
    getAccessToken: builder.query<any, void>({
      query: () => ({
        url: '',
        method: "POST",
      }),
      async onQueryStarted(_, { dispatch, queryFulfilled }) {
        try {
          const { data } = await queryFulfilled;
          if (data && data.access_token) {
            const tokenData = {
              accessToken: data.access_token,
              expiresIn: data.expires_in,
            };

            localStorage.setItem("apiAccessToken", data.access_token);
            localStorage.setItem("apiExpiresIn", data.expires_in.toString());
            console.log('tokenData: ,', tokenData);
            dispatch(setAccessToken(tokenData));
          } else {
            console.error("No access token found in the response:", data);
          }
        } catch (error) {
          console.error("Failed to fetch access token:", error);
        }
      },
    }),
  }),
});

const baseQuery = fetchBaseQuery({
  baseUrl: "/api/secure-api",
  prepareHeaders: (headers, { getState }) => {
    const token = (getState() as RootState).apiAuth.accessToken;
    if (token) {
      headers.set('Authorization', `Bearer ${token}`);
    }
    return headers;
  },
});

export const baseQueryWithReauth: typeof baseQuery = async (args, api, extraOptions) => {
  let result = await baseQuery(args, api, extraOptions);

  if (result.error && result.error.status === 401) {
    // Trigger the token refresh
    const refreshResult = await api.dispatch(
      generateSecureApiToken.endpoints.getAccessToken.initiate()
    ).unwrap();

    if (refreshResult && refreshResult.access_token) {
      // Store the new token in Redux and LocalStorage
      api.dispatch(setAccessToken({ accessToken: refreshResult.access_token, expiresIn: refreshResult.expires_in }));
      localStorage.setItem('apiAccessToken', refreshResult.access_token);
      localStorage.setItem('apiExpiresIn', refreshResult.expires_in.toString());

      // Retry the original query with the new token
      result = await baseQuery(args, api, extraOptions);
    } else {
      // Handle token refresh failure (optional)
      console.error("Failed to refresh access token.");
    }
  }

  return result;
};

// API Permissions

Upvotes: 0

Views: 260

Answers (1)

Rukmini
Rukmini

Reputation: 16064

Note that: If you want nonuser interaction to authenticate APIs, then you need to make use of Client credential flow only.

  • You cannot directly use the Client Credentials Flow in the frontend, because that requires exposing sensitive credentials like the client secretwhich is not safe in client-side code.
  • Instead, you could use On-Behalf-Of Flow where the frontend app requests a token on behalf of itself (using only client credentials) but doesn’t involve user login directly.

If you want nonuser interaction only, then try the below:

In Backend, securely authenticate using the Client Credentials Flow by passing the client ID and client secret to request an access token from Azure AD:

For sample:

const fetch = require('node-fetch');

async function getAccessToken() {
  const clientId = 'Your_Client_ID';
  const clientSecret = 'Your_Client_Secret';
  const tenantId = 'Your_Tenant_ID';
  const scope = 'api://Your_API_ID/.default';  
  const params = new URLSearchParams();
  params.append('grant_type', 'client_credentials');
  params.append('client_id', clientId);
  params.append('client_secret', clientSecret);
  params.append('scope', scope);

  const response = await fetch(`https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded',
    },
    body: params.toString(),
  });

  const data = await response.json();

  if (!response.ok) {
    throw new Error(`Error fetching token: ${data.error_description}`);
  }

  return data.access_token;
}

app.post('/api/get-access-token', async (req, res) => {
  try {
    const accessToken = await getAccessToken();
    res.json({ access_token: accessToken });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

And in the frontend, send a request to your backend API to obtain an access token:

async function getAccessTokenFromBackend() {
  const response = await fetch('/api/get-access-token', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
  });

  if (response.ok) {
    const data = await response.json();
    return data.access_token;
  } else {
    throw new Error('Failed to get access token');
  }
}

And call the API in the Frontend:

async function callApiWithAccessToken() {
  const accessToken = await getAccessTokenFromBackend();  
  const response = await fetch('/api/secure-endpoint', {
    method: 'GET',
    headers: {
      'Authorization': `Bearer ${accessToken}`,  
    },
  });

  const data = await response.json();
  console.log('Data from API:', data);
}

Hence, To authenticate your app without user login using MSAL, the frontend requests an access token from the backend, which securely retrieves it using the client credentials flow, and the frontend then uses the token for API requests.

Reference:

Authentication flow support in the Microsoft Authentication Library (MSAL) - Microsoft identity platform | Microsoft

Upvotes: 0

Related Questions