Skip to content

Commit

Permalink
feat(jans-auth-server): added ability to set client expiration via DCR
Browse files Browse the repository at this point in the history
…#5057 (#5185)

* feat(jans-auth-server): add ability to set client expiration via DCR #5057

* feat(jans-auth-server): added test for client expiration via DCR #5057

* feat(jans-auth-server): flip parmeters for equals assertion #5057

* feat(jans-auth-server): corrected references in swagger and doc #5057

* feat(jans-auth-server): sonar fixes #5057
  • Loading branch information
yuriyz authored Jun 9, 2023
1 parent a40b06c commit a15054b
Show file tree
Hide file tree
Showing 11 changed files with 183 additions and 4 deletions.
8 changes: 8 additions & 0 deletions docs/admin/auth-server/endpoints/client-registration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ public class RegisterRequest extends BaseRequest {
private List<AuthenticationMethod> additionalTokenEndpointAuthMethods;
private SignatureAlgorithm tokenEndpointAuthSigningAlg;
private Integer defaultMaxAge;
private Integer lifetime;
private List<String> defaultAcrValues;
private Integer minimumAcrLevel;
private Boolean minimumAcrLevelAutoresolve;
Expand Down Expand Up @@ -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
*
Expand Down Expand Up @@ -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()));
Expand Down Expand Up @@ -1809,6 +1829,9 @@ public void getParameters(BiFunction<String, Object, Void> 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));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,17 @@ public class ClientAttributes implements Serializable {
@JsonProperty("minimumAcrPriorityList")
private List<String> 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;
}
Expand Down Expand Up @@ -430,6 +441,7 @@ public String toString() {
", additionalTokenEndpointAuthMethods=" + additionalTokenEndpointAuthMethods +
", minimumAcrPriorityList=" + minimumAcrPriorityList +
", defaultPromptLogin=" + defaultPromptLogin +
", requestedLifetime=" + requestedLifetime +
'}';
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> defaultAcrValues = requestObject.getDefaultAcrValues();
if (defaultAcrValues != null && !defaultAcrValues.isEmpty()) {
client.setDefaultAcrValues(listAsArrayWithoutDuplicates(defaultAcrValues));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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()) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
6 changes: 6 additions & 0 deletions jans-auth-server/server/src/test/resources/testng.xml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,13 @@
<class name="io.jans.as.server.authorize.ws.rs.AuthorizeRestWebServiceValidatorTest" />
<class name="io.jans.as.server.authorize.ws.rs.AuthorizeRestWebServiceImplTest" />
<class name="io.jans.as.server.authorize.ws.rs.AuthorizeActionTest" />

<!-- REGISTER -->
<class name="io.jans.as.server.register.ws.rs.SsaValidationConfigServiceTest" />
<class name="io.jans.as.server.register.ws.rs.action.RegisterCreateActionTest" />
<class name="io.jans.as.server.register.ws.rs.RegisterServiceTest" />

<!-- SESSION -->
<class name="io.jans.as.server.session.ws.rs.EndSessionRestWebServiceImplTest" />
<class name="io.jans.as.server.session.ws.rs.EndSessionServiceTest" />
</classes>
Expand Down
2 changes: 2 additions & 0 deletions jans-config-api/docs/jans-config-api-swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8191,6 +8191,8 @@ components:
dynamicRegistrationExpirationTime:
type: integer
format: int32
dcrForbidExpirationTimeInRequest:
type: boolean
dynamicRegistrationPersistClientAuthorizations:
type: boolean
trustedClientEnabled:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down

0 comments on commit a15054b

Please sign in to comment.