Notice: This Wiki is now read only and edits are no longer possible. Please see: https://gitlab.eclipse.org/eclipsefdn/helpdesk/-/wikis/Wiki-shutdown-plan for the plan.
Tutorial: Extending the JaxRS Remote Services Provider
Contents
Introduction
The JaxRS 2.0 specification supports the extending JaxRS services interfaces and classes defined in several of the API package.
These interfaces allow the addition of server-and-or-client-side behavior before, during, and after remote calls. For example, the MessageBodyReader and the MessageBodyWriter may be implemented so that the reading and writing of the remote method arguments and return values may be customized.
Note that these extensions are defined by the JaxRS 2.0 specification and as such should be available on all implementations of the spec.
Jax-RS Extensions
The JaxRS specification supports the registration of extensions in two ways:
- Source Code Annotations
- Runtime registration via the Configurable API
ECF's JaxRS Remote Service Provider allows an additional way JaxRS remote services: via service components aka declarative services.
Service Components for Basic Auth Support
The ContainerRequestFilter and the ClientRequestFilter can be used together to add a username/password authentication to JaxRS requests. As described above, typically implementations of these interfaces will be registered via source code annotations (i.e. the Provider annotation) or by via the Configurable.register API at runtime.
For Basic Auth support, what we want is that every request from client to server will include an encoded username and password, communicated via standard Authentication request header. Once received on the server, the header should be decoded, compared agains acceptable values for username and password for access to making the request, and either continuing to make the request (if authenticated), or refusing to make the request (if authentication fails).
ClientRequestFilter
To implement this with JaxRS extensions, on the client we need to intercept the request, get the username and password to include, encrypt and put into the Authorization header. Here is an example bundle project with a single class: BasicAuthClientRequestFilter that implements the JaxRS ClientRequestFilter extension. Here's that implementation:
package org.eclipse.ecf.example.jersey.client.basicauth; import java.io.IOException; import java.util.Base64; import javax.ws.rs.client.ClientRequestContext; import javax.ws.rs.client.ClientRequestFilter; import org.osgi.service.component.annotations.Component; @Component(immediate=true,property = {"jaxrs-service-exported-config-target=ecf.jaxrs.jersey.client" }) public class BasicAuthClientRequestFilter implements ClientRequestFilter { private static final String AUTHORIZATION_PROPERTY = "Authorization"; private static final String AUTHENTICATION_SCHEME = "Basic "; private static final String testUsername = System.getProperty("rs.basicauth.username", "testusername"); private static final String testPassword = System.getProperty("rs.basicauth.password", "testpassword"); @Override public void filter(ClientRequestContext clientRequestContext) throws IOException { System.out.println("ContainerRequestFilter.filter for uris="+clientRequestContext.getUri()); clientRequestContext.getHeaders().add(AUTHORIZATION_PROPERTY, AUTHENTICATION_SCHEME + Base64.getEncoder().encodeToString(new String(testUsername + ":" + testPassword).getBytes())); } }
Note that what happens when the filter is called is that the testUsername and testPassword are separated by a ':', encoded with the Base64 encoder, and added as the value of the Authorization request header.
Note also that this request filter is defined as a Service Component with this OSGi annotation:
@Component(immediate=true,property = {"jaxrs-service-exported-config-target=ecf.jaxrs.jersey.client" })
Note that this component property jaxrs-service-exported-config-target is set to the configuration id of the jaxrs distribution provider, i.e. ecf.jaxrs.jersey.client. This property/value combination results in a singleton instance of this BasicAuthClientRequestFilter being assign to the ecf.jaxrs.jersey.client distribution provider. This property must be set to the appropriate id of the distribution provider that will be used for the client. This will typically be ecf.jaxrs.jersey.client, but could be some other value, if another JaxRS distribution provider (e.g. CXF) is used.
ContainerRequestFilter
On the JaxRS server-side, the specification provides the ContainerRequestFilter extension. Here is a project with the BasicAuthContainerRequestFilter class implementing the ContainerRequestFilter interface. Here is that code:
package org.eclipse.ecf.example.jersey.server.basicauth; import java.io.IOException; import java.util.Base64; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.StringTokenizer; import javax.ws.rs.container.ContainerRequestContext; import javax.ws.rs.container.ContainerRequestFilter; import javax.ws.rs.core.Response; import org.osgi.service.component.annotations.Component; @Component(immediate = true, property = { "jaxrs-service-exported-config-target=ecf.jaxrs.jersey.server", "jaxrs-component-intents=ecf.jaxrs.basicauth" }) public class BasicAuthContainerRequestFilter implements ContainerRequestFilter { private static final String AUTHORIZATION_PROPERTY = "Authorization"; private static final String AUTHENTICATION_SCHEME = "Basic"; // 'fake' password database private static final Map<String, String> usernamePasswordDb = new HashMap<String, String>(); static { usernamePasswordDb.put(System.getProperty("rs.basicauth.username", "testusername"), System.getProperty("rs.basicauth.password", "testpassword")); } class BasicAuthCredentials { private final String username; private final String password; public BasicAuthCredentials(ContainerRequestContext containerRequestContext) { List<String> authHeaders = containerRequestContext.getHeaders().get(AUTHORIZATION_PROPERTY); if (authHeaders == null) { throw new IllegalArgumentException("Request does not have Authorization header"); } String authHeaderValue = authHeaders.get(0); if (authHeaderValue == null) { throw new IllegalArgumentException("Request does not have authorization header value"); } final StringTokenizer tokenizer = new StringTokenizer(new String(Base64.getDecoder() .decode(authHeaderValue.replaceFirst(AUTHENTICATION_SCHEME + " ", "").getBytes())), ":"); this.username = tokenizer.nextToken(); this.password = tokenizer.nextToken(); } public boolean authenticate() { String password = usernamePasswordDb.get(this.username); if (password == null || this.password == null || !this.password.equals(password)) { return false; } return true; } } @Override public void filter(ContainerRequestContext containerRequestContext) throws IOException { try { // XXX as this is example, it prints to system out that we are here, so that // can verify this is being called at request time System.out.println( "ContainerRequestFilter.filter for uri=" + containerRequestContext.getUriInfo().getRequestUri()); BasicAuthCredentials authCreds = new BasicAuthCredentials(containerRequestContext); if (authCreds.authenticate()) { return; } else { throw new IOException("Incorrect or invalid username or password"); } } catch (IOException e) { throw e; } catch (Exception e) { // log error e.printStackTrace(System.err); containerRequestContext .abortWith(Response.status(Response.Status.UNAUTHORIZED).entity("Authentication error").build()); } } }
Note that in the entry-point filter method, the BasicAuthCredentials constructor reads the value of the Authorization header, decodes it, and sets the BasicAuthCredentials.username and password from the request header value. If the proper information is not present or not well-formed an exception is thrown.
Then the authCreds.authenticate() method is called to authenticate the given username/password (e.g. against username/password db) and if succeeds then the request is allowed to continue (exiting the filter method with no exception). If an exception is thrown and caught the request is aborted.
Note also the Component annotation:
@Component(immediate = true, property = { "jaxrs-service-exported-config-target=ecf.jaxrs.jersey.server", "jaxrs-component-intents=ecf.jaxrs.basicauth" })
the jaxrs-service-exported-config-target is set to the server config type id ecf.jaxrs.jersey.server.
Note also that another property is set:
jaxrs-component-intents=ecf.jaxrs.basicauth
This allows the extension to set intents on the remote service distribution provider, so that those intents will be included for the distribution provider. For example, here is the endpoint description produced by exporting a remote service via the ecf.jaxrs.jersey.server provider with the BasicAuthContainerRequestFilter extension:
<endpoint-descriptions xmlns="http://www.osgi.org/xmlns/rsa/v1.0.0"> <endpoint-description> <property name="ecf.endpoint.id" value-type="String" value="http://localhost:8080/rservices/rs2"/> <property name="ecf.endpoint.id.ns" value-type="String" value="ecf.namespace.jaxrs"/> <property name="ecf.endpoint.ts" value-type="Long" value="1610931247578"/> <property name="ecf.jaxrs.server.pathPrefix" value-type="String" value="/rs2"/> <property name="ecf.rsvc.id" value-type="Long" value="1"/> <property name="endpoint.framework.uuid" value-type="String" value="89e5b6d0-785d-4579-8d79-47d2411f65cc"/> <property name="endpoint.id" value-type="String" value="25ec3a8f-1e70-4f32-8683-0327b80654b7"/> <property name="endpoint.package.version.com.mycorp.examples.student" value-type="String" value="1.0.0"/> <property name="endpoint.service.id" value-type="Long" value="51"/> <property name="objectClass" value-type="String"> <array> <value>com.mycorp.examples.student.StudentService</value> </array> </property> <property name="osgi.basic.timeout" value-type="String" value="50000"/> <property name="remote.configs.supported" value-type="String"> <array> <value>ecf.jaxrs.jersey.server</value> </array> </property> <property name="remote.intents.supported" value-type="String"> <array> <value>passByValue</value> <value>exactlyOnce</value> <value>ordered</value> <value>osgi.async</value> <value>osgi.private</value> <value>osgi.confidential</value> <value>jaxrs</value> <value>ecf.jaxrs.basicauth</value> </array> </property> <property name="service.imported" value-type="String" value="true"/> <property name="service.imported.configs" value-type="String"> <array> <value>ecf.jaxrs.jersey.server</value> </array> </property> <property name="service.intents" value-type="String"> <array> <value>osgi.async</value> <value>jaxrs</value> </array> </property> </endpoint-description> </endpoint-descriptions>
Note the presence of the ecf.jaxrs.basicauth intent under the remote.intents.supported property.