001/* 002 * oauth2-oidc-sdk 003 * 004 * Copyright 2012-2016, Connect2id Ltd and contributors. 005 * 006 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use 007 * this file except in compliance with the License. You may obtain a copy of the 008 * License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, software distributed 013 * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 014 * CONDITIONS OF ANY KIND, either express or implied. See the License for the 015 * specific language governing permissions and limitations under the License. 016 */ 017 018package com.nimbusds.oauth2.sdk.assertions.saml2; 019 020 021import java.net.InetAddress; 022import java.net.UnknownHostException; 023import java.util.*; 024 025import static com.nimbusds.oauth2.sdk.assertions.saml2.SAML2Utils.buildSAMLObject; 026 027import com.nimbusds.oauth2.sdk.ParseException; 028import com.nimbusds.oauth2.sdk.SerializeException; 029import com.nimbusds.oauth2.sdk.assertions.AssertionDetails; 030import com.nimbusds.oauth2.sdk.id.Audience; 031import com.nimbusds.oauth2.sdk.id.Identifier; 032import com.nimbusds.oauth2.sdk.id.Issuer; 033import com.nimbusds.oauth2.sdk.id.Subject; 034import com.nimbusds.oauth2.sdk.util.CollectionUtils; 035import com.nimbusds.oauth2.sdk.util.MapUtils; 036import com.nimbusds.openid.connect.sdk.claims.ACR; 037import net.jcip.annotations.Immutable; 038import org.joda.time.DateTime; 039import org.opensaml.core.config.InitializationException; 040import org.opensaml.core.config.InitializationService; 041import org.opensaml.core.xml.XMLObject; 042import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport; 043import org.opensaml.core.xml.schema.XSString; 044import org.opensaml.core.xml.schema.impl.XSStringBuilder; 045import org.opensaml.saml.saml2.core.*; 046 047 048/** 049 * SAML 2.0 bearer assertion details for OAuth 2.0 client authentication and 050 * authorisation grants. 051 * 052 * <p>Used for {@link com.nimbusds.oauth2.sdk.SAML2BearerGrant SAML 2.0 bearer 053 * assertion grants}. 054 * 055 * <p>Example SAML 2.0 assertion: 056 * 057 * <pre> 058 * <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 InitializationService.initialize(); 295 } catch (InitializationException e) { 296 throw new SerializeException(e.getMessage(), e); 297 } 298 299 // Top level assertion element 300 Assertion a = buildSAMLObject(Assertion.class); 301 302 a.setID(getID().getValue()); 303 a.setIssueInstant(new DateTime(getIssueTime())); 304 305 // Issuer 306 org.opensaml.saml.saml2.core.Issuer iss = buildSAMLObject(org.opensaml.saml.saml2.core.Issuer.class); 307 iss.setValue(getIssuer().getValue()); 308 a.setIssuer(iss); 309 310 // Conditions 311 Conditions conditions = buildSAMLObject(Conditions.class); 312 313 // Audience restriction 314 AudienceRestriction audRestriction = buildSAMLObject(AudienceRestriction.class); 315 316 // ... with single audience - the authz server 317 for (Audience audItem: getAudience()) { 318 org.opensaml.saml.saml2.core.Audience aud = buildSAMLObject(org.opensaml.saml.saml2.core.Audience.class); 319 aud.setAudienceURI(audItem.getValue()); 320 audRestriction.getAudiences().add(aud); 321 } 322 conditions.getAudienceRestrictions().add(audRestriction); 323 324 a.setConditions(conditions); 325 326 327 // Subject elements 328 org.opensaml.saml.saml2.core.Subject sub = buildSAMLObject(org.opensaml.saml.saml2.core.Subject.class); 329 330 NameID nameID = buildSAMLObject(NameID.class); 331 nameID.setFormat(subjectFormat); 332 nameID.setValue(getSubject().getValue()); 333 sub.setNameID(nameID); 334 335 SubjectConfirmation subCm = buildSAMLObject(SubjectConfirmation.class); 336 subCm.setMethod(SubjectConfirmation.METHOD_BEARER); 337 338 SubjectConfirmationData subCmData = buildSAMLObject(SubjectConfirmationData.class); 339 subCmData.setNotOnOrAfter(new DateTime(getExpirationTime())); 340 subCmData.setNotBefore(getNotBeforeTime() != null ? new DateTime(getNotBeforeTime()) : null); 341 subCmData.setRecipient(getAudience().get(0).getValue()); // recipient is single-valued 342 343 if (clientAddress != null) { 344 subCmData.setAddress(clientAddress.getHostAddress()); 345 } 346 347 subCm.setSubjectConfirmationData(subCmData); 348 349 sub.getSubjectConfirmations().add(subCm); 350 351 a.setSubject(sub); 352 353 // Auth time and class? 354 if (subjectAuthTime != null || subjectACR != null) { 355 356 AuthnStatement authnStmt = buildSAMLObject(AuthnStatement.class); 357 358 if (subjectAuthTime != null) { 359 authnStmt.setAuthnInstant(new DateTime(subjectAuthTime)); 360 } 361 362 if (subjectACR != null) { 363 AuthnContext authnCtx = buildSAMLObject(AuthnContext.class); 364 AuthnContextClassRef acr = buildSAMLObject(AuthnContextClassRef.class); 365 acr.setAuthnContextClassRef(subjectACR.getValue()); 366 authnCtx.setAuthnContextClassRef(acr); 367 authnStmt.setAuthnContext(authnCtx); 368 } 369 370 a.getAuthnStatements().add(authnStmt); 371 } 372 373 // Attributes? 374 if (MapUtils.isNotEmpty(attrStatement)) { 375 376 AttributeStatement attrSet = buildSAMLObject(AttributeStatement.class); 377 378 for (Map.Entry<String,List<String>> entry: attrStatement.entrySet()) { 379 380 Attribute attr = buildSAMLObject(Attribute.class); 381 attr.setName(entry.getKey()); 382 383 XSStringBuilder stringBuilder = (XSStringBuilder)XMLObjectProviderRegistrySupport.getBuilderFactory().getBuilder(XSString.TYPE_NAME); 384 385 for (String v: entry.getValue()) { 386 XSString stringValue = stringBuilder.buildObject(AttributeValue.DEFAULT_ELEMENT_NAME, XSString.TYPE_NAME); 387 stringValue.setValue(v); 388 attr.getAttributeValues().add(stringValue); 389 } 390 391 attrSet.getAttributes().add(attr); 392 } 393 394 a.getAttributeStatements().add(attrSet); 395 } 396 397 return a; 398 } 399 400 401 /** 402 * Parses a SAML 2.0 bearer assertion details instance from the 403 * specified assertion object. 404 * 405 * @param assertion The assertion. Must not be {@code null}. 406 * 407 * @return The SAML 2.0 bearer assertion details. 408 * 409 * @throws ParseException If the assertion couldn't be parsed to a 410 * SAML 2.0 bearer assertion details instance. 411 */ 412 public static SAML2AssertionDetails parse(final Assertion assertion) 413 throws ParseException { 414 415 // Assertion > Issuer 416 if (assertion.getIssuer() == null) { 417 throw new ParseException("Missing Assertion Issuer element"); 418 } 419 420 final Issuer issuer = new Issuer(assertion.getIssuer().getValue()); 421 422 // Assertion > Subject 423 if (assertion.getSubject() == null) { 424 throw new ParseException("Missing Assertion Subject element"); 425 } 426 427 if (assertion.getSubject().getNameID() == null) { 428 throw new ParseException("Missing Assertion Subject NameID element"); 429 } 430 431 // Assertion > Subject > NameID 432 final Subject subject = new Subject(assertion.getSubject().getNameID().getValue()); 433 434 // Assertion > Subject > NameID : Format 435 final String subjectFormat = assertion.getSubject().getNameID().getFormat(); 436 437 // Assertion > AuthnStatement : AuthnInstant 438 Date subjectAuthTime = null; 439 440 // Assertion > AuthnStatement > AuthnContext > AuthnContextClassRef 441 ACR subjectACR = null; 442 443 if (CollectionUtils.isNotEmpty(assertion.getAuthnStatements())) { 444 445 for (AuthnStatement authStmt: assertion.getAuthnStatements()) { 446 447 if (authStmt == null) { 448 continue; // skip 449 } 450 451 if (authStmt.getAuthnInstant() != null) { 452 subjectAuthTime = authStmt.getAuthnInstant().toDate(); 453 } 454 455 if (authStmt.getAuthnContext() != null && authStmt.getAuthnContext().getAuthnContextClassRef() != null) { 456 subjectACR = new ACR(authStmt.getAuthnContext().getAuthnContextClassRef().getAuthnContextClassRef()); 457 } 458 } 459 } 460 461 List<SubjectConfirmation> subCms = assertion.getSubject().getSubjectConfirmations(); 462 463 if (CollectionUtils.isEmpty(subCms)) { 464 throw new ParseException("Missing SubjectConfirmation element"); 465 } 466 467 // Assertion > Subject > SubjectConfirmation : Method 468 boolean bearerMethodFound = false; 469 for (SubjectConfirmation subCm: subCms) { 470 if (SubjectConfirmation.METHOD_BEARER.equals(subCm.getMethod())) { 471 bearerMethodFound = true; 472 break; 473 } 474 } 475 476 if (! bearerMethodFound) { 477 throw new ParseException("Missing SubjectConfirmation Method " + SubjectConfirmation.METHOD_BEARER + " attribute"); 478 } 479 480 Conditions conditions = assertion.getConditions(); 481 482 if (conditions == null) { 483 throw new ParseException("Missing Conditions element"); 484 } 485 486 List<AudienceRestriction> audRestrictions = conditions.getAudienceRestrictions(); 487 488 if (CollectionUtils.isEmpty(audRestrictions)) { 489 throw new ParseException("Missing AudienceRestriction element"); 490 } 491 492 // Assertion > Conditions > AudienceRestriction > Audience 493 final Set<Audience> audSet = new HashSet<>(); // ensure no duplicates 494 495 for (AudienceRestriction audRestriction: audRestrictions) { 496 497 if (CollectionUtils.isEmpty(audRestriction.getAudiences())) { 498 continue; // skip 499 } 500 501 for (org.opensaml.saml.saml2.core.Audience aud: audRestriction.getAudiences()) { 502 audSet.add(new Audience(aud.getAudienceURI())); 503 } 504 } 505 506 // Optional recipient in 507 // Assertion > Subject > SubjectConfirmation > SubjectConfirmationData 508 for (SubjectConfirmation subCm: subCms) { 509 510 if (subCm.getSubjectConfirmationData() == null) { 511 continue; // skip 512 } 513 514 if (subCm.getSubjectConfirmationData().getRecipient() == null) { 515 throw new ParseException("Missing SubjectConfirmationData Recipient attribute"); 516 } 517 518 audSet.add(new Audience(subCm.getSubjectConfirmationData().getRecipient())); 519 } 520 521 // Set expiration and not-before times, try first in 522 // Assertion > Conditions 523 Date exp = conditions.getNotOnOrAfter() != null ? conditions.getNotOnOrAfter().toDate() : null; 524 Date nbf = conditions.getNotBefore() != null ? conditions.getNotBefore().toDate() : null; 525 if (exp == null) { 526 // Try in Assertion > Subject > SubjectConfirmation > SubjectConfirmationData 527 for (SubjectConfirmation subCm: subCms) { 528 if (subCm.getSubjectConfirmationData() == null) { 529 continue; // skip 530 } 531 532 exp = subCm.getSubjectConfirmationData().getNotOnOrAfter() != null ? 533 subCm.getSubjectConfirmationData().getNotOnOrAfter().toDate() 534 : null; 535 536 nbf = subCm.getSubjectConfirmationData().getNotBefore() != null ? 537 subCm.getSubjectConfirmationData().getNotBefore().toDate() 538 : null; 539 } 540 } 541 542 // Assertion : ID 543 if (assertion.getID() == null) { 544 throw new ParseException("Missing Assertion ID attribute"); 545 } 546 547 final Identifier id = new Identifier(assertion.getID()); 548 549 // Assertion : IssueInstant 550 if (assertion.getIssueInstant() == null) { 551 throw new ParseException("Missing Assertion IssueInstant attribute"); 552 } 553 554 final Date iat = assertion.getIssueInstant().toDate(); 555 556 // Assertion > Subject > SubjectConfirmation > SubjectConfirmationData > Address 557 InetAddress clientAddress = null; 558 559 for (SubjectConfirmation subCm: subCms) { 560 if (subCm.getSubjectConfirmationData() != null && subCm.getSubjectConfirmationData().getAddress() != null) { 561 try { 562 clientAddress = InetAddress.getByName(subCm.getSubjectConfirmationData().getAddress()); 563 } catch (UnknownHostException e) { 564 throw new ParseException("Invalid Address: " + e.getMessage(), e); 565 } 566 } 567 } 568 569 // Assertion > AttributeStatement > Attribute (: Name, > AttributeValue) 570 Map<String,List<String>> attrStatement = null; 571 572 if (CollectionUtils.isNotEmpty(assertion.getAttributeStatements())) { 573 574 attrStatement = new HashMap<>(); 575 576 for (AttributeStatement attrStmt: assertion.getAttributeStatements()) { 577 if (attrStmt == null) { 578 continue; // skip 579 } 580 581 for (Attribute attr: attrStmt.getAttributes()) { 582 String name = attr.getName(); 583 List<String> values = new LinkedList<>(); 584 for (XMLObject v: attr.getAttributeValues()) { 585 values.add(v.getDOM().getTextContent()); 586 } 587 attrStatement.put(name, values); 588 } 589 } 590 } 591 592 return new SAML2AssertionDetails(issuer, subject, subjectFormat, subjectAuthTime, subjectACR, 593 new ArrayList<>(audSet), exp, nbf, iat, id, clientAddress, attrStatement); 594 } 595}