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 * <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"> 045 * <Issuer>https://saml-idp.example.com</Issuer> 046 * <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#"> 047 * [...omitted for brevity...] 048 * </ds:Signature> 049 * <Subject> 050 * <NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"> 051 * [email protected] 052 * </NameID> 053 * <SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer"> 054 * <SubjectConfirmationData NotOnOrAfter="2010-10-01T20:12:34.619Z" 055 * Recipient="https://authz.example.net/token.oauth2"/> 056 * </SubjectConfirmation> 057 * </Subject> 058 * <Conditions> 059 * <AudienceRestriction> 060 * <Audience>https://saml-sp.example.net</Audience> 061 * </AudienceRestriction> 062 * </Conditions> 063 * <AuthnStatement AuthnInstant="2010-10-01T20:07:34.371Z"> 064 * <AuthnContext> 065 * <AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:X509</AuthnContextClassRef> 066 * </AuthnContext> 067 * </AuthnStatement> 068 * </Assertion> 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}