back ported prompt behavior to 1.1, closes #810, addresses #667

pull/873/head
Justin Richer 2015-05-27 12:12:01 -04:00
parent 9fd059d091
commit 5624c12232
3 changed files with 119 additions and 54 deletions

View File

@ -1,26 +1,25 @@
/******************************************************************************* /*******************************************************************************
* Copyright 2014 The MITRE Corporation * Copyright 2015 The MITRE Corporation
* and the MIT Kerberos and Internet Trust Consortium * and the MIT Kerberos and Internet Trust Consortium
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
******************************************************************************/ *******************************************************************************/
/** /**
* *
*/ */
package org.mitre.openid.connect.web; package org.mitre.openid.connect.web;
import java.lang.reflect.Type; import java.lang.reflect.Type;
import java.security.Principal;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
@ -31,10 +30,13 @@ import org.mitre.openid.connect.service.UserInfoService;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import org.springframework.web.servlet.view.RedirectView; import org.springframework.web.servlet.view.RedirectView;
import org.springframework.web.servlet.view.UrlBasedViewResolver;
import com.google.common.base.Strings;
import com.google.gson.Gson; import com.google.gson.Gson;
import com.google.gson.GsonBuilder; import com.google.gson.GsonBuilder;
import com.google.gson.JsonElement; import com.google.gson.JsonElement;
@ -69,46 +71,57 @@ public class UserInfoInterceptor extends HandlerInterceptorAdapter {
// or if there's already a userInfo object in there // or if there's already a userInfo object in there
// TODO: this is a patch to get around a potential information leak from #492 // TODO: this is a patch to get around a potential information leak from #492
if (!(modelAndView.getView() instanceof RedirectView)) {
// get our principal from the security context if (modelAndView.getView() instanceof RedirectView) {
Principal p = request.getUserPrincipal(); // don't add them
} else {
if (p instanceof Authentication && !modelAndView.getModel().containsKey("userAuthorities")){ if (Strings.isNullOrEmpty(modelAndView.getViewName())) {
Authentication auth = (Authentication)p; // add them
modelAndView.addObject("userAuthorities", gson.toJson(auth.getAuthorities())); injectUserInfo(modelAndView);
}
if (p instanceof OIDCAuthenticationToken) {
// if they're logging into this server from a remote OIDC server, pass through their user info
OIDCAuthenticationToken oidc = (OIDCAuthenticationToken) p;
if (oidc.getUserInfo() != null) {
modelAndView.addObject("userInfo", oidc.getUserInfo());
modelAndView.addObject("userInfoJson", oidc.getUserInfo().toJson());
} else {
modelAndView.addObject("userInfo", null);
modelAndView.addObject("userInfoJson", "null");
}
} else { } else {
// don't bother checking if we don't have a principal or a userInfoService to work with if (modelAndView.getViewName().startsWith(UrlBasedViewResolver.FORWARD_URL_PREFIX) ||
if (p != null && p.getName() != null && userInfoService != null) { modelAndView.getViewName().startsWith(UrlBasedViewResolver.REDIRECT_URL_PREFIX)) {
// don't add them
// try to look up a user based on the principal's name } else {
UserInfo user = userInfoService.getByUsername(p.getName()); // add them
injectUserInfo(modelAndView);
// if we have one, inject it so views can use it
if (user != null) {
modelAndView.addObject("userInfo", user);
modelAndView.addObject("userInfoJson", user.toJson());
}
} }
} }
} }
} }
} }
private void injectUserInfo(ModelAndView modelAndView) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth instanceof Authentication && !modelAndView.getModel().containsKey("userAuthorities")){
modelAndView.addObject("userAuthorities", gson.toJson(auth.getAuthorities()));
}
if (auth instanceof OIDCAuthenticationToken) {
// if they're logging into this server from a remote OIDC server, pass through their user info
OIDCAuthenticationToken oidc = (OIDCAuthenticationToken) auth;
if (oidc.getUserInfo() != null) {
modelAndView.addObject("userInfo", oidc.getUserInfo());
modelAndView.addObject("userInfoJson", oidc.getUserInfo().toJson());
} else {
modelAndView.addObject("userInfo", null);
modelAndView.addObject("userInfoJson", "null");
}
} else {
// don't bother checking if we don't have a principal or a userInfoService to work with
if (auth != null && auth.getName() != null && userInfoService != null) {
// try to look up a user based on the principal's name
UserInfo user = userInfoService.getByUsername(auth.getName());
// if we have one, inject it so views can use it
if (user != null) {
modelAndView.addObject("userInfo", user);
modelAndView.addObject("userInfoJson", user.toJson());
}
}
}
}
} }

View File

@ -19,6 +19,7 @@
*/ */
package org.mitre.oauth2.web; package org.mitre.oauth2.web;
import java.net.URISyntaxException;
import java.security.Principal; import java.security.Principal;
import java.util.Date; import java.util.Date;
import java.util.HashMap; import java.util.HashMap;
@ -27,6 +28,7 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import org.apache.http.client.utils.URIBuilder;
import org.mitre.oauth2.model.ClientDetailsEntity; import org.mitre.oauth2.model.ClientDetailsEntity;
import org.mitre.oauth2.model.SystemScope; import org.mitre.oauth2.model.SystemScope;
import org.mitre.oauth2.service.ClientDetailsEntityService; import org.mitre.oauth2.service.ClientDetailsEntityService;
@ -43,10 +45,12 @@ import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.oauth2.common.exceptions.OAuth2Exception; import org.springframework.security.oauth2.common.exceptions.OAuth2Exception;
import org.springframework.security.oauth2.provider.AuthorizationRequest; import org.springframework.security.oauth2.provider.AuthorizationRequest;
import org.springframework.security.oauth2.provider.endpoint.RedirectResolver;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.SessionAttributes; import org.springframework.web.bind.annotation.SessionAttributes;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import com.google.common.base.Joiner; import com.google.common.base.Joiner;
import com.google.common.base.Splitter; import com.google.common.base.Splitter;
@ -76,6 +80,9 @@ public class OAuthConfirmationController {
@Autowired @Autowired
private StatsService statsService; private StatsService statsService;
@Autowired
private RedirectResolver redirectResolver;
private static Logger logger = LoggerFactory.getLogger(OAuthConfirmationController.class); private static Logger logger = LoggerFactory.getLogger(OAuthConfirmationController.class);
@ -90,23 +97,10 @@ public class OAuthConfirmationController {
@PreAuthorize("hasRole('ROLE_USER')") @PreAuthorize("hasRole('ROLE_USER')")
@RequestMapping("/oauth/confirm_access") @RequestMapping("/oauth/confirm_access")
public String confimAccess(Map<String, Object> model, @ModelAttribute("authorizationRequest") AuthorizationRequest authRequest, public String confimAccess(Map<String, Object> model, @ModelAttribute("authorizationRequest") AuthorizationRequest authRequest,
Principal p) { Principal p, RedirectAttributes ra) {
// Check the "prompt" parameter to see if we need to do special processing // Check the "prompt" parameter to see if we need to do special processing
String prompt = (String)authRequest.getExtensions().get("prompt");
List<String> prompts = Splitter.on(" ").splitToList(Strings.nullToEmpty(prompt));
if (prompts.contains("none")) {
// we're not supposed to prompt, so "return an error"
logger.info("Client requested no prompt, returning 403 from confirmation endpoint");
model.put("code", HttpStatus.FORBIDDEN);
return HttpCodeView.VIEWNAME;
}
if (prompts.contains("consent")) {
model.put("consent", true);
}
//AuthorizationRequest clientAuth = (AuthorizationRequest) model.remove("authorizationRequest"); //AuthorizationRequest clientAuth = (AuthorizationRequest) model.remove("authorizationRequest");
ClientDetailsEntity client = null; ClientDetailsEntity client = null;
@ -123,6 +117,34 @@ public class OAuthConfirmationController {
return HttpCodeView.VIEWNAME; return HttpCodeView.VIEWNAME;
} }
String prompt = (String)authRequest.getExtensions().get("prompt");
List<String> prompts = Splitter.on(" ").splitToList(Strings.nullToEmpty(prompt));
if (prompts.contains("none")) {
// if we've got a redirect URI then we'll send it
String url = redirectResolver.resolveRedirect(authRequest.getRedirectUri(), client);
try {
URIBuilder uriBuilder = new URIBuilder(url);
uriBuilder.addParameter("error", "interaction_required");
if (!Strings.isNullOrEmpty(authRequest.getState())) {
uriBuilder.addParameter("state", authRequest.getState()); // copy the state parameter if one was given
}
return "redirect:" + uriBuilder.toString();
} catch (URISyntaxException e) {
logger.error("Can't build redirect URI for prompt=none, sending error instead", e);
model.put("code", HttpStatus.FORBIDDEN);
return HttpCodeView.VIEWNAME;
}
}
if (prompts.contains("consent")) {
model.put("consent", true);
}
if (client == null) { if (client == null) {
logger.error("confirmAccess: could not find client " + authRequest.getClientId()); logger.error("confirmAccess: could not find client " + authRequest.getClientId());
model.put("code", HttpStatus.NOT_FOUND); model.put("code", HttpStatus.NOT_FOUND);

View File

@ -20,6 +20,7 @@
package org.mitre.openid.connect.filter; package org.mitre.openid.connect.filter;
import java.io.IOException; import java.io.IOException;
import java.net.URISyntaxException;
import java.util.Date; import java.util.Date;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
@ -33,6 +34,7 @@ import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession; import javax.servlet.http.HttpSession;
import org.apache.http.client.utils.URIBuilder;
import org.mitre.oauth2.model.ClientDetailsEntity; import org.mitre.oauth2.model.ClientDetailsEntity;
import org.mitre.oauth2.service.ClientDetailsEntityService; import org.mitre.oauth2.service.ClientDetailsEntityService;
import org.mitre.openid.connect.web.AuthenticationTimeStamper; import org.mitre.openid.connect.web.AuthenticationTimeStamper;
@ -44,6 +46,7 @@ import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.common.exceptions.InvalidClientException; import org.springframework.security.oauth2.common.exceptions.InvalidClientException;
import org.springframework.security.oauth2.provider.AuthorizationRequest; import org.springframework.security.oauth2.provider.AuthorizationRequest;
import org.springframework.security.oauth2.provider.OAuth2RequestFactory; import org.springframework.security.oauth2.provider.OAuth2RequestFactory;
import org.springframework.security.oauth2.provider.endpoint.RedirectResolver;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.web.filter.GenericFilterBean; import org.springframework.web.filter.GenericFilterBean;
@ -68,6 +71,9 @@ public class PromptFilter extends GenericFilterBean {
@Autowired @Autowired
private ClientDetailsEntityService clientService; private ClientDetailsEntityService clientService;
@Autowired
private RedirectResolver redirectResolver;
/** /**
* *
*/ */
@ -102,17 +108,41 @@ public class PromptFilter extends GenericFilterBean {
List<String> prompts = Splitter.on(" ").splitToList(Strings.nullToEmpty(prompt)); List<String> prompts = Splitter.on(" ").splitToList(Strings.nullToEmpty(prompt));
if (prompts.contains("none")) { if (prompts.contains("none")) {
logger.info("Client requested no prompt");
// see if the user's logged in // see if the user's logged in
Authentication auth = SecurityContextHolder.getContext().getAuthentication(); Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null) { if (auth != null) {
// user's been logged in already (by session management) // user's been logged in already (by session management)
// we're OK, continue without prompting // we're OK, continue without prompting (in case the user already has a saved state)
chain.doFilter(req, res); chain.doFilter(req, res);
} else { } else {
logger.info("Client requested no prompt");
// user hasn't been logged in, we need to "return an error" // user hasn't been logged in, we need to "return an error"
logger.info("User not logged in, no prompt requested, returning 403 from filter"); if (client != null && authRequest.getRedirectUri() != null) {
// if we've got a redirect URI then we'll send it
String url = redirectResolver.resolveRedirect(authRequest.getRedirectUri(), client);
try {
URIBuilder uriBuilder = new URIBuilder(url);
uriBuilder.addParameter("error", "login_required");
if (!Strings.isNullOrEmpty(authRequest.getState())) {
uriBuilder.addParameter("state", authRequest.getState()); // copy the state parameter if one was given
}
response.sendRedirect(uriBuilder.toString());
return;
} catch (URISyntaxException e) {
logger.error("Can't build redirect URI for prompt=none, sending error instead", e);
response.sendError(HttpServletResponse.SC_FORBIDDEN, "Access Denied");
return;
}
}
logger.error("Can't build redirect URI for prompt=none, sending error instead");
response.sendError(HttpServletResponse.SC_FORBIDDEN, "Access Denied"); response.sendError(HttpServletResponse.SC_FORBIDDEN, "Access Denied");
return; return;
} }