1 package de.dlr.shepard.common.filters;
2
3 import de.dlr.shepard.auth.apikey.services.ApiKeyService;
4 import de.dlr.shepard.auth.security.ApiKeyLastSeenCache;
5 import de.dlr.shepard.auth.security.AuthenticationContext;
6 import de.dlr.shepard.auth.security.JwtPrincipal;
7 import de.dlr.shepard.auth.security.JwtSecurityContext;
8 import de.dlr.shepard.auth.security.RolesList;
9 import de.dlr.shepard.common.exceptions.ApiError;
10 import de.dlr.shepard.common.util.Constants;
11 import de.dlr.shepard.common.util.PkiHelper;
12 import io.jsonwebtoken.Claims;
13 import io.jsonwebtoken.Jws;
14 import io.jsonwebtoken.JwtException;
15 import io.jsonwebtoken.Jwts;
16 import io.jsonwebtoken.jackson.io.JacksonDeserializer;
17 import io.quarkus.logging.Log;
18 import jakarta.annotation.Priority;
19 import jakarta.enterprise.context.RequestScoped;
20 import jakarta.inject.Inject;
21 import jakarta.ws.rs.HttpMethod;
22 import jakarta.ws.rs.Priorities;
23 import jakarta.ws.rs.container.ContainerRequestContext;
24 import jakarta.ws.rs.container.ContainerRequestFilter;
25 import jakarta.ws.rs.core.HttpHeaders;
26 import jakarta.ws.rs.core.Response;
27 import jakarta.ws.rs.core.Response.Status;
28 import jakarta.ws.rs.ext.Provider;
29 import java.security.KeyFactory;
30 import java.security.NoSuchAlgorithmException;
31 import java.security.PublicKey;
32 import java.security.spec.InvalidKeySpecException;
33 import java.security.spec.X509EncodedKeySpec;
34 import java.util.Arrays;
35 import java.util.Base64;
36 import java.util.Map;
37 import java.util.Optional;
38 import java.util.UUID;
39 import org.eclipse.microprofile.config.inject.ConfigProperty;
40
41 @Provider
42 @Priority(Priorities.AUTHENTICATION)
43 @RequestScoped
44 public final class JwtFilter implements ContainerRequestFilter {
45
46 private PublicKey jwtPublicKey;
47
48 private PublicKey oidcPublicKey;
49
50 private String role;
51
52 private ApiKeyLastSeenCache apiKeyLastSeenCache;
53
54 private ApiKeyService apiKeyService;
55
56 private AuthenticationContext authenticationContext;
57
58 JwtFilter() {}
59
60 @Inject
61 public JwtFilter(
62 PkiHelper pkiHelper,
63 ApiKeyService apiKeyService,
64 ApiKeyLastSeenCache apiKeyLastSeenCache,
65 AuthenticationContext authenticationContext,
66 @ConfigProperty(name = "oidc.public") String oidcPublic,
67 @ConfigProperty(name = "oidc.role") Optional<String> oidcRole
68 ) throws NoSuchAlgorithmException, InvalidKeySpecException, IllegalArgumentException {
69 try {
70 this.apiKeyService = apiKeyService;
71 this.apiKeyLastSeenCache = apiKeyLastSeenCache;
72 this.authenticationContext = authenticationContext;
73 this.role = oidcRole.orElse("");
74
75 var kFactory = KeyFactory.getInstance("RSA");
76 byte[] kcDecoded;
77 try {
78 kcDecoded = Base64.getDecoder().decode(oidcPublic);
79 } catch (IllegalArgumentException e) {
80 throw new IllegalArgumentException("The given oidc public key is invalid", e);
81 }
82 var kcSpec = new X509EncodedKeySpec(kcDecoded);
83 oidcPublicKey = kFactory.generatePublic(kcSpec);
84
85 pkiHelper.init();
86 jwtPublicKey = pkiHelper.getPublicKey();
87 } catch (RuntimeException ex) {
88 var msg = "Cannot create instance of JwtFilter: %s".formatted(ex.getMessage());
89 Log.fatal(msg);
90 throw new JwtException(msg, ex);
91 }
92 }
93
94 @Override
95 public void filter(ContainerRequestContext requestContext) {
96 if (PublicEndpointRegistry.isRequestPathPublic(requestContext)) return;
97
98
99 if (HttpMethod.OPTIONS.equals(requestContext.getMethod())) {
100
101 return;
102 }
103
104 JwtPrincipal principal;
105
106
107 String authorizationHeader = requestContext.getHeaderString(HttpHeaders.AUTHORIZATION);
108 String apiKeyHeader = requestContext.getHeaderString(Constants.API_KEY_HEADER);
109 if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
110 principal = parseAccessToken(authorizationHeader);
111 } else if (apiKeyHeader != null) {
112 principal = parseApiKey(apiKeyHeader);
113 } else {
114 Log.warnf(
115 "Invalid/missing authorization header (Authorization: %s, X-API-KEY: %s) on endpoint %s",
116 authorizationHeader,
117 null,
118 requestContext.getUriInfo().getAbsolutePath()
119 );
120 requestContext.abortWith(
121 Response.status(Status.UNAUTHORIZED)
122 .entity(
123 new ApiError(
124 Status.UNAUTHORIZED.getStatusCode(),
125 "AuthenticationException",
126 "Invalid/missing authorization header"
127 )
128 )
129 .build()
130 );
131 return;
132 }
133 if (principal == null) {
134 requestContext.abortWith(
135 Response.status(Status.UNAUTHORIZED)
136 .entity(
137 new ApiError(Status.UNAUTHORIZED.getStatusCode(), "AuthenticationException", "Invalid Authentication")
138 )
139 .build()
140 );
141 return;
142 }
143
144 var securityContext = new JwtSecurityContext(requestContext.getSecurityContext(), principal);
145 requestContext.setSecurityContext(securityContext);
146 authenticationContext.setPrincipal(principal);
147 }
148
149 private JwtPrincipal parseAccessToken(String header) {
150 JwtPrincipal result = null;
151 Jws<Claims> jws = parseAccessTokenFromHeader(header);
152 if (jws != null) {
153 result = parsePrincipalFromAccessToken(jws);
154 }
155 return result;
156 }
157
158 private Jws<Claims> parseAccessTokenFromHeader(String header) {
159
160 String token = header.replace("Bearer ", "");
161 var parser = Jwts.parserBuilder()
162 .setSigningKey(oidcPublicKey)
163 .deserializeJsonWith(new JacksonDeserializer<>(Map.of("realm_access", RolesList.class)))
164 .build();
165
166 Jws<Claims> jws;
167
168 try {
169 jws = parser.parseClaimsJws(token);
170 Log.debugf("Valid token: %s", jws.getBody().getId());
171 } catch (JwtException ex) {
172 Log.warnf("Invalid token: %s", ex.getMessage());
173 return null;
174 }
175 return jws;
176 }
177
178 private JwtPrincipal parsePrincipalFromAccessToken(Jws<Claims> jws) {
179 var body = jws.getBody();
180 String keyId = body.getId();
181 String subject = body.getSubject();
182 String audience = body.getAudience();
183 String issuedFor = body.get("azp", String.class);
184 Optional<RolesList> realmAccess = Optional.ofNullable(body.get("realm_access", RolesList.class));
185
186 if (subject == null || subject.isEmpty()) {
187 Log.warn("Token is missing a subject");
188 return null;
189 }
190
191
192 if (!role.isBlank()) {
193 var realmRoles = realmAccess.map(RolesList::getRoles).orElse(new String[0]);
194 var hasRole = Arrays.stream(realmRoles).anyMatch(r -> r.equals(role));
195 if (!hasRole) {
196 Log.warnf("User is missing required role: %s", role);
197 return null;
198 }
199 }
200
201
202
203 var splitted = subject.split(":");
204 String username = splitted[splitted.length - 1];
205
206 var principal = new JwtPrincipal(audience, issuedFor, username, keyId, new String[0]);
207
208 return principal;
209 }
210
211 private Jws<Claims> parseApiKeyFromHeader(String token) {
212
213 Jws<Claims> jws;
214
215 try {
216 jws = Jwts.parserBuilder().setSigningKey(jwtPublicKey).build().parseClaimsJws(token);
217 Log.debugf("Valid token: %s", jws.getBody().getId());
218 } catch (JwtException ex) {
219 Log.warnf("Invalid token: %s", ex.getMessage());
220 return null;
221 }
222 return jws;
223 }
224
225 private JwtPrincipal parseApiKey(String header) {
226 JwtPrincipal principal = null;
227 Jws<Claims> jws = parseApiKeyFromHeader(header);
228 if (jws != null) {
229 principal = parsePrincipalFromApiKey(jws);
230 if (principal == null) return null;
231 UUID tokenId = UUID.fromString(jws.getBody().getId());
232
233 if (apiKeyLastSeenCache.isKeyCached(tokenId.toString())) {
234 return principal;
235 }
236
237 var storedKey = apiKeyService.getApiKey(tokenId);
238 if (!storedKey.getJws().equals(header)) {
239 Log.warn("Token from header is not equal to the token from database");
240 return null;
241 }
242 apiKeyLastSeenCache.cacheKey(tokenId.toString());
243 }
244 return principal;
245 }
246
247 private JwtPrincipal parsePrincipalFromApiKey(Jws<Claims> jws) {
248 var body = jws.getBody();
249 String subject = body.getSubject();
250 String keyId = body.getId();
251 if (subject == null || subject.isEmpty()) {
252 Log.warn("Token is missing a subject");
253 return null;
254 }
255
256 var principal = new JwtPrincipal(subject, keyId);
257 return principal;
258 }
259 }