Spring Security: Simple Authentication with Opaque Tokens & Comparison to JWT

Jan 14, 2025

šŸ” Spring Security: Simple Authentication with Opaque Tokens

This guide demonstrates opaque token authentication in Spring Boot and compares it with JWT for clarity.

Opaque tokens are random strings issued by an auth server. Only the server can interpret them. They are easy to revoke and secure by default. JWTs are self-contained tokens with encoded claims and a signature.


🧱 Key Components

  • AuthUser: Represents authenticated user data.
  • UserAuthentication: Implements Spring Security's Authentication.
  • TokenAuthenticationFilter: Validates incoming tokens.
  • SecurityConfig: Integrates the filter into Spring Security.

Step 1: Define AuthUser

package com.example.security.user;

import java.util.List;

public record AuthUser(String userId, List<String> roles) {}

Step 2: Implement UserAuthentication

package com.example.security.authentication;

import com.example.security.user.AuthUser;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;

import java.util.Collection;
import java.util.stream.Collectors;

public class UserAuthentication implements Authentication {

    private final AuthUser authUser;

    public UserAuthentication(AuthUser authUser) {
        this.authUser = authUser;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return authUser.roles().stream()
                .map(SimpleGrantedAuthority::new)
                .collect(Collectors.toList());
    }

    @Override
    public Object getCredentials() { return null; }

    @Override
    public Object getDetails() { return null; }

    @Override
    public Object getPrincipal() { return authUser; }

    @Override
    public boolean isAuthenticated() { return true; }

    @Override
    public void setAuthenticated(boolean isAuthenticated) {
        throw new UnsupportedOperationException();
    }

    @Override
    public String getName() { return authUser.userId(); }
}

Step 3: Token Authentication Filter

package com.example.security.filter;

import com.example.security.authentication.UserAuthentication;
import com.example.security.user.AuthUser;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class TokenAuthenticationFilter extends OncePerRequestFilter {

    private final TokenService tokenService;

    public TokenAuthenticationFilter(TokenService tokenService) {
        this.tokenService = tokenService;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {

        String token = extractToken(request);
        if (token != null && tokenService.isValid(token)) {
            AuthUser authUser = tokenService.getUserFromToken(token);
            UserAuthentication authentication = new UserAuthentication(authUser);
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }
        filterChain.doFilter(request, response);
    }

    private String extractToken(HttpServletRequest request) {
        String header = request.getHeader("Authorization");
        if (header != null && header.startsWith("Bearer ")) {
            return header.substring(7);
        }
        return null;
    }
}

Step 4: Security Configuration

package com.example.security.config;

import com.example.security.filter.TokenAuthenticationFilter;
import com.example.security.service.TokenService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    private final TokenService tokenService;

    public SecurityConfig(TokenService tokenService) {
        this.tokenService = tokenService;
    }

    @Bean
    public HttpSecurity configure(HttpSecurity http) throws Exception {
        http
            .addFilterBefore(new TokenAuthenticationFilter(tokenService), UsernamePasswordAuthenticationFilter.class)
            .authorizeRequests()
                .antMatchers("/login", "/register").permitAll()
                .anyRequest().authenticated();
        return http;
    }
}

āš–ļø Opaque Tokens vs JWT

1. Definition

Opaque Token: Random string, only auth server can interpret.

JWT (JSON Web Token): Self-contained token with encoded claims (user ID, roles, expiration) and a signature.

### 2. Structure
Feature	                Opaque Token	                            JWT (JSON Web Token)
Readable by client?	    āŒ No – token content is hidden	            āœ… Yes – can decode base64, but signature prevents tampering
Self-contained?	        āŒ No – server must store and check token	āœ… Yes – carries claims like user ID, roles, and expiration
Verification	        āœ… Must validate with authorization server	āœ… Can verify locally using the signature
Revocation	            āœ… Easy – simply delete token from server	āš ļø Difficult – requires blacklist or short expiration
Size	                Small – just a random string	            Larger – includes claims and signature
Flexibility	            Simple – no extra data inside	            High – can include roles, scopes, and expiration for authorization decisions

3. How They Work

Opaque Token Flow:

  1. Client gets token.

  2. Client calls API.

  3. API validates token with auth server.

  4. Server returns user info.

JWT Flow:

  1. Client gets JWT.

  2. Client calls API.

  3. API verifies signature locally.

  4. API reads claims for authorization.

4. Pros & Cons

Opaque Tokens

  • āœ… Secure by default

  • āœ… Easy to revoke

  • āŒ Requires server-side check per request

JWT

  • āœ… Stateless, no server call

  • āœ… Can include useful info in token

  • āŒ Harder to revoke

  • āŒ Larger size, may expose payload if not encrypted

5. When to Use Which

Opaque Tokens: Internal APIs, high security, easy revocation.

JWT: Microservices or distributed systems, stateless authentication, rarely changing claims.


āœ… Summary

Opaque tokens provide secure and easily revocable authentication, while JWTs allow stateless verification and include useful claims. Choose based on architecture and security needs.

ilia-kritiuk.dev