001package com.nimbusds.oauth2.sdk.assertions.saml2;
002
003
004import java.net.InetAddress;
005import java.net.UnknownHostException;
006import java.util.*;
007
008import com.nimbusds.oauth2.sdk.ParseException;
009import com.nimbusds.oauth2.sdk.SerializeException;
010import com.nimbusds.oauth2.sdk.assertions.AssertionDetails;
011import com.nimbusds.oauth2.sdk.id.Audience;
012import com.nimbusds.oauth2.sdk.id.Identifier;
013import com.nimbusds.oauth2.sdk.id.Issuer;
014import com.nimbusds.oauth2.sdk.id.Subject;
015import com.nimbusds.openid.connect.sdk.claims.ACR;
016import net.jcip.annotations.Immutable;
017import org.apache.commons.collections4.CollectionUtils;
018import org.apache.commons.collections4.MapUtils;
019import org.joda.time.DateTime;
020import org.opensaml.Configuration;
021import org.opensaml.DefaultBootstrap;
022import org.opensaml.common.SAMLObjectBuilder;
023import org.opensaml.saml2.core.*;
024import org.opensaml.xml.ConfigurationException;
025import org.opensaml.xml.XMLObject;
026import org.opensaml.xml.XMLObjectBuilderFactory;
027import org.opensaml.xml.schema.XSString;
028import org.opensaml.xml.schema.impl.XSStringBuilder;
029
030
031/**
032 * SAML 2.0 bearer assertion details for OAuth 2.0 client authentication and
033 * authorisation grants.
034 *
035 * <p>Used for {@link com.nimbusds.oauth2.sdk.SAML2BearerGrant SAML 2.0 bearer
036 * assertion grants}.
037 *
038 * <p>Example SAML 2.0 assertion:
039 *
040 * <pre>
041 * &lt;Assertion IssueInstant="2010-10-01T20:07:34.619Z"
042 *            ID="ef1xsbZxPV2oqjd7HTLRLIBlBb7"
043 *            Version="2.0"
044 *            xmlns="urn:oasis:names:tc:SAML:2.0:assertion"&gt;
045 *     &lt;Issuer&gt;https://saml-idp.example.com&lt;/Issuer&gt;
046 *     &lt;ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#"&gt;
047 *         [...omitted for brevity...]
048 *     &lt;/ds:Signature&gt;
049 *     &lt;Subject&gt;
050 *         &lt;NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"&gt;
051 *             [email protected]
052 *         &lt;/NameID&gt;
053 *         &lt;SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer"&gt;
054 *             &lt;SubjectConfirmationData NotOnOrAfter="2010-10-01T20:12:34.619Z"
055 *                                      Recipient="https://authz.example.net/token.oauth2"/&gt;
056 *         &lt;/SubjectConfirmation&gt;
057 *     &lt;/Subject&gt;
058 *     &lt;Conditions&gt;
059 *         &lt;AudienceRestriction&gt;
060 *             &lt;Audience&gt;https://saml-sp.example.net&lt;/Audience&gt;
061 *         &lt;/AudienceRestriction&gt;
062 *     &lt;/Conditions&gt;
063 *     &lt;AuthnStatement AuthnInstant="2010-10-01T20:07:34.371Z"&gt;
064 *         &lt;AuthnContext&gt;
065 *             &lt;AuthnContextClassRef&gt;urn:oasis:names:tc:SAML:2.0:ac:classes:X509&lt;/AuthnContextClassRef&gt;
066 *         &lt;/AuthnContext&gt;
067 *     &lt;/AuthnStatement&gt;
068 * &lt;/Assertion&gt;
069 * </pre>
070 *
071 * <p>Related specifications:
072 *
073 * <ul>
074 *     <li>Security Assertion Markup Language (SAML) 2.0 Profile for OAuth 2.0
075 *         Client Authentication and Authorization Grants (RFC 7522), section
076 *         3.
077 * </ul>
078 */
079@Immutable
080public class SAML2AssertionDetails extends AssertionDetails {
081
082
083        /**
084         * The subject format (optional).
085         */
086        private final String subjectFormat;
087
088
089        /**
090         * The subject authentication time (optional).
091         */
092        private final Date subjectAuthTime;
093
094
095        /**
096         * The subject Authentication Context Class Reference (ACR) (optional).
097         */
098        private final ACR subjectACR;
099        
100
101        /**
102         * The time before which this assertion must not be accepted for
103         * processing (optional).
104         */
105        private final Date nbf;
106
107
108        /**
109         * The client IPv4 or IPv6 address (optional).
110         */
111        private final InetAddress clientAddress;
112
113
114        /**
115         * The attribute statement (optional).
116         */
117        private final Map<String,List<String>> attrStatement;
118
119
120        /**
121         * Creates a new SAML 2.0 bearer assertion details instance. The
122         * expiration time is set to five minutes from the current system time.
123         * Generates a default identifier for the assertion. The issue time is
124         * set to the current system time.
125         *
126         * @param issuer   The issuer. Must not be {@code null}.
127         * @param subject  The subject. Must not be {@code null}.
128         * @param audience The audience, typically the URI of the authorisation
129         *                 server's token endpoint. Must not be {@code null}.
130         */
131        public SAML2AssertionDetails(final Issuer issuer,
132                                     final Subject subject,
133                                     final Audience audience) {
134
135                this(issuer, subject, null, null, null, audience.toSingleAudienceList(),
136                        new Date(new Date().getTime() + 5*60*1000L), null, new Date(),
137                        new Identifier(), null, null);
138        }
139
140
141        /**
142         * Creates a new SAML 2.0 bearer assertion details instance.
143         *
144         * @param issuer          The issuer. Must not be {@code null}.
145         * @param subject         The subject. Must not be {@code null}.
146         * @param subjectFormat   The subject format, {@code null} if not
147         *                        specified.
148         * @param subjectAuthTime The subject authentication time, {@code null}
149         *                        if not specified.
150         * @param subjectACR      The subject Authentication Context Class
151         *                        Reference (ACR), {@code null} if not
152         *                        specified.
153         * @param audience        The audience, typically including the URI of the
154         *                        authorisation server's token endpoint. Must not be
155         *                        {@code null}.
156         * @param exp             The expiration time. Must not be {@code null}.
157         * @param nbf             The time before which the assertion must not
158         *                        be accepted for processing, {@code null} if
159         *                        not specified.
160         * @param iat             The time at which the assertion was issued.
161         *                        Must not be {@code null}.
162         * @param id              Unique identifier for the assertion. Must not
163         *                        be {@code null}.
164         * @param clientAddress   The client address, {@code null} if not
165         *                        specified.
166         * @param attrStatement   The attribute statement (in simplified form),
167         *                        {@code null} if not specified.
168         */
169        public SAML2AssertionDetails(final Issuer issuer,
170                                     final Subject subject,
171                                     final String subjectFormat,
172                                     final Date subjectAuthTime,
173                                     final ACR subjectACR,
174                                     final List<Audience> audience,
175                                     final Date exp,
176                                     final Date nbf,
177                                     final Date iat,
178                                     final Identifier id,
179                                     final InetAddress clientAddress,
180                                     final Map<String,List<String>> attrStatement) {
181
182                super(issuer, subject, audience, iat, exp, id);
183
184                if (iat == null) {
185                        throw new IllegalArgumentException("The issue time must not be null");
186                }
187
188                if (id == null) {
189                        throw new IllegalArgumentException("The assertion identifier must not be null");
190                }
191
192                this.subjectFormat = subjectFormat;
193                this.subjectAuthTime = subjectAuthTime;
194                this.subjectACR = subjectACR;
195                this.clientAddress = clientAddress;
196                this.nbf = nbf;
197                this.attrStatement = attrStatement;
198        }
199
200
201        /**
202         * Returns the optional subject format.
203         *
204         * @return The subject format, {@code null} if not specified.
205         */
206        public String getSubjectFormat() {
207                return subjectFormat;
208        }
209
210
211        /**
212         * Returns the optional subject authentication time.
213         *
214         * @return The subject authentication time, {@code null} if not
215         *         specified.
216         */
217        public Date getSubjectAuthenticationTime() {
218                return subjectAuthTime;
219        }
220
221
222        /**
223         * Returns the optional subject Authentication Context Class Reference
224         * (ACR).
225         *
226         * @return The subject ACR, {@code null} if not specified.
227         */
228        public ACR getSubjectACR() {
229                return subjectACR;
230        }
231
232
233        /**
234         * Returns the optional not-before time.
235         *
236         * @return The not-before time, {@code null} if not specified.
237         */
238        public Date getNotBeforeTime() {
239                return nbf;
240        }
241
242
243        /**
244         * Returns the optional client address to which this assertion is
245         * bound.
246         *
247         * @return The client address, {@code null} if not specified.
248         */
249        public InetAddress getClientInetAddress() {
250                return clientAddress;
251        }
252
253
254        /**
255         * Returns the optional attribute statement.
256         *
257         * @return The attribute statement (in simplified form), {@code null}
258         *         if not specified.
259         */
260        public Map<String, List<String>> getAttributeStatement() {
261                return attrStatement;
262        }
263
264
265        /**
266         * Returns a SAML 2.0 assertion (unsigned) representation of this
267         * assertion details instance.
268         *
269         * @return The SAML 2.0 assertion (with no signature element).
270         *
271         * @throws SerializeException If serialisation failed.
272         */
273        public Assertion toSAML2Assertion()
274                throws SerializeException {
275
276                try {
277                        DefaultBootstrap.bootstrap();
278                } catch (ConfigurationException e) {
279                        throw new SerializeException(e.getMessage(), e);
280                }
281
282                final XMLObjectBuilderFactory builderFactory = Configuration.getBuilderFactory();
283
284                // Top level assertion element
285                SAMLObjectBuilder<Assertion> assertionBuilder = (SAMLObjectBuilder<Assertion>) builderFactory.getBuilder(Assertion.DEFAULT_ELEMENT_NAME);
286
287                Assertion a = assertionBuilder.buildObject();
288                a.setID(getID().getValue());
289                a.setIssueInstant(new DateTime(getIssueTime()));
290
291                // Issuer
292                SAMLObjectBuilder<org.opensaml.saml2.core.Issuer> issuerBuilder = (SAMLObjectBuilder<org.opensaml.saml2.core.Issuer>) builderFactory.getBuilder(org.opensaml.saml2.core.Issuer.DEFAULT_ELEMENT_NAME);
293                org.opensaml.saml2.core.Issuer iss = issuerBuilder.buildObject();
294                iss.setValue(getIssuer().getValue());
295                a.setIssuer(iss);
296
297                // Conditions
298                SAMLObjectBuilder<Conditions> conditionsBuilder = (SAMLObjectBuilder<Conditions>) builderFactory.getBuilder(Conditions.DEFAULT_ELEMENT_NAME);
299                Conditions conditions = conditionsBuilder.buildObject();
300
301                // Audience restriction
302                SAMLObjectBuilder<AudienceRestriction> audRestrictionBuilder = (SAMLObjectBuilder<AudienceRestriction>) builderFactory.getBuilder(AudienceRestriction.DEFAULT_ELEMENT_NAME);
303                AudienceRestriction audRestriction = audRestrictionBuilder.buildObject();
304
305                // ... with single audience - the authz server
306                SAMLObjectBuilder<org.opensaml.saml2.core.Audience> audBuilder = (SAMLObjectBuilder<org.opensaml.saml2.core.Audience>) builderFactory.getBuilder(org.opensaml.saml2.core.Audience.DEFAULT_ELEMENT_NAME);
307                for (Audience audItem: getAudience()) {
308                        org.opensaml.saml2.core.Audience aud = audBuilder.buildObject();
309                        aud.setAudienceURI(audItem.getValue());
310                        audRestriction.getAudiences().add(aud);
311                }
312                conditions.getAudienceRestrictions().add(audRestriction);
313
314                a.setConditions(conditions);
315
316
317                // Subject elements
318                SAMLObjectBuilder<org.opensaml.saml2.core.Subject> subBuilder = (SAMLObjectBuilder<org.opensaml.saml2.core.Subject>) builderFactory.getBuilder(org.opensaml.saml2.core.Subject.DEFAULT_ELEMENT_NAME);
319                org.opensaml.saml2.core.Subject sub = subBuilder.buildObject();
320
321                SAMLObjectBuilder<NameID> subIDBuilder = (SAMLObjectBuilder<NameID>) builderFactory.getBuilder(NameID.DEFAULT_ELEMENT_NAME);
322                NameID nameID = subIDBuilder.buildObject();
323                nameID.setFormat(subjectFormat);
324                nameID.setValue(getSubject().getValue());
325                sub.setNameID(nameID);
326
327                SAMLObjectBuilder<SubjectConfirmation> subCmBuilder = (SAMLObjectBuilder<SubjectConfirmation>) builderFactory.getBuilder(SubjectConfirmation.DEFAULT_ELEMENT_NAME);
328                SubjectConfirmation subCm = subCmBuilder.buildObject();
329                subCm.setMethod(SubjectConfirmation.METHOD_BEARER);
330
331                SAMLObjectBuilder<SubjectConfirmationData> subCmDataBuilder= (SAMLObjectBuilder<SubjectConfirmationData>) builderFactory.getBuilder(SubjectConfirmationData.DEFAULT_ELEMENT_NAME);
332                SubjectConfirmationData subCmData = subCmDataBuilder.buildObject();
333                subCmData.setNotOnOrAfter(new DateTime(getExpirationTime()));
334                subCmData.setNotBefore(getNotBeforeTime() != null ? new DateTime(getNotBeforeTime()) : null);
335                subCmData.setRecipient(getAudience().get(0).getValue()); // recipient is single-valued
336
337                if (clientAddress != null) {
338                        subCmData.setAddress(clientAddress.getHostAddress());
339                }
340
341                subCm.setSubjectConfirmationData(subCmData);
342
343                sub.getSubjectConfirmations().add(subCm);
344
345                a.setSubject(sub);
346
347                // Auth time and class?
348                if (subjectAuthTime != null || subjectACR != null) {
349
350                        SAMLObjectBuilder<AuthnStatement> authnStmtBuilder = (SAMLObjectBuilder<AuthnStatement>) builderFactory.getBuilder(AuthnStatement.DEFAULT_ELEMENT_NAME);
351                        AuthnStatement authnStmt = authnStmtBuilder.buildObject();
352
353                        if (subjectAuthTime != null) {
354                                authnStmt.setAuthnInstant(new DateTime(subjectAuthTime));
355                        }
356
357                        if (subjectACR != null) {
358                                SAMLObjectBuilder<AuthnContext> authnCtxBuilder = (SAMLObjectBuilder<AuthnContext>) builderFactory.getBuilder(AuthnContext.DEFAULT_ELEMENT_NAME);
359                                AuthnContext authnCtx = authnCtxBuilder.buildObject();
360                                SAMLObjectBuilder<AuthnContextClassRef> acrBuilder = (SAMLObjectBuilder<AuthnContextClassRef>) builderFactory.getBuilder(AuthnContextClassRef.DEFAULT_ELEMENT_NAME);
361                                AuthnContextClassRef acr = acrBuilder.buildObject();
362                                acr.setAuthnContextClassRef(subjectACR.getValue());
363                                authnCtx.setAuthnContextClassRef(acr);
364                                authnStmt.setAuthnContext(authnCtx);
365                        }
366
367                        a.getAuthnStatements().add(authnStmt);
368                }
369
370                // Attributes?
371                if (MapUtils.isNotEmpty(attrStatement)) {
372
373                        SAMLObjectBuilder<AttributeStatement> attrContainerBuilder = (SAMLObjectBuilder<AttributeStatement>) builderFactory.getBuilder(AttributeStatement.DEFAULT_ELEMENT_NAME);
374                        AttributeStatement attrSet = attrContainerBuilder.buildObject();
375
376                        SAMLObjectBuilder<Attribute> attrBuilder = (SAMLObjectBuilder<Attribute>) builderFactory.getBuilder(Attribute.DEFAULT_ELEMENT_NAME);
377
378                        for (Map.Entry<String,List<String>> entry: attrStatement.entrySet()) {
379
380                                Attribute attr = attrBuilder.buildObject();
381                                attr.setName(entry.getKey());
382
383                                XSStringBuilder stringBuilder = (XSStringBuilder) Configuration.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.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
593                return new SAML2AssertionDetails(issuer, subject, subjectFormat, subjectAuthTime, subjectACR,
594                        new ArrayList<>(audSet), exp, nbf, iat, id, clientAddress, attrStatement);
595        }
596}