I am trying to use Pac4j for a Lagom application that has a Play web front end with Keycloak.
If anyone knows of something like this; please let me know.
I had used Pac4j before for a Play/Scala application w/ Keycloak and I didn't expect to experience difficulties w/ Lagom since it also uses Play.
So, I tried to build on that example for my use case.
Compared to my earlier experience, this involves 2 significant changes;
1) Compile-time DI instead of Runtime DI.
Most of the Pac4J examples use Runtime DI w/ Guice whereas Lagom typically uses compile-time DI.
After experiencing both, I find compile-time DI much more useful because it forces to be explicit about everything that needs to be injected.
2) Using PlayCookieSessionStore
The doc mentions some differences between the two here:
I am also using Scala instead of Java; this ought to be OK; however, I run sometimes into problems when
I try to override some behavior implemented in Pac4j somewhere. The problems are typically due to Java idioms
that result in loosing type information, for example:
In Java, this line seems innocuous:
final ProfileManager manager = getProfileManager(context);
List<UserProfile> profiles = manager.getAll(loadProfilesFromSession);
With ProfileManager defined as:
public class ProfileManager<U extends CommonProfile> {
...
public List<U> getAll(final boolean readFromSession) { ... }
...
}
A direct translation of the Java code in Scala would result in a type error:
val manager: ProfileManager[_ <: CommonProfile] = getProfileManager(context)
val profiles: util.List[UserProfile] = manager.getAll(loadProfilesFromSession)
The Scala compiler detects a type error with the above; instead, we have to write:
val manager: ProfileManager[_ <: CommonProfile] = getProfileManager(context)
val profiles: util.List[_ <: CommonProfile] = manager.getAll(loadProfilesFromSession)
Beyond these Java/Scala type problems, there are a couple of bigger issues:
A) Pac4jScalaTemplateHelper is biased for PlayCacheSessionStore
Looking more closely, it seems a premature specialization; PlaySessionStore seems to be to be sufficient;
it would have enabled using Pac4jScalaTemplateHelper w/ either variant: PlayCookieSessionStore or PlayCacheSessionStore.
B) Differences between PlayCacheSessionStore and PlayCookieSessionStore
There is some doc about these differences here:
However, what is undocumented is the subtle behavior in this logic:
Clearing sensitive data only happens in PlayCookieSessionStore; as far as I can tell, there is no equivalent behavior in PlayCacheSessionStore.
In PlayCookieSessionStore, clearing sensitive data effectively deletes the access_token, refresh_token but leaves the id_token:
Why is this done?
Without access & refresh tokens, it is effectively impossible to use Pac4j to invoke Pac4j-secured routes (e.g., in Play) or retrieve the access token
to pass it as an "Authentication: Bearer <token>" header for invoking Pac4j-secured REST endpoints like the lagom-pac4j example.
I tried to disable this clearing of sensitive data but this results in an even more puzzling behavior:
- Initially, there is no Play session, no authentication; so, when navigating to a Pac4j-secured web page, DefaultSecurityLogic requests authentication.
- When DefaultCallbackLogic is invoked after the authorization succeeds, it somehow ends up triggering the DefaultSecurityLogic on a different thread which has no context.
- No context means no profiles means back to square one!
- Eventually, Keycloak puts a stop to this cycle of authorization.
When clearing of sensitive data is enabled (as is done by default), the behavior is different:
- Initially, there is no Play session, no authentication; so, when navigating to a Pac4j-secured web page, DefaultSecurityLogic requests authentication.
- When DefaultCallbackLogic is invoked after the authorization succeeds, it somehow ends up triggering the DefaultSecurityLogic on a the same thread which has maintained the same context.
- This time, the context has profiles except that with sensitive data cleared, there's no access nor refresh token.
I am really baffled why clearing sensitive data has such a big difference in behavior.
In particular, why is the redirection at the end of the callback logic happens on the same thread when it's cleared vs. a different thread when it isn't?
Of course, it would be easier if I had done this by modifying the lagom example.
However, I can honestly say that I'm very surprised that adding pac4j to a working lagom application would end up being so strange and complicated to understand.
- Nicolas.