2020import java .text .ParseException ;
2121
2222import javax .servlet .http .HttpServletRequest ;
23+ import javax .servlet .http .HttpServletResponse ;
24+ import javax .servlet .http .HttpSession ;
2325
2426import org .mitre .jwt .assertion .AssertionValidator ;
2527import org .mitre .jwt .assertion .impl .SelfAssertionValidator ;
28+ import org .mitre .oauth2 .model .ClientDetailsEntity ;
29+ import org .mitre .oauth2 .service .ClientDetailsEntityService ;
30+ import org .mitre .openid .connect .model .UserInfo ;
2631import org .mitre .openid .connect .service .UserInfoService ;
2732import org .slf4j .Logger ;
2833import org .slf4j .LoggerFactory ;
2934import org .springframework .beans .factory .annotation .Autowired ;
3035import org .springframework .security .core .Authentication ;
31- import org .springframework .security .web .servletapi .SecurityContextHolderAwareRequestWrapper ;
36+ import org .springframework .security .core .context .SecurityContextHolder ;
37+ import org .springframework .security .oauth2 .common .exceptions .InvalidClientException ;
38+ import org .springframework .security .web .authentication .logout .SecurityContextLogoutHandler ;
3239import org .springframework .stereotype .Controller ;
3340import org .springframework .ui .Model ;
3441import org .springframework .web .bind .annotation .RequestMapping ;
42+ import org .springframework .web .bind .annotation .RequestMethod ;
3543import org .springframework .web .bind .annotation .RequestParam ;
44+ import org .springframework .web .util .UriComponents ;
45+ import org .springframework .web .util .UriComponentsBuilder ;
46+ import org .springframework .web .util .UriUtils ;
3647
3748import com .google .common .base .Strings ;
49+ import com .google .common .collect .Iterables ;
3850import com .nimbusds .jwt .JWT ;
51+ import com .nimbusds .jwt .JWTClaimsSet ;
3952import com .nimbusds .jwt .JWTParser ;
4053
4154/**
@@ -47,48 +60,137 @@ public class EndSessionEndpoint {
4760
4861public static final String URL = "endsession" ;
4962
63+ private static final String CLIENT_KEY = "client" ;
64+ private static final String STATE_KEY = "state" ;
65+ private static final String REDIRECT_URI_KEY = "redirectUri" ;
66+
5067private static Logger logger = LoggerFactory .getLogger (EndSessionEndpoint .class );
5168
52- private AssertionValidator validator = new SelfAssertionValidator ();
69+ @ Autowired
70+ private SelfAssertionValidator validator ;
5371
5472@ Autowired
5573private UserInfoService userInfoService ;
5674
57- @ RequestMapping (value = "/" + URL )
75+ @ Autowired
76+ private ClientDetailsEntityService clientService ;
77+
78+ @ RequestMapping (value = "/" + URL , method = RequestMethod .GET )
5879public String endSession (@ RequestParam (value = "id_token_hint" , required = false ) String idTokenHint ,
5980 @ RequestParam (value = "post_logout_redirect_uri" , required = false ) String postLogoutRedirectUri ,
60- @ RequestParam (value = "state" , required = false ) String state ,
81+ @ RequestParam (value = STATE_KEY , required = false ) String state ,
6182 HttpServletRequest request ,
83+ HttpServletResponse response ,
84+ HttpSession session ,
6285 Authentication auth , Model m ) {
6386
87+ // conditionally filled variables
88+ JWTClaimsSet idTokenClaims = null ; // pulled from the parsed and validated ID token
89+ ClientDetailsEntity client = null ; // pulled from ID token's audience field
90+
91+ if (!Strings .isNullOrEmpty (postLogoutRedirectUri )) {
92+ session .setAttribute (REDIRECT_URI_KEY , postLogoutRedirectUri );
93+ }
94+ if (!Strings .isNullOrEmpty (state )) {
95+ session .setAttribute (STATE_KEY , state );
96+ }
97+
98+ // parse the ID token hint to see if it's valid
99+ if (!Strings .isNullOrEmpty (idTokenHint )) {
100+ try {
101+ JWT idToken = JWTParser .parse (idTokenHint );
102+
103+ if (validator .isValid (idToken )) {
104+ // we issued this ID token, figure out who it's for
105+ idTokenClaims = idToken .getJWTClaimsSet ();
106+
107+ String clientId = Iterables .getOnlyElement (idTokenClaims .getAudience ());
108+
109+ client = clientService .loadClientByClientId (clientId );
110+
111+ // save a reference in the session for us to pick up later
112+ //session.setAttribute("endSession_idTokenHint_claims", idTokenClaims);
113+ session .setAttribute (CLIENT_KEY , client );
114+ }
115+ } catch (ParseException e ) {
116+ // it's not a valid ID token, ignore it
117+ logger .debug ("Invalid id token hint" , e );
118+ } catch (InvalidClientException e ) {
119+ // couldn't find the client, ignore it
120+ logger .debug ("Invalid client" , e );
121+ }
122+ }
123+
64124// are we logged in or not?
65125if (auth == null || !request .isUserInRole ("ROLE_USER" )) {
66- // we're not logged in, process the logout
67- return null ;
126+ // we're not logged in anyway , process the final redirect bits if needed
127+ return processLogout ( null , request , response , session , auth , m ) ;
68128} else {
69129// we are logged in, need to prompt the user before we log out
70130
71- // parse the ID token hint to see if it's valid
72- if (!Strings .isNullOrEmpty (idTokenHint )) {
73- try {
74- JWT idToken = JWTParser .parse (idTokenHint );
75-
76- if (validator .isValid (idToken )) {
77- // we issued this ID token, figure out who it's for
78- String subject = idToken .getJWTClaimsSet ().getSubject ();
79-
80- userInfoService .getByUsername (subject );
81-
82- }
83- } catch (ParseException e ) {
84-
85- }
86-
131+ // see who the current user is
132+ UserInfo ui = userInfoService .getByUsername (auth .getName ());
133+
134+ if (idTokenClaims != null ) {
135+ String subject = idTokenClaims .getSubject ();
136+ // see if the current user is the same as the one in the ID token
137+ // TODO: should we do anything different in these cases?
138+ if (!Strings .isNullOrEmpty (subject ) && subject .equals (ui .getSub ())) {
139+ // it's the same user
140+ } else {
141+ // it's not the same user
142+ }
87143}
144+
145+ m .addAttribute ("client" , client );
146+ m .addAttribute ("idToken" , idTokenClaims );
88147
89- // display the end session page
90- return "endSession " ;
148+ // display the log out confirmation page
149+ return "logoutConfirmation " ;
91150}
92151}
93152
153+ @ RequestMapping (value = "/" + URL , method = RequestMethod .POST )
154+ public String processLogout (@ RequestParam (value = "approve" , required = false ) String approved ,
155+ HttpServletRequest request ,
156+ HttpServletResponse response ,
157+ HttpSession session ,
158+ Authentication auth , Model m ) {
159+
160+ String redirectUri = (String ) session .getAttribute (REDIRECT_URI_KEY );
161+ String state = (String ) session .getAttribute (STATE_KEY );
162+ ClientDetailsEntity client = (ClientDetailsEntity ) session .getAttribute (CLIENT_KEY );
163+
164+ if (!Strings .isNullOrEmpty (approved )) {
165+ // use approved, perform the logout
166+ if (auth != null ){
167+ new SecurityContextLogoutHandler ().logout (request , response , auth );
168+ }
169+ SecurityContextHolder .getContext ().setAuthentication (null );
170+ // TODO: hook into other logout post-processing
171+ }
172+
173+ // if the user didn't approve, don't log out but hit the landing page anyway for redirect as needed
174+
175+
176+
177+ // if we have a client AND the client has post-logout redirect URIs
178+ // registered AND the URI given is in that list, then...
179+ if (!Strings .isNullOrEmpty (redirectUri ) &&
180+ client != null && client .getPostLogoutRedirectUris () != null ) {
181+
182+ if (client .getPostLogoutRedirectUris ().contains (redirectUri )) {
183+ // TODO: future, add the redirect URI to the model for the display page for an interstitial
184+ // m.addAttribute("redirectUri", postLogoutRedirectUri);
185+
186+ UriComponents uri = UriComponentsBuilder .fromHttpUrl (redirectUri ).queryParam ("state" , state ).build ();
187+
188+ return "redirect:" + uri ;
189+ }
190+ }
191+
192+ // otherwise, return to a nice post-logout landing page
193+ return "postLogout" ;
194+ }
195+
94196}
0 commit comments