Credential Transformation Procedures#

Credential transformation procedures are used by the Credential Managers when custom credential verification is needed. More accurately the procedure is responsible for transforming the password by applying the appropriate algorithm. Normally it would be enough to use the built in types on the credential managers such as SHA256, SHA512 or BCrypt, but when faced with an existing user store where the passwords already are hashed using a custom scheme, these procedures can be used to hash the password before comparison.

Function#

A credential transformation procedure has the following format.

function result(context) {
    return context.getProvidedPassword().split("").reverse().join("");
}

The context object has type CredentialTransformationProcedureContext.

The functions receive the user-provided password and, if present, the stored password from the data source. These can be used to perform the transformation operation.

function result(context) {
    var comparablePassword = context.getComparablePassword()
    var providedPassword = context.getProvidedPassword()

    var result
    if (context.sha512CryptCheckPassword(providedPassword, comparablePassword))
    {
        result = comparablePassword;
    }
    else
    {
        result = context.sha512CryptHashPassword(
            providedPassword, context.generateRandomSaltSha512(20000))
    }

    return result;
}

The comparablePassword is the already hashed password retrieved from the credential store. Some algorithms require the stored password to extract the salt and other parameters needed to hash the providedPassword for comparison.

It is not the job of the transformation procedure to actually compare the passwords, but it is often needed in order to know if a new hash is needed or the existing hash should be reused.

The procedure context provides access to the internal PBKDF2 key derivation function, without any extra encoding applied. This is useful when needing to use stored passwords created by systems that use PBKDF2 with a different encoding scheme (e.g. ASP.NET).

context.subject contains the subject the credentials belong to, allowing the transformer to act differently depending on the subject. Note that this is the unvalidated identifier provided by the client (typically the username), so no further information about the account can be assumed from it.

It is recommended that the subject should be normalized according to the same rules that are used for user registration (e.g. lowercase the subject if it is considered case-insensitive by the system).

Examples#

Implementing a plaintext transformer#

A plaintext transformer does (as the name suggests) no transformation.

function result(context) {
    return context.getProvidedPassword();
}

Implementing a SHA512 transformer#

A SHA512 transformer operating exactly as the built in Sha2WithSha512 transformer

function result(context) {

    var comparablePassword = context.getComparablePassword()
    var providedPassword = context.getProvidedPassword()

    var result
    if (context.sha512CryptCheckPassword(providedPassword, comparablePassword))
    {
        result = comparablePassword;
    }
    else
    {
        result = context.sha512CryptHashPassword(providedPassword,
                    context.generateRandomSaltSha512(20000))
    }

    return result;
}

Using a Password Transformer#

The functions above cover the algorithms the server implements itself. To hash and verify with an algorithm provided by a Password Transformer plugin — such as argon2id from the external hardened-hashing plugin — configure a Password Transformer and obtain it in the procedure by its ID:

function result(context) {
    var transformer = context.getPasswordTransformer('main-argon2')

    var providedPassword = context.getProvidedPassword()
    var comparablePassword = context.getComparablePassword()

    if (comparablePassword !== null && transformer.checkPassword(providedPassword, comparablePassword)) {
        return comparablePassword;
    }

    return transformer.hashPassword(providedPassword);
}

The Password Transformer does not have to be used by any Credential Manager; configuring one only for a procedure to use is a valid setup. The algorithm and all of its settings come from the Password Transformer, so changing them there changes how the procedure hashes, with no edit to the script.

Call getPasswordTransformer on every run of the procedure. The object it returns belongs to the request being served; a JavaScript global variable outlives the request, so caching it in one is not supported.

Use matchPassword instead of checkPassword to see more than whether the passwords matched:

var match = transformer.matchPassword(providedPassword, comparablePassword)

match.isMatch()         // the passwords match
match.isShouldUpgrade() // they match, but the stored hash was made with settings that are no longer current
match.isInvalidData()   // the stored value is not in a format this algorithm can read at all

isInvalidData tells a stored value belonging to another algorithm apart from a valid hash of a different password, which is what makes a procedure able to fall back to one of the built-in functions during a migration:

function result(context) {
    var transformer = context.getPasswordTransformer('main-argon2')
    var providedPassword = context.getProvidedPassword()
    var comparablePassword = context.getComparablePassword()

    // Nothing to compare against: this is a new password being stored.
    if (comparablePassword === null) {
        return transformer.hashPassword(providedPassword);
    }

    var match = transformer.matchPassword(providedPassword, comparablePassword)

    if (match.isMatch()) {
        return comparablePassword;
    }

    // Not an argon2 hash at all — it may still be one the old store wrote.
    if (match.isInvalidData() && context.bcryptCheckPassword(providedPassword, comparablePassword)) {
        return comparablePassword;
    }

    return transformer.hashPassword(providedPassword);
}

Test comparablePassword === null before branching on isInvalidData, as above. isInvalidData reports false when there is no stored password, precisely so that a fallback branch is not entered while a brand new password is being hashed — a fallback that returned providedPassword there (the shape a migration from a plaintext store takes) would store the password in the clear on every registration.

Hashing with a memory-hard algorithm is deliberately expensive, and a procedure pays for every call it makes. The first example above hashes a second time when the password does not match, which doubles the cost of a failed login and, under a burst of wrong passwords, can exhaust resources. Ordering the checks as in this second example — return early when there is nothing to compare against, and do not hash again on a mismatch unless the result is actually needed — keeps a failed attempt to a single hash. If the procedure adds no logic beyond hash-and-verify, prefer a Credential Manager using the Password Transformer as its algorithm.

A procedure has to return the stored value for the credential to be accepted, so it cannot re-hash a password while verifying it. isShouldUpgrade is therefore useful to report on, or to refuse a credential and force a reset — not to upgrade a hash in place. Use a Credential Manager with the Password Transformer as its algorithm if hashes should be upgraded on login.

The ID is part of the script’s text, so it cannot be checked when the configuration is committed the way a Credential Manager’s reference to a Password Transformer is. An ID that matches no configured Password Transformer fails when the procedure runs. getPasswordTransformer also fails when the server runs in FIPS mode, because the server cannot know which cryptographic primitives a plugin uses.

Examples#

Implementing a plaintext transformer#

A plaintext transformer does (as the name suggests) no transformation.

function result(context) {
    return context.getProvidedPassword();
}

Implementing a SHA512 transformer#

A SHA512 transformer operating exactly as the built in Sha2WithSha512 transformer

function result(context) {

    var comparablePassword = context.getComparablePassword()
    var providedPassword = context.getProvidedPassword()

    var result
    if (context.sha512CryptCheckPassword(providedPassword, comparablePassword))
    {
        result = comparablePassword;
    }
    else
    {
        result = context.sha512CryptHashPassword(providedPassword,
                    context.generateRandomSaltSha512(20000))
    }

    return result;
}

Was this helpful?