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