Skip to content

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

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
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/*
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.test.context.support;

import org.springframework.core.annotation.AliasFor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.test.context.TestContext;
import org.springframework.test.web.servlet.MockMvc;

import java.lang.annotation.*;

/**
* When used with {@link WithSecurityContextTestExecutionListener} this annotation can be
* added to a test method to emulate running with a mocked user. In order to work with
* {@link MockMvc} The {@link SecurityContext} that is used will have the following
* properties:
*
* <ul>
* <li>The Authentication that is populated in the {@link SecurityContext} is of type {@link OAuth2AuthenticationToken}.</li>
* <li>The principal on the Authentication is Spring Security’s User object of type {@code OidcUser}.</li>
* <li>The default User has the user name "user". You can overwrite it the name with {@link #value()} or {@link #name()}.</li>
* <li>The default User has "ROLE_USER" and "SCOPE_openid" as {@link GrantedAuthority}.
* You can overwrite them with {@link #scopes()} or {@link #authorities()}. </li>
* </ul>
*
* @author Nena Raab
* @see WithUserDetails
* @since 5.4
*/
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@WithSecurityContext(factory = WithMockOidcUserSecurityContextFactory.class)
public @interface WithMockOidcUser {

/**
* Convenience mechanism for specifying the username. The default is "user". If
* {@link #name()} is specified it will be used instead of {@link #value()}
* @return
*/
String value() default "user";

/**
* The user name or user id (subject) to be used. Note that {@link #value()} is a synonym for
* {@link #name()}, but if {@link #name()} is specified it will take
* precedence.
* @return
*/
String name() default "";

/**
* <p>
* The scopes that should be mapped to {@code GrantedAuthority}.
* The default is "openid". Each value in scopes gets prefixed with "SCOPE_"
* and added to the list of {@link GrantedAuthority}.
* </p>
* <p>
* If {@link #authorities()} is specified this property cannot be changed from the default.
* </p>
*
* @return
*/
String[] scopes() default { "openid" };

/**
* <p>
* The authorities that should be mapped to {@code GrantedAuthority}.
* </p>
*
* <p>
* If this property is specified then {@link #scopes()} is not used. This differs from
* {@link #scopes()} in that it does not prefix the values passed in automatically.
* </p>
* @return
*/
String[] authorities() default { };

/**
* Determines when the {@link SecurityContext} is setup. The default is before
* {@link TestExecutionEvent#TEST_METHOD} which occurs during
* {@link org.springframework.test.context.TestExecutionListener#beforeTestMethod(TestContext)}
* @return the {@link TestExecutionEvent} to initialize before
*/
@AliasFor(annotation = WithSecurityContext.class)
TestExecutionEvent setupBefore() default TestExecutionEvent.TEST_METHOD;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/*
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.test.context.support;

import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

import java.time.Instant;
import java.util.*;

import static org.springframework.security.oauth2.core.oidc.IdTokenClaimNames.*;

/**
* Initializes the Spring Security Context with a OAuth2AuthenticationToken instance.
*
* @author Nena Raab
* @see WithMockOidcUser
*/
final class WithMockOidcUserSecurityContextFactory implements
WithSecurityContextFactory<WithMockOidcUser> {

public SecurityContext createSecurityContext(WithMockOidcUser withUser) {
String userId = StringUtils.hasLength(withUser.name()) ? withUser
.name() : withUser.value();
if (userId == null) {
Assert.notNull(userId, "@WithMockOidcUser cannot have null name on both name and value properties");
}

Set<GrantedAuthority> grantedAuthorities = new HashSet<>();
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...

}

if (grantedAuthorities.isEmpty()) {
for (String scope : withUser.scopes()) {
Assert.isTrue(!scope.startsWith("SCOPE_"), "scopes cannot start with SCOPE_ got " + scope);
grantedAuthorities.add(new SimpleGrantedAuthority("SCOPE_" + scope));
}
}
// To align this with the OidcUser that OidcUserService creates, this adds a ROLE_USER
grantedAuthorities.add(new OidcUserAuthority(getOidcTokenForUser(userId)));

OidcUser principal = new DefaultOidcUser(grantedAuthorities, getOidcTokenForUser(userId));

Authentication authentication = new OAuth2AuthenticationToken(
principal, principal.getAuthorities(), "client-id");

SecurityContext context = SecurityContextHolder.createEmptyContext();
context.setAuthentication(authentication);
return context;
}

private static OidcIdToken getOidcTokenForUser(String userId) {
Map<String, Object> claims = new HashMap<>();
final Instant issuedAt = Instant.now().minusSeconds(3);
final Instant expiredAt = Instant.now().plusSeconds(600);

claims.put(IAT, issuedAt.getEpochSecond());
claims.put(EXP, expiredAt.getEpochSecond());
claims.put(SUB, userId);

return new OidcIdToken("id-token", issuedAt, expiredAt, claims);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/*
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.test.context.support;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.*;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;

import java.time.Instant;

@RunWith(MockitoJUnitRunner.class)
public class WithMockOidcUserSecurityContextTests {
private final static String USER_VALUE = "valueUser";

@Mock
private WithMockOidcUser withUser;

private WithMockOidcUserSecurityContextFactory factory;

@Before
public void setup() {
factory = new WithMockOidcUserSecurityContextFactory();
when(withUser.value()).thenReturn(USER_VALUE);
when(withUser.authorities()).thenReturn(new String[]{});
when(withUser.scopes()).thenReturn(new String[]{"openid"});
when(withUser.name()).thenReturn("");
}

@Test(expected = IllegalArgumentException.class)
public void createSecurityContextWhenValueIsNullThenRaiseException() {
when(withUser.value()).thenReturn(null);
factory.createSecurityContext(withUser);
}

@Test
public void createSecurityContextWhenUserNameIsNullThenUseDefaultValue() {
when(withUser.name()).thenReturn(null);
assertThat(factory.createSecurityContext(withUser).getAuthentication().getName())
.isEqualTo(USER_VALUE);
}

@Test
public void createSecurityContextWhenUserNameIsEmptyThenUseDefaultValue() {
assertThat(factory.createSecurityContext(withUser).getAuthentication().getName())
.isEqualTo(USER_VALUE);
}

@Test
public void createSecurityContextWhenUserNameIsSetThenUseUserName() {
when(withUser.name()).thenReturn(USER_VALUE);

assertThat(factory.createSecurityContext(withUser).getAuthentication().getName())
.isEqualTo(USER_VALUE);
}

@Test
public void createSecurityContextWhenAuthoritiesSetThenUseAuthorities() {
when(withUser.authorities()).thenReturn(new String[]{"USER", "CUSTOM", "ROLE_USER"});

assertThat(
factory.createSecurityContext(withUser).getAuthentication()
.getAuthorities()).extracting("authority").containsExactlyInAnyOrder(
"USER", "CUSTOM", "ROLE_USER");
}

@Test
public void createSecurityContextWhenNoScopesAndAuthoritiesSetThenUseDefaultScope() {
assertThat(
factory.createSecurityContext(withUser).getAuthentication()
.getAuthorities()).extracting("authority").containsExactlyInAnyOrder(
"SCOPE_openid", "ROLE_USER");
}

@Test
public void createSecurityContextWhenScopesSetThenUseScopes() {
when(withUser.scopes()).thenReturn(new String[]{"DISPLAY", "ADMIN"});

assertThat(
factory.createSecurityContext(withUser).getAuthentication()
.getAuthorities()).extracting("authority").containsExactlyInAnyOrder(
"SCOPE_DISPLAY", "SCOPE_ADMIN", "ROLE_USER");
}

@Test
public void createSecurityContextThenOidcNotYetExpired() {
OidcUser oidcUser = (OidcUser) factory.createSecurityContext(withUser).getAuthentication().getPrincipal();
assertThat(oidcUser.getIssuedAt().compareTo(Instant.now())).isNegative();
assertThat(oidcUser.getExpiresAt().compareTo(Instant.now())).isPositive();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.test.context.support;

import static org.assertj.core.api.Assertions.assertThat;

import org.junit.Test;
import org.springframework.core.annotation.AnnotatedElementUtils;

public class WithMockOidcUserTests {

@Test
public void defaults() {
WithMockOidcUser mockUser = AnnotatedElementUtils.findMergedAnnotation(Annotated.class,
WithMockOidcUser.class);
assertThat(mockUser.value()).isEqualTo("user");
assertThat(mockUser.name()).isEmpty();
assertThat(mockUser.scopes()).containsOnly("openid");
assertThat(mockUser.authorities()).isEmpty();
assertThat(mockUser.setupBefore()).isEqualByComparingTo(TestExecutionEvent.TEST_METHOD);

WithSecurityContext context = AnnotatedElementUtils.findMergedAnnotation(Annotated.class,
WithSecurityContext.class);

assertThat(context.setupBefore()).isEqualTo(TestExecutionEvent.TEST_METHOD);
}

@WithMockOidcUser
private class Annotated {
}

@Test
public void findMergedAnnotationWhenSetupExplicitThenOverridden() {
WithSecurityContext context = AnnotatedElementUtils
.findMergedAnnotation(SetupExplicit.class,
WithSecurityContext.class);

assertThat(context.setupBefore()).isEqualTo(TestExecutionEvent.TEST_METHOD);
}

@WithMockOidcUser(setupBefore = TestExecutionEvent.TEST_METHOD)
private class SetupExplicit {
}

@Test
public void findMergedAnnotationWhenSetupOverriddenThenOverridden() {
WithSecurityContext context = AnnotatedElementUtils.findMergedAnnotation(SetupOverridden.class,
WithSecurityContext.class);

assertThat(context.setupBefore()).isEqualTo(TestExecutionEvent.TEST_EXECUTION);
}

@WithMockOidcUser(setupBefore = TestExecutionEvent.TEST_EXECUTION)
private class SetupOverridden {
}
}