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