JBoss.orgCommunity Documentation
Errai Security provides a lightweight security API for declaring RPC services and client-side UI elements which require authentication or authorization.
Use the Errai Forge Addon Add Errai Features command and select Errai Security to follow along with this section.
Checkout the Manual Setup Section for instructions on how to manually add Errai Security to your project.
Errai Security provides two main concepts:
Users
Roles
By default the server-side Errai Security module uses PicketLink for authentication. Later on we will explain how to use an alternative backend.
The simplest way to begin experimenting with Errai Security is to add Users and Roles to PicketLink programmatically. Here is some sample server-side code from the Errai Security Demo.
@Singleton
@Startup
public class PicketLinkDefaultUsers {
@Inject
private PartitionManager partitionManager;![]()
/**
* <p>Loads some users during the first construction.</p>
*/
@PostConstruct
public void create() {
final IdentityManager identityManager = partitionManager.createIdentityManager();
final RelationshipManager relationshipManager = partitionManager.createRelationshipManager();
User john = new User("john");
john.setEmail("john@doe.com");
john.setFirstName("John");
john.setLastName("Doe");
User hacker = new User("hacker");
hacker.setEmail("hacker@illegal.ru");
hacker.setFirstName("Hacker");
hacker.setLastName("anonymous");
identityManager.add(john);![]()
identityManager.add(hacker);
final Password defaultPassword = new Password("123");
identityManager.updateCredential(john, defaultPassword);
identityManager.updateCredential(hacker, defaultPassword);
Role roleDeveloper = new Role("simple");
Role roleAdmin = new Role("admin");
identityManager.add(roleDeveloper);
identityManager.add(roleAdmin);
relationshipManager.add(new Grant(john, roleDeveloper));![]()
relationshipManager.add(new Grant(john, roleAdmin));
}
}
Here are the important things that are happening here:
PicketLink uses the concept of partitions, which are sections that can contain different users and roles. What we really need to make users and roles are the | |
Here we add are new users to the | |
The |
Once you’ve created some users and roles, you’re ready to write some client-side code. Authentication is performed with the org.jboss.errai.security.shared.service.AuthenticationService
via Errai RPC.
Here is some sample code involving the user john from the previous Security Demo excerpt.
Injecting the Caller<AuthenticationService>
:
@Inject Caller<AuthenticationService> authServiceCaller;
Logging in:
authServiceCaller.call(new RemoteCallback<User>() {
@Override
public void callback(User user) {
// handle successful login
}
}, new ErrorCallback<Message>() {
@Override
public boolean error(Message message, Throwable t) {
if (t instanceof AuthenticationException) {
// handle authentication failure
}
// Returning true causes the error to propogate to top-level handlers
return true;
}
}).login("john", "123");
Getting the currently authenticated User:
authServiceCaller.call(new RemoteCallback<User>() {
@Override
public void callback(User user) {
if (!user.equals(User.ANONYMOUS)) {
// Do something because we're logged in.
}
else {
// Do something else because we're not logged in.
}
}
}).getUser();
Logging out:
authServiceCaller.call().logout();
Client-side interceptors are used for caching so that generally only calls to login
and logout
must be sent over the wire. The cache is automatically invalidated when a service throws an UnauthenticatedException
, but it can also be invalidated manually via the SecurityContext
.
The annotation @RestrictedAccess
is the only annotation necessary to secure a resource or UI element. In general, @RestrictedAccess
blocks a resource from users who are not logged in; if an array of roles are passed in, users without the declared roles are prevented access to the annotated resource. Below we will explain how different resources are blocked from unauthorized users.
To secure an Errai RPC service, simply annotate the RPC interface (either the entire type or just a method) with one of the security annotations.
For example:
All methods on this interface require an authenticated user to access:
@Remote
@RestrictedAccess
public interface UserOnlyStuff {
public void someMethod();
public void otherMethod();
}
Here the first method requires an authenticated user, and the second requires a user with the admin role:
@Remote
public interface MixedService {
@RestrictedAccess
public void userService();
@RestrictedAccess(roles = {"admin"})
public void adminService();
}
When access to a secured RPC service is denied an UnauthenticatedException
or UnauthorizedException
is thrown. This error is then transmitted back to the client, where it can be caught with an ErrorCallback
(provided when the RPC is invoked).
Here is how we would invoke the previous MixedService
example with error handling:
MessageBuilder.createCall(new RemoteCallback<Void>() {
@Override
public void callback(Void response) {
// ...
}
}, new ErrorCallback<Message>() {![]()
@Override
public boolean error(Message message, Throwable t) {
if (t instanceof UnauthenticatedException) {
// User is not logged in.
return false;
}
else if (t instanceof UnauthorizedException) {
// User is logged in but lacked sufficient roles.
return false;
}
else {
// Some other error has happened. Let it propogate.
return true;
}
}
}, MixedService.class).adminService();
This |
Errai Security provides a default global Bus RPC handler that catches any thrown UnauthenticatedException
or UnauthorizedException
and navigates to the page with the LoginPage
or SecurityError
role respectively.
JAX-RS RPCs are secured exactly as bus RPCs. Here is the first example from the previous section, but converted to use JAX-RS instead of the Errai Bus.
@Path("/rest-endpoint")
@RestrictedAccess
public interface UserOnlyStuff {
@Path("/some-method")
@GET
public void someMethod();
@Path("/other-method")
@GET
public void otherMethod();
}
There are two important differences when calling a secured JAX-RS RPC (in contrast to an Errai Bus RPC):
RestErrorCallback
(an interface extending ErrorCallback<Request>
).Because there is no global error-handling, you should always pass a RestErrorCallback
when using a JAX-RS RPC. Errai provides the DefaultRestSecurityErrorCallback
that provides the same default behaviour as the DefaultBusSecurityErrorCallback
mentioned above. It can also optionally wrap a provided callback as demonstrated below:
Injecting a callback Instance
:
@Inject
private Instance<DefaultRestSecurityErrorCallback> defaultCallbackInstance;
Wrapping a custom callback in a default callback:
void callSomeService() {
userOnlyStuffService.call(new RemoteCallback<Void>() {
@Override
public void callback(Void response) {
// Handle success...
}
}, defaultCallbackInstance.get()
.setWrappedErrorCallback(new RestErrorCallback() {
@Override
public boolean error(Request request, Throwable t) {
// Handle error...
// Returning true means the default navigation behaviour will occur
return true;
}
}
)).someMethod();
}
Using the default callback without a wrapped callback:
void callSomeService() {
userOnlyStuffService.call(new RemoteCallback<Void>() {
@Override
public void callback(Void response) {
// Handle success...
}
}, defaultCallbackInstance.get()).someMethod();
}
Any class annotated with @Page
can also be marked with @RestrictedAccess
. By doing so, users will be prevented from navigating to the given page if they are not logged in or lack authorization.
Here are two simple examples:
This page is only for logged in users:
@Page
@RestrictedAccess
public class UserProfilePage extends SimplePanel {
@Inject private Caller<AuthenticationService> authServiceCaller;
private User user;
@PageShowing
private void setupPage() {
authServiceCaller.call(new RemoteCallback<User>() {
@Override
public void callback(User response) {
// We don't have to check if this is a valid user, since the page requires authentication.
user = response;
// do setup...
}
}).getUser();
}
}
This page requires the user and admin roles:
@Page
@RestrictedAccess(roles = {"admin", "user"})
public class AdminManagementPage extends SimplePanel {
}
When a user is denied access to a page they will be redirected to a LoginPage (@Page(role = LoginPage.class))
or SecurityError (@Page(role = SecurityError.class))
page. To direct a user to the page they were trying to reach after successful login, @Inject
the SecurityContext
and invoke the navigateBackOrHome
method.
Security checks performed before page navigation do not use any RPC calls, but are instead performed from a cached (in-memory) instance of the org.jboss.errai.security.shared.api.identity.User
. This prevents the possibility of lengthy delays between page navigation while waiting for RPC return values.
But the drawback is that any attempts to navigate to a secured @Page
before the cache is populated will result in redirection to the LoginPage
— even if the user is in fact logged in.
In practice, this is only likely to happen if a user starts an Errai app with a URL to a secure page while still logged in on the server from a previous session.
One option offered by Errai is to persist the org.jboss.errai.security.shared.api.identity.User
object in a cookie. This can be done by adding the following to ErraiApp.properties
:
errai.security.user_cookie_enabled=true
With this option enabled the User
will be persisted in a browser cookie, which is loaded quickly enough to avoid the described navigation issue. This feature can also be used to allow an application to work offline, or allow the server to log in a user on an initial page request.
The errai.security.user_cookie_enabled=true
setting causes the User
to be stored in plain text. That includes the following information:
If you do not wish to use this feature you will likely want to handle this case in the @PageShowing
method of your LoginPage
. Here is an outline of what you might want to do:
@Page(role = LoginPage.class)
@Templated
public class ExampleLoginPage extends Composite {
@Inject
private SecurityContext securityContext;
@Inject
private Caller<AuthenticationService> authService;
@Inject
@DataField
private Label status;
@PageShowing
public void checkForPendingCache() {
// Check if cache is invalid.
if (!securityContext.isUserCacheValid()) {
// Update the status.
status.setText("loading...");
// Force cache to update by calling getUser
authService.call(new RemoteCallback<User> {
@Override
public void callback(User user) {
/* An interceptor will have updated the cache by now.
So check if we are logged in and redirect if necessary.
*/
if (!user.equals(User.ANONYMOUS)) {
/* This is a special transition that takes us back to
a secure page from which we were redirected. */
securityContext.navigateBackOrHome();
}
else {
status.setText("You are not logged in.");
}
}
}).getUser();
}
}
}
Errai Security annotations can also be used to hide Errai UI template fields. When a user is not logged in or lacks required roles the annotated field will have the CSS class "errai-restricted-access-style" added to it. By defining this style (for example with visibility: none
) you can hide or otherwise modify the display of the element for unautorized users.
Here is an example of an Errai UI templated class using this feature:
@Templated
public class NavBar extends Composite {
@Inject
@DataField
@RestrictedAccess
private Button logoutButton;
@Inject
@DataField
@RestrictedAccess(roles = {"admin"})
private Button dropAllTablesButton;
}
All Errai Security authentication is implemented with Errai Remote Procedure Calls to the AuthenticationService
. A default implementation of this interface using PicketLink is provided in the errai-security-picketlink
jar. But it is possible to use a different sever-side security framework by providing your own custom implementation of AuthenticationService
and annotating it with @Service
. In that case your project should not depend on errai-security-picketlink
.
If you do enable the Errai Security cookie, it is possible to use a form-based login from outside your GWT/Errai app. Here are the steps required:
AuthenticationService
. If a user’s login request is successful, use the org.jboss.errai.security.shared.api.UserCookieEncoder
to create a cookie.errai.security.user_cookie_enabled=true