What this example is about ?
Basically, we'll use PicketLink to provide the following features:
The TicketMonster users can be categorized in two types:
Most of the pages are public, so any TicketMonster user can access them. But only registered and authenticated users can book events. Also, only administrators have access to the admin pages.
This example will show you how to:
-
Configure a login page for your application that uses AJAX to invoke a REST login service;
-
Logout your users by invoking a REST logout service;
-
Restrict access for your application's URIs based on the user's roles;
-
Create a simple self-registration functionality using AJAX and a REST endpoint;
-
Share between client and server a simple security context for authenticated users;
Before you start
Before you start, it is important that you understand some key concepts like:
If you want to take a look at the sources of a TicketMonster version with all the changes discussed along this tutorial, please check:
PicketLink Configuration
PicketLink can be easily enabled in the TicketMonster application by using the PicketLink Base library. This library provides some integration points for CDI applications to create a security layer that provides all PicketLink security capabilities.
What are the steps ?
After having your TicketMonster application properly configured and running (with the administration UIs) you need to:
-
Configure the PicketLink IDM default schema dependenciy
As we're going to use a JPA-backed identity store, we need to provide some JPA entities in order to store identity related information. To make things easier, PicketLink already provides some default entities out-of-box.
Maven Dependencies
The easiest way to configure the PicketLink dependencies is using the JBoss BOM Security Stack:
If you select and copy the following pom.xml elements, you may need to paste them into a text editor and remove additional formatting.
<!-- Define the version of the JBoss EAP BOMs we want to import to specify tested stacks. -->
<version.jboss.bom.eap>6.2.0-redhat-1</version.jboss.bom.eap>
<!-- Define the version of the JBoss WFK BOMs we want to import to specify tested stacks. -->
<version.jboss.bom.wfk>2.4.0-redhat-1</version.jboss.bom.wfk>
<dependencyManagement>
<dependencies>
<!-- JBoss BOM for Java EE 6 APIs and PicketLink -->
<dependency>
<groupId>org.jboss.bom.eap</groupId>
<artifactId>jboss-javaee-6.0-with-security</artifactId>
<version>${version.jboss.bom.eap}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- JBoss BOM for Java EE 6 APIs and Apache Deltaspike -->
<dependency>
<groupId>org.jboss.bom.wfk</groupId>
<artifactId>jboss-javaee-6.0-with-deltaspike</artifactId>
<version>${version.jboss.bom.wfk}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
We're also configuring Apache Deltaspike BOM. We'll use Apache Deltaspike Security Annotations to demonstrate annotation-based authorization.
Now we have the BOMs properly configured, we just define the dependencies as following:
<!-- PicketLink dependencies. They will provide all security capabilities for the application. -->
<!-- PicketLink API provides authentication and authorization services. -->
<dependency>
<groupId>org.picketlink</groupId>
<artifactId>picketlink-api</artifactId>
<scope>compile</scope>
</dependency>
<!-- PicketLink Impl provides the default implementation for the PicketLink API. -->
<dependency>
<groupId>org.picketlink</groupId>
<artifactId>picketlink-impl</artifactId>
<scope>runtime</scope>
</dependency>
<!-- PicketLink IDM Schema provides some built-in JPA entities to be used with the JPA Identity Store. -->
<dependency>
<groupId>org.picketlink</groupId>
<artifactId>picketlink-idm-simple-schema</artifactId>
<scope>compile</scope>
</dependency>
<!-- Deltaspike API. We use compile scope as we need compile against its API -->
<dependency>
<groupId>org.apache.deltaspike.core</groupId>
<artifactId>deltaspike-core-api</artifactId>
<scope>compile</scope>
</dependency>
<!-- Deltaspike Impl. we use runtime scope as we need its implementation
dependencies only on runtime -->
<dependency>
<groupId>org.apache.deltaspike.core</groupId>
<artifactId>deltaspike-core-impl</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Deltaspike Security Module API. We use compile scope as we need
to compile against its API -->
<dependency>
<groupId>org.apache.deltaspike.modules</groupId>
<artifactId>deltaspike-security-module-api</artifactId>
<scope>compile</scope>
</dependency>
<!-- Deltaspike Security Impl. we use runtime scope as we need its implementation
dependencies only on runtime -->
<dependency>
<groupId>org.apache.deltaspike.modules</groupId>
<artifactId>deltaspike-security-module-impl</artifactId>
<scope>runtime</scope>
</dependency>
PicketLink Configuration
In our case, since we're using a JPA-backed Identity Store, we need to define the JPA entities provided by the sample schema in the META-INF/persistence.xml:
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.0"
xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">
<persistence-unit name="primary">
...
<!-- All those entity classes are provided by the picketlink-idm-simple-schema module. -->
<class>org.picketlink.idm.jpa.model.sample.simple.AttributedTypeEntity</class>
<class>org.picketlink.idm.jpa.model.sample.simple.AccountTypeEntity</class>
<class>org.picketlink.idm.jpa.model.sample.simple.RoleTypeEntity</class>
<class>org.picketlink.idm.jpa.model.sample.simple.GroupTypeEntity</class>
<class>org.picketlink.idm.jpa.model.sample.simple.IdentityTypeEntity</class>
<class>org.picketlink.idm.jpa.model.sample.simple.RelationshipTypeEntity</class>
<class>org.picketlink.idm.jpa.model.sample.simple.RelationshipIdentityTypeEntity</class>
<class>org.picketlink.idm.jpa.model.sample.simple.PartitionTypeEntity</class>
<class>org.picketlink.idm.jpa.model.sample.simple.PasswordCredentialTypeEntity</class>
<class>org.picketlink.idm.jpa.model.sample.simple.AttributeTypeEntity</class>
...
</persistence-unit>
</persistence>
For last, we need to tell PicketLink which EntityManager should be used when performing the IDM operations.
org.jboss.jdf.example.ticketmonster.util.Resources
@Produces
@PicketLink
@PersistenceContext(unitName = "primary")
private EntityManager picketLinkEntityManager;
For more information about how to configure other Identity Store (LDAP or File based) check the PicketLink documentation.
Authentication
Most of the functionality provided by TicketMonster is done with JavaScript and AJAX calls to the server. The authentication follows the same principle. Basically, what we have is a AJAX call to a REST endpoint that knows how to authenticate users using a username/password mechanism.
Login Service
The REST endpoint is very simple and uses the PicketLink API to authenticate users, as follows:
org.jboss.jdf.example.ticketmonster.security.rest.LoginService
@Inject
private Identity identity;
@Inject
private DefaultLoginCredentials credentials;
@POST
@Produces(MediaType.APPLICATION_JSON)
public SecurityContext login(DefaultLoginCredentials credential) {
if (!this.identity.isLoggedIn()) {
this.credentials.setUserId(credential.getUserId());
this.credentials.setPassword(credential.getPassword());
this.identity.login();
}
Account account = this.identity.getAccount();
if (account == null) {
account = new User();
}
return Response.ok().entity(account).type(MediaType.APPLICATION_JSON_TYPE).build();
}
PicketLink provides a core component called org.picketlink.Identity. If you have ever used JBoss Seam Security, you find it very familiar. Through this component you're able to authenticate your users using their credentials, check if the user is authenticated or even get the authenticated user information loaded from the underlying identity store.
In this case, we're using a simple username/password authentication. Providing the credentials using a request-scoped org.picketlink.credential.DefaultLoginCredentials instance and invoking the login method on the Identity bean.
If the authentication is successful we return an user instance populated with some information loaded from the IDM.
Logout Service
To logout an user you only need to invoke the logout method on the Identity bean.
org.jboss.jdf.example.ticketmonster.security.rest.LogoutService
@Inject
private Identity identity;
@POST
public void logout() {
if (this.identity.isLoggedIn()) {
this.identity.logout();
}
}
In this example we're using a REST endpoint because most of TicketMonster pages are JavaScript/AJAX based. You can also use the Identity bean directly from JSF pages to perform the logout. You can use a EL such as:
Authorization
Most of TicketMonster functionalities are for public access. But some of them require the user to be authenticated or have a specific role in order to access them.
Role-based Authorization Filter
To restrict access to the URIs there is a filter that intercepts every and single request to perform some authorization checks. This is a very simple filter, that uses the org.jboss.jdf.example.ticketmonster.security.AuthorizationManager to check if the user is allowed to access a specific URI.
org.jboss.jdf.example.ticketmonster.security.RoleBasedAuthorizationFilter
@Inject
private Instance<Identity> identity;
@Inject
private Instance<IdentityManager> identityManager;
@Inject
private AuthorizationManager authorizationManager;
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException,
ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
try {
if (this.authorizationManager.isAllowed(httpRequest)) {
performAuthorizedRequest(chain, httpRequest, httpResponse);
} else {
handleUnauthorizedRequest(httpRequest, httpResponse);
}
} catch (AccessDeniedException ade) {
handleUnauthorizedRequest(httpRequest, httpResponse);
} catch (Exception e) {
if (AccessDeniedException.class.isInstance(e.getCause())) {
handleUnauthorizedRequest(httpRequest, httpResponse);
} else {
httpResponse.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
}
}
}
Annotation-based Authorization
As discussed before, we need to protect the REST endpoints from
unauthorized access
.
In this example, we're using a simple security annotation to restrict method invocations only for authenticated users
.
Most of this functionality is provided by Apache Deltaspike Security module.
org.jboss.jdf.example.ticketmonster.rest.BookingService
@Consumes(MediaType.APPLICATION_JSON)
@UserLoggedIn
public Response createBooking(BookingRequest bookingRequest) {
...
}
As you noticed, the method is above is annotated with the @UserLoggedIn security annotation. Only authenticated users should be allowed to invoke this method.
org.jboss.jdf.example.ticketmonster.security.UserLoggedIn
@Target(value={TYPE,METHOD})
@Retention(value=java.lang.annotation.RetentionPolicy.RUNTIME)
@SecurityBindingType
@Documented
public @interface UserLoggedIn {
}
In order to associate the annotation above with a specific authorization logic, we need to annotate it with the @SecurityBindingType. Now we can provide the authorization logic in the following way:
org.jboss.jdf.example.ticketmonster.security.AuthorizationManager
@Secures
@UserLoggedIn
public boolean isUserLoggedIn(Identity identity) {
return identity.isLoggedIn();
}
Note that the method above was annotated with the @Secures and also the @UserLoggedIn. This tells PicketLink that if a method is annotated with the @UserLoggedIn it should first perform the authorization logic above. If this method returns false,
a org.apache.deltaspike.security.api.authorization.AccessDeniedException is thrown.
Using EL in JSF pages for Authorization
The org.picketlink.Identity is annotated with @Named. This means that you can easily access it from your JSF pages using EL. The example bellow shows how to check if an user is authenticated before displaying some content:
/src/main/webapp/resources/scaffold/pageTemplate.xhtml
<ui:fragment rendered="#{identity.loggedIn}">
<li><a href="#">| User: <span id="userLoggedInName" style="color:white;">#{identity.account.firstName} #{identity.account.lastName}</span></a></li>
<li><a href="#" onclick="performLogout();">| Logout |</a></li>
</ui:fragment>
You can also check for user roles or groups.
org.jboss.jdf.example.ticketmonster.security.AuthorizationManager
@Inject
private Instance<Identity> identity;
@Inject
private Instance<IdentityManager> identityManager;
@Inject
private Instance<RelationshipManager> relationshipManager;
public boolean isAdmin() {
Identity identity = getIdentity();
if (isUserLoggedIn(identity)) {
IdentityManager identityManager = getIdentityManager();
RelationshipManager relationshipManager = getRelationshipManager();
return BasicModel.hasRole(relationshipManager, identity.getAccount(), BasicModel.getRole(identityManager, "Administrator"));
}
return false;
}
Now you can use this method in your pages:
/src/main/webapp/resources/scaffold/pageTemplate.xhtml
<ui:fragment rendered="#{authorizationManager.admin}">
<li>
<h:outputLink value="#{request.contextPath}/admin">
<h:outputText value="Administration"/>
</h:outputLink>
</li>
</ui:fragment>
Identity Management
PicketLink also provides some Identity Management features that allows you to easily manage users, roles, groups, credentials and relationships. These features are provided by PicketLink IDM which provides a rich API for Identity Management.
User Self-Registration
The self-registration functionality is provided by a REST endpoint. Basically, this endpoint uses the org.picketlink.idm.IdentityManager and org.picketlink.idm.RelationshipManager to store user information and relationships.
org.jboss.jdf.example.ticketmonster.security.rest.SelfRegistrationService
@Inject
private IdentityManager identityManager;
@Inject
private RelationshipManager relationshipManager;
@Inject
private LoginService loginService;
@POST
@Produces(MediaType.APPLICATION_JSON)
public SecurityContext register(RegistrationRequest request) {
Map<String, Object> response = new HashMap<String, Object>();
if (!request.getPassword().equals(request.getPasswordConfirmation())) {
response.put(MESSAGE_RESPONSE_PARAMETER, "Password mismatch.");
} else {
try {
// if there is no user with the provided e-mail, perform registration
if (BasicModel.getUser(this.identityManager, request.getEmail()) == null) {
performRegistration(request);
// if the registration was successful, we perform a silent authentication.
return performSilentAuthentication(request);
} else {
response.put(MESSAGE_RESPONSE_PARAMETER, "This username is already in use. Try another one.");
}
} catch (IdentityManagementException ime) {
response.put(MESSAGE_RESPONSE_PARAMETER, "Oops ! Registration failed, try it later.");
}
}
return Response.ok().entity(response).type(MediaType.APPLICATION_JSON_TYPE).build();
}
private void performRegistration(RegistrationRequest request) {
User newUser = new User(request.getEmail());
newUser.setFirstName(request.getFirstName());
newUser.setLastName(request.getLastName());
this.identityManager.add(newUser);
Password password = new Password(request.getPassword());
this.identityManager.updateCredential(newUser, password);
Role userRole = BasicModel.getRole(this.identityManager, "User");
BasicModel.grantRole(this.relationshipManager, newUser, userRole);
Group userGroup = BasicModel.getGroup(this.identityManager, "Users");
BasicModel.addToGroup(this.relationshipManager, newUser, userGroup);
}
Initializing the Identity Store during startup
In order to create the default admin user and some roles and groups, we use a bean to initialize the configured identity stores (in our case a JPA-based store).
org.jboss.jdf.example.ticketmonster.security.IdentityManagementInitializer
@Inject
private PartitionManager partitionManager;
@PostConstruct
public void initialize() {
IdentityManager identityManager = this.partitionManager.createIdentityManager();
User admin = new User("admin@ticketmonster.org");
admin.setFirstName("Almight");
admin.setLastName("Administrator");
// let's store the admin user
identityManager.add(admin);
Password password = new Password("letmein!");
// updates the admin password
identityManager.updateCredential(admin, password);
Role adminRole = new Role("Administrator");
// stores the admin role
identityManager.add(adminRole);
Group adminGroup = new Group("Administrators");
// stores the admin group
identityManager.add(adminGroup);
RelationshipManager relationshipManager = this.partitionManager.createRelationshipManager();
// grants to the admin user the admin role
BasicModel.grantRole(relationshipManager, admin, adminRole);
// add the admin user to the admin group
BasicModel.addToGroup(relationshipManager, admin, adminGroup);
Role userRole = new Role("User");
identityManager.add(userRole);
Group usersGroup = new Group("Users");
identityManager.add(usersGroup);
}
This allows you to login as administrator providing the following credentials: