Skip to content

Latest commit

 

History

History
386 lines (239 loc) · 23.1 KB

identityStore.asciidoc

File metadata and controls

386 lines (239 loc) · 23.1 KB

Identity Store

This chapter describes the IdentityStore and IdentityStoreHandler interfaces and contracts.

Introduction

IdentityStore provides an abstraction of an identity store, which is a database or directory (store) of identity information about a population of users that includes an application’s callers. An identity store holds caller names, group membership information, and information sufficient to allow it to validate a caller’s credentials. An identity store may also contain other information, such as globally unique caller identifiers (if different from caller name), or other caller attributes.

Implementations of the IdentityStore interface are used to interact with identity stores to authenticate users (i.e., validate their credentials), and to retrieve caller groups. IdentityStore is roughly analogous to the JAAS LoginModule interface, which is often integrated into Jakarta EE products (albeit in vendor-specific ways). Unlike LoginModule, IdentityStore is intended specifically for Java EE, and provides only credential validation and group retrieval functions (i.e., functions that require interaction with an identity store). An IdentityStore does not collect caller credentials, or manipulate Subjects.

IdentityStore is intended primarily for use by HttpAuthenticationMechanism implementations, but could in theory be used by other types of authentication mechanisms (e.g., a JASPIC ServerAuthModule, or a container’s built-in authentication mechanisms). HttpAuthenticationMechanism implementations are not required to use IdentityStore — they can authenticate users in any manner they choose — but IdentityStore will often be a useful and convenient mechanism.

A significant advantage of using HttpAuthenticationMechanism and IdentityStore over container-provided BASIC or FORM implementations is that it allows an application to control the identity stores it will authenticate against, in a standard, portable way.

An IdentityStore is expected to perform only context- and environment-independent processing (for example, verifying usernames and passwords and returning caller data). It should provide a pure {credentials in, caller data out} function. An IdentityStore should not directly interact with the caller, or attempt to examine request context or application state.

The IdentityStoreHandler interface defines a mechanism for invoking on IdentityStore to validate a user credential. An HttpAuthenticationMechanism (or other caller) should not interact directly with an IdentityStore, but instead invoke the IdentityStoreHandler to validate credentials. The IdentityStoreHandler, in turn, invokes on the IdentityStore. An IdentityStoreHandler can also orchestrate an authentication across multiple IdentityStore instances, returning an aggregated result.

A default IdentityStoreHandler implementation is supplied by the container, but applications can also supply their own implementation. The orchestration behavior of the default IdentityStoreHandler is described in the "Handling Multiple Identity Stores" section below.

Interface and Theory of Operation

The IdentityStore interface defines two methods that are used by the runtime to validate a Credential or obtain caller information:

  • validate(Credential)

  • getCallerGroups(CredentialValidationResult)

An implementation of IdentityStore can choose to handle either or both of these methods, depending on its capabilities and configuration. It indicates which methods it handles through the set of values returned by its validationTypes() method:

  • VALIDATE to indicate that it handles validate()

  • PROVIDE_GROUPS to indicate that it handles getCallerGroups()

  • Both VALIDATE and PROVIDE_GROUPS to indicate that it handles both methods

This method of declaring capabilities was chosen so that an IdentityStore could be written to support both methods, but configured to support just one or the other in any particular deployment.

The full interface is shown below (without default behaviors; signatures only).

public interface IdentityStore {

    enum ValidationType { VALIDATE, PROVIDE_GROUPS }

    CredentialValidationResult validate(Credential credential);

    Set<String> getCallerGroups(CredentialValidationResult validationResult);

    int priority();

    Set<ValidationType> validationTypes();
}

Validating Credentials

The validate() method determines whether a Credential is valid, and, if so, returns information about the user identified by the Credential. It is an optional method that an IdentityStore may choose not to implement.

CredentialValidationResult validate(Credential credential);

The result of validation is returned as a CredentialValidationResult, which provides methods to obtain the resulting status value, and, for successful validations, the ID of the identity store that validated the credential, the caller principal, the caller’s unique ID in the identity store, and the caller’s group memberships, if any. Only the caller principal is required to be present for a successful validation.

The identity store ID, caller DN, and caller unique ID are provided to assist implementations of IdentityStore in cooperating across invocations of validate() and getCallerGroups(). They can be used to ensure that the correct caller’s groups are returned from getCallerGroups() even in environments where caller principal name alone is insufficient to uniquely identify the correct user account.

public class CredentialValidationResult {

    public enum Status { NOT_VALIDATED, INVALID, VALID };

    public Status getStatus();

    public String getIdentityStoreId();

    public CallerPrincipal getCallerPrincipal();

    public String getCallerDn();

    public String getCallerUniqueId();

    public Set<String> getCallerGroups();
}

The defined status values are:

  • VALID: Validation succeeded and the user is authenticated. The caller principal and groups (if any) are available ONLY with this result status.

  • INVALID: Validation failed. The supplied Credential was invalid, or the corresponding user was not found in the user store.

  • NOT_VALIDATED: Validation was not attempted, because the IdentityStore does not handle the supplied Credential type.

The Credential interface is a generic interface capable of representing any kind of token or user credential. An IdentityStore implementation can support multiple concrete Credential types, where a concrete Credential is an implementation of the Credential interface that represents a particular type of credential. It can do so by implementing the validate(Credential) method and testing the type of the Credential that’s passed in. As a convenience, the IdentityStore interface provides a default implementation of validate(Credential) that delegates to a method that can handle the provided Credential type, assuming such a method is implemented by the IdentityStore:

default CredentialValidationResult validate(Credential credential) {
    try {
        return CredentialValidationResult.class.cast(
            MethodHandles.lookup()
                .bind(this, "validate",
                      methodType(CredentialValidationResult.class,
                                 credential.getClass()))
                .invoke(credential));
    } catch (NoSuchMethodException e) {
        return NOT_VALIDATED_RESULT;
    } catch (Throwable e) {
        throw new IllegalStateException(e);
    }
}

So, for example, validate(Credential) would delegate to the following method of ExampleIdentityStore if passed a UsernamePasswordCredential:

public class ExampleIdentityStore implements IdentityStore {

    public CredentialValidationResult validate(
        UsernamePasswordCredential usernamePasswordCredential) {
        // Implementation ...
        return new CredentialValidationResult(...);
    }
}

Retrieving Caller Information

The getCallerGroups() method retrieves the set of groups associated with a validated caller. It is an optional method that an IdentityStore may choose not to implement.

Set<String> getCallerGroups(CredentialValidationResult validationResult);

The getCallerGroups() method supports aggregation of identity stores, where one identity store is used to authenticate users, but one or more other stores are used to retrieve additional groups. In such a scenario, it is necessary to query identity stores without validating the caller’s credential against the stores.

If an IdentityStore supports both validate() and getCallerGroups(), the behavior of both methods should be consistent with respect to groups. That is, for a given user "foo", the set of groups returned when calling validate() to authenticate user "foo" should be the same as the set of groups returned when calling getCallerGroups() for CallerPrincipal "foo". (Assuming no errors occur during either call — this requirement is intended as a normative description of expected behavior; it does not imply that an implementation must "make it right" if errors or other uncontrollable factors cause results to vary between any two calls.)

As a result, it is never necessary to call getCallerGroups() when there is only one IdentityStore, because the same groups are returned by the validate() method.

Note that getCallerGroups() is not intended as a general purpose API for retrieving user groups. It should be called only by an IdentityStoreHandler, in the course of orchestrating a validate() call across multiple identity stores.

Because getCallerGroups() enables its callers to access an external store as a privileged user (i.e., as an LDAP or database user with permission to search the store and retrieve information about arbitrary user accounts), it should be protected against unauthorized access.

Implementors of getCallerGroups() are strongly encouraged to check that the calling context has IdentityStorePermission, as shown below, before proceeding. (The built-in identity stores are REQUIRED to do so, see Annotations and Built-In IdentityStore Beans.)

SecurityManager securityManager = System.getSecurityManager();
if (securityManager != null) {
    securityManager.checkPermission(new IdentityStorePermission("getGroups"));
}

Declaring Capabilities

The IdentityStore interface includes methods for an implementation to declare its capabilities and ordinal priority. An IdentityStore implementation may allow these "capabilities" to be configured, so that an application can determine what a store is used for.

enum ValidationType { VALIDATE, PROVIDE_GROUPS }

Set<ValidationType> DEFAULT_VALIDATION_TYPES = EnumSet.of(VALIDATE, PROVIDE_GROUPS);

default int priority() {
    return 100;
}

default Set<ValidationType> validationTypes() {
    return DEFAULT_VALIDATION_TYPES;
}

The priority() method allows an IdentityStore to be configured with an ordinal number indicating the order in which it should be consulted when multiple IdentityStores are present (more precisely, when multiple enabled CDI Beans with type IdentityStore are available). Lower numbers represent higher priority, so an IdentityStore with a lower priority value is called before an IdentityStore with a higher priority value.

The validationTypes() method returns a Set of enum constants of type ValidationType, indicating the purposes for which an IdentityStore should be used:

  • VALIDATE, to indicate that it handles validate()

  • PROVIDE_GROUPS to indicate that it handles getCallerGroups()

  • Both VALIDATE and PROVIDE_GROUPS to indicate that it handles both methods

An IdentityStore's validation types determine whether the store is used for authentication only (meaning any group data it returns must be ignored), for providing groups only (meaning it’s not used for authentication, but only to obtain group data for a caller that was authenticated by a different IdentityStore), or for both (meaning it’s used for authentication and any group data it returns is used).

This method of declaring capabilities was chosen to enable applications to enable or disable IdentityStore capabilities via configuration.

Handling Multiple Identity Stores

Access to the IdentityStore is abstracted by the IdentityStoreHandler interface, which provides a single method:

public interface IdentityStoreHandler {
    CredentialValidationResult validate(Credential credential);
}

For the caller, the semantics of the validate() method are as described for the IdentityStore method with the same signature.

The purpose of the IdentityStoreHandler is to allow for multiple identity stores to logically act as a single IdentityStore to the HttpAuthenticationMechanism. A compliant implementation of this specification MUST provide a default implementation of the IdentityStoreHandler that is an enabled CDI bean with qualifier @Default, and scope @ApplicationScoped, as defined by the CDI specification.

The validate() method of the default implementation MUST do the following:

  • Call the validate(Credential credential) method on all available IdentityStore beans that declared themselves capable of doing validation, in the order induced by the return value of the getPriority() method of each IdentityStore. (Lower priority values imply a lower order, causing the corresponding validate(Credential credential) method to be called sooner. The calling order is undefined when two IdentityStore implementations return the same value.)

    • If a call to validate() returns a result with status INVALID, remember it, in case no IdentityStore returns a VALID result.

    • If a call to validate() returns a result with status VALID, remember this result and stop calling validate().

  • If all IdentityStore beans have been called but no result was returned with status VALID, then:

    • If a result was previously returned with status INVALID, return that result.

    • Otherwise, return a result with status NOT_VALIDATED.

  • If there is a VALID result:

    • Create an empty set of groups.

    • Add any groups returned in the CredentialValidationResult to the set of groups, if and only if the identity store that returned the VALID result declared the PROVIDE_GROUPS validation type.

    • Call the getCallerGroups() method on all available IdentityStore beans that declared only the PROVIDE_GROUPS validation type, in the order induced by the return value of the getPriority() method of each IdentityStore, passing in the CredentialValidationResult obtained during the previous phase. Add the groups returned by each call to the set of accumulated groups.

  • Return a new CredentialValidationResult with status VALID; the CallerPrincipal, CallerUniqueId, CallerDn, and IdentityStoreId that were returned from the successful validate(); and the accumulated collection of groups.

The default IdentityStoreHandler MUST make all calls to getCallerGroups() in the context of a PrivilegedAction. Other implementations of IdentityStoreHandler are strongly encouraged to do so as well.

The necessary permission grants (i.e., for IdentityStorePermission("getGroups")) should be configured if running with a SecurityManager.

See javadoc for additional information.

State

An IdentityStore is logically stateless. An IdentityStoreHandler should not make any assumptions about the state of an IdentityStore before, during, or after making calls to it. In particular, an IdentityStore store should not be aware of the point its caller has reached in the authentication process, and, even more specifically, an IdentityStore should not keep track of whether a caller is authenticated or not at any given moment in time.

An IdentityStore instance may make use of instance variables; for example, to store configuration data like an LDAP URL, to store actual caller data for in-memory lookup, for the caching, etc.

RememberMeIdentityStore

The RememberMeIdentityStore is a specialized interface that is similar to the standard IdentityStore interface, but is a distinct type (no inheritance relationship) and works differently.

Applications often want to remember logged in callers for extended periods of time — days or weeks — so that callers don’t have to log in every time they visit the application. A RememberMeIdentityStore can be used to:

  • Generate a login token ("remember me token") for a caller

  • Remember the caller associated with the login token

  • Validate the login token when the caller returns, and re-authenticate the caller without the need to provide additional credentials.

If the caller does not have a login token, or if the login token has expired, then the normal authentication process takes place.

public interface RememberMeIdentityStore {

    CredentialValidationResult validate(RememberMeCredential credential);

    String generateLoginToken(CallerPrincipal callerPrincipal, Set<String> groups);

    void removeLoginToken(String token);
}

RememberMeIdentityStore can only be used when an application includes an HttpAuthenticationMechanism or configures one of the built-in ones. The application must specify the RememberMe annotation on the HttpAuthenticationMechanism to configure the RememberMeIdentityStore.

See the description of the RememberMe annotation in Chapter 2, "Authentication Mechanism".

Installation and Configuration

Installation of an IdentityStore depends on the CDI specification. That is, an IdentityStore is considered installed and available for usage when it’s available to the CDI runtime as an enabled Bean. An IdentityStore is assumed to be normal scoped.

It MUST be possible for the definition of an IdentityStore to exist within the application archive. Alternatively such definition MAY also exists outside the application archive, for example in a jar added to the classpath of an application server.

As described above, in the "Declaring Capabilities" section, the IdentityStore interface includes two methods, validationTypes() and priority(), that enable an IdentityStore to declare its capabilities. Those capabilities may be intrinsic — determined by the IdentityStore's implementation — or they may be determined by the IdentityStore's configuration.

Annotations and Built-In IdentityStore Beans

A Jakarta EE container MUST support built-in beans for the following IdentityStore types, to be configured and made available via corresponding annotations:

  • LDAP — Supports caller data that is stored in an external LDAP server. This bean is activated and configured via the @LdapIdentityStoreDefinition annotation.

  • Database — Supports caller data that is stored in an external database accessible via a DataSource bound to JNDI. This bean is activated and configured via the @DatabaseIdentityStoreDefinition annotation.

Each of these beans MUST have the qualifier @Default and the scope @ApplicationScoped, as defined by the CDI specification.

The built-in identity stores MUST support validating UsernamePasswordCredential. They MAY support other credential types, but are NOT REQUIRED to.

The built-in identity stores MUST check whether a SecurityManager is configured, and, if so, check whether the calling context has IdentityStorePermission, as described in Retrieving Caller Information above, before proceeding.

Note that implementations are explicitly NOT REQUIRED to provide an LDAP server or database. The requirement is only to provide IdentityStore implementations that can work with an external LDAP or database server that may be present in the operating environment.

The corresponding annotations are defined as shown in the following sections.

LDAP Annotation

The LdapIdentityStoreDefinition annotation configures an instance of the built-in LDAP identity store. See javadoc for details of the configuration attributes.

@Retention(RUNTIME)
@Target(TYPE)
public @interface LdapIdentityStoreDefinition {

    enum LdapSearchScope { ONE_LEVEL, SUBTREE }

    String url() default "";

    String bindDn() default "";

    String bindDnPassword() default "";

    String callerBaseDn() default "";

    String callerNameAttribute() default "uid";

    String callerSearchBase() default "";

    String callerSearchFilter() default "";

    LdapSearchScope callerSearchScope() default LdapSearchScope.SUBTREE;

    String callerSearchScopeExpression() default "";

    String groupSearchBase() default "";

    String groupSearchFilter() default "";

    LdapSearchScope groupSearchScope() default LdapSearchScope.SUBTREE;

    String groupSearchScopeExpression() default "";

    String groupNameAttribute() default "cn";

    String groupMemberAttribute() default "member";

    String groupMemberOfAttribute() default "memberOf";

    int readTimeout() default 0;

    String readTimeoutExpression() default "";

    int maxResults() default 1000;

    String maxResultsExpression() default "";

    int priority() default 80;

    String priorityExpression() default "";

    ValidationType[] useFor() default {VALIDATE, PROVIDE_GROUPS};

    String useForExpression() default "";

}

Database Annotation

The DatabaseIdentityStoreDefinition annotation configures an instance of the built-in database identity store.

@Retention(RUNTIME)
@Target(TYPE)
public @interface DatabaseIdentityStoreDefinition {

    String dataSourceLookup() default "java:comp/DefaultDataSource";

    String callerQuery() default "";

    String groupsQuery() default "";

    Class<? extends PasswordHash> hashAlgorithm() default Pbkdf2PasswordHash.class;

    String[] hashAlgorithmParameters() default {};

    int priority() default 70;

    String priorityExpression() default "";

    ValidationType[] useFor() default {VALIDATE, PROVIDE_GROUPS};

    String useForExpression() default "";
}

Password hashing/hash verification is provided by an implementation of the PasswordHash interface, which must be made available as a dependent-scoped bean, and is configured by type on the hashAlgorithm() attribute. The specified type may refer to the actual implementation class, or to any type it implements or extends, as long as the specified type implements the PasswordHash interface.

Parameters for the configured PasswordHash can be provided using the hashAlgorithmParameters attribute, and will be passed to the initialize() method of the PasswordHash when the identity store is initialized.

The default hash algorithm, Pbkdf2PasswordHash, is an interface denoting a standard, built-in PasswordHash. All implementations of this specification MUST provide an implementation of the Pbkdf2PasswordHash interface, with configuration and behavior as described by the interface’s javadoc.

See javadoc for further details on PasswordHash and the DatabaseIdentityStoreDefinition annotation.

Relationship to Other Specifications

IdentityStore and IdentityStoreHandler implementations are CDI beans, as defined by [CDI12].