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.util.*; 022 023import com.nimbusds.jwt.JWTClaimsSet; 024import com.nimbusds.oauth2.sdk.ParseException; 025import com.nimbusds.oauth2.sdk.id.Audience; 026import com.nimbusds.oauth2.sdk.id.Issuer; 027import com.nimbusds.oauth2.sdk.id.JWTID; 028import com.nimbusds.oauth2.sdk.id.Subject; 029import com.nimbusds.oauth2.sdk.util.JSONObjectUtils; 030import net.minidev.json.JSONArray; 031import net.minidev.json.JSONObject; 032 033 034/** 035 * Back-channel logout token claims set, serialisable to a JSON object. 036 * 037 * <p>Example logout token claims set: 038 * 039 * <pre>o 040 * { 041 * "iss" : "https://server.example.com", 042 * "sub" : "248289761001", 043 * "aud" : "s6BhdRkqt3", 044 * "iat" : 1471566154, 045 * "jti" : "bWJq", 046 * "sid" : "08a5019c-17e1-4977-8f42-65a12843ea02", 047 * "events" : { "http://schemas.openid.net/event/backchannel-logout": { } } 048 * } 049 * </pre> 050 * 051 * <p>Related specifications: 052 * 053 * <ul> 054 * <li>OpenID Connect Back-Channel Logout 1.0, section 2.4 (draft 04). 055 * <li>Security Event Token (SET) (RFC 8417) 056 * </ul> 057 */ 058public class LogoutTokenClaimsSet extends CommonClaimsSet { 059 060 061 /** 062 * The JWT ID claim name. 063 */ 064 public static final String JTI_CLAIM_NAME = "jti"; 065 066 067 /** 068 * The events claim name. 069 */ 070 public static final String EVENTS_CLAIM_NAME = "events"; 071 072 073 /** 074 * The OpenID logout event type. 075 */ 076 public static final String EVENT_TYPE = "http://schemas.openid.net/event/backchannel-logout"; 077 078 079 /** 080 * The names of the standard top-level ID token claims. 081 */ 082 private static final Set<String> stdClaimNames = new LinkedHashSet<>(); 083 084 085 static { 086 stdClaimNames.add(ISS_CLAIM_NAME); 087 stdClaimNames.add(SUB_CLAIM_NAME); 088 stdClaimNames.add(AUD_CLAIM_NAME); 089 stdClaimNames.add(IAT_CLAIM_NAME); 090 stdClaimNames.add(JTI_CLAIM_NAME); 091 stdClaimNames.add(EVENTS_CLAIM_NAME); 092 stdClaimNames.add(SID_CLAIM_NAME); 093 } 094 095 096 /** 097 * Gets the names of the standard top-level logout token claims. 098 * 099 * @return The names of the standard top-level logout token claims 100 * (read-only set). 101 */ 102 public static Set<String> getStandardClaimNames() { 103 104 return Collections.unmodifiableSet(stdClaimNames); 105 } 106 107 108 /** 109 * Creates a new logout token claims set. Either the subject or the 110 * session ID must be set, or both. 111 * 112 * @param iss The issuer. Must not be {@code null}. 113 * @param sub The subject. Must not be {@code null} unless the session 114 * ID is set. 115 * @param aud The audience. Must not be {@code null}. 116 * @param iat The issue time. Must not be {@code null}. 117 * @param jti The JWT ID. Must not be {@code null}. 118 * @param sid The session ID. Must not be {@code null} unless the 119 * subject is set. 120 */ 121 public LogoutTokenClaimsSet(final Issuer iss, 122 final Subject sub, 123 final List<Audience> aud, 124 final Date iat, 125 final JWTID jti, 126 final SessionID sid) { 127 128 if (sub == null && sid == null) { 129 throw new IllegalArgumentException("Either the subject or the session ID must be set, or both"); 130 } 131 132 setClaim(ISS_CLAIM_NAME, iss.getValue()); 133 134 if (sub != null) { 135 setClaim(SUB_CLAIM_NAME, sub.getValue()); 136 } 137 138 JSONArray audList = new JSONArray(); 139 140 for (Audience a: aud) 141 audList.add(a.getValue()); 142 143 setClaim(AUD_CLAIM_NAME, audList); 144 145 setDateClaim(IAT_CLAIM_NAME, iat); 146 147 setClaim(JTI_CLAIM_NAME, jti.getValue()); 148 149 JSONObject events = new JSONObject(); 150 events.put(EVENT_TYPE, new JSONObject()); 151 setClaim(EVENTS_CLAIM_NAME, events); 152 153 if (sid != null) { 154 setClaim(SID_CLAIM_NAME, sid.getValue()); 155 } 156 } 157 158 159 /** 160 * Creates a new logout token claims set from the specified JSON 161 * object. 162 * 163 * @param jsonObject The JSON object. Must be verified to represent a 164 * valid logout token claims set and not be 165 * {@code null}. 166 * 167 * @throws ParseException If the JSON object doesn't represent a valid 168 * logout token claims set. 169 */ 170 private LogoutTokenClaimsSet(final JSONObject jsonObject) 171 throws ParseException { 172 173 super(jsonObject); 174 175 if (getStringClaim(ISS_CLAIM_NAME) == null) 176 throw new ParseException("Missing or invalid \"iss\" claim"); 177 178 if (getStringClaim(SUB_CLAIM_NAME) == null && getStringClaim(SID_CLAIM_NAME) == null) 179 throw new ParseException("Missing or invalid \"sub\" and / or \"sid\" claim(s)"); 180 181 if (getStringClaim(AUD_CLAIM_NAME) == null && getStringListClaim(AUD_CLAIM_NAME) == null || 182 getStringListClaim(AUD_CLAIM_NAME) != null && getStringListClaim(AUD_CLAIM_NAME).isEmpty()) 183 throw new ParseException("Missing or invalid \"aud\" claim"); 184 185 if (getDateClaim(IAT_CLAIM_NAME) == null) 186 throw new ParseException("Missing or invalid \"iat\" claim"); 187 188 if (getStringClaim(JTI_CLAIM_NAME) == null) 189 throw new ParseException("Missing or invalid \"jti\" claim"); 190 191 if (getClaim(EVENTS_CLAIM_NAME) == null) 192 throw new ParseException("Missing or invalid \"events\" claim"); 193 194 JSONObject events = getClaim(EVENTS_CLAIM_NAME, JSONObject.class); 195 196 if (JSONObjectUtils.getJSONObject(events, EVENT_TYPE, null) == null) { 197 throw new ParseException("Missing event type " + EVENT_TYPE); 198 } 199 200 if (jsonObject.containsKey("nonce")) { 201 throw new ParseException("Nonce is prohibited"); 202 } 203 } 204 205 206 /** 207 * Creates a new logout token claims set from the specified JSON Web 208 * Token (JWT) claims set. 209 * 210 * @param jwtClaimsSet The JWT claims set. Must not be {@code null}. 211 * 212 * @throws ParseException If the JWT claims set doesn't represent a 213 * valid logout token claims set. 214 */ 215 public LogoutTokenClaimsSet(final JWTClaimsSet jwtClaimsSet) 216 throws ParseException { 217 218 this(jwtClaimsSet.toJSONObject()); 219 } 220 221 222 /** 223 * Gets the JWT ID. Corresponds to the {@code jti} claim. 224 * 225 * @return The JWT ID. 226 */ 227 public JWTID getJWTID() { 228 229 return new JWTID(getStringClaim(JTI_CLAIM_NAME)); 230 } 231 232 233 @Override 234 public JSONObject toJSONObject() { 235 236 if (getClaim("nonce") != null) { 237 throw new IllegalStateException("Nonce is prohibited"); 238 } 239 240 return super.toJSONObject(); 241 } 242 243 244 @Override 245 public JWTClaimsSet toJWTClaimsSet() 246 throws ParseException { 247 248 if (getClaim("nonce") != null) { 249 throw new ParseException("Nonce is prohibited"); 250 } 251 252 return super.toJWTClaimsSet(); 253 } 254 255 256 /** 257 * Parses a logout token claims set from the specified JSON object 258 * string. 259 * 260 * @param json The JSON object string to parse. Must not be 261 * {@code null}. 262 * 263 * @return The logout token claims set. 264 * 265 * @throws ParseException If parsing failed. 266 */ 267 public static LogoutTokenClaimsSet parse(final String json) 268 throws ParseException { 269 270 JSONObject jsonObject = JSONObjectUtils.parse(json); 271 272 try { 273 return new LogoutTokenClaimsSet(jsonObject); 274 275 } catch (IllegalArgumentException e) { 276 277 throw new ParseException(e.getMessage(), e); 278 } 279 } 280}