Sending requests from spa to .netcore web api using adaljs always returns 401

374 views Asked by At

I have a spa that is sending a get request to a web api.

When I dont have the authorize attribute, I am able to get the values (of course!). Adding the authorize attribute always returns a 401 response.

After scratching my head on this for 2 weeks straight, I feel like only SO gods can help.

I have the following questions:

  1. What am I doing wrong?
  2. Is there a better way to do this?
  3. How can I log the incoming token on the server side? (Just so I can validate it at jwt.io)

Assuming my keys, tenant, client(id) etc are properly set up,

My code on spa is like this:

'use strict';
angular.module('todoApp')
    .controller('homeCtrl', ['$scope', '$http', 'adalAuthenticationService', '$location', function ($scope, $http, adalService, $location) {
        $scope.apiData = [];
        $scope.login = function () {
            adalService.login().then(function () {
                console.log('yay');
            });
        };
        $scope.logout = function () {
            adalService.logOut();
        };
        $scope.isActive = function (viewLocation) {
            return viewLocation === $location.path();
        };

        $scope.getData = function () {
            // #1: Set up ADAL
            var authContext = new AuthenticationContext({
                clientId: 'myclientid',
                postLogoutRedirectUri: window.location
            });

            var user = authContext.getCachedUser();
            if (user) {
                console.log(user);
                console.log('Signed in as: ' + user.userName);
            } else {
                console.log('Not signed in');
            }

            var tokenStored;
            authContext.acquireToken(
                'https://graph.windows.net',
                function (error, token) {

                    // TODO: Handle error obtaining access token
                    if (error || !token) {
                        console.log('Error no token');
                        return;
                    }
                    console.log("token is:" + token);
                    tokenStored = token;

                    $http.get('https://localhost:44301/api/values', {
                        headers: { 'Authorization': 'Bearer ' + tokenStored, }
                    }).then(function (response) {
                        $scope.apiData = response.data;
                        console.log(response);
                        alert('Data recieved');
                    });
                });

        };
    }]);

My Api Startup.cs looks like this:

 // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
        {
            //ToDo: Implement Logger Factory
            loggerFactory.AddConsole(Configuration.GetSection("Logging"));
            loggerFactory.AddDebug();



            // Shows UseCors with CorsPolicyBuilder.
            // global policy - assign here or on each controller
            app.UseCors("CorsPolicy");



            app.UseJwtBearerAuthentication(new JwtBearerOptions
            {
                AutomaticAuthenticate = true,
                AutomaticChallenge = true,
            //    TokenValidationParameters = tokenValidationParameters
            });

            app.UseMvc();

        }

My controller method looks like this

[Route("api/values")]
    [Authorize]
    [EnableCors("CorsPolicy")]
    public class ValuesController : Controller
    {
 // GET api/values
        [HttpGet]
        public IActionResult Get()
        {
            if (!HttpContext.User.Identity.IsAuthenticated)
            {
                var results = _interconnectCodesRepository.GetCodes();
                return Ok(results);
            }
            else
            {
                return BadRequest();
            }
        }
}
}

Any suggestions or hints will be much appreciated.

Thanks

1

There are 1 answers

10
Fei Xue On BEST ANSWER

To get the token from .net core web API project, we can add AuthenticationFailed event like below:

app.UseJwtBearerAuthentication(new JwtBearerOptions
{
    AutomaticAuthenticate = true,
    AutomaticChallenge = true,
    Authority = String.Format(Configuration["AzureAd:AadInstance"], Configuration["AzureAD:Tenant"]),
    Audience = Configuration["AzureAd:Audience"],
    Events = new JwtBearerEvents
    {
        OnAuthenticationFailed= AuthenticationFailed
    }
});

private Task AuthenticationFailed(AuthenticationFailedContext authenticationFailedContext)
{         
    Debug.WriteLine(authenticationFailedContext.Request.Headers["authorization"]);
    return Task.FromResult(0);
}

The token you were acquiring using the resrouce https://graph.windows.net in the code is for the Azure Graph REST instead of your API. There is no need to acquire the token manually in the client for the SPA application. The ADAL library will acquire and append the token based on the resource automatically. We just only need to init which endpoints we want to request. Here is the js code for your reference:

var myApp = angular.module('myApp', ['AdalAngular']).config(['$httpProvider', 'adalAuthenticationServiceProvider', function ($httpProvider, adalProvider) {

    //{Array} endpoints - Collection of {Endpoint-ResourceId} used for automatically attaching tokens in webApi calls.
    var endpoints = {
        "https://localhost:44327/": "https://adfei.onmicrosoft.com/ToGoAPI",
    };

    adalProvider.init(
    {
        instance: 'https://login.microsoftonline.com/',
        tenant: 'adfei.onmicrosoft.com',
        clientId: 'e2354bba-e915-4cb8-a48d-bcda101b8603',
        extraQueryParameter: 'nux=1',
        endpoints: endpoints,
    },
    $httpProvider
    );
}])

myApp.controller('homeCtrl', ['$scope', '$http', 'adalAuthenticationService', '$location', 'toGoListSvc', function ($scope, $http, adalService, $location, toGoListSvc) {
    $scope.double = function (value) { return value * 2; };

    $scope.login = function () {
        adalService.login();
    };
    $scope.logout = function () {
        adalService.logOut();
    };

    $scope.getData = function () {
        $http.defaults.useXDomain = true;
        delete $http.defaults.headers.common['X-Requested-With'];
        $http.get('https://localhost:44327/api/ToGoList').success(function (results) {
            console.log(results)
            $scope.toGoList = results;
        });
    }
}]);

For the web API side, we need to specify the the Authority and Audience or other parameters as you wanted( refer the first piece of code).

On the Azure side, we need to register two web applications. One presents the client and the other presents the resource protected by Azure AD. For example, in my test scenario, I registered ToDoSPA and ToGoAPI and grant the permission as figure below: enter image description here

And to make the ToDoSPA application integrate with Azure AD with implicit flow for the SPA app, we also need to modify its manifest to set the oauth2AllowImplicitFlow to true.

In addition, here are some helpful links about protecting the web API with Azure AD:

https://github.com/Azure-Samples/active-directory-dotnet-webapp-webapi-openidconnect-aspnetcore

https://github.com/Azure-Samples/active-directory-angularjs-singlepageapp-dotnet-webapi

https://github.com/AzureAD/azure-activedirectory-library-for-js

Update( custom AudienceValidator)

// Configure the app to use Jwt Bearer Authentication
app.UseJwtBearerAuthentication(new JwtBearerOptions
{
    TokenValidationParameters=new Microsoft.IdentityModel.Tokens.TokenValidationParameters()
    {
        AudienceValidator =  (audiences, securityToken, validationParameters) =>
        {
            string[] allowedAudiences = { "https://adfei.onmicrosoft.com/TodoListService", "https://graph.windows.net" };
            return allowedAudiences.Contains<string>(audiences.First<string>());
        },
    },
    AutomaticAuthenticate = true,
    AutomaticChallenge = true,
    Authority = String.Format(Configuration["AzureAd:AadInstance"], Configuration["AzureAD:Tenant"]),
    //Audience = Configuration["AzureAd:Audience"],              
});