Stateless Auth in Grails with JSON Web Tokens

Modern web services running on Grails are increasingly called upon to serve mobile clients, single-page front ends, and partner integrations from locations as varied as a Brisbane fintech office and a remote site in Western Australia. Each caller expects predictable, low-friction access to protected endpoints without the server remembering who logged in minutes earlier. That is exactly the problem JSON Web Tokens were designed to solve, and the Groovy ecosystem makes working with them refreshingly direct.

Token-based authentication replaces the classic server-side session table with a small, self-contained string the client carries with every request. Because the token is signed, the receiving service can verify the caller's identity on its own, scale horizontally without sticky sessions, and reason about the request purely from what is inside the bearer string. For teams shipping APIs from Melbourne to Manila, that portability is worth more than any single framework feature.

The remainder of this walk-through covers the moving parts of a JSON Web Token, the Grails filters that issue and validate them, and the practical hardening steps that align an implementation with the kind of expectations a local compliance team — or the Office of the Australian Information Commissioner — would have of a production system.

Why stateless authentication suits Grails applications

A traditional Grails app leans on the servlet container's session, which works beautifully for server-rendered views backed by a single JVM. As soon as the same codebase has to back a React dashboard used by a sales team in Adelaide and a native iOS app used by field engineers, that single-session assumption starts to crack. Multiple instances behind a load balancer need session replication, mobile clients have nowhere to store a JSESSIONID cookie reliably, and each new endpoint risks a hand-rolled permission check.

Stateless authentication sidesteps those problems by making the token itself the source of truth. When a client sends a request, the Grails filter chain inspects the bearer token, verifies its signature against a shared secret or public key, and trusts the claims inside. There is nothing to look up in a database on the common path, which keeps response times predictable and lets the same code base run on AWS ap-southeast-2 in Sydney as easily as on a developer's laptop in Parramatta.

Groovy adds a few niceties of its own. Closures keep filter definitions concise, the built-in JSON support makes payload parsing trivial, and dynamic typing means a token claim can be read with a single dotted expression. Combined, those qualities mean a fully working authentication layer fits comfortably in a few dozen lines, freeing more time to focus on what the application actually does.

Anatomy of a JSON Web Token

A JSON Web Token is three Base64URL-encoded segments separated by dots: a header that names the algorithm, a payload of claims about the subject, and a signature that proves the token has not been altered. The header usually carries alg and typ with values such as HS256 and JWT. The payload is where the application lives — it can hold a user identifier, roles, an issued-at timestamp, an expiry, and any custom fields the application finds useful, such as a tenant id for a multi-org SaaS product.

The signature is created by hashing the header and payload with a key known to the issuer. With HS256 the key is a shared secret; with RS256 or ES256 the issuer signs with a private key and verifiers use the matching public key. Choosing between symmetric and asymmetric signing is one of the more consequential early decisions, because it dictates how the application distributes trust across services. A single monolith deployed to a handful of containers in Sydney can live happily with HS256, while an ecosystem that includes a separate reporting service or a partner API gateway typically moves to RS256 so verifiers do not also gain signing power.

Standard claims worth committing to memory include iss for issuer, sub for subject, aud for audience, exp for expiry, nbf for not-before, and iat for issued-at. Each gives a verifier a small, defensible reason to accept or reject a token. Custom claims, by convention, are namespaced to avoid collisions — a property like https://grailsexample.net/tenant is clearer than a bare tenant and reduces the chance of clashes with claims introduced by upstream identity providers.

Setting up the Grails project and dependencies

A clean implementation starts with the right libraries. The jjwt library from Okta is a popular choice because it exposes a fluent builder for token creation and a clean parser for verification, and it is published to Maven Central alongside its runtime dependencies. In a Grails 6 or 7 project on Gradle, the relevant entries sit in build.gradle. For an older Groovy-based DSL or a BuildConfig.groovy, the equivalents live in the repositories block. Either way, the goal is to pull in io.jsonwebtoken:jjwt-api, jjwt-impl, and jjwt-jackson at matching versions.

Once the dependencies resolve, configuration belongs in application.yml rather than scattered across source files. Secrets such as the signing key, the issuer string, and the expected audience should live there, ideally supplied through environment variables when the artefact ships to a container host. Local development can rely on a .env file ignored by version control, while staging and production read from the orchestrator's secret store. A 256-bit random key is the minimum for HS256; for a multi-service deployment, generating an RSA keypair with openssl and storing the PEM blobs in a vault is the standard practice.

A small configuration holder bean makes those values injectable without littering controllers with grailsApplication.config lookups. The bean exposes the issuer, the audience, the signing key as a Key object, and a default validity window expressed as a Duration. With that in place, the rest of the codebase autowires a single, well-typed collaborator instead of re-reading configuration on every request.

Issuing tokens from a login endpoint

The login flow is where authentication becomes tangible to the client. A POST endpoint accepts a credential payload — typically a username and password, or for an enterprise integration, an assertion from an upstream identity provider — and returns a JSON body containing an access token, a refresh token, an expiry in seconds, and optional profile metadata. In Grails, that endpoint can live in a regular UrlMappings-wired controller, with the credential check delegated to a service that owns the user lookup, the password verification, and any rate-limit accounting.

When the credentials are valid, the service composes a token by calling Jwts.builder(), setting the issuer, the audience, the subject to the user's identifier, the issued-at and expiry values from the configuration holder, and any role or tenant claims the application needs. The signed compact string then flows back to the controller, which wraps it in a response object. A common pattern is to send the access token in the JSON body and the refresh token as an HTTP-only, secure, same-site cookie, so the browser-based front end never has to touch the long-lived credential directly.

Equally important is what happens when the credentials are wrong. Returning a generic 401 with a constant response time, regardless of whether the username exists, slows down credential-stuffing attempts. A local login service protecting a customer portal in Perth will face the same bots as one in San Francisco, so these small touches matter. Logging the failure with a hashed identifier and a request correlation id gives the security team something to work with without leaking useful signal to the attacker.

Verifying tokens with a Grails filter

The other half of the system is the filter that runs on every incoming request. In Grails this can be expressed as an interceptor, a servlet Filter registered in application.groovy, or a Spring OncePerRequestFilter. Each approach works; the servlet filter has the advantage of sitting in front of any URL, including static assets that the application might want to guard with role-based rules.

The filter reads the Authorization header, expects a Bearer prefix, parses the compact token with the same library used to issue it, and pulls the key, issuer, and audience from the configuration bean. Validation failures map to a clean 401 with a small JSON error body; validation success populates the request's authentication context, typically by setting a UsernamePasswordAuthenticationToken (or a custom subclass carrying the application's role list) on the SecurityContextHolder. Downstream controllers and services can then read from that context through the familiar Spring Security idioms, or via a thin Groovy helper that hides the plumbing.

One pattern worth adopting early is a small principal object on the request that carries the user's id, display name, and role set. It keeps controllers free of token parsing and gives the template layer a single bean to render user information in the few server-rendered views a hybrid app might still ship. For an internal tool used by a finance team in Canberra, that small ergonomics win pays back across every screen the team maintains.

Refresh tokens, expiry, and revocation

Access tokens should be short lived — fifteen minutes is a common starting point, sometimes shorter for a privileged endpoint. Long-lived sessions come from refresh tokens, which the client exchanges for a fresh access token without resending a password. The implementation is a small companion to the login endpoint: a POST that accepts the refresh token, verifies it against a different signing key or a database record, and returns a new access token, leaving the refresh token's own expiry unchanged.

Revocation is the awkward corner of token-based authentication. Because the verifier trusts the token on its own, a leaked access token remains valid until it expires. The simplest mitigation is a short access token lifetime combined with refresh token rotation, so a stolen access token is useful for only a few minutes. For more sensitive cases, the application can maintain a denylist of token identifiers that the filter consults, accepting the lookup cost in exchange for instant revocation. A sliding session table keyed on jti doubles as an audit log for any compliance review tied to the Notifiable Data Breaches scheme.

Refresh tokens themselves should be stored on the client with care. An HTTP-only, secure, same-site cookie is the safest option for browser-based callers, while native mobile apps can lean on the platform's secure keychain. Hard-coding a refresh token into a mobile build that ships to the Play Store is the kind of mistake that gets a security team in Sydney an unexpected call from the press, so the topic deserves real attention rather than a passing comment.

Hardening JWT in production

Production hardening is where a working demo becomes a defensible system. Beyond the obvious steps — keeping secrets out of source control, rotating signing keys, enforcing HTTPS — there are subtler choices. Algorithms should be pinned on the verifier: never trust the alg header, because accepting none or swapping HS256 for RS256 with the public key as the secret has been the source of real-world breaches. The parser should reject tokens whose exp is in the past, whose nbf is in the future beyond a small clock skew, whose issuer or audience does not match the configured values, and whose signature does not verify against the loaded key.

Operational concerns deserve equal weight. Logs should record the jti, the issuing endpoint, and the user id, but never the raw token or its claims containing personal information. A request correlation id lets the security team trace a session without exposing the bearer string. Backups of the signing key should be split across two secure locations, ideally with a documented recovery procedure, because losing the key is functionally equivalent to logging every user out — and, worse, forcing a coordinated re-issuance across every relying party.

Finally, the implementation should be reviewed against the controls that local regulation expects. The Privacy Act and the Notifiable Data Breaches scheme in Australia apply to most products that handle personal information, and an authentication layer that quietly leaks a token can quickly turn into a reportable incident. A modest investment in scope, logging, and rotation pays back the first time something unusual happens in production.

For teams that want a guided, video-led path through these patterns and a few dozen more, the grails example team publishes structured tutorials that pair the running code with explanations of the security trade-offs. Whether you are building a brand-new API in Brisbane or refactoring a legacy Grails 3 monolith in Adelaide, working through the examples alongside the official docs is a reliable way to end up with an authentication layer that holds up under audit.