OAuth2 Resource Server test strategies
OAuth2 is complex and good understanding of the concepts is required to do efficient testing of the various parts of an OAuth2 deployment. This post focus on testing resource servers, assuming that an access token already has been obtained.
Note: that there are a lot of good sources for learning OAuth2. A good start is OAuth 2 in Action ) or the actual specs. Diving directly into the microsoft documentation or various articles is not a particularly good idea, since an understanding of the basic concepts is already assumed.
First, let’s do a quick recap of the main concepts at play in an OAuth2 deployment.
Client
A client is an application normally acting with permissions delegated by a Resource Owner. This is why OAuth 2 is referred to as a delegation protocol. A resource owner is delegating permissions to a client, using the authorization server. Clients are registered at the Authorization Server and as an extra level of security they also need to authenticate themselves against the AS. (The exception are implicit flows, where a client is not required to authenticate itself). Clients can also act on their own (without Resource Owner involvement), using the Client Credentials flow.
Resource Owner
A Resource Owner is a user with access / ownership to resources.
Resource Server
A Resource Server is the server that contains the resources. The Resource Server needs to validate access to its resources requested by clients. This is done through access tokens.
Authorization Server
The authorization server issues access tokens to a client, which the client then use to access a resource. In order to do this, the Resource Owner is required to authenticate him/herself to the AS. Authentication is typically done via the Authorization Servers /authorize endpoint while tokens are issues from the /token endpoint (the exception, again, is implicit flows, where tokens are returned directly from the /authorize endpoint)
Access Token
A good analogy for an access token is an old fashioned bus ticket. The bus driver (resource server) does not care who (client) presents the ticket (access token). As long as the ticket is valid, e.g. issued to the right bus company, is not expired etc, it can be used to access a seat (resource). Access tokens should be signed or encrypted by the authorization server and the signature must then be validated by the resource server. Json Web Tokens (JWT) is a much used access token format.
Testing Strategies
Here are 4 different approaches to testing Resource Servers in an OAuth2 setting. Note that I’m not focusing on how a client obtains an access token here, I primarily want to verify that the resource server validates the token properly and only returns the resources it should.
I’ve split the functionality we want to test into two main parts:
- Token handling
- Access control
Token handling includes things such as verifying JWT claims (aud, sub, iss, …), the JWT signature and other things related to the access token envelope.
Access control is verifying that proper access is given or denied based on the user’s application roles and/or the clients scopes.
Strategy 1 - Full blown integration test
In this setup the Resource Server is running as a separate process. The test suite also runs as a separate process and contains a client and runs tests towards the resource server and checks the responses.
For the Authorization Server there are two options:
- Using Azure AD (or another “official” AS) directly.
- Pro’s: The actual Azure AD JWT token structure and signature is validated, as well as the https transport layer, with headers etc.
- Con’s: It is impractical to run tests as different users with different access rights. A possible solution is outlined here: AAD integration test, but I suspect in the long term this approach will be brittle and cumbersome to maintain.
- Using an ad-hoc authorization server just for the purpose of testing
- Pro’s: You validate that the RS does the correct steps (validates a signature, checks the “aud” (audience) claims, and so on). Testing as different users and clients is now trivial.
- Con’s: You need to make sure the JWT structure resembles Azure AD as closely as possible. Cannot test actual Azure AD signatures.
Strategy 2 - Mocking the OAuth2 protocol
This strategy is included here mainly for completeness. It allows you to test entire login sequence (OpenID, Authorization Code Grant etc) and delegation. It is described in more detail here: Fake an OAuth2 SSO
Strategy 3 - Embedding the Authorization Server and Resource Server in the test process
In this approach all the main components are running in the same process.
- Pro’s: Token handling (validation, expiration etc) can be tested. It runs a lot faster since everything is in the same process. Testing as different users and clients is trivial.
- Con’s: Somewhat complex set up. Again, actual Azure AD JWT structure is not tested.
A Java/Spring Boot example of this approach is found here: Resource Server Testing
Strategy 4 - Bypass token handling
By far the easiest approach. This allows access control code in the Resource Server to be tested, given that you can mock a Security Context in the Resource Server as if a token had been received from a client. Some more details for Java/Spring Boot developers are found here Bypass authentication. This Stack Overflow thread has some good tips in it, for different testing approaches.
In case you just need to test against application roles (that will normally would be sent via the roles claim in the JWT, such as for id tokens), a much simpler approach can be used, with MockMvc:
/*
* This configuration is used when Mocking a SecurityContext with the @WithUserDetails annotation.
* The endpoint security configuration is set up in the ResourceServerConfigurerAdapter (HttpSecurity)
*/
@Configuration
public class SecurityTestConfiguration {
@Autowired
private WebApplicationContext webApplicationContext;
@Bean
@Primary
public UserDetailsService userDetailsService() {
UserDetails basicUser = User.builder().
username("user@equinor.com").
authorities("ROLE_USER").
password("password").build();
UserDetails superUser = User.builder().
username("superuser@equinor.com").
authorities("ROLE_SUPER_USER").
password("password").build();
UserDetails adminUser = User.builder().
username("admin@equinor.com").
authorities("ROLE_ADMIN").
password("password").build();
return new InMemoryUserDetailsManager(Arrays.asList(
basicUser, superUser, adminUser
));
}
/**
* We need to set the OAuth2AuthenticationProcessingFilter to stateful after doing the springSecurity() setup
* otherwise it will clear the mocked Security Context
*
* @return Configured MockMvc
*/
@Bean
public MockMvc mockMvc() {
MockMvc mockMvc = MockMvcBuilders
.webAppContextSetup(webApplicationContext)
.apply(SecurityMockMvcConfigurers.springSecurity())
.build();
FilterChainProxy chain = (FilterChainProxy) webApplicationContext.getBean("springSecurityFilterChain");
List<Filter> filters = chain.getFilters("/");
for (Filter filter : filters) {
if (filter instanceof OAuth2AuthenticationProcessingFilter) {
((OAuth2AuthenticationProcessingFilter)filter).setStateless(false);
break;
}
}
return mockMvc;
}
}
Note that the setStateless(false) is critical. Otherwise the mocked SecurityContext will be cleared by the OAuth2AuthenticationProcessingFilter before the request hits the access control code.
The test code then looks similar to this:
@Test
@WithUserDetails("superuser@equinor.com")
public void createGradeAsSuperUser_expectOk() throws Exception {
//Create a grade linked to a region
GradeResource grade = GradeResourceBuilder...
mockMvc.perform(MockMvcRequestBuilders
.post("/ct/config/grade")
.content(objectMapper.writeValueAsString(grade))
.contentType(MediaType.APPLICATION_JSON_UTF8)
).andExpect(status().isOk())
.andExpect(jsonPath("$.id", containsString("-")))
.andExpect(jsonPath("$.name", equalTo("DALIA")))
.andDo(print());
}
Summary
A good place to start is Strategy 4, this allows you to focus on testing the access control code. If time allows, do Strategy 3. Strategy 2 is probably more relevant for authors of Authorization Servers. Strategy 1, with Azure AD can likely be tested in production.
Future possible topics:
- Testing Resource Servers based on scopes
- Testing Resource Servers based on scopes and roles (on behalf of flows)
- Spring Boot / Spring Security testing strategies