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