thymeleaf / thymeleaf-extras-springsecurity

Thymeleaf "extras" integration module for Spring Security 3.x and 4.x
http://www.thymeleaf.org
Apache License 2.0
477 stars 107 forks source link

Request/Bug - Problem with accessing custom User objects from within Spring Security in tests #74

Closed ottlinger closed 3 years ago

ottlinger commented 3 years ago

I'm a happy user of Thymeleaf and SpringBoot 2.4.2 - thanks for your work!

Context

As my application grows I had to rely on my own ApplicationUser extending org.springframework.security.core.userdetails.User to provide a new custom attribute to be visible inside of the application.

I wanted to show this extra attribute in the following easy way inside the view:

<span th:text="${#authentication.getPrincipal()?.getTenantName()}"></span>

which works fine.

Problem in integration tests

Whenever an integration test (e.g. WebMvcTest), that works with a fake authentication via:

@WithMockUser(username = "testuser", roles = {"USER", "ADMIN"})

is launched the application does not startup as the template fails to render. It fails as the given User under test is not an ApplicationUser.

If I run the application without tests, everything works fine and the additional custom attribute is properly rendered within the template.

Trial&Error

th:if

As a first idea I tried to add th:if, but it does not support instanceof:

<span th:if="${#authentication.getPrincipal() != null && #authentication.getPrincipal() instanceof foo.user.ApplicationUser}"
                                th:text="${#authentication.getPrincipal()?.getTenantName()}"></span>

and thus does not work.

Elvis operator

The elvis operator does not help as well as the problem is that the mocked user does not have the tenantName attribute.

Proposal: safe cast/if-else

Is there either a possibility to safely try to cast the used User principal? in order to provide an if-else-such as:

// code proposal - not working!
<span th:if="${#authentication.getPrincipal() instanceof foo.user.ApplicationUser}"
                                th:text="${#authentication.getPrincipal()?.getTenantName()}"></span>
<span th:unless="${#authentication.getPrincipal() instanceof foo.user.ApplicationUser}"
                                th:text="${#authentication.name()}"></span>

to allow tests to run properly.

Other approaches?

Is there a different approach to access custom User-object attributes in an integration-safe manner?

Thanks for any ideas on how to continue here. Web searches only yields tutorials on how to use custom User objects but no integration in Thymeleaf is shown with above problem.

ultraq commented 3 years ago

I was initially going to suggest a custom expression object (like how Thymeleaf comes with built-ins like #dates or how the module in this repo adds #authentication), for your application, but I did a search for instanceof operations inside Spring Expression Language blocks, and it looks like those are supported. The syntax however is: x instanceof T(Type), so looking at your example that might look something like: th:if="${authentication.getPrincipal() instanceof T(foo.user.ApplicationUser)"

Reference: https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#expressions-operators-relational

Does that help fix that for you?

ottlinger commented 3 years ago

@ultraq thanks, that did the trick. I'm always amazed at the syntax .... but as long as it works you just need to know how.

<span th:if="${#authentication.getPrincipal() != null && #authentication.getPrincipal() instanceof T(foo.user.ApplicationUser)}"
                                  th:text="${#authentication.getPrincipal()?.getTenantName()}"></span>