2.5. Multi-Level Authentication
Most systems rely on a single and unique security level to access their services. Once the user authenticates, he can access any service he is authorized regardless if that service provides any private or sensitive data that needs a more strong identity verification of the user. This is usually done by applying RBAC (role-based access control) or any other type of authorization policy (see Chapter 11, Authorization for more examples).
However, a single security level is not enough if you need a more fine-grained access control to your services, specially for those that maintain private and sensitive data such as transaction confirmation, purchases, change password or edit profile information, administrative operations and so forth. In these cases, you may also want to confirm the identity of an authenticated user by asking a different and more strong credential (eg.: TOTOP tokens, pseudo-random numbers sent to a smartphone, etc) before moving forward.
A security level let you determine how strong the identity of an user is or if he is really the user he claims to be. It is very clear that an user logged from inner company network is more trustworthy then someone logged in from the internet. The same applies if the user was authenticated using biometric or a ordinary username and password credential. Considering that, security levels let you determine different levels of security to your application based on the contextual information about an user (eg.: network, client device, etc) or even based on the supported credentials and authentication methods.
PicketLink Multi-Level Authentication support allows you to define, assign levels to your users and protect your services based on different strategies:
-
Contextual-based
-
Credential-based
-
Authenticator-based
By default, PicketLink sets a default level for every single user with a value
1
. Even if the user is not authenticated, he can be assigned with a specific level. Current security level is stored in the Identity
bean and can be obtained as follows:
@Inject private Identity identity; public void accessConfidentialInfo() { Level level = this.identity.getLevel(); if (level.compareTo(getConfidentialLevel()) >= 0)) { // user is allowed to access confidential stuff } }
As you may notice, the code above is returning the security level assigned to an user using
this.identity.getLevel()
. This method returns an instance of Level
that represents the level itself. By default, PicketLink create level instances from the DefaultLevel
type. However, PicketLink also allows you to provide your own security level types and factory, as we will see later along this book.
The default security level is used when none of the level resolvers were able to resolve the Level of the user. You can provide a default security level value by just producing a
Level
using the @DefaultSecurityLevel
annotation qualifier:
@PicketLink @DefaultSecurityLevel @Produces public Level getDefaultSecurityLevel() { return MyDefaultLevel(10); }
User is granted with a security level at two moments:
-
When
identity.getLevel()
is called and security level is not resolved yet (for example for unauthenticated user).Whenidentity.login()
is called. In this case the level will be resolved based on the credential, authenticator or any other contextual level resolver.
Note
One of the most important things you should keep in mind when dealing with security levels is that an user does not need to logout in order to increase his level. This can be achieved any time during user interaction with your application.
The diagram bellow summarizes how users are assigned with a level during the authentication, in other words, when
Identity.login
is invoked.
-
1 - The user invokes the
login()
method of theIdentity
bean. If the user is logged in, PicketLink will always check if the user is really trying to raise his level. If that is not the case, theIdentity
bean will throw aUserAlreadyLoggedInException
. -
1.1 - The
Identity
bean authenticates the user. At this momment, PicketLink will also check if the user is already authenticated. If so, it will check if the user performing the authentication is the same that was previously authenticated. If that is not the case, theIdentity bean
will throw aDifferentUserLoggedInExcpetion
. -
1.2 - If user authentication is successful, the
Identity
bean try to resolve the security level by invoking theresolve
method on eachSecurityLevelResolver
registered by the application. At this moment, the highest security level resolved from the resolvers is assigned to the user. -
2 - The user retrieve the security level from the
Identity
bean.
On the next sections we will take a closer look on how to extend PicketLink in order to resolve levels based on your own requirements and also how to create your own level types.
2.5.1. Security Level Resolver
As previously discussed, security levels are determined by calling all implementations of the
SecurityLevelResolver
interface registered by your application. The highest level of all implementation is then determined as the current level for the user. That is the rule.
SecurityLevelResolver
implementations are just regular CDI beans as follows:
@RequestScoped public class CustomSecurityLevelResolver implements SecurityLevelResolver { @Inject private DefaultLoginCredentials defaultLoginCredentials; @Override public Level resolve() { Object currentCredential = this.defaultLoginCredentials.getCredential(); if (TOTPCredentials.class.isInstance(currentCredential)) { return new DefaultLevel(2); } return null; } }
Resolvers must implement a single
resolve
method where all the logic to determine the proper security level resides. In the example above, your application will determine a level 2
when the user is being authenticated using a TOTP credential. In other words, if he proved his identity using two-factor authentication his level will be raised to 2
.
You can use whatever logic you want to resolve security levels from a resolver. For instance, gather information from the
HttpServletRequest
(if you are in a web application), time-based decisions and so forth. Just remember that only the highest level will be assigned to the user.
However, PicketLink also provides some built-in resolvers to support some very common use cases. In both cases, you can use the
@SecurityLevel
annotation to specify a security level for both authenticators or credentials.
-
Authenticator Security Level Resolver
-
Credential Security Level Resolver
The built-in
Authenticator Security Level Resolver
automatically resolves security levels from the Authenticator
(see Section 2.3.1, “A Basic Authenticator”) used to authenticate an user. That said, if you have created your own authenticators you can just define a security level for them as follows:
@SecurityLevel("3") @PicketLink class MyAuthenticator extends BaseAuthenticator { ... }
The same thing applies to the
Credential Security Level Resolver
. However, in this case, PicketLink will resolve the security level from the credential used to authenticate an user.
@SecurityLevel("4") public class MyCredential { ... }
Whatever you set in
DefaultLoginCredentials.setCredential
during user authentication, PicketLink will try to resolve the security level from the @SecurityLevel
annotation defined on the type of a credential instance.