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 * <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) 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}