[java-idp-plugin-oidc-rp] branch main updated: Add UserInfo token exchange and UserInfo claim validation
Phil Smart
philip.smart at jisc.ac.uk
Thu Feb 10 10:06:56 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=ebaa1b4b2958dad9a14914ee564e6895f7837d31
The following commit(s) were added to refs/heads/main by this push:
new ebaa1b4 Add UserInfo token exchange and UserInfo claim validation
ebaa1b4 is described below
commit ebaa1b4b2958dad9a14914ee564e6895f7837d31
Author: Phil Smart <philip.smart at jisc.ac.uk>
AuthorDate: Thu Feb 10 10:06:50 2022 +0000
Add UserInfo token exchange and UserInfo claim validation
Supports JWT UserInfo tokens (pending signature validation and
decryption) and plain JSON object tokens.
Merge claims from id_token and UserInfo claims.
---
...ontext.java => AccessTokenResponseContext.java} | 2 +-
.../oidc/rp/context/EndUserClaimsContext.java | 53 +++
.../oidc/rp/context/OpenIDConnectContext.java | 454 ---------------------
.../oidc/rp/context/UserInfoResponseContext.java | 51 +++
.../oidc/rp/messaging/JWTUserInfoResponse.java | 107 +++++
.../oidc/rp/messaging/PlainUserInfoResponse.java | 76 ++++
.../authn/oidc/rp/messaging/UserInfoResponse.java | 71 ++++
.../oidc/rp/messaging/JWTUserInfoResponseTest.java | 102 +++++
idp-oidc-rp-impl/pom.xml | 20 +
...va => AbstractJSONResponseDecoderFunction.java} | 68 +--
...Decoder.java => DefaultMapResponseDecoder.java} | 46 +--
.../impl/DefaultUserInfoResponseDecoder.java | 106 +++++
.../encoding/impl/DefaultTokenRequestEncoder.java | 1 +
.../impl/DefaultUserInfoRequestEncoder.java | 116 ++++++
.../impl/AbstractHttpOIDCAuthenticationAction.java | 101 ++++-
.../AbstractOIDCAuthenticationResponseAction.java | 2 +-
.../plugin/authn/oidc/rp/impl/AddAuthzRequest.java | 10 +-
.../oidc/rp/impl/AuthorizationController.java | 3 -
.../oidc/rp/impl/DefaultClaimMergingStrategy.java | 88 ++++
.../oidc/rp/impl/DefaultIDTokenLookupStrategy.java | 53 ++-
.../impl/DefaultUserInfoTokenLookupStrategy.java | 78 ++++
.../oidc/rp/impl/ExchangeCodeForAccessToken.java | 88 +---
.../oidc/rp/impl/ExtractIDTokenFromResponse.java | 30 +-
.../rp/impl/MergeUserInfoAndIDTokenClaims.java | 218 ++++++++++
.../impl/TokenResponseIDTokenLookupStrategy.java | 4 +-
.../authn/oidc/rp/impl/UserInfoEndpointLookup.java | 99 +++++
.../oidc/rp/impl/ValidateIDTokenSignature.java | 61 +--
.../rp/impl/ValidateOAuthAccessTokenResponse.java | 31 +-
.../oidc/rp/impl/ValidateOIDCAuthentication.java | 307 +++++++++-----
.../authn/oidc/rp/impl/ValidateTokenClaims.java | 28 +-
.../authn/oidc/rp/impl/ValidateUserInfoClaims.java | 171 ++++++++
.../META-INF/net.shibboleth.idp/postconfig.xml | 8 +
.../oidc-relying-party-authn-beans.xml | 112 ++++-
.../oidc-relying-party-authn-flow.xml | 61 ++-
.../attribute/registry/oidc-claim-rules.xml | 428 +++++++++++++++++++
.../idp/service/attribute/registry/postconfig.xml | 25 ++
.../resources/templates/oidc-request-form-post.vm | 4 +-
.../impl/DefaultTokenResponseDecoderTest.java | 4 +-
.../authn/oidc/rp/impl/AbstractOIDCTest.java | 10 +
.../rp/impl/DefaultClaimMergingStrategyTest.java | 97 +++++
.../oidc/rp/impl/ExchangeCodeForTokenTest.java | 17 +-
.../rp/impl/ExtractIDTokenFromResponseTest.java | 6 +-
.../plugin/authn/oidc/rp/impl/OIDCRPFlowTest.java | 374 +++++++++++++----
.../TokenResponseIDTokenLookupStrategyTest.java | 6 +-
.../attribute/registry/attribute-registry.xml | 442 ++++++++++++++++++++
.../resources/attribute/registry/postconfig.xml | 67 +++
.../src/test/resources/logback-test.xml | 1 +
pom.xml | 12 +
48 files changed, 3370 insertions(+), 949 deletions(-)
diff --git a/idp-oidc-rp-api/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/context/TokenResponseContext.java b/idp-oidc-rp-api/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/context/AccessTokenResponseContext.java
similarity index 97%
rename from idp-oidc-rp-api/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/context/TokenResponseContext.java
rename to idp-oidc-rp-api/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/context/AccessTokenResponseContext.java
index 104db77..b8a16a9 100644
--- a/idp-oidc-rp-api/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/context/TokenResponseContext.java
+++ b/idp-oidc-rp-api/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/context/AccessTokenResponseContext.java
@@ -26,7 +26,7 @@ import org.opensaml.messaging.context.BaseContext;
import com.nimbusds.jwt.JWT;
/** A context to hold an OIDC token request response.*/
-public class TokenResponseContext extends BaseContext {
+public class AccessTokenResponseContext extends BaseContext {
/** The raw token response as a map.*/
@Nullable private Map<String, Object> rawTokenResponse;
diff --git a/idp-oidc-rp-api/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/context/EndUserClaimsContext.java b/idp-oidc-rp-api/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/context/EndUserClaimsContext.java
new file mode 100644
index 0000000..f4c528b
--- /dev/null
+++ b/idp-oidc-rp-api/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/context/EndUserClaimsContext.java
@@ -0,0 +1,53 @@
+/*
+ * 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.context;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.BaseContext;
+
+import com.nimbusds.openid.connect.sdk.claims.ClaimsSet;
+
+import net.shibboleth.utilities.java.support.logic.Constraint;
+
+/** A context to hold the final set of claims associated with an authenticated end-user.*/
+public class EndUserClaimsContext extends BaseContext {
+
+ /** The claims associated with the authenticated end-user.*/
+ @Nullable private ClaimsSet endUserClaims;
+
+ /**
+ * Get the claims about the authenticated end-user.
+ *
+ * @return the claims.
+ */
+ @Nullable public ClaimsSet getEndUserClaims() {
+ return endUserClaims;
+ }
+
+ /**
+ * Set the claims about the authenticated end-user.
+ *
+ * @param claims the claims.
+ */
+ public void setEndUserClaims(@Nonnull final ClaimsSet claims) {
+ endUserClaims = Constraint.isNotNull(claims, "Claims can not be null");
+ }
+
+}
diff --git a/idp-oidc-rp-api/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/context/OpenIDConnectContext.java b/idp-oidc-rp-api/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/context/OpenIDConnectContext.java
deleted file mode 100644
index 7c16c92..0000000
--- a/idp-oidc-rp-api/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/context/OpenIDConnectContext.java
+++ /dev/null
@@ -1,454 +0,0 @@
-/*
- * 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.context;
-
-import java.net.URI;
-import java.net.URISyntaxException;
-import java.util.List;
-import java.util.Map;
-
-import javax.annotation.Nonnull;
-import javax.annotation.Nullable;
-import javax.servlet.http.HttpServletRequest;
-
-import net.shibboleth.idp.attribute.IdPAttribute;
-import net.shibboleth.idp.authn.context.AuthenticationContext;
-import net.shibboleth.utilities.java.support.annotation.constraint.Live;
-import net.shibboleth.utilities.java.support.annotation.constraint.NonnullElements;
-import net.shibboleth.utilities.java.support.annotation.constraint.NotLive;
-import net.shibboleth.utilities.java.support.annotation.constraint.Unmodifiable;
-import net.shibboleth.utilities.java.support.logic.Constraint;
-
-import org.opensaml.messaging.context.BaseContext;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import com.google.common.base.Predicates;
-import com.google.common.collect.Collections2;
-import com.google.common.collect.ImmutableList;
-import com.nimbusds.jwt.JWT;
-import com.nimbusds.oauth2.sdk.Scope;
-import com.nimbusds.oauth2.sdk.auth.Secret;
-import com.nimbusds.oauth2.sdk.id.ClientID;
-import com.nimbusds.oauth2.sdk.id.State;
-import com.nimbusds.openid.connect.sdk.AuthenticationSuccessResponse;
-import com.nimbusds.openid.connect.sdk.Display;
-import com.nimbusds.openid.connect.sdk.Nonce;
-import com.nimbusds.openid.connect.sdk.OIDCTokenResponse;
-import com.nimbusds.openid.connect.sdk.Prompt;
-import com.nimbusds.openid.connect.sdk.claims.ACR;
-import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata;
-
-
-/**
- * Context that carries OpenID Connect client and provider metadata used during authentication of a subject from
- * an OpenID Connect Provider.
- *
- * <p>Any OPTIONAL parameters of an ODIC authentication request are allowably <code>null</code> - even Lists.
- * For a list of OPTIONAL authn parameters, see OpenID Connect Core 1.0 section 3.1.2.1</p>
- *
- *
- * @parent {@link AuthenticationContext}
- * @added After the RelyingPartyUIContext has been added, before external redirect to the OpenID Connect servlet.
- *
- * @since 4.0.0
- */
-public class OpenIDConnectContext extends BaseContext {
-
- /** Class logger. */
- @Nonnull private final Logger log = LoggerFactory.getLogger(OpenIDConnectContext.class);
-
- /** Client Id. */
- @Nullable private ClientID clientID;
-
- /** Client Secret. */
- @Nullable private Secret clientSecret;
-
- /** Scope. Must contain an openid scope.*/
- @Nullable private Scope scope;
-
- /**
- * OIDC Prompt. Specifies whether the Authorization Server prompts
- * the End-User for re-authentication and consent.
- */
- @Nullable private Prompt prompt;
-
- /** OIDC Authentication Class Reference values.*/
- @Nullable @NonnullElements private List<ACR> acrs;
-
- /**
- * OIDC Display. Value that specifies how the Authorization Server
- * displays the authentication and consent user interface pages to the End-User.
- */
- @Nullable private Display display;
-
- /** OIDC provider metadata. */
- @Nullable private OIDCProviderMetadata oIDCProviderMetadata;
-
- /** OIDC authentication request URI. */
- @Nullable private URI authenticationRequestURI;
-
- /** OIDC authentication response URI. */
- @Nullable private URI authenticationResponseURI;
-
- /** OIDC authentication success response. */
- @Nullable private AuthenticationSuccessResponse authSuccessResponse;
-
- /** OIDC token response. */
- @Nullable private OIDCTokenResponse oidcTknResponse;
-
- /**
- * State parameter. Opaque value used to maintain state between the request
- * and the callback. Typically for CSRF protection.
- */
- @Nullable private State state;
-
- /**
- * Nonce parameter. String value used to associate a Client session with an
- * ID Token, and to mitigate replay attacks.
- */
- @Nullable private Nonce nonce;
-
- /** Redirect URI to which the response will be sent. */
- @Nullable private URI redirectURI;
-
- /** ID Token from the token endpoint response. */
- @Nullable private JWT idToken;
-
- /** Resolved attributes. */
- //TODO Resolved Attributes never get added to the Context.
- @Nullable private Map<String, IdPAttribute> resolvedIdPAttributes;
-
-
- /**
- * Get the resolved attributes.
- *
- * @return resolved attributes, may be null.
- */
- @Nullable @Live public Map<String, IdPAttribute> getResolvedIdPAttributes() {
- return resolvedIdPAttributes;
- }
-
- /**
- * Set resolved attributes to context to help form requested objects.
- *
- * @param idPAttributes resolved attributes
- */
- public void setResolvedIdPAttributes(@Nullable final Map<String, IdPAttribute> idPAttributes) {
- resolvedIdPAttributes = idPAttributes;
- }
-
- /**
- * Get the id token received from OpenID Connect Provider.
- *
- * @return id token or <code>null</code> if not set.
- */
- @Nullable public JWT getIDToken() {
- return idToken;
- }
-
- /**
- * Set the id token received from OpenID Connect Provider.
- *
- * @param token from the OpenID Connect Provider.
- */
- public void setIDToken(@Nullable final JWT token) {
- idToken = token;
- }
-
- /**
- * Get the redirect URI of the client.
- *
- * @return redirect uri
- */
- @Nullable public URI getRedirectURI() {
- return redirectURI;
- }
-
- /**
- * Set the redirect URI of the client. The redirect URI is required for authorisation code flows.
- *
- * @param uri of the client, must not be <code>null</code>.
- */
- public void setRedirectURI(@Nonnull final URI uri) {
- redirectURI = Constraint.isNotNull(uri, "Redirect URI cannot be null");
- }
-
- /**
- * Get the OAuth2 client id.
- *
- * @return client id.
- */
- @Nullable public ClientID getClientID() {
- return clientID;
- }
-
- /**
- * Set the Oauth2 client id. The client id can not be <code>null</code>.
- *
- * @param id Oauth2 Client ID, must not be <code>null</code>.
- */
- public void setClientID(@Nonnull final ClientID id) {
- clientID = Constraint.isNotNull(id, "Client ID cannot be null");
- }
-
- /**
- * Set the client secret.
- *
- * @param secret of the client, must not be <code>null</code>.
- */
- public void setClientSecret(@Nonnull final Secret secret) {
- clientSecret = Constraint.isNotNull(secret, "Client secret cannot be null");
- }
-
- /**
- * Get the client secret.
- *
- * @return client secret.
- */
- @Nullable public Secret getClientSecret() {
- return clientSecret;
- }
-
- /**
- * Get the scope used for authentication request.
- *
- * @return scope
- */
- public Scope getScope() {
- return scope;
- }
-
- /**
- * Set the scope used for forming an authentication request.
- *
- * @param scp scope of the request, must not be <code>null</code>
- */
- public void setScope(@Nonnull final Scope scp) {
- scope = Constraint.isNotNull(scp, "Scope cannot be null");
- }
-
- /**
- * Get the value for prompt used for forming an authentication request.
- *
- * @return prompt
- */
- @Nullable public Prompt getPrompt() {
- return prompt;
- }
-
- /**
- * Set the value for prompt used for forming an authentication request.
- *
- * @param prmpt used for forming an authentication request
- */
- public void setPrompt(@Nullable final Prompt prmpt) {
- prompt = prmpt;
- }
-
- /**
- * Get the values for acr used for forming authentication request.
- * Allowably a <code>null</code> {@link List} if not set.
- *
- * @return acrs or <code>null</code> if not set.
- */
- @Nullable @NonnullElements @Unmodifiable @NotLive public List<ACR> getAcrs() {
- return acrs;
- }
-
- /**
- * Set the values for acr used for forming authentication request. Sets a
- * <code>null</code> {@link List} if no ACRs are defined.
- *
- * @param acrList values used for forming authentication request
- */
- public void setAcrs(@Nullable @NonnullElements final List<ACR> acrList) {
- if (acrList!=null) {
- ImmutableList.copyOf(Collections2.filter(acrList, Predicates.notNull()));
- } else {
- acrs = null;
- }
-
- }
-
- /**
- * Get the display value used for forming authentication request.
- *
- * @return display
- */
- @Nullable public Display getDisplay() {
- return display;
- }
-
- /**
- * Set the display value used for forming authentication request.
- *
- * @param dspl value for forming authentication request.
- */
- public void setDisplay(@Nullable final Display dspl) {
- display = dspl;
- }
-
- /**
- * Get OpenID Connect Provider metadata.
- *
- * @return OpenID Connect Provider metadata
- */
- @Nullable public OIDCProviderMetadata getoIDCProviderMetadata() {
- return oIDCProviderMetadata;
- }
-
- /**
- * Set OpenID Connect Provider metatadata.
- *
- * @param oIDCPrvdrMtdt OpenID Connect Provider metadata, must not be <code>null</code>.
- */
- public void setoIDCProviderMetadata(@Nonnull final OIDCProviderMetadata oIDCPrvdrMtdt) {
- oIDCProviderMetadata = Constraint.isNotNull(oIDCPrvdrMtdt, "OpenID Connect metadata location cannot be null ");
- }
-
- /**
- * Returns the oidc authentication request URI to be used for authentication.
- *
- * @return request URI for authentication
- */
- @Nullable public URI getAuthenticationRequestURI() {
- return authenticationRequestURI;
- }
-
- /**
- * Set the oidc provider request URI for authentication.
- *
- * @param request to be used for authentication, must not be <code>null</code>.
- */
- public void setAuthenticationRequestURI(@Nonnull final URI request) {
- authenticationRequestURI = Constraint.isNotNull(request,
- "Authentication Request URI request can not be null");
-
- }
-
- /**
- * Returns the OIDC token response or null.
- *
- * @return token response
- */
- @Nullable public OIDCTokenResponse getOidcTokenResponse() {
- return oidcTknResponse;
- }
-
- /**
- * Sets both the token response and the <code>idToken</code> from the
- * token response if it is not <code>null</code>.
- *
- * @param oidcTokenResponse response from provider
- */
- public void setOidcTokenResponse(@Nullable final OIDCTokenResponse oidcTokenResponse) {
- oidcTknResponse = oidcTokenResponse;
- if (oidcTokenResponse != null && oidcTokenResponse.getOIDCTokens() != null) {
- idToken = oidcTokenResponse.getOIDCTokens().getIDToken();
- }
-
- }
-
- /**
- * Getter for State parameter.
- *
- * @return state parameter
- */
- @Nullable public State getState() {
- return state;
- }
-
- /**
- * Setter for State parameter.
- *
- * @param stateParam parameter
- */
- public void setState(@Nullable final State stateParam) {
- state = stateParam;
- }
-
- /**
- * Getter for Nonce parameter.
- *
- * @return nonce parameter.
- */
- @Nullable public Nonce getNonce() {
- return nonce;
- }
-
- /**
- * Setter for Nonce parameter.
- *
- * @param newNonce nonce parameter
- */
- public void setNonce(@Nullable final Nonce newNonce) {
- nonce = newNonce;
- }
-
- /**
- * Returns the authentication success response or null.
- *
- * @return authentication success response.
- */
- @Nullable public AuthenticationSuccessResponse getAuthenticationSuccessResponse() {
- return authSuccessResponse;
- }
-
- /**
- * Sets the authentication success response.
- *
- * @param authenticationSuccessResponse response from the OpenIDConenct Provider
- */
- public void setAuthenticationSuccessResponse(
- @Nonnull final AuthenticationSuccessResponse authenticationSuccessResponse) {
- authSuccessResponse = Constraint.isNotNull(authenticationSuccessResponse,
- "AuthenticationSuccessResponse can not be null");
-
- }
-
- /**
- * Returns the authentication response URI or null.
- *
- * @return authentication response URI
- */
- @Nullable public URI getAuthenticationResponseURI() {
- return authenticationResponseURI;
- }
-
- /**
- * Parses the authentication response URI from the authenticationResponseHttpRequest.
- *
- * @param authenticationResponseHttpRequest request
- *
- * @throws URISyntaxException if request has malformed URL and/or query parameters
- */
- public void setAuthenticationResponseURI(@Nonnull final HttpServletRequest authenticationResponseHttpRequest)
- throws URISyntaxException {
-
- Constraint.isNotNull(authenticationResponseHttpRequest, "Authentication response http request can not be null");
-
- final String temp = authenticationResponseHttpRequest.getRequestURL() + "?"
- + authenticationResponseHttpRequest.getQueryString();
- authenticationResponseURI = new URI(temp);
-
- }
-
-
-
-}
diff --git a/idp-oidc-rp-api/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/context/UserInfoResponseContext.java b/idp-oidc-rp-api/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/context/UserInfoResponseContext.java
new file mode 100644
index 0000000..3ad71c8
--- /dev/null
+++ b/idp-oidc-rp-api/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/context/UserInfoResponseContext.java
@@ -0,0 +1,51 @@
+/*
+ * 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.context;
+
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.BaseContext;
+
+import net.shibboleth.idp.plugin.authn.oidc.rp.messaging.UserInfoResponse;
+
+
+/** A context to hold the response from a UserInfo endpoint.*/
+public class UserInfoResponseContext extends BaseContext {
+
+ /** The UserInfo response.*/
+ @Nullable private UserInfoResponse userInfo;
+
+ /**
+ * Get the user info response.
+ *
+ * @return the user info.
+ */
+ @Nullable public UserInfoResponse getUserInfo() {
+ return userInfo;
+ }
+
+ /**
+ * Set the user info response.
+ *
+ * @param info the user info response.
+ */
+ public void setUserInfo(@Nullable final UserInfoResponse info) {
+ userInfo = info;
+ }
+
+}
diff --git a/idp-oidc-rp-api/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/messaging/JWTUserInfoResponse.java b/idp-oidc-rp-api/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/messaging/JWTUserInfoResponse.java
new file mode 100644
index 0000000..82a0423
--- /dev/null
+++ b/idp-oidc-rp-api/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/messaging/JWTUserInfoResponse.java
@@ -0,0 +1,107 @@
+/*
+ * 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.messaging;
+
+import java.text.ParseException;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import com.nimbusds.jwt.EncryptedJWT;
+import com.nimbusds.jwt.JWT;
+import com.nimbusds.jwt.SignedJWT;
+import com.nimbusds.openid.connect.sdk.claims.ClaimsSet;
+
+import net.shibboleth.utilities.java.support.logic.Constraint;
+
+/** A UserInfo response returned as a JWT which maybe signed and or encrypted.*/
+public class JWTUserInfoResponse implements UserInfoResponse {
+
+ /** The UserInfo claims inside a JWT.*/
+ @Nonnull private final JWT responseJwt;
+
+ /**
+ * The claims represented by the JWT, could be null if JWT is encrypted
+ * and has not yet been decrypted.
+ */
+ @Nullable private ClaimsSet claims;
+
+ /**
+ *
+ * Constructor.
+ *
+ * @param jwt the plain, signed, and or encrypted JWT representing UserInfo claims.
+ */
+ public JWTUserInfoResponse(@Nonnull final JWT jwt) {
+ responseJwt = Constraint.isNotNull(jwt, "JWT UserInfo response can not be null");
+
+ try {
+ if (jwt.getJWTClaimsSet() != null) {
+ claims = new ClaimsSet();
+ claims.putAll(jwt.getJWTClaimsSet().getClaims());
+ }
+ } catch (final ParseException e) {
+ // do nothing, claims are null, likely encrypted JWT.
+ claims = null;
+ }
+
+ }
+
+ /**
+ * Get the UserInfo JWT response.
+ *
+ * @return the UserInfo JWT response.
+ */
+ @Nonnull public JWT getResponseJwt() {
+ return responseJwt;
+ }
+
+ @Override
+ @Nullable public ClaimsSet getClaimsSet() {
+ return claims;
+ }
+
+ @Override
+ @Nullable public String getSub() {
+ if (claims != null) {
+ return claims.getStringClaim("sub");
+ }
+ return null;
+ }
+
+ @Override
+ public boolean isClaimsSetAvailable() {
+ return claims != null;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * There is no easy way to determine if signed when nested inside a JWE, unless first decrypted.
+ */
+ @Override
+ public boolean isSigned() {
+ return responseJwt instanceof SignedJWT;
+ }
+
+ @Override
+ public boolean isEncrypted() {
+ return responseJwt instanceof EncryptedJWT;
+ }
+
+}
diff --git a/idp-oidc-rp-api/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/messaging/PlainUserInfoResponse.java b/idp-oidc-rp-api/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/messaging/PlainUserInfoResponse.java
new file mode 100644
index 0000000..25587d0
--- /dev/null
+++ b/idp-oidc-rp-api/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/messaging/PlainUserInfoResponse.java
@@ -0,0 +1,76 @@
+/*
+ * 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.messaging;
+
+import javax.annotation.Nonnull;
+
+import com.nimbusds.openid.connect.sdk.claims.ClaimsSet;
+
+import net.shibboleth.utilities.java.support.logic.Constraint;
+
+/** A UserInfo response that was a plain JSON object with claims.*/
+public class PlainUserInfoResponse implements UserInfoResponse {
+
+ /** The UserInfo claims inside a JWT.*/
+ @Nonnull private final ClaimsSet userInfoClaims;
+
+ /**
+ *
+ * Constructor.
+ *
+ * @param claims UserInfo claims.
+ */
+ public PlainUserInfoResponse(@Nonnull final ClaimsSet claims) {
+ userInfoClaims = Constraint.isNotNull(claims, "UserInfo claims can not be null");
+ }
+
+ /**
+ * The consented UserInfo claims.
+ *
+ * @return the claims.
+ */
+ @Nonnull public ClaimsSet getUserInfoClaims() {
+ return userInfoClaims;
+ }
+
+ @Override
+ public ClaimsSet getClaimsSet() {
+ return userInfoClaims;
+ }
+
+ @Override
+ public String getSub() {
+ return userInfoClaims.getStringClaim("sub");
+ }
+
+ @Override
+ public boolean isClaimsSetAvailable() {
+ return true;
+ }
+
+ @Override
+ public boolean isSigned() {
+ return false;
+ }
+
+ @Override
+ public boolean isEncrypted() {
+ return false;
+ }
+
+}
diff --git a/idp-oidc-rp-api/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/messaging/UserInfoResponse.java b/idp-oidc-rp-api/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/messaging/UserInfoResponse.java
new file mode 100644
index 0000000..a7dc0ca
--- /dev/null
+++ b/idp-oidc-rp-api/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/messaging/UserInfoResponse.java
@@ -0,0 +1,71 @@
+/*
+ * 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.messaging;
+
+import javax.annotation.Nullable;
+
+import com.nimbusds.openid.connect.sdk.claims.ClaimsSet;
+
+/**
+ * A User information response. Contains consent claims about a subject. A response
+ * is either a plain JSON object or a JWT.
+ */
+public interface UserInfoResponse {
+
+ /**
+ * Get the claims for the authenticated end-user.
+ *
+ * @return the claims for the authenticated end-user. Can be {@literal null} if
+ * the underlying claims are not available and {@link #isClaimsSetAvailable()} is false
+ * e.g. an encrypted JWT which has not yet been decrypted. If {@link #isClaimsSetAvailable()}
+ * is true, this should never return {@literal null}.
+ */
+ @Nullable ClaimsSet getClaimsSet();
+
+ /**
+ * Get the subject identifier for the authenticated end-user.
+ *
+ * @return the subject identifier.
+ */
+ @Nullable String getSub();
+
+ /**
+ * Is the claims set available for use? if so, {@link #getClaimsSet()} should
+ * never return {@literal null}.
+ *
+ * @return true iff the claims set can be used.
+ */
+ boolean isClaimsSetAvailable();
+
+ /**
+ * Is the response signed i.e. a JWS response.
+ *
+ * @return true iff the response was signed.
+ */
+ boolean isSigned();
+
+ /**
+ * Is the response encrypted i.e. a JWE response. Note, it could also
+ * be signed.
+ *
+ * @return true iff the response is encrypted.
+ */
+ boolean isEncrypted();
+
+}
+
diff --git a/idp-oidc-rp-api/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/messaging/JWTUserInfoResponseTest.java b/idp-oidc-rp-api/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/messaging/JWTUserInfoResponseTest.java
new file mode 100644
index 0000000..63f23fe
--- /dev/null
+++ b/idp-oidc-rp-api/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/messaging/JWTUserInfoResponseTest.java
@@ -0,0 +1,102 @@
+package net.shibboleth.idp.plugin.authn.oidc.rp.messaging;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertNull;
+import static org.testng.Assert.assertTrue;
+
+import java.time.Instant;
+import java.util.Date;
+
+import org.testng.annotations.Test;
+
+import com.nimbusds.jose.EncryptionMethod;
+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.ECDSASigner;
+import com.nimbusds.jose.crypto.RSAEncrypter;
+import com.nimbusds.jose.jwk.Curve;
+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.JWT;
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.jwt.SignedJWT;
+
+/** Tests for JWTUserInfoResponse.*/
+public class JWTUserInfoResponseTest {
+
+ @Test
+ public void testSignedJwt() throws Exception {
+
+ 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("rp-proxy")
+ .subject("jdoe")
+ .expirationTime(Date.from(Instant.now().plusSeconds(120)))
+ .build();
+
+ final var signedJWT = new SignedJWT(header, payload);
+ signedJWT.sign(new ECDSASigner(key.toECPrivateKey()));
+
+ final JWTUserInfoResponse response = new JWTUserInfoResponse(signedJWT);
+ assertTrue(response.isClaimsSetAvailable());
+ assertNotNull(response.getClaimsSet());
+ assertEquals(response.getClaimsSet().getStringClaim("sub"), "jdoe");
+
+ }
+
+ @Test
+ public void testEncryptedJwt() throws Exception {
+
+ final var keySender = new ECKeyGenerator(Curve.P_256).keyID("1").generate();
+ final RSAKey keyRecipient = new RSAKeyGenerator(2048)
+ .keyID("2")
+ .keyUse(KeyUse.ENCRYPTION)
+ .generate();
+
+ final var header = new JWSHeader.Builder(JWSAlgorithm.ES256)
+ .type(JOSEObjectType.JWT)
+ .keyID(keySender.getKeyID())
+ .build();
+ final var payload = new JWTClaimsSet.Builder()
+ .issuer("issuer")
+ .audience("rp-proxy")
+ .subject("jdoe")
+ .expirationTime(Date.from(Instant.now().plusSeconds(120)))
+ .build();
+
+ final var signedJWT = new SignedJWT(header, payload);
+ signedJWT.sign(new ECDSASigner(keySender.toECPrivateKey()));
+
+ 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 JWT jwt = EncryptedJWT.parse(jweObject.serialize());
+
+ final JWTUserInfoResponse response = new JWTUserInfoResponse(jwt);
+ assertFalse(response.isClaimsSetAvailable());
+ assertNull(response.getClaimsSet());
+ assertTrue(response.getResponseJwt().getHeader().getAlgorithm().equals(JWEAlgorithm.RSA_OAEP_256));
+
+
+ }
+
+}
diff --git a/idp-oidc-rp-impl/pom.xml b/idp-oidc-rp-impl/pom.xml
index a209dfd..dcb64d2 100644
--- a/idp-oidc-rp-impl/pom.xml
+++ b/idp-oidc-rp-impl/pom.xml
@@ -76,6 +76,16 @@
<artifactId>oidc-common-metadata-api</artifactId>
<scope>provided</scope>
</dependency>
+ <dependency>
+ <groupId>net.shibboleth.oidc</groupId>
+ <artifactId>oidc-common-attribute-api</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>net.shibboleth.oidc</groupId>
+ <artifactId>oidc-common-attribute-impl</artifactId>
+ <scope>provided</scope>
+ </dependency>
<dependency>
<groupId>net.shibboleth.oidc</groupId>
<artifactId>oidc-common-profile-api</artifactId>
@@ -139,6 +149,16 @@
<scope>test</scope>
<type>test-jar</type>
</dependency>
+ <dependency>
+ <groupId>com.squareup.okhttp3</groupId>
+ <artifactId>mockwebserver</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.squareup.okhttp3</groupId>
+ <artifactId>okhttp-tls</artifactId>
+ <scope>test</scope>
+ </dependency>
</dependencies>
<build>
diff --git a/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/decoding/impl/DefaultTokenResponseDecoder.java b/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/decoding/impl/AbstractJSONResponseDecoderFunction.java
similarity index 51%
copy from idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/decoding/impl/DefaultTokenResponseDecoder.java
copy to idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/decoding/impl/AbstractJSONResponseDecoderFunction.java
index 4950a60..d14c86c 100644
--- a/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/decoding/impl/DefaultTokenResponseDecoder.java
+++ b/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/decoding/impl/AbstractJSONResponseDecoderFunction.java
@@ -17,20 +17,13 @@
package net.shibboleth.idp.plugin.authn.oidc.rp.decoding.impl;
-import java.util.Map;
import java.util.function.Function;
import javax.annotation.Nonnull;
-import javax.annotation.Nullable;
import org.apache.http.HttpResponse;
-import org.apache.http.HttpStatus;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
-import com.nimbusds.jose.util.IOUtils;
import net.shibboleth.utilities.java.support.annotation.constraint.NonnullAfterInit;
import net.shibboleth.utilities.java.support.component.AbstractInitializableComponent;
@@ -38,26 +31,17 @@ import net.shibboleth.utilities.java.support.component.ComponentInitializationEx
import net.shibboleth.utilities.java.support.component.ComponentSupport;
import net.shibboleth.utilities.java.support.logic.Constraint;
-/** Default token response decoder, which converts a succesful HTTP response into an Map.*/
-public class DefaultTokenResponseDecoder extends AbstractInitializableComponent
- implements Function<HttpResponse, Map<String, Object>> {
+/**
+ * Abstract class for JSON based response decoders.
+ *
+ * @param <T> the return type of the function.
+ */
+public abstract class AbstractJSONResponseDecoderFunction<T> extends AbstractInitializableComponent
+ implements Function<HttpResponse, T> {
- /** Class logger. */
- @Nonnull private final Logger log = LoggerFactory.getLogger(DefaultTokenResponseDecoder.class);
-
/** JSON object mapper. */
@NonnullAfterInit private ObjectMapper objectMapper;
-
- @Override
- protected void doInitialize() throws ComponentInitializationException {
- super.doInitialize();
-
- if (objectMapper == null) {
- throw new ComponentInitializationException("objectMapper cannot be null");
- }
- }
-
/**
* Set the JSON Object Mapper to use.
*
@@ -69,33 +53,23 @@ public class DefaultTokenResponseDecoder extends AbstractInitializableComponent
objectMapper = Constraint.isNotNull(mapper, "ObjectMapper cannot be null");
}
-
- //TODO we should handle the error response better? just return it and make the decision later?
+
+ /**
+ * Get the object mapper.
+ *
+ * @return the object mapper.
+ */
+ @NonnullAfterInit public ObjectMapper getObjectMapper() {
+ return objectMapper;
+ }
+
@Override
- @Nullable public Map<String, Object> apply(@Nonnull final HttpResponse httpResponse) {
+ protected void doInitialize() throws ComponentInitializationException {
+ super.doInitialize();
- try {
- final int httpStatusCode = httpResponse.getStatusLine().getStatusCode();
- if (httpStatusCode != HttpStatus.SC_OK) {
- //dump the body for logging - if one exists
- if (httpResponse.getEntity() != null && httpResponse.getEntity().getContent() != null) {
- final String errorContent = IOUtils.readInputStreamToString(httpResponse.getEntity().getContent());
- log.error("Token endpoint returned a Non-ok message of '{}'",errorContent);
- }
- log.warn("Non-ok status code ({}) returned from token endpoint: {}", httpStatusCode,
- httpResponse.getStatusLine().getReasonPhrase());
- return null;
- } else if (httpResponse.getEntity() == null || httpResponse.getEntity().getContent() == null) {
- log.warn("HTTP response does not contain a message entity, nothing to decode");
- return null;
- }
-
- return objectMapper.readValue(httpResponse.getEntity().getContent(),
- new TypeReference<Map<String, Object>>() {});
- } catch (final Exception e) {
- log.warn("Unable to decode OIDC Token Request Response", e);
+ if (objectMapper == null) {
+ throw new ComponentInitializationException("objectMapper cannot be null");
}
- return null;
}
}
diff --git a/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/decoding/impl/DefaultTokenResponseDecoder.java b/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/decoding/impl/DefaultMapResponseDecoder.java
similarity index 60%
rename from idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/decoding/impl/DefaultTokenResponseDecoder.java
rename to idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/decoding/impl/DefaultMapResponseDecoder.java
index 4950a60..3a66ff2 100644
--- a/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/decoding/impl/DefaultTokenResponseDecoder.java
+++ b/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/decoding/impl/DefaultMapResponseDecoder.java
@@ -18,7 +18,6 @@
package net.shibboleth.idp.plugin.authn.oidc.rp.decoding.impl;
import java.util.Map;
-import java.util.function.Function;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
@@ -29,46 +28,14 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.fasterxml.jackson.core.type.TypeReference;
-import com.fasterxml.jackson.databind.ObjectMapper;
import com.nimbusds.jose.util.IOUtils;
-import net.shibboleth.utilities.java.support.annotation.constraint.NonnullAfterInit;
-import net.shibboleth.utilities.java.support.component.AbstractInitializableComponent;
-import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
-import net.shibboleth.utilities.java.support.component.ComponentSupport;
-import net.shibboleth.utilities.java.support.logic.Constraint;
-
/** Default token response decoder, which converts a succesful HTTP response into an Map.*/
-public class DefaultTokenResponseDecoder extends AbstractInitializableComponent
- implements Function<HttpResponse, Map<String, Object>> {
+public class DefaultMapResponseDecoder extends AbstractJSONResponseDecoderFunction<Map<String, Object>> {
/** Class logger. */
- @Nonnull private final Logger log = LoggerFactory.getLogger(DefaultTokenResponseDecoder.class);
-
- /** JSON object mapper. */
- @NonnullAfterInit private ObjectMapper objectMapper;
-
-
- @Override
- protected void doInitialize() throws ComponentInitializationException {
- super.doInitialize();
-
- if (objectMapper == null) {
- throw new ComponentInitializationException("objectMapper cannot be null");
- }
- }
+ @Nonnull private final Logger log = LoggerFactory.getLogger(DefaultMapResponseDecoder.class);
- /**
- * Set the JSON Object Mapper to use.
- *
- * @param mapper the object mapper.
- */
- public void setObjectMapper(@Nonnull final ObjectMapper mapper) {
- ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
- ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
-
- objectMapper = Constraint.isNotNull(mapper, "ObjectMapper cannot be null");
- }
//TODO we should handle the error response better? just return it and make the decision later?
@Override
@@ -80,9 +47,9 @@ public class DefaultTokenResponseDecoder extends AbstractInitializableComponent
//dump the body for logging - if one exists
if (httpResponse.getEntity() != null && httpResponse.getEntity().getContent() != null) {
final String errorContent = IOUtils.readInputStreamToString(httpResponse.getEntity().getContent());
- log.error("Token endpoint returned a Non-ok message of '{}'",errorContent);
+ log.error("HTTP endpoint returned a Non-ok message of '{}'",errorContent);
}
- log.warn("Non-ok status code ({}) returned from token endpoint: {}", httpStatusCode,
+ log.warn("Non-ok status code ({}) returned from HTTP endpoint: {}", httpStatusCode,
httpResponse.getStatusLine().getReasonPhrase());
return null;
} else if (httpResponse.getEntity() == null || httpResponse.getEntity().getContent() == null) {
@@ -90,10 +57,11 @@ public class DefaultTokenResponseDecoder extends AbstractInitializableComponent
return null;
}
- return objectMapper.readValue(httpResponse.getEntity().getContent(),
+ return getObjectMapper().readValue(httpResponse.getEntity().getContent(),
new TypeReference<Map<String, Object>>() {});
+
} catch (final Exception e) {
- log.warn("Unable to decode OIDC Token Request Response", e);
+ log.warn("Unable to decode response", e);
}
return null;
}
diff --git a/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/decoding/impl/DefaultUserInfoResponseDecoder.java b/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/decoding/impl/DefaultUserInfoResponseDecoder.java
new file mode 100644
index 0000000..260a1c8
--- /dev/null
+++ b/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/decoding/impl/DefaultUserInfoResponseDecoder.java
@@ -0,0 +1,106 @@
+/*
+ * 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.decoding.impl;
+
+import java.util.Map;
+
+import javax.annotation.Nonnull;
+
+import org.apache.http.HttpResponse;
+import org.apache.http.HttpStatus;
+import org.apache.http.entity.ContentType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.http.MediaType;
+import org.springframework.util.MimeType;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.nimbusds.jose.util.IOUtils;
+import com.nimbusds.jwt.JWT;
+import com.nimbusds.jwt.JWTParser;
+import com.nimbusds.openid.connect.sdk.claims.ClaimsSet;
+
+import net.shibboleth.idp.plugin.authn.oidc.rp.messaging.JWTUserInfoResponse;
+import net.shibboleth.idp.plugin.authn.oidc.rp.messaging.PlainUserInfoResponse;
+import net.shibboleth.idp.plugin.authn.oidc.rp.messaging.UserInfoResponse;
+
+/** Response decoder for UserInfo responses.*/
+public class DefaultUserInfoResponseDecoder extends AbstractJSONResponseDecoderFunction<UserInfoResponse> {
+
+ /** The application/jwt media type.*/
+ @Nonnull private static final MediaType APPLICATION_JWT = new MediaType("application", "jwt");
+
+ /** Class logger. */
+ @Nonnull private final Logger log = LoggerFactory.getLogger(DefaultUserInfoResponseDecoder.class);
+
+ @Override
+ public UserInfoResponse apply(@Nonnull final HttpResponse httpResponse) {
+
+ try {
+
+ final int httpStatusCode = httpResponse.getStatusLine().getStatusCode();
+ if (httpStatusCode != HttpStatus.SC_OK) {
+ //dump the body for logging - if one exists
+ if (httpResponse.getEntity() != null && httpResponse.getEntity().getContent() != null) {
+ final String errorContent = IOUtils.readInputStreamToString(httpResponse.getEntity().getContent());
+ log.error("HTTP endpoint returned a Non-ok message of '{}'",errorContent);
+ }
+ log.warn("Non-ok status code ({}) returned from HTTP endpoint: {}", httpStatusCode,
+ httpResponse.getStatusLine().getReasonPhrase());
+ return null;
+ } else if (httpResponse.getEntity() == null || httpResponse.getEntity().getContent() == null) {
+ log.warn("HTTP response does not contain a message entity, nothing to decode");
+ return null;
+ }
+
+ final ContentType contentType = ContentType.get(httpResponse.getEntity());
+ if (contentType == null || contentType.getMimeType() == null) {
+ log.warn("HTTP response did not contain a content-type, must contain a content-type");
+ return null;
+ }
+
+ final String content = IOUtils.readInputStreamToString(httpResponse.getEntity().getContent());
+
+ // Is a JWT type or plain JSON object
+ if (APPLICATION_JWT.compareTo(MimeType.valueOf(contentType.getMimeType())) == 0) {
+ final JWT parsedJwt = JWTParser.parse(content);
+ log.trace("UserInfo response parsed a JWT type");
+ return new JWTUserInfoResponse(parsedJwt);
+
+ } else if (MediaType.APPLICATION_JSON.compareTo(MimeType.valueOf(contentType.getMimeType())) == 0){
+ final Map<String, Object> claims = getObjectMapper().readValue(content,
+ new TypeReference<Map<String, Object>>() {});
+ final ClaimsSet claimsSet = new ClaimsSet();
+ claimsSet.putAll(claims);
+ log.debug("UserInfo endpoint returned claims for '{}'", claimsSet.getStringClaim("sub"));
+ if (log.isTraceEnabled()) {
+ log.trace("UserInfo endpoint claims for '{}' are '{}'", claimsSet.getStringClaim("sub"),
+ claimsSet.toJSONString());
+ }
+ return new PlainUserInfoResponse(claimsSet);
+ }
+
+ } catch (final Exception e) {
+ log.warn("Unable to decode response", e);
+ return null;
+ }
+ return null;
+
+ }
+
+}
diff --git a/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/encoding/impl/DefaultTokenRequestEncoder.java b/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/encoding/impl/DefaultTokenRequestEncoder.java
index 3da272e..8d4b0cd 100644
--- a/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/encoding/impl/DefaultTokenRequestEncoder.java
+++ b/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/encoding/impl/DefaultTokenRequestEncoder.java
@@ -58,6 +58,7 @@ public class DefaultTokenRequestEncoder extends AbstractRequestEncoderFunction {
}
// Mandate HTTPS, so construct the URL from that.
final URI uri = new URIBuilder().setScheme(HTTPS)
+ .setPort(getProviderMetadataContext().getProviderInformation().getTokenEndpointURI().getPort())
.setHost(getProviderMetadataContext()
.getProviderInformation()
.getTokenEndpointURI().getHost())
diff --git a/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/encoding/impl/DefaultUserInfoRequestEncoder.java b/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/encoding/impl/DefaultUserInfoRequestEncoder.java
new file mode 100644
index 0000000..3c7499f
--- /dev/null
+++ b/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/encoding/impl/DefaultUserInfoRequestEncoder.java
@@ -0,0 +1,116 @@
+package net.shibboleth.idp.plugin.authn.oidc.rp.encoding.impl;
+
+import java.net.URI;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+
+import org.apache.http.client.methods.HttpUriRequest;
+import org.apache.http.client.methods.RequestBuilder;
+import org.apache.http.client.utils.URIBuilder;
+import org.apache.http.entity.ContentType;
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.profile.context.navigate.InboundMessageContextLookup;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.nimbusds.jose.util.StandardCharset;
+
+import net.shibboleth.idp.plugin.authn.oidc.rp.OIDCRPException;
+import net.shibboleth.idp.plugin.authn.oidc.rp.context.AccessTokenResponseContext;
+import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
+
+/**
+ * Default encoder for create UserInfo requests.
+ */
+//TODO This could be GET or POST - how to signal that? profile config, client metadata etc.
+public class DefaultUserInfoRequestEncoder extends AbstractRequestEncoderFunction {
+
+ /** The HTTPS scheme.*/
+ @Nonnull @NotEmpty private static final String HTTPS = "https";
+
+ /** Class logger. */
+ @Nonnull
+ private final Logger log = LoggerFactory.getLogger(DefaultUserInfoRequestEncoder.class);
+
+ /** Strategy used to look up the {@link AccessTokenResponseContext} to set the parameters for. */
+ @Nonnull private Function<ProfileRequestContext, AccessTokenResponseContext>
+ tokenResponseContextLookupStrategy;
+
+
+ /** Constructor.*/
+ public DefaultUserInfoRequestEncoder() {
+ tokenResponseContextLookupStrategy =
+ new ChildContextLookup<>(AccessTokenResponseContext.class, true).compose(
+ new InboundMessageContextLookup());
+ }
+
+ @Override
+ public HttpUriRequest doApply(@Nonnull final ProfileRequestContext profileRequestContext) {
+
+ try {
+ if (getClientMetadataContext().getClientInformation() == null) {
+ log.warn("No client information present to base token request off");
+ return null;
+ }
+ final AccessTokenResponseContext responseCtx = tokenResponseContextLookupStrategy.apply(profileRequestContext);
+ if (responseCtx == null) {
+ log.debug("No TokenResponseContext returned by lookup strategy");
+ return null;
+ }
+
+ // Mandate HTTPS GET, so construct the URL from that.
+ final URI uri = new URIBuilder().setScheme(HTTPS)
+ .setPort(getProviderMetadataContext().getProviderInformation().getUserInfoEndpointURI().getPort())
+ .setHost(getProviderMetadataContext()
+ .getProviderInformation()
+ .getUserInfoEndpointURI().getHost())
+ .setPath(getProviderMetadataContext()
+ .getProviderInformation()
+ .getUserInfoEndpointURI().getPath())
+ .build();
+
+ // Add headers and create request.
+ final RequestBuilder rb = RequestBuilder.get().setUri(uri)
+ .setHeader("Content-Type", ContentType.APPLICATION_FORM_URLENCODED.getMimeType())
+ .setCharset(StandardCharset.UTF_8);
+
+ // Add bearer token as authorization header.
+ addBearerToken(rb, responseCtx);
+
+ //If POST request add access_token= to query URL (unitutively)
+
+ final HttpUriRequest request = rb.build();
+ log.debug("UserInfo request URL '{}'",request);
+ return request;
+
+ } catch (final Exception e) {
+ log.warn("Unable to encode token request", e);
+ }
+ return null;
+ }
+
+ /**
+ * Add the bearer token to the Authorization header, used when issuing HTTP GET requests.
+ *
+ * @param rb the request builder to use.
+ * @param responseCtx the response context to find the access_token from.
+ *
+ * @throws OIDCRPException if there is an issue adding the bearer token to the Authorization header.
+ */
+ private void addBearerToken(@Nonnull final RequestBuilder rb, @Nonnull final AccessTokenResponseContext responseCtx)
+ throws OIDCRPException {
+
+ // Double check is bearer scheme
+ if ("Bearer".equals(responseCtx.getRawTokenResponse().get("token_type"))
+ && responseCtx.getRawTokenResponse().get("access_token") instanceof String) {
+ rb.addHeader("Authorization", "Bearer "+(String)responseCtx.getRawTokenResponse().get("access_token"));
+ } else {
+ throw new OIDCRPException("Access_token not found, or not Bearer type");
+ }
+
+ }
+
+
+}
diff --git a/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/AbstractHttpOIDCAuthenticationAction.java b/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/AbstractHttpOIDCAuthenticationAction.java
index 9dad4af..742bfc6 100644
--- a/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/AbstractHttpOIDCAuthenticationAction.java
+++ b/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/AbstractHttpOIDCAuthenticationAction.java
@@ -18,6 +18,8 @@
package net.shibboleth.idp.plugin.authn.oidc.rp.impl;
import java.io.IOException;
+import java.util.Map;
+import java.util.function.Function;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
@@ -26,11 +28,13 @@ import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.protocol.HttpClientContext;
+import org.opensaml.profile.context.ProfileRequestContext;
import org.opensaml.security.httpclient.HttpClientSecurityParameters;
import org.opensaml.security.httpclient.HttpClientSecuritySupport;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import net.shibboleth.idp.plugin.authn.oidc.rp.OIDCRPException;
import net.shibboleth.utilities.java.support.annotation.constraint.NonnullAfterInit;
import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
import net.shibboleth.utilities.java.support.component.ComponentSupport;
@@ -38,17 +42,26 @@ import net.shibboleth.utilities.java.support.logic.Constraint;
/**
* An abstract class for actions that want to make synchronous HTTP requests.
+ *
+ * @param <T> the response type of the object returned as a result of the request.
*/
-public class AbstractHttpOIDCAuthenticationAction extends AbstractOIDCAuthenticationAction {
+public class AbstractHttpOIDCAuthenticationAction<T> extends AbstractOIDCAuthenticationAction {
/** Class logger.*/
@Nonnull private final Logger log = LoggerFactory.getLogger(AbstractHttpOIDCAuthenticationAction.class);
- /** HttpClient for contacting Duo. */
+ /** The message encoder to encode the HTTP request into a {@link HttpUriRequest}.*/
+ @NonnullAfterInit private Function<ProfileRequestContext, HttpUriRequest> httpRequestEncoderStrategy;
+
+ /** The message decoder to decode the HTTP response.*/
+ @NonnullAfterInit private Function<HttpResponse, T> httpResponseDecoderStrategy;
+
+ /** HttpClient for contacting the endpoint. */
@NonnullAfterInit private HttpClient httpClient;
/** HTTP client security parameters. */
@Nullable private HttpClientSecurityParameters httpClientSecurityParameters;
+
@Override
protected void doInitialize() throws ComponentInitializationException {
@@ -57,6 +70,55 @@ public class AbstractHttpOIDCAuthenticationAction extends AbstractOIDCAuthentica
if (httpClient == null) {
throw new ComponentInitializationException("httpClient cannot be null");
}
+ if (httpRequestEncoderStrategy == null) {
+ throw new ComponentInitializationException("HTTP request encoder strategy cannot be null");
+ }
+ if (httpResponseDecoderStrategy == null) {
+ throw new ComponentInitializationException("HTTP response decoder strategy cannot be null");
+ }
+ }
+
+ /**
+ * Get the HTTP request encoder strategy.
+ *
+ * @return the request encoder.
+ */
+ @NonnullAfterInit public Function<ProfileRequestContext, HttpUriRequest> getHttpRequestEncoderStrategy() {
+ return httpRequestEncoderStrategy;
+ }
+
+ /**
+ * Get the HTTP response decoder strategy.
+ *
+ * @return the response decoder.
+ */
+ @NonnullAfterInit public Function<HttpResponse, T> getHttpResponseDecoderStrategy() {
+ return httpResponseDecoderStrategy;
+ }
+
+ /**
+ * Set the strategy used to map a HTTP response the response type.
+ *
+ * @param strategy the strategy
+ */
+ public void setHttpResponseDecoderStrategy(@Nonnull final Function<HttpResponse, T> strategy) {
+ ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+ ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
+
+ httpResponseDecoderStrategy = Constraint.isNotNull(strategy, "Http decoder strategy can not be null");
+ }
+
+ /**
+ * Set the strategy used to map a HTTP request to a {@link HttpUriRequest} object.
+ *
+ * @param strategy the strategy
+ */
+ public void setHttpRequestEncoderStrategy(
+ @Nonnull final Function<ProfileRequestContext, HttpUriRequest> strategy) {
+ ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+ ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
+
+ httpRequestEncoderStrategy = Constraint.isNotNull(strategy, "Http encoder strategy can not be null");
}
/**
@@ -82,15 +144,44 @@ public class AbstractHttpOIDCAuthenticationAction extends AbstractOIDCAuthentica
httpClientSecurityParameters = params;
}
+
+ /**
+ * Encode the request using the supplied request encoder strategy. Execute a synchronous HTTP request
+ * and decode the response using the supplied decoder strategy.
+ *
+ * @param profileRequestContext the context to pull information out of for encoding the request.
+ *
+ * @return the decoded response.
+ *
+ * @throws OIDCRPException on error making the request.
+ */
+ @Nonnull protected T handleRequest(
+ @Nonnull final ProfileRequestContext profileRequestContext) throws OIDCRPException {
+ try {
+ final HttpUriRequest request = getHttpRequestEncoderStrategy().apply(profileRequestContext);
+ if (request == null) {
+ throw new OIDCRPException("Unable to encode HTTP request");
+ }
+ final HttpResponse response = executeHttpRequest(request);
+ final T responseObject = getHttpResponseDecoderStrategy().apply(response);
+ if (responseObject == null) {
+ throw new OIDCRPException(
+ "Unable to decode HTTP response");
+ }
+ return responseObject;
+ } catch (final IOException e) {
+ log.error("{} Unable to perform HTTP request and return response",getLogPrefix(),e);
+ throw new OIDCRPException(e);
+ }
+ }
/**
- * Performs a call to an OIDC endpoint that expects a message in the body of the response.
- * Iff successful, the JSON response is mapped into the appropriate type.
+ * Performs a call to an HTTP endpoint using the configured HttpClient and security parameters.
*
* @param request the prepared HTTP request
*
- * @return the response, never {@code null}.
+ * @return the HTTP response, never {@code null}.
*
* @throws IOException if there is an error producing a response
*/
diff --git a/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/AbstractOIDCAuthenticationResponseAction.java b/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/AbstractOIDCAuthenticationResponseAction.java
index 83d8dc8..011597b 100644
--- a/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/AbstractOIDCAuthenticationResponseAction.java
+++ b/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/AbstractOIDCAuthenticationResponseAction.java
@@ -76,7 +76,7 @@ abstract class AbstractOIDCAuthenticationAction extends AbstractAuthenticationAc
@Nullable private OIDCProviderMetadataContext providerMetadataContext;
/** Constructor.*/
- public AbstractOIDCAuthenticationAction() {
+ protected AbstractOIDCAuthenticationAction() {
providerMetadataLookupStrategy = new ChildContextLookup<>(OIDCProviderMetadataContext.class).compose(
new ChildContextLookup<>(OIDCPeerEntityContext.class).compose(
new OutboundMessageContextLookup()));
diff --git a/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/AddAuthzRequest.java b/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/AddAuthzRequest.java
index 8049654..7cd1a66 100644
--- a/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/AddAuthzRequest.java
+++ b/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/AddAuthzRequest.java
@@ -33,6 +33,8 @@ import org.opensaml.profile.context.navigate.OutboundMessageContextLookup;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import com.nimbusds.openid.connect.sdk.Nonce;
+
import net.shibboleth.idp.authn.AbstractAuthenticationAction;
import net.shibboleth.idp.authn.context.AuthenticationContext;
import net.shibboleth.idp.plugin.authn.oidc.rp.context.OIDCPeerEntityContext;
@@ -219,7 +221,8 @@ public class AddAuthzRequest extends AbstractAuthenticationAction {
getLogPrefix(), authenticationContext.getAuthenticatingAuthority());
- final OIDCAuthenticationRequest request = new OIDCAuthenticationRequest(clientMetadata.getClientInformation().getID());
+ final OIDCAuthenticationRequest request =
+ new OIDCAuthenticationRequest(clientMetadata.getClientInformation().getID());
request.setResponseType(responseTypeAndModeContext.getResponseType());
//TODO spec says response mode not recommended if the default type for response_type. Check here?
@@ -227,6 +230,11 @@ public class AddAuthzRequest extends AbstractAuthenticationAction {
request.setEndpointURI(providerMetadata.getProviderInformation().getAuthorizationEndpointURI());
request.setRedirectURI(clientMetadata.getClientInformation().getMetadata().getRedirectionURI());
+ request.getScope().add("profile");
+
+ //TODO use strategy with injectable secure random implementation?
+ request.setNonce(new Nonce(OIDCProxySupport.generateNonce(16)));
+
//TODO if force-authn
// try {
// request.setPrompt(Prompt.parse("none"));
diff --git a/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/AuthorizationController.java b/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/AuthorizationController.java
index 137789d..c3d8b43 100644
--- a/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/AuthorizationController.java
+++ b/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/AuthorizationController.java
@@ -35,14 +35,12 @@ import org.opensaml.messaging.handler.MessageHandlerException;
import org.opensaml.profile.action.EventIds;
import org.opensaml.profile.context.EventContext;
import org.opensaml.profile.context.ProfileRequestContext;
-import org.opensaml.saml.common.messaging.context.SAMLMessageReceivedEndpointContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
-import com.nimbusds.oauth2.sdk.AuthorizationResponse;
import com.nimbusds.oauth2.sdk.id.State;
import net.shibboleth.idp.authn.ExternalAuthentication;
@@ -50,7 +48,6 @@ import net.shibboleth.idp.authn.ExternalAuthenticationException;
import net.shibboleth.idp.authn.context.AuthenticationContext;
import net.shibboleth.idp.plugin.authn.oidc.rp.OIDCProxyException;
import net.shibboleth.idp.plugin.authn.oidc.rp.context.OIDCAuthnContext;
-import net.shibboleth.idp.plugin.authn.oidc.rp.context.OpenIDConnectContext;
import net.shibboleth.idp.plugin.authn.oidc.rp.context.ResponseTypeAndModeContext;
import net.shibboleth.oidc.profile.core.OIDCAuthenticationRequest;
import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
diff --git a/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/DefaultClaimMergingStrategy.java b/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/DefaultClaimMergingStrategy.java
new file mode 100644
index 0000000..a72cc7b
--- /dev/null
+++ b/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/DefaultClaimMergingStrategy.java
@@ -0,0 +1,88 @@
+/*
+ * 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.util.HashMap;
+import java.util.Map;
+import java.util.function.BiFunction;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.openid.connect.sdk.claims.ClaimsSet;
+
+/**
+ * A default merging strategy for combing claims in the UserInfo response with those from the id_token.
+ * <ol>
+ * <li>If one of userInfo or idToken claims are null, the other is returned.</li>
+ * <li>If both input claims are null, an empty claimsset is returned.</li>
+ * <li>Merges the id_token claims into the UserInfo claims, the value of a claim from the id_token
+ * is taken over that from the UserInfo response if they claim keys clash.</li>
+ * </ol>
+ */
+//TODO maybe this should filter claims as well e.g. exp etc. Security check this logic.
+public class DefaultClaimMergingStrategy implements BiFunction<ClaimsSet, JWTClaimsSet, ClaimsSet> {
+
+ /** Class logger.*/
+ @Nonnull private final Logger log = LoggerFactory.getLogger(DefaultClaimMergingStrategy.class);
+
+ @Override
+ @Nonnull public ClaimsSet apply(@Nullable final ClaimsSet userInfo, @Nullable final JWTClaimsSet idToken) {
+
+ if (userInfo == null && idToken != null) {
+ final ClaimsSet singleSet = new ClaimsSet();
+ singleSet.putAll(idToken.getClaims());
+ return singleSet;
+ }
+ if (userInfo != null && idToken == null) {
+ final ClaimsSet singleSet = new ClaimsSet();
+ singleSet.putAll(userInfo.toJSONObject());
+ return singleSet;
+ }
+ if (userInfo == null && idToken == null) {
+ // return empty claimsset
+ return new ClaimsSet();
+ }
+
+
+
+ final Map<String, Object> idTokenAsMap = idToken.getClaims();
+ // Treat JSONObject as the base map representation.
+ final Map<String, Object> userInfoClaimsAsMap = userInfo.toJSONObject();
+
+ // add UserInfo claims as a base
+ final Map<String, Object> mergedClaimsMap = new HashMap<String, Object>(userInfoClaimsAsMap);
+
+ // Merge id_token claims into userInfo claims, take userinfo claim if conflict
+ idTokenAsMap.forEach((key, value) ->
+ mergedClaimsMap.merge(key, value, (v1, v2)-> {
+ log.trace("Claim '{}' exists in id_token and UserInfo response, taking id_token value '{}'",key, v2);
+ return v2;
+ }));
+
+
+ final ClaimsSet mergedClaimsSet = new ClaimsSet();
+ mergedClaimsSet.putAll(mergedClaimsMap);
+ return mergedClaimsSet;
+ }
+
+}
diff --git a/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/DefaultIDTokenLookupStrategy.java b/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/DefaultIDTokenLookupStrategy.java
index ddad58f..8a8366f 100644
--- a/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/DefaultIDTokenLookupStrategy.java
+++ b/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/DefaultIDTokenLookupStrategy.java
@@ -1,9 +1,27 @@
+/*
+ * 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.util.function.Function;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
+import javax.annotation.concurrent.ThreadSafe;
import org.opensaml.messaging.context.navigate.ChildContextLookup;
import org.opensaml.profile.context.ProfileRequestContext;
@@ -11,36 +29,39 @@ import org.opensaml.profile.context.navigate.InboundMessageContextLookup;
import com.nimbusds.jwt.JWT;
-import net.shibboleth.idp.plugin.authn.oidc.rp.context.TokenResponseContext;
+import net.shibboleth.idp.plugin.authn.oidc.rp.context.AccessTokenResponseContext;
import net.shibboleth.utilities.java.support.logic.Constraint;
-/** Function that extracts the id_token from the {@link TokenResponseContext}.*/
+/** Function that extracts the id_token from the {@link AccessTokenResponseContext}.*/
+ at ThreadSafe
public class DefaultIDTokenLookupStrategy implements Function<ProfileRequestContext, JWT> {
- /** Strategy used to locate the TokenResponseContext to extract the id_token from.*/
- @Nonnull private Function<ProfileRequestContext, TokenResponseContext> tokenResponseContextLookupStrategy;
+ /** Strategy used to locate the {@link AccessTokenResponseContext} to extract the id_token from.*/
+ @Nonnull
+ private final Function<ProfileRequestContext, AccessTokenResponseContext> tokenResponseContextLookupStrategy;
/** Constructor.*/
public DefaultIDTokenLookupStrategy() {
tokenResponseContextLookupStrategy =
- new ChildContextLookup<>(TokenResponseContext.class, true).compose(
+ new ChildContextLookup<>(AccessTokenResponseContext.class, true).compose(
new InboundMessageContextLookup());
}
- /**
- * Set the strategy used to look up a {@link TokenResponseContext}.
- *
- * @param strategy lookup strategy
- */
- public void setTokenResponseContextLookupStrategy(
- @Nonnull final Function<ProfileRequestContext, TokenResponseContext> strategy) {
- tokenResponseContextLookupStrategy = Constraint.isNotNull(strategy,
- "TokenResponseContext lookup strategy cannot be null");
- }
+ /**
+ *
+ * Constructor.
+ *
+ * @param strategy the AccessTokenResponseContext lookup strategy to use.
+ */
+ public DefaultIDTokenLookupStrategy(
+ @Nonnull final Function<ProfileRequestContext, AccessTokenResponseContext> strategy) {
+ tokenResponseContextLookupStrategy =
+ Constraint.isNotNull(strategy, "AccessTokenResponseContext lookup strategy can not be null");
+ }
@Override
@Nullable public JWT apply(@Nonnull final ProfileRequestContext prc) {
- final TokenResponseContext tokenContext = tokenResponseContextLookupStrategy.apply(prc);
+ final AccessTokenResponseContext tokenContext = tokenResponseContextLookupStrategy.apply(prc);
if (tokenContext == null) {
return null;
}
diff --git a/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/DefaultUserInfoTokenLookupStrategy.java b/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/DefaultUserInfoTokenLookupStrategy.java
new file mode 100644
index 0000000..9910f35
--- /dev/null
+++ b/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/DefaultUserInfoTokenLookupStrategy.java
@@ -0,0 +1,78 @@
+/*
+ * 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.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.ThreadSafe;
+
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.profile.context.navigate.InboundMessageContextLookup;
+
+import com.nimbusds.jwt.JWT;
+
+import net.shibboleth.idp.plugin.authn.oidc.rp.context.AccessTokenResponseContext;
+import net.shibboleth.idp.plugin.authn.oidc.rp.context.UserInfoResponseContext;
+import net.shibboleth.idp.plugin.authn.oidc.rp.messaging.JWTUserInfoResponse;
+import net.shibboleth.utilities.java.support.logic.Constraint;
+
+/** Function that extracts the id_token from the {@link AccessTokenResponseContext}.*/
+//TODO could this be replaced in the method using it by a ContextDataLookupFunction?
+ at ThreadSafe
+public class DefaultUserInfoTokenLookupStrategy implements Function<ProfileRequestContext, JWT> {
+
+ /** Strategy used to look up the {@link UserInfoResponseContext}. */
+ @Nonnull private final Function<ProfileRequestContext, UserInfoResponseContext>
+ userInfoResponseContextLookupStrategy;
+
+ /** Constructor.*/
+ public DefaultUserInfoTokenLookupStrategy() {
+ userInfoResponseContextLookupStrategy =
+ new ChildContextLookup<>(UserInfoResponseContext.class).compose(
+ new InboundMessageContextLookup());
+ }
+
+ /**
+ *
+ * Constructor.
+ *
+ * @param strategy the UserInfo response context lookup strategy to use.
+ */
+ public DefaultUserInfoTokenLookupStrategy(
+ @Nonnull final Function<ProfileRequestContext, UserInfoResponseContext> strategy) {
+ userInfoResponseContextLookupStrategy =
+ Constraint.isNotNull(strategy, "UserInfoResponseContext lookup strategy can not be null");
+ }
+
+
+ @Override
+ @Nullable public JWT apply(@Nonnull final ProfileRequestContext prc) {
+
+ final UserInfoResponseContext userInfoContext = userInfoResponseContextLookupStrategy.apply(prc);
+
+ if (userInfoContext == null || userInfoContext.getUserInfo() == null ||
+ !(userInfoContext.getUserInfo() instanceof JWTUserInfoResponse)) {
+ return null;
+ }
+ return ((JWTUserInfoResponse)userInfoContext.getUserInfo()).getResponseJwt();
+ }
+
+}
diff --git a/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/ExchangeCodeForAccessToken.java b/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/ExchangeCodeForAccessToken.java
index b71ce90..8d5934d 100644
--- a/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/ExchangeCodeForAccessToken.java
+++ b/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/ExchangeCodeForAccessToken.java
@@ -17,14 +17,11 @@
package net.shibboleth.idp.plugin.authn.oidc.rp.impl;
-import java.io.IOException;
import java.util.Map;
import java.util.function.Function;
import javax.annotation.Nonnull;
-import org.apache.http.HttpResponse;
-import org.apache.http.client.methods.HttpUriRequest;
import org.opensaml.messaging.context.navigate.ChildContextLookup;
import org.opensaml.profile.action.ActionSupport;
import org.opensaml.profile.action.EventIds;
@@ -35,87 +32,55 @@ import org.slf4j.LoggerFactory;
import net.shibboleth.idp.authn.AuthnEventIds;
import net.shibboleth.idp.authn.context.AuthenticationContext;
-import net.shibboleth.idp.plugin.authn.oidc.rp.context.TokenResponseContext;
-import net.shibboleth.utilities.java.support.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.idp.plugin.authn.oidc.rp.OIDCRPException;
+import net.shibboleth.idp.plugin.authn.oidc.rp.context.AccessTokenResponseContext;
import net.shibboleth.utilities.java.support.component.ComponentSupport;
import net.shibboleth.utilities.java.support.logic.Constraint;
/**
* Action to exchange the authorization code in the authentication response for an OAuth access token which
- * contains an OIDC id_token. Once obtained, adds the token to the {@link TokenResponseContext}.
+ * contains an OIDC id_token. Once obtained, adds the token to the {@link AccessTokenResponseContext}.
*
- * FIXME: these conditions
*
* @event {@link org.opensaml.profile.action.EventIds#PROCEED_EVENT_ID}
+ * @event {@link EventIds#INVALID_PROFILE_CTX}
* @event {@link AuthnEventIds#AUTHN_EXCEPTION}
- * @event {@link AuthnEventIds#NO_CREDENTIALS}
- * @pre <pre>ProfileRequestContext.getSubcontext(AuthenticationContext.class, false) != null and
- * AuthenticationContext.getSubcontect(DuoOIDCAuthenticationContext.class,false)!=null</pre>
- * @post Add the Duo authentication token to the context.
+ *
+ * @pre <pre>ProfileRequestContext.getSubcontext(AuthenticationContext.class, false) != null</pre>
+ * @post Add the Access Token Response to the {@link AccessTokenResponseContext}.
*/
-public class ExchangeCodeForAccessToken extends AbstractHttpOIDCAuthenticationAction {
+public class ExchangeCodeForAccessToken extends AbstractHttpOIDCAuthenticationAction<Map<String, Object>> {
/** Class logger.*/
@Nonnull private final Logger log = LoggerFactory.getLogger(ExchangeCodeForAccessToken.class);
- /** The message encoder to use encode the token request into a HttpRequest.*/
- @NonnullAfterInit private Function<ProfileRequestContext, HttpUriRequest> tokenRequestEncoderStrategy;
-
- /** The message decoder to use decode a HTTP response to a token object.*/
- @NonnullAfterInit private Function<HttpResponse, Map<String, Object>> tokenResponseDecoderStrategy;
- /** Strategy used to look up the {@link TokenResponseContext} to set the parameters for. */
- @Nonnull private Function<ProfileRequestContext, TokenResponseContext>
+ /** Strategy used to look up the {@link AccessTokenResponseContext} to set the parameters for. */
+ @Nonnull private Function<ProfileRequestContext, AccessTokenResponseContext>
tokenResponseContextLookupStrategy;
/** Constructor.*/
public ExchangeCodeForAccessToken() {
tokenResponseContextLookupStrategy =
- new ChildContextLookup<>(TokenResponseContext.class, true).compose(
+ new ChildContextLookup<>(AccessTokenResponseContext.class, true).compose(
new InboundMessageContextLookup());
}
/**
- * Set the strategy used to look up a {@link TokenResponseContext}.
+ * Set the strategy used to look up a {@link AccessTokenResponseContext}.
*
* @param strategy lookup strategy
*/
public void setTokenResponseContextLookupStrategy(
- @Nonnull final Function<ProfileRequestContext, TokenResponseContext> strategy) {
+ @Nonnull final Function<ProfileRequestContext, AccessTokenResponseContext> strategy) {
ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
tokenResponseContextLookupStrategy = Constraint.isNotNull(strategy,
"TokenResponseContext lookup strategy cannot be null");
}
-
- /**
- * Set the strategy used to map a request...TODO.
- *
- * @param strategy the strategy
- */
- public void setTokenResponseDecoderStrategy(@Nonnull final Function<HttpResponse, Map<String, Object>> strategy) {
- ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
- ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
-
- tokenResponseDecoderStrategy = Constraint.isNotNull(strategy, "Token decoder strategy can not be null");
- }
-
- /**
- * Set the strategy used to map a request...TODO.
- *
- * @param strategy the strategy
- */
- public void setTokenRequestEncoderStrategy(
- @Nonnull final Function<ProfileRequestContext, HttpUriRequest> strategy) {
- ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
- ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
-
- tokenRequestEncoderStrategy = Constraint.isNotNull(strategy, "Token encoder strategy can not be null");
- }
-
-
+
@Override protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
@Nonnull final AuthenticationContext authenticationContext) {
@@ -123,33 +88,20 @@ public class ExchangeCodeForAccessToken extends AbstractHttpOIDCAuthenticationAc
getAuthenticationResponse().getAuthorizationCode(),
authenticationContext.getAuthenticatingAuthority());
- final TokenResponseContext responseCtx = tokenResponseContextLookupStrategy.apply(profileRequestContext);
+ final AccessTokenResponseContext responseCtx =
+ tokenResponseContextLookupStrategy.apply(profileRequestContext);
if (responseCtx == null) {
log.debug("{} No TokenResponseContext returned by lookup strategy", getLogPrefix());
ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
return;
}
- try {
- final HttpUriRequest request = tokenRequestEncoderStrategy.apply(profileRequestContext);
- if (request == null) {
- log.warn("{} Unable to exhange authorization_code for token, request could "
- + "not be constructed", getLogPrefix());
- ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.AUTHN_EXCEPTION);
- return;
- }
- final HttpResponse response = executeHttpRequest(request);
- final Map<String, Object> responseObject = tokenResponseDecoderStrategy.apply(response);
- if (responseObject == null) {
- log.warn("{} Unable to exhange authorization_code for token, response could "
- + "not be decoded", getLogPrefix());
- ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.AUTHN_EXCEPTION);
- return;
- }
+ try {
+ final Map<String, Object> responseObject = handleRequest(profileRequestContext);
responseCtx.setRawTokenResponse(responseObject);
- log.debug("{}: Token request response '{}'",getLogPrefix(), responseObject);
+ log.trace("{}: Token request response '{}'",getLogPrefix(), responseObject);
- } catch (final IOException e) {
+ } catch (final OIDCRPException e) {
log.error("{} Unable to exchange authorisation code for token result",getLogPrefix(),e);
ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.AUTHN_EXCEPTION);
}
diff --git a/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/ExtractIDTokenFromResponse.java b/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/ExtractIDTokenFromResponse.java
index d76f4d8..cb88676 100644
--- a/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/ExtractIDTokenFromResponse.java
+++ b/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/ExtractIDTokenFromResponse.java
@@ -38,7 +38,7 @@ import com.nimbusds.jwt.EncryptedJWT;
import com.nimbusds.jwt.PlainJWT;
import com.nimbusds.jwt.SignedJWT;
-import net.shibboleth.idp.plugin.authn.oidc.rp.context.TokenResponseContext;
+import net.shibboleth.idp.plugin.authn.oidc.rp.context.AccessTokenResponseContext;
import net.shibboleth.idp.profile.AbstractProfileAction;
import net.shibboleth.oidc.profile.core.OidcEventIds;
import net.shibboleth.utilities.java.support.annotation.constraint.NonnullAfterInit;
@@ -46,7 +46,8 @@ import net.shibboleth.utilities.java.support.component.ComponentInitializationEx
import net.shibboleth.utilities.java.support.component.ComponentSupport;
import net.shibboleth.utilities.java.support.logic.Constraint;
-/** Action that extracts an id_token from the inbound message context and sets it onto a TODO context.*/
+/** Action that extracts an id_token from the access token response from the
+ * inbound message context and sets it onto a TODO context.*/
public class ExtractIDTokenFromResponse extends AbstractProfileAction {
/** Class logger. */
@@ -56,26 +57,26 @@ public class ExtractIDTokenFromResponse extends AbstractProfileAction {
@NonnullAfterInit private Function<ProfileRequestContext, String> rawIdTokenLookupStrategy;
/** The token response context to add the decoded id_token too.*/
- @NonnullAfterInit private TokenResponseContext responseCtx;
+ @NonnullAfterInit private AccessTokenResponseContext responseCtx;
- /** Strategy used to look up the {@link TokenResponseContext} to set the parameters for. */
- @Nonnull private Function<ProfileRequestContext, TokenResponseContext>
+ /** Strategy used to look up the {@link AccessTokenResponseContext} to set the parameters for. */
+ @Nonnull private Function<ProfileRequestContext, AccessTokenResponseContext>
tokenResponseContextLookupStrategy;
/** Constructor.*/
public ExtractIDTokenFromResponse() {
tokenResponseContextLookupStrategy =
- new ChildContextLookup<>(TokenResponseContext.class).compose(
+ new ChildContextLookup<>(AccessTokenResponseContext.class).compose(
new InboundMessageContextLookup());
}
/**
- * Set the strategy used to look up a {@link TokenResponseContext}.
+ * Set the strategy used to look up a {@link AccessTokenResponseContext}.
*
* @param strategy lookup strategy
*/
public void setTokenResponseContextLookupStrategy(
- @Nonnull final Function<ProfileRequestContext, TokenResponseContext> strategy) {
+ @Nonnull final Function<ProfileRequestContext, AccessTokenResponseContext> strategy) {
ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
tokenResponseContextLookupStrategy = Constraint.isNotNull(strategy,
@@ -101,8 +102,7 @@ public class ExtractIDTokenFromResponse extends AbstractProfileAction {
}
return true;
}
-
-
+
/**
* Set the strategy used to lookup a base64 encoded JWT from the profile request context.
*
@@ -126,7 +126,6 @@ public class ExtractIDTokenFromResponse extends AbstractProfileAction {
log.trace("{} (Assumed) Base64 encoded id_token is '{}'",getLogPrefix(), rawIdTokenValue);
try {
final JOSEObject joseObject = JOSEObject.parse(rawIdTokenValue);
- log.trace("{} Parsed JOSE Object '{}'",getLogPrefix(), joseObject);
if (joseObject instanceof PlainObject) {
responseCtx.setIdToken(PlainJWT.parse(rawIdTokenValue));
@@ -135,14 +134,11 @@ public class ExtractIDTokenFromResponse extends AbstractProfileAction {
} else if (joseObject instanceof JWEObject) {
responseCtx.setIdToken(EncryptedJWT.parse(rawIdTokenValue));
- }
-
+ }
} catch (final ParseException e) {
log.warn("{} Unable to parse id_token",getLogPrefix(), e);
ActionSupport.buildEvent(profileRequestContext,OidcEventIds.INVALID_ID_TOKEN);
- return;
-
- }
-
+ return;
+ }
}
}
diff --git a/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/MergeUserInfoAndIDTokenClaims.java b/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/MergeUserInfoAndIDTokenClaims.java
new file mode 100644
index 0000000..cd8c549
--- /dev/null
+++ b/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/MergeUserInfoAndIDTokenClaims.java
@@ -0,0 +1,218 @@
+/*
+ * 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.text.ParseException;
+import java.util.function.BiFunction;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.profile.context.navigate.InboundMessageContextLookup;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.openid.connect.sdk.claims.ClaimsSet;
+
+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.EndUserClaimsContext;
+import net.shibboleth.idp.plugin.authn.oidc.rp.context.UserInfoResponseContext;
+import net.shibboleth.utilities.java.support.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
+import net.shibboleth.utilities.java.support.component.ComponentSupport;
+import net.shibboleth.utilities.java.support.logic.Constraint;
+
+/**
+ * Merge the claims in the id_token with the claims from the UserInfo response.
+ */
+//TODO similar too ValidateUserInfoClaims, do we need to extend OIDC action
+public class MergeUserInfoAndIDTokenClaims extends AbstractOIDCAuthenticationAction {
+
+ /** Class logger.*/
+ @Nonnull private final Logger log = LoggerFactory.getLogger(MergeUserInfoAndIDTokenClaims.class);
+
+ /** Strategy used to look up the {@link UserInfoResponseContext}. */
+ @Nonnull private Function<ProfileRequestContext, UserInfoResponseContext>
+ userInfoResponseContextLookupStrategy;
+
+ /** Strategy used to look up the {@link AccessTokenResponseContext} . */
+ @Nonnull private Function<ProfileRequestContext, AccessTokenResponseContext>
+ tokenResponseContextLookupStrategy;
+
+ /** Strategy used to look up the {@link EndUserClaimsContext} to set the parameters for. */
+ @Nonnull private Function<ProfileRequestContext, EndUserClaimsContext>
+ endUserClaimsContextLookupStrategy;
+
+ /** The strategy used to merge UserInfo claims with id_token claims.*/
+ @NonnullAfterInit private BiFunction<ClaimsSet, JWTClaimsSet, ClaimsSet> claimMergingStrategy;
+
+ /** The stashed user info claims.*/
+ @Nullable private ClaimsSet userInfoClaims;
+
+ /** The stashed id_token claims.*/
+ @Nullable private JWTClaimsSet idTokenClaims;
+
+ /** Constructor.*/
+ public MergeUserInfoAndIDTokenClaims() {
+ userInfoResponseContextLookupStrategy =
+ new ChildContextLookup<>(UserInfoResponseContext.class).compose(
+ new InboundMessageContextLookup());
+
+ tokenResponseContextLookupStrategy =
+ new ChildContextLookup<>(AccessTokenResponseContext.class).compose(
+ new InboundMessageContextLookup());
+
+ endUserClaimsContextLookupStrategy =
+ new ChildContextLookup<>(EndUserClaimsContext.class, true).compose(
+ new InboundMessageContextLookup());
+
+ claimMergingStrategy = new DefaultClaimMergingStrategy();
+ }
+
+ @Override
+ protected void doInitialize() throws ComponentInitializationException {
+ super.doInitialize();
+
+ if (claimMergingStrategy == null) {
+ throw new ComponentInitializationException("ClaimMergingStrategy cannot be null");
+ }
+ }
+
+ /**
+ * Set the strategy used to merge UserInfo claims with id_token claims.
+ *
+ * @param strategy the strategy to use.
+ */
+ public void setClaimMergingStrategy(
+ @Nonnull final BiFunction<ClaimsSet, JWTClaimsSet, ClaimsSet> strategy) {
+ ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+
+ claimMergingStrategy = Constraint.isNotNull(strategy,
+ "ClaimMergingStrategy cannot be null");
+ }
+
+ /**
+ * Set the strategy used to lookup a {@link EndUserClaimsContext}.
+ *
+ * @param strategy the strategy
+ */
+ public void setEndUserClaimsContextLookupStrategy(
+ @Nonnull final Function<ProfileRequestContext, EndUserClaimsContext> strategy) {
+ ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+
+ endUserClaimsContextLookupStrategy = Constraint.isNotNull(strategy,
+ "EndUserClaimsContextLookupStrategy cannot be null");
+ }
+
+
+ /**
+ * Set the strategy used to look up a {@link AccessTokenResponseContext}.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setTokenResponseContextLookupStrategy(
+ @Nonnull final Function<ProfileRequestContext, AccessTokenResponseContext> strategy) {
+ ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+
+ tokenResponseContextLookupStrategy = Constraint.isNotNull(strategy,
+ "TokenResponseContext lookup strategy cannot be null");
+ }
+
+ /**
+ * Set the strategy used to look up a {@link UserInfoResponseContext}.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setUserInfoResponseContextLookupStrategy(
+ @Nonnull final Function<ProfileRequestContext, UserInfoResponseContext> strategy) {
+ ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+
+ userInfoResponseContextLookupStrategy = Constraint.isNotNull(strategy,
+ "UserInfoResponseContext lookup strategy cannot be null");
+ }
+
+ @Override protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext,
+ @Nonnull final AuthenticationContext authenticationContext) {
+
+ final UserInfoResponseContext userInfoCtx =
+ userInfoResponseContextLookupStrategy.apply(profileRequestContext);
+ if (userInfoCtx == null) {
+ log.debug("{} No UserInfo response context returned by lookup strategy", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+ return false;
+ }
+ if (userInfoCtx.getUserInfo() == null) {
+ log.debug("{} No UserInfo returned by lookup strategy", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+ return false;
+ }
+ userInfoClaims = userInfoCtx.getUserInfo().getClaimsSet();
+
+ final AccessTokenResponseContext tokenResponseCtx =
+ tokenResponseContextLookupStrategy.apply(profileRequestContext);
+ if (tokenResponseCtx == null) {
+ log.debug("{} No AccessTokenResponseContext returned by lookup strategy", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+ return false;
+ }
+ if (tokenResponseCtx.getIdToken() == null) {
+ log.debug("{} AccessTokenResponseContext did not contain an id_token", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+ return false;
+ }
+ try {
+ idTokenClaims = tokenResponseCtx.getIdToken().getJWTClaimsSet();
+ if (idTokenClaims == null) {
+ log.debug("{} AccessTokenResponseContext did not contain an id_token with accessible claims, "
+ + "possibly still encrypted",
+ getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+ return false;
+ }
+ } catch (final ParseException e) {
+ log.debug("{} Unable to parse claims from id_token", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+ }
+
+ return true;
+ }
+
+ @Override protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
+ @Nonnull final AuthenticationContext authenticationContext) {
+
+ log.trace("{} Merging UserInfo and id_token claims", getLogPrefix());
+
+ final ClaimsSet mergedClaims = claimMergingStrategy.apply(userInfoClaims, idTokenClaims);
+
+ // Add to end user claims context
+ endUserClaimsContextLookupStrategy.apply(profileRequestContext).setEndUserClaims(mergedClaims);
+
+ if (log.isTraceEnabled()) {
+ log.trace("{} Merged UserInfo and id_token claims to produce '{}'",
+ getLogPrefix(), mergedClaims.toJSONString());
+ }
+ }
+
+}
diff --git a/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/TokenResponseIDTokenLookupStrategy.java b/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/TokenResponseIDTokenLookupStrategy.java
index 49e7b63..dd4ebbb 100644
--- a/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/TokenResponseIDTokenLookupStrategy.java
+++ b/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/TokenResponseIDTokenLookupStrategy.java
@@ -27,7 +27,7 @@ import org.opensaml.profile.context.ProfileRequestContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import net.shibboleth.idp.plugin.authn.oidc.rp.context.TokenResponseContext;
+import net.shibboleth.idp.plugin.authn.oidc.rp.context.AccessTokenResponseContext;
/** Return the raw base64 encoded id_token from the TokenResponseContext, or null if not found.*/
public class TokenResponseIDTokenLookupStrategy implements Function<ProfileRequestContext, String> {
@@ -43,7 +43,7 @@ public class TokenResponseIDTokenLookupStrategy implements Function<ProfileReque
log.debug("Inbound message context was null, no id_token found");
return null;
}
- final TokenResponseContext tokenResponseContext = inbound.getSubcontext(TokenResponseContext.class);
+ final AccessTokenResponseContext tokenResponseContext = inbound.getSubcontext(AccessTokenResponseContext.class);
if (tokenResponseContext == null) {
log.debug("Token response context was null, no id_token found");
return null;
diff --git a/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/UserInfoEndpointLookup.java b/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/UserInfoEndpointLookup.java
new file mode 100644
index 0000000..615fa12
--- /dev/null
+++ b/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/UserInfoEndpointLookup.java
@@ -0,0 +1,99 @@
+/*
+ * 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.util.function.Function;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.profile.context.navigate.InboundMessageContextLookup;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import net.shibboleth.idp.authn.AuthnEventIds;
+import net.shibboleth.idp.authn.context.AuthenticationContext;
+import net.shibboleth.idp.plugin.authn.oidc.rp.OIDCRPException;
+import net.shibboleth.idp.plugin.authn.oidc.rp.context.AccessTokenResponseContext;
+import net.shibboleth.idp.plugin.authn.oidc.rp.context.UserInfoResponseContext;
+import net.shibboleth.idp.plugin.authn.oidc.rp.messaging.UserInfoResponse;
+import net.shibboleth.utilities.java.support.component.ComponentSupport;
+import net.shibboleth.utilities.java.support.logic.Constraint;
+
+/**
+ * Request information from the UserInfo OAuth2.0 endpoint using the access_token already present
+ * in the context. Return consented claims about the subject.
+ */
+public class UserInfoEndpointLookup extends AbstractHttpOIDCAuthenticationAction<UserInfoResponse> {
+
+ /** Class logger.*/
+ @Nonnull private final Logger log = LoggerFactory.getLogger(UserInfoEndpointLookup.class);
+
+ /** Strategy used to look up the {@link AccessTokenResponseContext} to set the parameters for. */
+ @Nonnull private Function<ProfileRequestContext, UserInfoResponseContext>
+ userInfoResponseContextLookupStrategy;
+
+
+ /** Constructor.*/
+ public UserInfoEndpointLookup() {
+ userInfoResponseContextLookupStrategy =
+ new ChildContextLookup<>(UserInfoResponseContext.class, true).compose(
+ new InboundMessageContextLookup());
+ }
+
+ /**
+ * Set the strategy used to look up a {@link UserInfoResponseContext}.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setUserInfoResponseContextLookupStrategy(
+ @Nonnull final Function<ProfileRequestContext, UserInfoResponseContext> strategy) {
+ ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+
+ userInfoResponseContextLookupStrategy = Constraint.isNotNull(strategy,
+ "UserInfoResponseContext lookup strategy cannot be null");
+ }
+
+ @Override protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
+ @Nonnull final AuthenticationContext authenticationContext) {
+
+ log.debug("{} Requesting claims from UserInfo endpoint from upstream OP '{}'", getLogPrefix(),
+ authenticationContext.getAuthenticatingAuthority());
+
+ final UserInfoResponseContext userInfoCtx =
+ userInfoResponseContextLookupStrategy.apply(profileRequestContext);
+ if (userInfoCtx == null) {
+ log.debug("{} No UserInfo response context returned by lookup strategy", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+ return;
+ }
+
+ try {
+ final UserInfoResponse responseObject = handleRequest(profileRequestContext);
+ userInfoCtx.setUserInfo(responseObject);
+
+ } catch (final OIDCRPException e) {
+ log.error("{} Unable to return claims from UserInfo endpoint",getLogPrefix(),e);
+ ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.AUTHN_EXCEPTION);
+ }
+ }
+
+}
diff --git a/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/ValidateIDTokenSignature.java b/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/ValidateIDTokenSignature.java
index 8e1b26b..992a6eb 100644
--- a/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/ValidateIDTokenSignature.java
+++ b/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/ValidateIDTokenSignature.java
@@ -21,32 +21,25 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.StringWriter;
-import java.security.interfaces.RSAPublicKey;
import java.text.ParseException;
import java.util.Map;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
-import net.shibboleth.idp.authn.AbstractAuthenticationAction;
-import net.shibboleth.idp.authn.AuthnEventIds;
-import net.shibboleth.idp.authn.context.AuthenticationContext;
-import net.shibboleth.idp.plugin.authn.oidc.rp.context.OpenIDConnectContext;
-
-import org.opensaml.profile.action.ActionSupport;
import org.opensaml.profile.context.ProfileRequestContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.base.Charsets;
import com.google.common.io.CharStreams;
-import com.nimbusds.jose.JOSEException;
-import com.nimbusds.jose.crypto.RSASSAVerifier;
-import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.shaded.json.JSONArray;
import com.nimbusds.jose.shaded.json.JSONObject;
import com.nimbusds.jose.util.JSONObjectUtils;
-import com.nimbusds.jwt.SignedJWT;
+
+import net.shibboleth.idp.authn.AbstractAuthenticationAction;
+import net.shibboleth.idp.authn.AuthnEventIds;
+import net.shibboleth.idp.authn.context.AuthenticationContext;
/**
* An action that verifies the signature of a JWS id_token using the RSA key belonging to the keyID
@@ -72,51 +65,7 @@ public class ValidateIDTokenSignature extends AbstractAuthenticationAction {
protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
@Nonnull final AuthenticationContext authenticationContext) {
- final OpenIDConnectContext oidcCtx =
- authenticationContext.getSubcontext(OpenIDConnectContext.class);
- if (oidcCtx == null) {
- log.error("{} Unable to find oidc context", getLogPrefix());
- ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
- return;
- }
- SignedJWT signedJWT = null;
- try {
- //TODO P.S. oidcCtx.getIDToken() could be null.
- signedJWT = SignedJWT.parse(oidcCtx.getIDToken().serialize());
- } catch (final ParseException e) {
- log.error("{} Error when parsing signed JWT", getLogPrefix(), e);
- ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
- return;
- }
- RSAPublicKey providerKey = null;
- try {
- final JSONObject key = getProviderRSAJWK(oidcCtx.getoIDCProviderMetadata().getJWKSetURI()
- .toURL().openStream(),signedJWT.getHeader().getKeyID());
- if (key == null) {
- log.error("{} Unable to find key to verify signature", getLogPrefix());
- ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
- return;
- }
- providerKey = RSAKey.parse(key).toRSAPublicKey();
- } catch (final IOException | java.text.ParseException | JOSEException e) {
- log.error("{} Error when parsing key to verify signature", getLogPrefix(), e);
- ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
- return;
- }
- final RSASSAVerifier verifier = new RSASSAVerifier(providerKey);
- try {
- if (!signedJWT.verify(verifier)) {
- log.error("{} JWT signature verification failed", getLogPrefix());
- ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
- return;
- }
- } catch (final JOSEException e) {
- log.error("{} JWT signature verification not performed", getLogPrefix(), e);
- ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
- return;
- }
- log.debug("{} ID Token signature verified",getLogPrefix());
- return;
+
}
/**
diff --git a/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/ValidateOAuthAccessTokenResponse.java b/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/ValidateOAuthAccessTokenResponse.java
index f442388..7e1b373 100644
--- a/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/ValidateOAuthAccessTokenResponse.java
+++ b/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/ValidateOAuthAccessTokenResponse.java
@@ -1,3 +1,20 @@
+/*
+ * 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.util.Map;
@@ -14,7 +31,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import net.shibboleth.idp.authn.context.AuthenticationContext;
-import net.shibboleth.idp.plugin.authn.oidc.rp.context.TokenResponseContext;
+import net.shibboleth.idp.plugin.authn.oidc.rp.context.AccessTokenResponseContext;
import net.shibboleth.oidc.profile.core.OidcEventIds;
import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
import net.shibboleth.utilities.java.support.component.ComponentSupport;
@@ -27,24 +44,24 @@ public class ValidateOAuthAccessTokenResponse extends AbstractOIDCAuthentication
/** Class logger. */
@Nonnull @NotEmpty private final Logger log = LoggerFactory.getLogger(ValidateOAuthAccessTokenResponse.class);
- /** Strategy used to look up the {@link TokenResponseContext} to validate. */
- @Nonnull private Function<ProfileRequestContext, TokenResponseContext>
+ /** Strategy used to look up the {@link AccessTokenResponseContext} to validate. */
+ @Nonnull private Function<ProfileRequestContext, AccessTokenResponseContext>
tokenResponseContextLookupStrategy;
/** Constructor.*/
public ValidateOAuthAccessTokenResponse() {
tokenResponseContextLookupStrategy =
- new ChildContextLookup<>(TokenResponseContext.class, true).compose(
+ new ChildContextLookup<>(AccessTokenResponseContext.class, true).compose(
new InboundMessageContextLookup());
}
/**
- * Set the strategy used to look up a {@link TokenResponseContext}.
+ * Set the strategy used to look up a {@link AccessTokenResponseContext}.
*
* @param strategy lookup strategy
*/
public void setTokenResponseContextLookupStrategy(
- @Nonnull final Function<ProfileRequestContext, TokenResponseContext> strategy) {
+ @Nonnull final Function<ProfileRequestContext, AccessTokenResponseContext> strategy) {
ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
tokenResponseContextLookupStrategy = Constraint.isNotNull(strategy,
@@ -55,7 +72,7 @@ public class ValidateOAuthAccessTokenResponse extends AbstractOIDCAuthentication
@Override protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
@Nonnull final AuthenticationContext authenticationContext) {
- final TokenResponseContext responseCtx = tokenResponseContextLookupStrategy.apply(profileRequestContext);
+ final AccessTokenResponseContext responseCtx = tokenResponseContextLookupStrategy.apply(profileRequestContext);
if (responseCtx == null) {
log.debug("{} No TokenResponseContext returned by lookup strategy", getLogPrefix());
ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
diff --git a/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/ValidateOIDCAuthentication.java b/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/ValidateOIDCAuthentication.java
index d0eac70..67d8fb0 100644
--- a/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/ValidateOIDCAuthentication.java
+++ b/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/ValidateOIDCAuthentication.java
@@ -18,35 +18,55 @@
package net.shibboleth.idp.plugin.authn.oidc.rp.impl;
import java.text.ParseException;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
+import java.util.Collection;
import java.util.Map;
+import java.util.function.Function;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.security.auth.Subject;
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.profile.context.navigate.InboundMessageContextLookup;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.collect.HashMultimap;
+import com.google.common.collect.Multimap;
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.openid.connect.sdk.claims.ClaimsSet;
+
+import net.minidev.json.JSONObject;
+import net.shibboleth.idp.attribute.AttributeDecodingException;
+import net.shibboleth.idp.attribute.AttributeEncodingException;
import net.shibboleth.idp.attribute.IdPAttribute;
-import net.shibboleth.idp.attribute.StringAttributeValue;
+import net.shibboleth.idp.attribute.transcoding.AttributeTranscoder;
+import net.shibboleth.idp.attribute.transcoding.AttributeTranscoderRegistry;
+import net.shibboleth.idp.attribute.transcoding.TranscoderSupport;
+import net.shibboleth.idp.attribute.transcoding.TranscodingRule;
import net.shibboleth.idp.authn.AbstractValidationAction;
import net.shibboleth.idp.authn.AuthenticationResult;
import net.shibboleth.idp.authn.AuthnEventIds;
import net.shibboleth.idp.authn.context.AuthenticationContext;
-import net.shibboleth.idp.plugin.authn.oidc.rp.context.OpenIDConnectContext;
import net.shibboleth.idp.authn.principal.IdPAttributePrincipal;
import net.shibboleth.idp.authn.principal.UsernamePrincipal;
+import net.shibboleth.idp.plugin.authn.oidc.rp.context.AccessTokenResponseContext;
+import net.shibboleth.idp.plugin.authn.oidc.rp.context.EndUserClaimsContext;
+import net.shibboleth.idp.profile.IdPEventIds;
+import net.shibboleth.idp.profile.context.RelyingPartyContext;
+import net.shibboleth.oidc.profile.config.OIDCAuthorizationConfiguration;
import net.shibboleth.utilities.java.support.annotation.constraint.Live;
+import net.shibboleth.utilities.java.support.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.utilities.java.support.annotation.constraint.NonnullElements;
+import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
+import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
import net.shibboleth.utilities.java.support.component.ComponentSupport;
-import net.shibboleth.utilities.java.support.primitive.StringSupport;
-
-import org.opensaml.profile.action.ActionSupport;
-import org.opensaml.profile.context.ProfileRequestContext;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import com.nimbusds.jwt.JWTClaimsSet;
-import com.nimbusds.openid.connect.sdk.claims.UserInfo;
+import net.shibboleth.utilities.java.support.logic.Constraint;
+import net.shibboleth.utilities.java.support.service.ReloadableService;
+import net.shibboleth.utilities.java.support.service.ServiceableComponent;
/**
* An action that builds an {@link AuthenticationResult} from the subject (sub claim) of the OpenID Connect token.
@@ -61,11 +81,13 @@ import com.nimbusds.openid.connect.sdk.claims.UserInfo;
* @post If AuthenticationContext.getSubcontext(OpenIDConnectContext.class, false).getIDToken()
* .getJWTClaimsSet().getSubject()!= null, then an {@link net.shibboleth.idp.authn.AuthenticationResult}
* is saved to the {@link AuthenticationContext}.
- *
- * @since 4.0.0
*/
- at SuppressWarnings({"rawtypes", "unchecked"})
+//TODO if we pull claims from a UserInfo endpoint, we need to augment with claims in the id_token and this
+// only looks at the id_token.
public class ValidateOIDCAuthentication extends AbstractValidationAction {
+
+ /** Default prefix for metrics. */
+ @Nonnull @NotEmpty private static final String DEFAULT_METRIC_NAME = "net.shibboleth.idp.authn.oidc.rp";
/** Class logger. */
@Nonnull private final Logger log = LoggerFactory.getLogger(ValidateOIDCAuthentication.class);
@@ -75,9 +97,57 @@ public class ValidateOIDCAuthentication extends AbstractValidationAction {
/** the subject received from id token. */
@Nullable private String oidcSubject;
+
+ /** Transcoder registry service object. */
+ @NonnullAfterInit private ReloadableService<AttributeTranscoderRegistry> transcoderRegistry;
+
+ /** Strategy used to look up a {@link RelyingPartyContext} for configuration options. */
+ @Nonnull private Function<ProfileRequestContext,RelyingPartyContext> relyingPartyContextLookupStrategy;
+
+ /** Store off profile config. */
+ @Nullable private OIDCAuthorizationConfiguration profileConfiguration;
+
+ /** Claims Set to extract and decode claims from. */
+ @Nullable private ClaimsSet claimsSet;
+
+ /** Strategy used to look up the {@link EndUserClaimsContext} to set the parameters for. */
+ @Nonnull private Function<ProfileRequestContext, EndUserClaimsContext>
+ endUserClaimsContextLookupStrategy;
+
+ /** Constructor.*/
+ public ValidateOIDCAuthentication() {
+ setMetricName(DEFAULT_METRIC_NAME);
+ relyingPartyContextLookupStrategy = new ChildContextLookup<>(RelyingPartyContext.class);
+
+ endUserClaimsContextLookupStrategy =
+ new ChildContextLookup<>(EndUserClaimsContext.class, true).compose(
+ new InboundMessageContextLookup());
+ }
+
+
+ /**
+ * Sets the registry of transcoding rules to apply to encode attributes.
+ *
+ * @param registry registry service interface
+ */
+ public void setTranscoderRegistry(@Nonnull final ReloadableService<AttributeTranscoderRegistry> registry) {
+ ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+
+ transcoderRegistry = Constraint.isNotNull(registry, "AttributeTranscoderRegistry cannot be null");
+ }
+
+ /**
+ * Set the strategy used to return the {@link RelyingPartyContext} for configuration options.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setRelyingPartyContextLookupStrategy(
+ @Nonnull final Function<ProfileRequestContext,RelyingPartyContext> strategy) {
+ ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
- /** The JWT Claim Set of the ID Token acquired from the token endpoint. */
- @Nullable private JWTClaimsSet jwtClaims;
+ relyingPartyContextLookupStrategy =
+ Constraint.isNotNull(strategy, "RelyingPartyContext lookup strategy cannot be null");
+ }
/**
* In MFA use case prior authentication may have created a usernameprincipal already with value not matching to MFA.
@@ -88,6 +158,16 @@ public class ValidateOIDCAuthentication extends AbstractValidationAction {
ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
avoidMultiplePrincipal = avoid;
}
+
+ /** {@inheritDoc} */
+ @Override
+ protected void doInitialize() throws ComponentInitializationException {
+ super.doInitialize();
+
+// if (transcoderRegistry == null) {
+// throw new ComponentInitializationException("AttributeTranscoderRegistry cannot be null");
+// }
+ }
/** {@inheritDoc} */
@Override
@@ -97,35 +177,36 @@ public class ValidateOIDCAuthentication extends AbstractValidationAction {
if (!super.doPreExecute(profileRequestContext, authenticationContext)) {
return false;
}
- log.trace("{}: Prerequisities fulfilled to start doPreExecute", getLogPrefix());
- final OpenIDConnectContext oidcCtx = authenticationContext.getSubcontext(OpenIDConnectContext.class);
- if (oidcCtx == null) {
- log.error("{} Unable to find oidc context", getLogPrefix());
- ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
+ final RelyingPartyContext rpContext = relyingPartyContextLookupStrategy.apply(profileRequestContext);
+ if (rpContext == null) {
+ log.error("{} Unable to locate RelyingPartyContext", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, IdPEventIds.INVALID_RELYING_PARTY_CTX);
return false;
- }
-
- if (oidcCtx.getIDToken() == null) {
- log.error("{} No ID Token in response", getLogPrefix());
- ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
+ } else if (rpContext.getProfileConfig() == null) {
+ log.error("{} Unable to locate profile configuration", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, IdPEventIds.INVALID_PROFILE_CONFIG);
+ return false;
+ } else if (!(rpContext.getProfileConfig() instanceof OIDCAuthorizationConfiguration)) {
+ log.error("{} No OIDC SSO profile configuration", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, IdPEventIds.INVALID_PROFILE_CONFIG);
return false;
}
- try {
- //TODO P.S. this will fail if not decrypted JWE.
- oidcSubject = StringSupport.trimOrNull(oidcCtx.getIDToken().getJWTClaimsSet().getSubject());
- jwtClaims = oidcCtx.getIDToken().getJWTClaimsSet();
- } catch (final ParseException e) {
- log.error("{} unable to parse ID Token", getLogPrefix());
- ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
+ profileConfiguration = (OIDCAuthorizationConfiguration) rpContext.getProfileConfig();
+
+ final EndUserClaimsContext claimsContext = endUserClaimsContextLookupStrategy.apply(profileRequestContext);
+ if (claimsContext == null) {
+ log.error("{} Unable to locate end-user claims context", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
return false;
}
-
- if (oidcSubject == null) {
- log.error("{} Subject is null in ID Token response", getLogPrefix());
- ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
+ if (claimsContext.getEndUserClaims() == null) {
+ log.error("{} Unable to locate end-user claims", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
return false;
}
+ claimsSet = claimsContext.getEndUserClaims();
+
return true;
}
@@ -134,64 +215,20 @@ public class ValidateOIDCAuthentication extends AbstractValidationAction {
protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
@Nonnull final AuthenticationContext authenticationContext) {
+ recordSuccess(profileRequestContext);
+
+ log.trace("{} Validating OIDC proxy authentication", getLogPrefix());
+
+ if (transcoderRegistry != null) {
+ processAttributes(profileRequestContext);
+ }
+ //TODO use an attributeExtractionStrategy
+
buildAuthenticationResult(profileRequestContext, authenticationContext);
return;
}
- /**
- * For each OIDC Standard Claim (OpenID Connect Core 1.0 section 5.1) in the JWT Claim set {@code jwtClaimSet},
- * build a {@link IdPAttributePrincipal} using a {@link StringAttributeValue}.
- * <p>
- * Any claim that is not understood (not in the standard claim set) *will* be ignored.
- * <p>
- * All claims are strings except {@code email_verified} and {code phone_verified) (both booleans), {@code address}
- * (JSON Object), and {@code updated_at} (number). Both booleans are represented as string attribute values, the
- * {@code address} and {@code updated_at} claims are currently ignored.
- *
- *
- * @return a list of standard OIDC claims as {@link IdPAttributePrincipal}s.
- */
- @Nonnull
- @Live
- private List<IdPAttributePrincipal> buildIdPAttributePrincipalsFromStandardClaims() {
-
- final List<IdPAttributePrincipal> claimPrincipals = new ArrayList<>();
- if (jwtClaims != null) {
- // jwtClaims.getClaims() is never null
- for (final Map.Entry<String, Object> claim : jwtClaims.getClaims().entrySet()) {
-
- if (UserInfo.getStandardClaimNames().contains(claim.getKey())) {
-
- String claimValue = null;
- if (claim.getValue() instanceof String) {
- claimValue = StringSupport.trimOrNull((String) claim.getValue());
- }
-
- if (claim.getValue() instanceof Boolean) {
- claimValue = StringSupport.trimOrNull(Boolean.toString((Boolean) claim.getValue()));
- }
- if (claimValue == null) {
- log.trace("{} JWT Claim [{}] is not of a supported type or is null/empty, ignored",
- getLogPrefix(), claim);
- continue;
- }
-
- final IdPAttribute idpAttr = new IdPAttribute(claim.getKey());
- idpAttr.setValues(Collections.singletonList(new StringAttributeValue(claimValue)));
-
- final IdPAttributePrincipal attrPrincipal = new IdPAttributePrincipal(idpAttr);
- log.trace("{} Constructed IdPAttributePrincipal from OIDC claim [{}]", getLogPrefix(),
- attrPrincipal);
- claimPrincipals.add(attrPrincipal);
- }
-
- }
- }
-
- return claimPrincipals;
-
- }
-
+
@Override
protected Subject populateSubject(@Nonnull final Subject subject) {
@@ -199,11 +236,89 @@ public class ValidateOIDCAuthentication extends AbstractValidationAction {
log.debug("{} Subject already contains principal, not populated", getLogPrefix());
} else {
- subject.getPrincipals().add(new UsernamePrincipal(oidcSubject));
- subject.getPrincipals().addAll(buildIdPAttributePrincipalsFromStandardClaims());
+ subject.getPrincipals().add(new UsernamePrincipal(claimsSet.getStringClaim("sub")));
+ //subject.getPrincipals().addAll(buildIdPAttributePrincipalsFromStandardClaims());
}
return subject;
}
+
+ /**
+ * Process the inbound OIDC claims.
+ *
+ * @param profileRequestContext current profile request context
+ */
+ private void processAttributes(@Nonnull final ProfileRequestContext profileRequestContext) {
+
+ log.debug("{} Decoding incoming OIDC claims", getLogPrefix());
+
+ final Multimap<String,IdPAttribute> mapped = HashMultimap.create();
+
+ ServiceableComponent<AttributeTranscoderRegistry> component = null;
+ try {
+ component = transcoderRegistry.getServiceableComponent();
+ if (component == null) {
+ log.error("Attribute transcoder service unavailable");
+ return;
+ }
+
+ for (final Map.Entry<String, Object> claim : claimsSet.toJSONObject().entrySet()) {
+ try {
+ //Only allow the standard claims? so no exp. etc
+ final JSONObject jsonClaim = new JSONObject();
+ jsonClaim.put(claim.getKey(), claim.getValue());
+ decodeAttribute(component.getComponent(), profileRequestContext, jsonClaim, mapped);
+ } catch (final AttributeDecodingException e) {
+ log.error("{} Error decoding inbound claim", getLogPrefix(), e);
+ }
+ }
+ } finally {
+ if (component != null) {
+ component.unpinComponent();
+ }
+ }
+
+ log.debug("{} Incoming OIDC Attributes mapped to attribute IDs: {}", getLogPrefix(), mapped.keySet());
+
+ if (!mapped.isEmpty()) {
+// attributeContext = profileRequestContext
+// .getSubcontext(RelyingPartyContext.class)
+// .getSubcontext(AttributeContext.class, true);
+// attributeContext.setUnfilteredIdPAttributes(mapped.values());
+// attributeContext.setIdPAttributes(null);
+// filterAttributes(profileRequestContext);
+ }
+ }
+
+
+ /**
+ * Access the registry of transcoding rules to transform (decode) the input claims to IdP Attributes.
+ *
+ * @param registry registry of transcoding rules
+ * @param profileRequestContext current profile request context
+ * @param input input attribute
+ * @param results collection to add results to
+ *
+ * @throws AttributeEncodingException if a non-ignorable error occurs
+ */
+ private void decodeAttribute(@Nonnull final AttributeTranscoderRegistry registry,
+ @Nonnull final ProfileRequestContext profileRequestContext, @Nonnull final JSONObject input,
+ @Nonnull @NonnullElements @Live final Multimap<String,IdPAttribute> results)
+ throws AttributeDecodingException {
+
+ final Collection<TranscodingRule> transcodingRules = registry.getTranscodingRules(input);
+ if (transcodingRules.isEmpty()) {
+ log.info("{} No transcoding rule for Attribute '{}'", getLogPrefix(), input);
+ return;
+ }
+
+ for (final TranscodingRule rules : transcodingRules) {
+ final AttributeTranscoder<JSONObject> transcoder = TranscoderSupport.getTranscoder(rules);
+ final IdPAttribute decodedAttribute = transcoder.decode(profileRequestContext, input, rules);
+ if (decodedAttribute != null) {
+ results.put(decodedAttribute.getId(), decodedAttribute);
+ }
+ }
+ }
}
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 a740a3d..b1a2bb9 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
@@ -1,3 +1,20 @@
+/*
+ * 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.text.ParseException;
@@ -18,7 +35,6 @@ import com.nimbusds.jwt.JWTClaimsSet;
import net.shibboleth.idp.authn.AuthnEventIds;
import net.shibboleth.idp.authn.context.AuthenticationContext;
import net.shibboleth.idp.plugin.authn.oidc.rp.OIDCRPException;
-import net.shibboleth.idp.plugin.authn.oidc.rp.context.OpenIDConnectContext;
import net.shibboleth.oidc.jwt.claims.ClaimsValidator;
import net.shibboleth.oidc.jwt.claims.JWTValidationException;
import net.shibboleth.utilities.java.support.annotation.constraint.NonnullAfterInit;
@@ -134,7 +150,7 @@ public class ValidateTokenClaims extends AbstractOIDCAuthenticationAction {
final JWT token = jwtLookupStrategy.apply(profileRequestContext);
if (token == null) {
- log.error("{} id_token was not located, nothing to validate", getLogPrefix());
+ log.error("{} JWT was not located, nothing to validate", getLogPrefix());
ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.INVALID_AUTHN_CTX);
return false;
}
@@ -145,7 +161,7 @@ public class ValidateTokenClaims extends AbstractOIDCAuthenticationAction {
throw new OIDCRPException("JWT ClaimsSet is null");
}
} catch (final ParseException | OIDCRPException e) {
- log.error("{} Claimset of id_token is not available", getLogPrefix(),e);
+ log.error("{} JWT Claimset is not available", getLogPrefix(),e);
ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.INVALID_AUTHN_CTX);
return false;
}
@@ -157,7 +173,7 @@ public class ValidateTokenClaims extends AbstractOIDCAuthenticationAction {
protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
@Nonnull final AuthenticationContext authenticationContext) {
- log.debug("{} Validating token claims for subject '{}'",getLogPrefix(),
+ log.debug("{} Validating JWT token claims for subject '{}'",getLogPrefix(),
claimsSet.getSubject() != null ? claimsSet.getSubject() : "unknown subject");
try {
@@ -166,7 +182,7 @@ public class ValidateTokenClaims extends AbstractOIDCAuthenticationAction {
cleanupHook.accept(profileRequestContext);
}
} catch (final JWTValidationException e) {
- log.error("{} Token verification failed for subject '{}'", getLogPrefix(),claimsSet.getSubject(),e);
+ log.error("{} JWT token verification failed for subject '{}'", getLogPrefix(),claimsSet.getSubject(),e);
ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
if (cleanupHook != null) {
cleanupHook.accept(profileRequestContext);
@@ -174,7 +190,7 @@ public class ValidateTokenClaims extends AbstractOIDCAuthenticationAction {
return;
}
//fine.
- log.debug("{} Token claims are valid for subject '{}'",getLogPrefix(),claimsSet.getSubject());
+ log.debug("{} JWT token claims are valid for subject '{}'",getLogPrefix(),claimsSet.getSubject());
}
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/ValidateUserInfoClaims.java
new file mode 100644
index 0000000..5254b4d
--- /dev/null
+++ b/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/ValidateUserInfoClaims.java
@@ -0,0 +1,171 @@
+/*
+ * 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.text.ParseException;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.profile.context.navigate.InboundMessageContextLookup;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.nimbusds.jwt.JWTClaimsSet;
+
+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;
+import net.shibboleth.idp.plugin.authn.oidc.rp.messaging.UserInfoResponse;
+import net.shibboleth.oidc.profile.core.OidcEventIds;
+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.
+ */
+public class ValidateUserInfoClaims extends AbstractOIDCAuthenticationAction {
+
+ /** Class logger.*/
+ @Nonnull private final Logger log = LoggerFactory.getLogger(ValidateUserInfoClaims.class);
+
+ /** Strategy used to look up the {@link UserInfoResponseContext}. */
+ @Nonnull private Function<ProfileRequestContext, UserInfoResponseContext>
+ userInfoResponseContextLookupStrategy;
+
+ /** Strategy used to look up the {@link AccessTokenResponseContext}. */
+ @Nonnull private Function<ProfileRequestContext, AccessTokenResponseContext>
+ tokenResponseContextLookupStrategy;
+
+ /** The stashed user info response context.*/
+ @Nullable private UserInfoResponseContext userInfoCtx;
+
+ /** The stashed id_token claims.*/
+ @Nullable private JWTClaimsSet idTokenClaims;
+
+ /** Constructor.*/
+ public ValidateUserInfoClaims() {
+ userInfoResponseContextLookupStrategy =
+ new ChildContextLookup<>(UserInfoResponseContext.class).compose(
+ new InboundMessageContextLookup());
+ tokenResponseContextLookupStrategy =
+ new ChildContextLookup<>(AccessTokenResponseContext.class).compose(
+ new InboundMessageContextLookup());
+ }
+
+ /**
+ * Set the strategy used to look up a {@link AccessTokenResponseContext}.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setTokenResponseContextLookupStrategy(
+ @Nonnull final Function<ProfileRequestContext, AccessTokenResponseContext> strategy) {
+ ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+
+ tokenResponseContextLookupStrategy = Constraint.isNotNull(strategy,
+ "TokenResponseContext lookup strategy cannot be null");
+ }
+
+ /**
+ * Set the strategy used to look up a {@link UserInfoResponseContext}.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setUserInfoResponseContextLookupStrategy(
+ @Nonnull final Function<ProfileRequestContext, UserInfoResponseContext> strategy) {
+ ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+
+ userInfoResponseContextLookupStrategy = Constraint.isNotNull(strategy,
+ "UserInfoResponseContext lookup strategy cannot be null");
+ }
+
+ @Override protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext,
+ @Nonnull final AuthenticationContext authenticationContext) {
+
+ userInfoCtx =
+ userInfoResponseContextLookupStrategy.apply(profileRequestContext);
+ if (userInfoCtx == null) {
+ log.debug("{} No UserInfo response context returned by lookup strategy", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+ return false;
+ }
+
+ final AccessTokenResponseContext tokenResponseCtx =
+ tokenResponseContextLookupStrategy.apply(profileRequestContext);
+ if (tokenResponseCtx == null) {
+ log.debug("{} No AccessTokenResponseContext returned by lookup strategy", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+ return false;
+ }
+ if (tokenResponseCtx.getIdToken() == null) {
+ log.debug("{} AccessTokenResponseContext did not contain an id_token", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+ return false;
+ }
+ try {
+ idTokenClaims = tokenResponseCtx.getIdToken().getJWTClaimsSet();
+ if (idTokenClaims == null) {
+ log.debug("{} AccessTokenResponseContext did not contain an id_token with accessible claims, "
+ + "possibly still encrypted",
+ getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+ return false;
+ }
+ } catch (final ParseException e) {
+ log.debug("{} Unable to parse claims from id_token", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+ }
+
+ return true;
+ }
+
+ @Override protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
+ @Nonnull final AuthenticationContext authenticationContext) {
+
+ log.trace("{} Validating UserInfo claims", getLogPrefix());
+
+ final UserInfoResponse response = userInfoCtx.getUserInfo();
+ if (!response.isClaimsSetAvailable()) {
+ log.debug("{} UserInfo claims are not available, check response not still encrypted", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_USERINFO_CLAIMS);
+ return;
+ }
+ final String subFromUserInfo = response.getClaimsSet().getStringClaim("sub");
+ if (subFromUserInfo == null) {
+ log.debug("{} UserInfo claims does not contain the 'sub' claim, it must", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_USERINFO_CLAIMS);
+ return;
+ }
+ // sub must match to id_token sub
+ if (!idTokenClaims.getSubject().equals(subFromUserInfo)){
+ log.debug("{} UserInfo claims about subject '{}' but id_token about subject '{}', mismatch",
+ getLogPrefix(), subFromUserInfo, idTokenClaims.getSubject());
+ ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_USERINFO_CLAIMS);
+ return;
+ }
+ log.debug("{} UserInfo claims are valid for '{}'", getLogPrefix(),
+ response.getClaimsSet().getStringClaim("sub"));
+
+ }
+
+}
diff --git a/idp-oidc-rp-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml b/idp-oidc-rp-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
index c5fe8cc..b464d4d 100644
--- a/idp-oidc-rp-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
+++ b/idp-oidc-rp-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
@@ -89,6 +89,14 @@
</property> -->
</bean>
+ <!-- Necessary for encoder parsing and claims mapping to function. -->
+
+ <bean parent="shibboleth.RegistryNamingFunction" c:claz="net.minidev.json.JSONObject">
+ <constructor-arg name="function">
+ <bean class="net.shibboleth.oidc.attribute.transcoding.AbstractOIDCAttributeTranscoder.NamingFunction" />
+ </constructor-arg>
+ </bean>
+
<!-- Controller implementation -->
<bean id="shibboleth.oidc.rp.OpenIDConnectStartServlet"
class="net.shibboleth.idp.plugin.authn.oidc.rp.impl.AuthorizationController" />
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 a8f2a01..95e9857 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
@@ -206,8 +206,8 @@
p:authenticationContextLookupStrategy-ref="ParentAuthenticiationContextLookup"
p:httpClient="#{getObject('shibboleth.authn.oidc.rp.HttpClient') ?: getObject('shibboleth.InternalHttpClient')}"
p:httpClientSecurityParameters="#{getObject('shibboleth.authn.oidc.rp.HttpClientSecurityParameters')}"
- p:tokenResponseDecoderStrategy="#{getObject('shibboleth.authn.oidc.rp.TokenResponseDecoder') ?: getObject('shibboleth.authn.oidc.rp.DefaultTokenResponseDecoder')}"
- p:tokenRequestEncoderStrategy="#{getObject('shibboleth.authn.oidc.rp.TokenRequestEncoder') ?: getObject('shibboleth.authn.oidc.rp.DefaultTokenRequestEncoder')}"/>
+ p:httpResponseDecoderStrategy="#{getObject('shibboleth.authn.oidc.rp.TokenResponseDecoder') ?: getObject('shibboleth.authn.oidc.rp.DefaultTokenResponseDecoder')}"
+ p:httpRequestEncoderStrategy="#{getObject('shibboleth.authn.oidc.rp.TokenRequestEncoder') ?: getObject('shibboleth.authn.oidc.rp.DefaultTokenRequestEncoder')}"/>
<bean id="ValidateOAuthAccessTokenResponse"
class="net.shibboleth.idp.plugin.authn.oidc.rp.impl.ValidateOAuthAccessTokenResponse"
@@ -224,7 +224,7 @@
<!-- could these be singletons? -->
<bean id="shibboleth.authn.oidc.rp.DefaultTokenResponseDecoder" scope="prototype"
- class="net.shibboleth.idp.plugin.authn.oidc.rp.decoding.impl.DefaultTokenResponseDecoder"
+ class="net.shibboleth.idp.plugin.authn.oidc.rp.decoding.impl.DefaultMapResponseDecoder"
p:objectMapper-ref="shibboleth.authn.oidc.rp.JSONObjectMapper"/>
<bean id="shibboleth.authn.oidc.rp.DefaultTokenRequestEncoder" scope="prototype"
@@ -275,15 +275,16 @@
<!-- Default id_token JWT validation wiring. -->
+ <!-- No default cleanup, maybe could be to remove nonce etc. -->
<bean id="ValidateIDTokenClaims" scope="prototype"
class="net.shibboleth.idp.plugin.authn.oidc.rp.impl.ValidateTokenClaims"
p:profileContextLookupStrategy-ref="shibboleth.ChildLookup.ProxyProfileRequestContext"
p:authenticationContextLookupStrategy-ref="ParentAuthenticiationContextLookup"
- p:cleanupHook="#{getObject('shibboleth.authn.oidc.rp.jwt.claims.CleanUpHook')
- ?: getObject('shibboleth.authn.oidc.rp.jwt.claims.DefaultCleanupHook')}"
- p:claimsValidator="#{getObject('shibboleth.authn.oidc.rp.IDTokenClaimsValidator')
+ p:cleanupHook="#{getObject('shibboleth.authn.oidc.rp.idtoken.jwt.claims.CleanUpHook')
+ ?: getObject('DefaultCleanupHook')}"
+ p:claimsValidator="#{getObject('shibboleth.authn.oidc.rp.idtoken.IDTokenClaimsValidator')
?: getObject('DefaultIDTokenClaimsValidator')}"
- p:jwtLookupStrategy="#{getObject('shibboleth.authn.oidc.rp.IDTokenLookupStrategy')
+ p:jwtLookupStrategy="#{getObject('shibboleth.authn.oidc.rp.idtoken.IDTokenLookupStrategy')
?: getObject('DefaultIDTokenLookupStrategy')}"/>
<bean id="DefaultIDTokenLookupStrategy"
@@ -291,7 +292,7 @@
<bean id="DefaultIDTokenClaimsValidator"
class="net.shibboleth.oidc.security.jwt.claims.impl.ChainingJWTClaimsValidator"
- p:claimValidators-ref="ClaimsValidators" />
+ p:claimValidators-ref="IDTokenClaimsValidators" />
<bean id="OIDCProviderMetadataContextChildLookup"
class="org.opensaml.messaging.context.navigate.ChildContextLookup"
@@ -366,7 +367,7 @@
</property>
</bean>
- <bean id="nonceClaimValidator"
+ <bean id="NonceClaimValidator"
class="net.shibboleth.oidc.security.jwt.claims.impl.ExactMatchClaimsValidator"
p:claimName="nonce"
p:valueToMatchLookupStrategy="#{getObject('shibboleth.authn.oidc.rp.jwt.NonceLookupStrategy') ?:
@@ -385,7 +386,7 @@
class="org.opensaml.messaging.context.navigate.ChildContextLookup"
c:type="#{ T(net.shibboleth.oidc.metadata.context.OIDCMetadataContext) }" />
- <util:list id="ClaimsValidators" value-type="net.shibboleth.oidc.jwt.claims.ClaimsValidator">
+ <util:list id="IDTokenClaimsValidators" value-type="net.shibboleth.oidc.jwt.claims.ClaimsValidator">
<ref bean="IssuerClaimsValidator" /> <!-- TODO prevent: if it contains additional audiences not trusted by the Client. -->
<ref bean="AudienceClaimsValidator" />
<ref bean="AzpClaimRequiredValidator"/>
@@ -393,24 +394,92 @@
<ref bean="ExpiryClaimsValidator" />
<ref bean="IssuedAtClaimsValidator" />
<ref bean="NotBeforeClaimsValidator" />
- <ref bean="nonceClaimValidator" />
- <!-- missing ACR? nonce, and auth_time, access_token at_hash. -->
+ <ref bean="NonceClaimValidator" />
+ <!-- missing ACR? and auth_time, access_token at_hash. -->
</util:list>
-
+ <bean id="ValidateOIDCAuthentication"
+ class="net.shibboleth.idp.plugin.authn.oidc.rp.impl.ValidateOIDCAuthentication" scope="prototype"
+ p:profileContextLookupStrategy-ref="shibboleth.ChildLookup.ProxyProfileRequestContext"
+ p:authenticationContextLookupStrategy-ref="ParentAuthenticiationContextLookup"
+ p:transcoderRegistry-ref="shibboleth.AttributeRegistryService"/>
+
+
+ <!-- UserInfo endpoint beans -->
+
+ <bean id="UserInfoEndpointLookup" class="net.shibboleth.idp.plugin.authn.oidc.rp.impl.UserInfoEndpointLookup"
+ p:profileContextLookupStrategy-ref="shibboleth.ChildLookup.ProxyProfileRequestContext"
+ p:authenticationContextLookupStrategy-ref="ParentAuthenticiationContextLookup"
+ p:httpClient="#{getObject('shibboleth.authn.oidc.rp.HttpClient') ?: getObject('shibboleth.InternalHttpClient')}"
+ p:httpClientSecurityParameters="#{getObject('shibboleth.authn.oidc.rp.HttpClientSecurityParameters')}"
+ p:httpResponseDecoderStrategy="#{getObject('shibboleth.authn.oidc.rp.UserInfoResponseDecoder') ?: getObject('shibboleth.authn.oidc.rp.DefaultUserInfoResponseDecoder')}"
+ p:httpRequestEncoderStrategy="#{getObject('shibboleth.authn.oidc.rp.UserInfoRequestEncoder') ?: getObject('shibboleth.authn.oidc.rp.DefaultUserInfoRequestEncoder')}"/>
+
+
+ <bean id="shibboleth.authn.oidc.rp.DefaultUserInfoResponseDecoder" scope="prototype"
+ class="net.shibboleth.idp.plugin.authn.oidc.rp.decoding.impl.DefaultUserInfoResponseDecoder"
+ p:objectMapper-ref="shibboleth.authn.oidc.rp.JSONObjectMapper"/>
+
+ <bean id="shibboleth.authn.oidc.rp.DefaultUserInfoRequestEncoder" scope="prototype"
+ class="net.shibboleth.idp.plugin.authn.oidc.rp.encoding.impl.DefaultUserInfoRequestEncoder"/>
+
+
+ <bean id="ValidateUserInfoClaims"
+ class="net.shibboleth.idp.plugin.authn.oidc.rp.impl.ValidateUserInfoClaims" scope="prototype"
+ p:profileContextLookupStrategy-ref="shibboleth.ChildLookup.ProxyProfileRequestContext"
+ p:authenticationContextLookupStrategy-ref="ParentAuthenticiationContextLookup"/>
+
+
+ <bean id="MergeUserInfoAndIDTokenClaims"
+ class="net.shibboleth.idp.plugin.authn.oidc.rp.impl.MergeUserInfoAndIDTokenClaims" scope="prototype"
+ p:profileContextLookupStrategy-ref="shibboleth.ChildLookup.ProxyProfileRequestContext"
+ p:authenticationContextLookupStrategy-ref="ParentAuthenticiationContextLookup"/>
+
+
+ <!-- UserInfo response JWT validation -->
+
+ <bean id="ValidateUserInfoTokenClaims" scope="prototype"
+ class="net.shibboleth.idp.plugin.authn.oidc.rp.impl.ValidateTokenClaims"
+ p:profileContextLookupStrategy-ref="shibboleth.ChildLookup.ProxyProfileRequestContext"
+ p:authenticationContextLookupStrategy-ref="ParentAuthenticiationContextLookup"
+ p:cleanupHook="#{getObject('shibboleth.authn.oidc.rp.userinfo.jwt.claims.CleanUpHook')}"
+ p:claimsValidator="#{getObject('shibboleth.authn.oidc.rp.userinfo.UserInfoTokenClaimsValidator')
+ ?: getObject('DefaultUserInfoTokenClaimsValidator')}"
+ p:jwtLookupStrategy="#{getObject('shibboleth.authn.oidc.rp.userinfo.UserInfoTokenLookupStrategy')
+ ?: getObject('DefaultUserInfoTokenLookupStrategy')}"/>
+
+ <bean id="DefaultUserInfoTokenClaimsValidator"
+ class="net.shibboleth.oidc.security.jwt.claims.impl.ChainingJWTClaimsValidator"
+ p:claimValidators-ref="UserInfoClaimsValidators" />
+
+ <bean id="DefaultUserInfoTokenLookupStrategy"
+ class="net.shibboleth.idp.plugin.authn.oidc.rp.impl.DefaultUserInfoTokenLookupStrategy"/>
+
+ <util:list id="UserInfoClaimsValidators" value-type="net.shibboleth.oidc.jwt.claims.ClaimsValidator">
+ <ref bean="IssuerClaimsValidator" />
+ <ref bean="AudienceClaimsValidator" />
+ </util:list>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
<!-- OLD STUFF -->
<!-- TODO Add user import here
<import resource="openidconnect-authn-config.xml" /> -->
- <bean id="SetRPUIInformation" class="net.shibboleth.idp.ui.impl.SetRPUIInformation"
- scope="prototype" p:activationCondition-ref="shibboleth.authn.oidc.rp.populateUIInfo"
- p:httpServletRequest-ref="shibboleth.HttpServletRequest">
- <property name="fallbackLanguages">
- <bean parent="shibboleth.CommaDelimStringArray" c:_0="%{idp.ui.fallbackLanguages:}" />
- </property>
- </bean>
<!-- Populate RP UI info from metadata? -->
@@ -418,8 +487,7 @@
<bean id="ValidateIDTokenSignature"
class="net.shibboleth.idp.plugin.authn.oidc.rp.impl.ValidateIDTokenSignature" />
- <bean id="ValidateOIDCAuthentication"
- class="net.shibboleth.idp.plugin.authn.oidc.rp.impl.ValidateOIDCAuthentication" scope="prototype" />
+
<bean id="shibboleth.authn.oidc.rp.jwt.DefaultAuthTimeActivationCondition"
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 443ed3b..f200c46 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
@@ -91,8 +91,7 @@
<evaluate expression="ExchangeCodeForAccessToken"/>
<evaluate expression="ValidateOAuthAccessTokenResponse"/>
<evaluate expression="ExtractIDTokenFromTokenResponse"/>
- <evaluate expression="'proceed'" />
-
+ <evaluate expression="'proceed'" />
<transition on="proceed" to="ValidateToken" />
</action-state>
@@ -119,16 +118,60 @@
<!-- <evaluate expression="PopulateTokenSignatureSigningParameters" /> -->
<!-- <evaluate expression="PopulateTokenEncryptionParameters" /> -->
<evaluate expression="ValidateIDTokenClaims" />
- <transition on="proceed" to="SetPrincipal" />
+ <evaluate expression="'proceed'" />
+ <transition on="proceed" to="CheckUserInfoRequired" />
</action-state>
-
- <!-- <action-state id="ValidateOIDCTokenResponse">
- <evaluate expression="ValidateIDTokenSignature" />
- <evaluate expression="ValidateTokenClaims" />
+
+ <!-- Should we request information from the UserInfo endpoint based on profile config -->
+ <decision-state id="CheckUserInfoRequired">
+ <if test="true"
+ then="UserInfoRequest" />
+ <!-- if else here, we need to set id_token claims into EndUserClaimsContext -->
+ </decision-state>
+
+ <action-state id="UserInfoRequest">
+ <evaluate expression="UserInfoEndpointLookup" />
+ <!-- Something needs to check TLS server certificate? -->
+ <evaluate expression="'proceed'" />
+ <transition on="proceed" to="CheckUserInfoResponseType" />
+ </action-state>
+
+ <!-- Make strategy for these conditions
+ Also, what about a plain JWT? need to extract out the claims for those too -->
+ <decision-state id="CheckUserInfoResponseType">
+ <if test="opensamlProfileRequestContext.getSubcontext('net.shibboleth.idp.authn.context.AuthenticationContext')
+ .getSubcontext('org.opensaml.profile.context.ProfileRequestContext').getInboundMessageContext()
+ .getSubcontext('net.shibboleth.idp.plugin.authn.oidc.rp.context.UserInfoResponseContext')
+ .getUserInfo().isSigned()"
+ then="ValidateSignedUserInfoJWT" />
+ <if test="opensamlProfileRequestContext.getSubcontext('net.shibboleth.idp.authn.context.AuthenticationContext')
+ .getSubcontext('org.opensaml.profile.context.ProfileRequestContext').getInboundMessageContext()
+ .getSubcontext('net.shibboleth.idp.plugin.authn.oidc.rp.context.UserInfoResponseContext')
+ .getUserInfo().isEncrypted()"
+ then="DecryptUserInfoJWT"
+ else="ValidateUserInfoClaimsSet"/>
+ </decision-state>
+
+ <action-state id="ValidateSignedUserInfoJWT">
+ <!-- <evaluate expression="PopulateTokenSignatureSigningParameters" /> -->
+ <evaluate expression="ValidateUserInfoTokenClaims" />
+ <evaluate expression="'proceed'" />
+ <transition on="proceed" to="ValidateUserInfoClaimsSet" />
+ </action-state>
+
+ <action-state id="DecryptUserInfoJWT">
+ <!-- <evaluate expression="PopulateTokenEncryptionParameters" /> -->
+ <evaluate expression="ValidateUserInfoTokenClaims" />
+ <evaluate expression="'proceed'" />
+ <transition on="proceed" to="ValidateUserInfoClaimsSet" />
+ </action-state>
+
+ <action-state id="ValidateUserInfoClaimsSet">
+ <evaluate expression="ValidateUserInfoClaims" />
+ <evaluate expression="MergeUserInfoAndIDTokenClaims" />
<evaluate expression="'proceed'" />
<transition on="proceed" to="SetPrincipal" />
- </action-state> -->
-
+ </action-state>
<action-state id="SetPrincipal">
<evaluate expression="ValidateOIDCAuthentication" />
diff --git a/idp-oidc-rp-impl/src/main/resources/META-INF/net/shibboleth/idp/service/attribute/registry/oidc-claim-rules.xml b/idp-oidc-rp-impl/src/main/resources/META-INF/net/shibboleth/idp/service/attribute/registry/oidc-claim-rules.xml
new file mode 100644
index 0000000..9de853b
--- /dev/null
+++ b/idp-oidc-rp-impl/src/main/resources/META-INF/net/shibboleth/idp/service/attribute/registry/oidc-claim-rules.xml
@@ -0,0 +1,428 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<beans xmlns="http://www.springframework.org/schema/beans"
+ xmlns:context="http://www.springframework.org/schema/context"
+ xmlns:util="http://www.springframework.org/schema/util"
+ xmlns:p="http://www.springframework.org/schema/p"
+ xmlns:c="http://www.springframework.org/schema/c"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
+ http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
+ http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd"
+
+ default-init-method="initialize"
+ default-destroy-method="destroy">
+
+ <!-- https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims -->
+
+ <bean parent="shibboleth.TranscodingRuleLoader">
+ <constructor-arg>
+ <list>
+
+ <!-- Typical inetOrgPerson attributes that map to standard claims. -->
+
+ <bean parent="shibboleth.TranscodingProperties">
+ <property name="properties">
+ <props merge="true">
+ <prop key="id">displayName</prop>
+ <prop key="transcoder">OIDCStringTranscoder</prop>
+ <prop key="oidc.name">name</prop>
+ <prop key="displayName.en">Display name</prop>
+ <prop key="displayName.de">Anzeigename</prop>
+ <prop key="displayName.fr">Nom</prop>
+ <prop key="displayName.it">Nome</prop>
+ <prop key="displayName.ja">表示名</prop>
+ <prop key="description.en">The name that should appear in white-pages-like applications for this person.</prop>
+ <prop key="description.de">Anzeigename</prop>
+ <prop key="description.fr">Nom complet d'affichage</prop>
+ <prop key="description.it">Nome</prop>
+ <prop key="description.ja">アプリケーションでの表示に用いられる英字氏名</prop>
+ </props>
+ </property>
+ </bean>
+
+ <bean parent="shibboleth.TranscodingProperties">
+ <property name="properties">
+ <props merge="true">
+ <prop key="id">givenName</prop>
+ <prop key="transcoder">OIDCStringTranscoder</prop>
+ <prop key="oidc.name">given_name</prop>
+ <prop key="displayName.en">Given name</prop>
+ <prop key="displayName.de">Vorname</prop>
+ <prop key="displayName.fr">Prénom</prop>
+ <prop key="displayName.it">Nome</prop>
+ <prop key="displayName.ja">名</prop>
+ <prop key="description.en">Given name of a person</prop>
+ <prop key="description.de">Vorname</prop>
+ <prop key="description.fr">Prénom de l'utilisateur</prop>
+ <prop key="description.it">Nome</prop>
+ <prop key="description.ja">氏名(名)の英語表記</prop>
+ </props>
+ </property>
+ </bean>
+
+ <bean parent="shibboleth.TranscodingProperties">
+ <property name="properties">
+ <props merge="true">
+ <prop key="id">homePhone</prop>
+ <prop key="transcoder">OIDCStringTranscoder</prop>
+ <prop key="oidc.name">phone_number</prop>
+ <prop key="displayName.en">Private phone number</prop>
+ <prop key="displayName.de">Telefon Privat</prop>
+ <prop key="displayName.fr">Teléphone personnel</prop>
+ <prop key="displayName.it">Numero di telefono privato</prop>
+ <prop key="displayName.ja">自宅電話番号</prop>
+ <prop key="description.en">Private phone number</prop>
+ <prop key="description.de">Private Telefonnummer</prop>
+ <prop key="description.fr">Numéro de téléphone de domicile de la personne</prop>
+ <prop key="description.it">Numero di telefono privato</prop>
+ <prop key="description.ja">自宅の電話番号</prop>
+ </props>
+ </property>
+ </bean>
+
+ <bean parent="shibboleth.TranscodingProperties">
+ <property name="properties">
+ <props merge="true">
+ <prop key="id">mail</prop>
+ <prop key="transcoder">OIDCStringTranscoder</prop>
+ <prop key="oidc.name">email</prop>
+ <prop key="displayName.en">E-mail</prop>
+ <prop key="displayName.de">E-Mail</prop>
+ <prop key="displayName.fr">Email</prop>
+ <prop key="displayName.it">E-mail</prop>
+ <prop key="displayName.ja">メールアドレス</prop>
+ <prop key="description.en">E-Mail: Preferred address for e-mail to be sent to this person</prop>
+ <prop key="description.de">E-Mail-Adresse</prop>
+ <prop key="description.de-ch">E-Mail Adresse</prop>
+ <prop key="description.fr">Adresse de courrier électronique</prop>
+ <prop key="description.it">E-Mail: l'indirizzo e-mail preferito dall'utente</prop>
+ <prop key="description.ja">メールアドレス</prop>
+ </props>
+ </property>
+ </bean>
+
+ <bean parent="shibboleth.TranscodingProperties">
+ <property name="properties">
+ <props merge="true">
+ <prop key="id">preferredLanguage</prop>
+ <prop key="transcoder">OIDCStringTranscoder</prop>
+ <prop key="oidc.name">locale</prop>
+ <prop key="displayName.en">Preferred Language</prop>
+ <prop key="displayName.de">Bevorzugte Sprache</prop>
+ <prop key="displayName.fr">Langue préférée</prop>
+ <prop key="displayName.it">Lingua preferita</prop>
+ <prop key="displayName.ja">希望言語</prop>
+ <prop key="description.en">Preferred language: Users preferred language (see RFC1766)</prop>
+ <prop key="description.de">Bevorzugte Sprache (siehe RFC1766)</prop>
+ <prop key="description.fr">Exemple: fr, de, it, en, ... (voir RFC1766)</prop>
+ <prop key="description.it">Lingua preferita: la lingua preferita dall'utente (cfr. RFC1766)</prop>
+ <prop key="description.ja">利用者が希望する言語(RFC1766 を参照)</prop>
+ </props>
+ </property>
+ </bean>
+
+ <bean parent="shibboleth.TranscodingProperties">
+ <property name="properties">
+ <props merge="true">
+ <prop key="id">sn</prop>
+ <prop key="transcoder">OIDCStringTranscoder</prop>
+ <prop key="oidc.name">family_name</prop>
+ <prop key="displayName.en">Surname</prop>
+ <prop key="displayName.de">Nachname</prop>
+ <prop key="displayName.fr">Nom de famille</prop>
+ <prop key="displayName.it">Cognome</prop>
+ <prop key="displayName.ja">姓</prop>
+ <prop key="description.en">Surname or family name</prop>
+ <prop key="description.de">Familienname</prop>
+ <prop key="description.fr">Nom de famille de l'utilisateur.</prop>
+ <prop key="description.it">Cognome dell'utilizzatore</prop>
+ <prop key="description.ja">氏名(姓)の英語表記</prop>
+ </props>
+ </property>
+ </bean>
+
+ <bean parent="shibboleth.TranscodingProperties">
+ <property name="properties">
+ <props merge="true">
+ <prop key="id">telephoneNumber</prop>
+ <prop key="transcoder">OIDCStringTranscoder</prop>
+ <prop key="oidc.name">phone_number</prop>
+ <prop key="displayName.en">Business phone number</prop>
+ <prop key="displayName.de">Telefon Geschäft</prop>
+ <prop key="displayName.fr">Teléphone professionnel</prop>
+ <prop key="displayName.it">Numero di telefono dell'ufficio</prop>
+ <prop key="displayName.ja">所属機関内電話番号</prop>
+ <prop key="description.en">Business phone number: Office or campus phone number</prop>
+ <prop key="description.de">Telefonnummer am Arbeitsplatz</prop>
+ <prop key="description.fr">Teléphone de l'institut, de l'université</prop>
+ <prop key="description.it">Numero di telefono dell'ufficio</prop>
+ <prop key="description.ja">所属機関での利用者の電話番号</prop>
+ </props>
+ </property>
+ </bean>
+
+ <bean parent="shibboleth.TranscodingProperties">
+ <property name="properties">
+ <props merge="true">
+ <prop key="id">uid</prop>
+ <prop key="transcoder">OIDCStringTranscoder</prop>
+ <prop key="oidc.name">preferred_username</prop>
+ <prop key="displayName.en">User ID</prop>
+ <prop key="displayName.de">Benutzer-ID</prop>
+ <prop key="displayName.fr">ID utilisateur</prop>
+ <prop key="displayName.it">ID dell'utente</prop>
+ <prop key="displayName.ja">ユーザID</prop>
+ <prop key="description.en">A unique identifier for a person, mainly used for user identification within the user's home organization.</prop>
+ <prop key="description.de">Eine eindeutige Nummer für eine Person, welche hauptsächlich zur Identifikation innerhalb der Organisation benutzt wird.</prop>
+ <prop key="description.fr">Identifiant de connexion d'une personnes sur les systèmes informatiques.</prop>
+ <prop key="description.it">Identificativo unico della persona, usato per l'identificazione dell'utente all'interno della organizzazione di appartenenza.</prop>
+ <prop key="description.ja">所属機関内で一意の利用者識別子</prop>
+ </props>
+ </property>
+ </bean>
+
+ <!-- eduPerson attributes that map to standard claims. -->
+
+ <bean parent="shibboleth.TranscodingProperties">
+ <property name="properties">
+ <props merge="true">
+ <prop key="id">eduPersonNickname</prop>
+ <prop key="transcoder">OIDCStringTranscoder</prop>
+ <prop key="oidc.name">nickname</prop>
+ <prop key="displayName.en">Nickname</prop>
+ <prop key="displayName.de">Kurzname</prop>
+ <prop key="displayName.de-ch">Übername</prop>
+ <prop key="displayName.fr">Surnom</prop>
+ <prop key="displayName.it">Diminutivo</prop>
+ <prop key="displayName.ja">ニックネーム</prop>
+ <prop key="description.en">Person's nickname, or the informal name by which they are accustomed to be hailed.</prop>
+ <prop key="description.de">Kurzname einer Person, oder üblicher Rufname zur Begrüßung.</prop>
+ <prop key="description.de-ch">Übername einer Person, oder üblicher Rufname zur Begrüssung.</prop>
+ <prop key="description.fr">Nom personnalisable pour un usage informel.</prop>
+ <prop key="description.it">Diminutivo della persona, o soprannome.</prop>
+ <prop key="description.ja">利用者のニックネームもしくは通称</prop>
+ </props>
+ </property>
+ </bean>
+
+ <!-- A subset of the major eduPerson attributes that have no standard claim mapping. -->
+
+ <bean parent="shibboleth.TranscodingProperties">
+ <property name="properties">
+ <props merge="true">
+ <prop key="id">eduPersonAssurance</prop>
+ <prop key="transcoder">OIDCStringTranscoder</prop>
+ <prop key="oidc.name">eduPersonAssurance</prop>
+ <prop key="displayName.en">Assurance level</prop>
+ <prop key="displayName.de">Vertrauensgrad</prop>
+ <prop key="displayName.fr">Niveau de confiance</prop>
+ <prop key="displayName.it">Livello di sicurezza</prop>
+ <prop key="displayName.ja">保証レベル</prop>
+ <prop key="description.en">Set of URIs that assert compliance with specific standards for identity assurance.</prop>
+ <prop key="description.de">URIs die eine gewisse Zusicherung für spezifische Standards des Vertrauens beinhalten</prop>
+ <prop key="description.fr">Un ensemble d'URI qui attestent la conformité selon un standard pour les niveaux d'assurance d'identités</prop>
+ <prop key="description.it">Un insieme di URI che asseriscono l'osservanza dei livelli di sicurezza richiesti</prop>
+ <prop key="description.ja">IDの保証レベルに関して特定の基準に準拠していることを示すURI</prop>
+ </props>
+ </property>
+ </bean>
+
+ <bean parent="shibboleth.TranscodingProperties">
+ <property name="properties">
+ <props merge="true">
+ <prop key="id">eduPersonEntitlement</prop>
+ <prop key="transcoder">OIDCStringTranscoder</prop>
+ <prop key="oidc.name">eduPersonEntitlement</prop>
+ <prop key="displayName.en">Entitlement</prop>
+ <prop key="displayName.de">Berechtigung</prop>
+ <prop key="displayName.fr">Entitlement</prop>
+ <prop key="displayName.it">Prerogativa</prop>
+ <prop key="displayName.ja">資格情報</prop>
+ <prop key="description.en">Member of: URI (either URL or URN) that indicates a set of rights to specific resources based on an agreement across the releavant community</prop>
+ <prop key="description.de">Zeichenkette, die Rechte für spezifische Ressourcen beschreibt</prop>
+ <prop key="description.fr">Membre de: URI (soit une URL ou une URN) décrivant un droit spécific d'accès.</prop>
+ <prop key="description.it">Membro delle seguenti URI (sia URL o URN) che rappresentano diritti specifici d'accesso validi in tutta la communità</prop>
+ <prop key="description.ja">特定のアプリケーションもしくはコミュニティ内の複数リソースへのアクセス権限を持つことを示すURI(URLもしくはURN)</prop>
+ </props>
+ </property>
+ </bean>
+
+ <bean parent="shibboleth.TranscodingProperties">
+ <property name="properties">
+ <props merge="true">
+ <prop key="id">eduPersonPrincipalName</prop>
+ <prop key="transcoder">OIDCScopedStringTranscoder</prop>
+ <prop key="oidc.name">eduPersonPrincipalName</prop>
+ <prop key="displayName.en">Principal name</prop>
+ <prop key="displayName.de">Persönliche ID</prop>
+ <prop key="displayName.fr">Principal Name</prop>
+ <prop key="displayName.it">Principal Name</prop>
+ <prop key="displayName.ja">プリンシパルID</prop>
+ <prop key="description.en">A unique identifier for a person, mainly for inter-institutional user identification.</prop>
+ <prop key="description.de">Eindeutige Benutzeridentifikation</prop>
+ <prop key="description.de-ch">Eindeutige Benützeridentifikation</prop>
+ <prop key="description.fr">L'identifiant unique de l'utilisateur</prop>
+ <prop key="description.it">Un ID personale che identifica chiaramente l'utente in seno alla sua organizzazione</prop>
+ <prop key="description.ja">フェデレーション内で一意かつ永続的な利用者識別子</prop>
+ </props>
+ </property>
+ </bean>
+
+ <bean parent="shibboleth.TranscodingProperties">
+ <property name="properties">
+ <props merge="true">
+ <prop key="id">eduPersonScopedAffiliation</prop>
+ <prop key="transcoder">OIDCScopedStringTranscoder</prop>
+ <prop key="oidc.name">eduPersonScopedAffiliation</prop>
+ <prop key="displayName.en">Scoped affiliation</prop>
+ <prop key="displayName.de">Zugehörigkeit</prop>
+ <prop key="displayName.fr">Affiliation</prop>
+ <prop key="displayName.it">Tipo di membro</prop>
+ <prop key="displayName.ja">スコープ付き職位</prop>
+ <prop key="description.en">Specifies the person's affiliation within a particular security domain</prop>
+ <prop key="description.de">Art der Zugehörigkeit zur Heimatorganisation</prop>
+ <prop key="description.de-ch">Art der Zugehörigkeit zur Heimorganisation</prop>
+ <prop key="description.fr">Type d'affiliation dans l'organisation</prop>
+ <prop key="description.it">Tipo di membro: Tipo di lavoro svolto per l'organizzazione</prop>
+ <prop key="description.ja">セキュリティドメインのスコープが付いた所属機関における職位</prop>
+ </props>
+ </property>
+ </bean>
+
+ <!-- The remainder are standard OIDC claims, which we map based on the actual claim name. -->
+
+ <bean parent="shibboleth.TranscodingProperties">
+ <property name="properties">
+ <props merge="true">
+ <prop key="id">address</prop>
+ <prop key="transcoder">OIDCStringTranscoder</prop>
+ <prop key="oidc.asObject">true</prop>
+ <prop key="oidc.name">address</prop>
+ <prop key="displayName.en">Postal address</prop>
+ </props>
+ </property>
+ </bean>
+
+ <bean parent="shibboleth.TranscodingProperties">
+ <property name="properties">
+ <props merge="true">
+ <prop key="id">birthdate</prop>
+ <prop key="transcoder">OIDCStringTranscoder</prop>
+ <prop key="oidc.name">birthdate</prop>
+ <prop key="displayName.en">Date of birth</prop>
+ </props>
+ </property>
+ </bean>
+
+ <bean parent="shibboleth.TranscodingProperties">
+ <property name="properties">
+ <props merge="true">
+ <prop key="id">email_verified</prop>
+ <prop key="transcoder">OIDCStringTranscoder</prop>
+ <prop key="oidc.name">email_verified</prop>
+ <prop key="oidc.asBoolean">true</prop>
+ <prop key="displayName.en">E-mail verification status</prop>
+ <prop key="description.en">Indicates whether e-mail address has been verified by the issuer</prop>
+ </props>
+ </property>
+ </bean>
+
+ <bean parent="shibboleth.TranscodingProperties">
+ <property name="properties">
+ <props merge="true">
+ <prop key="id">gender</prop>
+ <prop key="transcoder">OIDCStringTranscoder</prop>
+ <prop key="oidc.name">gender</prop>
+ <prop key="displayName.en">Gender</prop>
+ </props>
+ </property>
+ </bean>
+
+ <bean parent="shibboleth.TranscodingProperties">
+ <property name="properties">
+ <props merge="true">
+ <prop key="id">middle_name</prop>
+ <prop key="transcoder">OIDCStringTranscoder</prop>
+ <prop key="oidc.name">middle_name</prop>
+ <prop key="displayName.en">Middle name</prop>
+ </props>
+ </property>
+ </bean>
+
+ <bean parent="shibboleth.TranscodingProperties">
+ <property name="properties">
+ <props merge="true">
+ <prop key="id">phone_number_verified</prop>
+ <prop key="transcoder">OIDCStringTranscoder</prop>
+ <prop key="oidc.name">phone_number_verified</prop>
+ <prop key="oidc.asBoolean">true</prop>
+ <prop key="displayName.en">Phone number verification status</prop>
+ <prop key="description.en">Indicates whether phone number has been verified by the issuer</prop>
+ </props>
+ </property>
+ </bean>
+
+ <bean parent="shibboleth.TranscodingProperties">
+ <property name="properties">
+ <props merge="true">
+ <prop key="id">picture</prop>
+ <prop key="transcoder">OIDCStringTranscoder</prop>
+ <prop key="oidc.name">picture</prop>
+ <prop key="displayName.en">Picture</prop>
+ <prop key="description.en">URL of personal photo</prop>
+ </props>
+ </property>
+ </bean>
+
+ <bean parent="shibboleth.TranscodingProperties">
+ <property name="properties">
+ <props merge="true">
+ <prop key="id">profile</prop>
+ <prop key="transcoder">OIDCStringTranscoder</prop>
+ <prop key="oidc.name">profile</prop>
+ <prop key="displayName.en">Profile page</prop>
+ <prop key="description.en">URL of personal profile page</prop>
+ </props>
+ </property>
+ </bean>
+
+ <bean parent="shibboleth.TranscodingProperties">
+ <property name="properties">
+ <props merge="true">
+ <prop key="id">website</prop>
+ <prop key="transcoder">OIDCStringTranscoder</prop>
+ <prop key="oidc.name">website</prop>
+ <prop key="displayName.en">Web site</prop>
+ <prop key="description.en">URL to personal web site</prop>
+ </props>
+ </property>
+ </bean>
+
+ <bean parent="shibboleth.TranscodingProperties">
+ <property name="properties">
+ <props merge="true">
+ <prop key="id">updated_at</prop>
+ <prop key="transcoder">OIDCStringTranscoder</prop>
+ <prop key="oidc.name">updated_at</prop>
+ <prop key="oidc.asInteger">true</prop>
+ <prop key="displayName.en">Last update of information</prop>
+ </props>
+ </property>
+ </bean>
+
+ <bean parent="shibboleth.TranscodingProperties">
+ <property name="properties">
+ <props merge="true">
+ <prop key="id">zoneinfo</prop>
+ <prop key="transcoder">OIDCStringTranscoder</prop>
+ <prop key="oidc.name">zoneinfo</prop>
+ <prop key="displayName.en">Time zone</prop>
+ </props>
+ </property>
+ </bean>
+
+ </list>
+ </constructor-arg>
+ </bean>
+
+</beans>
diff --git a/idp-oidc-rp-impl/src/main/resources/META-INF/net/shibboleth/idp/service/attribute/registry/postconfig.xml b/idp-oidc-rp-impl/src/main/resources/META-INF/net/shibboleth/idp/service/attribute/registry/postconfig.xml
new file mode 100644
index 0000000..75ffe22
--- /dev/null
+++ b/idp-oidc-rp-impl/src/main/resources/META-INF/net/shibboleth/idp/service/attribute/registry/postconfig.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<beans xmlns="http://www.springframework.org/schema/beans"
+ xmlns:context="http://www.springframework.org/schema/context"
+ xmlns:util="http://www.springframework.org/schema/util"
+ xmlns:p="http://www.springframework.org/schema/p"
+ xmlns:c="http://www.springframework.org/schema/c"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
+ http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
+ http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd"
+
+ default-init-method="initialize" default-destroy-method="destroy">
+
+ <!-- Necessary for encoder parsing and claims mapping to function. -->
+
+ <bean id="OIDCByteTranscoder"
+ class="net.shibboleth.oidc.attribute.transcoding.impl.OIDCByteAttributeTranscoder" />
+
+ <bean id="OIDCStringTranscoder"
+ class="net.shibboleth.oidc.attribute.transcoding.impl.OIDCStringAttributeTranscoder" />
+
+ <bean id="OIDCScopedStringTranscoder"
+ class="net.shibboleth.oidc.attribute.transcoding.impl.OIDCScopedStringAttributeTranscoder" />
+
+</beans>
diff --git a/idp-oidc-rp-impl/src/main/resources/templates/oidc-request-form-post.vm b/idp-oidc-rp-impl/src/main/resources/templates/oidc-request-form-post.vm
index 8b303e0..e8381a2 100644
--- a/idp-oidc-rp-impl/src/main/resources/templates/oidc-request-form-post.vm
+++ b/idp-oidc-rp-impl/src/main/resources/templates/oidc-request-form-post.vm
@@ -33,7 +33,9 @@
<input type="hidden" name="prompt" value="${prompt}" />#end #if($request)
- <input type="hidden" name="request" value="${request}" />#end
+ <input type="hidden" name="request" value="${request}" />#end #if($nonce)
+
+ <input type="hidden" name="nonce" value="${nonce}" />#end
</div>
<noscript>
<div>
diff --git a/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/decoding/impl/DefaultTokenResponseDecoderTest.java b/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/decoding/impl/DefaultTokenResponseDecoderTest.java
index b006417..25587e5 100644
--- a/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/decoding/impl/DefaultTokenResponseDecoderTest.java
+++ b/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/decoding/impl/DefaultTokenResponseDecoderTest.java
@@ -41,12 +41,12 @@ import net.shibboleth.utilities.java.support.component.ComponentInitializationEx
public class DefaultTokenResponseDecoderTest extends AbstractOIDCTest {
/** The encoder to test.*/
- private DefaultTokenResponseDecoder decoder;
+ private DefaultMapResponseDecoder decoder;
@BeforeMethod
public void setup() throws Exception {
super.setup();
- decoder = new DefaultTokenResponseDecoder();
+ decoder = new DefaultMapResponseDecoder();
decoder.setObjectMapper(new ObjectMapper());
}
diff --git a/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/AbstractOIDCTest.java b/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/AbstractOIDCTest.java
index efe5b44..f2dede9 100644
--- a/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/AbstractOIDCTest.java
+++ b/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/AbstractOIDCTest.java
@@ -101,6 +101,16 @@ public abstract class AbstractOIDCTest {
+ " \"scope\": \"openid\"\n"
+ "}";
+ /** Mock response from the UserInfo endpoint.*/
+ @Nonnull @NotEmpty
+ protected final String USERINFO_RESPONSE ="{sub=user-subject-1234531, "
+ + "website=https://openid.net/, "
+ + "zoneinfo=America/Los_Angeles, "
+ + "birthdate=2000-02-03, gender=female, "
+ + "preferred_username=d.tu, "
+ + "given_name=Demo, middle_name=Theresa, locale=en-US, "
+ + "updated_at=1580000000, name=Demo T. User, nickname=Dee, family_name=User}";
+
/** Client metadata.*/
@Nonnull protected final String CLIENT_METADATA = "[\n"
+ " {\n"
diff --git a/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/DefaultClaimMergingStrategyTest.java b/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/DefaultClaimMergingStrategyTest.java
new file mode 100644
index 0000000..1ade5f1
--- /dev/null
+++ b/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/DefaultClaimMergingStrategyTest.java
@@ -0,0 +1,97 @@
+package net.shibboleth.idp.plugin.authn.oidc.rp.impl;
+
+import static org.junit.Assert.assertEquals;
+import static org.testng.Assert.assertNull;
+
+import javax.annotation.Nonnull;
+
+import org.testng.annotations.Test;
+
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.openid.connect.sdk.claims.ClaimsSet;
+
+import net.minidev.json.JSONObject;
+import net.minidev.json.parser.JSONParser;
+import net.minidev.json.parser.ParseException;
+import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
+
+/** Test for the DefaultClaimMergingStrategy.*/
+public class DefaultClaimMergingStrategyTest {
+
+ /** Mock response from the UserInfo endpoint.*/
+ @Nonnull @NotEmpty
+ protected final String USERINFO_RESPONSE ="{\n"
+ + " \"sub\": \"jdoe\",\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\": \"From UserInfo\",\n"
+ + " \"middle_name\": \"From UserInfo\",\n"
+ + " \"locale\": \"en-US\",\n"
+ + " \"updated_at\": 1580000000,\n"
+ + " \"name\": \"Demo T. User\",\n"
+ + " \"family_name\": \"User\"\n"
+ + "}";
+
+ @Test
+ public void testMerge() throws ParseException {
+
+ final JSONParser parser = new JSONParser(JSONParser.MODE_JSON_SIMPLE);
+ final ClaimsSet userInfo = new ClaimsSet((JSONObject)parser.parse(USERINFO_RESPONSE));
+
+ final JWTClaimsSet idTokenClaims = new JWTClaimsSet.Builder().subject("jdoe")
+ .claim("given_name", "FromIdToken")
+ .claim("nickname", "FromIdToken")
+ .build();
+
+ final DefaultClaimMergingStrategy strategy = new DefaultClaimMergingStrategy();
+ final ClaimsSet merged = strategy.apply(userInfo, idTokenClaims);
+
+ // This is a merged claim
+ assertEquals(merged.getClaim("given_name"), "FromIdToken");
+
+ // This is a claim only in the id_token
+ assertEquals(merged.getClaim("nickname"), "FromIdToken");
+
+ // This is a claim only in the userInfo response
+ assertEquals(merged.getClaim("middle_name"), "From UserInfo");
+ }
+
+ @Test
+ public void testMergeNullUserInfo() throws ParseException {
+
+ final JWTClaimsSet idTokenClaims = new JWTClaimsSet.Builder().subject("jdoe")
+ .claim("given_name", "FromIdToken")
+ .claim("nickname", "FromIdToken")
+ .build();
+
+ final DefaultClaimMergingStrategy strategy = new DefaultClaimMergingStrategy();
+ final ClaimsSet merged = strategy.apply(null, idTokenClaims);
+
+
+ assertEquals(merged.getClaim("given_name"), "FromIdToken");
+ assertEquals(merged.getClaim("nickname"), "FromIdToken");
+ // not in id_token
+ assertNull(merged.getClaim("middle_name"));
+ }
+
+ @Test
+ public void testMergeNullIdToken() throws ParseException {
+
+ final JSONParser parser = new JSONParser(JSONParser.MODE_JSON_SIMPLE);
+ final ClaimsSet userInfo = new ClaimsSet((JSONObject)parser.parse(USERINFO_RESPONSE));
+
+ final DefaultClaimMergingStrategy strategy = new DefaultClaimMergingStrategy();
+ final ClaimsSet merged = strategy.apply(userInfo, null);
+
+ assertEquals(merged.getClaim("given_name"), "From UserInfo");
+
+ assertEquals(merged.getClaim("middle_name"), "From UserInfo");
+ // not in UserInfo claims
+ assertNull(merged.getClaim("nickname"));
+
+ }
+
+}
diff --git a/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/ExchangeCodeForTokenTest.java b/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/ExchangeCodeForTokenTest.java
index c2dccf9..48ccf8b 100644
--- a/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/ExchangeCodeForTokenTest.java
+++ b/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/ExchangeCodeForTokenTest.java
@@ -18,7 +18,6 @@
package net.shibboleth.idp.plugin.authn.oidc.rp.impl;
import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
import static org.testng.Assert.assertNotNull;
import static org.testng.Assert.assertNull;
@@ -98,14 +97,14 @@ public class ExchangeCodeForTokenTest extends AbstractOIDCTest {
// create new client with mock response
exchangeAction.setHttpClient(httpClient);
- exchangeAction.setTokenRequestEncoderStrategy(prc -> {
+ exchangeAction.setHttpRequestEncoderStrategy(prc -> {
URI uri;
try {
uri = new URIBuilder().setScheme("https")
.setHost("op.example.com")
.setPath("/token")
.build();
- } catch (URISyntaxException e) {
+ } catch (final URISyntaxException e) {
return null;
}
@@ -115,12 +114,12 @@ public class ExchangeCodeForTokenTest extends AbstractOIDCTest {
.setCharset(StandardCharset.UTF_8);
return rb.build();
});
- exchangeAction.setTokenResponseDecoderStrategy(response -> {
- ObjectMapper mapper = new ObjectMapper();
+ exchangeAction.setHttpResponseDecoderStrategy(response -> {
+ final ObjectMapper mapper = new ObjectMapper();
try {
return mapper.readValue(
httpResponse.getEntity().getContent(), new TypeReference<Map<String, Object>>() {});
- } catch (UnsupportedOperationException | IOException e) {
+ } catch (final UnsupportedOperationException | IOException e) {
return null;
}
});
@@ -145,14 +144,14 @@ public class ExchangeCodeForTokenTest extends AbstractOIDCTest {
// create new client with mock response
exchangeAction.setHttpClient(httpClient);
- exchangeAction.setTokenRequestEncoderStrategy(prc -> {
+ exchangeAction.setHttpRequestEncoderStrategy(prc -> {
URI uri;
try {
uri = new URIBuilder().setScheme("https")
.setHost("op.example.com")
.setPath("/token")
.build();
- } catch (URISyntaxException e) {
+ } catch (final URISyntaxException e) {
return null;
}
@@ -162,7 +161,7 @@ public class ExchangeCodeForTokenTest extends AbstractOIDCTest {
.setCharset(StandardCharset.UTF_8);
return rb.build();
});
- exchangeAction.setTokenResponseDecoderStrategy(response -> null);
+ exchangeAction.setHttpResponseDecoderStrategy(response -> null);
exchangeAction.initialize();
diff --git a/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/ExtractIDTokenFromResponseTest.java b/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/ExtractIDTokenFromResponseTest.java
index 3f16ce5..eb12fe0 100644
--- a/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/ExtractIDTokenFromResponseTest.java
+++ b/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/ExtractIDTokenFromResponseTest.java
@@ -18,7 +18,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import com.nimbusds.jose.shaded.json.parser.JSONParser;
import net.shibboleth.idp.authn.context.AuthenticationContext;
-import net.shibboleth.idp.plugin.authn.oidc.rp.context.TokenResponseContext;
+import net.shibboleth.idp.plugin.authn.oidc.rp.context.AccessTokenResponseContext;
import net.shibboleth.idp.profile.context.navigate.WebflowRequestContextProfileRequestContextLookup;
import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
@@ -33,7 +33,7 @@ public class ExtractIDTokenFromResponseTest extends AbstractOIDCTest {
super.setup();
action = new ExtractIDTokenFromResponse();
- final TokenResponseContext trc = new TokenResponseContext();
+ final AccessTokenResponseContext trc = new AccessTokenResponseContext();
prc.getInboundMessageContext().addSubcontext(trc);
action.setProfileContextLookupStrategy(new ChildContextLookup<>(ProfileRequestContext.class).compose(
@@ -62,7 +62,7 @@ public class ExtractIDTokenFromResponseTest extends AbstractOIDCTest {
action.initialize();
final Event event = action.execute(src);
assertNull(event);
- assertNotNull(prc.getInboundMessageContext().getSubcontext(TokenResponseContext.class).getIdToken());
+ assertNotNull(prc.getInboundMessageContext().getSubcontext(AccessTokenResponseContext.class).getIdToken());
}
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 ca6bd64..a34bad7 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
@@ -17,11 +17,9 @@
package net.shibboleth.idp.plugin.authn.oidc.rp.impl;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
-
+import java.net.InetAddress;
import java.net.URI;
+import java.net.UnknownHostException;
import java.time.Instant;
import java.util.Date;
import java.util.HashMap;
@@ -30,16 +28,11 @@ import java.util.Map;
import javax.annotation.Nonnull;
-import org.apache.http.HttpResponse;
-import org.apache.http.StatusLine;
-import org.apache.http.client.HttpClient;
-import org.apache.http.client.ResponseHandler;
-import org.apache.http.client.methods.HttpGet;
-import org.apache.http.client.methods.HttpUriRequest;
-import org.apache.http.entity.StringEntity;
-import org.apache.http.protocol.HttpContext;
+import org.apache.http.conn.ssl.NoopHostnameVerifier;
+import org.apache.http.conn.ssl.TrustAllStrategy;
+import org.apache.http.impl.client.HttpClients;
+import org.apache.http.ssl.SSLContextBuilder;
import org.junit.Test;
-import org.mockito.Mockito;
import org.opensaml.messaging.context.MessageContext;
import org.opensaml.profile.context.ProfileRequestContext;
import org.slf4j.Logger;
@@ -56,14 +49,18 @@ import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.JOSEObjectType;
import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.JWSHeader;
-import com.nimbusds.jose.KeyLengthException;
+import com.nimbusds.jose.crypto.ECDSASigner;
import com.nimbusds.jose.crypto.MACSigner;
+import com.nimbusds.jose.jwk.Curve;
+import com.nimbusds.jose.jwk.gen.ECKeyGenerator;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.SignedJWT;
+import com.nimbusds.oauth2.sdk.ParseException;
import com.nimbusds.oauth2.sdk.ResponseMode;
import com.nimbusds.oauth2.sdk.ResponseType;
import com.nimbusds.oauth2.sdk.id.ClientID;
import com.nimbusds.oauth2.sdk.id.State;
+import com.nimbusds.openid.connect.sdk.AuthenticationResponse;
import com.nimbusds.openid.connect.sdk.AuthenticationResponseParser;
import com.nimbusds.openid.connect.sdk.Nonce;
import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata;
@@ -75,29 +72,40 @@ import net.minidev.json.parser.JSONParser;
import net.shibboleth.ext.spring.config.IdentifiableBeanPostProcessor;
import net.shibboleth.idp.authn.context.AuthenticationContext;
import net.shibboleth.idp.authn.context.ExternalAuthenticationContext;
+import net.shibboleth.idp.authn.context.SubjectCanonicalizationContext;
import net.shibboleth.idp.authn.impl.ExternalAuthenticationImpl;
import net.shibboleth.idp.plugin.authn.oidc.rp.context.OIDCPeerEntityContext;
import net.shibboleth.idp.plugin.authn.oidc.rp.context.ResponseTypeAndModeContext;
import net.shibboleth.idp.plugin.authn.test.flow.AbstractAuthnXmlFlowExecutionTests;
import net.shibboleth.idp.plugin.authn.test.flow.mock.MockFlowBuilder;
+import net.shibboleth.idp.profile.context.RelyingPartyContext;
import net.shibboleth.oidc.metadata.context.OIDCMetadataContext;
import net.shibboleth.oidc.metadata.context.OIDCProviderMetadataContext;
+import net.shibboleth.oidc.profile.config.OIDCAuthorizationConfiguration;
import net.shibboleth.oidc.profile.core.OIDCAuthenticationRequest;
import net.shibboleth.utilities.java.support.annotation.constraint.NonnullElements;
+import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
import net.shibboleth.utilities.java.support.annotation.constraint.Unmodifiable;
+import okhttp3.mockwebserver.MockResponse;
+import okhttp3.mockwebserver.MockWebServer;
+import okhttp3.tls.HandshakeCertificates;
+import okhttp3.tls.HeldCertificate;
/** Test the OIDC relying party flow.*/
public class OIDCRPFlowTest extends AbstractAuthnXmlFlowExecutionTests {
-
+ /**
+ * Example of good provider metadata. Endpoints are localhost to support the
+ * mock server that is started.
+ */
private final String GOOD_PROVIDER_CONFIGURATION_INFO = "{\n"
+ "\"issuer\": \"https://op.example.com\",\n"
- + "\"authorization_endpoint\": \"https://op.example.com/o/oauth2/v2/auth\",\n"
- + "\"device_authorization_endpoint\": \"https://op.example.com/device/code\",\n"
- + "\"token_endpoint\": \"https://op.example.com/token\",\n"
- + "\"userinfo_endpoint\": \"https://op.example.com/v1/userinfo\",\n"
- + "\"revocation_endpoint\": \"https://op.example.com/revoke\",\n"
- + "\"jwks_uri\": \"https://op.example.com/oauth2/v3/certs\",\n"
+ + "\"authorization_endpoint\": \"https://localhost:9918/o/oauth2/v2/auth\",\n"
+ + "\"device_authorization_endpoint\": \"https://localhost:9918/device/code\",\n"
+ + "\"token_endpoint\": \"https://localhost:9918/token\",\n"
+ + "\"userinfo_endpoint\": \"https://localhost:9918/v1/userinfo\",\n"
+ + "\"revocation_endpoint\": \"https://localhost:9918/revoke\",\n"
+ + "\"jwks_uri\": \"https://localhost:9918/oauth2/v3/certs\",\n"
+ "\"response_types_supported\": [\n"
+ "\"code\",\n"
+ "\"token\",\n"
@@ -165,7 +173,24 @@ public class OIDCRPFlowTest extends AbstractAuthnXmlFlowExecutionTests {
+ " }"
+ "]";
-
+ /** Mock JSON Object response from the UserInfo endpoint.*/
+ @Nonnull @NotEmpty
+ protected final String USERINFO_RESPONSE ="{\n"
+ + " \"sub\": \"jdoe\",\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.*/
@Nonnull private static final String FLOW =
@@ -188,6 +213,7 @@ public class OIDCRPFlowTest extends AbstractAuthnXmlFlowExecutionTests {
"classpath:/flows/authn/conditions/conditions-flow.xml","authn/conditions",
"classpath:/conf/authn/authn-events-flow.xml","authn.events");
+
/** Constructor.*/
public OIDCRPFlowTest() {
super("http://idp.example.org");
@@ -224,31 +250,11 @@ public class OIDCRPFlowTest extends AbstractAuthnXmlFlowExecutionTests {
try {
- //create Mock HttpClient
- /*
- * This is brittle and only works because the HttpGet request for the metadata
- * is different than that for the type of request to the token endpoint. If
- * more endpoints are needed, this should be done properly.
- */
- final HttpClient httpClient = mock(HttpClient.class);
-
- final HttpResponse httpResponse = Mockito.mock(HttpResponse.class);
- final StatusLine statusLine = Mockito.mock(StatusLine.class);
- when(httpResponse.getStatusLine()).thenReturn(statusLine);
- when(statusLine.getStatusCode()).thenReturn(200);
-
- when(httpClient.
- execute(any(HttpGet.class),any(ResponseHandler.class),any(HttpContext.class)))
- .thenReturn(OIDCProviderMetadata.parse(GOOD_PROVIDER_CONFIGURATION_INFO));
-
-
-
- Mockito.when(httpResponse.getEntity()).thenReturn(new StringEntity(createAccessTokenResponseJSON()));
- Mockito.when(httpClient.execute((HttpUriRequest) Mockito.any(), (HttpContext) Mockito.any()))
- .thenReturn(httpResponse);
-
- addBeanSingleton(builderContext, "shibboleth.InternalHttpClient", httpClient);
-
+ // Create a HttpClient which turns off hostname verification and trusts all certificates
+ addBeanSingleton(builderContext, "shibboleth.InternalHttpClient",
+ HttpClients.custom().setSSLContext(new SSLContextBuilder()
+ .loadTrustMaterial(null, TrustAllStrategy.INSTANCE).build())
+ .setSSLHostnameVerifier(NoopHostnameVerifier.INSTANCE).build());
} catch (final Exception e) {
log.error("Could not mock HTTP response",e);
@@ -262,6 +268,9 @@ public class OIDCRPFlowTest extends AbstractAuthnXmlFlowExecutionTests {
new ClassPathResource("conf/test-relyingparty-resolver-service.xml"));
loadBeanDefinitionsFromXmlFile(builderContext, new ClassPathResource("conf/additional-system-beans.xml"));
+
+ loadBeanDefinitionsFromXmlFile(builderContext,
+ new ClassPathResource("attribute/registry/postconfig.xml"));
}
/**
@@ -272,10 +281,9 @@ public class OIDCRPFlowTest extends AbstractAuthnXmlFlowExecutionTests {
*
* @return a serialized access token response.
*
- * @throws KeyLengthException on error
- * @throws JOSEException on error
+ * @throws Exception on error.
*/
- private String createAccessTokenResponseJSON() throws KeyLengthException, JOSEException {
+ private String createAccessTokenResponseJSON() throws Exception {
final var header = new JWSHeader.Builder(JWSAlgorithm.HS256)
.type(JOSEObjectType.JWT)
.build();
@@ -285,8 +293,10 @@ public class OIDCRPFlowTest extends AbstractAuthnXmlFlowExecutionTests {
.subject("jdoe")
.claim("nonce", "abadnonce")
.claim("azp", "demo_rp")
+ .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("Xp2s5v8y/B?E(H+MbQeThWmYq3t6w9z$"));
final String accessTokenSerialized = "{\n"
@@ -299,6 +309,119 @@ public class OIDCRPFlowTest extends AbstractAuthnXmlFlowExecutionTests {
return accessTokenSerialized;
}
+ private String createSignedUserInfoJWTResponseJSON(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 signedJWT.serialize();
+ }
+
+ /**
+ * Create a running server that mimics responses from an OpenID Connect provider.
+ * Creates a new self-signed certificate.
+ *
+ * @return the simple server.
+ *
+ * @throws UnknownHostException on error.
+ */
+ private MockWebServer createSimpleServer() throws UnknownHostException {
+ //start mock server
+ final MockWebServer mockServer = new MockWebServer();
+ final String localhost = InetAddress.getByName("localhost").getCanonicalHostName();
+ final HeldCertificate localhostCertificate = new HeldCertificate.Builder()
+ .addSubjectAlternativeName(localhost)
+ .build();
+ final HandshakeCertificates serverCertificates = new HandshakeCertificates.Builder()
+ .heldCertificate(localhostCertificate)
+ .build();
+ mockServer.useHttps(serverCertificates.sslSocketFactory(), false);
+
+ return mockServer;
+ }
+
+ /**
+ * Create an {@link OIDCPeerEntityContext}.
+ *
+ * @return the peer entity context.
+ *
+ * @throws ParseException on error.
+ */
+ private OIDCPeerEntityContext createPeerContext() throws ParseException {
+ final OIDCPeerEntityContext peerCtx = new OIDCPeerEntityContext();
+ final OIDCProviderMetadata providerMetadata = OIDCProviderMetadata.parse(GOOD_PROVIDER_CONFIGURATION_INFO);
+ final OIDCProviderMetadataContext providerMetadataCtx = new OIDCProviderMetadataContext();
+ providerMetadataCtx.setProviderInformation(providerMetadata);
+ peerCtx.addSubcontext(providerMetadataCtx);
+ return peerCtx;
+ }
+
+ /**
+ * Create an OIDC authentication request.
+ *
+ * @return the authentication request.
+ */
+ private OIDCAuthenticationRequest createAuthenticationRequest() {
+ final OIDCAuthenticationRequest request = new OIDCAuthenticationRequest(new ClientID("https://op.example.com"));
+ request.setState(new State("8df98fd63a53fa5b5433d6f8754bca5d.65317332"));
+ request.setNonce(new Nonce("abadnonce"));
+ return request;
+ }
+
+ /**
+ * Create a client metadata context
+ *
+ * @return the context.
+ *
+ * @throws Exception on error.
+ */
+ private OIDCMetadataContext createClientMedataContext() throws Exception {
+ final JSONParser parser = new JSONParser(JSONParser.MODE_JSON_SIMPLE);
+ final OIDCMetadataContext metadataContext = new OIDCMetadataContext();
+ metadataContext.setClientInformation(
+ OIDCClientInformation.parse((JSONObject)((JSONArray)parser.parse(CLIENT_METADATA)).get(0)));
+ return metadataContext;
+ }
+
+ /**
+ * Create a response mode and type context.
+ *
+ * @return the context.
+ */
+ private ResponseTypeAndModeContext createResponseTypeAndModeContext() {
+ final ResponseTypeAndModeContext respCtx = new ResponseTypeAndModeContext();
+ respCtx.setResponseMode(ResponseMode.QUERY);
+ respCtx.setResponseType(ResponseType.CODE);
+ return respCtx;
+ }
+
+ /**
+ * Create an authentication response.
+ *
+ * @return the OIDC authentication response.
+ *
+ * @throws Exception on error.
+ */
+ private AuthenticationResponse createAuthenticationResponse() throws Exception {
+ return AuthenticationResponseParser.parse(
+ new URI("/idp/profile/Authn/OIDC/RP/callback"
+ + "?state=8df98fd63a53fa5b5433d6f8754bca5d.65317332&code=z8C2DCp6sn0D9aGbEqlrFesdPVRXPtDX"));
+ }
+
@Override
@Nonnull protected ProfileRequestContext buildProfileRequestContext(@Nonnull final String flowId,
@@ -330,12 +453,14 @@ public class OIDCRPFlowTest extends AbstractAuthnXmlFlowExecutionTests {
final FlowExecution flowExecution = getFlowExecutionFactory().createFlowExecution(getFlowDefinition());
flowExecution.getConversationScope()
- .put("opensamlProfileRequestContext", buildProfileRequestContext("authn/OIDCRelyingParty",false,true));
+ .put("opensamlProfileRequestContext",
+ buildProfileRequestContext("authn/OIDCRelyingParty",false,true));
updateFlowExecution(flowExecution);
flowExecution.start(inputMap, externalContext);
assertCurrentStateEquals("AuthRequest");
}
+
/**
* Test the flow from the external authorization request to the end of the flow.
*
@@ -359,6 +484,18 @@ public class OIDCRPFlowTest extends AbstractAuthnXmlFlowExecutionTests {
"idp.authn.oidc.rp.proxyIssuer","https://op.example.com");
setMockProperties(mockProperties);
+
+ final MockWebServer mockOPServer = createSimpleServer();
+ // First is token exchange
+ mockOPServer.enqueue(new MockResponse().setResponseCode(200)
+ .setHeader("content-type", "application/json")
+ .setBody(createAccessTokenResponseJSON()));
+ // Second is userInfo
+ mockOPServer.enqueue(new MockResponse().setResponseCode(200)
+ .setHeader("content-type", "application/json")
+ .setBody(USERINFO_RESPONSE));
+ mockOPServer.start(9918);
+
final FlowExecutionImpl flowExecution = (FlowExecutionImpl)getFlowExecutionFactory()
.createFlowExecution(getFlowDefinition());
@@ -367,44 +504,115 @@ public class OIDCRPFlowTest extends AbstractAuthnXmlFlowExecutionTests {
// create a nested PRC under the authentication context
final ProfileRequestContext nestPrc = (ProfileRequestContext)
- prc.getSubcontext(AuthenticationContext.class).addSubcontext(new ProfileRequestContext(), true);
+ 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);
+ nestPrc.addSubcontext(partyContext);
- final MessageContext outMsgCtx = new MessageContext();
- final OIDCAuthenticationRequest request = new OIDCAuthenticationRequest(new ClientID("https://op.example.com"));
- request.setState(new State("8df98fd63a53fa5b5433d6f8754bca5d.65317332"));
- request.setNonce(new Nonce("abadnonce"));
- outMsgCtx.setMessage(request);
+ // Setup outbound context
+ final MessageContext outMsgCtx = new MessageContext();
+ outMsgCtx.setMessage(createAuthenticationRequest());
+ outMsgCtx.addSubcontext(createPeerContext());
+ outMsgCtx.addSubcontext(createResponseTypeAndModeContext());
+ outMsgCtx.addSubcontext(createClientMedataContext());
+ nestPrc.setOutboundMessageContext(outMsgCtx);
+
+ // Setup inbound context.
+ final MessageContext inMsgCtx = new MessageContext();
+ inMsgCtx.setMessage(createAuthenticationResponse());
+ nestPrc.setInboundMessageContext(inMsgCtx);
- final OIDCPeerEntityContext peerCtx = new OIDCPeerEntityContext();
- OIDCProviderMetadata providerMetadata = OIDCProviderMetadata.parse(GOOD_PROVIDER_CONFIGURATION_INFO);
- final OIDCProviderMetadataContext providerMetadataCtx = new OIDCProviderMetadataContext();
- providerMetadataCtx.setProviderInformation(providerMetadata);
- peerCtx.addSubcontext(providerMetadataCtx);
+ // Add prc to flow.
+ prc.getSubcontext(AuthenticationContext.class)
+ .addSubcontext(new ExternalAuthenticationContext(new ExternalAuthenticationImpl(false)));
+ flowExecution.getConversationScope().put("opensamlProfileRequestContext",prc);
+
- outMsgCtx.addSubcontext(peerCtx);
- nestPrc.setOutboundMessageContext(outMsgCtx);
+ updateFlowExecution(flowExecution);
- final ResponseTypeAndModeContext respCtx = new ResponseTypeAndModeContext();
- respCtx.setResponseMode(ResponseMode.QUERY);
- respCtx.setResponseType(ResponseType.CODE);
- nestPrc.getOutboundMessageContext().addSubcontext(respCtx);
+ //set start view and ending event to transition on.
+ externalContext.setEventId("proceed");
+ setCurrentState("AuthRequest");
+ resumeFlow(externalContext);
- final JSONParser parser = new JSONParser(JSONParser.MODE_JSON_SIMPLE);
- final OIDCMetadataContext metadataContext = new OIDCMetadataContext();
- metadataContext.setClientInformation(
- OIDCClientInformation.parse((JSONObject)((JSONArray)parser.parse(CLIENT_METADATA)).get(0)));
- nestPrc.getOutboundMessageContext().addSubcontext(metadataContext);
+ mockOPServer.shutdown();
+ //assert success conditions
+ assertFlowExecutionEnded();
+ assertNotNull(prc.getSubcontext(AuthenticationContext.class));
+ assertNotNull(prc.getSubcontext(SubjectCanonicalizationContext.class));
+ assertEquals(prc.getSubcontext(SubjectCanonicalizationContext.class).getPrincipalName(),"jdoe");
+
+
+ }
+
+ @Test
+ public void testAuthnFlowFromAuthorizationCallback_UsingJWTUserInfoResponse() throws Exception {
+
+ setFlowPath(FLOW);
+ setFlowModelResources(flowResources);
+ setSubflows(subflows);
+
+ final Map<String,String> mockProperties = Map.of(
+ "idp.service.clientinfo.failFast","false",
+ "idp.oidc.rp.clientID","clientId",
+ "idp.oidc.rp.clientSecret","secret",
+ "idp.oidc.rp.providerConfigurationDocument","provider_location",
+ "idp.oidc.rp.redirectURI","https://localhost:8443/idp/profile/Authn/OIDC/RP/callback",
+ "idp.oidc.rp.scope","email",
+ "idp.entityID", "http://idp.example.com/",
+ "idp.authn.oidc.rp.proxyIssuer","https://op.example.com");
+
+ setMockProperties(mockProperties);
+
+ final MockWebServer mockOPServer = createSimpleServer();
+ // First is token exchange
+ mockOPServer.enqueue(new MockResponse().setResponseCode(200)
+ .setHeader("content-type", "application/json")
+ .setBody(createAccessTokenResponseJSON()));
+ // Second is userInfo
+ mockOPServer.enqueue(new MockResponse().setResponseCode(200)
+ .setHeader("content-type", "application/jwt")
+ .setBody(createSignedUserInfoJWTResponseJSON("https://op.example.com","demo_rp")));
+ mockOPServer.start(9918);
+
+
+ final FlowExecutionImpl flowExecution = (FlowExecutionImpl)getFlowExecutionFactory()
+ .createFlowExecution(getFlowDefinition());
+ final ProfileRequestContext prc = buildProfileRequestContext("authn/OIDCRelyingParty", false,false);
+ prc.getSubcontext(AuthenticationContext.class).setAuthenticatingAuthority("http://op.example.com");
+
+ // 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);
+ nestPrc.addSubcontext(partyContext);
+
+ // Setup outbound context
+ final MessageContext outMsgCtx = new MessageContext();
+ outMsgCtx.setMessage(createAuthenticationRequest());
+ outMsgCtx.addSubcontext(createPeerContext());
+ outMsgCtx.addSubcontext(createResponseTypeAndModeContext());
+ outMsgCtx.addSubcontext(createClientMedataContext());
+ nestPrc.setOutboundMessageContext(outMsgCtx);
+
+ // Setup inbound context.
final MessageContext inMsgCtx = new MessageContext();
- inMsgCtx.setMessage(AuthenticationResponseParser.parse(
- new URI("/idp/profile/Authn/OIDC/RP/callback"
- + "?state=8df98fd63a53fa5b5433d6f8754bca5d.65317332&code=z8C2DCp6sn0D9aGbEqlrFesdPVRXPtDX")));
+ 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);
-
+
updateFlowExecution(flowExecution);
@@ -413,11 +621,15 @@ public class OIDCRPFlowTest extends AbstractAuthnXmlFlowExecutionTests {
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");
-
+
}
@@ -428,7 +640,8 @@ public class OIDCRPFlowTest extends AbstractAuthnXmlFlowExecutionTests {
* @throws Exception on error.
*/
@Test
- public void testAuthnFlowFromAuthorizationCallback_ErrorAuthenticationResponse() throws Exception {
+ public void testAuthnFlowFromAuthorizationCallback_ErrorAuthenticationResponse()
+ throws Exception {
setFlowPath(FLOW);
setFlowModelResources(flowResources);
@@ -481,9 +694,8 @@ public class OIDCRPFlowTest extends AbstractAuthnXmlFlowExecutionTests {
//assert success conditions
assertFlowExecutionEnded();
- //TODO FIX SUCCESS CONDITIONS
assertNotNull(prc.getSubcontext(AuthenticationContext.class));
-
+ assertNull(prc.getSubcontext(SubjectCanonicalizationContext.class));
}
}
diff --git a/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/TokenResponseIDTokenLookupStrategyTest.java b/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/TokenResponseIDTokenLookupStrategyTest.java
index fcfd3bb..299d5b6 100644
--- a/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/TokenResponseIDTokenLookupStrategyTest.java
+++ b/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/TokenResponseIDTokenLookupStrategyTest.java
@@ -13,7 +13,7 @@ import org.testng.annotations.Test;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
-import net.shibboleth.idp.plugin.authn.oidc.rp.context.TokenResponseContext;
+import net.shibboleth.idp.plugin.authn.oidc.rp.context.AccessTokenResponseContext;
/** Tests for TokenResponseIDTokenLookupStrategy.*/
public class TokenResponseIDTokenLookupStrategyTest extends AbstractOIDCTest {
@@ -30,7 +30,7 @@ public class TokenResponseIDTokenLookupStrategyTest extends AbstractOIDCTest {
final Map<String, Object> rawResponse = mapper.readValue(
new ByteArrayInputStream(ACCESS_TOKEN_RESPONSE.getBytes()), new TypeReference<Map<String, Object>>() {});
- final TokenResponseContext trc = new TokenResponseContext();
+ final AccessTokenResponseContext trc = new AccessTokenResponseContext();
trc.setRawTokenResponse(rawResponse);
prc.getInboundMessageContext().addSubcontext(trc);
@@ -57,7 +57,7 @@ public class TokenResponseIDTokenLookupStrategyTest extends AbstractOIDCTest {
@Test
public void testLookup_NoResponseContext() {
- prc.getInboundMessageContext().removeSubcontext(TokenResponseContext.class);
+ prc.getInboundMessageContext().removeSubcontext(AccessTokenResponseContext.class);
final String rawToken = strategy.apply(prc);
assertNull(rawToken);
diff --git a/idp-oidc-rp-impl/src/test/resources/attribute/registry/attribute-registry.xml b/idp-oidc-rp-impl/src/test/resources/attribute/registry/attribute-registry.xml
new file mode 100644
index 0000000..e609e84
--- /dev/null
+++ b/idp-oidc-rp-impl/src/test/resources/attribute/registry/attribute-registry.xml
@@ -0,0 +1,442 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<beans xmlns="http://www.springframework.org/schema/beans"
+ xmlns:context="http://www.springframework.org/schema/context"
+ xmlns:util="http://www.springframework.org/schema/util"
+ xmlns:p="http://www.springframework.org/schema/p"
+ xmlns:c="http://www.springframework.org/schema/c"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
+ http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
+ http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd"
+
+ default-init-method="initialize"
+ default-destroy-method="destroy">
+
+
+
+ <bean id="OIDCByteTranscoder"
+ class="net.shibboleth.oidc.attribute.transcoding.impl.OIDCByteAttributeTranscoder" />
+
+ <bean id="OIDCStringTranscoder"
+ class="net.shibboleth.oidc.attribute.transcoding.impl.OIDCStringAttributeTranscoder" />
+
+ <bean id="OIDCScopedStringTranscoder"
+ class="net.shibboleth.oidc.attribute.transcoding.impl.OIDCScopedStringAttributeTranscoder" />
+
+
+ <!-- https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims -->
+
+ <bean parent="shibboleth.TranscodingRuleLoader">
+ <constructor-arg>
+ <list>
+
+ <!-- Typical inetOrgPerson attributes that map to standard claims. -->
+
+ <bean parent="shibboleth.TranscodingProperties">
+ <property name="properties">
+ <props merge="true">
+ <prop key="id">displayName</prop>
+ <prop key="transcoder">OIDCStringTranscoder</prop>
+ <prop key="oidc.name">name</prop>
+ <prop key="displayName.en">Display name</prop>
+ <prop key="displayName.de">Anzeigename</prop>
+ <prop key="displayName.fr">Nom</prop>
+ <prop key="displayName.it">Nome</prop>
+ <prop key="displayName.ja">表示名</prop>
+ <prop key="description.en">The name that should appear in white-pages-like applications for this person.</prop>
+ <prop key="description.de">Anzeigename</prop>
+ <prop key="description.fr">Nom complet d'affichage</prop>
+ <prop key="description.it">Nome</prop>
+ <prop key="description.ja">アプリケーションでの表示に用いられる英字氏名</prop>
+ </props>
+ </property>
+ </bean>
+
+ <bean parent="shibboleth.TranscodingProperties">
+ <property name="properties">
+ <props merge="true">
+ <prop key="id">givenName</prop>
+ <prop key="transcoder">OIDCStringTranscoder</prop>
+ <prop key="oidc.name">given_name</prop>
+ <prop key="displayName.en">Given name</prop>
+ <prop key="displayName.de">Vorname</prop>
+ <prop key="displayName.fr">Prénom</prop>
+ <prop key="displayName.it">Nome</prop>
+ <prop key="displayName.ja">名</prop>
+ <prop key="description.en">Given name of a person</prop>
+ <prop key="description.de">Vorname</prop>
+ <prop key="description.fr">Prénom de l'utilisateur</prop>
+ <prop key="description.it">Nome</prop>
+ <prop key="description.ja">氏名(名)の英語表記</prop>
+ </props>
+ </property>
+ </bean>
+
+ <bean parent="shibboleth.TranscodingProperties">
+ <property name="properties">
+ <props merge="true">
+ <prop key="id">homePhone</prop>
+ <prop key="transcoder">OIDCStringTranscoder</prop>
+ <prop key="oidc.name">phone_number</prop>
+ <prop key="displayName.en">Private phone number</prop>
+ <prop key="displayName.de">Telefon Privat</prop>
+ <prop key="displayName.fr">Teléphone personnel</prop>
+ <prop key="displayName.it">Numero di telefono privato</prop>
+ <prop key="displayName.ja">自宅電話番号</prop>
+ <prop key="description.en">Private phone number</prop>
+ <prop key="description.de">Private Telefonnummer</prop>
+ <prop key="description.fr">Numéro de téléphone de domicile de la personne</prop>
+ <prop key="description.it">Numero di telefono privato</prop>
+ <prop key="description.ja">自宅の電話番号</prop>
+ </props>
+ </property>
+ </bean>
+
+ <bean parent="shibboleth.TranscodingProperties">
+ <property name="properties">
+ <props merge="true">
+ <prop key="id">mail</prop>
+ <prop key="transcoder">OIDCStringTranscoder</prop>
+ <prop key="oidc.name">email</prop>
+ <prop key="displayName.en">E-mail</prop>
+ <prop key="displayName.de">E-Mail</prop>
+ <prop key="displayName.fr">Email</prop>
+ <prop key="displayName.it">E-mail</prop>
+ <prop key="displayName.ja">メールアドレス</prop>
+ <prop key="description.en">E-Mail: Preferred address for e-mail to be sent to this person</prop>
+ <prop key="description.de">E-Mail-Adresse</prop>
+ <prop key="description.de-ch">E-Mail Adresse</prop>
+ <prop key="description.fr">Adresse de courrier électronique</prop>
+ <prop key="description.it">E-Mail: l'indirizzo e-mail preferito dall'utente</prop>
+ <prop key="description.ja">メールアドレス</prop>
+ </props>
+ </property>
+ </bean>
+
+ <bean parent="shibboleth.TranscodingProperties">
+ <property name="properties">
+ <props merge="true">
+ <prop key="id">preferredLanguage</prop>
+ <prop key="transcoder">OIDCStringTranscoder</prop>
+ <prop key="oidc.name">locale</prop>
+ <prop key="displayName.en">Preferred Language</prop>
+ <prop key="displayName.de">Bevorzugte Sprache</prop>
+ <prop key="displayName.fr">Langue préférée</prop>
+ <prop key="displayName.it">Lingua preferita</prop>
+ <prop key="displayName.ja">希望言語</prop>
+ <prop key="description.en">Preferred language: Users preferred language (see RFC1766)</prop>
+ <prop key="description.de">Bevorzugte Sprache (siehe RFC1766)</prop>
+ <prop key="description.fr">Exemple: fr, de, it, en, ... (voir RFC1766)</prop>
+ <prop key="description.it">Lingua preferita: la lingua preferita dall'utente (cfr. RFC1766)</prop>
+ <prop key="description.ja">利用者が希望する言語(RFC1766 を参照)</prop>
+ </props>
+ </property>
+ </bean>
+
+ <bean parent="shibboleth.TranscodingProperties">
+ <property name="properties">
+ <props merge="true">
+ <prop key="id">sn</prop>
+ <prop key="transcoder">OIDCStringTranscoder</prop>
+ <prop key="oidc.name">family_name</prop>
+ <prop key="displayName.en">Surname</prop>
+ <prop key="displayName.de">Nachname</prop>
+ <prop key="displayName.fr">Nom de famille</prop>
+ <prop key="displayName.it">Cognome</prop>
+ <prop key="displayName.ja">姓</prop>
+ <prop key="description.en">Surname or family name</prop>
+ <prop key="description.de">Familienname</prop>
+ <prop key="description.fr">Nom de famille de l'utilisateur.</prop>
+ <prop key="description.it">Cognome dell'utilizzatore</prop>
+ <prop key="description.ja">氏名(姓)の英語表記</prop>
+ </props>
+ </property>
+ </bean>
+
+ <bean parent="shibboleth.TranscodingProperties">
+ <property name="properties">
+ <props merge="true">
+ <prop key="id">telephoneNumber</prop>
+ <prop key="transcoder">OIDCStringTranscoder</prop>
+ <prop key="oidc.name">phone_number</prop>
+ <prop key="displayName.en">Business phone number</prop>
+ <prop key="displayName.de">Telefon Geschäft</prop>
+ <prop key="displayName.fr">Teléphone professionnel</prop>
+ <prop key="displayName.it">Numero di telefono dell'ufficio</prop>
+ <prop key="displayName.ja">所属機関内電話番号</prop>
+ <prop key="description.en">Business phone number: Office or campus phone number</prop>
+ <prop key="description.de">Telefonnummer am Arbeitsplatz</prop>
+ <prop key="description.fr">Teléphone de l'institut, de l'université</prop>
+ <prop key="description.it">Numero di telefono dell'ufficio</prop>
+ <prop key="description.ja">所属機関での利用者の電話番号</prop>
+ </props>
+ </property>
+ </bean>
+
+ <bean parent="shibboleth.TranscodingProperties">
+ <property name="properties">
+ <props merge="true">
+ <prop key="id">uid</prop>
+ <prop key="transcoder">OIDCStringTranscoder</prop>
+ <prop key="oidc.name">preferred_username</prop>
+ <prop key="displayName.en">User ID</prop>
+ <prop key="displayName.de">Benutzer-ID</prop>
+ <prop key="displayName.fr">ID utilisateur</prop>
+ <prop key="displayName.it">ID dell'utente</prop>
+ <prop key="displayName.ja">ユーザID</prop>
+ <prop key="description.en">A unique identifier for a person, mainly used for user identification within the user's home organization.</prop>
+ <prop key="description.de">Eine eindeutige Nummer für eine Person, welche hauptsächlich zur Identifikation innerhalb der Organisation benutzt wird.</prop>
+ <prop key="description.fr">Identifiant de connexion d'une personnes sur les systèmes informatiques.</prop>
+ <prop key="description.it">Identificativo unico della persona, usato per l'identificazione dell'utente all'interno della organizzazione di appartenenza.</prop>
+ <prop key="description.ja">所属機関内で一意の利用者識別子</prop>
+ </props>
+ </property>
+ </bean>
+
+ <!-- eduPerson attributes that map to standard claims. -->
+
+ <bean parent="shibboleth.TranscodingProperties">
+ <property name="properties">
+ <props merge="true">
+ <prop key="id">eduPersonNickname</prop>
+ <prop key="transcoder">OIDCStringTranscoder</prop>
+ <prop key="oidc.name">nickname</prop>
+ <prop key="displayName.en">Nickname</prop>
+ <prop key="displayName.de">Kurzname</prop>
+ <prop key="displayName.de-ch">Übername</prop>
+ <prop key="displayName.fr">Surnom</prop>
+ <prop key="displayName.it">Diminutivo</prop>
+ <prop key="displayName.ja">ニックネーム</prop>
+ <prop key="description.en">Person's nickname, or the informal name by which they are accustomed to be hailed.</prop>
+ <prop key="description.de">Kurzname einer Person, oder üblicher Rufname zur Begrüßung.</prop>
+ <prop key="description.de-ch">Übername einer Person, oder üblicher Rufname zur Begrüssung.</prop>
+ <prop key="description.fr">Nom personnalisable pour un usage informel.</prop>
+ <prop key="description.it">Diminutivo della persona, o soprannome.</prop>
+ <prop key="description.ja">利用者のニックネームもしくは通称</prop>
+ </props>
+ </property>
+ </bean>
+
+ <!-- A subset of the major eduPerson attributes that have no standard claim mapping. -->
+
+ <bean parent="shibboleth.TranscodingProperties">
+ <property name="properties">
+ <props merge="true">
+ <prop key="id">eduPersonAssurance</prop>
+ <prop key="transcoder">OIDCStringTranscoder</prop>
+ <prop key="oidc.name">eduPersonAssurance</prop>
+ <prop key="displayName.en">Assurance level</prop>
+ <prop key="displayName.de">Vertrauensgrad</prop>
+ <prop key="displayName.fr">Niveau de confiance</prop>
+ <prop key="displayName.it">Livello di sicurezza</prop>
+ <prop key="displayName.ja">保証レベル</prop>
+ <prop key="description.en">Set of URIs that assert compliance with specific standards for identity assurance.</prop>
+ <prop key="description.de">URIs die eine gewisse Zusicherung für spezifische Standards des Vertrauens beinhalten</prop>
+ <prop key="description.fr">Un ensemble d'URI qui attestent la conformité selon un standard pour les niveaux d'assurance d'identités</prop>
+ <prop key="description.it">Un insieme di URI che asseriscono l'osservanza dei livelli di sicurezza richiesti</prop>
+ <prop key="description.ja">IDの保証レベルに関して特定の基準に準拠していることを示すURI</prop>
+ </props>
+ </property>
+ </bean>
+
+ <bean parent="shibboleth.TranscodingProperties">
+ <property name="properties">
+ <props merge="true">
+ <prop key="id">eduPersonEntitlement</prop>
+ <prop key="transcoder">OIDCStringTranscoder</prop>
+ <prop key="oidc.name">eduPersonEntitlement</prop>
+ <prop key="displayName.en">Entitlement</prop>
+ <prop key="displayName.de">Berechtigung</prop>
+ <prop key="displayName.fr">Entitlement</prop>
+ <prop key="displayName.it">Prerogativa</prop>
+ <prop key="displayName.ja">資格情報</prop>
+ <prop key="description.en">Member of: URI (either URL or URN) that indicates a set of rights to specific resources based on an agreement across the releavant community</prop>
+ <prop key="description.de">Zeichenkette, die Rechte für spezifische Ressourcen beschreibt</prop>
+ <prop key="description.fr">Membre de: URI (soit une URL ou une URN) décrivant un droit spécific d'accès.</prop>
+ <prop key="description.it">Membro delle seguenti URI (sia URL o URN) che rappresentano diritti specifici d'accesso validi in tutta la communità</prop>
+ <prop key="description.ja">特定のアプリケーションもしくはコミュニティ内の複数リソースへのアクセス権限を持つことを示すURI(URLもしくはURN)</prop>
+ </props>
+ </property>
+ </bean>
+
+ <bean parent="shibboleth.TranscodingProperties">
+ <property name="properties">
+ <props merge="true">
+ <prop key="id">eduPersonPrincipalName</prop>
+ <prop key="transcoder">OIDCScopedStringTranscoder</prop>
+ <prop key="oidc.name">eduPersonPrincipalName</prop>
+ <prop key="displayName.en">Principal name</prop>
+ <prop key="displayName.de">Persönliche ID</prop>
+ <prop key="displayName.fr">Principal Name</prop>
+ <prop key="displayName.it">Principal Name</prop>
+ <prop key="displayName.ja">プリンシパルID</prop>
+ <prop key="description.en">A unique identifier for a person, mainly for inter-institutional user identification.</prop>
+ <prop key="description.de">Eindeutige Benutzeridentifikation</prop>
+ <prop key="description.de-ch">Eindeutige Benützeridentifikation</prop>
+ <prop key="description.fr">L'identifiant unique de l'utilisateur</prop>
+ <prop key="description.it">Un ID personale che identifica chiaramente l'utente in seno alla sua organizzazione</prop>
+ <prop key="description.ja">フェデレーション内で一意かつ永続的な利用者識別子</prop>
+ </props>
+ </property>
+ </bean>
+
+ <bean parent="shibboleth.TranscodingProperties">
+ <property name="properties">
+ <props merge="true">
+ <prop key="id">eduPersonScopedAffiliation</prop>
+ <prop key="transcoder">OIDCScopedStringTranscoder</prop>
+ <prop key="oidc.name">eduPersonScopedAffiliation</prop>
+ <prop key="displayName.en">Scoped affiliation</prop>
+ <prop key="displayName.de">Zugehörigkeit</prop>
+ <prop key="displayName.fr">Affiliation</prop>
+ <prop key="displayName.it">Tipo di membro</prop>
+ <prop key="displayName.ja">スコープ付き職位</prop>
+ <prop key="description.en">Specifies the person's affiliation within a particular security domain</prop>
+ <prop key="description.de">Art der Zugehörigkeit zur Heimatorganisation</prop>
+ <prop key="description.de-ch">Art der Zugehörigkeit zur Heimorganisation</prop>
+ <prop key="description.fr">Type d'affiliation dans l'organisation</prop>
+ <prop key="description.it">Tipo di membro: Tipo di lavoro svolto per l'organizzazione</prop>
+ <prop key="description.ja">セキュリティドメインのスコープが付いた所属機関における職位</prop>
+ </props>
+ </property>
+ </bean>
+
+ <!-- The remainder are standard OIDC claims, which we map based on the actual claim name. -->
+
+ <bean parent="shibboleth.TranscodingProperties">
+ <property name="properties">
+ <props merge="true">
+ <prop key="id">address</prop>
+ <prop key="transcoder">OIDCStringTranscoder</prop>
+ <prop key="oidc.asObject">true</prop>
+ <prop key="oidc.name">address</prop>
+ <prop key="displayName.en">Postal address</prop>
+ </props>
+ </property>
+ </bean>
+
+ <bean parent="shibboleth.TranscodingProperties">
+ <property name="properties">
+ <props merge="true">
+ <prop key="id">birthdate</prop>
+ <prop key="transcoder">OIDCStringTranscoder</prop>
+ <prop key="oidc.name">birthdate</prop>
+ <prop key="displayName.en">Date of birth</prop>
+ </props>
+ </property>
+ </bean>
+
+ <bean parent="shibboleth.TranscodingProperties">
+ <property name="properties">
+ <props merge="true">
+ <prop key="id">email_verified</prop>
+ <prop key="transcoder">OIDCStringTranscoder</prop>
+ <prop key="oidc.name">email_verified</prop>
+ <prop key="oidc.asBoolean">true</prop>
+ <prop key="displayName.en">E-mail verification status</prop>
+ <prop key="description.en">Indicates whether e-mail address has been verified by the issuer</prop>
+ </props>
+ </property>
+ </bean>
+
+ <bean parent="shibboleth.TranscodingProperties">
+ <property name="properties">
+ <props merge="true">
+ <prop key="id">gender</prop>
+ <prop key="transcoder">OIDCStringTranscoder</prop>
+ <prop key="oidc.name">gender</prop>
+ <prop key="displayName.en">Gender</prop>
+ </props>
+ </property>
+ </bean>
+
+ <bean parent="shibboleth.TranscodingProperties">
+ <property name="properties">
+ <props merge="true">
+ <prop key="id">middle_name</prop>
+ <prop key="transcoder">OIDCStringTranscoder</prop>
+ <prop key="oidc.name">middle_name</prop>
+ <prop key="displayName.en">Middle name</prop>
+ </props>
+ </property>
+ </bean>
+
+ <bean parent="shibboleth.TranscodingProperties">
+ <property name="properties">
+ <props merge="true">
+ <prop key="id">phone_number_verified</prop>
+ <prop key="transcoder">OIDCStringTranscoder</prop>
+ <prop key="oidc.name">phone_number_verified</prop>
+ <prop key="oidc.asBoolean">true</prop>
+ <prop key="displayName.en">Phone number verification status</prop>
+ <prop key="description.en">Indicates whether phone number has been verified by the issuer</prop>
+ </props>
+ </property>
+ </bean>
+
+ <bean parent="shibboleth.TranscodingProperties">
+ <property name="properties">
+ <props merge="true">
+ <prop key="id">picture</prop>
+ <prop key="transcoder">OIDCStringTranscoder</prop>
+ <prop key="oidc.name">picture</prop>
+ <prop key="displayName.en">Picture</prop>
+ <prop key="description.en">URL of personal photo</prop>
+ </props>
+ </property>
+ </bean>
+
+ <bean parent="shibboleth.TranscodingProperties">
+ <property name="properties">
+ <props merge="true">
+ <prop key="id">profile</prop>
+ <prop key="transcoder">OIDCStringTranscoder</prop>
+ <prop key="oidc.name">profile</prop>
+ <prop key="displayName.en">Profile page</prop>
+ <prop key="description.en">URL of personal profile page</prop>
+ </props>
+ </property>
+ </bean>
+
+ <bean parent="shibboleth.TranscodingProperties">
+ <property name="properties">
+ <props merge="true">
+ <prop key="id">website</prop>
+ <prop key="transcoder">OIDCStringTranscoder</prop>
+ <prop key="oidc.name">website</prop>
+ <prop key="displayName.en">Web site</prop>
+ <prop key="description.en">URL to personal web site</prop>
+ </props>
+ </property>
+ </bean>
+
+ <bean parent="shibboleth.TranscodingProperties">
+ <property name="properties">
+ <props merge="true">
+ <prop key="id">updated_at</prop>
+ <prop key="transcoder">OIDCStringTranscoder</prop>
+ <prop key="oidc.name">updated_at</prop>
+ <prop key="oidc.asInteger">true</prop>
+ <prop key="displayName.en">Last update of information</prop>
+ </props>
+ </property>
+ </bean>
+
+ <bean parent="shibboleth.TranscodingProperties">
+ <property name="properties">
+ <props merge="true">
+ <prop key="id">zoneinfo</prop>
+ <prop key="transcoder">OIDCStringTranscoder</prop>
+ <prop key="oidc.name">zoneinfo</prop>
+ <prop key="displayName.en">Time zone</prop>
+ </props>
+ </property>
+ </bean>
+
+ </list>
+ </constructor-arg>
+ </bean>
+
+
+
+</beans>
diff --git a/idp-oidc-rp-impl/src/test/resources/attribute/registry/postconfig.xml b/idp-oidc-rp-impl/src/test/resources/attribute/registry/postconfig.xml
new file mode 100644
index 0000000..29752a5
--- /dev/null
+++ b/idp-oidc-rp-impl/src/test/resources/attribute/registry/postconfig.xml
@@ -0,0 +1,67 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<beans xmlns="http://www.springframework.org/schema/beans"
+ xmlns:context="http://www.springframework.org/schema/context"
+ xmlns:util="http://www.springframework.org/schema/util"
+ xmlns:p="http://www.springframework.org/schema/p"
+ xmlns:c="http://www.springframework.org/schema/c"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
+ http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
+ http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd"
+
+ default-init-method="initialize" default-destroy-method="destroy">
+
+ <!-- Test config that pulls together various attribute encoders and registries to allow the flow
+ tests to run -->
+
+ <bean id="shibboleth.AttributeRegistryService" class="net.shibboleth.ext.spring.service.ReloadableSpringService"
+ p:failFast="false"
+ p:reloadCheckDelay="PT0S"
+ p:beanPostProcessors-ref="shibboleth.IdentifiableBeanPostProcessor"
+ p:beanFactoryPostProcessors-ref="shibboleth.PropertySourcesPlaceholderConfigurer">
+ <constructor-arg name="claz" value="net.shibboleth.idp.attribute.transcoding.AttributeTranscoderRegistry" />
+ <constructor-arg name="strategy">
+ <bean class="net.shibboleth.idp.attribute.transcoding.impl.AttributeRegistryServiceStrategy"
+ p:namingRegistry-ref="TestNamingRegistryList"/> <!-- Autowired not working in the test flow, so add manually -->
+ </constructor-arg>
+ <property name="serviceConfigurations">
+ <util:list>
+ <value>attribute/registry/attribute-registry.xml</value> <!-- load itself -->
+ </util:list>
+ </property>
+ </bean>
+
+ <!-- Add a naming registry manually, as Autowired is not enabled in this context. -->
+ <util:list id="TestNamingRegistryList">
+ <ref bean="OidcNamingFunction"/>
+ </util:list>
+
+ <bean id="shibboleth.RegistryNamingFunction" abstract="true"
+ class="net.shibboleth.idp.attribute.transcoding.BasicNamingFunction" />
+
+ <bean id="OidcNamingFunction" parent="shibboleth.RegistryNamingFunction" c:claz="net.minidev.json.JSONObject">
+ <constructor-arg name="function">
+ <bean class="net.shibboleth.oidc.attribute.transcoding.AbstractOIDCAttributeTranscoder.NamingFunction" />
+ </constructor-arg>
+ </bean>
+ <!-- done. -->
+
+ <bean id="shibboleth.TranscodingRuleLoader"
+ class="net.shibboleth.idp.attribute.transcoding.impl.TranscodingRuleLoader" abstract="true" />
+
+ <bean id="shibboleth.TranscodingRule"
+ class="net.shibboleth.idp.attribute.transcoding.TranscodingRule" abstract="true" />
+
+ <!-- TODO unsure why we need this if no OIDC specific information? -->
+ <bean id="shibboleth.TranscodingProperties" lazy-init="true"
+ class="org.springframework.beans.factory.config.PropertiesFactoryBean">
+ <property name="properties">
+ <props>
+ <prop key="saml1.encodeType">%{idp.service.attribute.registry.encodeType:true}</prop>
+ <prop key="saml2.encodeType">%{idp.service.attribute.registry.encodeType:true}</prop>
+ </props>
+ </property>
+ </bean>
+
+
+</beans>
diff --git a/idp-oidc-rp-impl/src/test/resources/logback-test.xml b/idp-oidc-rp-impl/src/test/resources/logback-test.xml
index 28948ad..5a9618d 100644
--- a/idp-oidc-rp-impl/src/test/resources/logback-test.xml
+++ b/idp-oidc-rp-impl/src/test/resources/logback-test.xml
@@ -2,6 +2,7 @@
<configuration>
<logger name="net.shibboleth" level="DEBUG"/>
+ <logger name="net.shibboleth.idp.plugin.authn" level="TRACE"/>
<logger name="org.springframework" level="INFO"/>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
diff --git a/pom.xml b/pom.xml
index c9959f6..2f965b9 100644
--- a/pom.xml
+++ b/pom.xml
@@ -139,6 +139,18 @@
<type>pom</type>
<scope>import</scope>
</dependency>
+ <dependency>
+ <groupId>com.squareup.okhttp3</groupId>
+ <artifactId>mockwebserver</artifactId>
+ <version>4.9.3</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.squareup.okhttp3</groupId>
+ <artifactId>okhttp-tls</artifactId>
+ <version>4.9.3</version>
+ <scope>test</scope>
+ </dependency>
</dependencies>
</dependencyManagement>
<build>
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.
More information about the commits
mailing list