From b7730d5e90419ce9fff7ec28acd13a5e5ae0df2e Mon Sep 17 00:00:00 2001 From: YuriyZ Date: Fri, 28 Oct 2022 07:39:26 +0300 Subject: [PATCH] feat(jans-auth-server): draft implementation of native sso Native SSO #2518 --- .../as/common/model/session/SessionId.java | 1 + .../model/configuration/AppConfiguration.java | 12 ++ .../authorize/ws/rs/AuthzRequestService.java | 4 + .../as/server/model/token/IdTokenFactory.java | 44 +++---- .../token/ws/rs/TokenExchangeService.java | 76 ++++++++++-- .../token/ws/rs/TokenRestWebServiceImpl.java | 17 ++- .../ws/rs/TokenRestWebServiceValidator.java | 24 ++-- .../ws/rs/AuthzRequestServiceTest.java | 25 ++++ .../token/ws/rs/TokenExchangeServiceTest.java | 116 ++++++++++++++++++ .../server/src/test/resources/testng.xml | 1 + 10 files changed, 274 insertions(+), 46 deletions(-) create mode 100644 jans-auth-server/server/src/test/java/io/jans/as/server/token/ws/rs/TokenExchangeServiceTest.java diff --git a/jans-auth-server/common/src/main/java/io/jans/as/common/model/session/SessionId.java b/jans-auth-server/common/src/main/java/io/jans/as/common/model/session/SessionId.java index 07e50cee392..090c7d8ee9c 100644 --- a/jans-auth-server/common/src/main/java/io/jans/as/common/model/session/SessionId.java +++ b/jans-auth-server/common/src/main/java/io/jans/as/common/model/session/SessionId.java @@ -95,6 +95,7 @@ public class SessionId implements Deletable, Serializable { @Expiration private int ttl; + @NotNull public List getDeviceSecrets() { if (deviceSecrets == null) deviceSecrets = new ArrayList<>(); return deviceSecrets; 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 7cbe69efe8d..c519bd7b64f 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 @@ -249,8 +249,12 @@ public class AppConfiguration implements Configuration { private Set authorizationRequestCustomAllowedParameters; private Boolean openidScopeBackwardCompatibility = false; private Boolean disableU2fEndpoint = false; + + // Token Exchange private Boolean rotateDeviceSecret = false; + private Boolean returnDeviceSecretFromAuthzEndpoint = false; + // DCR private Boolean dcrSignatureValidationEnabled = false; private String dcrSignatureValidationSharedSecret; private String dcrSignatureValidationSoftwareStatementJwksURIClaim; @@ -346,6 +350,14 @@ public void setAllowAllValueForRevokeEndpoint(Boolean allowAllValueForRevokeEndp this.allowAllValueForRevokeEndpoint = allowAllValueForRevokeEndpoint; } + public Boolean getReturnDeviceSecretFromAuthzEndpoint() { + return returnDeviceSecretFromAuthzEndpoint; + } + + public void setReturnDeviceSecretFromAuthzEndpoint(Boolean returnDeviceSecretFromAuthzEndpoint) { + this.returnDeviceSecretFromAuthzEndpoint = returnDeviceSecretFromAuthzEndpoint; + } + public Boolean getRotateDeviceSecret() { if (rotateDeviceSecret == null) rotateDeviceSecret = false; return rotateDeviceSecret; diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/authorize/ws/rs/AuthzRequestService.java b/jans-auth-server/server/src/main/java/io/jans/as/server/authorize/ws/rs/AuthzRequestService.java index f6304b7e3ba..2a6b564f16a 100644 --- a/jans-auth-server/server/src/main/java/io/jans/as/server/authorize/ws/rs/AuthzRequestService.java +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/authorize/ws/rs/AuthzRequestService.java @@ -49,6 +49,7 @@ import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; import org.apache.commons.lang.ArrayUtils; +import org.apache.commons.lang.BooleanUtils; import org.apache.commons.lang.StringUtils; import org.jetbrains.annotations.Nullable; import org.json.JSONObject; @@ -108,6 +109,9 @@ public class AuthzRequestService { private RedirectionUriService redirectionUriService; public void addDeviceSecretToSession(AuthzRequest authzRequest, SessionId sessionId) { + if (BooleanUtils.isFalse(appConfiguration.getReturnDeviceSecretFromAuthzEndpoint())) { + return; + } if (!Arrays.asList(authzRequest.getScope().split(" ")).contains(ScopeConstants.DEVICE_SSO)) { return; } 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 abb95e6e561..4fe604237dc 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 @@ -10,7 +10,9 @@ import io.jans.as.common.claims.Audience; import io.jans.as.common.model.common.User; import io.jans.as.common.model.registration.Client; +import io.jans.as.common.model.session.SessionId; import io.jans.as.common.service.AttributeService; +import io.jans.as.model.authorize.CodeVerifier; import io.jans.as.model.configuration.AppConfiguration; import io.jans.as.model.exception.InvalidClaimException; import io.jans.as.model.jwt.JwtClaimName; @@ -20,15 +22,7 @@ import io.jans.as.persistence.model.Scope; import io.jans.as.server.model.authorize.Claim; import io.jans.as.server.model.authorize.JwtAuthorizationRequest; -import io.jans.as.server.model.common.AbstractToken; -import io.jans.as.server.model.common.AccessToken; -import io.jans.as.server.model.common.AuthorizationCode; -import io.jans.as.server.model.common.CIBAGrant; -import io.jans.as.server.model.common.ExecutionContext; -import io.jans.as.server.model.common.IAuthorizationGrant; -import io.jans.as.server.model.common.RefreshToken; -import io.jans.as.common.model.session.SessionId; -import io.jans.as.server.model.common.UnmodifiableAuthorizationGrant; +import io.jans.as.server.model.common.*; import io.jans.as.server.service.ScopeService; import io.jans.as.server.service.SessionIdService; import io.jans.as.server.service.external.ExternalAuthenticationService; @@ -39,26 +33,19 @@ import io.jans.model.GluuAttribute; import io.jans.model.custom.script.conf.CustomScriptConfiguration; import io.jans.model.custom.script.type.auth.PersonAuthenticationType; +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.apache.logging.log4j.util.Strings; import org.json.JSONObject; import org.slf4j.Logger; -import jakarta.ejb.Stateless; -import jakarta.enterprise.context.ApplicationScoped; -import jakarta.inject.Inject; -import jakarta.inject.Named; -import java.util.ArrayList; -import java.util.Calendar; -import java.util.Collection; -import java.util.Date; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.UUID; +import java.util.*; import static io.jans.as.model.common.ScopeType.DYNAMIC; +import static io.jans.as.server.token.ws.rs.TokenExchangeService.DEVICE_SECRET; /** * JSON Web Token (JWT) is a compact token format intended for space constrained @@ -165,6 +152,8 @@ private void fillClaims(JsonWebResponse jwr, jwr.setClaim("sid", session.getOutsideSid()); } + addTokenExchangeClaims(jwr, executionContext, session); + if (authorizationGrant.getAcrValues() != null) { jwr.setClaim(JwtClaimName.AUTHENTICATION_CONTEXT_CLASS_REFERENCE, authorizationGrant.getAcrValues()); setAmrClaim(jwr, authorizationGrant.getAcrValues()); @@ -264,6 +253,17 @@ private void fillClaims(JsonWebResponse jwr, } } + private void addTokenExchangeClaims(JsonWebResponse jwr, ExecutionContext executionContext, SessionId sessionId) { + if (sessionId == null) { // unable to find session + return; + } + + String deviceSecret = executionContext.getHttpRequest().getParameter(DEVICE_SECRET); + if (StringUtils.isNotBlank(deviceSecret) && sessionId.getDeviceSecrets().contains(deviceSecret)) { + jwr.setClaim("ds_hash", CodeVerifier.s256(deviceSecret)); + } + } + /** * Filters some claims from id_token based on if access_token is issued or not. * openid-connect-core-1_0.html Section 5.4 diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/token/ws/rs/TokenExchangeService.java b/jans-auth-server/server/src/main/java/io/jans/as/server/token/ws/rs/TokenExchangeService.java index 1ae725a7923..23e9964e56b 100644 --- a/jans-auth-server/server/src/main/java/io/jans/as/server/token/ws/rs/TokenExchangeService.java +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/token/ws/rs/TokenExchangeService.java @@ -4,6 +4,8 @@ import io.jans.as.common.model.registration.Client; import io.jans.as.common.model.session.SessionId; import io.jans.as.common.service.AttributeService; +import io.jans.as.model.common.GrantType; +import io.jans.as.model.common.ScopeConstants; import io.jans.as.model.configuration.AppConfiguration; import io.jans.as.model.token.JsonWebResponse; import io.jans.as.server.model.audit.OAuth2AuditLog; @@ -16,6 +18,7 @@ import jakarta.inject.Inject; import jakarta.inject.Named; import jakarta.servlet.http.HttpServletRequest; +import org.apache.commons.lang.ArrayUtils; import org.apache.commons.lang.StringUtils; import org.apache.commons.lang3.BooleanUtils; import org.json.JSONException; @@ -37,6 +40,8 @@ @Named public class TokenExchangeService { + public static final String DEVICE_SECRET = "device_secret"; + @Inject private Logger log; @@ -61,15 +66,39 @@ public class TokenExchangeService { @Inject private AttributeService attributeService; - public String rotateDeviceSecret(SessionId sessionId, String actorToken) { - if (BooleanUtils.isFalse(appConfiguration.getRotateDeviceSecret())) { + public void rotateDeviceSecretOnRefreshToken(HttpServletRequest httpRequest, AuthorizationGrant refreshGrant, String scope) { + if (!scope.contains(ScopeConstants.DEVICE_SSO)) { + return; + } + if (StringUtils.isBlank(refreshGrant.getSessionDn())) { + return; + } + final SessionId sessionId = sessionIdService.getSessionByDn(refreshGrant.getSessionDn()); + if (sessionId == null) { + return; + } + + final String deviceSecret = httpRequest.getParameter(DEVICE_SECRET); + + // spec: rotate only if device_secret is not specified + if (StringUtils.isBlank(deviceSecret)) { + rotateDeviceSecret(sessionId, deviceSecret, true); + } + } + + public String rotateDeviceSecret(SessionId sessionId, String deviceSecret) { + return rotateDeviceSecret(sessionId, deviceSecret, false); + } + + public String rotateDeviceSecret(SessionId sessionId, String deviceSecret, boolean forceRotation) { + if (BooleanUtils.isFalse(appConfiguration.getRotateDeviceSecret()) && !forceRotation) { return null; } String newDeviceSecret = HandleTokenFactory.generateDeviceSecret(); final List deviceSecrets = sessionId.getDeviceSecrets(); - deviceSecrets.remove(actorToken); + deviceSecrets.remove(deviceSecret); deviceSecrets.add(newDeviceSecret); sessionIdService.updateSessionId(sessionId, false); @@ -85,19 +114,19 @@ public JSONObject processTokenExchange(String scope, Function +