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