diff --git a/jans-auth-server/client/src/main/java/io/jans/as/client/RegisterRequest.java b/jans-auth-server/client/src/main/java/io/jans/as/client/RegisterRequest.java index d6607432078..324f9655f72 100644 --- a/jans-auth-server/client/src/main/java/io/jans/as/client/RegisterRequest.java +++ b/jans-auth-server/client/src/main/java/io/jans/as/client/RegisterRequest.java @@ -11,11 +11,7 @@ import com.google.common.collect.Sets; import io.jans.as.client.model.SoftwareStatement; import io.jans.as.client.util.ClientUtil; -import io.jans.as.model.common.AuthenticationMethod; -import io.jans.as.model.common.BackchannelTokenDeliveryMode; -import io.jans.as.model.common.GrantType; -import io.jans.as.model.common.ResponseType; -import io.jans.as.model.common.SubjectType; +import io.jans.as.model.common.*; import io.jans.as.model.crypto.AuthCryptoProvider; import io.jans.as.model.crypto.encryption.BlockEncryptionAlgorithm; import io.jans.as.model.crypto.encryption.KeyEncryptionAlgorithm; @@ -31,78 +27,10 @@ import org.json.JSONObject; import javax.ws.rs.core.MediaType; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; - -import static io.jans.as.client.util.ClientUtil.booleanOrNull; -import static io.jans.as.client.util.ClientUtil.extractListByKey; -import static io.jans.as.client.util.ClientUtil.integerOrNull; -import static io.jans.as.model.register.RegisterRequestParam.ACCESS_TOKEN_AS_JWT; -import static io.jans.as.model.register.RegisterRequestParam.ACCESS_TOKEN_LIFETIME; -import static io.jans.as.model.register.RegisterRequestParam.ACCESS_TOKEN_SIGNING_ALG; -import static io.jans.as.model.register.RegisterRequestParam.ALLOW_SPONTANEOUS_SCOPES; -import static io.jans.as.model.register.RegisterRequestParam.APPLICATION_TYPE; -import static io.jans.as.model.register.RegisterRequestParam.AUTHORIZATION_ENCRYPTED_RESPONSE_ALG; -import static io.jans.as.model.register.RegisterRequestParam.AUTHORIZATION_ENCRYPTED_RESPONSE_ENC; -import static io.jans.as.model.register.RegisterRequestParam.AUTHORIZATION_SIGNED_RESPONSE_ALG; -import static io.jans.as.model.register.RegisterRequestParam.AUTHORIZED_ORIGINS; -import static io.jans.as.model.register.RegisterRequestParam.BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG; -import static io.jans.as.model.register.RegisterRequestParam.BACKCHANNEL_CLIENT_NOTIFICATION_ENDPOINT; -import static io.jans.as.model.register.RegisterRequestParam.BACKCHANNEL_LOGOUT_SESSION_REQUIRED; -import static io.jans.as.model.register.RegisterRequestParam.BACKCHANNEL_LOGOUT_URI; -import static io.jans.as.model.register.RegisterRequestParam.BACKCHANNEL_TOKEN_DELIVERY_MODE; -import static io.jans.as.model.register.RegisterRequestParam.BACKCHANNEL_USER_CODE_PARAMETER; -import static io.jans.as.model.register.RegisterRequestParam.CLAIMS; -import static io.jans.as.model.register.RegisterRequestParam.CLAIMS_REDIRECT_URIS; -import static io.jans.as.model.register.RegisterRequestParam.CLIENT_NAME; -import static io.jans.as.model.register.RegisterRequestParam.CLIENT_URI; -import static io.jans.as.model.register.RegisterRequestParam.CONTACTS; -import static io.jans.as.model.register.RegisterRequestParam.DEFAULT_ACR_VALUES; -import static io.jans.as.model.register.RegisterRequestParam.DEFAULT_MAX_AGE; -import static io.jans.as.model.register.RegisterRequestParam.FRONT_CHANNEL_LOGOUT_SESSION_REQUIRED; -import static io.jans.as.model.register.RegisterRequestParam.FRONT_CHANNEL_LOGOUT_URI; -import static io.jans.as.model.register.RegisterRequestParam.GRANT_TYPES; -import static io.jans.as.model.register.RegisterRequestParam.ID_TOKEN_ENCRYPTED_RESPONSE_ALG; -import static io.jans.as.model.register.RegisterRequestParam.ID_TOKEN_ENCRYPTED_RESPONSE_ENC; -import static io.jans.as.model.register.RegisterRequestParam.ID_TOKEN_SIGNED_RESPONSE_ALG; -import static io.jans.as.model.register.RegisterRequestParam.ID_TOKEN_TOKEN_BINDING_CNF; -import static io.jans.as.model.register.RegisterRequestParam.INITIATE_LOGIN_URI; -import static io.jans.as.model.register.RegisterRequestParam.JWKS; -import static io.jans.as.model.register.RegisterRequestParam.JWKS_URI; -import static io.jans.as.model.register.RegisterRequestParam.KEEP_CLIENT_AUTHORIZATION_AFTER_EXPIRATION; -import static io.jans.as.model.register.RegisterRequestParam.LOGO_URI; -import static io.jans.as.model.register.RegisterRequestParam.PAR_LIFETIME; -import static io.jans.as.model.register.RegisterRequestParam.POLICY_URI; -import static io.jans.as.model.register.RegisterRequestParam.POST_LOGOUT_REDIRECT_URIS; -import static io.jans.as.model.register.RegisterRequestParam.REDIRECT_URIS; -import static io.jans.as.model.register.RegisterRequestParam.REQUEST_OBJECT_ENCRYPTION_ALG; -import static io.jans.as.model.register.RegisterRequestParam.REQUEST_OBJECT_ENCRYPTION_ENC; -import static io.jans.as.model.register.RegisterRequestParam.REQUEST_OBJECT_SIGNING_ALG; -import static io.jans.as.model.register.RegisterRequestParam.REQUEST_URIS; -import static io.jans.as.model.register.RegisterRequestParam.REQUIRE_AUTH_TIME; -import static io.jans.as.model.register.RegisterRequestParam.REQUIRE_PAR; -import static io.jans.as.model.register.RegisterRequestParam.RESPONSE_TYPES; -import static io.jans.as.model.register.RegisterRequestParam.RPT_AS_JWT; -import static io.jans.as.model.register.RegisterRequestParam.RUN_INTROSPECTION_SCRIPT_BEFORE_ACCESS_TOKEN_CREATION_AS_JWT_AND_INCLUDE_CLAIMS; -import static io.jans.as.model.register.RegisterRequestParam.SCOPE; -import static io.jans.as.model.register.RegisterRequestParam.SECTOR_IDENTIFIER_URI; -import static io.jans.as.model.register.RegisterRequestParam.SOFTWARE_ID; -import static io.jans.as.model.register.RegisterRequestParam.SOFTWARE_STATEMENT; -import static io.jans.as.model.register.RegisterRequestParam.SOFTWARE_VERSION; -import static io.jans.as.model.register.RegisterRequestParam.SPONTANEOUS_SCOPES; -import static io.jans.as.model.register.RegisterRequestParam.SUBJECT_TYPE; -import static io.jans.as.model.register.RegisterRequestParam.TLS_CLIENT_AUTH_SUBJECT_DN; -import static io.jans.as.model.register.RegisterRequestParam.TOKEN_ENDPOINT_AUTH_METHOD; -import static io.jans.as.model.register.RegisterRequestParam.TOKEN_ENDPOINT_AUTH_SIGNING_ALG; -import static io.jans.as.model.register.RegisterRequestParam.TOS_URI; -import static io.jans.as.model.register.RegisterRequestParam.USERINFO_ENCRYPTED_RESPONSE_ALG; -import static io.jans.as.model.register.RegisterRequestParam.USERINFO_ENCRYPTED_RESPONSE_ENC; -import static io.jans.as.model.register.RegisterRequestParam.USERINFO_SIGNED_RESPONSE_ALG; +import java.util.*; + +import static io.jans.as.client.util.ClientUtil.*; +import static io.jans.as.model.register.RegisterRequestParam.*; import static io.jans.as.model.util.StringUtils.implode; import static io.jans.as.model.util.StringUtils.toJSONArray; @@ -111,7 +39,7 @@ * * @author Javier Rojas Blum * @author Yuriy Zabrovarnyy - * @version July 28, 2021 + * @version February 10, 2022 */ public class RegisterRequest extends BaseRequest { @@ -154,6 +82,7 @@ public class RegisterRequest extends BaseRequest { private Boolean runIntrospectionScriptBeforeAccessTokenAsJwtCreationAndIncludeClaims; private Boolean keepClientAuthorizationAfterExpiration; private SubjectType subjectType; + private String subjectIdentifierAttribute; private Boolean rptAsJwt; private Boolean accessTokenAsJwt; private SignatureAlgorithm accessTokenSigningAlg; @@ -697,6 +626,14 @@ public void setSubjectType(SubjectType subjectType) { this.subjectType = subjectType; } + public String getSubjectIdentifierAttribute() { + return subjectIdentifierAttribute; + } + + public void setSubjectIdentifierAttribute(String subjectIdentifierAttribute) { + this.subjectIdentifierAttribute = subjectIdentifierAttribute; + } + public Boolean getRptAsJwt() { return rptAsJwt; } @@ -1305,6 +1242,9 @@ public Map getParameters() { if (subjectType != null) { parameters.put(SUBJECT_TYPE.toString(), subjectType.toString()); } + if (StringUtils.isNotBlank(subjectIdentifierAttribute)) { + parameters.put(PUBLIC_SUBJECT_IDENTIFIER_ATTRIBUTE.getName(), subjectIdentifierAttribute); + } if (rptAsJwt != null) { parameters.put(RPT_AS_JWT.toString(), rptAsJwt.toString()); } @@ -1521,6 +1461,7 @@ public static RegisterRequest fromJson(JSONObject requestObject) throws JSONExce result.setJwks(requestObject.optString(JWKS.toString())); result.setSectorIdentifierUri(requestObject.optString(SECTOR_IDENTIFIER_URI.toString())); result.setSubjectType(SubjectType.fromString(requestObject.optString(SUBJECT_TYPE.toString()))); + result.setSubjectIdentifierAttribute(requestObject.optString(PUBLIC_SUBJECT_IDENTIFIER_ATTRIBUTE.getName())); result.setSoftwareId(requestObject.optString(SOFTWARE_ID.toString())); result.setSoftwareVersion(requestObject.optString(SOFTWARE_VERSION.toString())); result.setSoftwareStatement(requestObject.optString(SOFTWARE_STATEMENT.toString())); @@ -1616,6 +1557,9 @@ public JSONObject getJSONParameters() throws JSONException { if (subjectType != null) { parameters.put(SUBJECT_TYPE.toString(), subjectType.toString()); } + if (StringUtils.isNotBlank(subjectIdentifierAttribute)) { + parameters.put(PUBLIC_SUBJECT_IDENTIFIER_ATTRIBUTE.getName(), subjectIdentifierAttribute); + } if (rptAsJwt != null) { parameters.put(RPT_AS_JWT.toString(), rptAsJwt.toString()); } diff --git a/jans-auth-server/client/src/test/java/io/jans/as/client/client/Asserter.java b/jans-auth-server/client/src/test/java/io/jans/as/client/client/Asserter.java index 56782c70a70..a1982552fd5 100644 --- a/jans-auth-server/client/src/test/java/io/jans/as/client/client/Asserter.java +++ b/jans-auth-server/client/src/test/java/io/jans/as/client/client/Asserter.java @@ -11,6 +11,7 @@ import io.jans.as.client.RegisterResponse; import io.jans.as.client.TokenResponse; import io.jans.as.client.par.ParResponse; +import io.jans.as.model.common.ResponseType; import io.jans.as.model.crypto.signature.RSAPublicKey; import io.jans.as.model.crypto.signature.SignatureAlgorithm; import io.jans.as.model.exception.InvalidJwtException; @@ -24,15 +25,15 @@ import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.UnrecoverableKeyException; +import java.util.List; import static io.jans.as.client.BaseTest.clientEngine; -import static org.testng.Assert.assertEquals; -import static org.testng.Assert.assertNotNull; -import static org.testng.Assert.assertTrue; +import static org.testng.Assert.*; /** * @author Yuriy Zabrovarnyy - * @version 0.9, 25/03/2016 + * @author Javier Rojas Blum + * @version February 11, 2022 */ public class Asserter { @@ -54,6 +55,13 @@ public static void assertOk(RegisterResponse response) { assertNotNull(response.getClientSecretExpiresAt()); } + public static void assertBadRequest(RegisterResponse registerResponse) { + assertEquals(registerResponse.getStatus(), 400, "Unexpected response code"); + assertNotNull(registerResponse.getEntity()); + assertNotNull(registerResponse.getErrorType()); + assertNotNull(registerResponse.getErrorDescription()); + } + public static void assertTokenResponse(TokenResponse response) { assertNotNull(response); assertEquals(response.getStatus(), 200, "Unexpected response code: " + response.getStatus()); @@ -78,6 +86,26 @@ public static void assertAuthorizationResponse(AuthorizationResponse response, b } } + public static void assertAuthorizationResponse(AuthorizationResponse authorizationResponse, List responseTypes, boolean checkState) { + assertNotNull(authorizationResponse); + assertNotNull(authorizationResponse.getLocation(), "The location is null"); + assertNotNull(authorizationResponse.getScope(), "The scope is null"); + if (checkState) { + assertNotNull(authorizationResponse.getState(), "The state is null"); + } + if (responseTypes.contains(ResponseType.CODE)) { + assertNotNull(authorizationResponse.getCode(), "The authorization code is null"); + } + if (responseTypes.contains(ResponseType.TOKEN)) { + assertNotNull(authorizationResponse.getAccessToken(), "The access_token is null"); + assertNotNull(authorizationResponse.getTokenType()); + assertNotNull(authorizationResponse.getExpiresIn()); + } + if (responseTypes.contains(ResponseType.ID_TOKEN)) { + assertNotNull(authorizationResponse.getIdToken(), "The id_token is null"); + } + } + public static void validateIdToken(String idToken, String jwksUri, SignatureAlgorithm alg) throws InvalidJwtException, UnrecoverableKeyException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException { Jwt jwt = Jwt.parse(idToken); Asserter.assertIdToken(jwt, JwtClaimName.CODE_HASH); diff --git a/jans-auth-server/client/src/test/java/io/jans/as/client/ws/rs/SetPublicSubjectIdentifierPerClientTest.java b/jans-auth-server/client/src/test/java/io/jans/as/client/ws/rs/SetPublicSubjectIdentifierPerClientTest.java new file mode 100644 index 00000000000..641d966d97f --- /dev/null +++ b/jans-auth-server/client/src/test/java/io/jans/as/client/ws/rs/SetPublicSubjectIdentifierPerClientTest.java @@ -0,0 +1,139 @@ +/* + * 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.client.ws.rs; + +import io.jans.as.client.*; +import io.jans.as.client.client.Asserter; +import io.jans.as.model.common.ResponseType; +import io.jans.as.model.common.SubjectType; +import io.jans.as.model.crypto.signature.RSAPublicKey; +import io.jans.as.model.crypto.signature.SignatureAlgorithm; +import io.jans.as.model.exception.InvalidJwtException; +import io.jans.as.model.jws.RSASigner; +import io.jans.as.model.jwt.Jwt; +import io.jans.as.model.jwt.JwtClaimName; +import io.jans.as.model.jwt.JwtHeaderName; +import io.jans.as.model.register.ApplicationType; +import io.jans.as.model.util.StringUtils; +import org.jetbrains.annotations.NotNull; +import org.testng.ITestContext; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; + +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import static io.jans.as.model.common.ResponseType.*; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertTrue; + +/** + * @author Javier Rojas Blum + * @version February 11, 2022 + */ +public class SetPublicSubjectIdentifierPerClientTest extends BaseTest { + + @Test(dataProvider = "dataMethod") + public void setPublicSubjectIdentifierPerClient( + final String redirectUri, final List responseTypes, final String userId, final String userSecret, + final String subjectIdentifierAttribute, final String expectedSubValue) throws InvalidJwtException { + showTitle("setPublicSubjectIdentifierPerClient"); + + RegisterResponse registerResponse = clientRegistration(redirectUri, responseTypes, + SubjectType.PUBLIC, subjectIdentifierAttribute, false); + + String clientId = registerResponse.getClientId(); + + List scopes = Arrays.asList("openid", "profile", "address", "email"); + String state = UUID.randomUUID().toString(); + String nonce = UUID.randomUUID().toString(); + + AuthorizationRequest authorizationRequest = new AuthorizationRequest(responseTypes, clientId, scopes, redirectUri, nonce); + authorizationRequest.setState(state); + + AuthorizationResponse authorizationResponse = authenticateResourceOwnerAndGrantAccess( + authorizationEndpoint, authorizationRequest, userId, userSecret); + + Asserter.assertAuthorizationResponse(authorizationResponse, responseTypes, true); + + String idToken = authorizationResponse.getIdToken(); + + Jwt jwt = Jwt.parse(idToken); + RSAPublicKey publicKey = JwkClient.getRSAPublicKey( + jwksUri, + jwt.getHeader().getClaimAsString(JwtHeaderName.KEY_ID)); + RSASigner rsaSigner = new RSASigner(SignatureAlgorithm.RS256, publicKey); + assertTrue(rsaSigner.validate(jwt)); + + assertEquals(jwt.getClaims().getClaimAsString(JwtClaimName.SUBJECT_IDENTIFIER), expectedSubValue); + } + + @Parameters({"redirectUri"}) + @Test + public void setPublicSubjectIdentifierPerClientFail1(final String redirectUri) { + showTitle("setPublicSubjectIdentifierPerClientFail1"); + + List responseTypes = Arrays.asList( + ResponseType.TOKEN, + ResponseType.ID_TOKEN); + + clientRegistration(redirectUri, responseTypes, SubjectType.PAIRWISE, "mail", true); + } + + @Parameters({"redirectUri"}) + @Test + public void setPublicSubjectIdentifierPerClientFail2(final String redirectUri) { + showTitle("setPublicSubjectIdentifierPerClientFail2"); + + List responseTypes = Arrays.asList( + ResponseType.TOKEN, + ResponseType.ID_TOKEN); + + clientRegistration(redirectUri, responseTypes, SubjectType.PUBLIC, "invalid_attribute", true); + } + + @NotNull + private RegisterResponse clientRegistration( + String redirectUri, List responseTypes, SubjectType subjectType, + String subjectIdentifierAttribute, boolean checkError) { + List redirectUriList = Arrays.asList(redirectUri.split(StringUtils.SPACE)); + RegisterRequest registerRequest = new RegisterRequest(ApplicationType.WEB, "jans test app", redirectUriList); + registerRequest.setSubjectType(subjectType); + registerRequest.setResponseTypes(responseTypes); + registerRequest.setSubjectIdentifierAttribute(subjectIdentifierAttribute); + + RegisterClient registerClient = new RegisterClient(registrationEndpoint); + registerClient.setRequest(registerRequest); + RegisterResponse registerResponse = registerClient.exec(); + + showClient(registerClient); + + if (checkError) { + Asserter.assertBadRequest(registerResponse); + } else { + Asserter.assertOk(registerResponse); + } + + return registerResponse; + } + + @DataProvider(name = "dataMethod") + public Object[] dataMethod(ITestContext context) { + final String redirectUri = context.getCurrentXmlTest().getParameter("redirectUri"); + final String userId = context.getCurrentXmlTest().getParameter("userId"); + final String userSecret = context.getCurrentXmlTest().getParameter("userSecret"); + + return new Object[][]{ + {redirectUri, Arrays.asList(TOKEN, ID_TOKEN), userId, userSecret, "mail", "test_user@test.org"}, + {redirectUri, Arrays.asList(TOKEN, ID_TOKEN), userId, userSecret, "uid", "test_user"}, + {redirectUri, Arrays.asList(CODE, TOKEN, ID_TOKEN), userId, userSecret, "mail", "test_user@test.org"}, + {redirectUri, Arrays.asList(CODE, TOKEN, ID_TOKEN), userId, userSecret, "uid", "test_user"} + }; + } +} \ No newline at end of file diff --git a/jans-auth-server/client/src/test/resources/testng.xml b/jans-auth-server/client/src/test/resources/testng.xml index 0e07cb5abeb..28010605418 100644 --- a/jans-auth-server/client/src/test/resources/testng.xml +++ b/jans-auth-server/client/src/test/resources/testng.xml @@ -92,7 +92,7 @@ - + @@ -238,6 +238,12 @@ + + + + + + @@ -282,7 +288,7 @@ - + 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 a9f1411430c..ba14261b107 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 @@ -8,12 +8,7 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.google.common.collect.Lists; -import io.jans.as.model.common.ComponentType; -import io.jans.as.model.common.GrantType; -import io.jans.as.model.common.ResponseMode; -import io.jans.as.model.common.ResponseType; -import io.jans.as.model.common.SoftwareStatementValidationType; -import io.jans.as.model.common.WebKeyStorage; +import io.jans.as.model.common.*; import io.jans.as.model.error.ErrorHandlingMethod; import io.jans.as.model.jwk.KeySelectionStrategy; @@ -28,7 +23,7 @@ * @author Javier Rojas Blum * @author Yuriy Zabrovarnyy * @author Yuriy Movchan - * @version February 2, 2022 + * @version February 10, 2022 */ @JsonIgnoreProperties(ignoreUnknown = true) public class AppConfiguration implements Configuration { @@ -94,6 +89,8 @@ public class AppConfiguration implements Configuration { private int spontaneousScopeLifetime; private String openidSubAttribute; + private Boolean publicSubjectIdentifierPerClientEnabled = false; + private List subjectIdentifiersPerClientSupported; private Set> responseTypesSupported; private Set responseModesSupported; private Set grantTypesSupported; @@ -1028,6 +1025,30 @@ public void setOpenidSubAttribute(String openidSubAttribute) { this.openidSubAttribute = openidSubAttribute; } + public Boolean getPublicSubjectIdentifierPerClientEnabled() { + if (publicSubjectIdentifierPerClientEnabled == null) { + publicSubjectIdentifierPerClientEnabled = false; + } + + return publicSubjectIdentifierPerClientEnabled; + } + + public void setPublicSubjectIdentifierPerClientEnabled(Boolean publicSubjectIdentifierPerClientEnabled) { + this.publicSubjectIdentifierPerClientEnabled = publicSubjectIdentifierPerClientEnabled; + } + + public List getSubjectIdentifiersPerClientSupported() { + if (subjectIdentifiersPerClientSupported == null) { + subjectIdentifiersPerClientSupported = new ArrayList<>(); + } + + return subjectIdentifiersPerClientSupported; + } + + public void setSubjectIdentifiersPerClientSupported(List subjectIdentifiersPerClientSupported) { + this.subjectIdentifiersPerClientSupported = subjectIdentifiersPerClientSupported; + } + public String getIdGenerationEndpoint() { return idGenerationEndpoint; } diff --git a/jans-auth-server/model/src/main/java/io/jans/as/model/register/RegisterErrorResponseType.java b/jans-auth-server/model/src/main/java/io/jans/as/model/register/RegisterErrorResponseType.java index c29dcf90fcc..59dbfe27137 100644 --- a/jans-auth-server/model/src/main/java/io/jans/as/model/register/RegisterErrorResponseType.java +++ b/jans-auth-server/model/src/main/java/io/jans/as/model/register/RegisterErrorResponseType.java @@ -12,7 +12,7 @@ * Error codes for register error responses. * * @author Javier Rojas Blum - * @version 0.9 May 18, 2015 + * @version February 10, 2022 */ public enum RegisterErrorResponseType implements IErrorType { @@ -50,7 +50,9 @@ public enum RegisterErrorResponseType implements IErrorType { /** * The authorization server denied the request. */ - ACCESS_DENIED("access_denied"); + ACCESS_DENIED("access_denied"), + + INVALID_PUBLIC_SUBJECT_IDENTIFIER_ATTRIBUTE("invalid_public_subject_identifier_attribute"); private final String paramName; diff --git a/jans-auth-server/model/src/main/java/io/jans/as/model/register/RegisterRequestParam.java b/jans-auth-server/model/src/main/java/io/jans/as/model/register/RegisterRequestParam.java index e984bb1e765..5b594536451 100644 --- a/jans-auth-server/model/src/main/java/io/jans/as/model/register/RegisterRequestParam.java +++ b/jans-auth-server/model/src/main/java/io/jans/as/model/register/RegisterRequestParam.java @@ -13,7 +13,7 @@ * * @author Yuriy Zabrovarnyy * @author Javier Rojas Blum - * @version July 28, 2021 + * @version February 10, 2022 */ public enum RegisterRequestParam { @@ -345,7 +345,9 @@ public enum RegisterRequestParam { BACKCHANNEL_AUTHENTICATION_REQUEST_SIGNING_ALG("backchannel_authentication_request_signing_alg"), - BACKCHANNEL_USER_CODE_PARAMETER("backchannel_user_code_parameter"); + BACKCHANNEL_USER_CODE_PARAMETER("backchannel_user_code_parameter"), + + PUBLIC_SUBJECT_IDENTIFIER_ATTRIBUTE("public_subject_identifier_attribute"); /** * Parameter name diff --git a/jans-auth-server/persistence-model/src/main/java/io/jans/as/persistence/model/ClientAttributes.java b/jans-auth-server/persistence-model/src/main/java/io/jans/as/persistence/model/ClientAttributes.java index 66b8a62eacc..73c3d3fa1d6 100644 --- a/jans-auth-server/persistence-model/src/main/java/io/jans/as/persistence/model/ClientAttributes.java +++ b/jans-auth-server/persistence-model/src/main/java/io/jans/as/persistence/model/ClientAttributes.java @@ -16,6 +16,8 @@ /** * @author Yuriy Zabrovarnyy + * @author Javier Rojas Blum + * @version February 10, 2022 */ @JsonIgnoreProperties(ignoreUnknown = true) public class ClientAttributes implements Serializable { @@ -87,6 +89,9 @@ public class ClientAttributes implements Serializable { @JsonProperty("jansAuthEncRespEnc") private String authorizationEncryptedResponseEnc; + @JsonProperty("jansSubAttr") + private String publicSubjectIdentifierAttribute; + public List getRopcScripts() { if (ropcScripts == null) ropcScripts = new ArrayList<>(); return ropcScripts; @@ -268,6 +273,26 @@ public void setAuthorizationEncryptedResponseEnc(String authorizationEncryptedRe this.authorizationEncryptedResponseEnc = authorizationEncryptedResponseEnc; } + /** + * Return the custom subject identifier attribute. It is used for public subject type. + * If null, the default is used. Else use the custom attribute per client basis. + * + * @return The custom subject identifier attribute. + */ + public String getPublicSubjectIdentifierAttribute() { + return publicSubjectIdentifierAttribute; + } + + /** + * Sets the custom subject identifier attribute. It is used for public subject type. + * if null, the default is used. Else use the custom attribute per client basis. + * + * @param publicSubjectIdentifierAttribute The custom subject identifier attribute. + */ + public void setPublicSubjectIdentifierAttribute(String publicSubjectIdentifierAttribute) { + this.publicSubjectIdentifierAttribute = publicSubjectIdentifierAttribute; + } + @Override public String toString() { return "ClientAttributes{" + @@ -288,6 +313,7 @@ public String toString() { ", authorizationSignedResponseAlg=" + authorizationSignedResponseAlg + ", authorizationEncryptedResponseAlg=" + authorizationEncryptedResponseAlg + ", authorizationEncryptedResponseEnc=" + authorizationEncryptedResponseEnc + + ", publicSubjectIdentifierAttribute=" + publicSubjectIdentifierAttribute + '}'; } } diff --git a/jans-auth-server/server/conf/jans-config.json b/jans-auth-server/server/conf/jans-config.json index 64b04744c52..67a8e2ef864 100644 --- a/jans-auth-server/server/conf/jans-config.json +++ b/jans-auth-server/server/conf/jans-config.json @@ -27,6 +27,11 @@ "parEndpoint":"${config.oxauth.contextPath}/restv1/par", "backchannelDeviceRegistrationEndpoint":"${config.oxauth.contextPath}/restv1/bc-deviceRegistration", "openidSubAttribute":"inum", + "publicSubjectIdentifierPerClientEnabled": true, + "subjectIdentifiersPerClientSupported": [ + "mail", + "uid" + ], "responseTypesSupported":[ ["code"], ["code", "id_token"], diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/register/ws/rs/RegisterRestWebServiceImpl.java b/jans-auth-server/server/src/main/java/io/jans/as/server/register/ws/rs/RegisterRestWebServiceImpl.java index 86ed1bff2c1..4a60bebb883 100644 --- a/jans-auth-server/server/src/main/java/io/jans/as/server/register/ws/rs/RegisterRestWebServiceImpl.java +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/register/ws/rs/RegisterRestWebServiceImpl.java @@ -12,12 +12,7 @@ import io.jans.as.common.model.registration.Client; import io.jans.as.common.service.AttributeService; import io.jans.as.common.service.common.InumService; -import io.jans.as.model.common.AuthenticationMethod; -import io.jans.as.model.common.ComponentType; -import io.jans.as.model.common.GrantType; -import io.jans.as.model.common.ResponseType; -import io.jans.as.model.common.SoftwareStatementValidationType; -import io.jans.as.model.common.SubjectType; +import io.jans.as.model.common.*; import io.jans.as.model.config.Constants; import io.jans.as.model.config.StaticConfiguration; import io.jans.as.model.configuration.AppConfiguration; @@ -74,77 +69,10 @@ import javax.ws.rs.core.Response; import javax.ws.rs.core.SecurityContext; import java.net.URI; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Calendar; -import java.util.Date; -import java.util.GregorianCalendar; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.TimeZone; -import java.util.UUID; - -import static io.jans.as.model.register.RegisterRequestParam.ACCESS_TOKEN_AS_JWT; -import static io.jans.as.model.register.RegisterRequestParam.ACCESS_TOKEN_LIFETIME; -import static io.jans.as.model.register.RegisterRequestParam.ACCESS_TOKEN_SIGNING_ALG; -import static io.jans.as.model.register.RegisterRequestParam.ALLOW_SPONTANEOUS_SCOPES; -import static io.jans.as.model.register.RegisterRequestParam.APPLICATION_TYPE; -import static io.jans.as.model.register.RegisterRequestParam.AUTHORIZATION_ENCRYPTED_RESPONSE_ALG; -import static io.jans.as.model.register.RegisterRequestParam.AUTHORIZATION_ENCRYPTED_RESPONSE_ENC; -import static io.jans.as.model.register.RegisterRequestParam.AUTHORIZATION_SIGNED_RESPONSE_ALG; -import static io.jans.as.model.register.RegisterRequestParam.AUTHORIZED_ORIGINS; -import static io.jans.as.model.register.RegisterRequestParam.BACKCHANNEL_LOGOUT_SESSION_REQUIRED; -import static io.jans.as.model.register.RegisterRequestParam.BACKCHANNEL_LOGOUT_URI; -import static io.jans.as.model.register.RegisterRequestParam.CLAIMS; -import static io.jans.as.model.register.RegisterRequestParam.CLAIMS_REDIRECT_URIS; -import static io.jans.as.model.register.RegisterRequestParam.CLIENT_NAME; -import static io.jans.as.model.register.RegisterRequestParam.CLIENT_URI; -import static io.jans.as.model.register.RegisterRequestParam.CONTACTS; -import static io.jans.as.model.register.RegisterRequestParam.DEFAULT_ACR_VALUES; -import static io.jans.as.model.register.RegisterRequestParam.DEFAULT_MAX_AGE; -import static io.jans.as.model.register.RegisterRequestParam.FRONT_CHANNEL_LOGOUT_SESSION_REQUIRED; -import static io.jans.as.model.register.RegisterRequestParam.FRONT_CHANNEL_LOGOUT_URI; -import static io.jans.as.model.register.RegisterRequestParam.GRANT_TYPES; -import static io.jans.as.model.register.RegisterRequestParam.ID_TOKEN_ENCRYPTED_RESPONSE_ALG; -import static io.jans.as.model.register.RegisterRequestParam.ID_TOKEN_ENCRYPTED_RESPONSE_ENC; -import static io.jans.as.model.register.RegisterRequestParam.ID_TOKEN_SIGNED_RESPONSE_ALG; -import static io.jans.as.model.register.RegisterRequestParam.INITIATE_LOGIN_URI; -import static io.jans.as.model.register.RegisterRequestParam.JWKS; -import static io.jans.as.model.register.RegisterRequestParam.JWKS_URI; -import static io.jans.as.model.register.RegisterRequestParam.KEEP_CLIENT_AUTHORIZATION_AFTER_EXPIRATION; -import static io.jans.as.model.register.RegisterRequestParam.LOGO_URI; -import static io.jans.as.model.register.RegisterRequestParam.PAR_LIFETIME; -import static io.jans.as.model.register.RegisterRequestParam.POLICY_URI; -import static io.jans.as.model.register.RegisterRequestParam.POST_LOGOUT_REDIRECT_URIS; -import static io.jans.as.model.register.RegisterRequestParam.REDIRECT_URIS; -import static io.jans.as.model.register.RegisterRequestParam.REQUEST_OBJECT_ENCRYPTION_ALG; -import static io.jans.as.model.register.RegisterRequestParam.REQUEST_OBJECT_ENCRYPTION_ENC; -import static io.jans.as.model.register.RegisterRequestParam.REQUEST_OBJECT_SIGNING_ALG; -import static io.jans.as.model.register.RegisterRequestParam.REQUEST_URIS; -import static io.jans.as.model.register.RegisterRequestParam.REQUIRE_AUTH_TIME; -import static io.jans.as.model.register.RegisterRequestParam.REQUIRE_PAR; -import static io.jans.as.model.register.RegisterRequestParam.RESPONSE_TYPES; -import static io.jans.as.model.register.RegisterRequestParam.RPT_AS_JWT; -import static io.jans.as.model.register.RegisterRequestParam.RUN_INTROSPECTION_SCRIPT_BEFORE_ACCESS_TOKEN_CREATION_AS_JWT_AND_INCLUDE_CLAIMS; -import static io.jans.as.model.register.RegisterRequestParam.SCOPE; -import static io.jans.as.model.register.RegisterRequestParam.SECTOR_IDENTIFIER_URI; -import static io.jans.as.model.register.RegisterRequestParam.SOFTWARE_ID; -import static io.jans.as.model.register.RegisterRequestParam.SOFTWARE_STATEMENT; -import static io.jans.as.model.register.RegisterRequestParam.SOFTWARE_VERSION; -import static io.jans.as.model.register.RegisterRequestParam.SPONTANEOUS_SCOPES; -import static io.jans.as.model.register.RegisterRequestParam.SUBJECT_TYPE; -import static io.jans.as.model.register.RegisterRequestParam.TLS_CLIENT_AUTH_SUBJECT_DN; -import static io.jans.as.model.register.RegisterRequestParam.TOKEN_ENDPOINT_AUTH_METHOD; -import static io.jans.as.model.register.RegisterRequestParam.TOKEN_ENDPOINT_AUTH_SIGNING_ALG; -import static io.jans.as.model.register.RegisterRequestParam.TOS_URI; -import static io.jans.as.model.register.RegisterRequestParam.USERINFO_ENCRYPTED_RESPONSE_ALG; -import static io.jans.as.model.register.RegisterRequestParam.USERINFO_ENCRYPTED_RESPONSE_ENC; -import static io.jans.as.model.register.RegisterRequestParam.USERINFO_SIGNED_RESPONSE_ALG; -import static io.jans.as.model.register.RegisterResponseParam.CLIENT_ID_ISSUED_AT; -import static io.jans.as.model.register.RegisterResponseParam.CLIENT_SECRET; -import static io.jans.as.model.register.RegisterResponseParam.CLIENT_SECRET_EXPIRES_AT; -import static io.jans.as.model.register.RegisterResponseParam.REGISTRATION_CLIENT_URI; +import java.util.*; + +import static io.jans.as.model.register.RegisterRequestParam.*; +import static io.jans.as.model.register.RegisterResponseParam.*; import static io.jans.as.model.util.StringUtils.implode; import static io.jans.as.model.util.StringUtils.toList; import static org.apache.commons.lang3.BooleanUtils.isFalse; @@ -156,7 +84,7 @@ * @author Javier Rojas Blum * @author Yuriy Zabrovarnyy * @author Yuriy Movchan - * @version September 9, 2021 + * @version February 11, 2022 */ @Path("/") public class RegisterRestWebServiceImpl implements RegisterRestWebService { @@ -311,6 +239,8 @@ private Response registerClientImpl(String requestParams, HttpServletRequest htt throw errorResponseFactory.createWebApplicationException(Response.Status.BAD_REQUEST, RegisterErrorResponseType.INVALID_REDIRECT_URI, "Failed to validate redirect uris."); } + validateSubjectIdentifierAttribute(r); + if (!cibaRegisterParamsValidatorService.validateParams( r.getBackchannelTokenDeliveryMode(), r.getBackchannelClientNotificationEndpoint(), @@ -408,6 +338,34 @@ private Response registerClientImpl(String requestParams, HttpServletRequest htt return builder.build(); } + private void validateSubjectIdentifierAttribute(RegisterRequest registerRequest) { + if (StringUtils.isNotBlank(registerRequest.getSubjectIdentifierAttribute())) { + if (Boolean.FALSE.equals(appConfiguration.getPublicSubjectIdentifierPerClientEnabled())) { + throw errorResponseFactory.createWebApplicationException( + Response.Status.BAD_REQUEST, + RegisterErrorResponseType.INVALID_PUBLIC_SUBJECT_IDENTIFIER_ATTRIBUTE, + "The public subject identifier per client is disabled." + ); + } + + if (registerRequest.getSubjectType() != SubjectType.PUBLIC) { + throw errorResponseFactory.createWebApplicationException( + Response.Status.BAD_REQUEST, + RegisterErrorResponseType.INVALID_PUBLIC_SUBJECT_IDENTIFIER_ATTRIBUTE, + "The custom subject identifier requires public subject type." + ); + } + + if (!appConfiguration.getSubjectIdentifiersPerClientSupported().contains(registerRequest.getSubjectIdentifierAttribute())) { + throw errorResponseFactory.createWebApplicationException( + Response.Status.BAD_REQUEST, + RegisterErrorResponseType.INVALID_PUBLIC_SUBJECT_IDENTIFIER_ATTRIBUTE, + "Invalid subject identifier attribute." + ); + } + } + } + private void setClientName(RegisterRequest r, Client client) { if (StringUtils.isBlank(r.getClientName()) && r.getRedirectUris() != null && !r.getRedirectUris().isEmpty()) { try { @@ -874,6 +832,10 @@ private void updateClientFromRequestObject(Client client, RegisterRequest reques client.setSoftwareStatement(requestObject.getSoftwareStatement()); } + if (StringUtils.isNotBlank(requestObject.getSubjectIdentifierAttribute())) { + client.getAttributes().setPublicSubjectIdentifierAttribute(requestObject.getSubjectIdentifierAttribute()); + } + cibaRegisterClientMetadataService.updateClient(client, requestObject.getBackchannelTokenDeliveryMode(), requestObject.getBackchannelClientNotificationEndpoint(), requestObject.getBackchannelAuthenticationRequestSigningAlg(), requestObject.getBackchannelUserCodeParameter()); @@ -1159,6 +1121,7 @@ private JSONObject getJSONObject(Client client) throws JSONException, StringEncr Util.addToJSONObjectIfNotNull(responseJsonObject, SOFTWARE_ID.toString(), client.getSoftwareId()); Util.addToJSONObjectIfNotNull(responseJsonObject, SOFTWARE_VERSION.toString(), client.getSoftwareVersion()); Util.addToJSONObjectIfNotNull(responseJsonObject, SOFTWARE_STATEMENT.toString(), client.getSoftwareStatement()); + Util.addToJSONObjectIfNotNull(responseJsonObject, PUBLIC_SUBJECT_IDENTIFIER_ATTRIBUTE.getName(), client.getAttributes().getPublicSubjectIdentifierAttribute()); if (!Util.isNullOrEmpty(client.getJwks())) { Util.addToJSONObjectIfNotNull(responseJsonObject, JWKS.toString(), new JSONObject(client.getJwks())); @@ -1350,5 +1313,4 @@ private JSONObject modifyReadScript(JSONObject jsonObject, ExecutionContext exec } return jsonObject; } - } \ No newline at end of file diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/service/SectorIdentifierService.java b/jans-auth-server/server/src/main/java/io/jans/as/server/service/SectorIdentifierService.java index cb13602ff87..bb70234b667 100644 --- a/jans-auth-server/server/src/main/java/io/jans/as/server/service/SectorIdentifierService.java +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/service/SectorIdentifierService.java @@ -29,7 +29,7 @@ /** * @author Javier Rojas Blum - * @version April 10, 2020 + * @version February 11, 2022 */ @Stateless @Named @@ -151,6 +151,10 @@ public String getSub(Client client, User user, boolean isCibaGrant) { } String openidSubAttribute = appConfiguration.getOpenidSubAttribute(); + if (Boolean.TRUE.equals(appConfiguration.getPublicSubjectIdentifierPerClientEnabled()) + && StringUtils.isNotBlank(client.getAttributes().getPublicSubjectIdentifierAttribute())) { + openidSubAttribute = client.getAttributes().getPublicSubjectIdentifierAttribute(); + } if (StringHelper.equalsIgnoreCase(openidSubAttribute, "uid")) { return user.getUserId(); } diff --git a/jans-linux-setup/openbanking/templates/jans-auth/jans-auth-config.json b/jans-linux-setup/openbanking/templates/jans-auth/jans-auth-config.json index 18e6720029f..e2732190cf9 100644 --- a/jans-linux-setup/openbanking/templates/jans-auth/jans-auth-config.json +++ b/jans-linux-setup/openbanking/templates/jans-auth/jans-auth-config.json @@ -26,6 +26,11 @@ "backchannelDeviceRegistrationEndpoint":"https://%(hostname)s/jans-auth/restv1/bc-deviceRegistration", "deviceAuthzEndpoint":"https://%(hostname)s/jans-auth/restv1/device_authorization", "openidSubAttribute":"inum", + "publicSubjectIdentifierPerClientEnabled": false, + "subjectIdentifiersPerClientSupported": [ + "mail", + "uid" + ], "discoveryAllowedKeys": [ "issuer", "authorization_endpoint", diff --git a/jans-linux-setup/templates/jans-auth/jans-auth-config.json b/jans-linux-setup/templates/jans-auth/jans-auth-config.json index 061fda73325..12ce3c198df 100644 --- a/jans-linux-setup/templates/jans-auth/jans-auth-config.json +++ b/jans-linux-setup/templates/jans-auth/jans-auth-config.json @@ -26,6 +26,11 @@ "backchannelDeviceRegistrationEndpoint":"https://%(hostname)s/jans-auth/restv1/bc-deviceRegistration", "deviceAuthzEndpoint":"https://%(hostname)s/jans-auth/restv1/device_authorization", "openidSubAttribute":"inum", + "publicSubjectIdentifierPerClientEnabled": true, + "subjectIdentifiersPerClientSupported": [ + "mail", + "uid" + ], "responseTypesSupported":[ ["code"], ["code", "id_token"],