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:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@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:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@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(); | |
} |
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:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@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(); | |
} |
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:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@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(); | |
} | |
} |
Auth is a bean that contains information about users’ login details:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
} |
As demonstrated, TokenUtils is a class that uses the generateToken() method to generate a new token:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} |
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:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@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(); | |
} | |
} |
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:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@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(); | |
} | |
} |
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:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public interface Auth { | |
public boolean isLogged(String token); | |
public String login(String login, String password); | |
String logout(String token); | |
} |
Here’s the code block for the AuthImpl class, which is an implementation of the Auth interface as well as a stateless EJB:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@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); | |
} | |
} |
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!