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 net.jcip.annotations.ThreadSafe;
034import org.opensaml.core.config.InitializationException;
035import org.opensaml.core.config.InitializationService;
036import org.opensaml.core.xml.XMLObject;
037import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport;
038import org.opensaml.core.xml.io.UnmarshallingException;
039import org.opensaml.saml.saml2.core.Assertion;
040import org.opensaml.saml.security.impl.SAMLSignatureProfileValidator;
041import org.opensaml.security.credential.BasicCredential;
042import org.opensaml.security.credential.UsageType;
043import org.opensaml.xmlsec.signature.Signature;
044import org.opensaml.xmlsec.signature.support.SignatureException;
045import org.opensaml.xmlsec.signature.support.SignatureValidator;
046import org.w3c.dom.Document;
047import org.w3c.dom.Element;
048import org.xml.sax.InputSource;
049import org.xml.sax.SAXException;
050
051import com.nimbusds.oauth2.sdk.ParseException;
052import com.nimbusds.oauth2.sdk.id.Issuer;
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                        InitializationService.initialize();
073                } catch (InitializationException 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                
117                // Disable access to external entities in XML parsing
118                documentBuilderFactory.setAttribute("http://javax.xml.XMLConstants/property/accessExternalDTD", "");
119                documentBuilderFactory.setAttribute("http://javax.xml.XMLConstants/property/accessExternalSchema", "");
120                
121                documentBuilderFactory.setNamespaceAware(true);
122
123                XMLObject xmlObject;
124
125                try {
126                        DocumentBuilder docBuilder = documentBuilderFactory.newDocumentBuilder();
127
128                        Document document = docBuilder.parse(new InputSource(new ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8))));
129                        Element element = document.getDocumentElement();
130
131                        xmlObject = XMLObjectProviderRegistrySupport
132                                .getUnmarshallerFactory()
133                                .getUnmarshaller(element)
134                                .unmarshall(element);
135
136                } catch (ParserConfigurationException | IOException | SAXException | UnmarshallingException e) {
137                        throw new ParseException("SAML 2.0 assertion parsing failed: " + e.getMessage(), e);
138                }
139
140                if (! (xmlObject instanceof Assertion)) {
141                        throw new ParseException("Top-level XML element not a SAML 2.0 assertion");
142                }
143
144                return (Assertion)xmlObject;
145        }
146
147
148        /**
149         * Verifies the specified XML signature (HMAC, RSA or EC) with the
150         * provided key.
151         *
152         * @param signature The XML signature. Must not be {@code null}.
153         * @param key       The key to verify the signature. Should be an
154         *                  {@link SecretKey} instance for HMAC,
155         *                  {@link RSAPublicKey} for RSA signatures or
156         *                  {@link ECPublicKey} for EC signatures. Must not be
157         *                  {@code null}.
158         *
159         * @throws BadSAML2AssertionException If the key type doesn't match the
160         *                                    signature, or the signature is
161         *                                    invalid.
162         */
163        public static void verifySignature(final Signature signature, final Key key)
164                throws BadSAML2AssertionException {
165
166                SAMLSignatureProfileValidator profileValidator = new SAMLSignatureProfileValidator();
167                try {
168                        profileValidator.validate(signature);
169                } catch (SignatureException e) {
170                        throw new BadSAML2AssertionException("Invalid SAML 2.0 signature format: " + e.getMessage(), e);
171                }
172                
173                final BasicCredential credential;
174                if (key instanceof SecretKey) {
175                        credential = new BasicCredential((SecretKey)key);
176                } else if (key instanceof PublicKey) {
177                        credential = new BasicCredential((PublicKey)key);
178                        credential.setUsageType(UsageType.SIGNING);
179                } else {
180                        throw new BadSAML2AssertionException("Unsupported key type: " + key.getAlgorithm());
181                }
182
183                try {
184                        SignatureValidator.validate(signature, credential);
185                } catch (SignatureException e) {
186                        throw new BadSAML2AssertionException("Bad SAML 2.0 signature: " + e.getMessage(), e);
187                }
188        }
189
190
191        /**
192         * Validates the specified SAML 2.0 assertion.
193         *
194         * @param assertion      The SAML 2.0 assertion XML. Must not be
195         *                       {@code null}.
196         * @param expectedIssuer The expected issuer. Must not be {@code null}.
197         * @param key            The key to verify the signature. Should be an
198         *                       {@link SecretKey} instance for HMAC,
199         *                       {@link RSAPublicKey} for RSA signatures or
200         *                       {@link ECPublicKey} for EC signatures. Must
201         *                       not be {@code null}.
202         *
203         * @return The validated SAML 2.0 assertion.
204         *
205         * @throws BadSAML2AssertionException If the assertion is invalid.
206         */
207        public Assertion validate(final Assertion assertion,
208                                  final Issuer expectedIssuer,
209                                  final Key key)
210                throws BadSAML2AssertionException {
211
212                final SAML2AssertionDetails assertionDetails;
213
214                try {
215                        assertionDetails = SAML2AssertionDetails.parse(assertion);
216                } catch (ParseException e) {
217                        throw new BadSAML2AssertionException("Invalid SAML 2.0 assertion: " + e.getMessage(), e);
218                }
219
220                // Check the audience and time window details
221                detailsVerifier.verify(assertionDetails);
222
223                // Check the issuer
224                if (! expectedIssuer.equals(assertionDetails.getIssuer())) {
225                        throw new BadSAML2AssertionException("Unexpected issuer: " + assertionDetails.getIssuer());
226                }
227
228                if (! assertion.isSigned()) {
229                        throw new BadSAML2AssertionException("Missing XML signature");
230                }
231
232                // Verify the signature
233                verifySignature(assertion.getSignature(), key);
234
235                return assertion; // OK
236        }
237
238
239        /**
240         * Validates the specified SAML 2.0 assertion.
241         *
242         * @param xml            The SAML 2.0 assertion XML. Must not be
243         *                       {@code null}.
244         * @param expectedIssuer The expected issuer. Must not be {@code null}.
245         * @param key            The key to verify the signature. Should be an
246         *                       {@link SecretKey} instance for HMAC,
247         *                       {@link RSAPublicKey} for RSA signatures or
248         *                       {@link ECPublicKey} for EC signatures. Must
249         *                       not be {@code null}.
250         *
251         * @return The validated SAML 2.0 assertion.
252         *
253         * @throws BadSAML2AssertionException If the assertion is invalid.
254         */
255        public Assertion validate(final String xml,
256                                  final Issuer expectedIssuer,
257                                  final Key key)
258                throws BadSAML2AssertionException {
259
260                // Parse string to XML, then to SAML 2.0 assertion object
261                final Assertion assertion;
262
263                try {
264                        assertion = parse(xml);
265                } catch (ParseException e) {
266                        throw new BadSAML2AssertionException("Invalid SAML 2.0 assertion: " + e.getMessage(), e);
267                }
268
269                return validate(assertion, expectedIssuer, key);
270        }
271}