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