Skip to content

Commit

Permalink
feat(jans-auth-server): allow invoke consent script by acr #10548 (#1…
Browse files Browse the repository at this point in the history
…0712)

* feat(jans-auth-server): allow invoke consent script by acr #10548

Signed-off-by: YuriyZ <[email protected]>

* added consent script identification

Signed-off-by: YuriyZ <[email protected]>

* feat(jans-auth-server): agama consent script - set consent_flow into the session

Signed-off-by: YuriyZ <[email protected]>

* feat(jans-auth-server): agama consent script - added consetn gatherer service test

Signed-off-by: YuriyZ <[email protected]>

* doc(jans-auth-server): added documentation about consent script identification

Signed-off-by: YuriyZ <[email protected]>

---------

Signed-off-by: YuriyZ <[email protected]>
  • Loading branch information
yuriyz authored Jan 29, 2025
1 parent 11bddd1 commit e1982e1
Show file tree
Hide file tree
Showing 6 changed files with 258 additions and 24 deletions.
25 changes: 25 additions & 0 deletions docs/script-catalog/consent_gathering/consent-gathering.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,31 @@ tags:
## Overview
OAuth 2.0 allows providers to prompt users for consent before releasing their personal information to a client (application). The standard consent process is binary: approve or deny. Using the consent gathering interception script, the consent flow can be customized to meet unique business requirements, for instance to support payment authorization, where you need to present transactional information, or where you need to step-up authentication to add security.

## Script identification during execution

Consent script is executed during authorization step.
AS identifies consent gathering script to invoke in following order:
- if `consentGatheringScriptBackwardCompatibility` is `true` (`false` by default) - invoke first consent gathering script found in database.
- if `acrToConsentScriptNameMapping` has mapping, try to find consent script by that mapping and invoke it.
- if client has `consentGatheringScripts` that points to valid consent script, invoke it.
- if nothing from above worked try to invoke first script found in database

`acrToConsentScriptNameMapping` is simple acr to consent script mapping
```text
acr1 - consentScript1
acr2 - consentScript2
..
acrN - consentScriptN
```

**Agama**

If Agama Consent is used then typically `acrToAgamaConsentFlowMapping` AS configuration property has to be used as well
to determine consent flow.
`acrToAgamaConsentFlowMapping` - The acr mapping to agama consent flow name. When AS meets acr it tries to match agama consent name and set it into session attributes under `consent_flow` name.
This makes it available for main Agama Consent script, so it knows which flow to invoke.


## Interface
The consent gathering script implements the [ConsentGathering](https://github.com/JanssenProject/jans/blob/main/jans-core/script/src/main/java/io/jans/model/custom/script/type/authz/ConsentGatheringType.java) interface. This extends methods from the base script type in addition to adding new methods:

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -498,6 +498,12 @@ public class AppConfiguration implements Configuration {
@DocProperty(description = "The acr mappings. When AS meets key-value in map, it tries to replace 'key' with 'value' as very first thing and use that 'value' in further processing.")
private Map<String, String> acrMappings;

@DocProperty(description = "The acr mapping to consent script name. When AS meets acr it tries to match consent script name and invoke it during authorization. This takes higher precedence then client consent script configuration.")
private Map<String, String> acrToConsentScriptNameMapping;

@DocProperty(description = "The acr mapping to agama consent flow name. When AS meets acr it tries to match agama consent name and set it into session attributes under 'consent_flow' name. This makes it available for main Agama Consent script, so it knows which flow to invoke.")
private Map<String, String> acrToAgamaConsentFlowMapping;

@DocProperty(description = "Boolean value specifying whether to enable user authentication filters")
private Boolean authenticationFiltersEnabled;

Expand Down Expand Up @@ -3617,6 +3623,26 @@ public void setAcrMappings(Map<String, String> acrMappings) {
this.acrMappings = acrMappings;
}

public Map<String, String> getAcrToConsentScriptNameMapping() {
if (acrToConsentScriptNameMapping == null) acrToConsentScriptNameMapping = new HashMap<>();
return acrToConsentScriptNameMapping;
}

public AppConfiguration setAcrToConsentScriptNameMapping(Map<String, String> acrToConsentScriptNameMapping) {
this.acrToConsentScriptNameMapping = acrToConsentScriptNameMapping;
return this;
}

public Map<String, String> getAcrToAgamaConsentFlowMapping() {
if (acrToAgamaConsentFlowMapping == null) acrToAgamaConsentFlowMapping = new HashMap<>();
return acrToAgamaConsentFlowMapping;
}

public AppConfiguration setAcrToAgamaConsentFlowMapping(Map<String, String> acrToAgamaConsentFlowMapping) {
this.acrToAgamaConsentFlowMapping = acrToAgamaConsentFlowMapping;
return this;
}

public EngineConfig getAgamaConfiguration() {
return agamaConfiguration;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -481,9 +481,10 @@ public void checkPermissionGrantedInternal() throws IOException {
return;
}

log.trace("Starting external consent-gathering flow");
List<String> acrValuesList = sessionIdService.acrValuesList(this.acrValues);
log.trace("Starting external consent-gathering flow, acrValues {} ...", acrValuesList);

boolean result = consentGatherer.configure(session.getUserDn(), clientId, state);
boolean result = consentGatherer.configure(session.getUserDn(), clientId, state, acrValuesList);
if (!result) {
log.error("Failed to initialize external consent-gathering flow.");
permissionDenied();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@

package io.jans.as.server.authorize.ws.rs;

import io.jans.as.common.model.session.SessionId;
import io.jans.as.common.service.common.UserService;
import io.jans.as.model.authorize.AuthorizeRequestParam;
import io.jans.as.model.configuration.AppConfiguration;
import io.jans.as.model.util.StringUtils;
import io.jans.as.persistence.model.Scope;
import io.jans.as.server.i18n.LanguageBean;
import io.jans.as.server.model.authorize.ScopeChecker;
import io.jans.as.common.model.session.SessionId;
import io.jans.as.server.model.config.Constants;
import io.jans.as.server.service.AuthorizeService;
import io.jans.as.server.service.ClientService;
Expand All @@ -23,8 +23,6 @@
import io.jans.jsf2.service.FacesService;
import io.jans.model.custom.script.conf.CustomScriptConfiguration;
import io.jans.util.StringHelper;
import org.slf4j.Logger;

import jakarta.enterprise.context.RequestScoped;
import jakarta.faces.application.FacesMessage;
import jakarta.faces.context.ExternalContext;
Expand All @@ -33,12 +31,11 @@
import jakarta.inject.Named;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.slf4j.Logger;

import java.util.*;

import static org.apache.commons.lang3.BooleanUtils.isTrue;

/**
* @author Yuriy Movchan Date: 10/30/2017
Expand Down Expand Up @@ -86,22 +83,22 @@ public class ConsentGathererService {
@Inject
private ScopeChecker scopeChecker;

private final Map<String, String> pageAttributes = new HashMap<String, String>();
private final Map<String, String> pageAttributes = new HashMap<>();
private ConsentGatheringContext context;

public boolean configure(String userDn, String clientId, String state) {
public boolean configure(String userDn, String clientId, String state, List<String> acrValues) {
final HttpServletRequest httpRequest = (HttpServletRequest) externalContext.getRequest();
final HttpServletResponse httpResponse = (HttpServletResponse) externalContext.getResponse();

final SessionId session = sessionService.getConsentSession(httpRequest, httpResponse, userDn, true);

CustomScriptConfiguration script = determineConsentScript(clientId);
CustomScriptConfiguration script = determineConsentScript(clientId, acrValues);
if (script == null) {
log.error("Failed to determine consent-gathering script");
return false;
}

sessionService.configure(session, script.getName(), clientId, state);
sessionService.configure(session, script.getName(), clientId, state, acrValues);

this.context = new ConsentGatheringContext(script.getConfigurationAttributes(), httpRequest, httpResponse, session,
pageAttributes, sessionService, userService, facesService, appConfiguration);
Expand All @@ -122,24 +119,59 @@ public boolean configure(String userDn, String clientId, String state) {
return true;
}

private CustomScriptConfiguration determineConsentScript(String clientId) {
if (appConfiguration.getConsentGatheringScriptBackwardCompatibility()) {
private CustomScriptConfiguration determineConsentScript(String clientId, List<String> acrValues) {
log.trace("Trying to determine consent script, clientId {}, acrValues {} ...", clientId, acrValues);

if (isTrue(appConfiguration.getConsentGatheringScriptBackwardCompatibility())) {
// in 4.1 and earlier we returned default consent script
log.trace("determineConsentScript - falled back to default script {}", external.getDefaultExternalCustomScript().getName());
return external.getDefaultExternalCustomScript();
}

final CustomScriptConfiguration consentScriptByAcr = findConsentScriptByAcr(acrValues);
if (consentScriptByAcr != null) {
return consentScriptByAcr;
}

final List<String> consentGatheringScripts = clientService.getClient(clientId).getAttributes().getConsentGatheringScripts();
final List<CustomScriptConfiguration> scripts = external.getCustomScriptConfigurationsByDns(consentGatheringScripts);
if (!scripts.isEmpty()) {
final CustomScriptConfiguration script = Collections.max(scripts, Comparator.comparingInt(CustomScriptConfiguration::getLevel)); // flow supports single script, thus taking the one with higher level
log.debug("Determined consent gathering script `%s`", script.getName());
log.debug("Determined consent gathering script `{}`", script.getName());
return script;
}

log.debug("There no consent gathering script configured for client `%s`. Therefore taking default consent script.", clientId);
log.debug("There no consent gathering script configured for client `{}`. Therefore taking default consent script.", clientId);
return external.getDefaultExternalCustomScript();
}

public CustomScriptConfiguration findConsentScriptByAcr(List<String> acrValues) {
final Map<String, String> acrToConsentScriptMap = appConfiguration.getAcrToConsentScriptNameMapping();
if (acrToConsentScriptMap.isEmpty()) {
log.trace("findConsentScriptByAcr - 'acrToConsentScriptNameMapping' configuration property is empty");
return null;
}

for (Map.Entry<String, String> entry : acrToConsentScriptMap.entrySet()) {
for (String acr : acrValues) {
if (entry.getKey().equalsIgnoreCase(acr)) {
final String scriptName = entry.getValue();
log.trace("Found mapping to consent script {}, acr {}", scriptName, acr);
final CustomScriptConfiguration script = external.getCustomScriptConfigurationByName(scriptName);
if (script != null) {
log.trace("Found consent script by name {}, id {}", scriptName, script.getInum());
return script;
} else {
log.trace("Unable to find consent script by name {}", scriptName);
}
}
}
}

log.trace("findConsentScriptByAcr - unable to find consent script, acr: {}, acrToConsentScriptNameMapping: {}", acrValues, acrToConsentScriptMap);
return null;
}

public boolean authorize() {
try {
final HttpServletRequest httpRequest = (HttpServletRequest) externalContext.getRequest();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,23 @@

import io.jans.as.common.model.common.User;
import io.jans.as.common.model.registration.Client;
import io.jans.as.model.util.Util;
import io.jans.as.common.model.session.SessionId;
import io.jans.as.model.configuration.AppConfiguration;
import io.jans.as.model.util.Util;
import io.jans.as.server.service.ClientService;
import io.jans.as.server.service.CookieService;
import io.jans.as.server.service.SessionIdService;
import io.jans.orm.exception.EntryPersistenceException;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;

import jakarta.ejb.Stateless;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;

import java.util.List;
import java.util.Map;

/**
* @author Yuriy Movchan
Expand All @@ -43,6 +46,9 @@ public class ConsentGatheringSessionService {
@Inject
private ClientService clientService;

@Inject
private AppConfiguration appConfiguration;

public SessionId getConnectSession(HttpServletRequest httpRequest) {
String cookieId = cookieService.getSessionIdFromCookie(httpRequest);
log.trace("Cookie - session_id: {}", cookieId);
Expand Down Expand Up @@ -141,14 +147,55 @@ public void setStep(int step, SessionId session) {
session.getSessionAttributes().put("step", Integer.toString(step));
}

public void configure(SessionId session, String scriptName, String clientId, String state) {
public String getAcr(SessionId session) {
return session.getSessionAttributes().get("acr");
}

public void setAcr(List<String> acrValues, SessionId session) {
session.getSessionAttributes().put("acr", Util.listAsString(acrValues));
}

public String getConsentFlow(SessionId session) {
return session.getSessionAttributes().get("consent_flow");
}

public void setConsentFlow(String consentFlow, SessionId session) {
session.getSessionAttributes().put("consent_flow", consentFlow);
}

public void configure(SessionId session, String scriptName, String clientId, String state, List<String> acrValues) {
setStep(1, session);
setScriptName(session, scriptName);

setAcr(acrValues, session);
setConsentFlow(determineConsentFlow(acrValues), session);
setClientId(session, clientId);
persist(session);
}

private String determineConsentFlow(List<String> acrValues) {
if (acrValues == null || acrValues.isEmpty()) {
log.debug("determineConsentFlow - 'acrValues' is empty, return null for 'consent_flow'");
return null;
}

final Map<String, String> acrToConsent = appConfiguration.getAcrToConsentScriptNameMapping();
if (acrToConsent == null || acrToConsent.isEmpty()) {
log.debug("determineConsentFlow - 'acrToConsentScriptNameMapping' configuration property is empty, return null for 'consent_flow'");
return null;
}

for (String acr : acrValues) {
final String consentFlow = acrToConsent.get(acr);
if (StringUtils.isNotBlank(consentFlow)) {
log.debug("determineConsentFlow - found consent_flow: {} for acr: {}", consentFlow, acr);
return consentFlow;
}
}
log.debug("determineConsentFlow - unable to find any match for acr: {}, acrToConsentScriptNameMapping: {}", acrValues, acrToConsent);
return null;
}

public boolean isStepPassed(SessionId session, Integer step) {
return Boolean.parseBoolean(session.getSessionAttributes().get(String.format("consent_step_passed_%d", step)));
}
Expand Down
Loading

0 comments on commit e1982e1

Please sign in to comment.