Container managed auth, handle currently logged-in user

1.3k Views Asked by At

I have been digging through posts for too long now and getting dizzy, so I'm hoping one of the gurus here can help me with this.

I'm using Container Managed Authentication and it's working well. I have my Realm setup to authenticate against, set up protected urls in web.xml, have a login page etc.

But now I am hitting a problem...

I have JPA objects for my data model and some of these objects are 'audited' in that they track when they were created or updated and by who.

I'm using a @PrePersist handler in my code to set the createdOn and updatedOn fields on persist/update respectively, like so:

@PrePersist
protected void onCreate() {
    this.setCreatedOn(new Date());
}

This works great, but I am missing how I should get access to the currently logged in user from here... I need that to be able to set the createdBy field.

I'm using Resteasy and in my endpoint I do have access to the logged in username and am able to get my Account object:

@Path("/test")
public class TestEndpoint {
    @EJB
    AuthorizationService authService;

    @GET
    @Path("path")
    @Produces("application/json")
    @RolesAllowed("User")
    public Response test() {
        Account account = authService.getLoggedInAccount();
        return account == null ? Response.status(Status.NOT_FOUND).build() : Response.ok().entity(account).build();
    }
}

AuthorizationService is mine and looks like this:

@Stateless 
@LocalBean
public class AuthorizationService {
    @Inject 
    HttpServletRequest request;

    public Account getLoggedInAccount() {
        Account result = (Account) request.getAttribute(LOGGED_IN_USER);
        if (result == null) {
            Principal principal = request.getUserPrincipal();
            if (principal != null) {
                List<Account> results = crudService.find(Account.BY_NAME, Params.of("name", principal.getName()), 0, 0);
                if (results != null && results.size() > 0) {
                    result = results.get(0);
                    request.setAttribute(LOGGED_IN_USER, result);
                }
            }
        }
        return result; 
    }
}

This works. Notice that I cache the logged-in user on a request attribute so I don't send the query to the DB each time.

Until now I have been able to get by with this setup, but I feel I am doing this all wrong...

I would like to have one global interception point (filter?) where I populate ...something... (the request?) with the currently logged in user's Account object and then be able to inject it wherever it's needed... I would highly prefer solutions that do not create a session as I'm trying to make the app as scalable as possible.

Any tips on how to handle this? That golden link to the tutorial that explains this well maybe? Thanks for any help guys!

2

There are 2 best solutions below

5
On

There is a set of predefined beans in CDI applications. Among them the java.security.Principal represents the identity of the current caller. So you just have to @Inject Principal wherever you need it. Another thing you might consider for your specific needs is the Auditing feature of the delta spike data module project which might helps you cut custom code even more.

4
On

Your question is a bit unclear, but I suppose from the comments that you want to be able to inject current logged-in user's account into your CDI beans, like this:

@Inject @CurrentUser Account account;

For this, you need:

  • a CDI producer to customize account object creation
  • inject principal into the producer so that you may find appropriate Account for logged-in user
  • Custom @CurrentUser qualifier to match your injection points with the producer
  • producer should create a request scoped bean - hence call to producer (and then to DB) is cached and executed only first time per request

Now code example of the Producer:

public class CurrentAccountProducer {

    @Inject private Principal principal; // get logged-in principal

    /* called first time per request to find Account entity for principal
      - it will be cached and injected into @CurrentAccount @Inject points
    */
    @CurrentAccount 
    @RequestScoped
    @Produces
    public Account produceAccount() {
        if (principal == null) {
            return null; // null will be injected if user is not logged in
        } else {
            // part of your original code here...
            List<Account> results = crudService.find(Account.BY_NAME, Params.of("name", principal.getName()), 0, 0);
                if (results != null && results.size() > 0) {
                    return results.get(0);
                }
        }
    }
}

This producer is all you need to inject Account for logged-in user or null for anonymous. You may then modify your AuthorizationService like this:

@Stateless 
@LocalBean
public class AuthorizationService {
    @Inject 
    @CurrentAccount Account currentAccount;

    public Account getLoggedInAccount() {
        return currentAccount; 
    }
}

It can be even as easy as injecting Account directly into TestEndpoint, bypassing AuthorizationService, but it is better to encapsulate business logic into an EJB to run the logic in a transaction.