From 2aa12fc0e3cbf1d57d61d96a7a3608bcdbead754 Mon Sep 17 00:00:00 2001 From: Justin Richer Date: Fri, 28 Apr 2017 19:05:30 -0400 Subject: [PATCH] end session endpoint --- .../impl/SelfAssertionValidator.java | 7 +- .../WEB-INF/views/logoutConfirmation.jsp | 55 +++++++ .../main/webapp/WEB-INF/views/postLogout.jsp | 26 +++ .../webapp/WEB-INF/views/requestUserCode.jsp | 2 +- .../connect/web/EndSessionEndpoint.java | 150 +++++++++++++++--- 5 files changed, 214 insertions(+), 26 deletions(-) create mode 100644 openid-connect-server-webapp/src/main/webapp/WEB-INF/views/logoutConfirmation.jsp create mode 100644 openid-connect-server-webapp/src/main/webapp/WEB-INF/views/postLogout.jsp diff --git a/openid-connect-common/src/main/java/org/mitre/jwt/assertion/impl/SelfAssertionValidator.java b/openid-connect-common/src/main/java/org/mitre/jwt/assertion/impl/SelfAssertionValidator.java index cd169c509..1ddf77bbe 100644 --- a/openid-connect-common/src/main/java/org/mitre/jwt/assertion/impl/SelfAssertionValidator.java +++ b/openid-connect-common/src/main/java/org/mitre/jwt/assertion/impl/SelfAssertionValidator.java @@ -25,6 +25,7 @@ import org.mitre.openid.connect.config.ConfigurationPropertiesBean; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; import com.google.common.base.Strings; import com.nimbusds.jwt.JWT; @@ -37,6 +38,7 @@ import com.nimbusds.jwt.SignedJWT; * @author jricher * */ +@Component("selfAssertionValidator") public class SelfAssertionValidator implements AssertionValidator { private static Logger logger = LoggerFactory.getLogger(SelfAssertionValidator.class); @@ -62,16 +64,19 @@ public class SelfAssertionValidator implements AssertionValidator { return false; } + // make sure the issuer exists if (Strings.isNullOrEmpty(claims.getIssuer())) { logger.debug("No issuer for assertion, rejecting"); return false; } - if (claims.getIssuer().equals(config.getIssuer())) { + // make sure the issuer is us + if (!claims.getIssuer().equals(config.getIssuer())) { logger.debug("Issuer is not the same as this server, rejecting"); return false; } + // validate the signature based on our public key if (jwtService.validateSignature((SignedJWT) assertion)) { return true; } else { diff --git a/openid-connect-server-webapp/src/main/webapp/WEB-INF/views/logoutConfirmation.jsp b/openid-connect-server-webapp/src/main/webapp/WEB-INF/views/logoutConfirmation.jsp new file mode 100644 index 000000000..a53199e2f --- /dev/null +++ b/openid-connect-server-webapp/src/main/webapp/WEB-INF/views/logoutConfirmation.jsp @@ -0,0 +1,55 @@ +<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="utf-8"%> +<%@ page import="org.springframework.security.core.AuthenticationException"%> +<%@ page import="org.springframework.security.oauth2.common.exceptions.UnapprovedClientAuthenticationException"%> +<%@ page import="org.springframework.security.web.WebAttributes"%> +<%@ taglib prefix="authz" uri="http://www.springframework.org/security/tags"%> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%> +<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt"%> +<%@ taglib prefix="o" tagdir="/WEB-INF/tags"%> +<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %> +<%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%> + + + + +
+ +
+ +

+ +
+ +
+
+ + +
+ + +   + + + + + + + + + +
+
+ +
+ + +   + +
+
+ +
+ +
+
+ diff --git a/openid-connect-server-webapp/src/main/webapp/WEB-INF/views/postLogout.jsp b/openid-connect-server-webapp/src/main/webapp/WEB-INF/views/postLogout.jsp new file mode 100644 index 000000000..aafd0a0fb --- /dev/null +++ b/openid-connect-server-webapp/src/main/webapp/WEB-INF/views/postLogout.jsp @@ -0,0 +1,26 @@ +<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="utf-8"%> +<%@ taglib prefix="authz" uri="http://www.springframework.org/security/tags"%> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%> +<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt"%> +<%@ taglib prefix="o" tagdir="/WEB-INF/tags"%> +<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %> +<%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%> +<%@ taglib prefix="security" uri="http://www.springframework.org/security/tags"%> + + + + +
+ +
+

+ + +
+
+ +
+
+
+
+ diff --git a/openid-connect-server-webapp/src/main/webapp/WEB-INF/views/requestUserCode.jsp b/openid-connect-server-webapp/src/main/webapp/WEB-INF/views/requestUserCode.jsp index 69a6ed301..9551acbff 100644 --- a/openid-connect-server-webapp/src/main/webapp/WEB-INF/views/requestUserCode.jsp +++ b/openid-connect-server-webapp/src/main/webapp/WEB-INF/views/requestUserCode.jsp @@ -16,7 +16,7 @@
-

 

+

diff --git a/openid-connect-server/src/main/java/org/mitre/openid/connect/web/EndSessionEndpoint.java b/openid-connect-server/src/main/java/org/mitre/openid/connect/web/EndSessionEndpoint.java index 2856b6513..abaf20a7a 100644 --- a/openid-connect-server/src/main/java/org/mitre/openid/connect/web/EndSessionEndpoint.java +++ b/openid-connect-server/src/main/java/org/mitre/openid/connect/web/EndSessionEndpoint.java @@ -20,22 +20,35 @@ package org.mitre.openid.connect.web; import java.text.ParseException; import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; import org.mitre.jwt.assertion.AssertionValidator; import org.mitre.jwt.assertion.impl.SelfAssertionValidator; +import org.mitre.oauth2.model.ClientDetailsEntity; +import org.mitre.oauth2.service.ClientDetailsEntityService; +import org.mitre.openid.connect.model.UserInfo; import org.mitre.openid.connect.service.UserInfoService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.Authentication; -import org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestWrapper; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.common.exceptions.InvalidClientException; +import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.util.UriComponents; +import org.springframework.web.util.UriComponentsBuilder; +import org.springframework.web.util.UriUtils; import com.google.common.base.Strings; +import com.google.common.collect.Iterables; import com.nimbusds.jwt.JWT; +import com.nimbusds.jwt.JWTClaimsSet; import com.nimbusds.jwt.JWTParser; /** @@ -47,48 +60,137 @@ public class EndSessionEndpoint { public static final String URL = "endsession"; + private static final String CLIENT_KEY = "client"; + private static final String STATE_KEY = "state"; + private static final String REDIRECT_URI_KEY = "redirectUri"; + private static Logger logger = LoggerFactory.getLogger(EndSessionEndpoint.class); - private AssertionValidator validator = new SelfAssertionValidator(); + @Autowired + private SelfAssertionValidator validator; @Autowired private UserInfoService userInfoService; - @RequestMapping(value = "/" + URL) + @Autowired + private ClientDetailsEntityService clientService; + + @RequestMapping(value = "/" + URL, method = RequestMethod.GET) public String endSession(@RequestParam (value = "id_token_hint", required = false) String idTokenHint, @RequestParam (value = "post_logout_redirect_uri", required = false) String postLogoutRedirectUri, - @RequestParam (value = "state", required = false) String state, + @RequestParam (value = STATE_KEY, required = false) String state, HttpServletRequest request, + HttpServletResponse response, + HttpSession session, Authentication auth, Model m) { + // conditionally filled variables + JWTClaimsSet idTokenClaims = null; // pulled from the parsed and validated ID token + ClientDetailsEntity client = null; // pulled from ID token's audience field + + if (!Strings.isNullOrEmpty(postLogoutRedirectUri)) { + session.setAttribute(REDIRECT_URI_KEY, postLogoutRedirectUri); + } + if (!Strings.isNullOrEmpty(state)) { + session.setAttribute(STATE_KEY, state); + } + + // parse the ID token hint to see if it's valid + if (!Strings.isNullOrEmpty(idTokenHint)) { + try { + JWT idToken = JWTParser.parse(idTokenHint); + + if (validator.isValid(idToken)) { + // we issued this ID token, figure out who it's for + idTokenClaims = idToken.getJWTClaimsSet(); + + String clientId = Iterables.getOnlyElement(idTokenClaims.getAudience()); + + client = clientService.loadClientByClientId(clientId); + + // save a reference in the session for us to pick up later + //session.setAttribute("endSession_idTokenHint_claims", idTokenClaims); + session.setAttribute(CLIENT_KEY, client); + } + } catch (ParseException e) { + // it's not a valid ID token, ignore it + logger.debug("Invalid id token hint", e); + } catch (InvalidClientException e) { + // couldn't find the client, ignore it + logger.debug("Invalid client", e); + } + } + // are we logged in or not? if (auth == null || !request.isUserInRole("ROLE_USER")) { - // we're not logged in, process the logout - return null; + // we're not logged in anyway, process the final redirect bits if needed + return processLogout(null, request, response, session, auth, m); } else { // we are logged in, need to prompt the user before we log out - // parse the ID token hint to see if it's valid - if (!Strings.isNullOrEmpty(idTokenHint)) { - try { - JWT idToken = JWTParser.parse(idTokenHint); - - if (validator.isValid(idToken)) { - // we issued this ID token, figure out who it's for - String subject = idToken.getJWTClaimsSet().getSubject(); - - userInfoService.getByUsername(subject); - - } - } catch (ParseException e) { - - } - + // see who the current user is + UserInfo ui = userInfoService.getByUsername(auth.getName()); + + if (idTokenClaims != null) { + String subject = idTokenClaims.getSubject(); + // see if the current user is the same as the one in the ID token + // TODO: should we do anything different in these cases? + if (!Strings.isNullOrEmpty(subject) && subject.equals(ui.getSub())) { + // it's the same user + } else { + // it's not the same user + } } + + m.addAttribute("client", client); + m.addAttribute("idToken", idTokenClaims); - // display the end session page - return "endSession"; + // display the log out confirmation page + return "logoutConfirmation"; } } + @RequestMapping(value = "/" + URL, method = RequestMethod.POST) + public String processLogout(@RequestParam(value = "approve", required = false) String approved, + HttpServletRequest request, + HttpServletResponse response, + HttpSession session, + Authentication auth, Model m) { + + String redirectUri = (String) session.getAttribute(REDIRECT_URI_KEY); + String state = (String) session.getAttribute(STATE_KEY); + ClientDetailsEntity client = (ClientDetailsEntity) session.getAttribute(CLIENT_KEY); + + if (!Strings.isNullOrEmpty(approved)) { + // use approved, perform the logout + if (auth != null){ + new SecurityContextLogoutHandler().logout(request, response, auth); + } + SecurityContextHolder.getContext().setAuthentication(null); + // TODO: hook into other logout post-processing + } + + // if the user didn't approve, don't log out but hit the landing page anyway for redirect as needed + + + + // if we have a client AND the client has post-logout redirect URIs + // registered AND the URI given is in that list, then... + if (!Strings.isNullOrEmpty(redirectUri) && + client != null && client.getPostLogoutRedirectUris() != null) { + + if (client.getPostLogoutRedirectUris().contains(redirectUri)) { + // TODO: future, add the redirect URI to the model for the display page for an interstitial + // m.addAttribute("redirectUri", postLogoutRedirectUri); + + UriComponents uri = UriComponentsBuilder.fromHttpUrl(redirectUri).queryParam("state", state).build(); + + return "redirect:" + uri; + } + } + + // otherwise, return to a nice post-logout landing page + return "postLogout"; + } + }