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 com.nimbusds.oauth2.sdk.ParseException;
034import com.nimbusds.oauth2.sdk.id.Issuer;
035import net.jcip.annotations.ThreadSafe;
036import org.opensaml.core.config.InitializationException;
037import org.opensaml.core.config.InitializationService;
038import org.opensaml.core.xml.XMLObject;
039import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport;
040import org.opensaml.core.xml.io.UnmarshallingException;
041import org.opensaml.saml.saml2.core.Assertion;
042import org.opensaml.saml.security.impl.SAMLSignatureProfileValidator;
043import org.opensaml.security.credential.BasicCredential;
044import org.opensaml.security.credential.UsageType;
045import org.opensaml.xmlsec.signature.Signature;
046import org.opensaml.xmlsec.signature.support.SignatureException;
047import org.opensaml.xmlsec.signature.support.SignatureValidator;
048import org.w3c.dom.Document;
049import org.w3c.dom.Element;
050import org.xml.sax.InputSource;
051import org.xml.sax.SAXException;
052
053
054/**
055 * SAML 2.0 assertion validator. Supports RSA signatures and HMAC. Provides
056 * static methods for each validation step for putting together tailored
057 * assertion validation strategies.
058 */
059@ThreadSafe
060public class SAML2AssertionValidator {
061
062
063        /**
064         * The SAML 2.0 assertion details verifier.
065         */
066        private final SAML2AssertionDetailsVerifier detailsVerifier;
067
068
069        static {
070                try {
071                        InitializationService.initialize();
072                } catch (InitializationException e) {
073                        throw new RuntimeException(e.getMessage(), e);
074                }
075        }
076
077
078        /**
079         * Creates a new SAML 2.0 assertion validator.
080         *
081         * @param detailsVerifier The SAML 2.0 assertion details verifier. Must
082         *                        not be {@code null}.
083         */
084        public SAML2AssertionValidator(final SAML2AssertionDetailsVerifier detailsVerifier) {
085                if (detailsVerifier == null) {
086                        throw new IllegalArgumentException("The SAML 2.0 assertion details verifier must not be null");
087                }
088                this.detailsVerifier = detailsVerifier;
089        }
090
091
092        /**
093         * Gets the SAML 2.0 assertion details verifier.
094         *
095         * @return The SAML 2.0 assertion details verifier.
096         */
097        public SAML2AssertionDetailsVerifier getDetailsVerifier() {
098                return detailsVerifier;
099        }
100
101
102        /**
103         * Parses a SAML 2.0 assertion from the specified XML string.
104         *
105         * @param xml The XML string. Must not be {@code null}.
106         *
107         * @return The SAML 2.0 assertion.
108         *
109         * @throws ParseException If parsing of the assertion failed.
110         */
111        public static Assertion parse(final String xml)
112                throws ParseException {
113
114                DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
115                documentBuilderFactory.setNamespaceAware(true);
116
117                XMLObject xmlObject;
118
119                try {
120                        DocumentBuilder docBuilder = documentBuilderFactory.newDocumentBuilder();
121
122                        Document document = docBuilder.parse(new InputSource(new ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8))));
123                        Element element = document.getDocumentElement();
124
125                        xmlObject = XMLObjectProviderRegistrySupport
126                                .getUnmarshallerFactory()
127                                .getUnmarshaller(element)
128                                .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 (SignatureException e) {
164                        throw new BadSAML2AssertionException("Invalid SAML 2.0 signature format: " + e.getMessage(), e);
165                }
166                
167                final BasicCredential credential;
168                if (key instanceof SecretKey) {
169                        credential = new BasicCredential((SecretKey)key);
170                } else if (key instanceof PublicKey) {
171                        credential = new BasicCredential((PublicKey)key);
172                        credential.setUsageType(UsageType.SIGNING);
173                } else {
174                        throw new BadSAML2AssertionException("Unsupported key type: " + key.getAlgorithm());
175                }
176
177                try {
178                        SignatureValidator.validate(signature, credential);
179                } catch (SignatureException e) {
180                        throw new BadSAML2AssertionException("Bad SAML 2.0 signature: " + e.getMessage(), e);
181                }
182        }
183
184
185        /**
186         * Validates the specified SAML 2.0 assertion.
187         *
188         * @param assertion      The SAML 2.0 assertion XML. Must not be
189         *                       {@code null}.
190         * @param expectedIssuer The expected issuer. Must not be {@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
195         *                       not be {@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
237         *                       {@code null}.
238         * @param expectedIssuer The expected issuer. Must not be {@code null}.
239         * @param key            The key to verify the signature. Should be an
240         *                       {@link SecretKey} instance for HMAC,
241         *                       {@link RSAPublicKey} for RSA signatures or
242         *                       {@link ECPublicKey} for EC signatures. Must
243         *                       not be {@code null}.
244         *
245         * @return The validated SAML 2.0 assertion.
246         *
247         * @throws BadSAML2AssertionException If the assertion is invalid.
248         */
249        public Assertion validate(final String xml,
250                                  final Issuer expectedIssuer,
251                                  final Key key)
252                throws BadSAML2AssertionException {
253
254                // Parse string to XML, then to SAML 2.0 assertion object
255                final Assertion assertion;
256
257                try {
258                        assertion = parse(xml);
259                } catch (ParseException e) {
260                        throw new BadSAML2AssertionException("Invalid SAML 2.0 assertion: " + e.getMessage(), e);
261                }
262
263                return validate(assertion, expectedIssuer, key);
264        }
265}