Bad Request (400) when using Web API Token Authentication from Angular JS
P

1

7

I want to establish Web API Token Authentication with Angular JS as client. I am very new to this concept of Token Authentication inside Web API.

I do not want to use ASP.NET Identity default tables to add or authenticate user. I have my own database and a table called "EmployeeAccess" table which contains EmployeeNumber as User Id and Password. I want to authenticate the users against the values in this table and then want to grant token so that they gets authorized for subsequent call. I have used all required OWIN and ASP.NET References to achieve the result. Here is my code of different components:-

Global.asax

public class WebApiApplication : System.Web.HttpApplication
    {
        protected void Application_Start()
        {
           // AreaRegistration.RegisterAllAreas();
            GlobalConfiguration.Configure(WebApiConfig.Register);

        }

        protected void Application_BeginRequest()
        {
            if (HttpContext.Current.Request.HttpMethod == "OPTIONS")
            {
                // Cache the options request.
                HttpContext.Current.Response.AddHeader("Access-Control-Allow-Origin", HttpContext.Current.Request.Headers["Origin"]);
                HttpContext.Current.Response.AddHeader("Access-Control-Allow-Methods", "GET, PUT, DELETE, POST, OPTIONS");
                HttpContext.Current.Response.AddHeader("Access-Control-Allow-Headers", "Content-Type, Accept, Authorization");
                HttpContext.Current.Response.AddHeader("Access-Control-Max-Age", "1728000");
                HttpContext.Current.Response.End();
            }
        }
    }

WebApiConfig.cs

public static class WebApiConfig
    {
        public static void Register(HttpConfiguration config)
        {            
            // Web API routes
            config.MapHttpAttributeRoutes();

            config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/{controller}/{id}",
                defaults: new { id = RouteParameter.Optional }
            );

            config.Formatters.JsonFormatter.SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/html"));
            config.Formatters.Remove(config.Formatters.XmlFormatter);

            var cors = new EnableCorsAttribute("*", "*", "*");
            config.EnableCors(cors);
        }
    }

Startup.cs

[assembly: OwinStartup(typeof(Application.WebAPI.Startup))]

namespace Application.WebAPI
{
    public class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=316888
            app.UseCors(Microsoft.Owin.Cors.CorsOptions.AllowAll);
            var myProvider = new AuthorizationServerProvider();
            OAuthAuthorizationServerOptions options = new OAuthAuthorizationServerOptions
            {
                AllowInsecureHttp = true,
                TokenEndpointPath = new PathString("/Token"),
                AccessTokenExpireTimeSpan = TimeSpan.FromDays(1),
                Provider = myProvider
            };
            app.UseOAuthAuthorizationServer(options);
        }
    }
}

AuthorizationServerProvider.cs

 public class AuthorizationServerProvider : OAuthAuthorizationServerProvider
    {
        public override async Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
        {
            context.Validated(); // 
        }

        public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
        {
            //context.OwinContext.Response.Headers.Add("Access-Control-Allow-Origin", new[] { "*" });
            string userId = context.UserName;
            string password = context.Password;

            EmployeeAccessBLL chkEmpAccessBLL = new EmployeeAccessBLL();
            EmployeeAccessViewModel vmEmployeeAccess = chkEmpAccessBLL.CheckEmployeeAccess(Convert.ToInt32(userId), password);

            if(vmEmployeeAccess != null)
            {
                var identity = new ClaimsIdentity(context.Options.AuthenticationType);
                identity.AddClaim(new Claim("username", vmEmployeeAccess.EmpName));
                context.Validated(identity);
            }
            else
            {
                context.SetError("invalid_grant", "Provided username and password is incorrect");
                return;
            }
        }               
    } 

Login.html

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title></title>
    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.0/angular.min.js"></script>
    <script src="../Scripts/AngularControllers/LoginController.js"></script>
    <script src="../Scripts/AngularServices/ApiCallService.js"></script>
</head>
<body ng-app="appHome">
    <div ng-controller="ctrlLogin">
        <label>Employee Number</label>
        <input type="text" id="txtEmpNumber" ng-model="md_empnumber" />
        <br/>
        <br/>
        <label>Password</label>
        <input type="text" id="txtEmpNumber" ng-model="md_password"  />

        <button id="btnAdd" type="submit" ng-click="Login()">Login</button>
    </div>
</body>
</html>

LoginController.js

var myApp = angular.module('appHome', []);
myApp.controller("ctrlLogin", ['$scope', 'MetadataOrgFactory', '$location', function ($scope, MetadataOrgFactory, $location) {
    $scope.Login = function () {
        var objLogin = {
            'username' : $scope.md_empnumber,
            'password' : $scope.md_password,
            'grant_type' : 'password'
        };

        MetadataOrgFactory.postLoginCall('Token', objLogin, function (dataSuccess) {
            alert("Welcome " + dataSuccess);           
        }, function (dataError) {
        });
    }
}]);

ApiCallService.js

var appService = angular.module('appHome');
appService.factory('MetadataOrgFactory', ['$http', function ($http) {

    var url = 'http://localhost:60544';    
    var dataFactory = {};    
    dataFactory.postLoginCall = function (controllerName, objData, callbackSuccess, callbackError) {

        $http.post(url + '/' + controllerName, objData,{headers:{ 'Content-Type': 'application/x-www-form-urlencoded' }}).then
            (function success(response) {
                alert("Success");
                callbackSuccess(response.data);
            }, function error(response) {
                callbackError(response.status);
            });
    };
    return dataFactory;
}])

When I click on Login button then I am getting below error message:-

POST http://localhost:60544/Token 400 (Bad Request)

When I was debugging WebAPI code then I found that method "GrantResourceOwnerCredentials()" inside "AuthorizationServerProvider.cs" never gets executed. The error message comes before this. Only the method "ValidateClientAuthentication" and "MatchEndpoint" gets executed.

Please help me to run the scenario of Web API Token Authentication successfully. If any code is found redundant then please let me know so that I can remove this.

Plains answered 1/7, 2017 at 4:50 Comment(0)
B
13

Ok, this will be a long answer but hold on to the end:)

Step 1: Remove the Global.asax

The Global.asax is not needed when you run on the Owin pipeline. The Startup.cs is what I would say Owins Global.asax. They basically fills the same purpose so go ahead and remove it.

Step 2: Remove the Cors handling in the WebApiConfig.cs

This code is not needed as you already declare it in the Startup.cs.

app.UseCors(Microsoft.Owin.Cors.CorsOptions.AllowAll);

Your WebApiConfig.cs will then look like this

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {            
        // Web API routes
        config.MapHttpAttributeRoutes();
        config.Routes.MapHttpRoute(
            name: "DefaultApi",
            routeTemplate: "api/{controller}/{id}",
            defaults: new { id = RouteParameter.Optional }
        );
        config.Formatters.JsonFormatter.SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/html"));
        config.Formatters.Remove(config.Formatters.XmlFormatter);
    }
}

Step 3: Add Web Api and bearer token autentication to the Owin pipeline in Startup.cs

Instead of binding the WebApiConfig in the Global.asax you attach it to the pipeline. Also apply the bearer token handling to the pipeline.

Your Startup.cs will then look like this

public class Startup
{
    public void Configuration(IAppBuilder app)
    {
        app.UseCors(Microsoft.Owin.Cors.CorsOptions.AllowAll);
        OAuthAuthorizationServerOptions options = new OAuthAuthorizationServerOptions
        {
            AllowInsecureHttp = true,
            TokenEndpointPath = new PathString("/Token"),
            AccessTokenExpireTimeSpan = TimeSpan.FromDays(1),
            Provider = new AuthorizationServerProvider()
        };
        app.UseOAuthAuthorizationServer(options);
        app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions());

        //Register the web api to the pipeline 
        HttpConfiguration config = new HttpConfiguration();
        WebApiConfig.Register(config);
        app.UseWebApi(config);
    }
}

Step 4: Add headers to the request in the AuthorizationServerProvider.cs

public class AuthorizationServerProvider : OAuthAuthorizationServerProvider
{
    public override async Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
    {
        context.Validated(); 
    }
    public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
    {
        SetContextHeaders(context);
        string userId = context.UserName;
        string password = context.Password;

        EmployeeAccessBLL chkEmpAccessBLL = new EmployeeAccessBLL();
        EmployeeAccessViewModel vmEmployeeAccess = chkEmpAccessBLL.CheckEmployeeAccess(Convert.ToInt32(userId), password);

        if(vmEmployeeAccess != null)
        {
            var identity = new ClaimsIdentity(context.Options.AuthenticationType);
            identity.AddClaim(new Claim("username", vmEmployeeAccess.EmpName));
            context.Validated(identity);
        }
        else
        {
            context.SetError("invalid_grant", "Provided username and password is incorrect");
            return;
        }
    }
    private void SetContextHeaders(IOwinContext context)
    {
        context.Response.Headers.Add("Access-Control-Allow-Origin", new[] { "*" });
        context.Response.Headers.Add("Access-Control-Allow-Methods", new[] { "GET, PUT, DELETE, POST, OPTIONS" });
        context.Response.Headers.Add("Access-Control-Allow-Headers", new[] { "Content-Type, Accept, Authorization" });
        context.Response.Headers.Add("Access-Control-Max-Age", new[] { "1728000" });
    }
} 

Step 5: Make a correct request to the Oauth server

The request to the oauth server need to be of content type x-www-form-urlencoded which basically is a string. I also added promise instead of callbacks which is what the $q does. IMO I think the promise is more clear to read

TIP: don't send the credentials in clear text. You could endecode them to Base64 string with btoa(password) and then decode it in your backend.

angular.module('appHome').factory('MetadataOrgFactory', ['$http', function ($http) {

    var url = 'http://localhost:60544';    
    var dataFactory = {};  
    dataFactory.login = function (userName, password) {
        var deferred = $q.defer();

        $http({
            method: 'POST',
            url: url + '/Token',
            processData: false,
            contentType: 'application/x-www-form-urlencoded',
            data: "grant_type=password&username=" + userName + "&password=" + password,
        }).
            success(function (data) {
                deferred.resolve(data);
            }).
            error(function (message, status) {              
                console.log(message);
                deferred.reject(message, status);
            });

        return deferred.promise;
    };  
    return dataFactory;
}]);

Step 6: Make a login request from your controller

angular.module('appHome', []);
angular.module('appHome').controller("ctrlLogin", ['$scope', 'MetadataOrgFactory', '$location', function ($scope, MetadataOrgFactory, $location) {
    $scope.Login = function () {

        MetadataOrgFactory.postLoginCall($scope.md_empnumber, $scope.md_password).then(
            function (result) {
                //success
            },
                function (error, statusCode) {
                    console.log(error);
                }
            );;
    }
}]);

That's it.

Battlement answered 1/7, 2017 at 7:28 Comment(5)
Thanks Marcus for writing your answer so elaborately which help a novice like me to get the basic. I wish I could give +10 to you but the site does not allow..:-)Plains
@user1843970 Np, Happy to help!! Have a great daySoong
Thank you for the SetContextHeaders method ! the data error message is empty without it. But the correct type for the parameter is : private void SetContextHeaders(OAuthGrantResourceOwnerCredentialsContext context)Malisamalison
@MarcusHöglund I am facing the same Bad Request error, but only when the login is incorrect. When login is correct, all works perfectly (fortunately). I am using knockout to send the login request to /Token url. Any hint about this? why it does not work on invalid login only?Midge
@Midge Ok, do you have any DataAnnotations on your login-model? If yes, then mke sure users can't post from client without fullfilling those requirements. If no, then set breakpoint in the GrantResourceOwnerCredentials and view whats happeningSoong

© 2022 - 2024 — McMap. All rights reserved.