View Javadoc
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      // Allow CORS preflight requests
99      if (HttpMethod.OPTIONS.equals(requestContext.getMethod())) {
100       // Allow all requests with request method OPTIONS
101       return;
102     }
103 
104     JwtPrincipal principal;
105 
106     // Get the HTTP Authorization header from the request
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     // Extract the token from the HTTP Authorization header
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     // Read realm roles
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     // We only want the last part of the subject, since this is usually a human
202     // readable user name
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     // Extract the api key from the HTTP Authorization header
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 }