@@ -33,22 +33,29 @@ public class JwtConfig {
3333 @ Value ("${security.jwt.secret:ZmFrZS1zZWNyZXQtZmFrZS1zZWNyZXQtZmFrZS1zZWNyZXQtMTIzNDU2}" )
3434 private String secret ;
3535
36+ /** Long-lived refresh token expiry (default 7 days). Used for the refreshToken cookie. */
3637 @ Value ("${security.jwt.expiration-ms:604800000}" )
3738 private long expirationMs ;
3839
40+ /** Short-lived access token expiry (default 15 minutes). Returned in the JSON payload. */
41+ @ Value ("${security.jwt.access-token-expiration-ms:900000}" )
42+ private long accessTokenExpirationMs ;
43+
3944 @ Value ("${security.jwt.issuer:espaco-geek}" )
4045 private String issuer ;
4146
42- // Cookie settings
43- @ Value ("${security.jwt.cookie-name:EG_AUTH}" )
44- private String cookieName ;
45-
47+ // Cookie shared settings (path, domain, SameSite) used for both the refresh token cookie
48+ // and any other cookie-related operations. The legacy EG_AUTH cookie is no longer used.
4649 @ Value ("${security.jwt.cookie-path:/}" )
4750 private String cookiePath ;
4851
4952 @ Value ("${security.jwt.cookie-domain:}" )
5053 private String cookieDomain ;
5154
55+ // Refresh token cookie settings
56+ @ Value ("${security.jwt.refresh-token-cookie-name:refreshToken}" )
57+ private String refreshTokenCookieName ;
58+
5259 // When request is same-site, which SameSite to use (Lax or Strict). Default Lax.
5360 @ Value ("${security.jwt.same-site-when-same-site:Lax}" )
5461 private String sameSiteWhenSameSite ;
@@ -64,19 +71,13 @@ private SecretKey getSigningKey() {
6471 }
6572
6673 /**
67- * Generate a signed JWT for the given user.
68- * @param user the authenticated user
69- * @return compact JWT string
74+ * Build the normalized roles list for a user.
7075 */
71- public String generateToken (UserModel user ) {
72- Instant now = Instant .now ();
73-
74- // Build roles list from user.userRole (comma separated) and ensure ID_ claim is included
76+ private List <String > buildRolesList (UserModel user ) {
7577 List <String > rolesList = new ArrayList <>();
7678 String raw = user .getUserRole ();
7779 if (raw != null && !raw .isBlank ()) {
7880 String [] parts = raw .replaceAll ("\\ s" , "" ).split ("," );
79- // Normalize roles: if a role doesn't start with ROLE_ or ID_, prefix with ROLE_
8081 rolesList .addAll (Arrays .stream (parts )
8182 .map (s -> s == null ? null : s .trim ())
8283 .filter (s -> s != null && !s .isBlank ())
@@ -86,24 +87,68 @@ public String generateToken(UserModel user) {
8687 })
8788 .toList ());
8889 }
89- // Ensure at least ROLE_user is present as a fallback
9090 if (rolesList .isEmpty ()) {
9191 rolesList .add ("ROLE_user" );
9292 }
93- // Add device/user identifier role (keep ID_ as-is)
9493 rolesList .add ("ID_" + user .getId ());
94+ return rolesList ;
95+ }
96+
97+ /**
98+ * Generate a short-lived access token (15 min by default) for the given user.
99+ * The token carries {@code type=access} and is intended to be returned in the JSON payload.
100+ *
101+ * @param user the authenticated user
102+ * @return compact JWT string
103+ */
104+ public String generateAccessToken (UserModel user ) {
105+ Instant now = Instant .now ();
106+ return Jwts .builder ()
107+ .issuer (issuer )
108+ .subject (user .getEmail ())
109+ .issuedAt (Date .from (now ))
110+ .expiration (Date .from (now .plusMillis (accessTokenExpirationMs )))
111+ .claim ("uid" , user .getId ())
112+ .claim ("roles" , buildRolesList (user ))
113+ .claim ("type" , "access" )
114+ .signWith (getSigningKey (), Jwts .SIG .HS256 )
115+ .compact ();
116+ }
95117
118+ /**
119+ * Generate a long-lived refresh token (7 days by default) for the given user.
120+ * The token carries {@code type=refresh} and is stored in the database; it is
121+ * delivered to the client via an HttpOnly cookie named {@code refreshToken}.
122+ *
123+ * @param user the authenticated user
124+ * @return compact JWT string
125+ */
126+ public String generateRefreshToken (UserModel user ) {
127+ Instant now = Instant .now ();
96128 return Jwts .builder ()
97129 .issuer (issuer )
98130 .subject (user .getEmail ())
99131 .issuedAt (Date .from (now ))
100132 .expiration (Date .from (now .plusMillis (expirationMs )))
101133 .claim ("uid" , user .getId ())
102- .claim ("roles" , rolesList )
134+ .claim ("roles" , buildRolesList (user ))
135+ .claim ("type" , "refresh" )
103136 .signWith (getSigningKey (), Jwts .SIG .HS256 )
104137 .compact ();
105138 }
106139
140+ /**
141+ * Generate a signed JWT access token for the given user.
142+ * Delegates to {@link #generateAccessToken(UserModel)}.
143+ * Retained for backward compatibility with test code that calls this method directly.
144+ *
145+ * @param user the authenticated user
146+ * @return compact JWT string
147+ */
148+ public String generateToken (UserModel user ) {
149+ return generateAccessToken (user );
150+ }
151+
107152 /**
108153 * Validate a token and return its claims if valid.
109154 * @param token JWT string
@@ -130,46 +175,47 @@ public boolean isValid(String token) {
130175 }
131176
132177 /**
133- * Get the name of the auth cookie for clients .
178+ * Get the name of the refresh token HttpOnly cookie .
134179 */
135- public String cookieName () {
136- return cookieName ;
180+ public String refreshTokenCookieName () {
181+ return refreshTokenCookieName ;
137182 }
138183
139184 /**
140- * Build the Set-Cookie for the auth token with HttpOnly; Secure; Path=/ and appropriate SameSite .
141- * - If different site (domain/port/scheme) from backend: SameSite=None; Secure
142- * - If same site: SameSite=Lax/Strict based on configuration
185+ * Build the HttpOnly {@code refreshToken} Set-Cookie using the request for SameSite/ Secure detection .
186+ * The cookie is scoped to Path=/ so it is sent to the server on every request,
187+ * but since only the {@code refreshToken} mutation reads it, it is safe.
143188 */
144- public ResponseCookie buildAuthCookie (String token , HttpServletRequest request ) {
189+ public ResponseCookie buildRefreshTokenCookie (String token , HttpServletRequest request ) {
145190 boolean crossSite = isCrossSite (request );
146191 String sameSite = crossSite ? "None" : normalizeSameSite (sameSiteWhenSameSite );
147192 boolean secure = crossSite || request .isSecure ();
148193
149- ResponseCookie .ResponseCookieBuilder builder = ResponseCookie .from (cookieName , token )
194+ ResponseCookie .ResponseCookieBuilder builder = ResponseCookie .from (refreshTokenCookieName , token )
150195 .httpOnly (true )
151196 .secure (secure )
152197 .path (cookiePath )
153198 .maxAge (Duration .ofMillis (expirationMs ))
154199 .sameSite (sameSite );
155-
156200 if (cookieDomain != null && !cookieDomain .isBlank ()) {
157201 builder .domain (cookieDomain );
158202 }
159203 return builder .build ();
160204 }
161205
162- /** Build auth cookie using Origin header and server URI (for GraphQL interceptor). */
163- public ResponseCookie buildAuthCookie (String token , String originHeader , URI serverUri ) {
164- boolean crossSite = isCrossSite (originHeader , serverUri );
206+ /**
207+ * Clear the HttpOnly {@code refreshToken} cookie.
208+ */
209+ public ResponseCookie clearRefreshTokenCookie (HttpServletRequest request ) {
210+ boolean crossSite = isCrossSite (request );
165211 String sameSite = crossSite ? "None" : normalizeSameSite (sameSiteWhenSameSite );
166- boolean secure = crossSite || "https" . equalsIgnoreCase ( serverUri . getScheme () );
212+ boolean secure = crossSite || request . isSecure ( );
167213
168- ResponseCookie .ResponseCookieBuilder builder = ResponseCookie .from (cookieName , token )
214+ ResponseCookie .ResponseCookieBuilder builder = ResponseCookie .from (refreshTokenCookieName , "" )
169215 .httpOnly (true )
170216 .secure (secure )
171217 .path (cookiePath )
172- .maxAge (Duration .ofMillis ( expirationMs ) )
218+ .maxAge (Duration .ZERO )
173219 .sameSite (sameSite );
174220 if (cookieDomain != null && !cookieDomain .isBlank ()) {
175221 builder .domain (cookieDomain );
@@ -178,32 +224,36 @@ public ResponseCookie buildAuthCookie(String token, String originHeader, URI ser
178224 }
179225
180226 /**
181- * Build a Set-Cookie header that clears the auth cookie.
227+ * Build the HttpOnly {@code refreshToken} Set-Cookie using Origin/server URI for SameSite/Secure detection.
228+ * Used by {@link com.espacogeek.geek.config.GraphQlCookieInterceptor} which has access to
229+ * the {@code WebGraphQlRequest} headers and URI but not to the raw {@code HttpServletRequest}.
182230 */
183- public ResponseCookie clearAuthCookie ( HttpServletRequest request ) {
184- boolean crossSite = isCrossSite (request );
231+ public ResponseCookie buildRefreshTokenCookie ( String token , String originHeader , URI serverUri ) {
232+ boolean crossSite = isCrossSite (originHeader , serverUri );
185233 String sameSite = crossSite ? "None" : normalizeSameSite (sameSiteWhenSameSite );
186- boolean secure = crossSite || request . isSecure ( );
234+ boolean secure = crossSite || "https" . equalsIgnoreCase ( serverUri . getScheme () );
187235
188- ResponseCookie .ResponseCookieBuilder builder = ResponseCookie .from (cookieName , "" )
236+ ResponseCookie .ResponseCookieBuilder builder = ResponseCookie .from (refreshTokenCookieName , token )
189237 .httpOnly (true )
190238 .secure (secure )
191239 .path (cookiePath )
192- .maxAge (Duration .ZERO )
240+ .maxAge (Duration .ofMillis ( expirationMs ) )
193241 .sameSite (sameSite );
194242 if (cookieDomain != null && !cookieDomain .isBlank ()) {
195243 builder .domain (cookieDomain );
196244 }
197245 return builder .build ();
198246 }
199247
200- /** Clear auth cookie using Origin header and server URI (for GraphQL interceptor). */
201- public ResponseCookie clearAuthCookie (String originHeader , URI serverUri ) {
248+ /**
249+ * Clear the HttpOnly {@code refreshToken} cookie using Origin/server URI for SameSite/Secure detection.
250+ */
251+ public ResponseCookie clearRefreshTokenCookie (String originHeader , URI serverUri ) {
202252 boolean crossSite = isCrossSite (originHeader , serverUri );
203253 String sameSite = crossSite ? "None" : normalizeSameSite (sameSiteWhenSameSite );
204254 boolean secure = crossSite || "https" .equalsIgnoreCase (serverUri .getScheme ());
205255
206- ResponseCookie .ResponseCookieBuilder builder = ResponseCookie .from (cookieName , "" )
256+ ResponseCookie .ResponseCookieBuilder builder = ResponseCookie .from (refreshTokenCookieName , "" )
207257 .httpOnly (true )
208258 .secure (secure )
209259 .path (cookiePath )
0 commit comments