Introduction
This article gives a detailed explanation on how to use Token Based Authentication using OAuth and OWIN where application is using custom database having user credentials stored in legacy format.
Background
Since many days, I was going through articles about ASP.NET Web API “token based authentication”. Almost all were using ASP.NET Identity for user management features. (Which creates ASPNET* tables to manage users, roles, groups, etc.).
But I was unable to find an article which will show how to use OWIN & OAuth “token based authentication” for existing database having custom User management tables. This is necessary when it comes to apply OAuth authentication to legacy systems having their own user management feature already in place.
So I am writing this article after analyzing a standard template given by Visual Studio for Web API Individual Accounts authentication and tweaking it to use custom DB User tables. I have used Visual Studio 2015 for this.
Using the Code
As discussed above, our problem is, how an existing database User table having UserName
, Password
stored, can leverage Token based Authentication using OAuth and OWIN. So our typical DB query will look like below. (I have not considered Password encryption for now. I leave it to your application logic.)
Let’s start by creating a standard ASP.NET Web API project as shown below. Please select Web API option from New Project template. Note, we are using “No Authentication” option as we are going to configure it manually.
Since we want to use OAuth for authentication and Entity Framework for DB access, install the below Nuget packages: (They will install their dependants automatically.)
EntityFramework
Microsoft.AspNet.WebApi.Owin
Microsoft.Owin.Security
Microsoft.AspNet.Identity.Owin
Microsoft.Owin.Host.SystemWeb
Next, we need to add Entity Framework model connecting to our DB User Table. I won’t go in detail of that as it is a standard procedure of adding ADO.NET Entity Data Model. Resultant EDMX file should look like below:
Next, we need to add OWIN Startup
class. It will be a partial
class. Add “Startup.Auth.cs” under App_Start folder and paste the below code in it:
public partial class Startup
{
public static OAuthAuthorizationServerOptions OAuthOptions { get; private set; }
public static string PublicClientId { get; private set; }
public void ConfigureAuth(IAppBuilder app)
{
PublicClientId = "self";
OAuthOptions = new OAuthAuthorizationServerOptions
{
TokenEndpointPath = new PathString("/Token"),
Provider = new ApplicationOAuthProvider(PublicClientId),
AuthorizeEndpointPath =
new PathString("/api/Account/ExternalLogin"),
AccessTokenExpireTimeSpan = TimeSpan.FromMinutes(10),
AllowInsecureHttp = true
};
app.UseOAuthBearerTokens(OAuthOptions);
}
}
Again, add “Startup.cs” class at project level and paste the below code in it. Replace namespace string
with your project namespace
.
[assembly: OwinStartup(typeof(OAuth_Custom_DB.Startup))]
namespace OAuth_Custom_DB
{
public partial class Startup
{
public void Configuration(IAppBuilder app)
{
ConfigureAuth(app);
}
}
}
Note: Since Startup
is a partial class
, its namespace
should be the same in Startup.cs and Startup.Auth.cs.
Next, we need to tell Web API application to use Bearer Authentication and not Default Host Authentication. So add the below lines to WebApiConfig.cs.
config.SuppressDefaultHostAuthentication();
config.Filters.Add(new HostAuthenticationFilter(OAuthDefaults.AuthenticationType));
Next, we need to configure OAuth server and options. So add a folder named “Providers” and a class file named “ApplicationOAuthProvider.cs” under it.
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)
{
ClaimsIdentity oAuthIdentity =
new ClaimsIdentity(context.Options.AuthenticationType);
ClaimsIdentity cookiesIdentity =
new ClaimsIdentity(context.Options.AuthenticationType);
AuthenticationProperties properties = CreateProperties(context.UserName);
AuthenticationTicket ticket =
new AuthenticationTicket(oAuthIdentity, properties);
context.Validated(ticket);
context.Request.Context.Authentication.SignIn(cookiesIdentity);
}
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)
{
if (context.ClientId == null)
{
context.Validated();
}
return Task.FromResult<object>(null);
}
public override Task ValidateClientRedirectUri
(OAuthValidateClientRedirectUriContext context)
{
if (context.ClientId == _publicClientId)
{
Uri expectedRootUri = new Uri(context.Request.Uri, "/");
if (expectedRootUri.AbsoluteUri == context.RedirectUri)
{
context.Validated();
}
}
return Task.FromResult<object>(null);
}
public static AuthenticationProperties CreateProperties(string userName)
{
IDictionary<string, string>
data = new Dictionary<string, string>
{
{ "userName", userName }
};
return new AuthenticationProperties(data);
}
}
Actually, it is the same file which gets generated when we select “Individual Accounts” option in Authentication mode while selecting Web API Project template. The only difference is that we are changing the implementation of “GrantResourceOwnerCredentials
” method. Code inside the method should look like below:
ClaimsIdentity oAuthIdentity =
new ClaimsIdentity(context.Options.AuthenticationType);
ClaimsIdentity cookiesIdentity =
new ClaimsIdentity(context.Options.AuthenticationType);
AuthenticationProperties properties = CreateProperties(context.UserName);
AuthenticationTicket ticket =
new AuthenticationTicket(oAuthIdentity, properties);
context.Validated(ticket);
context.Request.Context.Authentication.SignIn(cookiesIdentity);
Last thing, apply normal [Authorize]
attribute to ValuesController
controller for testing purposes.
That’s it… !!!
Our application is ready to be tested for Token based authentication having a custom user ID/Password table.
Run the application so that API service starts running and is ready to be consumed. Your web page should look like below except the port number after locahost:
Now let’s test standard Web API URL directly if it works without authorization.
Open Chrome Postman and type GET URL for Values Controller. Something like http://localhost:56889/api/Values. It gives Authorization denied error as seen below, obviously because there is no authorization done.
So now, we will obtain a "Bearer Token" after authenticating user credentials. So our Token URL will as follows: http://localhost:56889/Token. Note, “/token” is the path specified as “TokenEndPoint
” in “ConfigureAuth
” method of Startup
class. So if it is configured as “/auth/token”, the complete URL would be http://localhost:56889/Auth/Token likewise.
If you look at the Postman entries, for Token URL, we are passing three parameters with content-type as “x-www-form-urlencoded
”. One of the parameters is “grant_type
” and its value is “password
”. This tells OAuth that user wants token to be issued.
After successful execution of the POST
url, we get token issued as below:
What happened here is, this URL has called GrantResourceOwnerCredentials
method. In this method, we have written our custom code of User authentication using Entity Framework. If it succeeds, it executes TokenEndpoint
method further to issue a bearer token.
Take a note of this token value and re-visit the GET
URL of ValuesController
. This time, we will be adding a header named “Authorization” and paste token value we have got after authentication as its value with pre-fixing it with “bearer
“.
Now this token will be valid till the time we have specified again in OAuthAuthorizationServerOptions
in ConfigureAuth
method in Startup
class.