Fernando
Fernando

Reputation: 646

msal-angualr returns 401 when accessing remote API

When working on a local copy of our Angular application we need to access the api on the dev server. However since upgrading to using msal-angular this does not work anymore, it returns 401 and goes into a redirection loop.

I'm not sure if there is a configuration we are missing to allow this to work. It worked fine when we were using adal but we need V2 tokens on the api now.

export const protectedResourceMap: [string, string[]][] = [
  ['https://graph.microsoft.com/v1.0/me', ['user.read']]
];

...

imports: [
    MsalModule.forRoot({
      clientID: 'Azure-App-Id',
      authority: 'https://login.microsoftonline.com/Azure-Tenant-Id',
      validateAuthority: true,
      redirectUri: window.location.origin,
      navigateToLoginRequestUrl: false,
      cacheLocation: 'localStorage',
      popUp: false,
      protectedResourceMap: protectedResourceMap
    })
],
providers: [
    { provide: HTTP_INTERCEPTORS, useClass: MsalInterceptor, multi: true }
],

... 

The above is my configuration in app.module.ts.

This works fine when I run my angular application from http://localhost:4200 against my asp.net core web api run from visual studio on https://localhost:5600.

It also runs fine when deployed on the server. However if I change the Angular app in dev environment to use the servers api (http://localhost:4200 -> https://www.api.azurewebsite.net), we always get authentication errors as the Angular app does not send the bearer token like it does on the other two cases.

Hopefully this is enough details.

Thank you.

Upvotes: 1

Views: 3623

Answers (2)

Cyclion
Cyclion

Reputation: 774

Yeap, for now I made own implementation of MsalInterceptor.
Vote for wildcard in protectedResourceMap https://github.com/AzureAD/microsoft-authentication-library-for-js/issues/1776 This is my custom code block:

// #region own workaround in order not to put every api endpoint url to settings
if (!scopes && req.url.startsWith(this.settingsService.apiUrl)) {
  scopes = [this.auth.getCurrentConfiguration().auth.clientId];
}
// #endregion

// If there are no scopes set for this request, do nothing.
if (!scopes) {
  return next.handle(req);
}

Upvotes: 0

Fernando
Fernando

Reputation: 646

I discovered the issue with with msal-angular's interceptor. I guess it does the right thing and the scopes are empty when going cross-domain.

We created our own interceptor from the msal-angular code to inject the scope if we are in development.

Thank you.

import { Injectable } from '@angular/core';
import {
    HttpRequest,
    HttpHandler,
    HttpEvent,
    HttpInterceptor, HttpErrorResponse
} from '@angular/common/http';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/fromPromise';
import 'rxjs/add/operator/mergeMap';
import { MsalService, BroadcastService } from '@azure/msal-angular';


@Injectable()
export class CustomMsalInterceptor implements HttpInterceptor {

    constructor(private auth: MsalService, private broadcastService: BroadcastService) { }

    intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {

        if (req.url.indexOf('geocode.arcgis.com') >= 0) {
            return next.handle(req);
        } else {

            let scopes = this.auth.getScopesForEndpoint(req.url);

            this.auth.verbose('Url: ' + req.url + ' maps to scopes: ' + scopes);
            if (scopes === null) {
                if (req.url.indexOf('azurewebsites')) {
                    scopes = ['****  APPLICAITON (CLIENT) ID *****'];
                } else {
                    return next.handle(req);
                }
            }
            const tokenStored = this.auth.getCachedTokenInternal(scopes);
            if (tokenStored && tokenStored.token) {
                req = req.clone({
                    setHeaders: {
                        Authorization: `Bearer ${tokenStored.token}`,
                    }
                });
                return next.handle(req).do(event => { }, err => {
                    if (err instanceof HttpErrorResponse && err.status === 401) {
                        const scopes1 = this.auth.getScopesForEndpoint(req.url);
                        const tokenStored1 = this.auth.getCachedTokenInternal(scopes1);
                        if (tokenStored1 && tokenStored1.token) {
                            this.auth.clearCacheForScope(tokenStored.token);
                        }
                    }
                });
            } else {
                return Observable.fromPromise(this.auth.acquireTokenSilent(scopes).then(token => {
                    const JWT = `Bearer ${token}`;
                    return req.clone({
                        setHeaders: {
                            Authorization: JWT,
                        },
                    });
                })).mergeMap(req1 => next.handle(req1).do(event => { }, err => {
                    if (err instanceof HttpErrorResponse && err.status === 401) {
                        const scopes2 = this.auth.getScopesForEndpoint(req1.url);
                        const tokenStored2 = this.auth.getCachedTokenInternal(scopes2);
                        if (tokenStored2 && tokenStored2.token) {
                            this.auth.clearCacheForScope(tokenStored2.token);
                        }
                    }
                }));
            }
        }
    }
}

Upvotes: 2

Related Questions