{"id":2311,"date":"2026-07-20T12:41:35","date_gmt":"2026-07-20T10:41:35","guid":{"rendered":"https:\/\/kindsonthegenius.com\/blog\/inventoryms-user-authentication-with-jwt\/"},"modified":"2026-08-27T15:46:52","modified_gmt":"2026-08-27T13:46:52","slug":"inventoryms-user-authentication-with-jwt","status":"publish","type":"post","link":"https:\/\/kindsonthegenius.com\/blog\/inventoryms-user-authentication-with-jwt\/","title":{"rendered":"InventoryMS \u2013 User Authentication with JWT"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\"><em>Learn how to add JWT authentication to a Spring Boot API with JJWT, including login, registration, Bearer tokens, security filters, and Postman testing.<\/em><\/p>\n\n\n<p><!-- ktg-updated-banner --><\/p>\n<p><em>Updated August 2026 \u2014 full tutorial restored for the InventoryMS JWT authentication URL.<\/em><\/p>\n<h2>TL;DR<\/h2>\n<ul>\n<li>\n<p><strong>JWT authentication<\/strong> lets InventoryMS use stateless Bearer tokens instead of Basic Auth sessions.<\/p>\n<\/li>\n<li>\n<p><strong>JJWT<\/strong> handles creating and validating signed tokens containing the authenticated user\u2019s identity.<\/p>\n<\/li>\n<li>\n<p>A <strong>JWT authentication filter<\/strong> reads the Bearer token on each request and loads the user\u2019s current authorities.<\/p>\n<\/li>\n<li>\n<p><strong>Spring Security<\/strong> can enforce InventoryMS privileges such as <code>VIEW_PRODUCT<\/code> and <code>CREATE_PRODUCT<\/code> on protected endpoints.<\/p>\n<\/li>\n<li>\n<p><strong>Postman<\/strong> provides a simple way to test registration, login, protected requests, authorization failures, and missing tokens.<\/p>\n<\/li>\n<\/ul>\n<p>In this tutorial we add <strong>JSON Web Token (JWT)<\/strong> authentication to the <strong>InventoryMS<\/strong> Spring Boot API. After login (or register), the client receives a signed token and sends it as <code>Authorization: Bearer \u2026<\/code> on every protected call \u2014 no Basic Auth session cookies.<\/p>\n<p>Previously we covered:<\/p>\n<ul>\n<li><a href=\"https:\/\/kindsonthegenius.com\/blog\/inventoryms-complete-application-spring-boot-api\/\">InventoryMS Complete Application \u2013 Spring Boot API<\/a><\/li>\n<li><a href=\"https:\/\/kindsonthegenius.com\/blog\/inventoryms-implementing-springboot-roles-and-privileges-1\/\">Roles and Privileges 1 \u2013 standard roles<\/a><\/li>\n<li><a href=\"https:\/\/kindsonthegenius.com\/blog\/inventoryms-springboot-roles-and-privileges-2-the-data-model-and-api\/\">Roles and Privileges 2 \u2013 data model and API<\/a><\/li>\n<li><a href=\"https:\/\/kindsonthegenius.com\/blog\/inventoryms-springboot-roles-and-privileges-3-implementing-granted-authorities\/\">Roles and Privileges 3 \u2013 Granted Authorities<\/a><\/li>\n<\/ul>\n<p>A general JWT walkthrough (good companion) is here: <a href=\"https:\/\/kindsonthegenius.com\/blog\/json-web-token-how-to-secure-rest-api-using-jwt-in-spring-boot\/\">Secure a REST API using JWT in Spring Boot<\/a>.<\/p>\n<p>We will cover:<\/p>\n<ol>\n<li><a href=\"#t1\">Why JWT for InventoryMS<\/a><\/li>\n<li><a href=\"#t2\">Dependencies and application properties<\/a><\/li>\n<li><a href=\"#t3\">JwtService \u2013 create and validate tokens<\/a><\/li>\n<li><a href=\"#t4\">Auth DTOs, register and login endpoints<\/a><\/li>\n<li><a href=\"#t5\">JwtAuthenticationFilter<\/a><\/li>\n<li><a href=\"#t6\">Wire SecurityFilterChain (stateless)<\/a><\/li>\n<li><a href=\"#t7\">Test with Postman<\/a><\/li>\n<li><a href=\"#t8\">Next steps (React UI)<\/a><\/li>\n<\/ol>\n<p><strong id=\"t1\">1. Why JWT for InventoryMS<\/strong><\/p>\n<p>So far InventoryMS can authorize with <code>hasAuthority(\"VIEW_PRODUCT\")<\/code> (and friends) via <code>UserPrincipal<\/code>. That works for Basic Auth, but a React SPA needs a <strong>stateless<\/strong> token:<\/p>\n<ul>\n<li>login once \u2192 receive JWT<\/li>\n<li>store token (memory or httpOnly cookie in production)<\/li>\n<li>send Bearer token on API calls<\/li>\n<li>server validates signature + expiry, then loads authorities<\/li>\n<\/ul>\n<p>We keep your privilege model. JWT only changes <em>how<\/em> the user proves identity.<\/p>\n<p><strong id=\"t2\">2. Dependencies and application properties<\/strong><\/p>\n<p><strong>Step 1:<\/strong> In <code>pom.xml<\/code> add JJWT (versions that match your Spring Boot 3.x line):<\/p>\n<pre><code>&lt;dependency&gt;\n  &lt;groupId&gt;io.jsonwebtoken&lt;\/groupId&gt;\n  &lt;artifactId&gt;jjwt-api&lt;\/artifactId&gt;\n  &lt;version&gt;0.12.5&lt;\/version&gt;\n&lt;\/dependency&gt;\n&lt;dependency&gt;\n  &lt;groupId&gt;io.jsonwebtoken&lt;\/groupId&gt;\n  &lt;artifactId&gt;jjwt-impl&lt;\/artifactId&gt;\n  &lt;version&gt;0.12.5&lt;\/version&gt;\n  &lt;scope&gt;runtime&lt;\/scope&gt;\n&lt;\/dependency&gt;\n&lt;dependency&gt;\n  &lt;groupId&gt;io.jsonwebtoken&lt;\/groupId&gt;\n  &lt;artifactId&gt;jjwt-jackson&lt;\/artifactId&gt;\n  &lt;version&gt;0.12.5&lt;\/version&gt;\n  &lt;scope&gt;runtime&lt;\/scope&gt;\n&lt;\/dependency&gt;\n<\/code><\/pre>\n<p><strong>Step 2:<\/strong> In <code>application.properties<\/code> (use a long random secret in real projects \u2014 never commit production keys):<\/p>\n<pre><code>app.jwt.secret=ChangeMeToALongRandomSecretAtLeast32Chars!!\napp.jwt.expiration-ms=86400000\n<\/code><\/pre>\n<p><strong id=\"t3\">3. JwtService \u2013 create and validate tokens<\/strong><\/p>\n<p>Create <code>security.JwtService<\/code> (or <code>jwt.JwtService<\/code>) that signs with HMAC and puts the username in the subject.<\/p>\n<pre><code>@Service\npublic class JwtService {\n\n    @Value(\"${app.jwt.secret}\")\n    private String secret;\n\n    @Value(\"${app.jwt.expiration-ms}\")\n    private long expirationMs;\n\n    private SecretKey key() {\n        return Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));\n    }\n\n    public String generateToken(UserDetails user) {\n        Date now = new Date();\n        Date exp = new Date(now.getTime() + expirationMs);\n        return Jwts.builder()\n                .subject(user.getUsername())\n                .issuedAt(now)\n                .expiration(exp)\n                .signWith(key())\n                .compact();\n    }\n\n    public String extractUsername(String token) {\n        return Jwts.parser().verifyWith(key()).build()\n                .parseSignedClaims(token).getPayload().getSubject();\n    }\n\n    public boolean isValid(String token, UserDetails user) {\n        String username = extractUsername(token);\n        return username.equals(user.getUsername()) &amp;&amp; !isExpired(token);\n    }\n\n    private boolean isExpired(String token) {\n        Date exp = Jwts.parser().verifyWith(key()).build()\n                .parseSignedClaims(token).getPayload().getExpiration();\n        return exp.before(new Date());\n    }\n}\n<\/code><\/pre>\n<p>We intentionally keep <strong>authorities out of the token body<\/strong> for InventoryMS: privileges can change without waiting for token expiry. On each request we load <code>UserPrincipal<\/code> (and privileges) from the database using the username claim.<\/p>\n<p><strong id=\"t4\">4. Auth DTOs, register and login endpoints<\/strong><\/p>\n<p><strong>Step 1:<\/strong> Simple request\/response records:<\/p>\n<pre><code>public record AuthRequest(String username, String password) {}\npublic record AuthResponse(String token, String tokenType) {\n    public AuthResponse(String token) { this(token, \"Bearer\"); }\n}\npublic record RegisterRequest(String username, String password, String email) {}\n<\/code><\/pre>\n<p><strong>Step 2:<\/strong> <code>AuthController<\/code> \u2014 keep paths matching your existing <code>permitAll<\/code> rules (<code>\/login<\/code>, <code>\/register<\/code>). Adjust to <code>\/api\/v1\/auth\/...<\/code> if that is how you designed routing.<\/p>\n<pre><code>@RestController\n@RequiredArgsConstructor\npublic class AuthController {\n\n    private final AuthenticationManager authenticationManager;\n    private final UserDetailsService userDetailsService;\n    private final PasswordEncoder passwordEncoder;\n    private final UserRepository userRepository;\n    private final JwtService jwtService;\n\n    @PostMapping(\"\/login\")\n    public AuthResponse login(@RequestBody AuthRequest request) {\n        authenticationManager.authenticate(\n                new UsernamePasswordAuthenticationToken(\n                        request.username(), request.password()));\n        UserDetails user = userDetailsService.loadUserByUsername(request.username());\n        return new AuthResponse(jwtService.generateToken(user));\n    }\n\n    @PostMapping(\"\/register\")\n    public AuthResponse register(@RequestBody RegisterRequest request) {\n        User user = new User();\n        user.setUsername(request.username());\n        user.setEmail(request.email());\n        user.setPassword(passwordEncoder.encode(request.password()));\n        userRepository.save(user);\n        UserDetails details = userDetailsService.loadUserByUsername(user.getUsername());\n        return new AuthResponse(jwtService.generateToken(details));\n    }\n}\n<\/code><\/pre>\n<p>Wire <code>AuthenticationManager<\/code> and <code>PasswordEncoder<\/code> beans if you have not already (standard Spring Security setup from the InventoryMS security package).<\/p>\n<p><strong id=\"t5\">5. JwtAuthenticationFilter<\/strong><\/p>\n<p>This filter runs once per request, reads the Bearer header, validates the JWT, and sets <code>SecurityContextHolder<\/code>.<\/p>\n<pre><code>@Component\n@RequiredArgsConstructor\npublic class JwtAuthenticationFilter extends OncePerRequestFilter {\n\n    private final JwtService jwtService;\n    private final UserDetailsService userDetailsService;\n\n    @Override\n    protected void doFilterInternal(HttpServletRequest request,\n                                    HttpServletResponse response,\n                                    FilterChain chain)\n            throws ServletException, IOException {\n\n        String header = request.getHeader(\"Authorization\");\n        if (header == null || !header.startsWith(\"Bearer \")) {\n            chain.doFilter(request, response);\n            return;\n        }\n\n        String token = header.substring(7);\n        String username = jwtService.extractUsername(token);\n        if (username != null &amp;&amp; SecurityContextHolder.getContext().getAuthentication() == null) {\n            UserDetails user = userDetailsService.loadUserByUsername(username);\n            if (jwtService.isValid(token, user)) {\n                UsernamePasswordAuthenticationToken auth =\n                        new UsernamePasswordAuthenticationToken(\n                                user, null, user.getAuthorities());\n                auth.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));\n                SecurityContextHolder.getContext().setAuthentication(auth);\n            }\n        }\n        chain.doFilter(request, response);\n    }\n}\n<\/code><\/pre>\n<p>Because <code>UserDetailsService<\/code> returns your <code>UserPrincipal<\/code>, <code>getAuthorities()<\/code> still comes from <code>UserPrivilegeAssignment<\/code> \u2014 same as Part 3.<\/p>\n<p><strong id=\"t6\">6. Wire SecurityFilterChain (stateless)<\/strong><\/p>\n<p>Update the chain you built in the privileges tutorial:<\/p>\n<pre><code>@Bean\nSecurityFilterChain filterChain(HttpSecurity http, JwtAuthenticationFilter jwtFilter) throws Exception {\n    http.csrf(csrf -&gt; csrf.disable())\n        .sessionManagement(sm -&gt;\n                sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))\n        .authorizeHttpRequests(auth -&gt; auth\n                .requestMatchers(\"\/register\", \"\/login\", \"\/v3\/api-docs\/**\", \"\/swagger-ui\/**\").permitAll()\n                .requestMatchers(HttpMethod.GET, \"\/api\/v1\/products\/**\").hasAuthority(\"VIEW_PRODUCT\")\n                .requestMatchers(HttpMethod.POST, \"\/api\/v1\/products\/**\").hasAuthority(\"CREATE_PRODUCT\")\n                .anyRequest().authenticated())\n        .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);\n    return http.build();\n}\n<\/code><\/pre>\n<p>Disable HTTP Basic for the SPA path (or leave it only for local debugging). CSRF stays off for pure Bearer JWT APIs.<\/p>\n<p><strong id=\"t7\">7. Test with Postman<\/strong><\/p>\n<ol>\n<li><code>POST \/register<\/code> with JSON <code>{\"username\":\"kindson\",\"password\":\"Secret123\",\"email\":\"k@example.com\"}<\/code> \u2192 copy <code>token<\/code>.<\/li>\n<li>Or <code>POST \/login<\/code> with username\/password \u2192 copy <code>token<\/code>.<\/li>\n<li><code>GET \/api\/v1\/products<\/code> with header <code>Authorization: Bearer &lt;token&gt;<\/code>.<\/li>\n<li>Assign <code>VIEW_PRODUCT<\/code> via your privilege assignment API if the call returns 403.<\/li>\n<li>Call again without the header \u2192 expect 401.<\/li>\n<\/ol>\n<p><strong id=\"t8\">8. Next steps (React UI)<\/strong><\/p>\n<ul>\n<li>On successful login, store the token (prefer memory + refresh flow in production).<\/li>\n<li>Axios\/fetch interceptor: <code>config.headers.Authorization = `Bearer ${token}`<\/code>.<\/li>\n<li>On 401, clear token and redirect to login.<\/li>\n<\/ul>\n<p>That wires cleanly into <a href=\"https:\/\/kindsonthegenius.com\/blog\/inventoryms-complete-application-react-ui\/\">InventoryMS Complete Application \u2013 React UI<\/a>.<\/p>\n<p><strong>Common mistakes<\/strong><\/p>\n<ul>\n<li><strong>401 on every call:<\/strong> missing <code>Bearer <\/code> prefix, wrong secret between instances, or filter not registered with <code>addFilterBefore<\/code>.<\/li>\n<li><strong>403 after login:<\/strong> token is valid but privileges are empty \u2014 assign <code>VIEW_PRODUCT<\/code> (etc.) via the UserPrivilegeAssignment API from Part 2\/3.<\/li>\n<li><strong>Weak secret:<\/strong> JJWT requires a long enough key for HS256; short secrets throw at runtime.<\/li>\n<li><strong>Putting roles only in the JWT:<\/strong> fine for demos; for InventoryMS we reload <code>UserPrincipal<\/code> so privilege changes apply immediately.<\/li>\n<li><strong>Leaving CSRF on<\/strong> while using Bearer-only SPA calls \u2014 disable CSRF for the API chain as shown above.<\/li>\n<\/ul>\n<p><strong>What you should have now<\/strong><\/p>\n<ul>\n<li>JWT issued on register\/login<\/li>\n<li>Stateless filter that authenticates Bearer tokens<\/li>\n<li>Same privilege authorities as before protecting InventoryMS modules<\/li>\n<\/ul>\n<p>If anything fails, check clock skew \/ secret length, and confirm <code>UserDetailsService<\/code> loads the same username you put in the JWT subject. When this works end-to-end, move on to sending the token from the React UI.<\/p>\n<h2>Final Thought<\/h2>\n<p>Adding JWT authentication gives InventoryMS a stateless security model that fits naturally with a React SPA. The token proves the user\u2019s identity, while Spring Security continues to enforce the same privilege-based authorization rules already established in the application.<\/p>\n<p>A particularly useful design choice in this tutorial is keeping privileges out of the JWT and loading the current <code>UserPrincipal<\/code> on each request. This means changes to a user\u2019s privileges can take effect without waiting for an existing token to expire.<\/p>\n<p>Once registration, login, token validation, and protected API calls work end-to-end, the next step is connecting the JWT flow to the React UI. From there, you can build on the foundation with refresh-token flows, stronger token management, error handling, and production-ready secret management.<\/p>","protected":false},"excerpt":{"rendered":"<p>Learn how to add JWT authentication to a Spring Boot API with JJWT, including login, registration, Bearer tokens, security filters, and Postman testing. Updated August &hellip; <\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"pagelayer_contact_templates":[],"_pagelayer_content":"","footnotes":""},"categories":[85],"tags":[],"class_list":["post-2311","post","type-post","status-publish","format-standard","hentry","category-java"],"acf":[],"_links":{"self":[{"href":"https:\/\/kindsonthegenius.com\/blog\/wp-json\/wp\/v2\/posts\/2311","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/kindsonthegenius.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/kindsonthegenius.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/kindsonthegenius.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/kindsonthegenius.com\/blog\/wp-json\/wp\/v2\/comments?post=2311"}],"version-history":[{"count":3,"href":"https:\/\/kindsonthegenius.com\/blog\/wp-json\/wp\/v2\/posts\/2311\/revisions"}],"predecessor-version":[{"id":2496,"href":"https:\/\/kindsonthegenius.com\/blog\/wp-json\/wp\/v2\/posts\/2311\/revisions\/2496"}],"wp:attachment":[{"href":"https:\/\/kindsonthegenius.com\/blog\/wp-json\/wp\/v2\/media?parent=2311"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/kindsonthegenius.com\/blog\/wp-json\/wp\/v2\/categories?post=2311"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/kindsonthegenius.com\/blog\/wp-json\/wp\/v2\/tags?post=2311"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}