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.RSAPublicKey; 026import java.security.interfaces.ECPublicKey; 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.DefaultBootstrap; 036import org.opensaml.saml2.core.Assertion; 037import org.opensaml.security.SAMLSignatureProfileValidator; 038import org.opensaml.xml.Configuration; 039import org.opensaml.xml.ConfigurationException; 040import org.opensaml.xml.XMLObject; 041import org.opensaml.xml.io.Unmarshaller; 042import org.opensaml.xml.io.UnmarshallerFactory; 043import org.opensaml.xml.io.UnmarshallingException; 044import org.opensaml.xml.security.credential.BasicCredential; 045import org.opensaml.xml.security.credential.UsageType; 046import org.opensaml.xml.signature.Signature; 047import org.opensaml.xml.signature.SignatureValidator; 048import org.opensaml.xml.validation.ValidationException; 049import org.w3c.dom.Document; 050import org.w3c.dom.Element; 051import org.xml.sax.InputSource; 052import org.xml.sax.SAXException; 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 DefaultBootstrap.bootstrap(); 073 } catch (ConfigurationException 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 documentBuilderFactory.setNamespaceAware(true); 117 118 XMLObject xmlObject; 119 120 try { 121 DocumentBuilder docBuilder = documentBuilderFactory.newDocumentBuilder(); 122 123 Document document = docBuilder.parse(new InputSource(new ByteArrayInputStream(xml.getBytes("utf-8")))); 124 Element element = document.getDocumentElement(); 125 126 UnmarshallerFactory unmarshallerFactory = Configuration.getUnmarshallerFactory(); 127 Unmarshaller unmarshaller = unmarshallerFactory.getUnmarshaller(element); 128 xmlObject = unmarshaller.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 (ValidationException e) { 164 throw new BadSAML2AssertionException("Invalid SAML 2.0 signature format: " + e.getMessage(), e); 165 } 166 167 BasicCredential credential = new BasicCredential(); 168 if (key instanceof SecretKey) { 169 credential.setSecretKey((SecretKey)key); 170 } else if (key instanceof PublicKey) { 171 credential.setPublicKey((PublicKey)key); 172 credential.setUsageType(UsageType.SIGNING); 173 } else { 174 throw new BadSAML2AssertionException("Unsupported key type: " + key.getAlgorithm()); 175 } 176 177 SignatureValidator signatureValidator = new SignatureValidator(credential); 178 try { 179 signatureValidator.validate(signature); 180 } catch (ValidationException e) { 181 throw new BadSAML2AssertionException("Bad SAML 2.0 signature: " + e.getMessage(), e); 182 } 183 } 184 185 186 /** 187 * Validates the specified SAML 2.0 assertion. 188 * 189 * @param assertion The SAML 2.0 assertion XML. Must not be 190 * {@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 not be 195 * {@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 {@code null}. 237 * @param key The key to verify the signature. Should be an 238 * {@link SecretKey} instance for HMAC, {@link RSAPublicKey} 239 * for RSA signatures or {@link ECPublicKey} for EC 240 * signatures. Must not be {@code null}. 241 * 242 * @return The validated SAML 2.0 assertion. 243 * 244 * @throws BadSAML2AssertionException If the assertion is invalid. 245 */ 246 public Assertion validate(final String xml, 247 final Issuer expectedIssuer, 248 final Key key) 249 throws BadSAML2AssertionException { 250 251 // Parse string to XML, then to SAML 2.0 assertion object 252 final Assertion assertion; 253 254 try { 255 assertion = parse(xml); 256 } catch (ParseException e) { 257 throw new BadSAML2AssertionException("Invalid SAML 2.0 assertion: " + e.getMessage(), e); 258 } 259 260 return validate(assertion, expectedIssuer, key); 261 } 262}