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