001package com.nimbusds.oauth2.sdk.assertions.saml2;
002
003
004import java.io.ByteArrayInputStream;
005import java.io.IOException;
006import java.security.interfaces.RSAPublicKey;
007import javax.crypto.SecretKey;
008import javax.xml.parsers.DocumentBuilder;
009import javax.xml.parsers.DocumentBuilderFactory;
010import javax.xml.parsers.ParserConfigurationException;
011
012import com.nimbusds.oauth2.sdk.ParseException;
013import com.nimbusds.oauth2.sdk.id.Issuer;
014import net.jcip.annotations.ThreadSafe;
015import org.opensaml.DefaultBootstrap;
016import org.opensaml.saml2.core.Assertion;
017import org.opensaml.security.SAMLSignatureProfileValidator;
018import org.opensaml.xml.Configuration;
019import org.opensaml.xml.ConfigurationException;
020import org.opensaml.xml.XMLObject;
021import org.opensaml.xml.io.Unmarshaller;
022import org.opensaml.xml.io.UnmarshallerFactory;
023import org.opensaml.xml.io.UnmarshallingException;
024import org.opensaml.xml.security.credential.BasicCredential;
025import org.opensaml.xml.security.credential.UsageType;
026import org.opensaml.xml.signature.Signature;
027import org.opensaml.xml.signature.SignatureValidator;
028import org.opensaml.xml.validation.ValidationException;
029import org.w3c.dom.Document;
030import org.w3c.dom.Element;
031import org.xml.sax.InputSource;
032import org.xml.sax.SAXException;
033
034
035/**
036 * SAML 2.0 assertion validator. Supports RSA signatures and HMAC. Provides
037 * static methods for each validation step for putting together tailored
038 * assertion validation strategies.
039 */
040@ThreadSafe
041public class SAML2AssertionValidator {
042
043
044        /**
045         * The SAML 2.0 assertion details verifier.
046         */
047        private final SAML2AssertionDetailsVerifier detailsVerifier;
048
049
050        static {
051                try {
052                        DefaultBootstrap.bootstrap();
053                } catch (ConfigurationException e) {
054                        throw new RuntimeException(e.getMessage(), e);
055                }
056        }
057
058
059        /**
060         * Creates a new SAML 2.0 assertion validator.
061         *
062         * @param detailsVerifier The SAML 2.0 assertion details verifier. Must
063         *                        not be {@code null}.
064         */
065        public SAML2AssertionValidator(final SAML2AssertionDetailsVerifier detailsVerifier) {
066                if (detailsVerifier == null) {
067                        throw new IllegalArgumentException("The SAML 2.0 assertion details verifier must not be null");
068                }
069                this.detailsVerifier = detailsVerifier;
070        }
071
072
073        /**
074         * Gets the SAML 2.0 assertion details verifier.
075         *
076         * @return The SAML 2.0 assertion details verifier.
077         */
078        public SAML2AssertionDetailsVerifier getDetailsVerifier() {
079                return detailsVerifier;
080        }
081
082
083        /**
084         * Parses a SAML 2.0 assertion from the specified XML string.
085         *
086         * @param xml The XML string. Must not be {@code null}.
087         *
088         * @return The SAML 2.0 assertion.
089         *
090         * @throws ParseException If parsing of the assertion failed.
091         */
092        public static Assertion parse(final String xml)
093                throws ParseException {
094
095                DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
096                documentBuilderFactory.setNamespaceAware(true);
097
098                XMLObject xmlObject;
099
100                try {
101                        DocumentBuilder docBuilder = documentBuilderFactory.newDocumentBuilder();
102
103                        Document document = docBuilder.parse(new InputSource(new ByteArrayInputStream(xml.getBytes("utf-8"))));
104                        Element element = document.getDocumentElement();
105
106                        UnmarshallerFactory unmarshallerFactory = Configuration.getUnmarshallerFactory();
107                        Unmarshaller unmarshaller = unmarshallerFactory.getUnmarshaller(element);
108                        xmlObject = unmarshaller.unmarshall(element);
109
110                } catch (ParserConfigurationException | IOException | SAXException | UnmarshallingException e) {
111                        throw new ParseException("SAML 2.0 assertion parsing failed: " + e.getMessage(), e);
112                }
113
114                if (! (xmlObject instanceof Assertion)) {
115                        throw new ParseException("Top-level XML element not a SAML 2.0 assertion");
116                }
117
118                return (Assertion)xmlObject;
119        }
120
121
122        /**
123         * Verifies the specified XML signature.
124         *
125         * @param signature The XML signature. Must not be {@code null}.
126         * @param publicKey The public RSA key to verify the signature. Must
127         *                  not be {@code null}.
128         *
129         * @throws BadSAML2AssertionException If the signature is invalid.
130         */
131        public static void verifySignature(final Signature signature, final RSAPublicKey publicKey)
132                throws BadSAML2AssertionException {
133
134                SAMLSignatureProfileValidator profileValidator = new SAMLSignatureProfileValidator();
135                try {
136                        profileValidator.validate(signature);
137                } catch (ValidationException e) {
138                        throw new BadSAML2AssertionException("Invalid SAML 2.0 signature format: " + e.getMessage(), e);
139                }
140
141                BasicCredential publicCredential = new BasicCredential();
142                publicCredential.setPublicKey(publicKey);
143                publicCredential.setUsageType(UsageType.SIGNING);
144                SignatureValidator signatureValidator = new SignatureValidator(publicCredential);
145
146                try {
147                        signatureValidator.validate(signature);
148                } catch (ValidationException e) {
149                        throw new BadSAML2AssertionException("Bad SAML 2.0 signature: " + e.getMessage(), e);
150                }
151        }
152
153
154        /**
155         * Verifies the specified XML HMAC.
156         *
157         * @param hmac    The XML HMAC. Must not be {@code null}.
158         * @param hmacKey The HMAC key. Must not be {@code null}.
159         *
160         * @throws BadSAML2AssertionException If the signature is invalid.
161         */
162        public static void verifyHMAC(final Signature hmac, final SecretKey hmacKey)
163                throws BadSAML2AssertionException {
164
165                SAMLSignatureProfileValidator profileValidator = new SAMLSignatureProfileValidator();
166                try {
167                        profileValidator.validate(hmac);
168                } catch (ValidationException e) {
169                        throw new BadSAML2AssertionException("Invalid SAML 2.0 signature format: " + e.getMessage(), e);
170                }
171
172                BasicCredential publicCredential = new BasicCredential();
173                publicCredential.setSecretKey(hmacKey);
174                SignatureValidator signatureValidator = new SignatureValidator(publicCredential);
175
176                try {
177                        signatureValidator.validate(hmac);
178                } catch (ValidationException e) {
179                        throw new BadSAML2AssertionException("Bad SAML 2.0 HMAC: " + e.getMessage(), e);
180                }
181        }
182
183
184        /**
185         * Validates the specified RSA-signed SAML 2.0 assertion.
186         *
187         * @param xml          The SAML 2.0 assertion XML. Must not be
188         *                     {@code null}.
189         * @param rsaPublicKey The public RSA key to validate the signature.
190         *                     Must not be {@code null}.
191         *
192         * @return The validated SAML 2.0 assertion.
193         *
194         * @throws BadSAML2AssertionException If the assertion is invalid.
195         */
196        public Assertion validate(final String xml,
197                                  final Issuer expectedIssuer,
198                                  final RSAPublicKey rsaPublicKey)
199                throws BadSAML2AssertionException {
200
201                // Parse string to XML, then to SAML 2.0 assertion object
202                final Assertion assertion;
203                final SAML2AssertionDetails assertionDetails;
204
205                try {
206                        assertion = parse(xml);
207                        assertionDetails = SAML2AssertionDetails.parse(assertion);
208                } catch (ParseException e) {
209                        throw new BadSAML2AssertionException("Invalid SAML 2.0 assertion: " + e.getMessage(), e);
210                }
211
212                // Check the audience and time window details
213                detailsVerifier.verify(assertionDetails);
214
215                // Check the issuer
216                if (! expectedIssuer.equals(assertionDetails.getIssuer())) {
217                        throw new BadSAML2AssertionException("Unexpected issuer: " + assertionDetails.getIssuer());
218                }
219
220                // Verify the signature
221                verifySignature(assertion.getSignature(), rsaPublicKey);
222
223                return assertion; // OK
224        }
225
226
227        /**
228         * Validates the specified HMAC-protected SAML 2.0 assertion.
229         *
230         * @param xml     The SAML 2.0 assertion XML. Must not be {@code null}.
231         * @param hmacKey The HMAC key. Must not be {@code null}.
232         *
233         * @return The validated SAML 2.0 assertion.
234         *
235         * @throws BadSAML2AssertionException If the assertion is invalid.
236         */
237        public Assertion validate(final String xml,
238                                  final Issuer expectedIssuer,
239                                  final SecretKey hmacKey)
240                throws BadSAML2AssertionException {
241
242                // Parse string to XML, then to SAML 2.0 assertion object
243                final Assertion assertion;
244                final SAML2AssertionDetails assertionDetails;
245
246                try {
247                        assertion = parse(xml);
248                        assertionDetails = SAML2AssertionDetails.parse(assertion);
249                } catch (ParseException e) {
250                        throw new BadSAML2AssertionException("Invalid SAML 2.0 assertion: " + e.getMessage(), e);
251                }
252
253                // Check the audience and time window details
254                detailsVerifier.verify(assertionDetails);
255
256                // Check the issuer
257                if (! expectedIssuer.equals(assertionDetails.getIssuer())) {
258                        throw new BadSAML2AssertionException("Unexpected issuer: " + assertionDetails.getIssuer());
259                }
260
261                // Verify the HMAC
262                verifyHMAC(assertion.getSignature(), hmacKey);
263
264                return assertion; // OK
265        }
266}