middleware

package
v0.54.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 9, 2026 License: MIT Imports: 31 Imported by: 6

Documentation

Overview

Package middleware holds the mandatory request pipeline.

Order matters and is not a matter of taste: Recover must be the outermost middleware, or a panic raised in any other middleware escapes without a page; Observe must come right after it, because everything below depends on the context it builds.

Index

Constants

View Source
const PasswordConfirmPath = "/auth/password/confirm"

PasswordConfirmPath is where RequireConfirmedPassword sends somebody who has a session but has not typed their password recently.

Fixed for the same reason SignInPath is: it is the address the starter kit registers for that screen, and two parts of a project that disagree about it produce a guard which redirects to a 404.

View Source
const SignInPath = "/auth/login"

SignInPath is where a guard sends somebody who has to sign in.

Fixed, for the reason security.SessionCookieName is fixed: it is the address the framework's auth module registers and the address the starter kit answers at, and a configurable one buys nothing while giving two parts of a project a way to disagree about where the sign-in screen is.

It is also the pattern that module registers the screen under, and the screen carries the name "auth.login" -- so a path built from that name is this string, by construction rather than by two declarations that happen to agree. Which of the two to reach for is decided by what the caller holds: a guard runs before any route has matched and has no table to ask, so it uses this; anything holding a route table asks it for "auth.login" and gets the same answer.

The address somebody is sent to when they are ALREADY signed in is a parameter, because that one genuinely differs -- a blog sends them to the front page, an application to its dashboard.

View Source
const StatusCSRFExpired = 419

StatusCSRFExpired is the status returned when the token is missing, invalid or expired. 419 is the conventional status for it, kept on purpose: HTMX can be told to reload the page on 419, which is the only useful reaction to an expired token.

Variables

View Source
var ErrUnknownToken = errors.New("middleware: unknown bearer token")

ErrUnknownToken is what a TokenResolver returns for a digest it cannot answer with a subject: a token it never issued, one that was revoked, one that expired.

It is one error for all of them on purpose. RequireToken answers each of them, and a request that carried no token at all, with the same 401, and a resolver that told them apart would be telling the guard something the guard is not allowed to repeat.

Functions

func CSRFProtect

func CSRFProtect(c *security.CSRF, sessionIDFrom func(*http.Request) string, opts ...CSRFOption) func(http.Handler) http.Handler

CSRFProtect refuses a state-changing request the browser reports as cross-origin, then validates the token on the ones that are left.

THE TRAP THIS SOLVES: with HTMX the token does not always arrive in a form field, it arrives in a header. Both sources are read, the X-CSRF-Token header first and the _token form field after it.

The body is read only when the header is absent, and only as a form: an application/x-www-form-urlencoded body is parsed with ParseForm, a multipart/form-data body with ParseMultipartForm, and any other body is left unread and the request refused as carrying no token. A request whose token is in the header reaches the handler with its body untouched, so a handler can still stream a multipart upload part by part.

A multipart form parsed here is removed when the handler returns, whatever the answer was. ParseMultipartForm writes the parts that do not fit in memory to temporary files, and net/http removes them only for the request the server created; the one reaching this middleware is a copy made further up the pipeline, so without the removal every upload that reached it -- a refused one included -- left its files on disk. The handler reads the same parsed form through the request it receives, and the files are gone once it has returned: one that keeps an upload has to copy it somewhere before then.

The field is named for the session key the token is stored under, which is the one name a form builder, a view and this middleware can all arrive at without agreeing on a second one first. A field spelled any other way is read by nothing here and the request is refused as though it carried no token.

A form carries the hidden _token field, so a submission is covered whether it posts natively or through hx-post. What that does not cover is a request no form backs -- hx-delete on a button, hx-patch on a toggle. Those carry the token only where the markup puts it, either once on the layout

<body hx-headers='{"X-CSRF-Token": "{{ .CSRFToken }}"}'>

or on the element that sends the request.

Nothing verifies that the line is there. A layout that loses it keeps rendering and every form goes on working; the first button that deletes something answers StatusCSRFExpired instead, which reads as an expired session rather than as missing markup.

The two checks answer different failures and neither replaces the other. The origin check reads Sec-Fetch-Site, which browsers have sent since 2023, and falls back to comparing Origin against Host; it costs no allocation and turns a form posted from another site away before any HMAC is computed. It cannot stand alone, because a request carrying neither header is allowed -- that is how a non-browser client reaches an application at all. The token is what covers those, and it is the layer that survives a browser too old to report where the request came from.

A cross-origin refusal is 403 rather than StatusCSRFExpired, because the two ask different things of whoever hit them. An expired token is fixed by reloading the page, which brings a fresh one; an origin is not, and telling the browser to reload would send it round the same refusal again.

There is no list of trusted origins to configure. A state-changing request from another site is exactly what this refuses. The one exception is by path, and it is the application's: CSRFExcept, passed in opts, names the paths a request is never checked on -- a webhook a provider posts to, which carries no session and no token and proves itself with a signature the route verifies. It is written where the middleware is wired, so the paths that skip the check are read in one line of the application's bootstrap rather than discovered in a handler.

A bearer token is not ambient

A request that carries Authorization: Bearer and no valid session cookie is not asked for a token, and binds no guest cookie: it is left to the guard on its route, RequireToken, which authenticates it or answers 401. The scheme is read by the same reader RequireToken uses, so the two cannot disagree about which requests carry one.

The token check exists against ambient authority -- a credential the browser attaches by itself to a request another site started. A bearer token is not one: a page on another site cannot attach an Authorization header without a CORS preflight, and a client that attaches it holds the token. Basic, Digest and Negotiate are ambient, because a browser attaches them again by itself after a 401 challenge, and those requests are checked like any other.

The origin check still applies, so a browser reporting the request as cross-origin is refused whatever header it carries.

With a valid session cookie the full check applies, bearer or not: the route behind may honour the cookie and ignore the header, and the cookie is exactly what a forged request rides on. A cookie sessionIDFrom does not accept -- a signature that does not verify -- carries no session, so it is as though there were none: a forged request with a junk cookie and no bearer token is still refused for carrying no CSRF token.

The token a page draws

It also issues the token, so that no controller has to. On a GET or a HEAD it issues one and puts it on the request context with hhttp.WithCSRFToken, where view.New reads it into Page.Token; on a request that passed the check it puts the token that was submitted there, so a form redrawn on the same request -- a refused sign-in, a validation answered in place -- carries a token that still validates. OPTIONS and TRACE draw nothing and get nothing.

The token is bound to what CSRF.Binding returns: the session id when the request carries a session cookie, and otherwise a random guest id carried in a signed cookie of its own, set on the first page a visitor without one loads. A token is therefore accepted only from the browser it was issued to, signed in or not, and the sign-in form -- submitted by somebody with no session yet -- is protected like every other. A request with neither cookie validates nothing.

Every token issued for a binding stays valid until its own expiry, so a page left open in one tab keeps working after another tab loaded a newer one. Nothing rotates them: the binding changes when the session does, at sign-in and sign-out, and those answers are a redirect to a page that issues afresh.

The guest cookie carries the Secure attribute unless the CSRF was built with Secure(false), which is for development over plain HTTP only: without it the browser never sends the cookie back, and every guest form answers StatusCSRFExpired.

A page that carries a token is a page for one visitor. A shared cache in front of it that serves one visitor's page to another serves a token bound to somebody else, and that form answers StatusCSRFExpired.

sessionIDFrom must return the id only for a valid session cookie -- pass SessionStore.IDFromRequest, which verifies the signature first.

opts is variadic so that the call with none is the call every application already writes. A nil option panics here, at wiring, rather than on the first request.

func Flash

func Flash(f *security.Flash) fhttp.Middleware

Flash consumes the one-shot flash and puts it on the request, so the page about to render can draw the messages of the request that was redirected here.

It is the middle of three steps that no application writes any of: fhttp.Reject puts the messages in the browser, this takes them out again, and view.New copies them onto the Page every screen already embeds. The handler in between passes nothing, which is the point -- the failure being fixed is a message that existed and did not reach the screen, and every hand-written hop is another place for it to stop.

The kernel installs it. It is not in the application's pipeline because there is nothing to decide about it: a pipeline entry an application can leave out is one an application leaves out, and the symptom is a form that comes back blank -- indistinguishable from having no flash at all.

func Idempotent added in v0.51.0

func Idempotent(store IdempotencyStore, ttl time.Duration) func(http.Handler) http.Handler

Idempotent makes a write that carries an Idempotency-Key header run once, and answers every retry of it with the answer the first copy got.

The first request with a key runs. Its status, the headers in a closed list -- Content-Type, Location, ETag and the other representation headers, never Set-Cookie -- and its body are stored for ttl. A retry with the same key and the same request is answered with that, byte for byte, carrying Idempotent-Replayed: true, and the handler does not run again. The same key with a different request -- another body, another method, another address -- is answered 422: it is a client reusing a key, and replaying the first answer to a second question would be wrong whichever way it went.

A retry that arrives while the first copy is still running is answered 409 with Retry-After, and does not run: two copies of a payment in flight at once is the failure this exists to remove, and it is the one a check-then-run would let through. The lock is taken in the store, so it holds across processes when the store is shared.

Only an answer below 400 is kept. A refusal changed nothing, by the meaning of the status, so its retry may run -- which is what lets a client fix the request and send it again under the same key. A server error is not kept either, so the client can retry it; making the handler safe to run again after one is the handler's transaction, not something a replay can supply. A request whose handler took the connection over keeps nothing either: what it sent went past the writer, so there is no answer to replay.

GET, HEAD, OPTIONS and TRACE pass through untouched, and so does a request with no key. A key is one header value of 1 to 255 visible ASCII characters, spaces allowed; anything else is answered 400.

The key belongs to the subject

The answer is stored under the tenant and the id of the subject on the request context, and the key the client sent, so two clients that happen to pick the same key never see each other's answers. The tenant comes from the subject and from nothing on the request. That makes the order of the pipeline part of the contract: Idempotent is mounted after the guard that carries the subject -- RequireToken, RequireAuth -- and a request that reaches it with a key and no subject panics, naming the fix, rather than keeping one caller's answer where another could replay it.

The body is read in full before the handler runs, to compare it with the stored one, and handed to the handler unchanged. A body over the limit a middleware mounted earlier set is answered 413.

A store that cannot answer before the handler runs is a panic, answered 500: running the write anyway is exactly what the caller asked not to happen. A store that cannot keep the answer after the handler ran is logged, because the answer has already been sent -- and a retry of that request will run again.

ttl is how long a key is remembered, and it must be positive; a day is a usual choice, long enough to outlast any client's retries.

func KeyByIP

func KeyByIP(r *http.Request) string

KeyByIP keys on the peer address: the whole address over IPv4, and the /64 it sits in over IPv6.

It reads RemoteAddr and never X-Forwarded-For: a header the client controls is a way to reset someone else's counter. Behind a proxy, have the proxy rewrite RemoteAddr, or key on something the proxy signs. A proxy that does neither gives every request in the world the same key, and then every limit keyed this way is a limit on the whole application -- which for the sign-in throttle means twenty-five wrong passwords a minute across every customer.

Why the IPv6 address is masked

Because otherwise it is not a limit. IPv4 addresses are scarce, so keying on the whole address costs an attacker money; a /64 is the smallest block any end site is given -- a home connection, a VPS, a phone -- and every one of them holds eighteen quintillion addresses that all reach this server. Keyed on the full address, one machine with a routed /64 had an unlimited number of budgets: it could walk a list of accounts forever, and fill the sign-in throttle's table on its own, from a single upstream link.

The /64 and not something wider, because it is the one boundary that is always a single link. A /48 would be one customer at some providers and a whole building at others, and grouping two subscribers under one budget is how a limit locks out somebody who did nothing.

A wrapper and not an alias, because a plain function has no alias form. The key it returns is byte-for-byte the one this package produced before the move, which matters: a counter in a shared store is keyed by this string, and a different prefix would hand every caller a fresh budget on deploy.

func KeyBySession

func KeyBySession(sessions Sessions) func(*http.Request) string

KeyBySession keys on the session id while the store still holds that session, and on the address otherwise. Pass the application's *security.SessionStore.

The cookie alone is not enough to key on. Its signature proves the id was issued here once, and every expired or signed-out id a client kept would be a fresh budget of its own; asking the store closes that, so a session that is gone falls back to the address like a request with no cookie at all.

The key is byte for byte the one the counter is kept under, and a store that cannot answer is a session that does not exist: the request is counted by its address rather than let through.

The declared return type stays func(*http.Request) string rather than hesape's named KeyFunc; the value returned is the same function either way.

func LoadSubject added in v0.50.0

func LoadSubject(sessions *security.SessionStore) func(http.Handler) http.Handler

LoadSubject puts the subject of the request's session on its context when there is one, and lets every request through either way.

It is for the page that is public and still wants to know who is looking -- the front page that shows the account menu to somebody signed in and the sign-in link to everybody else. A request with no session, or with a cookie whose session expired, reaches the handler exactly as it arrived: nothing is redirected, and nothing is put on the context, so Context.User answers false. No subject and an anonymous reader are different facts, and a guest is declared with security.Guest by the code that means it, never invented here.

Behind RequireAuth it adds nothing: that guard already carries the subject.

func Observe

func Observe(dev bool, tracingSecret string, recorder *observability.Recorder) func(http.Handler) http.Handler

Observe installs the request id, the request-scoped logger and -- in development, or under an authorized tracing header -- the Collector.

It must come right after Recover: everything below depends on the context it builds.

tracingSecret enables the Collector outside development for requests carrying it in X-Arandu-Trace. Leave it empty to keep production at zero cost.

recorder is the buffer behind /_arandu/debug. Pass kernel.Recorder(); nil records nothing, which is what production does.

func Recover

func Recover(dev bool, opts errorpage.Options) func(http.Handler) http.Handler

Recover captures panics and decides what to render.

In development: the full debug page -- stack, request, queries, dumps. Anywhere else: the status page, which leaks nothing and carries the request id so the operator can correlate it with the structured log.

This function is a bridge. It is removed in v1.0.0; build the handler with foundation/bootstrap.HandleExceptions and install github.com/arandu-io/hesape/exception.Recover:

h := bootstrap.HandleExceptions(cfg, AppModule, app.Diagnose)
app.Use(exception.Recover(h), ...)

Both spellings end in the same exception.Recover, so they catch the same things. Two differences survive the swap, and the second one changes what a client sees.

The first is what is left afterwards: this one builds the Handler from three fields and drops it, so there is no object to register an Error, Missing or Fatal callback on, none to hand the application's own error views to, and none to name the errors that must never reach the log. HandleExceptions returns that Handler, and the application keeps it.

The second is where the debug flag comes from, which is what decides between the debug page and the status page. This function takes it as an argument, so the caller decides; HandleExceptions reads Configuration.App.Debug. The two answer the same only while APP_DEBUG is left unset, because its default is whether the environment is development. Pass anything else here -- "the environment is development" is the usual one -- and the swap moves the page: an environment that is not development with APP_DEBUG set starts drawing the stack, the request and the environment, and development with APP_DEBUG=false stops drawing them. Passing cfg.App.Debug is what makes the two draw the same page before the swap and after it.

The death date above is what keeps this from being a second way to install one middleware.

func RedirectIfAuthenticated

func RedirectIfAuthenticated(sessions *security.SessionStore, to string) func(http.Handler) http.Handler

RedirectIfAuthenticated is the guest guard: it keeps somebody who is already signed in off the screens that exist to sign them in.

Without it the sign-in and registration screens render for a person who has a session, which reads to them as having been signed out -- and the next thing they do is sign in again, on top of a session that was never gone.

func RequireAuth

func RequireAuth(sessions *security.SessionStore) func(http.Handler) http.Handler

RequireAuth refuses a request that carries no session.

It sends the visitor to SignInPath rather than answering 403, because there is nothing they can do with a 403 and there is something they can do with the sign-in screen.

It remembers where they were going first. This is the only place that knows: by the time a password has been typed, the request that was refused is gone, and a sign-in that always ends at the front page makes somebody who followed a link to one invoice go and find it again. The address goes in a signed cookie rather than in the session, because the session is the thing that does not exist yet at the moment the guard fires -- see SessionStore.RememberIntended, and SessionStore.TakeIntended for the other end of it.

A request it lets through carries the subject it loaded, on the request context, so the handler reads it with Context.User or auth.SubjectFrom rather than loading the session a second time. That is who is asking and nothing more: whether they may touch a record is still the Policy's answer, and the Grant it issues is never put on the context.

func RequireConfirmedPassword

func RequireConfirmedPassword(sessions *security.SessionStore) func(http.Handler) http.Handler

RequireConfirmedPassword admits a request only when the password was typed again on this session less than security.PasswordConfirmationWindow ago.

Mount it on the handful of routes where holding the cookie is not proof enough that the person is there: changing the address the account is recovered through, revealing an API key, closing the account, moving money. The cost of not having it is concrete -- a session cookie lifted from a shared machine is a full account takeover with no step where the attacker has to know anything.

Where the line is against a second authorization path

It asks one question: was the password confirmed recently. It never asks may this subject touch this record -- that is the Policy's answer, on every service call the handler behind it makes, exactly as it is behind RequireAuth. A guard that started deciding about records would be a second authorization path, and of the two it is always the guard that gets forgotten: a Policy is written once per entity and reached from everywhere, a guard is mounted per route and the route somebody adds next month has none.

So this is a freshness check on the session, not permission. "Recently confirmed" and "allowed" are different facts, and an application that used this in place of a Policy would let a confirmed subject reach every record in its tenant.

Somebody with no session at all goes to the sign-in screen and not to the confirmation screen: there is no password to confirm yet, and sending them to a form that asks for one on top of no session is a loop.

func RequireRole

func RequireRole(sessions *security.SessionStore, roles ...string) func(http.Handler) http.Handler

RequireRole admits a subject carrying any one of the roles.

A visitor with no session is sent to the sign-in screen, exactly as RequireAuth sends them: "you are not signed in" and "you are signed in as somebody without this role" are different situations and only the second one is a refusal. That second one is 403 and not 404 -- the page exists, and pretending otherwise sends somebody looking for a typo.

It is still not an authorization decision about anything. It reads the roles off the session and stops there; the handler behind it goes through the Policy like every other handler.

func RequireToken added in v0.51.0

func RequireToken(tokens TokenResolver) func(http.Handler) http.Handler

RequireToken refuses a request that does not carry a bearer token the resolver knows, and carries the subject of one it does.

The token is read from the Authorization header, scheme Bearer, compared case-insensitively, and from nowhere else: a token in a query string reaches every log between the client and here, and a token in a cookie is a token a browser attaches to a request another site started. There is no fall back to the session either, for the second of those reasons -- an API route that took a cookie would be a route CSRF reaches.

A request with no token and a request with a token nobody knows are answered the same 401, byte for byte, with WWW-Authenticate: Bearer and nothing that says which of the two it was. A client that wants JSON gets a problem document; anything else gets the status and its sentence.

A request it lets through carries the subject the resolver returned, on the request context, so the handler reads it with Context.User or auth.SubjectFrom -- exactly as it does behind RequireAuth. That is who is asking and nothing more: whether they may touch a record is still the Policy's answer, and the Grant it issues is never put on the context.

The subject replaces any subject already on the request, rather than being kept beside it the way RequireAuth keeps one for the same account: a token is usually narrower than the session of the account that issued it, and a cookie that happened to ride along must not widen what the token may do.

A subject with no id is not somebody a token can name -- a declared guest is one, since security.Guest sets no id -- and is refused as an unknown token.

func SecurityHeaders

func SecurityHeaders(dev bool, imageOrigins ...string) func(http.Handler) http.Handler

SecurityHeaders applies the default headers.

The CSP is restrictive on purpose and still works with HTMX, because HTMX operates through attributes rather than inline script. There is no global 'unsafe-inline' in this framework.

imageOrigins are the addresses, besides this application's own, that an image may load from: the public address of a disk whose files the pages embed, such as a bucket served by a CDN. They reach img-src and no other directive. Each is a bare https origin, the scheme and the host with an optional port, and anything else panics while the pipeline is wired.

A wrapper and not an alias, because a plain function has no alias form. The declared return type stays func(http.Handler) http.Handler rather than hhttp.Middleware, which is the same type by two aliases: spelling it the way this package always has keeps the signature identical for every caller.

Types

type CSRFOption added in v0.52.0

type CSRFOption func(*csrfOptions)

CSRFOption changes what CSRFProtect checks. CSRFExcept is the one there is.

func CSRFExcept added in v0.52.0

func CSRFExcept(prefixes ...string) CSRFOption

CSRFExcept returns the option that exempts the paths the application names from the CSRF check: a state-changing request on one of them passes with no origin check and no token. A GET on one is still issued a token, like any other page.

A prefix that ends in '/' covers every path under it; one that does not covers exactly that path. "/webhooks/" exempts /webhooks/stripe, and "/webhooks" exempts /webhooks and never /webhooksx nor /webhooks/stripe: matching is by whole segments, never across a segment boundary.

The request path is cleaned before it is compared, the way the router cleans it, so /webhooks/../notes is /notes and is checked. A path that arrived with an escape its decoded form does not round-trip to -- %2F, %2E, anything that leaves url.URL.RawPath set -- is never exempt: the router matches the escaped path segment by segment, so /notes/%2E%2E/webhooks/x decodes to a path under /webhooks/ while it is routed under /notes/. Such a request is checked like any other.

An exempt route carries no protection from this middleware, so it has to prove the request itself, by the signature a provider computes over the body. The exemption is a decision about an application, which is why it is an argument written in its bootstrap and not a setting read from anywhere.

A prefix that is empty, does not begin with '/', is "/" -- which would turn the check off for every route -- or is not already clean, so that no cleaned path could ever equal it, panics: each is a wiring mistake, and a panic at start names it before any request is served.

type IdempotencyStore added in v0.51.0

type IdempotencyStore interface {
	// Get returns the bytes stored under key, or cache.ErrNotFound when there
	// are none or they expired. Any other error is a store that could not
	// answer, and the request is not run.
	Get(ctx context.Context, key string) ([]byte, error)

	// Put stores value under key for ttl, replacing whatever was there.
	Put(ctx context.Context, key string, value []byte, ttl time.Duration) error

	cache.Locking
}

IdempotencyStore is where Idempotent keeps the answers it replays, and the lock that keeps two copies of one request from running at once.

It is the part of a hesape cache store Idempotent uses, under the same method names, so a store that can hold a lock satisfies it as it stands: *cache.ArrayStore inside one process, the database store or the RESP store across several. Across several processes it has to be a shared one -- a key kept in one process's memory is replayed by that process and run a second time by the next.

type Sessions added in v0.50.0

type Sessions interface {
	// IDFromRequest is the session id the request's cookie names, once its
	// signature is verified, or the empty string.
	IDFromRequest(r *http.Request) string
	// Load returns the subject of the request's session, and an error when the
	// store does not hold it.
	Load(ctx context.Context, r *http.Request) (security.Subject, error)
}

Sessions is what KeyBySession reads a session through. *security.SessionStore satisfies it.

type TokenDigest added in v0.51.0

type TokenDigest [sha256.Size]byte

TokenDigest is the SHA-256 of a bearer token, and it is all a TokenResolver is ever handed.

The resolver never sees the token, so it has nothing to compare in variable time and nothing to write to a log. It looks the digest up -- an indexed column holding DigestToken of every token issued -- and a timing difference in that lookup can reveal, at most, part of a digest. A digest does not authenticate anybody: reaching RequireToken with it means presenting a token whose SHA-256 it is, which is what a preimage-resistant hash rules out.

That argument holds for a token nobody can guess, and only for one. A token is a random value from crypto/rand, 32 bytes or more; the digest is unsalted and fast, which is right for that value and wrong for a password, and it is why a token costs a hash per request and not a password hash per request.

func DigestToken added in v0.51.0

func DigestToken(token string) TokenDigest

DigestToken returns the digest RequireToken looks token up by.

The code that issues a token calls it once and stores the result -- never the token, which is shown to its owner when it is issued and kept nowhere else. A stored digest that leaks hands out no credential.

func (TokenDigest) String added in v0.51.0

func (d TokenDigest) String() string

String returns the digest as 64 lowercase hexadecimal characters, the form a text column stores it in.

type TokenResolver added in v0.51.0

type TokenResolver interface {
	// ResolveToken returns the subject the token with this digest was issued
	// to, or ErrUnknownToken when there is none -- never issued, revoked or
	// expired. Any other error is a failure to answer, and RequireToken does
	// not turn it into a 401: a token store that is down would otherwise tell
	// every client that its token was taken away.
	//
	// The subject's tenant is the one recorded when the token was issued. It is
	// the only place the request's tenant comes from: nothing on the request
	// names it, and RequireToken reads no header, path or body for it.
	//
	// What the token may do is the subject's Actions, as narrow as the token was
	// issued: a token never carries more than the account that issued it, and
	// the Policy still decides every record.
	ResolveToken(ctx context.Context, digest TokenDigest) (security.Subject, error)
}

TokenResolver turns the digest of a bearer token into the subject the token was issued to. The application supplies it, because the application is what issues, stores, scopes, expires and revokes its tokens.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL