I'm building app on spring webflux, and i'm stuck because spring security webflux (v.M5) did not behave like Spring 4 in term of exception handling.
I saw following post about how to customise spring security webflux:
Spring webflux custom authentication for API
If we throw exception let say in ServerSecurityContextRepository.load, Spring will update http header to 500 and nothing i can do to manipulate this exception.
However, any error thrown in controller can be handled using regular #ControllerAdvice, it just spring webflux security.
Is there anyway to handle exception in spring webflux security?
The solution I found is creating a component implementing ErrorWebExceptionHandler. The instances of ErrorWebExceptionHandler bean run before Spring Security filters. Here's a sample that I use:
#Slf4j
#Component
public class GlobalExceptionHandler implements ErrorWebExceptionHandler {
#Autowired
private DataBufferWriter bufferWriter;
#Override
public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {
HttpStatus status = HttpStatus.INTERNAL_SERVER_ERROR;
AppError appError = ErrorCode.GENERIC.toAppError();
if (ex instanceof AppException) {
AppException ae = (AppException) ex;
status = ae.getStatusCode();
appError = new AppError(ae.getCode(), ae.getText());
log.debug(appError.toString());
} else {
log.error(ex.getMessage(), ex);
}
if (exchange.getResponse().isCommitted()) {
return Mono.error(ex);
}
exchange.getResponse().setStatusCode(status);
return bufferWriter.write(exchange.getResponse(), appError);
}
}
If you're injecting the HttpHandler instead, then it's a bit different but the idea is the same.
UPDATE: For completeness, here's my DataBufferWriter object, which is a #Component:
#Component
#RequiredArgsConstructor(onConstructor = #__(#Autowired))
#Slf4j
public class DataBufferWriter {
private final ObjectMapper objectMapper;
public <T> Mono<Void> write(ServerHttpResponse httpResponse, T object) {
return httpResponse
.writeWith(Mono.fromSupplier(() -> {
DataBufferFactory bufferFactory = httpResponse.bufferFactory();
try {
return bufferFactory.wrap(objectMapper.writeValueAsBytes(object));
} catch (Exception ex) {
log.warn("Error writing response", ex);
return bufferFactory.wrap(new byte[0]);
}
}));
}
}
There is no need to register any bean and change default Spring behavior. Try more elegant solution instead:
We have:
The custom implementation of the ServerSecurityContextRepository
The method .load return Mono
public class HttpRequestHeaderSecurityContextRepository implements ServerSecurityContextRepository {
....
#Override
public Mono<SecurityContext> load(ServerWebExchange exchange) {
List<String> tokens = exchange.getRequest().getHeaders().get("X-Auth-Token");
String token = (tokens != null && !tokens.isEmpty()) ? tokens.get(0) : null;
Mono<Authentication> authMono = reactiveAuthenticationManager
.authenticate( new HttpRequestHeaderToken(token) );
return authMono
.map( auth -> (SecurityContext)new SecurityContextImpl(auth))
}
}
The problem is: if the authMono will contains an error instead of Authentication - spring will return the http response with 500 status (which means "an unknown internal error") instead of 401. Even the error is AuthenticationException or it's subclass - it makes no sense - Spring will return 500.
But it is clear for us: an AuthenticationException should produce the 401 error...
To solve the problem we have to help Spring how to convert an Exception into the HTTP response status code.
To make it we have can just use the appropriate Exception class: ResponseStatusException or just map an original exception to this one (for instance, by adding the onErrorMap() to the authMono object). See the final code:
public class HttpRequestHeaderSecurityContextRepository implements ServerSecurityContextRepository {
....
#Override
public Mono<SecurityContext> load(ServerWebExchange exchange) {
List<String> tokens = exchange.getRequest().getHeaders().get("X-Auth-Token");
String token = (tokens != null && !tokens.isEmpty()) ? tokens.get(0) : null;
Mono<Authentication> authMono = reactiveAuthenticationManager
.authenticate( new HttpRequestHeaderToken(token) );
return authMono
.map( auth -> (SecurityContext)new SecurityContextImpl(auth))
.onErrorMap(
er -> er instanceof AuthenticationException,
autEx -> new ResponseStatusException(HttpStatus.UNAUTHORIZED, autEx.getMessage(), autEx)
)
;
)
}
}
I just went trough lots of documentation, having a similar problem.
My solution was using ResponseStatusException. AccessException of Spring-security seems to be understood.
.doOnError(
t -> AccessDeniedException.class.isAssignableFrom(t.getClass()),
t -> AUDIT.error("Error {} {}, tried to access {}", t.getMessage(), principal, exchange.getRequest().getURI())) // if an error happens in the stream, show its message
.onErrorMap(
SomeOtherException.class,
t -> { return new ResponseStatusException(HttpStatus.NOT_FOUND, "Collection not found");})
;
If this goes in the right direction for you, I can provide a bit better sample.
Related
I have an application that connects to a SAML idP that only supports the POST Binding. After configuring my application which uses spring-security-saml2-service-provider to manually create a POST Authentication request, I looked at the XML that got generated and saw that it included the Signature information (which is expected) but not the Key Info. Then in the logs, I noticed it said:
No KeyInfoGenerator was supplied in parameters or resolveable for credential type org.opensaml.security.x509.X509Credential, No KeyInfo will be generated for Signature
This is what my code looks like to manually generate the POST Authentication request:
#Override
protected void configure(HttpSecurity http) throws Exception {
http.httpBasic()
.disable()
.csrf()
.disable();
}
#Bean
public RelyingPartyRegistration nnanetRelyingPartyRegistration() {
SAMLMetadataSignatureSigningParametersResolver resolver = new SAMLMetadataSignatureSigningParametersResolver();
return RelyingPartyRegistrations
.fromMetadataLocation("https://example.com/metadata.xml")
.entityId("example")
.registrationId("nnanet")
.assertingPartyDetails(party -> {
party.wantAuthnRequestsSigned(true)
.singleSignOnServiceLocation("https://example.com/login")
.entityId("https://example.com/login");
.verificationX509Credentials(saml2X509Credentials -> {
saml2X509Credentials.add(getVerificationCertificate());
});
})
.signingX509Credentials(saml2X509Credentials -> {
saml2X509Credentials.add(getSigningCredential());
})
.decryptionX509Credentials(saml2X509Credentials -> {
saml2X509Credentials.add(getSigningCredential());
})
.build();
}
#Bean
public RelyingPartyRegistrationRepository relyingPartyRegistrationRepository() {
return new InMemoryRelyingPartyRegistrationRepository(relyingPartyRegistration());
}
#Bean
public Saml2PostAuthenticationRequest saml2PostAuthenticationRequest() {
Saml2AuthenticationRequestContext.Builder contextBuilder = Saml2AuthenticationRequestContext.builder();
contextBuilder.assertionConsumerServiceUrl("http://localhost:8080/saml/SSO");
contextBuilder.relyingPartyRegistration(relyingPartyRegistration());
contextBuilder.issuer("issuer");
OpenSamlAuthenticationRequestFactory factory = new OpenSamlAuthenticationRequestFactory();
return factory.createPostAuthenticationRequest(contextBuilder.build());
}
I just call the saml2PostAuthenticationRequest() method from my Controller and generate a form to submit automatically due to some reasons that are outside the scope of this question. After looking further into the OpenSamlAuthenticationRequestFactory, it looks like this is creating the SignatureSigningParameters specifically without including the KeyInfoGenerator as it's only being created in the private methods. Does anyone have an idea on how to get around this, or perhaps point out if I'm doing something wrong?
Thanks!
This will be addressed in a future version of Spring Security SAML 2
Please review the PR - https://github.com/spring-projects/spring-security/pull/9746
I am trying to add interceptors for securing spring-ws by reading this tutorial at https://memorynotfound.com/spring-ws-certificate-authentication-wss4j/
I need to use two seperate public-private keys (one for signing,second for encryption) in a single keystore(server.jks- file).But i am not able to configure the security interceptor.
It works fine as in example if use a single keystore , but how should i set the following when seperate keys for signing and encryption
#Bean
public KeyStoreCallbackHandler securityCallbackHandler(){
KeyStoreCallbackHandler callbackHandler = new KeyStoreCallbackHandler();
callbackHandler.setPrivateKeyPassword("changeit");
return callbackHandler;
}
#Bean
public Wss4jSecurityInterceptor securityInterceptor() throws Exception {
Wss4jSecurityInterceptor securityInterceptor = new Wss4jSecurityInterceptor();
// validate incoming request
securityInterceptor.setValidationActions("Timestamp Signature Encrypt");
securityInterceptor.setValidationSignatureCrypto(getCryptoFactoryBean().getObject());
securityInterceptor.setValidationDecryptionCrypto(getCryptoFactoryBean().getObject());
securityInterceptor.setValidationCallbackHandler(securityCallbackHandler());
// encrypt the response
securityInterceptor.setSecurementEncryptionUser("client-public");
securityInterceptor.setSecurementEncryptionParts("{Content}{https://memorynotfound.com/beer}getBeerResponse");
securityInterceptor.setSecurementEncryptionCrypto(getCryptoFactoryBean().getObject());
// sign the response
securityInterceptor.setSecurementActions("Signature Encrypt");
securityInterceptor.setSecurementUsername("server");
securityInterceptor.setSecurementPassword("changeit");
securityInterceptor.setSecurementSignatureCrypto(getCryptoFactoryBean().getObject());
return securityInterceptor;
}
#Bean
public CryptoFactoryBean getCryptoFactoryBean() throws IOException {
CryptoFactoryBean cryptoFactoryBean = new CryptoFactoryBean();
cryptoFactoryBean.setKeyStorePassword("changeit");
cryptoFactoryBean.setKeyStoreLocation(new ClassPathResource("server.jks"));
return cryptoFactoryBean;
}
For encryption we have the method setSecurementEncryptionUser, but how do we configure setValidationDecryptionCrypto and setValidationSignatureCrypto with the alias to decrypt/validate
Could you try having 2 securityInterceptor with 2 keystores? One for signature and one for encryption. Then add both interceptors to the list of interceptors.
#Override
public void addInterceptors(List<EndpointInterceptor> interceptors) {
try {
interceptors.add(signatureSecurityInterceptor());
interceptors.add(encryptionSecurityInterceptor());
} catch (Exception e) {
throw new RuntimeException("could not initialize security interceptor");
}
}
PROBLEM: I am not getting Spring Security with Websockets to work in a Webflux project.
NOTE: I am using Kotlin instead of Java.
DEPENDENCIES:
Spring Boot 2.0.0
Spring Security 5.0.3
Spring WebFlux 5.0.4
IMPORTANT UPDATE: I have raised a Spring Issue bug (March 30) here and one of the Spring security maintainers said its NOT SUPPORTED but they can add it for Spring Security 5.1.0 M2.
LINK: Add WebFlux WebSocket Support #5188
Webflux Security Configuration
#EnableWebFluxSecurity
class SecurityConfig
{
#Bean
fun configure(http: ServerHttpSecurity): SecurityWebFilterChain
{
return http.authorizeExchange()
.pathMatchers("/").permitAll()
.anyExchange().authenticated()
.and().httpBasic()
.and().formLogin().disable().csrf().disable()
.build()
}
#Bean
fun userDetailsService(): MapReactiveUserDetailsService
{
val user = User.withDefaultPasswordEncoder()
.username("user")
.password("pass")
.roles("USER")
.build()
return MapReactiveUserDetailsService(user)
}
}
Webflux Websocket Configuration
#Configuration
class ReactiveWebSocketConfiguration
{
#Bean
fun webSocketMapping(handler: WebSocketHandler): HandlerMapping
{
val map = mapOf(Pair("/event", handler))
val mapping = SimpleUrlHandlerMapping()
mapping.order = -1
mapping.urlMap = map
return mapping
}
#Bean
fun handlerAdapter() = WebSocketHandlerAdapter()
#Bean
fun websocketHandler() = WebSocketHandler { session ->
// Should print authenticated principal BUT does show NULL
println("${session.handshakeInfo.principal.block()}")
// Just for testing we send hello world to the client
session.send(Mono.just(session.textMessage("hello world")))
}
}
Client Code
// Lets create a websocket and pass Basic Auth to it
new WebSocket("ws://user:pass#localhost:8000/event");
// ...
Obserservations
In the websocket handler the principal shows null
The client can connect without being authenticated. If I do WebSocket("ws://localhost:8000/event") without the Basic Auth it stills works! So Spring Security does not authenticate anything.
What I am missing?
What I do wrong?
I could advise you to implement your own authentication mechanism instead of exploiting Spring Security.
When WebSocket connection is about to establish it uses handshake mechanism accompanied by an UPGRADE request. Base on that, our idea would be to use our own handler for the request and perform authentication there.
Fortunately, Spring Boot has RequestUpgradeStrategy for such purpose. On top of that, based on the application server what you use, Spring provides a default implementation of those strategies. As I use Netty bellow the class would be ReactorNettyRequestUpgradeStrategy.
Here is the suggested prototype:
/**
* Based on {#link ReactorNettyRequestUpgradeStrategy}
*/
#Slf4j
#Component
public class BasicAuthRequestUpgradeStrategy implements RequestUpgradeStrategy {
private int maxFramePayloadLength = NettyWebSocketSessionSupport.DEFAULT_FRAME_MAX_SIZE;
private final AuthenticationService service;
public BasicAuthRequestUpgradeStrategy(AuthenticationService service) {
this.service = service;
}
#Override
public Mono<Void> upgrade(ServerWebExchange exchange, //
WebSocketHandler handler, //
#Nullable String subProtocol, //
Supplier<HandshakeInfo> handshakeInfoFactory) {
ServerHttpResponse response = exchange.getResponse();
HttpServerResponse reactorResponse = getNativeResponse(response);
HandshakeInfo handshakeInfo = handshakeInfoFactory.get();
NettyDataBufferFactory bufferFactory = (NettyDataBufferFactory) response.bufferFactory();
String originHeader = handshakeInfo.getHeaders()
.getOrigin();// you will get ws://user:pass#localhost:8080
return service.authenticate(originHeader)//returns Mono<Boolean>
.filter(Boolean::booleanValue)// filter the result
.doOnNext(a -> log.info("AUTHORIZED"))
.flatMap(a -> reactorResponse.sendWebsocket(subProtocol, this.maxFramePayloadLength, (in, out) -> {
ReactorNettyWebSocketSession session = //
new ReactorNettyWebSocketSession(in, out, handshakeInfo, bufferFactory, this.maxFramePayloadLength);
return handler.handle(session);
}))
.switchIfEmpty(Mono.just("UNATHORIZED")
.doOnNext(log::info)
.then());
}
private static HttpServerResponse getNativeResponse(ServerHttpResponse response) {
if (response instanceof AbstractServerHttpResponse) {
return ((AbstractServerHttpResponse) response).getNativeResponse();
} else if (response instanceof ServerHttpResponseDecorator) {
return getNativeResponse(((ServerHttpResponseDecorator) response).getDelegate());
} else {
throw new IllegalArgumentException("Couldn't find native response in " + response.getClass()
.getName());
}
}
}
Moreover, if you do not have crucial logical dependencies onto Spring Security in the project such as complex ACL logic, then I advise you to get rid of it and even do not use it at all.
The reason for that is that I see Spring Security as a violator of the reactive approach due to its, I would say, MVC legacy mindset. It entangles your application with tons of extra configurations and "not-on-the-surface" tunings and forces engineers to maintain those configurations, making them more and more complex. In most cases, things could be implemented very smoothly without touching Spring Security at all. Just create a component and use it in a proper way.
Hope it helps.
I have set up authentication in a Spring WebFlux application. The authentication mechanism appears to work fine. For example the following code is used to set up security web filter chain:
#Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
return http.authorizeExchange()
.pathMatchers("/path/to/resource").hasAuthority("A_ROLE")
.anyExchange().authenticated()
.and().httpBasic()
.and().build();
}
This works as expected in conjunction with the UserDetailsRepositoryReactiveAuthenticationManager and MapReactiveUserDetailsService. If a user doesn't have the required authority a forbidden error code is returned and otherwise the request is passed on to the handler.
I have a requirement to apply fine grained permission checks within the handler itself and figured that I should be able to retrieve the authorities from the request as follows:
public Mono<ServerResponse> getMyResource(ServerRequest serverRequest) {
Authentication authentication = (Authentication)serverRequest.principal().block();
...
}
However, I find that the principal is always null. First, is this the correct way to get a handle on the authorities, and if so is there possibly some upstream configuration I'm missing?
You are blocking the result before is available. You can simply flatmap it so that you don't have to block it.
public Mono<ServerResponse> getMyResource(ServerRequest serverRequest) {
return serverRequest.principal().flatMap((principal) -> ServerResponse.ok()
.body(fromObject("Hello " + principal.getName())));
}
UPDATE: If you want to retrieve the principal and body you could zip them.
public Mono<ServerResponse> getMyResource(ServerRequest serverRequest) {
return Mono.zip(
serverRequest.principal(),
serverRequest.bodyToMono(String.class)
).flatMap(tuple -> {
Principal principal = tuple.getT1();
String body = tuple.getT2();
return ServerResponse.ok().build();
});
}
I have a web application which sets a spring security context through a spring filter. Services are protected with spring annotations based on users roles. This works.
Asynchronous tasks are executed in JMS listeners (extend javax.jms.MessageListener). The setup of this listeners is done with Spring.
Messages are sent from the web application, at this time a user is authenticated. I need the same authentication in the JMS thread (user and roles) during message processing.
Today this is done by putting the spring authentication in the JMS ObjectMessage:
SecurityContext context = SecurityContextHolder.getContext();
Authentication auth = context.getAuthentication();
... put the auth object in jms message object
Then inside the JMS listener the authentication object is extracted and set in the context:
SecurityContext context = new SecurityContextImpl();
context.setAuthentication(auth);
SecurityContextHolder.setContext(context);
This works most of the time. But when there is a delay before the processing of a message, message will never be processed. I couldn't determine yet the cause of these messages loss, but I'm not sure the way we propagate authentication is good, even if it works in custer when the message is processed in another server.
Is this the right way to propagate a spring authentication ?
Regards,
Mickaƫl
I did not find better solution, but this one works for me just fine.
By sending of JMS Message I'am storing Authentication as Header and respectively by receiving recreating Security Context. In order to store Authentication as Header you have to serialise it as Base64:
class AuthenticationSerializer {
static String serialize(Authentication authentication) {
byte[] bytes = SerializationUtils.serialize(authentication);
return DatatypeConverter.printBase64Binary(bytes);
}
static Authentication deserialize(String authentication) {
byte[] decoded = DatatypeConverter.parseBase64Binary(authentication);
Authentication auth = (Authentication) SerializationUtils.deserialize(decoded);
return auth;
}
}
By sending just set Message header - you can create Decorator for Message Template, so that it will happen automatically. In you decorator just call such method:
private void attachAuthenticationContext(Message message){
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
String serialized = AuthenticationSerializer.serialize(auth);
message.setStringProperty("authcontext", serialized);
}
Receiving gets more complicated, but it can be also done automatically. Instead of applying #EnableJMS use following Configuration:
#Configuration
class JmsBootstrapConfiguration {
#Bean(name = JmsListenerConfigUtils.JMS_LISTENER_ANNOTATION_PROCESSOR_BEAN_NAME)
#Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public JmsListenerAnnotationBeanPostProcessor jmsListenerAnnotationProcessor() {
return new JmsListenerPostProcessor();
}
#Bean(name = JmsListenerConfigUtils.JMS_LISTENER_ENDPOINT_REGISTRY_BEAN_NAME)
public JmsListenerEndpointRegistry defaultJmsListenerEndpointRegistry() {
return new JmsListenerEndpointRegistry();
}
}
class JmsListenerPostProcessor extends JmsListenerAnnotationBeanPostProcessor {
#Override
protected MethodJmsListenerEndpoint createMethodJmsListenerEndpoint() {
return new ListenerEndpoint();
}
}
class ListenerEndpoint extends MethodJmsListenerEndpoint {
#Override
protected MessagingMessageListenerAdapter createMessageListenerInstance() {
return new ListenerAdapter();
}
}
class ListenerAdapter extends MessagingMessageListenerAdapter {
#Override
public void onMessage(Message jmsMessage, Session session) throws JMSException {
propagateSecurityContext(jmsMessage);
super.onMessage(jmsMessage, session);
}
private void propagateSecurityContext(Message jmsMessage) throws JMSException {
String authStr = jmsMessage.getStringProperty("authcontext");
Authentication auth = AuthenticationSerializer.deserialize(authStr);
SecurityContextHolder.getContext().setAuthentication(auth);
}
}
I have implemented for myself a different solution, which seems easier for me.
Already I have a message converter, the standard JSON Jackson message converter, which I need to configure on the JMSTemplate and the listeners.
So I created a MessageConverter implementation which wraps around another message converter, and propagates the security context via the JMS message properties.
(In my case, the propagated context is a JWT token which I can extract from the current context and apply to the security context of the listening thread).
This way the entire responsibility for propagation of security context is elegantly implemented in a single class, and requires only a little bit of configuration.
Thanks great but I am handling this in easy way . put one util file and solved .
public class AuthenticationSerializerUtil {
public static final String AUTH_CONTEXT = "authContext";
public static String serialize(Authentication authentication) {
byte[] bytes = SerializationUtils.serialize(authentication);
return DatatypeConverter.printBase64Binary(bytes);
}
public static Authentication deserialize(String authentication) {
byte[] decoded = DatatypeConverter.parseBase64Binary(authentication);
Authentication auth = (Authentication) SerializationUtils.deserialize(decoded);
return auth;
}
/**
* taking message and return string json from message & set current context
* #param message
* #return
*/
public static String jsonAndSetContext(Message message){
LongString authContext = (LongString)message.getMessageProperties().getHeaders().get(AUTH_CONTEXT);
Authentication auth = deserialize(authContext.toString());
SecurityContextHolder.getContext().setAuthentication(auth);
byte json[] = message.getBody();
return new String(json);
}
}