diff --git a/docs/admin/auth-server/endpoints/client-registration.md b/docs/admin/auth-server/endpoints/client-registration.md index 32117e0bd2b..6bb262c1fa6 100644 --- a/docs/admin/auth-server/endpoints/client-registration.md +++ b/docs/admin/auth-server/endpoints/client-registration.md @@ -31,6 +31,13 @@ The Janssen Authorization server will serve a DCR request if the following confi 1. `dynamicRegistrationEnabled` : `true` or `false` 2. `dynamicRegistrationExpirationTime` : Expiration time in seconds for clients created with dynamic registration, 0 or -1 means never expire +3. `dcrForbidExpirationTimeInRequest` : Boolean value specifying whether to allow to set client's expiration time in seconds during dynamic registration.. Default value is `false`. + +**Client expiration** + +Client expiration is set based on `dynamicRegistrationExpirationTime` AS configuration property or otherwise +if `dcrForbidExpirationTimeInRequest` is `false` then it can be requested in Dynamic Client Registration Request via `lifetime` parameter +which expected value in seconds. Configure the Janssen AS using steps explained in the [link](#curl-commands-to-configure-jans-auth-server) @@ -76,6 +83,7 @@ in example below: "backchannel_logout_session_required": false, "client_name": "my.jans.client", "par_lifetime": 600, + "lifetime": 3600, "spontaneous_scopes": [], "id_token_signed_response_alg": "RS256", "access_token_as_jwt": false, 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 559b94bb613..b84498d40c4 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 @@ -112,6 +112,7 @@ public class RegisterRequest extends BaseRequest { private List additionalTokenEndpointAuthMethods; private SignatureAlgorithm tokenEndpointAuthSigningAlg; private Integer defaultMaxAge; + private Integer lifetime; private List defaultAcrValues; private Integer minimumAcrLevel; private Boolean minimumAcrLevelAutoresolve; @@ -1068,6 +1069,24 @@ public void setDefaultMaxAge(Integer defaultMaxAge) { this.defaultMaxAge = defaultMaxAge; } + /** + * Gets client life time + * + * @return client life time + */ + public Integer getLifetime() { + return lifetime; + } + + /** + * Sets client life time + * + * @param lifetime life time + */ + public void setLifetime(Integer lifetime) { + this.lifetime = lifetime; + } + /** * Gets minimum acr level * @@ -1599,6 +1618,7 @@ public static RegisterRequest fromJson(JSONObject requestObject) throws JSONExce result.setParLifetime(integerOrNull(requestObject, PAR_LIFETIME.toString())); result.setRequirePar(booleanOrNull(requestObject, REQUIRE_PAR.toString())); result.setDefaultMaxAge(integerOrNull(requestObject, DEFAULT_MAX_AGE.toString())); + result.setLifetime(integerOrNull(requestObject, LIFETIME.toString())); result.setTlsClientAuthSubjectDn(requestObject.optString(TLS_CLIENT_AUTH_SUBJECT_DN.toString())); result.setAllowSpontaneousScopes(requestObject.optBoolean(ALLOW_SPONTANEOUS_SCOPES.toString())); result.setSpontaneousScopes(extractListByKey(requestObject, SPONTANEOUS_SCOPES.toString())); @@ -1809,6 +1829,9 @@ public void getParameters(BiFunction function) { if (defaultMaxAge != null) { function.apply(DEFAULT_MAX_AGE.toString(), defaultMaxAge.toString()); } + if (lifetime != null) { + function.apply(LIFETIME.toString(), lifetime.toString()); + } if (defaultAcrValues != null && !defaultAcrValues.isEmpty()) { function.apply(DEFAULT_ACR_VALUES.toString(), toJSONArray(defaultAcrValues)); } diff --git a/jans-auth-server/model/src/main/java/io/jans/as/model/configuration/AppConfiguration.java b/jans-auth-server/model/src/main/java/io/jans/as/model/configuration/AppConfiguration.java index 1f45b602241..e683a8477d7 100644 --- a/jans-auth-server/model/src/main/java/io/jans/as/model/configuration/AppConfiguration.java +++ b/jans-auth-server/model/src/main/java/io/jans/as/model/configuration/AppConfiguration.java @@ -633,6 +633,9 @@ public class AppConfiguration implements Configuration { private Boolean returnDeviceSecretFromAuthzEndpoint = false; // DCR + @DocProperty(description = "Boolean value specifying whether to allow to set client's expiration time in seconds during dynamic registration.", defaultValue = "false") + private Boolean dcrForbidExpirationTimeInRequest = false; + @DocProperty(description = "Boolean value enables DCR signature validation. Default is false", defaultValue = "false") private Boolean dcrSignatureValidationEnabled = false; @@ -2183,6 +2186,17 @@ public void setJansId(String jansId) { this.jansId = jansId; } + public Boolean getDcrForbidExpirationTimeInRequest() { + if (dcrForbidExpirationTimeInRequest == null) { + dcrForbidExpirationTimeInRequest = false; + } + return dcrForbidExpirationTimeInRequest; + } + + public void setDcrForbidExpirationTimeInRequest(Boolean dcrForbidExpirationTimeInRequest) { + this.dcrForbidExpirationTimeInRequest = dcrForbidExpirationTimeInRequest; + } + public int getDynamicRegistrationExpirationTime() { return dynamicRegistrationExpirationTime; } 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 18b6d0fa127..3ce6d1b1225 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 @@ -219,6 +219,11 @@ public enum RegisterRequestParam { */ DEFAULT_MAX_AGE("default_max_age"), + /** + * Client lifetime in seconds. + */ + LIFETIME("lifetime"), + /** * Boolean value specifying whether the auth_time Claim in the ID Token is required. It is required when the value * is true. The auth_time Claim request in the Request Object overrides this setting. 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 c060216449a..797d975fffc 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 @@ -119,6 +119,17 @@ public class ClientAttributes implements Serializable { @JsonProperty("minimumAcrPriorityList") private List minimumAcrPriorityList; + @JsonProperty("requestedLifetime") + private Integer requestedLifetime; // in seconds + + public Integer getRequestedLifetime() { + return requestedLifetime; + } + + public void setRequestedLifetime(Integer requestedLifetime) { + this.requestedLifetime = requestedLifetime; + } + public Boolean getMinimumAcrLevelAutoresolve() { return minimumAcrLevelAutoresolve; } @@ -430,6 +441,7 @@ public String toString() { ", additionalTokenEndpointAuthMethods=" + additionalTokenEndpointAuthMethods + ", minimumAcrPriorityList=" + minimumAcrPriorityList + ", defaultPromptLogin=" + defaultPromptLogin + + ", requestedLifetime=" + requestedLifetime + '}'; } } 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 2c36ed98b4e..daaaf4f0f08 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 @@ -325,6 +325,9 @@ public void updateClientFromRequestObject(Client client, RegisterRequest request if (requestObject.getDefaultMaxAge() != null) { client.setDefaultMaxAge(requestObject.getDefaultMaxAge()); } + if (!update) { // do not allow update it. It can be set only during creation + client.getAttributes().setRequestedLifetime(requestObject.getLifetime()); + } List defaultAcrValues = requestObject.getDefaultAcrValues(); if (defaultAcrValues != null && !defaultAcrValues.isEmpty()) { client.setDefaultAcrValues(listAsArrayWithoutDuplicates(defaultAcrValues)); 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 e63bba7897e..b4f04a93054 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 @@ -44,6 +44,7 @@ import java.net.URI; import java.util.*; +import static org.apache.commons.lang.BooleanUtils.isFalse; import static org.apache.commons.lang3.BooleanUtils.isTrue; /** @@ -145,13 +146,14 @@ public Response createClient(String requestParams, HttpServletRequest httpReques final Calendar calendar = new GregorianCalendar(TimeZone.getTimeZone("UTC")); client.setClientIdIssuedAt(calendar.getTime()); - if (appConfiguration.getDynamicRegistrationExpirationTime() > 0) { // #883 : expiration can be -1, mean does not expire - calendar.add(Calendar.SECOND, appConfiguration.getDynamicRegistrationExpirationTime()); + final int lifetime = getClientLifetime(r); + if (lifetime > 0) { // #883 : expiration can be -1, mean does not expire + calendar.add(Calendar.SECOND, lifetime); client.setClientSecretExpiresAt(calendar.getTime()); client.setExpirationDate(calendar.getTime()); - client.setTtl(appConfiguration.getDynamicRegistrationExpirationTime()); + client.setTtl(lifetime); } - client.setDeletable(client.getClientSecretExpiresAt() != null); + client.setDeletable(client.getExpirationDate() != null); setClientName(r, client); @@ -195,6 +197,15 @@ public Response createClient(String requestParams, HttpServletRequest httpReques return builder.build(); } + public int getClientLifetime(RegisterRequest registerRequest) { + int lifetime = appConfiguration.getDynamicRegistrationExpirationTime(); + final Integer requestedLifeTime = registerRequest.getLifetime(); + if (isFalse(appConfiguration.getDcrForbidExpirationTimeInRequest()) && requestedLifeTime != null) { + return requestedLifeTime; + } + return lifetime; + } + private void executeDynamicScrypt(RegisterRequest r, Client client, HttpServletRequest httpRequest) { boolean registerClient = true; if (externalDynamicClientRegistrationService.isEnabled()) { diff --git a/jans-auth-server/server/src/test/java/io/jans/as/server/register/ws/rs/action/RegisterCreateActionTest.java b/jans-auth-server/server/src/test/java/io/jans/as/server/register/ws/rs/action/RegisterCreateActionTest.java new file mode 100644 index 00000000000..9afc8eb80c4 --- /dev/null +++ b/jans-auth-server/server/src/test/java/io/jans/as/server/register/ws/rs/action/RegisterCreateActionTest.java @@ -0,0 +1,94 @@ +package io.jans.as.server.register.ws.rs.action; + +import io.jans.as.client.RegisterRequest; +import io.jans.as.common.service.common.InumService; +import io.jans.as.model.config.StaticConfiguration; +import io.jans.as.model.configuration.AppConfiguration; +import io.jans.as.model.error.ErrorResponseFactory; +import io.jans.as.server.audit.ApplicationAuditLogger; +import io.jans.as.server.model.registration.RegisterParamsValidator; +import io.jans.as.server.register.ws.rs.RegisterJsonService; +import io.jans.as.server.register.ws.rs.RegisterService; +import io.jans.as.server.register.ws.rs.RegisterValidator; +import io.jans.as.server.service.ClientService; +import io.jans.as.server.service.external.ExternalDynamicClientRegistrationService; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.testng.MockitoTestNGListener; +import org.slf4j.Logger; +import org.testng.annotations.Listeners; +import org.testng.annotations.Test; + +import static org.mockito.Mockito.when; +import static org.testng.Assert.assertEquals; + +/** + * @author Yuriy Z + */ +@SuppressWarnings("java:S5979") +@Listeners(MockitoTestNGListener.class) +public class RegisterCreateActionTest { + + @InjectMocks + @Spy + private RegisterCreateAction registerCreateAction; + + @Mock + private Logger log; + + @Mock + private ApplicationAuditLogger applicationAuditLogger; + + @Mock + private ErrorResponseFactory errorResponseFactory; + + @Mock + private InumService inumService; + + @Mock + private ClientService clientService; + + @Mock + private ExternalDynamicClientRegistrationService externalDynamicClientRegistrationService; + + @Mock + private RegisterParamsValidator registerParamsValidator; + + @Mock + private AppConfiguration appConfiguration; + + @Mock + private StaticConfiguration staticConfiguration; + + @Mock + private RegisterValidator registerValidator; + + @Mock + private RegisterJsonService registerJsonService; + + @Mock + private RegisterService registerService; + + @Test + public void getClientLifetime_whenUpdateForbiddenInRequest_shouldReturnValueFromServerConfiguration() { + when(appConfiguration.getDynamicRegistrationExpirationTime()).thenReturn(10); + when(appConfiguration.getDcrForbidExpirationTimeInRequest()).thenReturn(true); + + RegisterRequest request = new RegisterRequest(); + request.setLifetime(5); + + assertEquals(registerCreateAction.getClientLifetime(request), 10); + } + + @Test + public void getClientLifetime_whenUpdateIsNotForbiddenInRequest_shouldReturnValueFromRequest() { + when(appConfiguration.getDynamicRegistrationExpirationTime()).thenReturn(10); + when(appConfiguration.getDcrForbidExpirationTimeInRequest()).thenReturn(false); + + RegisterRequest request = new RegisterRequest(); + request.setLifetime(5); + + assertEquals(registerCreateAction.getClientLifetime(request), 5); + } +} diff --git a/jans-auth-server/server/src/test/resources/testng.xml b/jans-auth-server/server/src/test/resources/testng.xml index 5392f35cf10..8e4f5c8a9cc 100644 --- a/jans-auth-server/server/src/test/resources/testng.xml +++ b/jans-auth-server/server/src/test/resources/testng.xml @@ -29,7 +29,13 @@ + + + + + + diff --git a/jans-config-api/docs/jans-config-api-swagger.yaml b/jans-config-api/docs/jans-config-api-swagger.yaml index 3a5a2aeb2bb..b8ed3ffbc67 100644 --- a/jans-config-api/docs/jans-config-api-swagger.yaml +++ b/jans-config-api/docs/jans-config-api-swagger.yaml @@ -8191,6 +8191,8 @@ components: dynamicRegistrationExpirationTime: type: integer format: int32 + dcrForbidExpirationTimeInRequest: + type: boolean dynamicRegistrationPersistClientAuthorizations: type: boolean trustedClientEnabled: diff --git a/jans-linux-setup/jans_setup/templates/jans-auth/jans-auth-config.json b/jans-linux-setup/jans_setup/templates/jans-auth/jans-auth-config.json index 40094acb01f..b3a78c9b7d9 100644 --- a/jans-linux-setup/jans_setup/templates/jans-auth/jans-auth-config.json +++ b/jans-linux-setup/jans_setup/templates/jans-auth/jans-auth-config.json @@ -319,6 +319,7 @@ "jansOpenIdConnectVersion":"openidconnect-1.0", "jansId":"https://%(hostname)s/oxid/service/jans/inum", "dynamicRegistrationExpirationTime":-1, + "dcrForbidExpirationTimeInRequest": false, "dynamicRegistrationPersistClientAuthorizations":true, "trustedClientEnabled":true, "skipAuthorizationForOpenIdScopeAndPairwiseId": false,