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 static io.undertow.UndertowMessages.MESSAGES;
21
22 import java.io.IOException;
23 import java.nio.ByteBuffer;
24 import java.nio.charset.Charset;
25 import java.nio.charset.StandardCharsets;
26 import java.util.Collections;
27 import java.util.HashMap;
28 import java.util.LinkedHashMap;
29 import java.util.List;
30 import java.util.Locale;
31 import java.util.Map;
32 import java.util.regex.Pattern;
33
34 import io.undertow.UndertowLogger;
35 import io.undertow.UndertowMessages;
36 import io.undertow.security.api.AuthenticationMechanism;
37 import io.undertow.security.api.AuthenticationMechanismFactory;
38 import io.undertow.security.api.SecurityContext;
39 import io.undertow.security.idm.Account;
40 import io.undertow.security.idm.IdentityManager;
41 import io.undertow.security.idm.PasswordCredential;
42 import io.undertow.server.HttpServerExchange;
43 import io.undertow.server.handlers.form.FormParserFactory;
44 import io.undertow.util.FlexBase64;
45 import io.undertow.util.Headers;
46
47 import static io.undertow.util.Headers.AUTHORIZATION;
48 import static io.undertow.util.Headers.BASIC;
49 import static io.undertow.util.Headers.WWW_AUTHENTICATE;
50 import static io.undertow.util.StatusCodes.UNAUTHORIZED;
51
52 /**
53  * The authentication handler responsible for BASIC authentication as described by RFC2617
54  *
55  * @author <a href="mailto:darran.lofthouse@jboss.com">Darran Lofthouse</a>
56  */

57 public class BasicAuthenticationMechanism implements AuthenticationMechanism {
58
59     public static final AuthenticationMechanismFactory FACTORY = new Factory();
60
61     public static final String SILENT = "silent";
62     public static final String CHARSET = "charset";
63     /**
64      * A comma separated list of patterns and charsets. The pattern is a regular expression.
65      *
66      * Because different browsers user different encodings this allows for the correct encoding to be selected based
67      * on the current browser. In general though it is recommended that BASIC auth not be used when passwords contain
68      * characters outside ASCII, as some browsers use the current locate to determine encoding.
69      *
70      * This list must have an even number of elements, as it is interpreted as pattern,charset,pattern,charset,...
71      */

72     public static final String USER_AGENT_CHARSETS = "user-agent-charsets";
73
74     private final String name;
75     private final String challenge;
76
77     private static final String BASIC_PREFIX = BASIC + " ";
78     private static final String LOWERCASE_BASIC_PREFIX = BASIC_PREFIX.toLowerCase(Locale.ENGLISH);
79     private static final int PREFIX_LENGTH = BASIC_PREFIX.length();
80     private static final String COLON = ":";
81
82     /**
83      * If silent is true then this mechanism will only take effect if there is an Authorization header.
84      *
85      * This allows you to combine basic auth with form auth, so human users will use form based auth, but allows
86      * programmatic clients to login using basic auth.
87      */

88     private final boolean silent;
89
90     private final IdentityManager identityManager;
91
92     private final Charset charset;
93     private final Map<Pattern, Charset> userAgentCharsets;
94
95     public BasicAuthenticationMechanism(final String realmName) {
96         this(realmName, "BASIC");
97     }
98
99     public BasicAuthenticationMechanism(final String realmName, final String mechanismName) {
100         this(realmName, mechanismName, false);
101     }
102
103     public BasicAuthenticationMechanism(final String realmName, final String mechanismName, final boolean silent) {
104         this(realmName, mechanismName, silent, null);
105     }
106     public BasicAuthenticationMechanism(final String realmName, final String mechanismName, final boolean silent, final IdentityManager identityManager) {
107         this(realmName, mechanismName, silent, identityManager, StandardCharsets.UTF_8, Collections.emptyMap());
108     }
109
110     public BasicAuthenticationMechanism(final String realmName, final String mechanismName, final boolean silent, final IdentityManager identityManager, Charset charset, Map<Pattern, Charset> userAgentCharsets) {
111         this.challenge = BASIC_PREFIX + "realm=\"" + realmName + "\"";
112         this.name = mechanismName;
113         this.silent = silent;
114         this.identityManager = identityManager;
115         this.charset = charset;
116         this.userAgentCharsets = Collections.unmodifiableMap(new LinkedHashMap<>(userAgentCharsets));
117     }
118
119     @SuppressWarnings("deprecation")
120     private IdentityManager getIdentityManager(SecurityContext securityContext) {
121         return identityManager != null ? identityManager : securityContext.getIdentityManager();
122     }
123
124     /**
125      * @see io.undertow.server.HttpHandler#handleRequest(io.undertow.server.HttpServerExchange)
126      */

127     @Override
128     public AuthenticationMechanismOutcome authenticate(HttpServerExchange exchange, SecurityContext securityContext) {
129
130         List<String> authHeaders = exchange.getRequestHeaders().get(AUTHORIZATION);
131         if (authHeaders != null) {
132             for (String current : authHeaders) {
133                 if (current.toLowerCase(Locale.ENGLISH).startsWith(LOWERCASE_BASIC_PREFIX)) {
134
135                     String base64Challenge = current.substring(PREFIX_LENGTH);
136                     String plainChallenge = null;
137                     try {
138                         ByteBuffer decode = FlexBase64.decode(base64Challenge);
139
140                         Charset charset = this.charset;
141                         if(!userAgentCharsets.isEmpty()) {
142                             String ua = exchange.getRequestHeaders().getFirst(Headers.USER_AGENT);
143                             if(ua != null) {
144                                 for (Map.Entry<Pattern, Charset> entry : userAgentCharsets.entrySet()) {
145                                     if(entry.getKey().matcher(ua).find()) {
146                                         charset = entry.getValue();
147                                         break;
148                                     }
149                                 }
150                             }
151                         }
152
153                         plainChallenge = new String(decode.array(), decode.arrayOffset(), decode.limit(), charset);
154                         UndertowLogger.SECURITY_LOGGER.debugf("Found basic auth header (decoded using charset %s) in %s", charset, exchange);
155                     } catch (IOException e) {
156                         UndertowLogger.SECURITY_LOGGER.debugf(e, "Failed to decode basic auth header in %s", exchange);
157                     }
158                     int colonPos;
159                     if (plainChallenge != null && (colonPos = plainChallenge.indexOf(COLON)) > -1) {
160                         String userName = plainChallenge.substring(0, colonPos);
161                         char[] password = plainChallenge.substring(colonPos + 1).toCharArray();
162
163                         IdentityManager idm = getIdentityManager(securityContext);
164                         PasswordCredential credential = new PasswordCredential(password);
165                         try {
166                             final AuthenticationMechanismOutcome result;
167                             Account account = idm.verify(userName, credential);
168                             if (account != null) {
169                                 securityContext.authenticationComplete(account, name, false);
170                                 result = AuthenticationMechanismOutcome.AUTHENTICATED;
171                             } else {
172                                 securityContext.authenticationFailed(MESSAGES.authenticationFailed(userName), name);
173                                 result = AuthenticationMechanismOutcome.NOT_AUTHENTICATED;
174                             }
175                             return result;
176                         } finally {
177                             clear(password);
178                         }
179                     }
180
181                     // By this point we had a header we should have been able to verify but for some reason
182                     // it was not correctly structured.
183                     return AuthenticationMechanismOutcome.NOT_AUTHENTICATED;
184                 }
185             }
186         }
187
188         // No suitable header has been found in this request,
189         return AuthenticationMechanismOutcome.NOT_ATTEMPTED;
190     }
191
192     @Override
193     public ChallengeResult sendChallenge(HttpServerExchange exchange, SecurityContext securityContext) {
194         if(silent) {
195             //if this is silent we only send a challenge if the request contained auth headers
196             //otherwise we assume another method will send the challenge
197             String authHeader = exchange.getRequestHeaders().getFirst(AUTHORIZATION);
198             if(authHeader == null) {
199                 return ChallengeResult.NOT_SENT;
200             }
201         }
202         exchange.getResponseHeaders().add(WWW_AUTHENTICATE, challenge);
203         UndertowLogger.SECURITY_LOGGER.debugf("Sending basic auth challenge %s for %s", challenge, exchange);
204         return new ChallengeResult(true, UNAUTHORIZED);
205     }
206
207     private static void clear(final char[] array) {
208         for (int i = 0; i < array.length; i++) {
209             array[i] = 0x00;
210         }
211     }
212
213     public static class Factory implements AuthenticationMechanismFactory {
214
215         @Deprecated
216         public Factory(IdentityManager identityManager) {}
217
218         public Factory() {}
219
220         @Override
221         public AuthenticationMechanism create(String mechanismName,IdentityManager identityManager, FormParserFactory formParserFactory, Map<String, String> properties) {
222             String realm = properties.get(REALM);
223             String silent = properties.get(SILENT);
224             String charsetString = properties.get(CHARSET);
225             Charset charset = charsetString == null ? StandardCharsets.UTF_8 : Charset.forName(charsetString);
226             Map<Pattern, Charset> userAgentCharsets = new HashMap<>();
227             String userAgentString = properties.get(USER_AGENT_CHARSETS);
228             if(userAgentString != null) {
229                 String[] parts = userAgentString.split(",");
230                 if(parts.length % 2 != 0) {
231                     throw UndertowMessages.MESSAGES.userAgentCharsetMustHaveEvenNumberOfItems(userAgentString);
232                 }
233                 for(int i = 0; i < parts.length; i += 2) {
234                     Pattern pattern = Pattern.compile(parts[i]);
235                     Charset c = Charset.forName(parts[i + 1]);
236                     userAgentCharsets.put(pattern, c);
237                 }
238             }
239
240             return new BasicAuthenticationMechanism(realm, mechanismName, silent != null && silent.equals("true"), identityManager, charset, userAgentCharsets);
241         }
242     }
243
244 }
245