[java-idp-plugin-oidc-rp] branch main updated: Improvements to the flow tests
Phil Smart
philip.smart at jisc.ac.uk
Fri Jun 10 16:18:46 UTC 2022
This is an automated email from the git hooks/post-receive script.
philsmart pushed a commit to branch main
in repository java-idp-plugin-oidc-rp.
View the commit online:
http://git.shibboleth.net/view/?p=java-idp-plugin-oidc-rp.git;a=commit;h=a47fe2a54eedd381ccc1347f8a633a44eecaaffc
The following commit(s) were added to refs/heads/main by this push:
new a47fe2a Improvements to the flow tests
a47fe2a is described below
commit a47fe2a54eedd381ccc1347f8a633a44eecaaffc
Author: Phil Smart <philip.smart at jisc.ac.uk>
AuthorDate: Fri Jun 10 17:18:37 2022 +0100
Improvements to the flow tests
---
.../authn/oidc/rp/impl/ValidateTokenClaims.java | 2 +-
....java => ValidateUserInfoJSONObjectClaims.java} | 11 +-
.../oidc-relying-party-authn-beans.xml | 4 +-
.../oidc-relying-party-authn-flow.xml | 2 +-
.../plugin/authn/oidc/rp/impl/OIDCRPFlowTest.java | 634 ++++++++-------------
.../plugin/authn/oidc/rp/impl/TestTokenHelper.java | 327 +++++++++++
6 files changed, 559 insertions(+), 421 deletions(-)
diff --git a/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/ValidateTokenClaims.java b/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/ValidateTokenClaims.java
index 94b7ad9..e2e357b 100644
--- a/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/ValidateTokenClaims.java
+++ b/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/ValidateTokenClaims.java
@@ -83,7 +83,7 @@ public class ValidateTokenClaims extends AbstractOIDCAuthenticationAction {
/** A cleanup hook to execute after either a successful or unsuccessful claims validation. */
@Nullable private Consumer<ProfileRequestContext> cleanupHook;
-
+
/** The parsed claimset. */
@Nullable private JWTClaimsSet claimsSet;
diff --git a/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/ValidateUserInfoClaims.java b/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/ValidateUserInfoJSONObjectClaims.java
similarity index 94%
rename from idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/ValidateUserInfoClaims.java
rename to idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/ValidateUserInfoJSONObjectClaims.java
index af84011..889cb59 100644
--- a/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/ValidateUserInfoClaims.java
+++ b/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/ValidateUserInfoJSONObjectClaims.java
@@ -33,6 +33,7 @@ import org.slf4j.LoggerFactory;
import com.nimbusds.jwt.JWTClaimsSet;
+import net.shibboleth.idp.authn.AbstractAuthenticationAction;
import net.shibboleth.idp.authn.context.AuthenticationContext;
import net.shibboleth.idp.plugin.authn.oidc.rp.context.AccessTokenResponseContext;
import net.shibboleth.idp.plugin.authn.oidc.rp.context.UserInfoResponseContext;
@@ -42,12 +43,12 @@ import net.shibboleth.utilities.java.support.component.ComponentSupport;
import net.shibboleth.utilities.java.support.logic.Constraint;
/**
- * Validate a successful UserInfo Response according to section 5.3.2 of OpenID Connect Core 1.0.
+ * Validate a successful UserInfo JSON Object Response according to section 5.3.2 of OpenID Connect Core 1.0.
*/
-public class ValidateUserInfoClaims extends AbstractOIDCAuthenticationAction {
+public class ValidateUserInfoJSONObjectClaims extends AbstractAuthenticationAction {
/** Class logger.*/
- @Nonnull private final Logger log = LoggerFactory.getLogger(ValidateUserInfoClaims.class);
+ @Nonnull private final Logger log = LoggerFactory.getLogger(ValidateUserInfoJSONObjectClaims.class);
/** Strategy used to look up the {@link UserInfoResponseContext}. */
@Nonnull private Function<ProfileRequestContext, UserInfoResponseContext>
@@ -64,7 +65,7 @@ public class ValidateUserInfoClaims extends AbstractOIDCAuthenticationAction {
@Nullable private JWTClaimsSet idTokenClaims;
/** Constructor.*/
- public ValidateUserInfoClaims() {
+ public ValidateUserInfoJSONObjectClaims() {
userInfoResponseContextLookupStrategy =
new ChildContextLookup<>(UserInfoResponseContext.class).compose(
new InboundMessageContextLookup());
@@ -142,7 +143,7 @@ public class ValidateUserInfoClaims extends AbstractOIDCAuthenticationAction {
@Override protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
@Nonnull final AuthenticationContext authenticationContext) {
- log.trace("{} Validating UserInfo claims", getLogPrefix());
+ log.trace("{} Validating UserInfo JSON Object claims", getLogPrefix());
final UserInfoResponse response = userInfoCtx.getUserInfo();
if (!response.isClaimsSetAvailable()) {
diff --git a/idp-oidc-rp-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/OIDCRelyingParty/oidc-relying-party-authn-beans.xml b/idp-oidc-rp-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/OIDCRelyingParty/oidc-relying-party-authn-beans.xml
index 26d175a..9b7da49 100644
--- a/idp-oidc-rp-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/OIDCRelyingParty/oidc-relying-party-authn-beans.xml
+++ b/idp-oidc-rp-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/OIDCRelyingParty/oidc-relying-party-authn-beans.xml
@@ -630,9 +630,9 @@
<!-- UserInfo Decryption and Signature Validation Done -->
- <!-- TODO turn this into a ClaimsValidator actions -->
+ <!-- This is a very simplified and hard coded version of the claims verification used for a JWT. Maybe look to replace -->
<bean id="ValidateUserInfoPlainResponseClaims" parent="NestedWebFlowProfileActionAdaptor" scope="prototype"
- class="net.shibboleth.idp.plugin.authn.oidc.rp.impl.ValidateUserInfoClaims"
+ class="net.shibboleth.idp.plugin.authn.oidc.rp.impl.ValidateUserInfoJSONObjectClaims"
p:authenticationContextLookupStrategy-ref="ParentAuthenticiationContextLookup" />
diff --git a/idp-oidc-rp-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/OIDCRelyingParty/oidc-relying-party-authn-flow.xml b/idp-oidc-rp-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/OIDCRelyingParty/oidc-relying-party-authn-flow.xml
index 807aa84..8c6c8e9 100644
--- a/idp-oidc-rp-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/OIDCRelyingParty/oidc-relying-party-authn-flow.xml
+++ b/idp-oidc-rp-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/OIDCRelyingParty/oidc-relying-party-authn-flow.xml
@@ -134,7 +134,7 @@
<!-- A plain JWT will skip token validation and go straight to claims validation -->
<decision-state id="CheckUserInfoResponseType"> <if
test="CheckUserInfoPlainResponseTypeCondition.test(opensamlProfileRequestContext.getSubcontext('net.shibboleth.idp.authn.context.AuthenticationContext').getSubcontext('org.opensaml.profile.context.ProfileRequestContext'))"
- then="ValidateUserInfoClaimsSet" else="ValidateUserInfoJWT"/>
+ then="ValidateUserInfoPlaimClaimsSet" else="ValidateUserInfoJWT"/>
</decision-state>
<!-- Actions to perform if the UserInfo response is a JWT type -->
diff --git a/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/OIDCRPFlowTest.java b/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/OIDCRPFlowTest.java
index 525aae7..fc89950 100644
--- a/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/OIDCRPFlowTest.java
+++ b/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/OIDCRPFlowTest.java
@@ -217,6 +217,23 @@ public class OIDCRPFlowTest extends AbstractAuthnXmlFlowExecutionTests {
+ " \"nickname\": \"Dee\",\n"
+ " \"family_name\": \"User\"\n"
+ "}";
+
+ /** Mock JSON Object response from the UserInfo endpoint.*/
+ @Nonnull @NotEmpty
+ protected final String USERINFO_RESPONSE_NO_SUB ="{\n"
+ + " \"website\": \"https://openid.net/\",\n"
+ + " \"zoneinfo\": \"America/Los_Angeles\",\n"
+ + " \"birthdate\": \"2000-02-03\",\n"
+ + " \"gender\": \"female\",\n"
+ + " \"preferred_username\": \"d.tu\",\n"
+ + " \"given_name\": \"Demo\",\n"
+ + " \"middle_name\": \"Theresa\",\n"
+ + " \"locale\": \"en-US\",\n"
+ + " \"updated_at\": 1580000000,\n"
+ + " \"name\": \"Demo T. User\",\n"
+ + " \"nickname\": \"Dee\",\n"
+ + " \"family_name\": \"User\"\n"
+ + "}";
/** Path to the flow to be tested.*/
@@ -313,219 +330,7 @@ public class OIDCRPFlowTest extends AbstractAuthnXmlFlowExecutionTests {
new ClassPathResource("conf/authn/rp-credentials.xml"), null);
}
- /**
- * Create an OAuth access token with a runtime constructed id_token. This allows the
- * expiry to be current. The token is signed with the client_secret using HS256.
- *
- * TODO the JWT is signed with a local MAC and is not consistent with the OP metadata. Might need to change this
- *
- * @return a serialized access token response.
- *
- * @throws Exception on error.
- */
- private String createAccessTokenResponseJSON() throws Exception {
- final var header = new JWSHeader.Builder(JWSAlgorithm.HS256)
- .type(JOSEObjectType.JWT)
- .build();
- final var payload = new JWTClaimsSet.Builder()
- .issuer(OP_ISSUER_ID)
- .audience(List.of(CLIENT_ID,"demo_rp2"))
- .subject("jdoe")
- .claim("nonce", "abadnonce")
- .claim("azp", CLIENT_ID)
- .claim("name","jdoe")
- .expirationTime(Date.from(Instant.now().plusSeconds(120)))
- .build();
- payload.getClaims().forEach((k,v) -> log.debug("{}:{}",k,v));
- final var signedJWT = new SignedJWT(header,payload);
- signedJWT.sign(new MACSigner(CLIENT_SECRET));
- final String accessTokenSerialized = "{\n"
- + " \"access_token\": \"W0y5aDNAzEPNpSzu1cuMG904BZuQFZJUUwG5F3ct0zydZWy1ji\",\n"
- + " \"token_type\": \"Bearer\",\n"
- + " \"id_token\": \""+signedJWT.serialize()+"\",\n"
- + " \"scope\": \"openid\"\n"
- + "}";
- log.debug("Access token: \n {}",accessTokenSerialized);
- return accessTokenSerialized;
- }
- /**
- * Create an OAuth access token with a runtime constructed id_token. This allows the
- * expiry to be current. The token is signed using HS256 and encrypted using the 'Direct Encryption' (dir)
- * management mode i.e. no key wrapping. Both use the shared client_secret.
- *
- * @return a serialized access token response.
- *
- * @throws Exception on error.
- */
- private String createAccessTokenResponseJSONSignedAndEncrypted() throws Exception {
- final var header = new JWSHeader.Builder(JWSAlgorithm.HS256)
- .type(JOSEObjectType.JWT)
- .build();
- final var payload = new JWTClaimsSet.Builder()
- .issuer(OP_ISSUER_ID)
- .audience(List.of(CLIENT_ID,"demo_rp2"))
- .subject("jdoe")
- .claim("nonce", "abadnonce")
- .claim("azp", CLIENT_ID)
- .claim("name","jdoe")
- .expirationTime(Date.from(Instant.now().plusSeconds(120)))
- .build();
- payload.getClaims().forEach((k,v) -> log.debug("{}:{}",k,v));
- final var signedJWT = new SignedJWT(header,payload);
- signedJWT.sign(new MACSigner(CLIENT_SECRET));
-
- final JWEObject jweObject =
- new JWEObject(new JWEHeader.Builder(JWEAlgorithm.DIR, EncryptionMethod.A256GCM)
- .contentType("JWT")
- .build(),
- new Payload(signedJWT));
- jweObject.encrypt(new DirectEncrypter(CLIENT_SECRET.getBytes(StandardCharsets.UTF_8)));
- final EncryptedJWT jwe = EncryptedJWT.parse(jweObject.serialize());
-
- final String accessTokenSerialized = "{\n"
- + " \"access_token\": \"W0y5aDNAzEPNpSzu1cuMG904BZuQFZJUUwG5F3ct0zydZWy1ji\",\n"
- + " \"token_type\": \"Bearer\",\n"
- + " \"id_token\": \""+jwe.serialize()+"\",\n"
- + " \"scope\": \"openid\"\n"
- + "}";
- log.debug("Access token: \n {}",accessTokenSerialized);
- return accessTokenSerialized;
- }
-
- /**
- * Create an OAuth access token with a runtime constructed id_token. This allows the
- * expiry to be current. The token is signed using HS256 and encrypted using a key encryption
- * management mode.
- *
- * @return a serialized access token response.
- *
- * @throws Exception on error.
- */
- private Pair<String, RSAKey> createAccessTokenResponseJSONSignedAndAsymmetricEncrypted() throws Exception {
- final var header = new JWSHeader.Builder(JWSAlgorithm.HS256)
- .type(JOSEObjectType.JWT)
- .build();
- final var payload = new JWTClaimsSet.Builder()
- .issuer(OP_ISSUER_ID)
- .audience(List.of(CLIENT_ID,"demo_rp2"))
- .subject("jdoe")
- .claim("nonce", "abadnonce")
- .claim("azp", CLIENT_ID)
- .claim("name","jdoe")
- .expirationTime(Date.from(Instant.now().plusSeconds(120)))
- .build();
- payload.getClaims().forEach((k,v) -> log.debug("{}:{}",k,v));
- final var signedJWT = new SignedJWT(header,payload);
- signedJWT.sign(new MACSigner(CLIENT_SECRET));
-
-
- final RSAKey keyRecipient = new RSAKeyGenerator(2048)
- .keyID("1")
- .keyUse(KeyUse.ENCRYPTION)
- .generate();
-
- final JWEObject jweObject =
- new JWEObject(new JWEHeader.Builder(JWEAlgorithm.RSA_OAEP_256, EncryptionMethod.A256GCM)
- .contentType("JWT")
- .build(),
- new Payload(signedJWT));
- jweObject.encrypt(new RSAEncrypter(keyRecipient.toPublicJWK()));
- final EncryptedJWT jwe = EncryptedJWT.parse(jweObject.serialize());
-
- final String accessTokenSerialized = "{\n"
- + " \"access_token\": \"W0y5aDNAzEPNpSzu1cuMG904BZuQFZJUUwG5F3ct0zydZWy1ji\",\n"
- + " \"token_type\": \"Bearer\",\n"
- + " \"id_token\": \""+jwe.serialize()+"\",\n"
- + " \"scope\": \"openid\"\n"
- + "}";
- log.debug("Access token: \n {}",accessTokenSerialized);
- return new Pair<String, RSAKey>(accessTokenSerialized, keyRecipient);
- }
-
- /**
- * Create a signed UserInfo response JWT.
- *
- * @param issuer the issuer
- * @param audience the audience
- * @return the signed JWT
- * @throws JOSEException on error
- */
- private Pair<ECKey, SignedJWT> createAsymetricSignedUserInfoJWTResponseJSON(
- final String issuer, final String audience)
- throws JOSEException {
-
- final var key = new ECKeyGenerator(Curve.P_256).keyID("123").generate();
-
- final var header = new JWSHeader.Builder(JWSAlgorithm.ES256)
- .type(JOSEObjectType.JWT)
- .keyID(key.getKeyID())
- .build();
- final var payload = new JWTClaimsSet.Builder()
- .issuer(issuer)
- .audience(audience)
- .subject("jdoe")
- .claim("preferred_username", "jdoe")
- .claim("name", "J Doe")
- .build();
-
- final var signedJWT = new SignedJWT(header, payload);
- signedJWT.sign(new ECDSASigner(key.toECPrivateKey()));
- return new Pair(key,signedJWT);
- }
-
- /**
- * Create a Plain UserInfo response JWT.
- *
- * @param issuer the issuer
- * @param audience the audience
- * @return the signed JWT
- * @throws JOSEException on error
- */
- private PlainJWT createPlainUserInfoJWTResponseJSON(final String issuer, final String audience)
- throws JOSEException {
-
-
- final var payload = new JWTClaimsSet.Builder()
- .issuer(issuer)
- .audience(audience)
- .subject("jdoe")
- .claim("preferred_username", "jdoe")
- .claim("name", "J Doe")
- .build();
-
- return new PlainJWT(payload);
- }
-
- /**
- * Create an asymetrically signed and encrypted UserInfo response JWT. Return both signature and encryption
- * keys alongside the EncryptedJWT in the response.
- *
- * @param issuer the issuer
- * @param audience the audience
- * @return the signed JWT alongside the pair of signature and encryption keys
- *
- * @throws JOSEException on error
- */
- private Pair<Pair<ECKey, RSAKey>, EncryptedJWT> createAsymetricSignedAndAssymetricEncryptedUserInfoJWTResponse(
- final String issuer, final String audience) throws Exception {
-
- final RSAKey keyRecipient = new RSAKeyGenerator(2048)
- .keyID("2")
- .keyUse(KeyUse.ENCRYPTION)
- .generate();
-
- final var keySignedJWTPair = createAsymetricSignedUserInfoJWTResponseJSON(issuer, audience);
-
- final JWEObject jweObject =
- new JWEObject(new JWEHeader.Builder(JWEAlgorithm.RSA_OAEP_256, EncryptionMethod.A256GCM)
- .contentType("JWT")
- .build(),
- new Payload(keySignedJWTPair.getSecond()));
- jweObject.encrypt(new RSAEncrypter(keyRecipient.toPublicJWK()));
- return new Pair<Pair<ECKey, RSAKey>, EncryptedJWT>(
- new Pair(keySignedJWTPair.getFirst(),keyRecipient), EncryptedJWT.parse(jweObject.serialize()));
- }
/**
* Create a running server that mimics responses from an OpenID Connect provider.
@@ -613,6 +418,66 @@ public class OIDCRPFlowTest extends AbstractAuthnXmlFlowExecutionTests {
}
+ /**
+ * Create a direct encryption {@link JWKCredential} from the given shared secret.
+ *
+ * @param secret the secret to convert to a {@link JWKCredential}.
+ *
+ * @return the credential
+ */
+ private JWKCredential createDirectEncryptionCredentialFromSharedSecret(final String secret) {
+ final BasicExpiringJWKCredential jwkCredential = new BasicExpiringJWKCredential();
+ jwkCredential.setSecretKey(new SecretKeySpec(JWSAssemblyUtils.getSecretBytes(secret), "NONE"));
+ jwkCredential.setCredentialExpiresAt(Duration.ZERO);
+ jwkCredential.setUsageType(UsageType.UNSPECIFIED);
+ jwkCredential.getCredentialContextSet().add(
+ new JWKEncryptionCredentialContext(EncryptionMethod.A256GCM));
+ jwkCredential.setKid("mockKey");
+ jwkCredential.getKeyNames().add("mockKey");
+ jwkCredential.setAlgorithm(JWEAlgorithm.DIR);
+ return jwkCredential;
+ }
+
+ /**
+ * Create a simple client credential from from the given shared secret.
+ *
+ * @param secret the secret to convert to a {@link JWKCredential}.
+ *
+ * @return the credential
+ */
+ //TODO used for both signing and encryption, so alg needs to reflect this
+ private JWKCredential createClientSecretCredential(final String secret) {
+ final BasicExpiringJWKCredential jwkCredential = new BasicExpiringJWKCredential();
+ jwkCredential.setSecretKey(new SecretKeySpec(JWSAssemblyUtils.getSecretBytes(secret), "NONE"));
+ jwkCredential.setCredentialExpiresAt(Duration.ZERO);
+ jwkCredential.setUsageType(UsageType.UNSPECIFIED);
+ jwkCredential.setKid("mockKey");
+ jwkCredential.getKeyNames().add("mockKey");
+ return jwkCredential;
+ }
+
+ /**
+ * Create a direct encryption {@link JWKCredential} from the given shared secret.
+ *
+ * @param secret the secret to convert to a {@link JWKCredential}.
+ *
+ * @return the credential
+ * @throws JOSEException
+ */
+ private JWKCredential createKeyEncryptionCredential(final RSAKey secret) throws JOSEException {
+ final BasicExpiringJWKCredential jwkCredential = new BasicExpiringJWKCredential();
+ jwkCredential.setPrivateKey(secret.toPrivateKey());
+ jwkCredential.setPublicKey(secret.toPublicKey());
+ jwkCredential.setCredentialExpiresAt(Duration.ZERO);
+ jwkCredential.setUsageType(UsageType.ENCRYPTION);
+
+ jwkCredential.setKid(secret.getKeyID());
+ jwkCredential.getKeyNames().add("mockKey");
+ jwkCredential.setAlgorithm(secret.getAlgorithm());
+ return jwkCredential;
+ }
+
+
@Override
@Nonnull protected ProfileRequestContext buildProfileRequestContext(@Nonnull final String flowId,
@Nonnull final boolean forceAuthn,
@@ -654,14 +519,50 @@ public class OIDCRPFlowTest extends AbstractAuthnXmlFlowExecutionTests {
assertCurrentStateEquals("AuthRequest");
}
+ /**
+ * Create a basic security configuration, which can be overriden per test if required.
+ *
+
+ * @return the basic security configuration.
+ */
+ private OIDCSecurityConfiguration createBasicSecurityConfigAndValidationParams() {
+
+ final var securityConfig = new OIDCSecurityConfiguration();
+
+ final var idTokenSigValConfig = new BasicSignatureValidationConfiguration<SignedJWT>();
+ idTokenSigValConfig.setSignatureTrustEngine(
+ new ExplicitKeySignedJWTTrustEngine(new CriterionCredentialResolver(),
+ new BasicJOSEObjectCredentialResolver()));
+ securityConfig.setIdTokenJwtSignatureValidationConfig(idTokenSigValConfig);
+
+ final var userInfoTokenSigValConfig = new BasicSignatureValidationConfiguration<SignedJWT>();
+ userInfoTokenSigValConfig.setSignatureTrustEngine(
+ new ExplicitKeySignedJWTTrustEngine(new CriterionCredentialResolver(),
+ new BasicJOSEObjectCredentialResolver()));
+ securityConfig.setUserInfoTokenJwtSignatureValidationConfig(userInfoTokenSigValConfig);
+
+ //The CEK resolver just resolves keys from the criteria set.
+ final var idTokenDecryptConfig = new BasicJWTDecryptionConfiguration();
+ idTokenDecryptConfig.setContentEncryptionKeyCredentialResolver(new CriterionCredentialResolver());
+ securityConfig.setIdTokenJwtDecryptionConfig(idTokenDecryptConfig);
+
+ final var userInfoDecryptConfig = new BasicJWTDecryptionConfiguration();
+ userInfoDecryptConfig.setContentEncryptionKeyCredentialResolver(new CriterionCredentialResolver());
+ securityConfig.setUserInfoJwtDecryptionConfig(userInfoDecryptConfig);
+
+
+ return securityConfig;
+ }
+
/**
* Test the flow from the external authorization request to the end of the flow.
+ * Using a MAC signed id_token and plain UserInfo JSON Response.
*
* @throws Exception on error.
*/
@Test
- public void testAuthnFlowFromAuthorizationCallback() throws Exception {
+ public void testAuthnFlowFromAuthorizationCallback_Using_MACSignedIDToken_PlainUserInfo() throws Exception {
setFlowPath(FLOW);
setFlowModelResources(flowResources);
@@ -678,80 +579,74 @@ public class OIDCRPFlowTest extends AbstractAuthnXmlFlowExecutionTests {
// First is token exchange
mockOPServer.enqueue(new MockResponse().setResponseCode(200)
.setHeader("content-type", "application/json")
- .setBody(createAccessTokenResponseJSON()));
- // Second is userInfo
+ .setBody(TestTokenHelper.createAccessTokenResponseJSONWithHMACIDToken(
+ OP_ISSUER_ID, CLIENT_ID, List.of(CLIENT_ID, "demo_rp2"), CLIENT_SECRET)));
+ // Second is plain userInfo
mockOPServer.enqueue(new MockResponse().setResponseCode(200)
.setHeader("content-type", "application/json")
.setBody(USERINFO_RESPONSE));
mockOPServer.start(9918);
- final FlowExecutionImpl flowExecution = (FlowExecutionImpl)getFlowExecutionFactory()
+ final var flowExecution = (FlowExecutionImpl)getFlowExecutionFactory()
.createFlowExecution(getFlowDefinition());
- final ProfileRequestContext prc = buildProfileRequestContext("authn/OIDCRelyingParty", false,false);
- prc.getSubcontext(AuthenticationContext.class).setAuthenticatingAuthority(OP_ISSUER_ID);
-
- // create a nested PRC under the authentication context
- final ProfileRequestContext nestPrc = (ProfileRequestContext)
- prc.getSubcontext(AuthenticationContext.class).addSubcontext(new ProfileRequestContext(), true);
+ final ProfileRequestContext prc = populateBasicContextTreeFromAuthnResponse(flowExecution);
+
- // Add under nest PRC
- final RelyingPartyContext partyContext = new RelyingPartyContext();
- final OIDCAuthorizationConfiguration partyConfig = new OIDCAuthorizationConfiguration();
- partyContext.setProfileConfig(partyConfig);
- partyConfig.setClientAuthenticationMethod("client_secret_basic");
- partyConfig.setClientId(CLIENT_ID);
- partyConfig.setClientCredential(createDirectEncryptionCredentialFromSharedSecret(CLIENT_SECRET));
- // Set a default security config for the profile config
- final OIDCSecurityConfiguration secConfig = new OIDCSecurityConfiguration();
- final BasicSignatureValidationConfiguration<SignedJWT> sigValidation =
- new BasicSignatureValidationConfiguration<>();
- sigValidation.setSignatureTrustEngine(new ExplicitKeySignedJWTTrustEngine(
- new CredentialResolver() {
-
- @Override
- public Credential resolveSingle(final CriteriaSet criteria) throws ResolverException {
- final BasicJWKCredential jwkCredential = new BasicJWKCredential();
- jwkCredential.setAlgorithm(JWSAlgorithm.HS256);
- jwkCredential.setKid("secret_key");
- jwkCredential.setSecretKey(new SecretKeySpec(
- JWSAssemblyUtils.getSecretBytes(CLIENT_SECRET), "NONE"));
- return jwkCredential;
- }
-
- @Override
- public Iterable<Credential> resolve(final CriteriaSet criteria) throws ResolverException {
- return List.of(resolveSingle(criteria));
- }
- }));
+ updateFlowExecution(flowExecution);
- secConfig.setIdTokenJwtSignatureValidationConfig(sigValidation);
- partyConfig.setSecurityConfiguration(secConfig);
-
- final RelyingPartyConfiguration rPartyConfig = new RelyingPartyConfiguration();
- rPartyConfig.setResponderId("http://idp.example.com/");
- partyContext.setConfiguration(rPartyConfig);
- nestPrc.addSubcontext(partyContext);
+ //set start view and ending event to transition on.
+ externalContext.setEventId("proceed");
+ setCurrentState("AuthRequest");
+ resumeFlow(externalContext);
+ mockOPServer.shutdown();
+ //assert success conditions
+ assertFlowExecutionEnded();
+ assertNotNull(prc.getSubcontext(AuthenticationContext.class));
+ assertNotNull(prc.getSubcontext(SubjectCanonicalizationContext.class));
+ assertEquals(prc.getSubcontext(SubjectCanonicalizationContext.class).getPrincipalName(),"jdoe");
+
- // Setup outbound context
- final MessageContext outMsgCtx = new MessageContext();
- outMsgCtx.setMessage(createAuthenticationRequest());
- outMsgCtx.addSubcontext(createPeerContext());
- outMsgCtx.addSubcontext(createResponseTypeAndModeContext());
- nestPrc.setOutboundMessageContext(outMsgCtx);
- outMsgCtx.getSubcontext(OIDCPeerEntityContext.class).addSubcontext(createOAuth2ClientContext(CLIENT_ID,null));
+ }
+
+ /**
+ * Test the flow from the external authorization request to the end of the flow.
+ * Using a MAC signed id_token and an invalid UserInfo JSON Response - it has not subject.
+ *
+ * @throws Exception on error.
+ */
+ @Test
+ public void testAuthnFlowFromAuthorizationCallback_Using_MACSignedIDToken_InvalidPlainUserInfo() throws Exception {
- // Setup inbound context.
- final MessageContext inMsgCtx = new MessageContext();
- inMsgCtx.setMessage(createAuthenticationResponse());
- nestPrc.setInboundMessageContext(inMsgCtx);
+ setFlowPath(FLOW);
+ setFlowModelResources(flowResources);
+ setSubflows(subflows);
- // Add prc to flow.
- prc.getSubcontext(AuthenticationContext.class)
- .addSubcontext(new ExternalAuthenticationContext(new ExternalAuthenticationImpl(false)));
- flowExecution.getConversationScope().put("opensamlProfileRequestContext",prc);
+ final Map<String,String> mockProperties = Map.of(
+ "idp.entityID", "http://idp.example.com/",
+ "idp.authn.oidc.rp.proxyIssuer",OP_ISSUER_ID,
+ "idp.oidc.rp.redirecturl.allowedOrigins", RP_ALLOWED_ORIGINS);
+
+ setMockProperties(mockProperties);
+
+ final MockWebServer mockOPServer = createSimpleServer();
+ // First is token exchange
+ mockOPServer.enqueue(new MockResponse().setResponseCode(200)
+ .setHeader("content-type", "application/json")
+ .setBody(TestTokenHelper.createAccessTokenResponseJSONWithHMACIDToken(
+ OP_ISSUER_ID, CLIENT_ID, List.of(CLIENT_ID, "demo_rp2"), CLIENT_SECRET)));
+ // Second is plain userInfo
+ mockOPServer.enqueue(new MockResponse().setResponseCode(200)
+ .setHeader("content-type", "application/json")
+ .setBody(USERINFO_RESPONSE_NO_SUB));
+ mockOPServer.start(9918);
+
+
+ final var flowExecution = (FlowExecutionImpl)getFlowExecutionFactory()
+ .createFlowExecution(getFlowDefinition());
+ final ProfileRequestContext prc = populateBasicContextTreeFromAuthnResponse(flowExecution);
updateFlowExecution(flowExecution);
@@ -766,39 +661,14 @@ public class OIDCRPFlowTest extends AbstractAuthnXmlFlowExecutionTests {
//assert success conditions
assertFlowExecutionEnded();
assertNotNull(prc.getSubcontext(AuthenticationContext.class));
- assertNotNull(prc.getSubcontext(SubjectCanonicalizationContext.class));
- assertEquals(prc.getSubcontext(SubjectCanonicalizationContext.class).getPrincipalName(),"jdoe");
-
+ assertNull(prc.getSubcontext(SubjectCanonicalizationContext.class));
}
- private OIDCSecurityConfiguration createSecurityConfigAndValidationParamsForHMAC(
- @Nonnull final byte[] secret, @Nonnull final String algo) {
-
- final var securityConfig = new OIDCSecurityConfiguration();
- final var sigValConfig = new BasicSignatureValidationConfiguration<SignedJWT>();
- final var credResolver = new CredentialResolver() {
-
- @Override
- public Credential resolveSingle(final CriteriaSet criteria) throws ResolverException {
- final var cred = new BasicJWKCredential();
- cred.setAlgorithm(Algorithm.parse(algo));
- cred.setSecretKey(new SecretKeySpec(secret, algo));
- return cred;
- }
-
- @Override
- public Iterable<Credential> resolve(final CriteriaSet criteria) throws ResolverException {
- return List.of(resolveSingle(criteria));
- }
- };
- sigValConfig.setSignatureTrustEngine(new ExplicitKeySignedJWTTrustEngine(credResolver));
- securityConfig.setIdTokenJwtSignatureValidationConfig(sigValConfig);
- return securityConfig;
- }
+
@Test
- public void testAuthnFlowFromAuthorizationCallback_UsingSignedJWTUserInfoResponse() throws Exception {
+ public void testAuthnFlowFromAuthorizationCallback_Using_HMAC_UserInfo_And_IDToken_Response() throws Exception {
setFlowPath(FLOW);
setFlowModelResources(flowResources);
@@ -815,55 +685,20 @@ public class OIDCRPFlowTest extends AbstractAuthnXmlFlowExecutionTests {
// First is token exchange
mockOPServer.enqueue(new MockResponse().setResponseCode(200)
.setHeader("content-type", "application/json")
- .setBody(createAccessTokenResponseJSON()));
+ .setBody(TestTokenHelper.createAccessTokenResponseJSONWithHMACIDToken(
+ OP_ISSUER_ID, CLIENT_ID, List.of(CLIENT_ID, "demo_rp2"), CLIENT_SECRET)));
// Second is userInfo
+ final var userInfoToken = TestTokenHelper.createHMACSignedUserInfoJWTResponseJSON(
+ OP_ISSUER_ID,CLIENT_ID, List.of(CLIENT_ID), CLIENT_SECRET);
mockOPServer.enqueue(new MockResponse().setResponseCode(200)
.setHeader("content-type", "application/jwt")
- .setBody(createAsymetricSignedUserInfoJWTResponseJSON(OP_ISSUER_ID,CLIENT_ID).serialize()));
+ .setBody(userInfoToken.serialize()));
mockOPServer.start(9918);
- final FlowExecutionImpl flowExecution = (FlowExecutionImpl)getFlowExecutionFactory()
+ final var flowExecution = (FlowExecutionImpl)getFlowExecutionFactory()
.createFlowExecution(getFlowDefinition());
- final ProfileRequestContext prc = buildProfileRequestContext("authn/OIDCRelyingParty", false,false);
- prc.getSubcontext(AuthenticationContext.class).setAuthenticatingAuthority(OP_ISSUER_ID);
-
- // create a nested PRC under the authentication context
- final ProfileRequestContext nestPrc = (ProfileRequestContext)
- prc.getSubcontext(AuthenticationContext.class).addSubcontext(new ProfileRequestContext(), true);
-
- // Add under nest PRC
- final RelyingPartyContext partyContext = new RelyingPartyContext();
- final OIDCAuthorizationConfiguration partyConfig = new OIDCAuthorizationConfiguration();
- partyContext.setProfileConfig(partyConfig);
-
- partyConfig.setSecurityConfiguration(createSecurityConfigAndValidationParamsForHMAC(
- CLIENT_SECRET.getBytes(), "HS256"));
- partyConfig.setClientAuthenticationLookupStrategy(p ->
- new ClientSecretBasic(new ClientID(CLIENT_ID), new Secret("secret")));
-
- final RelyingPartyConfiguration rPartyConfig = new RelyingPartyConfiguration();
- rPartyConfig.setResponderId("http://idp.example.com/");
- partyContext.setConfiguration(rPartyConfig);
- nestPrc.addSubcontext(partyContext);
-
- // Setup outbound context
- final MessageContext outMsgCtx = new MessageContext();
- outMsgCtx.setMessage(createAuthenticationRequest());
- outMsgCtx.addSubcontext(createPeerContext());
- outMsgCtx.addSubcontext(createResponseTypeAndModeContext());
- nestPrc.setOutboundMessageContext(outMsgCtx);
- outMsgCtx.getSubcontext(OIDCPeerEntityContext.class).addSubcontext(createOAuth2ClientContext(CLIENT_ID,null));
-
- // Setup inbound context.
- final MessageContext inMsgCtx = new MessageContext();
- inMsgCtx.setMessage(createAuthenticationResponse());
- nestPrc.setInboundMessageContext(inMsgCtx);
-
- // Add prc to flow.
- prc.getSubcontext(AuthenticationContext.class)
- .addSubcontext(new ExternalAuthenticationContext(new ExternalAuthenticationImpl(false)));
- flowExecution.getConversationScope().put("opensamlProfileRequestContext",prc);
+ final ProfileRequestContext prc = populateBasicContextTreeFromAuthnResponse(flowExecution);
updateFlowExecution(flowExecution);
@@ -903,10 +738,12 @@ public class OIDCRPFlowTest extends AbstractAuthnXmlFlowExecutionTests {
// First is token exchange
mockOPServer.enqueue(new MockResponse().setResponseCode(200)
.setHeader("content-type", "application/json")
- .setBody(createAccessTokenResponseJSONSignedAndEncrypted()));
+ .setBody(TestTokenHelper.createAccessTokenResponseJSONIDTokenSignedAndDirEncrypted(
+ OP_ISSUER_ID, CLIENT_ID, List.of(CLIENT_ID, "demo_rp2"), CLIENT_SECRET)));
// Second is userInfo
- final var userInfoTokenAndKey =
- createAsymetricSignedAndAssymetricEncryptedUserInfoJWTResponse(OP_ISSUER_ID,CLIENT_ID);
+ final var userInfoTokenAndKey =
+ TestTokenHelper.createAsymetricSignedAndAsymetricEncryptedUserInfoJWTResponse(
+ OP_ISSUER_ID,List.of(CLIENT_ID), CLIENT_ID);
mockOPServer.enqueue(new MockResponse().setResponseCode(200)
.setHeader("content-type", "application/jwt")
.setBody(userInfoTokenAndKey.getSecond().serialize()));
@@ -1042,7 +879,7 @@ public class OIDCRPFlowTest extends AbstractAuthnXmlFlowExecutionTests {
}
/**
- * Uses symmetric MAC and asymmetric encryption.
+ * Uses symmetric MAC and asymmetric encryption. Plain UserInfo response.
*
* @throws Exception on error.
*/
@@ -1062,7 +899,9 @@ public class OIDCRPFlowTest extends AbstractAuthnXmlFlowExecutionTests {
setMockProperties(mockProperties);
- final Pair<String, RSAKey> accessTokenAndKey = createAccessTokenResponseJSONSignedAndAsymmetricEncrypted();
+ final Pair<String, RSAKey> accessTokenAndKey =
+ TestTokenHelper.createAccessTokenResponseJSONWithSignedAndAsymmetricEncryptedIDToken(
+ OP_ISSUER_ID, CLIENT_ID, List.of(CLIENT_ID), CLIENT_SECRET);
final MockWebServer mockOPServer = createSimpleServer();
// First is token exchange
mockOPServer.enqueue(new MockResponse().setResponseCode(200)
@@ -1169,8 +1008,9 @@ public class OIDCRPFlowTest extends AbstractAuthnXmlFlowExecutionTests {
}
+ //FIXME why are we testing a plain UserInfo JWT? is this permissible
@Test
- public void testAuthnFlowFromAuthorizationCallback_UsingPlainJWTUserInfoResponse()
+ public void testAuthnFlowFromAuthorizationCallback_UsingPlainJSONObjectUserInfoResponse()
throws Exception {
setFlowPath(FLOW);
@@ -1189,17 +1029,41 @@ public class OIDCRPFlowTest extends AbstractAuthnXmlFlowExecutionTests {
// First is token exchange
mockOPServer.enqueue(new MockResponse().setResponseCode(200)
.setHeader("content-type", "application/json")
- .setBody(createAccessTokenResponseJSON()));
+ .setBody(TestTokenHelper.createAccessTokenResponseJSONWithHMACIDToken(
+ OP_ISSUER_ID, CLIENT_ID, List.of(CLIENT_ID, "demo_rp2"), CLIENT_SECRET)));
// Second is userInfo
mockOPServer.enqueue(new MockResponse().setResponseCode(200)
.setHeader("content-type", "application/jwt")
- .setBody(createPlainUserInfoJWTResponseJSON(OP_ISSUER_ID,CLIENT_ID)
+ .setBody(TestTokenHelper.createPlainUserInfoJWTResponseJSON(OP_ISSUER_ID,CLIENT_ID)
.serialize()));
mockOPServer.start(9918);
- final FlowExecutionImpl flowExecution = (FlowExecutionImpl)getFlowExecutionFactory()
+ final var flowExecution = (FlowExecutionImpl)getFlowExecutionFactory()
.createFlowExecution(getFlowDefinition());
+ final ProfileRequestContext prc = populateBasicContextTreeFromAuthnResponse(flowExecution);
+
+ updateFlowExecution(flowExecution);
+
+ //set start view and ending event to transition on.
+ externalContext.setEventId("proceed");
+ setCurrentState("AuthRequest");
+ resumeFlow(externalContext);
+
+ mockOPServer.shutdown();
+
+ //assert success conditions
+ assertFlowExecutionEnded();
+ assertNotNull(prc.getSubcontext(AuthenticationContext.class));
+ assertNotNull(prc.getSubcontext(SubjectCanonicalizationContext.class));
+ assertEquals(prc.getSubcontext(SubjectCanonicalizationContext.class).getPrincipalName(),"jdoe");
+
+
+ }
+
+ private ProfileRequestContext populateBasicContextTreeFromAuthnResponse(
+ final FlowExecutionImpl flowExecution) throws Exception {
+
final ProfileRequestContext prc = buildProfileRequestContext("authn/OIDCRelyingParty", false,false);
prc.getSubcontext(AuthenticationContext.class).setAuthenticatingAuthority(OP_ISSUER_ID);
@@ -1211,12 +1075,14 @@ public class OIDCRPFlowTest extends AbstractAuthnXmlFlowExecutionTests {
final RelyingPartyContext partyContext = new RelyingPartyContext();
final OIDCAuthorizationConfiguration partyConfig = new OIDCAuthorizationConfiguration();
partyContext.setProfileConfig(partyConfig);
- partyConfig.setClientAuthenticationLookupStrategy(p ->
- new ClientSecretBasic(new ClientID(CLIENT_ID), new Secret("secret")));
+ partyConfig.setClientCredential(createClientSecretCredential(CLIENT_SECRET));
+ partyConfig.setClientAuthenticationMethod("client_secret_basic");
+ partyConfig.setClientId(CLIENT_ID);
final RelyingPartyConfiguration rPartyConfig = new RelyingPartyConfiguration();
rPartyConfig.setResponderId("http://idp.example.com/");
partyContext.setConfiguration(rPartyConfig);
nestPrc.addSubcontext(partyContext);
+ partyConfig.setSecurityConfiguration(createBasicSecurityConfigAndValidationParams());
// Setup outbound context
final MessageContext outMsgCtx = new MessageContext();
@@ -1236,66 +1102,10 @@ public class OIDCRPFlowTest extends AbstractAuthnXmlFlowExecutionTests {
.addSubcontext(new ExternalAuthenticationContext(new ExternalAuthenticationImpl(false)));
flowExecution.getConversationScope().put("opensamlProfileRequestContext",prc);
+ return prc;
- updateFlowExecution(flowExecution);
-
- //set start view and ending event to transition on.
- externalContext.setEventId("proceed");
- setCurrentState("AuthRequest");
- resumeFlow(externalContext);
-
- mockOPServer.shutdown();
-
- //assert success conditions
- assertFlowExecutionEnded();
- assertNotNull(prc.getSubcontext(AuthenticationContext.class));
- assertNotNull(prc.getSubcontext(SubjectCanonicalizationContext.class));
- assertEquals(prc.getSubcontext(SubjectCanonicalizationContext.class).getPrincipalName(),"jdoe");
-
-
- }
-
- /**
- * Create a direct encryption {@link JWKCredential} from the given shared secret.
- *
- * @param secret the secret to convert to a {@link JWKCredential}.
- *
- * @return the credential
- */
- private JWKCredential createDirectEncryptionCredentialFromSharedSecret(final String secret) {
- final BasicExpiringJWKCredential jwkCredential = new BasicExpiringJWKCredential();
- jwkCredential.setSecretKey(new SecretKeySpec(JWSAssemblyUtils.getSecretBytes(secret), "NONE"));
- jwkCredential.setCredentialExpiresAt(Duration.ZERO);
- jwkCredential.setUsageType(UsageType.UNSPECIFIED);
- jwkCredential.getCredentialContextSet().add(
- new JWKEncryptionCredentialContext(EncryptionMethod.A256GCM));
- jwkCredential.setKid("mockKey");
- jwkCredential.getKeyNames().add("mockKey");
- jwkCredential.setAlgorithm(JWEAlgorithm.DIR);
- return jwkCredential;
- }
-
- /**
- * Create a direct encryption {@link JWKCredential} from the given shared secret.
- *
- * @param secret the secret to convert to a {@link JWKCredential}.
- *
- * @return the credential
- * @throws JOSEException
- */
- private JWKCredential createKeyEncryptionCredential(final RSAKey secret) throws JOSEException {
- final BasicExpiringJWKCredential jwkCredential = new BasicExpiringJWKCredential();
- jwkCredential.setPrivateKey(secret.toPrivateKey());
- jwkCredential.setPublicKey(secret.toPublicKey());
- jwkCredential.setCredentialExpiresAt(Duration.ZERO);
- jwkCredential.setUsageType(UsageType.ENCRYPTION);
-
- jwkCredential.setKid(secret.getKeyID());
- jwkCredential.getKeyNames().add("mockKey");
- jwkCredential.setAlgorithm(secret.getAlgorithm());
- return jwkCredential;
}
-
+
/**
* Test the flow from the external authorization request to the end of the flow when an error
diff --git a/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/TestTokenHelper.java b/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/TestTokenHelper.java
new file mode 100644
index 0000000..e79dd48
--- /dev/null
+++ b/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/TestTokenHelper.java
@@ -0,0 +1,327 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements. See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+package net.shibboleth.idp.plugin.authn.oidc.rp.impl;
+
+import java.nio.charset.StandardCharsets;
+import java.time.Instant;
+import java.util.Date;
+import java.util.List;
+
+import com.nimbusds.jose.EncryptionMethod;
+import com.nimbusds.jose.JOSEException;
+import com.nimbusds.jose.JOSEObjectType;
+import com.nimbusds.jose.JWEAlgorithm;
+import com.nimbusds.jose.JWEHeader;
+import com.nimbusds.jose.JWEObject;
+import com.nimbusds.jose.JWSAlgorithm;
+import com.nimbusds.jose.JWSHeader;
+import com.nimbusds.jose.Payload;
+import com.nimbusds.jose.crypto.DirectEncrypter;
+import com.nimbusds.jose.crypto.ECDSASigner;
+import com.nimbusds.jose.crypto.MACSigner;
+import com.nimbusds.jose.crypto.RSAEncrypter;
+import com.nimbusds.jose.jwk.Curve;
+import com.nimbusds.jose.jwk.ECKey;
+import com.nimbusds.jose.jwk.KeyUse;
+import com.nimbusds.jose.jwk.RSAKey;
+import com.nimbusds.jose.jwk.gen.ECKeyGenerator;
+import com.nimbusds.jose.jwk.gen.RSAKeyGenerator;
+import com.nimbusds.jwt.EncryptedJWT;
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.jwt.PlainJWT;
+import com.nimbusds.jwt.SignedJWT;
+
+import net.shibboleth.utilities.java.support.collection.Pair;
+
+/** A helper class for generating tokens for testing.*/
+public final class TestTokenHelper {
+
+ /** Private constructor.*/
+ private TestTokenHelper() {
+
+ }
+
+ /**
+ * Build a basic {@link JWTClaimsSet} from the supplied parameters.
+ *
+ * @param subject the subject
+ * @param nonce the nonce
+ * @param name the name
+ * @param opIssuerId the OP's issuer ID
+ * @param clientId the client_id
+ * @param audience the list of audiences
+ *
+ * @return the JWT claims set.
+ */
+ public static JWTClaimsSet createBasicClaims(
+ final String subject, final String nonce, final String name,
+ final String opIssuerId, final String clientId,
+ final List<String> audience) {
+
+ return new JWTClaimsSet.Builder()
+ .issuer(opIssuerId)
+ .audience(audience)
+ .subject(subject)
+ .claim("nonce", nonce)
+ .claim("azp", clientId)
+ .claim("name",name)
+ .expirationTime(Date.from(Instant.now().plusSeconds(120)))
+ .build();
+ }
+
+ /**
+ * Create an OAuth access token with a runtime constructed id_token. This allows the
+ * expiry to be current. The token is signed with the client_secret using HS256.
+ *
+ * @param opIssuerId the OP's issuer ID
+ * @param clientId the client_id
+ * @param audience the list of audiences for the id_token token
+ * @param clientSecret the client_secret used to sign the id_token
+ *
+ *
+ * @return a serialized access token response.
+ *
+ * @throws Exception on error.
+ */
+ public static String createAccessTokenResponseJSONWithHMACIDToken(
+ final String opIssuerId, final String clientId,
+ final List<String> audience, final String clientSecret) throws Exception {
+
+ final var header = new JWSHeader.Builder(JWSAlgorithm.HS256)
+ .type(JOSEObjectType.JWT)
+ .build();
+
+ final var payload = createBasicClaims("jdoe", "abadnonce", "jdoe", opIssuerId, clientId, audience);
+
+ final var signedJWT = new SignedJWT(header,payload);
+ signedJWT.sign(new MACSigner(clientSecret));
+
+ return buildTemplateAccessTokenJSONResponse(signedJWT.serialize());
+ }
+
+ /**
+ * Create an OAuth access token with a runtime constructed id_token. This allows the
+ * expiry to be current. The token is signed using HS256 and encrypted using the 'Direct Encryption' (dir)
+ * management mode i.e. no key wrapping. Both use the shared client_secret.
+ *
+ * @param opIssuerId the OP's issuer ID
+ * @param clientId the client_id
+ * @param audience the list of audiences for the id_token token
+ * @param clientSecret the client_secret used to sign the id_token
+ *
+ * @return a serialized access token response.
+ *
+ * @throws Exception on error.
+ */
+ public static String createAccessTokenResponseJSONIDTokenSignedAndDirEncrypted(final String opIssuerId,
+ final String clientId, final List<String> audience, final String clientSecret) throws Exception {
+
+ final var header = new JWSHeader.Builder(JWSAlgorithm.HS256)
+ .type(JOSEObjectType.JWT)
+ .build();
+
+ final var payload = createBasicClaims("jdoe", "abadnonce", "jdoe", opIssuerId, clientId, audience);
+ final var signedJWT = new SignedJWT(header,payload);
+ signedJWT.sign(new MACSigner(clientSecret));
+
+ final JWEObject jweObject =
+ new JWEObject(new JWEHeader.Builder(JWEAlgorithm.DIR, EncryptionMethod.A256GCM)
+ .contentType("JWT")
+ .build(),
+ new Payload(signedJWT));
+ jweObject.encrypt(new DirectEncrypter(clientSecret.getBytes(StandardCharsets.UTF_8)));
+ final EncryptedJWT jwe = EncryptedJWT.parse(jweObject.serialize());
+
+
+ return buildTemplateAccessTokenJSONResponse(jwe.serialize());
+ }
+
+ /**
+ * Create an OAuth access token with a runtime constructed id_token. This allows the
+ * expiry to be current. The token is signed using HS256 (MAC) and encrypted using a key encryption
+ * management mode.
+ *
+ * @param opIssuerId the OP's issuer ID
+ * @param clientId the client_id
+ * @param audience the list of audiences for the id_token token
+ * @param clientSecret the client_secret used to sign the id_token
+ *
+ * @return a serialized access token response and the RSAKey used to encrypt the CEK.
+ *
+ * @throws Exception on error.
+ */
+ public static Pair<String, RSAKey> createAccessTokenResponseJSONWithSignedAndAsymmetricEncryptedIDToken(
+ final String opIssuerId,
+ final String clientId, final List<String> audience, final String clientSecret) throws Exception {
+
+ final var header = new JWSHeader.Builder(JWSAlgorithm.HS256)
+ .type(JOSEObjectType.JWT)
+ .build();
+
+ final var payload = createBasicClaims("jdoe", "abadnonce", "jdoe", opIssuerId, clientId, audience);
+ final var signedJWT = new SignedJWT(header,payload);
+ signedJWT.sign(new MACSigner(clientSecret));
+
+
+ final RSAKey keyRecipient = new RSAKeyGenerator(2048)
+ .keyID("1")
+ .keyUse(KeyUse.ENCRYPTION)
+ .generate();
+
+ final JWEObject jweObject =
+ new JWEObject(new JWEHeader.Builder(JWEAlgorithm.RSA_OAEP_256, EncryptionMethod.A256GCM)
+ .contentType("JWT")
+ .build(),
+ new Payload(signedJWT));
+ jweObject.encrypt(new RSAEncrypter(keyRecipient.toPublicJWK()));
+ final EncryptedJWT jwe = EncryptedJWT.parse(jweObject.serialize());
+
+ return new Pair<String, RSAKey>(buildTemplateAccessTokenJSONResponse(jwe.serialize()), keyRecipient);
+ }
+
+ /**
+ * Create an RSA asymetrically signed and encrypted UserInfo response JWT. Return both signature and encryption
+ * keys alongside the EncryptedJWT in the response.
+ *
+ * @param issuer the issuer
+ * @param audience the audience
+ * @param clientId the client_id
+ * @return the signed JWT alongside the pair of signature and encryption keys
+ * @throws Exception
+ *
+ * @throws JOSEException on error
+ */
+ public static Pair<Pair<ECKey, RSAKey>, EncryptedJWT> createAsymetricSignedAndAsymetricEncryptedUserInfoJWTResponse(
+ final String issuer, final List<String> audience, final String clientId) throws Exception {
+
+ final RSAKey keyRecipient = new RSAKeyGenerator(2048)
+ .keyID("2")
+ .keyUse(KeyUse.ENCRYPTION)
+ .generate();
+
+ final var keySignedJWTPair = createECDSASignedUserInfoJWTResponseJSON(issuer, clientId, audience);
+
+ final JWEObject jweObject =
+ new JWEObject(new JWEHeader.Builder(JWEAlgorithm.RSA_OAEP_256, EncryptionMethod.A256GCM)
+ .contentType("JWT")
+ .build(),
+ new Payload(keySignedJWTPair.getSecond()));
+ jweObject.encrypt(new RSAEncrypter(keyRecipient.toPublicJWK()));
+
+ return new Pair<Pair<ECKey, RSAKey>, EncryptedJWT>(
+ new Pair<ECKey, RSAKey>(keySignedJWTPair.getFirst(),keyRecipient),
+ EncryptedJWT.parse(jweObject.serialize()));
+ }
+
+ /**
+ * Create an ECDSA signed UserInfo response JWT.
+ *
+ * @param opIssuerId the issuer
+ * @param clientId the client_id
+ * @param audience the audience
+ *
+ * @return the signed JWT
+ *
+ * @throws JOSEException on error
+ */
+ public static Pair<ECKey, SignedJWT> createECDSASignedUserInfoJWTResponseJSON(
+ final String opIssuerId, final String clientId, final List<String> audience)
+ throws JOSEException {
+
+ final var key = new ECKeyGenerator(Curve.P_256).keyID("123").generate();
+
+ final var header = new JWSHeader.Builder(JWSAlgorithm.ES256)
+ .type(JOSEObjectType.JWT)
+ .keyID(key.getKeyID())
+ .build();
+
+ final var payload = createBasicClaims("jdoe", "adbadnonce", "Jdoe", opIssuerId, clientId, audience);
+
+ final var signedJWT = new SignedJWT(header, payload);
+ signedJWT.sign(new ECDSASigner(key.toECPrivateKey()));
+ return new Pair<ECKey, SignedJWT>(key,signedJWT);
+ }
+
+ /**
+ * Create a HMAC signed UserInfo response JWT.
+ *
+ * @param opIssuerId the issuer
+ * @param clientId the client_id
+ * @param audience the audience
+ * @param clientSecret the client_secret
+ *
+ * @return the signed JWT
+ *
+ * @throws JOSEException on error
+ */
+ public static SignedJWT createHMACSignedUserInfoJWTResponseJSON(
+ final String opIssuerId, final String clientId, final List<String> audience, final String clientSecret)
+ throws JOSEException {
+
+ final var header = new JWSHeader.Builder(JWSAlgorithm.HS256)
+ .type(JOSEObjectType.JWT)
+ .build();
+
+ final var payload = createBasicClaims("jdoe", "adbadnonce", "Jdoe", opIssuerId, clientId, audience);
+
+ final var signedJWT = new SignedJWT(header, payload);
+ signedJWT.sign(new MACSigner(clientSecret));
+ return signedJWT;
+ }
+
+ /**
+ * Create a Plain UserInfo response JWT.
+ *
+ * @param issuer the issuer
+ * @param audience the audience
+ * @return the signed JWT
+ * @throws JOSEException on error
+ */
+ public static PlainJWT createPlainUserInfoJWTResponseJSON(final String issuer, final String audience)
+ throws JOSEException {
+
+
+ final var payload = new JWTClaimsSet.Builder()
+ .issuer(issuer)
+ .audience(audience)
+ .subject("jdoe")
+ .claim("preferred_username", "jdoe")
+ .claim("name", "J Doe")
+ .build();
+
+ return new PlainJWT(payload);
+ }
+
+ /**
+ * Build a simple OAuth2.0/OIDC Access Token JSON response using the serialized JWT.
+ *
+ * @param serializedJWT the id_token serialized
+ *
+ * @return An access token response
+ */
+ private static String buildTemplateAccessTokenJSONResponse(final String serializedJWT) {
+ return "{\n"
+ + " \"access_token\": \"W0y5aDNAzEPNpSzu1cuMG904BZuQFZJUUwG5F3ct0zydZWy1ji\",\n"
+ + " \"token_type\": \"Bearer\",\n"
+ + " \"id_token\": \""+serializedJWT+"\",\n"
+ + " \"scope\": \"openid\"\n"
+ + "}";
+ }
+
+}
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.
More information about the commits
mailing list