Skip to content

danielemaddaluno/jaxrs-jws-jwt-web

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

11 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

jaxrs-jws-jwt-web

Credits to Cássio Mazzochi Molin whose stackoverflow response has helped me to structure this simple example project. See more in:

  1. Token-based authentication with JAX-RS 2.0
  2. StackOverflow Response
  3. JSON Web Token in action with JAX-RS
  4. JAX-RS Security using JSON Web Tokens (JWT) for Authentication and Authorization

Here below I explain how to test this sample, and then there is a detailed explanation of how to build it from scratch (credits to Cássio Mazzochi Molin response).

This image explain how a JSON Web Token works: JWT_AUTH_FLOW

  1. In the code I left a single TODO inside the class AuthenticationJwt in the method authenticate(String username, String password) in which you have to deal with an authentication against a database or LDAP or file or whatever to verify the identity and issue a token only if the user is authorized to get it. For now I leaved it blank so that any user which ask a JWT will receive a token.

  2. Firstly invoke http://localhost:8080/jaxrs-jws-jwt-web/rest-api/auth/auth with a POST and Content-Type set to application/json, finally fill the body with a json like this {"username":"client_username", "password":"client_password"} using postman you should have something like this: auth_post_1

  3. Sending the POST request you should get a result like this: auth_post_2

  4. If you're using postman and switch the view to Headers you could see the server-side generated JSON Web Token that the client could now use until the expiring time to invoke secured endopoints. Copy that value (in the sceenshot I highlighted the one that I copied) in the clipboard. auth_post_3

  5. Now invoke http://localhost:8080/jaxrs-jws-jwt-web/rest-api/test/1 with a DELETE, and Content-Type set to application/json add a new Header of type Authorization and fill it with the text Bearer + the previous JSON Web Token and send the request. The response should be something like this. auth_delete

How token-based authentication works (read this to fully understand the project jaxrs-jws-jwt-web)

A token is a piece of data generated by the server which identifies a user.

At a first look, token-based authentication follow these steps:

  1. The client sends its credentials (username and password) to the server.
  2. The server authenticates them and generates a token with an expiration date.
  3. The server stores the previously generated token in some storage with user identifier, such as a database or a map in memory.
  4. The server sends the generated token to the client.
  5. In every request, the client sends the token to the server.
  6. The server, in each request, extracts the token from the incoming request, looks up the user identifier with the token to obtain the user information to do the authentication/authorization.
  7. If the token is expired, the server generates another token and send it back to the client.

What you can do with JAX-RS 2.0 (Jersey, RESTEasy and Apache CXF)

This solution uses only the JAX-RS 2.0 API, avoiding any vendor specific solution. So, it should work with the most popular JAX-RS 2.0 implementations, such as Jersey, RESTEasy and Apache CXF.

It's important mention that if you are using a token-based authentication, you are not relying on the standard Java EE Web application security mechanisms offered by the Servlet container and configurable via application's web.xml descriptor.

Authenticate a user with their username and password and issue a token

Create a REST endpoint which receives and validates the credentials (username and password) and issue a token for the user:

@Path("/authentication")
public class AuthenticationEndpoint {

	@POST
	@Produces("application/json")
	@Consumes("application/x-www-form-urlencoded")
	public Response authenticateUser(@FormParam("username") String username, 
			@FormParam("password") String password) {

		try {

			// Authenticate the user using the credentials provided
			authenticate(username, password);

			// Issue a token for the user
			String token = issueToken(username);

			// Return the token on the response
			return Response.ok(token).build();

		} catch (Exception e) {
			return Response.status(Response.Status.UNAUTHORIZED).build();
		}      
	}

	private void authenticate(String username, String password) throws Exception {
		// Authenticate against a database, LDAP, file or whatever
		// Throw an Exception if the credentials are invalid
	}

	private String issueToken(String username) {
		// Issue a token (can be a random String persisted to a database or a JWT token)
		// The issued token must be associated to a user
		// Return the issued token
	}
}

If any exceptions happen when validating the credentials, a response with status 401 UNAUTHORIZED will be returned.

If the credentials are successfully validated, a response with status 200 OK will be returned and the issued token is sent to the client on the response. The client must send that token to the server in every request.

Using this approach, you expect your client will send the credentials in the following format in the body of the request:

username=admin&password=123456

Instead of form params, you can wrap the username and the password into a class:

public class Credentials implements Serializable {

	private String username;
	private String password;

	// Getters and setters omitted
}

And consume it as JSON:

@POST
@Produces("application/json")
@Consumes("application/json")
public Response authenticateUser(Credentials credentials) {

	String username = credentials.getUsername();
	String password = credentials.getPassword();

	// Authenticate the user, issue a token and return a response
}

Using this approach, you expect your client will send the credentials in the following format in the body of the request:

{ "username": "admin", "password": "123456" }

Extract the token from the request and validate it

The client should send the token on the standard HTTP Authorization header of the request. For example:

Authorization: Bearer

Note that the name of the standard HTTP header is unfortunate because it carries authentication information, not authorization.

JAX-RS provides @NameBinding, a meta-annotation used to create name-binding annotations for filters and interceptors:

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

The defined name-binding annotation @Secured will be used to decorate a filter class, which implements ContainerRequestFilter, allowing you to handle the request. The ContainerRequestContext helps you to extract the token from the HTTP request:

@Secured
@Provider
@Priority(Priorities.AUTHENTICATION)
public class AuthenticationFilter implements ContainerRequestFilter {

	@Override
	public void filter(ContainerRequestContext requestContext) throws IOException {

		// Get the HTTP Authorization header from the request
		String authorizationHeader = requestContext.getHeaderString(HttpHeaders.AUTHORIZATION);

		// Check if the HTTP Authorization header is present and formatted correctly 
		if (authorizationHeader == null || !authorizationHeader.startsWith("Bearer ")) {
			throw new NotAuthorizedException("Authorization header must be provided");
		}

		// Extract the token from the HTTP Authorization header
		String token = authorizationHeader.substring("Bearer".length()).trim();

		try {

			// Validate the token
			validateToken(token);

		} catch (Exception e) {
			requestContext.abortWith(Response.status(Response.Status.UNAUTHORIZED).build());
		}
	}

	private void validateToken(String token) throws Exception {
		// Check if it was issued by the server and if it's not expired
		// Throw an Exception if the token is invalid
	}
}

If any problems happen during the token validation, a response with status 401 UNAUTHORIZED will be returned.

Otherwise, the request will proceed to an endpoint.

Securing your REST endpoints

Bind the filter to your endpoints methods or classes by annotating them with the @Secured annotation created above. For the methods and/or classes which are annotated, the filter will be executed. It means that these endpoints only will be reached if the request is performed with a valid token.

If some methods or classes do not need authentication, simply do not annotate them.

@Path("/")
public class MyEndpoint {

	@GET
	@Path("{id}")
	@Produces("application/json")
	public Response myUnsecuredMethod(@PathParam("id") Long id) {
		// This method is not annotated with @Secured
		// The authentication filter won't be executed before invoking this method
		...
	}

	@DELETE
	@Secured
	@Path("{id}")
	@Produces("application/json")
	public Response mySecuredMethod(@PathParam("id") Long id) {
		// This method is annotated with @Secured
		// The authentication filter will be executed before invoking this method
		// The HTTP request must be performed with a valid token
		...
	}
}

In the example above, the filter will be executed only for mySecuredMethod(Long) because it's annotated with @Secured.

Identifying the current user

It's very likely you will need to know the user who is performing the request within your REST endpoints. The following approaches can be useful to do it:

Overriding the SecurityContext

Within your ContainerRequestFilter.filter(ContainerRequestContext) method, you can set a new security context information for the current request.

Override the SecurityContext.getUserPrincipal(), returning a Principal instance.

The Principal's name is the username of the user you issued the token for. You will have to know it when validating the token.

requestContext.setSecurityContext(new SecurityContext() {

	@Override
	public Principal getUserPrincipal() {

		return new Principal() {

			@Override
			public String getName() {
				return username;
			}
		};
	}

	@Override
	public boolean isUserInRole(String role) {
		return true;
	}

	@Override
	public boolean isSecure() {
		return false;
	}

	@Override
	public String getAuthenticationScheme() {
		return null;
	}
});

Inject a proxy of the SecurityContext in any REST endpoint class:

@Context
SecurityContext securityContext;

The same can be done in a method:

@GET
@Secured
@Path("{id}")
@Produces("application/json")
public Response myMethod(@PathParam("id") Long id, 
		@Context SecurityContext securityContext) {
	...
}

And get the Principal:

Principal principal = securityContext.getUserPrincipal(); String username = principal.getName();

Using CDI (Context and Dependency Injection)

If, for some reason, you don't want overriding the SecurityContext, you can use CDI, which provides useful features such as events and producers.

Create a CDI qualifier which will be used when handling the authentication event and when injecting the authenticated user in your beans:

@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER})
public @interface AuthenticatedUser { }

In your AuthenticationFilter created above, inject an Event:

@Inject
@AuthenticatedUser
Event<String> userAuthenticatedEvent;

When the user authenticates, fire the event passing the username as parameter (remember, your token must be associated to a user and you need to be able to retrieve the username from a token):

userAuthenticatedEvent.fire(username);

Probably you have a class which represents a user in your application. Let's call this class User.

The piece of code below handles the authentication event, finds a User instance with the correspondent username and assigns it to the field authenticatedUser:

@RequestScoped
public class AuthenticatedUserProducer {

	@Produces
	@RequestScoped
	@AuthenticatedUser
	private User authenticatedUser;

	public void handleAuthenticationEvent(@Observes @AuthenticatedUser String username) {
		this.authenticatedUser = findUser(username);
	}

	private User findUser(String username) {
		// Hit the the database or a service to find a user by its username and return it
		// Return the User instance
	}
}

The authenticatedUser field produces a User instance which can be injected in your beans, such as JAX-RS services, CDI beans, servlets and EJBs:

@Inject
@AuthenticatedUser
User authenticatedUser;

Note that the CDI @Produces annotation is different from the JAX-RS @Produces annotation:

Supporting role-based authorization

Besides authentication you can also support role-based authorization in your REST endpoints.

Create an enumeration and define the roles according to your needs:

public enum Role {
	ROLE_1,
	ROLE_2,
	ROLE_3
}

Change the @Secured name binding annotation created above to support roles:

@NameBinding
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface Secured {
	Role[] value() default {};
}

Annotate your endpoints to perform role-based authorization.

Note that the @Secured annotation can the used in classes and/or methods. So let's make the method annotations override the class annotations:

@Path("/example")
@Secured({Role.ROLE_1})
public class MyEndpoint {

	@GET
	@Path("{id}")
	@Produces("application/json")
	public Response myMethod(@PathParam("id") Long id) {
		// This method is not annotated with @Secured
		// But it's declared within a class annotated with @Secured({Role.ROLE_1})
		// So it only can be executed by the users who have the ROLE_1 role
		...
	}

	@DELETE
	@Path("{id}")    
	@Produces("application/json")
	@Secured({Role.ROLE_1, Role.ROLE_2})
	public Response myOtherMethod(@PathParam("id") Long id) {
		// This method is annotated with @Secured({Role.ROLE_1, Role.ROLE_2})
		// The method annotation overrides the class annotation
		// So it only can be executed by the users who have the ROLE_1 or ROLE_2 roles
		...
	}
}

Create a filter with the AUTHORIZATION priority, which is executed after the AUTHENTICATION priority filter defined previously.

The ResourceInfo can be used to get the Method and Class which match with the requested URL and extract the annotations from them:

@Secured
@Provider
@Priority(Priorities.AUTHORIZATION)
public class AuthorizationFilter implements ContainerRequestFilter {

	@Context
	private ResourceInfo resourceInfo;

	@Override
	public void filter(ContainerRequestContext requestContext) throws IOException {

		// Get the resource class which matches with the requested URL
		// Extract the roles declared by it
		Class<?> resourceClass = resourceInfo.getResourceClass();
		List<Role> classRoles = extractRoles(resourceClass);

		// Get the resource method which matches with the requested URL
		// Extract the roles declared by it
		Method resourceMethod = resourceInfo.getResourceMethod();
		List<Role> methodRoles = extractRoles(resourceMethod);

		try {

			// Check if the user is allowed to execute the method
			// The method annotations override the class annotations
			if (methodRoles.isEmpty()) {
				checkPermissions(classRoles);
			} else {
				checkPermissions(methodRoles);
			}

		} catch (Exception e) {
			requestContext.abortWith(
					Response.status(Response.Status.FORBIDDEN).build());
		}
	}

	// Extract the roles from the annotated element
	private List<Role> extractRoles(AnnotatedElement annotatedElement) {
		if (annotatedElement == null) {
			return new ArrayList<Role>();
		} else {
			Secured secured = annotatedElement.getAnnotation(Secured.class);
			if (secured == null) {
				return new ArrayList<Role>();
			} else {
				Role[] allowedRoles = secured.value();
				return Arrays.asList(allowedRoles);
			}
		}
	}

	private void checkPermissions(List<Role> allowedRoles) throws Exception {
		// Check if the user contains one of the allowed roles
		// Throw an Exception if the user has not permission to execute the method
	}
}

If the user has no permission to execute the method, the request is aborted with a 403 FORBIDDEN.

To know the user who is performing the request, see the section above. You can get it from the SecurityContext (which should be already set in the ContainerRequestContext) or inject it using CDI, depending on the approach you are using.

If a @Secured annotation has no roles declared, you can assume all authenticated users can access that endpoint, independent the roles the users have.

How to issue a token

A token can be opaque which reveals no details other than the value itself (like a random string) or can be self-contained (like JSON Web Token).

Random string

A token can be issued by generating a random string and persisting it to a database with an expiration date and with a user identifier associated to it. A good example of how to generate a random string in Java can be seen here:

Random random = new SecureRandom(); String token = new BigInteger(130, random).toString(32);

JSON Web Token (JWT)

JSON Web Token (JWT) is a standard method for representing claims securely between two parties and is defined by the RFC 7519. It's a self-contained token and enables you to store a user identifier, an expiration date and whatever you want (but don't store passwords) in a payload, which is a JSON encoded as Base64.

The payload can be read by the client and the integrity of the token can be easily checked by verifying its signature on the server.

You won't need to persist JWT tokens if you don't need to track them. Althought, by persisting the tokens, you will have the possibility of invalidating and revoking the access of them. To keep the track of JWT tokens, instead of persisting the whole token, you could persist the token identifier (the jti claim) and some metadata (the user you issued the token for, the expiration date, etc) if you need.

There are a few Java libraries to issue and validate JWT tokens (have a look here and here). To find some other great resources to work with JWT, have a look at http://jwt.io.

Your application can provide some functionality to revoke the tokens, but it's recommended revoking the tokens when the users change their password.

When persisting tokens, always consider removing the old ones in order to prevent your database from growing indefinitely.

Additional information

  • It doesn't matter which type of authentication you are using. Always use HTTPS to prevent the man-in-the-middle attack.
  • Take a look at this question from Information Security for more information about tokens.
  • In this article you will find some useful information about token-based authentication.
  • Apache DeltaSpike provides portable CDI extensions such as a security module, which can be used to secure REST applications.
  • Interested in an OAuth 2.0 protocol implementation in Java? Check the Apache Oltu project.

About

Jaxrs with authentication using jws and jwt

Resources

License

Stars

Watchers

Forks

Packages

No packages published