Web API 2 OWIN Bearer token authentication - AccessTokenFormat null?
asp.net-identity, asp.net-mvc-5, asp.net-web-api, oauth-2.0, owin
Solution
I had the same problem - it was to do with my initialisation in Startup().
Like you, I was storing the OAuthBearerOptions in a static field:
OAuthBearerOptions = new OAuthBearerAuthenticationOptions();
But then I was wrongly using a new instance of the same class later on:
app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions()); // wrong!
Obviously the fix was to use the static field instead:
app.UseOAuthBearerAuthentication(OAuthBearerOptions);
In fact, it doesn't look like you call UseOAuthBearerAuthentication() at all. I followed this excellent series of posts by Taiseer Joudeh.
Full Startup.cs:
public class Startup
{
public static OAuthBearerAuthenticationOptions OAuthBearerOptions { get; private set; }
public void Configuration(IAppBuilder app)
{
HttpConfiguration config = new HttpConfiguration();
ConfigureOAuth(app);
WebApiConfig.Register(config);
app.UseCors(Microsoft.Owin.Cors.CorsOptions.AllowAll);
app.UseWebApi(config);
}
public void ConfigureOAuth(IAppBuilder app)
{
//use a cookie to temporarily store information about a user logging in with a third party login provider
app.UseExternalSignInCookie(Microsoft.AspNet.Identity.DefaultAuthenticationTypes.ExternalCookie);
OAuthBearerOptions = new OAuthBearerAuthenticationOptions();
OAuthAuthorizationServerOptions OAuthServerOptions = new OAuthAuthorizationServerOptions() {
AllowInsecureHttp = true,
TokenEndpointPath = new PathString("/token"),
AccessTokenExpireTimeSpan = TimeSpan.FromDays(1),
Provider = new SimpleAuthorizationServerProvider() // see post
};
// Token Generation
app.UseOAuthAuthorizationServer(OAuthServerOptions);
app.UseOAuthBearerAuthentication(OAuthBearerOptions);
//[Configure External Logins...]
}
}
Problem
I have an existing ASP.NET MVC 5 project and I'm adding a Web API 2 project to it. I want to use bearer token authentication and have followed Hongye Sun's tutorial "OWIN Bearer Token Authentication with Web API Sample" and this question as well. In my `Login` method, for the line `Startup.OAuthBearerOptions.AccessTokenFormat.Protect(ticket);`, the AccessTokenFormat is null. Any idea why? My AccountController: ``` [RoutePrefix("api")] public class AccountController : ApiController { public AccountController() {} // POST api/login [HttpPost] [Route("login")] public HttpResponseMessage Login(int id, string pwd) { if (id > 0) // testing - not authenticating right now { var identity = new ClaimsIdentity(Startup.OAuthBearerOptions.AuthenticationType); identity.AddClaim(new Claim(ClaimTypes.Name, id.ToString())); AuthenticationTicket ticket = new AuthenticationTicket(identity, new AuthenticationProperties()); var currentUtc = new SystemClock().UtcNow; ticket.Properties.IssuedUtc = currentUtc; ticket.Properties.ExpiresUtc = currentUtc.Add(TimeSpan.FromMinutes(30)); var token = Startup.OAuthBearerOptions.AccessTokenFormat.Protect(ticket); return new HttpResponseMessage(HttpStatusCode.OK) { Content = new ObjectContent<object>(new { UserName = id.ToString(), AccessToken = token }, Configuration.Formatters.JsonFormatter) }; } return new HttpResponseMessage(HttpStatusCode.BadRequest); } // POST api/token [Route("token")] [HttpPost] public HttpResponseMessage Token(int id, string pwd) { // Never reaches here. Do I need this method? return new HttpResponseMessage(HttpStatusCode.OK); } } ``` Startup class: ``` public class Startup { private static readonly ILog _log = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType); public static OAuthBearerAuthenticationOptions OAuthBearerOptions { get; private set; } public static OAuthAuthorizationServerOptions OAuthOptions { get; private set; } public static Func<MyUserManager> UserManagerFactory { get; set; } public static string PublicClientId { get; private set; } static Startup() { PublicClientId = "MyWeb"; UserManagerFactory = () => new MyUserManager(new UserStore<MyIdentityUser>()); OAuthBearerOptions = new OAuthBearerAuthenticationOptions(); OAuthOptions = new OAuthAuthorizationServerOptions { TokenEndpointPath = new PathString("/api/token"), Provider = new MyWebOAuthProvider(PublicClientId, UserManagerFactory), AuthorizeEndpointPath = new PathString("/api/login"), AccessTokenExpireTimeSpan = TimeSpan.FromDays(14), AllowInsecureHttp = true }; } public void Configuration(IAppBuilder app) { // Enable the application to use bearer tokens to authenticate users app.UseOAuthBearerTokens(OAuthOptions); app.UseCookieAuthentication(new CookieAuthenticationOptions { AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie, LoginPath = new PathString("/api/login") }); // Configure Web API to use only bearer token authentication. var config = GlobalConfiguration.Configuration; config.SuppressDefaultHostAuthentication(); config.Filters.Add(new HostAuthenticationFilter(OAuthBearerOptions.AuthenticationType)); app.UseWebApi(config); } } ``` MyIdentityUser just adds an extra property: ``` public class MyIdentityUser : IdentityUser { public int SecurityLevel { get; set; } } ``` MyUserManager calls my custom user authentication method to an internal server: ``` public class MyUserManager : UserManager<MyIdentityUser> { public MyUserManager(IUserStore<MyIdentityUser> store) : base(store) { } public MyIdentityUser ValidateUser(int id, string pwd) { LoginIdentityUser user = null; if (MyApplication.ValidateUser(id, pwd)) { // user = ??? - not yet implemented } return user; } } ``` MyWebOAuthProvider (I took this from the SPA template. Only `GrantResourceOwnerCredentials` has been changed): ``` public class MyWebOAuthProvider : OAuthAuthorizationServerProvider { private readonly string _publicClientId; private readonly Func<MyUserManager> _userManagerFactory; public MyWebOAuthProvider(string publicClientId, Func<MyUserManager> userManagerFactory) { if (publicClientId == null) { throw new ArgumentNullException("publicClientId"); } if (userManagerFactory == null) { throw new ArgumentNullException("userManagerFactory"); } _publicClientId = publicClientId; _userManagerFactory = userManagerFactory; } public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context) { using (MyUserManager userManager = _userManagerFactory()) { MyIdentityUser user = null; var ctx = context as MyWebOAuthGrantResourceOwnerCredentialsContext; if (ctx != null) { user = userManager.ValidateUser(ctx.Id, ctx.Pwd); } if (user == null) { context.SetError("invalid_grant", "The user name or password is incorrect."); return; } ClaimsIdentity oAuthIdentity = await userManager.CreateIdentityAsync(user, context.Options.AuthenticationType); ClaimsIdentity cookiesIdentity = await userManager.CreateIdentityAsync(user, CookieAuthenticationDefaults.AuthenticationType); AuthenticationProperties properties = CreateProperties(user.UserName); AuthenticationTicket ticket = new AuthenticationTicket(oAuthIdentity, properties); context.Validated(ticket); context.Request.Context.Authentication.SignIn(cookiesIdentity); } } public override Task TokenEndpoint(OAuthTokenEndpointContext context) { ... // unchanged from SPA template } public override Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context) { ... // unchanged from SPA template } public override Task ValidateClientRedirectUri(OAuthValidateClientRedirectUriContext context) { ... // unchanged from SPA template } public static AuthenticationProperties CreateProperties(string userName) { ... // unchanged from SPA template } } ``` MyWebOAuthGrantResourceOwnerCredientialsContext: ``` public class MyWebOAuthGrantResourceOwnerCredentialsContext : OAuthGrantResourceOwnerCredentialsContext { public MyWebOAuthGrantResourceOwnerCredentialsContext (IOwinContext context, OAuthAuthorizationServerOptions options, string clientId, string userName, string password, IList<string> scope) : base(context, options, clientId, userName, password, scope) { } public int Id { get; set; } public string Pwd { get; set; } } ``` How is AccessTokenFormat set? Is what I've set up correct? I'm not authenticating against any external services, just a legacy internal server. Thanks.