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