package org.opentripplanner.standalone;
import com.google.common.collect.Maps;
import com.google.common.io.BaseEncoding;
import java.security.Principal;
import java.util.Map;
import javax.annotation.Priority;
import javax.ws.rs.Priorities;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerRequestFilter;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import javax.ws.rs.core.SecurityContext;
/**
* This ContainerRequestFilter adds basic authentication support to the Grizzly + Jersey server.
* Basic authentication is insecure by itself, but short of more complex solutions like oauth it is the best
* option when coupled with transport-layer security (secure sockets).
*
* It seems wasteful to encrypt all communication with the server, but TLS uses very efficient symmetric-key
* encryption on the messages themselves. Only the TLS handshake uses compute-intensive public key encryption,
* which establishes a shared key.
*
* In Jersey 2 this filter should configure the SecurityContext which will be passed through to the web resources.
*/
@Priority(Priorities.AUTHENTICATION)
// Authentication priority comes before Authorization, which is handled by RolesAllowedDynamicFeature
public class AuthFilter implements ContainerRequestFilter {
private final Map<String, String> passwords = Maps.newHashMap(); // roles are same as user names
/* Case-sensitive. */
public AuthFilter() {
passwords.put("ROUTERS", "ultra_secret");
}
/* Throw an exception if a user is unauthenticated. requestContext.abortWith()? */
private static void unauthenticated (String user) {
String message = String.format("Incorrect password for OpenTripPlanner user '%s'", user);
throw new WebApplicationException(Response.status(Status.UNAUTHORIZED)
.header(HttpHeaders.WWW_AUTHENTICATE, "Basic realm=\"OpenTripPlanner\"")
.entity(message)
.build());
}
/* Throw an exception if user attempts to do basic auth over an unencrypted connection. */
private static void unencrypted () {
throw new WebApplicationException(Response.status(Status.UNAUTHORIZED)
.header(HttpHeaders.WWW_AUTHENTICATE, "Basic realm=\"OpenTripPlanner\"")
.entity("OpenTripPlanner refuses to do basic auth without transport layer security (HTTPS).")
.build());
}
@Override
public void filter(ContainerRequestContext containerRequest) throws WebApplicationException {
// Get the authentication passed in HTTP headers parameters
String auth = containerRequest.getHeaderString(HttpHeaders.AUTHORIZATION);
if (auth != null) {
if (auth.startsWith("Basic ") || auth.startsWith("basic ")) {
if ( ! containerRequest.getSecurityContext().isSecure()) unencrypted();
auth = auth.replaceFirst("[Bb]asic ", "");
String[] split = new String(BaseEncoding.base64().decode(auth)).split(":", 2);
if (split.length != 2) return;
String user = split[0];
String pass = split[1];
if (pass.equals(passwords.get(user))) {
containerRequest.setSecurityContext(makeSecurityContext(user, user));
} else {
unauthenticated (user);
}
}
}
}
public static SecurityContext makeSecurityContext (final String name, final String... roles) {
return new SecurityContext() {
@Override
public Principal getUserPrincipal() {
return new Principal() {
@Override
public String getName() {
return name;
}
};
}
@Override
public boolean isUserInRole(String role) {
for (String r : roles) { // TODO make a real class with a Set
if (r.equals(role)) {
return true;
}
}
return false;
}
@Override
public String getAuthenticationScheme() { return SecurityContext.BASIC_AUTH; }
@Override
public boolean isSecure() {
// Is this happening over a secure channel like HTTPS?
// Yes, we already checked in the filter before creating a security context.
return true;
}
};
}
}