[java-idp-oidc] branch main updated: JOIDC-201 - Support for OAuth 2.0 Demonstrating Proof of Possession (DPoP)
Henri Mikkonen
henri.mikkonen at iki.fi
Tue Aug 13 11:43:55 UTC 2024
This is an automated email from the git hooks/post-receive script.
hjmikkon pushed a commit to branch main
in repository java-idp-oidc.
View the commit online:
http://git.shibboleth.net/view/?p=java-idp-oidc.git;a=commit;h=c2355c6c2386a35eab785a6b1d48c5360bd9a189
The following commit(s) were added to refs/heads/main by this push:
new c2355c6c JOIDC-201 - Support for OAuth 2.0 Demonstrating Proof of Possession (DPoP)
c2355c6c is described below
commit c2355c6c2386a35eab785a6b1d48c5360bd9a189
Author: Henri Mikkonen <henri.mikkonen at iki.fi>
AuthorDate: Tue Aug 13 14:43:15 2024 +0300
JOIDC-201 - Support for OAuth 2.0 Demonstrating Proof of Possession (DPoP)
https://shibboleth.atlassian.net/browse/JOIDC-201
Add support for 'alwaysIssueBearerAccessToken' profile configuration option
- If the flag is true, the DPoP proof thumbprint is not included to the access tokens
- SetAccessTokenToResponseContext action exploits the flag when creating the access token object (with type)
---
.../context/OIDCAuthenticationResponseContext.java | 24 ++++---
.../op/oauth2/profile/impl/BuildAccessToken.java | 24 ++++++-
.../impl/SetAccessTokenToResponseContext.java | 33 ++++++++-
.../plugin/oidc/op/profile/flow/TokenFlowTest.java | 84 ++++++++++++++++++++++
.../plugin/oidc/op/profile/flow/UserInfoTest.java | 12 ++++
.../shibboleth/idp/module/conf/relying-party.xml | 16 +++++
6 files changed, 177 insertions(+), 16 deletions(-)
diff --git a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/messaging/context/OIDCAuthenticationResponseContext.java b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/messaging/context/OIDCAuthenticationResponseContext.java
index ce7d7bac..3a9d5680 100644
--- a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/messaging/context/OIDCAuthenticationResponseContext.java
+++ b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/messaging/context/OIDCAuthenticationResponseContext.java
@@ -29,7 +29,6 @@ import com.nimbusds.oauth2.sdk.AuthorizationCode;
import com.nimbusds.oauth2.sdk.Scope;
import com.nimbusds.oauth2.sdk.token.AccessToken;
import com.nimbusds.oauth2.sdk.token.BearerAccessToken;
-import com.nimbusds.oauth2.sdk.token.DPoPAccessToken;
import com.nimbusds.oauth2.sdk.token.RefreshToken;
import com.nimbusds.openid.connect.sdk.OIDCClaimsRequest;
import com.nimbusds.openid.connect.sdk.claims.ACR;
@@ -476,17 +475,20 @@ public class OIDCAuthenticationResponseContext extends BaseContext {
*/
public void setAccessToken(@Nullable final String token, @Nonnull final Duration lifeTime,
@Nullable final Scope scope) {
- if (token == null) {
- accessToken = null;
- } else {
- if (getDpopProofJwkThumbprint() == null) {
- accessToken = new BearerAccessToken(token, lifeTime.getSeconds(), scope);
- } else {
- accessToken = new DPoPAccessToken(token, lifeTime.getSeconds(), scope);
- }
- }
+ setAccessToken(token == null ? null : new BearerAccessToken(token, lifeTime.getSeconds(), scope));
}
-
+
+ /**
+ * Set access token.
+ *
+ * @param token the access token
+ *
+ * @since 4.2.0
+ */
+ public void setAccessToken(@Nullable final AccessToken token) {
+ accessToken = token;
+ }
+
/**
* Get refresh token.
*
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/BuildAccessToken.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/BuildAccessToken.java
index 4cf16f6d..383fc041 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/BuildAccessToken.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/BuildAccessToken.java
@@ -58,6 +58,7 @@ import net.shibboleth.idp.plugin.oidc.op.token.support.RefreshTokenClaimsSet;
import net.shibboleth.idp.plugin.oidc.op.token.support.TokenClaimsSet;
import net.shibboleth.idp.plugin.oidc.op.token.support.AccessTokenClaimsSet.Builder;
import net.shibboleth.idp.profile.IdPEventIds;
+import net.shibboleth.oidc.profile.config.logic.AlwaysIssueBearerAccessTokenPredicate;
import net.shibboleth.oidc.profile.config.logic.AttributeConsentFlowEnabledPredicate;
import net.shibboleth.oidc.profile.config.navigate.AccessTokenClaimsSetManipulationStrategyLookupFunction;
import net.shibboleth.oidc.profile.config.navigate.AccessTokenLifetimeLookupFunction;
@@ -145,6 +146,9 @@ public class BuildAccessToken extends AbstractOIDCResponseAction {
private Function<ProfileRequestContext,BiFunction<ProfileRequestContext,Map<String,Object>,Map<String,Object>>>
tokenClaimsSetManipulationStrategyLookupStrategy;
+ /** Condition used to determine whether to always issue bearer access token. */
+ @Nonnull private Predicate<ProfileRequestContext> alwaysIssueBearerAccessTokenCondition;
+
/** The strategy used for manipulating the token claims set. */
@Nullable private BiFunction<ProfileRequestContext,Map<String,Object>,Map<String,Object>> manipulationStrategy;
@@ -208,6 +212,7 @@ public class BuildAccessToken extends AbstractOIDCResponseAction {
tokenClaimsSetManipulationStrategyLookupStrategy =
new AccessTokenClaimsSetManipulationStrategyLookupFunction();
xmlSafeIdentifier = true;
+ alwaysIssueBearerAccessTokenCondition = new AlwaysIssueBearerAccessTokenPredicate();
}
/**
@@ -368,6 +373,18 @@ public class BuildAccessToken extends AbstractOIDCResponseAction {
xmlSafeIdentifier = flag;
}
+ /**
+ * Set the condition used to determine whether to always issue bearer access token.
+ *
+ * @param condition condition to use
+ *
+ * @since 4.2.0
+ */
+ public void setAlwaysIssueBearerAccessTokenCondition(@Nonnull final Predicate<ProfileRequestContext> condition) {
+ alwaysIssueBearerAccessTokenCondition =
+ Constraint.isNotNull(condition, "Always issue bearer access token condition cannot be null");
+ }
+
/** {@inheritDoc} */
@Override
protected void doInitialize() throws ComponentInitializationException {
@@ -499,6 +516,9 @@ public class BuildAccessToken extends AbstractOIDCResponseAction {
final AccessTokenClaimsSet.Builder builder;
+ final boolean alwaysBearerToken = alwaysIssueBearerAccessTokenCondition.test(profileRequestContext);
+ log.debug("{} Always issue bearer access token set to {}", getLogPrefix(), alwaysBearerToken);
+
if (tokenClaimsSet != null) {
final TokenClaimsSet nonNullClaimsSet = tokenClaimsSet;
// We may not use original claims as input for scope / delivery claims as they may have been reduced.
@@ -514,7 +534,7 @@ public class BuildAccessToken extends AbstractOIDCResponseAction {
assert idGenerator != null;
builder.setJWTID(idGenerator, xmlSafeIdentifier);
builder.setSessionIdentifier(responseCtx.getSessionId());
- builder.setDpopProofJwkThumbprint(responseCtx.getDpopProofJwkThumbprint());
+ builder.setDpopProofJwkThumbprint(alwaysBearerToken ? null : responseCtx.getDpopProofJwkThumbprint());
// Set root token identifier to contain jit from the claims set used for building the new token
if (StringSupport.trimOrNull(nonNullClaimsSet.getRootTokenIdentifier()) == null) {
builder.setRootTokenIdentifier(nonNullClaimsSet.getID());
@@ -536,7 +556,7 @@ public class BuildAccessToken extends AbstractOIDCResponseAction {
.setConsentedClaims(consented)
.setConsentEnabled(consentEnabledPredicate.test(profileRequestContext))
.setSessionIdentifier(responseCtx.getSessionId())
- .setDpopProofJwkThumbprint(responseCtx.getDpopProofJwkThumbprint());
+ .setDpopProofJwkThumbprint(alwaysBearerToken ? null : responseCtx.getDpopProofJwkThumbprint());
assert subjectCtx != null;
final String principal = subjectCtx.getPrincipalName();
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/SetAccessTokenToResponseContext.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/SetAccessTokenToResponseContext.java
index f1a245c0..7d876351 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/SetAccessTokenToResponseContext.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/SetAccessTokenToResponseContext.java
@@ -16,6 +16,7 @@ package net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl;
import java.time.Duration;
import java.util.function.Function;
+import java.util.function.Predicate;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
@@ -26,10 +27,13 @@ import org.opensaml.profile.context.navigate.OutboundMessageContextLookup;
import org.slf4j.Logger;
import com.nimbusds.jwt.JWT;
+import com.nimbusds.oauth2.sdk.token.BearerAccessToken;
+import com.nimbusds.oauth2.sdk.token.DPoPAccessToken;
import net.shibboleth.idp.plugin.oidc.op.messaging.context.AccessTokenContext;
import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCAuthenticationResponseContext;
import net.shibboleth.idp.plugin.oidc.op.profile.impl.AbstractOIDCResponseAction;
+import net.shibboleth.oidc.profile.config.logic.AlwaysIssueBearerAccessTokenPredicate;
import net.shibboleth.shared.logic.Constraint;
import net.shibboleth.shared.primitive.LoggerFactory;
@@ -48,7 +52,10 @@ public class SetAccessTokenToResponseContext extends AbstractOIDCResponseAction
/** Strategy used to locate the subcontext with the token. */
@Nonnull private Function<ProfileRequestContext,AccessTokenContext> accessTokenContextLookupStrategy;
-
+
+ /** Condition used to determine whether to always issue bearer access token. */
+ @Nonnull private Predicate<ProfileRequestContext> alwaysIssueBearerAccessTokenCondition;
+
/** Token context. */
@Nullable private AccessTokenContext tokenCtx;
@@ -61,6 +68,7 @@ public class SetAccessTokenToResponseContext extends AbstractOIDCResponseAction
new OutboundMessageContextLookup()));
assert atcls != null;
accessTokenContextLookupStrategy = atcls;
+ alwaysIssueBearerAccessTokenCondition = new AlwaysIssueBearerAccessTokenPredicate();
}
/**
@@ -75,7 +83,19 @@ public class SetAccessTokenToResponseContext extends AbstractOIDCResponseAction
accessTokenContextLookupStrategy =
Constraint.isNotNull(strategy, "AccessTokenContext lookup strategy cannot be null");
}
-
+
+ /**
+ * Set the condition used to determine whether to always issue bearer access token.
+ *
+ * @param condition condition to use
+ *
+ * @since 4.2.0
+ */
+ public void setAlwaysIssueBearerAccessTokenCondition(@Nonnull final Predicate<ProfileRequestContext> condition) {
+ alwaysIssueBearerAccessTokenCondition =
+ Constraint.isNotNull(condition, "Always issue bearer access token condition cannot be null");
+ }
+
/** {@inheritDoc} */
@Override
protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
@@ -125,7 +145,14 @@ public class SetAccessTokenToResponseContext extends AbstractOIDCResponseAction
assert oidcResponseContext != null;
final Duration lifetime = nonNullTokenCtx.getLifetime();
assert lifetime != null;
- oidcResponseContext.setAccessToken(token, lifetime, oidcResponseContext.getScope());
+ final boolean alwaysBearer = alwaysIssueBearerAccessTokenCondition.test(profileRequestContext);
+ if (oidcResponseContext.getDpopProofJwkThumbprint() == null || alwaysBearer) {
+ oidcResponseContext.setAccessToken(
+ new BearerAccessToken(token, lifetime.getSeconds(), oidcResponseContext.getScope()));
+ } else {
+ oidcResponseContext.setAccessToken(
+ new DPoPAccessToken(token, lifetime.getSeconds(), oidcResponseContext.getScope()));
+ }
}
}
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/TokenFlowTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/TokenFlowTest.java
index 9b6a49ba..dd17561f 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/TokenFlowTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/TokenFlowTest.java
@@ -101,6 +101,8 @@ public class TokenFlowTest extends AbstractOidcClientAuthenticationFlowTest {
String clientIdJwtRefreshToken = "mockClientIdNotMDDrivenRefreshTokenJwt";
String clientIdDPoPAccessToken = "mockClientIdDPoPAccessToken";
String clientIdDPoPJwtAccessToken = "mockClientIdDPoPJwtAccessToken";
+ String clientIdDPoPJwtBearerAccessToken = "mockClientIdDPoPJwtAlwaysBearerAccessToken";
+ String clientIdAlwaysBearerAccessToken = "mockClientIdAlwaysBearerAccessToken";
String codeVerifier = "9234567812345678123456781234567812345678123456781234567812345678";
String resourceUri = "https://rp.example.org";
@@ -135,6 +137,8 @@ public class TokenFlowTest extends AbstractOidcClientAuthenticationFlowTest {
removeMetadata(storageService, clientIdNoIdTokenViaRefreshToken);
removeMetadata(storageService, clientIdDPoPAccessToken);
removeMetadata(storageService, clientIdDPoPJwtAccessToken);
+ removeMetadata(storageService, clientIdDPoPJwtBearerAccessToken);
+ removeMetadata(storageService, clientIdAlwaysBearerAccessToken);
}
@Test
@@ -747,6 +751,47 @@ public class TokenFlowTest extends AbstractOidcClientAuthenticationFlowTest {
Assert.assertNotNull(response.getOIDCTokens().getIDToken());
}
+ @Test
+ public void testDPoPValidGrantThumbprintIncludedMatchingProof_alwaysBearerToken() throws Exception {
+ final String clientId = clientIdAlwaysBearerAccessToken;
+ final AuthorizeCodeClaimsSet.Builder builder = new AuthorizeCodeClaimsSet.Builder();
+ final SignedJWT dpopProof = buildDPoPProof("POST", "http://localhost/idp/profile/oidc/token",
+ createValidDPoPNonce());
+ final String jkt = dpopProof.getHeader().getJWK().computeThumbprint().toString();
+ builder.setJWTID(new SecureRandomIdentifierGenerationStrategy())
+ .setClientID(new ClientID(clientId))
+ .setIssuer("https://op.example.org")
+ .setPrincipal("jdoe")
+ .setSubject("mock")
+ .setIssuedAt(Instant.now())
+ .setExpiresAt(Instant.now().plusSeconds(100))
+ .setAuthenticationTime(Instant.now())
+ .setRedirectURI(new URI(redirectUri))
+ .setScope(scope)
+ .setDpopProofJwkThumbprint(jkt);
+ final String authorizationCode = builder.build().serialize(getDataSealer());
+ request.addHeader("DPoP", dpopProof.serialize());
+
+ initializeGrantAndRequest(clientId, createRequestParameters(redirectUri,
+ "authorization_code",
+ authorizationCode, clientId));
+ storeConsent(storageService, "jdoe", clientId, "mail");
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ final OIDCTokenResponse response = parseSuccessResponse(result, OIDCTokenResponse.class);
+ final AccessToken accessToken = response.getTokens().getAccessToken();
+ Assert.assertNotNull(accessToken);
+ Assert.assertEquals(accessToken.getType(), AccessTokenType.BEARER);
+ final AccessTokenClaimsSet accessTokenClaims = AccessTokenClaimsSet.parse(accessToken.getValue(),
+ getDataSealer());
+ Assert.assertNull(accessTokenClaims.getDpopProofJwkThumbprint());
+ final RefreshToken refreshToken = response.getTokens().getRefreshToken();
+ Assert.assertNotNull(refreshToken);
+ final RefreshTokenClaimsSet refreshTokenClaims = RefreshTokenClaimsSet.parse(refreshToken.getValue(),
+ getDataSealer());
+ Assert.assertEquals(refreshTokenClaims.getDpopProofJwkThumbprint(), jkt);
+ Assert.assertNotNull(response.getOIDCTokens().getIDToken());
+ }
+
@Test
public void testDPoPValidGrantThumbprintIncludedMatchingProof_publicClient() throws Exception {
final String clientId = clientIdPkcePlainPublic;
@@ -1162,6 +1207,45 @@ public class TokenFlowTest extends AbstractOidcClientAuthenticationFlowTest {
Assert.assertNotNull(response.getOIDCTokens().getIDToken());
}
+ @Test
+ public void testDPoPJwtEnforcedValidGrantThumbprintIncludedMatchingProof_alwaysBearerToken() throws Exception {
+ final String clientId = clientIdDPoPJwtBearerAccessToken;
+ final AuthorizeCodeClaimsSet.Builder builder = new AuthorizeCodeClaimsSet.Builder();
+ final SignedJWT dpopProof = buildDPoPProof("POST", "http://localhost/idp/profile/oidc/token",
+ createValidDPoPNonce());
+ builder.setJWTID(new SecureRandomIdentifierGenerationStrategy())
+ .setClientID(new ClientID(clientId))
+ .setIssuer("https://op.example.org")
+ .setPrincipal("jdoe")
+ .setSubject("mock")
+ .setIssuedAt(Instant.now())
+ .setExpiresAt(Instant.now().plusSeconds(100))
+ .setAuthenticationTime(Instant.now())
+ .setRedirectURI(new URI(redirectUri))
+ .setScope(scope)
+ .setDpopProofJwkThumbprint(dpopProof.getHeader().getJWK().computeThumbprint().toString());
+ final String authorizationCode = builder.build().serialize(getDataSealer());
+ request.addHeader("DPoP", dpopProof.serialize());
+
+ initializeGrantAndRequest(clientId, createRequestParameters(redirectUri,
+ "authorization_code",
+ authorizationCode, clientId));
+ storeConsent(storageService, "jdoe", clientId, "mail");
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ final OIDCTokenResponse response = parseSuccessResponse(result, OIDCTokenResponse.class);
+ Assert.assertNotNull(response.getTokens().getAccessToken());
+ Assert.assertEquals(response.getTokens().getAccessToken().getType(), AccessTokenType.BEARER);
+ final SignedJWT signedJwt = SignedJWT.parse(response.getTokens().getAccessToken().getValue());
+ Assert.assertNull(signedJwt.getJWTClaimsSet().getJSONObjectClaim("cnf"));
+ final RefreshToken refreshToken = response.getTokens().getRefreshToken();
+ Assert.assertNotNull(refreshToken);
+ final RefreshTokenClaimsSet refreshTokenClaims = RefreshTokenClaimsSet.parse(refreshToken.getValue(),
+ getDataSealer());
+ Assert.assertEquals(refreshTokenClaims.getDpopProofJwkThumbprint(),
+ dpopProof.getHeader().getJWK().computeThumbprint().toString());
+ Assert.assertNotNull(response.getOIDCTokens().getIDToken());
+ }
+
@Test
public void testDPoPJwtEnforcedValidGrantThumbprintNotIncluded() throws Exception {
final String clientId = clientIdDPoPJwtAccessToken;
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/UserInfoTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/UserInfoTest.java
index e90d1fb2..74d61b2c 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/UserInfoTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/UserInfoTest.java
@@ -407,6 +407,18 @@ public class UserInfoTest extends AbstractOidcApiFlowTest {
Assert.assertNull(response.getUserInfoJWT());
}
+ @Test
+ public void testFailWithBearerTokenWhenDPoPEnforced() throws URISyntaxException, NoSuchAlgorithmException, DataSealerException,
+ ComponentInitializationException, IOException {
+ final String clientId = "mockClientIdDPoPProofEnforced";
+ final BearerAccessToken token = buildToken(clientId, subject, new Scope("openid"));
+ storeMetadata(storageService, clientId, "mockSecret", scope);
+ request.addHeader("Authorization", getTokenHeaderValue(token));
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ assertInvalidDPoPToken(result);
+ removeMetadata(storageService, clientId);
+ }
+
@SuppressWarnings("null")
@Test
public void testFailWithJWTDPoP_noAth() throws URISyntaxException, NoSuchAlgorithmException, DataSealerException,
diff --git a/idp-oidc-extension-impl/src/test/resources/net/shibboleth/idp/module/conf/relying-party.xml b/idp-oidc-extension-impl/src/test/resources/net/shibboleth/idp/module/conf/relying-party.xml
index c4c44acc..c75c3c7b 100644
--- a/idp-oidc-extension-impl/src/test/resources/net/shibboleth/idp/module/conf/relying-party.xml
+++ b/idp-oidc-extension-impl/src/test/resources/net/shibboleth/idp/module/conf/relying-party.xml
@@ -149,6 +149,15 @@
</list>
</property>
</bean>
+ <bean parent="RelyingPartyByName" c:relyingPartyIds="mockClientIdAlwaysBearerAccessToken">
+ <property name="profileConfigurations">
+ <list>
+ <bean parent="OAUTH2.Token.MDDriven" p:alwaysIssueBearerAccessToken="true"/>
+ <bean parent="OIDC.UserInfo.MDDriven" />
+ <bean parent="OAUTH2.Introspection.MDDriven"/>
+ </list>
+ </property>
+ </bean>
<bean parent="RelyingPartyByName" c:relyingPartyIds="mockClientIdRequestObjectEnforced">
<property name="profileConfigurations">
<list>
@@ -263,6 +272,13 @@
</list>
</property>
</bean>
+ <bean parent="RelyingPartyByName" c:relyingPartyIds="mockClientIdDPoPJwtAlwaysBearerAccessToken">
+ <property name="profileConfigurations">
+ <list>
+ <bean parent="OAUTH2.Token.MDDriven" p:requireDpopProof="true" p:accessTokenType="JWT" p:encryptionOptional="true" p:alwaysIssueBearerAccessToken="true" />
+ </list>
+ </property>
+ </bean>
<bean parent="RelyingPartyByName" c:relyingPartyIds="mockClientIdNoRefreshTokensInSSOProfile">
<property name="profileConfigurations">
<list>
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.
More information about the commits
mailing list