Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Securing REST services in Jersey

I am very much new to web services. I have exposed some REST services using Jersey 2 in integration with Spring. Now I need to secure those rest services using authentication with username/password. I am told not to use Spring Security.

I have no idea of how to do this. I did search on the net but various links show various implementation and I am unable to decide how to proceed with it.

like image 566
Anand Avatar asked Dec 02 '22 17:12

Anand


2 Answers

A common way for authenticating with username and password is to use Basic Authentication. Basically the client needs to send a request header Authorization, with the the header value as Basic Base64Encoded(username:password). So is my username is peeskillet and my password is pass, I, as a client, should set the header as

Authorization: Basic cGVlc2tpbGxldDpwYXNz

In a servlet environment, the container should have support for Basic authentication. You would configure this support on the web.xml. You can see an example in 48.2 Securing Web Applications of the Java EE tutorial. You will also notice in an example

<transport-guarantee>CONFIDENTIAL</transport-guarantee>

That is for SSL support. This is recommended for Basic Authentication.

If you don't want to deal with the hassle of working with security domains and login modules, realm, and such, that would be required to customize the servlet support, or if you're just not in a servlet environment, implementing Basic Auth in a ContainerRequestFilter is really not too difficult.

You can see a complete example of how this could be done at jersey/examples/https-clientserver-grizzly. You should focus on the SecurityFilter

The basic flow in the filter goes something like this

  1. Get the Authorization header. If it doesn't exist, throw an AuthenticationException. In which case the AuthenticationExceptionMapper will send out the header "WWW-Authenticate", "Basic realm=\"" + e.getRealm() + "\", which is part of the Basic Auth protocol

  2. Once we have the header, we parse it just to get the Base64 encoded username:password. Then we decode it, then split it, then separate the user name and password. If any of this process fails, again throw the WebApplicationException that maps to a 400 Bad Request.

  3. Check the username and password. The example source code just checks if the username is user and the password is password, but you will want to use some service in the filter to verify this information. If either of these fail, throw an AuthenticationException

  4. If all goes well, a User is created from the authenticate method, and is injected into an Authorizer (which is a SecurityContext). In JAX-RS, the SecurityContext is normally used for authorization`.

For the authorization, if you want to secure certain areas for certain resources, you can use the @RolesAllowed annotation for your classes or methods. Jersey has support for this annotation, by registering the RolesAllowedDynamicFeature.

What happens under the hood is that the SecurityContext will be obtained from the request. With the example I linked to, you can see the Authorizer, it has an overridden method isUserInRole. This method will be called to check against the value(s) in @RolesAllowed({"ADMIN"}). So when you create the SecurityContext, you should make sure to include on the overridden method, the roles of the user.

For testing, you can simply use a browser. If everything is set up correctly, when you try and access the resource, you should see (in Firefox) a dialog as seen in this post. If you use cURL, you could do

C:/>curl -v -u username:password http://localhost:8080/blah/resource

This will send out a Basic Authenticated request. Because of the -v switch, you should see all the headers involved. If you just want to test with the client API, you can see here how to set it up. In any of the three cases mentioned, the Base64 encoding will be done for you, so you don't have to worry about it.

As for the SSL, you should look into the documentation of your container for information about how to set it up.

like image 159
Paul Samsotha Avatar answered Dec 19 '22 15:12

Paul Samsotha


So this is really a matter what you would like to achieve. My case was to get this thing running with mobile and a One-Page-App JavaScript.

Basically all you need to do is generate some kind of header that value that will be needed in every consecutive request you client will make.

So you do a endpoint in which you wait for a post with user/password:

@Path("/login")
public class AuthenticationResource {

@POST
@Consumes("application/json")
public Response authenticate(Credentials credential) {
    boolean canBeLoggedIn = (...check in your DB or anywher you need to)

    if (canBeLoggedIn) {
        UUID uuid = UUID.randomUUID();
        Token token = new Token();
        token.setToken(uuid.toString());
        //save your token with associated with user
        (...)

        return Response.ok(token).type(MediaType.APPLICATION_JSON_TYPE).build();
    } else {
        return Response.status(Response.Status.UNAUTHORIZED).build();
    }
}
}

Now you need to secure resource with need for that token:

   @Path("/payment")
   @AuthorizedWithToken
   public class Payments {

    @GET
    @Produces("application/json")
    public Response sync() {
     (...)
    }

    }

Notice the @AuthorizedWithToken annotation. This annotaation you can create on your own using special meta annotation @NameBinding

@NameBinding
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface AuthorizedWithToken {}

And now for the filter that implements checking of the header:

@AuthorizedWithToken
@Provider
public class XAuthTokenFilter implements ContainerRequestFilter {

    private static String X_Auth_Token = "X-Auth-Token";

    @Override
    public void filter(ContainerRequestContext crc) throws IOException {
        String headerValue = crc.getHeaderString(X_Auth_Token);
        if (headerValue == null) {
            crc.abortWith(Response.status(Response.Status.FORBIDDEN).entity("Missing " + X_Auth_Token + " value").build());
            return;
        }

        if(! TOKEN_FOUND_IN_DB) {
            crc.abortWith(Response.status(Response.Status.UNAUTHORIZED).entity("Wrong " + X_Auth_Token + " value").build());
            return;
        }
    }
}

You can create any number of your own annotations checking for various things in the http request and mix them. However you need to pay attention to Priorities but that actually easy thing to find. This method needs using https but that is obvious.

like image 31
almendar Avatar answered Dec 19 '22 15:12

almendar