package io.undertow.security.impl;
import static io.undertow.UndertowMessages.MESSAGES;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.regex.Pattern;
import io.undertow.UndertowLogger;
import io.undertow.UndertowMessages;
import io.undertow.security.api.AuthenticationMechanism;
import io.undertow.security.api.AuthenticationMechanismFactory;
import io.undertow.security.api.SecurityContext;
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.server.handlers.form.FormParserFactory;
import io.undertow.util.FlexBase64;
import io.undertow.util.Headers;
import static io.undertow.util.Headers.AUTHORIZATION;
import static io.undertow.util.Headers.BASIC;
import static io.undertow.util.Headers.WWW_AUTHENTICATE;
import static io.undertow.util.StatusCodes.UNAUTHORIZED;
public class BasicAuthenticationMechanism implements AuthenticationMechanism {
public static final AuthenticationMechanismFactory FACTORY = new Factory();
public static final String SILENT = "silent";
public static final String CHARSET = "charset";
public static final String USER_AGENT_CHARSETS = "user-agent-charsets";
private final String name;
private final String challenge;
private static final String BASIC_PREFIX = BASIC + " ";
private static final String LOWERCASE_BASIC_PREFIX = BASIC_PREFIX.toLowerCase(Locale.ENGLISH);
private static final int PREFIX_LENGTH = BASIC_PREFIX.length();
private static final String COLON = ":";
private final boolean silent;
private final IdentityManager identityManager;
private final Charset charset;
private final Map<Pattern, Charset> userAgentCharsets;
public BasicAuthenticationMechanism(final String realmName) {
this(realmName, "BASIC");
}
public BasicAuthenticationMechanism(final String realmName, final String mechanismName) {
this(realmName, mechanismName, false);
}
public BasicAuthenticationMechanism(final String realmName, final String mechanismName, final boolean silent) {
this(realmName, mechanismName, silent, null);
}
public BasicAuthenticationMechanism(final String realmName, final String mechanismName, final boolean silent, final IdentityManager identityManager) {
this(realmName, mechanismName, silent, identityManager, StandardCharsets.UTF_8, Collections.emptyMap());
}
public BasicAuthenticationMechanism(final String realmName, final String mechanismName, final boolean silent, final IdentityManager identityManager, Charset charset, Map<Pattern, Charset> userAgentCharsets) {
this.challenge = BASIC_PREFIX + "realm=\"" + realmName + "\"";
this.name = mechanismName;
this.silent = silent;
this.identityManager = identityManager;
this.charset = charset;
this.userAgentCharsets = Collections.unmodifiableMap(new LinkedHashMap<>(userAgentCharsets));
}
@SuppressWarnings("deprecation")
private IdentityManager getIdentityManager(SecurityContext securityContext) {
return identityManager != null ? identityManager : securityContext.getIdentityManager();
}
@Override
public AuthenticationMechanismOutcome authenticate(HttpServerExchange exchange, SecurityContext securityContext) {
List<String> authHeaders = exchange.getRequestHeaders().get(AUTHORIZATION);
if (authHeaders != null) {
for (String current : authHeaders) {
if (current.toLowerCase(Locale.ENGLISH).startsWith(LOWERCASE_BASIC_PREFIX)) {
String base64Challenge = current.substring(PREFIX_LENGTH);
String plainChallenge = null;
try {
ByteBuffer decode = FlexBase64.decode(base64Challenge);
Charset charset = this.charset;
if(!userAgentCharsets.isEmpty()) {
String ua = exchange.getRequestHeaders().getFirst(Headers.USER_AGENT);
if(ua != null) {
for (Map.Entry<Pattern, Charset> entry : userAgentCharsets.entrySet()) {
if(entry.getKey().matcher(ua).find()) {
charset = entry.getValue();
break;
}
}
}
}
plainChallenge = new String(decode.array(), decode.arrayOffset(), decode.limit(), charset);
UndertowLogger.SECURITY_LOGGER.debugf("Found basic auth header (decoded using charset %s) in %s", charset, exchange);
} catch (IOException e) {
UndertowLogger.SECURITY_LOGGER.debugf(e, "Failed to decode basic auth header in %s", exchange);
}
int colonPos;
if (plainChallenge != null && (colonPos = plainChallenge.indexOf(COLON)) > -1) {
String userName = plainChallenge.substring(0, colonPos);
char[] password = plainChallenge.substring(colonPos + 1).toCharArray();
IdentityManager idm = getIdentityManager(securityContext);
PasswordCredential credential = new PasswordCredential(password);
try {
final AuthenticationMechanismOutcome result;
Account account = idm.verify(userName, credential);
if (account != null) {
securityContext.authenticationComplete(account, name, false);
result = AuthenticationMechanismOutcome.AUTHENTICATED;
} else {
securityContext.authenticationFailed(MESSAGES.authenticationFailed(userName), name);
result = AuthenticationMechanismOutcome.NOT_AUTHENTICATED;
}
return result;
} finally {
clear(password);
}
}
return AuthenticationMechanismOutcome.NOT_AUTHENTICATED;
}
}
}
return AuthenticationMechanismOutcome.NOT_ATTEMPTED;
}
@Override
public ChallengeResult sendChallenge(HttpServerExchange exchange, SecurityContext securityContext) {
if(silent) {
String authHeader = exchange.getRequestHeaders().getFirst(AUTHORIZATION);
if(authHeader == null) {
return ChallengeResult.NOT_SENT;
}
}
exchange.getResponseHeaders().add(WWW_AUTHENTICATE, challenge);
UndertowLogger.SECURITY_LOGGER.debugf("Sending basic auth challenge %s for %s", challenge, exchange);
return new ChallengeResult(true, UNAUTHORIZED);
}
private static void clear(final char[] array) {
for (int i = 0; i < array.length; i++) {
array[i] = 0x00;
}
}
public static class Factory implements AuthenticationMechanismFactory {
@Deprecated
public Factory(IdentityManager identityManager) {}
public Factory() {}
@Override
public AuthenticationMechanism create(String mechanismName,IdentityManager identityManager, FormParserFactory formParserFactory, Map<String, String> properties) {
String realm = properties.get(REALM);
String silent = properties.get(SILENT);
String charsetString = properties.get(CHARSET);
Charset charset = charsetString == null ? StandardCharsets.UTF_8 : Charset.forName(charsetString);
Map<Pattern, Charset> userAgentCharsets = new HashMap<>();
String userAgentString = properties.get(USER_AGENT_CHARSETS);
if(userAgentString != null) {
String[] parts = userAgentString.split(",");
if(parts.length % 2 != 0) {
throw UndertowMessages.MESSAGES.userAgentCharsetMustHaveEvenNumberOfItems(userAgentString);
}
for(int i = 0; i < parts.length; i += 2) {
Pattern pattern = Pattern.compile(parts[i]);
Charset c = Charset.forName(parts[i + 1]);
userAgentCharsets.put(pattern, c);
}
}
return new BasicAuthenticationMechanism(realm, mechanismName, silent != null && silent.equals("true"), identityManager, charset, userAgentCharsets);
}
}
}