1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 package org.mycore.restapi;
20
21 import java.io.IOException;
22 import java.net.UnknownHostException;
23 import java.nio.charset.StandardCharsets;
24 import java.security.Principal;
25 import java.util.Arrays;
26 import java.util.Base64;
27 import java.util.LinkedHashMap;
28 import java.util.List;
29 import java.util.Map;
30 import java.util.Optional;
31 import java.util.stream.Collectors;
32
33 import org.apache.commons.io.output.ProxyOutputStream;
34 import org.apache.logging.log4j.LogManager;
35 import org.apache.logging.log4j.Logger;
36 import org.mycore.common.MCRSession;
37 import org.mycore.common.MCRSessionMgr;
38 import org.mycore.common.MCRSystemUserInformation;
39 import org.mycore.common.MCRTransactionHelper;
40 import org.mycore.common.MCRUserInformation;
41 import org.mycore.common.config.MCRConfiguration2;
42 import org.mycore.frontend.MCRFrontendUtil;
43 import org.mycore.frontend.jersey.MCRJWTUtil;
44 import org.mycore.frontend.jersey.resources.MCRJWTResource;
45 import org.mycore.restapi.v1.MCRRestAPIAuthentication;
46 import org.mycore.restapi.v1.utils.MCRRestAPIUtil;
47 import org.mycore.user2.MCRUser;
48 import org.mycore.user2.MCRUserManager;
49
50 import com.auth0.jwt.JWT;
51 import com.auth0.jwt.exceptions.JWTVerificationException;
52 import com.auth0.jwt.interfaces.Claim;
53 import com.auth0.jwt.interfaces.DecodedJWT;
54
55 import jakarta.annotation.Priority;
56 import jakarta.servlet.http.HttpServletRequest;
57 import jakarta.ws.rs.InternalServerErrorException;
58 import jakarta.ws.rs.NotAuthorizedException;
59 import jakarta.ws.rs.Priorities;
60 import jakarta.ws.rs.container.ContainerRequestContext;
61 import jakarta.ws.rs.container.ContainerRequestFilter;
62 import jakarta.ws.rs.container.ContainerResponseContext;
63 import jakarta.ws.rs.container.ContainerResponseFilter;
64 import jakarta.ws.rs.core.Application;
65 import jakarta.ws.rs.core.CacheControl;
66 import jakarta.ws.rs.core.Context;
67 import jakarta.ws.rs.core.HttpHeaders;
68 import jakarta.ws.rs.core.Response;
69 import jakarta.ws.rs.core.SecurityContext;
70 import jakarta.ws.rs.ext.Provider;
71 import jakarta.ws.rs.ext.RuntimeDelegate;
72
73 @Provider
74 @Priority(Priorities.AUTHENTICATION)
75 public class MCRSessionFilter implements ContainerRequestFilter, ContainerResponseFilter {
76
77 public static final Logger LOGGER = LogManager.getLogger();
78
79 private static final String PROP_RENEW_JWT = "mcr:renewJWT";
80
81 private static final List<String> ALLOWED_JWT_SESSION_ATTRIBUTES = MCRConfiguration2
82 .getString("MCR.RestAPI.JWT.AllowedSessionAttributePrefixes").stream()
83 .flatMap(MCRConfiguration2::splitValue)
84 .collect(Collectors.toList());
85
86 @Context
87 HttpServletRequest httpServletRequest;
88
89 @Context
90 Application app;
91
92
93
94
95
96
97
98
99 private static void addJWTToResponse(ContainerRequestContext requestContext,
100 ContainerResponseContext responseContext) {
101 MCRSession currentSession = MCRSessionMgr.getCurrentSession();
102 boolean renewJWT = Optional.ofNullable(requestContext.getProperty(PROP_RENEW_JWT))
103 .map(Boolean.class::cast)
104 .orElse(Boolean.FALSE);
105 Optional.ofNullable(requestContext.getHeaderString(HttpHeaders.AUTHORIZATION))
106 .filter(s -> s.startsWith("Bearer "))
107 .filter(s -> !responseContext.getStatusInfo().getFamily().equals(Response.Status.Family.CLIENT_ERROR))
108 .filter(s -> responseContext.getHeaderString(HttpHeaders.AUTHORIZATION) == null)
109 .map(h -> renewJWT ? ("Bearer " + MCRRestAPIAuthentication
110 .getToken(currentSession, currentSession.getCurrentIP())
111 .orElseThrow(() -> new InternalServerErrorException("Could not get JSON Web Token"))) : h)
112 .ifPresent(h -> {
113 responseContext.getHeaders().putSingle(HttpHeaders.AUTHORIZATION, h);
114
115 Optional.ofNullable(requestContext.getHeaderString(HttpHeaders.CACHE_CONTROL))
116 .map(RuntimeDelegate.getInstance()
117 .createHeaderDelegate(CacheControl.class)::fromString)
118 .filter(cc -> !cc.isPrivate())
119 .ifPresent(cc -> {
120 cc.setPrivate(true);
121 responseContext.getHeaders().putSingle(HttpHeaders.CACHE_CONTROL, cc);
122 });
123 });
124 }
125
126 @Override
127 public void filter(ContainerRequestContext requestContext) {
128 LOGGER.debug("Filter start.");
129 boolean isSecure = requestContext.getSecurityContext().isSecure();
130 if (MCRSessionMgr.hasCurrentSession()) {
131 throw new InternalServerErrorException("Session is already attached.");
132 }
133 MCRSessionMgr.unlock();
134 MCRSession currentSession = MCRSessionMgr.getCurrentSession();
135 currentSession.setCurrentIP(MCRFrontendUtil.getRemoteAddr(httpServletRequest));
136 MCRTransactionHelper.beginTransaction();
137
138 Optional<MCRUserInformation> userInformation = Optional.empty();
139 String authorization = requestContext.getHeaderString(HttpHeaders.AUTHORIZATION);
140
141 if (authorization == null) {
142 LOGGER.debug("No 'Authorization' header");
143 return;
144 }
145
146 String basicPrefix = "Basic ";
147 if (authorization.startsWith(basicPrefix)) {
148 LOGGER.debug("Using 'Basic' authentication.");
149 byte[] encodedAuth = authorization.substring(basicPrefix.length()).trim()
150 .getBytes(StandardCharsets.ISO_8859_1);
151 String userPwd = new String(Base64.getDecoder().decode(encodedAuth), StandardCharsets.ISO_8859_1);
152 if (userPwd.contains(":") && userPwd.length() > 1) {
153 String[] upSplit = userPwd.split(":");
154 String username = upSplit[0];
155 String password = upSplit[1];
156 userInformation = Optional.ofNullable(MCRUserManager.checkPassword(username, password))
157 .map(MCRUserInformation.class::cast)
158 .map(Optional::of)
159 .orElseThrow(() -> {
160 LinkedHashMap<String, String> attrs = new LinkedHashMap<>();
161 attrs.put("error", "invalid_login");
162 attrs.put("error_description", "Wrong login or password.");
163 return new NotAuthorizedException(Response.status(Response.Status.UNAUTHORIZED)
164 .header(HttpHeaders.WWW_AUTHENTICATE,
165 MCRRestAPIUtil.getWWWAuthenticateHeader(null, attrs, app))
166 .build());
167 });
168 }
169 }
170
171 String bearerPrefix = "Bearer ";
172 if (authorization.startsWith(bearerPrefix)) {
173 LOGGER.debug("Using 'JSON Web Token' authentication.");
174
175 String token = authorization.substring(bearerPrefix.length()).trim();
176
177 try {
178 DecodedJWT jwt = JWT.require(MCRJWTUtil.getJWTAlgorithm())
179 .build()
180 .verify(token);
181
182 checkIPClaim(jwt.getClaim(MCRJWTUtil.JWT_CLAIM_IP), MCRFrontendUtil.getRemoteAddr(httpServletRequest));
183
184 Optional<String> audience = jwt.getAudience().stream()
185 .filter(s -> MCRJWTResource.AUDIENCE.equals(s) || MCRRestAPIAuthentication.AUDIENCE.equals(s))
186 .findAny();
187 if (audience.isPresent()) {
188 switch (audience.get()) {
189 case MCRJWTResource.AUDIENCE:
190 MCRJWTResource.validate(token);
191 break;
192 case MCRRestAPIAuthentication.AUDIENCE:
193 requestContext.setProperty(PROP_RENEW_JWT, true);
194 MCRRestAPIAuthentication.validate(token);
195 break;
196 default:
197 LOGGER.warn("Cannot validate JWT for '{}' audience.", audience.get());
198 }
199 }
200 userInformation = Optional.of(new MCRJWTUserInformation(jwt));
201 if (!ALLOWED_JWT_SESSION_ATTRIBUTES.isEmpty()) {
202 for (Map.Entry<String, Claim> entry : jwt.getClaims().entrySet()) {
203 if (entry.getKey().startsWith(MCRJWTUtil.JWT_SESSION_ATTRIBUTE_PREFIX)) {
204 final String key = entry.getKey()
205 .substring(MCRJWTUtil.JWT_SESSION_ATTRIBUTE_PREFIX.length());
206 for (String prefix : ALLOWED_JWT_SESSION_ATTRIBUTES) {
207 if (key.startsWith(prefix)) {
208 currentSession.put(key, entry.getValue().asString());
209 break;
210 }
211 }
212 }
213 }
214 }
215 } catch (JWTVerificationException e) {
216 LOGGER.error(e.getMessage());
217 LinkedHashMap<String, String> attrs = new LinkedHashMap<>();
218 attrs.put("error", "invalid_token");
219 attrs.put("error_description", e.getMessage());
220 throw new NotAuthorizedException(e.getMessage(), e,
221 MCRRestAPIUtil.getWWWAuthenticateHeader("Bearer", attrs, app));
222 }
223 }
224
225 if (userInformation.isEmpty()) {
226 LOGGER.warn(() -> "Unsupported " + HttpHeaders.AUTHORIZATION + " header: " + authorization);
227 }
228
229 userInformation
230 .ifPresent(ui -> {
231 currentSession.setUserInformation(ui);
232 requestContext.setSecurityContext(new MCRRestSecurityContext(ui, isSecure));
233 });
234 LOGGER.info("user detected: " + currentSession.getUserInformation().getUserID());
235 }
236
237 private static void checkIPClaim(Claim ipClaim, String remoteAddr) {
238 try {
239 if (ipClaim.isNull() || !MCRFrontendUtil.isIPAddrAllowed(ipClaim.asString(), remoteAddr)) {
240 throw new JWTVerificationException(
241 "The Claim '" + MCRJWTUtil.JWT_CLAIM_IP + "' value doesn't match the required one.");
242 }
243 } catch (UnknownHostException e) {
244 throw new JWTVerificationException(
245 "The Claim '" + MCRJWTUtil.JWT_CLAIM_IP + "' value doesn't match the required one.", e);
246 }
247 }
248
249 @Override
250 public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) {
251 LOGGER.debug("ResponseFilter start");
252 try {
253 MCRSessionMgr.unlock();
254 MCRSession currentSession = MCRSessionMgr.getCurrentSession();
255 if (responseContext.getStatus() == Response.Status.FORBIDDEN.getStatusCode() && currentSession
256 .getUserInformation().getUserID().equals(MCRSystemUserInformation.getGuestInstance().getUserID())) {
257 LOGGER.debug("Guest detected, change response from FORBIDDEN to UNAUTHORIZED.");
258 responseContext.setStatus(Response.Status.UNAUTHORIZED.getStatusCode());
259 responseContext.getHeaders().putSingle(HttpHeaders.WWW_AUTHENTICATE,
260 MCRRestAPIUtil.getWWWAuthenticateHeader("Basic", null, app));
261 }
262 if (responseContext.getStatus() == Response.Status.UNAUTHORIZED.getStatusCode()
263 && doNotWWWAuthenticate(requestContext)) {
264 LOGGER.debug("Remove {} header.", HttpHeaders.WWW_AUTHENTICATE);
265 responseContext.getHeaders().remove(HttpHeaders.WWW_AUTHENTICATE);
266 }
267 addJWTToResponse(requestContext, responseContext);
268 if (responseContext.hasEntity()) {
269 responseContext.setEntityStream(new ProxyOutputStream(responseContext.getEntityStream()) {
270 @Override
271 public void close() throws IOException {
272 LOGGER.debug("Closing EntityStream");
273 try {
274 super.close();
275 } finally {
276 closeSessionIfNeeded();
277 LOGGER.debug("Closing EntityStream done");
278 }
279 }
280 });
281 } else {
282 LOGGER.debug("No Entity in response, closing MCRSession");
283 closeSessionIfNeeded();
284 }
285 } finally {
286 LOGGER.debug("ResponseFilter stop");
287 }
288 }
289
290
291 private static boolean doNotWWWAuthenticate(ContainerRequestContext requestContext) {
292 return "XMLHttpRequest".equals(requestContext.getHeaderString("X-Requested-With")) ||
293 requestContext.getAcceptableMediaTypes()
294 .stream()
295 .findFirst()
296 .filter(m -> "image".equals(m.getType()))
297 .isPresent();
298 }
299
300 private static void closeSessionIfNeeded() {
301 if (MCRSessionMgr.hasCurrentSession()) {
302 MCRSession currentSession = MCRSessionMgr.getCurrentSession();
303 try {
304 if (MCRTransactionHelper.isTransactionActive()) {
305 LOGGER.debug("Active MCRSession and JPA-Transaction found. Clearing up");
306 if (MCRTransactionHelper.transactionRequiresRollback()) {
307 MCRTransactionHelper.rollbackTransaction();
308 } else {
309 MCRTransactionHelper.commitTransaction();
310 }
311 } else {
312 LOGGER.debug("Active MCRSession found. Clearing up");
313 }
314 } finally {
315 MCRSessionMgr.releaseCurrentSession();
316 currentSession.close();
317 LOGGER.debug("Session closed.");
318 }
319 }
320 }
321
322 private static class MCRJWTUserInformation implements MCRUserInformation {
323
324 private final DecodedJWT jwt;
325
326 MCRJWTUserInformation(DecodedJWT token) {
327 this.jwt = token;
328 }
329
330 @Override
331 public String getUserID() {
332 return jwt.getSubject();
333 }
334
335 @Override
336 public boolean isUserInRole(String role) {
337 return Arrays.asList(jwt.getClaim("mcr:roles").asArray(String.class)).contains(role);
338 }
339
340 @Override
341 public String getUserAttribute(String attribute) {
342 if (MCRUserInformation.ATT_REAL_NAME.equals(attribute)) {
343 return jwt.getClaim("name").asString();
344 }
345 if (MCRUserInformation.ATT_EMAIL.equals(attribute)) {
346 return jwt.getClaim("email").asString();
347 }
348 return jwt.getClaim(MCRJWTUtil.JWT_USER_ATTRIBUTE_PREFIX + attribute).asString();
349 }
350 }
351
352 private static class MCRRestSecurityContext implements SecurityContext {
353 private final MCRUserInformation ui;
354
355 private final boolean isSecure;
356
357 private final Principal principal;
358
359 MCRRestSecurityContext(MCRUserInformation ui, boolean isSecure) {
360 this.principal = ui::getUserID;
361 this.ui = ui;
362 this.isSecure = isSecure;
363 }
364
365 @Override
366 public Principal getUserPrincipal() {
367 return principal;
368 }
369
370 @Override
371 public boolean isUserInRole(String role) {
372 return ui.isUserInRole(role);
373 }
374
375 @Override
376 public boolean isSecure() {
377 return isSecure;
378 }
379
380 @Override
381 public String getAuthenticationScheme() {
382 if (ui.getUserID().equals(MCRSystemUserInformation.getGuestInstance().getUserID())) {
383 return null;
384 }
385 if (ui instanceof MCRUser) {
386 return SecurityContext.BASIC_AUTH;
387 }
388 if (ui instanceof MCRJWTUserInformation) {
389 return "BEARER";
390 }
391 return null;
392 }
393 }
394 }