io.undertow.security.impl.SecurityContextImpl Maven / Gradle / Ivy
/*
* 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 static io.undertow.security.api.SecurityNotification.EventType.AUTHENTICATED;
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.api.NotificationReceiver;
import io.undertow.security.api.SecurityNotification;
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
* @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.
*/
private Node 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.
*
* @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 cur = authMechanisms;
while (cur.next != null) {
cur = cur.next;
}
cur.next = new Node<>(handler);
}
if (handler instanceof FormAuthenticationMechanism) {
registerNotificationReceiver(new NotificationReceiver() {
@Override
public void handleNotification(final SecurityNotification notification) {
if (notification.getEventType() == AUTHENTICATED) {
((FormAuthenticationMechanism) handler).restoreOriginalSessionTimeout(exchange);
}
}
});
}
}
@Override
@Deprecated
public List getAuthenticationMechanisms() {
List ret = new LinkedList<>();
Node 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() {
@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 currentMethod;
private final HttpServerExchange exchange;
private AuthAttempter(Node 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.
*/
private class ChallengeSender {
private Node currentMethod;
private final HttpServerExchange exchange;
private Integer chosenStatusCode = null;
private boolean challengeSent = false;
private ChallengeSender(Node 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.
*/
enum AuthenticationState {
NOT_ATTEMPTED,
ATTEMPTED,
AUTHENTICATED,
CHALLENGE_SENT;
}
/**
* To reduce allocations we use a custom linked list data structure
* @param
*/
private static final class Node {
final T item;
Node next;
private Node(T item) {
this.item = item;
}
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy