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.net.InetAddress;
022import java.net.UnknownHostException;
023import java.util.*;
024
025import static com.nimbusds.oauth2.sdk.assertions.saml2.SAML2Utils.buildSAMLObject;
026
027import com.nimbusds.oauth2.sdk.ParseException;
028import com.nimbusds.oauth2.sdk.SerializeException;
029import com.nimbusds.oauth2.sdk.assertions.AssertionDetails;
030import com.nimbusds.oauth2.sdk.id.Audience;
031import com.nimbusds.oauth2.sdk.id.Identifier;
032import com.nimbusds.oauth2.sdk.id.Issuer;
033import com.nimbusds.oauth2.sdk.id.Subject;
034import com.nimbusds.oauth2.sdk.util.CollectionUtils;
035import com.nimbusds.oauth2.sdk.util.MapUtils;
036import com.nimbusds.openid.connect.sdk.claims.ACR;
037import net.jcip.annotations.Immutable;
038import org.joda.time.DateTime;
039import org.opensaml.core.config.InitializationException;
040import org.opensaml.core.config.InitializationService;
041import org.opensaml.core.xml.XMLObject;
042import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport;
043import org.opensaml.core.xml.schema.XSString;
044import org.opensaml.core.xml.schema.impl.XSStringBuilder;
045import org.opensaml.saml.saml2.core.*;
046
047
048/**
049 * SAML 2.0 bearer assertion details for OAuth 2.0 client authentication and
050 * authorisation grants.
051 *
052 * <p>Used for {@link com.nimbusds.oauth2.sdk.SAML2BearerGrant SAML 2.0 bearer
053 * assertion grants}.
054 *
055 * <p>Example SAML 2.0 assertion:
056 *
057 * <pre>
058 * &lt;Assertion IssueInstant="2010-10-01T20:07:34.619Z"
059 *            ID="ef1xsbZxPV2oqjd7HTLRLIBlBb7"
060 *            Version="2.0"
061 *            xmlns="urn:oasis:names:tc:SAML:2.0:assertion"&gt;
062 *     &lt;Issuer&gt;https://saml-idp.example.com&lt;/Issuer&gt;
063 *     &lt;ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#"&gt;
064 *         [...omitted for brevity...]
065 *     &lt;/ds:Signature&gt;
066 *     &lt;Subject&gt;
067 *         &lt;NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"&gt;
068 *             [email protected]
069 *         &lt;/NameID&gt;
070 *         &lt;SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer"&gt;
071 *             &lt;SubjectConfirmationData NotOnOrAfter="2010-10-01T20:12:34.619Z"
072 *                                      Recipient="https://authz.example.net/token.oauth2"/&gt;
073 *         &lt;/SubjectConfirmation&gt;
074 *     &lt;/Subject&gt;
075 *     &lt;Conditions&gt;
076 *         &lt;AudienceRestriction&gt;
077 *             &lt;Audience&gt;https://saml-sp.example.net&lt;/Audience&gt;
078 *         &lt;/AudienceRestriction&gt;
079 *     &lt;/Conditions&gt;
080 *     &lt;AuthnStatement AuthnInstant="2010-10-01T20:07:34.371Z"&gt;
081 *         &lt;AuthnContext&gt;
082 *             &lt;AuthnContextClassRef&gt;urn:oasis:names:tc:SAML:2.0:ac:classes:X509&lt;/AuthnContextClassRef&gt;
083 *         &lt;/AuthnContext&gt;
084 *     &lt;/AuthnStatement&gt;
085 * &lt;/Assertion&gt;
086 * </pre>
087 *
088 * <p>Related specifications:
089 *
090 * <ul>
091 *     <li>Security Assertion Markup Language (SAML) 2.0 Profile for OAuth 2.0
092 *         Client Authentication and Authorization Grants (RFC 7522), section
093 *         3.
094 * </ul>
095 */
096@Immutable
097public class SAML2AssertionDetails extends AssertionDetails {
098
099
100        /**
101         * The subject format (optional).
102         */
103        private final String subjectFormat;
104
105
106        /**
107         * The subject authentication time (optional).
108         */
109        private final Date subjectAuthTime;
110
111
112        /**
113         * The subject Authentication Context Class Reference (ACR) (optional).
114         */
115        private final ACR subjectACR;
116        
117
118        /**
119         * The time before which this assertion must not be accepted for
120         * processing (optional).
121         */
122        private final Date nbf;
123
124
125        /**
126         * The client IPv4 or IPv6 address (optional).
127         */
128        private final InetAddress clientAddress;
129
130
131        /**
132         * The attribute statement (optional).
133         */
134        private final Map<String,List<String>> attrStatement;
135
136
137        /**
138         * Creates a new SAML 2.0 bearer assertion details instance. The
139         * expiration time is set to five minutes from the current system time.
140         * Generates a default identifier for the assertion. The issue time is
141         * set to the current system time.
142         *
143         * @param issuer   The issuer. Must not be {@code null}.
144         * @param subject  The subject. Must not be {@code null}.
145         * @param audience The audience, typically the URI of the authorisation
146         *                 server's token endpoint. Must not be {@code null}.
147         */
148        public SAML2AssertionDetails(final Issuer issuer,
149                                     final Subject subject,
150                                     final Audience audience) {
151
152                this(issuer, subject, null, null, null, audience.toSingleAudienceList(),
153                        new Date(new Date().getTime() + 5*60*1000L), null, new Date(),
154                        new Identifier(), null, null);
155        }
156
157
158        /**
159         * Creates a new SAML 2.0 bearer assertion details instance.
160         *
161         * @param issuer          The issuer. Must not be {@code null}.
162         * @param subject         The subject. Must not be {@code null}.
163         * @param subjectFormat   The subject format, {@code null} if not
164         *                        specified.
165         * @param subjectAuthTime The subject authentication time, {@code null}
166         *                        if not specified.
167         * @param subjectACR      The subject Authentication Context Class
168         *                        Reference (ACR), {@code null} if not
169         *                        specified.
170         * @param audience        The audience, typically including the URI of the
171         *                        authorisation server's token endpoint. Must not be
172         *                        {@code null}.
173         * @param exp             The expiration time. Must not be {@code null}.
174         * @param nbf             The time before which the assertion must not
175         *                        be accepted for processing, {@code null} if
176         *                        not specified.
177         * @param iat             The time at which the assertion was issued.
178         *                        Must not be {@code null}.
179         * @param id              Unique identifier for the assertion. Must not
180         *                        be {@code null}.
181         * @param clientAddress   The client address, {@code null} if not
182         *                        specified.
183         * @param attrStatement   The attribute statement (in simplified form),
184         *                        {@code null} if not specified.
185         */
186        public SAML2AssertionDetails(final Issuer issuer,
187                                     final Subject subject,
188                                     final String subjectFormat,
189                                     final Date subjectAuthTime,
190                                     final ACR subjectACR,
191                                     final List<Audience> audience,
192                                     final Date exp,
193                                     final Date nbf,
194                                     final Date iat,
195                                     final Identifier id,
196                                     final InetAddress clientAddress,
197                                     final Map<String,List<String>> attrStatement) {
198
199                super(issuer, subject, audience, iat, exp, id);
200
201                if (iat == null) {
202                        throw new IllegalArgumentException("The issue time must not be null");
203                }
204
205                if (id == null) {
206                        throw new IllegalArgumentException("The assertion identifier must not be null");
207                }
208
209                this.subjectFormat = subjectFormat;
210                this.subjectAuthTime = subjectAuthTime;
211                this.subjectACR = subjectACR;
212                this.clientAddress = clientAddress;
213                this.nbf = nbf;
214                this.attrStatement = attrStatement;
215        }
216
217
218        /**
219         * Returns the optional subject format.
220         *
221         * @return The subject format, {@code null} if not specified.
222         */
223        public String getSubjectFormat() {
224                return subjectFormat;
225        }
226
227
228        /**
229         * Returns the optional subject authentication time.
230         *
231         * @return The subject authentication time, {@code null} if not
232         *         specified.
233         */
234        public Date getSubjectAuthenticationTime() {
235                return subjectAuthTime;
236        }
237
238
239        /**
240         * Returns the optional subject Authentication Context Class Reference
241         * (ACR).
242         *
243         * @return The subject ACR, {@code null} if not specified.
244         */
245        public ACR getSubjectACR() {
246                return subjectACR;
247        }
248
249
250        /**
251         * Returns the optional not-before time.
252         *
253         * @return The not-before time, {@code null} if not specified.
254         */
255        public Date getNotBeforeTime() {
256                return nbf;
257        }
258
259
260        /**
261         * Returns the optional client address to which this assertion is
262         * bound.
263         *
264         * @return The client address, {@code null} if not specified.
265         */
266        public InetAddress getClientInetAddress() {
267                return clientAddress;
268        }
269
270
271        /**
272         * Returns the optional attribute statement.
273         *
274         * @return The attribute statement (in simplified form), {@code null}
275         *         if not specified.
276         */
277        public Map<String, List<String>> getAttributeStatement() {
278                return attrStatement;
279        }
280
281
282        /**
283         * Returns a SAML 2.0 assertion (unsigned) representation of this
284         * assertion details instance.
285         *
286         * @return The SAML 2.0 assertion (with no signature element).
287         *
288         * @throws SerializeException If serialisation failed.
289         */
290        public Assertion toSAML2Assertion()
291                throws SerializeException {
292
293                try {
294                        InitializationService.initialize();
295                } catch (InitializationException e) {
296                        throw new SerializeException(e.getMessage(), e);
297                }
298                
299                // Top level assertion element
300                Assertion a = buildSAMLObject(Assertion.class);
301                
302                a.setID(getID().getValue());
303                a.setIssueInstant(new DateTime(getIssueTime()));
304
305                // Issuer
306                org.opensaml.saml.saml2.core.Issuer iss = buildSAMLObject(org.opensaml.saml.saml2.core.Issuer.class);
307                iss.setValue(getIssuer().getValue());
308                a.setIssuer(iss);
309
310                // Conditions
311                Conditions conditions = buildSAMLObject(Conditions.class);
312
313                // Audience restriction
314                AudienceRestriction audRestriction = buildSAMLObject(AudienceRestriction.class);
315
316                // ... with single audience - the authz server
317                for (Audience audItem: getAudience()) {
318                        org.opensaml.saml.saml2.core.Audience aud = buildSAMLObject(org.opensaml.saml.saml2.core.Audience.class);
319                        aud.setAudienceURI(audItem.getValue());
320                        audRestriction.getAudiences().add(aud);
321                }
322                conditions.getAudienceRestrictions().add(audRestriction);
323
324                a.setConditions(conditions);
325
326
327                // Subject elements
328                org.opensaml.saml.saml2.core.Subject sub = buildSAMLObject(org.opensaml.saml.saml2.core.Subject.class);
329
330                NameID nameID = buildSAMLObject(NameID.class);
331                nameID.setFormat(subjectFormat);
332                nameID.setValue(getSubject().getValue());
333                sub.setNameID(nameID);
334
335                SubjectConfirmation subCm = buildSAMLObject(SubjectConfirmation.class);
336                subCm.setMethod(SubjectConfirmation.METHOD_BEARER);
337
338                SubjectConfirmationData subCmData = buildSAMLObject(SubjectConfirmationData.class);
339                subCmData.setNotOnOrAfter(new DateTime(getExpirationTime()));
340                subCmData.setNotBefore(getNotBeforeTime() != null ? new DateTime(getNotBeforeTime()) : null);
341                subCmData.setRecipient(getAudience().get(0).getValue()); // recipient is single-valued
342
343                if (clientAddress != null) {
344                        subCmData.setAddress(clientAddress.getHostAddress());
345                }
346
347                subCm.setSubjectConfirmationData(subCmData);
348
349                sub.getSubjectConfirmations().add(subCm);
350
351                a.setSubject(sub);
352
353                // Auth time and class?
354                if (subjectAuthTime != null || subjectACR != null) {
355
356                        AuthnStatement authnStmt = buildSAMLObject(AuthnStatement.class);
357
358                        if (subjectAuthTime != null) {
359                                authnStmt.setAuthnInstant(new DateTime(subjectAuthTime));
360                        }
361
362                        if (subjectACR != null) {
363                                AuthnContext authnCtx = buildSAMLObject(AuthnContext.class);
364                                AuthnContextClassRef acr = buildSAMLObject(AuthnContextClassRef.class);
365                                acr.setAuthnContextClassRef(subjectACR.getValue());
366                                authnCtx.setAuthnContextClassRef(acr);
367                                authnStmt.setAuthnContext(authnCtx);
368                        }
369
370                        a.getAuthnStatements().add(authnStmt);
371                }
372
373                // Attributes?
374                if (MapUtils.isNotEmpty(attrStatement)) {
375
376                        AttributeStatement attrSet = buildSAMLObject(AttributeStatement.class);
377
378                        for (Map.Entry<String,List<String>> entry: attrStatement.entrySet()) {
379
380                                Attribute attr = buildSAMLObject(Attribute.class);
381                                attr.setName(entry.getKey());
382                                
383                                XSStringBuilder stringBuilder = (XSStringBuilder)XMLObjectProviderRegistrySupport.getBuilderFactory().getBuilder(XSString.TYPE_NAME);
384                                
385                                for (String v: entry.getValue()) {
386                                        XSString stringValue = stringBuilder.buildObject(AttributeValue.DEFAULT_ELEMENT_NAME, XSString.TYPE_NAME);
387                                        stringValue.setValue(v);
388                                        attr.getAttributeValues().add(stringValue);
389                                }
390
391                                attrSet.getAttributes().add(attr);
392                        }
393
394                        a.getAttributeStatements().add(attrSet);
395                }
396
397                return a;
398        }
399
400
401        /**
402         * Parses a SAML 2.0 bearer assertion details instance from the
403         * specified assertion object.
404         *
405         * @param assertion The assertion. Must not be {@code null}.
406         *
407         * @return The SAML 2.0 bearer assertion details.
408         *
409         * @throws ParseException If the assertion couldn't be parsed to a
410         *                        SAML 2.0 bearer assertion details instance.
411         */
412        public static SAML2AssertionDetails parse(final Assertion assertion)
413                throws ParseException {
414
415                // Assertion > Issuer
416                if (assertion.getIssuer() == null) {
417                        throw new ParseException("Missing Assertion Issuer element");
418                }
419
420                final Issuer issuer = new Issuer(assertion.getIssuer().getValue());
421
422                // Assertion > Subject
423                if (assertion.getSubject() == null) {
424                        throw new ParseException("Missing Assertion Subject element");
425                }
426
427                if (assertion.getSubject().getNameID() == null) {
428                        throw new ParseException("Missing Assertion Subject NameID element");
429                }
430
431                // Assertion > Subject > NameID
432                final Subject subject = new Subject(assertion.getSubject().getNameID().getValue());
433
434                // Assertion > Subject > NameID : Format
435                final String subjectFormat = assertion.getSubject().getNameID().getFormat();
436
437                // Assertion > AuthnStatement : AuthnInstant
438                Date subjectAuthTime = null;
439
440                // Assertion > AuthnStatement > AuthnContext > AuthnContextClassRef
441                ACR subjectACR = null;
442
443                if (CollectionUtils.isNotEmpty(assertion.getAuthnStatements())) {
444
445                        for (AuthnStatement authStmt: assertion.getAuthnStatements()) {
446
447                                if (authStmt == null) {
448                                        continue; // skip
449                                }
450
451                                if (authStmt.getAuthnInstant() != null) {
452                                        subjectAuthTime = authStmt.getAuthnInstant().toDate();
453                                }
454
455                                if (authStmt.getAuthnContext() != null && authStmt.getAuthnContext().getAuthnContextClassRef() != null) {
456                                        subjectACR = new ACR(authStmt.getAuthnContext().getAuthnContextClassRef().getAuthnContextClassRef());
457                                }
458                        }
459                }
460
461                List<SubjectConfirmation> subCms = assertion.getSubject().getSubjectConfirmations();
462
463                if (CollectionUtils.isEmpty(subCms)) {
464                        throw new ParseException("Missing SubjectConfirmation element");
465                }
466
467                // Assertion > Subject > SubjectConfirmation : Method
468                boolean bearerMethodFound = false;
469                for (SubjectConfirmation subCm: subCms) {
470                        if (SubjectConfirmation.METHOD_BEARER.equals(subCm.getMethod())) {
471                                bearerMethodFound = true;
472                                break;
473                        }
474                }
475
476                if (! bearerMethodFound) {
477                        throw new ParseException("Missing SubjectConfirmation Method " + SubjectConfirmation.METHOD_BEARER + " attribute");
478                }
479
480                Conditions conditions = assertion.getConditions();
481
482                if (conditions == null) {
483                        throw new ParseException("Missing Conditions element");
484                }
485
486                List<AudienceRestriction> audRestrictions = conditions.getAudienceRestrictions();
487
488                if (CollectionUtils.isEmpty(audRestrictions)) {
489                        throw new ParseException("Missing AudienceRestriction element");
490                }
491
492                // Assertion > Conditions > AudienceRestriction > Audience
493                final Set<Audience> audSet = new HashSet<>(); // ensure no duplicates
494
495                for (AudienceRestriction audRestriction: audRestrictions) {
496
497                        if (CollectionUtils.isEmpty(audRestriction.getAudiences())) {
498                                continue; // skip
499                        }
500
501                        for (org.opensaml.saml.saml2.core.Audience aud: audRestriction.getAudiences()) {
502                                audSet.add(new Audience(aud.getAudienceURI()));
503                        }
504                }
505
506                // Optional recipient in
507                // Assertion > Subject > SubjectConfirmation > SubjectConfirmationData
508                for (SubjectConfirmation subCm: subCms) {
509
510                        if (subCm.getSubjectConfirmationData() == null) {
511                                continue; // skip
512                        }
513
514                        if (subCm.getSubjectConfirmationData().getRecipient() == null) {
515                                throw new ParseException("Missing SubjectConfirmationData Recipient attribute");
516                        }
517
518                        audSet.add(new Audience(subCm.getSubjectConfirmationData().getRecipient()));
519                }
520
521                // Set expiration and not-before times, try first in
522                // Assertion > Conditions
523                Date exp = conditions.getNotOnOrAfter() != null ? conditions.getNotOnOrAfter().toDate() : null;
524                Date nbf = conditions.getNotBefore() != null ? conditions.getNotBefore().toDate() : null;
525                if (exp == null) {
526                        // Try in Assertion > Subject > SubjectConfirmation > SubjectConfirmationData
527                        for (SubjectConfirmation subCm: subCms) {
528                                if (subCm.getSubjectConfirmationData() == null) {
529                                        continue; // skip
530                                }
531
532                                exp = subCm.getSubjectConfirmationData().getNotOnOrAfter() != null ?
533                                        subCm.getSubjectConfirmationData().getNotOnOrAfter().toDate()
534                                        : null;
535
536                                nbf = subCm.getSubjectConfirmationData().getNotBefore() != null ?
537                                        subCm.getSubjectConfirmationData().getNotBefore().toDate()
538                                        : null;
539                        }
540                }
541
542                // Assertion : ID
543                if (assertion.getID() == null) {
544                        throw new ParseException("Missing Assertion ID attribute");
545                }
546
547                final Identifier id = new Identifier(assertion.getID());
548
549                // Assertion : IssueInstant
550                if (assertion.getIssueInstant() == null) {
551                        throw new ParseException("Missing Assertion IssueInstant attribute");
552                }
553
554                final Date iat = assertion.getIssueInstant().toDate();
555
556                // Assertion > Subject > SubjectConfirmation > SubjectConfirmationData > Address
557                InetAddress clientAddress = null;
558
559                for (SubjectConfirmation subCm: subCms) {
560                        if (subCm.getSubjectConfirmationData() != null && subCm.getSubjectConfirmationData().getAddress() != null) {
561                                try {
562                                        clientAddress = InetAddress.getByName(subCm.getSubjectConfirmationData().getAddress());
563                                } catch (UnknownHostException e) {
564                                        throw new ParseException("Invalid Address: " + e.getMessage(), e);
565                                }
566                        }
567                }
568
569                // Assertion > AttributeStatement > Attribute (: Name, > AttributeValue)
570                Map<String,List<String>> attrStatement = null;
571
572                if (CollectionUtils.isNotEmpty(assertion.getAttributeStatements())) {
573
574                        attrStatement = new HashMap<>();
575
576                        for (AttributeStatement attrStmt: assertion.getAttributeStatements()) {
577                                if (attrStmt == null) {
578                                        continue; // skip
579                                }
580
581                                for (Attribute attr: attrStmt.getAttributes()) {
582                                        String name = attr.getName();
583                                        List<String> values = new LinkedList<>();
584                                        for (XMLObject v: attr.getAttributeValues()) {
585                                                values.add(v.getDOM().getTextContent());
586                                        }
587                                        attrStatement.put(name, values);
588                                }
589                        }
590                }
591
592                return new SAML2AssertionDetails(issuer, subject, subjectFormat, subjectAuthTime, subjectACR,
593                        new ArrayList<>(audSet), exp, nbf, iat, id, clientAddress, attrStatement);
594        }
595}