JBoss.orgCommunity Documentation
While the IDM module of PicketLink provides authentication features, for common use cases involving standard username and password based authentication in a Java EE environment, PicketLink provides a more streamlined method of authentication. Please refer to Chapter 2, Authentication for more information.
PicketLink IDM provides an authentication subsystem that allows user credentials to be validated thereby confirming
that an authenticating user is who they claim to be. The IdentityManager
interface provides a single
method for performing credential validation, as follows:
void validateCredentials(Credentials credentials);
The validateCredentials()
method accepts a single Credentials
parameter, which should
contain all of the state required to determine who is attempting to authenticate, and the credential (such as a
password, certificate, etc) that they are authenticating with. Let's take a look at the Credentials
interface:
public interface Credentials {
public enum Status {
UNVALIDATED, IN_PROGRESS, INVALID, VALID, EXPIRED
};
Account getValidatedAccount();
Status getStatus();
void invalidate();
}
The Status
enum defines the following values, which reflect the various credential states:
UNVALIDATED
- The credential is yet to be validated.
IN_PROGRESS
- The credential is in the process of being validated.
INVALID
- The credential has been validated unsuccessfully
VALID
- The credential has been validated successfully
EXPIRED
- The credential has expired
getValidatedAccount()
- If the credential was successfully validated, this method returns the
Account
object representing the validated user.
getStatus()
- Returns the current status of the credential, i.e. one of the above enum values.
invalidate()
- Invalidate the credential. Implementations of Credential
should use
this method to clean up internal credential state.
Let's take a look at a concrete example - UsernamePasswordCredentials
is a Credentials
implementation that supports traditional username/password-based authentication:
public class UsernamePasswordCredentials extends AbstractBaseCredentials {
private String username;
private Password password;
public UsernamePasswordCredentials() { }
public UsernamePasswordCredentials(String userName, Password password) {
this.username = userName;
this.password = password;
}
public String getUsername() {
return username;
}
public UsernamePasswordCredentials setUsername(String username) {
this.username = username;
return this;
}
public Password getPassword() {
return password;
}
public UsernamePasswordCredentials setPassword(Password password) {
this.password = password;
return this;
}
@Override
public void invalidate() {
setStatus(Status.INVALID);
password.clear();
}
}
The first thing we may notice about the above code is that the UsernamePasswordCredentials
class extends
AbstractBaseCredentials
. This abstract base class implements the basic functionality required by the
Credentials
interface. Next, we can see that two fields are defined; username
and password
.
These fields are used to hold the username and password state, and can be set either via the constructor,
or by their associated setter methods. Finally, we can also see that the invalidate()
method sets the status to
INVALID
, and also clears the password value.
Let's take a look at an example of the above classes in action. The following code demonstrates how we would authenticate a user with a username of "john" and a password of "abcde":
Credentials creds = new UsernamePasswordCredentials("john",
new Password("abcde"));
identityManager.validate(creds);
if (Status.VALID.equals(creds.getStatus())) {
// authentication was successful
}
We can also test if the credentials that were provided have expired (if an expiry date was set). In this case we might redirect the user to a form where they can enter a new password.
Credentials creds = new UsernamePasswordCredentials("john",
new Password("abcde"));
identityManager.validate(creds);
if (Status.EXPIRED.equals(creds.getStatus())) {
// password has expired, redirect the user to a password change screen
}
Updating user credentials is even easier than validating them. The IdentityManager
interface provides the following
two methods for updating credentials:
void updateCredential(Account account, Object credential);
void updateCredential(Account account, Object credential, Date effectiveDate, Date expiryDate);
Both of these methods essentially do the same thing; they update a credential value for a specified Account
.
The second overloaded method however also accepts effectiveDate
and expiryDate
parameters, which allow some temporal control over when the credential will be valid. Use cases for this feature
include implementing a strict password expiry policy (by providing an expiry date), or creating a new account that might not
become active until a date in the future (by providing an effective date). Invoking the first overloaded method will
store the credential with an effective date of the current date and time, and no expiry date.
One important point to note is that the credential
parameter is of type java.lang.Object
. Since
credentials can come in all shapes and sizes (and may even be defined by third party libraries), there is no common base
interface for credential implementations to extend. To support this type of flexibility in an extensible way, PicketLink
provides an SPI that allows custom credential handlers to be configured that override or extend the default credential
handling logic. Please see the next section for more information on how this SPI may be used.
Let's take a look at a couple of examples. Here's some code demonstrating how a password can be assigned to user "jsmith":
User user = BasicModel.getUser(identityManager, "jsmith");
identityManager.updateCredential(user, new Password("abcd1234"));
This example creates a digest and assigns it to user "jdoe":
User user = BasicModel.getUser(identityManager, "jdoe");
Digest digest = new Digest();
digest.setRealm("default");
digest.setUsername(user.getLoginName());
digest.setPassword("abcd1234");
identityManager.updateCredential(user, digest);
For IdentityStore
implementations that support multiple credential types, PicketLink provides
an optional SPI to allow the default credential handling logic to be easily customized and extended. To get
a better picture of the overall workings of the Credential Handler SPI, let's take a look at the sequence
of events during the credential validation process when validating a username and password against
JPAIdentityStore
:
1 -
The user (or some other code) first invokes the validateCredentials()
method on IdentityManager
,
passing in the Credentials
instance to validate.
1.1 -
After looking up the correct IdentityStore
(i.e. the one that has been configured to validate credentials)
the IdentityManager
invokes the store's validateCredentials()
method, passing in the
IdentityContext
and the credentials to validate.
1.1.1 -
In JPAIdentityStore
's implementation of the validateCredentials()
method, the
IdentityContext
is used to look up the CredentialHandler
implementation that has been
configured to process validation requests for usernames and passwords, which is then stored in a local variable
called handler
.
1.1.2 -
The validate()
method is invoked on the CredentialHandler
, passing in the security context,
the credentials value and a reference back to the identity store. The reference to the identity store is important
as the credential handler may require it to invoke certain methods upon the store to validate the credentials.
The CredentialHandler
interface declares three methods, as follows:
public interface CredentialHandler {
void setup(IdentityStore<?> identityStore);
void validate(IdentityContext context, Credentials credentials,
IdentityStore<?> identityStore);
void update(IdentityContext context, Account account, Object credential,
IdentityStore<?> identityStore, Date effectiveDate, Date expiryDate);
}
The setup()
method is called once, when the CredentialHandler
instance is first created.
Credential handler instantiation is controlled by the CredentialHandlerFactory
, which creates a single
instance of each CredentialHandler
implementation to service all credential requests for that handler.
Each CredentialHandler
implementation must declare the types of credentials that it is capable of supporting,
which is done by annotating the implementation class with the @SupportsCredentials
annotation like so:
@SupportsCredentials(
credentialClass = { UsernamePasswordCredentials.class, Password.class },
credentialStorage = EncodedPasswordStorage.class
)
public class PasswordCredentialHandler implements CredentialHandler {
Since the validate()
and update()
methods receive different parameter types (validate()
takes a Credentials
parameter value while update()
takes an Object
that represents
a single credential value), the @SupportsCredentials
annotation must contain a complete list of all types supported
by that handler.
Similarly, if the IdentityStore
implementation makes use of the credential handler SPI then it also must declare
which credential handlers support that identity store. This is done using the @CredentialHandlers
annotation;
for example, the following code shows how JPAIdentityStore
is configured to be capable of handling credential
requests for usernames and passwords, X509 certificates and digest-based authentication:
@CredentialHandlers({ PasswordCredentialHandler.class,
X509CertificateCredentialHandler.class, DigestCredentialHandler.class })
public class JPAIdentityStore implements IdentityStore<JPAIdentityStoreConfiguration>,
CredentialStore {
For IdentityStore
implementations that support multiple credential types (such as JPAIdentityStore
and FileBasedIdentityStore
), the implementation may choose to also implement the CredentialStore
interface to simplify the interaction between the CredentialHandler
and the IdentityStore
. The
CredentialStore
interface declares methods for storing and retrieving credential values within an identity
store, as follows:
public interface CredentialStore {
void storeCredential(IdentityContext context, Account account,
CredentialStorage storage);
<T extends CredentialStorage> T retrieveCurrentCredential(IdentityContext context,
Account account, Class<T> storageClass);
<T extends CredentialStorage> List<T> retrieveCredentials(IdentityContext context,
Account account, Class<T> storageClass);
}
The CredentialStorage
interface is quite simple and only declares two methods, getEffectiveDate()
and getExpiryDate()
:
public interface CredentialStorage {
@Stored Date getEffectiveDate();
@Stored Date getExpiryDate();
}
The most important thing to note above is the usage of the @Stored
annotation. This annotation is used to
mark the properties of the CredentialStorage
implementation that should be persisted. The only requirement
for any property values that are marked as @Stored
is that they are serializable (i.e. they implement the
java.io.Serializable
interface). The @Stored
annotation may be placed on either the getter
method or the field variable itself. Here's an example of one of a CredentialStorage
implementation that
is built into PicketLink - EncodedPasswordStorage
is used to store a password hash and salt value:
public class EncodedPasswordStorage implements CredentialStorage {
private Date effectiveDate;
private Date expiryDate;
private String encodedHash;
private String salt;
@Override @Stored
public Date getEffectiveDate() {
return effectiveDate;
}
public void setEffectiveDate(Date effectiveDate) {
this.effectiveDate = effectiveDate;
}
@Override @Stored
public Date getExpiryDate() {
return expiryDate;
}
public void setExpiryDate(Date expiryDate) {
this.expiryDate = expiryDate;
}
@Stored
public String getEncodedHash() {
return encodedHash;
}
public void setEncodedHash(String encodedHash) {
this.encodedHash = encodedHash;
}
@Stored
public String getSalt() {
return this.salt;
}
public void setSalt(String salt) {
this.salt = salt;
}
}
PicketLink provides built-in support for the following credential types:
Not all built-in IdentityStore
implementations support all credential types. For example, since the
LDAPIdentityStore
is backed by an LDAP directory server, only password credentials are supported. The
following table lists the built-in IdentityStore
implementations that support each credential type.
Table 4.1. Built-in credential types
Credential type | Description | Supported by |
---|---|---|
org.picketlink.idm.credential.Password | A standard text-based password |
JPAIdentityStore
FileBasedIdentityStore
LDAPIdentityStore
|
org.picketlink.idm.credential.Digest | Used for digest-based authentication |
JPAIdentityStore
FileBasedIdentityStore
|
java.security.cert.X509Certificate | Used for X509 certificate based authentication |
JPAIdentityStore
FileBasedIdentityStore
|
org.picketlink.idm.credential.TOTPCredential | Used for Time-based One-time Password authentication |
JPAIdentityStore
FileBasedIdentityStore
|
The next sections will describe each of these built-in types individually.
This credential handlers supports a username/password based authentication.
Credentials can be updated as follows:
User user = BasicModel.getUser(identityManager, "jsmith");
identityManager.updateCredential(user, new Password("abcd1234"));
In order to validate a credential you need to the following code:
UsernamePasswordCredentials credential = new UsernamePasswordCredentials();
Password password = new Password("abcd1234");
credential.setUsername("jsmith");
credential.setPassword(password);
identityManager.validateCredentials(credential);
if (Status.VALID.equals(credential.getStatus()) {
// successful validation
} else {
// invalid credential
}
The following table describes all configuration parameters supported by this credential handler:
Table 4.2. Configuration Parameters
Parameter | Description | |
---|---|---|
PasswordCredentialHandler.PASSWORD_ENCODER | It must be a org.picketlink.idm.credential.encoder.PasswordEncoder sub-type. It defines how passwords are encoded. Defaults to SHA-512. | |
PasswordCredentialHandler.SECURE_RANDOM_PROVIDER | It must be a org.picketlink.common.random.SecureRandomProvider sub-type. It defines how SecureRandom are created in order to be used to generate random numbers to salt passwords. Defaults to SHA1PRNG with default seed. | |
PasswordCredentialHandler.RENEW_RANDOM_NUMBER_GENERATOR_INTERVAL | To increase the security of generated salted passwords, SecureRandom instances can be renewed from time to time. This option defines the time in milliseconds.
Defaults to disabled, what means that a single instance is used during the life-time of the application. | |
PasswordCredentialHandler.ALGORITHM_RANDOM_NUMBER | It defines the algorithm to be used by the default SecureRandomProvider . Defaults to SHA1PRNG. | |
PasswordCredentialHandler.KEY_LENGTH_RANDOM_NUMBER | It defines the key length of seeds when using the default SecureRandomProvider . Defaults to 0, what means it is disabled. |
This credential handlers supports a DIGEST based authentication.
Credentials can be updated as follows:
User user = BasicModel.getUser(identityManager, "jsmith");
Digest digest = new Digest();
digest.setRealm("PicketLink Realm");
digest.setUsername(user.getLoginName());
digest.setPassword("abcd1234");
identityManager.updateCredential(user, digest);
In order to validate a credential you need to the following code:
User user = BasicModel.getUser(identityManager, "jsmith");
Digest digest = new Digest();
digest.setRealm("PicketLink Realm");
digest.setUsername(user.getLoginName());
digest.setPassword("abcd1234");
digest.setDigest(DigestUtil.calculateA1(user.getLoginName(), digest.getRealm(), digest.getPassword().toCharArray()));
DigestCredentials credential = new DigestCredentials(digestPassword);
identityManager.validateCredentials(credential);
if (Status.VALID.equals(credential.getStatus()) {
// successful validation
} else {
// invalid credential
}
This credential handlers supports a X509 certificates based authentication.
Credentials can be updated as follows:
User user = BasicModel.getUser(identityManager, "jsmith");
java.security.cert.X509Certificate clientCert = // get user certificate
identityManager.updateCredential(user, clientCert);
In order to validate a credential you need to the following code:
User user = BasicModel.getUser(identityManager, "jsmith");
java.security.cert.X509Certificate clientCert = // get user certificate
X509CertificateCredentials credential = new X509CertificateCredentials(clientCert);
identityManager.validateCredentials(credential);
if (Status.VALID.equals(credential.getStatus()) {
// successful validation
} else {
// invalid credential
}
In some cases, you just want to trust the provided certificate and only check the existence of the principal:
User user = BasicModel.getUser(identityManager, "jsmith");
java.security.cert.X509Certificate clientCert = // get user certificate
X509CertificateCredentials credential = new X509CertificateCredentials(clientCert);
// trust the certificate and only check the principal existence
credential.setTrusted(true);
identityManager.validateCredentials(credential);
if (Status.VALID.equals(credential.getStatus()) {
// successful validation
} else {
// invalid credential
}
This credential handlers supports a username/password based authentication.
Credentials can be updated as follows:
User user = BasicModel.getUser(identityManager, "jsmith");
TOTPCredential credential = new TOTPCredential("abcd1234", "my_totp_secret");
identityManager.updateCredential(user, credential);
Users can have multiple TOTP tokens, one for each device. You can provide configure tokens for a specific user device as follows:
User user = BasicModel.getUser(identityManager, "jsmith");
TOTPCredential credential = new TOTPCredential("abcd1234", "my_totp_secret");
credential.setDevice("My Cool Android Phone");
identityManager.updateCredential(user, credential);
In order to validate a credential you need to the following code:
User user = BasicModel.getUser(identityManager, "jsmith");
TOTPCredentials credential = new TOTPCredentials();
credential.setUsername(user.getLoginName());
credential.setPassword(new Password("abcd1234"));
TimeBasedOTP totp = new TimeBasedOTP();
// let's manually generate a token based on the user secret
String token = totp.generate("my_totp_secret");
credential.setToken(token);
// if you want to validate the token for a specific device
// credential.setDevice("My Cool Android Phone");
identityManager.validateCredentials(credential);
if (Status.VALID.equals(credential.getStatus()) {
// successful validation
} else {
// invalid credential
}
The following table describes all configuration parameters supported by this credential handler:
Table 4.3. Configuration Parameters
Parameter | Description | |
---|---|---|
TOTPCredentialHandler.ALGORITHM | The encryption algorithm. Defaults to HmacSHA1. | |
TOTPCredentialHandler.INTERVAL_SECONDS | The number of seconds a token is valid. Defaults to 30 seconds. | |
TOTPCredentialHandler.NUMBER_DIGITS | The number of digits for a token. Defaults to 6 digits. | |
TOTPCredentialHandler.DELAY_WINDOW | the number of previous intervals that should be used to validate tokens. Defaults to 1 interval of 30 seconds. |
The built-in credential handlers can be used without any specific configuration if you're using the basic identity model. But in certain cases you may
want to provide your own Account
type and get it able to be authenticated.
Custom types require some additional steps in order to get their credentials updated/validated. Basically, what you need to do is:
getAccount
method in order to retrieve your custom type instance.
As an example, let's suppose we have a custom Account
type named SalesAgent
and the application must support password-based authentication for this type:
public class UserPasswordCredentialHandler extends PasswordCredentialHandler {
@Override
protected Account getAccount(final IdentityContext context, final UsernamePasswordCredentials credentials) {
IdentityManager identityManager = getIdentityManager(context);
IdentityQuery<SalesAgent> query = identityManager.createIdentityQuery(SalesAgent.class);
query.setParameter(SalesAgent.LOGIN_NAME, credentials.getUsername());
List<SalesAgent> result = query.getResultList();
if (result.isEmpty()) {
return null;
}
return result.get(0);
}
}
If you're using a custom Account
type and trying to authenticate using this type without providing a specific credential handler, the authentication will always fail.