UserFilter.java

package de.dlr.shepard.filters;

import de.dlr.shepard.exceptions.ApiError;
import de.dlr.shepard.exceptions.ShepardProcessingException;
import de.dlr.shepard.neo4Core.entities.User;
import de.dlr.shepard.neo4Core.services.UserService;
import de.dlr.shepard.security.JWTPrincipal;
import de.dlr.shepard.security.UserGracePeriod;
import de.dlr.shepard.security.Userinfo;
import de.dlr.shepard.security.UserinfoService;
import io.quarkus.logging.Log;
import jakarta.annotation.Priority;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.ws.rs.Priorities;
import jakarta.ws.rs.container.ContainerRequestContext;
import jakarta.ws.rs.container.ContainerRequestFilter;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.Response.Status;
import jakarta.ws.rs.ext.Provider;
import java.io.IOException;

@Provider
@Priority(Priorities.AUTHENTICATION + 1)
@ApplicationScoped
public class UserFilter implements ContainerRequestFilter {

  private UserGracePeriod lastSeen;
  private UserService userService;
  private UserinfoService userInfoService;

  UserFilter() {}

  @Inject
  public UserFilter(UserGracePeriod lastSeen, UserService userService, UserinfoService userInfoService) {
    this.lastSeen = lastSeen;
    this.userService = userService;
    this.userInfoService = userInfoService;
  }

  @Override
  public void filter(ContainerRequestContext requestContext) throws IOException {
    if (PublicEndpointRegistry.isRequestPathPublic(requestContext)) return;
    var principal = requestContext.getSecurityContext().getUserPrincipal();
    if (!(principal instanceof JWTPrincipal)) {
      Log.warnf("Unknown principal %s", principal);
      abort(requestContext, "User could not be read from the request context");
      return;
    }
    var jwtPrincipal = (JWTPrincipal) principal;
    var header = requestContext.getHeaderString(HttpHeaders.AUTHORIZATION);
    if (header != null && header.startsWith("Bearer ") && !lastSeen.elementIsKnown(jwtPrincipal.getUsername())) {
      User user;
      Userinfo userinfo;
      try {
        userinfo = userInfoService.fetchUserinfo(header);
      } catch (ShepardProcessingException e) {
        abort(requestContext, "User info could not be retrieved");
        return;
      }
      user = parseUserFromUserinfo(userinfo);
      if (!jwtPrincipal.getUsername().equals(user.getUsername())) {
        Log.warn("The usernames from the access token and the userinfo response do not match");
        abort(requestContext, "The usernames from the access token and the userinfo response do not match");
        return;
      }
      var created = userService.updateUser(user);
      if (created == null) {
        Log.warn("The user could not be updated or created");
        abort(requestContext, "The user could not be updated or created");
        return;
      }
      lastSeen.elementSeen(jwtPrincipal.getUsername());
    }
  }

  private void abort(ContainerRequestContext requestContext, String reason) {
    requestContext.abortWith(
      Response.status(Status.UNAUTHORIZED)
        .entity(new ApiError(Status.UNAUTHORIZED.getStatusCode(), "AuthenticationException", reason))
        .build()
    );
  }

  private User parseUserFromUserinfo(Userinfo userinfo) {
    // We only want the last part of the subject, since this is usually a human
    // readable user name
    var splitted = userinfo.getSub().split(":");
    String username = splitted[splitted.length - 1];

    User user = new User(username, userinfo.getGivenName(), userinfo.getFamilyName(), userinfo.getEmail());
    return user;
  }
}