I am having some problems when testing an oauth2 resource server using #WebMvcTest and the POST HTTP method.
I always receive a 403 status code when I don't send the csrf token, even though the token is not required when I am using a bearer token.
Here is the POST method that I want to test.
#PostMapping("/message")
public String createMessage(#RequestBody String message) {
return String.format("Message was created. Content: %s", message);
}
Here is my security config:
http.authorizeRequests(authorizeRequests -> authorizeRequests
.antMatchers("/message/**")
.hasAuthority("SCOPE_message:read")
.anyRequest().authenticated()
).oauth2ResourceServer(oauth2ResourceServer ->
oauth2ResourceServer
.jwt(withDefaults())
);
I am following the tests provided in the samples of spring-security.
The following test was supposed to pass but it fails because the csrf token is not sent in the request.
mockMvc.perform(post("/message").content("Hello message")
.with(jwt(jwt -> jwt.claim("scope", "message:read")))
.andExpect(status().isOk())
.andExpect(content().string(is("Message was created. Content: Hello message")));
When I add the csrf token to the request, the test passes:
mockMvc.perform(post("/message").content("Hello message")
.with(jwt(jwt -> jwt.claim("scope", "message:read")))
.with(csrf()))
.andExpect(status().isOk())
.andExpect(content().string(is("Message was created. Content: Hello message")));
When I run the application, there is no need to send a csrf token in the POST request.
I have forked the Spring Security GitHub repository and the project with this failing test is available at this link.
Is there a way for me to configure my tests so I don't need to send the csrf token in the POST request?
In order for the CSRF filter to detect that you are using a JWT token, you will need to include the JWT token in your request as an Authorization header, or as a request parameter.
The tests that you have mentioned have a mock JwtDecoder, which means you can use any string as your token and mock the decoded value.
Your test would then become:
Jwt jwt = Jwt.withTokenValue("token")
.header("alg", "none")
.claim("scope", "message:read")
.build();
when(jwtDecoder.decode(anyString())).thenReturn(jwt);
mockMvc.perform(post("/message")
.content("Hello message")
.header("Authorization", "Bearer " + jwt.getTokenValue()))
.andExpect(status().isOk())
.andExpect(content().string(is("Message was created. Content: Hello message")));
If you are not mocking the JwtDecoder then you would need to retrieve a valid bearer token and pass that in the Authorization header.
First with Bearer access-token, you might be able to disable sessions (STATELESS session-management), and, as so, CSRF protection (CSRF attack vector is session).
Second there is a .csrf() MockMvc request post-processor which would simulate the CSRF token if you want to keep cessions (and CSRF protection).
Last, what jwt() request post-processor does is configuring directly the security context with a JwtAuthenticationToken instance, not creating a valid JWT token set as Bearer access-token. With MockMvc, Authorization header is not decoded nor converted to an Authentication instance. => What you should set before MockMvc request execution for your test to pass are authorities, not scope claim:
.with(jwt(jwt -> jwt.authorities(List.of(new SimpleGrantedAuthority("SCOPE_message:read")))))
Note that you could also simply decorate your test function with:
#WithMockJwtAuth("SCOPE_message:read")
From this lib I maintain
Related
I am currently using a rather simple approach to restrict a certain suburl (everything under /api/rest) and all of its subpaths via WebFluxSecurity. Some paths (everything directly under the root NOT in /api/rest) are excluded so that they can be access without authorization. However, sometimes the accessing party might send an empty authorization header which leads to unsecured endpoints returning a 401.
See the relevant code here:
#Configuration
#EnableWebFluxSecurity
public class SecurityConfiguration {
#Value(value = "${...}")
private String user;
#Value(value = "${...}")
private String pw;
#Bean
public MapReactiveUserDetailsService userDetailsService() {
PasswordEncoder encoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
UserDetails user = User
.withUsername(user)
.password(encoder.encode(pw))
.roles("USER")
.build();
return new MapReactiveUserDetailsService(user);
}
#Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http
.authorizeExchange(exchanges -> exchanges
.pathMatchers("/api/rest/**")
.authenticated()
.anyExchange()
.permitAll()
)
.httpBasic(withDefaults());
return http.build();
}
}
On stackoverflow I've only found a few suggestions how to handle this with WebSecurity. However, this is not possible for me as I use webflux security.
See e.g.
Springboot webflux throwing 401 when Authorization header sent to unrestricted endpoint
Spring Boot 2: Basic Http Auth causes unprotected endpoints to respond with 401 "Unauthorized" if Authorization header is attached
TL;DR
If you pass invalid credentials to any endpoint with httpBasic() enabled, it will return a 401 response.
One important distinction that's relevant here is the difference between authentication and authorization. The httpBasic() DSL method adds the AuthenticationWebFilter configured for HTTP Basic. The authorizeExchange(...) DSL method defines authorization rules, such as authenticated() and permitAll().
The authentication filter appears earlier in the Spring Security filter chain than the authorization filter, and so authentication happens first which we would expect. Based on your comments, it seems you are expecting authentication not to happen if you mark an endpoint as permitAll(), but this is not the case.
Whether authentication is actually attempted against a particular request depends on how the authentication filter matches the request. In the case of AuthenticationWebFilter, a ServerWebExchangeMatcher (requiresAuthenticationMatcher) determines whether authentication is required. For httpBasic(), every request requires authentication. If you pass invalid credentials to any endpoint with httpBasic() enabled, it will return a 401 response.
Additionally, a ServerAuthenticationConverter (authenticationConverter) is used to read the Authorization header and parse the credentials. This is what would fail if an invalid token (or Authorization header) is given. ServerHttpBasicAuthenticationConverter is used for httpBasic() and is fairly forgiving of invalid header values. I don't find any scenarios that fail and produce a 401 response except invalid credentials.
Working on an Azure AD-based authentication using OAuth2 and its On-Behalf-Of authentication flow.
I am wondering how to actually check the token comparison between the initial token provided by the user and the final token being provided by the middle service principal to the third or other service principal.
My current approach is creating another endpoint inside my application #GetMapping("/otherSP") and then call it within my application(internal API call) like:
String token = webclient.get()
.uri(newServicePrincipalUri)
.attributes(clientRegistrationId("currentServicePrincipalId"))
.retrieve()
.bodyToMono(String.class)
.block();
The endpoint within is like:
#GetMapping("/otherSP")
public String getExchangeToken(#RegisteredOAuth2AuthorizedClient("otherSP-id")OAuth2AuthorizedClient oAuth2AuthorizedClient){
return oAuth2AuthorizedClient.getAccessToken().getTokenValue();
}
For the purpose of acquiring the JWT, this approach seems barbaric. Is there any better approach or proper approach on how to actually check it or obtain the exchanged token?
I have a controller method that has an argument OAuth2AuthorizedClient with annotation #RegisteredOAuth2AuthorizedClient.
ResponseEntity<String> getFoo(
#RegisteredOAuth2AuthorizedClient("custom") OAuth2AuthorizedClient client) {
OAuth2AccessToken token = client.getAccessToken();
....
}
In order to resolve this, spring look for OAuth token on OAuth2AuthorizedClientService which actually stores the previously authenticated tokens in memory.
Now, I have a scenario where I obtain JWT token from OAuth server outside of this spring resource server, and trying to authenticate this server by passing token using Authorization header.
But when spring trying to resolve OAuth2AuthorizedClient it is looking for token in-memory with the principle of JWT (which will obviously not found since token is not obtained on this server). Hence send new login redirection for new token.
Overall question would be, is it possible to resolve OAuth2AuthorizedClient with the JWT token (obtained ourside of the server) passed in Authorization header?
I am trying to do a JUnit Tests that executes an OAuth flow.
My customer built a OAuth provider, when I make a test using postman, the postman show me a screen to fill down the credentials, after that, the postman store the information (access_token, id_token, all JWT informations), it is ok.
See the example:
My code to test is:
#Test
public void getAccessTokenViaSpringSecurityOAuthClient() {
try {
OAuth2ProtectedResourceDetails resourceDetails = googleOAuth2Details();
OAuth2RestTemplate oAuthRestTemplate = new OAuth2RestTemplate(resourceDetails);
org.springframework.http.HttpHeaders headers = new org.springframework.http.HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
OAuth2AccessToken token = oAuthRestTemplate.getAccessToken();
System.out.println(oAuthRestTemplate.getResource());
System.out.println(oAuthRestTemplate.getOAuth2ClientContext());
System.out.println(token);
Assert.assertTrue(token != null);
} catch (Exception e) {
e.printStackTrace();
}
}
public OAuth2ProtectedResourceDetails googleOAuth2Details() {
AuthorizationCodeResourceDetails googleOAuth2Details = new AuthorizationCodeResourceDetails();
googleOAuth2Details.setClientId("xxxxx");
googleOAuth2Details.setUserAuthorizationUri("https://xxx/yyy/oauth2/authorize");
googleOAuth2Details.setAccessTokenUri("https://xxx/yyy/oauth2/token");
googleOAuth2Details.setScope(Arrays.asList("openid"));
googleOAuth2Details.setPreEstablishedRedirectUri("https://www.getpostman.com/oauth2/callback");
googleOAuth2Details.setAuthenticationScheme(AuthenticationScheme.query);
googleOAuth2Details.setClientAuthenticationScheme(AuthenticationScheme.form);
return googleOAuth2Details;
}
When I run the task, this exception happens:
org.springframework.security.oauth2.client.resource.UserRedirectRequiredException: A redirect is required to get the users approval
Is it possible to test this flow?
How can I do it?
The exception explains the problem : A redirect is required to get the users approval.
Postman is hiding the fact that once the authorization process is over, the authorization server redirects your browser to a redirect_uri, or Callback URL as Postman names it. This URL collects the authorization code delivered by the authorization server, and requests a token.
See the authorization code flow for more details :
OAuth 2.0 RFC 6749
a simplfied diagram
This means that you cannot "unit test" the authorization code grant of an authorization server. You need some kind of web server to process the callback of the authorization code flow.
You could probably test your authorization a lot faster by creating a #SpringBootTest with a Spring Boot application using #EnableOAuth2Sso. Spring Boot will auto-configure an OAuth2RestTemplate for you, and you can have it #Autowired in your JUnit test.
In lots of descriptions the first step is that user tries to acces a resource on server something like
https://fhirblog.files.wordpress.com/2014/06/oauth2sequencediagram.png
Now i got a Rest API with severel endpoints:
GET /server/resource1
DELETE /server/resource1/{uuid}
GET /server/resource2
...
implementation looks something like this:
#DELETE
public Response deleteResource(
#ApiParam(value = "The id", required=true)
#PathParam("uuid") String uuid,
#Context SecurityContext securityContext)
throws NotFoundException {
Until now i have implemented an apikey which is passed by header into the api and a filter that verifies teh apikey.
Now i want to implement a full (three/two legged) oauth 2.0 flow. But i am now wondering about the first step.
So Question is:
Do i have to add a mechanism on each endpoint that verifies if the request has a token? and if not redirect the request to an auth endpoint?
(Also
Can i send the Tokens in the HttpHeader or do the Tokens have to be in the Body of the Request?)
Or:
Do i have to create just one endpoint that does the token stuff and in my other resource endpoints i only verify if the token is valid?
Okay here are the explanations,
Do i have to add a mechanism on each endpoint that verifies if the request has a token? and if not redirect the request to an auth endpoint?
This question has two parts, so i will explain it separately for better understanding,
Do i have to add a mechanism on each endpoint that verifies if the request has a token?
Yes, in general the endpoints would be an APIs, so you need to setup middleware or interceptor or filters, to check to see does this endpoint need authorization if so check access token, if valid then proceed with request, if not return 401 Unauthorized as Http response, for example:
All request to /server/* must be accessed with access token, then you need to setup filter for those paths and check the access token,
if not redirect the request to an auth endpoint?
No, if access token is not provided or invalid or expired any case, you need to return Unauthorized http response like below,
Status Code:401
{"ok":false,"errors":[{"code":"unauthorized_request","message":"unauthroized request, access token either invalid / expired"}]}
here its json response, but any format works
So while the client make http request to access the endpoint, they need to pass the access token in HTTP Header like below,
Authorization: Bearer {access_token}
Do i have to create just one endpoint that does the token stuff and in my other resource endpoints i only verify if the token is valid?
Yes, you need to create an endpoint like /auth (typically called Auth Endpoints) to handle the authentication process, code exchange, refresh, revocation etc.
Then all other resource endpoints should just check token and process the request, and these endpoints wont take part in token management process