diff --git a/jans-auth-server/client/src/main/java/io/jans/as/client/AuthorizationResponse.java b/jans-auth-server/client/src/main/java/io/jans/as/client/AuthorizationResponse.java index 459b1c38a24..46c77d6d9e6 100644 --- a/jans-auth-server/client/src/main/java/io/jans/as/client/AuthorizationResponse.java +++ b/jans-auth-server/client/src/main/java/io/jans/as/client/AuthorizationResponse.java @@ -31,19 +31,7 @@ import java.util.List; import java.util.Map; -import static io.jans.as.model.authorize.AuthorizeResponseParam.ACCESS_TOKEN; -import static io.jans.as.model.authorize.AuthorizeResponseParam.AUD; -import static io.jans.as.model.authorize.AuthorizeResponseParam.CODE; -import static io.jans.as.model.authorize.AuthorizeResponseParam.EXP; -import static io.jans.as.model.authorize.AuthorizeResponseParam.EXPIRES_IN; -import static io.jans.as.model.authorize.AuthorizeResponseParam.ID_TOKEN; -import static io.jans.as.model.authorize.AuthorizeResponseParam.ISS; -import static io.jans.as.model.authorize.AuthorizeResponseParam.RESPONSE; -import static io.jans.as.model.authorize.AuthorizeResponseParam.SCOPE; -import static io.jans.as.model.authorize.AuthorizeResponseParam.SESSION_ID; -import static io.jans.as.model.authorize.AuthorizeResponseParam.SID; -import static io.jans.as.model.authorize.AuthorizeResponseParam.STATE; -import static io.jans.as.model.authorize.AuthorizeResponseParam.TOKEN_TYPE; +import static io.jans.as.model.authorize.AuthorizeResponseParam.*; /** * Represents an authorization response received from the authorization server. @@ -64,6 +52,7 @@ public class AuthorizationResponse extends BaseResponse { private String state; private String sessionId; private String sid; + private String deviceSecret; private Map customParams; private ResponseMode responseMode; @@ -203,6 +192,7 @@ private void processLocation() { } } + @SuppressWarnings("java:S3776") private void loadParams(Map params) throws UnsupportedEncodingException { if (params.containsKey(CODE)) { code = params.get(CODE); @@ -216,6 +206,10 @@ private void loadParams(Map params) throws UnsupportedEncodingEx sid = params.get(SID); params.remove(SID); } + if (params.containsKey(DEVICE_SECRET)) { + deviceSecret = params.get(DEVICE_SECRET); + params.remove(DEVICE_SECRET); + } if (params.containsKey(ACCESS_TOKEN)) { accessToken = params.get(ACCESS_TOKEN); params.remove(ACCESS_TOKEN); @@ -319,6 +313,14 @@ public void setSid(String sid) { this.sid = sid; } + public String getDeviceSecret() { + return deviceSecret; + } + + public void setDeviceSecret(String deviceSecret) { + this.deviceSecret = deviceSecret; + } + /** * Gets session id. * 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 035ddaabf67..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 @@ -16,9 +16,7 @@ import org.jetbrains.annotations.NotNull; import java.io.Serializable; -import java.util.Date; -import java.util.Map; -import java.util.UUID; +import java.util.*; /** * @author Yuriy Zabrovarnyy @@ -76,6 +74,9 @@ public class SessionId implements Deletable, Serializable { @AttributeName(name = "jansSessAttr") private Map sessionAttributes; + @AttributeName(name = "deviceSecret") + private List deviceSecrets; + @AttributeName(name = "exp") private Date expirationDate; @@ -94,6 +95,16 @@ public class SessionId implements Deletable, Serializable { @Expiration private int ttl; + @NotNull + public List getDeviceSecrets() { + if (deviceSecrets == null) deviceSecrets = new ArrayList<>(); + return deviceSecrets; + } + + public void setDeviceSecrets(List deviceSecrets) { + this.deviceSecrets = deviceSecrets; + } + public int getTtl() { return ttl; } @@ -305,6 +316,7 @@ public String toString() { sb.append(", permissionGrantedMap=").append(permissionGrantedMap); sb.append(", sessionAttributes=").append(sessionAttributes); sb.append(", persisted=").append(persisted); + sb.append(", deviceSecrets=").append(deviceSecrets); sb.append("}"); return sb.toString(); } diff --git a/jans-auth-server/model/src/main/java/io/jans/as/model/authorize/AuthorizeResponseParam.java b/jans-auth-server/model/src/main/java/io/jans/as/model/authorize/AuthorizeResponseParam.java index a7214cb147f..f955edde637 100644 --- a/jans-auth-server/model/src/main/java/io/jans/as/model/authorize/AuthorizeResponseParam.java +++ b/jans-auth-server/model/src/main/java/io/jans/as/model/authorize/AuthorizeResponseParam.java @@ -28,6 +28,7 @@ public final class AuthorizeResponseParam { public static final String ISS = "iss"; public static final String AUD = "aud"; public static final String EXP = "exp"; + public static final String DEVICE_SECRET = "device_secret"; /** * String that represents the End-User's login state at the OP. 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 b10e3836902..79bfb0ee8ce 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 @@ -78,6 +78,11 @@ public enum GrantType implements HasParamName, AttributeEnum { */ OXAUTH_UMA_TICKET("urn:ietf:params:oauth:grant-type:uma-ticket"), + /** + * Token exchange grant type for OAuth 2.0 + */ + TOKEN_EXCHANGE("urn:ietf:params:oauth:grant-type:token-exchange"), + /** * CIBA (Client Initiated Backchannel Authentication) Grant Type. */ diff --git a/jans-auth-server/model/src/main/java/io/jans/as/model/common/ScopeConstants.java b/jans-auth-server/model/src/main/java/io/jans/as/model/common/ScopeConstants.java index 56928af5d00..9ad00c48a52 100644 --- a/jans-auth-server/model/src/main/java/io/jans/as/model/common/ScopeConstants.java +++ b/jans-auth-server/model/src/main/java/io/jans/as/model/common/ScopeConstants.java @@ -13,6 +13,7 @@ public class ScopeConstants { public static final String OPENID = "openid"; public static final String OFFLINE_ACCESS = "offline_access"; + public static final String DEVICE_SSO = "device_sso"; private ScopeConstants() { } 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 57a7cdfa679..569edd95521 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 @@ -49,6 +49,9 @@ private Constants() { public static final String NO_CACHE = "no-cache"; public static final String X_CLIENTCERT = "X-ClientCert"; public static final String WWW_AUTHENTICATE = "WWW-Authenticate"; + public static final String SUBJECT_TOKEN_TYPE_ID_TOKEN = "urn:ietf:params:oauth:token-type:id_token"; + public static final String ACTOR_TOKEN_TYPE_DEVICE_SECRET = "urn:x-oath:params:oauth:token-type:device-secret"; + public static final String TOKEN_TYPE_ACCESS_TOKEN = "urn:ietf:params:oauth:token-type:access_token"; public static final String CONTENT_TYPE_APPLICATION_JSON_UTF_8 = "application/json;charset=UTF-8"; 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 74f6de16c01..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 @@ -250,6 +250,11 @@ public class AppConfiguration implements Configuration { 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; @@ -345,6 +350,23 @@ 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; + } + + public void setRotateDeviceSecret(Boolean rotateDeviceSecret) { + this.rotateDeviceSecret = rotateDeviceSecret; + } + public Boolean getRequirePkce() { if (requirePkce == null) requirePkce = false; return requirePkce; diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/authorize/ws/rs/AuthorizeRestWebServiceImpl.java b/jans-auth-server/server/src/main/java/io/jans/as/server/authorize/ws/rs/AuthorizeRestWebServiceImpl.java index 91353d03274..2731fb17192 100644 --- a/jans-auth-server/server/src/main/java/io/jans/as/server/authorize/ws/rs/AuthorizeRestWebServiceImpl.java +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/authorize/ws/rs/AuthorizeRestWebServiceImpl.java @@ -114,6 +114,8 @@ @Path("/") public class AuthorizeRestWebServiceImpl implements AuthorizeRestWebService { + private static final String SUCCESSFUL_RP_REDIRECT_COUNT = "successful_rp_redirect_count"; + @Inject private Logger log; @@ -502,7 +504,7 @@ private ResponseBuilder authorize(AuthzRequest authzRequest) throws AcrChangedEx ResponseBuilder builder = RedirectUtil.getRedirectResponseBuilder(authzRequest.getRedirectUriResponse().getRedirectUri(), authzRequest.getHttpRequest()); addCustomHeaders(builder, authzRequest); - updateSessionRpRedirect(sessionUser); + updateSession(authzRequest, sessionUser); runCiba(authzRequest.getAuthReqId(), client, authzRequest.getHttpRequest(), authzRequest.getHttpResponse()); processDeviceAuthorization(deviceAuthzUserCode, user); @@ -910,11 +912,13 @@ private Response redirectTo(String pathToRedirect, AuthzRequest authzRequest, Li return builder.build(); } - private void updateSessionRpRedirect(SessionId sessionUser) { - int rpRedirectCount = Util.parseIntSilently(sessionUser.getSessionAttributes().get("successful_rp_redirect_count"), 0); + private void updateSession(AuthzRequest authzRequest, SessionId sessionUser) { + authzRequestService.addDeviceSecretToSession(authzRequest, sessionUser); + + int rpRedirectCount = Util.parseIntSilently(sessionUser.getSessionAttributes().get(SUCCESSFUL_RP_REDIRECT_COUNT), 0); rpRedirectCount++; - sessionUser.getSessionAttributes().put("successful_rp_redirect_count", Integer.toString(rpRedirectCount)); + sessionUser.getSessionAttributes().put(SUCCESSFUL_RP_REDIRECT_COUNT, Integer.toString(rpRedirectCount)); sessionIdService.updateSessionId(sessionUser); } @@ -925,7 +929,7 @@ private boolean unauthenticateSession(String sessionId, HttpServletRequest httpR private boolean unauthenticateSession(String sessionId, HttpServletRequest httpRequest, boolean isPromptFromJwt) { SessionId sessionUser = identity.getSessionId(); - if (isPromptFromJwt && sessionUser != null && !sessionUser.getSessionAttributes().containsKey("successful_rp_redirect_count")) { + if (isPromptFromJwt && sessionUser != null && !sessionUser.getSessionAttributes().containsKey(SUCCESSFUL_RP_REDIRECT_COUNT)) { return false; // skip unauthentication because there were no at least one successful rp redirect } 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 4c11e80899b..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 @@ -4,11 +4,15 @@ import com.google.common.collect.Sets; 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.util.CommonUtils; import io.jans.as.common.util.RedirectUri; import io.jans.as.model.authorize.AuthorizeErrorResponseType; +import io.jans.as.model.authorize.AuthorizeResponseParam; +import io.jans.as.model.common.GrantType; import io.jans.as.model.common.Prompt; import io.jans.as.model.common.ResponseMode; +import io.jans.as.model.common.ScopeConstants; import io.jans.as.model.config.WebKeysConfiguration; import io.jans.as.model.configuration.AppConfiguration; import io.jans.as.model.crypto.AbstractCryptoProvider; @@ -34,6 +38,7 @@ import io.jans.as.server.model.authorize.IdTokenMember; import io.jans.as.server.model.authorize.JwtAuthorizationRequest; import io.jans.as.server.model.authorize.ScopeChecker; +import io.jans.as.server.model.token.HandleTokenFactory; import io.jans.as.server.par.ws.rs.ParService; import io.jans.as.server.service.*; import io.jans.as.server.util.ServerUtil; @@ -44,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; @@ -52,6 +58,7 @@ import java.net.URLDecoder; import java.nio.charset.StandardCharsets; import java.security.PrivateKey; +import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Set; @@ -101,6 +108,27 @@ public class AuthzRequestService { @Inject 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; + } + if (!ArrayUtils.contains(authzRequest.getClient().getGrantTypes(), GrantType.TOKEN_EXCHANGE)) { + log.debug("Skip device secret. Scope has {} value but client does not have Token Exchange Grant Type enabled ('urn:ietf:params:oauth:grant-type:token-exchange')", ScopeConstants.DEVICE_SSO); + return; + } + + final String newDeviceSecret = HandleTokenFactory.generateDeviceSecret(); + + final List deviceSecrets = sessionId.getDeviceSecrets(); + deviceSecrets.add(newDeviceSecret); + + authzRequest.getRedirectUriResponse().getRedirectUri().addResponseParameter(AuthorizeResponseParam.DEVICE_SECRET, newDeviceSecret); + } + + public boolean processPar(AuthzRequest authzRequest) { boolean isPar = Util.isPar(authzRequest.getRequestUri()); if (!isPar && isTrue(appConfiguration.getRequirePar())) { 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 3ee753e19d7..7aab5392bf1 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 @@ -130,6 +130,13 @@ public ResourceOwnerPasswordCredentialsGrant createResourceOwnerPasswordCredenti return grant; } + @Override + public TokenExchangeGrant createTokenExchangeGrant(User user, Client client) { + TokenExchangeGrant grant = grantInstance.select(TokenExchangeGrant.class).get(); + grant.init(user, client); + return grant; + } + @Override public CIBAGrant createCIBAGrant(CibaRequestCacheControl request) { CIBAGrant grant = grantInstance.select(CIBAGrant.class).get(); @@ -308,6 +315,12 @@ public AuthorizationGrant asGrant(TokenEntity tokenEntity) { result = deviceCodeGrant; break; + case TOKEN_EXCHANGE: + TokenExchangeGrant tokenExchangeGrant = grantInstance.select(TokenExchangeGrant.class).get(); + tokenExchangeGrant.init(user, AuthorizationGrantType.TOKEN_EXCHANGE, client, tokenEntity.getCreationDate()); + + result = tokenExchangeGrant; + break; default: return null; } diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/model/common/AuthorizationGrantType.java b/jans-auth-server/server/src/main/java/io/jans/as/server/model/common/AuthorizationGrantType.java index 617c5e8f7d8..9075753ebf9 100644 --- a/jans-auth-server/server/src/main/java/io/jans/as/server/model/common/AuthorizationGrantType.java +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/model/common/AuthorizationGrantType.java @@ -61,6 +61,8 @@ public enum AuthorizationGrantType implements HasParamName { * grant types are not available (such as an authorization code). */ RESOURCE_OWNER_PASSWORD_CREDENTIALS("resource_owner_password_credentials"), + + TOKEN_EXCHANGE("urn:ietf:params:oauth:grant-type:token-exchange"), /** * An extension grant for Client Initiated Backchannel Authentication. */ diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/model/common/IAuthorizationGrantList.java b/jans-auth-server/server/src/main/java/io/jans/as/server/model/common/IAuthorizationGrantList.java index 712384bd930..2402aef640c 100644 --- a/jans-auth-server/server/src/main/java/io/jans/as/server/model/common/IAuthorizationGrantList.java +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/model/common/IAuthorizationGrantList.java @@ -31,6 +31,8 @@ public interface IAuthorizationGrantList { ResourceOwnerPasswordCredentialsGrant createResourceOwnerPasswordCredentialsGrant(User user, Client client); + TokenExchangeGrant createTokenExchangeGrant(User user, Client client); + CIBAGrant createCIBAGrant(CibaRequestCacheControl request); AuthorizationCodeGrant getAuthorizationCodeGrant(String authorizationCode); diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/model/common/TokenExchangeGrant.java b/jans-auth-server/server/src/main/java/io/jans/as/server/model/common/TokenExchangeGrant.java new file mode 100644 index 00000000000..2498f2203e1 --- /dev/null +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/model/common/TokenExchangeGrant.java @@ -0,0 +1,20 @@ +package io.jans.as.server.model.common; + +import io.jans.as.common.model.common.User; +import io.jans.as.common.model.registration.Client; +import io.jans.as.model.common.GrantType; + +/** + * @author Yuriy Z + */ +public class TokenExchangeGrant extends AuthorizationGrant { + + public void init(User user, Client client) { + super.init(user, AuthorizationGrantType.TOKEN_EXCHANGE, client, null); + } + + @Override + public GrantType getGrantType() { + return GrantType.TOKEN_EXCHANGE; + } +} diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/model/token/HandleTokenFactory.java b/jans-auth-server/server/src/main/java/io/jans/as/server/model/token/HandleTokenFactory.java index 451e1f40ad7..0155b8cd10d 100644 --- a/jans-auth-server/server/src/main/java/io/jans/as/server/model/token/HandleTokenFactory.java +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/model/token/HandleTokenFactory.java @@ -40,4 +40,8 @@ public class HandleTokenFactory { public static String generateHandleToken() { return UUID.randomUUID().toString(); } + + public static String generateDeviceSecret() { + return UUID.randomUUID().toString(); + } } \ No newline at end of file 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/service/SessionIdService.java b/jans-auth-server/server/src/main/java/io/jans/as/server/service/SessionIdService.java index 568027979b3..be0e71e0360 100644 --- a/jans-auth-server/server/src/main/java/io/jans/as/server/service/SessionIdService.java +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/service/SessionIdService.java @@ -786,6 +786,19 @@ public SessionId getSessionBySid(@Nullable String sid) { return entries.get(0); } + @Nullable + public SessionId getSessionByDeviceSecret(@Nullable String deviceSecret) { + if (StringUtils.isBlank(deviceSecret)) { + return null; + } + + final List entries = persistenceEntryManager.findEntries(staticConfiguration.getBaseDn().getSessions(), SessionId.class, Filter.createEqualityFilter("deviceSecret", deviceSecret)); + if (entries == null || entries.size() != 1) { + return null; + } + return entries.get(0); + } + @Nullable public SessionId getSessionByDn(@Nullable String dn, boolean silently) { if (StringUtils.isBlank(dn)) { diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/token/ws/rs/TokenCreatorService.java b/jans-auth-server/server/src/main/java/io/jans/as/server/token/ws/rs/TokenCreatorService.java new file mode 100644 index 00000000000..aabaed31fbd --- /dev/null +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/token/ws/rs/TokenCreatorService.java @@ -0,0 +1,96 @@ +package io.jans.as.server.token.ws.rs; + +import com.google.common.base.Strings; +import io.jans.as.common.model.common.User; +import io.jans.as.common.model.registration.Client; +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.error.ErrorResponseFactory; +import io.jans.as.model.token.TokenErrorResponseType; +import io.jans.as.server.model.common.AbstractAuthorizationGrant; +import io.jans.as.server.model.common.AuthorizationGrant; +import io.jans.as.server.model.common.ExecutionContext; +import io.jans.as.server.model.common.RefreshToken; +import io.jans.as.server.service.external.ExternalUpdateTokenService; +import io.jans.as.server.service.external.context.ExternalUpdateTokenContext; +import jakarta.ejb.Stateless; +import jakarta.inject.Inject; +import jakarta.inject.Named; +import jakarta.ws.rs.WebApplicationException; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import org.apache.commons.lang.BooleanUtils; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; + +import java.util.Arrays; + +import static org.apache.commons.lang.BooleanUtils.isTrue; + +/** + * @author Yuriy Z + */ +@Stateless +@Named +public class TokenCreatorService { + + @Inject + private Logger log; + + @Inject + private AppConfiguration appConfiguration; + + @Inject + private ExternalUpdateTokenService externalUpdateTokenService; + + @Inject + private ErrorResponseFactory errorResponseFactory; + + @Inject + private AttributeService attributeService; + + public boolean isRefreshTokenAllowed(Client client, String requestedScope, AbstractAuthorizationGrant grant) { + if (isTrue(appConfiguration.getForceOfflineAccessScopeToEnableRefreshToken()) && !grant.getScopes().contains(ScopeConstants.OFFLINE_ACCESS) && !Strings.nullToEmpty(requestedScope).contains(ScopeConstants.OFFLINE_ACCESS)) { + return false; + } + return Arrays.asList(client.getGrantTypes()).contains(GrantType.REFRESH_TOKEN); + } + + @Nullable + public RefreshToken createRefreshToken(@NotNull ExecutionContext executionContext, @NotNull String scope) { + final AuthorizationGrant grant = executionContext.getGrant(); + if (!isRefreshTokenAllowed(executionContext.getClient(), scope, grant)) { + return null; + } + + checkUser(grant); + + final ExternalUpdateTokenContext context = new ExternalUpdateTokenContext(executionContext.getHttpRequest(), grant, executionContext.getClient(), appConfiguration, attributeService); + context.setExecutionContext(executionContext); + + final int refreshTokenLifetimeInSeconds = externalUpdateTokenService.getRefreshTokenLifetimeInSeconds(context); + if (refreshTokenLifetimeInSeconds > 0) { + return grant.createRefreshToken(executionContext, refreshTokenLifetimeInSeconds); + } + return grant.createRefreshToken(executionContext); + } + + private void checkUser(AuthorizationGrant authorizationGrant) { + if (BooleanUtils.isFalse(appConfiguration.getCheckUserPresenceOnRefreshToken())) { + return; + } + + final User user = authorizationGrant.getUser(); + if (user == null || "inactive".equalsIgnoreCase(user.getStatus())) { + log.trace("The user associated with this grant is not found or otherwise with status=inactive."); + throw new WebApplicationException(error(400, TokenErrorResponseType.INVALID_GRANT, "The user associated with this grant is not found or otherwise with status=inactive.").build()); + } + } + + public Response.ResponseBuilder error(int status, TokenErrorResponseType type, String reason) { + return Response.status(status).type(MediaType.APPLICATION_JSON_TYPE).entity(errorResponseFactory.errorAsJson(type, reason)); + } +} 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 new file mode 100644 index 00000000000..23e9964e56b --- /dev/null +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/token/ws/rs/TokenExchangeService.java @@ -0,0 +1,201 @@ +package io.jans.as.server.token.ws.rs; + +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.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; +import io.jans.as.server.model.common.*; +import io.jans.as.server.model.token.HandleTokenFactory; +import io.jans.as.server.service.SessionIdService; +import io.jans.as.server.service.external.ExternalUpdateTokenService; +import io.jans.as.server.service.external.context.ExternalUpdateTokenContext; +import jakarta.ejb.Stateless; +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; +import org.json.JSONObject; +import org.slf4j.Logger; + +import java.util.List; +import java.util.function.Function; + +import static com.google.common.base.Preconditions.checkNotNull; +import static io.jans.as.model.config.Constants.OPENID; +import static io.jans.as.model.config.Constants.TOKEN_TYPE_ACCESS_TOKEN; +import static org.apache.commons.lang.BooleanUtils.isTrue; + +/** + * @author Yuriy Z + */ +@Stateless +@Named +public class TokenExchangeService { + + public static final String DEVICE_SECRET = "device_secret"; + + @Inject + private Logger log; + + @Inject + private AppConfiguration appConfiguration; + + @Inject + private SessionIdService sessionIdService; + + @Inject + private TokenRestWebServiceValidator tokenRestWebServiceValidator; + + @Inject + private AuthorizationGrantList authorizationGrantList; + + @Inject + private ExternalUpdateTokenService externalUpdateTokenService; + + @Inject + private TokenCreatorService tokenCreatorService; + + @Inject + private AttributeService attributeService; + + 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(deviceSecret); + deviceSecrets.add(newDeviceSecret); + + sessionIdService.updateSessionId(sessionId, false); + + return newDeviceSecret; + } + + public JSONObject processTokenExchange(String scope, Function idTokenPreProcessing, ExecutionContext executionContext) { + final HttpServletRequest httpRequest = executionContext.getHttpRequest(); + final Client client = executionContext.getClient(); + final OAuth2AuditLog auditLog = executionContext.getAuditLog(); + + final String audience = httpRequest.getParameter("audience"); + final String subjectToken = httpRequest.getParameter("subject_token"); + final String subjectTokenType = httpRequest.getParameter("subject_token_type"); + final String deviceSecret = httpRequest.getParameter("actor_token"); + final String actorTokenType = httpRequest.getParameter("actor_token_type"); + + tokenRestWebServiceValidator.validateAudience(audience, auditLog); + tokenRestWebServiceValidator.validateSubjectTokenType(subjectTokenType, auditLog); + tokenRestWebServiceValidator.validateActorTokenType(actorTokenType, auditLog); + tokenRestWebServiceValidator.validateActorToken(deviceSecret, auditLog); + + final SessionId sessionId = sessionIdService.getSessionByDeviceSecret(deviceSecret); + tokenRestWebServiceValidator.validateSessionForTokenExchange(sessionId, deviceSecret, auditLog); + checkNotNull(sessionId); // it checked by validator, added it only to relax static bugs checker + + tokenRestWebServiceValidator.validateSubjectToken(deviceSecret, subjectToken, sessionId, auditLog); + + TokenExchangeGrant tokenExchangeGrant = authorizationGrantList.createTokenExchangeGrant(new User(), client); + tokenExchangeGrant.setSessionDn(sessionId.getDn()); + + executionContext.setGrant(tokenExchangeGrant); + + scope = tokenExchangeGrant.checkScopesPolicy(scope); + + AccessToken accessToken = tokenExchangeGrant.createAccessToken(executionContext); // create token after scopes are checked + + IdToken idToken = null; + if (isTrue(appConfiguration.getOpenidScopeBackwardCompatibility()) && tokenExchangeGrant.getScopes().contains(OPENID)) { + boolean includeIdTokenClaims = Boolean.TRUE.equals( + appConfiguration.getLegacyIdTokenClaims()); + + ExternalUpdateTokenContext context = new ExternalUpdateTokenContext(httpRequest, tokenExchangeGrant, client, appConfiguration, attributeService); + + executionContext.setIncludeIdTokenClaims(includeIdTokenClaims); + executionContext.setPreProcessing(idTokenPreProcessing); + executionContext.setPostProcessor(externalUpdateTokenService.buildModifyIdTokenProcessor(context)); + + idToken = tokenExchangeGrant.createIdToken( + null, null, null, null, null, executionContext); + } + + RefreshToken reToken = tokenCreatorService.createRefreshToken(executionContext, scope); + + executionContext.getAuditLog().updateOAuth2AuditLog(tokenExchangeGrant, true); + + String rotatedDeviceSecret = rotateDeviceSecret(sessionId, deviceSecret); + + JSONObject jsonObj = new JSONObject(); + try { + TokenRestWebServiceImpl.fillJsonObject(jsonObj, accessToken, accessToken.getTokenType(), accessToken.getExpiresIn(), reToken, scope, idToken); + jsonObj.put("issued_token_type", TOKEN_TYPE_ACCESS_TOKEN); + if (StringUtils.isNotBlank(rotatedDeviceSecret)) { + jsonObj.put(DEVICE_SECRET, rotatedDeviceSecret); + } + } catch (JSONException e) { + log.error(e.getMessage(), e); + } + + return jsonObj; + } + + public void putNewDeviceSecret(JSONObject jsonObj, String sessionDn, Client client, String scope) { + if (!scope.contains(ScopeConstants.DEVICE_SSO)) { + return; + } + if (!ArrayUtils.contains(client.getGrantTypes(), GrantType.TOKEN_EXCHANGE)) { + log.debug("Skip device secret. Scope has {} value but client does not have Token Exchange Grant Type enabled ('urn:ietf:params:oauth:grant-type:token-exchange')", ScopeConstants.DEVICE_SSO); + return; + } + + try { + final SessionId sessionId = sessionIdService.getSessionByDn(sessionDn); + if (sessionId == null) { + log.debug("Unable to find session by dn: {}", sessionDn); + return; + } + + String newDeviceSecret = HandleTokenFactory.generateDeviceSecret(); + sessionId.getDeviceSecrets().add(newDeviceSecret); + + sessionIdService.updateSessionId(sessionId, false); + + jsonObj.put("device_token", newDeviceSecret); + } catch (Exception e) { + log.error("Failed to generate device_secret", e); + } + } +} diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/token/ws/rs/TokenRestWebServiceImpl.java b/jans-auth-server/server/src/main/java/io/jans/as/server/token/ws/rs/TokenRestWebServiceImpl.java index 80b454130d4..f94da0bbd51 100644 --- a/jans-auth-server/server/src/main/java/io/jans/as/server/token/ws/rs/TokenRestWebServiceImpl.java +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/token/ws/rs/TokenRestWebServiceImpl.java @@ -13,7 +13,10 @@ 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.common.*; +import io.jans.as.model.common.BackchannelTokenDeliveryMode; +import io.jans.as.model.common.FeatureFlagType; +import io.jans.as.model.common.GrantType; +import io.jans.as.model.common.TokenType; import io.jans.as.model.configuration.AppConfiguration; import io.jans.as.model.crypto.binding.TokenBindingMessage; import io.jans.as.model.error.ErrorResponseFactory; @@ -52,9 +55,7 @@ import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.Response.ResponseBuilder; import jakarta.ws.rs.core.SecurityContext; -import org.apache.commons.lang.BooleanUtils; import org.apache.commons.lang.StringUtils; -import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.json.JSONException; import org.json.JSONObject; @@ -62,7 +63,6 @@ import java.security.NoSuchAlgorithmException; import java.security.NoSuchProviderException; -import java.util.Arrays; import java.util.Date; import java.util.function.Function; @@ -134,6 +134,12 @@ public class TokenRestWebServiceImpl implements TokenRestWebService { @Inject private TokenRestWebServiceValidator tokenRestWebServiceValidator; + @Inject + private TokenExchangeService tokenExchangeService; + + @Inject + private TokenCreatorService tokenCreatorService; + @Override public Response requestAccessToken(String grantType, String code, String redirectUri, String username, String password, String scope, @@ -188,9 +194,7 @@ public Response requestAccessToken(String grantType, String code, if (gt == GrantType.AUTHORIZATION_CODE) { return processAuthorizationCode(code, scope, codeVerifier, sessionIdObj, executionContext); - } - - if (gt == GrantType.REFRESH_TOKEN) { + } else if (gt == GrantType.REFRESH_TOKEN) { return processRefreshTokenGrant(scope, refreshToken, idTokenPreProcessing, executionContext); } else if (gt == GrantType.CLIENT_CREDENTIALS) { return processClientGredentials(scope, request, auditLog, client, idTokenPreProcessing, executionContext); @@ -200,6 +204,9 @@ public Response requestAccessToken(String grantType, String code, return processCIBA(scope, authReqId, idTokenPreProcessing, executionContext); } else if (gt == GrantType.DEVICE_CODE) { return processDeviceCodeGrantType(executionContext, deviceCode, scope); + } else if (gt == GrantType.TOKEN_EXCHANGE) { + final JSONObject responseJson = tokenExchangeService.processTokenExchange(scope, idTokenPreProcessing, executionContext); + return response(Response.ok().entity(responseJson.toString()), auditLog); } } catch (WebApplicationException e) { throw e; @@ -245,7 +252,7 @@ private Response processROPC(String username, String password, String scope, Gra } } - RefreshToken reToken = createRefreshToken(executionContext, scope); + RefreshToken reToken = tokenCreatorService.createRefreshToken(executionContext, scope); scope = resourceOwnerPasswordCredentialsGrant.checkScopesPolicy(scope); @@ -327,7 +334,7 @@ private Response processRefreshTokenGrant(String scope, String refreshToken, Fun RefreshToken reToken = null; if (isFalse(appConfiguration.getSkipRefreshTokenDuringRefreshing())) { if (isTrue(appConfiguration.getRefreshTokenExtendLifetimeOnRotation())) { - reToken = createRefreshToken(executionContext, scope); // extend lifetime + reToken = tokenCreatorService.createRefreshToken(executionContext, scope); // extend lifetime } else { log.trace("Create refresh token with fixed (not extended) lifetime taken from previous refresh token."); @@ -359,6 +366,8 @@ private Response processRefreshTokenGrant(String scope, String refreshToken, Fun grantService.removeByCode(refreshToken); // remove refresh token after access token and id_token is created. } + tokenExchangeService.rotateDeviceSecretOnRefreshToken(executionContext.getHttpRequest(), authorizationGrant, scope); + auditLog.updateOAuth2AuditLog(authorizationGrant, true); return response(Response.ok().entity(getJSonResponse(accToken, @@ -384,7 +393,7 @@ private Response processAuthorizationCode(String code, String scope, String code authorizationCodeGrant.setIsCachedWithNoPersistence(false); authorizationCodeGrant.save(); - RefreshToken reToken = createRefreshToken(executionContext, scope); + RefreshToken reToken = tokenCreatorService.createRefreshToken(executionContext, scope); scope = authorizationCodeGrant.checkScopesPolicy(scope); @@ -417,8 +426,15 @@ private Response processAuthorizationCode(String code, String scope, String code grantService.removeAuthorizationCode(authorizationCodeGrant.getAuthorizationCode().getCode()); - final String entity = getJSonResponse(accToken, accToken.getTokenType(), accToken.getExpiresIn(), reToken, scope, idToken); - return response(Response.ok().entity(entity), executionContext.getAuditLog()); + JSONObject jsonObj = new JSONObject(); + try { + fillJsonObject(jsonObj, accToken, accToken.getTokenType(), accToken.getExpiresIn(), reToken, scope, idToken); + tokenExchangeService.putNewDeviceSecret(jsonObj, authorizationCodeGrant.getSessionDn(), client, scope); + } catch (JSONException e) { + log.error(e.getMessage(), e); + } + + return response(Response.ok().entity(jsonObj.toString()), executionContext.getAuditLog()); } @Nullable @@ -454,37 +470,6 @@ private User authenticateUser(String username, String password, ExecutionContext return user; } - private void checkUser(AuthorizationGrant authorizationGrant) { - if (BooleanUtils.isFalse(appConfiguration.getCheckUserPresenceOnRefreshToken())) { - return; - } - - final User user = authorizationGrant.getUser(); - if (user == null || "inactive".equalsIgnoreCase(user.getStatus())) { - log.trace("The user associated with this grant is not found or otherwise with status=inactive."); - throw new WebApplicationException(error(400, TokenErrorResponseType.INVALID_GRANT, "The user associated with this grant is not found or otherwise with status=inactive.").build()); - } - } - - @Nullable - private RefreshToken createRefreshToken(@NotNull ExecutionContext executionContext, @NotNull String scope) { - final AuthorizationGrant grant = executionContext.getGrant(); - if (!isRefreshTokenAllowed(executionContext.getClient(), scope, grant)) { - return null; - } - - checkUser(grant); - - final ExternalUpdateTokenContext context = new ExternalUpdateTokenContext(executionContext.getHttpRequest(), grant, executionContext.getClient(), appConfiguration, attributeService); - context.setExecutionContext(executionContext); - - final int refreshTokenLifetimeInSeconds = externalUpdateTokenService.getRefreshTokenLifetimeInSeconds(context); - if (refreshTokenLifetimeInSeconds > 0) { - return grant.createRefreshToken(executionContext, refreshTokenLifetimeInSeconds); - } - return grant.createRefreshToken(executionContext); - } - /** * Processes token request for device code grant type. * @@ -505,7 +490,7 @@ private Response processDeviceCodeGrantType(ExecutionContext executionContext, f throw new WebApplicationException(response(error(400, TokenErrorResponseType.INVALID_GRANT, REASON_CLIENT_NOT_AUTHORIZED), executionContext.getAuditLog())); } - RefreshToken refToken = createRefreshToken(executionContext, scope); + RefreshToken refToken = tokenCreatorService.createRefreshToken(executionContext, scope); AccessToken accessToken = deviceCodeGrant.createAccessToken(executionContext); @@ -564,13 +549,6 @@ private Response processDeviceCodeGrantType(ExecutionContext executionContext, f } } - private boolean isRefreshTokenAllowed(Client client, String requestedScope, AbstractAuthorizationGrant grant) { - if (isTrue(appConfiguration.getForceOfflineAccessScopeToEnableRefreshToken()) && !grant.getScopes().contains(ScopeConstants.OFFLINE_ACCESS) && !Strings.nullToEmpty(requestedScope).contains(ScopeConstants.OFFLINE_ACCESS)) { - return false; - } - return Arrays.asList(client.getGrantTypes()).contains(GrantType.REFRESH_TOKEN); - } - private void validatePKCE(AuthorizationCodeGrant grant, String codeVerifier, OAuth2AuditLog oAuth2AuditLog) { log.trace("PKCE validation, code_verifier: {}, code_challenge: {}, method: {}", codeVerifier, grant.getCodeChallenge(), grant.getCodeChallengeMethod()); @@ -632,20 +610,7 @@ public String getJSonResponse(AccessToken accessToken, TokenType tokenType, IdToken idToken) { JSONObject jsonObj = new JSONObject(); try { - jsonObj.put("access_token", accessToken.getCode()); // Required - jsonObj.put("token_type", tokenType.toString()); // Required - if (expiresIn != null) { // Optional - jsonObj.put("expires_in", expiresIn); - } - if (refreshToken != null) { // Optional - jsonObj.put("refresh_token", refreshToken.getCode()); - } - if (scope != null) { // Optional - jsonObj.put("scope", scope); - } - if (idToken != null) { - jsonObj.put("id_token", idToken.getCode()); - } + fillJsonObject(jsonObj, accessToken, tokenType, expiresIn, refreshToken, scope, idToken); } catch (JSONException e) { log.error(e.getMessage(), e); } @@ -653,6 +618,25 @@ public String getJSonResponse(AccessToken accessToken, TokenType tokenType, return jsonObj.toString(); } + public static void fillJsonObject(JSONObject jsonObj, AccessToken accessToken, TokenType tokenType, + Integer expiresIn, RefreshToken refreshToken, String scope, + IdToken idToken) { + jsonObj.put("access_token", accessToken.getCode()); // Required + jsonObj.put("token_type", tokenType.toString()); // Required + if (expiresIn != null) { // Optional + jsonObj.put("expires_in", expiresIn); + } + if (refreshToken != null) { // Optional + jsonObj.put("refresh_token", refreshToken.getCode()); + } + if (scope != null) { // Optional + jsonObj.put("scope", scope); + } + if (idToken != null) { + jsonObj.put("id_token", idToken.getCode()); + } + } + private Response processCIBA(String scope, String authReqId, Function idTokenPreProcessing, ExecutionContext executionContext) { errorResponseFactory.validateFeatureEnabled(FeatureFlagType.CIBA); @@ -670,7 +654,7 @@ private Response processCIBA(String scope, String authReqId, Function + diff --git a/jans-linux-setup/jans_setup/schema/jans_schema.json b/jans-linux-setup/jans_setup/schema/jans_schema.json index bb3915f37af..8c05c23f9d5 100644 --- a/jans-linux-setup/jans_setup/schema/jans_schema.json +++ b/jans-linux-setup/jans_setup/schema/jans_schema.json @@ -2405,6 +2405,17 @@ "syntax": "1.3.6.1.4.1.1466.115.121.1.15", "x_origin": "Jans created attribute" }, + { + "desc": "deviceSecret", + "equality": "caseIgnoreMatch", + "names": [ + "deviceSecret" + ], + "oid": "jansAttr", + "substr": "caseIgnoreSubstringsMatch", + "syntax": "1.3.6.1.4.1.1466.115.121.1.15", + "x_origin": "Jans created attribute" + }, { "desc": "jans Sess DN", "equality": "caseIgnoreMatch", @@ -3625,6 +3636,7 @@ "jansJwt", "jansPermissionGrantedMap", "jansInvolvedClnts", + "deviceSecret", "jansSessAttr" ], "must": [ diff --git a/jans-linux-setup/jans_setup/static/rdbm/mysql_index.json b/jans-linux-setup/jans_setup/static/rdbm/mysql_index.json index 86a5604e2a9..c12885a8f10 100644 --- a/jans-linux-setup/jans_setup/static/rdbm/mysql_index.json +++ b/jans-linux-setup/jans_setup/static/rdbm/mysql_index.json @@ -99,7 +99,8 @@ "jansSessId": { "fields": [ "jansUsrDN", - "sid" + "sid", + "deviceSecret" ], "custom": [ "`del`, `exp`" diff --git a/jans-linux-setup/jans_setup/static/rdbm/pgsql_index.json b/jans-linux-setup/jans_setup/static/rdbm/pgsql_index.json index b0502e03760..7d484e338ce 100644 --- a/jans-linux-setup/jans_setup/static/rdbm/pgsql_index.json +++ b/jans-linux-setup/jans_setup/static/rdbm/pgsql_index.json @@ -99,7 +99,8 @@ "jansSessId": { "fields": [ "jansUsrDN", - "sid" + "sid", + "deviceSecret" ], "custom": [ "(\"del\", \"exp\")" diff --git a/jans-linux-setup/jans_setup/static/rdbm/spanner_index.json b/jans-linux-setup/jans_setup/static/rdbm/spanner_index.json index fd495c7e138..9e1cfbf99b3 100644 --- a/jans-linux-setup/jans_setup/static/rdbm/spanner_index.json +++ b/jans-linux-setup/jans_setup/static/rdbm/spanner_index.json @@ -115,7 +115,8 @@ "jansSessId": { "fields": [ "jansUsrDN", - "sid" + "sid", + "deviceSecret" ], "custom": [ "`del`, `exp`"