Skip to main content
Version: 7.2405.x.x LTS

Developing AuthStates

Now that we are familiar with the most important concepts of nevisAuth, we can dive into the actual development.

Prerequisites

When writing an authentication plugin, we strongly recommend starting with a Maven project, generated using the archetype provided in the nevisAuth SDK. This will set the correct dependency versions and scopes. How to generate a Maven project based on the provided archetype is described in the chapter Quickstart.

Two libraries are officially delivered as part of the nevisAuth SDK. nevisauth-authstate-api and nevisauth-commons. The classes contained in nevisauth-authstate-api are always provided in a nevisAuth runtime environment. nevisauth-authstate-api is thus declared with scope provided in Maven projects generated from the archetype. It does not have to be deployed together with the authentication plugin. Unlike the classes in nevisauth-authstate-api, the classes in nevisauth-commons are not mandatory to develop an authentication plugin. But they offer some useful functionality which simplifies development of authentication plugins. They are not necessarily provided in a nevisAuth runtime environment and thus are declared with scope compile in Maven projects generated from the archetype. nevisauth-commons must be deployed together with the authentication plugin.

caution

Do not use any classes in ch.nevis.\* other than the classes contained in the libraries officially delivered as part of the nevisAuth SDK. Other classes and libraries are considered to be internal and are subject to change without further notice.

AuthState Basic Structure

An AuthState is a Java class, which inherits from the abstract class ch.nevis.esauth.auth.engine.AuthState. During nevisAuth start-up, for each configured AuthState the init() method is called. The AuthState properties are passed from the configuration file to the method, which allows the initialization of the AuthState with configured values.

As mentioned in the chapter Service Operations, there are four operations for different processing contexts: authenticate, stepup, unlock and logout. These operations are also reflected in the AuthState super class as Java methods and can be overridden one-by-one. These methods are called from the process() method, which checks which service operation is currently ongoing and then calls the matching implementation. Since, most of the time, an AuthState does not have to differentiate between different operations, it is common practice to override the process() method itself.

The request and response objects constitute the context and are passed as arguments to the AuthState process() method.

The minimal structure of the AuthState looks like this:

public class MyAuthState extends AuthState {

@Override
public void process(AuthRequest request, AuthResponse response) throws AuthStateException {
Context context = new Context(request, response);
// ...
}
}
caution

As described in AuthState Lifecycle, AuthStates are multithreaded. I.e., process() is called for every request with a different thread. This means that process(), authenticate(), stepup(), logout() and unlock() should never override any instance variables, only consume them.

Reading and Manipulating the Context

The request and response objects, which are passed to the process() method, define the context and are used to evaluate whether the user can be authenticated and authorized. By wrapping the incoming request and response objects in a Context object, you will have access to multiple convenience methods:

Context context = new Context(req, res);

Let us assume you want to check whether a correct user name and password were provided by the input arguments. You can access the input arguments through the context by referencing the source inArgs as follows:

public class MyAuthState extends AuthState {
@Override
public void process(AuthRequest request, AuthResponse response) throws AuthStateException {
Context context = new Context(request, response);

String uid = context.getValue(Scope.INARGS, "username");
String pwd = context.getValue(Scope.INARGS, "password");
// ...
}
}

To manipulate the context, e.g., to store data in the session or in the temporary storage, you can use the putValue() method:

public class MyAuthState extends AuthState {
@Override
public void process(AuthRequest request, AuthResponse response) throws AuthStateException {
// ...
context.putValue(Scope.SESSION, "returnUri", "https://www.service-provider.com");
// ...
}
}

AuthState Result

In the Request Processing chapter, we have learned that the AuthState produces results to help the AuthEngine decide which AuthState to process next. This is done by calling setResult() on the AuthResponse object.

Let’s assume our AuthState sets the following results somewhere in the process() method. If the request is successful:

response.setResult("ok");

If it is not successful, it sets:

response.setResult("failed");

That means that two triggering conditions with the name ok and failed must exist in the AuthState configuration and must point to the next AuthState.

Let us revisit our example from the Basic Concepts chapter:

<AuthState name="MyCustomState" class="ch.my.custom.MyAuthState" final="false">
<ResultCond name="ok" next="AuthDone"/>
<ResultCond name="failed" next="AuthError"/>
...
</AuthState>

We see that if the AuthState sets ok as a result, the AuthEngine will decide to pass the request to AuthDone next. If the AuthState sets failed as a result, the AuthEngine will decide to pass the request to AuthError next.

AuthState Initialization

To allow the flexible use of AuthStates and re-use of code, AuthStates can be initialized with properties that are configured in nevisAuth’s configuration file. For example, if you want to have a setup where the name of the input argument is configurable, you need to read the properties specified in the AuthState's configuration. By overriding the init() method of the abstract super class AuthState, you will have access to the configuration properties of that instance of the AuthState.

Let's assume you want to read the following configuration:

<AuthState name="MyCustomState" class="ch.my.custom.MyAuthState" final="false">
<ResultCond name="ok" next="AuthDone"/>
<ResultCond name="failed" next="AuthError"/>
<Response value="AUTH_CONTINUE">
...
</Response>
<property name="usernameKey" value="userid" />
</AuthState>

During start-up of nevisAuth, a new instance of MyAuthState is created and initialized with the property usernameKey.

You can read the config properties that were passed to the init() method, as the following example demonstrates. By wrapping the properties in a Configuration object, you will have access to a set of convenience methods.

private String usernameKey;

@Override
public void init(Properties cfg) throws BadConfigurationException {
Configuration configuration = new Configuration(cfg);
this.usernameKey = configuration.getPropertyAsString("usernameKey", "username");
}

In the above example, the variable from which the user name should be taken is determined. If no usernameKey was configured, the default value username is used. The variable can then be referenced from other methods like process().

info

Note that for every time your AuthState is configured using an <AuthState> element, nevisAuth creates only one instance of your AuthState. This is done during start-up by first instantiating the AuthState and then calling its init() method. This means that instance variables in AuthStates are not thread-safe. nevisAuth does not create a new instance of the AuthState class for each request. It reuses the existing one.

Variable Expressions

To make the AuthState configuration even more flexible, variable expressions can be used. Variable expressions can be written in nevisAuth's own variable expression language or with the Java Unified expression language (JUEL).

For nevisAuth's own variable expression language, the syntax for accessing and possibly filtering or modifying values in nevisAuth is as follows:

${source:name[:filter[:pattern]]}

The source and name define which variable the AuthState loads. The filter and pattern may transform the value of the variable before loading it into memory. Filter and pattern may be undefined, in which case the output of the variable expression is the value of the referenced variable.

A filter may be defined in the form of a regular expression (as defined in Java Regex Patterns). If it is defined, the output of the variable expression will be:

  • An empty string, if the value did not match the regular expression.
  • The whole matching substring of the value, if the regular expression matches, but does not define any groupings.
  • The pattern, if the value matches the regular expression and a pattern is defined.
  • The value of the grouping, if the regular expression matches and defines a grouping.

JUEL is a more powerful tool for reading and manipulating variable values. For more details have a look at the tutorial and the nevisAuth Reference Guide chapter Java EL expressions.

info

For nevisAuth AuthState development it is irrelevant whether nevisAuth's expression language or JUEL is used, because the mechanism for evaluating any of the expressions is the same.

AuthStates that want to allow the evaluation of variable expressions can pass the expression value to the evaluateExpression() method. For example, let's assume an AuthState allows the configuration of a property with the name usernameKey. The same AuthState could be used to fetch the username from the inargs (i.e., incoming POST or GET parameters) or from the notes scope (which is a short-living store for variables that previous AuthStates prepared). The two examples below show the differences in the configuration:

<AuthState name="MyCustomState" class="ch.my.custom.MyAuthState" final="false">
<ResultCond name="ok" next="AuthDone"/>
<ResultCond name="failed" next="AuthError"/>
<property name="usernameKey" value="${inargs:username}" />
</AuthState>
<AuthState name="MyCustomState" class="ch.my.custom.MyAuthState" final="false">
<ResultCond name="ok" next="AuthDone"/>
<ResultCond name="failed" next="AuthError"/>
<property name="usernameKey" value="${notes:username}" />
</AuthState>

The retrieval of the user name value is carried out by evaluating the configured variable expression. The following code shows how this can be achieved. The expression is stored as a field variable during initialization.

private String usernameKey;

@Override
public void init(Properties cfg) throws BadConfigurationException {
Configuration configuration = new Configuration(cfg);
this.usernameKey = configuration.getPropertyAsString("usernameKey", "{inargs:username}");
...
}

In the process() method, you can then evaluate the usernameKey variable and use it to retrieve the username.

@Override
public void process(AuthRequest req, AuthResponse res) {
Context context = new Context(req, res);

String uid = context.evaluateExpression(usernameKey);
// ...
}

User Authentication

To mark a request as authenticated, the AuthState flow must reach an AUTH_DONE state at some point. When a request ends in AUTH_DONE, a security token is issued and, therefore, the user id and login id must exist in the session. In the example below, the user id and login id are set in the AuthResponse, which results in the values being set in the user's session.

response.setLoginId(username);
response.setUserId(username);

The login id is set to the value that the user used to authenticate. The user id is set to the value that the user is associated with in the system. Often, login id and user id have the same value, but it is also possible that there is more than one login ID mapping to the same user ID.

User Authorization

Some resources are only accessible if the user has the expected roles. The SecToken therefore also contains roles.

In the AuthState, you can add them like this:

response.addActualRole(role);

Logging

To write into the standard nevisAuth log file, you can use the Slf4j logging framework. A logger can be instantiated as follows:

private final static Logger LOG = LoggerFactory.getLogger("MyCustomLogger");

Logging can then be done on separate levels, such as:

LOG.debug("For verbose logging statements");
LOG.info("For important information");
LOG.warning("For warnings that might influence the systems behaviour negatively");
LOG.error("For serious and unexpected problems that require administrator intervention");

Audit Log

The audit log is a security-relevant chronological record of successful and unsuccessful login attempts. In the nevisAuth context this means that the audit log is written if the AuthEngine ends up in either the AUTH_DONE or the AUTH_ERROR state. The information that is written into the audit log is prepared by the AuthStates, which have been traversed during the attempt to login.

To prepare the audit trail, an AuthState can create a new AuthMarker in the code by calling markAuthenticate() on the response object:

res.markAuthentication(new AuthMarker(this, "X509", AuthenticationType.TOKEN, userId));

The parameters that are passed to the AuthMarker constructor are:

  • The AuthState that is the initiator of the Audit event.
  • A descriptive name of the technology that was used in this AuthState.
  • The authentication type, which can be one of the following: USERNAME_PASSWORD, TOKEN, CHALLENGE_RESPONSE, EXTERN, FEDERATION, ONE_TIME_PASSWORD, SELECTION, MUTATION, or NONE.
  • The user, who is involved in the request.

Error Reporting

Error handling is essential in nevisAuth for qualifying problems quickly by reading the nevisAuth log. If something goes severely wrong, nevisAuth should log errors in its logfile using the trace level error, such that an operator can identify the source of the issue easily and quickly. AuthState errors can be categorized into two types: Start-up errors and processing errors.

Start-up errors happen during execution of the init() method, for example if a mandatory config property is not set. They should be propagated by throwing a BadConfigurationException. The AuthEngine will catch BadConfigurationExceptions and log the name of the failed AuthState and its failure cause into the log file, e.g.:

ENGINE ERROR:      State 'MyAuthState' failed initialization:
Missing required property 'usernameSource' in AuthState 'MyCustomState' of class 'ch.my.custom.MyAuthState'

When the initialization of an AuthState fails, the LOG.error() method should not be called directly in the AuthState. Instead it is best practice to throw the BadConfigurationException with a descriptive message, since the AuthEngine will log it with the trace level error once it is caught.

During processing, the procedure is similar: Error cases are covered by throwing an AuthStateException with a descriptive message. Error logging should not be done directly in the AuthState. For example:

try {
// somewhere in here, ScriptException is thrown.
} catch (ScriptException e) {
throw new AuthStateException("Failed to evaluate script: " + e, e);
}

GUI Generation

For GUI generation, the AuthEngine calls the AuthState method generate(). This method implements the construction of the GUI descriptor, which instructs nevisLogRend what data must be displayed in the GUI.

In rare cases, e.g., if a direct response must be prepared instead of a GUI descriptor, the generate() method can be overridden. This means that nevisAuth can instruct clients like nevisProxy to send a response generated in nevisAuth to the user agent directly instead of passing the GUI descriptor to nevisLogRend first.

caution

As with process(), the generate() method is also not thread-safe and should never override any instance variables, only consume them.

In most common cases, the generate() method does not have to be overridden, though.

Access to Configured Keys

In some cases the custom AuthState might require to access some key material to process the request as expected. For example an AuthState might require a certificate to sign a result, another AuthState might need to have access to a public key to decrypt some of the contents received in the request, etc. The configuration of nevisAuth allows to specify certificates in the esauth4.xml file. These keys can be accessed using the class ch.nevis.nevisauth.commons.config.KeyUtil of the API.

The following example describes how to navigate through all the keys defined in the configuration:

package ch.example;

import java.io.PrintStream;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;

import ch.nevis.nevisauth.commons.config.ConfiguredKeyStore;
import ch.nevis.nevisauth.commons.config.KeyObject;
import ch.nevis.nevisauth.commons.config.KeyService;
import ch.nevis.nevisauth.commons.config.KeyUtil;

/**
* A class that prints all the key material defined in the nevisAuth configuration file.
*/
public final class KeyMaterialPrinter {
private KeyMaterialPrinter() {
}

public static void printKeys(PrintStream out) {
KeyService keyService = KeyUtil.defaultKeyService();
keyService.configuredKeyStores().forEach(keyStore -> printKeyStore(out, keyStore));
}

private static void printKeyStore(PrintStream out, ConfiguredKeyStore keyStore) {
out.print("KeyStore name: ");
out.println(keyStore.name());
keyStore.keyObjects().forEach(keyObject -> printKeyObject(out, keyObject));
out.println();
}

private static void printKeyObject(PrintStream out, KeyObject keyObject) {
out.print(" KeyObject name: ");
out.println(keyObject.name());
keyObject.privateKey().ifPresent(privateKey -> printPrivateKey(out, privateKey));
keyObject.certificates().forEach(certificate -> printCertificate(out, certificate));
}

private static void printPrivateKey(PrintStream out, PrivateKey privateKey) {
// Print the contents of the private key...
}

private static void printCertificate(PrintStream out, X509Certificate certificate) {
// Print the contents of the certificate...
}
}

A Complete Example

A complete example of an AuthState implementation could look as follows:

package ch.example;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.Properties;

import ch.nevis.esauth.BadConfigurationException;
import ch.nevis.esauth.auth.engine.AuthConst;
import ch.nevis.esauth.auth.engine.AuthRequest;
import ch.nevis.esauth.auth.engine.AuthResponse;
import ch.nevis.esauth.auth.engine.AuthState;
import ch.nevis.nevisauth.commons.config.Configuration;
import ch.nevis.nevisauth.commons.context.Context;
import ch.nevis.nevisauth.commons.context.Scope;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;


/**
* This class is an example for how the nevisauth-authstate-api can be used.
* It implements a use case where a user is authenticated against a password
* file.
* The password file should be a delimited text file. Per default the
* UseridPasswordFileAuthState expects a ':' separated file where the password
* are SHA-256 hashes in a hexadecimal format. The location of the password
* file can be configured in the AuthState configuration file so that it
* gets passed as a property to the {@link #init(Properties)} method.
* The logic for parsing the file and checking whether a user has provided the
* correct credentials is extracted into a separate class {@link CredentialProvider}
*
**/
public class UseridPasswordFileAuthState extends AuthState {

/*
* Logger used to log messages to the esauth4.log file.
* The Logger's logging level can be parameterized in
* the log4j.xml file.
*/
private final static Logger LOG = LoggerFactory.getLogger("ApiAuthStates");

private CredentialProvider credentialProvider;

private boolean logPassword = false;

@Override
public void init(Properties cfg) throws BadConfigurationException {
Configuration configuration = new Configuration(cfg);

String passwordFileLocation = configuration.getPropertyAsString("passwordFileLocation");
logPassword = configuration.getPropertyAsBoolean("logPassword", false);

try {
this.credentialProvider = new CredentialProvider(new FilePasswordRepository(passwordFileLocation, ":"));
} catch (FileNotFoundException e) {
String errorString = "PasswordFile location: " + passwordFileLocation + " is invalid.";
LOG.debug(errorString);
throw new BadConfigurationException(errorString, e);
} catch (IOException e) {
String errorString = "Error while processing: " + passwordFileLocation;
LOG.debug(errorString);
throw new UncheckedIOException(errorString, e);
}

LOG.info("Initialized UseridPasswordFileAuthState");
}


@Override
public void authenticate(AuthRequest req, AuthResponse res) {
Context context = new Context(req, res);

String uid = context.getValue(Scope.INARGS, "username");
String pwd = context.getValue(Scope.INARGS, "password");

res.setLoginId(uid);

if (uid != null && credentialProvider.isUserAuthentic(uid, pwd)) {
if(logPassword) {
LOG.info("successfully authenticate user: {}, password: {}", uid , pwd);
} else {
LOG.info("successfully authenticate user: {}", uid);
}
res.setUserId(uid);
res.setResult("ok");
} else{
LOG.info("user: {} could not be authenticated", uid);
res.setError(AuthConst.AUTH_FAILED, "wrong credentials");
res.setResult("failed");
}
}
}

Using HTTP clients

For auth states with outgoing HTTP connections, we recommend using the nevisAuth HTTP client.