Skip to content

Commit 768b38c

Browse files
authored
Merge pull request #1 from ahunigel/pre-token
emulate full oauth2 process
2 parents b420439 + a99eee0 commit 768b38c

File tree

13 files changed

+212
-10
lines changed

13 files changed

+212
-10
lines changed

README.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ _Note: Most code came from the open network. I refactor and enhanced the code, t
1919
- @WithMockUserAndClaims
2020
- enhanced @WithMockUser, attach Map-based claims as authentication details
2121
- equal to @WithMockUser + @AttachClaims
22+
- @WithToken
23+
- add `bearer` token to request header to extract a `PreAuthenticatedAuthenticationToken`,
24+
load existing OAuth2Authentication from SecurityContext
2225

2326
## How to use
2427

@@ -65,9 +68,7 @@ Refer to https://jitpack.io/#ahunigel/spring-security-oauth2-test for details.
6568

6669
## TODOs
6770

68-
1. Mock full oauth2 process, add `bearer` token to request header to extract a `PreAuthenticatedAuthenticationToken`
69-
70-
2. For oauth2 request, add ability to set ResourceServerSecurityConfigurer.stateless to false, maybe an
71+
- For oauth2 request, add ability to set ResourceServerSecurityConfigurer.stateless to false, maybe add an
7172
annotation like `@ResourceStateLess(false)`
7273

73-
3. Add support for `RestTemplate`
74+
- Add support for `RestTemplate`

build.gradle

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ apply plugin: 'java'
1717
apply plugin: "jacoco"
1818

1919
group 'com.github.ahunigel'
20-
version '1.0-SNAPSHOT'
20+
version '1.1-SNAPSHOT'
2121

2222
sourceCompatibility = 1.8
2323

@@ -26,6 +26,7 @@ dependencies {
2626
implementation "org.springframework.security.oauth:spring-security-oauth2:2.2.1.RELEASE"
2727
implementation "org.springframework.security:spring-security-test:5.0.6.RELEASE"
2828
implementation group: 'commons-beanutils', name: 'commons-beanutils', version: '1.9.3'
29+
compileOnly group: 'javax.servlet', name: 'javax.servlet-api', version: '4.0.1'
2930
}
3031

3132
repositories {

src/main/java/com/github/ahunigel/test/security/AttachClaims.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
* Created by Nigel Zheng on 8/3/2018.
77
* <p>
88
* Attach claims as map to current authentication details
9+
*
10+
* @author nigel
911
*/
1012
@Target({ElementType.METHOD, ElementType.TYPE})
1113
@Retention(RetentionPolicy.RUNTIME)

src/main/java/com/github/ahunigel/test/security/AttachClaimsTestExecutionListener.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,12 @@
1515
import java.util.Map;
1616

1717
/**
18-
* Created by Nigel.Zheng on 8/3/2018.
18+
* Created by Nigel Zheng on 8/3/2018.
19+
*
20+
* @author nigel
1921
*/
2022
public class AttachClaimsTestExecutionListener extends AbstractTestExecutionListener {
21-
private static final Logger logger = LoggerFactory.getLogger(AttachClaimsTestExecutionListener.class);
23+
private static final Logger log = LoggerFactory.getLogger(AttachClaimsTestExecutionListener.class);
2224

2325
@Override
2426
public void beforeTestClass(TestContext testContext) throws Exception {
@@ -52,7 +54,7 @@ public void attachClaimsToAuthentication(AttachClaims annotation) {
5254
((AbstractAuthenticationToken) authentication).setDetails(claims);
5355
}
5456
} else {
55-
logger.warn("No authentication found, do not attach claims");
57+
log.warn("No authentication found, do not attach claims");
5658
}
5759
}
5860

src/main/java/com/github/ahunigel/test/security/WithMockUserAndClaims.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
* <p>
1919
* Emulate running with a mocked user,
2020
* attach claims as map to mocked authentication details
21+
*
22+
* @author nigel
2123
*/
2224
@Target({ElementType.METHOD, ElementType.TYPE})
2325
@Retention(RetentionPolicy.RUNTIME)
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package com.github.ahunigel.test.security.oauth2;
2+
3+
import org.springframework.boot.test.mock.mockito.MockBean;
4+
import org.springframework.boot.test.mock.mockito.MockReset;
5+
import org.springframework.security.oauth2.provider.token.ResourceServerTokenServices;
6+
7+
import java.lang.annotation.Retention;
8+
import java.lang.annotation.RetentionPolicy;
9+
10+
/**
11+
* Created by Nigel Zheng on 8/7/2018.
12+
*/
13+
@Retention(RetentionPolicy.RUNTIME)
14+
@MockBean(value = {ResourceServerTokenServices.class}, reset = MockReset.AFTER)
15+
public @interface MockTokenServices {
16+
}

src/main/java/com/github/ahunigel/test/security/oauth2/WithMockOAuth2Client.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
* <p>
2121
* Emulate running with a mocked oauth2 client
2222
*/
23+
@WithToken
2324
@Retention(RetentionPolicy.RUNTIME)
2425
@WithSecurityContext(factory = WithMockOAuth2Client.WithMockOAuth2ClientSecurityContextFactory.class)
2526
public @interface WithMockOAuth2Client {

src/main/java/com/github/ahunigel/test/security/oauth2/WithMockOAuth2User.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
* Emulate running with a mocked oauth2 client on behalf of user,
2121
* attach claims as map to mocked oauth2 authentication details
2222
*/
23+
@WithToken
2324
@Retention(RetentionPolicy.RUNTIME)
2425
@WithSecurityContext(factory = WithMockOAuth2User.WithMockOAuth2UserSecurityContextFactory.class)
2526
public @interface WithMockOAuth2User {
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package com.github.ahunigel.test.security.oauth2;
2+
3+
import org.springframework.security.oauth2.provider.token.ResourceServerTokenServices;
4+
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken;
5+
6+
import java.lang.annotation.Retention;
7+
import java.lang.annotation.RetentionPolicy;
8+
9+
/**
10+
* Created by Nigel Zheng on 2018/8/7.
11+
* <p>
12+
* Emulate an OAuth2 token request, would extract an {@link PreAuthenticatedAuthenticationToken}
13+
* <p>
14+
* require annotation {@link MockTokenServices} on test class level or {@link ResourceServerTokenServices} @MockBean exists
15+
*
16+
* @author nigel
17+
*/
18+
@Retention(RetentionPolicy.RUNTIME)
19+
public @interface WithToken {
20+
21+
String FAKE_BEARER_TOKEN = "fake.bearer.token";
22+
23+
String value() default FAKE_BEARER_TOKEN;
24+
}
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
package com.github.ahunigel.test.security.oauth2;
2+
3+
import org.mockito.Mockito;
4+
import org.slf4j.Logger;
5+
import org.slf4j.LoggerFactory;
6+
import org.springframework.core.annotation.AnnotatedElementUtils;
7+
import org.springframework.mock.web.MockHttpServletRequest;
8+
import org.springframework.security.core.Authentication;
9+
import org.springframework.security.core.context.SecurityContext;
10+
import org.springframework.security.oauth2.provider.OAuth2Authentication;
11+
import org.springframework.security.oauth2.provider.token.ResourceServerTokenServices;
12+
import org.springframework.security.test.context.TestSecurityContextHolder;
13+
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken;
14+
import org.springframework.test.context.TestContext;
15+
import org.springframework.test.context.support.AbstractTestExecutionListener;
16+
import org.springframework.test.util.ReflectionTestUtils;
17+
import org.springframework.test.web.servlet.MockMvc;
18+
import org.springframework.test.web.servlet.RequestBuilder;
19+
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
20+
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
21+
import org.springframework.test.web.servlet.request.RequestPostProcessor;
22+
import org.springframework.util.Assert;
23+
import org.springframework.util.StringUtils;
24+
25+
import java.lang.annotation.Annotation;
26+
import java.lang.reflect.AnnotatedElement;
27+
import java.lang.reflect.Method;
28+
29+
import static org.mockito.Mockito.when;
30+
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.testSecurityContext;
31+
32+
/**
33+
* Created by Nigel Zheng on 2018/8/6.
34+
* <p>
35+
* Add <code>Authorization</code> header to token request, extract an {@link PreAuthenticatedAuthenticationToken},
36+
* and then load an existing {@link OAuth2Authentication} from {@link SecurityContext}
37+
*
38+
* @author nigel
39+
*/
40+
public class WithTokenTestExecutionListener extends AbstractTestExecutionListener {
41+
private static final Logger log = LoggerFactory.getLogger(WithTokenTestExecutionListener.class);
42+
43+
private static final String ORIGINAL_REQUEST_BUILDER = "originalRequestBuilder";
44+
45+
@Override
46+
public void beforeTestClass(TestContext testContext) throws Exception {
47+
Annotation annotation = AnnotatedElementUtils.findMergedAnnotation(testContext.getTestClass(), WithToken.class);
48+
if (annotation != null) {
49+
verifyTokenServicesMocked(testContext.getTestClass());
50+
addAuthHeader(testContext.getTestClass(), testContext, (WithToken) annotation);
51+
}
52+
}
53+
54+
@Override
55+
public void beforeTestMethod(TestContext testContext) throws Exception {
56+
Annotation annotation = AnnotatedElementUtils.findMergedAnnotation(testContext.getTestMethod(), WithToken.class);
57+
if (annotation != null) {
58+
verifyTokenServicesMocked(testContext.getTestClass());
59+
addAuthHeader(testContext.getTestMethod(), testContext, (WithToken) annotation);
60+
}
61+
}
62+
63+
@Override
64+
public void afterTestMethod(TestContext testContext) throws Exception {
65+
Annotation annotation = AnnotatedElementUtils.findMergedAnnotation(testContext.getTestMethod(), WithToken.class);
66+
if (annotation != null) {
67+
removeAuthHeader(testContext.getTestMethod(), testContext);
68+
}
69+
}
70+
71+
@Override
72+
public void afterTestClass(TestContext testContext) throws Exception {
73+
Annotation annotation = AnnotatedElementUtils.findMergedAnnotation(testContext.getTestClass(), WithToken.class);
74+
if (annotation != null) {
75+
removeAuthHeader(testContext.getTestClass(), testContext);
76+
}
77+
}
78+
79+
private void verifyTokenServicesMocked(Class<?> testClass) {
80+
MockTokenServices annotation = AnnotatedElementUtils.findMergedAnnotation(testClass, MockTokenServices.class);
81+
Assert.state(annotation != null, "Missing @MockTokenServices on class level");
82+
}
83+
84+
private void addAuthHeader(AnnotatedElement annotated, TestContext testContext, WithToken withToken) {
85+
Assert.state(withToken != null, "No @WithToken exists!!!");
86+
MockMvc mockMvc = testContext.getApplicationContext().getBean(MockMvc.class);
87+
MockHttpServletRequestBuilder requestBuilder = MockMvcRequestBuilders.get("/")
88+
.with(testSecurityContext()).with(bearerAuthPostProcessor(withToken.value()));
89+
// stash original default request builder
90+
RequestBuilder originalRequestBuilder = (RequestBuilder) ReflectionTestUtils.getField(mockMvc, MockMvc.class,
91+
"defaultRequestBuilder");
92+
testContext.setAttribute(attributeName(annotated), originalRequestBuilder);
93+
setDefaultRequestBuilder(mockMvc, requestBuilder);
94+
ResourceServerTokenServices tokenServices = testContext.getApplicationContext().getBean(ResourceServerTokenServices.class);
95+
when(tokenServices.loadAuthentication(withToken.value())).thenAnswer(invocation -> {
96+
Authentication authentication = TestSecurityContextHolder.getContext().getAuthentication();
97+
if (authentication instanceof OAuth2Authentication) {
98+
return (OAuth2Authentication) authentication;
99+
}
100+
return null;
101+
});
102+
}
103+
104+
private void removeAuthHeader(AnnotatedElement annotated, TestContext testContext) {
105+
MockMvc mockMvc = testContext.getApplicationContext().getBean(MockMvc.class);
106+
Object originalRequestBuilder = testContext.getAttribute(attributeName(annotated));
107+
if (originalRequestBuilder instanceof RequestBuilder) {
108+
// reset default request builder
109+
setDefaultRequestBuilder(mockMvc, (RequestBuilder) originalRequestBuilder);
110+
}
111+
Mockito.reset(new ResourceServerTokenServices[]{
112+
testContext.getApplicationContext().getBean(ResourceServerTokenServices.class)
113+
});
114+
}
115+
116+
private String attributeName(AnnotatedElement annotated) {
117+
return ORIGINAL_REQUEST_BUILDER + annotated.getClass().getSimpleName()
118+
+ (annotated instanceof Method ? ((Method) annotated).getName() : "");
119+
}
120+
121+
private void setDefaultRequestBuilder(MockMvc mockMvc, RequestBuilder requestBuilder) {
122+
ReflectionTestUtils.invokeSetterMethod(mockMvc, "setDefaultRequest", requestBuilder, RequestBuilder.class);
123+
}
124+
125+
private RequestPostProcessor bearerAuthPostProcessor(String token) {
126+
return new BearerAuthPostProcessor(token);
127+
}
128+
129+
class BearerAuthPostProcessor implements RequestPostProcessor {
130+
private final String token;
131+
132+
BearerAuthPostProcessor(String token) {
133+
this.token = token;
134+
}
135+
136+
@Override
137+
public MockHttpServletRequest postProcessRequest(MockHttpServletRequest request) {
138+
String authHeader = request.getHeader("Authorization");
139+
if (!StringUtils.hasText(authHeader)) {
140+
request.addHeader("Authorization", "Bearer " + token);
141+
} else {
142+
log.warn("DO NOT OVERRIDE existing authorization header: {}", authHeader);
143+
}
144+
return request;
145+
}
146+
}
147+
}

0 commit comments

Comments
 (0)