/* * JBoss, Home of Professional Open Source. * Copyright 2012, Red Hat, Inc., and individual contributors * as indicated by the @author tags. See the copyright.txt file in the * distribution for a full listing of individual contributors. * * This is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. */ package org.jboss.as.domain.http.server.security; import static io.undertow.util.Headers.AUTHORIZATION; import static io.undertow.util.Headers.HOST; import static io.undertow.util.Headers.LOCATION; import static io.undertow.util.Headers.REFERER; import static io.undertow.util.Headers.USER_AGENT; import io.undertow.io.IoCallback; import io.undertow.security.api.AuthenticationMechanism; import io.undertow.security.idm.DigestAlgorithm; import io.undertow.security.impl.BasicAuthenticationMechanism; import io.undertow.security.impl.DigestAuthenticationMechanism; import io.undertow.security.impl.DigestQop; import io.undertow.security.impl.SimpleNonceManager; import io.undertow.server.HttpHandler; import io.undertow.server.HttpServerExchange; import io.undertow.util.FlexBase64; import io.undertow.util.HeaderMap; import io.undertow.util.StatusCodes; import static java.nio.charset.StandardCharsets.UTF_8; import java.net.URI; import java.net.URISyntaxException; import java.nio.ByteBuffer; import java.util.Collections; import java.util.Deque; import java.util.List; import java.util.Map; /** * * @author Jason T. Greene * @author <a href="mailto:darran.lofthouse@jboss.com">Darran Lofthouse</a> */ public class LogoutHandler implements HttpHandler { public static final String PATH = "/logout"; public static final String CONTEXT = "org.jboss.as.console.logout.context"; private static final String EXIT = "org.jboss.as.console.logout.exit"; private static final String HIT_ESCAPE = "HIT THE ESCAPE KEY"; private static final String BASIC = "BASIC"; private static final String DIGEST = "DIGEST"; private static final String MECHANISM = "mechanism"; private final DigestAuthenticationMechanism digestMechanism; private final DigestAuthenticationMechanism fakeRealmdigestMechanism; private final BasicAuthenticationMechanism basicMechanism; private final BasicAuthenticationMechanism fakeRealmBasicMechanism; public LogoutHandler(final String realmName) { List<DigestAlgorithm> digestAlgorithms = Collections.singletonList(DigestAlgorithm.MD5); List<DigestQop> digestQops = Collections.emptyList(); digestMechanism = new DigestAuthenticationMechanism(digestAlgorithms, digestQops, realmName, "/management", new SimpleNonceManager()); fakeRealmdigestMechanism = new DigestAuthenticationMechanism(digestAlgorithms, digestQops, HIT_ESCAPE, "/management", new SimpleNonceManager()); basicMechanism = new BasicAuthenticationMechanism(realmName); fakeRealmBasicMechanism = new BasicAuthenticationMechanism(HIT_ESCAPE); } @Override public void handleRequest(HttpServerExchange exchange) throws Exception { final HeaderMap requestHeaders = exchange.getRequestHeaders(); final HeaderMap responseHeaders = exchange.getResponseHeaders(); String referrer = responseHeaders.getFirst(REFERER); String protocol = exchange.getRequestScheme(); String host = null; if (referrer != null) { try { URI uri = new URI(referrer); protocol = uri.getScheme(); host = uri.getHost() + portPortion(protocol, uri.getPort()); } catch (URISyntaxException e) { } } if (host == null) { host = requestHeaders.getFirst(HOST); if (host == null) { exchange.setStatusCode(StatusCodes.INTERNAL_SERVER_ERROR); return; } } /* * Main sequence of events: * * 1. Redirect to self using user:pass@host form of authority. This forces Safari to overwrite its cache. (Also * forces FF and Chrome, but not absolutely necessary) Set the exit flag as a state signal for step 3 * * 2. Send 401 digest without a nonce stale marker, this will force FF and Chrome and likely other browsers to * assume an invalid (old) password. In the case of Opera, which doesn't invalidate under such a circumstance, * send an invalid realm. This will overwrite its auth cache, since it indexes it by host and not realm. * * 3. The credentials in 307 redirect wlll be transparently accepted and a final redirect to the console is * performed. Opera ignores these, so the user must hit escape which will use javascript to perform the redirect * * In the case of Internet Explorer, all of this will be bypassed and will simply redirect to the console. The console * MUST use a special javascript call before redirecting to logout. */ String userAgent = requestHeaders.getFirst(USER_AGENT); boolean opera = userAgent != null && userAgent.contains("Opera"); boolean win = !opera && userAgent != null && userAgent.contains("MSIE"); String rawQuery = exchange.getQueryString(); boolean exit = rawQuery != null && rawQuery.contains(EXIT); if (win) { responseHeaders.add(LOCATION, protocol + "://" + host + "/"); exchange.setStatusCode(StatusCodes.TEMPORARY_REDIRECT); } else { // Do the redirects to finish the logout String authorization = requestHeaders.getFirst(AUTHORIZATION); boolean digest = true; Map<String, Deque<String>> parameters = exchange.getQueryParameters(); if (parameters.containsKey(MECHANISM)) { digest = !BASIC.equals(parameters.get(MECHANISM).getFirst()); } if (authorization != null && authorization.length() > BASIC.length() && BASIC.equalsIgnoreCase(authorization.substring(0, BASIC.length()))) { digest = false; ByteBuffer decode = FlexBase64.decode(authorization.substring(6)); authorization = new String(decode.array(), decode.arrayOffset(), decode.limit(), UTF_8); } if (authorization == null || !authorization.contains("enter-login-here")) { if (!exit) { responseHeaders.add(LOCATION, protocol + "://enter-login-here:blah@" + host + "/logout?" + EXIT + "&" + MECHANISM + "=" + (digest ? DIGEST : BASIC)); exchange.setStatusCode(StatusCodes.TEMPORARY_REDIRECT); return; } mechanism(opera, digest).sendChallenge(exchange, null); String reply = "<html><script type='text/javascript'>window.location=\"" + protocol + "://" + host + "/\";</script></html>"; exchange.setStatusCode(StatusCodes.UNAUTHORIZED); exchange.getResponseSender().send(reply, IoCallback.END_EXCHANGE); return; } // Success, now back to the login screen responseHeaders.add(LOCATION, protocol + "://" + host + "/"); exchange.setStatusCode(StatusCodes.TEMPORARY_REDIRECT); } } private AuthenticationMechanism mechanism(final boolean opera, final boolean digest) { if (digest) { return opera ? fakeRealmdigestMechanism : digestMechanism; } else { return opera ? fakeRealmBasicMechanism : basicMechanism; } } private String portPortion(final String scheme, final int port) { if (port == -1 || "http".equals(scheme) && port == 80 || "https".equals(scheme) && port == 443) { return ""; } return ":" + String.valueOf(port); } }