70

I am developing a REST API in ASP.Net Web API. My API will be only accessible via non-browser based clients. I need to implement security for my API so I decided to go with Token based authentication. I have a fair understanding of token based authentication and have read a few tutorials, but they all have some user interface for login. I don't need any UI for login as the login details will be passed by the client through HTTP POST which will be authorized from our database. How can I implement token based authentication in my API? Please note- my API will be accessed in high frequency so I also have to take care of performance. Please let me know if I can explain it any better.

Venu K
  • 50
  • 8
Souvik Ghosh
  • 4,456
  • 13
  • 56
  • 78
  • 1
    someone, somewhere would have to put the username and password in in order to do the initial validation; are you suggesting that anyone who gets a copy of your app will use the same username and password? and if that's the case, are you intending to hard code the username and password values in your code? – Claies Jul 29 '16 at 14:22
  • I can have multiple registered users, so initial login details will be passed by them via HTTP POST. Next is what? – Souvik Ghosh Jul 29 '16 at 14:24
  • that doesn't make any sense. how can you server pass credentials to your client? how is the server supposed to know which client is which? – Claies Jul 29 '16 at 14:24
  • 1
    You will have to programmatically authenticate, passing credentials to your Authentication service in your preferred way that doesn't require your user interface, which will pass you back a token. You can then use this token to make calls as you would usually. – plusheen Jul 29 '16 at 14:25
  • @Claies Sorry for the confusion. The idea is to have the clients pass the login details and my API generate a token. Is this feasible? Please let me know if there is any other approach. – Souvik Ghosh Jul 29 '16 at 14:28
  • so again, how would the clients pass login details if you don't present a login form? – Claies Jul 29 '16 at 14:29
  • @Claies Think about this- Usually they have to login and submit the form. In my case, the clients will post the login details in JSON format. I have created a .Net console app. It posts JSON using HttpClient which hits my controller. Here's a similar link but it doesn't help me- http://stackoverflow.com/questions/19724082/restful-authentication-for-non-browser-consumers – Souvik Ghosh Jul 29 '16 at 14:33
  • right, so it's still not clear what your question is? You asked how to do authentication without any user interface, but now you are describing a user interface in your client? – Claies Jul 29 '16 at 14:40
  • if you have examples that show using a specific UI, but you already have another UI in place in your client, you should just be able to ensure that your UI provides the same details to the API as the one in the examples, and everything else should work identically. – Claies Jul 29 '16 at 14:44

2 Answers2

92

I think there is some confusion about the difference between MVC and Web Api. In short, for MVC you can use a login form and create a session using cookies. For Web Api there is no session. That's why you want to use the token.

You do not need a login form. The Token endpoint is all you need. Like Win described you'll send the credentials to the token endpoint where it is handled.

Here's some client side C# code to get a token:

    //using System;
    //using System.Collections.Generic;
    //using System.Net;
    //using System.Net.Http;
    //string token = GetToken("https://localhost:<port>/", userName, password);

    static string GetToken(string url, string userName, string password) {
        var pairs = new List<KeyValuePair<string, string>>
                    {
                        new KeyValuePair<string, string>( "grant_type", "password" ), 
                        new KeyValuePair<string, string>( "username", userName ), 
                        new KeyValuePair<string, string> ( "Password", password )
                    };
        var content = new FormUrlEncodedContent(pairs);
        ServicePointManager.ServerCertificateValidationCallback += (sender, cert, chain, sslPolicyErrors) => true;
        using (var client = new HttpClient()) {
            var response = client.PostAsync(url + "Token", content).Result;
            return response.Content.ReadAsStringAsync().Result;
        }
    }

In order to use the token add it to the header of the request:

    //using System;
    //using System.Collections.Generic;
    //using System.Net;
    //using System.Net.Http;
    //var result = CallApi("https://localhost:<port>/something", token);

    static string CallApi(string url, string token) {
        ServicePointManager.ServerCertificateValidationCallback += (sender, cert, chain, sslPolicyErrors) => true;
        using (var client = new HttpClient()) {
            if (!string.IsNullOrWhiteSpace(token)) {
                var t = JsonConvert.DeserializeObject<Token>(token);

                client.DefaultRequestHeaders.Clear();
                client.DefaultRequestHeaders.Add("Authorization", "Bearer " + t.access_token);
            }
            var response = client.GetAsync(url).Result;
            return response.Content.ReadAsStringAsync().Result;
        }
    }

Where Token is:

//using Newtonsoft.Json;

class Token
{
    public string access_token { get; set; }
    public string token_type { get; set; }
    public int expires_in { get; set; }
    public string userName { get; set; }
    [JsonProperty(".issued")]
    public string issued { get; set; }
    [JsonProperty(".expires")]
    public string expires { get; set; }
}

Now for the server side:

In Startup.Auth.cs

        var oAuthOptions = new OAuthAuthorizationServerOptions
        {
            TokenEndpointPath = new PathString("/Token"),
            Provider = new ApplicationOAuthProvider("self"),
            AccessTokenExpireTimeSpan = TimeSpan.FromDays(14),
            // https
            AllowInsecureHttp = false
        };
        // Enable the application to use bearer tokens to authenticate users
        app.UseOAuthBearerTokens(oAuthOptions);

And in ApplicationOAuthProvider.cs the code that actually grants or denies access:

//using Microsoft.AspNet.Identity.Owin;
//using Microsoft.Owin.Security;
//using Microsoft.Owin.Security.OAuth;
//using System;
//using System.Collections.Generic;
//using System.Security.Claims;
//using System.Threading.Tasks;

public class ApplicationOAuthProvider : OAuthAuthorizationServerProvider
{
    private readonly string _publicClientId;

    public ApplicationOAuthProvider(string publicClientId)
    {
        if (publicClientId == null)
            throw new ArgumentNullException("publicClientId");

        _publicClientId = publicClientId;
    }

    public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
    {
        var userManager = context.OwinContext.GetUserManager<ApplicationUserManager>();

        var user = await userManager.FindAsync(context.UserName, context.Password);
        if (user == null)
        {
            context.SetError("invalid_grant", "The user name or password is incorrect.");
            return;
        }

        ClaimsIdentity oAuthIdentity = await user.GenerateUserIdentityAsync(userManager);
        var propertyDictionary = new Dictionary<string, string> { { "userName", user.UserName } };
        var properties = new AuthenticationProperties(propertyDictionary);

        AuthenticationTicket ticket = new AuthenticationTicket(oAuthIdentity, properties);
        // Token is validated.
        context.Validated(ticket);
    }

    public override Task TokenEndpoint(OAuthTokenEndpointContext context)
    {
        foreach (KeyValuePair<string, string> property in context.Properties.Dictionary)
        {
            context.AdditionalResponseParameters.Add(property.Key, property.Value);
        }
        return Task.FromResult<object>(null);
    }

    public override Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
    {
        // Resource owner password credentials does not provide a client ID.
        if (context.ClientId == null)
            context.Validated();

        return Task.FromResult<object>(null);
    }

    public override Task ValidateClientRedirectUri(OAuthValidateClientRedirectUriContext context)
    {
        if (context.ClientId == _publicClientId)
        {
            var expectedRootUri = new Uri(context.Request.Uri, "/");

            if (expectedRootUri.AbsoluteUri == context.RedirectUri)
                context.Validated();
        }
        return Task.FromResult<object>(null);
    }

}

As you can see there is no controller involved in retrieving the token. In fact, you can remove all MVC references if you want a Web Api only. I have simplified the server side code to make it more readable. You can add code to upgrade the security.

Make sure you use SSL only. Implement the RequireHttpsAttribute to force this.

You can use the Authorize / AllowAnonymous attributes to secure your Web Api. Additionally you can add filters (like RequireHttpsAttribute) to make your Web Api more secure.

starball
  • 20,030
  • 7
  • 43
  • 238
  • 2
    u said "for MVC you can use a login form and create a session using cookies. For Web Api there is no session" but form auth can be implemented in web api. so client can send the credentials to web api and web api will issue auth cookie to client. for all subsequent call client has to pass auth cookie to web api......i guess this is possible. – Monojit Sarkar Aug 03 '16 at 08:10
  • picking your code as follows. `new KeyValuePair( "grant_type", "password" ), new KeyValuePair( "username", userName ), new KeyValuePair ( "Password", password )` what does mean `grant_type=password ?` what other option we can use with `grant_type instead of password` ? please share knowledge. thanks – Monojit Sarkar Aug 03 '16 at 08:13
  • @MonojitSarkar These values are part of the OAuth 2.0 specifications. In particular https://tools.ietf.org/html/rfc6749. A nice summary of specifications can be found here: http://docs.identityserver.io/en/release/intro/specs.html –  Apr 05 '18 at 20:44
  • but how to check with this token in next process? means we store token into a database or not? after successful login how to maintain token? – Brijesh Mavani Jun 06 '18 at 10:11
  • How can this be configured as a sub site where its parent is using windows authentication – mercu Oct 10 '18 at 13:36
  • This seems like a lot of work just to produce a token for authentication. Argh – Anonymous Oct 19 '18 at 20:11
  • "In Startup.Auth.cs", in gray area, code is written, can I paste it there inside the brackets of class? definitely not. Can you please mention what should be the name of the method XYZ if it matters? This is where I am stick right now. – Lali Apr 20 '19 at 23:57
  • @Lali As [documented](https://learn.microsoft.com/en-us/aspnet/aspnet/overview/owin-and-katana/owin-oauth-20-authorization-server#code-try-1) –  Apr 21 '19 at 06:33
  • You should never set ServicePointManager's validation callback to accept all certificates - bypasses a crucial piece of HTTPS security – Joshua Evensen Jan 17 '20 at 15:04
22

ASP.Net Web API has Authorization Server build-in already. You can see it inside Startup.cs when you create a new ASP.Net Web Application with Web API template.

OAuthOptions = new OAuthAuthorizationServerOptions
{
    TokenEndpointPath = new PathString("/Token"),
    Provider = new ApplicationOAuthProvider(PublicClientId),
    AuthorizeEndpointPath = new PathString("/api/Account/ExternalLogin"),
    AccessTokenExpireTimeSpan = TimeSpan.FromDays(14),
    // In production mode set AllowInsecureHttp = false
    AllowInsecureHttp = true
};

All you have to do is to post URL encoded username and password inside query string.

/Token/userName=johndoe%40example.com&password=1234&grant_type=password

If you want to know more detail, you can watch User Registration and Login - Angular Front to Back with Web API by Deborah Kurata.

Win
  • 61,100
  • 13
  • 102
  • 181
  • So I will create a POST request to /TOKEN with username and password in the HTTP Header/Body? I will have username and hashed password for all the users in my app database. How should I implement this? – Souvik Ghosh Jul 29 '16 at 16:04
  • You need ASP.Net Identity *(I believe you already have one)*. If not, create a ASP.Net Web API project and see the source code. – Win Jul 29 '16 at 16:09
  • 2
    what is grant_type=password ? please share knowledge. thanks – Monojit Sarkar Aug 03 '16 at 08:11
  • 5
    I think putting a username and password in the query string is a BAD BAD BAD idea. – frenchie Nov 05 '18 at 11:24
  • 2
    @frenchie `grant_type=password` is OAuth 2.0 - Resource Owner Password Credentials Grant Type. It is not something we see in browser's navigation bar. – Win Nov 05 '18 at 17:01