From 51101e4c65f838853f6bbc9c3f50961bacad6f7f Mon Sep 17 00:00:00 2001 From: YuriyZ Date: Fri, 28 Jun 2024 17:49:02 +0300 Subject: [PATCH] feat(jans-auth-server): Token Status List support (#8620) * chore(jans-auth-server): renamed OXAUTH_UMA_TICKET -> UMA_TICKET Signed-off-by: YuriyZ * feat(jans-auth-server): Token Status List support https://github.com/JanssenProject/jans/issues/8562 Signed-off-by: YuriyZ * fix(jans-auth-server): corrected requestContext and azd decoding https://github.com/JanssenProject/jans/issues/8562 Signed-off-by: YuriyZ * feat(jans-auth-server): added token status list endpoint and status claim with index. https://github.com/JanssenProject/jans/issues/8562 Signed-off-by: YuriyZ * feat(jans-auth): new cluster beans and services Signed-off-by: Yuriy Movchan * feat(jans-auth-server): added head index to list https://github.com/JanssenProject/jans/issues/8562 Signed-off-by: YuriyZ * feat(jans-auth): move beans to core model Signed-off-by: Yuriy Movchan * feat(jans-auth): add index range to TokenPool Signed-off-by: Yuriy Movchan * feat(jans-auth-server): added application/statuslist+json support https://github.com/JanssenProject/jans/issues/8562 Signed-off-by: YuriyZ * feat(jans-auth): add methods to allocate/release TokenPool Signed-off-by: Yuriy Movchan * feat(jans-auth): fix TokenPool sort Signed-off-by: Yuriy Movchan * feat(jans-auth): implement method to get nextIndex for token Signed-off-by: Yuriy Movchan * feat(jans-auth): implement method to get nextIndex for token Signed-off-by: Yuriy Movchan * feat(jans-auth): instead of using token list status use expiration date Signed-off-by: Yuriy Movchan * fix(jans-auth-server): fixed index during list joins and npe on nextIndex. https://github.com/JanssenProject/jans/issues/8562 Signed-off-by: YuriyZ * feat(jans-auth-server): populate statusListIndex in access and id tokens https://github.com/JanssenProject/jans/issues/8562 Signed-off-by: YuriyZ * feat(jans-auth): add ClusterNode services Signed-off-by: Yuriy Movchan * feat(jans-auth): add node base dn Signed-off-by: Yuriy Movchan * feat(jans-auth-server): added status list update on revoke https://github.com/JanssenProject/jans/issues/8562 Signed-off-by: YuriyZ * fix after merge Signed-off-by: YuriyZ * feat(jans-auth): add schema for new entries Signed-off-by: Yuriy Movchan * feat(jans-auth): fix allocate Signed-off-by: Yuriy Movchan * feat(jans-auth): fix cluster nodes expiration Signed-off-by: Yuriy Movchan * feat(jans-auth-server): added status list as jwt support https://github.com/JanssenProject/jans/issues/8562 Signed-off-by: YuriyZ * feat(jans-auth): Deprecate TokenPoolStatus Signed-off-by: Yuriy Movchan * feat(jans-auth): implement updateWithLock for concurent lock on revoke Signed-off-by: Yuriy Movchan * feat(jans-auth-server): use updateWithLock during status update index https://github.com/JanssenProject/jans/issues/8562 Signed-off-by: YuriyZ * feat(jans-auth-server): update status list on token revoke in separate thread https://github.com/JanssenProject/jans/issues/8562 Signed-off-by: YuriyZ * feat(jans-auth-server): renamed TokenPool -> StatusTokenPool, TokenPoolService -> StatusTokenPoolService https://github.com/JanssenProject/jans/issues/8562 Signed-off-by: YuriyZ * feat(jans-auth-server): removed token head index (we are using status token pools instead) https://github.com/JanssenProject/jans/issues/8562 Signed-off-by: YuriyZ * feat(jans-auth-server): added status list to swagger https://github.com/JanssenProject/jans/issues/8562 Signed-off-by: YuriyZ * feat(jans-auth-server): added ou=node,o=jans to config https://github.com/JanssenProject/jans/issues/8562 Signed-off-by: YuriyZ * feat(jans-auth-server): throw configuration exception if node baseDn is missed https://github.com/JanssenProject/jans/issues/8562 Signed-off-by: YuriyZ * feat(jans-auth-server): set status_list feature flag enabled by default https://github.com/JanssenProject/jans/issues/8562 Signed-off-by: YuriyZ * fix(jans-auth-server): fixed node allocation https://github.com/JanssenProject/jans/issues/8562 Signed-off-by: YuriyZ * fix(jans-auth-server): corrected bug in getClusterNodeLast https://github.com/JanssenProject/jans/issues/8562 Signed-off-by: YuriyZ * feat(jans-auth-server): keep lockKey static and save in jansNode after locking https://github.com/JanssenProject/jans/issues/8562 Signed-off-by: YuriyZ * fix(jans-auth-server): different fixes for cluster node management https://github.com/JanssenProject/jans/issues/8562 Signed-off-by: YuriyZ * fix(jans-auth-server): fixed allocation of status index pools https://github.com/JanssenProject/jans/issues/8562 Signed-off-by: YuriyZ * chore(jans-auth-server): added more logs for status index pool allocation https://github.com/JanssenProject/jans/issues/8562 Signed-off-by: YuriyZ * feat(jans-auth): igore timezone when DB is PostgresSQL Signed-off-by: Yuriy Movchan * feat(jans-auth): fetch all node entries if DB is LDAP Signed-off-by: Yuriy Movchan * feat(jans-auth-server): added status list client https://github.com/JanssenProject/jans/issues/8562 Signed-off-by: YuriyZ * fix(jans-auth-server): fixed pool allocation https://github.com/JanssenProject/jans/issues/8562 Signed-off-by: YuriyZ * chore(jans-auth-server): renamed endpoint /token_status_list -> /status_list https://github.com/JanssenProject/jans/issues/8562 Signed-off-by: YuriyZ * feat(jans-orm): resovle bean property name with AttributeName #8773 * chore(jans-auth-server): renamed token_status_list -> status_list https://github.com/JanssenProject/jans/issues/8562 Signed-off-by: YuriyZ * chore(jans-auth-server): token statuses VALID - 0, INVALID - 1 https://github.com/JanssenProject/jans/issues/8562 Signed-off-by: YuriyZ * chore(jans-auth-server): moved status list to model for re-using https://github.com/JanssenProject/jans/issues/8562 Signed-off-by: YuriyZ * feat(jans-auth-server): added batch index update and fixed concurrent update issue https://github.com/JanssenProject/jans/issues/8562 Signed-off-by: YuriyZ * feat(jans-auth-server): use new index update method in existing revoke code https://github.com/JanssenProject/jans/issues/8562 Signed-off-by: YuriyZ * fix(jans-auth-server): fixed status pool index joining https://github.com/JanssenProject/jans/issues/8562 Signed-off-by: YuriyZ * chore(jans-auth-server): code improvements https://github.com/JanssenProject/jans/issues/8562 Signed-off-by: YuriyZ * test(jans-auth-server): added full integration test for status list https://github.com/JanssenProject/jans/issues/8562 Signed-off-by: YuriyZ * test(jans-auth-server): added test for CN case https://github.com/JanssenProject/jans/issues/8562 Signed-off-by: YuriyZ * feat(jans-auth-server): mark indexes which we are about to re-use as VALID https://github.com/JanssenProject/jans/issues/8562 Signed-off-by: YuriyZ * code re-format Signed-off-by: YuriyZ * docs(config-api): regenerating config swagger api Signed-off-by: pujavs --------- Signed-off-by: YuriyZ Signed-off-by: Yuriy Movchan Signed-off-by: pujavs Co-authored-by: Yuriy Movchan Co-authored-by: pujavs Co-authored-by: Mohammad Abudayyeh <47318409+moabu@users.noreply.github.com> --- .../java/io/jans/agama/dsl/Transpiler.java | 41 +-- .../as/client/OpenIdConfigurationClient.java | 1 + .../client/OpenIdConfigurationResponse.java | 20 ++ .../io/jans/as/client/StatusListClient.java | 54 +++ .../io/jans/as/client/StatusListRequest.java | 18 + .../io/jans/as/client/StatusListResponse.java | 98 ++++++ .../test/java/io/jans/as/client/BaseTest.java | 11 + .../ws/rs/GrantTypesRestrictionHttpTest.java | 32 +- .../RegistrationRestWebServiceHttpTest.java | 4 +- .../ws/rs/token/StatusListHttpTest.java | 169 ++++++++++ .../AccessProtectedResourceFlowHttpTest.java | 6 +- ...ntAuthenticationByAccessTokenHttpTest.java | 2 +- .../rs/uma/UmaSpontaneousScopeHttpTest.java | 2 +- .../client/src/test/resources/testng.xml | 6 + jans-auth-server/docs/swagger.yaml | 47 +++ .../jans/as/model/common/FeatureFlagType.java | 3 + .../io/jans/as/model/common/GrantType.java | 2 +- .../as/model/config/BaseDnConfiguration.java | 19 ++ .../io/jans/as/model/config/Constants.java | 3 + .../model/configuration/AppConfiguration.java | 49 +++ .../ConfigurationResponseClaim.java | 1 + .../as/model/error/ErrorResponseFactory.java | 9 + .../java/io/jans/as/model/jwt/JwtType.java | 3 +- jans-auth-server/server/conf/jans-config.json | 3 +- .../server/conf/jans-static-conf.json | 4 +- .../common/AbstractAuthorizationGrant.java | 9 + .../as/server/model/common/AbstractToken.java | 20 ++ .../model/common/AuthorizationGrant.java | 26 ++ .../model/common/AuthorizationGrantList.java | 7 + .../server/model/common/ExecutionContext.java | 9 + .../as/server/model/token/IdTokenFactory.java | 6 + .../jans/as/server/model/token/JwtSigner.java | 6 +- .../revoke/RevokeRestWebServiceImpl.java | 21 +- .../as/server/service/AppInitializer.java | 5 + .../jans/as/server/service/CleanerTimer.java | 1 - .../jans/as/server/service/ClientService.java | 26 +- .../as/server/service/DiscoveryService.java | 35 +- .../jans/as/server/service/GrantService.java | 50 ++- .../server/service/ResteasyInitializer.java | 2 + .../jans/as/server/service/ScopeService.java | 37 +-- .../cdi/event/TokenPoolUpdateEvent.java | 13 + .../service/cluster/ClusterNodeManager.java | 107 ++++++ .../service/cluster/ClusterNodeService.java | 251 ++++++++++++++ .../cluster/StatusIndexPoolService.java | 309 ++++++++++++++++++ .../service/token/StatusListIndexService.java | 133 ++++++++ .../service/token/StatusListService.java | 201 ++++++++++++ .../ws/rs/StatusListRestWebService.java | 39 +++ .../as/server/uma/service/UmaRptService.java | 2 +- .../uma/service/UmaValidationService.java | 4 +- .../as/server/uma/ws/rs/UmaMetadataWS.java | 2 +- .../as/server/service/GrantServiceTest.java | 4 +- .../java/io/jans/as/test/UmaTestUtil.java | 2 +- .../docs/jans-config-api-swagger.yaml | 18 +- .../plugins/docs/lock-plugin-swagger.yaml | 2 + .../plugins/docs/scim-plugin-swagger.yaml | 4 + .../plugins/docs/user-mgt-plugin-swagger.yaml | 4 +- .../io/jans/model/tokenstatus/StatusList.java | 170 ++++++++++ .../jans/model/tokenstatus/TokenStatus.java | 34 ++ .../io/jans/model/cluster/ClusterNode.java | 98 ++++++ .../io/jans/model/token/StatusIndexPool.java | 142 ++++++++ .../io/jans/model/token/TokenAttributes.java | 10 + .../model/tokenstatus/StatusListTest.java | 127 +++++++ .../jans_setup/schema/jans_schema.json | 85 +++++ .../jans_setup/templates/base.ldif | 10 + .../templates/jans-auth/jans-auth-config.json | 5 +- .../jans-auth/jans-auth-static-conf.json | 4 +- .../io/jans/orm/impl/BaseEntryManager.java | 81 ++++- .../persistence/SqlEntryManagerSample.java | 7 +- .../jans/orm/sql/impl/SqlFilterConverter.java | 7 +- .../operation/impl/SqlConnectionProvider.java | 10 + .../impl/SqlOperationServiceImpl.java | 7 +- 71 files changed, 2595 insertions(+), 164 deletions(-) create mode 100644 jans-auth-server/client/src/main/java/io/jans/as/client/StatusListClient.java create mode 100644 jans-auth-server/client/src/main/java/io/jans/as/client/StatusListRequest.java create mode 100644 jans-auth-server/client/src/main/java/io/jans/as/client/StatusListResponse.java create mode 100644 jans-auth-server/client/src/test/java/io/jans/as/client/ws/rs/token/StatusListHttpTest.java create mode 100644 jans-auth-server/server/src/main/java/io/jans/as/server/service/cdi/event/TokenPoolUpdateEvent.java create mode 100644 jans-auth-server/server/src/main/java/io/jans/as/server/service/cluster/ClusterNodeManager.java create mode 100644 jans-auth-server/server/src/main/java/io/jans/as/server/service/cluster/ClusterNodeService.java create mode 100644 jans-auth-server/server/src/main/java/io/jans/as/server/service/cluster/StatusIndexPoolService.java create mode 100644 jans-auth-server/server/src/main/java/io/jans/as/server/service/token/StatusListIndexService.java create mode 100644 jans-auth-server/server/src/main/java/io/jans/as/server/service/token/StatusListService.java create mode 100644 jans-auth-server/server/src/main/java/io/jans/as/server/status/ws/rs/StatusListRestWebService.java create mode 100644 jans-core/model/src/main/java/io/jans/model/tokenstatus/StatusList.java create mode 100644 jans-core/model/src/main/java/io/jans/model/tokenstatus/TokenStatus.java create mode 100644 jans-core/service/src/main/java/io/jans/model/cluster/ClusterNode.java create mode 100644 jans-core/service/src/main/java/io/jans/model/token/StatusIndexPool.java create mode 100644 jans-core/service/src/test/java/io/jans/model/tokenstatus/StatusListTest.java diff --git a/agama/transpiler/src/main/java/io/jans/agama/dsl/Transpiler.java b/agama/transpiler/src/main/java/io/jans/agama/dsl/Transpiler.java index 2e72395f428..179f54e02e7 100644 --- a/agama/transpiler/src/main/java/io/jans/agama/dsl/Transpiler.java +++ b/agama/transpiler/src/main/java/io/jans/agama/dsl/Transpiler.java @@ -5,47 +5,24 @@ import freemarker.template.Template; import freemarker.template.TemplateException; import freemarker.template.TemplateExceptionHandler; - import io.jans.agama.antlr.AuthnFlowLexer; import io.jans.agama.antlr.AuthnFlowParser; -import io.jans.agama.dsl.error.SyntaxException; import io.jans.agama.dsl.error.RecognitionErrorListener; +import io.jans.agama.dsl.error.SyntaxException; +import net.sf.saxon.dom.NodeOverNodeInfo; +import net.sf.saxon.s9api.*; +import net.sf.saxon.sapling.SaplingDocument; +import org.antlr.v4.runtime.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.OutputStream; -import java.io.OutputStreamWriter; -import java.io.Reader; -import java.io.StringWriter; +import java.io.*; import java.nio.file.Files; import java.nio.file.Paths; -import java.util.HashSet; -import java.util.List; -import java.util.Objects; -import java.util.Optional; -import java.util.Map; -import java.util.Set; +import java.util.*; import java.util.stream.Collectors; import java.util.stream.Stream; -import net.sf.saxon.dom.NodeOverNodeInfo; -import net.sf.saxon.s9api.Processor; -import net.sf.saxon.s9api.SaxonApiException; -import net.sf.saxon.s9api.XPathCompiler; -import net.sf.saxon.s9api.XdmItem; -import net.sf.saxon.s9api.XdmNode; -import net.sf.saxon.sapling.SaplingDocument; - -import org.antlr.v4.runtime.CharStream; -import org.antlr.v4.runtime.CharStreams; -import org.antlr.v4.runtime.CommonTokenStream; -import org.antlr.v4.runtime.RecognitionException; -import org.antlr.v4.runtime.Token; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import static java.nio.charset.StandardCharsets.UTF_8; public class Transpiler { diff --git a/jans-auth-server/client/src/main/java/io/jans/as/client/OpenIdConfigurationClient.java b/jans-auth-server/client/src/main/java/io/jans/as/client/OpenIdConfigurationClient.java index 60c11117619..e1061deb615 100644 --- a/jans-auth-server/client/src/main/java/io/jans/as/client/OpenIdConfigurationClient.java +++ b/jans-auth-server/client/src/main/java/io/jans/as/client/OpenIdConfigurationClient.java @@ -105,6 +105,7 @@ public static void parse(String json, OpenIdConfigurationResponse response) { response.setIssuer(jsonObj.optString(ISSUER, null)); response.setAuthorizationEndpoint(jsonObj.optString(AUTHORIZATION_ENDPOINT, null)); response.setAuthorizationChallengeEndpoint(jsonObj.optString(AUTHORIZATION_CHALLENGE_ENDPOINT, null)); + response.setStatusListEndpoint(jsonObj.optString(STATUS_LIST_ENDPOINT, null)); response.setTokenEndpoint(jsonObj.optString(TOKEN_ENDPOINT, null)); response.setRevocationEndpoint(jsonObj.optString(REVOCATION_ENDPOINT, null)); response.setSessionRevocationEndpoint(jsonObj.optString(SESSION_REVOCATION_ENDPOINT, null)); diff --git a/jans-auth-server/client/src/main/java/io/jans/as/client/OpenIdConfigurationResponse.java b/jans-auth-server/client/src/main/java/io/jans/as/client/OpenIdConfigurationResponse.java index 88d1722c5ec..250d49be2bf 100644 --- a/jans-auth-server/client/src/main/java/io/jans/as/client/OpenIdConfigurationResponse.java +++ b/jans-auth-server/client/src/main/java/io/jans/as/client/OpenIdConfigurationResponse.java @@ -28,6 +28,7 @@ public class OpenIdConfigurationResponse extends BaseResponse implements Seriali private String issuer; private String authorizationEndpoint; private String authorizationChallengeEndpoint; + private String statusListEndpoint; private String tokenEndpoint; private String revocationEndpoint; private String sessionRevocationEndpoint; @@ -239,6 +240,24 @@ public void setAuthorizationChallengeEndpoint(String authorizationChallengeEndpo this.authorizationChallengeEndpoint = authorizationChallengeEndpoint; } + /** + * Gets status list + * + * @return status list + */ + public String getStatusListEndpoint() { + return statusListEndpoint; + } + + /** + * Sets status list + * + * @param statusListEndpoint status list + */ + public void setStatusListEndpoint(String statusListEndpoint) { + this.statusListEndpoint = statusListEndpoint; + } + /** * Returns the URL of the Token endpoint. * @@ -1300,6 +1319,7 @@ public String toString() { "issuer='" + issuer + '\'' + ", authorizationEndpoint='" + authorizationEndpoint + '\'' + ", authorizationChallengeEndpoint='" + authorizationChallengeEndpoint + '\'' + + ", statusListEndpoint='" + statusListEndpoint + '\'' + ", tokenEndpoint='" + tokenEndpoint + '\'' + ", revocationEndpoint='" + revocationEndpoint + '\'' + ", userInfoEndpoint='" + userInfoEndpoint + '\'' + diff --git a/jans-auth-server/client/src/main/java/io/jans/as/client/StatusListClient.java b/jans-auth-server/client/src/main/java/io/jans/as/client/StatusListClient.java new file mode 100644 index 00000000000..b56f558b313 --- /dev/null +++ b/jans-auth-server/client/src/main/java/io/jans/as/client/StatusListClient.java @@ -0,0 +1,54 @@ +package io.jans.as.client; + +import jakarta.ws.rs.HttpMethod; +import jakarta.ws.rs.client.Invocation; +import org.apache.log4j.Logger; + +/** + * @author Yuriy Z + */ +public class StatusListClient extends BaseClient { + + private static final Logger LOG = Logger.getLogger(StatusListClient.class); + + /** + * Constructs a client for status list. + * + * @param url status list endpoint + */ + public StatusListClient(String url) { + super(url); + } + + @Override + public String getHttpMethod() { + return HttpMethod.POST; + } + + public StatusListResponse exec(StatusListRequest request) { + setRequest(request); + return exec(); + } + + public StatusListResponse exec() { + initClient(); + + Invocation.Builder clientRequest = webTarget.request(); + applyCookies(clientRequest); + + clientRequest.header("Content-Type", request.getContentType()); + + try { + clientResponse = clientRequest.buildGet().invoke(); + + final StatusListResponse response = new StatusListResponse(clientResponse); + setResponse(response); + } catch (Exception e) { + LOG.error(e.getMessage(), e); + } finally { + closeConnection(); + } + + return getResponse(); + } +} diff --git a/jans-auth-server/client/src/main/java/io/jans/as/client/StatusListRequest.java b/jans-auth-server/client/src/main/java/io/jans/as/client/StatusListRequest.java new file mode 100644 index 00000000000..af3db18a834 --- /dev/null +++ b/jans-auth-server/client/src/main/java/io/jans/as/client/StatusListRequest.java @@ -0,0 +1,18 @@ +package io.jans.as.client; + +import io.jans.as.model.config.Constants; + +/** + * @author Yuriy Z + */ +public class StatusListRequest extends BaseRequest { + + public StatusListRequest() { + setContentType(Constants.CONTENT_TYPE_STATUSLIST_JSON); + } + + @Override + public String getQueryString() { + return null; + } +} diff --git a/jans-auth-server/client/src/main/java/io/jans/as/client/StatusListResponse.java b/jans-auth-server/client/src/main/java/io/jans/as/client/StatusListResponse.java new file mode 100644 index 00000000000..606b8114dff --- /dev/null +++ b/jans-auth-server/client/src/main/java/io/jans/as/client/StatusListResponse.java @@ -0,0 +1,98 @@ +package io.jans.as.client; + +import io.jans.as.model.config.Constants; +import io.jans.as.model.jwt.Jwt; +import io.jans.as.model.session.EndSessionErrorResponseType; +import io.jans.model.tokenstatus.StatusList; +import jakarta.ws.rs.core.Response; +import org.json.JSONObject; + +import java.io.IOException; + +/** + * @author Yuriy Z + */ +public class StatusListResponse extends BaseResponseWithErrors { + + private String lst; + private int bits; + private Jwt jwt; + + public StatusListResponse() { + } + + public StatusListResponse(Response clientResponse) { + super(clientResponse); + injectData(clientResponse); + } + + public StatusList getStatusList() throws IOException { + return StatusList.fromEncoded(lst, bits); + } + + @Override + public EndSessionErrorResponseType fromString(String params) { + return EndSessionErrorResponseType.fromString(params); + } + + public void injectData(Response clientResponse) { + injectErrorIfExistSilently(entity); + if (getErrorType() != null) { + return; + } + + if (clientResponse.getStatus() != 200) { + return; + } + + final String contentType = clientResponse.getHeaderString("Content-Type"); + if (Constants.CONTENT_TYPE_STATUSLIST_JWT.equalsIgnoreCase(contentType)) { + jwt = Jwt.parseSilently(entity); + if (jwt != null) { + final JSONObject statusList = jwt.getClaims().getClaimAsJSON("status_list"); + lst = statusList.getString("lst"); + bits = statusList.getInt("bits"); + } + } else if (Constants.CONTENT_TYPE_STATUSLIST_JSON.equalsIgnoreCase(contentType)) { + final JSONObject json = new JSONObject(entity); + final JSONObject statusList = json.getJSONObject("status_list"); + lst = statusList.getString("lst"); + bits = statusList.getInt("bits"); + } else { + throw new UnsupportedOperationException("Unable to recognize content-type: " + contentType); + } + } + + public String getLst() { + return lst; + } + + public void setLst(String lst) { + this.lst = lst; + } + + public int getBits() { + return bits; + } + + public void setBits(int bits) { + this.bits = bits; + } + + public Jwt getJwt() { + return jwt; + } + + public void setJwt(Jwt jwt) { + this.jwt = jwt; + } + + @Override + public String toString() { + return "StatusListResponse{" + + "lst='" + lst + '\'' + + ", bits=" + bits + + ", jwt=" + jwt + + "} " + super.toString(); + } +} diff --git a/jans-auth-server/client/src/test/java/io/jans/as/client/BaseTest.java b/jans-auth-server/client/src/test/java/io/jans/as/client/BaseTest.java index 75f43cfe31b..d624af96d5a 100644 --- a/jans-auth-server/client/src/test/java/io/jans/as/client/BaseTest.java +++ b/jans-auth-server/client/src/test/java/io/jans/as/client/BaseTest.java @@ -94,6 +94,7 @@ public abstract class BaseTest { protected String gluuConfigurationEndpoint; protected String tokenEndpoint; protected String tokenRevocationEndpoint; + protected String statusListEndpoint; protected String userInfoEndpoint; protected String clientInfoEndpoint; protected String checkSessionIFrame; @@ -304,6 +305,14 @@ public void setAuthorizationChallengeEndpoint(String authorizationChallengeEndpo this.authorizationChallengeEndpoint = authorizationChallengeEndpoint; } + public String getStatusListEndpoint() { + return statusListEndpoint; + } + + public void setStatusListEndpoint(String statusListEndpoint) { + this.statusListEndpoint = statusListEndpoint; + } + public String getTokenEndpoint() { return tokenEndpoint; } @@ -1000,6 +1009,7 @@ public void discovery(ITestContext context) throws Exception { authorizationEndpoint = response.getAuthorizationEndpoint(); authorizationChallengeEndpoint = response.getAuthorizationChallengeEndpoint(); + statusListEndpoint = response.getStatusListEndpoint(); tokenEndpoint = response.getTokenEndpoint(); tokenRevocationEndpoint = response.getRevocationEndpoint(); userInfoEndpoint = response.getUserInfoEndpoint(); @@ -1024,6 +1034,7 @@ public void discovery(ITestContext context) throws Exception { authorizationEndpoint = context.getCurrentXmlTest().getParameter("authorizationEndpoint"); authorizationChallengeEndpoint = context.getCurrentXmlTest().getParameter("authorizationChallengeEndpoint"); + statusListEndpoint = context.getCurrentXmlTest().getParameter("statusListEndpoint"); tokenEndpoint = context.getCurrentXmlTest().getParameter("tokenEndpoint"); tokenRevocationEndpoint = context.getCurrentXmlTest().getParameter("tokenRevocationEndpoint"); userInfoEndpoint = context.getCurrentXmlTest().getParameter("userInfoEndpoint"); diff --git a/jans-auth-server/client/src/test/java/io/jans/as/client/ws/rs/GrantTypesRestrictionHttpTest.java b/jans-auth-server/client/src/test/java/io/jans/as/client/ws/rs/GrantTypesRestrictionHttpTest.java index ce5907650d2..fbd0bb66e98 100644 --- a/jans-auth-server/client/src/test/java/io/jans/as/client/ws/rs/GrantTypesRestrictionHttpTest.java +++ b/jans-auth-server/client/src/test/java/io/jans/as/client/ws/rs/GrantTypesRestrictionHttpTest.java @@ -374,8 +374,8 @@ public Object[][] omittedResponseTypesFailDataProvider(ITestContext context) { { Arrays.asList(), Arrays.asList(), - Arrays.asList(GrantType.OXAUTH_UMA_TICKET), - Arrays.asList(GrantType.OXAUTH_UMA_TICKET), + Arrays.asList(GrantType.UMA_TICKET), + Arrays.asList(GrantType.UMA_TICKET), userId, userSecret, redirectUris, redirectUri, sectorIdentifierUri, postLogoutRedirectUri, logoutUri }, // @@ -424,8 +424,8 @@ public Object[][] omittedResponseTypesFailDataProvider(ITestContext context) { { Arrays.asList(ResponseType.CODE), Arrays.asList(ResponseType.CODE), - Arrays.asList(GrantType.OXAUTH_UMA_TICKET), - Arrays.asList(GrantType.AUTHORIZATION_CODE, GrantType.REFRESH_TOKEN, GrantType.OXAUTH_UMA_TICKET), + Arrays.asList(GrantType.UMA_TICKET), + Arrays.asList(GrantType.AUTHORIZATION_CODE, GrantType.REFRESH_TOKEN, GrantType.UMA_TICKET), userId, userSecret, redirectUris, redirectUri, sectorIdentifierUri, postLogoutRedirectUri, logoutUri }, // @@ -474,8 +474,8 @@ public Object[][] omittedResponseTypesFailDataProvider(ITestContext context) { { Arrays.asList(ResponseType.TOKEN), Arrays.asList(ResponseType.TOKEN), - Arrays.asList(GrantType.OXAUTH_UMA_TICKET), - Arrays.asList(GrantType.IMPLICIT, GrantType.OXAUTH_UMA_TICKET), + Arrays.asList(GrantType.UMA_TICKET), + Arrays.asList(GrantType.IMPLICIT, GrantType.UMA_TICKET), userId, userSecret, redirectUris, redirectUri, sectorIdentifierUri, postLogoutRedirectUri, logoutUri }, // @@ -524,8 +524,8 @@ public Object[][] omittedResponseTypesFailDataProvider(ITestContext context) { { Arrays.asList(ResponseType.ID_TOKEN), Arrays.asList(ResponseType.ID_TOKEN), - Arrays.asList(GrantType.OXAUTH_UMA_TICKET), - Arrays.asList(GrantType.IMPLICIT, GrantType.OXAUTH_UMA_TICKET), + Arrays.asList(GrantType.UMA_TICKET), + Arrays.asList(GrantType.IMPLICIT, GrantType.UMA_TICKET), userId, userSecret, redirectUris, redirectUri, sectorIdentifierUri, postLogoutRedirectUri, logoutUri }, // @@ -574,8 +574,8 @@ public Object[][] omittedResponseTypesFailDataProvider(ITestContext context) { { Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN), Arrays.asList(ResponseType.TOKEN, ResponseType.ID_TOKEN), - Arrays.asList(GrantType.OXAUTH_UMA_TICKET), - Arrays.asList(GrantType.IMPLICIT, GrantType.OXAUTH_UMA_TICKET), + Arrays.asList(GrantType.UMA_TICKET), + Arrays.asList(GrantType.IMPLICIT, GrantType.UMA_TICKET), userId, userSecret, redirectUris, redirectUri, sectorIdentifierUri, postLogoutRedirectUri, logoutUri }, // @@ -624,8 +624,8 @@ public Object[][] omittedResponseTypesFailDataProvider(ITestContext context) { { Arrays.asList(ResponseType.CODE, ResponseType.ID_TOKEN), Arrays.asList(ResponseType.CODE, ResponseType.TOKEN, ResponseType.ID_TOKEN), - Arrays.asList(GrantType.OXAUTH_UMA_TICKET), - Arrays.asList(GrantType.AUTHORIZATION_CODE, GrantType.REFRESH_TOKEN, GrantType.IMPLICIT, GrantType.OXAUTH_UMA_TICKET), + Arrays.asList(GrantType.UMA_TICKET), + Arrays.asList(GrantType.AUTHORIZATION_CODE, GrantType.REFRESH_TOKEN, GrantType.IMPLICIT, GrantType.UMA_TICKET), userId, userSecret, redirectUris, redirectUri, sectorIdentifierUri, postLogoutRedirectUri, logoutUri }, // @@ -674,8 +674,8 @@ public Object[][] omittedResponseTypesFailDataProvider(ITestContext context) { { Arrays.asList(ResponseType.CODE, ResponseType.TOKEN), Arrays.asList(ResponseType.CODE, ResponseType.TOKEN), - Arrays.asList(GrantType.OXAUTH_UMA_TICKET), - Arrays.asList(GrantType.AUTHORIZATION_CODE, GrantType.REFRESH_TOKEN, GrantType.IMPLICIT, GrantType.OXAUTH_UMA_TICKET), + Arrays.asList(GrantType.UMA_TICKET), + Arrays.asList(GrantType.AUTHORIZATION_CODE, GrantType.REFRESH_TOKEN, GrantType.IMPLICIT, GrantType.UMA_TICKET), userId, userSecret, redirectUris, redirectUri, sectorIdentifierUri, postLogoutRedirectUri, logoutUri }, // @@ -724,8 +724,8 @@ public Object[][] omittedResponseTypesFailDataProvider(ITestContext context) { { Arrays.asList(ResponseType.CODE, ResponseType.TOKEN, ResponseType.ID_TOKEN), Arrays.asList(ResponseType.CODE, ResponseType.TOKEN, ResponseType.ID_TOKEN), - Arrays.asList(GrantType.OXAUTH_UMA_TICKET), - Arrays.asList(GrantType.AUTHORIZATION_CODE, GrantType.REFRESH_TOKEN, GrantType.IMPLICIT, GrantType.OXAUTH_UMA_TICKET), + Arrays.asList(GrantType.UMA_TICKET), + Arrays.asList(GrantType.AUTHORIZATION_CODE, GrantType.REFRESH_TOKEN, GrantType.IMPLICIT, GrantType.UMA_TICKET), userId, userSecret, redirectUris, redirectUri, sectorIdentifierUri, postLogoutRedirectUri, logoutUri }, }; diff --git a/jans-auth-server/client/src/test/java/io/jans/as/client/ws/rs/RegistrationRestWebServiceHttpTest.java b/jans-auth-server/client/src/test/java/io/jans/as/client/ws/rs/RegistrationRestWebServiceHttpTest.java index f50e297c728..cd8313edb3a 100644 --- a/jans-auth-server/client/src/test/java/io/jans/as/client/ws/rs/RegistrationRestWebServiceHttpTest.java +++ b/jans-auth-server/client/src/test/java/io/jans/as/client/ws/rs/RegistrationRestWebServiceHttpTest.java @@ -38,7 +38,7 @@ import static io.jans.as.model.common.GrantType.AUTHORIZATION_CODE; import static io.jans.as.model.common.GrantType.CLIENT_CREDENTIALS; import static io.jans.as.model.common.GrantType.IMPLICIT; -import static io.jans.as.model.common.GrantType.OXAUTH_UMA_TICKET; +import static io.jans.as.model.common.GrantType.UMA_TICKET; import static io.jans.as.model.common.GrantType.REFRESH_TOKEN; import static io.jans.as.model.common.GrantType.RESOURCE_OWNER_PASSWORD_CREDENTIALS; import static io.jans.as.model.common.ResponseType.CODE; @@ -78,7 +78,7 @@ public void requestClientAssociate1(final String redirectUris, final String sect RESOURCE_OWNER_PASSWORD_CREDENTIALS, CLIENT_CREDENTIALS, REFRESH_TOKEN, - OXAUTH_UMA_TICKET)); + UMA_TICKET)); registerRequest.setResponseTypes(Arrays.asList( CODE, TOKEN, diff --git a/jans-auth-server/client/src/test/java/io/jans/as/client/ws/rs/token/StatusListHttpTest.java b/jans-auth-server/client/src/test/java/io/jans/as/client/ws/rs/token/StatusListHttpTest.java new file mode 100644 index 00000000000..0f7131ff971 --- /dev/null +++ b/jans-auth-server/client/src/test/java/io/jans/as/client/ws/rs/token/StatusListHttpTest.java @@ -0,0 +1,169 @@ +package io.jans.as.client.ws.rs.token; + +import com.google.common.collect.Lists; +import io.jans.as.client.*; +import io.jans.as.client.client.AssertBuilder; +import io.jans.as.model.common.*; +import io.jans.as.model.exception.InvalidJwtException; +import io.jans.as.model.jwt.Jwt; +import io.jans.as.model.register.ApplicationType; +import io.jans.model.tokenstatus.StatusList; +import io.jans.model.tokenstatus.TokenStatus; +import io.jans.util.Pair; +import org.apache.commons.lang3.StringUtils; +import org.testng.Assert; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import static org.testng.AssertJUnit.assertEquals; + +/** + * @author Yuriy Z + */ +public class StatusListHttpTest extends BaseTest { + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri"}) + @Test + public void statusList( + final String userId, final String userSecret, final String redirectUris, final String redirectUri) throws IOException, InvalidJwtException, InterruptedException { + showTitle("statusList"); + + List responseTypes = Arrays.asList( + ResponseType.CODE, + ResponseType.ID_TOKEN); + List scopes = Arrays.asList("openid", "profile", "address", "email", "phone", "user_name"); + + // 1. Register client + Pair clientIdAndSecret = getOrRegisterClient(redirectUris, responseTypes, scopes); + + String clientId = clientIdAndSecret.getFirst(); + String clientSecret = clientIdAndSecret.getSecond(); + + // 2. Request authorization and receive the authorization code. + String nonce = UUID.randomUUID().toString(); + AuthorizationResponse authorizationResponse = requestAuthorization(userId, userSecret, redirectUri, responseTypes, scopes, clientId, nonce); + + String authorizationCode = authorizationResponse.getCode(); + + // 3. Request access token using the authorization code. + TokenRequest tokenRequest = new TokenRequest(GrantType.AUTHORIZATION_CODE); + tokenRequest.setCode(authorizationCode); + tokenRequest.setRedirectUri(redirectUri); + tokenRequest.setAuthUsername(clientId); + tokenRequest.setAuthPassword(clientSecret); + tokenRequest.setAuthenticationMethod(AuthenticationMethod.CLIENT_SECRET_BASIC); + + TokenClient tokenClient = newTokenClient(tokenRequest); + tokenClient.setRequest(tokenRequest); + TokenResponse tokenResponse = tokenClient.exec(); + + showClient(tokenClient); + System.out.println("ACCESS_TOKEN"); + System.out.println(tokenResponse.getAccessToken()); + Jwt accessTokenJwt = Jwt.parseOrThrow(tokenResponse.getAccessToken()); + final int accessTokenIndex = accessTokenJwt.getClaims().getClaimAsJSON("status").getJSONObject("status_list").getInt("idx"); + System.out.println("ACCESS_TOKEN idx: " + accessTokenIndex); + + assertEquals(TokenStatus.VALID, loadStatus(accessTokenIndex)); + + revokeAccessToken(clientIdAndSecret, tokenResponse.getAccessToken()); + + System.out.println("ACCESS_TOKEN idx: " + accessTokenIndex); // re-print for convenience + // give time to let status went to list + Thread.sleep(2000); + assertEquals(TokenStatus.INVALID, loadStatus(accessTokenIndex)); + } + + private void revokeAccessToken(Pair clientIdAndSecret, String accessToken) { + String clientId = clientIdAndSecret.getFirst(); + String clientSecret = clientIdAndSecret.getSecond(); + + TokenRevocationRequest revocationRequest = new TokenRevocationRequest(); + revocationRequest.setToken(accessToken); + revocationRequest.setTokenTypeHint(TokenTypeHint.ACCESS_TOKEN); + revocationRequest.setAuthUsername(clientId); + revocationRequest.setAuthPassword(clientSecret); + + TokenRevocationClient revocationClient = new TokenRevocationClient(tokenRevocationEndpoint); + revocationClient.setRequest(revocationRequest); + + TokenRevocationResponse revocationResponse = revocationClient.exec(); + + showClient(revocationClient); + Assert.assertEquals(revocationResponse.getStatus(), 200, "Unexpected response code: " + revocationResponse.getStatus()); + } + + private TokenStatus loadStatus(int index) throws IOException { + StatusListRequest statusListRequest = new StatusListRequest(); + StatusListClient statusListClient = new StatusListClient(statusListEndpoint); + StatusListResponse statusListResponse = statusListClient.exec(statusListRequest); + showClient(statusListClient); + System.out.println(String.format("bits: %s, lst: %s", statusListResponse.getBits(), statusListResponse.getLst())); + + StatusList statusList = statusListResponse.getStatusList(); + final int status = statusList.get(index); + return TokenStatus.fromValue(status); + } + + private AuthorizationResponse requestAuthorization(final String userId, final String userSecret, final String redirectUri, + List responseTypes, List scopes, String clientId, String nonce) { + String state = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + authorizationRequest.setAcrValues(Lists.newArrayList("agama_basic")); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + AssertBuilder.authorizationResponse(authorizationResponse).check(); + return authorizationResponse; + } + + public Pair getOrRegisterClient(final String redirectUris, List responseTypes, List scopes) { + final String clientId = System.getProperty("CLIENT_ID"); + final String clientSecret = System.getProperty("CLIENT_SECRET"); + if (StringUtils.isNotBlank(clientId) && StringUtils.isNotBlank(clientSecret)) { + return new Pair<>(clientId, clientSecret); + } + + RegisterResponse registerResponse = registerClient(redirectUris, responseTypes, scopes); + + return new Pair<>(registerResponse.getClientId(), registerResponse.getClientSecret()); + } + + public RegisterResponse registerClient(final String redirectUris, List responseTypes, List scopes) { + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "jans test app", + io.jans.as.model.util.StringUtils.spaceSeparatedToList(redirectUris)); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setScope(scopes); + registerRequest.setSubjectType(SubjectType.PUBLIC); + registerRequest.setAccessTokenAsJwt(true); + + RegisterClient registerClient = newRegisterClient(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + AssertBuilder.registerResponse(registerResponse).created().check(); + return registerResponse; + } + + public static void main(String[] args) throws IOException { + StatusList before = StatusList.fromEncoded("eNoDAAAAAAE", 2); + StatusList after = StatusList.fromEncoded("eNoLYGEYhIAFADIjAFk", 2); + } + + @Parameters({"userId", "userSecret", "redirectUris", "redirectUri"}) + @Test(enabled = false) + public void statusListPerformanceLoad( + final String userId, final String userSecret, final String redirectUris, final String redirectUri) throws IOException, InvalidJwtException, InterruptedException { + for (int i = 0; i < 10; i++) { + statusList(userId, userSecret, redirectUris, redirectUri); + } + } +} diff --git a/jans-auth-server/client/src/test/java/io/jans/as/client/ws/rs/uma/AccessProtectedResourceFlowHttpTest.java b/jans-auth-server/client/src/test/java/io/jans/as/client/ws/rs/uma/AccessProtectedResourceFlowHttpTest.java index a42069d19ab..2480e09b5f0 100644 --- a/jans-auth-server/client/src/test/java/io/jans/as/client/ws/rs/uma/AccessProtectedResourceFlowHttpTest.java +++ b/jans-auth-server/client/src/test/java/io/jans/as/client/ws/rs/uma/AccessProtectedResourceFlowHttpTest.java @@ -105,7 +105,7 @@ public void requestRptAndGetNeedsInfo(String umaPatClientId, String umaPatClient try { tokenService.requestRpt( "Basic " + encodeCredentials(umaPatClientId, umaPatClientSecret), - GrantType.OXAUTH_UMA_TICKET.getValue(), + GrantType.UMA_TICKET.getValue(), permissionFlowTest.ticket, null, null, null, null, null); } catch (ClientErrorException ex) { @@ -164,7 +164,7 @@ public void successfulRptRequest(String umaPatClientId, String umaPatClientSecre UmaTokenResponse response = tokenService.requestRpt( "Basic " + encodeCredentials(umaPatClientId, umaPatClientSecret), - GrantType.OXAUTH_UMA_TICKET.getValue(), + GrantType.UMA_TICKET.getValue(), claimsGatheringTicket, null, null, null, null, null); assertIt(response); @@ -184,7 +184,7 @@ public void repeatRptRequest(String umaPatClientId, String umaPatClientSecret) t UmaTokenResponse response = tokenService.requestRpt( "Basic " + encodeCredentials(umaPatClientId, umaPatClientSecret), - GrantType.OXAUTH_UMA_TICKET.getValue(), + GrantType.UMA_TICKET.getValue(), claimsGatheringTicket, null, null, null, this.rpt, "oxd"); assertIt(response); diff --git a/jans-auth-server/client/src/test/java/io/jans/as/client/ws/rs/uma/ClientAuthenticationByAccessTokenHttpTest.java b/jans-auth-server/client/src/test/java/io/jans/as/client/ws/rs/uma/ClientAuthenticationByAccessTokenHttpTest.java index 3a8eb604947..ff0c3aa1d3a 100644 --- a/jans-auth-server/client/src/test/java/io/jans/as/client/ws/rs/uma/ClientAuthenticationByAccessTokenHttpTest.java +++ b/jans-auth-server/client/src/test/java/io/jans/as/client/ws/rs/uma/ClientAuthenticationByAccessTokenHttpTest.java @@ -209,7 +209,7 @@ public void requestRptAndGetNeedsInfo() throws Exception { try { tokenService.requestRpt( "AccessToken " + userAccessToken, - GrantType.OXAUTH_UMA_TICKET.getValue(), + GrantType.UMA_TICKET.getValue(), permissionFlowTest.ticket, null, null, null, null, null); } catch (ClientErrorException ex) { diff --git a/jans-auth-server/client/src/test/java/io/jans/as/client/ws/rs/uma/UmaSpontaneousScopeHttpTest.java b/jans-auth-server/client/src/test/java/io/jans/as/client/ws/rs/uma/UmaSpontaneousScopeHttpTest.java index 8ba4c4e65d7..c6f04d9ba6e 100644 --- a/jans-auth-server/client/src/test/java/io/jans/as/client/ws/rs/uma/UmaSpontaneousScopeHttpTest.java +++ b/jans-auth-server/client/src/test/java/io/jans/as/client/ws/rs/uma/UmaSpontaneousScopeHttpTest.java @@ -116,7 +116,7 @@ public void successfulRptRequest() throws Exception { UmaTokenResponse response = tokenService.requestRpt( "Basic " + AccessProtectedResourceFlowHttpTest.encodeCredentials(clientResponse.getClientId(), clientResponse.getClientSecret()), - GrantType.OXAUTH_UMA_TICKET.getValue(), + GrantType.UMA_TICKET.getValue(), permissionFlowTest.ticket, null, null, null, null, null); assertIt(response); diff --git a/jans-auth-server/client/src/test/resources/testng.xml b/jans-auth-server/client/src/test/resources/testng.xml index 8d25c292eed..60613ffb8e3 100644 --- a/jans-auth-server/client/src/test/resources/testng.xml +++ b/jans-auth-server/client/src/test/resources/testng.xml @@ -53,6 +53,12 @@ + + + + + + diff --git a/jans-auth-server/docs/swagger.yaml b/jans-auth-server/docs/swagger.yaml index 9d0b098a3be..41219773a16 100644 --- a/jans-auth-server/docs/swagger.yaml +++ b/jans-auth-server/docs/swagger.yaml @@ -2674,6 +2674,53 @@ paths: $ref: '#/components/responses/Unauthorized' 500: $ref: '#/components/responses/InternalServerError' + + /status_list: + get: + tags: + - Status List + summary: The Status List Endpoint. + description: The Status List Endpoint. + operationId: get-status-list + parameters: + - name: Accept + in: header + required: true + description: Indicates the requested response type. Possible values are application/statuslist+json and application/statuslist+jwt. Defaults to application/statuslist+jwt if not specified. + schema: + type: string + example: application/statuslist+jwt or application/statuslist+json + responses: + 200: + description: OK + content: + application/statuslist+json: + schema: + title: StatusListResponseJson + description: Status list response as JSON (depending on request's "Accept" header value). + type: object + required: + - bits + - lst + properties: + bits: + type: integer + description: JSON Integer specifying the number of bits per Referenced Token in the Status List (lst). The allowed values for bits are 1,2,4 and 8. + example: 2 + lst: + type: string + description: JSON String that contains the status values for all the Referenced Tokens it conveys statuses for. The value MUST be the base64url-encoded + example: eNrbuRgAAhcBXQ + application/statuslist+jwt: + schema: + title: StatusListResponseJwt + description: Status list response as JWT. + type: object + 401: + $ref: '#/components/responses/Unauthorized' + 500: + $ref: '#/components/responses/InternalServerError' + /global-token-revocation: post: tags: diff --git a/jans-auth-server/model/src/main/java/io/jans/as/model/common/FeatureFlagType.java b/jans-auth-server/model/src/main/java/io/jans/as/model/common/FeatureFlagType.java index fc8b601a325..a2a3985c547 100644 --- a/jans-auth-server/model/src/main/java/io/jans/as/model/common/FeatureFlagType.java +++ b/jans-auth-server/model/src/main/java/io/jans/as/model/common/FeatureFlagType.java @@ -42,6 +42,9 @@ public enum FeatureFlagType { @DocFeatureFlag(description = "Enable/Disable global token revocation endpoint", defaultValue = "Enabled") GLOBAL_TOKEN_REVOCATION("global_token_revocation"), + @DocFeatureFlag(description = "Enable/Disable status list endpoint", + defaultValue = "Enabled") + STATUS_LIST("status_list"), @DocFeatureFlag(description = "Enable/Disable active session endpoint", defaultValue = "Enabled") ACTIVE_SESSION("active_session"), diff --git a/jans-auth-server/model/src/main/java/io/jans/as/model/common/GrantType.java b/jans-auth-server/model/src/main/java/io/jans/as/model/common/GrantType.java index 79bfb0ee8ce..399627e9672 100644 --- a/jans-auth-server/model/src/main/java/io/jans/as/model/common/GrantType.java +++ b/jans-auth-server/model/src/main/java/io/jans/as/model/common/GrantType.java @@ -76,7 +76,7 @@ public enum GrantType implements HasParamName, AttributeEnum { * an OAuth 2.0 access token to gain access to a protected resource * asynchronously from the time a resource owner grants access. */ - OXAUTH_UMA_TICKET("urn:ietf:params:oauth:grant-type:uma-ticket"), + UMA_TICKET("urn:ietf:params:oauth:grant-type:uma-ticket"), /** * Token exchange grant type for OAuth 2.0 diff --git a/jans-auth-server/model/src/main/java/io/jans/as/model/config/BaseDnConfiguration.java b/jans-auth-server/model/src/main/java/io/jans/as/model/config/BaseDnConfiguration.java index 93f47e9f4fe..c043ba75355 100644 --- a/jans-auth-server/model/src/main/java/io/jans/as/model/config/BaseDnConfiguration.java +++ b/jans-auth-server/model/src/main/java/io/jans/as/model/config/BaseDnConfiguration.java @@ -67,6 +67,10 @@ public class BaseDnConfiguration { private String fido2Assertion; @XmlElement(name = "archivedJwks") private String archivedJwks; + @XmlElement(name = "node") + private String node; + @XmlElement(name = "statusIndexPool") + private String statusIndexPool; public String getArchivedJwks() { return archivedJwks; @@ -244,4 +248,19 @@ public void setFido2Assertion(String fido2Assertion) { this.fido2Assertion = fido2Assertion; } + public String getNode() { + return node; + } + + public void setNode(String node) { + this.node = node; + } + + public String getStatusIndexPool() { + return statusIndexPool; + } + + public void setStatusIndexPool(String statusIndexPool) { + this.statusIndexPool = statusIndexPool; + } } diff --git a/jans-auth-server/model/src/main/java/io/jans/as/model/config/Constants.java b/jans-auth-server/model/src/main/java/io/jans/as/model/config/Constants.java index f45ec1aae50..1ad0128bfa6 100644 --- a/jans-auth-server/model/src/main/java/io/jans/as/model/config/Constants.java +++ b/jans-auth-server/model/src/main/java/io/jans/as/model/config/Constants.java @@ -60,6 +60,9 @@ private Constants() { public static final String CONTENT_TYPE_APPLICATION_JSON_UTF_8 = "application/json;charset=UTF-8"; + public static final String CONTENT_TYPE_STATUSLIST_JSON = "application/statuslist+json"; + public static final String CONTENT_TYPE_STATUSLIST_JWT = "application/statuslist+jwt"; + public static final String AUTHORIZATION = "Authorization"; public static final String AUTHORIZATION_BEARER = "Authorization: Bearer "; public static final String AUTHORIZATION_BASIC = "Authorization: Basic "; 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 3710f8869f7..917d637cd1d 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 @@ -11,6 +11,7 @@ import com.google.common.collect.Lists; import io.jans.agama.model.EngineConfig; import io.jans.as.model.common.*; +import io.jans.as.model.crypto.signature.SignatureAlgorithm; import io.jans.as.model.error.ErrorHandlingMethod; import io.jans.as.model.jwk.KeySelectionStrategy; import io.jans.as.model.ssa.SsaConfiguration; @@ -37,6 +38,10 @@ public class AppConfiguration implements Configuration { public static final String DEFAULT_STAT_SCOPE = "jans_stat"; public static final String DEFAULT_AUTHORIZATION_CHALLENGE_ACR = "default_challenge"; + public static final int DEFAULT_STATUS_LIST_RESPONSE_JWT_LIFETIME = 600; // 10min + public static final int DEFAULT_STATUS_LIST_BIT_SIZE = 2; + public static final int DEFAULT_STATUS_LIST_INDEX_ALLOCATION_BLOCK_SIZE = 100; + @DocProperty(description = "URL using the https scheme that OP asserts as Issuer identifier") private String issuer; @@ -199,6 +204,18 @@ public class AppConfiguration implements Configuration { @DocProperty(description = "The lifetime of spontaneous scope in seconds") private int spontaneousScopeLifetime; + @DocProperty(description = "Specifies status list bit size. (2 bits - 4 statuses, 4 bits - 16 statuses). Defaults to 2.") + private int statusListBitSize = DEFAULT_STATUS_LIST_BIT_SIZE; + + @DocProperty(description = "The status list signature algorithm to sign response JWT. Defaults to RS256.") + private String statusListResponseJwtSignatureAlgorithm = SignatureAlgorithm.RS256.getName(); + + @DocProperty(description = "The status list response JWT lifetime (used to set exp claim in JWT).") + private int statusListResponseJwtLifetime = DEFAULT_STATUS_LIST_RESPONSE_JWT_LIFETIME; + + @DocProperty(description = "Specifies how many status list indexes AS can reserve at once within pool (when status_list feature flag is enabled). Defaults to 100.") + private int statusListIndexAllocationBlockSize = DEFAULT_STATUS_LIST_INDEX_ALLOCATION_BLOCK_SIZE; + @DocProperty(description = "Specifies which LDAP attribute is used for the subject identifier claim") private String openidSubAttribute; @@ -2394,6 +2411,38 @@ public void setSpontaneousScopeLifetime(int spontaneousScopeLifetime) { this.spontaneousScopeLifetime = spontaneousScopeLifetime; } + public int getStatusListResponseJwtLifetime() { + return statusListResponseJwtLifetime; + } + + public void setStatusListResponseJwtLifetime(int statusListResponseJwtLifetime) { + this.statusListResponseJwtLifetime = statusListResponseJwtLifetime; + } + + public String getStatusListResponseJwtSignatureAlgorithm() { + return statusListResponseJwtSignatureAlgorithm; + } + + public void setStatusListResponseJwtSignatureAlgorithm(String statusListResponseJwtSignatureAlgorithm) { + this.statusListResponseJwtSignatureAlgorithm = statusListResponseJwtSignatureAlgorithm; + } + + public int getStatusListBitSize() { + return statusListBitSize; + } + + public void setStatusListBitSize(int statusListBitSize) { + this.statusListBitSize = statusListBitSize; + } + + public int getStatusListIndexAllocationBlockSize() { + return statusListIndexAllocationBlockSize; + } + + public void setStatusListIndexAllocationBlockSize(int statusListIndexAllocationBlockSize) { + this.statusListIndexAllocationBlockSize = statusListIndexAllocationBlockSize; + } + public int getCleanServiceInterval() { return cleanServiceInterval; } diff --git a/jans-auth-server/model/src/main/java/io/jans/as/model/configuration/ConfigurationResponseClaim.java b/jans-auth-server/model/src/main/java/io/jans/as/model/configuration/ConfigurationResponseClaim.java index 3c771928b33..02bf5d03866 100644 --- a/jans-auth-server/model/src/main/java/io/jans/as/model/configuration/ConfigurationResponseClaim.java +++ b/jans-auth-server/model/src/main/java/io/jans/as/model/configuration/ConfigurationResponseClaim.java @@ -18,6 +18,7 @@ private ConfigurationResponseClaim() { public static final String ISSUER = "issuer"; public static final String AUTHORIZATION_ENDPOINT = "authorization_endpoint"; public static final String AUTHORIZATION_CHALLENGE_ENDPOINT = "authorization_challenge_endpoint"; + public static final String STATUS_LIST_ENDPOINT = "status_list_endpoint"; public static final String TOKEN_ENDPOINT = "token_endpoint"; public static final String REVOCATION_ENDPOINT = "revocation_endpoint"; public static final String SESSION_REVOCATION_ENDPOINT = "session_revocation_endpoint"; diff --git a/jans-auth-server/model/src/main/java/io/jans/as/model/error/ErrorResponseFactory.java b/jans-auth-server/model/src/main/java/io/jans/as/model/error/ErrorResponseFactory.java index e5b7434a122..359be28ea46 100644 --- a/jans-auth-server/model/src/main/java/io/jans/as/model/error/ErrorResponseFactory.java +++ b/jans-auth-server/model/src/main/java/io/jans/as/model/error/ErrorResponseFactory.java @@ -105,6 +105,15 @@ public String errorAsJson(IErrorType type, String reason) { return error.toJSonString(); } + public boolean isFeatureFlagEnabled(FeatureFlagType flagType) { + final Set enabledFlags = FeatureFlagType.from(appConfiguration); + if (enabledFlags.isEmpty()) { // no restrictions + return true; + } + + return enabledFlags.contains(flagType); + } + public void validateFeatureEnabled(FeatureFlagType flagType) { final Set enabledFlags = FeatureFlagType.from(appConfiguration); if (enabledFlags.isEmpty()) { // no restrictions diff --git a/jans-auth-server/model/src/main/java/io/jans/as/model/jwt/JwtType.java b/jans-auth-server/model/src/main/java/io/jans/as/model/jwt/JwtType.java index 0ddad5a8d4d..89cb3b68486 100644 --- a/jans-auth-server/model/src/main/java/io/jans/as/model/jwt/JwtType.java +++ b/jans-auth-server/model/src/main/java/io/jans/as/model/jwt/JwtType.java @@ -14,7 +14,8 @@ public enum JwtType { JWT("JWT"), TX_TOKEN("txn_token"), - DPOP_PLUS_JWT("dpop+jwt"); + DPOP_PLUS_JWT("dpop+jwt"), + STATUS_LIST_JWT("statuslist+jwt"); private final String paramName; diff --git a/jans-auth-server/server/conf/jans-config.json b/jans-auth-server/server/conf/jans-config.json index 4b5eb105012..fa9b03b67d5 100644 --- a/jans-auth-server/server/conf/jans-config.json +++ b/jans-auth-server/server/conf/jans-config.json @@ -19,7 +19,8 @@ "stat", "par", "ssa", - "global_token_revocation" + "global_token_revocation", + "status_list" ], "issuer":"${config.oxauth.issuer}", "loginPage":"${config.oxauth.contextPath}/login.htm", diff --git a/jans-auth-server/server/conf/jans-static-conf.json b/jans-auth-server/server/conf/jans-static-conf.json index 6e3becbac89..923b634fa74 100644 --- a/jans-auth-server/server/conf/jans-static-conf.json +++ b/jans-auth-server/server/conf/jans-static-conf.json @@ -18,6 +18,8 @@ "ciba": "ou=ciba,o=jans", "stat": "ou=stat,o=jans", "par": "ou=par,o=jans", - "archivedJwks": "ou=archived_jwks,o=jans" + "archivedJwks": "ou=archived_jwks,o=jans", + "node": "ou=node,o=jans", + "statusIndexPool": "ou=status_index_pool,o=jans" } } diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/model/common/AbstractAuthorizationGrant.java b/jans-auth-server/server/src/main/java/io/jans/as/server/model/common/AbstractAuthorizationGrant.java index 553bde0efef..94b69aac662 100644 --- a/jans-auth-server/server/src/main/java/io/jans/as/server/model/common/AbstractAuthorizationGrant.java +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/model/common/AbstractAuthorizationGrant.java @@ -73,6 +73,7 @@ public abstract class AbstractAuthorizationGrant implements IAuthorizationGrant private String claims; private String dpopJkt; private String referenceId; + private Integer statusListIndex; private String acrValues; private String sessionDn; @@ -107,6 +108,14 @@ public void setReferenceId(String referenceId) { this.referenceId = referenceId; } + public Integer getStatusListIndex() { + return statusListIndex; + } + + public void setStatusListIndex(Integer statusListIndex) { + this.statusListIndex = statusListIndex; + } + public String getDpopJkt() { return dpopJkt; } diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/model/common/AbstractToken.java b/jans-auth-server/server/src/main/java/io/jans/as/server/model/common/AbstractToken.java index 376231b0c51..76ded1f764a 100644 --- a/jans-auth-server/server/src/main/java/io/jans/as/server/model/common/AbstractToken.java +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/model/common/AbstractToken.java @@ -58,6 +58,8 @@ public abstract class AbstractToken implements Serializable, Deletable { @AttributeName(name = "jansId") private String referenceId; + private Integer statusListIndex; + @Expiration private int ttl; @@ -232,6 +234,24 @@ public void setReferenceId(String referenceId) { this.referenceId = referenceId; } + /** + * Gets status list index + * + * @return status list index + */ + public Integer getStatusListIndex() { + return statusListIndex; + } + + /** + * Sets status list index + * + * @param statusListIndex status list index + */ + public void setStatusListIndex(Integer statusListIndex) { + this.statusListIndex = statusListIndex; + } + /** * Return true if the token has expired. * diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/model/common/AuthorizationGrant.java b/jans-auth-server/server/src/main/java/io/jans/as/server/model/common/AuthorizationGrant.java index 96f1738fb67..2f6760af13d 100644 --- a/jans-auth-server/server/src/main/java/io/jans/as/server/model/common/AuthorizationGrant.java +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/model/common/AuthorizationGrant.java @@ -12,6 +12,7 @@ import io.jans.as.common.model.registration.Client; import io.jans.as.common.service.AttributeService; import io.jans.as.model.authzdetails.AuthzDetails; +import io.jans.as.model.common.FeatureFlagType; import io.jans.as.model.common.ScopeConstants; import io.jans.as.model.config.WebKeysConfiguration; import io.jans.as.model.crypto.signature.SignatureAlgorithm; @@ -34,6 +35,8 @@ import io.jans.as.server.service.external.context.ExternalIntrospectionContext; import io.jans.as.server.service.external.context.ExternalUpdateTokenContext; import io.jans.as.server.service.stat.StatService; +import io.jans.as.server.service.token.StatusListIndexService; +import io.jans.as.server.service.token.StatusListService; import io.jans.as.server.util.ServerUtil; import io.jans.as.server.util.TokenHashUtil; import io.jans.model.metric.MetricType; @@ -102,6 +105,12 @@ public abstract class AuthorizationGrant extends AbstractAuthorizationGrant { @Inject private ErrorResponseFactory errorResponseFactory; + @Inject + private StatusListService statusListService; + + @Inject + private StatusListIndexService statusListIndexService; + private boolean isCachedWithNoPersistence = false; protected AuthorizationGrant() { @@ -119,10 +128,17 @@ public void init(User user, AuthorizationGrantType authorizationGrantType, Clien private IdToken createIdTokenInternal(AuthorizationCode authorizationCode, AccessToken accessToken, RefreshToken refreshToken, ExecutionContext executionContext) throws Exception { executionContext.initFromGrantIfNeeded(this); + Integer statusListIndex = null; + if (errorResponseFactory.isFeatureFlagEnabled(FeatureFlagType.STATUS_LIST)) { + statusListIndex = statusListIndexService.next(); + executionContext.setStatusListIndex(statusListIndex); + } + JsonWebResponse jwr = idTokenFactory.createJwr(this, authorizationCode, accessToken, refreshToken, executionContext); final IdToken idToken = new IdToken(jwr.toString(), jwr.getClaims().getClaimAsDate(JwtClaimName.ISSUED_AT), jwr.getClaims().getClaimAsDate(JwtClaimName.EXPIRATION_TIME)); idToken.setReferenceId(executionContext.getTokenReferenceId()); + idToken.setStatusListIndex(statusListIndex); if (log.isTraceEnabled()) log.trace("Created id_token: {}", idToken.getCode()); return idToken; @@ -206,6 +222,14 @@ public AccessToken createAccessToken(ExecutionContext context) { context.generateRandomTokenReferenceId(); final AccessToken accessToken = super.createAccessToken(context); + + Integer statusListIndex = null; + if (errorResponseFactory.isFeatureFlagEnabled(FeatureFlagType.STATUS_LIST)) { + statusListIndex = statusListIndexService.next(); + context.setStatusListIndex(statusListIndex); + accessToken.setStatusListIndex(statusListIndex); + } + if (accessToken.getExpiresIn() < 0) { log.trace("Failed to create access token with negative expiration time"); return null; @@ -301,6 +325,7 @@ public JwtSigner createAccessTokenAsJwt(AccessToken accessToken, ExecutionContex } Audience.setAudience(jwt.getClaims(), getClient()); + statusListService.addStatusClaimWithIndex(jwt, context); if (isTrue(client.getAttributes().getRunIntrospectionScriptBeforeJwtCreation())) { runIntrospectionScriptAndInjectValuesIntoJwt(jwt, context); @@ -494,6 +519,7 @@ public TokenEntity asTokenEntity(AbstractToken token) { result.setClientId(getClientId()); result.setReferenceId(token.getReferenceId()); + result.getAttributes().setStatusListIndex(token.getStatusListIndex()); result.getAttributes().setX5cs256(token.getX5ts256()); result.getAttributes().setDpopJkt(getDpopJkt()); diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/model/common/AuthorizationGrantList.java b/jans-auth-server/server/src/main/java/io/jans/as/server/model/common/AuthorizationGrantList.java index 1df796dcd63..6bcf6ffbf85 100644 --- a/jans-auth-server/server/src/main/java/io/jans/as/server/model/common/AuthorizationGrantList.java +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/model/common/AuthorizationGrantList.java @@ -353,6 +353,7 @@ public AuthorizationGrant asGrant(TokenEntity tokenEntity) { result.setDpopJkt(tokenEntity.getAttributes().getDpopJkt()); result.setTokenEntity(tokenEntity); result.setReferenceId(tokenEntity.getReferenceId()); + result.setStatusListIndex(tokenEntity.getAttributes().getStatusListIndex()); if (StringUtils.isNotBlank(grantId)) { result.setGrantId(grantId); } @@ -382,6 +383,7 @@ public AuthorizationGrant asGrant(TokenEntity tokenEntity) { final AuthorizationCodeGrant g = (AuthorizationCodeGrant) result; code.setX5ts256(g.getX5ts256()); code.setReferenceId(tokenEntity.getReferenceId()); + code.setStatusListIndex(tokenEntity.getAttributes().getStatusListIndex()); g.setAuthorizationCode(code); } break; @@ -389,6 +391,7 @@ public AuthorizationGrant asGrant(TokenEntity tokenEntity) { final RefreshToken refreshToken = new RefreshToken(tokenEntity.getTokenCode(), tokenEntity.getCreationDate(), tokenEntity.getExpirationDate()); refreshToken.setX5ts256(result.getX5ts256()); refreshToken.setReferenceId(tokenEntity.getReferenceId()); + refreshToken.setStatusListIndex(tokenEntity.getAttributes().getStatusListIndex()); result.setRefreshTokens(Collections.singletonList(refreshToken)); break; case ACCESS_TOKEN: @@ -396,6 +399,7 @@ public AuthorizationGrant asGrant(TokenEntity tokenEntity) { accessToken.setDpop(tokenEntity.getDpop()); accessToken.setX5ts256(result.getX5ts256()); accessToken.setReferenceId(tokenEntity.getReferenceId()); + accessToken.setStatusListIndex(tokenEntity.getAttributes().getStatusListIndex()); result.setAccessTokens(Collections.singletonList(accessToken)); break; case TX_TOKEN: @@ -403,18 +407,21 @@ public AuthorizationGrant asGrant(TokenEntity tokenEntity) { txToken.setDpop(tokenEntity.getDpop()); txToken.setX5ts256(result.getX5ts256()); txToken.setReferenceId(tokenEntity.getReferenceId()); + txToken.setStatusListIndex(tokenEntity.getAttributes().getStatusListIndex()); result.setTxTokens(Collections.singletonList(txToken)); break; case ID_TOKEN: final IdToken idToken = new IdToken(tokenEntity.getTokenCode(), tokenEntity.getCreationDate(), tokenEntity.getExpirationDate()); idToken.setX5ts256(result.getX5ts256()); idToken.setReferenceId(tokenEntity.getReferenceId()); + idToken.setStatusListIndex(tokenEntity.getAttributes().getStatusListIndex()); result.setIdToken(idToken); break; case LONG_LIVED_ACCESS_TOKEN: final AccessToken longLivedAccessToken = new AccessToken(tokenEntity.getTokenCode(), tokenEntity.getCreationDate(), tokenEntity.getExpirationDate()); longLivedAccessToken.setX5ts256(result.getX5ts256()); longLivedAccessToken.setReferenceId(tokenEntity.getReferenceId()); + longLivedAccessToken.setStatusListIndex(tokenEntity.getAttributes().getStatusListIndex()); result.setLongLivedAccessToken(longLivedAccessToken); break; } diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/model/common/ExecutionContext.java b/jans-auth-server/server/src/main/java/io/jans/as/server/model/common/ExecutionContext.java index 94c4b272fe7..f54e385e1af 100644 --- a/jans-auth-server/server/src/main/java/io/jans/as/server/model/common/ExecutionContext.java +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/model/common/ExecutionContext.java @@ -67,6 +67,7 @@ public class ExecutionContext { private String nonce; private String state; private String tokenReferenceId = IdUtil.randomShortUUID(); + private Integer statusListIndex; private boolean includeIdTokenClaims; @@ -160,6 +161,14 @@ public static ExecutionContext of(ExecutionContext context) { return executionContext; } + public Integer getStatusListIndex() { + return statusListIndex; + } + + public void setStatusListIndex(Integer statusListIndex) { + this.statusListIndex = statusListIndex; + } + public String generateRandomTokenReferenceId() { tokenReferenceId = IdUtil.randomShortUUID(); return tokenReferenceId; diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/model/token/IdTokenFactory.java b/jans-auth-server/server/src/main/java/io/jans/as/server/model/token/IdTokenFactory.java index ac16c678af9..2c7eb73cf9b 100644 --- a/jans-auth-server/server/src/main/java/io/jans/as/server/model/token/IdTokenFactory.java +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/model/token/IdTokenFactory.java @@ -32,6 +32,7 @@ import io.jans.as.server.service.external.ExternalUpdateTokenService; import io.jans.as.server.service.external.context.DynamicScopeExternalContext; import io.jans.as.server.service.external.context.ExternalUpdateTokenContext; +import io.jans.as.server.service.token.StatusListService; import io.jans.model.JansAttribute; import io.jans.model.custom.script.conf.CustomScriptConfiguration; import io.jans.model.custom.script.type.auth.PersonAuthenticationType; @@ -98,6 +99,9 @@ public class IdTokenFactory { @Inject private DateFormatterService dateFormatterService; + @Inject + private StatusListService statusListService; + private void setAmrClaim(JsonWebResponse jwt, String acrValues) { List amrList = Lists.newArrayList(); @@ -158,6 +162,8 @@ private void fillClaims(JsonWebResponse jwr, jwr.setClaim("sid", session.getOutsideSid()); } + statusListService.addStatusClaimWithIndex(jwr, executionContext); + addTokenExchangeClaims(jwr, executionContext, session); String acrValues = authorizationGrant.getAcrValues(); diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/model/token/JwtSigner.java b/jans-auth-server/server/src/main/java/io/jans/as/server/model/token/JwtSigner.java index 41a7b9b0b24..3d70ec0f95d 100644 --- a/jans-auth-server/server/src/main/java/io/jans/as/server/model/token/JwtSigner.java +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/model/token/JwtSigner.java @@ -80,7 +80,7 @@ public static JwtSigner newJwtSigner(AppConfiguration appConfiguration, JSONWebK return new JwtSigner(appConfiguration, webKeys, signatureAlgorithm, client.getClientId(), decryptedSecret); } - public Jwt newJwt() throws Exception { + public Jwt newJwt() throws CryptoProviderException { jwt = new Jwt(); // Header @@ -93,7 +93,9 @@ public Jwt newJwt() throws Exception { // Claims jwt.getClaims().setIssuer(appConfiguration.getIssuer()); - jwt.getClaims().setAudience(audience); + if (StringUtils.isNotBlank(audience)) { + jwt.getClaims().setAudience(audience); + } return jwt; } diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/revoke/RevokeRestWebServiceImpl.java b/jans-auth-server/server/src/main/java/io/jans/as/server/revoke/RevokeRestWebServiceImpl.java index c81853355ba..41367a2fc6a 100644 --- a/jans-auth-server/server/src/main/java/io/jans/as/server/revoke/RevokeRestWebServiceImpl.java +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/revoke/RevokeRestWebServiceImpl.java @@ -27,12 +27,6 @@ import io.jans.as.server.util.ServerUtil; import io.jans.model.token.TokenEntity; import io.jans.model.token.TokenType; - -import org.apache.commons.lang.ArrayUtils; -import org.apache.commons.lang.BooleanUtils; -import org.apache.commons.lang.StringUtils; -import org.slf4j.Logger; - import jakarta.inject.Inject; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; @@ -41,7 +35,12 @@ import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.SecurityContext; +import org.apache.commons.lang.ArrayUtils; +import org.apache.commons.lang.BooleanUtils; +import org.apache.commons.lang.StringUtils; +import org.slf4j.Logger; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -192,15 +191,21 @@ public void validateSameClient(AuthorizationGrant grant, Client client) { } private void removeAllTokens(TokenTypeHint tth, ExecutionContext executionContext) { - final List tokens = grantService.getGrantsOfClient(executionContext.getClient().getClientId()); + final String clientId = executionContext.getClient().getClientId(); + final List tokens = grantService.getGrantsOfClient(clientId); + log.debug("Revoking all tokens of client {}...", clientId); + + List tokensToRemove = new ArrayList<>(); for (TokenEntity token : tokens) { if (tth == null || (tth == TokenTypeHint.ACCESS_TOKEN && token.getTokenTypeEnum() == TokenType.ACCESS_TOKEN) || (tth == TokenTypeHint.TX_TOKEN && token.getTokenTypeEnum() == TokenType.TX_TOKEN) || (tth == TokenTypeHint.REFRESH_TOKEN && token.getTokenTypeEnum() == TokenType.REFRESH_TOKEN)) { - grantService.removeSilently(token); + tokensToRemove.add(token); } } + grantService.removeSilently(tokensToRemove); + log.debug("Revoked all tokens of client {}.", clientId); } private AuthorizationGrant findAuthorizationGrant(String token, TokenTypeHint tth) { diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/service/AppInitializer.java b/jans-auth-server/server/src/main/java/io/jans/as/server/service/AppInitializer.java index d1dca5e1844..d0fb6a91146 100644 --- a/jans-auth-server/server/src/main/java/io/jans/as/server/service/AppInitializer.java +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/service/AppInitializer.java @@ -15,6 +15,7 @@ import io.jans.as.server.service.cdi.event.AuthConfigurationEvent; import io.jans.as.server.service.cdi.event.ReloadAuthScript; import io.jans.as.server.service.ciba.CibaRequestsProcessorJob; +import io.jans.as.server.service.cluster.ClusterNodeManager; import io.jans.as.server.service.expiration.ExpirationNotificatorTimer; import io.jans.as.server.service.external.ExternalAuthenticationService; import io.jans.as.server.service.logger.LoggerService; @@ -138,6 +139,9 @@ public class AppInitializer { @Inject private PythonService pythonService; + + @Inject + private ClusterNodeManager clusterManager; @Inject private MetricService metricService; @@ -248,6 +252,7 @@ public void applicationInitialized(@Observes @Initialized(ApplicationScoped.clas initSchedulerService(); // Schedule timer tasks + clusterManager.initTimer(); metricService.initTimer(); configurationFactory.initTimer(); loggerService.initTimer(true); diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/service/CleanerTimer.java b/jans-auth-server/server/src/main/java/io/jans/as/server/service/CleanerTimer.java index b057ea561a8..b047cde1209 100644 --- a/jans-auth-server/server/src/main/java/io/jans/as/server/service/CleanerTimer.java +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/service/CleanerTimer.java @@ -51,7 +51,6 @@ */ @ApplicationScoped @DependsOn("appInitializer") -@Named public class CleanerTimer { public static final int BATCH_SIZE = 1000; diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/service/ClientService.java b/jans-auth-server/server/src/main/java/io/jans/as/server/service/ClientService.java index 95025bbb794..75d2663be9f 100644 --- a/jans-auth-server/server/src/main/java/io/jans/as/server/service/ClientService.java +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/service/ClientService.java @@ -6,8 +6,19 @@ package io.jans.as.server.service; +import static org.apache.commons.lang3.BooleanUtils.isTrue; + +import java.util.Collection; +import java.util.List; +import java.util.Set; + +import org.apache.commons.lang3.BooleanUtils; +import org.json.JSONArray; +import org.slf4j.Logger; + import com.google.common.base.Preconditions; import com.google.common.collect.Sets; + import io.jans.as.common.model.registration.Client; import io.jans.as.model.common.AuthenticationMethod; import io.jans.as.model.config.StaticConfiguration; @@ -23,18 +34,8 @@ import io.jans.util.StringHelper; import io.jans.util.security.StringEncrypter; import io.jans.util.security.StringEncrypter.EncryptionException; -import jakarta.ejb.Stateless; +import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; -import jakarta.inject.Named; -import org.apache.commons.lang3.BooleanUtils; -import org.json.JSONArray; -import org.slf4j.Logger; - -import java.util.Collection; -import java.util.List; -import java.util.Set; - -import static org.apache.commons.lang3.BooleanUtils.isTrue; /** * Provides operations with clients. @@ -43,8 +44,7 @@ * @author Yuriy Movchan Date: 04/15/2014 * @version October 22, 2016 */ -@Stateless -@Named +@ApplicationScoped public class ClientService { @Inject diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/service/DiscoveryService.java b/jans-auth-server/server/src/main/java/io/jans/as/server/service/DiscoveryService.java index 22d256a3c10..76648e76d78 100644 --- a/jans-auth-server/server/src/main/java/io/jans/as/server/service/DiscoveryService.java +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/service/DiscoveryService.java @@ -40,28 +40,28 @@ public class DiscoveryService { private Logger log; @Inject - private transient AppConfiguration appConfiguration; + private AppConfiguration appConfiguration; @Inject - private transient ExternalAuthzDetailTypeService externalAuthzDetailTypeService; + private ExternalAuthzDetailTypeService externalAuthzDetailTypeService; @Inject - private transient CIBAConfigurationService cibaConfigurationService; + private CIBAConfigurationService cibaConfigurationService; @Inject - private transient LocalResponseCache localResponseCache; + private LocalResponseCache localResponseCache; @Inject - private transient ExternalAuthenticationService externalAuthenticationService; + private ExternalAuthenticationService externalAuthenticationService; @Inject - private transient ExternalDynamicScopeService externalDynamicScopeService; + private ExternalDynamicScopeService externalDynamicScopeService; @Inject - private transient ScopeService scopeService; + private ScopeService scopeService; @Inject - private transient AttributeService attributeService; + private AttributeService attributeService; public JSONObject process() { JSONObject jsonObj = new JSONObject(); @@ -74,6 +74,8 @@ public JSONObject process() { jsonObj.put(ARCHIVED_JWKS_URI, appConfiguration.getArchivedJwksUri()); jsonObj.put(CHECK_SESSION_IFRAME, appConfiguration.getCheckSessionIFrame()); + if (appConfiguration.isFeatureEnabled(FeatureFlagType.STATUS_LIST)) + jsonObj.put(STATUS_LIST_ENDPOINT, getTokenStatusListEndpoint()); if (appConfiguration.isFeatureEnabled(FeatureFlagType.REVOKE_TOKEN)) jsonObj.put(REVOCATION_ENDPOINT, appConfiguration.getTokenRevocationEndpoint()); if (appConfiguration.isFeatureEnabled(FeatureFlagType.REVOKE_SESSION)) @@ -223,9 +225,16 @@ public JSONObject process() { return jsonObj; } + public String endpointUrl(String path) { + return endpointUrl(appConfiguration.getEndSessionEndpoint(), path); + } + + public static String endpointUrl(String endSessionEndpoint, String path) { + return StringUtils.replace(endSessionEndpoint, "/end_session", path); + } - private String endpointUrl(String path) { - return StringUtils.replace(appConfiguration.getEndSessionEndpoint(), "/end_session", path); + public String getTokenStatusListEndpoint() { + return endpointUrl("/status_list"); } @@ -279,6 +288,8 @@ private void addMtlsAliases(JSONObject jsonObj) { aliases.put(AUTHORIZATION_CHALLENGE_ENDPOINT, appConfiguration.getMtlsAuthorizationChallengeEndpoint()); if (StringUtils.isNotBlank(appConfiguration.getMtlsTokenEndpoint())) aliases.put(TOKEN_ENDPOINT, appConfiguration.getMtlsTokenEndpoint()); + if (appConfiguration.isFeatureEnabled(FeatureFlagType.STATUS_LIST) && StringUtils.isNotBlank(appConfiguration.getMtlsEndSessionEndpoint())) + aliases.put(STATUS_LIST_ENDPOINT, endpointUrl(appConfiguration.getMtlsEndSessionEndpoint(), "/status_list")); if (StringUtils.isNotBlank(appConfiguration.getMtlsJwksUri())) aliases.put(JWKS_URI, appConfiguration.getMtlsJwksUri()); if (StringUtils.isNotBlank(appConfiguration.getMtlsCheckSessionIFrame())) @@ -286,9 +297,9 @@ private void addMtlsAliases(JSONObject jsonObj) { if (appConfiguration.isFeatureEnabled(FeatureFlagType.REVOKE_TOKEN) && StringUtils.isNotBlank(appConfiguration.getMtlsTokenRevocationEndpoint())) aliases.put(REVOCATION_ENDPOINT, appConfiguration.getMtlsTokenRevocationEndpoint()); if (appConfiguration.isFeatureEnabled(FeatureFlagType.REVOKE_SESSION) && StringUtils.isNotBlank(appConfiguration.getMtlsEndSessionEndpoint())) - aliases.put(SESSION_REVOCATION_ENDPOINT, StringUtils.replace(appConfiguration.getMtlsEndSessionEndpoint(), "/end_session", "/revoke_session")); + aliases.put(SESSION_REVOCATION_ENDPOINT, endpointUrl(appConfiguration.getMtlsEndSessionEndpoint(), "/revoke_session")); if (appConfiguration.isFeatureEnabled(FeatureFlagType.GLOBAL_TOKEN_REVOCATION) && StringUtils.isNotBlank(appConfiguration.getMtlsEndSessionEndpoint())) - aliases.put(GLOBAL_TOKEN_REVOCATION_ENDPOINT, StringUtils.replace(appConfiguration.getMtlsEndSessionEndpoint(), "/end_session", "/global-token-revocation")); + aliases.put(GLOBAL_TOKEN_REVOCATION_ENDPOINT, endpointUrl(appConfiguration.getMtlsEndSessionEndpoint(), "/global-token-revocation")); if (appConfiguration.isFeatureEnabled(FeatureFlagType.USERINFO) && StringUtils.isNotBlank(appConfiguration.getMtlsUserInfoEndpoint())) aliases.put(USER_INFO_ENDPOINT, appConfiguration.getMtlsUserInfoEndpoint()); if (appConfiguration.isFeatureEnabled(FeatureFlagType.CLIENTINFO) && StringUtils.isNotBlank(appConfiguration.getMtlsClientInfoEndpoint())) diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/service/GrantService.java b/jans-auth-server/server/src/main/java/io/jans/as/server/service/GrantService.java index 8db9cf86ffd..813755d01fd 100644 --- a/jans-auth-server/server/src/main/java/io/jans/as/server/service/GrantService.java +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/service/GrantService.java @@ -12,18 +12,18 @@ import io.jans.as.model.configuration.LockMessageConfig; import io.jans.as.server.model.common.AuthorizationGrant; import io.jans.as.server.model.common.CacheGrant; +import io.jans.as.server.service.token.StatusListIndexService; import io.jans.as.server.util.TokenHashUtil; import io.jans.model.token.TokenEntity; import io.jans.model.token.TokenType; +import io.jans.model.tokenstatus.TokenStatus; import io.jans.orm.PersistenceEntryManager; import io.jans.orm.search.filter.Filter; import io.jans.service.CacheService; import io.jans.service.MessageService; -import io.jans.service.cache.CacheConfiguration; import io.jans.util.StringHelper; -import jakarta.ejb.Stateless; +import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; -import jakarta.inject.Named; import org.apache.commons.lang.StringUtils; import org.slf4j.Logger; @@ -31,6 +31,8 @@ import java.util.Collections; import java.util.List; import java.util.UUID; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import static org.apache.commons.lang.BooleanUtils.isTrue; @@ -39,10 +41,16 @@ * @author Javier Rojas Blum * @version November 28, 2018 */ -@Stateless -@Named +@ApplicationScoped public class GrantService { + private static final ExecutorService statusListPool = Executors.newFixedThreadPool(5, runnable -> { + Thread thread = new Thread(runnable); + thread.setName("grant_service_status_list_pool"); + thread.setDaemon(true); + return thread; + }); + @Inject private Logger log; @@ -65,7 +73,7 @@ public class GrantService { private AppConfiguration appConfiguration; @Inject - private CacheConfiguration cacheConfiguration; + private StatusListIndexService statusListIndexService; public static String generateGrantId() { return UUID.randomUUID().toString(); @@ -153,6 +161,13 @@ public void removeSilently(TokenEntity token) { if (shouldSaveInCache()) { cacheService.remove(token.getTokenCode()); } + + statusListPool.execute(() -> { + final Integer index = token.getAttributes().getStatusListIndex(); + if (index != null && index > 0) { + statusListIndexService.updateStatusAtIndexes(Lists.newArrayList(index), TokenStatus.INVALID); + } + }); } catch (Exception e) { log.error(e.getMessage(), e); } @@ -172,9 +187,30 @@ public void remove(List entries) { public void removeSilently(List entries) { if (entries != null && !entries.isEmpty()) { + List indexes = new ArrayList<>(); for (TokenEntity t : entries) { - removeSilently(t); + try { + remove(t); + + if (StringUtils.isNotBlank(t.getAuthorizationCode())) { + cacheService.remove(CacheGrant.cacheKey(t.getAuthorizationCode(), t.getGrantId())); + } + if (shouldSaveInCache()) { + cacheService.remove(t.getTokenCode()); + } + + final Integer index = t.getAttributes().getStatusListIndex(); + if (index != null && index >= 0) { + indexes.add(index); + } + } catch (Exception e) { + log.error(e.getMessage(), e); + } } + + statusListPool.execute(() -> { + statusListIndexService.updateStatusAtIndexes(indexes, TokenStatus.INVALID); + }); } } diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/service/ResteasyInitializer.java b/jans-auth-server/server/src/main/java/io/jans/as/server/service/ResteasyInitializer.java index 1bae2506ddd..cd1f78a8b2b 100644 --- a/jans-auth-server/server/src/main/java/io/jans/as/server/service/ResteasyInitializer.java +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/service/ResteasyInitializer.java @@ -25,6 +25,7 @@ import io.jans.as.server.session.ws.rs.EndSessionRestWebServiceImpl; import io.jans.as.server.session.ws.rs.SessionRestWebService; import io.jans.as.server.ssa.ws.rs.SsaRestWebServiceImpl; +import io.jans.as.server.status.ws.rs.StatusListRestWebService; import io.jans.as.server.token.ws.rs.TokenRestWebServiceImpl; import io.jans.as.server.uma.ws.rs.*; import io.jans.as.server.userinfo.ws.rs.UserInfoRestWebServiceImpl; @@ -56,6 +57,7 @@ public Set> getClasses() { classes.add(RevokeRestWebServiceImpl.class); classes.add(RevokeSessionRestWebService.class); classes.add(GlobalTokenRevocationRestWebService.class); + classes.add(StatusListRestWebService.class); classes.add(JwkRestWebServiceImpl.class); classes.add(ArchivedJwksWebServiceImpl.class); classes.add(IntrospectionWebService.class); diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/service/ScopeService.java b/jans-auth-server/server/src/main/java/io/jans/as/server/service/ScopeService.java index d4db7e458f5..5b675104848 100644 --- a/jans-auth-server/server/src/main/java/io/jans/as/server/service/ScopeService.java +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/service/ScopeService.java @@ -6,7 +6,18 @@ package io.jans.as.server.service; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.commons.lang.StringUtils; +import org.json.JSONArray; +import org.slf4j.Logger; + import com.google.common.collect.Lists; + import io.jans.as.common.model.common.User; import io.jans.as.common.service.AttributeService; import io.jans.as.model.config.StaticConfiguration; @@ -22,23 +33,14 @@ import io.jans.service.CacheService; import io.jans.service.LocalCacheService; import io.jans.util.StringHelper; -import org.apache.commons.lang.StringUtils; -import org.json.JSONArray; -import org.slf4j.Logger; - +import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; -import jakarta.inject.Named; -import java.util.ArrayList; -import java.util.Date; -import java.util.HashMap; -import java.util.List; -import java.util.Map; /** * @author Javier Rojas Blum Date: 07.05.2012 * @author Yuriy Movchan Date: 2016/04/26 */ -@Named +@ApplicationScoped public class ScopeService { @Inject @@ -53,9 +55,6 @@ public class ScopeService { @Inject private LocalCacheService localCacheService; - @Inject - private PersistenceEntryManager ldapEntryManager; - @Inject private StaticConfiguration staticConfiguration; @@ -73,7 +72,7 @@ public class ScopeService { public List getAllScopesList() { String scopesBaseDN = staticConfiguration.getBaseDn().getScopes(); - return ldapEntryManager.findEntries(scopesBaseDN, + return entryManager.findEntries(scopesBaseDN, Scope.class, Filter.createPresenceFilter("inum")); } @@ -125,7 +124,7 @@ public List getScopeIdsByDns(List dns) { */ public Scope getScopeByDn(String dn) { BaseCacheService usedCacheService = getCacheService(); - final Scope scope = usedCacheService.getWithPut(dn, () -> ldapEntryManager.find(Scope.class, dn), 60); + final Scope scope = usedCacheService.getWithPut(dn, () -> entryManager.find(Scope.class, dn), 60); if (scope != null && StringUtils.isNotBlank(scope.getId())) { usedCacheService.put(scope.getId(), scope); // put also by id, since we call it by id and dn } @@ -160,7 +159,7 @@ public Scope getScopeById(String id) { return (Scope) cached; try { - List scopes = ldapEntryManager.findEntries( + List scopes = entryManager.findEntries( staticConfiguration.getBaseDn().getScopes(), Scope.class, Filter.createEqualityFilter("jansId", id)); if ((scopes != null) && (scopes.size() > 0)) { final Scope scope = scopes.get(0); @@ -186,7 +185,7 @@ public List getScopeByClaim(String claimDn) { Filter filter = Filter.createEqualityFilter("jansClaim", claimDn); String scopesBaseDN = staticConfiguration.getBaseDn().getScopes(); - scopes = ldapEntryManager.findEntries(scopesBaseDN, Scope.class, filter); + scopes = entryManager.findEntries(scopesBaseDN, Scope.class, filter); putInCache(claimDn, scopes); } @@ -237,7 +236,7 @@ private static String getClaimDnCacheKey(String claimDn) { } public void persist(Scope scope) { - ldapEntryManager.persist(scope); + entryManager.persist(scope); } private BaseCacheService getCacheService() { diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/service/cdi/event/TokenPoolUpdateEvent.java b/jans-auth-server/server/src/main/java/io/jans/as/server/service/cdi/event/TokenPoolUpdateEvent.java new file mode 100644 index 00000000000..3c97fe9ffc3 --- /dev/null +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/service/cdi/event/TokenPoolUpdateEvent.java @@ -0,0 +1,13 @@ +/* + * Janssen Project software is available under the Apache License (2004). See http://www.apache.org/licenses/ for full text. + * + * Copyright (c) 2020, Janssen Project + */ + +package io.jans.as.server.service.cdi.event; + +/** + * @author Yuriy Movchan Date: 06/07/2024 + */ +public class TokenPoolUpdateEvent { +} diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/service/cluster/ClusterNodeManager.java b/jans-auth-server/server/src/main/java/io/jans/as/server/service/cluster/ClusterNodeManager.java new file mode 100644 index 00000000000..c0dee8de247 --- /dev/null +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/service/cluster/ClusterNodeManager.java @@ -0,0 +1,107 @@ +/* + * Janssen Project software is available under the Apache License (2004). See http://www.apache.org/licenses/ for full text. + * + * Copyright (c) 2020, Janssen Project + */ + +package io.jans.as.server.service.cluster; + +import com.google.common.base.Preconditions; +import io.jans.as.server.service.cdi.event.TokenPoolUpdateEvent; +import io.jans.model.cluster.ClusterNode; +import io.jans.service.cdi.async.Asynchronous; +import io.jans.service.cdi.event.Scheduled; +import io.jans.service.timer.event.TimerEvent; +import io.jans.service.timer.schedule.TimerSchedule; +import jakarta.annotation.PostConstruct; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.context.BeforeDestroyed; +import jakarta.enterprise.event.Event; +import jakarta.enterprise.event.Observes; +import jakarta.inject.Inject; +import jakarta.servlet.ServletContext; +import org.slf4j.Logger; + +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * @author Yuriy Movchan + * @version 1.0, 06/03/2024 + */ +@ApplicationScoped +public class ClusterNodeManager { + + @Inject + private Logger log; + + @Inject + private ClusterNodeService clusterNodeService; + + @Inject + private Event timerEvent; + + private AtomicBoolean isActive; + + private ClusterNode node; + + @PostConstruct + public void init() { + log.info("Initializing Cluster Node Manager ..."); + this.isActive = new AtomicBoolean(false); + + this.node = clusterNodeService.allocate(); + if (node != null) { + log.info("Assigned cluster node id '{}' for this instance", node.getId()); + } else { + log.error("Failed to initialize Cluster Node Manager."); + } + } + + public void initTimer() { + log.debug("Initializing Policy Download Service Timer"); + + final int delayInSeconds = 30; + final int intervalInSeconds = 30; + + timerEvent.fire(new TimerEvent(new TimerSchedule(delayInSeconds, intervalInSeconds), new TokenPoolUpdateEvent(), + Scheduled.Literal.INSTANCE)); + } + + @Asynchronous + public void reloadNodesTimerEvent(@Observes @Scheduled TokenPoolUpdateEvent tokenPoolUpdateEvent) { + if (this.isActive.get()) { + return; + } + + if (!this.isActive.compareAndSet(false, true)) { + return; + } + + try { + updateNode(); + } catch (Throwable ex) { + log.error("Exception happened while reloading nodes", ex); + } finally { + this.isActive.set(false); + } + } + + private void updateNode() { + checkNodeNotNull(); + clusterNodeService.refresh(node); + } + + + public void destroy(@Observes @BeforeDestroyed(ApplicationScoped.class) ServletContext init) { + log.info("Stopped cluster manager"); + } + + public Integer getClusterNodeId() { + checkNodeNotNull(); + return node.getId(); + } + + private void checkNodeNotNull() { + Preconditions.checkNotNull(node, "Failed to allocate cluster node."); + } +} \ No newline at end of file diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/service/cluster/ClusterNodeService.java b/jans-auth-server/server/src/main/java/io/jans/as/server/service/cluster/ClusterNodeService.java new file mode 100644 index 00000000000..4dbe69a75c5 --- /dev/null +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/service/cluster/ClusterNodeService.java @@ -0,0 +1,251 @@ +/* + * Janssen Project software is available under the Apache License (2004). See http://www.apache.org/licenses/ for full text. + * + * Copyright (c) 2020, Janssen Project + */ + +package io.jans.as.server.service.cluster; + +import io.jans.as.model.config.StaticConfiguration; +import io.jans.exception.ConfigurationException; +import io.jans.model.cluster.ClusterNode; +import io.jans.orm.PersistenceEntryManager; +import io.jans.orm.exception.EntryPersistenceException; +import io.jans.orm.model.PagedResult; +import io.jans.orm.model.SortOrder; +import io.jans.orm.search.filter.Filter; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import org.apache.tika.utils.StringUtils; +import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.UUID; + +/** + * @author Yuriy Movchan + * @version 1.0, 06/03/2024 + */ +@ApplicationScoped +public class ClusterNodeService { + + public static final int ATTEMPT_LIMIT = 10; + + public static final long DELAY_AFTER_EXPIRATION = 3 * 60 * 1000L; // 3 minutes + public static final String CLUSTER_TYPE_JANS_AUTH = "jans-auth"; + public static final String JANS_TYPE_ATTR_NAME = "jansType"; + + public static final String LOCK_KEY = UUID.randomUUID().toString(); + + @Inject + private Logger log; + + @Inject + private StaticConfiguration staticConfiguration; + + @Inject + private PersistenceEntryManager entryManager; + + /** + * returns ClusterNode by Dn + * + * @return ClusterNode + */ + public ClusterNode getClusterNodeByDn(String dn) { + return entryManager.find(ClusterNode.class, dn); + } + + /** + * returns ClusterNode by Id + * + * @return ClusterNode + */ + public ClusterNode getClusterNodeById(Integer id) { + return entryManager.find(ClusterNode.class, getDnForClusterNode(id)); + } + + /** + * returns a list of all ClusterNodes + * + * @return list of ClusterNodes + */ + public List getAllClusterNodes() { + String clusterNodesBaseDn = staticConfiguration.getBaseDn().getNode(); + + return entryManager.findEntries(clusterNodesBaseDn, ClusterNode.class, getTypeFilter()); + } + + public List getClusterNodesDns(List nodeIds) { + List clusterNodesDns = new ArrayList<>(); + + for (Integer nodeId : nodeIds) { + ClusterNode clusterNode = getClusterNodeById(nodeId); + if (clusterNode != null) { + clusterNodesDns.add(clusterNode.getDn()); + } + } + + return clusterNodesDns; + } + + /** + * returns last TokenPool or null if none + * + * @return TokenPool + */ + public ClusterNode getClusterNodeLast() { + String clusterNodesBaseDn = staticConfiguration.getBaseDn().getNode(); + + int count = 1; + if (PersistenceEntryManager.PERSITENCE_TYPES.ldap.name().equals(entryManager.getPersistenceType(clusterNodesBaseDn))) { + count = Integer.MAX_VALUE; + } + + PagedResult pagedResult = entryManager.findPagedEntries(clusterNodesBaseDn, ClusterNode.class, + Filter.createEqualityFilter("jansType", CLUSTER_TYPE_JANS_AUTH), null, "jansNum", SortOrder.DESCENDING, + 0, count, count); + if (pagedResult.getEntriesCount() >= 1) { + return pagedResult.getEntries().get(0); + } + + return null; + } + + /** + * returns a list of expired ClusterNodes + * + * @return list of ClusterNodes + */ + public List getClusterNodesExpired() { + String clusterNodesBaseDn = staticConfiguration.getBaseDn().getNode(); + if (StringUtils.isBlank(clusterNodesBaseDn)) { + throw new ConfigurationException("ou=node is not configured in static configuration of AS (jansConfStatic)."); + } + + Date expirationDate = new Date(System.currentTimeMillis() - DELAY_AFTER_EXPIRATION); + + Filter filter = Filter.createANDFilter(Filter.createEqualityFilter("jansType", CLUSTER_TYPE_JANS_AUTH), + Filter.createORFilter(Filter.createEqualityFilter("jansLastUpd", null), Filter.createLessOrEqualFilter( + "jansLastUpd", entryManager.encodeTime(clusterNodesBaseDn, expirationDate)))); + + return entryManager.findEntries(clusterNodesBaseDn, ClusterNode.class, filter); + } + + @NotNull + private Filter getTypeFilter() { + return Filter.createEqualityFilter(JANS_TYPE_ATTR_NAME, CLUSTER_TYPE_JANS_AUTH); + } + + protected void persist(ClusterNode clusterNode) { + entryManager.persist(clusterNode); + } + + public void update(ClusterNode clusterNode) { + entryManager.merge(clusterNode); + } + + // all logs must be INFO here, because allocate method is called during initialization before + // LoggerService set loggingLevel from config. + public ClusterNode allocate() { + log.info("Allocating node, LOCK_KEY {}... ", LOCK_KEY); + + // Try to use existing expired entry (node is expired if not used for 3 minutes) + List expiredNodes = getClusterNodesExpired(); + log.info("Allocation - found {} expired nodes.", expiredNodes.size()); + + for (ClusterNode expiredNode : expiredNodes) { + // Do lock operation in try/catch for safety and do not throw error to upper levels + try { + Date currentTime = new Date(); + + expiredNode.setCreationDate(currentTime); + expiredNode.setLastUpdate(currentTime); + expiredNode.setLockKey(LOCK_KEY); + + update(expiredNode); + + // Load node after update + ClusterNode lockedNode = getClusterNodeByDn(expiredNode.getDn()); + + // If lock is ours reset entry and return it + if (LOCK_KEY.equals(lockedNode.getLockKey())) { + log.info("Re-using existing node {}, LOCK_KEY {}", lockedNode.getId(), LOCK_KEY); + return lockedNode; + } + + log.info("Failed to lock node {}, LOCK_KEY {}", lockedNode.getId(), LOCK_KEY); + } catch (EntryPersistenceException ex) { + log.debug("Unexpected error happened during entry lock", ex); + } + } + + // There are no free entries. server need to add new one with next index + int attempt = 1; + do { + log.info("Attempting to persist new node. Attempt {} out of {} ...", attempt, ATTEMPT_LIMIT); + + ClusterNode lastClusterNode = getClusterNodeLast(); + log.info("lastClusterNode - {}, LOCK_KEY {}", lastClusterNode != null ? lastClusterNode.getId() : -1, LOCK_KEY); + + Integer lastClusterNodeIndex = lastClusterNode == null ? 0 : lastClusterNode.getId() + 1; + + Date currentTime = new Date(); + + ClusterNode node = new ClusterNode(); + node.setId(lastClusterNodeIndex); + node.setDn(getDnForClusterNode(lastClusterNodeIndex)); + node.setCreationDate(currentTime); + node.setLastUpdate(currentTime); + node.setType(CLUSTER_TYPE_JANS_AUTH); + node.setLockKey(LOCK_KEY); + + // Do persist operation in try/catch for safety and do not throw error to upper levels + try { + persist(node); + + // Load node after update + ClusterNode lockedNode = getClusterNodeByDn(node.getDn()); + + // if lock is ours return it + if (LOCK_KEY.equals(lockedNode.getLockKey())) { + log.info("Successfully created new cluster node {}", node); + return lockedNode; + } else { + log.info("Locked key does not match. nodeLockKey {} of node {}", lockedNode.getLockKey(), lockedNode.getId()); + } + } catch (EntryPersistenceException ex) { + log.debug("Unexpected error happened during entry lock, LOCK_KEY " + LOCK_KEY, ex); + } + + attempt++; + } while (attempt <= ATTEMPT_LIMIT); + + return null; + } + + public void refresh(ClusterNode node) { + node.setLastUpdate(new Date()); + + log.trace("Refreshing node: {}", node); + update(node); + } + + public ClusterNode reset(ClusterNode node) { + Date currentTime = new Date(); + node.setCreationDate(currentTime); + node.setLastUpdate(currentTime); + + log.trace("Reseting node: {}", node); + update(node); + + return node; + } + + public String getDnForClusterNode(Integer id) { + return String.format("jansNum=%d,%s", id, staticConfiguration.getBaseDn().getNode()); + } + +} \ No newline at end of file diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/service/cluster/StatusIndexPoolService.java b/jans-auth-server/server/src/main/java/io/jans/as/server/service/cluster/StatusIndexPoolService.java new file mode 100644 index 00000000000..12ac6ab926b --- /dev/null +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/service/cluster/StatusIndexPoolService.java @@ -0,0 +1,309 @@ +/* + * Janssen Project software is available under the Apache License (2004). See http://www.apache.org/licenses/ for full text. + * + * Copyright (c) 2020, Janssen Project + */ + +package io.jans.as.server.service.cluster; + +import io.jans.as.model.config.StaticConfiguration; +import io.jans.as.model.configuration.AppConfiguration; +import io.jans.as.server.service.token.StatusListIndexService; +import io.jans.model.token.StatusIndexPool; +import io.jans.model.tokenstatus.TokenStatus; +import io.jans.orm.PersistenceEntryManager; +import io.jans.orm.exception.EntryPersistenceException; +import io.jans.orm.model.PagedResult; +import io.jans.orm.model.SortOrder; +import io.jans.orm.search.filter.Filter; +import io.jans.service.cdi.util.CdiUtil; +import jakarta.annotation.PostConstruct; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import org.slf4j.Logger; + +import java.util.Date; +import java.util.List; + +import static io.jans.as.server.service.cluster.ClusterNodeService.LOCK_KEY; + +/** + * @author Yuriy Movchan + * @version 1.0, 06/03/2024 + */ +@ApplicationScoped +public class StatusIndexPoolService { + + public static final int ATTEMPT_LIMIT = 10; + public static long DELAY_AFTER_EXPIRATION = 3 * 60 * 60 * 1000L; // 3 hours + public static long LOCK_WAIT_BEFORE_UPDATE = 3 * 1000L; // 30 seconds + public static long DELAY_IF_LOCKED = 500; // 50 milliseconds + @Inject + private Logger log; + + @Inject + private StaticConfiguration staticConfiguration; + + @Inject + private AppConfiguration appConfiguration; + + @Inject + private PersistenceEntryManager entryManager; + + // Don't allow to change it after server start up. After setting new value we need to restart cluster + private int indexAllocationBlockSize; + + public static StatusIndexPool setIndexes(StatusIndexPool pool, int indexAllocationBlockSize) { + if (pool == null) { + return pool; + } + + int index = pool.getId(); + + pool.setStartIndex(index * indexAllocationBlockSize); + pool.setEndIndex((index + 1) * indexAllocationBlockSize - 1); + + return pool; + } + + @PostConstruct + public void init() { + log.info("Initializing Status Index Pool Service ..."); + indexAllocationBlockSize = appConfiguration.getStatusListIndexAllocationBlockSize(); + } + + /** + * Returns pool by dn + * + * @return pool + */ + public StatusIndexPool getPoolByDn(String dn) { + return setIndexes(entryManager.find(StatusIndexPool.class, dn)); + } + + /** + * Returns pool by Id + * + * @return pool + */ + public StatusIndexPool getPoolById(int id) { + return getPoolByDn(createDn(id)); + } + + /** + * Returns a list of all pools + * + * @return list of pools + */ + public List getAllPools() { + return setIndexes(entryManager.findEntries(baseDn(), StatusIndexPool.class, Filter.createPresenceFilter("jansNum"))); + } + + /** + * Returns last (max) pool or null if none + * + * @return pool + */ + public StatusIndexPool getPoolLast() { + String baseDn = baseDn(); + + int count = 1; + if (PersistenceEntryManager.PERSITENCE_TYPES.ldap.name().equals(entryManager.getPersistenceType(baseDn))) { + count = Integer.MAX_VALUE; + } + + PagedResult pagedResult = entryManager.findPagedEntries(baseDn, StatusIndexPool.class, + Filter.createPresenceFilter("jansNum"), null, "jansNum", SortOrder.DESCENDING, 0, count, count); + if (pagedResult.getEntriesCount() >= 1) { + return setIndexes(pagedResult.getEntries().get(0)); + } + + return null; + } + + /** + * Gets pool by global status list index + * + * @param index status list index + * @return pool + */ + public StatusIndexPool getPoolByIndex(int index) { + int poolId = index / indexAllocationBlockSize; + + return getPoolById(poolId); + } + + /** + * Returns a list of all StatusIndexPools associated with ClusterNode + * + * @return list of pools + */ + public List getNodePools(Integer nodeId) { + String baseDn = baseDn(); + + return setIndexes(entryManager.findEntries(baseDn, StatusIndexPool.class, Filter.createEqualityFilter("jansNodeId", nodeId))); + } + + /** + * Returns a list of expired pools + * + * @return list of pools + */ + public List getPoolsExpired() { + final String baseDn = baseDn(); + + Date expirationDate = new Date(System.currentTimeMillis() - DELAY_AFTER_EXPIRATION); + + Filter filter = Filter.createORFilter(Filter.createEqualityFilter("exp", null), + Filter.createLessOrEqualFilter("exp", entryManager.encodeTime(baseDn, expirationDate))); + + return setIndexes(entryManager.findEntries(baseDn, StatusIndexPool.class, filter)); + } + + protected void persist(StatusIndexPool pool) { + entryManager.persist(pool); + } + + public void update(StatusIndexPool pool) { + entryManager.merge(pool); + } + + public StatusIndexPool updateWithLock(StatusIndexPool pool) { + log.debug("Attempt to update pool {} with lock {}...", pool.getId(), LOCK_KEY); + + int attempt = 1; + do { + StatusIndexPool loadedPool = getPoolByDn(pool.getDn()); + + loadedPool.setLockKey(LOCK_KEY); + loadedPool.setData(pool.getData()); + loadedPool.setLastUpdate(new Date()); + loadedPool.setExpirationDate(pool.getExpirationDate()); + + update(loadedPool); + + // reload and check lock + loadedPool = getPoolByDn(loadedPool.getDn()); + + // if lock is ours do data update and release lock + if (LOCK_KEY.equals(loadedPool.getLockKey())) { + log.debug("Updated pool {} with lock with attempt {}", loadedPool.getId(), LOCK_KEY, attempt); + return loadedPool; + } else { + log.debug("Failed to update pool {} with lock {} with attempt {}", loadedPool.getId(), LOCK_KEY, attempt); + } + + attempt++; + } while (attempt < ATTEMPT_LIMIT); + + log.error("Unable to update pool {} with lock {} with attempt {}", pool.getId(), LOCK_KEY, attempt); + + return null; + } + + public StatusIndexPool allocate(int nodeId) { + log.debug("Allocating status index pool, node {}, LOCK_KEY {}... ", nodeId, LOCK_KEY); + + // Try to use existing expired entry + List expiredPools = getPoolsExpired(); + + log.debug("Allocation - found {} expired status index pools, node {}.", expiredPools.size(), nodeId); + + // expiration date of the pool is double of access token lifetime + Date expirationDate = new Date(System.currentTimeMillis() + 2 * appConfiguration.getAccessTokenLifetime() * 1000); + + for (StatusIndexPool pool : expiredPools) { + // Do lock operation in try/catch for safety and do not throw error to upper levels + try { + pool.setLockKey(LOCK_KEY); + pool.setExpirationDate(expirationDate); + pool.setLastUpdate(new Date()); + pool.setNodeId(nodeId); + + update(pool); + + // Load pool after update + StatusIndexPool lockedPool = getPoolByDn(pool.getDn()); + + // If lock is ours reset entry and return it + if (LOCK_KEY.equals(lockedPool.getLockKey()) && lockedPool.getNodeId().equals(nodeId)) { + log.debug("Re-using existing status index pool {}, node {}, LOCK_KEY {}", lockedPool.getId(), nodeId, LOCK_KEY); + + // mark all indexes which we are re-using as VALID + StatusListIndexService indexService = CdiUtil.bean(StatusListIndexService.class); + indexService.updateStatusAtIndexes(lockedPool.enumerateAllIndexes(), TokenStatus.VALID); + return lockedPool; + } + } catch (EntryPersistenceException ex) { + log.debug("Unexpected error happened during entry lock, node " + nodeId, ex); + } + } + + // There are no free entries. server need to add new one with next index + int attempt = 1; + do { + log.debug("Attempting to persist new status index pool. Attempt {} out of {}. Node {}.", attempt, ATTEMPT_LIMIT, nodeId); + + StatusIndexPool lastPool = getPoolLast(); + + int lastPoolIndex = lastPool == null ? 0 : lastPool.getId() + 1; + + StatusIndexPool pool = new StatusIndexPool(); + pool.setId(lastPoolIndex); + pool.setDn(createDn(lastPoolIndex)); + pool.setNodeId(nodeId); + pool.setLastUpdate(new Date()); + pool.setLockKey(LOCK_KEY); + + // Do persist operation in try/catch for safety and do not throw error to upper + // levels + try { + expirationDate = new Date(System.currentTimeMillis() + 2 * appConfiguration.getAccessTokenLifetime() * 1000); + pool.setExpirationDate(expirationDate); + + persist(pool); + + // Load pool after update + StatusIndexPool lockedPool = getPoolByDn(pool.getDn()); + + // if lock is ours return it + if (LOCK_KEY.equals(lockedPool.getLockKey()) && lockedPool.getNodeId().equals(nodeId)) { + log.debug("Successfully created new status index pool {}, node {}", lockedPool.getId(), nodeId); + return setIndexes(lockedPool); + } else { + log.debug("Failed to create new status index pool {}, node {}", lockedPool.getId(), nodeId); + } + } catch (EntryPersistenceException ex) { + log.debug("Unexpected error happened during entry lock, node " + nodeId, ex); + } + attempt++; + } while (attempt <= ATTEMPT_LIMIT); + + // This should not happens + throw new EntryPersistenceException(String.format("Failed to allocate StatusIndexPool for node %s!!!", nodeId)); + } + + private StatusIndexPool setIndexes(StatusIndexPool pool) { + return setIndexes(pool, indexAllocationBlockSize); + } + + private List setIndexes(List pools) { + if (pools == null) { + return pools; + } + + for (StatusIndexPool pool : pools) { + setIndexes(pool); + } + return pools; + } + + public String baseDn() { + return staticConfiguration.getBaseDn().getStatusIndexPool(); + } + + public String createDn(int id) { + String baseDn = baseDn(); + return String.format("jansNum=%d,%s", id, baseDn); + } +} \ No newline at end of file diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/service/token/StatusListIndexService.java b/jans-auth-server/server/src/main/java/io/jans/as/server/service/token/StatusListIndexService.java new file mode 100644 index 00000000000..fbbe78dcd2c --- /dev/null +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/service/token/StatusListIndexService.java @@ -0,0 +1,133 @@ +package io.jans.as.server.service.token; + +import io.jans.as.model.configuration.AppConfiguration; +import io.jans.as.server.service.cluster.ClusterNodeManager; +import io.jans.as.server.service.cluster.StatusIndexPoolService; +import io.jans.model.token.StatusIndexPool; +import io.jans.model.tokenstatus.StatusList; +import io.jans.model.tokenstatus.TokenStatus; +import io.jans.util.Pair; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.locks.ReentrantLock; + +/** + * @author Yuriy Z + * @author Yuriy Movchan + * + * @version 1.0, 06/03/2024 + */ +@ApplicationScoped +public class StatusListIndexService { + + @Inject + private Logger log; + + @Inject + private StatusIndexPoolService statusTokenPoolService; + + @Inject + private AppConfiguration appConfiguration; + + @Inject + private ClusterNodeManager clusterManager; + + private final ReentrantLock allocatedLock = new ReentrantLock(); + + private StatusIndexPool tokenPool = null; + + public synchronized void updateStatusAtIndexes(List indexes, TokenStatus status) { + try { + log.debug("Updating status list at indexes {} with status {} ...", indexes, status); + + if (indexes == null || indexes.isEmpty()) { + return; // invalid + } + + final int bitSize = appConfiguration.getStatusListBitSize(); + + // first load pool for indexes and update in-memory + Map pools = new HashMap<>(); + for (Integer index : indexes) { + int poolId = index / appConfiguration.getStatusListIndexAllocationBlockSize(); + + StatusIndexPool indexHolder = pools.get(poolId); + if (indexHolder == null) { + indexHolder = statusTokenPoolService.getPoolByIndex(index); + log.debug("Found pool {} by index {}", indexHolder.getDn(), index); + pools.put(indexHolder.getId(), indexHolder); + } + + final String data = indexHolder.getData(); + + final StatusList statusList = StringUtils.isNotBlank(data) ? StatusList.fromEncoded(data, bitSize) : new StatusList(bitSize); + statusList.set(index, status.getValue()); + + indexHolder.setData(statusList.getLst()); + } + + for (StatusIndexPool pool : pools.values()) { + updateWithLockSilently(pool); + } + + log.debug("Updated status list at index {} with status {} successfully.", indexes, status); + + } catch (Exception e) { + log.error("Failed to update token list status at index " + indexes + " with status " + status, e); + } + } + + private void updateWithLockSilently(StatusIndexPool pool) { + try { + statusTokenPoolService.updateWithLock(pool); + } catch (Exception e) { + log.error("Failed to persist status index pool " + pool.getId(), e); + } + } + + public Integer next() { + return nextIndex().getFirst(); + } + + public Pair nextIndex() { + // Create copy of variable to make sure that another Thread not changed it + StatusIndexPool localTokenPool = tokenPool; + int newIndex = -1; + if (localTokenPool != null) { + newIndex = localTokenPool.nextIndex(); + if (newIndex != -1) { + return new Pair<>(newIndex, localTokenPool); + } + } + + // Attempt to lock before TokenPool allocating + allocatedLock.lock(); + try { + // Check if TokenPool were changed since method call + if (System.identityHashCode(localTokenPool) != System.identityHashCode(tokenPool)) { + // Try to get index from new pool which another threads gets already + localTokenPool = tokenPool; + if (localTokenPool != null) { + newIndex = localTokenPool.nextIndex(); + if (newIndex != -1) { + return new Pair<>(newIndex, localTokenPool); + } + } + } + + // Allocate new TokenPool + tokenPool = statusTokenPoolService.allocate(clusterManager.getClusterNodeId()); + + newIndex = tokenPool.nextIndex(); + return new Pair<>(newIndex, tokenPool); + } finally { + allocatedLock.unlock(); + } + } +} diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/service/token/StatusListService.java b/jans-auth-server/server/src/main/java/io/jans/as/server/service/token/StatusListService.java new file mode 100644 index 00000000000..c8c40cd7d45 --- /dev/null +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/service/token/StatusListService.java @@ -0,0 +1,201 @@ +package io.jans.as.server.service.token; + +import io.jans.as.model.common.FeatureFlagType; +import io.jans.as.model.config.WebKeysConfiguration; +import io.jans.as.model.configuration.AppConfiguration; +import io.jans.as.model.crypto.signature.SignatureAlgorithm; +import io.jans.as.model.error.ErrorResponseFactory; +import io.jans.as.model.exception.InvalidJwtException; +import io.jans.as.model.jwt.Jwt; +import io.jans.as.model.jwt.JwtType; +import io.jans.as.model.token.JsonWebResponse; +import io.jans.as.server.model.common.ExecutionContext; +import io.jans.as.server.model.token.JwtSigner; +import io.jans.as.server.service.DiscoveryService; +import io.jans.as.server.service.cluster.StatusIndexPoolService; +import io.jans.model.token.StatusIndexPool; +import io.jans.model.tokenstatus.StatusList; +import io.jans.model.tokenstatus.TokenStatus; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.ws.rs.WebApplicationException; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import org.apache.commons.lang.StringUtils; +import org.json.JSONObject; +import org.slf4j.Logger; + +import java.util.Calendar; +import java.util.Date; +import java.util.List; + +import static io.jans.as.model.config.Constants.CONTENT_TYPE_STATUSLIST_JSON; +import static io.jans.as.model.config.Constants.CONTENT_TYPE_STATUSLIST_JWT; + +/** + * @author Yuriy Z + */ +@ApplicationScoped +public class StatusListService { + + @Inject + private Logger log; + + @Inject + private AppConfiguration appConfiguration; + + @Inject + private ErrorResponseFactory errorResponseFactory; + + @Inject + private DiscoveryService discoveryService; + + @Inject + private StatusIndexPoolService statusTokenPoolService; + + @Inject + private WebKeysConfiguration webKeysConfiguration; + + public Response requestStatusList(String acceptHeader) { + log.debug("Attempting to request status_list, acceptHeader: {} ...", acceptHeader); + + errorResponseFactory.validateFeatureEnabled(FeatureFlagType.STATUS_LIST); + + try { + final List pools = statusTokenPoolService.getAllPools(); + final StatusList statusList = join(pools); + + final boolean isJsonRequested = CONTENT_TYPE_STATUSLIST_JSON.equalsIgnoreCase(acceptHeader); + + final String entity = createEntity(isJsonRequested, statusList); + final String responseType = isJsonRequested ? CONTENT_TYPE_STATUSLIST_JSON : CONTENT_TYPE_STATUSLIST_JWT; + + if (log.isTraceEnabled()) { + log.trace("Response entity {}, responseType {}", entity, responseType); + } + + return Response.status(Response.Status.OK).entity(entity).type(responseType).build(); + } catch (WebApplicationException e) { + if (log.isTraceEnabled()) { + log.trace(e.getMessage(), e); + } + throw e; + } catch (Exception e) { + log.error(e.getMessage(), e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR).type(MediaType.APPLICATION_JSON_TYPE).build(); + } + } + + private String createEntity(boolean isJsonRequested, StatusList statusList) throws Exception { + JSONObject jsonObject = new JSONObject(statusList.encodeAsJSON()); + + if (isJsonRequested) { + return jsonObject.toString(); + } + + return createResponseJwt(jsonObject); + } + + public StatusList join(List pools) { + final int bitSize = appConfiguration.getStatusListBitSize(); + + StatusList result = new StatusList(bitSize); + for (StatusIndexPool pool : pools) { + try { + final String data = pool.getData(); + if (StringUtils.isBlank(data)) { + continue; + } + + StatusList poolStatusList = StatusList.fromEncoded(data, bitSize); + for (int i = 0; i < poolStatusList.getBitSetLength(); i++) { + int value = poolStatusList.get(i); + boolean isNotDefault = value != TokenStatus.VALID.getValue(); + if (isNotDefault) { + result.set(i, value); + } + } + } catch (Exception e) { + String msg = String.format("Failed to process status list from pool: %s, nodeId: %s", pool.getId(), pool.getNodeId()); + log.error(msg, e); + } + } + return result; + } + + public void addStatusClaimWithIndex(JsonWebResponse jwr, ExecutionContext executionContext) { + if (!errorResponseFactory.isFeatureFlagEnabled(FeatureFlagType.STATUS_LIST)) { + log.trace("Skipped status claim addition because {} feature flag is disabled.", FeatureFlagType.STATUS_LIST.getValue()); + return; + } + + final Integer index = executionContext.getStatusListIndex(); + if (index == null || index < 0) { + return; // index is not set. It must be set into context to be saved in both entity bean and jwt consistently + } + + final JSONObject indexAndUri = new JSONObject(); + indexAndUri.put("idx", index); + indexAndUri.put("uri", getSub()); + + final JSONObject statusList = new JSONObject(); + statusList.put("status_list", indexAndUri); + + jwr.getClaims().setClaim("status", statusList); + } + + /** + * Returns sub of status list. It must be equal in both issued token jwt ("uri") and status list jwt ("sub"). + * + * @return Returns sub of status list + */ + public String getSub() { + return discoveryService.getTokenStatusListEndpoint(); + } + + public String createResponseJwt(JSONObject response) throws Exception { + log.trace("Creating status list JWT response {} ...", response); + + final JwtSigner jwtSigner = newJwtSigner(); + final Jwt jwt = jwtSigner.newJwt(); + jwt.getHeader().setType(JwtType.STATUS_LIST_JWT); + + fillPayload(jwt, response); + final String jwtString = jwtSigner.sign().toString(); + log.trace("Created status list JWT response {}", jwtString); + return jwtString; + } + + private JwtSigner newJwtSigner() { + SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.fromString(appConfiguration.getDefaultSignatureAlgorithm()); + if (appConfiguration.getStatusListResponseJwtSignatureAlgorithm() != null) { + signatureAlgorithm = SignatureAlgorithm.fromString(appConfiguration.getStatusListResponseJwtSignatureAlgorithm()); + } + + return new JwtSigner(appConfiguration, webKeysConfiguration, signatureAlgorithm, "", null); + } + + public void fillPayload(JsonWebResponse jwr, JSONObject response) throws InvalidJwtException { + final int lifetime = appConfiguration.getStatusListResponseJwtLifetime(); + + final Calendar calendar = Calendar.getInstance(); + final Date issuedAt = calendar.getTime(); + calendar.add(Calendar.SECOND, lifetime); + final Date expiration = calendar.getTime(); + + jwr.getClaims().setExpirationTime(expiration); + jwr.getClaims().setIssuedAt(issuedAt); + jwr.getClaims().setClaim("ttl", lifetime); + jwr.getClaims().setClaim("sub", getSub()); + + try { + jwr.getClaims().setClaim("status_list", response); + } catch (Exception e) { + log.error("Failed to put claims into status list jwt. Key: status_list, response: " + response.toString(), e); + } + + if (log.isTraceEnabled()) { + log.trace("Response before signing: {}", jwr.getClaims().toJsonString()); + } + } +} diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/status/ws/rs/StatusListRestWebService.java b/jans-auth-server/server/src/main/java/io/jans/as/server/status/ws/rs/StatusListRestWebService.java new file mode 100644 index 00000000000..e5906b7abdb --- /dev/null +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/status/ws/rs/StatusListRestWebService.java @@ -0,0 +1,39 @@ +package io.jans.as.server.status.ws.rs; + +import io.jans.as.server.service.token.StatusListService; +import jakarta.inject.Inject; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.Response; +import org.slf4j.Logger; + +import static io.jans.as.model.config.Constants.CONTENT_TYPE_STATUSLIST_JSON; +import static io.jans.as.model.config.Constants.CONTENT_TYPE_STATUSLIST_JWT; + +/** + * @author Yuriy Z + */ +@Path("/") +public class StatusListRestWebService { + + @Inject + private Logger log; + + @Inject + private StatusListService statusService; + + @GET + @Path("/status_list") + @Consumes({CONTENT_TYPE_STATUSLIST_JSON, CONTENT_TYPE_STATUSLIST_JWT}) + @Produces({CONTENT_TYPE_STATUSLIST_JSON, CONTENT_TYPE_STATUSLIST_JWT}) + public Response requestStatusList(@HeaderParam("Accept") String acceptHeader) { + try { + return statusService.requestStatusList(acceptHeader); + } catch (WebApplicationException e) { + log.debug(e.getMessage(), e); + throw e; + } catch (Exception e) { + log.error(e.getMessage(), e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build(); + } + } +} diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/uma/service/UmaRptService.java b/jans-auth-server/server/src/main/java/io/jans/as/server/uma/service/UmaRptService.java index ecc32c8075d..493e21bd4b4 100644 --- a/jans-auth-server/server/src/main/java/io/jans/as/server/uma/service/UmaRptService.java +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/uma/service/UmaRptService.java @@ -220,7 +220,7 @@ public UmaRPT createRPTAndPersist(ExecutionContext executionContext, List encodeAsJSON() throws IOException { + String encodedList = encodeAsString(); + Map object = new HashMap<>(); + object.put("bits", this.bits); + object.put("lst", encodedList); + return object; + } + + public void decode(String input) throws IOException { + byte[] zipped = Base64.getUrlDecoder().decode(input + "=".repeat((4 - input.length() % 4) % 4)); +// System.out.println("decode - zipped: " + new String(zipped)); + this.list = BitSet.valueOf(decompress(zipped)); + } + + public void set(int pos, int value) { +// int rest = pos % this.divisor; +// int floored = pos / this.divisor; +// int shift = rest * this.bits; +// int mask = 0xFF ^ (((1 << this.bits) - 1) << shift); +// this.list[floored] = (byte) ((this.list[floored] & mask) + (value << shift)); + for (int i = 0; i < this.bits; i++) { + boolean bitValue = ((value >> i) & 1) == 1; + this.list.set(pos * this.bits + i, bitValue); + } + } + + public void validateSetValue(int value) { + if (value < 0) { + throw new IllegalArgumentException("Status value can't be less then 0."); + } + + switch (bits) { + case 1: + if (value > 1) { + throw new IllegalArgumentException("Value can't be more then 1. With bits " + bits + "."); + } + return; + case 2: + if (value > 3) { + throw new IllegalArgumentException("Value can't be more then 3. With bits " + bits + "."); + } + return; + case 4: + if (value > 15) { + throw new IllegalArgumentException("Value can't be more then 15. With bits " + bits + "."); + } + return; + case 8: + if (value > 255) { + throw new IllegalArgumentException("Value can't be more then 255. With bits " + bits + "."); + } + return; + } + + throw new IllegalArgumentException(String.format("Status value can't be %s. With bits %s.", value, bits)); + } + + public int get(int pos) { +// int rest = pos % this.divisor; +// int floored = pos / this.divisor; +// int shift = rest * this.bits; +// return (this.list[floored] & 0xff & (((1 << this.bits) - 1) << shift)) >> shift; + int value = 0; + for (int i = 0; i < this.bits; i++) { + if (this.list.get(pos * this.bits + i)) { + value |= (1 << i); + } + } + return value; + } + + public int getBitSetLength() { + return list.length(); + } + + @Override + public String toString() { + StringBuilder val = new StringBuilder(); + int size = list.length() / 8 * this.divisor; + for (int x = 0; x < size; x++) { + val.append(Integer.toHexString(this.get(x))); + } + return val.toString(); + } + + private static byte[] compress(byte[] data) throws IOException { + Deflater deflater = new Deflater(Deflater.BEST_COMPRESSION); + deflater.setInput(data); + deflater.finish(); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(data.length); + byte[] buffer = new byte[1024]; + while (!deflater.finished()) { + int count = deflater.deflate(buffer); + outputStream.write(buffer, 0, count); + } + outputStream.close(); + return outputStream.toByteArray(); + } + + private static byte[] decompress(byte[] data) throws IOException { + try (ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(data); + InflaterInputStream inflaterInputStream = new InflaterInputStream(byteArrayInputStream); + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) { + + byte[] buffer = new byte[1024]; + int len; + while ((len = inflaterInputStream.read(buffer)) > 0) { + byteArrayOutputStream.write(buffer, 0, len); + } + return byteArrayOutputStream.toByteArray(); + } + } + + public int getBits() { + return bits; + } + + public int getDivisor() { + return divisor; + } + + public String getLst() throws IOException { + return encodeAsString(); + } +} diff --git a/jans-core/model/src/main/java/io/jans/model/tokenstatus/TokenStatus.java b/jans-core/model/src/main/java/io/jans/model/tokenstatus/TokenStatus.java new file mode 100644 index 00000000000..827f1843f1a --- /dev/null +++ b/jans-core/model/src/main/java/io/jans/model/tokenstatus/TokenStatus.java @@ -0,0 +1,34 @@ +package io.jans.model.tokenstatus; + +import java.util.HashMap; +import java.util.Map; + +/** + * @author Yuriy Z + */ +public enum TokenStatus { + VALID(0), + INVALID(1); + + private final int value; + + private static final Map mapByValues = new HashMap<>(); + + static { + for (TokenStatus enumType : values()) { + mapByValues.put(enumType.getValue(), enumType); + } + } + + TokenStatus(int value) { + this.value = value; + } + + public int getValue() { + return value; + } + + public static TokenStatus fromValue(int value) { + return mapByValues.get(value); + } +} diff --git a/jans-core/service/src/main/java/io/jans/model/cluster/ClusterNode.java b/jans-core/service/src/main/java/io/jans/model/cluster/ClusterNode.java new file mode 100644 index 00000000000..1ce7bc930d5 --- /dev/null +++ b/jans-core/service/src/main/java/io/jans/model/cluster/ClusterNode.java @@ -0,0 +1,98 @@ +package io.jans.model.cluster; + +import io.jans.orm.annotation.AttributeName; +import io.jans.orm.annotation.DataEntry; +import io.jans.orm.annotation.ObjectClass; +import io.jans.orm.model.base.BaseEntry; + +import java.util.Date; + +/** + * @author Yuriy Movchan + * @version 1.0, 06/03/2024 + */ +@DataEntry(sortBy = "jansNum") +@ObjectClass(value = "jansNode") +public class ClusterNode extends BaseEntry { + + private static final long serialVersionUID = -2122431771066187529L; + + @AttributeName(ignoreDuringUpdate = true, name = "jansNum") + private Integer id; + + @AttributeName(name = "jansType") + private String type; + + @AttributeName(name = "creationDate") + private Date creationDate; + + @AttributeName(name = "jansLastUpd") + private Date lastUpdate; + + @AttributeName(name = "lockKey") + private String lockKey; + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + // workaround: io.jans.orm.exception.PropertyNotFoundException: Could not find a getter for jansNum in class io.jans.model.cluster.ClusterNode + // at io.jans.orm.reflect.property.BasicPropertyAccessor.createGetter(BasicPropertyAccessor.java:214) + public Integer getJansNum() { + return getId(); + } + + // workaround: io.jans.orm.exception.PropertyNotFoundException: Could not find a getter for jansNum in class io.jans.model.cluster.ClusterNode + // at io.jans.orm.reflect.property.BasicPropertyAccessor.createGetter(BasicPropertyAccessor.java:214) + public void setJansNum(Integer id) { + setId(id); + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public Date getCreationDate() { + return creationDate; + } + + public void setCreationDate(Date creationDate) { + this.creationDate = creationDate; + } + + public Date getLastUpdate() { + return lastUpdate; + } + + public void setLastUpdate(Date lastUpdate) { + this.lastUpdate = lastUpdate; + } + + public String getLockKey() { + return lockKey; + } + + public void setLockKey(String lockKey) { + this.lockKey = lockKey; + } + + @Override + public String toString() { + return "ClusterNode{" + + "id=" + id + + ", type='" + type + '\'' + + ", creationDate=" + creationDate + + ", lastUpdate=" + lastUpdate + + ", lockKey='" + lockKey + '\'' + + ", dn='" + getDn() + '\'' + + "} "; + } +} diff --git a/jans-core/service/src/main/java/io/jans/model/token/StatusIndexPool.java b/jans-core/service/src/main/java/io/jans/model/token/StatusIndexPool.java new file mode 100644 index 00000000000..11c5b946e00 --- /dev/null +++ b/jans-core/service/src/main/java/io/jans/model/token/StatusIndexPool.java @@ -0,0 +1,142 @@ +package io.jans.model.token; + +import io.jans.orm.annotation.AttributeName; +import io.jans.orm.annotation.DataEntry; +import io.jans.orm.annotation.ObjectClass; +import io.jans.orm.model.base.BaseEntry; +import jakarta.persistence.Transient; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * @author Yuriy Movchan + * @version 1.0, 06/03/2024 + */ +@DataEntry(sortBy = "jansNum") +@ObjectClass(value = "jansStatusIdxPool") +public class StatusIndexPool extends BaseEntry { + + private static final long serialVersionUID = -2122431771066187529L; + public static final String JANS_NUM_ATTRIBUTE_NAME = "jansNum"; + + @AttributeName(ignoreDuringUpdate = true, name = "jansNum") + private Integer id; + + @AttributeName(ignoreDuringUpdate = true, name = "jansNodeId") + private Integer nodeId; + + @AttributeName(name = "dat") + private String data; + + @AttributeName(name = "jansLastUpd") + private Date lastUpdate; + + @AttributeName(name = "exp") + private Date expirationDate; + + @AttributeName(name = "lockKey") + private String lockKey; + + @Transient + private transient Integer startIndex; + + @Transient + private transient Integer endIndex; + + @Transient + private transient AtomicInteger currentIndex = new AtomicInteger(-1); + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public Integer getNodeId() { + return nodeId; + } + + public void setNodeId(Integer nodeId) { + this.nodeId = nodeId; + } + + public String getData() { + return data; + } + + public void setData(String data) { + this.data = data; + } + + public Date getLastUpdate() { + return lastUpdate; + } + + public void setLastUpdate(Date lastUpdate) { + this.lastUpdate = lastUpdate; + } + + public Date getExpirationDate() { + return expirationDate; + } + + public void setExpirationDate(Date expirationDate) { + this.expirationDate = expirationDate; + } + + public String getLockKey() { + return lockKey; + } + + public void setLockKey(String lockKey) { + this.lockKey = lockKey; + } + + public List enumerateAllIndexes() { + List indexes = new ArrayList<>(); + for (int i = getStartIndex(); i <= getEndIndex(); i++ ) { + indexes.add(i); + } + return indexes; + } + + public Integer getStartIndex() { + return startIndex; + } + + public void setStartIndex(Integer startIndex) { + this.startIndex = startIndex; + this.currentIndex.set(startIndex); + } + + public Integer getEndIndex() { + return endIndex; + } + + public void setEndIndex(Integer endIndex) { + this.endIndex = endIndex; + } + + public int nextIndex() { + // This block not used for while and were expired already + if ((expirationDate == null) || expirationDate.before(new Date())) { + return -1; + } + + int nextIndex = currentIndex.getAndIncrement(); + if (nextIndex > endIndex) { + // Index out of pool range + return -1; + } + + // Correct index + return nextIndex; + + } + +} diff --git a/jans-core/service/src/main/java/io/jans/model/token/TokenAttributes.java b/jans-core/service/src/main/java/io/jans/model/token/TokenAttributes.java index e84a9b9ebab..c5a6961d7b2 100644 --- a/jans-core/service/src/main/java/io/jans/model/token/TokenAttributes.java +++ b/jans-core/service/src/main/java/io/jans/model/token/TokenAttributes.java @@ -32,6 +32,16 @@ public class TokenAttributes implements Serializable { private String dpopJkt; @JsonProperty("authorizationDetails") private String authorizationDetails; + @JsonProperty("statusListIndex") + private Integer statusListIndex; + + public Integer getStatusListIndex() { + return statusListIndex; + } + + public void setStatusListIndex(Integer statusListIndex) { + this.statusListIndex = statusListIndex; + } public String getAuthorizationDetails() { return authorizationDetails; diff --git a/jans-core/service/src/test/java/io/jans/model/tokenstatus/StatusListTest.java b/jans-core/service/src/test/java/io/jans/model/tokenstatus/StatusListTest.java new file mode 100644 index 00000000000..e8a908b26d7 --- /dev/null +++ b/jans-core/service/src/test/java/io/jans/model/tokenstatus/StatusListTest.java @@ -0,0 +1,127 @@ +package io.jans.model.tokenstatus; + +import org.testng.annotations.Test; + +import java.io.IOException; +import java.util.Random; + +import static org.testng.AssertJUnit.assertEquals; + +/** + * @author Yuriy Z + */ +public class StatusListTest { + + @Test + public void get_withStatusSize1_shouldReturnCorrectSetValue() { + StatusList statusList = new StatusList( 1); + statusList.set(0, 1); + statusList.set(1, 0); + statusList.set(2, 0); + statusList.set(3, 1); + statusList.set(4, 1); + statusList.set(5, 1); + statusList.set(6, 0); + statusList.set(7, 1); + statusList.set(8, 1); + statusList.set(9, 1); + statusList.set(10, 0); + statusList.set(11, 0); + statusList.set(12, 0); + statusList.set(13, 1); + statusList.set(14, 0); + statusList.set(15, 1); + + assertEquals(1, statusList.get(0)); + assertEquals(0, statusList.get(1)); + assertEquals(0, statusList.get(2)); + assertEquals(1, statusList.get(3)); + assertEquals(1, statusList.get(4)); + assertEquals(1, statusList.get(5)); + assertEquals(0, statusList.get(6)); + assertEquals(1, statusList.get(7)); + assertEquals(1, statusList.get(8)); + assertEquals(1, statusList.get(9)); + assertEquals(0, statusList.get(10)); + assertEquals(0, statusList.get(11)); + assertEquals(0, statusList.get(12)); + assertEquals(1, statusList.get(13)); + assertEquals(0, statusList.get(14)); + assertEquals(1, statusList.get(15)); + } + + @Test + public void getLst_withStatusSize1_shouldReturnCorrectValue() throws IOException { + StatusList statusList = new StatusList( 1); + statusList.set(0, 1); + statusList.set(1, 0); + statusList.set(2, 0); + statusList.set(3, 1); + statusList.set(4, 1); + statusList.set(5, 1); + statusList.set(6, 0); + statusList.set(7, 1); + statusList.set(8, 1); + statusList.set(9, 1); + statusList.set(10, 0); + statusList.set(11, 0); + statusList.set(12, 0); + statusList.set(13, 1); + statusList.set(14, 0); + statusList.set(15, 1); + + assertEncodedValueAndPrintDecoded("eNrbuRgAAhcBXQ", statusList); + } + + @Test + public void getLst_withStatusSize2_shouldReturnCorrectValue() throws IOException { + StatusList statusList = new StatusList( 2); + statusList.set(0, 1); + statusList.set(1, 2); + statusList.set(2, 0); + statusList.set(3, 3); + statusList.set(4, 0); + statusList.set(5, 1); + statusList.set(6, 0); + statusList.set(7, 1); + statusList.set(8, 1); + statusList.set(9, 2); + statusList.set(10, 3); + statusList.set(11, 3); + + assertEncodedValueAndPrintDecoded("eNo76fITAAPfAgc", statusList); + } + + @Test(enabled = true) // it's test for manual run to understand the size of list depending on tokens count + public void getLst_forManualRun() throws IOException { + + // bit size = 2 (means 4 statuses) + // token count - to list length + // 1000 - 326 + // 10000 - 2720 + // 100000 - 26836 + // 1000000 - 268278 + // 10M - 2682738 + // 100M - 26825504 + // 1000M - 268252111 + Random random = new Random(); + StatusList statusList = new StatusList( 2); + + for (int i = 0; i < 10000000; i++) { + final int randomValue = random.nextInt(3); + statusList.set(i, randomValue); + } + + System.out.println(statusList.getLst().length()); + } + + private static void assertEncodedValueAndPrintDecoded(String expectedEncodedValue, StatusList statusList) throws IOException { + System.out.println("Decoded: " + statusList.toString()); + final String encoded = statusList.getLst(); + + assertEquals(expectedEncodedValue, encoded); + + StatusList decodedList = StatusList.fromEncoded(encoded, statusList.getBits()); + System.out.println("Decoded List: " + decodedList.toString()); + } +} diff --git a/jans-linux-setup/jans_setup/schema/jans_schema.json b/jans-linux-setup/jans_setup/schema/jans_schema.json index 0c48d2ccbad..a251e5e8d59 100644 --- a/jans-linux-setup/jans_setup/schema/jans_schema.json +++ b/jans-linux-setup/jans_setup/schema/jans_schema.json @@ -298,6 +298,28 @@ "syntax": "1.3.6.1.4.1.1466.115.121.1.15", "x_origin": "Jans created attribute" }, + { + "desc": "Type of the entry", + "equality": "caseIgnoreMatch", + "names": [ + "jansType" + ], + "oid": "jansAttr", + "substr": "caseIgnoreSubstringsMatch", + "syntax": "1.3.6.1.4.1.1466.115.121.1.15", + "x_origin": "Jans created attribute" + }, + { + "desc": "Entry lock key", + "equality": "caseIgnoreMatch", + "names": [ + "lockKey" + ], + "oid": "jansAttr", + "substr": "caseIgnoreSubstringsMatch", + "syntax": "1.3.6.1.4.1.1466.115.121.1.15", + "x_origin": "Jans created attribute" + }, { "desc": "exclude login page configuration", "equality": "caseIgnoreMatch", @@ -1336,6 +1358,26 @@ "syntax": "1.3.6.1.4.1.1466.115.121.1.15", "x_origin": "Jans created attribute" }, + { + "desc": "Numeric identifier", + "equality": "integerMatch", + "names": [ + "jansNum" + ], + "oid": "jansAttr", + "syntax": "1.3.6.1.4.1.1466.115.121.1.27", + "x_origin": "Jans created attribute" + }, + { + "desc": "Numeric node identifier", + "equality": "integerMatch", + "names": [ + "jansNodeId" + ], + "oid": "jansAttr", + "syntax": "1.3.6.1.4.1.1466.115.121.1.27", + "x_origin": "Jans created attribute" + }, { "desc": "Sess Identifier", "equality": "caseIgnoreMatch", @@ -5227,6 +5269,49 @@ "top" ], "x_origin": "Jans created objectclass" + }, + { + "kind": "STRUCTURAL", + "may": [ + "jansNum", + "jansType", + "creationDate", + "jansLastUpd", + "lockKey" + ], + "must": [ + "objectclass" + ], + "names": [ + "jansNode" + ], + "oid": "jansObjClass", + "sup": [ + "top" + ], + "x_origin": "Jans created objectclass" + }, + { + "kind": "STRUCTURAL", + "may": [ + "jansNum", + "jansNodeId", + "dat", + "jansLastUpd", + "exp", + "lockKey" + ], + "must": [ + "objectclass" + ], + "names": [ + "jansStatusIdxPool" + ], + "oid": "jansObjClass", + "sup": [ + "top" + ], + "x_origin": "Jans created objectclass" } ], "oidMacros": { diff --git a/jans-linux-setup/jans_setup/templates/base.ldif b/jans-linux-setup/jans_setup/templates/base.ldif index ccce2e72b02..fdf0b9009dd 100644 --- a/jans-linux-setup/jans_setup/templates/base.ldif +++ b/jans-linux-setup/jans_setup/templates/base.ldif @@ -148,6 +148,16 @@ objectClass: top objectClass: organizationalUnit ou: archived_jwks +dn: ou=node,o=jans +objectClass: top +objectClass: organizationalUnit +ou: node + +dn: ou=status_index_pool,o=jans +objectClass: top +objectClass: organizationalUnit +ou: status_index_pool + dn: ou=document,o=jans objectClass: top objectClass: organizationalUnit 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 d9d92f575ad..e0954498a39 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 @@ -19,7 +19,8 @@ "stat", "par", "ssa", - "global_token_revocation" + "global_token_revocation", + "status_list" ], "issuer":"https://%(hostname)s", "baseEndpoint":"https://%(hostname)s/jans-auth/restv1", @@ -375,6 +376,8 @@ "idTokenLifetime":3600, "txTokenLifetime":180, "spontaneousScopeLifetime":86400, + "tokenIndexAllocationBlockSize":10, + "tokenIndexLimit":10000000, "accessTokenLifetime":300, "umaResourceLifetime":1728000, "umaRptLifetime": 3600, diff --git a/jans-linux-setup/jans_setup/templates/jans-auth/jans-auth-static-conf.json b/jans-linux-setup/jans_setup/templates/jans-auth/jans-auth-static-conf.json index fd20d4dfc8c..f34d87d0c36 100644 --- a/jans-linux-setup/jans_setup/templates/jans-auth/jans-auth-static-conf.json +++ b/jans-linux-setup/jans_setup/templates/jans-auth/jans-auth-static-conf.json @@ -18,6 +18,8 @@ "sectorIdentifiers": "ou=sector_identifiers,o=jans", "archivedJwks": "ou=archived_jwks,o=jans", "ciba": "ou=ciba,o=jans", - "ssa": "ou=ssa,o=jans" + "ssa": "ou=ssa,o=jans", + "node": "ou=node,o=jans", + "statusIndexPool": "ou=status_index_pool,o=jans" } } diff --git a/jans-orm/core/src/main/java/io/jans/orm/impl/BaseEntryManager.java b/jans-orm/core/src/main/java/io/jans/orm/impl/BaseEntryManager.java index 4d3cbc87209..75597e4f49b 100644 --- a/jans-orm/core/src/main/java/io/jans/orm/impl/BaseEntryManager.java +++ b/jans-orm/core/src/main/java/io/jans/orm/impl/BaseEntryManager.java @@ -6,9 +6,48 @@ package io.jans.orm.impl; +import static io.jans.orm.model.base.LocalizedString.EMPTY_LANG_TAG; +import static io.jans.orm.model.base.LocalizedString.LOCALIZED; + +import java.io.Serializable; +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.IdentityHashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.stream.Collectors; + +import org.apache.commons.codec.binary.Base64; +import org.json.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import com.fasterxml.jackson.databind.ObjectMapper; + import io.jans.orm.PersistenceEntryManager; -import io.jans.orm.annotation.*; +import io.jans.orm.annotation.AttributeEnum; +import io.jans.orm.annotation.AttributeName; +import io.jans.orm.annotation.AttributesList; +import io.jans.orm.annotation.CustomObjectClass; +import io.jans.orm.annotation.DN; +import io.jans.orm.annotation.DataEntry; +import io.jans.orm.annotation.Expiration; +import io.jans.orm.annotation.JsonObject; +import io.jans.orm.annotation.LanguageTag; +import io.jans.orm.annotation.ObjectClass; +import io.jans.orm.annotation.SchemaEntry; import io.jans.orm.exception.EntryPersistenceException; import io.jans.orm.exception.InvalidArgumentException; import io.jans.orm.exception.MappingException; @@ -28,19 +67,6 @@ import io.jans.orm.search.filter.FilterProcessor; import io.jans.orm.util.ArrayHelper; import io.jans.orm.util.StringHelper; -import org.apache.commons.codec.binary.Base64; -import org.json.JSONObject; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.Serializable; -import java.lang.annotation.Annotation; -import java.lang.reflect.Method; -import java.util.*; -import java.util.Map.Entry; -import java.util.stream.Collectors; - -import static io.jans.orm.model.base.LocalizedString.*; /** * Abstract Entry Manager @@ -1251,6 +1277,8 @@ public void sortListByProperties(Class entryClass, List entries, boole "Invalid list of sortBy properties " + Arrays.toString(sortByProperties)); } + List propertiesAnnotations = getEntryPropertyAnnotations(entryClass); + // Get getters for all properties Getter[][] propertyGetters = new Getter[sortByProperties.length][]; for (int i = 0; i < sortByProperties.length; i++) { @@ -1261,7 +1289,8 @@ public void sortListByProperties(Class entryClass, List entries, boole if (j > 0) { currentEntryClass = propertyGetters[i][j - 1].getReturnType(); } - propertyGetters[i][j] = getGetter(currentEntryClass, tmpProperties[j]); + String beanProperty = resolveBeanPropertyByAttribute(propertiesAnnotations, tmpProperties[j]); + propertyGetters[i][j] = getGetter(currentEntryClass, beanProperty); } if (propertyGetters[i][tmpProperties.length - 1] == null) { @@ -2172,6 +2201,28 @@ private String getEntryKey(Object dnValue, boolean caseSensetive, List propertiesAnnotations, String attributeName) { + for (PropertyAnnotation propertiesAnnotation : propertiesAnnotations) { + String propertyName = propertiesAnnotation.getPropertyName(); + Annotation ldapAttribute; + + ldapAttribute = ReflectHelper.getAnnotationByType(propertiesAnnotation.getAnnotations(), + AttributeName.class); + if (ldapAttribute != null) { + String ldapAttributeName = ((AttributeName) ldapAttribute).name(); + if (StringHelper.isEmpty(ldapAttributeName)) { + ldapAttributeName = propertyName; + } + ldapAttributeName = ldapAttributeName.toLowerCase(); + if (StringHelper.equalsIgnoreCase(attributeName, ldapAttributeName)) { + return propertiesAnnotation.getPropertyName(); + } + } + } + + return attributeName; + } private void addPropertyWithValuesToKey(StringBuilder sb, String propertyName, String[] values) { sb.append(':').append(propertyName).append('='); diff --git a/jans-orm/sql-sample/src/main/java/io/jans/orm/sql/persistence/SqlEntryManagerSample.java b/jans-orm/sql-sample/src/main/java/io/jans/orm/sql/persistence/SqlEntryManagerSample.java index d041965c6a4..b26230def02 100644 --- a/jans-orm/sql-sample/src/main/java/io/jans/orm/sql/persistence/SqlEntryManagerSample.java +++ b/jans-orm/sql-sample/src/main/java/io/jans/orm/sql/persistence/SqlEntryManagerSample.java @@ -29,15 +29,10 @@ private Properties getSampleConnectionProperties() { connectionProperties.put("sql#db.schema.name", "jansdb"); connectionProperties.put("sql#connection.uri", "jdbc:mysql://localhost:3306/jansdb?profileSQL=true"); connectionProperties.put("sql#connection.driver-property.serverTimezone", "GMT+2"); - - connectionProperties.put("sql#auth.userName", "jans"); - connectionProperties.put("sql#auth.userPassword", "secret"); } else { connectionProperties.put("sql#db.schema.name", "public"); connectionProperties.put("sql#connection.uri", "jdbc:postgresql://localhost:5432/jansdb"); - - connectionProperties.put("sql#auth.userName", "jans"); - connectionProperties.put("sql#auth.userPassword", "secret"); + connectionProperties.put("sql#db.disable.time-zone", "true"); } connectionProperties.put("sql#connection.driver-property.serverTimezone", "GMT+2"); diff --git a/jans-orm/sql/src/main/java/io/jans/orm/sql/impl/SqlFilterConverter.java b/jans-orm/sql/src/main/java/io/jans/orm/sql/impl/SqlFilterConverter.java index 56f4b432f8b..25b13f1143f 100644 --- a/jans-orm/sql/src/main/java/io/jans/orm/sql/impl/SqlFilterConverter.java +++ b/jans-orm/sql/src/main/java/io/jans/orm/sql/impl/SqlFilterConverter.java @@ -23,6 +23,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.querydsl.core.types.Expression; import com.querydsl.core.types.ExpressionUtils; +import com.querydsl.core.types.NullExpression; import com.querydsl.core.types.Operation; import com.querydsl.core.types.Ops; import com.querydsl.core.types.Path; @@ -208,7 +209,11 @@ private ConvertedExpression convertToSqlFilterImpl(TableMapping tableMapping, Fi return ConvertedExpression.build(operation, jsonAttributes); } } - return ConvertedExpression.build(ExpressionUtils.eq(columnExpression, buildTypedExpression(tableMapping, currentGenericFilter)), jsonAttributes); + Expression typedExpression = buildTypedExpression(tableMapping, currentGenericFilter); + if (typedExpression instanceof NullExpression) { + return ConvertedExpression.build(ExpressionUtils.isNull(columnExpression), jsonAttributes); + } + return ConvertedExpression.build(ExpressionUtils.eq(columnExpression, typedExpression), jsonAttributes); } if (FilterType.LESS_OR_EQUAL == type) { diff --git a/jans-orm/sql/src/main/java/io/jans/orm/sql/operation/impl/SqlConnectionProvider.java b/jans-orm/sql/src/main/java/io/jans/orm/sql/operation/impl/SqlConnectionProvider.java index 92627a6e2c2..fb20c12b038 100644 --- a/jans-orm/sql/src/main/java/io/jans/orm/sql/operation/impl/SqlConnectionProvider.java +++ b/jans-orm/sql/src/main/java/io/jans/orm/sql/operation/impl/SqlConnectionProvider.java @@ -91,6 +91,8 @@ public class SqlConnectionProvider { private Map> tableColumnsMap; private Map tableEnginesMap = new HashMap<>(); private Map> tableJsonColumnsMap = new HashMap<>(); + + private boolean disableTimeZone = false; protected SqlConnectionProvider() { } @@ -142,6 +144,10 @@ protected void init() throws Exception { connectionProperties.setProperty("user", userName); connectionProperties.setProperty("password", userPassword); + + if (props.containsKey("db.disable.time-zone")) { + disableTimeZone = StringHelper.toBoolean(props.getProperty("db.disable.time-zone"), false); + } this.objectPoolConfig = new GenericObjectPoolConfig<>(); @@ -511,4 +517,8 @@ public SupportedDbType getDbType() { return dbType; } + public boolean isDisableTimeZone() { + return disableTimeZone; + } + } diff --git a/jans-orm/sql/src/main/java/io/jans/orm/sql/operation/impl/SqlOperationServiceImpl.java b/jans-orm/sql/src/main/java/io/jans/orm/sql/operation/impl/SqlOperationServiceImpl.java index b446ca30ddf..b623f2f89f4 100644 --- a/jans-orm/sql/src/main/java/io/jans/orm/sql/operation/impl/SqlOperationServiceImpl.java +++ b/jans-orm/sql/src/main/java/io/jans/orm/sql/operation/impl/SqlOperationServiceImpl.java @@ -906,7 +906,12 @@ public Date decodeTime(String date, boolean silent) { } // Add ending Z if necessary - String dateZ = date.endsWith("Z") ? date : date + "Z"; + String dateZ; + if (connectionProvider.isDisableTimeZone()) { + dateZ = date.endsWith("Z") ? date.substring(0, date.length() - 1) : date; + } else { + dateZ = date.endsWith("Z") ? date : date + "Z"; + } try { return new Date(Instant.parse(dateZ).toEpochMilli()); } catch (DateTimeParseException ex) {