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 java.io.ByteArrayInputStream; 022import java.io.IOException; 023import java.nio.charset.StandardCharsets; 024import java.security.Key; 025import java.security.PublicKey; 026import java.security.interfaces.ECPublicKey; 027import java.security.interfaces.RSAPublicKey; 028import javax.crypto.SecretKey; 029import javax.xml.parsers.DocumentBuilder; 030import javax.xml.parsers.DocumentBuilderFactory; 031import javax.xml.parsers.ParserConfigurationException; 032 033import net.jcip.annotations.ThreadSafe; 034import org.opensaml.core.config.InitializationException; 035import org.opensaml.core.config.InitializationService; 036import org.opensaml.core.xml.XMLObject; 037import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport; 038import org.opensaml.core.xml.io.UnmarshallingException; 039import org.opensaml.saml.saml2.core.Assertion; 040import org.opensaml.saml.security.impl.SAMLSignatureProfileValidator; 041import org.opensaml.security.credential.BasicCredential; 042import org.opensaml.security.credential.UsageType; 043import org.opensaml.xmlsec.signature.Signature; 044import org.opensaml.xmlsec.signature.support.SignatureException; 045import org.opensaml.xmlsec.signature.support.SignatureValidator; 046import org.w3c.dom.Document; 047import org.w3c.dom.Element; 048import org.xml.sax.InputSource; 049import org.xml.sax.SAXException; 050 051import com.nimbusds.oauth2.sdk.ParseException; 052import com.nimbusds.oauth2.sdk.id.Issuer; 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 if (detailsVerifier == null) { 087 throw new IllegalArgumentException("The SAML 2.0 assertion details verifier must not be null"); 088 } 089 this.detailsVerifier = detailsVerifier; 090 } 091 092 093 /** 094 * Gets the SAML 2.0 assertion details verifier. 095 * 096 * @return The SAML 2.0 assertion details verifier. 097 */ 098 public SAML2AssertionDetailsVerifier getDetailsVerifier() { 099 return detailsVerifier; 100 } 101 102 103 /** 104 * Parses a SAML 2.0 assertion from the specified XML string. 105 * 106 * @param xml The XML string. Must not be {@code null}. 107 * 108 * @return The SAML 2.0 assertion. 109 * 110 * @throws ParseException If parsing of the assertion failed. 111 */ 112 public static Assertion parse(final String xml) 113 throws ParseException { 114 115 DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); 116 117 // Disable access to external entities in XML parsing 118 documentBuilderFactory.setAttribute("http://javax.xml.XMLConstants/property/accessExternalDTD", ""); 119 documentBuilderFactory.setAttribute("http://javax.xml.XMLConstants/property/accessExternalSchema", ""); 120 121 documentBuilderFactory.setNamespaceAware(true); 122 123 XMLObject xmlObject; 124 125 try { 126 DocumentBuilder docBuilder = documentBuilderFactory.newDocumentBuilder(); 127 128 Document document = docBuilder.parse(new InputSource(new ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8)))); 129 Element element = document.getDocumentElement(); 130 131 xmlObject = XMLObjectProviderRegistrySupport 132 .getUnmarshallerFactory() 133 .getUnmarshaller(element) 134 .unmarshall(element); 135 136 } catch (ParserConfigurationException | IOException | SAXException | UnmarshallingException e) { 137 throw new ParseException("SAML 2.0 assertion parsing failed: " + e.getMessage(), e); 138 } 139 140 if (! (xmlObject instanceof Assertion)) { 141 throw new ParseException("Top-level XML element not a SAML 2.0 assertion"); 142 } 143 144 return (Assertion)xmlObject; 145 } 146 147 148 /** 149 * Verifies the specified XML signature (HMAC, RSA or EC) with the 150 * provided key. 151 * 152 * @param signature The XML signature. Must not be {@code null}. 153 * @param key The key to verify the signature. Should be an 154 * {@link SecretKey} instance for HMAC, 155 * {@link RSAPublicKey} for RSA signatures or 156 * {@link ECPublicKey} for EC signatures. Must not be 157 * {@code null}. 158 * 159 * @throws BadSAML2AssertionException If the key type doesn't match the 160 * signature, or the signature is 161 * invalid. 162 */ 163 public static void verifySignature(final Signature signature, final Key key) 164 throws BadSAML2AssertionException { 165 166 SAMLSignatureProfileValidator profileValidator = new SAMLSignatureProfileValidator(); 167 try { 168 profileValidator.validate(signature); 169 } catch (SignatureException e) { 170 throw new BadSAML2AssertionException("Invalid SAML 2.0 signature format: " + e.getMessage(), e); 171 } 172 173 final BasicCredential credential; 174 if (key instanceof SecretKey) { 175 credential = new BasicCredential((SecretKey)key); 176 } else if (key instanceof PublicKey) { 177 credential = new BasicCredential((PublicKey)key); 178 credential.setUsageType(UsageType.SIGNING); 179 } else { 180 throw new BadSAML2AssertionException("Unsupported key type: " + key.getAlgorithm()); 181 } 182 183 try { 184 SignatureValidator.validate(signature, credential); 185 } catch (SignatureException e) { 186 throw new BadSAML2AssertionException("Bad SAML 2.0 signature: " + e.getMessage(), e); 187 } 188 } 189 190 191 /** 192 * Validates the specified SAML 2.0 assertion. 193 * 194 * @param assertion The SAML 2.0 assertion XML. Must not be 195 * {@code null}. 196 * @param expectedIssuer The expected issuer. Must not be {@code null}. 197 * @param key The key to verify the signature. Should be an 198 * {@link SecretKey} instance for HMAC, 199 * {@link RSAPublicKey} for RSA signatures or 200 * {@link ECPublicKey} for EC signatures. Must 201 * not be {@code null}. 202 * 203 * @return The validated SAML 2.0 assertion. 204 * 205 * @throws BadSAML2AssertionException If the assertion is invalid. 206 */ 207 public Assertion validate(final Assertion assertion, 208 final Issuer expectedIssuer, 209 final Key key) 210 throws BadSAML2AssertionException { 211 212 final SAML2AssertionDetails assertionDetails; 213 214 try { 215 assertionDetails = SAML2AssertionDetails.parse(assertion); 216 } catch (ParseException e) { 217 throw new BadSAML2AssertionException("Invalid SAML 2.0 assertion: " + e.getMessage(), e); 218 } 219 220 // Check the audience and time window details 221 detailsVerifier.verify(assertionDetails); 222 223 // Check the issuer 224 if (! expectedIssuer.equals(assertionDetails.getIssuer())) { 225 throw new BadSAML2AssertionException("Unexpected issuer: " + assertionDetails.getIssuer()); 226 } 227 228 if (! assertion.isSigned()) { 229 throw new BadSAML2AssertionException("Missing XML signature"); 230 } 231 232 // Verify the signature 233 verifySignature(assertion.getSignature(), key); 234 235 return assertion; // OK 236 } 237 238 239 /** 240 * Validates the specified SAML 2.0 assertion. 241 * 242 * @param xml The SAML 2.0 assertion XML. Must not be 243 * {@code null}. 244 * @param expectedIssuer The expected issuer. Must not be {@code null}. 245 * @param key The key to verify the signature. Should be an 246 * {@link SecretKey} instance for HMAC, 247 * {@link RSAPublicKey} for RSA signatures or 248 * {@link ECPublicKey} for EC signatures. Must 249 * not be {@code null}. 250 * 251 * @return The validated SAML 2.0 assertion. 252 * 253 * @throws BadSAML2AssertionException If the assertion is invalid. 254 */ 255 public Assertion validate(final String xml, 256 final Issuer expectedIssuer, 257 final Key key) 258 throws BadSAML2AssertionException { 259 260 // Parse string to XML, then to SAML 2.0 assertion object 261 final Assertion assertion; 262 263 try { 264 assertion = parse(xml); 265 } catch (ParseException e) { 266 throw new BadSAML2AssertionException("Invalid SAML 2.0 assertion: " + e.getMessage(), e); 267 } 268 269 return validate(assertion, expectedIssuer, key); 270 } 271}