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 either not logged in or who lack required roles. Roles are defined through the @RestrictedAccess
annotation in one of the following two ways.
Simple roles are roles that can be directly mapped to Strings (the String value being the role name). Simple roles are defined by defining an array of Strings in the roles
parameter of @RestrictedAccess
. Two simple roles are equivalent if they have the same name.
Here is an example usage of @RestrictedAccess
with two simple roles, "user" and "admin":
@RestrictedAccess(roles = { "user", "admin" })
Conceptually, a provided can be used to implement a more complex security system. In practice, a provided role is some concrete type that implements the Role
interface and overrides Object.equals(Object)
. Provided roles are declared on a resource by creating a RequiredRolesProvider
that produces these roles and assigning the type to the providers
parameter of @RestrictedAccess
.
Here is an sample of a RequireRolesProvider
and its usage with @RestrictedAccess
. This example defines equivalent roles to the above example using simple roles.
@Dependent![]()
public class AdminRolesProvider implements RequiredRolesProvider {
@Override
public Set<Role> getRoles() {
return new HashSet<Role>(Arrays.asList(
new RoleImpl("user"),![]()
new RoleImpl("admin")
));
}
}
@RestrictedAccess(providers = { AdminRolesProvider.class })
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();
}
If a RequiredRolesProvider
is used on an RPC interface, the provider type must be located in a shared package. Security checks for RPCs are performed on the client and the server, so placing the type in a client- or server-only package will result in run-time errors.
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;
}
If you do enable the Errai Security cookie, it is possible to use a form-based login from outside your GWT/Errai app. The errai-security-server
jar contains a servlet filter for encoding the currently authenticated user as a cookie in the http response. Here are the steps for setting this up:
org.picketlink.authentication.web.AuthenticationFilter
servlet-filter. Otherwise, you will need to implement one yourself that authenticates the user by calling AuthenticationService.login(String, String)
method.Add this filter-mapping for setting the Errai Security user cookie:
<filter-mapping>
<filter-name>ErraiUserCookieFilter</filter-name>
<url-mapping>/gwt-host-page.html</url-mapping>
</filter-mapping>
The mapped URL should be that of your GWT Host Page.
If this filter maps to the same URL as the filter for authentication, this filter must come after the authentication filter or else it will set the cookie before the user has logged in.
errai.security.user_cookie_enabled=true
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
.
Keycloak is is a new project that provides integrated SSO and IDM for browser apps and RESTful web services. By using Keycloak it is possible to outsource the responsibility of authentication and account management from your application entirely. Errai Security provides an optional errai-security-keycloak
jar that provides an implementation of the AuthenticationService
that works with Keycloak.
From the perspective of a visitor, here is what happens when she attempts to log in:
Behind the scenes, when the visitor successfully submits credentials she is redirected back to the web app with a Keycloak Access Token, which contains information that is configurable from within Keycloak. A servlet filter is used to extract the token from the request and assign it to the AuthenticationService
implementation. At this point the User is now logged in to your application.
This demo can be configured to work with Keycloak in just a few simple steps as outlined in the README file!
To start from scratch and add Keycloak integration to your application:
Select the Clients tab and click Create, then fill in the following to add the client application to this realm:
Client ID
: the name of your client application (i.e. errai-security-demo)Access Type
: publicRedirect URI
: the url of your application (i.e. http://localhost:8080/[your-application]/*
)After saving your application, choose the new application in the menu and make sure the following are set:
keycloak.json
and copy the contents in your WEB-INF/keycloak.json
file.Click on Users on the side-panel to add a user:
Username
, Email
, First Name
, and Last Name
with any values.at least one
role to the Assigned Roles
for your application (scroll down to Application Roles
and select your application to do this).errai-security-keycloak
jar to your project and make sure it’s being deployed to the server. In maven, the dependency is org.jboss.errai:errai-security-keycloak
.Configure the ErraiUserCookieFilter
in your web.xml
. All that is necessary is adding a filter-mapping for your GWT host page like so:
<filter-mapping>
<filter-name>ErraiUserCookieFilter</filter-name>
<url-pattern>/index.html</url-pattern>
</filter-mapping>
Configure the ErraiLoginRedirectFilter
in your web.xml
.
Create a filter-mapping of this filter onto a path that will act as a url to the Keycloak login page. For example, if your deployed app is called my-app
and you wanted <server-uri>/my-app/app-login
as your login url then you would add the following:
<filter-mapping>
<filter-name>ErraiLoginRedirectFilter</filter-name>
<url-pattern>/app-login</url-pattern>
</filter-mapping>
Add a security-constraint to login url. This is what actually causes the redirection to Keycloak. All the filter does is redirect back to your app (which happens after the login completes). For the previous example, the constraint would look like this:
<security-constraint>
<web-resource-collection>
<web-resource-name>Login</web-resource-name>
<url-pattern>/app-login</url-pattern>
</web-resource-collection>
<auth-constraint>
<role-name>*</role-name>
</auth-constraint>
</security-constraint>
Optionally configure the URL that the ErraiLoginRedirectFilter
redirects to. You can do this with the redirectLocation
param, which takes a path relative to the app context:
<filter>
<filter-name>ErraiLoginRedirectFilter</filter-name>
<init-param>
<param-name>redirectLocation</param-name>
<param-value>/index.jsp</param-value>
</init-param>
</filter>
Set the login method to use Keycloak in you web.xml
:
<login-config>
<auth-method>KEYCLOAK</auth-method>
<realm-name>[your-realm-name]</realm-name>
</login-config>
Add roles available to users in your application to the web.xml
. Here is an example declaration of a "user" role:
<security-role>
<role-name>user</role-name>
</security-role>
With this configuration all users must have at least a single role, or else they will not be redirected propertly. Unfortunately, there is no way to define a security-constraint that only requires authentication. The simplest solution is to add a default role to your realm.