/* * Copyright 2016 Red Hat, Inc. and/or its affiliates * and other contributors as indicated by the @author tags. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.keycloak.authentication.authenticators.browser; import org.jboss.logging.Logger; import org.jboss.resteasy.spi.HttpRequest; import org.keycloak.authentication.AuthenticationFlowContext; import org.keycloak.authentication.AuthenticationFlowError; import org.keycloak.authentication.Authenticator; import org.keycloak.common.constants.KerberosConstants; import org.keycloak.events.Errors; import org.keycloak.forms.login.LoginFormsProvider; import org.keycloak.models.CredentialValidationOutput; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserCredentialModel; import org.keycloak.models.UserModel; import org.keycloak.services.messages.Messages; import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import java.net.URI; import java.util.Map; /** * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a> * @version $Revision: 1 $ */ public class SpnegoAuthenticator extends AbstractUsernameFormAuthenticator implements Authenticator{ public static final String KERBEROS_DISABLED = "kerberos_disabled"; private static final Logger logger = Logger.getLogger(SpnegoAuthenticator.class); @Override public boolean requiresUser() { return false; } @Override public void action(AuthenticationFlowContext context) { context.attempted(); return; } @Override public void authenticate(AuthenticationFlowContext context) { HttpRequest request = context.getHttpRequest(); String authHeader = request.getHttpHeaders().getRequestHeaders().getFirst(HttpHeaders.AUTHORIZATION); if (authHeader == null) { Response challenge = challengeNegotiation(context, null); context.forceChallenge(challenge); return; } String[] tokens = authHeader.split(" "); if (tokens.length == 0) { // assume not supported logger.debug("Invalid length of tokens: " + tokens.length); context.attempted(); return; } if (!KerberosConstants.NEGOTIATE.equalsIgnoreCase(tokens[0])) { logger.debug("Unknown scheme " + tokens[0]); context.attempted(); return; } if (tokens.length != 2) { context.failure(AuthenticationFlowError.INVALID_CREDENTIALS); return; } String spnegoToken = tokens[1]; UserCredentialModel spnegoCredential = UserCredentialModel.kerberos(spnegoToken); CredentialValidationOutput output = context.getSession().userCredentialManager().authenticate(context.getSession(), context.getRealm(), spnegoCredential); if (output == null) { logger.warn("Received kerberos token, but there is no user storage provider that handles kerberos credentials."); context.attempted(); return; } if (output.getAuthStatus() == CredentialValidationOutput.Status.AUTHENTICATED) { context.setUser(output.getAuthenticatedUser()); if (output.getState() != null && !output.getState().isEmpty()) { for (Map.Entry<String, String> entry : output.getState().entrySet()) { context.getAuthenticationSession().setUserSessionNote(entry.getKey(), entry.getValue()); } } context.success(); } else if (output.getAuthStatus() == CredentialValidationOutput.Status.CONTINUE) { String spnegoResponseToken = (String) output.getState().get(KerberosConstants.RESPONSE_TOKEN); Response challenge = challengeNegotiation(context, spnegoResponseToken); context.challenge(challenge); } else { context.getEvent().error(Errors.INVALID_USER_CREDENTIALS); context.failure(AuthenticationFlowError.INVALID_CREDENTIALS); } } private Response challengeNegotiation(AuthenticationFlowContext context, final String negotiateToken) { String negotiateHeader = negotiateToken == null ? KerberosConstants.NEGOTIATE : KerberosConstants.NEGOTIATE + " " + negotiateToken; if (logger.isTraceEnabled()) { logger.trace("Sending back " + HttpHeaders.WWW_AUTHENTICATE + ": " + negotiateHeader); } if (context.getExecution().isRequired()) { return context.getSession().getProvider(LoginFormsProvider.class) .setStatus(Response.Status.UNAUTHORIZED) .setResponseHeader(HttpHeaders.WWW_AUTHENTICATE, negotiateHeader) .setError(Messages.KERBEROS_NOT_ENABLED).createErrorPage(); } else { return optionalChallengeRedirect(context, negotiateHeader); } } // This is used for testing only. Selenium will execute the HTML challenge sent back which results in the javascript // redirecting. Our old Selenium tests expect that the current URL will be the original openid redirect. public static boolean bypassChallengeJavascript = false; /** * 401 challenge sent back that bypasses * @param context * @param negotiateHeader * @return */ protected Response optionalChallengeRedirect(AuthenticationFlowContext context, String negotiateHeader) { String accessCode = context.generateAccessCode(); URI action = context.getActionUrl(accessCode); StringBuilder builder = new StringBuilder(); builder.append("<HTML>"); builder.append("<HEAD>"); builder.append("<TITLE>Kerberos Unsupported</TITLE>"); builder.append("</HEAD>"); if (bypassChallengeJavascript) { builder.append("<BODY>"); } else { builder.append("<BODY Onload=\"document.forms[0].submit()\">"); } builder.append("<FORM METHOD=\"POST\" ACTION=\"" + action.toString() + "\">"); builder.append("<NOSCRIPT>"); builder.append("<P>JavaScript is disabled. We strongly recommend to enable it. You were unable to login via Kerberos. Click the button below to login via an alternative method .</P>"); builder.append("<INPUT name=\"continue\" TYPE=\"SUBMIT\" VALUE=\"CONTINUE\" />"); builder.append("</NOSCRIPT>"); builder.append("</FORM></BODY></HTML>"); return Response.status(Response.Status.UNAUTHORIZED) .header(HttpHeaders.WWW_AUTHENTICATE, negotiateHeader) .type(MediaType.TEXT_HTML_TYPE) .entity(builder.toString()).build(); } @Override public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) { return true; } @Override public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) { } @Override public void close() { } }