Enterprise Java Development@TOPIC@
HTTP BASIC authentification with JAX-RS
HTTPS private connections
JAX-RS Filters
Declarative Access Control
Add/modify deployment descriptors in WEB-INF
$ jar tf target/securePingJaxRsWAR-5.0.0-SNAPSHOT.war ... WEB-INF/beans.xml WEB-INF/web.xml WEB-INF/jboss-web.xml
# jboss-web.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE jboss-web>
<jboss-web xmlns="http://www.jboss.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.jboss.com/xml/ns/javaee http://www.jboss.org/j2ee/schema/jboss-web_12_0.xsd"
version="12.0">
<security-domain>other</security-domain>
</jboss-web>
default-security domain was assigned within Wildfly configuration - making this optional
BASIC - username and password passed in "Authentication" header Base64 encoded
FORM - credentials submitted as part of a form response
CLIENT-CERT - client public key authenticated as part of HTTPS connection
DIGEST - an encyrpted form of BASIC
EXTERNAL
# web.xml
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
version="4.0">
...
</web-app>
# web.xml
<login-config>
<auth-method>BASIC</auth-method>
<realm-name>ApplicationRealm</realm-name>
</login-config>
May need separate WARs for mixed solutions using BASIC (API) and FORM
Wildfly legacy security offers the following option
# web.xml
<!-- if mixing JAX-RS BASIC with HTML FORM
http://undertow.io/undertow-docs/undertow-docs-1.3.0/index.html#servlet-security
-->
<auth-method>BASIC?silent=true,FORM</auth-method>
API will silently accept BASIC Authorization header if supplied
API will not provide any response codes or headers making browser believe it accepts BASIC
Web UI will act as if it only uses FORM
@ApplicationPath("api")
public class SecurePingJaxRsApplication extends Application {
@Path("ping")
public class SecurePingResource {
//this injection requires CDI, which requires a WEB-INF/beans.xml file be in place to activate
@EJB(beanName="SecurePingEJB", beanInterface=SecurePingLocal.class)
private SecurePing secureService;
@Context
private SecurityContext ctx;
Accessible via "/api/ping"
Injected with EJB for business tier integration
Injected with SecurityContext for programmatic security checks
Return the authenticated userName of the caller from the EJB tier
@Path("whoAmI")
@GET
@Produces(MediaType.TEXT_PLAIN)
public Response whoAmI() {
ResponseBuilder rb = null;
try {
rb = Response.ok(secureService.whoAmI());
} catch (Exception ex) {
rb=makeExceptionResponse(ex);
}
return rb.build();
}
Accessible via GET "/api/ping/whoAmI"
Return the role query result for current user from EJB tier
@Path("roles/{role}")
@GET
@Produces(MediaType.TEXT_PLAIN)
public Response isCallerInRole(@PathParam("role") String role) {
ResponseBuilder rb = null;
try {
rb = Response.ok(secureService.isCallerInRole(role));
} catch (Exception ex) {
rb=makeExceptionResponse(ex);
}
return rb.build();
}
Accessible via GET "/api/ping/roles/{role}"
Form the Authorization value for the header
# client
String credentials = userUser + ":" + userPassword;
String authn = "Basic " + Base64.getEncoder().encodeToString(credentials.getBytes());
Results in the following value
Basic dXNlcjE6cGFzc3dvcmQxIQ==
Add Authorizaton header to JAX-RS request
# client
Response response = target.request(MediaType.TEXT_PLAIN)
.header("Authorization", authn)
.get();
Results in the following header being addedi to request
Authorization: Basic dXNlcjE6cGFzc3dvcmQxIQ==
Above unwrapped solution works - but mixes business communication details with security details
Better to use a filter - promotes separation of concerns
import java.io.IOException;
import java.util.Base64;
import javax.ws.rs.client.ClientRequestContext;
import javax.ws.rs.client.ClientRequestFilter;
import javax.ws.rs.core.MultivaluedMap;
public class BasicAuthnFilter implements ClientRequestFilter {
private final String authn;
public BasicAuthnFilter(String username, String password) {
String credentials = username + ":" + password;
authn = "Basic " + Base64.getEncoder().encodeToString(credentials.getBytes());
}
@Override
public void filter(ClientRequestContext requestContext) throws IOException {
MultivaluedMap<String, Object> headers = requestContext.getHeaders();
headers.add("Authorization", authn);
}
}
Instantiate the filter
# client
ClientRequestFilter authnFilter = new BasicAuthnFilter(userUser, userPassword);
Register filter with JAX-RS Client
# client
Client jaxRsClient = ClientBuilder.newClient();
jaxRsClient.register(authnFilter);
Leave remaining calls untouched
# client
URI whoAmIUri = UriBuilder.fromUri(baseHttpUrl).path("whoAmI").build();
WebTarget target = jaxRsClient.target(whoAmIUri);
logger.debug("GET {}", target.getUri());
Response response = target.request(MediaType.TEXT_PLAIN).get();
If switching identities, you will need multiple JAX-RS Client instances. Best performance to cache Client instances rather than re-construct each and their filter chain each time.
private Map<String, Client> jaxRsClients = new HashMap<>();
Problem - HTTP will send credential in the clear
Base64 is an encoding - not an encyption
NONE - (default) no confidentiality required (HTTP)
CONFIDENTIAL - confidentiality required (HTTPS)
# web.xml
<security-constraint>
<web-resource-collection>
<web-resource-name>methods</web-resource-name>
<url-pattern>/api/ping/whoAmI</url-pattern>
<url-pattern>/api/ping/canAccess</url-pattern>
</web-resource-collection>
<user-data-constraint>
<transport-guarantee>CONFIDENTIAL</transport-guarantee>
</user-data-constraint>
</security-constraint>
Specifying CONFIDENTIAL will disable HTTP for url-pattern and cause re-direct to HTTPS
Inspect response headers using Filter
ClientResponseFilter provides convenient access to request and response for debug
# client
public class LoggingFilter implements ClientResponseFilter {
private final Logger logger;
public LoggingFilter(Logger logger) {
this.logger = logger;
}
@Override
public void filter(ClientRequestContext requestContext,
ClientResponseContext responseContext) {
String method = requestContext.getMethod();
String uri = requestContext.getUri().toString();
StatusType status = responseContext.getStatusInfo();
logger.debug("{} {}, returned {}/{}\nhdrs sent: {}\nhdrs rcvd: {}",
method, uri, status.getStatusCode(), status,
requestContext.getStringHeaders(),
responseContext.getHeaders());
}
}
Register Filter with Client
# client
ClientResponseFilter loggingFilter = new LoggingFilter(logger);
jaxRsClient.register(loggingFilter);
HTTP Headers - Redirect
# client -GET http://localhost:8080/securePingApi/api/ping/whoAmI, returned 302/Found hdrs sent: [Accept=text/plain, Authorization=Basic dXNlcjE6cGFzc3dvcmQxIQ==] hdrs rcvd: [Connection=keep-alive, Content-Length=0, Date=Tue, 13 Nov 2018 14:03:28 GMT, Location=https://localhost:8443/securePingApi/api/ping/whoAmI]
302/Found response re-directs to Location
Location contains full HTTPS URL
Use HTTPS URL
Change URL to use HTTPS versus HTTP
This is same URL returned in 302/FOUND re-direct
# client
if (response.getStatus()==302) {
String redirectTo = response.getHeaderString("Location");
target = jaxRsClient.target(redirectTo);
logger.debug("GET {}", target.getUri());
response = target.request(MediaType.TEXT_PLAIN).get();
}
-GET https://localhost:8443/securePingApi/api/ping/whoAmI, returned 200/OK hdrs sent: [Accept=text/plain, Authorization=Basic dXNlcjE6cGFzc3dvcmQxIQ==] hdrs rcvd: [Connection=keep-alive, Content-Length=5, Content-Type=text/plain;charset=UTF-8, Date=Tue, 13 Nov 2018 14:43:27 GMT]
Java clients require server or server's certificate authority (CA) in trustStore
HTTPS will fail without Wildfly certificate in client trustStore
Define a path to Java Keystore with Server Public Key
# IT client pom.xml
<properties>
<java.truststore>${jboss.home}/standalone/configuration/application.keystore</java.truststore>
Example uses server's identity keyStore for simplicity
keyStore contains server's public key (and private key)
no password required to access keyStore public keys
Pass trustStore System Property using failsafe configuration
# IT client pom.xml
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<version>${maven-failsafe-plugin.version}</version>
<configuration>
<systemPropertyVariables>
<javax.net.ssl.trustStore>${java.truststore}</javax.net.ssl.trustStore>
</systemPropertyVariables>
</configuration>
...
results in process launched with -Djavax.net.ssl.trustStore=.../application.keystore
Java requires the common name (CN) of certificate from trustStore to match the hostname returned from server
Development scenarios can cause some mis-matches
Certificate generated for localhost used externally (expected localhost or 127.0.0.1)
Certificate generated for external used internally (expected xyz.com)
Can resolve thru Hostname Verifier Override
# IT client
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLSession;
# IT client
private static class MyHostnameVerifier implements HostnameVerifier {
@Override
public boolean verify(String hostname, SSLSession session) {
return true; //or whatever you wish to check for
}
}
Register verifier with ClientBuilder used to create JAX-RS Client
# IT client
ClientBuilder clientBuilder = ClientBuilder.newBuilder()
.hostnameVerifier(new MyHostnameVerifier());
Client jaxRsClient = clientBuilder.build();
Results in connection being established if certificate recognized - even if hostname mismatch
@Path("secured")
public Pinger authenticated() {
return new Pinger();
}
@Path("unsecured")
public Pinger anonymous() {
return new Pinger();
}
public class Pinger {
@Path("pingUser")
@GET
@Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML})
public Response pingUser() {
ResponseBuilder rb = null;
try {
PingResult result = makeResourcePayload(secureService!=null ?
secureService.pingUser() : "no ejb injected!!!");
rb = secureService!=null ?
Response.ok(result) :
Response.serverError().entity(result);
} catch (EJBAccessException ex) {
PingResult entity = makeResourcePayload(ex.toString());
rb = Response.status(Status.FORBIDDEN).entity(entity);
} catch (Exception ex) {
rb=makeExceptionResponse(ex);
}
return rb.build();
}
Define role restrictions for url-patterns
# web.xml
<security-constraint>
<web-resource-collection>
<web-resource-name>secured</web-resource-name>
<url-pattern>/api/ping/secured/pingUser</url-pattern>
</web-resource-collection>
<auth-constraint>
<role-name>user</role-name>
</auth-constraint>
</security-constraint>
/api/ping/secured/pingUser constrained to callers with "user" role
CONFIDENTIAL transport-guarantee left off for example simplicity
Response
200/OK - if credentials supplied, successfully authenticated, and authorized
403/FORBIDDEN - if credentials supplied, successfully authenticated, but not authorized
401/UNAUTHORIZED - if not successfully authenticated
No web access control, delegating to EJB - Resource method called for all callers
GET https://localhost:8443/securePingApi/api/ping/pingUser
Web API access control - Resource method only invoked for authorized callers
GET https://localhost:8443/securePingApi/api/ping/secured/pingUser