Skip to content

Commit

Permalink
feat(jans-auth-server): set public subject identifier per client (#800)
Browse files Browse the repository at this point in the history
* feat(jans-auth-server): set public subject identifier per client

* feat(jans-auth-server): set public subject identifier per client

* feat(jans-auth-server): set public subject identifier per client

* feat(jans-auth-server): set public subject identifier per client
  • Loading branch information
qbert2k authored Feb 14, 2022
1 parent d1b427e commit c303bbc
Show file tree
Hide file tree
Showing 13 changed files with 324 additions and 175 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

Expand All @@ -111,7 +39,7 @@
*
* @author Javier Rojas Blum
* @author Yuriy Zabrovarnyy
* @version July 28, 2021
* @version February 10, 2022
*/
public class RegisterRequest extends BaseRequest {

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -1305,6 +1242,9 @@ public Map<String, String> 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());
}
Expand Down Expand Up @@ -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()));
Expand Down Expand Up @@ -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());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 {
Expand All @@ -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());
Expand All @@ -78,6 +86,26 @@ public static void assertAuthorizationResponse(AuthorizationResponse response, b
}
}

public static void assertAuthorizationResponse(AuthorizationResponse authorizationResponse, List<ResponseType> 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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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<ResponseType> 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<String> 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<ResponseType> 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<ResponseType> responseTypes = Arrays.asList(
ResponseType.TOKEN,
ResponseType.ID_TOKEN);

clientRegistration(redirectUri, responseTypes, SubjectType.PUBLIC, "invalid_attribute", true);
}

@NotNull
private RegisterResponse clientRegistration(
String redirectUri, List<ResponseType> responseTypes, SubjectType subjectType,
String subjectIdentifierAttribute, boolean checkError) {
List<String> 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"}
};
}
}
10 changes: 8 additions & 2 deletions jans-auth-server/client/src/test/resources/testng.xml
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@

<test name="Authorization Support Custom Params" enabled="true">
<classes>
<class name="io.jans.as.client.ws.rs.AuthorizationSupportCustomParams"></class>
<class name="io.jans.as.client.ws.rs.AuthorizationSupportCustomParams"/>
</classes>
</test>

Expand Down Expand Up @@ -238,6 +238,12 @@
</classes>
</test>

<test name="set Public Subject Identifier per Client" enabled="true">
<classes>
<class name="io.jans.as.client.ws.rs.SetPublicSubjectIdentifierPerClientTest"/>
</classes>
</test>

<!-- SSO with Multiple Backend Services test -->
<test name="SSO with Multiple Backend Services test (HTTP)" enabled="true">
<classes>
Expand Down Expand Up @@ -282,7 +288,7 @@

<test name="Turn off consent for pairwise / openid-only scope" enabled="true">
<classes>
<class name="io.jans.as.client.ws.rs.TurnOffConsentForPairwiseOpenIdOnlyConsentTest"></class>
<class name="io.jans.as.client.ws.rs.TurnOffConsentForPairwiseOpenIdOnlyConsentTest"/>
</classes>
</test>

Expand Down
Loading

0 comments on commit c303bbc

Please sign in to comment.