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.openid.connect.sdk.claims; 019 020 021import java.net.URI; 022import java.net.URL; 023import java.util.*; 024 025import net.minidev.json.JSONArray; 026import net.minidev.json.JSONAware; 027import net.minidev.json.JSONObject; 028 029import com.nimbusds.jwt.JWTClaimsSet; 030import com.nimbusds.jwt.util.DateUtils; 031import com.nimbusds.langtag.LangTag; 032import com.nimbusds.langtag.LangTagUtils; 033import com.nimbusds.oauth2.sdk.ParseException; 034import com.nimbusds.oauth2.sdk.id.Audience; 035import com.nimbusds.oauth2.sdk.id.Issuer; 036import com.nimbusds.oauth2.sdk.util.JSONObjectUtils; 037 038 039/** 040 * Claims set with basic getters and setters, serialisable to a JSON object. 041 */ 042public class ClaimsSet implements JSONAware { 043 044 045 /** 046 * The issuer claim name. 047 */ 048 public static final String ISS_CLAIM_NAME = "iss"; 049 050 051 /** 052 * The audience claim name. 053 */ 054 public static final String AUD_CLAIM_NAME = "aud"; 055 056 057 /** 058 * The names of the standard top-level claims. 059 */ 060 private static final Set<String> STD_CLAIM_NAMES = Collections.unmodifiableSet( 061 new HashSet<>(Arrays.asList( 062 ISS_CLAIM_NAME, 063 AUD_CLAIM_NAME 064 ))); 065 066 067 /** 068 * Gets the names of the standard top-level claims. 069 * 070 * @return The names of the standard top-level claims (read-only set). 071 */ 072 public static Set<String> getStandardClaimNames() { 073 074 return STD_CLAIM_NAMES; 075 } 076 077 078 /** 079 * The JSON object representation of the claims set. 080 */ 081 protected final JSONObject claims; 082 083 084 /** 085 * Creates a new empty claims set. 086 */ 087 public ClaimsSet() { 088 089 claims = new JSONObject(); 090 } 091 092 093 /** 094 * Creates a new claims set from the specified JSON object. 095 * 096 * @param jsonObject The JSON object. Must not be {@code null}. 097 */ 098 public ClaimsSet(final JSONObject jsonObject) { 099 100 if (jsonObject == null) 101 throw new IllegalArgumentException("The JSON object must not be null"); 102 103 claims = jsonObject; 104 } 105 106 107 /** 108 * Puts all claims from the specified other claims set. 109 * 110 * @param other The other claims set. Must not be {@code null}. 111 */ 112 public void putAll(final ClaimsSet other) { 113 114 putAll(other.claims); 115 } 116 117 118 /** 119 * Puts all claims from the specified map. 120 * 121 * @param claims The claims to put. Must not be {@code null}. 122 */ 123 public void putAll(final Map<String,Object> claims) { 124 125 this.claims.putAll(claims); 126 } 127 128 129 /** 130 * Gets a claim. 131 * 132 * @param name The claim name. Must not be {@code null}. 133 * 134 * @return The claim value, {@code null} if not specified. 135 */ 136 public Object getClaim(final String name) { 137 138 return claims.get(name); 139 } 140 141 142 /** 143 * Gets a claim that casts to the specified class. 144 * 145 * @param name The claim name. Must not be {@code null}. 146 * @param clazz The Java class that the claim value should cast to. 147 * Must not be {@code null}. 148 * 149 * @return The claim value, {@code null} if not specified or casting 150 * failed. 151 */ 152 public <T> T getClaim(final String name, final Class<T> clazz) { 153 154 try { 155 return JSONObjectUtils.getGeneric(claims, name, clazz); 156 } catch (ParseException e) { 157 return null; 158 } 159 } 160 161 162 /** 163 * Returns a map of all instances, including language-tagged, of a 164 * claim with the specified base name. 165 * 166 * <p>Example JSON serialised claims set: 167 * 168 * <pre> 169 * { 170 * "month" : "January", 171 * "month#de" : "Januar" 172 * "month#es" : "enero", 173 * "month#it" : "gennaio" 174 * } 175 * </pre> 176 * 177 * <p>The "month" claim instances as java.util.Map: 178 * 179 * <pre> 180 * null = "January" (no language tag) 181 * "de" = "Januar" 182 * "es" = "enero" 183 * "it" = "gennaio" 184 * </pre> 185 * 186 * @param name The claim name. Must not be {@code null}. 187 * @param clazz The Java class that the claim values should cast to. 188 * Must not be {@code null}. 189 * 190 * @return The matching language-tagged claim values, empty map if 191 * none. A {@code null} key indicates the value has no language 192 * tag (corresponds to the base name). 193 */ 194 public <T> Map<LangTag,T> getLangTaggedClaim(final String name, final Class<T> clazz) { 195 196 Map<LangTag,Object> matches = LangTagUtils.find(name, claims); 197 Map<LangTag,T> out = new HashMap<>(); 198 199 for (Map.Entry<LangTag,Object> entry: matches.entrySet()) { 200 201 LangTag langTag = entry.getKey(); 202 String compositeKey = name + (langTag != null ? "#" + langTag : ""); 203 204 try { 205 out.put(langTag, JSONObjectUtils.getGeneric(claims, compositeKey, clazz)); 206 } catch (ParseException e) { 207 // skip 208 } 209 } 210 211 return out; 212 } 213 214 215 /** 216 * Sets a claim. 217 * 218 * @param name The claim name, with an optional language tag. Must not 219 * be {@code null}. 220 * @param value The claim value. Should serialise to a JSON entity. If 221 * {@code null} any existing claim with the same name will 222 * be removed. 223 */ 224 public void setClaim(final String name, final Object value) { 225 226 if (value != null) 227 claims.put(name, value); 228 else 229 claims.remove(name); 230 } 231 232 233 /** 234 * Sets a claim with an optional language tag. 235 * 236 * @param name The claim name. Must not be {@code null}. 237 * @param value The claim value. Should serialise to a JSON entity. 238 * If {@code null} any existing claim with the same name 239 * and language tag (if any) will be removed. 240 * @param langTag The language tag of the claim value, {@code null} if 241 * not tagged. 242 */ 243 public void setClaim(final String name, final Object value, final LangTag langTag) { 244 245 String keyName = langTag != null ? name + "#" + langTag : name; 246 setClaim(keyName, value); 247 } 248 249 250 /** 251 * Gets a string-based claim. 252 * 253 * @param name The claim name. Must not be {@code null}. 254 * 255 * @return The claim value, {@code null} if not specified or casting 256 * failed. 257 */ 258 public String getStringClaim(final String name) { 259 260 try { 261 return JSONObjectUtils.getString(claims, name, null); 262 } catch (ParseException e) { 263 return null; 264 } 265 } 266 267 268 /** 269 * Gets a string-based claim with an optional language tag. 270 * 271 * @param name The claim name. Must not be {@code null}. 272 * @param langTag The language tag of the claim value, {@code null} to 273 * get the non-tagged value. 274 * 275 * @return The claim value, {@code null} if not specified or casting 276 * failed. 277 */ 278 public String getStringClaim(final String name, final LangTag langTag) { 279 280 return langTag == null ? getStringClaim(name) : getStringClaim(name + '#' + langTag); 281 } 282 283 284 /** 285 * Gets a boolean-based claim. 286 * 287 * @param name The claim name. Must not be {@code null}. 288 * 289 * @return The claim value, {@code null} if not specified or casting 290 * failed. 291 */ 292 public Boolean getBooleanClaim(final String name) { 293 294 try { 295 return JSONObjectUtils.getBoolean(claims, name); 296 } catch (ParseException e) { 297 return null; 298 } 299 } 300 301 302 /** 303 * Gets a number-based claim. 304 * 305 * @param name The claim name. Must not be {@code null}. 306 * 307 * @return The claim value, {@code null} if not specified or casting 308 * failed. 309 */ 310 public Number getNumberClaim(final String name) { 311 312 try { 313 return JSONObjectUtils.getNumber(claims, name); 314 } catch (ParseException e) { 315 return null; 316 } 317 } 318 319 320 /** 321 * Gets an URL string based claim. 322 * 323 * @param name The claim name. Must not be {@code null}. 324 * 325 * @return The claim value, {@code null} if not specified or parsing 326 * failed. 327 */ 328 public URL getURLClaim(final String name) { 329 330 try { 331 return JSONObjectUtils.getURL(claims, name); 332 } catch (ParseException e) { 333 return null; 334 } 335 } 336 337 338 /** 339 * Sets an URL string based claim. 340 * 341 * @param name The claim name. Must not be {@code null}. 342 * @param value The claim value. If {@code null} any existing claim 343 * with the same name will be removed. 344 */ 345 public void setURLClaim(final String name, final URL value) { 346 347 if (value != null) 348 setClaim(name, value.toString()); 349 else 350 claims.remove(name); 351 } 352 353 354 /** 355 * Gets an URI string based claim. 356 * 357 * @param name The claim name. Must not be {@code null}. 358 * 359 * @return The claim value, {@code null} if not specified or parsing 360 * failed. 361 */ 362 public URI getURIClaim(final String name) { 363 364 try { 365 return JSONObjectUtils.getURI(claims, name, null); 366 } catch (ParseException e) { 367 return null; 368 } 369 } 370 371 372 /** 373 * Sets an URI string based claim. 374 * 375 * @param name The claim name. Must not be {@code null}. 376 * @param value The claim value. If {@code null} any existing claim 377 * with the same name will be removed. 378 */ 379 public void setURIClaim(final String name, final URI value) { 380 381 if (value != null) 382 setClaim(name, value.toString()); 383 else 384 claims.remove(name); 385 } 386 387 388 /** 389 * Gets a date / time based claim, represented as the number of seconds 390 * from 1970-01-01T0:0:0Z as measured in UTC until the date / time. 391 * 392 * @param name The claim name. Must not be {@code null}. 393 * 394 * @return The claim value, {@code null} if not specified or parsing 395 * failed. 396 */ 397 public Date getDateClaim(final String name) { 398 399 try { 400 return DateUtils.fromSecondsSinceEpoch(JSONObjectUtils.getNumber(claims, name).longValue()); 401 } catch (Exception e) { 402 return null; 403 } 404 } 405 406 407 /** 408 * Sets a date / time based claim, represented as the number of seconds 409 * from 1970-01-01T0:0:0Z as measured in UTC until the date / time. 410 * 411 * @param name The claim name. Must not be {@code null}. 412 * @param value The claim value. If {@code null} any existing claim 413 * with the same name will be removed. 414 */ 415 public void setDateClaim(final String name, final Date value) { 416 417 if (value != null) 418 setClaim(name, DateUtils.toSecondsSinceEpoch(value)); 419 else 420 claims.remove(name); 421 } 422 423 424 /** 425 * Gets a string list based claim. 426 * 427 * @param name The claim name. Must not be {@code null}. 428 * 429 * @return The claim value, {@code null} if not specified or parsing 430 * failed. 431 */ 432 public List<String> getStringListClaim(final String name) { 433 434 try { 435 return JSONObjectUtils.getStringList(claims, name); 436 } catch (ParseException e) { 437 return null; 438 } 439 } 440 441 442 /** 443 * Gets a JSON object based claim. 444 * 445 * @param name The claim name. Must not be {@code null}. 446 * 447 * @return The claim value, {@code null} if not specified or parsing 448 * failed. 449 */ 450 public JSONObject getJSONObjectClaim(final String name) { 451 452 try { 453 return JSONObjectUtils.getJSONObject(claims, name); 454 } catch (ParseException e) { 455 return null; 456 } 457 } 458 459 460 /** 461 * Gets a JSON array based claim. 462 * 463 * @param name The claim name. Must not be {@code null}. 464 * 465 * @return The claim value, {@code null} if not specified or parsing 466 * failed. 467 */ 468 public JSONArray getJSONArrayClaim(final String name) { 469 470 try { 471 return JSONObjectUtils.getJSONArray(claims, name); 472 } catch (ParseException e) { 473 return null; 474 } 475 } 476 477 478 /** 479 * Gets the issuer. Corresponds to the {@code iss} claim. 480 * 481 * @return The issuer, {@code null} if not specified. 482 */ 483 public Issuer getIssuer() { 484 485 String iss = getStringClaim(ISS_CLAIM_NAME); 486 487 return iss != null ? new Issuer(iss) : null; 488 } 489 490 491 /** 492 * Sets the issuer. Corresponds to the {@code iss} claim. 493 * 494 * @param iss The issuer, {@code null} if not specified. 495 */ 496 public void setIssuer(final Issuer iss) { 497 498 if (iss != null) 499 setClaim(ISS_CLAIM_NAME, iss.getValue()); 500 else 501 setClaim(ISS_CLAIM_NAME, null); 502 } 503 504 505 /** 506 * Gets the audience. Corresponds to the {@code aud} claim. 507 * 508 * @return The audience, {@code null} if not specified. 509 */ 510 public List<Audience> getAudience() { 511 512 if (getClaim(AUD_CLAIM_NAME) instanceof String) { 513 // Special case - aud is a string 514 return new Audience(getStringClaim(AUD_CLAIM_NAME)).toSingleAudienceList(); 515 } 516 517 // General case - JSON string array 518 List<String> rawList = getStringListClaim(AUD_CLAIM_NAME); 519 520 if (rawList == null) { 521 return null; 522 } 523 524 List<Audience> audList = new ArrayList<>(rawList.size()); 525 526 for (String s: rawList) 527 audList.add(new Audience(s)); 528 529 return audList; 530 } 531 532 533 /** 534 * Sets the audience. Corresponds to the {@code aud} claim. 535 * 536 * @param aud The audience, {@code null} if not specified. 537 */ 538 public void setAudience(final Audience aud) { 539 540 if (aud != null) 541 setAudience(aud.toSingleAudienceList()); 542 else 543 setClaim(AUD_CLAIM_NAME, null); 544 } 545 546 547 /** 548 * Sets the audience list. Corresponds to the {@code aud} claim. 549 * 550 * @param audList The audience list, {@code null} if not specified. 551 */ 552 public void setAudience(final List<Audience> audList) { 553 554 if (audList != null) 555 setClaim(AUD_CLAIM_NAME, Audience.toStringList(audList)); 556 else 557 setClaim(AUD_CLAIM_NAME, null); 558 } 559 560 561 /** 562 * Gets the JSON object representation of this claims set. 563 * 564 * <p>Example: 565 * 566 * <pre> 567 * { 568 * "country" : "USA", 569 * "country#en" : "USA", 570 * "country#de_DE" : "Vereinigte Staaten", 571 * "country#fr_FR" : "Etats Unis" 572 * } 573 * </pre> 574 * 575 * @return The JSON object representation. 576 */ 577 public JSONObject toJSONObject() { 578 579 JSONObject out = new JSONObject(); 580 out.putAll(claims); 581 return out; 582 } 583 584 585 @Override 586 public String toJSONString() { 587 return toJSONObject().toJSONString(); 588 } 589 590 591 /** 592 * Gets the JSON Web Token (JWT) claims set for this claim set. 593 * 594 * @return The JWT claims set. 595 * 596 * @throws ParseException If the conversion to a JWT claims set fails. 597 */ 598 public JWTClaimsSet toJWTClaimsSet() 599 throws ParseException { 600 601 try { 602 // Parse from JSON string to handle nested JSONArray & JSONObject properly 603 // Work around https://bitbucket.org/connect2id/nimbus-jose-jwt/issues/347/revise-nested-jsonarray-and-jsonobject 604 return JWTClaimsSet.parse(claims.toJSONString()); 605 606 } catch (java.text.ParseException e) { 607 608 throw new ParseException(e.getMessage(), e); 609 } 610 } 611 612 613 @Override 614 public boolean equals(Object o) { 615 if (this == o) return true; 616 if (!(o instanceof ClaimsSet)) return false; 617 ClaimsSet claimsSet = (ClaimsSet) o; 618 return claims.equals(claimsSet.claims); 619 } 620 621 622 @Override 623 public int hashCode() { 624 return Objects.hash(claims); 625 } 626 627 628 @Override 629 public String toString() { 630 return toJSONString(); 631 } 632}