ASP.NET Web API (.NET Framework) · Cross-Cutting

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.

Intermediate 7 min Lesson 23 of 36Covers ASP.NET Web API 2.2 on .NET Framework 4.8.
Before you start:Controllers and Endpoints
0/36 in ASP.NET Web API
On this page · 7 min · Intermediate

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#

Text Illustrative
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#

C#Startup.cs Illustrative
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 });
    }
}
Expected output
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#

  1. Startup registers the token server at /token and bearer-token reading for every request.
  2. GrantResourceOwnerCredentials checks the credentials; on success it builds a ClaimsIdentity with the name and roles.
  3. context.Validated(identity) issues an encrypted token containing those claims, valid for one hour.
  4. HostAuthenticationFilter makes Web API use only the bearer token to set User.
  5. [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#

AllowInsecureHttp = true in production

Passwords and tokens would travel unencrypted. Only allow HTTP on a local development machine.

Plain-text passwords

Never store or compare plain passwords. Use ASP.NET Identity’s password hasher or another slow hash (PBKDF2, bcrypt).

Tokens in the URL

?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?
[AllowAnonymous] on the action overrides [Authorize] on the controller.
Show solution
C# Illustrative
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#

Token Authentication quizMultiple choice Question 1 of 3

Which header carries the token on API requests?

Score so far: 0/0

Want more? Practise all ASP.NET Web API MCQs · ASP.NET Web API interview questions

My notes#

Saved in this browser.

Progress is saved in this browser. Sign in to sync.