From b0034ec509ace1a4e30a7e9c6dd23dca48178c62 Mon Sep 17 00:00:00 2001 From: YuriyZ Date: Mon, 28 Nov 2022 13:55:30 +0200 Subject: [PATCH] feat(jans-auth-server): specify minimum acr for clients #343 (#3083) * feat(jans-auth-server): specify minimum acr for clients #343 * feat(jans-auth-server): added minimum acr properties to dynamic registration #343 * doc(jans-auth-server): added docs and updated swagger with new minimum acr related properties #343 --- .../configuration/client-config.md | 46 +++++ .../io/jans/as/client/RegisterRequest.java | 70 ++++++++ .../common/src/test/resources/testng.xml | 3 +- jans-auth-server/docs/swagger.yaml | 38 ++++- .../model/register/RegisterRequestParam.java | 18 ++ .../persistence/model/ClientAttributes.java | 38 +++++ .../ws/rs/AuthorizeRestWebServiceImpl.java | 2 +- .../authorize/ws/rs/AuthzRequestService.java | 96 ++++++++++- .../register/ws/rs/RegisterService.java | 13 ++ .../ws/rs/action/RegisterCreateAction.java | 3 +- .../ws/rs/AuthzRequestServiceTest.java | 157 +++++++++++++++++- 11 files changed, 469 insertions(+), 15 deletions(-) create mode 100644 docs/admin/auth-server/client-management/configuration/client-config.md diff --git a/docs/admin/auth-server/client-management/configuration/client-config.md b/docs/admin/auth-server/client-management/configuration/client-config.md new file mode 100644 index 00000000000..f03bd35dbc1 --- /dev/null +++ b/docs/admin/auth-server/client-management/configuration/client-config.md @@ -0,0 +1,46 @@ +--- +tags: + - administration + - client + - configuration +--- + +# Client Configuration + +## ACR client configuration + +There are 4 client configuration properties related to ACR: + +- `default_acr_values` - string array, default acr values which are set when `acr_values` is missed in authorization request. +- `minimumAcrLevel` - integer value which sets minimum acr level. +- `minimumAcrLevelAutoresolve` - boolean value, if `false` and `minimumAcrLevel` is higher then current `acr_values` then reject request. If `true` - resolve acr according to either client's `minimumAcrPriorityList` or AS `auth_level_mapping` +- `minimumAcrPriorityList` - string array, enables client to specify the acr order of preference, rather then just the next lowest integer value + +AS process properties in following order: +1. if `acr_values` is absent, set `acr_values` from `default_acr_values` +2. Otherwise if present, checking minimum acr level: +- check `minimumAcrLevel`, if current acr level is higher or equals to `minimumAcrLevel` then proceed request processing without changes +- if `minimumAcrLevel` is less then current acr level and `minimumAcrLevelAutoresolve=false` -> reject request (return bad request error) +- if `minimumAcrLevel` is less then current acr level and `minimumAcrLevelAutoresolve=true` -> pickup value from `minimumAcrPriorityList` or if it's empty take nearest acr value that satisfy `minimumAcrLevel` + +For example, given: +1. `minimumAcrLevel` = 14 +1. `default_acr_values` = "basic" +1. `minimumAcrPriorityList` = ["u2f", "passkey", "usb_fido_key", "super_gluu"] +1. OP `auth_level_mapping` : +``` +"auth_level_mapping": { + "1": ["basic"], + "5": ["otp"], + "10": ["u2f"], + "11": ["super_gluu"], + "20": ["passkey"], + "30": ["usb_fido_key"] + } +``` + +- if current `acr_values=u2f` and `minimumAcrLevelAutoresolve=false` -> request is rejected +- if current `acr_values=u2f` and `minimumAcrLevelAutoresolve=true` -> `acr_values` set to `acr_values=passkey` and request continue processing +- if current `acr_values=usb_fido_key` -> current acr is higher then minimum. Thus nothing to do. + +If `minimumAcrPriorityList` is missing, then the AS can pick the next highest acr in the `auth_level_mapping`. In the example above, that would be `passkey`. \ No newline at end of file diff --git a/jans-auth-server/client/src/main/java/io/jans/as/client/RegisterRequest.java b/jans-auth-server/client/src/main/java/io/jans/as/client/RegisterRequest.java index 36a0b3da172..a830b1271a7 100644 --- a/jans-auth-server/client/src/main/java/io/jans/as/client/RegisterRequest.java +++ b/jans-auth-server/client/src/main/java/io/jans/as/client/RegisterRequest.java @@ -104,6 +104,9 @@ public class RegisterRequest extends BaseRequest { private SignatureAlgorithm tokenEndpointAuthSigningAlg; private Integer defaultMaxAge; private List defaultAcrValues; + private Integer minimumAcrLevel; + private Boolean minimumAcrLevelAutoresolve; + private List minimumAcrPriorityList; private String initiateLoginUri; private List groups; private List postLogoutRedirectUris; @@ -154,6 +157,7 @@ public RegisterRequest() { this.grantTypes = new ArrayList<>(); this.contacts = new ArrayList<>(); this.defaultAcrValues = new ArrayList<>(); + this.minimumAcrPriorityList = new ArrayList<>(); this.postLogoutRedirectUris = new ArrayList<>(); this.groups = new ArrayList<>(); this.requestUris = new ArrayList<>(); @@ -1039,6 +1043,60 @@ public void setDefaultMaxAge(Integer defaultMaxAge) { this.defaultMaxAge = defaultMaxAge; } + /** + * Gets minimum acr level + * + * @return minimum acr level + */ + public Integer getMinimumAcrLevel() { + return minimumAcrLevel; + } + + /** + * Sets minimum acr level + * + * @param minimumAcrLevel minimum acr level + */ + public void setMinimumAcrLevel(Integer minimumAcrLevel) { + this.minimumAcrLevel = minimumAcrLevel; + } + + /** + * Gets minimum acr level auto resolve + * + * @return minimum acr level auto resolve + */ + public Boolean getMinimumAcrLevelAutoresolve() { + return minimumAcrLevelAutoresolve; + } + + /** + * Sets minimum acr level auto resolve + * + * @param minimumAcrLevelAutoresolve minimum acr level auto resolve + */ + public void setMinimumAcrLevelAutoresolve(Boolean minimumAcrLevelAutoresolve) { + this.minimumAcrLevelAutoresolve = minimumAcrLevelAutoresolve; + } + + /** + * Gets minimum acr priority list + * + * @return minimum acr priority list + */ + public List getMinimumAcrPriorityList() { + return minimumAcrPriorityList; + } + + /** + * Sets minimum acr priority list + * + * @param minimumAcrPriorityList minimum acr priority list + */ + public void setMinimumAcrPriorityList(List minimumAcrPriorityList) { + this.minimumAcrPriorityList = minimumAcrPriorityList; + } + /** * Returns the Default requested Authentication Context Class Reference values. * @@ -1379,6 +1437,9 @@ public static RegisterRequest fromJson(JSONObject requestObject) throws JSONExce result.setPostLogoutRedirectUris(extractListByKey(requestObject, POST_LOGOUT_REDIRECT_URIS.toString())); result.setGroups(extractListByKey(requestObject, GROUPS.toString())); result.setDefaultAcrValues(extractListByKey(requestObject, DEFAULT_ACR_VALUES.toString())); + result.setMinimumAcrLevel(integerOrNull(requestObject, MINIMUM_ACR_LEVEL.toString())); + result.setMinimumAcrLevelAutoresolve(requestObject.optBoolean(MINIMUM_ACR_LEVEL_AUTORESOLVE.toString())); + result.setMinimumAcrPriorityList(extractListByKey(requestObject, MINIMUM_ACR_PRIORITY_LIST.toString())); result.setFrontChannelLogoutUri(requestObject.optString(FRONT_CHANNEL_LOGOUT_URI.toString())); result.setFrontChannelLogoutSessionRequired(requestObject.optBoolean(FRONT_CHANNEL_LOGOUT_SESSION_REQUIRED.toString())); result.setBackchannelLogoutUris(extractListByKey(requestObject, BACKCHANNEL_LOGOUT_URI.toString())); @@ -1588,6 +1649,15 @@ public void getParameters(BiFunction function) { if (defaultAcrValues != null && !defaultAcrValues.isEmpty()) { function.apply(DEFAULT_ACR_VALUES.toString(), toJSONArray(defaultAcrValues)); } + if (minimumAcrLevel != null) { + function.apply(MINIMUM_ACR_LEVEL.toString(), minimumAcrLevel.toString()); + } + if (minimumAcrLevelAutoresolve != null) { + function.apply(MINIMUM_ACR_LEVEL_AUTORESOLVE.toString(), minimumAcrLevelAutoresolve.toString()); + } + if (minimumAcrPriorityList != null) { + function.apply(MINIMUM_ACR_PRIORITY_LIST.toString(), toJSONArray(minimumAcrPriorityList)); + } if (StringUtils.isNotBlank(initiateLoginUri)) { function.apply(INITIATE_LOGIN_URI.toString(), initiateLoginUri); } diff --git a/jans-auth-server/common/src/test/resources/testng.xml b/jans-auth-server/common/src/test/resources/testng.xml index e2dd5fc8949..1fa27b78234 100644 --- a/jans-auth-server/common/src/test/resources/testng.xml +++ b/jans-auth-server/common/src/test/resources/testng.xml @@ -1,10 +1,11 @@ - + + diff --git a/jans-auth-server/docs/swagger.yaml b/jans-auth-server/docs/swagger.yaml index 1a9c115560d..cfd3246b4cb 100644 --- a/jans-auth-server/docs/swagger.yaml +++ b/jans-auth-server/docs/swagger.yaml @@ -1274,6 +1274,18 @@ paths: processing requests from the Client. items: type: string + minimum_acr_level: + type: integer + description: Integer value which sets minimum acr level. + example: 10 + minimum_acr_level_autoresolve: + type: boolean + description: boolean value, if false and minimum_acr_level is higher then current acr_values then reject request. If true - resolve acr according to either client's minimum_acr_priority_list or AS auth_level_mapping + minimum_acr_priority_list: + type: array + description: enables client to specify the acr order of preference, rather then just the next lowest integer value + items: + type: string groups: type: array description: Array of client's groups. @@ -1614,6 +1626,18 @@ paths: processing requests from the Client. items: type: string + minimum_acr_level: + type: integer + description: Integer value which sets minimum acr level. + example: 10 + minimum_acr_level_autoresolve: + type: boolean + description: boolean value, if false and minimum_acr_level is higher then current acr_values then reject request. If true - resolve acr according to either client's minimum_acr_priority_list or AS auth_level_mapping + minimum_acr_priority_list: + type: array + description: enables client to specify the acr order of preference, rather then just the next lowest integer value + items: + type: string initiate_login_uri: type: string description: Specifies the URI using the https scheme that the authorization server can call to initiate a login at the client. @@ -1956,6 +1980,18 @@ paths: processing requests from the Client. items: type: string + minimum_acr_level: + type: integer + description: Integer value which sets minimum acr level. + example: 10 + minimum_acr_level_autoresolve: + type: boolean + description: boolean value, if false and minimum_acr_level is higher then current acr_values then reject request. If true - resolve acr according to either client's minimum_acr_priority_list or AS auth_level_mapping + minimum_acr_priority_list: + type: array + description: enables client to specify the acr order of preference, rather then just the next lowest integer value + items: + type: string initiate_login_uri: type: string description: Specifies the URI using the https scheme that the authorization server can call to initiate a login at the client. @@ -4282,7 +4318,7 @@ paths: - SSA summary: Create SSA. description: Create `SSA` for the organization with `expiration` (optional). - operationId: post-register + operationId: post-register-ssa security: - bearer: [ ] requestBody: diff --git a/jans-auth-server/model/src/main/java/io/jans/as/model/register/RegisterRequestParam.java b/jans-auth-server/model/src/main/java/io/jans/as/model/register/RegisterRequestParam.java index 3af8051eac8..9f5bb4e5c54 100644 --- a/jans-auth-server/model/src/main/java/io/jans/as/model/register/RegisterRequestParam.java +++ b/jans-auth-server/model/src/main/java/io/jans/as/model/register/RegisterRequestParam.java @@ -226,6 +226,24 @@ public enum RegisterRequestParam { */ DEFAULT_ACR_VALUES("default_acr_values"), + /** + * Integer value which sets minimum acr level. + */ + MINIMUM_ACR_LEVEL("minimum_acr_level"), + + /** + * Boolean value, + * - if false and minimumAcrLevel is higher then current acr_values then reject request + * - if true - resolve acr according to either client's minimumAcrPriorityList or AS auth_level_mapping + */ + MINIMUM_ACR_LEVEL_AUTORESOLVE("minimum_acr_level_autoresolve"), + + /** + * Array of strings, + * - enables client to specify the acr order of preference, rather then just the next lowest integer value + */ + MINIMUM_ACR_PRIORITY_LIST("minimum_acr_priority_list"), + /** * URI using the https scheme that the Authorization Server can call to initiate a login at the Client. */ diff --git a/jans-auth-server/persistence-model/src/main/java/io/jans/as/persistence/model/ClientAttributes.java b/jans-auth-server/persistence-model/src/main/java/io/jans/as/persistence/model/ClientAttributes.java index cfa458061d1..0f58ba3538a 100644 --- a/jans-auth-server/persistence-model/src/main/java/io/jans/as/persistence/model/ClientAttributes.java +++ b/jans-auth-server/persistence-model/src/main/java/io/jans/as/persistence/model/ClientAttributes.java @@ -107,6 +107,41 @@ public class ClientAttributes implements Serializable { @JsonProperty("allowOfflineAccessWithoutConsent") private Boolean allowOfflineAccessWithoutConsent; + @JsonProperty("minimumAcrLevel") + private Integer minimumAcrLevel = -1; + + @JsonProperty("minimumAcrLevelAutoresolve") + private Boolean minimumAcrLevelAutoresolve; + + @JsonProperty("minimumAcrPriorityList") + private List minimumAcrPriorityList; + + public Boolean getMinimumAcrLevelAutoresolve() { + return minimumAcrLevelAutoresolve; + } + + public void setMinimumAcrLevelAutoresolve(Boolean minimumAcrLevelAutoresolve) { + this.minimumAcrLevelAutoresolve = minimumAcrLevelAutoresolve; + } + + public List getMinimumAcrPriorityList() { + if (minimumAcrPriorityList == null) minimumAcrPriorityList = new ArrayList<>(); + return minimumAcrPriorityList; + } + + public void setMinimumAcrPriorityList(List minimumAcrPriorityList) { + this.minimumAcrPriorityList = minimumAcrPriorityList; + } + + public Integer getMinimumAcrLevel() { + if (minimumAcrLevel == null) minimumAcrLevel = -1; + return minimumAcrLevel; + } + + public void setMinimumAcrLevel(Integer minimumAcrLevel) { + this.minimumAcrLevel = minimumAcrLevel; + } + public Boolean getAllowOfflineAccessWithoutConsent() { return allowOfflineAccessWithoutConsent; } @@ -378,6 +413,9 @@ public String toString() { ", publicSubjectIdentifierAttribute=" + publicSubjectIdentifierAttribute + ", redirectUrisRegex=" + redirectUrisRegex + ", allowOfflineAccessWithoutConsent=" + allowOfflineAccessWithoutConsent + + ", minimumAcrLevel=" + minimumAcrLevel + + ", minimumAcrLevelAutoresolve=" + minimumAcrLevelAutoresolve + + ", minimumAcrPriorityList=" + minimumAcrPriorityList + ", defaultPromptLogin=" + defaultPromptLogin + '}'; } diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/authorize/ws/rs/AuthorizeRestWebServiceImpl.java b/jans-auth-server/server/src/main/java/io/jans/as/server/authorize/ws/rs/AuthorizeRestWebServiceImpl.java index ef4c662e2b9..4def82ed7ad 100644 --- a/jans-auth-server/server/src/main/java/io/jans/as/server/authorize/ws/rs/AuthorizeRestWebServiceImpl.java +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/authorize/ws/rs/AuthorizeRestWebServiceImpl.java @@ -331,7 +331,7 @@ private ResponseBuilder authorize(AuthzRequest authzRequest) throws AcrChangedEx authorizeRestWebServiceValidator.validate(authzRequest, responseTypes, client); authorizeRestWebServiceValidator.validatePkce(authzRequest.getCodeChallenge(), authzRequest.getRedirectUriResponse()); - authzRequestService.setDefaultAcrsIfNeeded(authzRequest, client); + authzRequestService.setAcrsIfNeeded(authzRequest); checkOfflineAccessScopes(responseTypes, prompts, client, scopes); checkResponseType(authzRequest, responseTypes, client); diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/authorize/ws/rs/AuthzRequestService.java b/jans-auth-server/server/src/main/java/io/jans/as/server/authorize/ws/rs/AuthzRequestService.java index 2a6b564f16a..8e6f8ad3313 100644 --- a/jans-auth-server/server/src/main/java/io/jans/as/server/authorize/ws/rs/AuthzRequestService.java +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/authorize/ws/rs/AuthzRequestService.java @@ -1,5 +1,7 @@ package io.jans.as.server.authorize.ws.rs; +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; import com.google.common.collect.Lists; import com.google.common.collect.Sets; import io.jans.as.common.model.common.User; @@ -41,6 +43,7 @@ import io.jans.as.server.model.token.HandleTokenFactory; import io.jans.as.server.par.ws.rs.ParService; import io.jans.as.server.service.*; +import io.jans.as.server.service.external.ExternalAuthenticationService; import io.jans.as.server.util.ServerUtil; import jakarta.ejb.Stateless; import jakarta.inject.Inject; @@ -58,10 +61,8 @@ import java.net.URLDecoder; import java.nio.charset.StandardCharsets; import java.security.PrivateKey; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Set; +import java.util.*; +import java.util.concurrent.TimeUnit; import static io.jans.as.model.util.StringUtils.implode; import static org.apache.commons.lang3.BooleanUtils.isTrue; @@ -74,6 +75,8 @@ public class AuthzRequestService { public static final String INVALID_JWT_AUTHORIZATION_REQUEST = "Invalid JWT authorization request"; + private static final long ACR_TO_LEVEL_CACHE_LIFETIME_IN_MINUTES = 15; + private static final String ACR_TO_LEVEL_KEY = "ACR_TO_LEVEL_KEY"; @Inject private Logger log; @@ -108,6 +111,22 @@ public class AuthzRequestService { @Inject private RedirectionUriService redirectionUriService; + @Inject + private ExternalAuthenticationService externalAuthenticationService; + + private final Cache> acrToLevelCache = CacheBuilder.newBuilder() + .expireAfterWrite(ACR_TO_LEVEL_CACHE_LIFETIME_IN_MINUTES, TimeUnit.MINUTES).build(); + + public Map getAcrToLevelMap() { + Map map = acrToLevelCache.getIfPresent(ACR_TO_LEVEL_KEY); + if (map != null) { + return map; + } + map = externalAuthenticationService.acrToLevelMapping(); + acrToLevelCache.put(ACR_TO_LEVEL_KEY, map); + return map; + } + public void addDeviceSecretToSession(AuthzRequest authzRequest, SessionId sessionId) { if (BooleanUtils.isFalse(appConfiguration.getReturnDeviceSecretFromAuthzEndpoint())) { return; @@ -459,10 +478,73 @@ public void fillRedirectUriResponseforJARM(RedirectUriResponse redirectUriRespon } } - public void setDefaultAcrsIfNeeded(AuthzRequest authzRequest, Client client) { - if (StringUtils.isBlank(authzRequest.getAcrValues()) && !ArrayUtils.isEmpty(client.getDefaultAcrValues())) { - authzRequest.setAcrValues(implode(client.getDefaultAcrValues(), " ")); + public void setAcrsIfNeeded(AuthzRequest authzRequest) { + Client client = authzRequest.getClient(); + + // explicitly set acrs via getDefaultAcrValues() + if (StringUtils.isBlank(authzRequest.getAcrValues())) { + if (!ArrayUtils.isEmpty(client.getDefaultAcrValues())) { + authzRequest.setAcrValues(implode(client.getDefaultAcrValues(), " ")); + } + return; + } + + final int currentMinAcrLevel = getCurrentMinAcrLevel(authzRequest); + if (currentMinAcrLevel >= client.getAttributes().getMinimumAcrLevel()) { + return; // do nothing -> current level is enough + } + + if (BooleanUtils.isNotTrue(client.getAttributes().getMinimumAcrLevelAutoresolve())) { + log.error("Current acr level is less then minimum required. currentMinAcrLevel: {}, clientMinAcrLevel: {}", currentMinAcrLevel, client.getAttributes().getMinimumAcrLevel()); + throw new WebApplicationException(Response + .status(Response.Status.BAD_REQUEST) + .entity(errorResponseFactory.getErrorAsJson(AuthorizeErrorResponseType.INVALID_REQUEST, authzRequest.getState(), "Current acr level is less then minimum required by client")) + .build()); + } + + final Map acrToLevelMap = getAcrToLevelMap(); + if (client.getAttributes().getMinimumAcrPriorityList().isEmpty()) { // no priority list -> pick up next higher then minimum + for (Map.Entry entry : acrToLevelMap.entrySet()) { + if (currentMinAcrLevel < entry.getValue()) { + authzRequest.setAcrValues(entry.getKey()); + return; + } + } + } + + for (String acr : client.getAttributes().getMinimumAcrPriorityList()) { + final Integer acrLevel = acrToLevelMap.get(acr); + if (acrLevel != null && acrLevel >= currentMinAcrLevel) { + authzRequest.setAcrValues(acr); + return; + } + } + + log.error("Current acr level is less then minimum required by client. currentMinAcrLevel: {}, clientAttributes: {}", currentMinAcrLevel, client.getAttributes()); + throw new WebApplicationException(Response + .status(Response.Status.BAD_REQUEST) + .entity(errorResponseFactory.getErrorAsJson(AuthorizeErrorResponseType.INVALID_REQUEST, authzRequest.getState(), "Current acr level is less then minimum required by client:" + client.getClientId())) + .build()); + } + + public int getCurrentMinAcrLevel(AuthzRequest authzRequest) { + if (StringUtils.isBlank(authzRequest.getAcrValues())) { + return -1; + } + + Integer currentLevel = null; + final Map acrToLevelMap = getAcrToLevelMap(); + for (String acr : authzRequest.getAcrValuesList()) { + Integer level = acrToLevelMap.get(acr); + if (currentLevel == null) { + currentLevel = level; + continue; + } + if (level != null && level < currentLevel) { + currentLevel = level; + } } + return currentLevel != null ? currentLevel : -1; } public void createRedirectUriResponse(AuthzRequest authzRequest) { diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/register/ws/rs/RegisterService.java b/jans-auth-server/server/src/main/java/io/jans/as/server/register/ws/rs/RegisterService.java index 8bf8a968fce..782cf3aaccf 100644 --- a/jans-auth-server/server/src/main/java/io/jans/as/server/register/ws/rs/RegisterService.java +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/register/ws/rs/RegisterService.java @@ -256,6 +256,19 @@ public void updateClientFromRequestObject(Client client, RegisterRequest request client.setInitiateLoginUri(requestObject.getInitiateLoginUri()); } + final Integer minimumAcrLevel = requestObject.getMinimumAcrLevel(); + if (minimumAcrLevel != null) { + client.getAttributes().setMinimumAcrLevel(minimumAcrLevel); + } + final Boolean minimumAcrLevelAutoresolve = requestObject.getMinimumAcrLevelAutoresolve(); + if (minimumAcrLevelAutoresolve != null) { + client.getAttributes().setMinimumAcrLevelAutoresolve(minimumAcrLevelAutoresolve); + } + final List minimumAcrPriorityList = requestObject.getMinimumAcrPriorityList(); + if (minimumAcrPriorityList != null) { + client.getAttributes().setMinimumAcrPriorityList(new ArrayList<>(new HashSet<>(minimumAcrPriorityList))); + } + final List groups = requestObject.getGroups(); if (groups != null && !groups.isEmpty()) { client.setGroups(new HashSet<>(groups).toArray(new String[0])); // remove duplicates diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/register/ws/rs/action/RegisterCreateAction.java b/jans-auth-server/server/src/main/java/io/jans/as/server/register/ws/rs/action/RegisterCreateAction.java index 1532032640a..e6d685fefc7 100644 --- a/jans-auth-server/server/src/main/java/io/jans/as/server/register/ws/rs/action/RegisterCreateAction.java +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/register/ws/rs/action/RegisterCreateAction.java @@ -100,6 +100,8 @@ public Response createClient(String requestParams, HttpServletRequest httpReques Response.ResponseBuilder builder = Response.status(Response.Status.CREATED); OAuth2AuditLog oAuth2AuditLog = new OAuth2AuditLog(ServerUtil.getIpAddress(httpRequest), Action.CLIENT_REGISTRATION); try { + log.trace("Registration request = {}", requestParams); + final JSONObject requestObject = registerService.parseRequestObjectWithoutValidation(requestParams); final JSONObject softwareStatement = registerValidator.validateSoftwareStatement(httpRequest, requestObject); overrideRequestObjectFromSoftwareStatement(requestObject, softwareStatement); @@ -112,7 +114,6 @@ public Response createClient(String requestParams, HttpServletRequest httpReques log.info("Attempting to register client: applicationType = {}, clientName = {}, redirectUris = {}, isSecure = {}, sectorIdentifierUri = {}, defaultAcrValues = {}", r.getApplicationType(), r.getClientName(), r.getRedirectUris(), securityContext.isSecure(), r.getSectorIdentifierUri(), r.getDefaultAcrValues()); - log.trace("Registration request = {}", requestParams); registerValidator.validatePasswordGrantType(r); registerValidator.validateDcrAuthorizationWithClientCredentials(r); diff --git a/jans-auth-server/server/src/test/java/io/jans/as/server/authorize/ws/rs/AuthzRequestServiceTest.java b/jans-auth-server/server/src/test/java/io/jans/as/server/authorize/ws/rs/AuthzRequestServiceTest.java index d5b10badb44..fd86713b4d4 100644 --- a/jans-auth-server/server/src/test/java/io/jans/as/server/authorize/ws/rs/AuthzRequestServiceTest.java +++ b/jans-auth-server/server/src/test/java/io/jans/as/server/authorize/ws/rs/AuthzRequestServiceTest.java @@ -14,7 +14,9 @@ import io.jans.as.server.service.RedirectUriResponse; import io.jans.as.server.service.RedirectionUriService; import io.jans.as.server.service.RequestParameterService; +import io.jans.as.server.service.external.ExternalAuthenticationService; import jakarta.servlet.http.HttpServletRequest; +import jakarta.ws.rs.WebApplicationException; import org.apache.commons.lang.StringUtils; import org.mockito.InjectMocks; import org.mockito.Mock; @@ -23,6 +25,10 @@ import org.testng.annotations.Listeners; import org.testng.annotations.Test; +import java.util.Collections; +import java.util.HashMap; + +import static org.junit.Assert.fail; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import static org.testng.Assert.assertEquals; @@ -70,12 +76,155 @@ public class AuthzRequestServiceTest { @Mock private RedirectionUriService redirectionUriService; + @Mock + private ExternalAuthenticationService externalAuthenticationService; + + @Test + public void setAcrsIfNeeded_whenAcrsAreNotSetButDefaultAcrsAreConfigured_shouldSetDefaultAcrs() { + Client client = new Client(); + client.setDefaultAcrValues(new String[]{"passkey"}); + + AuthzRequest authzRequest = new AuthzRequest(); + authzRequest.setClient(client); + + authzRequestService.setAcrsIfNeeded(authzRequest); + assertEquals(authzRequest.getAcrValues(), "passkey"); + } + + @Test + public void setAcrsIfNeeded_whenAcrsHasEnoughLevel_shouldRaiseNoError() { + when(externalAuthenticationService.acrToLevelMapping()).thenReturn(new HashMap() {{ + put("basic", 1); + put("otp", 5); + put("u2f", 10); + put("super_gluu", 11); + put("passkey", 20); + put("usb_fido_key", 30); + }}); + + Client client = new Client(); + client.getAttributes().setMinimumAcrLevel(14); + + AuthzRequest authzRequest = new AuthzRequest(); + authzRequest.setAcrValues("passkey"); + authzRequest.setClient(client); + + authzRequestService.setAcrsIfNeeded(authzRequest); + assertEquals(authzRequest.getAcrValues(), "passkey"); + } + + @Test + public void setAcrsIfNeeded_whenAcrsHasNoEnoughLevel_shouldRaiseError() { + when(externalAuthenticationService.acrToLevelMapping()).thenReturn(new HashMap() {{ + put("basic", 1); + put("otp", 5); + put("u2f", 10); + put("super_gluu", 11); + put("passkey", 20); + put("usb_fido_key", 30); + }}); + + Client client = new Client(); + client.getAttributes().setMinimumAcrLevel(14); + + AuthzRequest authzRequest = new AuthzRequest(); + authzRequest.setAcrValues("super_gluu"); + authzRequest.setClient(client); + + try { + authzRequestService.setAcrsIfNeeded(authzRequest); + } catch (WebApplicationException e) { + return; // successfully got error + } + + fail("Failed to throw error."); + } + + @Test + public void setAcrsIfNeeded_whenAcrsHasNoEnoughLevelButAutoResolveIsTrue_shouldRaiseNoError() { + when(externalAuthenticationService.acrToLevelMapping()).thenReturn(new HashMap() {{ + put("basic", 1); + put("otp", 5); + put("u2f", 10); + put("super_gluu", 11); + put("passkey", 20); + put("usb_fido_key", 30); + }}); + + Client client = new Client(); + client.getAttributes().setMinimumAcrLevel(14); + client.getAttributes().setMinimumAcrLevelAutoresolve(true); + + AuthzRequest authzRequest = new AuthzRequest(); + authzRequest.setAcrValues("super_gluu"); + authzRequest.setClient(client); + + authzRequestService.setAcrsIfNeeded(authzRequest); + + assertEquals(authzRequest.getAcrValues(), "passkey"); + assertTrue(externalAuthenticationService.acrToLevelMapping().get(authzRequest.getAcrValues()) > 14); + } + + @Test + public void setAcrsIfNeeded_whenAcrsHasNoEnoughLevelButAutoResolveIsTrueAndPriorityListSet_shouldHaveAcrFromPriorityListSet() { + when(externalAuthenticationService.acrToLevelMapping()).thenReturn(new HashMap() {{ + put("basic", 1); + put("otp", 5); + put("u2f", 10); + put("super_gluu", 11); + put("passkey", 20); + put("usb_fido_key", 30); + }}); + + Client client = new Client(); + client.getAttributes().setMinimumAcrLevel(14); + client.getAttributes().setMinimumAcrLevelAutoresolve(true); + client.getAttributes().setMinimumAcrPriorityList(Collections.singletonList("usb_fido_key")); + + AuthzRequest authzRequest = new AuthzRequest(); + authzRequest.setAcrValues("super_gluu"); + authzRequest.setClient(client); + + authzRequestService.setAcrsIfNeeded(authzRequest); + + assertEquals(authzRequest.getAcrValues(), "usb_fido_key"); + } + + @Test + public void setAcrsIfNeeded_whenAcrsHasNoEnoughLevelButAutoResolveIsTrueAndPriorityListSet_shouldGetErrorIfPriorityListClashWithMinimalLevel() { + when(externalAuthenticationService.acrToLevelMapping()).thenReturn(new HashMap() {{ + put("basic", 1); + put("otp", 5); + put("u2f", 10); + put("super_gluu", 11); + put("passkey", 20); + put("usb_fido_key", 30); + }}); + + Client client = new Client(); + client.getAttributes().setMinimumAcrLevel(14); + client.getAttributes().setMinimumAcrLevelAutoresolve(true); + client.getAttributes().setMinimumAcrPriorityList(Collections.singletonList("u2f")); + + AuthzRequest authzRequest = new AuthzRequest(); + authzRequest.setAcrValues("super_gluu"); + authzRequest.setClient(client); + + try { + authzRequestService.setAcrsIfNeeded(authzRequest); + } catch (WebApplicationException e) { + return; // successfully got error + } + + fail("Must fail because priority list has acr which has level lower then minumumAcrLevel"); + } + @Test public void addDeviceSecretToSession_withoutUnabledConfiguration_shouldNotGenerateDeviceSecret() { when(appConfiguration.getReturnDeviceSecretFromAuthzEndpoint()).thenReturn(false); Client client = new Client(); - client.setGrantTypes(new GrantType[] { GrantType.AUTHORIZATION_CODE, GrantType.TOKEN_EXCHANGE}); + client.setGrantTypes(new GrantType[]{GrantType.AUTHORIZATION_CODE, GrantType.TOKEN_EXCHANGE}); AuthzRequest authzRequest = new AuthzRequest(); authzRequest.setScope("openid device_sso"); @@ -93,7 +242,7 @@ public void addDeviceSecretToSession_withoutDeviceSsoScope_shouldNotGenerateDevi when(appConfiguration.getReturnDeviceSecretFromAuthzEndpoint()).thenReturn(true); Client client = new Client(); - client.setGrantTypes(new GrantType[] { GrantType.AUTHORIZATION_CODE, GrantType.TOKEN_EXCHANGE}); + client.setGrantTypes(new GrantType[]{GrantType.AUTHORIZATION_CODE, GrantType.TOKEN_EXCHANGE}); AuthzRequest authzRequest = new AuthzRequest(); authzRequest.setScope("openid"); @@ -111,7 +260,7 @@ public void addDeviceSecretToSession_withDeviceSsoScope_shouldGenerateDeviceSecr when(appConfiguration.getReturnDeviceSecretFromAuthzEndpoint()).thenReturn(true); Client client = new Client(); - client.setGrantTypes(new GrantType[] { GrantType.AUTHORIZATION_CODE, GrantType.TOKEN_EXCHANGE}); + client.setGrantTypes(new GrantType[]{GrantType.AUTHORIZATION_CODE, GrantType.TOKEN_EXCHANGE}); AuthzRequest authzRequest = new AuthzRequest(); authzRequest.setRedirectUriResponse(new RedirectUriResponse(mock(RedirectUri.class), "", mock(HttpServletRequest.class), mock(ErrorResponseFactory.class))); @@ -131,7 +280,7 @@ public void addDeviceSecretToSession_withClientWithoutTokenExchangeGrantType_sho when(appConfiguration.getReturnDeviceSecretFromAuthzEndpoint()).thenReturn(true); Client client = new Client(); - client.setGrantTypes(new GrantType[] { GrantType.AUTHORIZATION_CODE}); + client.setGrantTypes(new GrantType[]{GrantType.AUTHORIZATION_CODE}); AuthzRequest authzRequest = new AuthzRequest(); authzRequest.setRedirectUriResponse(new RedirectUriResponse(mock(RedirectUri.class), "", mock(HttpServletRequest.class), mock(ErrorResponseFactory.class)));