Skip to content
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

spring-security-test @WithMockOidcuser gh-8459 #8461

Closed

Conversation

nenaraab
Copy link
Contributor

@nenaraab nenaraab commented May 5, 2020

Adds @WithMockOidcUser to spring-security-test library that allows you to fill the SecurityContext properly when using oauth2Login as I was used to with @WithMockUser

Example Web Mvc test:

@Test
@WithMockOidcUser(name = "Alice_salesOrders@test.com", authorities = {"read:salesOrders"})
public void readWith_Alice_salesOrders_200() {
        mockMvc.perform(get("/salesOrders"))
                .andExpect(status().isOk());
}

Example other Spring Tests:

@Autowired
DataService dataService;
    
@Test
@WithMockOidcUser(authorities = {"Admin"})
public void accessService_authorizedUser() {
        String data = dataService.readSensitiveData();
        assertFalse(data.isEmpty());
}

Github issue

Find further details discussed here: ##8459

@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged label May 5, 2020
@rwinch
Copy link
Member

rwinch commented May 5, 2020

Thanks for the PR @nenaraab!

When we did gh-7618, I think we avoided annotation support for OIDC because it was not very easy for users to customize the OidcUser and OAuth2AuthenticationToken objects (the attributes are rich objects which cannot easily be modified by an annotation). Instead, we felt that user's could set SecurityContextHolder directly for method support or use the oidcLogin support for MockMvc style testing.

Given you find value in this, it is worth us reconsidering. However, I want to ensure that we remember all the concerns we had before we merge this. I'm assigning this to @jzheaux since he worked on the test support for some of the other OAuth bits and he might remember better what we discussed. As a heads up he is out on PTO so his response will be delayed.

Thanks again for your PR!

@nenaraab
Copy link
Contributor Author

nenaraab commented May 6, 2020

Hi @rwinch and @jzheaux, that's cool, thanks for initial feedback and reconsidering.

In our context the usage of @WithMockOidcUser is quite meaningful and elegant :-), have a look at this sample:
https://github.com/SAP/cloud-security-xsuaa-integration/blob/cas/samples/spring-security-cas/src/test/java/com/sap/cloud/security/samples/SalesOrderControllerTest.java

Btw, to make the configuration of @WithMockOidcUser more generic, there is the option to enhance @WithMockOidcUser with custom claims, as documented here:
https://stackoverflow.com/questions/13569079/can-java-annotation-have-complex-return-type-like-hashmap

Enjoy your day!

@rwinch
Copy link
Member

rwinch commented May 27, 2020

Here is a link to some of our previous discussions around annotation based testing with more complex objects #6557

Copy link
Contributor

@jzheaux jzheaux left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR, @nenaraab! I agree that it'd be good to introduce simple annotation support without claims.

I've left some feedback inline.

*
* @author Nena Raab
* @see WithUserDetails
* @since 5.3
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Go ahead and change this to 5.4 since that's the next minor release

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sure, thanks for the detailed feedback!

**
* @return
*/
String[] authorities() default { "SCOPE_openid" };
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you already consider adding a scopes attribute as well? Doing scopes={ "openid", "profile" } would mirror roles in @WithMockUser

Copy link
Contributor Author

@nenaraab nenaraab May 28, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mhhh...

  • I dislike mutually exclusive options, which would be the case when introducing scopes in addition to authorities.
    Then we have to implement it similar to @WithMockUser where scopes gets "ignored" in case authorities are defined.

  • scopes only is not a good option, as I need the flexibility to add authorities without Spring's prefix.

  • authorities only might confuse the one or the other consumer, because he/she has to add "SCOPE_" prefix, which exposes Spring Security implementation detail.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No problem, I don't have strong feelings about it, and we can always wait for a community member to request it.

The rest is just for future reference:

mutual exclusivity

Btw, while I see your point about mutual exclusivity, I don't see it as a problem as you do. The DSL allows oauth2ResourceServer().jwt() OR oauth2ResourceServer().opaqueToken(), for example.

scopes only

agreed

authorities only ... add "SCOPE_" prefix

The application only has to do this if it's using the "SCOPE_" prefix in its authorization rules, in which case the tester already knows about it. I don't see this as an issue.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

then - lets provide both :-)

* The password to be used. The default is "clientId".
* @return
*/
String clientId() default "clientId";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think let's leave out any additional claims for the time being.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, for sure!

final Instant issuedAt = new GregorianCalendar().toInstant().minusSeconds(3);

OidcIdTokenFactory(String userId, String clientId, String userIdClaimName) {
claims.put("client_id", clientId); // mandatory
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you clarify why this is mandatory?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, its not required (any more) it can be removed. Thanks!

return context;
}

private class OidcIdTokenFactory {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is a separate class necessary? I'm wondering if you'd get the same outcome using methods.


List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
for (String authority : withUser.authorities()) {
grantedAuthorities.add(new SimpleGrantedAuthority(authority));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To align this with the OidcUser that OidcUserService creates, I think an OidcUserAuthority should also be added.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only thing it does now: it adds another GrantedAuthority with "ROLE_USER", not sure why required...


OidcIdTokenFactory(String userId, String clientId, String userIdClaimName) {
claims.put("client_id", clientId); // mandatory
claims.put("iat", issuedAt.getEpochSecond());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be easier to maintain if these used the claim names defined in IdTokenClaimNames.


private class OidcIdTokenFactory {
private Map<String, Object> claims = new HashMap<>();
final Instant expiredAt = new GregorianCalendar().toInstant().plusSeconds(600);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason that you are using new GregorianCalendar().toInstant() instead of Instant.now()?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

factory.createSecurityContext(withUser);
}

public void valueDefaultsUserIdWhenUserNameIsNull() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would you please re-arrange the method names so that they look like valueWhenUserNameIsNullThenDefaultsUserId? In new classes, the team has standardized on methodNameWhenCircumstanceThenResult.

* {@link TestExecutionEvent#TEST_METHOD} which occurs during
* {@link org.springframework.test.context.TestExecutionListener#beforeTestMethod(TestContext)}
* @return the {@link TestExecutionEvent} to initialize before
* @since 5.1
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reminder to remove this @since, which probably came over from a copy-paste

thanks for the detailed and valuable review!
@nenaraab
Copy link
Contributor Author

@jzheaux
I'm done with the changes, happy to receive further feedback, in case there are further findings :-)
Thanks again for your valuable review and explanations!

@jzheaux jzheaux added in: test An issue in spring-security-test type: enhancement A general enhancement and removed status: waiting-for-triage An issue we've not yet triaged labels May 28, 2020
@jzheaux jzheaux added this to the 5.4.0-M2 milestone May 28, 2020
@jzheaux jzheaux modified the milestones: 5.4.0-M2, 5.4.0-RC1 Jul 1, 2020
@jgrandja jgrandja modified the milestones: 5.4.0-RC1, 5.4.0 Aug 5, 2020
@jzheaux jzheaux removed this from the 5.4.0 milestone Sep 8, 2020
@jzheaux
Copy link
Contributor

jzheaux commented Feb 1, 2022

@nenaraab thanks so much for this contribution and for responding to my change requests. After letting this sit for some time now, I feel like we don't want to add claims to the annotation.

Without claims, it provides no information that isn't already available in Authentication:

public String method(Authentication authentication) {
    String name = authentication.getName();
    Collection<GrantedAuthority> authorities = authentication.getAuthorities();
}

in which case tying the application to OidcUser is needless. I'd prefer not to simplify the use case where folks are tying their applications needlessly to OidcUser.

Given that, I'm going to close this, but please feel free to comment further if I've missed a use case of yours.

@jzheaux jzheaux closed this Feb 1, 2022
@jzheaux jzheaux added the status: declined A suggestion or change that we don't feel we should currently apply label Feb 1, 2022
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 status: declined A suggestion or change that we don't feel we should currently apply type: enhancement A general enhancement
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants