---
title: "07 — Authentication: Who Is Calling?"
uid: authentication
tags: ["security", "basic-auth", "oidc", "authentication", "jwt", "roadmap:api-design", "session", "oauth2"]
excerpt: "Authentication is 'who are you,' and every method — Basic, sessions, bearer tokens, JWTs, OAuth 2.0, OIDC — is a different way to carry and verify a proof of identity. The right one depends on who the client is."
date: 2026-08-13T03:28:33+0000
source: https://www.aveshina.my.id/en/blog/authentication
---

"Add a login" used to mean one thing to me until I saw three very different clients — a script, a browser app, a third party — each needing a different proof of identity. The model that finally clicked: **every authentication method is a different way to carry and verify a proof of identity, and the choice is driven by who the client is and how much complexity they can bear.** [1] A script calling an internal API, a browser app with a logged-in user, and a third-party asking to act on a user's behalf are three very different problems, and the methods exist because no single one fits all three.

The thread connecting these methods is a tradeoff curve: **simplicity on one end, delegated trust on the other.** Basic Auth is trivially simple and weak. Sessions add server-side memory. Tokens move the proof to the client. JWTs make the proof self-verifying. OAuth 2.0 delegates the identity check to someone the user already trusts. OIDC adds "and here's who they are" on top of OAuth's "and here's what you may do." Each step adds power and complexity.

```figure
<svg viewBox="0 0 720 280" xmlns="http://www.w3.org/2000/svg" class="my-6 w-full max-w-2xl" role="img" aria-label="Authentication methods on a spectrum. Left to right: Basic Auth (simple, weak), Session (server memory), Token/JWT (stateless, self-verifying), OAuth 2.0 (delegated authorization), OIDC (identity on top of OAuth). An arrow labelled 'simplicity →' points left and 'delegated trust →' points right.">
  <g font-family="ui-sans-serif, system-ui, sans-serif" text-rendering="geometricPrecision">
    <text x="360" y="22" font-size="12" font-weight="700" fill="#475569" text-anchor="middle">simplicity  ←  →  delegated trust</text>

    <!-- Basic -->
    <rect x="30" y="60" width="120" height="60" rx="8" fill="#fee2e2" stroke="#dc2626" stroke-width="1.4"/>
    <text x="90" y="84" font-size="12" font-weight="700" fill="#7f1d1d" text-anchor="middle">Basic Auth</text>
    <text x="90" y="102" font-size="9" fill="#7f1d1d" text-anchor="middle">user:pass in header</text>

    <!-- Session -->
    <rect x="165" y="60" width="120" height="60" rx="8" fill="#fef9c3" stroke="#ca8a04" stroke-width="1.4"/>
    <text x="225" y="84" font-size="12" font-weight="700" fill="#422006" text-anchor="middle">Session</text>
    <text x="225" y="102" font-size="9" fill="#422006" text-anchor="middle">cookie + server store</text>

    <!-- Token/JWT -->
    <rect x="300" y="60" width="120" height="60" rx="8" fill="#dcfce7" stroke="#16a34a" stroke-width="1.4"/>
    <text x="360" y="84" font-size="12" font-weight="700" fill="#052e16" text-anchor="middle">Token / JWT</text>
    <text x="360" y="102" font-size="9" fill="#052e16" text-anchor="middle">stateless bearer</text>

    <!-- OAuth2 -->
    <rect x="435" y="60" width="120" height="60" rx="8" fill="#e0e7ff" stroke="#6366f1" stroke-width="1.4"/>
    <text x="495" y="84" font-size="12" font-weight="700" fill="#1e1b4b" text-anchor="middle">OAuth 2.0</text>
    <text x="495" y="102" font-size="9" fill="#1e1b4b" text-anchor="middle">delegated access</text>

    <!-- OIDC -->
    <rect x="570" y="60" width="120" height="60" rx="8" fill="#fce7f3" stroke="#db2777" stroke-width="1.4"/>
    <text x="630" y="84" font-size="12" font-weight="700" fill="#500724" text-anchor="middle">OIDC</text>
    <text x="630" y="102" font-size="9" fill="#500724" text-anchor="middle">+ identity layer</text>

    <!-- arrows -->
    <g font-size="9" fill="#64748b" text-anchor="middle">
      <path d="M150,90 L163,90" stroke="#64748b" stroke-width="1.2"/>
      <path d="M285,90 L298,90" stroke="#64748b" stroke-width="1.2"/>
      <path d="M420,90 L433,90" stroke="#64748b" stroke-width="1.2"/>
      <path d="M555,90 L568,90" stroke="#64748b" stroke-width="1.2"/>
    </g>

    <!-- client fit row -->
    <g font-size="10" fill="#64748b" text-anchor="middle">
      <text x="90" y="160">scripts,</text>
      <text x="90" y="174">internal</text>
      <text x="225" y="160">browser apps</text>
      <text x="225" y="174">(first-party)</text>
      <text x="360" y="160">mobile, SPA,</text>
      <text x="360" y="174">service-to-service</text>
      <text x="495" y="160">third-party apps</text>
      <text x="495" y="174">acting for a user</text>
      <text x="630" y="160">"Sign in with</text>
      <text x="630" y="174">Google/GitHub"</text>
    </g>
  </g>
</svg>
```

## Authentication vs. authorization

One distinction I had to nail down before any of this made sense: **authentication asks "who are you"; authorization asks "what are you allowed to do."** This post is about the first. The methods here prove identity; the next post in the series covers the access-control models that decide what an authenticated caller may then touch. OAuth 2.0 straddles the line — it's technically an authorization framework — which is exactly why OIDC exists as a separate identity layer on top of it.

## Basic Auth: the simplest, weakest

**Basic Authentication** sends the username and password on every request, base64-encoded in the Authorization header [2]:

```
Authorization: Basic dXNlcjpwYXNzd29yZA==
```

The thing to internalize: **base64 is encoding, not encryption.** [2] Anyone who intercepts the request reads the credentials instantly. Basic Auth is only acceptable over HTTPS, and even then it's weak because the raw password is sent on every call. It's fine for quick internal scripts or dev APIs, and unacceptable for anything public-facing. I treat Basic Auth as "the one you reach for when simplicity is the only priority and the channel is already trusted and encrypted."

## Session-based auth: the server remembers

**Session-based authentication** is the classic web model: the user logs in once with credentials, the server creates a **session** and stores it server-side, and the session ID travels in a cookie on every subsequent request [3]. The server looks up the session to identify the caller.

The tradeoff is server-side state. Every authenticated request hits the session store, which means sessions don't scale across servers without shared storage (Redis, a database). Sessions are a natural fit for rendered web apps where the browser handles cookies automatically, and a poor fit for mobile or third-party clients where cookie semantics and CSRF concerns get awkward. The model I hold: sessions optimize for "the server is in charge of remembering"; tokens (next) optimize for "the client carries the proof."

## Token-based auth: the client carries the proof

**Token-based authentication** flips the storage: after login, the server issues a **token** (an opaque string or a JWT), and the client includes it in the Authorization header of every request [4]. The server verifies the token instead of looking up a session.

```
Authorization: Bearer eyJhbGciOi...
```

Two properties make tokens attractive. They're **stateless** — the server can verify them without a session store, which scales horizontally. And they're **client-agnostic** — a mobile app, a SPA, and a service-to-service call all use the same Bearer header pattern, no cookies required. The cost is that you now own token issuance, expiration, and revocation, none of which a session store handled for you.

## JWT: a self-verifying token

A **JSON Web Token (JWT)** is a specific, popular token format — a compact, URL-safe string with three base64-encoded parts separated by dots [5]:

```
eyJhbGciOi... . eyJzdWIiOi... . SflKxwRJSm...
   header            payload        signature
```

- **Header** — the algorithm used to sign.
- **Payload** — claims about the caller (user ID, roles, expiry).
- **Signature** — the server's cryptographic signature over header+payload.

The signature is the clever part: because the server signed it, any server with the verification key can confirm the token is genuine and untampered **without a database lookup** [5]. That's what makes JWTs stateless. The catches I watch for: JWTs are unreadable once issued (you can't easily revoke one before its exp), so keep expiries short and use refresh tokens for long sessions. And never put secrets in the payload — it's only base64-encoded, not encrypted; anyone who holds the token can read it.

## OAuth 2.0: delegated authorization

**OAuth 2.0** solves a different problem: letting a third-party app act on a user's behalf *without the user giving that app their password.* [6] When you "connect" an app to your GitHub account, that's OAuth 2.0. The user authenticates with the provider (GitHub), and the provider issues the third-party an **access token** scoped to specific permissions — never the user's password.

OAuth defines four roles: the **resource owner** (the user), the **client** (the third-party app), the **authorization server** (issues tokens), and the **resource server** (your API, validates tokens). The flows (Authorization Code, Client Credentials, etc.) are how tokens get issued for different scenarios. The reason OAuth matters for API design: it's the standard way to let other apps call your API on behalf of users *without* the security catastrophe of password sharing.

## OIDC: identity on top of OAuth

Here's the subtlety that used to confuse me: **OAuth 2.0 is authorization, not authentication.** It answers "may this app do X," not "who is the user." **OpenID Connect (OIDC)** is the thin identity layer added on top of OAuth 2.0 to answer the second question [7]. OIDC introduces an **ID token** — a signed JWT containing claims about the authenticated user (name, email, sub).

OIDC is the standard behind "Sign in with Google / GitHub / Apple." When you see that button, you're using OIDC: the provider authenticates the user, returns an ID token proving who they are, and your API trusts that proof because it's signed by a provider you configured. The rule I use: if my API needs to verify *identity* (login), it's OIDC; if it only needs to grant *access* (a token to call an API), plain OAuth 2.0 suffices. Most real systems end up with both.

## How I use this

The choice is a matrix of "who is the client." Internal script or dev tool over HTTPS — Basic Auth is tolerable. First-party browser app with rendered pages — sessions. Mobile app, SPA, or service-to-service — bearer tokens, usually JWTs. Third-party apps acting on a user's behalf — OAuth 2.0. "Sign in with X" or any federated login — OIDC on top of OAuth 2.0. The mistake I try to avoid is using one method for every situation — putting raw Basic Auth on a public API, or reaching for full OAuth when a simple JWT would do. Each method optimizes for a specific client and threat model; the work is matching them.

## References

[1] Postman, "API Authentication," 2024. [Online]. Available: [https://www.postman.com/api-platform/api-authentication/](https://www.postman.com/api-platform/api-authentication/)

[2] Swagger, "Basic Authentication," 2024. [Online]. Available: [https://swagger.io/docs/specification/authentication/basic-authentication/](https://swagger.io/docs/specification/authentication/basic-authentication/)

[3] Authgear, "Session vs Token Authentication," 2024. [Online]. Available: [https://www.authgear.com/post/session-vs-token-authentication](https://www.authgear.com/post/session-vs-token-authentication)

[4] Okta, "What Is Token-Based Authentication?," 2024. [Online]. Available: [https://www.okta.com/uk/identity-101/what-is-token-based-authentication/](https://www.okta.com/uk/identity-101/what-is-token-based-authentication/)

[5] jwt.io, "Introduction to JSON Web Tokens," 2024. [Online]. Available: [https://jwt.io/introduction](https://jwt.io/introduction)

[6] auth0, "What is OAuth 2.0?," 2024. [Online]. Available: [https://auth0.com/intro-to-iam/what-is-oauth-2](https://auth0.com/intro-to-iam/what-is-oauth-2)

[7] Fortinet, "OIDC: Simplifying Secure Authentication With OpenID Connect," 2024. [Online]. Available: [https://www.fortinet.com/resources/cyberglossary/oidc](https://www.fortinet.com/resources/cyberglossary/oidc)

```quiz
Q: Authentication answers which question?
- "What is the caller allowed to do?"
- "Who is the caller?"
correct: 1
explain: Authentication verifies identity (who). Authorization decides permissions (what they may do). The next post in the series covers authorization.

Q: Why is Basic Auth considered weak even over a reliable channel?
- The raw username:password travels on every request, only base64-encoded (which is not encryption)
- It can't be used with HTTPS
correct: 0
explain: Base64 is encoding, not encryption. The credentials are recoverable by anyone who sees the request, and they're sent on every call.

Q: A JWT can be verified without a database lookup because…
- the token carries the server's cryptographic signature over its header and payload
- JWTs are encrypted with the caller's public key
correct: 0
explain: The signature lets any server with the verification key confirm the token is genuine and untampered — stateless verification, no session store needed.

Q: You see a "Sign in with Google" button. Which standard is most likely providing the identity proof?
- Plain OAuth 2.0
- OpenID Connect (OIDC)
correct: 1
explain: OAuth 2.0 is authorization only. OIDC is the identity layer on top that issues a signed ID token proving who the user is — that's what federated login uses.

Q: Why do JWTs need short expiries and a refresh-token flow?
- because a JWT can't be easily revoked before its expiry once issued
- because JWTs expire automatically after 1 minute
correct: 0
explain: JWTs are stateless and self-verifying, so there's no built-in revocation. Keeping expiries short (and using refresh tokens) limits the damage if a token leaks.
```
