Master Spring Security JWT Implementation

Your device team is ready to ship. Firmware is stable, cloud APIs are almost there, and the only thing standing between launch and a painful security review is authentication. That last part usually gets underestimated. A basic login flow might be enough for an internal prototype, but it won’t hold up when product security, legal,…

Master Spring Security JWT Implementation

Your device team is ready to ship. Firmware is stable, cloud APIs are almost there, and the only thing standing between launch and a painful security review is authentication.

That last part usually gets underestimated. A basic login flow might be enough for an internal prototype, but it won’t hold up when product security, legal, and compliance teams start asking how tokens are signed, how sessions are revoked, and how authentication events support post-market obligations in the EU market.

For regulated products, spring security jwt is useful because it gives you a practical way to build stateless API authentication that scales across devices, services, and user-facing applications. The value isn’t just cleaner architecture. It’s being able to explain, with evidence, how authentication works, how keys are managed, and how access is controlled when your product is reviewed against security requirements.

Securing Your APIs for the Modern Digital Market

A connected product rarely has a single client. You may have a mobile app, a web dashboard, factory tools, support tooling, and the device itself all calling the same backend. If you protect that estate with sticky server-side sessions, the design usually becomes brittle fast.

A hand-drawn illustration showing a light bulb connected to a cloud icon with a padlock and question mark.

JWT-based authentication changes that operating model. The server validates a signed bearer token on each request instead of looking up an application session in shared storage. For teams shipping into Europe, that matters because authentication is no longer just an app concern. It becomes part of how you support secure updates, vulnerability workflows, and evidence for technical documentation.

A good API design still matters before security configuration starts. Teams that need a quick refresher on endpoint consistency, error handling, and versioning should review these API Development Best Practices. Poor API hygiene makes secure authentication harder to operate.

Three practical rules shape most successful implementations:

  • Separate public and protected paths early. Keep endpoints like /authenticate or /signin public, and require authentication everywhere else.
  • Design for evidence, not only access control. Product teams need to show what the API accepts, how it validates tokens, and how it rejects invalid access.
  • Treat auth as part of your threat model. Broken token validation quickly overlaps with common web risks already captured in resources like https://goregulus.com/cra-basics/owasp-top-ten/

Practical rule: If your team can’t explain who issues tokens, who verifies them, and where signing keys live, the design isn’t ready for production.

Understanding JWT and Stateless Authentication

JWT stands for JSON Web Token. In practice, it’s a compact signed token that carries claims about a subject, usually a user, device, or service account.

A diagram explaining the anatomy of a JSON Web Token featuring header, payload, and signature components.

What the token contains

A JWT has three parts:

PartWhat it holdsWhy it matters
HeaderMetadata such as token type and signing algorithmTells the verifier how the token was produced
PayloadClaims such as sub, iss, exp, scopes, or product-specific identifiersCarries the identity and authorisation context
SignatureCryptographic proof over header and payloadDetects tampering

The payload is readable if someone gets the token. That’s why sensitive secrets never belong inside it. Use claims for identity and permissions, not passwords, API secrets, or internal notes.

Why stateless beats session lookup for distributed systems

With server-side sessions, every authenticated request often depends on session storage. That works for a monolith and a small user base. It becomes awkward when devices connect intermittently, regional services scale independently, or multiple APIs need to trust the same identity.

With JWT, the resource server verifies the token signature and claims locally. That’s the core idea behind stateless authentication. The server doesn’t need to remember a session for every active client.

The Spring Security reference material is especially relevant here because Spring Security JWT reached a mature state in Spring Security 6.1, including automatic JWK rotation and Nimbus JOSE support for signature parsing, which supports stronger validation patterns for modern resource servers (Spring Security reference).

Claims are where product security decisions show up

The useful claims are usually boring. That’s good.

  • sub identifies the principal.
  • iss identifies the issuer.
  • exp limits token lifetime.
  • Scopes or authorities tell Spring what the caller is allowed to do.

For a regulated product, those claims support a defensible access model. A firmware update service might accept tokens with an update-related scope. A support portal might require a different scope. A manufacturing tool might need a separate issuer entirely.

Validate more than the signature. A token can be perfectly signed and still be wrong for your API if the issuer, audience, or timing claims don’t match.

If your team tests security assumptions late, auth failures tend to surface in production. A disciplined review against guidance like https://goregulus.com/cra-basics/owasp-testing-guide/ helps catch claim validation gaps before release.

Configuring a Modern Spring Security Filter Chain

If you’re on Spring Boot 3, use the current Spring Security model, not the old adapter-based examples still floating around in blogs and codebases. Spring Security’s JWT integration received major enhancements in version 6.0, released on 17 November 2022, with native resource server support through spring-security-oauth2-resource-server and spring-security-oauth2-jose, plus the move to lambda-based SecurityFilterChain configuration (DevGenius guide).

Add the right dependencies

Maven:

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-oauth2-jose</artifactId>
    </dependency>
</dependencies>

Gradle:

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-security'
    implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server'
    implementation 'org.springframework.security:spring-security-oauth2-jose'
}

Use a resource server configuration, not a custom JWT filter by default

A lot of teams start by writing a OncePerRequestFilter that parses bearer tokens manually. That’s often unnecessary. Spring Security already knows how to validate JWTs if you configure the resource server properly.

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf.disable())
            .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/authenticate", "/signin").permitAll()
                .anyRequest().authenticated()
            )
            .oauth2ResourceServer(oauth2 -> oauth2.jwt());

        return http.build();
    }
}

Each line matters:

  • csrf.disable() is appropriate for a stateless API using bearer tokens rather than browser-managed sessions.
  • STATELESS ensures Spring Security won’t create or rely on server-side sessions.
  • Public auth endpoints let callers obtain tokens without already being authenticated.
  • oauth2ResourceServer().jwt() enables built-in JWT authentication support.

Prefer RS256 for production systems

For regulated products, use asymmetric signing. The private key stays with the issuer. Resource servers only need the public key or JWK set.

That design is safer operationally because verifiers don’t need access to signing secrets. It also fits distributed product teams better. Identity can issue tokens centrally, while backend services validate them independently.

Here’s a simple decoder bean using an RSA public key:

@Bean
JwtDecoder jwtDecoder(RSAPublicKey publicKey) {
    return NimbusJwtDecoder.withPublicKey(publicKey).build();
}

If your issuer exposes a JWKS endpoint, Spring can fetch keys and validate signatures against them. That also supports key rotation more cleanly than shipping shared secrets between services.

A production-minded baseline

Use this checklist before you call the configuration done:

  • Lock the session policy down. If the app creates sessions accidentally, your “stateless” design is only stateless on slides.
  • Keep endpoint rules explicit. Avoid broad permitAll() patterns that grow over time.
  • Fail closed. If token validation fails, the request should die at the filter chain.
  • Instrument security events. Expose enough telemetry to detect invalid token spikes and auth failures. Operational visibility matters as much as the config itself, and teams already using actuator should think carefully about what they expose and how they secure it, especially if they rely on guidance such as https://goregulus.com/cra-basics/spring-boot-actuator/

The best spring security jwt setups are boring in production. They authenticate predictably, reject bad tokens quickly, and don’t depend on hand-written filter logic unless there’s a real need.

Implementing Token Generation and Validation

Resource server configuration protects your endpoints. You still need an issuing side that creates tokens with the right claims and signs them correctly.

A diagram illustrating the Spring Security Filter Chain with a JWT Token Service component for generation and validation.

Build tokens with claims you can defend

A token should answer four questions:

  1. Who is this?
  2. Who issued it?
  3. Until when is it valid?
  4. What can it do?

A practical issuing service using Spring Security’s JWT support looks like this:

@Service
public class JwtTokenService {

    private final JwtEncoder jwtEncoder;

    public JwtTokenService(JwtEncoder jwtEncoder) {
        this.jwtEncoder = jwtEncoder;
    }

    public String generateToken(String username, List<String> scopes) {
        Instant now = Instant.now();

        JwtClaimsSet claims = JwtClaimsSet.builder()
            .issuer("https://auth.example.eu")
            .subject(username)
            .issuedAt(now)
            .expiresAt(now.plusSeconds(900))
            .claim("scope", String.join(" ", scopes))
            .build();

        JwsHeader header = JwsHeader.with(SignatureAlgorithm.RS256).build();

        return this.jwtEncoder.encode(JwtEncoderParameters.from(header, claims)).getTokenValue();
    }
}

The Spring Security reference is clear on the shape of this model. JwtEncoder signs tokens, JwtDecoder verifies them, and claims such as exp are part of the protection against replay-style misuse in real systems. The same reference also notes the maturity of automatic JWK rotation and Nimbus JOSE support in Spring Security 6.1, which makes this model more reliable for production resource servers.

Expose an authentication endpoint that does less, not more

Keep the login flow narrow. Authenticate credentials, issue a token, return it. Don’t let the controller grow into an identity platform.

@RestController
@RequestMapping("/auth")
public class AuthController {

    private final AuthenticationManager authenticationManager;
    private final JwtTokenService jwtTokenService;

    public AuthController(AuthenticationManager authenticationManager,
                          JwtTokenService jwtTokenService) {
        this.authenticationManager = authenticationManager;
        this.jwtTokenService = jwtTokenService;
    }

    @PostMapping("/authenticate")
    public Map<String, String> authenticate(@RequestBody LoginRequest request) {
        Authentication authentication =
            authenticationManager.authenticate(
                new UsernamePasswordAuthenticationToken(
                    request.username(), request.password()
                )
            );

        String token = jwtTokenService.generateToken(
            authentication.getName(),
            authentication.getAuthorities().stream()
                .map(GrantedAuthority::getAuthority)
                .map(a -> a.startsWith("SCOPE_") ? a.substring(6) : a)
                .toList()
        );

        return Map.of("access_token", token);
    }
}

Make validation stricter than the defaults when needed

By default, Spring will validate structure, signature, and standard timing rules. In regulated environments, add issuer validation and authority mapping deliberately.

@Bean
JwtAuthenticationConverter jwtAuthenticationConverter() {
    JwtGrantedAuthoritiesConverter scopes = new JwtGrantedAuthoritiesConverter();
    scopes.setAuthorityPrefix("SCOPE_");
    scopes.setAuthoritiesClaimName("scope");

    JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
    converter.setJwtGrantedAuthoritiesConverter(scopes);
    return converter;
}

Then wire it into the resource server:

@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http,
                                        JwtAuthenticationConverter converter) throws Exception {
    http
        .csrf(csrf -> csrf.disable())
        .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
        .authorizeHttpRequests(auth -> auth
            .requestMatchers("/auth/authenticate").permitAll()
            .anyRequest().authenticated()
        )
        .oauth2ResourceServer(oauth2 -> oauth2
            .jwt(jwt -> jwt.jwtAuthenticationConverter(converter))
        );

    return http.build();
}

A protected endpoint should stay simple

If token validation is correct, controllers become small:

@RestController
public class ProfileController {

    @GetMapping("/profile")
    public Map<String, Object> profile(Jwt jwt) {
        return Map.of(
            "subject", jwt.getSubject(),
            "issuer", jwt.getIssuer().toString(),
            "scopes", jwt.getClaimAsString("scope")
        );
    }
}

That’s the point. Your business endpoints shouldn’t care how signatures work. They should trust the security layer and read the authenticated principal.

Store signing keys outside the codebase. For teams already standardising on managed secret storage, the operational discipline behind https://goregulus.com/cra-basics/aws-secrets-manager/ maps well to this problem, especially for key material and related configuration.

Architecting a Secure Refresh Token Flow

Most spring security jwt tutorials stop after the first access token. That’s where production trouble starts.

The gap matters because token lifecycle management and refresh token strategy remain underexplored in mainstream tutorials, even though they’re critical for CRA-aligned manufacturers that need durable authentication controls and auditable behaviour around long-lived access (OneUptime analysis).

Why the basic pattern breaks down

A short-lived access token is good security practice. It limits exposure if a token leaks. But users and devices still need continuity.

If you force a full credential login every time an access token expires, you’ll either harm usability or push teams into making access tokens live too long. Both are bad outcomes.

The safer pattern

Use two credentials with different jobs:

TokenPurposeHandling rule
Access tokenCalls APIsKeep it short-lived and validate on every request
Refresh tokenObtains a new access tokenStore server-side state and rotate it on use

The refresh token shouldn’t just be “another JWT that lives longer”. For most product systems, it’s better to treat it as a server-tracked credential with revocation state, device context, and audit visibility.

What works in practice

A durable refresh flow usually looks like this:

  • Issue both tokens at login. The access token is presented to APIs. The refresh token is used only at the token renewal endpoint.
  • Store refresh tokens with state. Persist a token identifier, subject, expiry, revocation flag, and client or device metadata.
  • Rotate on every use. When a client presents a refresh token, invalidate it and issue a new one.
  • Detect replay. If an already-rotated refresh token appears again, treat it as suspicious and revoke the token family or the affected session set.
  • Log the event. Authentication lifecycle events matter for incident response and compliance evidence.

Here’s the mistake to avoid: stateless access tokens do not mean every auth artefact must be stateless. Refresh tokens are where controlled state becomes valuable.

If a refresh token can be reused indefinitely after theft, your login flow is only pretending to be secure.

Minimal endpoint shape

A practical /auth/refresh endpoint should do four things in order:

  1. Look up the presented refresh token record.
  2. Verify it hasn’t expired or been revoked.
  3. Revoke it immediately and mint a replacement.
  4. Issue a fresh access token.

That pattern gives you revocation, auditability, and replay detection. It also gives product security teams something concrete to document when they’re asked how long-lived sessions are controlled.

Security Hardening and Operational Best Practices

A working implementation isn’t the same as a hardened one. The difference usually shows up during an incident.

A pencil sketch style drawing of a shield containing a wrench, a lock, and the words Hardened Security.

Pitfall and mitigation

  • Using HS256 in a distributed productShared-secret signing looks simple, but it creates key distribution problems. In a multi-service environment, every verifier that can validate may also become part of the signing secret exposure path. Prefer RS256 so the private key stays with the issuer and verifiers only need the public key.

  • Treating key rotation as optionalRotation plans shouldn’t live in a runbook no one has tested. Use JWK-based distribution where possible, publish active keys cleanly, and rehearse cutover before you need it.

  • Logging bearer tokensDebug logs, reverse proxies, and error handlers often leak the Authorization header by accident. Scrub tokens from logs and traces. If you need correlation, log token identifiers or request IDs, not the token value.

  • Storing tokens carelessly on the clientBrowser storage and device storage choices matter. Keep transmission over HTTPS only, and avoid patterns that leave tokens broadly accessible to client-side scripts or uncontrolled local persistence.

Operational checks worth enforcing

  • Validate all expected claims. Signature-only validation is incomplete.
  • Separate issuers by trust boundary. Don’t let internal tooling and customer-facing apps share loose trust rules.
  • Build revocation paths. Incident response needs more than “wait for expiry”.
  • Review adjacent controls. Teams working through broader secure access models may also benefit from guidance on secure access protocols, especially when multiple actors touch the same regulated platform.

Good JWT security is mostly operational discipline. The cryptography is the easy part. Running keys, trust rules, revocation, and telemetry correctly is where mature teams separate themselves.

Frequently Asked Questions about Spring Security JWT

Should I write my own JWT filter?

Usually, no. Start with Spring Security’s resource server support. Write custom filters only when you have a specific requirement that the framework can’t already handle cleanly.

Is JWT always better than sessions?

No. JWT is a strong fit for APIs, distributed services, and device-facing systems. If you’re building a simple server-rendered application, session-based auth may still be the simpler and safer choice.

Should refresh tokens also be JWTs?

Sometimes, but that’s not the default I recommend for regulated products. Server-tracked refresh tokens give you better revocation, replay detection, and audit control.

What claims should every access token include?

At minimum, include a subject, issuer, expiry, and the authorisation data your API needs. Keep claims minimal. If the API doesn’t need a field, don’t put it in the token.

How do I justify this design to compliance teams?

Document the trust boundaries clearly. Identify the issuer, the signing method, the validation rules, token lifetime, refresh token handling, revocation process, and logging approach. If your team can explain those controls plainly, you’re in a much stronger position during review.


If your team is preparing a digital product for the EU market, Regulus helps turn requirements into an actionable CRA compliance plan. It’s built for manufacturers, IoT vendors, and product teams that need clarity on applicability, classification, technical documentation, and post-market obligations without managing the whole process in spreadsheets.

Last reviewed:

More

CRA Free Tools

CRA Scope Wizard Icon

CRA Scope Wizard

CRA Cost Calculator Icon

CRA Cost Calculator

CRA declaration of conformity generator showing a filled Annex V Declaration of Conformity document for EU CRA compliance

CRA DoC Generator

Regulus Logo
Privacy Overview

This website uses cookies so that we can provide you with the best user experience possible. Cookie information is stored in your browser and performs functions such as recognising you when you return to our website and helping our team to understand which sections of the website you find most interesting and useful.