Skip to content

Commit

Permalink
GEODE-7851: Pulse refreshes expired access tokens (apache#4977)
Browse files Browse the repository at this point in the history
If a user's access token expires, Pulse attempts to refresh it. If the
refresh fails, Pulse logs the user out and redirects the browser to
/pulse/clusterLogout.

Changes in Repository:
- When OAuth is configured, before returning the user's cluster,
  getCluster() checks whether the user's access token has expired.
- If the access token has expired, the repository attempts to refresh
  it.  If the refresh succeeds, the repository reconnects the user's
  cluster to JMX and returns it.
- If the refresh fails, the repository disconnects the user's cluster
  from JMX, removes the cluster from the repository, and throws an
  authentication or authorization exception.

Changes in PulseController:
- If the service call throws an authentication or authorization
  exception, PulseController.  getPulseUpdate() returns a 401 status.

Changes in pulsescript/common.js:
- If a Pulse ajax call returns a 401 status, ajaxPost() redirects the
  browser to /pulse/clusterLogout to log the user out and request
  re-authorization.

Co-authored-by: Joris Melchior <[email protected]>
Co-authored-by: Dale Emery <[email protected]>
Co-authored-by: Jinmei Liao <[email protected]>

Co-authored-by: Kirk Lund <[email protected]>
Co-authored-by: Joris Melchior <[email protected]>
Co-authored-by: Jinmei Liao <[email protected]>
  • Loading branch information
4 people authored Apr 24, 2020
1 parent 0a1701e commit 2999414
Show file tree
Hide file tree
Showing 5 changed files with 251 additions and 113 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@
import java.time.Duration;
import java.time.Instant;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

Expand All @@ -45,8 +44,9 @@
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.security.oauth2.core.user.OAuth2UserAuthority;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.ContextConfiguration;
Expand Down Expand Up @@ -85,53 +85,67 @@ public void setup() {
}

@Test
public void usesCurrentSessionAccessTokenAsCredentialToConnectToGemFire() throws Exception {
String userName = "some-user-name";
String accessTokenValue = "the-access-token-value";
String urlThatTriggersPulseToConnectToGemFire = "/dataBrowserRegions";
public void usesCurrentSessionAccessTokenValueAsCredentialToConnectToGemFire() throws Exception {
String subject = "some-subject";

Cluster clusterForUser = mock(Cluster.class);
when(clusterFactory.create(any(), any(), eq(userName), any(), any()))
.thenReturn(clusterForUser);
when(clusterFactory.create(any(), any(), eq(subject), any(), any())).thenReturn(clusterForUser);

MockHttpSession session = sessionWithAuthenticatedUser(userName, accessTokenValue);
String tokenValue = "the-token-value";
MockHttpSession session = sessionWithAuthenticatedUser("some-user-name", subject, tokenValue);

String urlThatTriggersPulseToConnectToGemFire = "/dataBrowserRegions";
mvc.perform(get(urlThatTriggersPulseToConnectToGemFire).session(session));

verify(clusterForUser).connectToGemFire(accessTokenValue);
verify(clusterForUser).connectToGemFire(tokenValue);
}

private MockHttpSession sessionWithAuthenticatedUser(String userName, String subject,
String tokenValue) {
OAuth2AccessToken accessToken = accessToken(tokenValue);
OidcIdToken idToken = idToken(userName, subject, accessToken);
OAuth2AuthenticationToken authenticationToken = authenticationToken(idToken);
authorizeClient(authenticationToken, accessToken);
return sessionWithAuthenticationToken(authenticationToken);
}

private void authorizeClient(
OAuth2AuthenticationToken authenticationToken, OAuth2AccessToken accessToken) {
private static OAuth2AuthenticationToken authenticationToken(OidcIdToken idToken) {
List<GrantedAuthority> userAuthorities = allGeodeAuthorities(idToken.getClaims());
OidcUser user = new DefaultOidcUser(userAuthorities, idToken);
return new OAuth2AuthenticationToken(user, userAuthorities, AUTHENTICATION_PROVIDER_ID);
}

private void authorizeClient(OAuth2AuthenticationToken authenticationToken,
OAuth2AccessToken accessToken) {
String userName = authenticationToken.getPrincipal().getName();
OAuth2AuthorizedClient authorizedClient =
new OAuth2AuthorizedClient(clientRegistration(),
authenticationToken.getPrincipal().getName(), accessToken);
new OAuth2AuthorizedClient(clientRegistration(), userName, accessToken);
authorizedClientService.saveAuthorizedClient(authorizedClient, authenticationToken);
}

private MockHttpSession sessionWithAuthenticatedUser(String username, String tokenValue) {
OAuth2AuthenticationToken authenticationToken = authenticationToken(username);
authorizeClient(authenticationToken, accessToken(tokenValue));
return sessionWithAuthenticationToken(authenticationToken);
private static OidcIdToken idToken(String userName, String subject,
OAuth2AccessToken accessToken) {
return OidcIdToken.withTokenValue(accessToken.getTokenValue())
.subject(subject)
.claim("user_name", userName)
.issuedAt(accessToken.getIssuedAt())
.expiresAt(accessToken.getExpiresAt())
.build();
}

private static OAuth2AccessToken accessToken(String tokenValue) {
return new OAuth2AccessToken(BEARER, tokenValue, Instant.now(),
Instant.now().plus(Duration.ofHours(1)));
Instant issuedAt = Instant.now();
Instant expiresAt = issuedAt.plus(Duration.ofDays(12));
return new OAuth2AccessToken(BEARER, tokenValue, issuedAt, expiresAt);
}

private static OAuth2AuthenticationToken authenticationToken(String userName) {
Map<String, Object> attributes = new HashMap<>();
attributes.put("sub", userName);

List<GrantedAuthority> authorities = Arrays.asList(
new OAuth2UserAuthority("ROLE_USER", attributes),
new OAuth2UserAuthority("SCOPE_CLUSTER:READ", attributes),
new OAuth2UserAuthority("SCOPE_CLUSTER:WRITE", attributes),
new OAuth2UserAuthority("SCOPE_DATA:READ", attributes),
new OAuth2UserAuthority("SCOPE_DATA:WRITE", attributes));
OAuth2User user = new DefaultOAuth2User(authorities, attributes, "sub");
return new OAuth2AuthenticationToken(user, authorities, AUTHENTICATION_PROVIDER_ID);
private static List<GrantedAuthority> allGeodeAuthorities(Map<String, Object> userAttributes) {
return Arrays.asList(
new OAuth2UserAuthority("ROLE_USER", userAttributes),
new OAuth2UserAuthority("SCOPE_CLUSTER:READ", userAttributes),
new OAuth2UserAuthority("SCOPE_CLUSTER:WRITE", userAttributes),
new OAuth2UserAuthority("SCOPE_DATA:READ", userAttributes),
new OAuth2UserAuthority("SCOPE_DATA:WRITE", userAttributes));
}

private static ClientRegistration clientRegistration() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2AuthorizationException;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
Expand Down Expand Up @@ -106,6 +109,10 @@ public void getPulseUpdate(HttpServletRequest request, HttpServletResponse respo
try {
PulseService pulseService = pulseServiceFactory.getPulseServiceInstance(serviceName);
responseMap.set(serviceName, pulseService.execute(request));
} catch (OAuth2AuthenticationException | OAuth2AuthorizationException e) {
logger.warn("serviceException [for service {}] = {}", serviceName, e.getMessage());
response.setStatus(HttpStatus.UNAUTHORIZED.value());
return;
} catch (Exception serviceException) {
logger.warn("serviceException [for service {}] = {}", serviceName,
serviceException.getMessage());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

package org.apache.geode.tools.pulse.internal.data;

import java.io.IOException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
Expand Down Expand Up @@ -2759,6 +2760,17 @@ public boolean deleteQueryById(String userId, String queryId) {
return getDataBrowser().deleteQueryById(userId, queryId);
}

public void reconnectToGemFire(Object credentials) {
if (jmxConnector != null) {
try {
jmxConnector.close();
} catch (IOException e) {
logger.info("Could not close old connection on reconnect attempt", e);
}
jmxConnector = updater.connect(credentials);
}
}

public void connectToGemFire(Object credentials) {
jmxConnector = updater.connect(credentials);

Expand Down
Loading

0 comments on commit 2999414

Please sign in to comment.