/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2014 Red Hat, Inc., and individual 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 io.undertow.security.impl;

import io.undertow.UndertowLogger;
import io.undertow.UndertowMessages;
import io.undertow.security.api.AuthenticationMechanism;
import io.undertow.security.api.AuthenticationMechanism.AuthenticationMechanismOutcome;
import io.undertow.security.api.AuthenticationMechanism.ChallengeResult;
import io.undertow.security.api.AuthenticationMechanismContext;
import io.undertow.security.api.AuthenticationMode;
import io.undertow.security.idm.Account;
import io.undertow.security.idm.IdentityManager;
import io.undertow.security.idm.PasswordCredential;
import io.undertow.server.HttpServerExchange;
import io.undertow.util.StatusCodes;

import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;

The internal SecurityContext used to hold the state of security for the current exchange.
Author:Darran Lofthouse, Stuart Douglas
/** * The internal SecurityContext used to hold the state of security for the current exchange. * * @author <a href="mailto:darran.lofthouse@jboss.com">Darran Lofthouse</a> * @author Stuart Douglas */
public class SecurityContextImpl extends AbstractSecurityContext implements AuthenticationMechanismContext { private static final RuntimePermission PERMISSION = new RuntimePermission("MODIFY_UNDERTOW_SECURITY_CONTEXT"); private AuthenticationState authenticationState = AuthenticationState.NOT_ATTEMPTED; private final AuthenticationMode authenticationMode; private String programaticMechName = "Programatic";
the authentication mechanisms. Note that in order to reduce the allocation of list and iterator structures we use a custom linked list structure.
/** * the authentication mechanisms. Note that in order to reduce the allocation of list and iterator structures * we use a custom linked list structure. */
private Node<AuthenticationMechanism> authMechanisms = null; private final IdentityManager identityManager; public SecurityContextImpl(final HttpServerExchange exchange, final IdentityManager identityManager) { this(exchange, AuthenticationMode.PRO_ACTIVE, identityManager); } public SecurityContextImpl(final HttpServerExchange exchange, final AuthenticationMode authenticationMode, final IdentityManager identityManager) { super(exchange); this.authenticationMode = authenticationMode; this.identityManager = identityManager; if (System.getSecurityManager() != null) { System.getSecurityManager().checkPermission(PERMISSION); } } /* * Authentication can be represented as being at one of many states with different transitions depending on desired outcome. * * NOT_ATTEMPTED * ATTEMPTED * AUTHENTICATED * CHALLENGED_SENT */ @Override public boolean authenticate() { UndertowLogger.SECURITY_LOGGER.debugf("Attempting to authenticate %s, authentication required: %s", exchange.getRequestPath(), isAuthenticationRequired()); if(authenticationState == AuthenticationState.ATTEMPTED || (authenticationState == AuthenticationState.CHALLENGE_SENT && !exchange.isResponseStarted())) { //we are re-attempted, so we just reset the state //see UNDERTOW-263 authenticationState = AuthenticationState.NOT_ATTEMPTED; } return !authTransition(); } private boolean authTransition() { if (authTransitionRequired()) { switch (authenticationState) { case NOT_ATTEMPTED: authenticationState = attemptAuthentication(); break; case ATTEMPTED: authenticationState = sendChallenges(); break; default: throw new IllegalStateException("It should not be possible to reach this."); } return authTransition(); } else { UndertowLogger.SECURITY_LOGGER.debugf("Authentication result was %s for %s", authenticationState, exchange.getRequestPath()); // Keep in mind this switch statement is only called after a call to authTransitionRequired. switch (authenticationState) { case NOT_ATTEMPTED: // No constraint was set that mandated authentication so not reason to hold up the request. case ATTEMPTED: // Attempted based on incoming request but no a failure so allow the request to proceed. case AUTHENTICATED: // Authentication was a success - no responses sent. return false; default: // Remaining option is CHALLENGE_SENT to request processing must end. return true; } } } private AuthenticationState attemptAuthentication() { return new AuthAttempter(authMechanisms,exchange).transition(); } private AuthenticationState sendChallenges() { UndertowLogger.SECURITY_LOGGER.debugf("Sending authentication challenge for %s", exchange); return new ChallengeSender(authMechanisms, exchange).transition(); } private boolean authTransitionRequired() { switch (authenticationState) { case NOT_ATTEMPTED: // There has been no attempt to authenticate the current request so do so either if required or if we are set to // be pro-active. return isAuthenticationRequired() || authenticationMode == AuthenticationMode.PRO_ACTIVE; case ATTEMPTED: // To be ATTEMPTED we know it was not AUTHENTICATED so if it is required we need to transition to send the // challenges. return isAuthenticationRequired(); default: // At this point the state would either be AUTHENTICATED or CHALLENGE_SENT - either of which mean no further // transitions applicable for this request. return false; } }
Set the name of the mechanism used for authentication to be reported if authentication was handled programatically.
Params:
  • programaticMechName –
/** * Set the name of the mechanism used for authentication to be reported if authentication was handled programatically. * * @param programaticMechName */
public void setProgramaticMechName(final String programaticMechName) { this.programaticMechName = programaticMechName; } @Override public void addAuthenticationMechanism(final AuthenticationMechanism handler) { // TODO - Do we want to change this so we can ensure the mechanisms are not modifiable mid request? if(authMechanisms == null) { authMechanisms = new Node<>(handler); } else { Node<AuthenticationMechanism> cur = authMechanisms; while (cur.next != null) { cur = cur.next; } cur.next = new Node<>(handler); } } @Override @Deprecated public List<AuthenticationMechanism> getAuthenticationMechanisms() { List<AuthenticationMechanism> ret = new LinkedList<>(); Node<AuthenticationMechanism> cur = authMechanisms; while (cur != null) { ret.add(cur.item); cur = cur.next; } return Collections.unmodifiableList(ret); } @Override @Deprecated public IdentityManager getIdentityManager() { return identityManager; } @Override public boolean login(final String username, final String password) { UndertowLogger.SECURITY_LOGGER.debugf("Attempting programatic login for user %s for request %s", username, exchange); final Account account; if(System.getSecurityManager() == null) { account = identityManager.verify(username, new PasswordCredential(password.toCharArray())); } else { account = AccessController.doPrivileged(new PrivilegedAction<Account>() { @Override public Account run() { return identityManager.verify(username, new PasswordCredential(password.toCharArray())); } }); } if (account == null) { return false; } authenticationComplete(account, programaticMechName, true); this.authenticationState = AuthenticationState.AUTHENTICATED; return true; } @Override public void logout() { Account authenticatedAccount = getAuthenticatedAccount(); if(authenticatedAccount != null) { UndertowLogger.SECURITY_LOGGER.debugf("Logging out user %s for %s", authenticatedAccount.getPrincipal().getName(), exchange); } else { UndertowLogger.SECURITY_LOGGER.debugf("Logout called with no authenticated user in exchange %s", exchange); } super.logout(); this.authenticationState = AuthenticationState.NOT_ATTEMPTED; } private class AuthAttempter { private Node<AuthenticationMechanism> currentMethod; private final HttpServerExchange exchange; private AuthAttempter(Node<AuthenticationMechanism> currentMethod, final HttpServerExchange exchange) { this.exchange = exchange; this.currentMethod = currentMethod; } private AuthenticationState transition() { if (currentMethod != null) { final AuthenticationMechanism mechanism = currentMethod.item; currentMethod = currentMethod.next; AuthenticationMechanismOutcome outcome = mechanism.authenticate(exchange, SecurityContextImpl.this); if(UndertowLogger.SECURITY_LOGGER.isDebugEnabled()) { UndertowLogger.SECURITY_LOGGER.debugf("Authentication outcome was %s with method %s for %s", outcome, mechanism, exchange.getRequestURI()); if(UndertowLogger.SECURITY_LOGGER.isTraceEnabled()) { UndertowLogger.SECURITY_LOGGER.tracef("Contents of exchange after authentication attempt is %s", exchange); } } if (outcome == null) { throw UndertowMessages.MESSAGES.authMechanismOutcomeNull(); } switch (outcome) { case AUTHENTICATED: // TODO - Should verify that the mechanism did register an authenticated Account. return AuthenticationState.AUTHENTICATED; case NOT_AUTHENTICATED: // A mechanism attempted to authenticate but could not complete, this now means that // authentication is required and challenges need to be sent. setAuthenticationRequired(); return AuthenticationState.ATTEMPTED; case NOT_ATTEMPTED: // Time to try the next mechanism. return transition(); default: throw new IllegalStateException(); } } else { // Reached the end of the mechanisms and no mechanism authenticated for us to reach this point. return AuthenticationState.ATTEMPTED; } } }
Class responsible for sending the authentication challenges.
/** * Class responsible for sending the authentication challenges. */
private class ChallengeSender { private Node<AuthenticationMechanism> currentMethod; private final HttpServerExchange exchange; private Integer chosenStatusCode = null; private boolean challengeSent = false; private ChallengeSender(Node<AuthenticationMechanism> currentMethod, final HttpServerExchange exchange) { this.exchange = exchange; this.currentMethod = currentMethod; } private AuthenticationState transition() { if (currentMethod != null) { final AuthenticationMechanism mechanism = currentMethod.item; currentMethod = currentMethod.next; ChallengeResult result = mechanism.sendChallenge(exchange, SecurityContextImpl.this); if(result == null) { throw UndertowMessages.MESSAGES.sendChallengeReturnedNull(mechanism); } if (result.isChallengeSent()) { challengeSent = true; Integer desiredCode = result.getDesiredResponseCode(); if (desiredCode != null && (chosenStatusCode == null || chosenStatusCode.equals(StatusCodes.OK))) { chosenStatusCode = desiredCode; if (chosenStatusCode.equals(StatusCodes.OK) == false) { if(!exchange.isResponseStarted()) { exchange.setStatusCode(chosenStatusCode); } } } } // We always transition so we can reach the end of the list and hit the else. return transition(); } else { if(!exchange.isResponseStarted()) { // Iterated all mechanisms, if OK it will not be set yet. if (chosenStatusCode == null) { if (challengeSent == false) { // No mechanism generated a challenge so send a 403 as our challenge - i.e. just rejecting the request. exchange.setStatusCode(StatusCodes.FORBIDDEN); } } else if (chosenStatusCode.equals(StatusCodes.OK)) { exchange.setStatusCode(chosenStatusCode); } } return AuthenticationState.CHALLENGE_SENT; } } }
Representation of the current authentication state of the SecurityContext.
/** * Representation of the current authentication state of the SecurityContext. */
enum AuthenticationState { NOT_ATTEMPTED, ATTEMPTED, AUTHENTICATED, CHALLENGE_SENT; }
To reduce allocations we use a custom linked list data structure
Type parameters:
  • <T> –
/** * To reduce allocations we use a custom linked list data structure * @param <T> */
private static final class Node<T> { final T item; Node<T> next; private Node(T item) { this.item = item; } } }