1
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
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
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
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
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
182
183 return AuthenticationMechanismOutcome.NOT_AUTHENTICATED;
184 }
185 }
186 }
187
188
189 return AuthenticationMechanismOutcome.NOT_ATTEMPTED;
190 }
191
192 @Override
193 public ChallengeResult sendChallenge(HttpServerExchange exchange, SecurityContext securityContext) {
194 if(silent) {
195
196
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