-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathOAuth2JsonSuccessHandler.java
More file actions
149 lines (131 loc) · 5.9 KB
/
Copy pathOAuth2JsonSuccessHandler.java
File metadata and controls
149 lines (131 loc) · 5.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
package com.vsp.endpointinsightsapi.authentication;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.vsp.endpointinsightsapi.config.AuthenticationProperties;
import com.vsp.endpointinsightsapi.config.SecurityConfig;
import com.vsp.endpointinsightsapi.exception.CustomException;
import com.vsp.endpointinsightsapi.exception.CustomExceptionBuilder;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.time.Instant;
import java.util.Map;
import java.util.Objects;
/**
* Handles successful OIDC authentication and returns JWT token details as JSON.
*
* <p>This handler is invoked after successful OAuth2/OIDC authentication completes.
* Instead of redirecting to a page, it returns a JSON response containing the ID token
* that the frontend can store and use for subsequent API requests.
*
* <h2>Response Format:</h2>
* <pre>{@code
* {
* "idToken": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
* "expiresAt": 1234567890,
* "username": "testuser",
* "email": "test@example.com"
* }
* }</pre>
*
* <h2>Validations:</h2>
* <ul>
* <li>Ensures authentication is OAuth2AuthenticationToken</li>
* <li>Ensures principal is OidcUser (not generic OAuth2User)</li>
* <li>Validates ID token is present</li>
* <li>Validates required claims (username, email, expiration)</li>
* </ul>
*
* <h2>Error Handling:</h2>
* <p>Returns 400 Bad Request if:
* <ul>
* <li>Authentication type is invalid</li>
* <li>ID token is missing</li>
* <li>Required claims are missing or empty</li>
* </ul>
*
* @see SecurityConfig
* @see AuthenticationProperties
*/
@Component
public class OAuth2JsonSuccessHandler implements AuthenticationSuccessHandler {
private static final Logger LOG = LoggerFactory.getLogger(OAuth2JsonSuccessHandler.class);
private final ObjectMapper objectMapper;
private final AuthenticationProperties authProperties;
public OAuth2JsonSuccessHandler(AuthenticationProperties authProperties) {
this.authProperties = authProperties;
this.objectMapper = new ObjectMapper();
}
/**
* Processes successful OIDC authentication and returns JSON response with token details.
*
* @param request the HTTP request
* @param response the HTTP response
* @param authentication the authentication object from Spring Security
* @throws IOException if response writing fails
* @throws CustomException with 400 if authentication or token validation fails
*/
@Override
public void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response,
Authentication authentication) throws IOException {
if (!(authentication instanceof OAuth2AuthenticationToken oauth2Token)) {
LOG.error("Invalid authentication token: expected '{}', got '{}", OAuth2AuthenticationToken.class.getSimpleName(), authentication.getClass().getSimpleName());
throw new CustomExceptionBuilder()
.withStatus(HttpStatus.BAD_REQUEST)
.build();
}
if (!(oauth2Token.getPrincipal() instanceof OidcUser oidcUser)) {
LOG.error("Expected OidcUser principal, got: {}",
Objects.requireNonNull(oauth2Token.getPrincipal()).getClass().getSimpleName());
throw new CustomExceptionBuilder()
.withStatus(HttpStatus.BAD_REQUEST)
.build();
}
LOG.info("OAuth2 authentication success for user '{}'", oidcUser.getName());
OidcIdToken idToken = oidcUser.getIdToken();
if (idToken == null) {
LOG.error("authentication missing id token for user '{}'", oidcUser.getName());
throw new CustomExceptionBuilder()
.withStatus(HttpStatus.BAD_REQUEST)
.build();
}
String username = oidcUser.getAttribute(authProperties.getClaims().getUsername());
String email = oidcUser.getAttribute(authProperties.getClaims().getEmail());
Instant expiresAt = idToken.getExpiresAt();
if (username == null || username.trim().isEmpty()) {
LOG.error("ID token missing preferred_username for user '{}'", oidcUser.getName());
throw new CustomExceptionBuilder()
.withStatus(HttpStatus.BAD_REQUEST)
.build();
}
if (email == null || email.trim().isEmpty()) {
LOG.error("ID token missing email for user '{}'", oidcUser.getName());
throw new CustomExceptionBuilder()
.withStatus(HttpStatus.BAD_REQUEST)
.build();
}
if (expiresAt == null) {
LOG.error("ID token missing expiry time for user '{}'", oidcUser.getName());
throw new CustomExceptionBuilder()
.withStatus(HttpStatus.BAD_REQUEST)
.build();
}
Cookie tokenCookie = new Cookie("authToken", idToken.getTokenValue());
//todo: set to secure only once we implement tls
// tokenCookie.setSecure(true);
tokenCookie.setPath("/");
tokenCookie.setMaxAge((int) (expiresAt.getEpochSecond() - Instant.now().getEpochSecond()));
response.addCookie(tokenCookie);
response.sendRedirect(authProperties.getCallbackUri());
}
}