1 /*
2  * JBoss, Home of Professional Open Source.
3  * Copyright 2014 Red Hat, Inc., and individual contributors
4  * as indicated by the @author tags.
5  *
6  * Licensed under the Apache License, Version 2.0 (the "License");
7  * you may not use this file except in compliance with the License.
8  * You may obtain a copy of the License at
9  *
10  *     http://www.apache.org/licenses/LICENSE-2.0
11  *
12  *  Unless required by applicable law or agreed to in writing, software
13  *  distributed under the License is distributed on an "AS IS" BASIS,
14  *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15  *  See the License for the specific language governing permissions and
16  *  limitations under the License.
17  */

18 package io.undertow.security.impl;
19
20 import io.undertow.UndertowLogger;
21 import io.undertow.UndertowMessages;
22 import io.undertow.security.api.AuthenticationMechanism;
23 import io.undertow.security.api.AuthenticationMechanism.AuthenticationMechanismOutcome;
24 import io.undertow.security.api.AuthenticationMechanism.ChallengeResult;
25 import io.undertow.security.api.AuthenticationMechanismContext;
26 import io.undertow.security.api.AuthenticationMode;
27 import io.undertow.security.idm.Account;
28 import io.undertow.security.idm.IdentityManager;
29 import io.undertow.security.idm.PasswordCredential;
30 import io.undertow.server.HttpServerExchange;
31 import io.undertow.util.StatusCodes;
32
33 import java.security.AccessController;
34 import java.security.PrivilegedAction;
35 import java.util.Collections;
36 import java.util.LinkedList;
37 import java.util.List;
38
39 /**
40  * The internal SecurityContext used to hold the state of security for the current exchange.
41  *
42  * @author <a href="mailto:darran.lofthouse@jboss.com">Darran Lofthouse</a>
43  * @author Stuart Douglas
44  */

45 public class SecurityContextImpl extends AbstractSecurityContext implements AuthenticationMechanismContext {
46
47
48     private static final RuntimePermission PERMISSION = new RuntimePermission("MODIFY_UNDERTOW_SECURITY_CONTEXT");
49
50     private AuthenticationState authenticationState = AuthenticationState.NOT_ATTEMPTED;
51     private final AuthenticationMode authenticationMode;
52
53     private String programaticMechName = "Programatic";
54
55     /**
56      * the authentication mechanisms. Note that in order to reduce the allocation of list and iterator structures
57      * we use a custom linked list structure.
58      */

59     private Node<AuthenticationMechanism> authMechanisms = null;
60     private final IdentityManager identityManager;
61
62     public SecurityContextImpl(final HttpServerExchange exchange, final IdentityManager identityManager) {
63         this(exchange, AuthenticationMode.PRO_ACTIVE, identityManager);
64     }
65
66     public SecurityContextImpl(final HttpServerExchange exchange, final AuthenticationMode authenticationMode, final IdentityManager identityManager) {
67         super(exchange);
68         this.authenticationMode = authenticationMode;
69         this.identityManager = identityManager;
70         if (System.getSecurityManager() != null) {
71             System.getSecurityManager().checkPermission(PERMISSION);
72         }
73     }
74
75     /*
76      * Authentication can be represented as being at one of many states with different transitions depending on desired outcome.
77      *
78      * NOT_ATTEMPTED
79      * ATTEMPTED
80      * AUTHENTICATED
81      * CHALLENGED_SENT
82      */

83
84     @Override
85     public boolean authenticate() {
86         UndertowLogger.SECURITY_LOGGER.debugf("Attempting to authenticate %s, authentication required: %s", exchange.getRequestPath(), isAuthenticationRequired());
87         if(authenticationState == AuthenticationState.ATTEMPTED || (authenticationState == AuthenticationState.CHALLENGE_SENT && !exchange.isResponseStarted())) {
88             //we are re-attempted, so we just reset the state
89             //see UNDERTOW-263
90             authenticationState = AuthenticationState.NOT_ATTEMPTED;
91         }
92         return !authTransition();
93     }
94
95     private boolean authTransition() {
96         if (authTransitionRequired()) {
97             switch (authenticationState) {
98                 case NOT_ATTEMPTED:
99                     authenticationState = attemptAuthentication();
100                     break;
101                 case ATTEMPTED:
102                     authenticationState = sendChallenges();
103                     break;
104                 default:
105                     throw new IllegalStateException("It should not be possible to reach this.");
106             }
107             return authTransition();
108
109         } else {
110             UndertowLogger.SECURITY_LOGGER.debugf("Authentication result was %s for %s", authenticationState, exchange.getRequestPath());
111             // Keep in mind this switch statement is only called after a call to authTransitionRequired.
112             switch (authenticationState) {
113                 case NOT_ATTEMPTED: // No constraint was set that mandated authentication so not reason to hold up the request.
114                 case ATTEMPTED: // Attempted based on incoming request but no a failure so allow the request to proceed.
115                 case AUTHENTICATED: // Authentication was a success - no responses sent.
116                     return false;
117                 default:
118                     // Remaining option is CHALLENGE_SENT to request processing must end.
119                     return true;
120             }
121         }
122     }
123
124     private AuthenticationState attemptAuthentication() {
125         return new AuthAttempter(authMechanisms,exchange).transition();
126     }
127
128     private AuthenticationState sendChallenges() {
129         UndertowLogger.SECURITY_LOGGER.debugf("Sending authentication challenge for %s", exchange);
130         return new ChallengeSender(authMechanisms, exchange).transition();
131     }
132
133     private boolean authTransitionRequired() {
134         switch (authenticationState) {
135             case NOT_ATTEMPTED:
136                 // There has been no attempt to authenticate the current request so do so either if required or if we are set to
137                 // be pro-active.
138                 return isAuthenticationRequired() || authenticationMode == AuthenticationMode.PRO_ACTIVE;
139             case ATTEMPTED:
140                 // To be ATTEMPTED we know it was not AUTHENTICATED so if it is required we need to transition to send the
141                 // challenges.
142                 return isAuthenticationRequired();
143             default:
144                 // At this point the state would either be AUTHENTICATED or CHALLENGE_SENT - either of which mean no further
145                 // transitions applicable for this request.
146                 return false;
147         }
148     }
149
150     /**
151      * Set the name of the mechanism used for authentication to be reported if authentication was handled programatically.
152      *
153      * @param programaticMechName
154      */

155     public void setProgramaticMechName(final String programaticMechName) {
156         this.programaticMechName = programaticMechName;
157     }
158
159     @Override
160     public void addAuthenticationMechanism(final AuthenticationMechanism handler) {
161         // TODO - Do we want to change this so we can ensure the mechanisms are not modifiable mid request?
162         if(authMechanisms == null) {
163             authMechanisms = new Node<>(handler);
164         } else {
165             Node<AuthenticationMechanism> cur = authMechanisms;
166             while (cur.next != null) {
167                 cur = cur.next;
168             }
169             cur.next = new Node<>(handler);
170         }
171     }
172
173     @Override
174     @Deprecated
175     public List<AuthenticationMechanism> getAuthenticationMechanisms() {
176         List<AuthenticationMechanism> ret = new LinkedList<>();
177         Node<AuthenticationMechanism> cur = authMechanisms;
178         while (cur != null) {
179             ret.add(cur.item);
180             cur = cur.next;
181         }
182         return Collections.unmodifiableList(ret);
183     }
184
185     @Override
186     @Deprecated
187     public IdentityManager getIdentityManager() {
188         return identityManager;
189     }
190
191     @Override
192     public boolean login(final String username, final String password) {
193
194         UndertowLogger.SECURITY_LOGGER.debugf("Attempting programatic login for user %s for request %s", username, exchange);
195
196         final Account account;
197         if(System.getSecurityManager() == null) {
198             account = identityManager.verify(username, new PasswordCredential(password.toCharArray()));
199         } else {
200             account = AccessController.doPrivileged(new PrivilegedAction<Account>() {
201                 @Override
202                 public Account run() {
203                     return identityManager.verify(username, new PasswordCredential(password.toCharArray()));
204                 }
205             });
206         }
207
208         if (account == null) {
209             return false;
210         }
211
212         authenticationComplete(account, programaticMechName, true);
213         this.authenticationState = AuthenticationState.AUTHENTICATED;
214
215         return true;
216     }
217
218     @Override
219     public void logout() {
220         Account authenticatedAccount = getAuthenticatedAccount();
221         if(authenticatedAccount != null) {
222             UndertowLogger.SECURITY_LOGGER.debugf("Logging out user %s for %s", authenticatedAccount.getPrincipal().getName(), exchange);
223         } else {
224             UndertowLogger.SECURITY_LOGGER.debugf("Logout called with no authenticated user in exchange %s", exchange);
225         }
226         super.logout();
227         this.authenticationState = AuthenticationState.NOT_ATTEMPTED;
228     }
229
230
231     private class AuthAttempter {
232
233         private Node<AuthenticationMechanism> currentMethod;
234         private final HttpServerExchange exchange;
235
236         private AuthAttempter(Node<AuthenticationMechanism> currentMethod, final HttpServerExchange exchange) {
237             this.exchange = exchange;
238             this.currentMethod = currentMethod;
239         }
240
241         private AuthenticationState transition() {
242             if (currentMethod != null) {
243                 final AuthenticationMechanism mechanism = currentMethod.item;
244                 currentMethod = currentMethod.next;
245                 AuthenticationMechanismOutcome outcome = mechanism.authenticate(exchange, SecurityContextImpl.this);
246                 if(UndertowLogger.SECURITY_LOGGER.isDebugEnabled()) {
247                     UndertowLogger.SECURITY_LOGGER.debugf("Authentication outcome was %s with method %s for %s", outcome, mechanism, exchange.getRequestURI());
248                     if(UndertowLogger.SECURITY_LOGGER.isTraceEnabled()) {
249                         UndertowLogger.SECURITY_LOGGER.tracef("Contents of exchange after authentication attempt is %s", exchange);
250                     }
251                 }
252
253                 if (outcome == null) {
254                     throw UndertowMessages.MESSAGES.authMechanismOutcomeNull();
255                 }
256
257                 switch (outcome) {
258                     case AUTHENTICATED:
259                         // TODO - Should verify that the mechanism did register an authenticated Account.
260                         return AuthenticationState.AUTHENTICATED;
261                     case NOT_AUTHENTICATED:
262                         // A mechanism attempted to authenticate but could not complete, this now means that
263                         // authentication is required and challenges need to be sent.
264                         setAuthenticationRequired();
265                         return AuthenticationState.ATTEMPTED;
266                     case NOT_ATTEMPTED:
267                         // Time to try the next mechanism.
268                         return transition();
269                     default:
270                         throw new IllegalStateException();
271                 }
272
273             } else {
274                 // Reached the end of the mechanisms and no mechanism authenticated for us to reach this point.
275                 return AuthenticationState.ATTEMPTED;
276             }
277         }
278
279     }
280
281     /**
282      * Class responsible for sending the authentication challenges.
283      */

284     private class ChallengeSender {
285
286         private Node<AuthenticationMechanism> currentMethod;
287         private final HttpServerExchange exchange;
288
289         private Integer chosenStatusCode = null;
290         private boolean challengeSent = false;
291
292         private ChallengeSender(Node<AuthenticationMechanism> currentMethod, final HttpServerExchange exchange) {
293             this.exchange = exchange;
294             this.currentMethod = currentMethod;
295         }
296
297         private AuthenticationState transition() {
298             if (currentMethod != null) {
299                 final AuthenticationMechanism mechanism = currentMethod.item;
300                 currentMethod = currentMethod.next;
301                 ChallengeResult result = mechanism.sendChallenge(exchange, SecurityContextImpl.this);
302                 if(result == null) {
303                     throw UndertowMessages.MESSAGES.sendChallengeReturnedNull(mechanism);
304                 }
305                 if (result.isChallengeSent()) {
306                     challengeSent = true;
307                     Integer desiredCode = result.getDesiredResponseCode();
308                     if (desiredCode != null && (chosenStatusCode == null || chosenStatusCode.equals(StatusCodes.OK))) {
309                         chosenStatusCode = desiredCode;
310                         if (chosenStatusCode.equals(StatusCodes.OK) == false) {
311                             if(!exchange.isResponseStarted()) {
312                                 exchange.setStatusCode(chosenStatusCode);
313                             }
314                         }
315                     }
316                 }
317
318                 // We always transition so we can reach the end of the list and hit the else.
319                 return transition();
320
321             } else {
322                 if(!exchange.isResponseStarted()) {
323                     // Iterated all mechanisms, if OK it will not be set yet.
324                     if (chosenStatusCode == null) {
325                         if (challengeSent == false) {
326                             // No mechanism generated a challenge so send a 403 as our challenge - i.e. just rejecting the request.
327                             exchange.setStatusCode(StatusCodes.FORBIDDEN);
328                         }
329                     } else if (chosenStatusCode.equals(StatusCodes.OK)) {
330                         exchange.setStatusCode(chosenStatusCode);
331                     }
332                 }
333
334                 return AuthenticationState.CHALLENGE_SENT;
335             }
336         }
337
338     }
339
340     /**
341      * Representation of the current authentication state of the SecurityContext.
342      */

343     enum AuthenticationState {
344         NOT_ATTEMPTED,
345
346         ATTEMPTED,
347
348         AUTHENTICATED,
349
350         CHALLENGE_SENT;
351     }
352
353     /**
354      * To reduce allocations we use a custom linked list data structure
355      * @param <T>
356      */

357     private static final class Node<T> {
358         final T item;
359         Node<T> next;
360
361         private Node(T item) {
362             this.item = item;
363         }
364     }
365
366 }
367