← Back to Articles         
JWT Roles, Claims, Authorization & Refresh Tokens

JWT Roles, Claims, Authorization & Refresh Tokens

Published on 01 Oct 2026     14 min read Angular
Authentication

JWT Roles, Claims, Authorization & Refresh Tokens

These nine questions belong to one authentication flow, so I’ll connect them while keeping each topic separate.


1. How Do Roles Work Between Angular and ASP.NET Core?

What is a Role?

A role represents a category of user and is commonly used to decide what that user can access.

Examples:

Admin
Manager
User
Editor

A typical JWT flow is:

User logs in
    ↓
ASP.NET Core validates user
    ↓
Backend determines user's roles
    ↓
Roles are added as JWT claims
    ↓
JWT returned to Angular
    ↓
Angular sends JWT with API requests
    ↓
ASP.NET Core validates JWT
    ↓
[Authorize(Roles = "Admin")]

Example: Adding a Role to JWT

var claims = new[]
{
    new Claim(ClaimTypes.Name, user.Username),

    new Claim(
        ClaimTypes.Role,
        "Admin")
};

The token now carries information representing:

Name = syed
Role = Admin

ASP.NET Core Uses the Role

[Authorize(Roles = "Admin")]
[HttpDelete("{id}")]
public IActionResult Delete(int id)
{
    return Ok();
}

Angular Can Use the Role for UI

Angular might use the user's role to decide whether to display:

Admin Dashboard
Delete button
User Management menu

But:

Angular role checks are UI/navigation logic. ASP.NET Core must enforce the actual authorization.


2. How Do Claims Work?

What is a Claim?

A claim is a piece of information about an authenticated identity.

Examples:

UserId = 1001
Name = Syed
Email = syed@example.com
Role = Admin
Department = IT
Permission = Products.Delete

A role is effectively a specialized type of claim.

Creating Claims

var claims = new List<Claim>
{
    new Claim(ClaimTypes.NameIdentifier, "1001"),

    new Claim(ClaimTypes.Name, "syed"),

    new Claim(ClaimTypes.Role, "Admin"),

    new Claim("department", "IT"),

    new Claim(
        "permission",
        "products.delete")
};

Reading Claims in ASP.NET Core

var userId =
    User.FindFirst(
        ClaimTypes.NameIdentifier)?.Value;

var username =
    User.Identity?.Name;

var department =
    User.FindFirst("department")?.Value;

Checking a role:

bool isAdmin =
    User.IsInRole("Admin");

Checking a custom claim:

bool canDelete =
    User.HasClaim(
        "permission",
        "products.delete");

3. How Do You Implement Role-Based Authorization?

Full Simple Example

When generating the JWT:

var claims = new List<Claim>
{
    new Claim(
        ClaimTypes.Name,
        "syed"),

    new Claim(
        ClaimTypes.Role,
        "Admin")
};

Then protect the controller:

[Authorize(Roles = "Admin")]
[HttpDelete("{id}")]
public IActionResult Delete(int id)
{
    return Ok("Product deleted");
}

For multiple allowed roles:

[Authorize(Roles = "Admin,Manager")]

This means:

Admin OR Manager

Flow

Angular
   ↓
Bearer JWT
   ↓
ASP.NET Core validates JWT
   ↓
Creates ClaimsPrincipal
   ↓
Role claim = Admin
   ↓
[Authorize(Roles = "Admin")]
   ↓
Access granted

If the JWT is valid but the user doesn't have the required role:

403 Forbidden

4. How Do You Implement Policy-Based Authorization?

What is Policy-Based Authorization?

Policy-based authorization provides more flexible authorization rules than simply checking roles.

For example:

User must have the products.delete permission.

Instead of:

[Authorize(Roles = "Admin")]

you can define:

[Authorize(Policy = "CanDeleteProducts")]

Full Simple Example

Program.cs

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy(
        "CanDeleteProducts",
        policy =>
        {
            policy.RequireClaim(
                "permission",
                "products.delete");
        });
});

JWT contains:

new Claim(
    "permission",
    "products.delete")

Controller:

[Authorize(
    Policy = "CanDeleteProducts")]
[HttpDelete("{id}")]
public IActionResult Delete(int id)
{
    return Ok("Product deleted");
}

Flow

JWT
 ↓
permission = products.delete
 ↓
ASP.NET Core
 ↓
CanDeleteProducts policy
 ↓
RequireClaim(
 "permission",
 "products.delete")
 ↓
Claim exists?
 ↙       ↘
Yes       No
 ↓         ↓
Allow     403

Role vs Policy

Role-based Policy-based
Checks roles Checks requirements
Simple More flexible
Admin products.delete
Manager orders.approve
[Authorize(Roles=...)] [Authorize(Policy=...)]

For fine-grained authorization, policies are often cleaner than creating many roles.


5. What is an Access Token?

What is it?

An access token is a credential that the client presents to an API to access protected resources.

In our Angular application, the JWT is commonly used as an access token.

Example:

Authorization: Bearer eyJhbGciOi...

An access token is usually short-lived.

For example:

15 minutes
30 minutes
1 hour

Example token creation:

var token = new JwtSecurityToken(
    issuer: issuer,
    audience: audience,
    claims: claims,

    expires:
        DateTime.UtcNow.AddMinutes(15),

    signingCredentials: credentials
);

Purpose

Angular
   ↓
Access Token
   ↓
ASP.NET Core API
   ↓
Protected Resource

6. What is a Refresh Token?

What is it?

A refresh token is a separate, longer-lived credential used to obtain a new access token without asking the user to log in again.

Example:

Access Token
Lifetime = 15 minutes

Refresh Token
Lifetime = 7 days

The refresh token itself should not normally be sent with every API request.

It is used only with the token-refresh mechanism.

Access token expires
       ↓
Use refresh token
       ↓
Authentication server validates it
       ↓
New access token

7. Why Do We Need Refresh Tokens?

Suppose we make an access token valid for:

30 days

If it is stolen, an attacker potentially has a long window in which that token can be used.

Instead, use:

Access Token  → Short lifetime
Refresh Token → Longer lifetime

For example:

Access Token
15 minutes
    ↓
Used for normal API calls


Refresh Token
7 days
    ↓
Used to obtain another
access token

This gives a better balance between:

Security
+
User Experience

Without refresh tokens:

Access token expires
       ↓
User must login again

With refresh tokens:

Access token expires
       ↓
Refresh credentials
       ↓
New access token
       ↓
Continue session

Refresh tokens also give the server a place to implement controls such as revocation and rotation.


8. How Do You Implement Refresh-Token Flow?

Let's build a simplified implementation.

Overall Architecture

Angular
   ↓
Login
   ↓
ASP.NET Core
   ↓
Validate credentials
   ↓
Generate:
 Access Token
 Refresh Token
   ↓
Angular

Later:

Access Token expires
        ↓
Refresh request
        ↓
ASP.NET Core validates Refresh Token
        ↓
Generate new Access Token
        ↓
Rotate Refresh Token
        ↓
Angular continues

Full Simple ASP.NET Core Example

TokenResponse.cs

public class TokenResponse
{
    public string AccessToken { get; set; }
        = string.Empty;

    public string RefreshToken { get; set; }
        = string.Empty;
}

RefreshTokenRequest.cs

public class RefreshTokenRequest
{
    public string RefreshToken { get; set; }
        = string.Empty;
}

Simplified Refresh Token Model

public class RefreshToken
{
    public string Token { get; set; }
        = string.Empty;

    public string Username { get; set; }
        = string.Empty;

    public DateTime ExpiresAt { get; set; }

    public bool IsRevoked { get; set; }
}

In a real application, store refresh-token records in a persistent data store and consider storing a hash of the refresh token rather than the raw token.


Token Service

using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Text;

public class TokenService
{
    private readonly IConfiguration _configuration;

    public TokenService(
        IConfiguration configuration)
    {
        _configuration = configuration;
    }


    public string GenerateAccessToken(
        string username,
        string role)
    {
        var claims = new[]
        {
            new Claim(
                ClaimTypes.Name,
                username),

            new Claim(
                ClaimTypes.Role,
                role)
        };


        var key =
            new SymmetricSecurityKey(
                Encoding.UTF8.GetBytes(
                    _configuration["Jwt:Key"]!
                )
            );


        var credentials =
            new SigningCredentials(
                key,
                SecurityAlgorithms.HmacSha256
            );


        var token =
            new JwtSecurityToken(

                issuer:
                    _configuration["Jwt:Issuer"],

                audience:
                    _configuration["Jwt:Audience"],

                claims:
                    claims,

                expires:
                    DateTime.UtcNow
                        .AddMinutes(15),

                signingCredentials:
                    credentials
            );


        return new JwtSecurityTokenHandler()
            .WriteToken(token);
    }


    public string GenerateRefreshToken()
    {
        var randomBytes =
            RandomNumberGenerator
                .GetBytes(64);

        return Convert.ToBase64String(
            randomBytes);
    }
}

Notice the difference:

Access Token
    ↓
JWT containing claims


Refresh Token
    ↓
Cryptographically random value

A refresh token does not have to be a JWT.


AuthController.cs

For simplicity, we'll use an in-memory collection. Production applications should use persistent storage.

using Microsoft.AspNetCore.Mvc;

[ApiController]
[Route("api/[controller]")]
public class AuthController : ControllerBase
{
    private static readonly
        List<RefreshToken> RefreshTokens = [];

    private readonly TokenService _tokenService;


    public AuthController(
        TokenService tokenService)
    {
        _tokenService = tokenService;
    }


    [HttpPost("login")]
    public IActionResult Login(
        LoginRequest request)
    {
        // Demo only.
        // Validate against database in production.

        if (request.Username != "admin" ||
            request.Password != "admin123")
        {
            return Unauthorized();
        }


        var accessToken =
            _tokenService
                .GenerateAccessToken(
                    request.Username,
                    "Admin");


        var refreshTokenValue =
            _tokenService
                .GenerateRefreshToken();


        RefreshTokens.Add(
            new RefreshToken
            {
                Token = refreshTokenValue,

                Username =
                    request.Username,

                ExpiresAt =
                    DateTime.UtcNow
                        .AddDays(7),

                IsRevoked = false
            });


        return Ok(
            new TokenResponse
            {
                AccessToken =
                    accessToken,

                RefreshToken =
                    refreshTokenValue
            });
    }


    [HttpPost("refresh")]
    public IActionResult Refresh(
        RefreshTokenRequest request)
    {
        var existingToken =
            RefreshTokens
                .FirstOrDefault(x =>
                    x.Token ==
                        request.RefreshToken);


        if (existingToken == null)
        {
            return Unauthorized();
        }


        if (existingToken.IsRevoked)
        {
            return Unauthorized();
        }


        if (existingToken.ExpiresAt
            <= DateTime.UtcNow)
        {
            return Unauthorized();
        }


        // Rotate old refresh token

        existingToken.IsRevoked = true;


        var newAccessToken =
            _tokenService
                .GenerateAccessToken(
                    existingToken.Username,
                    "Admin");


        var newRefreshToken =
            _tokenService
                .GenerateRefreshToken();


        RefreshTokens.Add(
            new RefreshToken
            {
                Token =
                    newRefreshToken,

                Username =
                    existingToken.Username,

                ExpiresAt =
                    DateTime.UtcNow
                        .AddDays(7)
            });


        return Ok(
            new TokenResponse
            {
                AccessToken =
                    newAccessToken,

                RefreshToken =
                    newRefreshToken
            });
    }
}

Register the service:

builder.Services.AddScoped<TokenService>();

Angular Auth Service

import { inject, Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { tap } from 'rxjs';


interface TokenResponse {

  accessToken: string;

  refreshToken: string;
}


@Injectable({
  providedIn: 'root'
})
export class AuthService {

  private http = inject(HttpClient);

  private apiUrl =
    'https://localhost:7001/api/auth';


  login(
    username: string,
    password: string
  ) {

    return this.http
      .post<TokenResponse>(
        `${this.apiUrl}/login`,
        {
          username,
          password
        }
      )
      .pipe(

        tap(response => {

          localStorage.setItem(
            'accessToken',
            response.accessToken
          );

          localStorage.setItem(
            'refreshToken',
            response.refreshToken
          );

        })

      );
  }


  refreshToken() {

    const refreshToken =
      localStorage.getItem(
        'refreshToken'
      );


    return this.http
      .post<TokenResponse>(
        `${this.apiUrl}/refresh`,
        {
          refreshToken
        }
      )
      .pipe(

        tap(response => {

          localStorage.setItem(
            'accessToken',
            response.accessToken
          );

          localStorage.setItem(
            'refreshToken',
            response.refreshToken
          );

        })

      );
  }
}

This demonstrates the protocol flow, but storing a long-lived refresh token in localStorage increases exposure to XSS. A production browser application often uses a more carefully designed storage approach, such as a securely configured HttpOnly, Secure cookie for refresh credentials.


Angular Interceptor

The access token is sent with normal API requests:

import {
  HttpInterceptorFn
} from '@angular/common/http';


export const authInterceptor:
  HttpInterceptorFn =
  (req, next) => {

    const accessToken =
      localStorage.getItem(
        'accessToken'
      );


    if (!accessToken) {

      return next(req);
    }


    const authRequest =
      req.clone({

        setHeaders: {

          Authorization:
            `Bearer ${accessToken}`

        }

      });


    return next(authRequest);
  };

The request becomes:

GET /api/products
Authorization: Bearer <access-token>

Refresh Token Flow

The complete flow is:

LOGIN

Angular
   │
   │ username/password
   ▼
ASP.NET Core
   │
   ├── Validate user
   │
   ├── Access Token  → 15 min
   │
   └── Refresh Token → 7 days
   │
   ▼
Angular

Then:

NORMAL REQUEST

Angular
   │
   │ Access Token
   ▼
ASP.NET Core
   │
   ├── Validate access token
   │
   ▼
200 OK

Later:

ACCESS TOKEN EXPIRES

Angular
   │
   │ Expired Access Token
   ▼
ASP.NET Core
   │
   ▼
401 Unauthorized

Angular then performs:

Angular
   │
   │ Refresh credentials
   ▼
POST /api/auth/refresh
   │
   ▼
ASP.NET Core
   │
   ├── Find refresh token
   ├── Check expiration
   ├── Check revocation
   ├── Revoke/rotate old token
   ├── Generate new access token
   └── Generate new refresh token
   │
   ▼
Angular

9. What Happens When an Access Token Expires?

Suppose the access token is valid for:

15 minutes

After 15 minutes, Angular may still make:

GET /api/products
Authorization: Bearer <expired-token>

ASP.NET Core has:

ValidateLifetime = true;

Therefore:

Expired Access Token
        ↓
ASP.NET Core JWT validation
        ↓
Lifetime validation fails
        ↓
401 Unauthorized

Angular can then invoke the refresh mechanism.

API Request
    ↓
401
    ↓
Refresh Token
    ↓
POST /api/auth/refresh
    ↓
New Access Token
    ↓
Retry original request
    ↓
200 OK

In a more complete Angular implementation, this behavior is usually centralized in an HTTP interceptor rather than repeated in every component/service.


What if the Refresh Token Also Expires?

Then the user must authenticate again.

Access Token expired
       ↓
Try Refresh Token
       ↓
Refresh Token expired
       ↓
Refresh rejected
       ↓
Clear local authentication state
       ↓
Redirect to Login
       ↓
User logs in again

Access Token vs Refresh Token

Feature Access Token Refresh Token
Purpose Access APIs Obtain new access token
Lifetime Short Longer
Sent with normal API requests Yes No
Usually Bearer header Yes Usually not as normal API bearer token
Contains claims JWT often does Not necessarily
Used by API authorization Yes No
Server-side revocation commonly maintained Sometimes Commonly
If expired Refresh/re-authenticate Login again

Refresh Token Rotation

A good refresh-token design commonly uses rotation.

Instead of repeatedly using:

RefreshToken-A

the flow becomes:

RefreshToken-A
      ↓
Refresh
      ↓
A revoked
      ↓
RefreshToken-B
      ↓
Refresh
      ↓
B revoked
      ↓
RefreshToken-C

This limits the usefulness of previously used refresh tokens.

More mature implementations can also detect refresh-token reuse, revoke the related token family/session, and require reauthentication.


Roles vs Claims vs Policies

This is important for interviews.

CLAIM
  │
  ├── Name = Syed
  ├── Role = Admin
  ├── Department = IT
  └── Permission = products.delete

A role is a type of claim:

Role = Admin

Role-based authorization:

[Authorize(Roles = "Admin")]

Policy-based authorization:

[Authorize(
    Policy = "CanDeleteProducts")]

Policy:

CanDeleteProducts
        ↓
Requires
        ↓
permission = products.delete

So a useful mental model is:

Claims
   ↓
Information about user

Roles
   ↓
User category/group

Policies
   ↓
Authorization rules

Key Points

  • Roles and claims are normally established by the trusted backend/Identity Provider.
  • Roles can be represented as claims in the access token.
  • Angular may read role/claim information for UI behavior, but should not be the security authority.
  • ASP.NET Core performs the real authorization.
  • [Authorize(Roles = "Admin")] provides role-based authorization.
  • [Authorize(Policy = "...")] provides policy-based authorization.
  • Policies can evaluate roles, claims, or custom requirements.
  • Access tokens are normally short-lived.
  • Refresh tokens are longer-lived credentials used to obtain new access tokens.
  • Refresh tokens should not be sent with every normal API call.
  • When an access token expires, a protected API normally returns 401.
  • A valid refresh credential can be exchanged for a new access token.
  • Refresh-token rotation is preferable to endlessly reusing the same refresh token.
  • If the refresh token is expired/revoked, the user must authenticate again.
  • Browser token storage is a security-sensitive design choice; long-lived refresh credentials deserve stronger protection than a simple demo's localStorage.

Interview Questions & Answers

1. What is the difference between a role and a claim?

A claim is information about the authenticated identity, such as user ID, department, permission, or role. A role is a specialized claim representing a user category such as Admin or Manager.


2. How do roles work between Angular and ASP.NET Core?

The trusted backend/Identity Provider places role information into the access token. Angular may use it for UI/navigation, while ASP.NET Core validates the token and enforces roles using server-side authorization.


3. How do you implement role-based authorization?

Use:

[Authorize(Roles = "Admin")]

The authenticated user must contain the required role.


4. What is policy-based authorization?

Policy-based authorization defines named authorization rules that can require claims, roles, or custom requirements.

[Authorize(
    Policy = "CanDeleteProducts")]

5. Role-based vs policy-based authorization?

Role-based authorization asks:

Is this user an Admin?

Policy-based authorization can express more specific requirements:

Does this user have
products.delete permission?

6. What is an access token?

An access token is a short-lived credential presented to a protected API.

Authorization: Bearer <access-token>

7. What is a refresh token?

A refresh token is a longer-lived credential used to obtain a new access token without requiring the user to enter credentials again.


8. Why should access tokens be short-lived?

If an access token is stolen, a shorter expiration limits how long that token can normally be used.


9. Why do we need refresh tokens?

They allow short-lived access tokens for security while maintaining a convenient user session by obtaining replacement access tokens without repeated logins.


10. Should a refresh token be sent with every API request?

No.

Normal API calls use the access token. The refresh credential is used only for the refresh operation.


11. What happens when an access token expires?

The API rejects it, normally with:

401 Unauthorized

The client can attempt a refresh. If refresh succeeds, it receives a new access token and can retry the request.


12. What happens when the refresh token expires?

The refresh request fails, and the user normally needs to authenticate again.


13. What is refresh-token rotation?

After successfully using a refresh token, the server invalidates it and issues a replacement:

RT1 → used/revoked
        ↓
       RT2
        ↓
RT2 → used/revoked
        ↓
       RT3

This reduces the risk associated with replaying an old refresh token.


Interview Quick Revision

LOGIN
  ↓
ASP.NET Core validates credentials
  ↓
Create Claims
  ├── UserId
  ├── Role
  └── Permissions
  ↓
Generate Access Token
+
Generate Refresh Token
  ↓
Angular


NORMAL API REQUEST
  ↓
Access Token
  ↓
ASP.NET Core validates JWT
  ↓
Role / Policy authorization
  ↓
API


ACCESS TOKEN EXPIRES
  ↓
401
  ↓
Refresh credentials
  ↓
Validate Refresh Token
  ↓
Rotate Refresh Token
  ↓
New Access Token
  ↓
Continue


REFRESH TOKEN EXPIRES / REVOKED
  ↓
Login Again

One-line interview answer

ASP.NET Core/Identity Provider issues a short-lived access token containing the user's relevant claims and roles, while Angular presents that token to protected APIs. ASP.NET Core enforces role- or policy-based authorization. When the access token expires, the client can use a valid refresh credential to obtain a new access token; if the refresh token is expired or revoked, the user must authenticate again.

← Back to Articles