mvc - spring security oauth2 example mkyong
HttpSession null después de reemplazar AuthorizationRequest (2)
¿Has resuelto tu problema? He estado buscando una muestra completa de 2FA junto con spring-security-oauth2. Es genial que haya publicado sus conceptos completos y las fuentes completas.
Probé tu paquete y tu problema simplemente puede resolverse cambiando solo 1 línea de código en tu AuthserverApplication.java
@Override
protected void configure(HttpSecurity http) throws Exception {
// @formatter:off
http
.formLogin().loginPage("/login").permitAll()
.and()
.requestMatchers().antMatchers("/login", "/oauth/authorize", "/secure/two_factor_authentication", "/pincode")
.and()
.authorizeRequests().anyRequest().authenticated();
// @formatter:on
}
Su configuración original pasó la cadena de autenticación de seguridad de primavera que le devolvió un objeto nulo de autenticación.
También le recomendaría que cambie la creación de Bean de CustomOAuth2RequestFactory a la siguiente que anule todas las OAuth2RequestFactory en la cadena
@Bean
public OAuth2RequestFactory customOAuth2RequestFactory(){
return new CustomOAuth2RequestFactory(clientDetailsService);
}
Para el código que ha agregado para manejar el CSRF, simplemente puede eliminarlos, por ej. el controlador 2FA:
@Controller
@RequestMapping(TwoFactorAuthenticationController.PATH)
public class TwoFactorAuthenticationController {
private static final Logger LOG = LoggerFactory.getLogger(TwoFactorAuthenticationController.class);
public static final String PATH = "/secure/two_factor_authentication";
public static final String AUTHORIZE_PATH = "/oauth/authorize";
public static final String ROLE_TWO_FACTOR_AUTHENTICATED = "ROLE_TWO_FACTOR_AUTHENTICATED";
private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
@RequestMapping(method = RequestMethod.GET)
public String auth(HttpServletRequest request, HttpSession session, HttpServletResponse resp/*, ....*/) {
System.out.println("-------- inside GET /secure/two_factor_authentication --------------");
if (AuthenticationUtil.isAuthenticatedWithAuthority(ROLE_TWO_FACTOR_AUTHENTICATED)) {
LOG.info("User {} already has {} authority - no need to enter code again", ROLE_TWO_FACTOR_AUTHENTICATED);
// throw ....;
}
else if (session.getAttribute(CustomOAuth2RequestFactory.SAVED_AUTHORIZATION_REQUEST_SESSION_ATTRIBUTE_NAME) == null) {
// LOG.warn("Error while entering 2FA code - attribute {} not found in session.", CustomOAuth2RequestFactory.SAVED_AUTHORIZATION_REQUEST_SESSION_ATTRIBUTE_NAME);
// throw ....;
}
return "pinCode";
}
@RequestMapping(method = RequestMethod.POST)
public String auth(FormData formData, HttpServletRequest req, HttpServletResponse resp,
SessionStatus sessionStatus, Principal principal, Model model)
throws IOException{
if (formData.getPinVal()!=null) {
if(formData.getPinVal().equals("5309")){
AuthenticationUtil.addAuthority(ROLE_TWO_FACTOR_AUTHENTICATED);
return "redirect:"+AUTHORIZE_PATH;
};
};
return "pinCode";
}
}
Por favor, hágamelo saber si desea un código fuente completo después de la limpieza.
El código completo y las instrucciones para reproducir rápidamente el problema se detallan a continuación.
HttpSession
pasa a ser null
después de que una implementación personalizada de DefaultOAuth2RequestFactory
reemplaza la AuthorizationRequest
actual con una AuthorizationRequest
guardada. Esto provoca un error en la solicitud posterior a /oauth/token
porque CsrfFilter en la cadena de filtro de Spring Security que precede al punto final /oauth/token
no puede encontrar un Csrf token
session
en la session
null
para comparar con el Csrf token
la request
. FLUJO DE CONTROL DURANTE EL ERROR: El siguiente diagrama de flujo ilustra dónde el Paso 14 y el Paso 15 null
alguna manera la HttpSession
. (O posiblemente no coincida con un JSESSIONID
.) Un SYSO
al comienzo de CustomOAuth2RequestFactory.java
en el Paso 14 muestra que, de hecho, hay una HttpSession
que de hecho contiene el CsrfToken
correcto. Sin embargo, de alguna manera, la HttpSession
ha vuelto null
cuando el Paso 15 desencadena una llamada del cliente en el localhost:8080/login
url de localhost:8080/login
regreso al localhost:9999/oauth/token
punto final localhost:9999/oauth/token
.
Se agregaron HttpSessionSecurityContextRepository
de HttpSessionSecurityContextRepository
a cada línea del HttpSessionSecurityContextRepository
mencionado en los registros de depuración a continuación. (Se encuentra en la carpeta Maven Dependencies
del proyecto authserver
eclipse.) Estos puntos de interrupción confirmaron que HttpSession
es null
cuando la solicitud final a /oauth/token
se realiza en el siguiente diagrama de flujo. (Abajo, a la izquierda del diagrama de flujo). La HttpSession
null
puede deberse a que el JSESSIONID
que permanece en el navegador queda desactualizado después de que se DefaultOAuth2RequestFactory
código DefaultOAuth2RequestFactory
personalizado.
¿Cómo se puede solucionar este problema, para que la misma HttpSession
permanezca durante la llamada final al punto final /oauth/token
, después del final del Paso 15 en el diagrama de flujo?
El código completo de CustomOAuth2RequestFactory.java
se puede ver en un sitio para compartir archivos haciendo clic en este enlace. Podemos suponer que la session
null
se debe a 1.) el JSESSIONID
no está siendo actualizado en el navegador por el código en CustomOAuth2RequestFactory
, o 2.) la HttpSession
realmente está null
.
Los registros de depuración de Spring Boot para la llamada a /oauth/token
después del Paso 15 indican claramente que no hay HttpSession
en ese punto, y se pueden leer de la siguiente manera:
2016-05-30 15:33:42.630 DEBUG 13897 --- [io-9999-exec-10] o.s.security.web.FilterChainProxy : /oauth/token at position 1 of 12 in additional filter chain; firing Filter: ''WebAsyncManagerIntegrationFilter''
2016-05-30 15:33:42.631 DEBUG 13897 --- [io-9999-exec-10] o.s.security.web.FilterChainProxy : /oauth/token at position 2 of 12 in additional filter chain; firing Filter: ''SecurityContextPersistenceFilter''
2016-05-30 15:33:42.631 DEBUG 13897 --- [io-9999-exec-10] w.c.HttpSessionSecurityContextRepository : No HttpSession currently exists
2016-05-30 15:33:42.631 DEBUG 13897 --- [io-9999-exec-10] w.c.HttpSessionSecurityContextRepository : No SecurityContext was available from the HttpSession: null. A new one will be created.
2016-05-30 15:33:42.631 DEBUG 13897 --- [io-9999-exec-10] o.s.security.web.FilterChainProxy : /oauth/token at position 3 of 12 in additional filter chain; firing Filter: ''HeaderWriterFilter''
2016-05-30 15:33:42.631 DEBUG 13897 --- [io-9999-exec-10] o.s.s.w.header.writers.HstsHeaderWriter : Not injecting HSTS header since it did not match the requestMatcher org.springframework.security.web.header.writers.HstsHeaderWriter$SecureRequestMatcher@2fe29f4b
2016-05-30 15:33:42.631 DEBUG 13897 --- [io-9999-exec-10] o.s.security.web.FilterChainProxy : /oauth/token at position 4 of 12 in additional filter chain; firing Filter: ''CsrfFilter''
2016-05-30 15:33:42.644 DEBUG 13897 --- [io-9999-exec-10] o.s.security.web.csrf.CsrfFilter : Invalid CSRF token found for http://localhost:9999/uaa/oauth/token
2016-05-30 15:33:42.644 DEBUG 13897 --- [io-9999-exec-10] w.c.HttpSessionSecurityContextRepository : SecurityContext is empty or contents are anonymous - context will not be stored in HttpSession.
2016-05-30 15:33:42.644 DEBUG 13897 --- [io-9999-exec-10] s.s.w.c.SecurityContextPersistenceFilter : SecurityContextHolder now cleared, as request processing completed
RE-CREAR EL PROBLEMA EN SU COMPUTADORA:
Puede volver a crear el problema en cualquier computadora en solo unos minutos siguiendo estos simples pasos:
1.) Descargue la versión comprimida de la aplicación desde un sitio para compartir archivos haciendo clic en este enlace .
2.) Descomprima la aplicación escribiendo: tar -zxvf oauth2.tar(4).gz
3.) Inicie la aplicación oauth2/authserver
navegando a oauth2/authserver
y luego escribiendo mvn spring-boot:run
.
4.) Inicie la aplicación de resource
navegando a oauth2/resource
y luego escribiendo mvn spring-boot:run
5.) Inicie la aplicación ui
navegando a oauth2/ui
y luego escribiendo mvn spring-boot:run
6.) Abra un navegador web y navegue a http : // localhost : 8080
7.) Haga clic en Login
y luego ingrese Frodo
como el usuario y MyRing
como la contraseña, y haga clic para enviar.
8.) Ingrese 5309
como Pin Code
y haga clic en enviar. Esto activará el error que se muestra arriba.
Los registros de depuración Spring Boot mostrarán MUCHO SYSO
, que proporciona los valores de variables como XSRF-TOKEN
HttpSession
y HttpSession
en cada paso que se muestra en el diagrama de flujo . SYSO
ayuda a segmentar los registros de depuración para que sean más fáciles de interpretar. Y todo el SYSO
se realiza en una clase llamada por las otras clases, por lo que puede manipular la clase SYSO
para cambiar los informes en todas partes en el flujo de control. El nombre de la clase TestHTTP
es TestHTTP
, y su código fuente se puede encontrar en el mismo paquete de demo
.
1.) Seleccione la ventana del terminal que ejecuta la aplicación authserver
y escriba Ctrl-C
para detener la aplicación authserver
.
2.) Importe las tres aplicaciones ( authserver
, resource
y ui
) en eclipse como proyectos maven existentes .
3.) En el eclipse Project Explorer de la aplicación authserver
, haga clic para expandir la carpeta Maven Dependencies
, luego desplácese hacia abajo para hacer clic para expandir Spring-Security-web...
jar como se muestra en un círculo en naranja en la imagen siguiente. A continuación, desplácese para buscar y expandir el paquete org.springframework.security.web.context
. Luego haga doble clic para abrir la clase HttpSessionSecurityContextRepository
resaltada en azul en la captura de pantalla siguiente. Agregue puntos de interrupción a cada línea en esta clase. Es posible que desee hacer lo mismo con la clase SecurityContextPersistenceFilter
en el mismo paquete. Estos puntos de interrupción le permitirán ver el valor de la HttpSession
, que actualmente se vuelve null
antes del final del flujo de control, pero necesita tener un valor válido que pueda correlacionarse con un XSRF-TOKEN
para resolver este OP.
4.) En el paquete de demo
la aplicación, agregue puntos de interrupción dentro de CustomOAuth2RequestFactory.java
. Luego, Debug As... Spring Boot App
para iniciar el depurador.
5.) Luego repita los pasos 6 a 8 anteriores. Es posible que desee borrar el caché del navegador antes de cada nuevo intento. Y es posible que desee que se abra la pestaña Red de las herramientas para desarrolladores del navegador.
La sesión no es nula en su aplicación authserver
en el momento de la llamada final a localhost :9999/uaa/oauth/token
. No solo hay una sesión, sino que el JSESSIONID
y el token csrf
de los valores válidos de coincidencia de sesión presentes en el flujo de control entre el punto donde el usuario envía el pin correcto y el punto donde se realiza la solicitud fallida a /oauth/token
.
El problema es que hay dos valores JSESSIONID
y se selecciona el error de los dos valores para ingresar la llamada a /oauth/token
. Por lo tanto, la solución debe provenir de la modificación de los filtros para eliminar el JSESSIONID
incorrecto, de modo que se pueda enviar el valor correcto.
Lo siguiente resumirá:
HttpSessionListener
identificó el JSESSIONID
válido Para aislar el problema, creé una implementación de HttpSessionListener
y luego lo llamé desde una implementación personalizada de HttpLListener
, de la siguiente manera:
public class HttpSessionCollector implements HttpSessionListener, ServletContextListener {
private static final Set<HttpSession> sessions = ConcurrentHashMap.newKeySet();
public void sessionCreated(HttpSessionEvent event) {
sessions.add(event.getSession());
}
public void sessionDestroyed(HttpSessionEvent event) {
sessions.remove(event.getSession());
}
public static Set<HttpSession> getSessions() {
return sessions;
}
public void contextCreated(ServletContextEvent event) {
event.getServletContext().setAttribute("HttpSessionCollector.instance", this);
}
public static HttpSessionCollector getCurrentInstance(ServletContext context) {
return (HttpSessionCollector) context.getAttribute("HttpSessionCollector.instance");
}
@Override
public void contextDestroyed(ServletContextEvent arg0) {
}
@Override
public void contextInitialized(ServletContextEvent arg0) {
}
}
Luego llamé al HttpSessionListener
anterior en una implementación personalizada de OncePerRequestFilter
, que OncePerRequestFilter
en la cadena de filtros de Spring Security de la aplicación OncePerRequestFilter
para proporcionar información de diagnóstico, de la siguiente manera:
@Component
public class DiagnoseSessionFilter extends OncePerRequestFilter implements ServletContextAware {
@Override
protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain fc) throws ServletException, IOException {
System.out.println("...........///////////// START OF DiagnoseSessionFilter.doFilterInternal() ///////////...........");
//start of request stuff
System.out.println("////////// REQUEST ATTRIBUTES ARE: ");
if(req.getAttribute("_csrf")!=null){
System.out.println("_csrf is: " + req.getAttribute("_csrf").toString());
}
if(req.getAttribute("org.springframework.security.web.csrf.CsrfToken")!=null){
CsrfToken ucsrf = (CsrfToken) req.getAttribute("org.springframework.security.web.csrf.CsrfToken");
System.out.println("ucsrf.getToken() is: " + ucsrf.getToken());
}
String reqXSRF = req.getHeader("XSRF-TOKEN");
System.out.println("request XSRF-TOKEN header is: " + reqXSRF);
String reqCookie = req.getHeader("Cookie");
System.out.println("request Cookie header is: " + reqCookie);
String reqSetCookie = req.getHeader("Set-Cookie");
System.out.println("request Set-Cookie header is: " + reqSetCookie);
String reqReferrer = req.getHeader("referrer");
System.out.println("request referrer header is: " + reqReferrer);
HttpSession rsess = req.getSession(false);
System.out.println("request.getSession(false) is: " + rsess);
if(rsess!=null){
String sessid = rsess.getId();
System.out.println("session.getId() is: "+sessid);
}
System.out.println("/////////// END OF REQUEST ATTRIBUTES ");
//end of request stuff
ServletContext servletContext = req.getServletContext();
System.out.println("////////// START OF SESSION COLLECTOR STUFF ");
HttpSessionCollector collector = HttpSessionCollector.getCurrentInstance(servletContext);
Set<HttpSession> sessions = collector.getSessions();
System.out.println("sessions.size() is: " + sessions.size());
for(HttpSession sess : sessions){
System.out.println("sess is: " + sess);
System.out.println("sess.getId() is: " + sess.getId());
CsrfToken sessCsrf = (CsrfToken) sess.getAttribute("org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository.CSRF_TOKEN");
System.out.println("csrf is: " + sessCsrf);
if(sessCsrf!=null){
if(sessCsrf.getToken()!=null){
System.out.println("sessCsrf.getToken() is: " + sessCsrf.getToken());
} else { System.out.println("sessCsrf.getToken() is: null "); }
} else { System.out.println("sessCsrf is: null "); }
System.out.println("sess.getAttribute(SPRING_SECURITY_SAVED_REQUEST) is: " + sess.getAttribute("SPRING_SECURITY_SAVED_REQUEST") );
if(sess.getAttribute("SPRING_SECURITY_SAVED_REQUEST") instanceof DefaultSavedRequest){
System.out.println("_____ START PRINTING SAVED REQUEST");
DefaultSavedRequest savedReq = (DefaultSavedRequest) sess.getAttribute("SPRING_SECURITY_SAVED_REQUEST");
List<Cookie> savedCookies = savedReq.getCookies();
for(Cookie cook : savedCookies){
String name = cook.getName();String value = cook.getValue();
System.out.println("cookie name, value are: " + name + " , " + value);
}
Collection<String> savedHeaderNames = savedReq.getHeaderNames();
for(String headerName : savedHeaderNames){
System.out.println("headerName is: " + headerName);
}
List<Locale> savedLocales = savedReq.getLocales();
for(Locale loc : savedLocales){
System.out.println("loc.getLanguage() is: " + loc.getLanguage());
}
String savedMethod = savedReq.getMethod();
System.out.println("savedMethod is: " + savedMethod);
Map<String, String[]> savedParamMap = savedReq.getParameterMap();
Iterator<Entry<String, String[]>> it = savedParamMap.entrySet().iterator();
while (it.hasNext()) {
Entry<String, String[]> pair = it.next();
System.out.println("savedParamMap: " + pair.getKey() + " = " + pair.getValue());
it.remove(); // avoids a ConcurrentModificationException
}
Collection<String> savedParamNames = savedReq.getParameterNames();
for(String savedParamName : savedParamNames){
System.out.println("savedParamName: " + savedParamNames);
}
System.out.println("_____ DONE PRINTING SAVED REQUEST");
}
// System.out.println("sess.getAttribute(SPRING_SECURITY_CONTEXT) is: " + sess.getAttribute("SPRING_SECURITY_CONTEXT") );
if(sess.getAttribute("SPRING_SECURITY_CONTEXT") instanceof SecurityContextImpl){
SecurityContext ctxt = (SecurityContext) sess.getAttribute("SPRING_SECURITY_CONTEXT");
Authentication auth = ctxt.getAuthentication();
if(auth.getDetails() instanceof WebAuthenticationDetails){
WebAuthenticationDetails dets = (WebAuthenticationDetails) auth.getDetails();
System.out.println( "dets.getSessionId() is: " + dets.getSessionId() );
}
System.out.println("auth.getAuthorities() is: " + auth.getAuthorities() );
System.out.println("auth.isAuthenticated() is: " + auth.isAuthenticated() );
}
}
SecurityContext context = SecurityContextHolder.getContext();
System.out.println("...........///////////// END OF DiagnoseSessionFilter.doFilterInternal() ///////////...........");
fc.doFilter(req, res);
}
}
Aislando el código del problema:
A continuación, se combinan y resumen los datos de diagnóstico de HttpSessionListener
con las herramientas de desarrollador del navegador web para los pasos entre el usuario que hace clic en enviar en la vista de código PIN de envío y el navegador devuelve un rechazo del /oauth/token
.
Como puede ver, hay dos valores de JSESSIONID
flotando alrededor. Uno de los valores es correcto, mientras que el otro valor no es. El valor incorrecto se transfiere a la solicitud a /oauth/token
y provoca el rechazo, aunque el csrf
pasado sea correcto. Por lo tanto, la solución a este problema probablemente vendrá de la modificación de los pasos a continuación para dejar de colocar el JSESSIONID
incorrecto en lugar del JSESSIONID
:
1.) POST http://localhost:9999/uaa/secure/two_factor_authentication
request headers:
Referer: 9999/uaa/secure/two_factor_authentication
Cookie:
JSESSIONID: ....95CB77
....918636
XSRF-TOKEN: ....862a73
filter chain:
DiagnoseSessionFilter:
request stuff:
Cookie header:
JSESSIONID: ....95CB77
....918636
XSRF-TOKEN: ....862a73
request.getSession(false).getId(): ....95CB77
session collector stuff:
JSESSIONID: ....95CB77
csrf: ....862a73
SPRING_SECURITY_SAVED_REQUEST is null
user details (from Authentication object with user/request
JSESSIONID: ....ED927C
Authenticated = true, with roles
Complete the filter chain
DiagnoseSessionFilter (again)
request stuff:
csrf attribute: ....862a73
Cookie header:
JSESSIONID: ....95CB77
....918636
XSRF-TOKEN: ....862a73
request.getSession(false).getId(): 95CB77
session collector stuff:
JSESSIONID: ....95CB77
csrf is: 862a73
SPRING_SECURITY_SAVED_REQUEST is null
user details (Authentication for user/session/request)
JSESSIONID: ....ED927C
Authenticated = true, with authorities
POST/secure/two_factor_authenticationControllerMethod
do some stuff
response:
Location: 9999/uaa/oauth/authorize?....
XSRF-TOKEN: ....862a73
2.) GET http://localhost:9999/uaa/oauth/authorize?...
request headers:
Host: localhost:9999
Referer: 9999/uaa/secure/two_factor_authentication
Cookie:
JSESSIONID: ....95CB77
....918636
XSRF-TOKEN: ....862a73
FilterChain
DiagnoseSessionFilter
request stuff:
Cookie header is: JSESSIONID: ....95CB77
....918636
XSRF-TOKEN: ....862a73
request.getSession(false).getId(): 95CB77
session collector stuff:
JSESSIONID: ....95CB77
csrf is: ....862a73
SPRING_SECURITY_SAVED_REQUEST is: null
user details (Authentication object with user/session/req)
JSESSIONID: ....ED927C
Authenticated = true with ALL roles.
rest of filter chain
TwoFactorAuthenticationFilter
request stuff:
csrf request attribute is: ....862a73
cookie header:
JSESSIONID: ....95CB77
....918636
XSRF-TOKEN: ....862a73
request.getSession(false).getId() is: ....95CB77
updateCsrf is: ....862a73
response stuff:
XSRF-TOKEN header (after manual update): ....862a73
DiagnoseSessionFilter:
request stuff:
_csrf request attribute: ....862a73
Cookie header:
JSESSIONID: ....95CB77
....918636
XSRF-TOKEN: ....862a73
request.getSession(false).getId() is: ....95CB77
session collector stuff:
JSESSIONID: ....95CB77
csrf is: ....862a73
SPRING_SECURITY_SAVED_REQUEST is: null
user details (Authentication for user/session/request)
JSESSIONID: ....ED927C
Authenticated is true, with ALL roles.
CustomOAuth2RequestFactory
request stuff:
_csrf request parameter is: ....862a73
Cookie header:
JSESSIONID: ....95CB77
....918636
XSRF-TOKEN: ....862a73
request.getSession(false).getId() is: ....95CB77
updateCsrf: ....862a73
response stuff:
XSRF-TOKEN header: ....862a73
session attribute printout
csrf: ....862a73
SPRING_SECURITY_CONTEXT (not printed, so don''t know values)
response:
Location: 8080/login?code=myNwd7&state=f6b3Km
XSRF-TOKEN: ....862a73
3.) GET http://localhost:8080/login?code=myNwd7&state=f6b3Km
request headers:
Host: localhost:8080
Referer: 9999/uaa/secure/two_factor_authentication
Cookie:
JSESSIONID: ....918636
XSRF-TOKEN: ....862a73
UiAppFilterChain:
HttpSessionSecurityContextRepository
creates new SPRING_SECURITY_CONTEXT to replace null one
OAuth2ClientAuthenticationProcessingFilter (position 8 of 14)
AuthorizationCodeAccessTokenProvider
Retrieving token from 9999/uaa/oauth/token
AuthServerFilterChain:
DiagnoseSessionFilter
request stuff:
XSRF-TOKEN header is: null
Cookie header is: null
Set-Cookie header is: null
referrer header is: null
request.getSession(false) is: null
session collector stuff:
JSESSIONID: ....95CB77
sessCsrf.getToken() is: 862a73
SPRING_SECURITY_SAVED_REQUEST is: null
Authenticated is true but with ONLY these roles:
ROLE_HOBBIT, ROLE_TWO_FACTOR_AUTHENTICATION_ENABLED
SecurityContextPersistenceFilter
reports no HttpSession and no SPRING_SECURITY_CONTEXT
CsrfFilter
rejects request to /oauth/token due to no session % csrf
response headers:
Set-Cookie:
XSRF-TOKEN: ....527fbe
X-Frame-Options: DENY
Trataré de pasar un poco más de tiempo con esto para aislar aún más la solución, dada la cantidad de puntos que está ofreciendo. Pero lo anterior debería reducir sustancialmente el problema.
Estoy publicando esto antes de que esté completamente terminado porque su período de recompensa está a punto de caducar.