How to handle or change Invalid JSON from Server Vaadin 21 - vaadin

I am trying to display a custom error when there is a session expired or a internal server error using VaadinService.getCurrent().setSystemMessagesProvider(PARAM);
Right know, Vaadin won't display the right error when I am disconnected due to connexion from another browser, and displays me this message :
"Invalid JSON From server", followed by a HTML body, which is not user friendly.
I am using Vaadin 21, and this is what I do now :
import org.springframework.stereotype.Component;
import com.vaadin.flow.router.BeforeEnterEvent;
import com.vaadin.flow.server.CustomizedSystemMessages;
import com.vaadin.flow.server.ServiceException;
import com.vaadin.flow.server.ServiceInitEvent;
import com.vaadin.flow.server.SessionInitEvent;
import com.vaadin.flow.server.SessionInitListener;
import com.vaadin.flow.server.VaadinService;
import com.vaadin.flow.server.VaadinServiceInitListener;
import my.project.utils.security.SecurityUtils;
import my.project.vaadin.ConstantesRoutes;
import my.project.vaadin.view.login.ChangePasswordView;
import my.project.vaadin.view.login.ForgotPasswordView;
import my.project.vaadin.view.login.LoginView;
/**
* Allow the navigation listener to all the route
* This provide the re-reroute action if the user isn't login or not allow to access
*
*/
#Component
public class ConfigureUIServiceInitListener implements VaadinServiceInitListener, SessionInitListener {
/**
* Init the service to provide the beforeEnter action
*
* #param event at the serviceInit
*/
#Override
public void serviceInit(final ServiceInitEvent event) {
...
event.getSource().addSessionInitListener(this);
}
/**
* Reroutes the user if (s)he is not authorized to access the view or not login
*
* #param event
* before navigation event with event details
*/
private void beforeEnter(final BeforeEnterEvent event) {
...
}
#Override
public void sessionInit(final SessionInitEvent event) throws ServiceException {
VaadinService.getCurrent().setSystemMessagesProvider(systemMessagesInfo -> {
final CustomizedSystemMessages messages = new CustomizedSystemMessages();
messages.setInternalErrorCaption("Internal Server Error");
messages.setInternalErrorMessage("This is bad.");
messages.setInternalErrorNotificationEnabled(true);
messages.setInternalErrorURL("http://vaadin.com/");
messages.setSessionExpiredCaption("Comm Err session");
messages.setSessionExpiredMessage("This is bad. session");
messages.setSessionExpiredNotificationEnabled(true);
messages.setSessionExpiredURL(ConstantesRoutes.LOGIN_URL);
return messages;
});
}
}
Any idea ? Thank you !

Related

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;
}

Trying to build hello world in Vaadin connecting to my Domino Server

I created a basic Vaadin application then added my Domino Jar files.
When I run the application, I get
[com.vaadin.server.ServiceException: java.lang.NoClassDefFoundError: lotus/domino/NotesException]
I've read a bunch of articles that talk about using OSGI etc. Isn't there a simple way to access Domino data from Vaadin without all the plug-ins etc? If not can someone explain why?
This is the calling code
package com.lms.helloDomino;
import javax.servlet.annotation.WebServlet;
import com.lms.service.StarService;
import com.vaadin.annotations.Theme;
import com.vaadin.annotations.VaadinServletConfiguration;
import com.vaadin.server.VaadinRequest;
import com.vaadin.server.VaadinServlet;
import com.vaadin.ui.Button;
import com.vaadin.ui.Label;
import com.vaadin.ui.TextField;
import com.vaadin.ui.UI;
import com.vaadin.ui.VerticalLayout;
import lotus.domino.NotesException;
/**
* This UI is the application entry point. A UI may either represent a browser window
* (or tab) or some part of an HTML page where a Vaadin application is embedded.
* <p>
* The UI is initialized using {#link #init(VaadinRequest)}. This method is intended to be
* overridden to add component to the user interface and initialize non-component functionality.
*/
#Theme("mytheme")
public class MyUI extends UI {
#Override
protected void init(VaadinRequest vaadinRequest) {
StarService myStarService = null;
try
{
myStarService = new StarService();
myStarService.openStarDB();
} catch ( Exception e1 )
{
// TODO Auto-generated catch block
e1.printStackTrace();
}
final VerticalLayout layout = new VerticalLayout();
final TextField name = new TextField();
name.setCaption("Your Domino Name");
name.setValue( myStarService.getNABProfile( "" ).fullName.toString() );
Button button = new Button("Click Me");
button.addClickListener(e -> {
layout.addComponent(new Label("Thanks " + name.getValue()
+ ", it works!"));
});
layout.addComponents(name, button);
setContent(layout);
}
#WebServlet(urlPatterns = "/*", name = "MyUIServlet", asyncSupported = true)
#VaadinServletConfiguration(ui = MyUI.class, productionMode = false)
public static class MyUIServlet extends VaadinServlet {
}
}
Here is the domino code
package com.lms.service;
import lotus.domino.NotesException;
import lotus.domino.Session;
import lotus.domino.NotesFactory;
public class StarService
{
public static Session notesSession = null;
public static Session getNotesSession()
{
if( notesSession == null )
try
{
notesSession = NotesFactory.createSession( "testHostServer", "testUser", "testPassword" );
} catch ( NotesException e )
{
e.printStackTrace();
}
return notesSession;
}
public StarService() throws NotesException
{
System.out.println( "Begin StarService Constructor" );
// Setup the notes connectivity
getNotesSession();
System.out.print( getNotesSession().getUserName() );
System.out.println( "End STARService Constructor" );
}
}
Turns out it was a build path issue. A big thank you to Karsten Lehmann from mindoo.de who helped me figure this out.
I didn't realize when running an Apache web server which serves up the Vaadin application, required my Domino .jar files on it's build path as well. He showed my how to add the .jar files to Apache's as follows:
Double click the Apache server under the servers tab
Click the Open Launch Configuration
Click the Class Path Tab
Highlight User Entries and Add External Jar files.
I've been looking for this off / on for a year now. Can't believe it's finally working!!!

AccessDeniedException Spring Security Can Not Catched By AbstractRequestCycleListener wicket

I have custom class from AbstractRequestCycleListener:
package com.rudiwijaya.rcommerce.security;
import java.util.List;
import org.apache.wicket.Session;
import org.apache.wicket.core.request.handler.PageProvider;
import org.apache.wicket.core.request.handler.RenderPageRequestHandler;
import org.apache.wicket.core.request.handler.RenderPageRequestHandler.RedirectPolicy;
import org.apache.wicket.request.IRequestHandler;
import org.apache.wicket.request.Url;
import org.apache.wicket.request.cycle.AbstractRequestCycleListener;
import org.apache.wicket.request.cycle.RequestCycle;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.access.AccessDeniedException;
import com.google.common.base.Throwables;
import com.google.common.collect.Iterables;
import com.rudiwijaya.rcommerce.WicketSession;
import com.rudiwijaya.rcommerce.pages.AccessDeniedPage;
public class NotAuthorizedRequestCycleListener extends AbstractRequestCycleListener {
private static final Logger log = LoggerFactory.getLogger(NotAuthorizedRequestCycleListener.class);
#Override
public IRequestHandler onException(RequestCycle cycle, Exception ex) {
// Since our actual exception is wrapped, we need to find based on the chain
final List<Throwable> chain = Throwables.getCausalChain(ex);
final AccessDeniedException notAuthorizedException = Iterables.getFirst(
Iterables.filter(chain, AccessDeniedException.class), null);
if (notAuthorizedException != null) {
final Url url = cycle.getRequest().getClientUrl();
log.debug("Got exception " + notAuthorizedException.getClass().getName() + " on " + url, ex);
final WicketSession wSession = (WicketSession) Session.get();
wSession.error("Anda tidak memilik akses.");
wSession.dirty();
return new RenderPageRequestHandler(new PageProvider(AccessDeniedPage.class), RedirectPolicy.NEVER_REDIRECT);
} else {
return super.onException(cycle, ex);
}
}
}
In init() method from AuthenticatedWebApplication, I added:
getRequestCycleListeners().add(new NotAuthorizedRequestCycleListener());
How can I to catch the AccessDeniedException spring security in wicket's AbstractRequestCycleListener?
My point is showing custom access denied page with keep the url, in Wicket command like:
return new RenderPageRequestHandler(new PageProvider(AccessDeniedPage.class), RedirectPolicy.NEVER_REDIRECT);
Most probably Spring Security filter is executed before WicketFilter, so Wicket doesn't have a chance to do anything.
You may need custom AuthenticationEntryPoint to handle such failures via Spring Security APIs.

throwing org.atmosphere.cpr.AtmosphereResourceImpl: Exception during suspend() operation java.lang.NullPointerException

I am new to atmosphere-websocket. I am working on private chat. I am using dropwizard framework.
Below is my code:
service.java:
#Override
public void run(ServiceConfiguration config, Environment environment)throws Exception {
AtmosphereServlet servlet = new AtmosphereServlet();
servlet.framework().addInitParameter("com.sun.jersey.config.property.packages", "dk.cooldev.chatroom.resources.websocket");
servlet.framework().addInitParameter(ApplicationConfig.WEBSOCKET_CONTENT_TYPE, "application/json");
servlet.framework().addInitParameter(ApplicationConfig.WEBSOCKET_SUPPORT, "true");
ServletRegistration.Dynamic servletHolder = environment.servlets().addServlet("Chat", servlet);
servletHolder.addMapping("/chat/*");``
}
ChatRoom.java:
package resource;
import org.atmosphere.config.service.DeliverTo;
import org.atmosphere.config.service.Disconnect;
import org.atmosphere.config.service.ManagedService;
import org.atmosphere.config.service.Message;
import org.atmosphere.config.service.PathParam;
import org.atmosphere.config.service.Ready;
import org.atmosphere.cpr.AtmosphereResource;
import org.atmosphere.cpr.AtmosphereResourceEvent;
import org.atmosphere.cpr.AtmosphereResourceFactory;
import org.atmosphere.cpr.Broadcaster;
import org.atmosphere.cpr.BroadcasterFactory;
import org.atmosphere.cpr.MetaBroadcaster;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import representation.UserMessage;
import utility.ChatProtocol;
import utility.JacksonEncoder;
import utility.ProtocolDecoder;
import utility.UserDecoder;
import javax.inject.Inject;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.concurrent.ConcurrentHashMap;
#ManagedService(path = "/chat/{room: [a-zA-Z][a-zA-Z_0-9]*}")
public class ChatRoom {
private final Logger logger = LoggerFactory.getLogger(ChatRoom.class);
private final ConcurrentHashMap<String, String> users = new ConcurrentHashMap<String, String>();
private final static String CHAT = "/chat/";
#PathParam("room")
private String chatroomName;
#Inject
private BroadcasterFactory factory;
#Inject
private AtmosphereResourceFactory resourceFactory;
#Inject
private MetaBroadcaster metaBroadcaster;
/**
* Invoked when the connection as been fully established and suspended, e.g ready for receiving messages.
*
* #param r
*/
#Ready(encoders = {JacksonEncoder.class})
#DeliverTo(DeliverTo.DELIVER_TO.ALL)
public ChatProtocol onReady(final AtmosphereResource r) {
r.suspend();
logger.info("Browser {} connected.", r.uuid());
return new ChatProtocol(users.keySet(), getRooms(factory.lookupAll()));
}
private static Collection<String> getRooms(Collection<Broadcaster> broadcasters) {
Collection<String> result = new ArrayList<String>();
for (Broadcaster broadcaster : broadcasters) {
if (!("/*".equals(broadcaster.getID()))) {
// if no room is specified, use ''
String[] p = broadcaster.getID().split("/");
result.add(p.length > 2 ? p[2] : "");
}
};
return result;
}
/**
* Invoked when the client disconnect or when an unexpected closing of the underlying connection happens.
*
* #param event
*/
#Disconnect
public void onDisconnect(AtmosphereResourceEvent event) {
if (event.isCancelled()) {
// We didn't get notified, so we remove the user.
users.values().remove(event.getResource().uuid());
logger.info("Browser {} unexpectedly disconnected", event.getResource().uuid());
} else if (event.isClosedByClient()) {
logger.info("Browser {} closed the connection", event.getResource().uuid());
}
}
/**
* Simple annotated class that demonstrate how {#link org.atmosphere.config.managed.Encoder} and {#link org.atmosphere.config.managed.Decoder
* can be used.
*
* #param message an instance of {#link ChatProtocol }
* #return
* #throws IOException
*/
#Message(encoders = {JacksonEncoder.class}, decoders = {ProtocolDecoder.class})
public ChatProtocol onMessage(ChatProtocol message) throws IOException {
if (!users.containsKey(message.getAuthor())) {
users.put(message.getAuthor(), message.getUuid());
return new ChatProtocol(message.getAuthor(), " entered room " + chatroomName, users.keySet(), getRooms(factory.lookupAll()));
}
if (message.getMessage().contains("disconnecting")) {
users.remove(message.getAuthor());
return new ChatProtocol(message.getAuthor(), " disconnected from room " + chatroomName, users.keySet(), getRooms(factory.lookupAll()));
}
message.setUsers(users.keySet());
logger.info("{} just send {}", message.getAuthor(), message.getMessage());
return new ChatProtocol(message.getAuthor(), message.getMessage(), users.keySet(), getRooms(factory.lookupAll()));
}
#Message(decoders = {UserDecoder.class})
public void onPrivateMessage(UserMessage user) throws IOException {
String userUUID = users.get(user.getUser());
if (userUUID != null) {
// Retrieve the original AtmosphereResource
AtmosphereResource r = resourceFactory.find(userUUID);
if (r != null) {
ChatProtocol m = new ChatProtocol(user.getUser(), " sent you a private message: " + user.getMessage().split(":")[1], users.keySet(), getRooms(factory.lookupAll()));
if (!user.getUser().equalsIgnoreCase("all")) {
factory.lookup(CHAT + chatroomName).broadcast(m, r);
}
}
} else {
ChatProtocol m = new ChatProtocol(user.getUser(), " sent a message to all chatroom: " + user.getMessage().split(":")[1], users.keySet(), getRooms(factory.lookupAll()));
metaBroadcaster.broadcastTo("/*", m);
}
}
}
Now, when I run my application , its throwing
resource.ChatRoom: Browser 72196f81-3425-4137-8f37-c5aa6b134534 connected.
WARN: org.atmosphere.cpr.AtmosphereResourceImpl: Exception during suspend() operation java.lang.NullPointerException
Please help me out to resolve this exception.
If you are getting that warning then your socket should have been removed from the broadcast listeners list . Message that is delivered to the broadcaster which calls outgoingBroadcast(Object message) is null and null case isn't handled . In drop-wizard that message becomes null for onReady Method . Quick fix is to extend the broadcaster and override that method and handle the null case .
I hope it helps . I was using RedisBroadcaster and I had same issue and I fixed by handling the null case .
public void outgoingBroadcast(Object message) {
// Marshal the message outside of the sync block.
if(message == null){
return;
}
String contents = message.toString();
I was just setting up the chat samples my self and hit the same issue. In my case, an injected field was not available.
In your case, I would say it is one of
#Inject
private BroadcasterFactory factory;
#Inject
private AtmosphereResourceFactory resourceFactory;
#Inject
private MetaBroadcaster metaBroadcaster;
That are not wired in correctly. I have no used drop-wizard before so I can't advise on how to make sure these are configured correctly.
This type of issue was easy enough to see once I configured atmosphere for trace logging. e.g. for logback add the following line
<logger name="org.atmosphere" level="TRACE" />
Thanks
Ryan

Phonegap Push Notifications for Android

I'm building an application for Android using Phonegap & jQuery Mobile. I want to implement push notifications, I found some methods like: Urban-Air & Phonegap Plugins
But they don't seem to support Cordova 1.9... So are there other new versions that I can use ?
You could try push SDK from Pushwoosh: http://pushwoosh.com, they are free and already have support for Cordova 1.9 and GCM (unlike UrbanAirship).
Urban Airship if a paid service and the provided plugin is for iOS only... Android uses CGM to deliver PUSH notifications now.
Since CGM is quite new and it was preceded by C2DM, I don't have any manuals for CGM handy, but maybe this code I have could help you start with your development:
Main application JAR file:
/** Called when the activity is first created. */
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// register for PUSH notifications - you will need a registered Google e-mail for it
C2DMessaging.register(this /*the application context*/, DeviceRegistrar.SENDER_ID);
super.loadUrl("file:///android_asset/www/index.html");
}
DeviceRegistrar.java
package YOURPACKAGE;
import android.content.Context;
import android.util.Log;
public class DeviceRegistrar {
public static final String SENDER_ID = "YOUR-GOOGLE-REGISTERED-EMAIL";
private static final String TAG = "YOUR_APP_NAME";
// just so you can work with the registration token from C2DM
public static String token;
public static void registerWithServer(Context context, String registrationId)
{
token = registrationId;
// insert code to supplement this device registration with your 3rd party server
Log.d(TAG, "successfully registered, ID = " + registrationId);
}
public static void unregisterWithServer(Context context, String registrationId)
{
// insert code to supplement unregistration with your 3rd party server
Log.d(TAG, "succesfully unregistered with 3rd party app server");
}
}
C2DMReceiver.java (you will need c2dm.jar file and add it to your libraries)
package YOURPACKAGE;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.media.RingtoneManager;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.Vibrator;
import android.util.Log;
import com.google.android.c2dm.C2DMBaseReceiver;
import com.google.android.c2dm.C2DMessaging;
public class C2DMReceiver extends C2DMBaseReceiver
{
public static final String TAG = "YOUR_APP_NAME";
public static String lastMessage = "";
public static List<Integer> lastNotifications = new ArrayList<Integer>();
public static Boolean isForegrounded = true;
public C2DMReceiver()
{
//send the email address you set up earlier
super(DeviceRegistrar.SENDER_ID);
}
#Override
public void onRegistered(Context context, String registrationId) throws IOException
{
Log.d(TAG, "successfully registered with C2DM server; registrationId: " + registrationId);
DeviceRegistrar.registerWithServer(context, registrationId);
}
#Override
public void onError(Context context, String errorId)
{
//notify the user
Log.e(TAG, "error with C2DM receiver: " + errorId);
if ("ACCOUNT_MISSING".equals(errorId)) {
//no Google account on the phone; ask the user to open the account manager and add a google account and then try again
//TODO
} else if ("AUTHENTICATION_FAILED".equals(errorId)) {
//bad password (ask the user to enter password and try. Q: what password - their google password or the sender_id password? ...)
//i _think_ this goes hand in hand with google account; have them re-try their google account on the phone to ensure it's working
//and then try again
//TODO
} else if ("TOO_MANY_REGISTRATIONS".equals(errorId)) {
//user has too many apps registered; ask user to uninstall other apps and try again
//TODO
} else if ("INVALID_SENDER".equals(errorId)) {
//this shouldn't happen in a properly configured system
//TODO: send a message to app publisher?, inform user that service is down
} else if ("PHONE_REGISTRATION_ERROR".equals(errorId)) {
//the phone doesn't support C2DM; inform the user
//TODO
} //else: SERVICE_NOT_AVAILABLE is handled by the super class and does exponential backoff retries
}
}
#Override
protected void onMessage(Context context, Intent intent)
{
Bundle extras = intent.getExtras();
if (extras != null) {
//parse the message and do something with it.
//For example, if the server sent the payload as "data.message=xxx", here you would have an extra called "message"
String message = extras.getString("message");
Log.i(TAG, "received message: " + message);
}
}
}

Resources