
io.datakernel.http.BasicAuth Maven / Gradle / Ivy
package io.datakernel.http;
import io.datakernel.async.Promise;
import io.datakernel.exception.UncheckedException;
import org.jetbrains.annotations.NotNull;
import java.util.Base64;
import java.util.Map;
import java.util.function.BiFunction;
import java.util.function.BiPredicate;
import java.util.function.Function;
import static io.datakernel.http.ContentTypes.PLAIN_TEXT_UTF_8;
import static io.datakernel.http.HttpHeaders.AUTHORIZATION;
import static io.datakernel.http.HttpHeaders.CONTENT_TYPE;
import static java.nio.charset.StandardCharsets.UTF_8;
/**
* This is a simple reference implementation of the HTTP Basic Auth protocol.
*
* It operates over some servlet that it restricts access to and the async predicate for the credentials.
*
* Also the credentials are {@link HttpRequest#attach attached} to the request so that the private servlet
* could then receive and use it.
*/
public final class BasicAuth implements AsyncServlet {
public static final BiPredicate SILLY = (login, pass) -> true;
private static final String PREFIX = "Basic ";
private static final Base64.Decoder DECODER = Base64.getDecoder();
private final AsyncServlet next;
private final String challenge;
private final BiFunction> credentialsLookup;
private Function failureResponse =
request -> request
.withHeader(CONTENT_TYPE, HttpHeaderValue.ofContentType(PLAIN_TEXT_UTF_8))
.withBody("Authentification is required".getBytes(UTF_8));
public BasicAuth(AsyncServlet next, String realm, BiFunction> credentialsLookup) {
this.next = next;
this.credentialsLookup = credentialsLookup;
challenge = PREFIX + "realm=\"" + realm + "\", charset=\"UTF-8\"";
}
public BasicAuth withFailureResponse(Function failureResponse) {
this.failureResponse = failureResponse;
return this;
}
public static AsyncServletDecorator decorator(String realm, BiFunction> credentialsLookup) {
return next -> new BasicAuth(next, realm, credentialsLookup);
}
public static Function decorator(String realm,
BiFunction> credentialsLookup,
Function failureResponse) {
return next -> new BasicAuth(next, realm, credentialsLookup)
.withFailureResponse(failureResponse);
}
@NotNull
@Override
public Promise serve(@NotNull HttpRequest request) throws UncheckedException {
String header = request.getHeader(AUTHORIZATION);
if (header == null || !header.startsWith(PREFIX)) {
return Promise.of(failureResponse.apply(HttpResponse.unauthorized401(challenge)));
}
byte[] raw;
try {
raw = DECODER.decode(header.substring(PREFIX.length()));
} catch (IllegalArgumentException e) {
// all the messages in decode method's illegal argument exception are informative enough
return Promise.ofException(HttpException.ofCode(400,"Base64: " + e.getMessage()));
}
String[] authData = new String(raw, UTF_8).split(":", 2);
if (authData.length != 2) {
return Promise.ofException(HttpException.ofCode(400, "No ':' separator"));
}
return credentialsLookup.apply(authData[0], authData[1])
.then(result -> {
if (result) {
request.attach(new BasicAuthCredentials(authData[0], authData[1]));
return next.serve(request);
}
return Promise.of(failureResponse.apply(HttpResponse.unauthorized401(challenge)));
});
}
public static final class BasicAuthCredentials {
private String username;
private String password;
public BasicAuthCredentials(String username, String password) {
this.username = username;
this.password = password;
}
public String getUsername() {
return username;
}
public String getPassword() {
return password;
}
}
public static BiPredicate lookupFrom(Map credentials) {
return (login, pass) -> pass.equals(credentials.get(login));
}
}