The auth-service module is the runnable implementation of the Auth microservice. It exposes the endpoints defined in auth, handles JWT generation via JwtService, and delegates account management to the account microservice via Feign. The RouterValidator component — also introduced here — is the piece that tells the Gateway which routes require authentication and which are open to everyone.
1. Open vs. Secured Routes
Not every endpoint requires authentication. Registration and login must be publicly accessible — otherwise, users could never authenticate in the first place. All other routes are secured: they require a valid JWT cookie.
The RouterValidator encodes this policy as a simple predicate:
flowchart LR
request[Incoming Request]
open{Is open route?}
secured[Proceed to filter]
forward[Forward request]
request --> open
open -- Yes --> forward
open -- No --> secured
secured --> forward
The open routes defined in this platform are:
Method
Path
Purpose
POST
/auth/register
Create a new account
POST
/auth/login
Authenticate and receive a JWT cookie
GET
/auth/logout
Clear the JWT cookie
Everything else — including all /accounts/** endpoints — requires a valid cookie.
2. The auth-service Module
2.1 Repository
Create a new git repository for the auth service and add it as a submodule:
Spring Boot entry point; enables Feign clients from the store.account package
AuthResource
REST controller implementing AuthController; sets the JWT cookie on login/logout
AuthService
Business logic: delegates registration to account, calls JwtService to generate tokens
JwtService
Wraps the JJWT library; signs and parses JWTs using an HMAC secret key
2.3 JwtService in detail
JwtService is responsible for two things:
Generating a token (generate): it builds a JWT with the account's id as the jti claim, the account's name as subject, and the email as a custom claim. The token is signed with an HMAC key derived from the base64-encoded secret, and it expires after the configured duration (milliseconds).
Validating a token (getId): it parses and verifies the signature, then checks that the token is neither premature (nbf) nor expired (exp). If valid, it returns the account's id.
sequenceDiagram
autonumber
actor User
User->>+AuthResource: POST /auth/login (LoginIn)
AuthResource->>+AuthService: login(email, password)
AuthService->>+AccountController: findByEmailAndPassword(AccountIn)
AccountController-->>-AuthService: AccountOut
AuthService->>+JwtService: generate(AccountOut, duration)
JwtService-->>-AuthService: signed JWT string
AuthService-->>-AuthResource: TokenOut
AuthResource->>-User: Set-Cookie: __store_jwt_token=<jwt>
server:port:8080spring:application:name:authstore:jwt:secretKey:${JWT_SECRET_KEY}duration:86400000# 24 hours in millisecondshttpOnly:${JWT_HTTP_ONLY}logging:level:root:infostore:debugorg.springframework.web:debug
packagestore.auth;importjava.util.Base64;importjava.util.Date;importjava.util.Map;importjavax.crypto.SecretKey;importorg.springframework.beans.factory.annotation.Value;importorg.springframework.http.HttpStatus;importorg.springframework.stereotype.Service;importorg.springframework.web.server.ResponseStatusException;importio.jsonwebtoken.Claims;importio.jsonwebtoken.JwtParser;importio.jsonwebtoken.Jwts;importio.jsonwebtoken.security.Keys;importstore.account.AccountOut;@ServicepublicclassJwtService{@Value("${store.jwt.secretKey}")privateStringsecretKey;publicStringgenerate(AccountOutaccount,longduration){Datenow=newDate();returnJwts.builder().header().and().id(account.id()).issuer("Insper::PMA").claims(Map.of("email",account.email())).signWith(getKey()).subject(account.name()).notBefore(now).expiration(newDate(now.getTime()+duration)).compact();}privateSecretKeygetKey(){returnKeys.hmacShaKeyFor(Base64.getDecoder().decode(secretKey));}publicStringgetId(Stringjwt){JwtParserparser=Jwts.parser().verifyWith(getKey()).build();Claimsclaims=parser.parseSignedClaims(jwt).getPayload();Datenow=newDate();if(claims.getNotBefore().after(now)){thrownewResponseStatusException(HttpStatus.UNAUTHORIZED,"Token is not valid yet!");}if(claims.getExpiration().before(now)){thrownewResponseStatusException(HttpStatus.UNAUTHORIZED,"Token is expired!");}returnclaims.getId();}}
The JWT is delivered to the browser as an HttpOnly cookie named __store_jwt_token. Using an HttpOnly cookie instead of storing the token in localStorage prevents JavaScript from reading it, which mitigates XSS attacks.
Additional cookie flags used:
Flag
Value
Purpose
HttpOnly
true
JavaScript cannot access the cookie
Secure
true
Cookie is only sent over HTTPS
SameSite
None
Cookie is sent on cross-site requests (needed for SPA frontends)
Path
/
Cookie applies to all paths
SameSite=None requires Secure=true
Browsers reject SameSite=None cookies without the Secure flag. This means the frontend and the API must communicate over HTTPS in production.
Done! The auth-service is ready. In the next section we will implement the Gateway filter that intercepts every request, extracts the cookie, and asks auth-service to resolve the token.