Token Authentication
APIs do not use login cookies and sessions — they use tokens. The client signs in once at /token, receives a bearer token, and sends it with every request. Web API 2 does this with OWIN middleware.
On this page · 7 min · Intermediate
On this page
- Learning objectives
- Explanation
- Syntax
- Code example
- Step by step
- Real-world use
- Common mistakes
- Best practices
- Remember this
- Practice exercise
- Mini quiz
- Related topics
- My notes
- Reading time
- 7 min
- Difficulty
- Intermediate
- Updated
- Oct 28, 2026
Related tutorials
Learning objectives#
- Explain the bearer-token flow
- Configure the OWIN OAuth authorization server and bearer authentication
- Validate users and add claims in GrantResourceOwnerCredentials
- Protect endpoints with [Authorize]
Explanation#
Flow: the client POSTs username and password to /token (form-encoded, grant_type=password). The server checks them and returns an access_token. The client then sends Authorization: Bearer <token> on every API call.
Web API 2 uses Katana/OWIN middleware for this (the “Individual User Accounts” template sets it up). A Startup class with Configuration(IAppBuilder app) wires it in.
UseOAuthAuthorizationServer issues tokens; your OAuthAuthorizationServerProvider decides who gets one and which claims (name, roles) go inside.
UseOAuthBearerAuthentication reads incoming tokens and sets User. SuppressDefaultHostAuthentication + HostAuthenticationFilter make Web API trust only the bearer token, not IIS/Windows auth.
[Authorize] on a controller or action returns 401 when there is no valid token. [AllowAnonymous] opens a single action.
These tokens are protected with the machine key, not JWTs. For tokens other systems must read, or for new work, use JWT (Microsoft.Owin.Security.Jwt) or, better, move to ASP.NET Core with an identity provider such as Microsoft Entra ID.
Syntax#
POST /token
Content-Type: application/x-www-form-urlencoded
grant_type=password&username=asha&password=P%40ssw0rd!
→ {"access_token":"eyJ...","token_type":"bearer","expires_in":3599}
GET /api/profile
Authorization: Bearer eyJ...
Code example: OWIN startup, provider and a protected controller#
using System;
using System.Security.Claims;
using System.Threading.Tasks;
using System.Web.Http;
using Microsoft.Owin;
using Microsoft.Owin.Security.OAuth;
using Owin;
// [assembly: OwinStartup(typeof(ShopApi.Startup))] — put this line above the namespace in Startup.cs
public class Startup
{
public void Configuration(IAppBuilder app)
{
app.UseOAuthAuthorizationServer(new OAuthAuthorizationServerOptions
{
TokenEndpointPath = new PathString("/token"),
Provider = new ShopAuthorizationServerProvider(new DemoUserStore()),
AccessTokenExpireTimeSpan = TimeSpan.FromHours(1),
AllowInsecureHttp = false, // HTTPS only
});
app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions());
var config = new HttpConfiguration();
config.SuppressDefaultHostAuthentication();
config.Filters.Add(new HostAuthenticationFilter(OAuthDefaults.AuthenticationType));
config.MapHttpAttributeRoutes();
app.UseWebApi(config);
}
}
public interface IUserStore
{
// Returns the user's roles, or null if the credentials are wrong.
Task<string[]> ValidateAsync(string userName, string password);
}
public class DemoUserStore : IUserStore
{
public Task<string[]> ValidateAsync(string userName, string password)
{
// Real apps check a hashed password in the database (e.g. ASP.NET Identity).
bool ok = userName == "asha" && password == "P@ssw0rd!";
return Task.FromResult(ok ? new[] { "Customer" } : null);
}
}
public class ShopAuthorizationServerProvider : OAuthAuthorizationServerProvider
{
private readonly IUserStore _users;
public ShopAuthorizationServerProvider(IUserStore users) { _users = users; }
public override Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
{
context.Validated(); // no client id/secret in this simple setup
return Task.FromResult(0);
}
public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
{
var roles = await _users.ValidateAsync(context.UserName, context.Password);
if (roles == null)
{
context.SetError("invalid_grant", "The user name or password is incorrect.");
return;
}
var identity = new ClaimsIdentity(context.Options.AuthenticationType);
identity.AddClaim(new Claim(ClaimTypes.Name, context.UserName));
foreach (var role in roles)
identity.AddClaim(new Claim(ClaimTypes.Role, role));
context.Validated(identity); // a token is issued with these claims
}
}
[Authorize]
public class ProfileController : ApiController
{
[HttpGet, Route("api/profile")]
public IHttpActionResult Get()
{
return Ok(new { name = User.Identity.Name });
}
}
POST /token grant_type=password&username=asha&password=P%40ssw0rd!
→ 200 {"access_token":"Ah3k...Zq","token_type":"bearer","expires_in":3599}
POST /token grant_type=password&username=asha&password=wrong
→ 400 {"error":"invalid_grant","error_description":"The user name or password is incorrect."}
GET /api/profile Authorization: Bearer Ah3k...Zq → 200 {"name":"asha"}
GET /api/profile (no header) → 401 {"Message":"Authorization has been denied for this request."}This example is illustrative. Copy or download it and run it in your own environment; the expected output is shown above.
Step-by-step explanation#
- Startup registers the token server at /token and bearer-token reading for every request.
- GrantResourceOwnerCredentials checks the credentials; on success it builds a ClaimsIdentity with the name and roles.
- context.Validated(identity) issues an encrypted token containing those claims, valid for one hour.
- HostAuthenticationFilter makes Web API use only the bearer token to set User.
- [Authorize] blocks ProfileController for anonymous callers with 401; with a token, User.Identity.Name is “asha”.
Real-world use case#
A Xamarin mobile app signs in with /token, stores the token securely, and attaches it to every call. When the token expires after an hour, the app asks the user to sign in again (or uses a refresh token).
Common mistakes#
Passwords and tokens would travel unencrypted. Only allow HTTP on a local development machine.
Never store or compare plain passwords. Use ASP.NET Identity’s password hasher or another slow hash (PBKDF2, bcrypt).
?access_token=... ends up in IIS logs and browser history. Send tokens in the Authorization header.
Best practices#
- HTTPS only for /token and the API
- Short token lifetimes
- Roles and user ids as claims, not extra database lookups on every call
- Plan a move to JWT/OpenID Connect for new systems
Remember this#
POST /token once → Bearer token on every request.
GrantResourceOwnerCredentials = check user + add claims.
[Authorize] → 401 without a valid token.
Practice exercise#
Your task: Add an [AllowAnonymous] action GET api/profile/public-info inside the [Authorize] ProfileController that returns { app = "Shop" } without a token.
Need a hint?
Show solution
using System.Web.Http;
[Authorize]
public class ProfileController : ApiController
{
[HttpGet, Route("api/profile")]
public IHttpActionResult Get() => Ok(new { name = User.Identity.Name });
[AllowAnonymous]
[HttpGet, Route("api/profile/public-info")]
public IHttpActionResult PublicInfo() => Ok(new { app = "Shop" });
}
Mini quiz#
Which header carries the token on API requests?
Want more? Practise all ASP.NET Web API MCQs · ASP.NET Web API interview questions
My notes#
Saved in this browser.