Skip to content

Authorization Challenge Custom Script#

Overview#

The Jans-Auth server implements OAuth 2.0 for First-Party Native Applications. This script is used to control/customize Authorization Challenge Endpoint.

Behavior#

In request to Authorization Challenge Endpoint to is expected to have acr_values request parameter which specifies name of the custom script. If parameter is absent or AS can't find script with this name then it falls back to script with name default_challenge.

This script is provided during installation and performs basic username/password authentication.

POST /jans-auth/restv1/authorization_challenge HTTP/1.1
Host: yuriyz-fond-skink.gluu.info

client_id=999e13b8-f4a2-4fed-ad3c-6c88bd2c92ea&scope=openid+profile+address+email+phone+user_name&state=b4a41b29-51c8-4354-9c8c-fda38b4dbd43&nonce=3a56f8d0-f78e-4b15-857c-3e792801be68&acr_values=&request_session_id=false&password=secret&username=admin

There is authorizationChallengeDefaultAcr AS configuration property which allows to change fallback script name from default_challenge to some other value (value must be valid script name present on AS).

Interface#

The Authorization Challenage script implements the AuthorizationChallenageType interface. This extends methods from the base script type in addition to adding new methods:

Inherited Methods#

Method header Method description
def init(self, customScript, configurationAttributes) This method is only called once during the script initialization. It can be used for global script initialization, initiate objects etc
def destroy(self, configurationAttributes) This method is called once to destroy events. It can be used to free resource and objects created in the init() method
def getApiVersion(self, configurationAttributes, customScript) The getApiVersion method allows API changes in order to do transparent migration from an old script to a new API. Only include the customScript variable if the value for getApiVersion is greater than 10

New methods#

Method header Method description
def authorize(self, context) Called when the request is received.
def getAuthenticationMethodClaims(self, context) Called to get authn method claims. It is injected into id_token. Returns key-value map.

authorize method returns true/false which indicates to server whether to issue authorization_code in response or not.

If parameters is not present then error has to be created and false returned. If all is good script has to return true and it's strongly recommended to set user context.getExecutionContext().setUser(user); so AS can keep tracking what exactly user is authenticated.

Objects#

Object name Object description
customScript The custom script object. Reference
context Reference

Common Use Case: Authorize user by username/password#

Script Type: Java#

import io.jans.as.common.model.common.User;
import io.jans.as.common.model.session.DeviceSession;
import io.jans.as.server.authorize.ws.rs.DeviceSessionService;
import io.jans.as.server.service.UserService;
import io.jans.as.server.service.external.context.ExternalScriptContext;
import io.jans.model.SimpleCustomProperty;
import io.jans.model.custom.script.model.CustomScript;
import io.jans.model.custom.script.type.authzchallenge.AuthorizationChallengeType;
import io.jans.orm.PersistenceEntryManager;
import io.jans.service.cdi.util.CdiUtil;
import io.jans.service.custom.script.CustomScriptManager;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Map;
import java.util.HashMap;
import java.util.UUID;

/**
 * @author Yuriy Z
 */
public class AuthorizationChallenge implements AuthorizationChallengeType {

    public static final String USERNAME_PARAMETER = "username";
    public static final String PASSWORD_PARAMETER = "password";

    private static final Logger log = LoggerFactory.getLogger(AuthorizationChallenge.class);
    private static final Logger scriptLogger = LoggerFactory.getLogger(CustomScriptManager.class);

    /**
     * Return true if Authorization Challenge Endpoint should return code successfully or otherwise false if error should be returned.
     * <p>
     * Implementation of this method should consist of 3 main parts:
     * 1. validate all parameters are present and if not -> set error and return false
     * 2. main authorization logic, if ok -> set authorized user into "context.getExecutionContext().setUser(user);" and return true
     * 3. if not ok -> set error which explains what is wrong and return false
     *
     * @param scriptContext ExternalScriptContext, see https://github.com/JanssenProject/jans/blob/main/jans-auth-server/server/src/main/java/io/jans/as/server/service/external/context/ExternalScriptContext.java
     * @return true if Authorization Challenge Endpoint should return code successfully or otherwise false if error should be returned.
     */
    @Override
    public boolean authorize(Object scriptContext) {
        ExternalScriptContext context = (ExternalScriptContext) scriptContext;

        // 1. validate all required parameters are present
        final String username = getParameterOrCreateError(context, USERNAME_PARAMETER);
        if (StringUtils.isBlank(username)) {
            return false;
        }

        final String password = getParameterOrCreateError(context, PASSWORD_PARAMETER);
        if (StringUtils.isBlank(password)) {
            return false;
        }

        scriptLogger.trace("All required parameters are present");

        // 2. main authorization logic, if ok -> set authorized user into "context.getExecutionContext().setUser(user);" and return true
        UserService userService = CdiUtil.bean(UserService.class);
        PersistenceEntryManager entryManager = CdiUtil.bean(PersistenceEntryManager.class);

        final User user = userService.getUser(username);
        if (user == null) {
            scriptLogger.trace("User is not found by username {}", username);
            createError(context, "username_invalid");
            return false;
        }

        final boolean ok = entryManager.authenticate(user.getDn(), User.class, password);
        if (ok) {
            context.getExecutionContext().setUser(user); // <- IMPORTANT : without user set, user relation will not be associated with token
            scriptLogger.trace("User {} is authenticated successfully.", username);
            return true;
        }

        // 3. not ok -> set error which explains what is wrong and return false
        scriptLogger.trace("Failed to authenticate user {}. Please check username and password.", username);
        createError(context, "username_or_password_invalid");
        return false;
    }

    private String getParameterOrCreateError(ExternalScriptContext context, String parameterName) {
        String value = context.getHttpRequest().getParameter(parameterName);

        if (StringUtils.isBlank(value)) {
            scriptLogger.trace("No '{}' parameter in request", parameterName);
            value = getParameterFromDeviceSession(context, parameterName);
        }

        if (StringUtils.isBlank(value)) {
            scriptLogger.trace("{} is not provided", parameterName);
            createError(context, String.format("%s_required", parameterName));
            return null;
        }

        return value;
    }

    private void createError(ExternalScriptContext context, String errorCode) {
        String deviceSessionPart = prepareDeviceSessionSubJson(context);

        final String entity = String.format("{\"error\": \"%s\"%s}", errorCode, deviceSessionPart);
        context.createWebApplicationException(401, entity);
    }

    private String prepareDeviceSessionSubJson(ExternalScriptContext context) {
        DeviceSession deviceSessionObject = context.getAuthzRequest().getDeviceSessionObject();
        if (deviceSessionObject != null) {
            prepareDeviceSession(context, deviceSessionObject);
            return String.format(",\"device_session\":\"%s\"", deviceSessionObject.getId());
        } else if (context.getAuthzRequest().isUseDeviceSession()) {
            deviceSessionObject = prepareDeviceSession(context, null);
            return String.format(",\"device_session\":\"%s\"", deviceSessionObject.getId());
        }
        return "";
    }

    private DeviceSession prepareDeviceSession(ExternalScriptContext context, DeviceSession deviceSessionObject) {
        DeviceSessionService deviceSessionService = CdiUtil.bean(DeviceSessionService.class);
        boolean newSave = deviceSessionObject == null;
        if (newSave) {
            final String id = UUID.randomUUID().toString();
            deviceSessionObject = new DeviceSession();
            deviceSessionObject.setId(id);
            deviceSessionObject.setDn(deviceSessionService.buildDn(id));
        }

        String username = context.getHttpRequest().getParameter(USERNAME_PARAMETER);
        if (StringUtils.isNotBlank(username)) {
            deviceSessionObject.getAttributes().getAttributes().put(USERNAME_PARAMETER, username);
        }

        String password = context.getHttpRequest().getParameter(PASSWORD_PARAMETER);
        if (StringUtils.isNotBlank(password)) {
            deviceSessionObject.getAttributes().getAttributes().put(PASSWORD_PARAMETER, password);
        }

        String clientId = context.getHttpRequest().getParameter("client_id");
        if (StringUtils.isNotBlank(clientId)) {
            deviceSessionObject.getAttributes().getAttributes().put("client_id", clientId);
        }

        String acrValues = context.getHttpRequest().getParameter("acr_values");
        if (StringUtils.isNotBlank(acrValues)) {
            deviceSessionObject.getAttributes().getAttributes().put("acr_values", acrValues);
        }

        if (newSave) {
            deviceSessionService.persist(deviceSessionObject);
        } else {
            deviceSessionService.merge(deviceSessionObject);
        }

        return deviceSessionObject;
    }

    private String getParameterFromDeviceSession(ExternalScriptContext context, String parameterName) {
        final DeviceSession deviceSessionObject = context.getAuthzRequest().getDeviceSessionObject();
        if (deviceSessionObject != null) {
            return deviceSessionObject.getAttributes().getAttributes().get(parameterName);
        }
        return null;
    }

    @Override
    public boolean init(Map<String, SimpleCustomProperty> configurationAttributes) {
        scriptLogger.info("Initialized Default AuthorizationChallenge Java custom script.");
        return true;
    }

    @Override
    public boolean init(CustomScript customScript, Map<String, SimpleCustomProperty> configurationAttributes) {
        scriptLogger.info("Initialized Default AuthorizationChallenge Java custom script.");
        return true;
    }

    @Override
    public boolean destroy(Map<String, SimpleCustomProperty> configurationAttributes) {
        scriptLogger.info("Destroyed Default AuthorizationChallenge Java custom script.");
        return true;
    }

    @Override
    public int getApiVersion() {
        return 11;
    }

    @Override
    public Map<String, String> getAuthenticationMethodClaims(Object context) {
        return new HashMap<>();
    }
}

Multi-step authorization by username and OTP#

Script Type: Java#

import io.jans.as.common.model.common.User;
import io.jans.as.common.model.session.DeviceSession;
import io.jans.as.server.authorize.ws.rs.DeviceSessionService;
import io.jans.as.server.service.UserService;
import io.jans.as.server.service.external.context.ExternalScriptContext;
import io.jans.model.SimpleCustomProperty;
import io.jans.model.custom.script.model.CustomScript;
import io.jans.model.custom.script.type.authzchallenge.AuthorizationChallengeType;
import io.jans.orm.PersistenceEntryManager;
import io.jans.service.cdi.util.CdiUtil;
import io.jans.service.custom.script.CustomScriptManager;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Map;
import java.util.HashMap;
import java.util.UUID;

/**
 * Script is to demo 2 steps flow:
 * 1) First step -> send username
 * 2) Second step -> send OTP
 *
 * AS tracks data by 'device_session'. See "prepareDeviceSession()" method implemention for details.
 *
 * @author Yuriy Z
 */
public class AuthorizationChallenge implements AuthorizationChallengeType {

    public static final String USERNAME_PARAMETER = "username";
    public static final String OTP_PARAMETER = "otp";

    private static final Logger log = LoggerFactory.getLogger(AuthorizationChallenge.class);
    private static final Logger scriptLogger = LoggerFactory.getLogger(CustomScriptManager.class);

    /**
     * Return true if Authorization Challenge Endpoint should return code successfully or otherwise false if error should be returned.
     * <p>
     * Implementation of this method should consist of 3 main parts:
     * 1. validate all parameters are present and if not -> set error and return false
     * 2. main authorization logic, if ok -> set authorized user into "context.getExecutionContext().setUser(user);" and return true
     * 3. if not ok -> set error which explains what is wrong and return false
     *
     * @param scriptContext ExternalScriptContext, see https://github.com/JanssenProject/jans/blob/main/jans-auth-server/server/src/main/java/io/jans/as/server/service/external/context/ExternalScriptContext.java
     * @return true if Authorization Challenge Endpoint should return code successfully or otherwise false if error should be returned.
     */
    @Override
    public boolean authorize(Object scriptContext) {
        ExternalScriptContext context = (ExternalScriptContext) scriptContext;

        // 1. As first step we get username
        final String username = getParameterOrCreateError(context, USERNAME_PARAMETER);
        if (StringUtils.isBlank(username)) {
            return false;
        }

        // 2. During first execution OTP is not present, so error will be returned with device_session (which has saved username)
        //    Note: prepareDeviceSession method implemention
        final String otp = getParameterOrCreateError(context, OTP_PARAMETER);
        if (StringUtils.isBlank(otp)) {
            return false;
        }

        scriptLogger.trace("All required parameters are present");

        // 2. main authorization logic, if ok -> set authorized user into "context.getExecutionContext().setUser(user);" and return true
        UserService userService = CdiUtil.bean(UserService.class);
        PersistenceEntryManager entryManager = CdiUtil.bean(PersistenceEntryManager.class);

        final User user = userService.getUser(username);
        if (user == null) {
            scriptLogger.trace("User is not found by username {}", username);
            createError(context, "username_invalid");
            return false;
        }

        // for simplicity OTP is here just username password but in real world scenario real OTP validation is needed.
        final boolean ok = entryManager.authenticate(user.getDn(), User.class, otp);
        if (ok) {
            context.getExecutionContext().setUser(user); // <- IMPORTANT : without user set, user relation will not be associated with token
            scriptLogger.trace("User {} is authenticated successfully.", username);
            return true;
        }

        // 3. not ok -> set error which explains what is wrong and return false
        scriptLogger.trace("Failed to authenticate user {}. Please check username and OTP.", username);
        createError(context, "username_or_otp_invalid");
        return false;
    }

    private String getParameterOrCreateError(ExternalScriptContext context, String parameterName) {
        String value = context.getHttpRequest().getParameter(parameterName);

        if (StringUtils.isBlank(value)) {
            scriptLogger.trace("No '{}' parameter in request", parameterName);
            value = getParameterFromDeviceSession(context, parameterName);
        }

        if (StringUtils.isBlank(value)) {
            scriptLogger.trace("{} is not provided", parameterName);
            createError(context, String.format("%s_required", parameterName));
            return null;
        }

        return value;
    }

    private void createError(ExternalScriptContext context, String errorCode) {
        String deviceSessionPart = prepareDeviceSessionSubJson(context);

        final String entity = String.format("{\"error\": \"%s\"%s}", errorCode, deviceSessionPart);
        context.createWebApplicationException(401, entity);
    }

    private String prepareDeviceSessionSubJson(ExternalScriptContext context) {
        DeviceSession deviceSessionObject = context.getAuthzRequest().getDeviceSessionObject();
        if (deviceSessionObject != null) {
            prepareDeviceSession(context, deviceSessionObject);
            return String.format(",\"device_session\":\"%s\"", deviceSessionObject.getId());
        } else if (context.getAuthzRequest().isUseDeviceSession()) {
            deviceSessionObject = prepareDeviceSession(context, null);
            return String.format(",\"device_session\":\"%s\"", deviceSessionObject.getId());
        }
        return "";
    }

    private DeviceSession prepareDeviceSession(ExternalScriptContext context, DeviceSession deviceSessionObject) {
        DeviceSessionService deviceSessionService = CdiUtil.bean(DeviceSessionService.class);
        boolean newSave = deviceSessionObject == null;
        if (newSave) {
            final String id = UUID.randomUUID().toString();
            deviceSessionObject = new DeviceSession();
            deviceSessionObject.setId(id);
            deviceSessionObject.setDn(deviceSessionService.buildDn(id));
        }

        String username = context.getHttpRequest().getParameter(USERNAME_PARAMETER);
        if (StringUtils.isNotBlank(username)) {
            deviceSessionObject.getAttributes().getAttributes().put(USERNAME_PARAMETER, username);
        }

        String otp = context.getHttpRequest().getParameter(OTP_PARAMETER);
        if (StringUtils.isNotBlank(otp)) {
            deviceSessionObject.getAttributes().getAttributes().put(OTP_PARAMETER, otp);
        }

        String clientId = context.getHttpRequest().getParameter("client_id");
        if (StringUtils.isNotBlank(clientId)) {
            deviceSessionObject.getAttributes().getAttributes().put("client_id", clientId);
        }

        String acrValues = context.getHttpRequest().getParameter("acr_values");
        if (StringUtils.isNotBlank(acrValues)) {
            deviceSessionObject.getAttributes().getAttributes().put("acr_values", acrValues);
        }

        if (newSave) {
            deviceSessionService.persist(deviceSessionObject);
        } else {
            deviceSessionService.merge(deviceSessionObject);
        }

        return deviceSessionObject;
    }

    private String getParameterFromDeviceSession(ExternalScriptContext context, String parameterName) {
        final DeviceSession deviceSessionObject = context.getAuthzRequest().getDeviceSessionObject();
        if (deviceSessionObject != null) {
            return deviceSessionObject.getAttributes().getAttributes().get(parameterName);
        }
        return null;
    }

    @Override
    public boolean init(Map<String, SimpleCustomProperty> configurationAttributes) {
        scriptLogger.info("Initialized Default AuthorizationChallenge Java custom script.");
        return true;
    }

    @Override
    public boolean init(CustomScript customScript, Map<String, SimpleCustomProperty> configurationAttributes) {
        scriptLogger.info("Initialized Default AuthorizationChallenge Java custom script.");
        return true;
    }

    @Override
    public boolean destroy(Map<String, SimpleCustomProperty> configurationAttributes) {
        scriptLogger.info("Destroyed Default AuthorizationChallenge Java custom script.");
        return true;
    }

    @Override
    public int getApiVersion() {
        return 11;
    }

    @Override
    public Map<String, String> getAuthenticationMethodClaims(Object context) {
        return new HashMap<>();
    }
}

Sample Scripts#


Last update: 2024-08-23
Created: 2023-08-11