In my Spring Boot1.2.7/JSF2.2.12/PrimeFaces5.2/Tomcat 8 application I'm trying to implement redirect to login page after AJAX call on a website where /logout has been performed.
In order to do this I have added JsfRedirectStrategy:
/**
* Inspired by <a href=
* "http://stackoverflow.com/questions/10143539/jsf-2-spring-security-3-x-and-richfaces-4-redirect-to-login-page-on-session-tim">StackOverflow.com</a>
* and by <a href=http://www.icesoft.org/wiki/display/ICE/Spring+Security#SpringSecurity-Step4%3AConfigureYourSpringSecurityredirectStrategy">
* Spring Security 3 and ICEfaces 3 Tutorial</a>.
*
* #author banterCZ
*/
public class JsfRedirectStrategy implements InvalidSessionStrategy {
final static Logger logger = LoggerFactory.getLogger(JsfRedirectStrategy.class);
private static final String FACES_REQUEST_HEADER = "faces-request";
private String invalidSessionUrl;
/**
* {#inheritDoc}
*/
#Override
public void onInvalidSessionDetected(HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException {
boolean ajaxRedirect = "partial/ajax".equals(request.getHeader(FACES_REQUEST_HEADER));
if (ajaxRedirect) {
String contextPath = request.getContextPath();
String redirectUrl = contextPath + invalidSessionUrl;
logger.debug("Session expired due to ajax request, redirecting to '{}'", redirectUrl);
String ajaxRedirectXml = createAjaxRedirectXml(redirectUrl);
logger.debug("Ajax partial response to redirect: {}", ajaxRedirectXml);
response.setContentType("text/xml");
response.getWriter().write(ajaxRedirectXml);
} else {
String requestURI = getRequestUrl(request);
logger.debug(
"Session expired due to non-ajax request, starting a new session and redirect to requested url '{}'",
requestURI);
request.getSession(true);
response.sendRedirect(requestURI);
}
}
private String getRequestUrl(HttpServletRequest request) {
StringBuffer requestURL = request.getRequestURL();
String queryString = request.getQueryString();
if (StringUtils.hasText(queryString)) {
requestURL.append("?").append(queryString);
}
return requestURL.toString();
}
private String createAjaxRedirectXml(String redirectUrl) {
return new StringBuilder().append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>")
.append("<partial-response><redirect url=\"").append(redirectUrl)
.append("\"></redirect></partial-response>").toString();
}
public void setInvalidSessionUrl(String invalidSessionUrl) {
this.invalidSessionUrl = invalidSessionUrl;
}
}
This is my WebSecurityConfig
#Configuration
#EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private UserDetailsServiceImpl userDetailsService;
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService);
}
#Override
#Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
#Override
protected void configure(HttpSecurity http) throws Exception {
// #formatter:off
http
.addFilterBefore(sessionManagementFilter(), AnonymousAuthenticationFilter.class)
.csrf().disable()
.authorizeRequests()
.antMatchers("/invite.xhtml").permitAll()
.antMatchers("/forgotpassword.xhtml").permitAll()
.antMatchers("/resetpwd.xhtml").permitAll()
.antMatchers("/admin/**").hasRole(Roles.ROLE_ADMIN.getSpringSecName())
.antMatchers("/**").authenticated()
.antMatchers("/actuator/**").permitAll()
.and()
.formLogin()
.loginPage("/login.xhtml").permitAll()
//.failureUrl("/login?error").permitAll()
.and()
.logout()
.logoutUrl("/logout")
.logoutRequestMatcher( new AntPathRequestMatcher("/logout"))
.logoutSuccessUrl("/login.xhtml")
.invalidateHttpSession(true)
.deleteCookies("JSESSIONID")
.permitAll();
http.headers().frameOptions().disable();
// #formatter:on
}
#Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/javax.faces.resource/**");
}
#Bean
public SessionManagementFilter sessionManagementFilter() {
SessionManagementFilter sessionManagementFilter = new SessionManagementFilter(httpSessionSecurityContextRepository());
sessionManagementFilter.setInvalidSessionStrategy(jsfRedirectStrategy());
return sessionManagementFilter;
}
public HttpSessionSecurityContextRepository httpSessionSecurityContextRepository() {
return new HttpSessionSecurityContextRepository();
}
#Bean
public JsfRedirectStrategy jsfRedirectStrategy() {
JsfRedirectStrategy jsfRedirectStrategy = new JsfRedirectStrategy();
jsfRedirectStrategy.setInvalidSessionUrl("/login.xhtml");
return jsfRedirectStrategy;
}
}
This is logout link:
<div id="LogoutContainer" class="PFTopLinks floatRight boldFont">
<h:form rendered="#{not empty request.remoteUser}">
<h:graphicImage name="main/images/pfPush.svg" />
<h:outputLink value="${pageContext.request.contextPath}/logout">
<span class="PFDarkText">Logout</span>
</h:outputLink>
</h:form>
</div>
The problem: Right now JsfRedirectStrategy.onInvalidSessionDetected is never invoked on AJAX JSF call because request.isRequestedSessionIdValid() in SessionManagementFilter.doFilter() always returns true.
There after logout I have an instance of org.apache.catalina.session.StandardSessionFacade
Whats wrong with my code ?
I have reimplemented this approach with a following code(based on this topic http://forum.primefaces.org/viewtopic.php?f=3&t=33380):
I have added AjaxTimeoutPhaseListener phase listener:
public class AjaxTimeoutPhaseListener implements PhaseListener {
private static final long serialVersionUID = 2639152532235352192L;
public static Logger logger = LoggerFactory.getLogger(AjaxTimeoutPhaseListener.class);
#Override
public void afterPhase(PhaseEvent ev) {
}
#Override
public void beforePhase(PhaseEvent ev) {
FacesContext fc = FacesUtils.getContext();
RequestContext rc = RequestContext.getCurrentInstance();
HttpServletResponse response = FacesUtils.getResponse();
HttpServletRequest request = FacesUtils.getRequest();
if (FacesUtils.getExternalContext().getUserPrincipal() == null) {
if (FacesUtils.getExternalContext().isResponseCommitted()) {
// redirect is not possible
return;
}
try {
if (((rc != null && rc.isAjaxRequest())
|| (fc != null && fc.getPartialViewContext().isPartialRequest()))
&& fc.getResponseWriter() == null && fc.getRenderKit() == null) {
response.setCharacterEncoding(request.getCharacterEncoding());
RenderKitFactory factory = (RenderKitFactory) FactoryFinder
.getFactory(FactoryFinder.RENDER_KIT_FACTORY);
RenderKit renderKit = factory.getRenderKit(fc,
fc.getApplication().getViewHandler().calculateRenderKitId(fc));
ResponseWriter responseWriter = renderKit.createResponseWriter(response.getWriter(), null,
request.getCharacterEncoding());
responseWriter = new PartialResponseWriter(responseWriter);
fc.setResponseWriter(responseWriter);
FacesUtils.redirect("/login.xhtml");
}
} catch (IOException ex) {
StringBuilder error = new StringBuilder("Redirect to the specified page '");
error.append("/login.xhtml");
error.append("' failed");
logger.error(error.toString(), ex);
throw new FacesException(ex);
}
} else {
return; // This is not a timeout case . Do nothing !
}
}
public PhaseId getPhaseId() {
return PhaseId.RESTORE_VIEW;
}
}
Also added FacesUtils class(extracted from OmniFaces lib):
public class FacesUtils {
public static Logger logger = LoggerFactory.getLogger(FacesUtils.class);
/**
* Returns the current faces context.
* <p>
* <i>Note that whenever you absolutely need this method to perform a general task, you might want to consider to
* submit a feature request to OmniFaces in order to add a new utility method which performs exactly this general
* task.</i>
* #return The current faces context.
* #see FacesContext#getCurrentInstance()
*/
public static FacesContext getContext() {
return FacesContext.getCurrentInstance();
}
/**
* Returns the HTTP servlet response.
* <p>
* <i>Note that whenever you absolutely need this method to perform a general task, you might want to consider to
* submit a feature request to OmniFaces in order to add a new utility method which performs exactly this general
* task.</i>
* #return The HTTP servlet response.
* #see ExternalContext#getResponse()
*/
public static HttpServletResponse getResponse() {
return getResponse(getContext());
}
/**
* {#inheritDoc}
* #see Faces#getResponse()
*/
public static HttpServletResponse getResponse(FacesContext context) {
return (HttpServletResponse) context.getExternalContext().getResponse();
}
/**
* Returns the HTTP servlet request.
* <p>
* <i>Note that whenever you absolutely need this method to perform a general task, you might want to consider to
* submit a feature request to OmniFaces in order to add a new utility method which performs exactly this general
* task.</i>
* #return The HTTP servlet request.
* #see ExternalContext#getRequest()
*/
public static HttpServletRequest getRequest() {
return getRequest(getContext());
}
/**
* {#inheritDoc}
* #see Faces#getRequest()
*/
public static HttpServletRequest getRequest(FacesContext context) {
return (HttpServletRequest) context.getExternalContext().getRequest();
}
/**
* Returns the current external context.
* <p>
* <i>Note that whenever you absolutely need this method to perform a general task, you might want to consider to
* submit a feature request to OmniFaces in order to add a new utility method which performs exactly this general
* task.</i>
* #return The current external context.
* #see FacesContext#getExternalContext()
*/
public static ExternalContext getExternalContext() {
return getContext().getExternalContext();
}
/**
* Returns the HTTP request context path. It's the webapp context name, with a leading slash. If the webapp runs
* on context root, then it returns an empty string.
* #return The HTTP request context path.
* #see ExternalContext#getRequestContextPath()
*/
public static String getRequestContextPath() {
return getRequestContextPath(getContext());
}
/**
* {#inheritDoc}
* #see Faces#getRequestContextPath()
*/
public static String getRequestContextPath(FacesContext context) {
return context.getExternalContext().getRequestContextPath();
}
/**
* Does a regular or ajax redirect.
*/
public static void redirect(String redirectPage) throws FacesException {
checkViewRoot(FacesUtils.getContext(), FacesUtils.getRequestContextPath());
FacesContext fc = FacesUtils.getContext();
ExternalContext ec = fc.getExternalContext();
try {
if (ec.isResponseCommitted()) {
// redirect is not possible
return;
}
// fix for renderer kit (Mojarra's and PrimeFaces's ajax redirect)
if ((RequestContext.getCurrentInstance().isAjaxRequest() || fc.getPartialViewContext().isPartialRequest())
&& fc.getResponseWriter() == null && fc.getRenderKit() == null) {
ServletResponse response = (ServletResponse) ec.getResponse();
ServletRequest request = (ServletRequest) ec.getRequest();
response.setCharacterEncoding(request.getCharacterEncoding());
RenderKitFactory factory = (RenderKitFactory) FactoryFinder
.getFactory(FactoryFinder.RENDER_KIT_FACTORY);
RenderKit renderKit = factory.getRenderKit(fc,
fc.getApplication().getViewHandler().calculateRenderKitId(fc));
ResponseWriter responseWriter = renderKit.createResponseWriter(response.getWriter(), null,
request.getCharacterEncoding());
fc.setResponseWriter(responseWriter);
}
ec.redirect(ec.getRequestContextPath() + (redirectPage != null ? redirectPage : ""));
} catch (IOException e) {
logger.error("Redirect to the specified page '" + redirectPage + "' failed");
throw new FacesException(e);
}
}
public static void checkViewRoot(FacesContext ctx, String viewId) {
if (ctx.getViewRoot() == null) {
UIViewRoot viewRoot = ctx.getApplication().getViewHandler().createView(ctx, viewId);
if (viewRoot != null) {
ctx.setViewRoot(viewRoot);
}
}
}
}
also added following lines to faces-config.xml:
<lifecycle>
<phase-listener>com.domain.AjaxTimeoutPhaseListener</phase-listener>
</lifecycle>
Now everything works fine
Related
I have following simple proxy integration flow. The main task of which is to take request from the proxy send it to the actual endpoint, get the respond and send it back to the client. I would like to handle different type of exceptions.
#SpringBootApplication
#EnableIntegration
public class IntegrationApp {
#Value("${narko.pin}")
private String pinUrl;
public static void main(String[] args) {
SpringApplication.run(MinzdravApplication.class, args);
}
#Bean
public DirectChannel requestPinChannel() {
return new DirectChannel();
}
#Bean
public DirectChannel replyPinChannel() {
return new DirectChannel();
}
#Bean
public IntegrationFlow httpProxyFlowPin() throws Exception {
return IntegrationFlows
.from(Http.inboundGateway("/narko/api/patient/by-pinpp")
.requestChannel(requestPinChannel())
.mappedRequestHeaders("activityid")
.errorChannel("httpProxyErrorFlow.input")
)
.wireTap(sf->sf.handle(new InwardMessageHandler()))
.enrichHeaders(h -> h.header("Content-Type", "application/json"))
.handle(Http.outboundGateway(pinUrl).charset("utf-8")
.expectedResponseType(String.class))
.get();
}
#Bean
public IntegrationFlow httpProxyErrorFlow() {
return f -> f
.transform(Throwable::getCause)
.<RuntimeException>handle(
(p, h) ->
MessageBuilder.fromMessage(new Message<ErrorDto>() {
final Map<String, Object> headers=new HashMap<>();
#Override
public ErrorDto getPayload() {
if(p instanceof JSONException){
headers.put(HttpHeaders.STATUS_CODE,HttpStatus.BAD_REQUEST);
return new ErrorDto(HttpStatus.BAD_REQUEST.value(),p.getMessage());
}else if(p instanceof MethodNotAllowedException){
headers.put(HttpHeaders.STATUS_CODE,HttpStatus.METHOD_NOT_ALLOWED);
return new ErrorDto(HttpStatus.METHOD_NOT_ALLOWED.value(),p.getMessage());
}
else{
headers.put(HttpHeaders.STATUS_CODE,HttpStatus.INTERNAL_SERVER_ERROR);
return new ErrorDto(HttpStatus.INTERNAL_SERVER_ERROR.value(),p.getMessage());
}
}
#Override
public MessageHeaders getHeaders() {
return new MessageHeaders(headers);
}
})
).transform(Transformers.toJson())
;
}
As you can see the code above I have to check every possible exception type, then form corresponding ErrorDto, which makes the code difficult to maintain. Is it possible to handle them as one can do it with #ControllerAdvice? For instance :
#ControllerAdvice
public class ApiExceptionHandler {
#ExceptionHandler(JSONException.class)
#ResponseStatus(HttpStatus.BAD_REQUEST)
public ResponseEntity<ApiError> onRuntimeException(JSONException ex) {
ErrorDto apiError = new ErrorDto(HttpStatus.BAD_REQUEST, ex.getMessage(), ex);
return buildResponseEntity(apiError);
}
#ExceptionHandler(MethodNotAllowedException.class)
#ResponseStatus(HttpStatus.METHOD_NOT_ALLOWED)
public ResponseEntity<ApiError> onIllegalException(MethodNotAllowedException ex) {
ErrorDto apiError = new ErrorDto(HttpStatus.METHOD_NOT_ALLOWED, ex.getMessage(), ex);
return buildResponseEntity(apiError);
}
...
}
Sure! You can do something similar with Spring Integration. See an ErrorMessageExceptionTypeRouter and its Java DSL routeByException():
/**
* Populate the {#link ErrorMessageExceptionTypeRouter} with options from the {#link RouterSpec}.
* Typically, used with a Lambda expression:
* <pre class="code">
* {#code
* .routeByException(r -> r
* .channelMapping(IllegalArgumentException.class, "illegalArgumentChannel")
* .subFlowMapping(MessageHandlingException.class, sf ->
* sf.handle(...))
* )
* }
* </pre>
* #param routerConfigurer the {#link Consumer} to provide {#link ErrorMessageExceptionTypeRouter} options.
* #return the current {#link BaseIntegrationFlowDefinition}.
* #see ErrorMessageExceptionTypeRouter
*/
public B routeByException(
Consumer<RouterSpec<Class<? extends Throwable>, ErrorMessageExceptionTypeRouter>> routerConfigurer) {
https://docs.spring.io/spring-integration/docs/current/reference/html/message-routing.html#router-implementations-exception-router
You also can just throw those exceptions back to the proxy and have that #ControllerAdvice for handling them on the MVC level.
I have been trying to add a request parameter to the AuthorizationCode request, spring oauth2 filter makes to google, as part of the oauth2 authentication flow. Specifically, I need to add a login_hint parameter to prevent google from directing users to pick their accounts when the email address is already known.
This is my initial configuration:
#Configuration
#EnableOAuth2Client
#RequiredArgsConstructor
public class OAuthSecurityConfiguration extends WebSecurityConfigurerAdapter {
private static final String LOGIN_PATH = "/oauth/login";
private static final int OAUTH2_CLIENT_FILTER_ORDER = -100;
static {
SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_INHERITABLETHREADLOCAL);
}
private final OAuth2ClientContext oauth2ClientContext;
private final OAuth2ClientContextFilter oAuth2ClientContextFilter;
#Override
protected void configure(HttpSecurity http) throws Exception {
final OAuth2ClientAuthenticationProcessingFilter ssoFilter = new OAuth2ClientAuthenticationProcessingFilter(LOGIN_PATH);
ssoFilter.setRestTemplate(googleRestTemplate());
ssoFilter.setTokenServices(tokenServices());
ssoFilter.setAuthenticationManager(oAuth2AuthenticationManager());
final OAuth2AuthenticationProcessingFilter clientFilter = new OAuth2AuthenticationProcessingFilter();
clientFilter.setAuthenticationManager(oAuth2AuthenticationManager());
clientFilter.setStateless(false);
// #formatter:off
http
.csrf().disable()
.cors().disable()
.headers()
.frameOptions().sameOrigin()
.cacheControl().disable()
.and()
.antMatcher("/**")
.csrf().disable()
.httpBasic().disable()
.rememberMe().disable()
.addFilterBefore(ssoFilter, BasicAuthenticationFilter.class)
.addFilterBefore(clientFilter, OAuth2ClientAuthenticationProcessingFilter.class)
.authorizeRequests()
.antMatchers("/api/**").fullyAuthenticated()
.anyRequest().permitAll()
.and()
.logout().logoutUrl("/logout")
.and() .exceptionHandling().authenticationEntryPoint(authenticationEntryPoint());
// #formatter:on
}
The Only way I managed to do this is as follows:
#Bean
public OAuth2RestTemplate googleRestTemplate() {
MyAuthorizationCodeAccessTokenProvider myAuthorizationCodeAccessTokenProvider =
new MyAuthorizationCodeAccessTokenProvider();
AccessTokenProvider accessTokenProvider = new AccessTokenProviderChain(
Arrays.<AccessTokenProvider>asList(
myAuthorizationCodeAccessTokenProvider, new ImplicitAccessTokenProvider(),
new ResourceOwnerPasswordAccessTokenProvider(), new ClientCredentialsAccessTokenProvider()));
OAuth2RestTemplate oAuth2RestTemplate = new OAuth2RestTemplate(googleClient(), oauth2ClientContext);
oAuth2RestTemplate.setAccessTokenProvider(accessTokenProvider);
return oAuth2RestTemplate;
}
static class MyAuthorizationCodeAccessTokenProvider extends AuthorizationCodeAccessTokenProvider {
private static String EMAIL_PARAM_NAME = "email";
#Override
public OAuth2AccessToken obtainAccessToken(OAuth2ProtectedResourceDetails details, AccessTokenRequest request)
throws UserRedirectRequiredException, UserApprovalRequiredException, AccessDeniedException,
OAuth2AccessDeniedException {
try {
return super.obtainAccessToken(details, request);
} catch (UserRedirectRequiredException ex) {
String email = request.containsKey(EMAIL_PARAM_NAME) ? request.get(EMAIL_PARAM_NAME).get(0) : null;
if (email != null) {
ex.getRequestParams().put("login_hint", email);
}
throw ex;
}
}
}
}
Is this the best way to customize the spring oauth2 implementation to set the login_hint parameter on the authorization request?
I needed to solve the exact same problem with Okta as the IdP. I know this is an old post, but my post answers the question with a general approach that can be used for any additional parameters (not just login_hint) that need to be appended to the authorization URL. I think my answer fits well into how Spring expects developers to solve this. My main resource for a solution is found here (https://github.com/spring-projects/spring-security/issues/5244). Another good resource is here (https://github.com/spring-projects/spring-security/issues/5521). Here's how it works: Spring uses a filter called OAuth2AuthorizationRequestRedirectFilter which is responsible for building the OAuth2 authorization request. The authorization request (OAuth2AuthorizationRequest) contains the authorization request URI. The authorization request URI needs to be altered by adding additional parameters (i.e. login_hint). Spring uses an OAuth2AuthorizationRequestResolver implementation that creates the OAuth2AuthorizationRequest. By providing your own OAuth2AuthorizationRequestResolver, you can add additional parameters to the authorization URL. So for my implementation, I created a class called ConfigurableOAuth2AuthorizationRequestResolver that wraps Spring's DefaultOAuth2AuthorizationRequestResolver. Here is some of my code.
// Credit to Joe Grandja for providing test cases found here:
// https://raw.githubusercontent.com/spring-projects/spring-security/779597af2a6ed777707f08ae8106818e0b8e299e/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationRequestRedirectFilterTests.java
// Much of the below code comes from his test examples.
public class ConfigurableOAuth2AuthorizationRequestResolver implements OAuth2AuthorizationRequestResolver
{
private final OAuth2AuthorizationRequestResolver oAuth2AuthorizationRequestResolver;
/**
* Wraps a OAuth2AuthorizationRequestResolver so that calls to the resolve method can be delegated.
* In our implementation, the value passed here will be a DefaultOAuth2AuthorizationRequestResolver object.
* #param oAuth2AuthorizationRequestResolver usually the default resolver from Spring.
*/
public ConfigurableOAuth2AuthorizationRequestResolver(OAuth2AuthorizationRequestResolver oAuth2AuthorizationRequestResolver)
{
this.oAuth2AuthorizationRequestResolver = oAuth2AuthorizationRequestResolver;
}
/**
* Adds our custom code to check for extra parameters in the session. We use the session because there
* are several redirects to the browser causing any request variable to be lost. Don't really like
* storing anything in the HTTP session but OK as long as it is removed immediately after use.
*
* #param request needed for resolve method delegation and to retrieve the HTTP session.
* #return the <code>OAuth2AuthorizationRequest</code>
*/
#Override
public OAuth2AuthorizationRequest resolve(HttpServletRequest request)
{
OAuth2AuthorizationRequest authorizationRequest = this.oAuth2AuthorizationRequestResolver.resolve(request);
return processAdditionalParameters(request, authorizationRequest);
}
/**
* Required method for implementation but not used in the standard use case.
*
* #param request needed for resolve method delegation and to retrieve the HTTP session.
* #param clientRegistrationId (e. g. google, okta)
* #return the <code>OAuth2AuthorizationRequest</code>
*/
#Override
public OAuth2AuthorizationRequest resolve(HttpServletRequest request, String clientRegistrationId)
{
OAuth2AuthorizationRequest authorizationRequest = this.oAuth2AuthorizationRequestResolver.resolve(request, clientRegistrationId);
return processAdditionalParameters(request, authorizationRequest);
}
/**
* This method does the important task of appending any special query string parameters to the authorization
* request. For now, we are only looking for login_hint in the session. This method can be changed to
* support more parameters. We expect the login_hint key to be found in the session.
*
* #param request needed to retrieve the HTTP session.
* #param authorizationRequest can be null as a valid scenario. Not null when registrationId matches Okta (or whatever).
* #return the <code>OAuth2AuthorizationRequest</code>
*/
private OAuth2AuthorizationRequest processAdditionalParameters(HttpServletRequest request, OAuth2AuthorizationRequest authorizationRequest)
{
if (authorizationRequest == null)
{
return null;
}
// NOTE: this can be improved to support multiple parameters by storing a list instead of a single param
Map<String, Object> additionalParameters = new HashMap<>(authorizationRequest.getAdditionalParameters());
HttpSession session = request.getSession();
additionalParameters.put(SSO_OAUTH2_AUTH_PARAM_LOGIN_HINT, session.getAttribute(SSO_OAUTH2_AUTH_PARAM_LOGIN_HINT));
session.removeAttribute(SSO_OAUTH2_AUTH_PARAM_LOGIN_HINT); // remove immediately after use
String customAuthorizationRequestUri = UriComponentsBuilder
.fromUriString(authorizationRequest.getAuthorizationRequestUri())
.queryParam(SSO_OAUTH2_AUTH_PARAM_LOGIN_HINT, additionalParameters.get(SSO_OAUTH2_AUTH_PARAM_LOGIN_HINT))
.build(true).toUriString();
return OAuth2AuthorizationRequest.from(authorizationRequest)
.additionalParameters(additionalParameters)
.authorizationRequestUri(customAuthorizationRequestUri)
.build();
}
}
The next bit of code shows how to integrate your resolver instead of Spring's default.
#Override
protected void configure(HttpSecurity http) throws Exception
{
logger.info("Configuring OAuth/OIDC HTTP security ...");
http
.oauth2Login()
.authorizationEndpoint()
.authorizationRequestResolver(this.authorizationRequestResolver()) // adds custom resolver
}
#Bean
public OAuth2AuthorizationRequestResolver authorizationRequestResolver()
{
OAuth2AuthorizationRequestResolver defaultAuthorizationRequestResolver = new DefaultOAuth2AuthorizationRequestResolver(
yourClientRegistrationRepository, OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI);
return new ConfigurableOAuth2AuthorizationRequestResolver(defaultAuthorizationRequestResolver);
}
Is this the "best" approach, IDK. As of 5.1, this is the accepted approach IMHO.
Definitely borrowed heavily from Jim Kennedy's post as well as from https://www.baeldung.com/spring-security-custom-oauth-requests.
/**
* Overriding DefaultOAuth2AuthorizationRequestResolver behavior to add login_hint paramter.
*
* #see https://www.baeldung.com/spring-security-custom-oauth-requests
*/
public class CustomAuthorizationRequestResolver implements OAuth2AuthorizationRequestResolver {
public static final String SSO_OAUTH2_AUTH_PARAM_LOGIN_HINT = "SSO_OAUTH2_AUTH_PARAM_LOGIN_HINT";
private OAuth2AuthorizationRequestResolver defaultResolver;
public CustomAuthorizationRequestResolver(ClientRegistrationRepository repo, String authorizationRequestBaseUri) {
defaultResolver = new DefaultOAuth2AuthorizationRequestResolver(repo, authorizationRequestBaseUri);
}
#Override
public OAuth2AuthorizationRequest resolve(HttpServletRequest request) {
OAuth2AuthorizationRequest req = defaultResolver.resolve(request);
if (req != null) {
req = customizeAuthorizationRequest(request, req);
}
return req;
}
#Override
public OAuth2AuthorizationRequest resolve(HttpServletRequest request, String clientRegistrationId) {
OAuth2AuthorizationRequest req = defaultResolver.resolve(request, clientRegistrationId);
if (req != null) {
req = customizeAuthorizationRequest(request, req);
}
return req;
}
private OAuth2AuthorizationRequest customizeAuthorizationRequest(HttpServletRequest request, OAuth2AuthorizationRequest req) {
Map<String, Object> additionalParameters = new HashMap<String, Object>(req.getAdditionalParameters());
// add login_hint
copySessionAttributeValueToParameter(SSO_OAUTH2_AUTH_PARAM_LOGIN_HINT, "login_hint", additionalParameters, request);
return OAuth2AuthorizationRequest
.from(req)
.additionalParameters(additionalParameters)
.build();
}
private void copySessionAttributeValueToParameter(String attrName, String paramName, Map<String, Object> additionalParameters, HttpServletRequest request) {
Object attrValue = getAndDeleteSessionAttributeValue(attrName, request);
if (attrValue != null) {
additionalParameters.put(paramName, attrValue);
}
}
private Object getAndDeleteSessionAttributeValue(String attrName, HttpServletRequest request) {
HttpSession session = request.getSession();
Object attrValue = session.getAttribute(attrName);
session.removeAttribute(attrName);
return attrValue;
}
}
SecurityConfig.java:
http
.oauth2Login()
.authorizationEndpoint()
.authorizationRequestResolver(new CustomAuthorizationRequestResolver(clientRegistrationRepository, OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI));
I am new to JSF 2.0 / PrimeFaces and I've created one webapp using JSF 2.0 + Spring 4. For session timeout, I've done below mapping in web.xml :
<session-config>
<session-timeout>1</session-timeout>
</session-config>
<error-page>
<exception-type>javax.faces.application.ViewExpiredException</exception-type>
<location>/resources/login/timeout.xhtml</location>
</error-page>
After login, user redirects to admin.xhtml in which there element as
<h:link value="showAnotherPage" outcome="other.xhtml"/>
But after 2 or 5 minutes, when I click on the link, it redirects me to other.xhtml page not on timeout page.
Is anything that I'm missing to configure? Please help.
I had this problem. You need to make sure there is no client call polling (ajax) server side resource, if this happens the session will not expire. I had a <p:poll /> tag that didn't let the session expire.
The ViewExpiredException will be thrown whenever the javax.faces.STATE_SAVING_METHOD is set to server (default) and the enduser sends a HTTP POST request on a view via <h:form> with <h:commandLink>, <h:commandButton> or <f:ajax>, while the associated view state isn't available in the session anymore.
The <h:link> will fire a full GET request so no POST request.
A good post with a more in-depth explanation can be found here
I'd personally implement it in another way.
Define an Exception handler factory in your faces-config.xml as follows:
<factory>
<exception-handler-factory>
com.package.faces.FullAjaxExceptionHandlerFactory
</exception-handler-factory>
</factory>
Create the exception handler factory that extends javax.faces.context.ExceptionHandlerFactory. It should return your own implementation of the ExceptionHandler. This could be an example:
import javax.faces.context.ExceptionHandler;
import javax.faces.context.ExceptionHandlerFactory;
public class FullAjaxExceptionHandlerFactory extends ExceptionHandlerFactory {
private ExceptionHandlerFactory wrapped;
/**
* Construct a new full ajax exception handler factory around the given wrapped factory.
* #param wrapped The wrapped factory.
*/
public FullAjaxExceptionHandlerFactory(ExceptionHandlerFactory wrapped) {
this.wrapped = wrapped;
}
/**
* Returns a new instance of {#link FullAjaxExceptionHandler} which wraps the original exception handler.
*/
#Override
public ExceptionHandler getExceptionHandler() {
return new FullAjaxExceptionHandler(wrapped.getExceptionHandler());
}
/**
* Returns the wrapped factory.
*/
#Override
public ExceptionHandlerFactory getWrapped() {
return wrapped;
}
}
In the end, extend the javax.faces.context.ExceptionHandlerWrapper to handle all exceptions. An example is the following:
public class FullAjaxExceptionHandler extends ExceptionHandlerWrapper {
private ExceptionHandler wrapped;
public FullAjaxExceptionHandler(ExceptionHandler wrapped) {
this.wrapped = wrapped;
}
private static Throwable extractCustomException(Throwable ex) {
Throwable t = ex;
while (t != null) {
if (t instanceof YourOwnExceptionInterface) {
return t;
}
t = t.getCause();
}
return ex;
}
private static String extractMessage(Throwable t) {
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
t.printStackTrace(pw);
return matchJmillErrorTag(sw.toString());
}
public static boolean handleException(Throwable original) {
Throwable ex = extractCustomException(original);
if (ex instanceof ViewExpiredException) {
// redirect to login page
return false;
} else if (ex instanceof YourOwnExceptionInterface) {
((YourOwnExceptionInterface) ex).handle();
return true;
} else if (ex instanceof NonexistentConversationException) {
FacesContext.getCurrentInstance().getExternalContext().invalidateSession();
// redirect to login page
return false;
} else {
String message = extractMessage(ex);
final FacesContext fc = FacesContext.getCurrentInstance();
original.printStackTrace();
// redirect to error page
fc.responseComplete();
return true;
}
}
#Override
public void handle() throws FacesException {
final Iterator<ExceptionQueuedEvent> i = getUnhandledExceptionQueuedEvents().iterator();
FacesContext facesContext = FacesContext.getCurrentInstance();
if (Redirector.isRedirectingToLogin(facesContext)) {
return;
}
while (i.hasNext()) {
ExceptionQueuedEvent event = i.next();
ExceptionQueuedEventContext context = (ExceptionQueuedEventContext) event.getSource();
i.remove();
if (!handleException(context.getException())) {
return;
}
}
getWrapped().handle();
}
#Override
public ExceptionHandler getWrapped() {
return wrapped;
}
}
Check the public static boolean handleException(Throwable original) in the previous class. You could use that to manage all your exceptions.
At one point I put a condition there about YourOwnExceptionInterface which is an interface with a handle() method that I'd implement for example by a NotAuthorizedException kind of exception. In this case in the handle() method of the NotAuthorizedException I'd notify the user that he can't complete a certain operation through a p:growl component for example. I'd use this in my beans as throw new NotAuthorizedException("message");
The custom exception class should of course extend the RuntimeException.
I have implement custom AuthenticationProvider spring login security. When i post it using its /login url it gives me 302 HTTP status code. whats the issue in that i have on custom filter for every request for handle every request
Filter
public class AppFilter implements Filter {
private Logger logger = Logger.getLogger(AppFilter.class);
private CommonService commonService;
public AppFilter() {}
public void destroy() {}
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
try {
if (httpServletRequest.getRequestURI().contains("/resources/")) {
filterChain.doFilter(httpServletRequest, servletResponse);
}
else {
HttpSession session = httpServletRequest.getSession();
if(session != null && session.getAttribute(CommonConstant.XXX) == null) {
List<Map<String, Object>> clientDetails = commonService.getXXX(XX);
if (clientDetails != null && !clientDetails.isEmpty()){
session.setAttribute(CommonConstant.CLIENT_SESSION_DATABEAN, clientSessionDataBean);
}
}
HttpServletRequest request = new CustomHttpServletRequestWrapper(httpServletRequest);
filterChain.doFilter(request, servletResponse);
}
} catch (Exception e) {
logger.error(httpServletRequest.getRequestURI(), e);
servletRequest.getRequestDispatcher("/WEB-INF/views/common/unauthorized-access.jsp").forward(servletRequest, servletResponse);
}
}
public void init(FilterConfig arg0) throws ServletException {
ApplicationContext ctx = WebApplicationContextUtils.getRequiredWebApplicationContext(arg0.getServletContext());
commonService = ctx.getBean(CommonService.class);
}
class CustomHttpServletRequestWrapper extends HttpServletRequestWrapper {
private Map<String, String[]> allReqParam;
public CustomHttpServletRequestWrapper(HttpServletRequest request) throws Exception {
super(request);
allReqParam = new TreeMap<String, String[]>();
if(ServletFileUpload.isMultipartContent(request)) {
allReqParam = request.getParameterMap();
List<FileItem> multipartItems = new ServletFileUpload(new DiskFileItemFactory()).parseRequest(request);
for (FileItem multipartItem : multipartItems) {
if (multipartItem.isFormField()) {
processFormField(multipartItem, allReqParam);
} else {
processFileField(multipartItem, request);
}
}
} else {
Map<String, String[]> reqParamMap = super.getParameterMap();
if(reqParamMap != null && !reqParamMap.isEmpty()) {
for(Entry<String, String[]> entry : reqParamMap.entrySet()) {
String paramName = entry.getKey();
String[] paramValue = entry.getValue();
identifyHiddenFields(paramName, paramValue);
}
}
}
}
/**
* Process multipart request item as regular form field. The name and value of each regular
* form field will be added to the given parameterMap.
* #param formField The form field to be processed.
* #param parameterMap The parameterMap to be used for the HttpServletRequest.
* #throws Exception
*/
private void processFormField(FileItem formField, Map<String, String[]> parameterMap) throws Exception {
String name = formField.getFieldName();
String value = formField.getString();
String[] values = null;
if(parameterMap != null && !parameterMap.isEmpty()) {
values = parameterMap.get(name);
}
if (values == null) {
identifyHiddenFields(name, new String[] {value});
} else {
int length = values.length;
String[] newValues = new String[length + 1];
System.arraycopy(values, 0, newValues, 0, length);
newValues[length] = value;
allReqParam.put(name, newValues);
identifyHiddenFields(name, newValues);
}
}
/**
* Process multipart request item as file field. The name and FileItem object of each file field
* will be added as attribute of the given HttpServletRequest. If a FileUploadException has
* occurred when the file size has exceeded the maximum file size, then the FileUploadException
* will be added as attribute value instead of the FileItem object.
* #param fileField The file field to be processed.
* #param request The involved HttpServletRequest.
* #throws IOException
*/
private void processFileField(FileItem fileField, HttpServletRequest request) throws IOException {
request.setAttribute(fileField.getFieldName(), fileField);
allReqParam.put(fileField.getFieldName(), new String[] {fileField.toString()});
}
private void identifyHiddenFields(String paramName, String[] paramValue) throws Exception {
if(paramName.startsWith("hidden")) {
String[] tParamVal = new String[paramValue.length];
for(int i=0; i < paramValue.length; i++) {
tParamVal[i] = AppCryptoUtil.decryptHV(paramValue[i]);
}
allReqParam.put(paramName.substring(6), tParamVal);
} else {
allReqParam.put(paramName, paramValue);
}
}
#Override
public String getParameter(final String name) {
String[] strings = allReqParam.get(name);
if (strings != null) {
return strings[0];
}
return super.getParameter(name);
}
#Override
public Map<String, String[]> getParameterMap() {
return Collections.unmodifiableMap(allReqParam);
}
#Override
public Enumeration<String> getParameterNames() {
return Collections.enumeration(getParameterMap().keySet());
}
#Override
public String[] getParameterValues(final String name) {
return getParameterMap().get(name);
}
}
}
i am wondering how i could implement a post logout redirection using a custom logout handler. I have implemented a CustomLogoutSuccessHandler but i have no way off access http session data that has previous been set by the user who has logged in. The data is alway empty...
class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
private static final ThreadLocal<Authentication> AUTH_HOLDER = new ThreadLocal<Authentication>()
void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
AUTH_HOLDER.set authentication
// reading session variable...
request.session?.variable // but this is always empty
try {
super.handle(request, response, authentication)
}
finally {
AUTH_HOLDER.remove()
}
}
#Override
protected String determineTargetUrl(HttpServletRequest request, HttpServletResponse response) {
Authentication auth = AUTH_HOLDER.get()
String url = super.determineTargetUrl(request, response)
// do something with the url based on session data..
url
}
}
I do not know if there is any easy way to do this but came up with the below solution.
All you have to do is set the setTargetUrlParameter in your LogoutSuccessHandler. For that I made use of the implementation of HttpServletRequestWrapper written by Lincoln Baxter, III here for adding a parameter to the current request. Here is the relevant code.
public class PrettyFacesWrappedRequest extends HttpServletRequestWrapper
{
private final Map<String, String[]> modifiableParameters;
private Map<String, String[]> allParameters = null;
/**
* Create a new request wrapper that will merge additional parameters into
* the request object without prematurely reading parameters from the
* original request.
*
* #param request
* #param additionalParams
*/
public PrettyFacesWrappedRequest(final HttpServletRequest request,
final Map<String, String[]> additionalParams)
{
super(request);
modifiableParameters = new TreeMap<String, String[]>();
modifiableParameters.putAll(additionalParams);
}
#Override
public String getParameter(final String name)
{
String[] strings = getParameterMap().get(name);
if (strings != null)
{
return strings[0];
}
return super.getParameter(name);
}
#Override
public Map<String, String[]> getParameterMap()
{
if (allParameters == null)
{
allParameters = new TreeMap<String, String[]>();
allParameters.putAll(super.getParameterMap());
allParameters.putAll(modifiableParameters);
}
//Return an unmodifiable collection because we need to uphold the interface contract.
return Collections.unmodifiableMap(allParameters);
}
#Override
public Enumeration<String> getParameterNames()
{
return Collections.enumeration(getParameterMap().keySet());
}
#Override
public String[] getParameterValues(final String name)
{
return getParameterMap().get(name);
}
}
and then in the CustomLogoutSuccessHandler, I add this targetUrl as the parameter like this:
#Component
public class MyCustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
#Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
HttpServletRequest wrappedRequest = request;
if (authentication != null) {
//do something with the Principal and add the corresponding url
Map<String, String[]> extraParams = new TreeMap<String, String[]>();
extraParams.put("targetUrl", new String[] {"/target.xhtml"});
wrappedRequest = new PrettyFacesWrappedRequest(request, extraParams);
setTargetUrlParameter("targetUrl");
}
setDefaultTargetUrl("/general/main.xhtml");
super.onLogoutSuccess(wrappedRequest, response, authentication);
}
}
and the relevant change to the applicationContext:
<http>
<logout logout-url="/j_spring_security_logout"
success-handler-ref="myCustomLogoutSuccessHandler"
invalidate-session="true"/>
</http>
<beans:bean id="myCustomLogoutSuccessHandler" class="com.examples.MyCustomLogoutSuccessHandler"/>