JAX-RS Core Part III

Moving ahead, we'll look at how the JAX-RS Client API works along with the it's Security related features

  • More Injection: what else can JAX-RS inject for free ?

  • Client API: a standards based HTTP Client

  • Security: authentication, authorisation, token based security etc.

Injection part II

In the previous chapter, we saw how we used specific annotations to inject HTTP URI parameters, headers, cookies etc. We'll take this one step further and see what else JAX-RS has in store for us in terms of useful injectables

JAX-RS provides the @Context annotation is used as a general purpose injection to inject a variety of resources in your RESTful services. Some of the most commonly injected components are HTTP headers, HTTP URI related information. Here is a complete list (in no specific order)

HTTP headers

Although HTTP headers can be injected using the @HeaderParam annotation, JAX-RS also provides the facility of injecting an instance of the HttpHeaders interface (as an instance variable or method parameter). This is useful when you want to iterate over all possible headers rather than injecting a specific header value by name

//alternate way to work with HTTP headers

@Path("testinject")
public class InjectURIDetails{
    //localhost:8080/<root-context>/testinject/httpheaders
    @GET
    @Path("httpheaders")
    public void test(@Context HttpHeaders headers){
        log(headers.getRequestHeaders().toString());
        log(headers.getHeaderString("Accept"));
        log(headers.getCookies().get("TestCookie").getValue());
    }
}

HTTP URI details

UriInfo is another interface whose instance can be injected by JAX-RS (as an instance variable or method parameter). Use this instance to fetch additional details related to the request URI and its parameters (query, path)

//plucking out the URI information

@Path("testinject")
public class InjectURIDetails{
  //localhost:8080/<root-context>/testinject/uriinfo
  @GET
  @Path("uriinfo")
  public void test(@Context UriInfo uriDetails){
     log("ALL query parameters -- "+ uriDetails.getQueryParameters().toString());
     log("'id' query parameter -- "+ uriDetails.getQueryParameters.get("id"));
     log("Complete URI -- "+ uriDetails.getRequestUri());
  }
}

Resource Context

It can be injected to help with creation and initialization, or just initialization, of instances created by an application.

//plucking out the ResourceContext

@Path("testinject")
public class InjectRctx{

  @Context
  ResourceContext rctx;

  //localhost:8080/<root-context>/testinject/rctx
  @GET
  @Path("rctx")
  public MyResource test(){
        //sub resource locator logic
     rctx.initResource(new MyResource());
  }
}

Request

//Inject Request

@Path("testinject")
public class InjectRequestObj{

  //localhost:8080/<root-context>/testinject/req
  @GET
  @Path("req")
  public Response test(@Context Request req){
        Date lastUpdated = …;
     req.evaluatePreConditions(lastUpdated);
     //continue further . . .
  }
}

More on practical usage of the Request instance in the chapter JAX-RS for Power Users: Part II

Configuration

Used to inject the configuration data associated with a Configurable component (e.g. Resource, Client etc.)

//Retrieving Configuration data

@Path("testinject")
public class InjectConfigDetails{

  @Context
  Configuration config;

  //localhost:8080/<root-context>/testinject/config
  @GET
  @Path("config")
  public void test(){
     log(config.isEnabled(MyFeature.class));
  }
}

Application

JAX-RS allows injection of Application subclasses as well

//Injecting the Application subclass

@Path("testinject")
public class InjectApplicationImpl{

  @Context
  Application theApp;

  //localhost:8080/<root-context>/testinject/app
  @GET
  @Path("app")
  public void test(){
     log(theApp.getClasses());
  }
}

Providers

An instance of the Providers interface can be injected using @Context. One needs to be aware of the fact that this is only valid within an existing provider. A Providers instance enables the current Provider to search for other registered providers in the current JAX-RS container

Please do not get confused between @Provider (the annotation) and Providers (the interface). More on this in the chapter `JAX-RS Providers Part III

Security Context

Inject an instance of the javax.ws.rs.core.SecurityContext interface (as an instance variable or method parameter) if you want to gain more insight into identity of the entity invoking your RESTful service.

Much more on this is the last section of this chapter

Client API

Prior to the addition of a full-fledged Client API, developers had to resort to third party implementations or interact with the HTTPUrlConnection API in the JDK to interact with HTTP oriented (REST) services. The Client API (part of javax.ws.rs.client package) is fairly compact, lean and fluent. It's classes and interfaces have been discussed below, followed by some code examples

A ClientBuilder allows you to initiate the invocation process by providing an entry point via its overloaded newClient methods and the build method. An instance of Client helps create WebTarget instance with the help of overloaded target methods. WebTarget is a representation of the URI endpoint for HTTP request invocation. It helps configure various attributes such as query, matrix and path parameters and exposes overloaded request methods to obtain an instance of Invocation.Builder. Invocation.Builder is responsible for further building the HTTP request and configuring attributes such as headers, cookies, cache control along with content negotiation parameters like media types, language and encoding. Finally, it helps obtain an instance of the Invocation object by using one of its build methods. An instance of Invocation encapsulates a HTTP request and allows synchronous and asynchronous request submission via the overloaded versions of the invoke and submit method respectively.

//Basic example

Client client = ClientBuilder.newClient();
WebTarget webTarget = 
client.target("https://api.github.com");
Invocation.Builder builder = webTarget
.path("search").path("repositories")
.queryParam("q", "JAX-RS")
.request("application/json");
Invocation invocation = builder.buildGet();
Response respone = invocation.invoke();

Let's dissect the code snippet to gain a better understanding of what's going on

  • An instance of Client is obtained via ClientBuilder class

  • The Client instance is used to specify the target URI as well (in this example it is https://api.github.com)

  • An instance of a WebTarget is created as a result and it is further used to specify the expected response/media type (equivalent to an Accept HTTP header) and associated URI (path & query) parameters. - This creates an instance of Invocation.Builder which further builds a complete HTTP GET request-

  • The Invocation instance is used to deliver the request to the server

The Configurable interface

The Client, ClientBuilder, WebTarget and Invocation objects implement the javax.ws.rs.core.Configurable interface. This allows them to define custom JAX-RS components such as filters, interceptors, entity providers (message readers and writers) etc. This is made possible using the overloaded versions of the register method [more on this later]

This is applicable to server side JAX-RS components (filters, interceptors etc) as well

Security

This section explores security aspects of the JAX-RS API

  • Supported security features (default)

  • Configuration style: Declarative & programmatic security

  • Implementing Stateless security

JAX-RS specification does not define dedicated security related features except for a few API constructs (which act as high level abstractions). For server side JAX-RS users (Java EE) it's critical to understand that the JAX-RS framework leverages the security capabilities of the container itself. To be specific, since JAX-RS is built on top the Servlet API, it has access to all the security features defined by the specification

  • If you're using JAX-RS, you do not need to reinvent the wheel for securing your application

  • You're free to use both declarative and programmatic security (or combination of both)

  • It is flexible enough to accommodate usage of custom security frameworks if needed

Authentication & authorisation are familiar terms, so let's go over them briefly and then delve into how they can be enforced

  • Authentication: It is the act of identification. In the context of JAX-RS, authentication involves ensuring that the caller is really who he/she/it claims to be

  • Authorization: It is a process via which the privileges of an authenticated entity are determined. In a JAX-RS application, this would help answer

Another vital security measure, Transport Layer Security can be enforced using HTTPS

Declarative

Declarative security can be configured by using

  • deployment descriptor (web.xml) or

  • annotations (for role based authorization)

web.xml

web.xml is the standard deployment descriptor used by the Servlet specification. It's contained within a WAR (inside the WEB-INF folder). Amongst other parameters, it contains elements which help configuring authentication as well as role based authorization.

Let's look at a simple example

//Declarative JAX-RS security

<web-app>
    <security-constraint>
        <web-resource-collection>
            <web-resource-name>New Book Creation</web-resource-name>
            <url-pattern>/rest/books</url-pattern>
            <http-method>POST</http-method>
        </web-resource-collection>
        <auth-constraint>
            <role-name>admin</role-name>
        </auth-constraint>
        <user-data-constraint>
            <transport-guarantee>CONFIDENTIAL</transport-guarantee>
        </user-data-constraint>
    </security-constraint>
    <security-constraint>
        <web-resource-collection>
            <web-resource-name>Book Details</web-resource-name>
            <url-pattern>/rest/books</url-pattern>
            <http-method>GET</http-method>
        </web-resource-collection>
        <auth-constraint>
            <role-name>*</role-name>
        </auth-constraint>
        <user-data-constraint>
            <transport-guarantee>CONFIDENTIAL</transport-guarantee>
        </user-data-constraint>
    </security-constraint>
    <login-config>
        <auth-method>BASIC</auth-method>
        <realm-name>dev-ldap</realm-name>
    </login-config>
    <security-role>
        <role-name>admin</role-name>
    </security-role>
</web-app>
//Protected JX-RS service

@Path("books")
public class BooksResource{

  //no role needed
  @GET
  @Path("{isbn}")
  public Response get(@PathParam("isbn") String isbn){
    return Response.ok(getBook(isbn)).build();
  }

  //admin role members only
  @POST
  public Response create(Book book){
    return Response.created(createBook(book).getID()).build();
  }
}

Implications of the above security configuration ?

  • Authentication: Enforced using element. Users will need to use their credentials and will be authenticated against the realm dev-ldap (imaginary LDAP directory) specified using

  • Authorisation (role based): Any authenticated user (in any role) is allowed to fetch (using GET) book details. This is specified by the element. However, the book creation (using POST) service is restricted to users in admin role only, thanks to once again

  • Transport Layer (encryption): enforced by . Both GET and POST can be invoked over HTTPS only

Annotation based

All the annotations in this section belong to the Common Annotations specification. The container/environment where they execute (in this case its the Servlet container) defines their expected behaviour and implements these annotations in a way that they are honoured at runtime.

@DeclareRoles

As the name itself indicates, this Class level annotation is used to declare a list of roles available within the application. Specifically, it comes into picture when programmatic (API based) authorization check is initiated by the SecurityContext.isUserInRole(String role) method

@RolesAllowed

This annotation can be used on classes, individual methods or both. It specifies one or more roles which are permitted to invoke bean methods. In case the annotation is used on both class and individual methods of the bean class, the method level annotation takes precedence

@PermitAll

It can be used on both class and individual methods. If applied on a class, this annotation allows all its methods to be executed without any restrictions unless a method is explicitly annotated using @RolesAllowed

@DenyAll

This can be applied on a class or on specific methods. It instructs the container to forbid execution of the particular method guarded by this annotation. Please note that the method can still be used internally within the bean class

@RunAs

The use of this annotation helps impersonate a user identity (on purpose) i.e. it allows a bean method to be invoked under the context of a specific role by. This annotation can only be used on a class and is implicitly enforced on all the its methods

Programmatic

SecurityContext is a JAX-RS abstraction over HTTPServletRequest for security related information only

It can be used for

  • figuring out how the caller was authenticated

  • extracting the authenticated Principal info

  • Role membership confirmation (programmatic authorization), and,

  • whether or not the request was initiated securely (over HTTPS)

Custom SecurityContext implementation

It helps when you have a custom authentication mechanism not implemented using standard Java EE security realm. A typical example is token based authentication based on custom (app specific) HTTP headers

  • the web container is not be aware of the authentication detail. Hence, the SecurityContext instance will not contain the subject, role and other details

  • the JAX-RS request pipeline needs to be aware of the associated security context & make use of it within its business logic

SecurityContext is an interface after all. All you need to do is just implement, inject (using @Context) and use it !

//Custom SecurityContext implementation

@Priority(Priorities.AUTHENTICATION)
public class AuthFilterWithCustomSecurityContext implements ContainerRequestFilter {
    @Context
    UriInfo uriInfo;

    @Override
    public void filter(ContainerRequestContext requestContext) throws IOException {
        String authHeaderVal = requestContext.getHeaderString("Auth-Token");
        //execute custom authentication
        String subject = validateToken(authHeaderVal); 
        if (subject!=null) {
            final SecurityContext securityContext = requestContext
                                .getSecurityContext();
            requestContext.setSecurityContext(new SecurityContext() {
                        @Override
                        public Principal getUserPrincipal() {
                            return new Principal() {
                                @Override
                                public String getName() {
                                    return subject;
                                }
                            };
                        }

                        @Override
                        public boolean isUserInRole(String role) {
                            List<Role> roles = findUserRoles(subject);
                            return roles.contains(role);
                        }

                        @Override
                        public boolean isSecure() {
                            return uriInfo.getAbsolutePath().toString()
                                        .startsWith("https");
                        }

                        @Override
                        public String getAuthenticationScheme() {
                            return "Token-Based-Auth-Scheme";
                        }
                    });
        }

    }
}

Stateless 'token' based security

This section discusses

  • Provides a quick intro to Json Web Token (JWT)

  • Shows how to use it with JAX-RS (for authentication) with an example

The jose4j library was used for JWT creation and validation

Brief intro to JWT

  • A standard defined by RFC 7519

  • Used to exchange claims

  • Has a pre-defined structure

Anatomy of a JWT

It consists of three parts

  • Header: consists of info like signature mechanism, token type etc.

  • Body (Claims): the meat of the payload

  • Signature*: signature of the contents to protect against tampered/malicious JWTs

These three components come together to form the actual token

JWT building blocks

//header

{
  "alg": "HS256",
  "typ": "JWT"
}

//payload/claims

{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true
}

//the formula

encoded_part = base64Of(header) + "." base64Of(payload)
//assume that algo is HS256 and secret key is 'secret'

signature = signedUsingHS256WithSecret(encoded_part) 
JWT = encoded_part + "." + sigature

//the JWT ( notice the separator/period --> "." )

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 //base-64 encoded header
.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9 //base64 encoded payload
.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ //the signature

Benefits of using JWT

  • Useful for implementing Stateless authentication

  • Compact: less verbose compared to other counterparts like SAML)

  • Flexible: Although its backed by a standard, you are free to choose your signature, claim attributes etc.

  • JWT is not only an authentication mechanism. It's more about information exchange & it's usage is limited by your imagination

  • It's signed, not encrypted: its contents can be picked up over the wire if you do not secure your transport layer (e.g. using HTTPS)

Using JWT with JAX-RS

Let's look at an example of how we might use JWT in a JAX-RS based application. As stated earlier, this sample uses JWT as a stateless authentication token. The process is split into distinct steps

Getting hold of the JWT

Why do we need a JWT in the first place? It is because the JAX-RS resource is protected and its access is dependent on the presence of a JWT token within the HTTP request (this is achieved by a JAX-RS filter). Think of JWT as a proxy to the actual username/password (or any other authentication criteria) for your application. You need to actually authenticate using the method required by your application in order to get access to the JWT. In this example, a successfully executed HTTP Basic authentication is the gateway to the token This is what happens

  1. The application executes a GET request to the URL http://host:port/context-root/auth/token with the HTTP Authorization header containing user credentials

  2. HTTP Basic authentication kicks in. This is enforced by the web.xml (snippet below) which ensures that any request to the /auth/* is not allowed to pass unauthenticated

  3. In case of a successful authentication, the JWT is returned in the HTTP response header

JWT creation

//exception handling excluded to avoid verbosity

RsaJsonWebKey rsaJsonWebKey = RsaKeyProducer.produce();

JwtClaims claims = new JwtClaims();
claims.setSubject("user1");

JsonWebSignature jws = new JsonWebSignature();
jws.setPayload(claims.toJson());
jws.setKey(rsaJsonWebKey.getPrivateKey());
jws.setAlgorithmHeaderValue(AlgorithmIdentifiers.RSA_USING_SHA256);

String jwt = jws.getCompactSerialization();

//the encoded JWT

eyJhbGciOiJSUzI1NiJ9
.eyJzdWIiOiJ1c2VyMSJ9
.HG9GCQPuC6w6pulbYE2uurCzpEwoWvz_8Ps5ZjgtfomyY4LWacDEzlHLnyMj9H7aqgcePC7_4l2wDXQV-S0BQRsIZfJeUUmWxlTlLzvKZr_2eEx00YZPPFZNoFCfwB-ajLHLLenROy4aSjPo_Vg9o7N-p0DZ1yZQoJhkvoVJgkhX9FeAf65kIZkbuJC9dmVkzXSOpVf4GZeCpNDJJYSo6IAnL3UEoWek6V9BtWgV-a4xvydp7vxkdDXmzmalGLYuWbuVG7rWcbWwSfsg38iEG-mqptqA_Kzk1VmjwWNo_BfvLuzjzuosqi732-5SRzBP-2zqGghBqMYsGgkqkH2n7A

//human readable format

{
  "alg": "RS256" //header
}

{
  "sub": "user1" //claim payload
}

Leveraging the JWT

  • The JWT is sent by app in the subsequent request for the JAX-RS resource i.e.http://host:port/context-root/resources/books

  • The JAX-RS Container Request Filter kicks in - it checks for the presence of the JWT, verifies it. The verification process implicitly checks for presence of the required claim attributes as well as the signature validation

Extracting JWT from HTTP header

@Priority(Priorities.AUTHENTICATION)
public class JWTAuthFilter implements ContainerRequestFilter{
  @Override
  public void filter(ContainerRequestContext requestContext) throws IOException {
        String authHeaderVal = requestContext.getHeaderString("Authorization");

            //consume JWT i.e. execute signature validation
            if(authHeaderVal.startsWith("Bearer")){
            try {
                validate(authHeaderVal.split(" ")[1]);
            } catch (InvalidJwtException ex) {
                requestContext
                .abortWith(Response.status(Response.Status.UNAUTHORIZED).build());
            }
            }else{
                requestContext
                .abortWith(Response.status(Response.Status.UNAUTHORIZED).build());
            }
  }

JWT verification

//should be the same as the one used to build the JWT previously
RsaJsonWebKey rsaJsonWebKey = getCachedRSAKey(); 

JwtConsumer jwtConsumer = new JwtConsumerBuilder()
        // the JWT must have a subject claim
      .setRequireSubject() 
      // verify the signature with the public key
       .setVerificationKey(rsaJsonWebKey.getKey()) 
        .build(); // create the JwtConsumer instance

JwtClaims jwtClaims = jwtConsumer.processToClaims(jwt);
  • It allows the request to go through in case of successful verification, otherwise, the filter returns a HTTP 401 Unauthorized response to the client

  • A container response filter ensures that the JWT is added as a part of the response header again. It only does so when the JWT verification was successful - this is made possible using the contextual state/information sharing feature provided by JAX-RS Request Filters

//Response filter makes use of the JWT validation result

public class JWTResponseFilter implements ContainerResponseFilter {

    @Override
    public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) throws IOException {
        System.out.println("response filter invoked...");
        if (requestContext.getProperty("auth-failed") != null) {
            Boolean failed = (Boolean) requestContext.getProperty("auth-failed");
            if (failed) {
                System.out.println("JWT auth failed. No need to return JWT token");
                return;
            }
        }

        List<Object> jwt = new ArrayList<Object>();
        jwt.add(requestContext.getHeaderString("Authorization").split(" ")[1]);
        responseContext.getHeaders().put("jwt", jwt);
        System.out.println("Added JWT to response header 'jwt'");

    }
}

Other considerations

Choice of claim attributes

In this example, we just used the standard sub (subject) attribute in the claim. You are free to use others. I would highly recommend reading section 4 of the JWT RFC for deeper insight

JWT expiration

One should also consider expiring the JWT token after a finite time. You would need to

  • Make use of the exp claim attribute (standard)

  • Think about refreshing the JWT token (after expiry)

Revisiting the Stateless paradigm

Although the initial authentication was executed using HTTP Basic, the application does not rely on a Session ID for authorising subsequent requests from the same user. This has the following implications

  • There is no need to store the session ID on the server side

  • There is no need to sync this session ID to multiple application nodes in a cluster

As stated above, JWT is helping us with Stateless authentication (it is not very different from the HTTP protocol itself)

  • Our JWT contains all the required data (claim) for the conversation (in this case authentication)

  • We pass the token with each HTTP request (only to access resources which are protected by the JWT to begin with)

  • The application does not need to repetitively authenticate the user (via the username-password combo)

Now we can scale ! You can have multiple instances (horizontally scaled across various nodes/clusters) of your JAX-RS service and yet you need not sync the state of the token between various nodes. If a subsequent request goes to different node than the previous request, the authentication will still happen (provided you pass the JWT token)

Last updated