Spring Security SAML Logout - Failing at Saml2LogoutResponseFilter - spring-security

We have a simple Saml2-Logout. The user is logged out in the IdP, but the last step, the callback to allow us to kill the session in the application leads to a Http Status: Not Found.
This is our RelyingPartyRegistrationRepository and other configs related to saml2
import org.opensaml.xmlsec.signature.support.SignatureConstants;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.saml2.core.Saml2X509Credential;
import org.springframework.security.saml2.provider.service.registration.InMemoryRelyingPartyRegistrationRepository;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding;
import org.springframework.security.web.SecurityFilterChain;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import static org.springframework.security.config.Customizer.withDefaults;
#Profile("!local")
#EnableWebSecurity
#Configuration
public class Saml2WebSecurityConfig {
private static final String LOGOUT_CALLBACK_URL = "/logout/saml2/slo";
private final EIAMConfigProperties eiamConfigProperties;
public Saml2WebSecurityConfig(EIAMConfigProperties eiamConfigProperties) {
this.eiamConfigProperties = eiamConfigProperties;
}
#Bean
public RelyingPartyRegistrationRepository relyingPartyRegistrationRepository() throws Exception {
final X509Certificate[] cert = ...;
final PrivateKey privateKey = ...;
final X509Certificate[] verificationCertificate = ...;
Saml2X509Credential signingCredential = Saml2X509Credential.signing(privateKey, cert[0]);
Saml2X509Credential verificationCredential = Saml2X509Credential.verification(verificationCertificate[0]);
RelyingPartyRegistration registration = RelyingPartyRegistration
.withRegistrationId(eiamConfigProperties.getRegistrationId())
.entityId(eiamConfigProperties.getEntity())
.signingX509Credentials(c -> c.add(signingCredential))
.assertingPartyDetails(party -> party.entityId(eiamConfigProperties.getAssertionId())
.singleSignOnServiceLocation(eiamConfigProperties.getSingleSignOnServiceLocation())
.singleSignOnServiceBinding(Saml2MessageBinding.POST)
.singleLogoutServiceLocation(eiamConfigProperties.getSingleLogoutServiceLocation())
.singleLogoutServiceBinding(Saml2MessageBinding.POST)
.wantAuthnRequestsSigned(true)
.signingAlgorithms(sign -> sign.add(SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA256))
.verificationX509Credentials(c -> c.add(verificationCredential))
)
.build();
return new InMemoryRelyingPartyRegistrationRepository(registration);
}
#Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(authorize -> authorize
.anyRequest().authenticated())
.saml2Login(withDefaults())
.saml2Logout((saml2) -> saml2
.logoutRequest((request) -> request.logoutUrl(LOGOUT_CALLBACK_URL))
.logoutResponse((response) -> response.logoutUrl(LOGOUT_CALLBACK_URL))
);
http.csrf().disable();
return http.build();
}
#Bean
public WebSecurityCustomizer webSecurityCustomizer() {
return web -> web.ignoring().requestMatchers(LOGOUT_CALLBACK_URL);
}
}
How do I get Spring Security to expose /logout/saml2/slo and kill the session (or allow me to implement my own session destroy logic).
Edit: With enabled debug-logging I see this:
2023-02-03 14:42:37.845 DEBUG 1 --- [p-nio-80-exec-2] .s.s.p.s.w.a.l.Saml2LogoutResponseFilter : Failed to validate LogoutResponse: [[invalid_destination] Failed to match destination to configured destination] 

I found the solution and will post it for others to help or for future me :)
What was missing was the singleLogoutServiceResponseLocation on RelyingPartyRegistration, which must match the LogoutResponse Destination. Otherwise an exception is thrown by the Saml2LogoutResponseFilter.
#Profile("!local")
#EnableWebSecurity
#Configuration
public class Saml2WebSecurityConfig {
private final EIAMConfigProperties eiamConfigProperties;
public Saml2WebSecurityConfig(EIAMConfigProperties eiamConfigProperties) {
this.eiamConfigProperties = eiamConfigProperties;
}
#Bean
public RelyingPartyRegistrationRepository relyingPartyRegistrationRepository() throws Exception {
final X509Certificate[] cert = ...;
final PrivateKey privateKey = ...;
final X509Certificate[] verificationCertificate = ...;
Saml2X509Credential signingCredential = Saml2X509Credential.signing(privateKey, cert[0]);
Saml2X509Credential verificationCredential = Saml2X509Credential.verification(verificationCertificate[0]);
RelyingPartyRegistration registration = RelyingPartyRegistration
.withRegistrationId(eiamConfigProperties.getRegistrationId())
// this was missing and must match the LogoutResponse Destination
.singleLogoutServiceResponseLocation(eiamConfigProperties.getSingleLogoutServiceResponseLocation())
.entityId(eiamConfigProperties.getEntity())
.signingX509Credentials(c -> c.add(signingCredential))
.assertingPartyDetails(party -> party.entityId(eiamConfigProperties.getAssertionId())
.singleSignOnServiceLocation(eiamConfigProperties.getSingleSignOnServiceLocation())
.singleSignOnServiceBinding(Saml2MessageBinding.POST)
.singleLogoutServiceLocation(eiamConfigProperties.getSingleLogoutServiceLocation())
.singleLogoutServiceBinding(Saml2MessageBinding.POST)
.wantAuthnRequestsSigned(true)
.signingAlgorithms(sign -> sign.add(SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA256))
.verificationX509Credentials(c -> c.add(verificationCredential))
)
.build();
return new InMemoryRelyingPartyRegistrationRepository(registration);
}
#Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(authorize -> authorize
.anyRequest().authenticated())
.saml2Login(withDefaults())
.saml2Logout(withDefaults());
http.csrf().disable();
return http.build();
}
}

Related

My SparkJava resource server gets 403 errors when trying to validate access tokens

I want to set up a very basic REST API using Spark-java, which just checks an access token obtained from my own authorisation server. It creates a GET request to the authorisation server's /oauth/authorize endpoint followed by ?token=$ACCESS_TOKEN.
Whenever I try this, I get diverted to the /error endpoint and a 403 error.
Here's my API class:
import org.apache.http.client.methods.HttpGet;
import org.apache.http.entity.ContentType;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import spark.utils.StringUtils;
import java.io.IOException;
import static spark.Spark.*;
public class SampleAPI {
private static final Logger logger = LoggerFactory.getLogger("SampleAPI");
public static void main(String[] args) {
// Run on port 9782
port(9782);
// Just returns "Hello there" to the client's console
before((preRequest, preResponse) -> {
System.out.println("Getting token from request");
final String authHeader = preRequest.headers("Authorization");
//todo validate token, don't just accept it because it's not null/empty
if(StringUtils.isEmpty(authHeader) || !isAuthenticated(authHeader)){
halt(401, "Access not authorised");
} else {
System.out.println("Token = " + authHeader);
}
});
get("/", (res, req) -> "Hello there");
}
private static boolean isAuthenticated(final String authHeader) {
String url = "http://localhost:9780/oauth/authorize";
//"Bearer " is before the actual token in authHeader, so we need to extract the token itself as a substring
String token = authHeader.substring(7);
HttpGet getAuthRequest = new HttpGet(url + "?token=" + token);
getAuthRequest.setHeader("Content-Type", ContentType.APPLICATION_FORM_URLENCODED.getMimeType());
CloseableHttpClient httpClient = HttpClients.createMinimal();
try {
CloseableHttpResponse response = httpClient.execute(getAuthRequest);
int statusCode = response.getStatusLine().getStatusCode();
System.out.println("Status code " + statusCode + " returned for access token " + authHeader);
return statusCode == 200;
} catch (IOException ioException) {
System.out.println("Exception when trying to validate access token " + ioException);
}
return false;
}
}
The System.out.println statements are just for debugging.
Here's my authorisation server's WebSecurityConfigurerAdapter class:
package main.config;
import main.service.ClientAppDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
#Configuration
#EnableWebSecurity(debug = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private UserDetailsService userDetailsService;
#Override
#Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
//returns AuthenticationManager from the superclass for authenticating users
return super.authenticationManagerBean();
}
#Bean
public PasswordEncoder getPasswordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
#Override
public void configure(WebSecurity web) throws Exception {
//Allow for DB access without any credentials
web.ignoring().antMatchers("/h2-console/**");
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//configures user details, and uses the custom UserDetailsService to check user credentials
auth.userDetailsService(userDetailsService).passwordEncoder(getPasswordEncoder());
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().authenticated();
//disable CORS and CSRF protection for Postman testing
http.cors().disable().anonymous().disable();
http.headers().frameOptions().disable();
http.csrf().disable();
}
}
And here's my authorisation server's application.properties:
server.port=9780
#in-memory database, will get populated using data.sql
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=admin
spring.datasource.password=syst3m
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.jpa.properties.hibernate.format_sql=true
#adds to existing DB instead of tearing it down and re-populating it every time the app is started
spring.jpa.hibernate.ddl-auto=update
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console
spring.h2.console.settings.trace=false
spring.h2.console.settings.web-allow-others=false
What have I done wrong? Do I need to specify my API as a resource server using Spring Security? Do I need to add it to the authorisation server's application.properties?
If you want to use Spring as a security framework then the most common option is to configure it as a resource server. Here is a getting started tutorial. The API will then never get redirected.
With Spark another option is to just provide a basic filter that uses a JWT validation library, such as jose4j. This tends to provide better control over error responses and gives you better visibility over what is going on. See this Kotlin example, which will be easy enough to translate to Java.

Spring fails for userinfo endpoint returning signed JWT

We're working on a Spring Boot application that is an OIDC client. The identity provider (IdP) is a third-party service and fully OpenID Connect and OAuth 2.0 compliant (as far as we can tell). As it's built with high security in mind, its UserInfo endpoint returns a signed JWT (instead of a regular one).
It seems that Spring Security does not support it. The authentication flow ends with an error message (displayed in an HTML page generated by our Spring application):
[invalid_user_info_response] An error occurred while attempting to
retrieve the UserInfo Resource: Could not extract response: no
suitable HttpMessageConverter found for response type
[java.util.Map] and content type
[application/jwt;charset=UTF-8]
My questions:
Is it correct that Spring does not currently support UserInfo endpoints returning signed JWTs?
If so, how can we add support for signed JWTs (including verfication of the signature)?
Our analysis has shown that the DefaultOAuth2UserService requests (Accept: application/json) and expects a JSON response from the IdP. However, being configured for high security, the IdP returns a signed JWT with the content type application/jwt. The response looks like the example on jwt.io. As the RestTemplate has no message converter capable of handling the content type application/jwt, the authenticaton fails.
Our sample app is as simple as it gets:
build.gradle
plugins {
id 'java'
id 'org.springframework.boot' version '2.2.4.RELEASE'
id 'io.spring.dependency-management' version '1.0.9.RELEASE'
}
group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
}
DemoApplication.java
package demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
#SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
application.yml
server:
port: 8081
spring:
security:
oauth2:
client:
registration:
demo:
client-id: our-client-id
client-secret: our-client-secret
clientAuthenticationMethod: post
provider: our-idp
scope:
- profile
- email
provider:
our-idp:
issuer-uri: https://login.idp.com:443/idp/oauth2
HomeController.java
package demo;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
#RestController
public class HomeController {
#GetMapping("/")
String hello() { return "hello"; }
}
After more analysis, it seems Spring Boot does not support UserInfo endpoints returning signed JWTs. It's obviously an unusual setup (but still within the OAuth 2.0 / OIDC specification). What I haven't mentioned so far is that the JWT is signed with the client secret.
While Spring Boot doesn't support it, it can be added. The solution consists of:
A user service supporting signed JWTs (as a replacment for DefaultOAuth2UserService)
A HttpMessageConverter supporting JWTs (used in the user service's RestTemplate)
A JwtDecoder using the client secret
A security configuration that puts the pieces together
Note that we have changed from OAuth 2.0 to OIDC in the mean-time, thus our application.yml now includes the openid scope.
spring:
security:
oauth2:
client:
registration:
demo:
client-id: our-client-id
client-secret: our-client-secret
clientAuthenticationMethod: post
provider: our-idp
scope:
- profile
- email
provider:
our-idp:
issuer-uri: https://login.idp.com:443/idp/oauth2
The security configuration is:
package demoapp;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
#Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final ClientRegistrationRepository clientRegistrationRepository;
public SecurityConfig(ClientRegistrationRepository clientRegistrationRepository) {
this.clientRegistrationRepository = clientRegistrationRepository;
}
#Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.authorizeRequests()
.anyRequest().authenticated()
.and()
.oauth2Login()
.userInfoEndpoint()
.oidcUserService(oidcUserService());
}
#Bean
OidcUserService oidcUserService() {
OidcUserService userService = new OidcUserService();
userService.setOauth2UserService(new ValidatingOAuth2UserService(jwtDecoderUsingClientSecret("demo")));
return userService;
}
JwtDecoder jwtDecoderUsingClientSecret(String registrationId) {
ClientRegistration registration = clientRegistrationRepository.findByRegistrationId(registrationId);
SecretKeySpec key = new SecretKeySpec(registration.getClientSecret().getBytes(StandardCharsets.UTF_8), "HS256");
return NimbusJwtDecoder.withSecretKey(key).build();
}
}
If you are using OAuth 2.0 instead of OIDC (i.e. you don't use the scope 'openid'), the configuration is simpler:
package demo;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
#Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final ClientRegistrationRepository clientRegistrationRepository;
public SecurityConfig(ClientRegistrationRepository clientRegistrationRepository) {
this.clientRegistrationRepository = clientRegistrationRepository;
}
#Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.authorizeRequests()
.anyRequest().authenticated()
.and()
.oauth2Login()
.userInfoEndpoint()
.userService(new ValidatingOAuth2UserService(jwtDecoderUsingClientSecret("demo")));
}
JwtDecoder jwtDecoderUsingClientSecret(String registrationId) {
ClientRegistration registration = clientRegistrationRepository.findByRegistrationId(registrationId);
SecretKeySpec key = new SecretKeySpec(registration.getClientSecret().getBytes(StandardCharsets.UTF_8), "HS256");
return NimbusJwtDecoder.withSecretKey(key).build();
}
}
The ValidatingOAuth2UserService class is - for the most part - a copy of DefaultOAuth2UserService:
/*
* Copyright 2002-2018 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 demo;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import org.springframework.core.convert.converter.Converter;
import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.client.http.OAuth2ErrorResponseErrorHandler;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequestEntityConverter;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2AuthorizationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.security.oauth2.core.user.OAuth2UserAuthority;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.web.client.ResponseErrorHandler;
import org.springframework.web.client.RestClientException;
import org.springframework.web.client.RestOperations;
import org.springframework.web.client.RestTemplate;
/**
* An implementation of an {#link OAuth2UserService} that supports standard OAuth 2.0 Provider's.
* <p>
* This provider supports <i>UserInfo</i> endpoints returning user details
* in signed JWTs (content-type {#code application/jwt}).
* </p>
* <p>
* For standard OAuth 2.0 Provider's, the attribute name used to access the user's name
* from the UserInfo response is required and therefore must be available via
* {#link ClientRegistration.ProviderDetails.UserInfoEndpoint#getUserNameAttributeName() UserInfoEndpoint.getUserNameAttributeName()}.
* <p>
* <b>NOTE:</b> Attribute names are <b>not</b> standardized between providers and therefore will vary.
* Please consult the provider's API documentation for the set of supported user attribute names.
*
* #see org.springframework.security.oauth2.client.userinfo.OAuth2UserService
* #see OAuth2UserRequest
* #see OAuth2User
* #see DefaultOAuth2User
*/
public class ValidatingOAuth2UserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {
private static final String MISSING_USER_INFO_URI_ERROR_CODE = "missing_user_info_uri";
private static final String MISSING_USER_NAME_ATTRIBUTE_ERROR_CODE = "missing_user_name_attribute";
private static final String INVALID_USER_INFO_RESPONSE_ERROR_CODE = "invalid_user_info_response";
private Converter<OAuth2UserRequest, RequestEntity<?>> requestEntityConverter = new OAuth2UserRequestEntityConverter();
private RestOperations restOperations;
private JwtDecoder jwtDecoder;
public ValidatingOAuth2UserService(JwtDecoder jwtDecoder) {
this.jwtDecoder = jwtDecoder;
RestTemplate restTemplate = new RestTemplate();
restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler());
restTemplate.getMessageConverters().add(new JwtHttpMessageConverter());
this.restOperations = restTemplate;
}
#Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
Assert.notNull(userRequest, "userRequest cannot be null");
if (!StringUtils.hasText(userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUri())) {
OAuth2Error oauth2Error = new OAuth2Error(
MISSING_USER_INFO_URI_ERROR_CODE,
"Missing required UserInfo Uri in UserInfoEndpoint for Client Registration: " +
userRequest.getClientRegistration().getRegistrationId(),
null
);
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
}
String userNameAttributeName = userRequest.getClientRegistration().getProviderDetails()
.getUserInfoEndpoint().getUserNameAttributeName();
if (!StringUtils.hasText(userNameAttributeName)) {
OAuth2Error oauth2Error = new OAuth2Error(
MISSING_USER_NAME_ATTRIBUTE_ERROR_CODE,
"Missing required \"user name\" attribute name in UserInfoEndpoint for Client Registration: " +
userRequest.getClientRegistration().getRegistrationId(),
null
);
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
}
RequestEntity<?> request = this.requestEntityConverter.convert(userRequest);
ResponseEntity<String> response;
try {
response = this.restOperations.exchange(request, String.class);
} catch (OAuth2AuthorizationException ex) {
OAuth2Error oauth2Error = ex.getError();
StringBuilder errorDetails = new StringBuilder();
errorDetails.append("Error details: [");
errorDetails.append("UserInfo Uri: ").append(
userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUri());
errorDetails.append(", Error Code: ").append(oauth2Error.getErrorCode());
if (oauth2Error.getDescription() != null) {
errorDetails.append(", Error Description: ").append(oauth2Error.getDescription());
}
errorDetails.append("]");
oauth2Error = new OAuth2Error(INVALID_USER_INFO_RESPONSE_ERROR_CODE,
"An error occurred while attempting to retrieve the UserInfo Resource: " + errorDetails.toString(), null);
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString(), ex);
} catch (RestClientException ex) {
OAuth2Error oauth2Error = new OAuth2Error(INVALID_USER_INFO_RESPONSE_ERROR_CODE,
"An error occurred while attempting to retrieve the UserInfo Resource: " + ex.getMessage(), null);
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString(), ex);
}
Jwt jwt = decodeAndValidateJwt(response.getBody());
Map<String, Object> userAttributes = jwt.getClaims();
Set<GrantedAuthority> authorities = new LinkedHashSet<>();
authorities.add(new OAuth2UserAuthority(userAttributes));
OAuth2AccessToken token = userRequest.getAccessToken();
for (String authority : token.getScopes()) {
authorities.add(new SimpleGrantedAuthority("SCOPE_" + authority));
}
return new DefaultOAuth2User(authorities, userAttributes, userNameAttributeName);
}
private Jwt decodeAndValidateJwt(String token) {
return jwtDecoder.decode(token);
}
/**
* Sets the {#link Converter} used for converting the {#link OAuth2UserRequest}
* to a {#link RequestEntity} representation of the UserInfo Request.
*
* #since 5.1
* #param requestEntityConverter the {#link Converter} used for converting to a {#link RequestEntity} representation of the UserInfo Request
*/
public final void setRequestEntityConverter(Converter<OAuth2UserRequest, RequestEntity<?>> requestEntityConverter) {
Assert.notNull(requestEntityConverter, "requestEntityConverter cannot be null");
this.requestEntityConverter = requestEntityConverter;
}
/**
* Sets the {#link RestOperations} used when requesting the UserInfo resource.
*
* <p>
* <b>NOTE:</b> At a minimum, the supplied {#code restOperations} must be configured with the following:
* <ol>
* <li>{#link ResponseErrorHandler} - {#link OAuth2ErrorResponseErrorHandler}</li>
* </ol>
*
* #since 5.1
* #param restOperations the {#link RestOperations} used when requesting the UserInfo resource
*/
public final void setRestOperations(RestOperations restOperations) {
Assert.notNull(restOperations, "restOperations cannot be null");
this.restOperations = restOperations;
}
}
And finally the JwtHttpMessageConverter class:
package demo;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.AbstractGenericHttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
/**
* Message converter for reading JWTs transmitted with content type {#code application/jwt}.
* <p>
* The JWT is returned as a string and not validated.
* </p>
*/
public class JwtHttpMessageConverter extends AbstractGenericHttpMessageConverter<String> {
public JwtHttpMessageConverter() {
super(MediaType.valueOf("application/jwt"));
}
#Override
protected String readInternal(Class<? extends String> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
return getBodyAsString(inputMessage.getBody());
}
#Override
public String read(Type type, Class<?> contextClass, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
return readInternal(null, inputMessage);
}
private String getBodyAsString(InputStream bodyStream) throws IOException {
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
byte[] chunk = new byte[64];
int len;
while ((len = bodyStream.read(chunk)) != -1) {
buffer.write(chunk, 0, len);
}
return buffer.toString(StandardCharsets.US_ASCII);
}
#Override
protected void writeInternal(String stringObjectMap, Type type, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
throw new UnsupportedOperationException();
}
}
Thanks #Codo. You saved my day. I did not have to use JwtHttpMessageConverter class though. I added the below in SecurityConfig class. I had to use JwksUri and SignatureAlgorithm.RS512 in my situation.
#Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.
authorizeRequests().
anyRequest().authenticated().
and().
oauth2Login().userInfoEndpoint().oidcUserService(oidcUserService());
return http.build();
}
OidcUserService oidcUserService() {
logger.info("OidcUserService bean");
OidcUserService userService = new OidcUserService();
//userService.setOauth2UserService(new ExampleOAuth2UserService(jwtDecoderUsingClientSecret("web-client")));
userService.setOauth2UserService(new ExampleOAuth2UserService(jwtDecoder()));
return userService;
}
public JwtDecoder jwtDecoder() {
String jwksUri = Config.getJwksUri();
System.out.println("jwtDecoder jwksUri="+jwksUri);
return NimbusJwtDecoder.withJwkSetUri(jwksUri).jwsAlgorithm(SignatureAlgorithm.RS512).build();
}
public class ExampleOAuth2UserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {
...............
...............
private JwtDecoder jwtDecoder;
public ExampleAuth2UserService(JwtDecoder jwtDecoder) {
this.jwtDecoder = jwtDecoder;
RestTemplate restTemplate = new RestTemplate();
restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler());
this.restOperations = restTemplate;
}

Connecting to a remote RabbitMQ server using letsencrypt ssl

I have this configuration to be able to connect to compose.io rabbitmq service
package com.gandalf.configuration;
import org.springframework.amqp.rabbit.connection.CachingConnectionFactory;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory;
import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer;
import org.springframework.amqp.rabbit.listener.adapter.MessageListenerAdapter;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.rabbit.core.RabbitAdmin;
import org.springframework.amqp.core.AmqpAdmin;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.rabbit.connection.RabbitConnectionFactoryBean;
#Configuration
public class SpringAmqpConfiguration {
#Bean
public ConnectionFactory connectionFactory() {
CachingConnectionFactory connectionFactory = new CachingConnectionFactory();
connectionFactory.setAddresses("portal274-33.***,....");
connectionFactory.setUsername("amqpuser");
connectionFactory.setPassword("muggledone");
connectionFactory.setUseSsl(true);
return connectionFactory;
}
#Bean
public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory() {
SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
factory.setConnectionFactory(connectionFactory());
factory.setConcurrentConsumers(3);
//If you want a fixed number of consumers, omit the max.
factory.setMaxConcurrentConsumers(10);
return factory;
}
#Bean
public RabbitTemplate rabbitTemplate() {
RabbitTemplate template = new RabbitTemplate(connectionFactory());
return template;
}
}
However, upon running the program, i get an error in this line
connectionFactory.setUseSsl(true);
This is the error i get
Compilation failure
/SpringAmqpConfiguration.java:[24,26] cannot find symbol
[ERROR] symbol: method setUseSsl(boolean)
[ERROR] location: variable connectionFactory of type org.springframework.amqp.rabbit.connection.CachingConnectionFactory
Why is connectionFactory.setUseSsl(true); not being found?.
There is no property useSsl on the CachingConnectionFactory; there is a useSslProtocol property on the underlying com.rabbitmq.client.ConnectionFactory.
Use connectionFactory.getRabbitConnectionFactory().useSslProtocol().

Oauth2 bad credentials Spring Boot

I am getting the error:
"error": "invalid_grant",
"error_description": "Bad credentials"
Here is the request which I make:
POST /oauth/token HTTP/1.1
Host: localhost:8443
Authorization: Basic bW9iaWxlOg==
Cache-Control: no-cache
Content-Type: application/x-www-form-urlencoded
username=admin&password=pass&client_id=mobile&grant_type=password&client_secret=
My code is from here: https://github.com/juleswhite/mobilecloud-14/tree/master/examples/9-VideoServiceWithOauth2
Here is the code:
Application.java:
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.data.rest.webmvc.config.RepositoryRestMvcConfiguration;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import com.capstone.auth.OAuth2SecurityConfiguration;
import com.google.common.io.BaseEncoding;
#Configuration
#EnableAutoConfiguration
#ComponentScan
#EnableWebMvc
#Import(OAuth2SecurityConfiguration.class)
public class Application extends RepositoryRestMvcConfiguration{
public static void main(String[] args) {
ApplicationContext ctx = SpringApplication.run(Application.class, args);
}
}
ClientAndUserDetailsService.java
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.oauth2.provider.ClientDetails;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.ClientRegistrationException;
import org.springframework.security.oauth2.provider.client.ClientDetailsUserDetailsService;
/**
* A class that combines a UserDetailsService and ClientDetailsService
* into a single object.
*
* #author jules
*
*/
public class ClientAndUserDetailsService implements UserDetailsService,
ClientDetailsService {
private final ClientDetailsService clients_;
private final UserDetailsService users_;
private final ClientDetailsUserDetailsService clientDetailsWrapper_;
public ClientAndUserDetailsService(ClientDetailsService clients,
UserDetailsService users) {
super();
clients_ = clients;
users_ = users;
clientDetailsWrapper_ = new ClientDetailsUserDetailsService(clients_);
}
#Override
public ClientDetails loadClientByClientId(String clientId)
throws ClientRegistrationException {
return clients_.loadClientByClientId(clientId);
}
#Override
public UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException {
UserDetails user = null;
try{
user = users_.loadUserByUsername(username);
}catch(UsernameNotFoundException e){
user = clientDetailsWrapper_.loadUserByUsername(username);
}
return user;
}
}
OAuth2SecurityConfiguration.java
#Configuration
public class OAuth2SecurityConfiguration {
// This first section of the configuration just makes sure that Spring
// Security picks
// up the UserDetailsService that we create below.
#Configuration
#EnableWebSecurity
protected static class WebSecurityConfiguration extends
WebSecurityConfigurerAdapter {
#Autowired
private UserDetailsService userDetailsService;
#Autowired
protected void registerAuthentication(
final AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService);
}
}
/**
* This method is used to configure who is allowed to access which parts of
* our resource server (i.e. the "/video" endpoint)
*/
#Configuration
#EnableResourceServer
protected static class ResourceServer extends
ResourceServerConfigurerAdapter {
// This method configures the OAuth scopes required by clients to access
// all of the paths in the video service.
#Override
public void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
http.authorizeRequests().antMatchers("/oauth/token").anonymous();
// If you were going to reuse this class in another
// application, this is one of the key sections that you
// would want to change
// Require all GET requests to have client "read" scope
http.authorizeRequests().antMatchers(HttpMethod.GET, "/**")
.access("#oauth2.hasScope('read')");
// Require all other requests to have "write" scope
http.authorizeRequests().antMatchers("/**")
.access("#oauth2.hasScope('write')");
}
}
/**
* This class is used to configure how our authorization server (the
* "/oauth/token" endpoint) validates client credentials.
*/
#Configuration
#EnableAuthorizationServer
#Order(Ordered.LOWEST_PRECEDENCE - 100)
protected static class OAuth2Config extends
AuthorizationServerConfigurerAdapter {
// Delegate the processing of Authentication requests to the framework
#Autowired
private AuthenticationManager authenticationManager;
// A data structure used to store both a ClientDetailsService and a
// UserDetailsService
private ClientAndUserDetailsService combinedService_;
/**
*
* This constructor is used to setup the clients and users that will be
* able to login to the system. This is a VERY insecure setup that is
* using hard-coded lists of clients / users / passwords and should
* never be used for anything other than local testing on a machine that
* is not accessible via the Internet. Even if you use this code for
* testing, at the bare minimum, you should consider changing the
* passwords listed below and updating the VideoSvcClientApiTest.
*
* #param auth
* #throws Exception
*/
public OAuth2Config() throws Exception {
// If you were going to reuse this class in another
// application, this is one of the key sections that you
// would want to change
// Create a service that has the credentials for all our clients
ClientDetailsService csvc = new InMemoryClientDetailsServiceBuilder()
// Create a client that has "read" and "write" access to the
// video service
.withClient("mobile")
.authorizedGrantTypes("password")
.authorities("ROLE_CLIENT", "ROLE_TRUSTED_CLIENT")
.scopes("read", "write")
.resourceIds("test")
.and()
// Create a second client that only has "read" access to the
// video service
.withClient("mobileReader")
.authorizedGrantTypes("password")
.authorities("ROLE_CLIENT").scopes("read")
.resourceIds("test").accessTokenValiditySeconds(3600)
.and().build();
// Create a series of hard-coded users.
UserDetailsService svc = new InMemoryUserDetailsManager(
Arrays.asList(
User.create("admin", "pass", "ADMIN", "USER"),
User.create("user0", "pass", "USER"),
User.create("username", "password", "USER")));
// Since clients have to use BASIC authentication with the client's
// id/secret,
// when sending a request for a password grant, we make each client
// a user
// as well. When the BASIC authentication information is pulled from
// the
// request, this combined UserDetailsService will authenticate that
// the
// client is a valid "user".
combinedService_ = new ClientAndUserDetailsService(csvc, svc);
}
/**
* Return the list of trusted client information to anyone who asks for
* it.
*/
#Bean
public ClientDetailsService clientDetailsService() throws Exception {
return combinedService_;
}
/**
* Return all of our user information to anyone in the framework who
* requests it.
*/
#Bean
public UserDetailsService userDetailsService() {
return combinedService_;
}
/**
* This method tells our AuthorizationServerConfigurerAdapter to use the
* delegated AuthenticationManager to process authentication requests.
*/
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints)
throws Exception {
endpoints.authenticationManager(authenticationManager);
}
/**
* This method tells the AuthorizationServerConfigurerAdapter to use our
* self-defined client details service to authenticate clients with.
*/
#Override
public void configure(ClientDetailsServiceConfigurer clients)
throws Exception {
clients.withClientDetails(clientDetailsService());
}
}
// This version uses the Tomcat web container and configures it to
// support HTTPS. The code below performs the configuration of Tomcat
// for HTTPS. Each web container has a different API for configuring
// HTTPS.
//
// The app now requires that you pass the location of the keystore and
// the password for your private key that you would like to setup HTTPS
// with. In Eclipse, you can set these options by going to:
// 1. Run->Run Configurations
// 2. Under Java Applications, select your run configuration for this app
// 3. Open the Arguments tab
// 4. In VM Arguments, provide the following information to use the
// default keystore provided with the sample code:
//
// -Dkeystore.file=src/main/resources/private/keystore
// -Dkeystore.pass=changeit
//
// 5. Note, this keystore is highly insecure! If you want more securtiy, you
// should obtain a real SSL certificate:
//
// http://tomcat.apache.org/tomcat-7.0-doc/ssl-howto.html
//
#Bean
EmbeddedServletContainerCustomizer containerCustomizer(
#Value("${keystore.file:src/main/resources/private/keystore}") String keystoreFile,
#Value("${keystore.pass:changeit}") final String keystorePass)
throws Exception {
// If you were going to reuse this class in another
// application, this is one of the key sections that you
// would want to change
final String absoluteKeystoreFile = new File(keystoreFile)
.getAbsolutePath();
return new EmbeddedServletContainerCustomizer() {
#Override
public void customize(ConfigurableEmbeddedServletContainer container) {
TomcatEmbeddedServletContainerFactory tomcat = (TomcatEmbeddedServletContainerFactory) container;
tomcat.addConnectorCustomizers(new TomcatConnectorCustomizer() {
#Override
public void customize(Connector connector) {
connector.setPort(8443);
connector.setSecure(true);
connector.setScheme("https");
Http11NioProtocol proto = (Http11NioProtocol) connector
.getProtocolHandler();
proto.setSSLEnabled(true);
proto.setKeystoreFile(absoluteKeystoreFile);
proto.setKeystorePass(keystorePass);
proto.setKeystoreType("JKS");
proto.setKeyAlias("tomcat");
}
});
}
};
}
}
Thanks for your attention and time!
Short answer: you can't #Autowired an AuthenticationManager into a AuthorizationServerConfigurerAdapter if you are using Spring Boot (yet).
Long anwswer: this sample works though because it autowires the AuthenticationManagerBuilder instead, and constructs a lazy-init version of the AuthenticationManager for the token granter to use. With Spring OAuth2 2.0.3 you will have to create that lazy AuthenticationManager yourself (like this:
authenticationManager = new AuthenticationManager() {
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
return auth.getOrBuild().authenticate(authentication);
}
};
With snapshots (or 2.0.4 when it is released) you can just use the new overloaded method in AuthorizationServerConfigurerAdapter.

How can I test HMAC authentication using Dropwizard?

I'm just getting started with Dropwizard 0.4.0, and I would like some help with HMAC authentication. Has anybody got any advice?
Thank you in advance.
At present Dropwizard doesn't support HMAC authentication right out of the box, so you'd have to write your own authenticator. A typical choice for HMAC authentication is to use the HTTP Authorization header. The following code expects this header in the following format:
Authorization: <algorithm> <apiKey> <digest>
An example would be
Authorization: HmacSHA1 abcd-efgh-1234 sdafkljlkansdaflk2354jlkj5345345dflkmsdf
The digest is built from the content of the body (marshalled entity) prior to URL encoding with the HMAC shared secret appended as base64. For a non-body request, such as GET or HEAD, the content is taken as the complete URI path and parameters with the secret key appended.
To implement this in a way that Dropwizard can work with it requires you to copy the BasicAuthenticator code present in the dropwizard-auth module into your own code and modify it with something like this:
import com.google.common.base.Optional;
import com.sun.jersey.api.core.HttpContext;
import com.sun.jersey.server.impl.inject.AbstractHttpContextInjectable;
import com.yammer.dropwizard.auth.AuthenticationException;
import com.yammer.dropwizard.auth.Authenticator;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
class HmacAuthInjectable<T> extends AbstractHttpContextInjectable<T> {
private static final String PREFIX = "HmacSHA1";
private static final String HEADER_VALUE = PREFIX + " realm=\"%s\"";
private final Authenticator<HmacCredentials, T> authenticator;
private final String realm;
private final boolean required;
HmacAuthInjectable(Authenticator<HmacCredentials, T> authenticator, String realm, boolean required) {
this.authenticator = authenticator;
this.realm = realm;
this.required = required;
}
public Authenticator<HmacCredentials, T> getAuthenticator() {
return authenticator;
}
public String getRealm() {
return realm;
}
public boolean isRequired() {
return required;
}
#Override
public T getValue(HttpContext c) {
try {
final String header = c.getRequest().getHeaderValue(HttpHeaders.AUTHORIZATION);
if (header != null) {
final String[] authTokens = header.split(" ");
if (authTokens.length != 3) {
// Malformed
HmacAuthProvider.LOG.debug("Error decoding credentials (length is {})", authTokens.length);
throw new WebApplicationException(Response.Status.BAD_REQUEST);
}
final String algorithm = authTokens[0];
final String apiKey = authTokens[1];
final String signature = authTokens[2];
final String contents;
// Determine which part of the request will be used for the content
final String method = c.getRequest().getMethod().toUpperCase();
if ("GET".equals(method) ||
"HEAD".equals(method) ||
"DELETE".equals(method)) {
// No entity so use the URI
contents = c.getRequest().getRequestUri().toString();
} else {
// Potentially have an entity (even in OPTIONS) so use that
contents = c.getRequest().getEntity(String.class);
}
final HmacCredentials credentials = new HmacCredentials(algorithm, apiKey, signature, contents);
final Optional<T> result = authenticator.authenticate(credentials);
if (result.isPresent()) {
return result.get();
}
}
} catch (IllegalArgumentException e) {
HmacAuthProvider.LOG.debug(e, "Error decoding credentials");
} catch (AuthenticationException e) {
HmacAuthProvider.LOG.warn(e, "Error authenticating credentials");
throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR);
}
if (required) {
throw new WebApplicationException(Response.status(Response.Status.UNAUTHORIZED)
.header(HttpHeaders.AUTHORIZATION,
String.format(HEADER_VALUE, realm))
.entity("Credentials are required to access this resource.")
.type(MediaType.TEXT_PLAIN_TYPE)
.build());
}
return null;
}
}
The above is not perfect, but it'll get you started. You may want to refer to the MultiBit Merchant release candidate source code (MIT license) for a more up to date version and the various supporting classes.
The next step is to integrate the authentication process into your ResourceTest subclass. Unfortunately, Dropwizard doesn't provide a good entry point for authentication providers in v0.4.0, so you may want to introduce your own base class, similar to this:
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.sun.jersey.api.client.Client;
import com.sun.jersey.test.framework.AppDescriptor;
import com.sun.jersey.test.framework.JerseyTest;
import com.sun.jersey.test.framework.LowLevelAppDescriptor;
import com.xeiam.xchange.utils.CryptoUtils;
import com.yammer.dropwizard.bundles.JavaBundle;
import com.yammer.dropwizard.jersey.DropwizardResourceConfig;
import com.yammer.dropwizard.jersey.JacksonMessageBodyProvider;
import com.yammer.dropwizard.json.Json;
import org.codehaus.jackson.map.Module;
import org.junit.After;
import org.junit.Before;
import org.multibit.mbm.auth.hmac.HmacAuthProvider;
import org.multibit.mbm.auth.hmac.HmacAuthenticator;
import org.multibit.mbm.persistence.dao.UserDao;
import org.multibit.mbm.persistence.dto.User;
import org.multibit.mbm.persistence.dto.UserBuilder;
import java.io.UnsupportedEncodingException;
import java.security.GeneralSecurityException;
import java.util.List;
import java.util.Set;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
/**
* A base test class for testing Dropwizard resources.
*/
public abstract class BaseResourceTest {
private final Set<Object> singletons = Sets.newHashSet();
private final Set<Object> providers = Sets.newHashSet();
private final List<Module> modules = Lists.newArrayList();
private JerseyTest test;
protected abstract void setUpResources() throws Exception;
protected void addResource(Object resource) {
singletons.add(resource);
}
public void addProvider(Object provider) {
providers.add(provider);
}
protected void addJacksonModule(Module module) {
modules.add(module);
}
protected Json getJson() {
return new Json();
}
protected Client client() {
return test.client();
}
#Before
public void setUpJersey() throws Exception {
setUpResources();
this.test = new JerseyTest() {
#Override
protected AppDescriptor configure() {
final DropwizardResourceConfig config = new DropwizardResourceConfig();
for (Object provider : JavaBundle.DEFAULT_PROVIDERS) { // sorry, Scala folks
config.getSingletons().add(provider);
}
for (Object provider : providers) {
config.getSingletons().add(provider);
}
Json json = getJson();
for (Module module : modules) {
json.registerModule(module);
}
config.getSingletons().add(new JacksonMessageBodyProvider(json));
config.getSingletons().addAll(singletons);
return new LowLevelAppDescriptor.Builder(config).build();
}
};
test.setUp();
}
#After
public void tearDownJersey() throws Exception {
if (test != null) {
test.tearDown();
}
}
/**
* #param contents The content to sign with the default HMAC process (POST body, GET resource path)
* #return
*/
protected String buildHmacAuthorization(String contents, String apiKey, String secretKey) throws UnsupportedEncodingException, GeneralSecurityException {
return String.format("HmacSHA1 %s %s",apiKey, CryptoUtils.computeSignature("HmacSHA1", contents, secretKey));
}
protected void setUpAuthenticator() {
User user = UserBuilder
.getInstance()
.setUUID("abc123")
.setSecretKey("def456")
.build();
//
UserDao userDao = mock(UserDao.class);
when(userDao.getUserByUUID("abc123")).thenReturn(user);
HmacAuthenticator authenticator = new HmacAuthenticator();
authenticator.setUserDao(userDao);
addProvider(new HmacAuthProvider<User>(authenticator, "REST"));
}
}
Again, the above code is not perfect, but the idea is to allow a mocked up UserDao to provide a standard user with a known shared secret key. You'd have to introduce your own UserBuilder implementation for testing purposes.
Finally, with the above code a Dropwizard Resource that had an endpoint like this:
import com.google.common.base.Optional;
import com.yammer.dropwizard.auth.Auth;
import com.yammer.metrics.annotation.Timed;
import org.multibit.mbm.core.Saying;
import org.multibit.mbm.persistence.dto.User;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;
import java.util.concurrent.atomic.AtomicLong;
#Path("/")
#Produces(MediaType.APPLICATION_JSON)
public class HelloWorldResource {
private final String template;
private final String defaultName;
private final AtomicLong counter;
public HelloWorldResource(String template, String defaultName) {
this.template = template;
this.defaultName = defaultName;
this.counter = new AtomicLong();
}
#GET
#Timed
#Path("/hello-world")
public Saying sayHello(#QueryParam("name") Optional<String> name) {
return new Saying(counter.incrementAndGet(),
String.format(template, name.or(defaultName)));
}
#GET
#Timed
#Path("/secret")
public Saying saySecuredHello(#Auth User user) {
return new Saying(counter.incrementAndGet(),
"You cracked the code!");
}
}
could be tested with a unit test that was configured like this:
import org.junit.Test;
import org.multibit.mbm.core.Saying;
import org.multibit.mbm.test.BaseResourceTest;
import javax.ws.rs.core.HttpHeaders;
import static org.junit.Assert.assertEquals;
public class HelloWorldResourceTest extends BaseResourceTest {
#Override
protected void setUpResources() {
addResource(new HelloWorldResource("Hello, %s!","Stranger"));
setUpAuthenticator();
}
#Test
public void simpleResourceTest() throws Exception {
Saying expectedSaying = new Saying(1,"Hello, Stranger!");
Saying actualSaying = client()
.resource("/hello-world")
.get(Saying.class);
assertEquals("GET hello-world returns a default",expectedSaying.getContent(),actualSaying.getContent());
}
#Test
public void hmacResourceTest() throws Exception {
String authorization = buildHmacAuthorization("/secret", "abc123", "def456");
Saying actual = client()
.resource("/secret")
.header(HttpHeaders.AUTHORIZATION, authorization)
.get(Saying.class);
assertEquals("GET secret returns unauthorized","You cracked the code!", actual.getContent());
}
}
Hope this helps you get started.

Resources