001/******************************************************************************* 002 * Copyright 2017 The MIT Internet Trust Consortium 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 *******************************************************************************/ 016 017package org.mitre.openid.connect.web; 018 019import java.text.ParseException; 020 021import javax.servlet.http.HttpServletRequest; 022import javax.servlet.http.HttpServletResponse; 023import javax.servlet.http.HttpSession; 024 025import org.mitre.jwt.assertion.AssertionValidator; 026import org.mitre.jwt.assertion.impl.SelfAssertionValidator; 027import org.mitre.oauth2.model.ClientDetailsEntity; 028import org.mitre.oauth2.service.ClientDetailsEntityService; 029import org.mitre.openid.connect.model.UserInfo; 030import org.mitre.openid.connect.service.UserInfoService; 031import org.slf4j.Logger; 032import org.slf4j.LoggerFactory; 033import org.springframework.beans.factory.annotation.Autowired; 034import org.springframework.security.core.Authentication; 035import org.springframework.security.core.context.SecurityContextHolder; 036import org.springframework.security.oauth2.common.exceptions.InvalidClientException; 037import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler; 038import org.springframework.stereotype.Controller; 039import org.springframework.ui.Model; 040import org.springframework.web.bind.annotation.RequestMapping; 041import org.springframework.web.bind.annotation.RequestMethod; 042import org.springframework.web.bind.annotation.RequestParam; 043import org.springframework.web.util.UriComponents; 044import org.springframework.web.util.UriComponentsBuilder; 045import org.springframework.web.util.UriUtils; 046 047import com.google.common.base.Strings; 048import com.google.common.collect.Iterables; 049import com.nimbusds.jwt.JWT; 050import com.nimbusds.jwt.JWTClaimsSet; 051import com.nimbusds.jwt.JWTParser; 052 053/** 054 * Implementation of the End Session Endpoint from OIDC session management 055 * 056 * @author jricher 057 * 058 */ 059@Controller 060public class EndSessionEndpoint { 061 062 public static final String URL = "endsession"; 063 064 private static final String CLIENT_KEY = "client"; 065 private static final String STATE_KEY = "state"; 066 private static final String REDIRECT_URI_KEY = "redirectUri"; 067 068 private static Logger logger = LoggerFactory.getLogger(EndSessionEndpoint.class); 069 070 @Autowired 071 private SelfAssertionValidator validator; 072 073 @Autowired 074 private UserInfoService userInfoService; 075 076 @Autowired 077 private ClientDetailsEntityService clientService; 078 079 @RequestMapping(value = "/" + URL, method = RequestMethod.GET) 080 public String endSession(@RequestParam (value = "id_token_hint", required = false) String idTokenHint, 081 @RequestParam (value = "post_logout_redirect_uri", required = false) String postLogoutRedirectUri, 082 @RequestParam (value = STATE_KEY, required = false) String state, 083 HttpServletRequest request, 084 HttpServletResponse response, 085 HttpSession session, 086 Authentication auth, Model m) { 087 088 // conditionally filled variables 089 JWTClaimsSet idTokenClaims = null; // pulled from the parsed and validated ID token 090 ClientDetailsEntity client = null; // pulled from ID token's audience field 091 092 if (!Strings.isNullOrEmpty(postLogoutRedirectUri)) { 093 session.setAttribute(REDIRECT_URI_KEY, postLogoutRedirectUri); 094 } 095 if (!Strings.isNullOrEmpty(state)) { 096 session.setAttribute(STATE_KEY, state); 097 } 098 099 // parse the ID token hint to see if it's valid 100 if (!Strings.isNullOrEmpty(idTokenHint)) { 101 try { 102 JWT idToken = JWTParser.parse(idTokenHint); 103 104 if (validator.isValid(idToken)) { 105 // we issued this ID token, figure out who it's for 106 idTokenClaims = idToken.getJWTClaimsSet(); 107 108 String clientId = Iterables.getOnlyElement(idTokenClaims.getAudience()); 109 110 client = clientService.loadClientByClientId(clientId); 111 112 // save a reference in the session for us to pick up later 113 //session.setAttribute("endSession_idTokenHint_claims", idTokenClaims); 114 session.setAttribute(CLIENT_KEY, client); 115 } 116 } catch (ParseException e) { 117 // it's not a valid ID token, ignore it 118 logger.debug("Invalid id token hint", e); 119 } catch (InvalidClientException e) { 120 // couldn't find the client, ignore it 121 logger.debug("Invalid client", e); 122 } 123 } 124 125 // are we logged in or not? 126 if (auth == null || !request.isUserInRole("ROLE_USER")) { 127 // we're not logged in anyway, process the final redirect bits if needed 128 return processLogout(null, request, response, session, auth, m); 129 } else { 130 // we are logged in, need to prompt the user before we log out 131 132 // see who the current user is 133 UserInfo ui = userInfoService.getByUsername(auth.getName()); 134 135 if (idTokenClaims != null) { 136 String subject = idTokenClaims.getSubject(); 137 // see if the current user is the same as the one in the ID token 138 // TODO: should we do anything different in these cases? 139 if (!Strings.isNullOrEmpty(subject) && subject.equals(ui.getSub())) { 140 // it's the same user 141 } else { 142 // it's not the same user 143 } 144 } 145 146 m.addAttribute("client", client); 147 m.addAttribute("idToken", idTokenClaims); 148 149 // display the log out confirmation page 150 return "logoutConfirmation"; 151 } 152 } 153 154 @RequestMapping(value = "/" + URL, method = RequestMethod.POST) 155 public String processLogout(@RequestParam(value = "approve", required = false) String approved, 156 HttpServletRequest request, 157 HttpServletResponse response, 158 HttpSession session, 159 Authentication auth, Model m) { 160 161 String redirectUri = (String) session.getAttribute(REDIRECT_URI_KEY); 162 String state = (String) session.getAttribute(STATE_KEY); 163 ClientDetailsEntity client = (ClientDetailsEntity) session.getAttribute(CLIENT_KEY); 164 165 if (!Strings.isNullOrEmpty(approved)) { 166 // use approved, perform the logout 167 if (auth != null){ 168 new SecurityContextLogoutHandler().logout(request, response, auth); 169 } 170 SecurityContextHolder.getContext().setAuthentication(null); 171 // TODO: hook into other logout post-processing 172 } 173 174 // if the user didn't approve, don't log out but hit the landing page anyway for redirect as needed 175 176 177 178 // if we have a client AND the client has post-logout redirect URIs 179 // registered AND the URI given is in that list, then... 180 if (!Strings.isNullOrEmpty(redirectUri) && 181 client != null && client.getPostLogoutRedirectUris() != null) { 182 183 if (client.getPostLogoutRedirectUris().contains(redirectUri)) { 184 // TODO: future, add the redirect URI to the model for the display page for an interstitial 185 // m.addAttribute("redirectUri", postLogoutRedirectUri); 186 187 UriComponents uri = UriComponentsBuilder.fromHttpUrl(redirectUri).queryParam("state", state).build(); 188 189 return "redirect:" + uri; 190 } 191 } 192 193 // otherwise, return to a nice post-logout landing page 194 return "postLogout"; 195 } 196 197}