Insights into Company
← Backend Concepts in Depth topics

Tokens, Authentication, Authorization & Roles

How a system knows who is calling (authentication), what they may do (authorization), how tokens carry identity, and how roles and permissions are modelled. Examples from net banking, an e-commerce store and a payment gateway in Laravel, Spring Boot, React and MySQL.

Running examples: a bank customer transferring money, a shopper placing an order, a merchant calling a payment API

1. Authentication vs authorization

Authentication answers "who are you?". The caller proves identity with something they know (password, PIN), something they have (OTP on a phone, hardware key) or something they are (fingerprint). Once proven, the server issues a session or a token so the caller does not send the password on every request.

Authorization answers "what are you allowed to do?". It runs after authentication, on every request, and decides whether this identity may perform this action on this record.

The two fail differently. A failed authentication returns 401 Unauthorized (log in again). A failed authorization returns 403 Forbidden (you are known but not allowed), or 404 when you do not want to reveal that the record exists.

The same request seen by both checks
StepNet bankingE-commercePayment gateway API
AuthenticationCustomer ID + password + OTPEmail + password, or Google loginMerchant API key / OAuth client credentials
Proof carried afterwardsSession cookie (web) or access token (mobile app)Session cookieBearer token or HMAC-signed request
Authorization questionIs account 1234 owned by this customer and is the amount within the daily limit?Is order 187 placed by this shopper?Does this key have the refunds:write scope and belong to the merchant that owns payment pay_9X?
Failure response403 + "Limit exceeded"404 (hide other people's orders)403 + error code INSUFFICIENT_SCOPE

Every protected request

  1. Read credential (cookie / Authorization header)
  2. Authenticate: resolve the user or client
  3. Authorize: role, permission, ownership, limits
  4. Run business logic
  5. Audit-log sensitive actions

2. What a token is and the main token types

A token is a string that represents a proven identity for a limited time. The client stores it and sends it with each request, usually as "Authorization: Bearer <token>". The server checks it instead of asking for the password again.

There are two families. Opaque tokens are random strings; the server looks them up in a database or cache to find the user (Laravel Sanctum personal access tokens, OAuth tokens stored by Passport). Self-contained tokens, mainly JWT (JSON Web Token), carry the user id, roles and expiry inside the token itself, signed by the server, so any service holding the key can verify them without a database lookup.

Choosing a token type
TypeHow it is verifiedRevocationTypical use
Session id cookieLookup in session storeDelete the sessionServer-rendered web apps, Laravel Blade, admin panels
Opaque token (Sanctum)Hash lookup in personal_access_tokensDelete the row, instantMobile apps and SPAs talking to one Laravel API
JWT access tokenVerify signature + expiry, no DBHard: valid until it expires, so keep it short (5–15 min)Microservices: API gateway issues, many services verify
Refresh tokenDB lookup, rotated on each useDelete or mark usedGetting a new short access token without re-login
API keyHash lookupRotate the keyServer-to-server: merchant calling a payment gateway

Anatomy of a JWT (header.payload.signature, each part base64url-encoded)

// header
{ "alg": "RS256", "typ": "JWT", "kid": "2026-key-1" }
// payload (claims) - readable by anyone, so never put secrets here
{ "sub": "customer-42", "roles": ["CUSTOMER"], "scope": "accounts:read transfers:write",
  "iat": 1791700000, "exp": 1791700900, "iss": "https://auth.bank.example" }
// signature = RSA-SHA256(header + "." + payload, private key of the auth server)

The payload is only encoded, not encrypted. Anyone can decode it, but nobody can change it without breaking the signature. That is why a JWT may contain a user id and roles but never a password, card number or Aadhaar number.

3. Laravel: login, tokens and protecting routes

Sanctum token login for a mobile banking app

// routes/api.php
Route::post('/login', LoginController::class)->middleware('throttle:5,1');
Route::middleware('auth:sanctum')->group(function () {
    Route::get('/accounts', [AccountController::class, 'index']);
    Route::post('/transfers', [TransferController::class, 'store'])->middleware(['ability:transfers:write', 'otp.verified']);
});

// LoginController
public function __invoke(Request $request)
{
    $data = $request->validate(['customer_id' => 'required', 'password' => 'required', 'device' => 'required|max:100']);
    $user = User::where('customer_id', $data['customer_id'])->first();

    if (! $user || ! Hash::check($data['password'], $user->password)) {
        throw ValidationException::withMessages(['customer_id' => 'Invalid credentials.']);   // same message for both cases
    }

    $token = $user->createToken($data['device'], ['accounts:read', 'transfers:write'], now()->addMinutes(30));
    return ['token' => $token->plainTextToken, 'expires_in' => 1800];
}

Sanctum stores only a SHA-256 hash of the token in personal_access_tokens, so a leaked database does not expose working tokens. Abilities (scopes) limit what a token can do: a read-only token issued to a budgeting app cannot move money even though it belongs to the same customer.

Web pages in the same Laravel app do not need tokens at all: the auth middleware uses the session cookie. Use tokens only for clients that are not your own Blade pages: mobile apps, SPAs on another domain, partners.

Ownership check with a policy (authorization)

class AccountPolicy
{
    public function view(User $user, Account $account): bool
    {
        return $account->customer_id === $user->customer_id;
    }
}

public function show(Account $account)
{
    $this->authorize('view', $account);   // 403 if it is someone else's account
    return new AccountResource($account->load('recentTransactions'));
}

4. Spring Boot: Spring Security with JWT

Resource server that validates JWTs and checks roles

@Configuration
@EnableMethodSecurity
public class SecurityConfig {
    @Bean
    SecurityFilterChain api(HttpSecurity http) throws Exception {
        return http
            .csrf(csrf -> csrf.disable())                         // stateless API, no cookies
            .sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/actuator/health", "/api/login").permitAll()
                .requestMatchers(HttpMethod.POST, "/api/loans/*/approve").hasRole("BRANCH_MANAGER")
                .anyRequest().authenticated())
            .oauth2ResourceServer(o -> o.jwt(Customizer.withDefaults()))   // verifies signature + expiry
            .build();
    }
}

@RestController
@RequestMapping("/api/accounts")
class AccountController {
    @GetMapping("/{id}")
    @PreAuthorize("@accountGuard.owns(authentication, #id)")      // ownership, not just role
    public AccountDto get(@PathVariable Long id) { return service.find(id); }
}

Spring Security is a chain of servlet filters. The JWT filter authenticates and stores an Authentication object in the SecurityContext; @PreAuthorize then authorizes at method level. The idea is the same as Laravel's auth middleware followed by a policy.

5. React: storing and sending tokens safely

API client with an in-memory access token and refresh on 401

let accessToken = null;                       // kept in memory, not localStorage

export async function api(path, options = {}) {
  const send = () => fetch(`/api${path}`, {
    ...options,
    credentials: 'include',                   // sends the HttpOnly refresh cookie
    headers: { 'Content-Type': 'application/json', ...(accessToken && { Authorization: `Bearer ${accessToken}` }) },
  });
  let res = await send();
  if (res.status === 401) {                   // access token expired: rotate once, then retry
    const r = await fetch('/api/auth/refresh', { method: 'POST', credentials: 'include' });
    if (!r.ok) { window.location.assign('/login'); return; }
    accessToken = (await r.json()).access_token;
    res = await send();
  }
  return res;
}

// Route guard: UX only, the server still authorizes every call
function RequireRole({ role, children }) {
  const { user } = useAuth();
  return user?.roles.includes(role) ? children : <Navigate to="/forbidden" />;
}

Anything in localStorage can be read by any script on the page, so one XSS bug leaks the token. Keeping the short-lived access token in memory and the refresh token in an HttpOnly, Secure, SameSite cookie limits that damage. Hiding a button in React is never security; the API must check again.

6. Roles and permissions (RBAC) with the MySQL design

Role-Based Access Control groups permissions into roles and assigns roles to users. Code checks permissions ("loans.approve"), not role names, so a new role such as "Deputy Manager" needs no code change, only rows in the database.

Real systems add conditions roles cannot express: a branch manager may approve loans only for their own branch and only up to Rs 25 lakh. Those rules live in policies (attribute-based checks) on top of RBAC.

roles / permissions / role_user / permission_role

roles / permissions / role_user / permission_role — educational schema
ColumnType / constraint
roles.id, roles.nameCUSTOMER, TELLER, BRANCH_MANAGER, AUDITOR
permissions.id, permissions.nameaccounts.view, transfers.create, loans.approve, reports.export
permission_role(role_id, permission_id)many-to-many, composite PK
role_user(user_id, role_id, branch_id NULL)role scoped to a branch when needed

Packages such as spatie/laravel-permission create exactly these tables.

Checking permissions plus a limit

// LoanPolicy
public function approve(User $user, Loan $loan): Response
{
    if (! $user->hasPermissionTo('loans.approve')) return Response::deny('Not allowed to approve loans.');
    if ($loan->branch_id !== $user->branch_id) return Response::denyAsNotFound();
    if ($loan->amount > $user->approval_limit) return Response::deny('Above your approval limit; escalate to regional office.');
    return Response::allow();
}
Roles in the three example systems
SystemRolesRule that roles alone cannot express
BankingCustomer, Teller, Branch Manager, Auditor (read-only)Maker-checker: the person who creates a payment cannot approve it
E-commerceShopper, Seller, Support agent, AdminA seller edits only their own products
Payment gatewayMerchant owner, Developer, Finance, Read-onlyRefunds above Rs 50,000 need the owner role and 2FA

7. Interview questions

Session or JWT for a banking web app?

Sessions (or opaque tokens): they can be revoked instantly on logout, password change or fraud alerts. JWTs suit distributed services where a central lookup is expensive, but they need very short expiry plus refresh-token rotation.

How do you log out a JWT user?

Delete the refresh token so no new access token can be issued, and keep access tokens short. For immediate revocation keep a deny-list of token ids (jti) in Redis until they expire.

What is IDOR?

Insecure Direct Object Reference: an authenticated user changes /orders/187 to /orders/188 and sees another person's order. Fix it with ownership checks (policies or scoped queries) on every record.

Where should the role check happen in a React + Laravel app?

In Laravel (middleware, policies). React checks only decide what to show.