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 com.nimbusds.oauth2.sdk.ParseException; 026import com.nimbusds.oauth2.sdk.SerializeException; 027import com.nimbusds.oauth2.sdk.assertions.AssertionDetails; 028import com.nimbusds.oauth2.sdk.id.Audience; 029import com.nimbusds.oauth2.sdk.id.Identifier; 030import com.nimbusds.oauth2.sdk.id.Issuer; 031import com.nimbusds.oauth2.sdk.id.Subject; 032import com.nimbusds.openid.connect.sdk.claims.ACR; 033import net.jcip.annotations.Immutable; 034import org.apache.commons.collections4.CollectionUtils; 035import org.apache.commons.collections4.MapUtils; 036import org.joda.time.DateTime; 037import org.opensaml.Configuration; 038import org.opensaml.DefaultBootstrap; 039import org.opensaml.common.SAMLObjectBuilder; 040import org.opensaml.saml2.core.*; 041import org.opensaml.xml.ConfigurationException; 042import org.opensaml.xml.XMLObject; 043import org.opensaml.xml.XMLObjectBuilderFactory; 044import org.opensaml.xml.schema.XSString; 045import org.opensaml.xml.schema.impl.XSStringBuilder; 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 * <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"> 062 * <Issuer>https://saml-idp.example.com</Issuer> 063 * <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#"> 064 * [...omitted for brevity...] 065 * </ds:Signature> 066 * <Subject> 067 * <NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"> 068 * [email protected] 069 * </NameID> 070 * <SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer"> 071 * <SubjectConfirmationData NotOnOrAfter="2010-10-01T20:12:34.619Z" 072 * Recipient="https://authz.example.net/token.oauth2"/> 073 * </SubjectConfirmation> 074 * </Subject> 075 * <Conditions> 076 * <AudienceRestriction> 077 * <Audience>https://saml-sp.example.net</Audience> 078 * </AudienceRestriction> 079 * </Conditions> 080 * <AuthnStatement AuthnInstant="2010-10-01T20:07:34.371Z"> 081 * <AuthnContext> 082 * <AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:X509</AuthnContextClassRef> 083 * </AuthnContext> 084 * </AuthnStatement> 085 * </Assertion> 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 DefaultBootstrap.bootstrap(); 295 } catch (ConfigurationException e) { 296 throw new SerializeException(e.getMessage(), e); 297 } 298 299 final XMLObjectBuilderFactory builderFactory = Configuration.getBuilderFactory(); 300 301 // Top level assertion element 302 SAMLObjectBuilder<Assertion> assertionBuilder = (SAMLObjectBuilder<Assertion>) builderFactory.getBuilder(Assertion.DEFAULT_ELEMENT_NAME); 303 304 Assertion a = assertionBuilder.buildObject(); 305 a.setID(getID().getValue()); 306 a.setIssueInstant(new DateTime(getIssueTime())); 307 308 // Issuer 309 SAMLObjectBuilder<org.opensaml.saml2.core.Issuer> issuerBuilder = (SAMLObjectBuilder<org.opensaml.saml2.core.Issuer>) builderFactory.getBuilder(org.opensaml.saml2.core.Issuer.DEFAULT_ELEMENT_NAME); 310 org.opensaml.saml2.core.Issuer iss = issuerBuilder.buildObject(); 311 iss.setValue(getIssuer().getValue()); 312 a.setIssuer(iss); 313 314 // Conditions 315 SAMLObjectBuilder<Conditions> conditionsBuilder = (SAMLObjectBuilder<Conditions>) builderFactory.getBuilder(Conditions.DEFAULT_ELEMENT_NAME); 316 Conditions conditions = conditionsBuilder.buildObject(); 317 318 // Audience restriction 319 SAMLObjectBuilder<AudienceRestriction> audRestrictionBuilder = (SAMLObjectBuilder<AudienceRestriction>) builderFactory.getBuilder(AudienceRestriction.DEFAULT_ELEMENT_NAME); 320 AudienceRestriction audRestriction = audRestrictionBuilder.buildObject(); 321 322 // ... with single audience - the authz server 323 SAMLObjectBuilder<org.opensaml.saml2.core.Audience> audBuilder = (SAMLObjectBuilder<org.opensaml.saml2.core.Audience>) builderFactory.getBuilder(org.opensaml.saml2.core.Audience.DEFAULT_ELEMENT_NAME); 324 for (Audience audItem: getAudience()) { 325 org.opensaml.saml2.core.Audience aud = audBuilder.buildObject(); 326 aud.setAudienceURI(audItem.getValue()); 327 audRestriction.getAudiences().add(aud); 328 } 329 conditions.getAudienceRestrictions().add(audRestriction); 330 331 a.setConditions(conditions); 332 333 334 // Subject elements 335 SAMLObjectBuilder<org.opensaml.saml2.core.Subject> subBuilder = (SAMLObjectBuilder<org.opensaml.saml2.core.Subject>) builderFactory.getBuilder(org.opensaml.saml2.core.Subject.DEFAULT_ELEMENT_NAME); 336 org.opensaml.saml2.core.Subject sub = subBuilder.buildObject(); 337 338 SAMLObjectBuilder<NameID> subIDBuilder = (SAMLObjectBuilder<NameID>) builderFactory.getBuilder(NameID.DEFAULT_ELEMENT_NAME); 339 NameID nameID = subIDBuilder.buildObject(); 340 nameID.setFormat(subjectFormat); 341 nameID.setValue(getSubject().getValue()); 342 sub.setNameID(nameID); 343 344 SAMLObjectBuilder<SubjectConfirmation> subCmBuilder = (SAMLObjectBuilder<SubjectConfirmation>) builderFactory.getBuilder(SubjectConfirmation.DEFAULT_ELEMENT_NAME); 345 SubjectConfirmation subCm = subCmBuilder.buildObject(); 346 subCm.setMethod(SubjectConfirmation.METHOD_BEARER); 347 348 SAMLObjectBuilder<SubjectConfirmationData> subCmDataBuilder= (SAMLObjectBuilder<SubjectConfirmationData>) builderFactory.getBuilder(SubjectConfirmationData.DEFAULT_ELEMENT_NAME); 349 SubjectConfirmationData subCmData = subCmDataBuilder.buildObject(); 350 subCmData.setNotOnOrAfter(new DateTime(getExpirationTime())); 351 subCmData.setNotBefore(getNotBeforeTime() != null ? new DateTime(getNotBeforeTime()) : null); 352 subCmData.setRecipient(getAudience().get(0).getValue()); // recipient is single-valued 353 354 if (clientAddress != null) { 355 subCmData.setAddress(clientAddress.getHostAddress()); 356 } 357 358 subCm.setSubjectConfirmationData(subCmData); 359 360 sub.getSubjectConfirmations().add(subCm); 361 362 a.setSubject(sub); 363 364 // Auth time and class? 365 if (subjectAuthTime != null || subjectACR != null) { 366 367 SAMLObjectBuilder<AuthnStatement> authnStmtBuilder = (SAMLObjectBuilder<AuthnStatement>) builderFactory.getBuilder(AuthnStatement.DEFAULT_ELEMENT_NAME); 368 AuthnStatement authnStmt = authnStmtBuilder.buildObject(); 369 370 if (subjectAuthTime != null) { 371 authnStmt.setAuthnInstant(new DateTime(subjectAuthTime)); 372 } 373 374 if (subjectACR != null) { 375 SAMLObjectBuilder<AuthnContext> authnCtxBuilder = (SAMLObjectBuilder<AuthnContext>) builderFactory.getBuilder(AuthnContext.DEFAULT_ELEMENT_NAME); 376 AuthnContext authnCtx = authnCtxBuilder.buildObject(); 377 SAMLObjectBuilder<AuthnContextClassRef> acrBuilder = (SAMLObjectBuilder<AuthnContextClassRef>) builderFactory.getBuilder(AuthnContextClassRef.DEFAULT_ELEMENT_NAME); 378 AuthnContextClassRef acr = acrBuilder.buildObject(); 379 acr.setAuthnContextClassRef(subjectACR.getValue()); 380 authnCtx.setAuthnContextClassRef(acr); 381 authnStmt.setAuthnContext(authnCtx); 382 } 383 384 a.getAuthnStatements().add(authnStmt); 385 } 386 387 // Attributes? 388 if (MapUtils.isNotEmpty(attrStatement)) { 389 390 SAMLObjectBuilder<AttributeStatement> attrContainerBuilder = (SAMLObjectBuilder<AttributeStatement>) builderFactory.getBuilder(AttributeStatement.DEFAULT_ELEMENT_NAME); 391 AttributeStatement attrSet = attrContainerBuilder.buildObject(); 392 393 SAMLObjectBuilder<Attribute> attrBuilder = (SAMLObjectBuilder<Attribute>) builderFactory.getBuilder(Attribute.DEFAULT_ELEMENT_NAME); 394 395 for (Map.Entry<String,List<String>> entry: attrStatement.entrySet()) { 396 397 Attribute attr = attrBuilder.buildObject(); 398 attr.setName(entry.getKey()); 399 400 XSStringBuilder stringBuilder = (XSStringBuilder) Configuration.getBuilderFactory().getBuilder(XSString.TYPE_NAME); 401 402 for (String v: entry.getValue()) { 403 XSString stringValue = stringBuilder.buildObject(AttributeValue.DEFAULT_ELEMENT_NAME, XSString.TYPE_NAME); 404 stringValue.setValue(v); 405 attr.getAttributeValues().add(stringValue); 406 } 407 408 attrSet.getAttributes().add(attr); 409 } 410 411 a.getAttributeStatements().add(attrSet); 412 } 413 414 return a; 415 } 416 417 418 /** 419 * Parses a SAML 2.0 bearer assertion details instance from the 420 * specified assertion object. 421 * 422 * @param assertion The assertion. Must not be {@code null}. 423 * 424 * @return The SAML 2.0 bearer assertion details. 425 * 426 * @throws ParseException If the assertion couldn't be parsed to a 427 * SAML 2.0 bearer assertion details instance. 428 */ 429 public static SAML2AssertionDetails parse(final Assertion assertion) 430 throws ParseException { 431 432 // Assertion > Issuer 433 if (assertion.getIssuer() == null) { 434 throw new ParseException("Missing Assertion Issuer element"); 435 } 436 437 final Issuer issuer = new Issuer(assertion.getIssuer().getValue()); 438 439 // Assertion > Subject 440 if (assertion.getSubject() == null) { 441 throw new ParseException("Missing Assertion Subject element"); 442 } 443 444 if (assertion.getSubject().getNameID() == null) { 445 throw new ParseException("Missing Assertion Subject NameID element"); 446 } 447 448 // Assertion > Subject > NameID 449 final Subject subject = new Subject(assertion.getSubject().getNameID().getValue()); 450 451 // Assertion > Subject > NameID : Format 452 final String subjectFormat = assertion.getSubject().getNameID().getFormat(); 453 454 // Assertion > AuthnStatement : AuthnInstant 455 Date subjectAuthTime = null; 456 457 // Assertion > AuthnStatement > AuthnContext > AuthnContextClassRef 458 ACR subjectACR = null; 459 460 if (CollectionUtils.isNotEmpty(assertion.getAuthnStatements())) { 461 462 for (AuthnStatement authStmt: assertion.getAuthnStatements()) { 463 464 if (authStmt == null) { 465 continue; // skip 466 } 467 468 if (authStmt.getAuthnInstant() != null) { 469 subjectAuthTime = authStmt.getAuthnInstant().toDate(); 470 } 471 472 if (authStmt.getAuthnContext() != null && authStmt.getAuthnContext().getAuthnContextClassRef() != null) { 473 subjectACR = new ACR(authStmt.getAuthnContext().getAuthnContextClassRef().getAuthnContextClassRef()); 474 } 475 } 476 } 477 478 List<SubjectConfirmation> subCms = assertion.getSubject().getSubjectConfirmations(); 479 480 if (CollectionUtils.isEmpty(subCms)) { 481 throw new ParseException("Missing SubjectConfirmation element"); 482 } 483 484 // Assertion > Subject > SubjectConfirmation : Method 485 boolean bearerMethodFound = false; 486 for (SubjectConfirmation subCm: subCms) { 487 if (SubjectConfirmation.METHOD_BEARER.equals(subCm.getMethod())) { 488 bearerMethodFound = true; 489 break; 490 } 491 } 492 493 if (! bearerMethodFound) { 494 throw new ParseException("Missing SubjectConfirmation Method " + SubjectConfirmation.METHOD_BEARER + " attribute"); 495 } 496 497 Conditions conditions = assertion.getConditions(); 498 499 if (conditions == null) { 500 throw new ParseException("Missing Conditions element"); 501 } 502 503 List<AudienceRestriction> audRestrictions = conditions.getAudienceRestrictions(); 504 505 if (CollectionUtils.isEmpty(audRestrictions)) { 506 throw new ParseException("Missing AudienceRestriction element"); 507 } 508 509 // Assertion > Conditions > AudienceRestriction > Audience 510 final Set<Audience> audSet = new HashSet<>(); // ensure no duplicates 511 512 for (AudienceRestriction audRestriction: audRestrictions) { 513 514 if (CollectionUtils.isEmpty(audRestriction.getAudiences())) { 515 continue; // skip 516 } 517 518 for (org.opensaml.saml2.core.Audience aud: audRestriction.getAudiences()) { 519 audSet.add(new Audience(aud.getAudienceURI())); 520 } 521 } 522 523 // Optional recipient in 524 // Assertion > Subject > SubjectConfirmation > SubjectConfirmationData 525 for (SubjectConfirmation subCm: subCms) { 526 527 if (subCm.getSubjectConfirmationData() == null) { 528 continue; // skip 529 } 530 531 if (subCm.getSubjectConfirmationData().getRecipient() == null) { 532 throw new ParseException("Missing SubjectConfirmationData Recipient attribute"); 533 } 534 535 audSet.add(new Audience(subCm.getSubjectConfirmationData().getRecipient())); 536 } 537 538 // Set expiration and not-before times, try first in 539 // Assertion > Conditions 540 Date exp = conditions.getNotOnOrAfter() != null ? conditions.getNotOnOrAfter().toDate() : null; 541 Date nbf = conditions.getNotBefore() != null ? conditions.getNotBefore().toDate() : null; 542 if (exp == null) { 543 // Try in Assertion > Subject > SubjectConfirmation > SubjectConfirmationData 544 for (SubjectConfirmation subCm: subCms) { 545 if (subCm.getSubjectConfirmationData() == null) { 546 continue; // skip 547 } 548 549 exp = subCm.getSubjectConfirmationData().getNotOnOrAfter() != null ? 550 subCm.getSubjectConfirmationData().getNotOnOrAfter().toDate() 551 : null; 552 553 nbf = subCm.getSubjectConfirmationData().getNotBefore() != null ? 554 subCm.getSubjectConfirmationData().getNotBefore().toDate() 555 : null; 556 } 557 } 558 559 // Assertion : ID 560 if (assertion.getID() == null) { 561 throw new ParseException("Missing Assertion ID attribute"); 562 } 563 564 final Identifier id = new Identifier(assertion.getID()); 565 566 // Assertion : IssueInstant 567 if (assertion.getIssueInstant() == null) { 568 throw new ParseException("Missing Assertion IssueInstant attribute"); 569 } 570 571 final Date iat = assertion.getIssueInstant().toDate(); 572 573 // Assertion > Subject > SubjectConfirmation > SubjectConfirmationData > Address 574 InetAddress clientAddress = null; 575 576 for (SubjectConfirmation subCm: subCms) { 577 if (subCm.getSubjectConfirmationData() != null && subCm.getSubjectConfirmationData().getAddress() != null) { 578 try { 579 clientAddress = InetAddress.getByName(subCm.getSubjectConfirmationData().getAddress()); 580 } catch (UnknownHostException e) { 581 throw new ParseException("Invalid Address: " + e.getMessage(), e); 582 } 583 } 584 } 585 586 // Assertion > AttributeStatement > Attribute (: Name, > AttributeValue) 587 Map<String,List<String>> attrStatement = null; 588 589 if (CollectionUtils.isNotEmpty(assertion.getAttributeStatements())) { 590 591 attrStatement = new HashMap<>(); 592 593 for (AttributeStatement attrStmt: assertion.getAttributeStatements()) { 594 if (attrStmt == null) { 595 continue; // skip 596 } 597 598 for (Attribute attr: attrStmt.getAttributes()) { 599 String name = attr.getName(); 600 List<String> values = new LinkedList<>(); 601 for (XMLObject v: attr.getAttributeValues()) { 602 values.add(v.getDOM().getTextContent()); 603 } 604 attrStatement.put(name, values); 605 } 606 } 607 } 608 609 610 return new SAML2AssertionDetails(issuer, subject, subjectFormat, subjectAuthTime, subjectACR, 611 new ArrayList<>(audSet), exp, nbf, iat, id, clientAddress, attrStatement); 612 } 613}