Skip to content

Ease controllers unit tests in OAuth2 secured apps #6557

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
ch4mpy opened this issue Feb 24, 2019 · 31 comments
Closed

Ease controllers unit tests in OAuth2 secured apps #6557

ch4mpy opened this issue Feb 24, 2019 · 31 comments
Assignees
Labels
in: test An issue in spring-security-test type: enhancement A general enhancement

Comments

@ch4mpy
Copy link
Contributor

ch4mpy commented Feb 24, 2019

Summary

I faced a few difficulties unit testing my controllers in a RESTful app secured with OAuth2 (JWT) and wrote a lib that quite improved my developer experience. Just wanted to share this work, maybe will you pick some ideas / code ?

What I did can be reduced to a few steps:

  1. create two annotations to decorate test cases with desired OAuth2 authentication: @WithMockOauth2Client and @WithMockOauth2User (later relies on first for client configuration and on @WithMockUser for username and password configuration)
  2. mock ResourceServerTokenServices to intercept specific Authorization headers and populate OAuth2 security context according to authentication described with preceding annotations
  3. wrap MockMvc to add a specific Authorization header to the request when any of the two annotations described at step 1. was used
  4. this isn't security related (any kind of REST controller unit test could benefit it) but still in the same lib I wrote and maybe worth being contributed to the framework too (mvc-test ?). Wrap MockMvc to:
    4.1. add Content-type header for each POST, PUT and PATCH request
    4.2. add Accept header for each GET, POST and OPTION request
    4.3. provide with fine grained MockHttpServletRequestBuilder factories (pre-configured for a get request, a post request with a body, etc.)
    4.4. provide with shortcuts to create, configure, build and perform mocked MVC requests in one call
    4.5. auto serialize requests payloads according to Content-type using registered message converters (see SerializationHelper)

Actual Behavior

Considering communities threads (stackoverflow being a sample), unit testing controllers in an app secured with OAuth2 is commonly considered as a painful task.

Expected Behavior

  • Annotations to configure any kind of OAuth2 authentication (client connecting on behalf of an end-user or not)
  • Security context being populated as described with such annotations
  • less boiler-plate code when using MockMvc

Sample

Overall result in some controller unit tests:

@WebMvcTest(UserController.class)
@Import({ResourceServerConfig.class})
@EnableSpringDataWebSupport
public class UserControllerTest extends OAuth2ControllerTest {

    @MockBean
    UserRepository userRepo;

    @Test
    @WithMockOAuth2User(
    		client = @WithMockOAuth2Client(clientId = "webClient"), //of no use here, added for the show-case
    		user = @WithMockUser(username = "admin", authorities = {"READ_USERS"}))
    public void whenAuthenticatedWithReadUserPrivilegeThenListUsersReturnsUsersPage() throws Exception {
        final List<User> users = Arrays.asList(admin, user);
        when(userRepo.findAll(any(Pageable.class))).thenAnswer(invocation ->
                new PageImpl<>(users, (Pageable) invocation.getArguments()[0], users.size()));

        api.get("/users/")
                .andExpect(status().isOk())
                .andExpect(jsonPath("$._embedded.elements", hasSize(users.size())))
                .andDo(document("users-collection",
                        ignorePage(responseFields(), "elements"),
                        links()));
    }
}

In this sample:

  • api is a MockMvc wrapper instance
  • Authorization and Accept headers are transparently added
  • MockHttpServletRequestBuilder is created, configured, build and performed in one call
  • you can browse my source for additional samples involving further request builder configuration (cookies or additional headers)

P.S.

This is my first request to Spring framework, please point me to the right instructions if I do it the wrong way

@ch4mpy ch4mpy changed the title Provide with an OAuth2 aware MockMvc Ease developers life when it comes to writing OAuth2 secured controllers unit tests Feb 24, 2019
@ch4mpy ch4mpy changed the title Ease developers life when it comes to writing OAuth2 secured controllers unit tests Ease controllers unit tests in OAuth2 secured apps Feb 25, 2019
@jgrandja
Copy link
Contributor

@ch4mpy Thanks for sharing this with us! We are definitely in need of OAuth 2.0 Client test support.

Quick question regarding above...

mock ResourceServerTokenServices

ResourceServerTokenServices lives in Spring Security OAuth and we're not looking to add the support in that project. For more details see this blog post.

We would welcome a PR targeted for the upcoming release of Spring Security 5.2.0 for initial test support for OAuth 2.0 Client. Would you be interested?

Related #4833

@jgrandja jgrandja self-assigned this Feb 28, 2019
@jgrandja jgrandja added status: waiting-for-feedback We need additional information before we can continue in: oauth2 An issue in OAuth2 modules (oauth2-core, oauth2-client, oauth2-resource-server, oauth2-jose) in: test An issue in spring-security-test labels Feb 28, 2019
@ch4mpy
Copy link
Contributor Author

ch4mpy commented Feb 28, 2019

@jgrandja, sure, I am interested.
The thing is I'm currently circum-navigating and might be off the radars quite often...
The work I share was first written almost a year ago, so libs had time to freeze / change...

My first guess was you might be interested in keeping @WithMockOAuth2Client and @WithMockOAuth2User almost as I wrote it. For the rest, I doubt: for instance, maybe there is some elegant way to configure MockMvc (rather than proxying it) to had Authentication header.

I'll try to have a look at what is related to token services in Spring-security 5.2.0

@jgrandja
Copy link
Contributor

jgrandja commented Mar 1, 2019

@ch4mpy Regarding...

My first guess was you might be interested in keeping @WithMockOAuth2Client and @WithMockOAuth2User almost as I wrote it

Both @WithMockOAuth2Client and @WithMockOAuth2User are based off of Spring Security OAuth so we could not use it as-is (or as a starting point) for Spring Security 5.2

@ch4mpy
Copy link
Contributor Author

ch4mpy commented Mar 4, 2019

Good news !

I had time this week-end to get a closer look at spring-security 5 OAuth2 implementation and re-think the code I had written a year ago for spring-security-oauth.

This led me to create a new lib: spring-security-oauth2-resource-server-test in which I put @WithMockJwtAuthentication and @WithMockOAuth2BearerTokenAuthentication (both still being clearly inspired by @WithMockUser, as were @WithMockOAuth2Client and @WithMockOAuth2User in my initial proposal, but closer to spring-security 5 OAuth2 authentication implementations). This should be enough to mock most OAuth2 authentication scenari.

This is available in a forked spring-security repo, on the gh-6557 branch.

I also added a OAuth2ResourceServerControllerTest unit test to spring-security-samples-boot-oauth2resourceserver project to demonstrate the new feature:

	@Test
	@WithMockJwtAuthentication
	public void test() throws Exception {
		api.perform(get("/"))
			.andDo(print())
			.andExpect(status().isOk())
			.andExpect(content().string(is("Hello, " + WithMockJwtAuthentication.DEFAULT_CLAIMS_SUBJECT + "!")));
	}

But

I have still something pending: find a way to register a JwtDecoder stub in test-context when @WithMockJwtAuthentication is used (I currently @MockBean it in each test :/)

@jgrandja , Unfortunately I'm about to sail from Guadeloupe to Cuba with only a short stop in Antigua. It means I could not have any internet connection until I reach Jamaica by the end of the month. Can I get a quick feeling about this few commits ?

@jzheaux
Copy link
Contributor

jzheaux commented Mar 5, 2019

Poor guy, @ch4mpy, having to sail to all those exotic places... ;)

@jgrandja
Copy link
Contributor

jgrandja commented Mar 5, 2019

@ch4mpy Thanks for putting this together. I'd like to keep the PR as small and focused as possible so let's just focus on @WithMockJwtAuthentication and leave @WithMockOAuth2BearerTokenAuthentication out for now (possibly a follow-up PR).

My initial thought is, should @WithMockJwtAuthentication be @WithMockJwt instead? More precisely, should we mock the Jwt as part of the request? @jzheaux What are your thoughts?

Also, JwtClaims does not allow for custom claims - only the standard claims which are all optional anyway. We'll need to allow the user to configure custom claims - similar to how JwtHeader allows for it.

@ch4mpy
Copy link
Contributor Author

ch4mpy commented Mar 5, 2019

As, as far as I understood, Spring 5 provides with only two OAuth2 authentication implementations. I did an annotation (and a security-context factory) for each, but sure, I can separate PRs.

My initial thought is, should @WithMockJwtAuthentication be @WithMockJwt instead?

The names might be too long and could be changed to @WithMockJwt and @WithMockOAuth2BearerToken.

should we mock the Jwt as part of the request?

The reason why I named the annotations @WithMockJwtAuthentication and @WithMockOAuth2BearerTokenAuthentication is because it best describes my intention : have controller unit-test cases run within SecurityContexts populated with OAuth2 authentications as finely configured as possible.

Maybe I misunderstand your remark but, as a unit-test writer, all I need is the ability to control the security-context authentication each @Controller test case runs with. To me, the way the token is signed, decoded, processed and turned into an authentication (or an error), and how the security context is then set-up is an integration-tests matter (and as so, not the scope of the ticket I opened).

As a side note, @WithMockUser does just that : instantiate a UsernamePasswordAuthenticationToken with details taken from the test annotation and populates a new empty SecurityContext with it (no UserDetailsService etc. involved).

In this PR, I'm interest in building a Jwt just because it is a member of the JwtAuthenticationToken I need in the test security-context for @PreAuthorize expressions to be correctly interpreted, @AuthenticationPrincipal argument to be resolved and so on.

Regarding claims:

  • what about moving what is inside @JwtClaims to @WithMockJwt and exposing an additionalClaims()?
  • how should be handled standard claims duplicated in custom ones?
  • any clue on how to turn claim <String, String> key/values into <String, Object> ?

With named properties in the annotation, I knew what the value should be turned into (so instantiated JWT claims actually are Map<String, Object>), but if you look closer at my current implementation, headers are Map<String, String>, which is likely to be an issue if it is accessed from the controller...

But should really a developer introduce a dependency on JWT headers from a controller ?

Maybe could we just drop headers configuration from this PR ?

Maybe could we also simplify claims configuration? For instance, username and subject are of primary importance as are likely to end into principal name, but what is the relevance of checking issue or expiry dates from within a controller ?

@jgrandja
Copy link
Contributor

jgrandja commented Mar 6, 2019

@ch4mpy Regarding my comment...

My initial thought is, should @WithMockJwtAuthentication be @WithMockJwt instead? More precisely, should we mock the Jwt as part of the request?

Just to clarify so I don't miscommunicate here, @WithMockJwtAuthentication is very different compared to @WithMockJwt as @WithMockJwt is meant to mock the request whereas @WithMockJwtAuthentication is meant to mock the SecurityContext. I understand your intention is to mock the SecurityContext using @WithMockJwtAuthentication. So forget about @WithMockJwt as this would be a separate issue to work on - possibly if it makes sense.

I see that you have a lot of questions but it would be much easier to answer these within a PR. So my suggestion is to prepare/submit a PR for @WithMockJwtAuthentication and we can go from there. Sounds good?

@ch4mpy
Copy link
Contributor Author

ch4mpy commented Mar 7, 2019

So my suggestion is to prepare/submit a PR for @WithMockJwtAuthentication

#6595

  • moved @WithMockOAuth2BearerTokenAuthentication on a new branch, so solely @WithMockJwtAuthentication for now in the PR
  • token headers are still built as Map<String, String> (@StringEntry values are not parsed)
  • no custom claims, just what is listed in JwtClaimNames (minus subject which is populated with authentication name)

@ch4mpy
Copy link
Contributor Author

ch4mpy commented Mar 9, 2019

Added a commit with a solution to headers and claims values parsing from String to Obect.
All in properties package.

I couldn't delay my departure anymore. So one more wifi connection some night next week then nothing for three weeks or so => do not necessarily wait for my feedback...

@jzheaux
Copy link
Contributor

jzheaux commented Mar 12, 2019

As a side note, @WithMockUser does just that : instantiate a UsernamePasswordAuthenticationToken with details taken from the test annotation and populates a new empty SecurityContext with it

Sorry that I'm late to the party, but the above comment is why I believe @WithMockJwt to be more reasonable. @WithMockUser reads like so:

"Please supply the security context with an authentication whose principal is of type User"

And so @WithMockJwt would say:

"Please supply the security context with an authentication whose principal is of type Jwt"

We can add @WithMockUsernamePasswordAuthenticationToken if necessary to give the tester more expressive power, but for now, @WithMockUser has been sufficient. I believe the same is reasonable with Jwts. @WithMockJwt would create a JwtAuthenticationToken just as @WithMockUser creates a UsernamePasswordAuthenticationToken.

Maybe I misunderstand your remark but, as a unit-test writer, all I need is the ability to control the security-context authentication each @controller test case runs with.

Agreed, though I would go a step further: what an application typically needs is the principal. If the user specifically needs an instance of JwtAuthenticationToken, I'd wonder why they are so tightly coupled in their controllers.

should we mock the Jwt as part of the request?

If I'm understanding the context correctly, the answer here is "yes". The tester should be able to supply the contents of the would-be Jwt as part of the annotation. Annotations like @JwtClaim are one way we could do this.

The names might be too long and could be changed to @WithMockJwt

The length is a little off-putting, but I believe this is more about aligning to the semantics of @WithMockUser.

@ch4mpy
Copy link
Contributor Author

ch4mpy commented Mar 12, 2019

Latest commits were prepared offline. Will integrate todays comments in an other offline session ;)

@jzheaux jzheaux added the for: team-attention This ticket should be discussed as a team before proceeding label Mar 13, 2019
@jzheaux
Copy link
Contributor

jzheaux commented Mar 13, 2019

Thanks, @ch4mpy.

I've started looking through the PR, and it really feels very cumbersome to specify so much through an annotation. It makes me feel like it's not the right spot to be specifying claims and headers.

This drives me to something like:

@WithMockJwt(token="ey...", authorities={...})

instead. But, then, that would end up exercising a fair amount of Spring Security's jwt support to decode the token... and it starts feeling like an integration test.

My next step, then, was to take that out, too, which simply leaves us with:

@WithMockJwt(name="sub", authorities={...})

which seems rather weak.

So, I chatted with @rwinch about this today, and he brought up an idea which I liked, which is to instead do a MockMvc request post processor:

this.mvc.perform(get("/")
    .with(jwt(instanceOfJwt).authorities(...))

and maybe

this.mvc.perform(get("/")
    .with(jwt().claim(SUB, "sub").authorities(...))

And, ostensibly, the reactive equivalent using WebTestClientConfigurer and MockServerConfigurer.

One of the big benefits of this approach is users don't have to suffer specifying claim values under the type limitations that annotations impose. Another benefit is the test support footprint would be smaller, making it easier to maintain.

I've marked the ticket for team discussion to see if @fhanik, @jgrandja, and @rwinch have thoughts one way or another.

@rwinch
Copy link
Member

rwinch commented Mar 13, 2019

I second what @jzheaux has said above. I really think that adding .with(jwt()) approach makes sense. Using annotations for something with as many options as a JWT feels a little forced at this point. If we find later on that there is a gap we can adequately fill with providing something like @WithMockJwt, then we can revisit it at that time.

@ch4mpy
Copy link
Contributor Author

ch4mpy commented Mar 14, 2019

I've started looking through the PR, and it really feels very cumbersome to specify so much through an annotation. It makes me feel like it's not the right spot to be specifying claims and headers.

What commit did you look at?
I iterated to different solutions with your remarks and dropped some time ago the idea to expose annotation attributes for each claim. Very few remain: name, authorities, claims, headers and additionalParsers.
All code related to Map<String, Object> construction from test annotation (headers and claims) is in properties package.

which seems rather weak.

Only name and authorities will be needed in most cases. As tokens are not decoded, it doesn't need to be complete. It just need what controller checks. So most claims and headers can be skipped. Just provide relevant ones (and only it) when needed.

A few samples of current solution will illustrate better than many words :

//all 5 tests pass in `spring-security-samples-boot-oauth2resourceserver` project

    // Default name, empty authorities. Not of much use beyond "isAuthenticated()"
    @Test
    @WithMockJwt()
    public void testDefaultJwt() throws Exception {
        mockMvc.perform(get("/")).andDo(print()).andExpect(status().isOk()).andExpect(
                content().string(is("Hello, " + WithMockJwt.DEFAULT_AUTH_NAME + "!")));
    }

    // Specify authorities only, keep default name
    @Test
    @WithMockJwt({ "ROLE_DEV", "CONTROLLER_UNIT_TESTER" })
    public void testWithCustomAuthoritiesJwt() throws Exception {
        mockMvc.perform(get("/")).andDo(print()).andExpect(status().isOk())
                .andExpect(content().string(is("Hello, " + WithMockJwt.DEFAULT_AUTH_NAME + "!")));
    }

    // Specify authentication name (and token subject claim), empty authorities
    @Test
    @WithMockJwt(name = "ch4mpy")
    public void testWithCustomNameJwt() throws Exception {
        mockMvc.perform(get("/")).andDo(print()).andExpect(status().isOk())
                .andExpect(content().string(is("Hello, ch4mpy!")));
    }

    @Test
    @WithMockJwt(name = "ch4mpy", authorities = { "ROLE_DEV", "CONTROLLER_UNIT_TESTER" })
    public void testWithCustomNameAndAuthoritiesJwt() throws Exception {
        mockMvc.perform(get("/")).andDo(print()).andExpect(status().isOk())
                .andExpect(content().string(is("Hello, ch4mpy!")));
    }

    // Assumes the controller accesses "just_a_string" and "important_instant" JWT claims
    @Test
    @WithMockJwt(
        name = "ch4mpy",
        authorities = { "ROLE_DEV", "CONTROLLER_UNIT_TESTER" },
        claims = {
            @Property(name = "just_a_string", value = "abracadabra"),
            @Property(name = "important_instant", value = "2019-03-13T02:42:51Z", parser = "org.springframework.security.test.context.support.oauth2.properties.InstantPropertyParser") })
    public void testWithAdvancedCustomJwt() throws Exception {
        mockMvc.perform(get("/")).andDo(print()).andExpect(status().isOk())
                .andExpect(content().string(is("Hello, ch4mpy!")));
    }

It would be similar for an app secured with Bearer access tokens:

    @Test
    @WithMockOAuth2AccessToken(
        name = "ch4mpy",
        authorities = {"ROLE_DEV", "CONTROLLER_UNIT_TESTER"},
        claims = {
            @Property(name = OAuth2IntrospectionClaimNames.SCOPE, value = "truc", parser = "org.springframework.security.test.context.support.oauth2.properties.StringSetPropertyParser"),
            @Property(name = OAuth2IntrospectionClaimNames.SCOPE, value = "chose", parser = "org.springframework.security.test.context.support.oauth2.properties.StringSetPropertyParser")})
    public void testWithAdvancedCustomAccessToken() throws Exception { ... }

The tester should be able to supply the contents of the would-be Jwt as part of the annotation. Annotations like @JwtClaim are one way we could do this.

I used a pretty generic name: @Property. As JWT claims, JWT headers and access-token authentication attributes all are Map<String, Object> entries, described by an annotation with a name, a String value and a parser to turn this String into some Object. Similar need for Bearer and OpenID tokens.

Things I did offline (before I could read latest comments):

  • renamed @WithMockJwtAuthentication to @WithMockJwt.
  • polish JavaDoc (check @WithMockJwt, @Property or PropertyParsersHelper)
  • as things were getting very messy, reset gh-6557 branch and dropped all intermediate commits.
  • deleted gh-6557--WithMockOAuth2BearerTokenAuthentication branch to create a new gh-6557--WithMockBearerToken based on latest gh-6557 HEAD. This branch is for @WithMockOAuth2AccessToken. I don't think of relevent use case for @WithMockOAuth2RefreshToken yet.

P.S. I'm leaving Antigua / Barbuda tonight. It is very likely that I won't have a chance to read from you nor push anything for the next three weeks.

@jzheaux
Copy link
Contributor

jzheaux commented Mar 15, 2019

What commit did you look at?

The unsquashed equivalent of the most recent one. The current PR requires a host of conversion utilities and a new annotation (@Property). Using a request post processor requires neither of those and affords the tester equal expressive power.

Very few remain: name, authorities, claims, headers and additionalParsers.

It's not about the number, but what's going into them. claims implies a list of @Property name/value pairs that must be expressed through primitive types, creating friction for the tester.

and dropped some time ago the idea to expose annotation attributes for each claim.

Annotations are still very limiting for this kind of thing either way. I immediately run into the same problem with @WithMockJwt(iat = "2019-01-01T13:45:45Z"). That said, I think an annotation that supports only the standard JWT claims does make some sense since these have well-defined semantics.

Another benefit to a request post processor is that the user can easily extend the behavior:

@Test
public void myTest() {
    this.mvc.perform(get("/").with(userJwt("bob")));
}

private static RequestPostProcessor userJwt(String user) {
    return jwt().claim(SUB, user).claim("user_id", user);
}

which Java gives us for free. Java doesn't support this kind of thing with annotations.

P.S.: I envy you! Enjoy your well-deserved trip, sir. :)

@ch4mpy
Copy link
Contributor Author

ch4mpy commented Mar 16, 2019

I immediately run into the same problem with @WithMockJwt(iat = "2019-01-01T13:45:45Z").

spring-security-samples-boot-oauth2resourceserver is perfect sandbox to try this spring-security-oauth2-resource-server-test lib.

I tried the following and it works (following test passes):

Edit https://github.com/ch4mpy/spring-security/blob/gh-6557/samples/boot/oauth2resourceserver/src/main/java/sample/OAuth2ResourceServerController.java to add:

	@GetMapping("/test")
	public String getTest(@AuthenticationPrincipal final Jwt principal) {
		final Instant iat = (Instant) principal.getClaims().get("iat");
		return "Claim is an Instant with value: " + iat.toString();
	}

Then you can edit https://github.com/ch4mpy/spring-security/blob/gh-6557/samples/boot/oauth2resourceserver/src/test/java/sample/OAuth2ResourceServerControllerTest.java and add:

	@Test
	@WithMockJwt(
			claims = @Property(
					name = "iat",
					value = "2019-01-01T13:45:45Z",
					parser = "org.springframework.security.test.context.support.oauth2.properties.InstantPropertyParser"))
	public void testInstantClaim() throws Exception {
		mockMvc.perform(get("/test"))
				.andExpect(content().string(is("Claim is an Instant with value: 2019-01-01T13:45:45Z")));
	}

P.S.: I envy you! Enjoy your well-deserved trip, sir. :)

This required a few years to organize things, leave everything (job included) and prepare a boat, but, wow, if you could see my desk today... West Indies definitely are a dream place for sailing.

@jzheaux
Copy link
Contributor

jzheaux commented Mar 16, 2019

@ch4mpy, I understand that your PR supports dates. My point is that doing so through an annotation is painful.

I'd like the focus of this conversation to move from "can we" to "should we". I think that the annotation approach here is too cumbersome, so "can we"? Yes. "Should we"? Not at this point.

Can you explain to me why you feel we should add this annotation support over doing a request post processor? Please see if reading my last few comments illustrates why I believe that a request post processor will have much less friction for the user and a much smaller test support code footprint.

@ch4mpy
Copy link
Contributor Author

ch4mpy commented Mar 26, 2019

Could read your message a few more times offline without the pressure of WiFi plan count down and got your point about turning around annotations limitations late.
Sure I do king of hack around language limitations here. On the other hand, I believe Map<String, Object> is an extreme and maybe was even considered a worst practice at the time annotations specifications was written.

Using parsers is the sole solution I found to custom claims which I believe is a must have. Again if a team puts custom claims in a token, it is very likely to use it somewhere in a controller or service component.

Now, regarding the way I retrieve and instantiate parsers, there are different ways to go. I used parseTo to allow

  • easy overriding of a default parser
  • usage of lambdas for those default parsers
  • using registered singletons instead of instantiating a new parser instance for each parsed attribute

A simpler way to go I like is to just drop it and type "parser" as Class<? extends Parser>:

@claims = {
    @Attribute(name="scope", value="message:read", parser=StringListParser.class),
    @Attribute(name="ownedEntities", value="1,42,51", parser=CsvAttributeParser.class) }

Thinking of it longer, I believe it is a better way to go (actually, I don't even see any good reason to keep the design as it is), provided you agree that custom claims are needed and reflecting a parser to de-serialize a value is considered an acceptable solution.

@ch4mpy
Copy link
Contributor Author

ch4mpy commented Apr 6, 2019

Hi there ! Sorry for long silence, I was not allowed to stop at wish to find internet connection. I'm really exited to share my latest work. I spent way more time on this lib than I initially thought, but I feel the result is worth the effort.

@jzheaux, did you have any opportunity to talk with @jgrandja who initially mentioned the need to support custom claims and headers (comment from March the 5th)?

Also, JwtClaims does not allow for custom claims - only the standard claims which are all optional anyway. We'll need to allow the user to configure custom claims - similar to how JwtHeader allows for it.

@jzheaux, What do you think of my updated @StringAttribute and StringAttributeParserSupport implementations?

This long offline session gave me time to refine existing code and augment the lib. I worked on quite a few axes:

  • one more way to configure unit tests SecurityContext. So we now have:
    • annotations
    • flow API for servlet apps (MockMvc request post-processor)
    • flow API for reactive apps (WebTestClient configurers-mutators)
  • 3 authentication types for each above:
    • JwtAuthenticationToken, principal being Jwt
    • OAuth2IntrospectionAuthenticationToken, principal being OAuth2AccessToken
    • OAuth2LoginAuthenticationToken, principal being DefaultOidcUser
  • factor as much code as possible. Each feature code footprint is now quite small.
  • simplify implementations each wherever I found it was possible
  • provide with a decent unit-test coverage (and fix the few bugs it higlighted)
  • improve attribute annotation (for custom claims and headers):
    • replaced parseTo and parserOverride with parser only (value is a Class, not a String any-more)
    • AttributeValueParser was actually a StringAttributeValueParser (I mean the type of the value to parse could only be String). So made input type generic too.
    • Attribute is renamed to StringAttribute beacause of its value type. Other annotations such as LongAttribute or DoubleAttribute could be added but StringAttribute seams enough for now
    • AttributeParserSupport is renamed to StringAttributeParserSupport because it is designed to work with @StringAttribute only. It is also quite simplified

This leads to 10 branches and many potential trees, so please tell me:

  • what granularity would you like for the PRs ?
  • in which order do you prefer things to come ?

As first guess and because, when I left, there where still ongoing discussions about their scope (should it support custom claims or not), I put annotations last:

  1. gh-6634--support is a common base declaring the project and containing shared code
  2. gh-6634--jwt-servlet-flow provides with mockJwt(), entry-point for flow API to configure a JwtAuthenticationToken in a MockMvc request SecurityContext
  3. gh-6634--access-token-servlet-flow augment MockMvc request flow API with mockAccessToken()
  4. gh-6634--oidcid-servlet-flow augment MockMvc request flow API with mockOidcId()
  5. gh-6634--jwt-reactive-flow provides with a flow API to configure reactive WebTestClient with a JwtAuthenticationToken
  6. gh-6634--access-token-reactive-flow augment reactive flow with mockAccessToken()
  7. gh-6634--oidcid-reactive-flow augment reactive flow with mockOidcId()
  8. gh-6557--jwt-annotation provides with annotations to configure SecurityContext while unit-testing any quind of @Component, MessageServiceTest being a show-case
  9. gh-6557--access-token-annotation augment annotations with @WithMockAccessToken
  10. gh-6557--oidcid-annotation augment annotations with @WithMockOidcId

Check-out this latest branch allows to play with any feature. Unit-tests and sample projects are perfect sand-boxes.

Do not hesitate to ask for different branch organization. As I've already been rebasing a lot lately, I'm quite familiar with related STS & git commands...

@ch4mpy
Copy link
Contributor Author

ch4mpy commented May 17, 2019

If anybody gets there some day, it's now faster to start from https://github.com/ch4mpy/spring-addons/tree/master/spring-security-test-oauth2-addons.

It contains flow APIs for both JWT and Introspection. It also exposes annotations in case you need it to test secured @Service (or just prefer annotations over flow APIs).

P.S. I publish this lib on maven-central.

@jzheaux jzheaux added type: enhancement A general enhancement and removed for: team-attention This ticket should be discussed as a team before proceeding status: waiting-for-feedback We need additional information before we can continue in: oauth2 An issue in OAuth2 modules (oauth2-core, oauth2-client, oauth2-resource-server, oauth2-jose) labels Sep 13, 2019
@elch78
Copy link

elch78 commented Oct 31, 2019

I haven't read the whole comments yet but want to give my 2 cents to the naming of the annotation. In my opinion @WithMockJwtAuthentication or @WithMockJwt is not specific enough since JWT is only a format and there are different tokens in JWT format around in OAuth2. Namely access tokens, id tokens, refresh token. So I would suggest something like @WithMockAccessToken for the annotation.

@rnavarropiris
Copy link

We avoided the issue in Spring Boot 2.1.x of no JWT support by just disabling security in the test (which is marked as deprecated):

@WebMvcTest(controllers =  MyController.class, secure = false)

however, this was removed in Spring Boot 2.2.x, so we cannot upgrade.

What is the roadmap/target release for this fix?

@ch4mpy
Copy link
Contributor Author

ch4mpy commented Dec 16, 2019

@rnavarropiris JWT authentication testing support is integrated in spring security 5.2 with "flow" API (see samples)

For annotation (or introspection) support you can see this lib

@jzheaux
Copy link
Contributor

jzheaux commented Dec 19, 2019

@rnavarropiris, to add to @ch4mpy's comment, Spring Security's MockMvc support for JWTs is documented in the reference.

@rnavarropiris
Copy link

@ch4mpy @jzheaux
thanks for the links, I missed that!
We'll give it a try, many thanks! :)

@tobske
Copy link

tobske commented May 22, 2020

Just in case someone needs it: Link to docs about MockMvc support for JWT has moved to here.

@jzheaux
Copy link
Contributor

jzheaux commented Jun 10, 2020

We aren't going to be adding support for custom claims via annotation-based security at this time. Since this appears to be a core part of this proposal, I'm closing this ticket. If there is some gap identified that can't be addressed otherwise, we can reopen this ticket or create a new one.

Note that @WithMockOidcUser is being discussed in isolation in a separate ticket. If you've got thoughts on @WithMockOidcUser, then your comments are quite welcome over on that ticket.

@jzheaux jzheaux closed this as completed Jun 10, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
in: test An issue in spring-security-test type: enhancement A general enhancement
Projects
None yet
Development

No branches or pull requests

7 participants