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.security.Key;
024import java.security.PublicKey;
025import java.security.interfaces.RSAPublicKey;
026import java.security.interfaces.ECPublicKey;
027import javax.crypto.SecretKey;
028import javax.xml.parsers.DocumentBuilder;
029import javax.xml.parsers.DocumentBuilderFactory;
030import javax.xml.parsers.ParserConfigurationException;
031
032import com.nimbusds.oauth2.sdk.ParseException;
033import com.nimbusds.oauth2.sdk.id.Issuer;
034import net.jcip.annotations.ThreadSafe;
035import org.opensaml.DefaultBootstrap;
036import org.opensaml.saml2.core.Assertion;
037import org.opensaml.security.SAMLSignatureProfileValidator;
038import org.opensaml.xml.Configuration;
039import org.opensaml.xml.ConfigurationException;
040import org.opensaml.xml.XMLObject;
041import org.opensaml.xml.io.Unmarshaller;
042import org.opensaml.xml.io.UnmarshallerFactory;
043import org.opensaml.xml.io.UnmarshallingException;
044import org.opensaml.xml.security.credential.BasicCredential;
045import org.opensaml.xml.security.credential.UsageType;
046import org.opensaml.xml.signature.Signature;
047import org.opensaml.xml.signature.SignatureValidator;
048import org.opensaml.xml.validation.ValidationException;
049import org.w3c.dom.Document;
050import org.w3c.dom.Element;
051import org.xml.sax.InputSource;
052import org.xml.sax.SAXException;
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                        DefaultBootstrap.bootstrap();
073                } catch (ConfigurationException 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                documentBuilderFactory.setNamespaceAware(true);
117
118                XMLObject xmlObject;
119
120                try {
121                        DocumentBuilder docBuilder = documentBuilderFactory.newDocumentBuilder();
122
123                        Document document = docBuilder.parse(new InputSource(new ByteArrayInputStream(xml.getBytes("utf-8"))));
124                        Element element = document.getDocumentElement();
125
126                        UnmarshallerFactory unmarshallerFactory = Configuration.getUnmarshallerFactory();
127                        Unmarshaller unmarshaller = unmarshallerFactory.getUnmarshaller(element);
128                        xmlObject = unmarshaller.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 (ValidationException e) {
164                        throw new BadSAML2AssertionException("Invalid SAML 2.0 signature format: " + e.getMessage(), e);
165                }
166
167                BasicCredential credential = new BasicCredential();
168                if (key instanceof SecretKey) {
169                        credential.setSecretKey((SecretKey)key);
170                } else if (key instanceof PublicKey) {
171                        credential.setPublicKey((PublicKey)key);
172                        credential.setUsageType(UsageType.SIGNING);
173                } else {
174                        throw new BadSAML2AssertionException("Unsupported key type: " + key.getAlgorithm());
175                }
176
177                SignatureValidator signatureValidator = new SignatureValidator(credential);
178                try {
179                        signatureValidator.validate(signature);
180                } catch (ValidationException e) {
181                        throw new BadSAML2AssertionException("Bad SAML 2.0 signature: " + e.getMessage(), e);
182                }
183        }
184
185
186        /**
187         * Validates the specified SAML 2.0 assertion.
188         *
189         * @param assertion The SAML 2.0 assertion XML. Must not be
190         *                  {@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 not be
195         *                  {@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 {@code null}.
237         * @param key The key to verify the signature. Should be an
238         *            {@link SecretKey} instance for HMAC, {@link RSAPublicKey}
239         *            for RSA signatures or {@link ECPublicKey} for EC
240         *            signatures. Must not be {@code null}.
241         *
242         * @return The validated SAML 2.0 assertion.
243         *
244         * @throws BadSAML2AssertionException If the assertion is invalid.
245         */
246        public Assertion validate(final String xml,
247                                  final Issuer expectedIssuer,
248                                  final Key key)
249                throws BadSAML2AssertionException {
250
251                // Parse string to XML, then to SAML 2.0 assertion object
252                final Assertion assertion;
253
254                try {
255                        assertion = parse(xml);
256                } catch (ParseException e) {
257                        throw new BadSAML2AssertionException("Invalid SAML 2.0 assertion: " + e.getMessage(), e);
258                }
259
260                return validate(assertion, expectedIssuer, key);
261        }
262}