Make things in the OIDC protocol handler properly configurable
Project: http://git-wip-us.apache.org/repos/asf/cxf-fediz/repo Commit: http://git-wip-us.apache.org/repos/asf/cxf-fediz/commit/9375d3e5 Tree: http://git-wip-us.apache.org/repos/asf/cxf-fediz/tree/9375d3e5 Diff: http://git-wip-us.apache.org/repos/asf/cxf-fediz/diff/9375d3e5 Branch: refs/heads/master Commit: 9375d3e5465ff157b1cb8a463f34ee64e2bee78f Parents: 88afda3 Author: Colm O hEigeartaigh <[email protected]> Authored: Wed Feb 24 15:57:58 2016 +0000 Committer: Colm O hEigeartaigh <[email protected]> Committed: Wed Feb 24 15:57:58 2016 +0000 ---------------------------------------------------------------------- .../TrustedIdpOIDCProtocolHandler.java | 152 +++++++++++++++---- .../oidc/src/test/resources/entities-realma.xml | 22 +-- 2 files changed, 124 insertions(+), 50 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/cxf-fediz/blob/9375d3e5/services/idp/src/main/java/org/apache/cxf/fediz/service/idp/protocols/TrustedIdpOIDCProtocolHandler.java ---------------------------------------------------------------------- diff --git a/services/idp/src/main/java/org/apache/cxf/fediz/service/idp/protocols/TrustedIdpOIDCProtocolHandler.java b/services/idp/src/main/java/org/apache/cxf/fediz/service/idp/protocols/TrustedIdpOIDCProtocolHandler.java index 52e007e..1e1c199 100644 --- a/services/idp/src/main/java/org/apache/cxf/fediz/service/idp/protocols/TrustedIdpOIDCProtocolHandler.java +++ b/services/idp/src/main/java/org/apache/cxf/fediz/service/idp/protocols/TrustedIdpOIDCProtocolHandler.java @@ -30,6 +30,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.List; +import java.util.Map; import javax.security.auth.callback.Callback; import javax.security.auth.callback.CallbackHandler; @@ -57,6 +58,7 @@ import org.apache.cxf.rs.security.jose.jwa.SignatureAlgorithm; import org.apache.cxf.rs.security.jose.jws.JwsJwtCompactConsumer; import org.apache.cxf.rs.security.jose.jwt.JwtConstants; import org.apache.cxf.rs.security.jose.jwt.JwtToken; +import org.apache.cxf.rs.security.jose.jwt.JwtUtils; import org.apache.cxf.rs.security.oauth2.common.ClientAccessToken; import org.apache.cxf.rs.security.oauth2.provider.OAuthJSONProvider; import org.apache.cxf.rs.security.oauth2.utils.OAuthConstants; @@ -81,6 +83,32 @@ import org.springframework.webflow.execution.RequestContext; @Component public class TrustedIdpOIDCProtocolHandler implements TrustedIdpProtocolHandler { + /** + * The client_id value to send to the OIDC IdP. + */ + public static final String CLIENT_ID = "client.id"; + + /** + * The secret associated with the client to authenticate to the OIDC IdP. + */ + public static final String CLIENT_SECRET = "client.secret"; + + /** + * The Token endpoint. The authorization endpoint is specified by TrustedIdp.url. + */ + public static final String TOKEN_ENDPOINT = "token.endpoint"; + + /** + * The signature algorithm to use in verifying the IdToken. The default is "RS256". + */ + public static final String SIGNATURE_ALGORITHM = "signature.algorithm"; + + /** + * The Claim in which to extract the Subject username to insert into the generated SAML token. + * It defaults to "preferred_username", otherwise it falls back to the "sub" claim. + */ + public static final String SUBJECT_CLAIM = "subject.claim"; + public static final String PROTOCOL = "openid-connect-1.0"; private static final Logger LOG = LoggerFactory.getLogger(TrustedIdpOIDCProtocolHandler.class); @@ -99,15 +127,21 @@ public class TrustedIdpOIDCProtocolHandler implements TrustedIdpProtocolHandler @Override public URL mapSignInRequest(RequestContext context, Idp idp, TrustedIdp trustedIdp) { + String clientId = getProperty(trustedIdp, CLIENT_ID); + if (clientId == null || clientId.isEmpty()) { + LOG.warn("A CLIENT_ID must be configured to use the OIDCProtocolHandler"); + throw new IllegalStateException("No CLIENT_ID specified"); + } + try { StringBuilder sb = new StringBuilder(); sb.append(trustedIdp.getUrl()); sb.append("?"); sb.append("response_type").append('='); - sb.append("code"); //TODO + sb.append("code"); sb.append("&"); sb.append("client_id").append('='); - sb.append("consumer-id"); //TODO + sb.append(clientId); sb.append("&"); sb.append("redirect_uri").append('='); sb.append(URLEncoder.encode(idp.getIdpUrl().toString(), "UTF-8")); @@ -121,13 +155,6 @@ public class TrustedIdpOIDCProtocolHandler implements TrustedIdpProtocolHandler sb.append(wctx); } - /* - String wfresh = context.getFlowScope().getString(FederationConstants.PARAM_FRESHNESS); - if (wfresh != null) { - sb.append("&").append(FederationConstants.PARAM_FRESHNESS).append('='); - sb.append(URLEncoder.encode(wfresh, "UTF-8")); - } - */ return new URL(sb.toString()); } catch (MalformedURLException ex) { LOG.error("Invalid Redirect URL for Trusted Idp", ex); @@ -143,27 +170,41 @@ public class TrustedIdpOIDCProtocolHandler implements TrustedIdpProtocolHandler String code = (String) WebUtils.getAttributeFromFlowScope(context, OAuthConstants.CODE_RESPONSE_TYPE); - if (code != null) { - // Here we need to get the IdToken using the authorization code - String address = "http://localhost:8080/auth/realms/realmb/protocol/openid-connect/token"; + if (code != null && !code.isEmpty()) { + + String tokenEndpoint = getProperty(trustedIdp, TOKEN_ENDPOINT); + if (tokenEndpoint == null || tokenEndpoint.isEmpty()) { + LOG.warn("A TOKEN_ENDPOINT must be configured to use the OIDCProtocolHandler"); + throw new IllegalStateException("No TOKEN_ENDPOINT specified"); + } + + String clientId = getProperty(trustedIdp, CLIENT_ID); + String clientSecret = getProperty(trustedIdp, CLIENT_SECRET); + if (clientSecret == null || clientSecret.isEmpty()) { + LOG.warn("A CLIENT_SECRET must be configured to use the OIDCProtocolHandler"); + throw new IllegalStateException("No CLIENT_SECRET specified"); + } + // Here we need to get the IdToken using the authorization code List<Object> providers = new ArrayList<Object>(); providers.add(new OAuthJSONProvider()); WebClient client = - WebClient.create(address, providers, "consumer-id", "90d5da25-e900-443f-a5d5-feb3bb060800", null); + WebClient.create(tokenEndpoint, providers, clientId, clientSecret, null); ClientConfiguration config = WebClient.getConfig(client); - config.getOutInterceptors().add(new LoggingOutInterceptor()); - config.getInInterceptors().add(new LoggingInInterceptor()); + if (LOG.isDebugEnabled()) { + config.getOutInterceptors().add(new LoggingOutInterceptor()); + config.getInInterceptors().add(new LoggingInInterceptor()); + } client.type("application/x-www-form-urlencoded").accept("application/json"); Form form = new Form(); form.param("grant_type", "authorization_code"); form.param("code", code); - form.param("client_id", "consumer-id"); + form.param("client_id", clientId); form.param("redirect_uri", idp.getIdpUrl().toString()); Response response = client.post(form); @@ -192,23 +233,24 @@ public class TrustedIdpOIDCProtocolHandler implements TrustedIdpProtocolHandler JwsJwtCompactConsumer jwtConsumer = new JwsJwtCompactConsumer(idToken); JwtToken jwt = jwtConsumer.getJwtToken(); - if (!jwtConsumer.verifySignatureWith(validatingCert, SignatureAlgorithm.RS256)) { + // Validate the Signature + String sigAlgo = getProperty(trustedIdp, SIGNATURE_ALGORITHM); + if (sigAlgo == null || sigAlgo.isEmpty()) { + sigAlgo = "RS256"; + } + if (!jwtConsumer.verifySignatureWith(validatingCert, SignatureAlgorithm.getAlgorithm(sigAlgo))) { LOG.warn("Signature does not validate"); return null; } - Date created = new Date(); - if (jwt.getClaim(JwtConstants.CLAIM_ISSUED_AT) != null) { - created = new Date((long)jwt.getClaim(JwtConstants.CLAIM_ISSUED_AT) * 1000L); - } - if (jwt.getClaim(JwtConstants.CLAIM_EXPIRY) == null) { - LOG.warn("No expiry in the token"); - return null; - } + // Make sure the received token is valid according to the spec + validateToken(jwt, clientId); + + Date created = new Date((long)jwt.getClaim(JwtConstants.CLAIM_ISSUED_AT) * 1000L); Date expires = new Date((long)jwt.getClaim(JwtConstants.CLAIM_EXPIRY) * 1000L); // Convert into a SAML Token - SamlAssertionWrapper assertion = createSamlAssertion(idp, jwt, created, expires); + SamlAssertionWrapper assertion = createSamlAssertion(idp, trustedIdp, jwt, created, expires); Document doc = DOMUtils.createDocument(); Element token = assertion.toDOM(doc); @@ -234,6 +276,33 @@ public class TrustedIdpOIDCProtocolHandler implements TrustedIdpProtocolHandler return null; } + protected void validateToken(JwtToken jwt, String clientId) { + // We must have the following claims + if (jwt.getClaim(JwtConstants.CLAIM_ISSUER) == null + || jwt.getClaim(JwtConstants.CLAIM_SUBJECT) == null + || jwt.getClaim(JwtConstants.CLAIM_AUDIENCE) == null + || jwt.getClaim(JwtConstants.CLAIM_EXPIRY) == null + || jwt.getClaim(JwtConstants.CLAIM_ISSUED_AT) == null) { + LOG.warn("The IdToken is missing a required claim"); + throw new IllegalStateException("The IdToken is missing a required claim"); + } + + // The audience must match the client_id of this client + boolean match = false; + for (String audience : jwt.getClaims().getAudiences()) { + if (clientId.equals(audience)) { + match = true; + break; + } + } + if (!match) { + LOG.warn("The audience of the token does not match this client"); + throw new IllegalStateException("The audience of the token does not match this client"); + } + + JwtUtils.validateTokenClaims(jwt.getClaims(), 300, 0, false); + } + private Crypto getCrypto(String certificate) throws ProcessingException { if (certificate == null) { return null; @@ -292,16 +361,29 @@ public class TrustedIdpOIDCProtocolHandler implements TrustedIdpProtocolHandler } } - private SamlAssertionWrapper createSamlAssertion(Idp idp, JwtToken token, + protected SamlAssertionWrapper createSamlAssertion(Idp idp, TrustedIdp trustedIdp, JwtToken token, Date created, Date expires) throws Exception { SamlCallbackHandler callbackHandler = new SamlCallbackHandler(); - callbackHandler.setIssuer(idp.getServiceDisplayName()); + String issuer = idp.getServiceDisplayName(); + if (issuer == null) { + issuer = idp.getRealm(); + } + if (issuer != null) { + callbackHandler.setIssuer(issuer); + } // Subject - // TODO + String subjectName = getProperty(trustedIdp, SUBJECT_CLAIM); + if (subjectName == null || token.getClaim(subjectName) == null) { + subjectName = "preferred_username"; + if (subjectName == null || token.getClaim(subjectName) == null) { + subjectName = JwtConstants.CLAIM_SUBJECT; + } + } + SubjectBean subjectBean = - new SubjectBean((String)token.getClaim("preferred_username"), + new SubjectBean((String)token.getClaim(subjectName), SAML2Constants.NAMEID_FORMAT_UNSPECIFIED, SAML2Constants.CONF_BEARER); callbackHandler.setSubjectBean(subjectBean); @@ -329,6 +411,16 @@ public class TrustedIdpOIDCProtocolHandler implements TrustedIdpProtocolHandler return assertion; } + private String getProperty(TrustedIdp trustedIdp, String property) { + Map<String, String> parameters = trustedIdp.getParameters(); + + if (parameters != null && parameters.containsKey(property)) { + return parameters.get(property); + } + + return null; + } + private static class SamlCallbackHandler implements CallbackHandler { private ConditionsBean conditionsBean; private SubjectBean subjectBean; http://git-wip-us.apache.org/repos/asf/cxf-fediz/blob/9375d3e5/systests/federation/oidc/src/test/resources/entities-realma.xml ---------------------------------------------------------------------- diff --git a/systests/federation/oidc/src/test/resources/entities-realma.xml b/systests/federation/oidc/src/test/resources/entities-realma.xml index ab17601..54a2855 100644 --- a/systests/federation/oidc/src/test/resources/entities-realma.xml +++ b/systests/federation/oidc/src/test/resources/entities-realma.xml @@ -73,7 +73,6 @@ <property name="trustedIdps"> <util:list> <ref bean="trusted-idp-realmB" /> - <ref bean="trusted-idp-realmC" /> </util:list> </property> <property name="claimTypesOffered"> @@ -100,25 +99,8 @@ <property name="description" value="Realm B description" /> <property name="parameters"> <util:map> - </util:map> - </property> - </bean> - - <bean id="trusted-idp-realmC" - class="org.apache.cxf.fediz.service.idp.service.jpa.TrustedIdpEntity"> - <property name="realm" value="urn:org:apache:cxf:fediz:idp:realm-C" /> - <property name="cacheTokens" value="true" /> - <property name="url" value="https://localhost:${idp.samlsso.https.port}/idp/samlsso" /> - <property name="certificate" value="realmb.cert" /> - <property name="trustType" value="PEER_TRUST" /> - <property name="protocol" value="urn:oasis:names:tc:SAML:2.0:profiles:SSO:browser" /> - <property name="federationType" value="FEDERATE_IDENTITY" /> - <property name="name" value="Realm C" /> - <property name="description" value="SAML Web Profile - Response POST Binding" /> - <property name="parameters"> - <util:map> - <entry key="sign.request" value="true" /> - <entry key="support.deflate.encoding" value="true" /> + <entry key="client.id" value="consumer-id"/> + <entry key="token.endpoint" value="http://localhost:8080/auth/realms/realmb/protocol/openid-connect/token"/> </util:map> </property> </bean>
