How to Implement the Single Sign-On Pattern

Learn how to implement single sign-on in Java EE 8 in this tutorial by Rhuan Rocha, the author of Java EE 8 Design Patterns and Best Practices.

This tutorial shows an example of implementing single sign-on (SSO) where you’ll create the authentication service through a custom process to authenticate the users and will also allow the user to log in. After this, one token will be generated and sent to the user.

Further, you’ll create two applications (App1 and App2). When the user tries to access these applications without having logged in, the application will authenticate the user on the authentication service, so the user can access App1 and App2 without having to log in again.

The authentication service will be a REST application written using JAX-RS, and App1 and App2 will be applications that implement a JAX-RS client to validate user access. With this, the following classes will be created to use with the example:

  • AuthenticationResource: This is responsible for processing the login request and validating the authentication of a user. The class is written using JAX-RS and is inside the authentication service application.
  • AuthSession: This is a session that contains login data and information. This class has the application scope, that is, a Java EE scope.
  • Auth: This is a bean that represents the logged-in user. This class contains the user’s login details, password, and the date of last login.
  • TokenUtils: This is a class that contains a method for generating tokens.
  • App1: This app sends the Welcome to App1 text if the user is logged in. If the user is not logged in, the application launches an error.
  • App2: This app sends the Welcome to App2 text if the user is logged in. If the user is not logged in, the application launches an error.
  • Auth: This is an interface with methods responsible for calling the authentication services.
  • AuthImpl: This is an EJB class that implements the Auth interface.

The App1 and App2 applications don’t have any process or logic that is required in order to log a user in. This is the responsibility of the authentication service (the resource that validates the authentication), which has the AuthenticationResource class with this responsibility.

Implementing the AuthenticationResource class

AuthenticationResource is a JAX-RS resource, which allows logging in and validates the authentication of the application. The following code shows its implementation:

@Path("auth")
public class AuthenticationResource {
@Inject
private AuthSession authSession;
@POST
@Consumes("application/x-www-form-urlencoded")
public Response login(@FormParam("login") String login, @FormParam("password") String password) {
//If user already logged, then get it token
Optional<String> key = authSession.getToken(login, password);
if (key.isPresent()) {
return Response.ok(key.get()).build();
}
//Validade login and password on data source
if (!authSession.getDataSource().containsKey(login)
|| !authSession.getDataSource()
.get(login)
.getPassword()
.equals(password)) {
return Response.status(Response.Status.UNAUTHORIZED).build();
}
String token = TokenUtils.generateToken();
//Persist the information of authentication on AuthSession
authSession.putAuthenticated(token, new Auth(login, password, new Date()));
return Response.ok(token).build();
}
@HEAD
@Path("/{token}")
public Response checkAuthentication(@PathParam("token") String token) {
if (authSession.getAuthenticated().containsKey(token)) {
return Response.ok().build();
}
return Response.status(Response.Status.UNAUTHORIZED).build();
}
}

AuthenticationResource contains the authSession attribute used to persist the information about the login on the data source and obtain access to data sources that contain user information used to validate login credentials. Further, AuthenticationResource has two methods: login(String login, String password), is used to process the login request, and checkAuthentication( String token), used to allow clients to check whether a user is authenticated. In the following code block, you have the login method, which is used to log a user in:

@POST
@Consumes("application/x-www-form-urlencoded")
public Response login(@FormParam("login") String login, @FormParam("password") String password) {
//If user already logged, then get it token
Optional<String> key = authSession.getToken(login, password);
if (key.isPresent()) {
return Response.ok(key.get()).build();
}
//Validate the login and password on data source
if (!authSession.getDataSource().containsKey(login)
|| !authSession.getDataSource()
.get(login)
.getPassword()
.equals(password)) {
return Response.status(Response.Status.UNAUTHORIZED).build();
}
String token = TokenUtils.generateToken();
//Persiste the information of authentication on the AuthSession.
authSession.putAuthenticated(token, new Auth(login, password, new Date()));
return Response.ok(token).status(Response.Status.CREATED).build();
}

view raw
Login.java
hosted with ❤ by GitHub

You can see that if a user is already logged in, the token is returned as a response. If the user is not logged in, the login ID and password details are validated, and a new token is generated and returned as a response. Note that this method is called when the client sends a POST request to this resource.

The other method is checkAuthentication( String token), which is used to allow clients to check whether a user is authenticated. The method returns the 200 HTTP status code to the client if it is logged in, or returns the 401 HTTP status code if it is not logged in:

@HEAD
@Path("/{token}")
public Response checkAuthentication(@PathParam("token") String token) {
if (authSession.getAuthenticated().containsKey(token)) {
return Response.ok().build();
}
return Response.status(Response.Status.UNAUTHORIZED).build();
}

view raw
Authentication.java
hosted with ❤ by GitHub

Note that the checkAuthentication(String token) method is called when the client sends a HEAD request.

The AuthSession class is used in the AuthenticationResource class. The AuthSession class has an application scope and is used to persist information about users that are logged in and has a data source that contains all the login credentials:

@ApplicationScoped
public class AuthSession {
private Map<String, Auth> authenticated;
private Map<String, Auth> dataSource;
@PostConstruct
public void init() {
authenticated = new HashMap<>();
dataSource = new HashMap<>();
for (int i = 1; i <= 50; i++) {
dataSource.put("login" + i, new Auth("login" + i, "123456"));
}
}
public AuthSession putAuthenticated(String key, Auth auth) {
authenticated.put(key, auth);
return this;
}
public AuthSession removeAuthenticated(String key, Auth auth) {
authenticated.remove(key, auth);
return this;
}
public Map<String, Auth> getAuthenticated() {
return authenticated;
}
public Map<String, Auth> getDataSource() {
return dataSource;
}
public Optional<String> getToken(String login, String password) {
for (String key : authenticated.keySet()) {
Auth auth = authenticated.get(key);
if (auth.getLogin().equals(login)
&& auth.getPassword().equals(password)) {
return Optional.of(key);
}
}
return Optional.empty();
}
}

view raw
AuthSession.java
hosted with ❤ by GitHub

Auth is a bean that contains information about users’ login details:

public class Auth {
private String login;
private String password;
private Date loginDate;
public Auth() {
}
public Auth(String login, String password) {
this.login = login;
this.password = password;
}
public Auth(String login, String password, Date loginDate) {
this.login = login;
this.password = password;
this.loginDate = loginDate;
}
public String getLogin() {
return login;
}
public void setLogin(String login) {
this.login = login;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public Date getLoginDate() {
return loginDate;
}
public void setLoginDate(Date loginDate) {
this.loginDate = loginDate;
}
}

view raw
Auth.java
hosted with ❤ by GitHub

As demonstrated, TokenUtils is a class that uses the generateToken() method to generate a new token:

public class TokenUtils {
public static String generateToken() {
SecureRandom random = new SecureRandom();
long longToken = Math.abs(random.nextLong());
return Long.toString(new Date().getTime()) + Long.toString(longToken, 16);
}
}

view raw
TokenUtils.java
hosted with ❤ by GitHub

Implementing the App1 and App2 classes

In the code block of the previous section, you have the code of the App1 application. When this application is accessed by a GET request, a request is sent to the authentication service to validate whether the user has already logged in:

@Path("app1")
public class App1 {
@Inject
private Auth auth;
@GET
public Response helloWorld(String token) {
if (!auth.isLogged(token)) {
throw new WebApplicationException(Response.Status.UNAUTHORIZED);
}
return Response.ok("Hello World. Welcome to App1!").build();
}
@POST
@Consumes("application/x-www-form-urlencoded")
public Response helloWorld(@FormParam("login") String login, @FormParam("password") String password) {
if (Objects.isNull(login) || Objects.isNull(password)) {
throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR);
}
String token = auth.login(login, password);
return Response
.ok("Hello World. Welcome to App1!")
.header("token", token)
.build();
}
}

view raw
App1.java
hosted with ❤ by GitHub

In the above code, you have the App1 class, which contains the auth parameter, an EJB used to integrate with the authentication service. Further, this class has two methods, called helloWorld, with different signatures. In helloWorld( String login, String password ), the login is completed and then the Hello World. Welcome to App1! message is sent to the user. In helloWorld( String token ), the token is validated; if it is a valid token and the user is logged in, the Hello World. Welcome to App1! message is sent to the user.

The following code block is for the App2 class. The code is the same as that of App1 but prints a different message to the user:

@Path("app2")
public class App2 {
@Inject
private Auth auth;
@GET
public Response helloWorld(String token) {
if (!auth.isLogged(token)) {
throw new WebApplicationException(Response.Status.UNAUTHORIZED);
}
return Response.ok("Hello World. Welcome to App2!").build();
}
@POST
@Consumes("application/x-www-form-urlencoded")
public Response helloWorld(@FormParam("login") String login, @FormParam("password") String password) {
if (Objects.isNull(login) || Objects.isNull(password)) {
throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR);
}
String token = auth.login(login, password);
return Response
.ok("Hello World. Welcome to App2!")
.header("token", token)
.build();
}
}

view raw
App2.java
hosted with ❤ by GitHub

The following code block contains the Auth interface. This interface details the contract with the methods responsible for integrating with the authentication service, validating the authentication, and logging in:

public interface Auth {
public boolean isLogged(String token);
public String login(String login, String password);
String logout(String token);
}

view raw
Auth.java
hosted with ❤ by GitHub

Here’s the code block for the AuthImpl class, which is an implementation of the Auth interface as well as a stateless EJB:

@Stateless
public class AuthImpl implements Auth {
private String URL = "http://localhost:8080/javaEE8ExampleSSOAppService/resources/auth";
@Override
public boolean isLogged(String token) {
return prepareWebTarget().path("/" + token)
.request()
.head().getStatus() == 200;
}
@Override
public String login(String login, String password) {
return prepareWebTarget()
.request()
.post(Entity.form(new Form("login", login)
.param("password", password)),
String.class);
}
@Override
public String logout(String token) {
return prepareWebTarget().path("/" + token)
.request()
.delete(String.class);
}
protected WebTarget prepareWebTarget() {
return ClientBuilder.newClient().target(URL);
}
}

view raw
AuthImpl.java
hosted with ❤ by GitHub

The above code block has three methods, called isLogged, login, and logout, with the signatures isLogged(String token), login(String login, String password), and logout(String token), respectively. When a user logs in to an application (either App1 or App2) and navigates to another application using the token, he/she won’t need to log in again.

That’s it! If you found this tutorial interesting, you can explore Java EE 8 Design Patterns and Best Practices to build enterprise-ready scalable applications with architectural design patterns. If you’re a Java developer wanting to implement clean design patterns to build robust and scalable applications, this book is a must-read!

Leave a Reply

Your email address will not be published. Required fields are marked *