<!--
  Authentication & Authorization
  From the Iris book, chapter 10, covering Iris 14.0.1 on Go 1.27.
  Import path: github.com/kataras/iris/v14
  Canonical page: https://iris-go.com/docs/authentication-authorization/
-->
# Chapter 10: Authentication & Authorization

Iris ships several layers of authentication support, from a single-line HTTP
Basic Auth middleware to a complete OAuth2 identity server. This chapter helps
you pick the right layer, then walks through each one: `middleware/basicauth`
for internal surfaces, the generic `auth` SDK for token-based user-facing
APIs, `middleware/jwt` for raw token signing and verification,
`middleware/sessions` for browser applications, and a tour of the
`auth/identity` and `auth/oauth2` packages. Authentication answers *who* the
caller is; authorization answers *what* the caller may do. The chapter closes
the loop with plain guard handlers for roles and permissions.

## Background

### Two Questions, Two Answers

Authentication and authorization are separate problems that are easy to
blur. Authentication establishes identity: is this request really from
Maria? Authorization decides rights: Maria is real, but may she delete
this record? HTTP encodes the difference in two status codes you will
meet throughout the chapter: 401 means "I do not know who you are", 403
means "I know who you are, and the answer is no".

### Credentials, Tokens, Sessions and Cookies

A credential is the proof of identity a caller presents: a username and
password, an API key, a client certificate. The complication is that
HTTP is stateless (the server forgets you the moment a response is
sent), so *every* request must carry proof. Sending the password itself
each time (which is exactly what Basic Authentication does) multiplies
the chances of it leaking, so most systems exchange the password once
for something weaker and shorter-lived.

That something takes two shapes. A token is a string the server issues
after one successful credential check; the client attaches it to later
requests instead of the password. Most tokens are *bearer* tokens:
whoever holds one is treated as the user, no further questions, which
is why tokens expire quickly and why revocation matters. A session is
the server-side variant: the server keeps the signed-in state in its own
storage, and the client holds only a random session identifier that
points at it. Tokens push state to the client; sessions keep it on the
server.

A cookie is the browser-side transport for either. It is a small piece
of data a server asks the browser to store, which the browser then sends
back automatically with every request to that site. Three attributes
recur in this chapter: `HttpOnly` (page scripts cannot read the cookie,
which blunts token theft via injected JavaScript), `Secure` (sent over
HTTPS only) and `SameSite` (limits sending on cross-site requests, the
raw material of request forgery).

### How a JWT Works

A JSON Web Token (JWT, RFC 7519) is a token with structure: three
base64url-encoded parts (header, payload, signature) joined by dots.
The payload carries *claims*: statements such as the user's ID, roles
and an expiry time. The signature is computed over the other two parts
with a key, so a verifier can prove the token was issued by the key
holder and has not been altered, without a database lookup. That
self-contained verification is the JWT's whole appeal for APIs. The
catch to internalize: a JWT is signed, not encrypted. Anyone who obtains
one can decode and read every claim, so the payload must never contain
secrets, which is why this chapter insists the user type embedded in
tokens stays small and clean. And because a stateless token cannot be
recalled once issued, access tokens are kept short-lived and paired with
refresh tokens or server-side revocation checks.

### Password Hashing

Storing passwords as-is turns any database leak into a catastrophe:
every account is compromised at once, and (since people reuse
passwords) so are their accounts elsewhere. The remedy is hashing: a
one-way function transforms the password into a fixed string that
cannot be reversed; you store only the hash and re-run the function at
sign-in to compare. General-purpose hashes like SHA-256 are too *fast*:
attackers test billions of guesses per second offline. Password hashing
therefore uses deliberately slow, salted algorithms such as bcrypt, the
one used in this chapter's examples. The salt is a random per-user value
mixed in so identical passwords produce different hashes and
precomputed lookup tables are useless.

## Choosing an Approach

| You are building                                                         | Reach for                           |
| ------------------------------------------------------------------------ | ----------------------------------- |
| An internal or admin surface with a handful of operators                 | `middleware/basicauth`              |
| Another service, a cron job or a webhook sender, with no user to sign in | `middleware/apikey`                 |
| A user-facing API with access and refresh tokens                         | the `auth` package (`auth.Auth[T]`) |
| A resource server that only verifies tokens issued elsewhere             | `middleware/jwt`                    |
| A browser application with server-side state                             | `middleware/sessions`               |
| A standalone identity provider for many applications                     | `auth/identity` + `auth/oauth2`     |

The layers compose. A typical production application protects its admin
routes with basic auth, its public API with the auth SDK, and adds small
guard handlers for role checks, all registered per router, so protection
lives where the routes are mounted.

## Basic Authentication

HTTP Basic Authentication sends a username and password with every request.
It requires no token infrastructure, which makes it the right tool for
internal dashboards and admin APIs served over HTTPS, and the wrong tool
for anything user-facing.

(On the wire it is an `Authorization: Basic` header holding
`username:password` base64-encoded. Base64 is an encoding, not
encryption: anyone who can observe the traffic can read the
credentials, which is why HTTPS is non-negotiable here.)

The middleware is a plain `iris.Handler`, so it applies wherever you mount
it, a specific child router, not the whole application:

```go
package main

import (
    "github.com/kataras/iris/v14"
    "github.com/kataras/iris/v14/middleware/basicauth"
)

func main() {
    app := iris.New()

    admin := app.Router("/admin", basicauth.Default(map[string]string{
        "admin": "admin_password",
        "ops":   "ops_password",
    }))

    admin.Get("/stats", func(ctx iris.Context) {
        username, _ := ctx.User().GetUsername()
        ctx.Writef("hello, %s", username)
    })

    app.Listen(":8080")
}
```

On success the middleware stores the authenticated user on the request;
`ctx.User()` retrieves it. Unauthenticated requests receive a 401 challenge
automatically.

In builder-based applications the same handler slots into the `API` step,
exactly as [Chapter 3](https://iris-go.com/docs/iris-newbuilder/) shows:

```go
API("/admin", basicauth.Default(map[string]string{
    cfg.AdminUser: cfg.AdminPassword,
}), api.NewAdminAPI).
```

### Hashed Passwords from a File

Never commit plain-text passwords. `basicauth.Load` reads a JSON or YAML
user list and, with the `BCRYPT` option, compares the user input against
bcrypt hashes:

```yaml
# users.yml — either a flat "username: password" map or a list of
# entries with at least username and password fields.
- username: admin
  password: "$2a$10$..." # bcrypt hash
  role: admin
- username: ops
  password: "$2a$10$..."
  role: operator
```

```go
auth := basicauth.Load("users.yml", basicauth.BCRYPT)
```

`Load` keeps each entry as a `basicauth.Map`, so the `role` above survives.
To decode the same file into your own struct instead, pass
`basicauth.AllowUsersFile[Operator]("users.yml", basicauth.BCRYPT)` as the
`Allow` of `basicauth.New`, described next.

(The `$2a$10$` prefix of a bcrypt hash records the algorithm variant and
its cost factor, here 2^10 internal rounds. Raising the cost makes
every guess proportionally slower for an attacker, at the price of a
slower comparison at sign-in.)

### Custom Validation and Rate Limiting

`basicauth.New` accepts an `Options[U]` struct, typed by your own user,
when you need database-backed users, credential expiration or a failure
limit:

```go
import (
    "time"

    "github.com/kataras/iris/v14"
    "github.com/kataras/iris/v14/middleware/basicauth"

    "golang.org/x/crypto/bcrypt" // third-party
)

func newAdminAuth(repo *OperatorRepository) iris.Handler {
    return basicauth.New(basicauth.Options[*Operator]{
        Realm:    basicauth.DefaultRealm,
        MaxAge:   2 * time.Hour, // re-ask for credentials after 2h.
        MaxTries: 3,             // 403 after 3 failures.
        GC:       basicauth.GC{Every: 3 * time.Hour},
        Allow: func(ctx iris.Context, username,
            password string) (*Operator, bool) {
            op, err := repo.GetByUsername(ctx, username)
            if err != nil {
                return nil, false
            }

            err = bcrypt.CompareHashAndPassword(
                []byte(op.PasswordHash), []byte(password))
            if err != nil {
                return nil, false
            }

            return op, true
        },
    })
}
```

Whatever `Allow` returns is what the handlers get back, typed:

```go
admin.Get("/me", func(ctx iris.Context) {
    op, _ := basicauth.GetUser[*Operator](ctx) // no type assertion
    since, _ := basicauth.AuthorizedAt(ctx)     // first login, needs MaxAge
    ctx.JSON(iris.Map{"username": op.Username, "since": since})
})
```

`ctx.User()` still returns the same value as a `context.User`, and
`ctx.Logout()` forgets the login. For a static list of your own type,
`basicauth.AllowUsers(operators)` takes a `[]Operator` and reads the
credentials from the `Username` and `Password` fields (json tags count),
or from wherever a `basicauth.Credentials` option points.

## API Keys for Machine Callers

Basic auth and the SDK both assume a person: a password to remember, a
session to end, a token to refresh. A cron job, a webhook sender, a
reporting service or a CLI tool has none of those. It has one long random
string it was handed once and presents on every request, and what the
server owes it is a constant-time comparison and a name in the log.

`middleware/apikey` is that. The key arrives on a header, by default
`Authorization: Bearer <key>`, and the middleware matches it against the
configured keys:

```go
package main

import (
    "os"

    "github.com/kataras/iris/v14"
    "github.com/kataras/iris/v14/middleware/apikey"
)

func main() {
    app := iris.New()

    guard := apikey.New(apikey.Options{
        Keys: map[string]string{
            "reporting": os.Getenv("REPORTING_API_KEY"),
            "billing":   os.Getenv("BILLING_API_KEY"),
        },
    })

    api := app.Router("/api", guard)
    api.Get("/orders", func(ctx iris.Context) {
        principal, _ := apikey.Get(ctx) // "reporting" or "billing".
        ctx.JSON(iris.Map{"called_by": principal})
    })

    app.Listen(":8080")
}
```

`Keys` maps the principal to its secret and not the other way round, so
two callers cannot share an entry: one key per consumer is what makes a
leak traceable and a revocation survivable.

**The default is to deny.** A middleware with no `Keys` and no `Lookup`
rejects every request and logs a warning on the first one, rather than
letting everyone through while the deployment is missing its
configuration. `AllowAnonymous` opens that door for local development and
is ignored the moment a key exists.

The scheme is matched case-insensitively, per RFC 7235. A caller that
cannot set `Authorization` gets its own header instead, and then the value
is bare:

```go
apikey.New(apikey.Options{
    Header: "X-API-Key",
    Scheme: apikey.NoScheme, // the "-" sentinel: accept a bare value.
    Keys:   keys,
})
```

`NoScheme` is a sentinel rather than the empty string on purpose: an
unset `Scheme` field keeps the `Bearer` default, because a field nobody
filled in must not turn a check off.

Two options keep the working credential out of the configuration. `Hash`
is applied to the presented key before comparison, so `Keys` hold digests:

```go
apikey.New(apikey.Options{
    Hash: apikey.SHA256,
    Keys: map[string]string{
        "reporting": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
    },
})
```

A shared secret is a high-entropy random string and not a password, so a
plain digest is the right hash here: there is nothing to brute-force.
`Lookup` covers the keys that live in a table, and is consulted only after
the static keys miss:

```go
apikey.New(apikey.Options{
    Lookup: func(ctx iris.Context, key string) (string, bool, error) {
        row, err := tokens.FindByHash(ctx, apikey.SHA256(key))
        if stderrors.Is(err, sql.ErrNoRows) {
            return "", false, nil // not a match: a 401.
        }
        if err != nil {
            return "", false, err // the store is down: a 500.
        }

        return row.Name, !row.Revoked, nil
    },
})
```

The distinction in that last branch matters: an error is a 500 and not a
401, because a database that is down must not read to the caller as an
invalid key.

`Options.Query` reads the key from a query parameter when the header is
absent, for a caller that cannot set headers at all. Turn it on only after
reading what it costs: a key in a URL ends up in browser history, proxy
logs and referrer headers, and, when `middleware/accesslog` is registered,
in your own access log, which records every query argument. Its `Redact`
hook covers request and response bodies only, so the parameter has to be
dropped or masked in a custom formatter (`ac.SetFormatter`) before you
enable it.

On success the principal is on the context. `apikey.Get(ctx)` returns it,
and when nothing else has set a user the middleware also stores a
`context.SimpleUser` whose `ID` and `Username` are that principal, so code
written against `ctx.User()` sees the caller without knowing where the
credential came from. An existing user is never replaced.

The rejection reasons are `apikey.ErrMissing` (no key, or a value the
scheme does not fit) and `apikey.ErrInvalid` (a key that matched nothing),
wrapped into the error an `ErrorHandler` receives, so the two can be
logged differently. The default renders the `rest/errors`
`Unauthenticated` envelope.

## The Auth SDK

The `auth` package is the recommended way to secure a user-facing API. Its
central type, `auth.Auth[T]`, signs and verifies JSON Web Tokens for your
own user type `T`. It ships ready-made handlers for the whole token
lifecycle (`SigninHandler`, `RefreshHandler`, `SignoutHandler`,
`SignoutAllHandler`) plus a `VerifyHandler` middleware, EdDSA (Ed25519)
signatures, encrypted refresh tokens with rotation, and optional secure
cookie storage for browser clients.

You supply two things: a configuration (the keys) and a `Provider`
(the user store).

A note on the key material before the YAML below: EdDSA is an
asymmetric signature algorithm (Ed25519 key pairs). Asymmetric means the
two keys split the roles (the private key signs tokens, the public key
can only verify them), so other services can check your tokens without
ever holding a key that could forge one. The `/.well-known/jwks.json`
endpoint you will meet shortly publishes exactly that public key, in the
standard JSON Web Key Set format. Symmetric algorithms such as HS256 use
one shared secret for both jobs, which is simpler but means every
verifier can also mint tokens.

### Configuration and Keys

The configuration declares which headers carry the token, an optional
cookie, and the signing keys. It usually loads from a YAML file:

```yaml
# auth.yml
Headers:
  - "Authorization"
  - "X-Authorization"
Cookie: # optional; enables encrypted cookie storage for browsers.
  Name: "iris_auth_cookie"
  Secure: true
  Hash: "..."  # 64 characters, HMAC authentication.
  Block: "..." # 32 characters, AES-256 encryption.
Keys:
  - ID: IRIS_AUTH_ACCESS # required: short-lived access tokens.
    Alg: EdDSA
    MaxAge: 2h
    Private: |+
      -----BEGIN PRIVATE KEY-----
      ...
      -----END PRIVATE KEY-----
    Public: |+
      -----BEGIN PUBLIC KEY-----
      ...
      -----END PUBLIC KEY-----
  - ID: IRIS_AUTH_REFRESH # optional: enables the refresh flow.
    Alg: EdDSA
    MaxAge: 720h # 30 days; then the user must sign in again.
    EncryptionKey: "..." # refresh tokens travel encrypted.
    Private: |+ # own key pair, same PEM shape as above.
      ...
    Public: |+
      ...
```

The `IRIS_AUTH_ACCESS` key is mandatory; adding `IRIS_AUTH_REFRESH`
enables refresh tokens. If the file does not exist, the load error prints
a freshly generated example configuration you can start from. Loading
options:

```go
// One call: bind the file and construct the instance (panics on error).
sso := auth.MustLoad[Customer]("auth.yml")

// Or bind manually, e.g. when auth config is part of your app config
// (see Chapter 11):
var cfg auth.Configuration
if err := cfg.BindFile("auth.yml"); err != nil {
    log.Fatal(err)
}
sso := auth.Must[Customer](cfg)

// Development and tests only: random, non-persistent keys.
var devCfg auth.Configuration
_ = devCfg.BindRandom()
```

### The User Type and a Database-Backed Provider

`T` is any struct: it becomes the JWT payload, so keep it small and free
of secrets. A `Provider[T]` connects the SDK to your user store:

```go
type Provider[T User] interface {
    Signin(ctx context.Context, username, password string) (T, error)
    Refresh(ctx context.Context, t T) (T, error)
    ValidateToken(ctx context.Context, claims StandardClaims, t T) error
    InvalidateToken(ctx context.Context, claims StandardClaims, t T) error
    InvalidateTokens(ctx context.Context, t T) error
}
```

`Signin` runs on sign-in, `Refresh` before a new token pair is issued,
`ValidateToken` on every verified request, and the two `Invalidate`
methods implement server-side sign-out. A complete provider against a SQL
database, with a single `revocations` table for both "sign out this
token" and "sign out everywhere":

```go
package main

import (
    "context"
    "database/sql"
    "fmt"
    "time"

    "github.com/kataras/iris/v14/auth"

    "golang.org/x/crypto/bcrypt" // third-party
)

// Customer is embedded in the token payload: small, no secrets.
type Customer struct {
    ID     string   `json:"id"`
    Email  string   `json:"email"`
    Roles  []string `json:"roles"`
    Active bool     `json:"active"`
}

// CustomerProvider implements auth.Provider[Customer].
//
// Schema used below:
//
//    customers(id, email, active, password_hash)
//    revocations(token_id, customer_id, revoked_at)
type CustomerProvider struct {
    DB *sql.DB
}

func (p *CustomerProvider) Signin(ctx context.Context,
    username, password string) (Customer, error) {
    var (
        c    Customer
        hash string
    )

    err := p.DB.QueryRowContext(ctx,
        `SELECT id, email, active, password_hash
         FROM customers WHERE email = $1`, username).
        Scan(&c.ID, &c.Email, &c.Active, &hash)
    if err != nil {
        return Customer{}, fmt.Errorf("invalid credentials")
    }

    err = bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
    if err != nil {
        return Customer{}, fmt.Errorf("invalid credentials")
    }

    return c, nil
}

// Refresh reloads the customer before a new token pair is signed,
// so profile or role changes propagate on the next refresh.
func (p *CustomerProvider) Refresh(ctx context.Context,
    c Customer) (Customer, error) {
    err := p.DB.QueryRowContext(ctx,
        `SELECT id, email, active FROM customers WHERE id = $1`,
        c.ID).Scan(&c.ID, &c.Email, &c.Active)
    return c, err
}

// ValidateToken runs on every verified request. A token is rejected if
// its ID (or, for refresh tokens, the access-token ID it originates
// from) was revoked, or if all of the customer's tokens issued before a
// "sign out everywhere" event are revoked.
func (p *CustomerProvider) ValidateToken(ctx context.Context,
    claims auth.StandardClaims, c Customer) error {
    var revoked bool
    err := p.DB.QueryRowContext(ctx,
        `SELECT EXISTS (SELECT 1 FROM revocations
         WHERE token_id IN ($1, $2)
            OR (customer_id = $3 AND revoked_at >= $4))`,
        claims.ID, claims.OriginID, c.ID, claims.IssuedAt).
        Scan(&revoked)
    if err != nil {
        return err
    }
    if revoked {
        return fmt.Errorf("token revoked")
    }
    return nil
}

func (p *CustomerProvider) InvalidateToken(ctx context.Context,
    claims auth.StandardClaims, c Customer) error {
    _, err := p.DB.ExecContext(ctx,
        `INSERT INTO revocations (token_id, revoked_at)
         VALUES ($1, $2)`, claims.ID, time.Now().Unix())
    return err
}

func (p *CustomerProvider) InvalidateTokens(ctx context.Context,
    c Customer) error {
    _, err := p.DB.ExecContext(ctx,
        `INSERT INTO revocations (customer_id, revoked_at)
         VALUES ($1, $2)`, c.ID, time.Now().Unix())
    return err
}
```

For fully stateless deployments, `ValidateToken` and the `Invalidate`
methods may simply return nil; tokens then remain valid until they
expire. The context passed to every method is the request's
`iris.Context` when invoked through the built-in handlers, so a provider
can inspect the request if it needs to.

### Registering as a Service

Construct the instance once, attach the provider, and register it as a
service so any controller can receive it:

```go
package main

import (
    "database/sql"
    "log"

    "github.com/kataras/iris/v14"
    "github.com/kataras/iris/v14/auth"
)

func main() {
    db, err := sql.Open("pgx", "postgres://localhost/shop")
    if err != nil {
        log.Fatal(err)
    }

    sso := auth.MustLoad[Customer]("auth.yml")
    sso.AddProvider(&CustomerProvider{DB: db})

    app := iris.NewBuilder(iris.Defaults()).
        Prefix("/api").
        AllowOrigin("https://shop.example.com").
        Compression(true).
        LogRequests(true).
        NoHealth().
        Deferrables(db).
        Services(db, sso).
        Middlewares().
        API("/account", NewAccountAPI).
        Build()

    // Optional, at the root: lets other services verify your tokens.
    app.Get("/.well-known/jwks.json", sso.JWKSHandler())

    app.Listen(":8080")
}
```

### Verifying Requests per Router

The controller receives the `*auth.Auth[Customer]` through its
constructor and applies `VerifyHandler` exactly where protection is
needed: sign-in and refresh stay public, everything under `/me` requires
a valid token:

```go
package main

import (
    "github.com/kataras/iris/v14"
    "github.com/kataras/iris/v14/auth"
)

type AccountAPI struct {
    sso *auth.Auth[Customer]
}

func NewAccountAPI(sso *auth.Auth[Customer]) *AccountAPI {
    return &AccountAPI{sso: sso}
}

func (api *AccountAPI) Routing(r iris.Router) {
    r.Post("/signin", api.sso.SigninHandler)
    r.Post("/refresh", api.sso.RefreshHandler)

    me := r.Router("/me", api.sso.VerifyHandler())
    me.Get("/", api.profile)
    me.Post("/signout", api.sso.SignoutHandler)
    me.Post("/signout-all", api.sso.SignoutAllHandler)
}

func (api *AccountAPI) profile(ctx iris.Context) {
    ctx.OK(auth.GetUser[Customer](ctx), nil)
}
```

The flow, end to end, is four acts. In each one the SDK's handler is the
only thing your routes see; it drives your `Provider` and the key registry
behind it:

\[Diagram: Each Auth handler drives your Provider and the kid key registry: signin mints an access and refresh pair linked by OriginID, verify checks the access token and loads the user, refresh rotates both, and signout invalidates them.]

One consequence of that design is worth guarding against. `verify` accepts
any token signed by a *configured* key, and the refresh key is configured,
so a client that presents its refresh token as
`Authorization: Bearer <refresh_token>` passes `VerifyHandler`. The
refresh token's claims carry `OriginID` (the access token's `jti`) and an
access token's do not, so the check belongs in your `ValidateToken`:
require `claims.OriginID == ""` and reject anything else.

### Claims Checks with VerifyUserFunc

`VerifyHandler` accepts optional functions that further validate the
already-verified user. Use them for cheap, declarative checks that apply
to a whole router:

```go
import stdContext "context"

active := r.Router("/orders", api.sso.VerifyHandler(
    func(ctx stdContext.Context, c Customer) error {
        if !c.Active {
            return fmt.Errorf("account disabled")
        }
        return nil
    }))
```

A failing check produces the same 401 response as an invalid token,
through the instance's `ErrorHandler` (replaceable via
`SetErrorHandler`).

### Reading the User in Handlers

After `VerifyHandler`, three package-level helpers are available:

```go
// The verified user value.
user := auth.GetUser[Customer](ctx)

// The verified token: raw bytes plus standard claims.
token, ok := auth.GetToken(ctx)

// True when a verified user is stored on this request.
if auth.UserAuthorized(ctx) { /* ... */ }
```

### The Request Scope

In a multi-tenant API almost every query needs one value off the verified
user: the tenant, the customer, the organisation the request belongs to. A
handler that derives it again is a second chance to get it wrong, and a
handler that forgets to filter by it is a data leak. `auth.Scope` derives
it once, right after `VerifyHandler`, and every handler after that reads
it back:

```go
app.Use(a.VerifyHandler(), auth.Scope(func(u Customer) string {
    return u.TenantID
}))

func listInvoices(ctx iris.Context) {
    tenantID, ok := auth.ScopeID[string](ctx)
    if !ok {
        return
    }

    invoices, err := svc.InvoicesOf(ctx, tenantID)
    ctx.List(invoices, err)
}
```

`ScopeID` is a function of the `auth` package rather than a context
method, because the scope is stored by `auth`. It reports `false` when
`Scope` did not run or when it stored an id of another type, so a zero
id you must not trust cannot be mistaken for a real one.

Two rulings are worth knowing before you rely on it. A missing user is a
401 with `auth.ErrMissingUser`, the same failure `VerifyHandler` answers
with, and so is a user of a *different* type: reading the user untyped and
deriving a scope from a zero value would answer 200 with an empty scope,
which is a cross-tenant read wearing the disguise of the next sentence. A
derived *zero* id, on the other hand, is stored as it is, because a global
administrator may legitimately have no scope; guard those requests by
checking the id in the handler, not by expecting `Scope` to refuse them.

The cost is one `Values` read, the `derive` call and one `Values` set per
request; `ScopeID` is one map lookup and one type assertion. No reflection
is involved on either side. The stamping half, writing the scope onto
every body a client sends, is `mvc.BeforeWrite` in
[Chapter 24](https://iris-go.com/docs/advanced-topics/).

### Cookie Storage for Browser Clients

The cookie the SDK issues is both encrypted and authenticated: the
`Block` key encrypts the content (AES), and the `Hash` key signs it
(HMAC), so the browser stores the token but can neither read nor
undetectably alter it.

When `Cookie.Name` is set (with `Hash` and `Block`, both required),
`SigninHandler` also stores the access token in an encrypted, HttpOnly,
SameSite=Lax cookie, and `VerifyHandler` falls back to that cookie when
no `Authorization` header is present. The same `Auth[T]` instance then
serves API clients and browser pages alike. A sign-in request may carry a
`?redirect_to=/path` query; for safety, only same-site relative paths are
followed; absolute or scheme-bearing targets are ignored and the token
is returned as JSON instead.

## The JWT Middleware

`middleware/jwt` is the thin layer beneath the auth SDK: a `Signer` and a
`Verifier` without providers, refresh flows or user stores. Reach for it
when you only need to mint or verify tokens: for example, a resource
server that trusts tokens issued by another service.

```go
package main

import (
    "time"

    "github.com/kataras/iris/v14"
    "github.com/kataras/iris/v14/middleware/jwt"
    "github.com/kataras/iris/v14/rest/errors"
)

type userClaims struct {
    Username string `json:"username"`
}

func main() {
    secret := []byte("your-256-bit-secret-keep-it-safe")

    signer := jwt.NewSigner(jwt.HS256, secret, 15*time.Minute)
    verifier := jwt.NewVerifier(jwt.HS256, secret)

    app := iris.New()

    app.Post("/token", func(ctx iris.Context) {
        // Authenticate the caller first, then:
        token, err := signer.Sign(userClaims{Username: "makis"})
        if errors.HandleError(ctx, err) {
            return // logged, and a canonical INTERNAL body sent.
        }
        ctx.Write(token)
    })

    api := app.Router("/api",
        verifier.Verify(func() any { return new(userClaims) }))
    api.Get("/profile", func(ctx iris.Context) {
        claims, _ := jwt.Get[*userClaims](ctx)
        ctx.JSON(claims)
    })

    app.Listen(":8080")
}
```

`verifier.Verify` returns a middleware that extracts, verifies and
decodes the token, then exposes the claims through `jwt.Get[T](ctx)` and
the full token through `jwt.GetVerifiedToken(ctx)`. `T` is what the
constructor passed to `Verify` returns, here `*userClaims`; `Get`
reports false instead of panicking when the request carries no claims or
claims of another type. Invalid or missing tokens receive a 401 via the
verifier's replaceable `ErrorHandler`. The default one stores the reason
as a private error, so under `errors.Handler` the body reads
`"message":"Unauthorized"` whatever your error options map: a client
cannot read from it why its token was refused, and the reason stays in
your logs and collector.

Notes for v14:

- By default the verifier reads the `Authorization: Bearer <token>`
  header **only**. Tokens in the URL query string leak into access logs,
  proxy logs and browser history, so the `?token=` extractor is opt-in:
  `verifier.WithQueryToken()`.
- Server-side revocation is available through
  `verifier.WithDefaultBlocklist()` (in-memory); a blocklisted request
  can then be invalidated with `ctx.Logout()`.
- Both sides support AES-GCM payload encryption:
  `signer.WithEncryption(key, nil)` and
  `verifier.WithDecryption(key, nil)`.
- Production systems should prefer asymmetric algorithms (`jwt.EdDSA`,
  RSA/ECDSA variants) so verifying services never hold the signing key.
- The header check is exact: `alg`, `typ` and nothing else. A token from
  an identity provider such as Auth0 also names its key in a `kid` field,
  so it is refused with `jwt.ErrTokenAlg` until you set the verifier's
  optional `HeaderValidator` field. The `ValidateHeader` method of a
  `github.com/kataras/jwt` key set fits there. It loads the issuer's
  public keys from its JWKS URL and picks one by the `kid` of each token:

```go
import kjwt "github.com/kataras/jwt"

keySet, err := kjwt.NewRemoteKeySet(jwksURL)
if err != nil {
    panic(err)
}
defer keySet.Close()

verifier := jwt.NewVerifier(nil, nil)
verifier.HeaderValidator = keySet.ValidateHeader
```

## Role and Permission Guards

Authorization in Iris does not require a framework feature: a guard is a
plain handler that inspects the verified user and either calls
`ctx.Next()` or writes an error and stops. Combined with the
[rest/errors](https://iris-go.com/docs/error-handling/) codes, guards stay small and
composable:

```go
package main

import (
    "slices"

    "github.com/kataras/iris/v14"
    "github.com/kataras/iris/v14/auth"
    "github.com/kataras/iris/v14/rest/errors"
)

// RequireRoles allows the request only when the verified customer
// holds every required role. Register it AFTER VerifyHandler.
func RequireRoles(required ...string) iris.Handler {
    return func(ctx iris.Context) {
        user := auth.GetUser[Customer](ctx)

        for _, role := range required {
            if !slices.Contains(user.Roles, role) {
                errors.PermissionDenied.Message(ctx,
                    "missing required role: "+role)
                return
            }
        }

        ctx.Next()
    }
}
```

Mount guards per router, in order: authentication first, then
authorization:

```go
func (api *AccountAPI) Routing(r iris.Router) {
    // ...
    admin := r.Router("/admin",
        api.sso.VerifyHandler(), RequireRoles("admin"))
    admin.Get("/customers", api.listCustomers)
}
```

The status codes carry the semantics: a missing or invalid token is 401
(`errors.Unauthenticated`) because the caller is unknown; a known caller
lacking rights is 403 (`errors.PermissionDenied`). Fine-grained,
per-resource decisions ("is this user the owner of order 42?") belong in
the handler or service layer, where the resource is already loaded.
Compare against `auth.GetUser[Customer](ctx).ID` there instead of writing
a guard.

The same pattern works for permission strings, plan tiers or any other
claim your user type carries; because guards are ordinary handlers, they
also slot directly into the builder's `API` step:

```go
API("/admin", sso.VerifyHandler(), RequireRoles("admin"),
    NewAdminAPI).
```

## Session-Based Authentication

For browser applications that render pages and keep state on the server,
`middleware/sessions` provides typed sessions with secure-by-default
cookies. In v14, `sessions.NewOptions` returns options with `Secure:
true`, `SameSite: Lax` and a 24-hour `MaxAge`; session cookies are always
HttpOnly.

```go
package main

import (
    "time"

    "github.com/kataras/iris/v14"
    "github.com/kataras/iris/v14/middleware/sessions"
    "github.com/kataras/iris/v14/rest/errors"
)

// UserSession is the typed session payload.
type UserSession struct {
    UserID   string
    Username string
    Roles    []string
    SigninAt time.Time
}

func main() {
    // Persist these keys; regenerating them invalidates all sessions.
    hashKey := []byte("hash-key-32-characters-long-str!")  // 32 or 64
    blockKey := []byte("block-key-24-characters!") // 16, 24 or 32

    opts := sessions.NewOptions(hashKey, blockKey)
    opts.MaxAge = 8 * time.Hour
    // Secure=true and SameSite=Lax are the defaults. Loosen only for
    // local plain-HTTP development:
    // opts.Secure = false

    db := sessions.NewCookieDatabase[UserSession](opts)

    app := iris.New()

    site := app.Router("/", sessions.Handler(db, "app_session"))
    site.Post("/signin", signin)
    site.Post("/signout", signout)
    site.Get("/profile", requireUser, profile)

    app.Listen(":8080")
}
```

`sessions.Handler` loads (or creates) the session before your handlers
run and commits it afterwards when the request changed it: its data, a
flash message, its id or its `Options`. A new session counts as changed.
It does not commit when a handler already did, or when the response is
an error status. A cookie it cannot decode, because it was signed with
other keys or is older than `MaxAge`, counts as no cookie: the request
runs with a new session (`IsNew` is true) and the commit replaces the
cookie. Only a failure of the store itself answers 500.

A request that only reads the session, such as `GET /profile` above,
sends no cookie and writes nothing to the store. That matters when
requests overlap: a read that is still running while another request
signs the visitor out has nothing to store, so it cannot bring the
session back. Two requests that both change the session at the same
moment still lose one of the two writes.

That is the rule for the three databases of the package: the cookie one
above, and the Redis and Postgres ones below. The session of a
`Database` you write yourself is committed after every request, as
before.

The same rule sets the lifetime. The eight hours of `MaxAge` count from
the last change, not from the last request, so a visitor who signs in
and then only reads is signed out eight hours after signing in. To keep
the session alive on a route that only reads it, call
`session.Commit(ctx)` in that handler: called by hand it saves the
session and sends the cookie again, changed or not. A deployment that
changes `Secure`, `Path` or `MaxAge` in the session options waits in
the same way: it reaches a visitor at their next change of the session,
not at their next request.

A read that sends no cookie has one consequence to check. `Set-Cookie`
was what kept such a page out of shared caches, and now nothing does. A
reverse proxy that skips responses with `Set-Cookie` can store one
visitor's page and serve it to the next. When such a proxy caches in
front of the application, send `Cache-Control: private` on every page
that depends on the session: `ctx.SetHeader("Cache-Control", "private")`
in the handler. The `httpcache` middleware is safe without the header
as long as its handler is registered before `sessions.Handler`: it
caches a request with cookies only when its key names every one of them
([Chapter 14](https://iris-go.com/docs/caching-strategies/)).

### Sign-In with Fixation Defense

A session fixation attack plants a known session identifier in the
victim's browser before sign-in, then reuses it once the session becomes
privileged. The defense is to rotate the identifier at the moment the
privilege level changes, which is exactly what `RegenerateID` does:

```go
// Account lives in your user store; verifyCredentials performs the
// lookup and bcrypt comparison, as in the provider example above.
type Account struct {
    ID       string
    Username string
    Roles    []string
}

func verifyCredentials(username, password string) (Account, bool) {
    // Query your database, compare the bcrypt hash...
    return Account{}, false
}

type signinRequest struct {
    Username string `json:"username"`
    Password string `json:"password"`
}

func signin(ctx iris.Context) {
    req, ok := ctx.BindJSON[signinRequest]()
    if !ok {
        return // 400 already written.
    }

    account, ok := verifyCredentials(req.Username, req.Password)
    if !ok {
        errors.Unauthenticated.Message(ctx, "invalid credentials")
        return
    }

    session := sessions.Get[UserSession](ctx)
    session.Set(UserSession{
        UserID:   account.ID,
        Username: account.Username,
        Roles:    account.Roles,
        SigninAt: time.Now(),
    })

    // Rotate the session identifier now that the session is
    // privileged. For server-side stores this deletes the record kept
    // under the old ID and re-issues the cookie under a fresh one; for
    // cookie-based sessions (no server-side ID) it is a harmless no-op.
    if err := session.RegenerateID(ctx); err != nil {
        errors.Internal.Err(ctx, err)
        return
    }

    ctx.OK(iris.Map{"message": "signed in"}, nil)
}
```

Set the authenticated data *before* calling `RegenerateID`: it persists
the session itself, so no extra `Commit` is required. The Handler
commits nothing more after it, so data set after the call is never
stored on Redis or Postgres.

### Guarding and Sign-Out

```go
func requireUser(ctx iris.Context) {
    session := sessions.Get[UserSession](ctx)
    if session == nil || session.Data.UserID == "" {
        errors.Unauthenticated.Message(ctx, "sign in required")
        return
    }
    ctx.Next()
}

func profile(ctx iris.Context) {
    session := sessions.Get[UserSession](ctx)
    ctx.JSON(session.Data)
}

func signout(ctx iris.Context) {
    session := sessions.Get[UserSession](ctx)
    if err := session.Delete(ctx); err != nil {
        errors.Internal.Err(ctx, err)
        return
    }
    ctx.OK(iris.Map{"message": "signed out"}, nil)
}
```

### Server-Side Stores

The cookie database keeps everything in the (encrypted, authenticated)
cookie itself: zero infrastructure, but sessions cannot be revoked
server-side and the payload must stay small. For revocable sessions use a
server-side store; only an opaque ID travels in the cookie:

```go
// Redis (third-party client: github.com/redis/go-redis/v9).
client := redis.NewClient(&redis.Options{Addr: "localhost:6379"})
db := sessions.NewRedisDatabase[UserSession](client, opts)

// PostgreSQL (third-party pool: github.com/jackc/pgx/v5/pgxpool).
db := sessions.NewPostgresDatabase[UserSession](
    sessions.DefaultTableName, conn, opts)
```

Both implement the same `sessions.Database[T]` interface, so the
`sessions.Handler` registration and every handler above stay unchanged.
With a server-side store, `RegenerateID` performs a real rotation.

## Identity Server and OAuth2

Two heavier subpackages build on the auth SDK. Both are overviews here;
each ships with its own documentation.

First, the terms. An identity provider is a service whose only job is
knowing who users are, so many applications can share one sign-in.
OAuth 2.0 is the standard protocol those applications use to obtain
access on a user's behalf without ever seeing the user's password: the
user authenticates at the identity provider, which hands the
application a token with agreed-upon scope. Its different *flows* are
choreographies for different client shapes: a web app that can redirect
the browser (authorization code), a TV or CLI with no browser (device
code), a trusted backend service (password grant, machine-to-machine).

### auth/identity: a Complete Identity Server

`auth/identity` is a production identity server you mount into an Iris
application. It is backed by PostgreSQL (via `github.com/kataras/pg`) and
provides, out of the box:

- User management under `/u`: signup, signin, refresh, signout,
  password reset, soft delete and restore, bulk operations, paginated
  and filtered listing, and user groups.
- Declarative user schema validation configured in YAML (text, number,
  email, date, phone, bool, uuid, slice, map field types).
- OAuth2 client applications: user applications (authorization code and
  device code flows) and server applications (password grant,
  machine-to-machine) via `AddUserApplication`, which takes an
  `identity.UserApplicationRegistration` value, and
  `AddServerApplication`.
- Client management for an admin client: `AddAdminApplication`
  registers a server application that also holds the `clients:*`
  scopes, and with its token you create, list, count, read and delete
  clients, issue a client a new token and revoke its tokens under
  `/clients`. The SDK wraps these as `AdminCreateClient`,
  `AdminListClients`, `AdminCountClients`, `AdminGetClient`,
  `AdminDeleteClient`, `AdminIssueClientToken` and
  `AdminRevokeClientTokens`.
- The OAuth2 endpoints of the section below, plus
  `/.well-known/jwks.json`, optional Redis-backed response caching and
  usage metering.

```go
import "github.com/kataras/iris/v14/auth/identity"

identityServer := identity.Load("identity.yml")
app.Router("/auth", identityServer)
```

A typed Go client for the whole HTTP API lives in
`auth/identity/sdk`: `sdk.New[Profile](opts)` gives admin operations
(signup, list, partial updates), `UserSignin` via the OAuth2 password
grant, and `sdk.NewAuth[Profile]`, a ready `*auth.Auth[Profile]` that
verifies identity-server tokens in your own services. See the `auth`
package README (`auth/README.md`) in the framework repository for details.

### auth/oauth2: an OAuth2 Authorization Server

`auth/oauth2` implements the server side of OAuth 2.0, generic over your
user data type. It supports the authorization code flow (with a built-in
or custom consent page), the device code flow for browserless devices,
the password grant for trusted server applications, and refresh token
rotation. Mounting the server registers:

- `GET /authorize`, `GET|POST /consent`, `GET /consent/revoke`
- `POST /device/code`
- `POST /token`
- `POST /token/introspect`

Client and code stores have in-memory and PostgreSQL implementations.
Authorization is expressed as resource-action policies
(`oauth2.ResourcePolicy`) enforced by the `oauth2.AllowPolicies`
middleware, with optional Open Policy Agent (Rego) integration under
`auth/oauth2/opa`. The package README (`auth/oauth2/README.md`) walks
through a complete server and all four client flows.

## Summary

- Pick the lightest tool that fits: `basicauth` for internal surfaces,
  the `auth` SDK for user-facing token APIs, `middleware/jwt` for raw
  token verification, `middleware/sessions` for browser state, and
  `auth/identity`/`auth/oauth2` when you need a standalone identity
  provider.
- `auth.Auth[T]` covers the full token lifecycle with five handlers and
  one middleware; you contribute the YAML key configuration and a
  `Provider[T]` over your user store. Register the instance as a service
  and apply `VerifyHandler` per router, with optional `VerifyUserFunc`
  claims checks; read the user with `auth.GetUser[T]`.
- Authorization is plain handlers: guards that call `ctx.Next()` or
  respond with `errors.PermissionDenied` (403), distinct from
  `errors.Unauthenticated` (401).
- v14 defaults are secure: the JWT verifier reads the `Authorization`
  header only, and session cookies default to `Secure`, `SameSite=Lax`
  and HttpOnly. Call `RegenerateID` after sign-in to defeat session
  fixation.
- Testing authentication flows with `httptest` is covered in
  [Chapter 16](https://iris-go.com/docs/testing/); broader hardening guidance lives in
  [Chapter 18](https://iris-go.com/docs/security-best-practices/).

## Further Reading

- [MDN: HTTP authentication](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication):
  the `Authorization` header, challenges and the Basic scheme.
- [MDN: Using HTTP cookies](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies):
  how cookies work, with `HttpOnly`, `Secure` and `SameSite` explained.
- [RFC 7519 — JSON Web Token (JWT)](https://www.rfc-editor.org/rfc/rfc7519):
  the token structure and the registered claims (`exp`, `iss`, `sub`,
  `jti`) behind `StandardClaims`.
- [OWASP Authentication Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html):
  practical guidance on sign-in flows, credential handling and errors.
- [OWASP Password Storage Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html):
  current recommendations for bcrypt and friends, including cost
  factors.
- [OWASP Session Management Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html):
  session identifiers, expiration and the fixation attack that
  `RegenerateID` defeats.
