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