Reputation: 421
In our application we are using Quarkus and the SmallRye JWT extension (see here) for role based AuthN and AuthZ.
We have defined an annotation CurrentUser
:
@Qualifier
@Retention(AnnotationRetention.RUNTIME)
annotation class CurrentUser
The annotation has a concrete implementation in the UserProvider
:
import ---.User
import ---.UserRepository
import jakarta.enterprise.context.RequestScoped
import jakarta.enterprise.inject.Produces
import org.eclipse.microprofile.jwt.Claim
@RequestScoped
class UserProvider(
private val userRepository: UserRepository,
@Claim("upn") private val currentUserEmail: String?
) {
@Produces
@CurrentUser
fun currentUser(): User? {
return if (currentUserEmail != null) {
userRepository.findByEmail(currentUserEmail)
} else {
null
}
}
}
As you can quite easily see, the UserProvider
loads a User
from the database based on the upn
claim found in a potential JWT.
In the following scenario, the annotation works well:
@Path("auditions")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
@PermitAll
class AuditionResource(
@CurrentUser private val currentUser: User?
) {
// Endpoints
}
As you can see, when we inject the current user in every request, then the @CurrentUser
annotation is working as intended.
However, this might become a performance penalty as not every endpoint always requires the current user loaded from the db. Therefore, we wanted to EXTEND the use of @CurrentUser
to support a more granular basis, e.g. as follows:
@Path("auditions")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
@PermitAll
class AuditionResource {
@PATCH
fun foo(
@CurrentUser currentUser: User?
) {
println("HI")
}
}
Unfortunately, in this case, the currentUser()
method of UserProvider
does NOT get called.
How can we achieve support for both use case 1 and use case 2?
Upvotes: 0
Views: 37
Reputation: 1235
I've a similar usecase and I've achieved it as follows.
First make sure you have defined a @UserDefinition
entity, see also this link
Then build a "hook" into Quarkus' JpaIdentityStore
, this will be called from Quarkus' HttpAuthenticationMechanism, for more info check this link. Here you will basically tell Quarkus how to retrieve the user:
@Singleton
@Priority(1)
public class UserIdentityProvider extends JpaIdentityProvider {
@Override
public SecurityIdentity authenticate(EntityManager em, UsernamePasswordAuthenticationRequest request) {
try {
var user = em.createQuery("from UserEntity u where u.username = ?1", UserEntity.class)
.setParameter(1, request.getUsername())
.getSingleResult();
if (!UserEntity.passwordMatches(request.getPassword().getPassword(), user.password, user.salt)) {
throw new AuthenticationFailedException("Username and/or password incorrect: %s".formatted(request.getUsername()));
}
logUser(em, user.username);
return QuarkusSecurityIdentity.builder()
.setPrincipal(new QuarkusPrincipal(user.username))
.addRoles(user.roles)
.addAttribute("email", user.email)
.build();
} catch (Exception e) {
Log.errorf("Unknown user login attempt: '%s'", request.getUsername());
throw new AuthenticationFailedException("Unknown User");
}
}
private static void logUser(EntityManager em, String username) {
em.getTransaction().begin();
UserEntity.update("lastLoggedIn = ?1 where username = ?2", OffsetDateTime.now(), username);
em.getTransaction().commit();
Log.infof("User %s******* logged in", username.substring(0, 3));
}
}
@Path("/auth")
public class AuthenticationController {
@Inject
SecurityIdentity identity;
@POST
@Path("/token")
@Authenticated
public Response accessToken() {
var username = this.identity.getPrincipal().getName();
var email = this.identity.getAttribute("email").toString();
var roles = this.identity.getRoles();
return Response
.ok(createUserInfo(username, email, roles))
.cookie(cookie("access_token", generateAccessToken()))
.build();
}
// will use exp.time, issuer & audience from application.properties automatically (if set)
private String generateAccessToken() {
return Jwt.upn(this.identity.getPrincipal().getName())
.subject(this.identity.getPrincipal().getName())
.groups(this.identity.getRoles())
.sign();
}
private NewCookie cookie(String name, String value) {
return new NewCookie.Builder(name)
.secure(true)
.sameSite(STRICT)
.httpOnly(true)
.path("/")
.value(value)
.build();
}
private Map<String, Object> createUserInfo(String username, String email, Set<String> roles) {
return Map.of(
"username", username,
"roles", roles,
"email", email);
}
}
So this is the flow when a http basic auth. request comes in:
http basic auth request to POST: /auth/token -> HttpAuthenticationMechanism intercepts -> forwards to UserIdentityProvider -> retrieves user from db -> creates a SecurityIdentity -> forwards from the security chain to AuhtenticationController.
And if your endpoints contain @PermitAll
, then the db won't hit.
Hope this helps you.
Upvotes: 0