Skip to content

Commit

Permalink
GEODE-7851: Pulse logout requests end of OAuth session
Browse files Browse the repository at this point in the history
When Pulse is configured to use OAuth, and a user logs out of Pulse,
Pulse redirects the browser to a page where the user can take action to
end their session. The available actions depend on the OAuth provider,
but may include revoking the token or logging out of the OAuth provider
entirely.

Main changes:

- Changed OAuthSecurityConfig to install two logout handlers: A
  RepositoryLogoutHandler (renamed from LogoutHandler) and an
  OidcClientInitiatedLogoutSuccessHandler.

- Added a pulse.security.oauth.endSessionEndpoint property to specify
  the URL to which the OidcClientInitiatedLogoutSuccessHandler should
  redirect the browser on logout.

- Configured the OAuthSecurityConfig to add the "end session endpoint"
  property value to the client configuration metadata.  On logout, the
  OidcClientInitiatedLogoutSuccessHandler redirects the browser to this
  endpoint, where the user can take action to end the session.

- In the OAuthClientConfig class (extracted from OAuthSecurityConfig),
  restored the code to explicitly list the scopes that Pulse is
  requesting, in particular to list "openid" in the scopes. Though
  authentication works just fine without that explicit list, the
  OidcClientInitiatedLogoutSuccessHandler does not. The
  OidcClientInitiatedLogoutSuccessHandler handles logout only if the the
  principal is an OidcUser. If "openid" is not explicitly listed in the
  client's scopes. Spring creates OAuth2User principals instead of
  OidcUser principals, and OidcClientInitiatedLogoutSuccessHandler
  return without redirecting the browser.

Also refactored to support the above changes:

- Moved the oauth client service configuration from OAuthSecurityConfig
  to a new OAuthClientConfig class. This breaks Respository's dependence
  on OAuthSecurityConfig, which in turn (through the LogoutHandler)
  depended on Repository. Repository now gets its
  OAuth2AuthorizedClientService from the OAuthClientConfig class, which
  does not in turn depend on Repository.

- Marked two Repository constructors as non-required. Spring will pick
  whichever one has the most dependencies it can satisfy. So if the
  profile specifies an OAuth2AuthorizedClientService, Spring will call
  the constructor that takes one of those. Otherwise Spring will call
  the no-args constructor.

- Renamed LogoutHandler to RepositoryLogoutHandler to better reflect its
  specific responsibilities.

- Changed RepositoryLogoutHandler to implement LogoutHandler instead of
  LogoutSuccessHandler. Now it does its work *during* logout instead of
  *after.*

- Changed DefaultSecurityConfig to specify the logout success URL
  directly instead of via a logout success handler. (OAuthSecurityConfig
  no longer needs a logout success URL, because the OIDC logout handler
  redirects to the OAuth provider instead.)

Co-authored-by: Dale Emery <[email protected]>
Co-authored-by: Joris Melchior <[email protected]>
  • Loading branch information
demery-pivotal and jmelchio committed Apr 16, 2020
1 parent 88b3603 commit f9d9479
Show file tree
Hide file tree
Showing 12 changed files with 191 additions and 163 deletions.
5 changes: 5 additions & 0 deletions geode-docs/tools_modules/pulse/pulse-auth.html.md.erb
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,10 @@ After you set up the authentication provider properly, create a properties file
The URI for your OAuth provider's JSON Web Key (JWK) Set endpoint.
- **pulse.oauth.endSessionEndpoint**
The URI for your OAuth provider's endpoint to request that the End-User be logged out. See the `end_session_endpoint` parameter of the OpenID Provider Discovery Metadata standard proposal.
- **pulse.oauth.userNameAttributeName**
The attribute name used to access the user's name from your OAuth provider's user info response.
Expand All @@ -169,6 +173,7 @@ pulse.oauth.authorizationUri=http://example.com/uaa/oauth/authorize
pulse.oauth.tokenUri=http://example.com/uaa/oauth/token
pulse.oauth.userInfoUri=http://example.com/uaa/userinfo
pulse.oauth.jwkSetUri=http://example.com/uaa/token_keys
pulse.oauth.endSessionEndpoint=http://example.com/uaa/profile
pulse.oauth.userNameAttributeName=user_name
```
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,19 +59,18 @@ public class Repository {
Locale locale =
new Locale(PulseConstants.APPLICATION_LANGUAGE, PulseConstants.APPLICATION_COUNTRY);

private ResourceBundle resourceBundle =
private final ResourceBundle resourceBundle =
ResourceBundle.getBundle(PulseConstants.LOG_MESSAGES_FILE, locale);

private PulseConfig pulseConfig = new PulseConfig();
private final PulseConfig pulseConfig = new PulseConfig();

@Autowired(required = false)
public Repository() {
this(null);
}

// The authorizedClientService is required only when using OAuth2 security.
@Autowired
public Repository(
@Autowired(required = false) OAuth2AuthorizedClientService authorizedClientService) {
@Autowired(required = false)
public Repository(OAuth2AuthorizedClientService authorizedClientService) {
this(authorizedClientService, Cluster::new);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,26 @@
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;


/**
* Configures Pulse to use the authentication manager defined by the
* {@code pulse-authentication-custom.xml} file, which <em>must</em> define an authentication
* manager. This configuration is applied when the {@code pulse.authentication.custom} profile is
* active.
*/
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
@Profile("pulse.authentication.custom")
@ImportResource("classpath:pulse-authentication-custom.xml")
public class CustomSecurityConfig extends DefaultSecurityConfig {
// the pulse-authentication-custom.xml should configure an <authentication-manager>
private final AuthenticationManager authenticationManager;

@Autowired
private AuthenticationManager authenticationManager;
CustomSecurityConfig(AuthenticationManager authenticationManager,
RepositoryLogoutHandler repositoryLogoutHandler) {
super(repositoryLogoutHandler);
this.authenticationManager = authenticationManager;
}

@Override
protected void configure(AuthenticationManagerBuilder authenticationManagerBuilder) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import java.util.HashMap;
import java.util.Map;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
Expand All @@ -33,14 +34,20 @@
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.ExceptionMappingAuthenticationFailureHandler;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
@Profile("pulse.authentication.default")
public class DefaultSecurityConfig extends WebSecurityConfigurerAdapter {

private final RepositoryLogoutHandler repositoryLogoutHandler;

@Autowired
DefaultSecurityConfig(RepositoryLogoutHandler repositoryLogoutHandler) {
this.repositoryLogoutHandler = repositoryLogoutHandler;
}

@Bean
public AuthenticationFailureHandler failureHandler() {
ExceptionMappingAuthenticationFailureHandler exceptionMappingAuthenticationFailureHandler =
Expand All @@ -55,11 +62,6 @@ public AuthenticationFailureHandler failureHandler() {
return exceptionMappingAuthenticationFailureHandler;
}

@Bean
public LogoutSuccessHandler logoutHandler() {
return new LogoutHandler("/login.html");
}

@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity.authorizeRequests(authorize -> authorize
Expand All @@ -78,7 +80,8 @@ protected void configure(HttpSecurity httpSecurity) throws Exception {
.defaultSuccessUrl("/clusterDetail.html", true))
.logout(logout -> logout
.logoutUrl("/clusterLogout")
.logoutSuccessHandler(logoutHandler()))
.addLogoutHandler(repositoryLogoutHandler)
.logoutSuccessUrl("/login.html"))
.exceptionHandling(exception -> exception
.accessDeniedPage("/accessDenied.html"))
.headers(header -> header
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,9 @@
*/
public class GemFireAuthentication extends UsernamePasswordAuthenticationToken {

private JMXConnector jmxc = null;

public GemFireAuthentication(Object principal, Object credentials,
Collection<GrantedAuthority> list, JMXConnector jmxc) {
Collection<GrantedAuthority> list) {
super(principal, credentials, list);
this.jmxc = jmxc;
}

private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,15 @@

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Profile;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.stereotype.Component;

import org.apache.geode.tools.pulse.internal.data.Repository;

Expand All @@ -36,11 +39,13 @@
*
* @since GemFire version 9.0
*/
@Component
@Profile("pulse.authentication.gemfire")
public class GemFireAuthenticationProvider implements AuthenticationProvider {

private static final Logger logger = LogManager.getLogger();
private final Repository repository;

@Autowired
public GemFireAuthenticationProvider(Repository repository) {
this.repository = repository;
}
Expand All @@ -49,8 +54,9 @@ public GemFireAuthenticationProvider(Repository repository) {
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
if (authentication instanceof GemFireAuthentication) {
GemFireAuthentication gemAuth = (GemFireAuthentication) authentication;
if (gemAuth.isAuthenticated())
if (gemAuth.isAuthenticated()) {
return gemAuth;
}
}

String name = authentication.getName();
Expand All @@ -65,7 +71,7 @@ public Authentication authenticate(Authentication authentication) throws Authent

Collection<GrantedAuthority> list = GemFireAuthentication.populateAuthorities(jmxc);
GemFireAuthentication auth = new GemFireAuthentication(authentication.getPrincipal(),
authentication.getCredentials(), list, jmxc);
authentication.getCredentials(), list);
logger.debug("For user " + name + " authList=" + list);
return auth;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,28 +18,27 @@
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;

import org.apache.geode.tools.pulse.internal.data.Repository;

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
@Profile("pulse.authentication.gemfire")
public class GemfireSecurityConfig extends DefaultSecurityConfig {

private final Repository repository;
private final AuthenticationProvider authenticationProvider;

@Autowired
public GemfireSecurityConfig(Repository repository) {
this.repository = repository;
public GemfireSecurityConfig(GemFireAuthenticationProvider gemFireAuthenticationProvider,
RepositoryLogoutHandler repositoryLogoutHandler) {
super(repositoryLogoutHandler);
authenticationProvider = gemFireAuthenticationProvider;
}

@Override
protected void configure(AuthenticationManagerBuilder authenticationManagerBuilder) {
authenticationManagerBuilder
.authenticationProvider(new GemFireAuthenticationProvider(repository));
authenticationManagerBuilder.authenticationProvider(authenticationProvider);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more contributor license
* agreements. See the NOTICE file distributed with this work for additional information regarding
* copyright ownership. The ASF licenses this file to You 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
*
* http://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 org.apache.geode.tools.pulse.internal.security;

import static java.util.Collections.singletonMap;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.context.annotation.PropertySource;
import org.springframework.security.oauth2.client.InMemoryOAuth2AuthorizedClientService;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;
import org.springframework.security.oauth2.client.oidc.web.logout.OidcClientInitiatedLogoutSuccessHandler;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
import org.springframework.security.oauth2.client.web.AuthenticatedPrincipalOAuth2AuthorizedClientRepository;
import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository;
import org.springframework.security.oauth2.core.AuthorizationGrantType;

/**
* Configures Pulse to use the OAuth 2 provider defined by properties in {@code pulse.properties}.
*/
@Configuration
@Profile("pulse.authentication.oauth")
@PropertySource("classpath:pulse.properties")
public class OAuthClientConfig {
@Value("${pulse.oauth.providerId}")
private String providerId;
@Value("${pulse.oauth.providerName}")
private String providerName;
@Value("${pulse.oauth.clientId}")
private String clientId;
@Value("${pulse.oauth.clientSecret}")
private String clientSecret;
@Value("${pulse.oauth.authorizationUri}")
private String authorizationUri;
@Value("${pulse.oauth.tokenUri}")
private String tokenUri;
@Value("${pulse.oauth.userInfoUri}")
private String userInfoUri;
@Value("${pulse.oauth.jwkSetUri}")
private String jwkSetUri;
@Value("${pulse.oauth.endSessionEndpoint}")
private String endSessionEndpoint;
@Value("${pulse.oauth.userNameAttributeName}")
private String userNameAttributeName;

@Bean
ClientRegistration clientRegistration() {
return ClientRegistration.withRegistrationId(providerId)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.redirectUriTemplate("{baseUrl}/login/oauth2/code/{registrationId}")
.clientId(clientId)
.clientSecret(clientSecret)
.scope("openid", "CLUSTER:READ", "CLUSTER:WRITE", "DATA:READ", "DATA:WRITE")
.authorizationUri(authorizationUri)
.tokenUri(tokenUri)
.userInfoUri(userInfoUri)
.jwkSetUri(jwkSetUri)
.providerConfigurationMetadata(
singletonMap("end_session_endpoint", endSessionEndpoint))
// When Spring shows the login page, it displays a link to the OAuth provider's
// authorization URI. Spring uses the value passed to clientName() as the text for that
// link. We pass the providerName property here, to let the user know which OAuth provider
// they will be redirected to.
.clientName(providerName)
.userNameAttributeName(userNameAttributeName)
.build();
}

@Bean
public ClientRegistrationRepository clientRegistrationRepository(
ClientRegistration clientRegistration) {
return new InMemoryClientRegistrationRepository(clientRegistration);
}

@Bean
public OAuth2AuthorizedClientService authorizedClientService(
ClientRegistrationRepository clientRegistrationRepository) {
return new InMemoryOAuth2AuthorizedClientService(clientRegistrationRepository);
}

@Bean
public OAuth2AuthorizedClientRepository authorizedClientRepository(
OAuth2AuthorizedClientService authorizedClientService) {
return new AuthenticatedPrincipalOAuth2AuthorizedClientRepository(authorizedClientService);
}

@Bean
public OidcClientInitiatedLogoutSuccessHandler oidcLogoutHandler(
ClientRegistrationRepository clientRegistrationRepository) {
return new OidcClientInitiatedLogoutSuccessHandler(clientRegistrationRepository);
}
}
Loading

0 comments on commit f9d9479

Please sign in to comment.