Owin Bearer Token Authentication + Authorize controller

Colin picture Colin · Aug 1, 2014 · Viewed 31.4k times · Source

I'm trying to do authentication with Bearer tokens and owin.

I can issue the token fine using the grant type password and overriding GrantResourceOwnerCredentials in AuthorizationServerProvider.cs.

But I can't reach a controller method with the Authorize attribute.

Here's my code:

Startup.cs

public class Startup
{
    public static OAuthAuthorizationServerOptions OAuthOptions { get; private set; }

    // normal
    public Startup() : this(false) { }

    // testing
    public Startup(bool isDev)
    {
        // add settings
        Settings.Configure(isDev);

        OAuthOptions = new OAuthAuthorizationServerOptions
        {
            AllowInsecureHttp = true,
            TokenEndpointPath = new PathString("/Token"),
            AccessTokenExpireTimeSpan = TimeSpan.FromDays(1),
            Provider = new AuthorizationServerProvider()
        };
    }

    public void Configuration(IAppBuilder app)
    {
        // Configure the db context, user manager and role manager to use a single instance per request
        app.CreatePerOwinContext(ApplicationDbContext.Create);
        app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);
        app.CreatePerOwinContext<ApplicationRoleManager>(ApplicationRoleManager.Create);
        app.CreatePerOwinContext<ApplicationSignInManager>(ApplicationSignInManager.Create);
        app.CreatePerOwinContext<LoanManager>(BaseManager.Create);

        var config = new HttpConfiguration();
        WebApiConfig.Register(config);
        app.UseWebApi(config);

        // token generation
        app.UseOAuthAuthorizationServer(OAuthOptions);
        app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions
        {
            AuthenticationType = "Bearer",
            AuthenticationMode = AuthenticationMode.Active
        });
    }
}

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[] { "*" });
        var userManager = context.OwinContext.GetUserManager<ApplicationUserManager>();

        IdentityUser user = await userManager.FindAsync(context.UserName, context.Password);

        if (user == null)
        {
            context.SetError("invalid_grant", "The user name or password is incorrect.");
            return;
        }

        var identity = new ClaimsIdentity(context.Options.AuthenticationType);
        identity.AddClaim(new Claim("sub", context.UserName));
        identity.AddClaim(new Claim("role", "user"));

        context.Validated(identity);
    }
}

WebApiConfig.cs

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        config.MapHttpAttributeRoutes();

        // enable CORS for all hosts, headers and methods
        var cors = new EnableCorsAttribute("*", "*", "*");
        config.EnableCors(cors);

        config.Routes.MapHttpRoute(
            name: "optional params",
            routeTemplate: "api/{controller}"
        );

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

        // stop cookie auth
        config.SuppressDefaultHostAuthentication();
        // add token bearer auth
        config.Filters.Add(new MyAuthenticationFilter());
        //config.Filters.Add(new HostAuthenticationFilter(Startup.OAuthOptions.AuthenticationType));

        config.Filters.Add(new ValidateModelAttribute());

        if (Settings.IsDev == false)
        {
            config.Filters.Add(new AuthorizeAttribute());
        }

        // make properties on model camelCased
        var jsonFormatter = config.Formatters.OfType<JsonMediaTypeFormatter>().First();
        jsonFormatter.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();

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

MyAuthenticationFilter.cs Custom filter used for debugging purposes

public class MyAuthenticationFilter : ActionFilterAttribute, IAuthenticationFilter
{
    public Task AuthenticateAsync(HttpAuthenticationContext context, CancellationToken cancellationToken)
    {
        if (context.Principal != null && context.Principal.Identity.IsAuthenticated)
        {
        }

        return Task.FromResult(0);
    }

    public Task ChallengeAsync(HttpAuthenticationChallengeContext context, CancellationToken cancellationToken)
    {
        throw new System.NotImplementedException();
    }
}

If I debug AuthenticateAsync in MyAuthenticationFilter.cs I see the header in the request:

Authorization: Bearer AQAAANCMnd8BFdERjHoAwE_Cl...

But the Identity Claims are empty and context.Principal.Identity.IsAuthenticated is false.

Any ideas?

Answer

Shadi picture Shadi · Oct 26, 2014

I was looking for the same solution, I spent a week or so on this and I left it. Today I started to search again, I found your questions and I was hoping to find an answer.

So I spent the whole day doing nothing other than trying all the possible solutions, merging suggestions with each other, I found some solution but they were long workarounds, to make the long story short here is what I found.

First of all if you need to authenticate the Web site with a custom third party identity provider token you need to have them both using the same machineKey or you need to have them both on the same server.

You need to add the machineKey to the system.web section as following:

Web.Config

<system.web>
    <authentication mode="None" />
    <compilation debug="true" targetFramework="4.5" />
    <httpRuntime targetFramework="4.5" />
    <machineKey validationKey="*****" decryptionKey="***" validation="SHA1" decryption="AES" />
</system.web>

Here is a link to generate a new machineKey :

Now you need to move to the Startup.Auth.cs file where you can find the Startup.cs partial class, you need to define the OAuthBearerOptions

Startup.Auth.cs

public partial class Startup
{
    public static OAuthBearerAuthenticationOptions OAuthBearerOptions { get; private set; }
    ...

    public void ConfigureAuth(IAppBuilder app)
    {
        // Configure the db context, user manager and signin manager to use a single instance per    request
        app.CreatePerOwinContext(ApplicationDbContext.Create);
        app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);

        OAuthBearerOptions = new OAuthBearerAuthenticationOptions();
        app.UseOAuthBearerAuthentication(OAuthBearerOptions);
        ...
    }
}

Replace your Login action inside AccountController with the following:

AccountController.cs

[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Login(LoginViewModel model, string returnUrl)
{
    /*This will depend totally on how you will get access to the identity provider and get your token, this is just a sample of how it would be done*/
    /*Get Access Token Start*/
    HttpClient httpClient = new HttpClient();
    httpClient.BaseAddress = new Uri("https://youridentityproviderbaseurl");
    var postData = new List<KeyValuePair<string, string>>();
    postData.Add(new KeyValuePair<string, string>("UserName", model.Email));
    postData.Add(new KeyValuePair<string, string>("Password", model.Password));
    HttpContent content = new FormUrlEncodedContent(postData);


    HttpResponseMessage response = await httpClient.PostAsync("yourloginapi", content);
    response.EnsureSuccessStatusCode();
    string AccessToken = Newtonsoft.Json.JsonConvert.DeserializeObject<string>(await response.Content.ReadAsStringAsync());
    /*Get Access Token End*/

    If(!string.IsNullOrEmpty(AccessToken))
    {
            var ticket = Startup.OAuthBearerOptions.AccessTokenFormat.Unprotect(AccessToken);
            var id = new ClaimsIdentity(ticket.Identity.Claims, DefaultAuthenticationTypes.ApplicationCookie);
            AuthenticationManager.SignIn(new AuthenticationProperties() { IsPersistent = true }, id);

            return RedirectToLocal(returnUrl);

   }

   ModelState.AddModelError("Error", "Invalid Authentication");
   return View();
}

The last thing you need to do is to place this line of code in the Global.asax.cs to avoid Anti Forgery exceptions:

Global.asax.cs

public class MvcApplication : System.Web.HttpApplication
{
    protected void Application_Start()
    {
        AntiForgeryConfig.UniqueClaimTypeIdentifier = ClaimTypes.NameIdentifier;

        …
    }
}

Hope this would work for you.