001/* 002 * oauth2-oidc-sdk 003 * 004 * Copyright 2012-2016, Connect2id Ltd and contributors. 005 * 006 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use 007 * this file except in compliance with the License. You may obtain a copy of the 008 * License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, software distributed 013 * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 014 * CONDITIONS OF ANY KIND, either express or implied. See the License for the 015 * specific language governing permissions and limitations under the License. 016 */ 017 018package com.nimbusds.oauth2.sdk.assertions.saml2; 019 020 021import com.nimbusds.oauth2.sdk.ParseException; 022import com.nimbusds.oauth2.sdk.id.Issuer; 023import net.jcip.annotations.ThreadSafe; 024import org.opensaml.core.config.InitializationException; 025import org.opensaml.core.config.InitializationService; 026import org.opensaml.core.xml.XMLObject; 027import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport; 028import org.opensaml.core.xml.io.UnmarshallingException; 029import org.opensaml.saml.saml2.core.Assertion; 030import org.opensaml.saml.security.impl.SAMLSignatureProfileValidator; 031import org.opensaml.security.credential.BasicCredential; 032import org.opensaml.security.credential.UsageType; 033import org.opensaml.xmlsec.signature.Signature; 034import org.opensaml.xmlsec.signature.support.SignatureException; 035import org.opensaml.xmlsec.signature.support.SignatureValidator; 036import org.w3c.dom.Document; 037import org.w3c.dom.Element; 038import org.xml.sax.InputSource; 039import org.xml.sax.SAXException; 040 041import javax.crypto.SecretKey; 042import javax.xml.parsers.DocumentBuilder; 043import javax.xml.parsers.DocumentBuilderFactory; 044import javax.xml.parsers.ParserConfigurationException; 045import java.io.ByteArrayInputStream; 046import java.io.IOException; 047import java.nio.charset.StandardCharsets; 048import java.security.Key; 049import java.security.PublicKey; 050import java.security.interfaces.ECPublicKey; 051import java.security.interfaces.RSAPublicKey; 052import java.util.Objects; 053 054 055/** 056 * SAML 2.0 assertion validator. Supports RSA signatures and HMAC. Provides 057 * static methods for each validation step for putting together tailored 058 * assertion validation strategies. 059 */ 060@ThreadSafe 061public class SAML2AssertionValidator { 062 063 064 /** 065 * The SAML 2.0 assertion details verifier. 066 */ 067 private final SAML2AssertionDetailsVerifier detailsVerifier; 068 069 070 static { 071 try { 072 InitializationService.initialize(); 073 } catch (InitializationException e) { 074 throw new RuntimeException(e.getMessage(), e); 075 } 076 } 077 078 079 /** 080 * Creates a new SAML 2.0 assertion validator. 081 * 082 * @param detailsVerifier The SAML 2.0 assertion details verifier. Must 083 * not be {@code null}. 084 */ 085 public SAML2AssertionValidator(final SAML2AssertionDetailsVerifier detailsVerifier) { 086 this.detailsVerifier = Objects.requireNonNull(detailsVerifier); 087 } 088 089 090 /** 091 * Gets the SAML 2.0 assertion details verifier. 092 * 093 * @return The SAML 2.0 assertion details verifier. 094 */ 095 public SAML2AssertionDetailsVerifier getDetailsVerifier() { 096 return detailsVerifier; 097 } 098 099 100 /** 101 * Parses a SAML 2.0 assertion from the specified XML string. 102 * 103 * @param xml The XML string. Must not be {@code null}. 104 * 105 * @return The SAML 2.0 assertion. 106 * 107 * @throws ParseException If parsing of the assertion failed. 108 */ 109 public static Assertion parse(final String xml) 110 throws ParseException { 111 112 DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); 113 114 // Disable access to external entities in XML parsing 115 documentBuilderFactory.setAttribute("http://javax.xml.XMLConstants/property/accessExternalDTD", ""); 116 documentBuilderFactory.setAttribute("http://javax.xml.XMLConstants/property/accessExternalSchema", ""); 117 118 documentBuilderFactory.setNamespaceAware(true); 119 120 XMLObject xmlObject; 121 122 try { 123 DocumentBuilder docBuilder = documentBuilderFactory.newDocumentBuilder(); 124 125 Document document = docBuilder.parse(new InputSource(new ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8)))); 126 Element element = document.getDocumentElement(); 127 128 xmlObject = XMLObjectProviderRegistrySupport 129 .getUnmarshallerFactory() 130 .getUnmarshaller(element) 131 .unmarshall(element); 132 133 } catch (ParserConfigurationException | IOException | SAXException | UnmarshallingException e) { 134 throw new ParseException("SAML 2.0 assertion parsing failed: " + e.getMessage(), e); 135 } 136 137 if (! (xmlObject instanceof Assertion)) { 138 throw new ParseException("Top-level XML element not a SAML 2.0 assertion"); 139 } 140 141 return (Assertion)xmlObject; 142 } 143 144 145 /** 146 * Verifies the specified XML signature (HMAC, RSA or EC) with the 147 * provided key. 148 * 149 * @param signature The XML signature. Must not be {@code null}. 150 * @param key The key to verify the signature. Should be an 151 * {@link SecretKey} instance for HMAC, 152 * {@link RSAPublicKey} for RSA signatures or 153 * {@link ECPublicKey} for EC signatures. Must not be 154 * {@code null}. 155 * 156 * @throws BadSAML2AssertionException If the key type doesn't match the 157 * signature, or the signature is 158 * invalid. 159 */ 160 public static void verifySignature(final Signature signature, final Key key) 161 throws BadSAML2AssertionException { 162 163 SAMLSignatureProfileValidator profileValidator = new SAMLSignatureProfileValidator(); 164 try { 165 profileValidator.validate(signature); 166 } catch (SignatureException e) { 167 throw new BadSAML2AssertionException("Invalid SAML 2.0 signature format: " + e.getMessage(), e); 168 } 169 170 final BasicCredential credential; 171 if (key instanceof SecretKey) { 172 credential = new BasicCredential((SecretKey)key); 173 } else if (key instanceof PublicKey) { 174 credential = new BasicCredential((PublicKey)key); 175 credential.setUsageType(UsageType.SIGNING); 176 } else { 177 throw new BadSAML2AssertionException("Unsupported key type: " + key.getAlgorithm()); 178 } 179 180 try { 181 SignatureValidator.validate(signature, credential); 182 } catch (SignatureException e) { 183 throw new BadSAML2AssertionException("Bad SAML 2.0 signature: " + e.getMessage(), e); 184 } 185 } 186 187 188 /** 189 * Validates the specified SAML 2.0 assertion. 190 * 191 * @param assertion The SAML 2.0 assertion XML. Must not be 192 * {@code null}. 193 * @param expectedIssuer The expected issuer. Must not be {@code null}. 194 * @param key The key to verify the signature. Should be an 195 * {@link SecretKey} instance for HMAC, 196 * {@link RSAPublicKey} for RSA signatures or 197 * {@link ECPublicKey} for EC signatures. Must 198 * not be {@code null}. 199 * 200 * @return The validated SAML 2.0 assertion. 201 * 202 * @throws BadSAML2AssertionException If the assertion is invalid. 203 */ 204 public Assertion validate(final Assertion assertion, 205 final Issuer expectedIssuer, 206 final Key key) 207 throws BadSAML2AssertionException { 208 209 final SAML2AssertionDetails assertionDetails; 210 211 try { 212 assertionDetails = SAML2AssertionDetails.parse(assertion); 213 } catch (ParseException e) { 214 throw new BadSAML2AssertionException("Invalid SAML 2.0 assertion: " + e.getMessage(), e); 215 } 216 217 // Check the audience and time window details 218 detailsVerifier.verify(assertionDetails); 219 220 // Check the issuer 221 if (! expectedIssuer.equals(assertionDetails.getIssuer())) { 222 throw new BadSAML2AssertionException("Unexpected issuer: " + assertionDetails.getIssuer()); 223 } 224 225 if (! assertion.isSigned()) { 226 throw new BadSAML2AssertionException("Missing XML signature"); 227 } 228 229 // Verify the signature 230 verifySignature(assertion.getSignature(), key); 231 232 return assertion; // OK 233 } 234 235 236 /** 237 * Validates the specified SAML 2.0 assertion. 238 * 239 * @param xml The SAML 2.0 assertion XML. Must not be 240 * {@code null}. 241 * @param expectedIssuer The expected issuer. Must not be {@code null}. 242 * @param key The key to verify the signature. Should be an 243 * {@link SecretKey} instance for HMAC, 244 * {@link RSAPublicKey} for RSA signatures or 245 * {@link ECPublicKey} for EC signatures. Must 246 * not be {@code null}. 247 * 248 * @return The validated SAML 2.0 assertion. 249 * 250 * @throws BadSAML2AssertionException If the assertion is invalid. 251 */ 252 public Assertion validate(final String xml, 253 final Issuer expectedIssuer, 254 final Key key) 255 throws BadSAML2AssertionException { 256 257 // Parse string to XML, then to SAML 2.0 assertion object 258 final Assertion assertion; 259 260 try { 261 assertion = parse(xml); 262 } catch (ParseException e) { 263 throw new BadSAML2AssertionException("Invalid SAML 2.0 assertion: " + e.getMessage(), e); 264 } 265 266 return validate(assertion, expectedIssuer, key); 267 } 268}