From 17a931e0e364cfbc5a69676092befb7f750f583e Mon Sep 17 00:00:00 2001 From: YuriyZ Date: Mon, 24 Oct 2022 19:54:47 +0300 Subject: [PATCH] feat(jans-auth-server): added device secret generation #2518 --- .../jans/as/client/AuthorizationResponse.java | 28 +++--- .../as/common/model/session/SessionId.java | 17 +++- .../authorize/AuthorizeResponseParam.java | 1 + .../jans/as/model/common/ScopeConstants.java | 1 + .../ws/rs/AuthorizeRestWebServiceImpl.java | 14 ++- .../authorize/ws/rs/AuthzRequestService.java | 19 ++++ .../as/server/service/SessionIdService.java | 13 +++ .../ws/rs/AuthzRequestServiceTest.java | 95 +++++++++++++++++++ .../jans_setup/schema/jans_schema.json | 12 +++ 9 files changed, 179 insertions(+), 21 deletions(-) create mode 100644 jans-auth-server/server/src/test/java/io/jans/as/server/authorize/ws/rs/AuthzRequestServiceTest.java 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..07e50cee392 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,15 @@ public class SessionId implements Deletable, Serializable { @Expiration private int ttl; + 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 +315,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/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/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..4bb8ed1fc53 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,14 @@ 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.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 +37,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; @@ -52,6 +56,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 +106,20 @@ public class AuthzRequestService { @Inject private RedirectionUriService redirectionUriService; + public void addDeviceSecretToSession(AuthzRequest authzRequest, SessionId sessionId) { + if (!Arrays.asList(authzRequest.getScope().split(" ")).contains(ScopeConstants.DEVICE_SSO)) { + return; + } + + final String newDeviceSecret = HandleTokenFactory.generateHandleToken(); + + 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/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/test/java/io/jans/as/server/authorize/ws/rs/AuthzRequestServiceTest.java b/jans-auth-server/server/src/test/java/io/jans/as/server/authorize/ws/rs/AuthzRequestServiceTest.java new file mode 100644 index 00000000000..a3d0f0d6b2a --- /dev/null +++ b/jans-auth-server/server/src/test/java/io/jans/as/server/authorize/ws/rs/AuthzRequestServiceTest.java @@ -0,0 +1,95 @@ +package io.jans.as.server.authorize.ws.rs; + +import io.jans.as.common.model.session.SessionId; +import io.jans.as.common.util.RedirectUri; +import io.jans.as.model.config.WebKeysConfiguration; +import io.jans.as.model.configuration.AppConfiguration; +import io.jans.as.model.crypto.AbstractCryptoProvider; +import io.jans.as.model.error.ErrorResponseFactory; +import io.jans.as.server.model.authorize.ScopeChecker; +import io.jans.as.server.par.ws.rs.ParService; +import io.jans.as.server.service.ClientService; +import io.jans.as.server.service.RedirectUriResponse; +import io.jans.as.server.service.RedirectionUriService; +import io.jans.as.server.service.RequestParameterService; +import jakarta.servlet.http.HttpServletRequest; +import org.apache.commons.lang.StringUtils; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.testng.MockitoTestNGListener; +import org.slf4j.Logger; +import org.testng.annotations.Listeners; +import org.testng.annotations.Test; + +import static org.mockito.Mockito.mock; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertTrue; + +/** + * @author Yuriy Z + */ +@Listeners(MockitoTestNGListener.class) +public class AuthzRequestServiceTest { + + @InjectMocks + private AuthzRequestService authzRequestService; + + @Mock + private Logger log; + + @Mock + private AppConfiguration appConfiguration; + + @Mock + private ErrorResponseFactory errorResponseFactory; + + @Mock + private AuthorizeRestWebServiceValidator authorizeRestWebServiceValidator; + + @Mock + private ParService parService; + + @Mock + private AbstractCryptoProvider cryptoProvider; + + @Mock + private ScopeChecker scopeChecker; + + @Mock + private RequestParameterService requestParameterService; + + @Mock + private WebKeysConfiguration webKeysConfiguration; + + @Mock + private ClientService clientService; + + @Mock + private RedirectionUriService redirectionUriService; + + @Test + public void addDeviceSecretToSession_withoutDeviceSsoScope_shouldNotGenerateDeviceSecret() { + AuthzRequest authzRequest = new AuthzRequest(); + authzRequest.setScope("openid"); + authzRequest.setRedirectUriResponse(new RedirectUriResponse(mock(RedirectUri.class), "", mock(HttpServletRequest.class), mock(ErrorResponseFactory.class))); + + SessionId sessionId = new SessionId(); + + authzRequestService.addDeviceSecretToSession(authzRequest, sessionId); + assertTrue(sessionId.getDeviceSecrets().isEmpty()); + } + + @Test + public void addDeviceSecretToSession_withDeviceSsoScope_shouldGenerateDeviceSecret() { + AuthzRequest authzRequest = new AuthzRequest(); + authzRequest.setRedirectUriResponse(new RedirectUriResponse(mock(RedirectUri.class), "", mock(HttpServletRequest.class), mock(ErrorResponseFactory.class))); + authzRequest.setScope("openid device_sso"); + + SessionId sessionId = new SessionId(); + + assertTrue(sessionId.getDeviceSecrets().isEmpty()); + authzRequestService.addDeviceSecretToSession(authzRequest, sessionId); + assertEquals(1, sessionId.getDeviceSecrets().size()); + assertTrue(StringUtils.isNotBlank(sessionId.getDeviceSecrets().get(0))); + } +} 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": [