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.URISyntaxException;
023import java.util.*;
024
025import net.minidev.json.JSONObject;
026
027import com.nimbusds.jwt.JWT;
028import com.nimbusds.jwt.JWTClaimsSet;
029import com.nimbusds.jwt.JWTParser;
030import com.nimbusds.oauth2.sdk.ParseException;
031import com.nimbusds.oauth2.sdk.id.Subject;
032import com.nimbusds.oauth2.sdk.token.AccessToken;
033import com.nimbusds.oauth2.sdk.token.TypelessAccessToken;
034import com.nimbusds.oauth2.sdk.util.JSONObjectUtils;
035import com.nimbusds.openid.connect.sdk.assurance.claims.VerifiedClaimsSet;
036
037
038/**
039 * UserInfo claims set, serialisable to a JSON object.
040 *
041 * <p>Supports normal, aggregated and distributed claims.
042 *
043 * <p>Example UserInfo claims set:
044 *
045 * <pre>
046 * {
047 *   "sub"                : "248289761001",
048 *   "name"               : "Jane Doe",
049 *   "given_name"         : "Jane",
050 *   "family_name"        : "Doe",
051 *   "preferred_username" : "j.doe",
052 *   "email"              : "[email protected]",
053 *   "picture"            : "http://example.com/janedoe/me.jpg"
054 * }
055 * </pre>
056 *
057 * <p>Related specifications:
058 *
059 * <ul>
060 *     <li>OpenID Connect Core 1.0, sections 5.1 and 5.6.
061 *     <li>OpenID Connect for Identity Assurance 1.0, section 3.1.
062 * </ul>
063 */
064public class UserInfo extends PersonClaims {
065
066
067        /**
068         * The subject claim name.
069         */
070        public static final String SUB_CLAIM_NAME = "sub";
071        
072        
073        
074        /**
075         * The verified claims claim name.
076         */
077        public static final String VERIFIED_CLAIMS_CLAIM_NAME = "verified_claims";
078        
079        
080        /**
081         * Gets the names of the standard top-level UserInfo claims.
082         *
083         * @return The names of the standard top-level UserInfo claims 
084         *         (read-only set).
085         */
086        public static Set<String> getStandardClaimNames() {
087        
088                Set<String> names = new HashSet<>(PersonClaims.getStandardClaimNames());
089                names.add(SUB_CLAIM_NAME);
090                names.add(VERIFIED_CLAIMS_CLAIM_NAME);
091                return Collections.unmodifiableSet(names);
092        }
093        
094        
095        /**
096         * Creates a new minimal UserInfo claims set.
097         *
098         * @param sub The subject. Must not be {@code null}.
099         */
100        public UserInfo(final Subject sub) {
101        
102                super();
103                setClaim(SUB_CLAIM_NAME, sub.getValue());
104        }
105
106
107        /**
108         * Creates a new UserInfo claims set from the specified JSON object.
109         *
110         * @param jsonObject The JSON object. Must not be {@code null}.
111         *
112         * @throws IllegalArgumentException If the JSON object doesn't contain
113         *                                  a subject {@code sub} string claim.
114         */
115        public UserInfo(final JSONObject jsonObject) {
116
117                super(jsonObject);
118
119                if (getStringClaim(SUB_CLAIM_NAME) == null)
120                        throw new IllegalArgumentException("Missing or invalid \"sub\" claim");
121        }
122
123
124        /**
125         * Creates a new UserInfo claims set from the specified JSON Web Token
126         * (JWT) claims set.
127         *
128         * @param jwtClaimsSet The JWT claims set. Must not be {@code null}.
129         *
130         * @throws IllegalArgumentException If the JWT claims set doesn't
131         *                                  contain a subject {@code sub}
132         *                                  string claim.
133         */
134        public UserInfo(final JWTClaimsSet jwtClaimsSet) {
135
136                this(jwtClaimsSet.toJSONObject());
137        }
138
139
140        /**
141         * Puts all claims from the specified other UserInfo claims set.
142         * Aggregated and distributed claims are properly merged.
143         *
144         * @param other The other UserInfo. Must have the same
145         *              {@link #getSubject subject}. Must not be {@code null}.
146         *
147         * @throws IllegalArgumentException If the other UserInfo claims set
148         *                                  doesn't have an identical subject,
149         *                                  or if the external claims source ID
150         *                                  of the other UserInfo matches an
151         *                                  existing source ID.
152         */
153        public void putAll(final UserInfo other) {
154
155                Subject otherSubject = other.getSubject();
156
157                if (otherSubject == null)
158                        throw new IllegalArgumentException("The subject of the other UserInfo is missing");
159
160                if (! otherSubject.equals(getSubject()))
161                        throw new IllegalArgumentException("The subject of the other UserInfo must be identical");
162                
163                // Save present aggregated and distributed claims, to prevent
164                // overwrite by put to claims JSON object
165                Set<AggregatedClaims> savedAggregatedClaims = getAggregatedClaims();
166                Set<DistributedClaims> savedDistributedClaims = getDistributedClaims();
167                
168                // Save other present aggregated and distributed claims
169                Set<AggregatedClaims> otherAggregatedClaims = other.getAggregatedClaims();
170                Set<DistributedClaims> otherDistributedClaims = other.getDistributedClaims();
171                
172                // Ensure external source IDs don't conflict during merge
173                Set<String> externalSourceIDs = new HashSet<>();
174                
175                if (savedAggregatedClaims != null) {
176                        for (AggregatedClaims ac: savedAggregatedClaims) {
177                                externalSourceIDs.add(ac.getSourceID());
178                        }
179                }
180                
181                if (savedDistributedClaims != null) {
182                        for (DistributedClaims dc: savedDistributedClaims) {
183                                externalSourceIDs.add(dc.getSourceID());
184                        }
185                }
186                
187                if (otherAggregatedClaims != null) {
188                        for (AggregatedClaims ac: otherAggregatedClaims) {
189                                if (externalSourceIDs.contains(ac.getSourceID())) {
190                                        throw new IllegalArgumentException("Aggregated claims source ID conflict: " + ac.getSourceID());
191                                }
192                        }
193                }
194                
195                if (otherDistributedClaims != null) {
196                        for (DistributedClaims dc: otherDistributedClaims) {
197                                if (externalSourceIDs.contains(dc.getSourceID())) {
198                                        throw new IllegalArgumentException("Distributed claims source ID conflict: " + dc.getSourceID());
199                                }
200                        }
201                }
202                
203                putAll((ClaimsSet)other);
204                
205                // Merge saved external claims, if any
206                if (savedAggregatedClaims != null) {
207                        for (AggregatedClaims ac: savedAggregatedClaims) {
208                                addAggregatedClaims(ac);
209                        }
210                }
211                
212                if (savedDistributedClaims != null) {
213                        for (DistributedClaims dc: savedDistributedClaims) {
214                                addDistributedClaims(dc);
215                        }
216                }
217        }
218        
219        
220        /**
221         * Gets the UserInfo subject. Corresponds to the {@code sub} claim.
222         *
223         * @return The subject.
224         */
225        public Subject getSubject() {
226        
227                return new Subject(getStringClaim(SUB_CLAIM_NAME));
228        }
229        
230        
231        /**
232         * Gets the verified claims. Corresponds to the {@code verified_claims}
233         * claim from OpenID Connect for Identity Assurance 1.0.
234         *
235         * @return List of the verified claims sets, {@code null} if not
236         *         specified or parsing failed.
237         */
238        public List<VerifiedClaimsSet> getVerifiedClaims() {
239                
240                // Try JSON object first
241                Object value = getClaim(VERIFIED_CLAIMS_CLAIM_NAME);
242                
243                if (value instanceof JSONObject) {
244                        
245                        // Single verified_claims
246                        try {
247                                return Collections.singletonList(VerifiedClaimsSet.parse((JSONObject)value));
248                        } catch (ParseException e) {
249                                return null;
250                        }
251                        
252                } else if (value instanceof List) {
253                        
254                        // JSON array of verified_claims
255                        
256                        List<?> rawList = (List<?>)value;
257                        
258                        if (rawList.isEmpty()) {
259                                return null;
260                        }
261                        
262                        List<VerifiedClaimsSet> list = new LinkedList<>();
263                        
264                        for (Object item : rawList) {
265                                if (item instanceof JSONObject) {
266                                        try {
267                                                list.add(VerifiedClaimsSet.parse((JSONObject) item));
268                                        } catch (ParseException e) {
269                                                return null;
270                                        }
271                                } else {
272                                        return null;
273                                }
274                        }
275                        
276                        return list;
277                } else {
278                        // Invalid
279                        return null;
280                }
281        }
282        
283        
284        /**
285         * Sets the verified claims. Corresponds to the {@code verified_claims}
286         * claim from OpenID Connect for Identity Assurance 1.0.
287         *
288         * @param verifiedClaims The verified claims set, {@code null} if not
289         *                       specified.
290         */
291        public void setVerifiedClaims(final VerifiedClaimsSet verifiedClaims) {
292                
293                if (verifiedClaims != null) {
294                        setClaim(VERIFIED_CLAIMS_CLAIM_NAME, verifiedClaims.toJSONObject());
295                } else {
296                        setClaim(VERIFIED_CLAIMS_CLAIM_NAME, null);
297                }
298        }
299        
300        
301        /**
302         * Sets a list of verified claims with separate verifications.
303         * Corresponds to the {@code verified_claims} claim from OpenID Connect
304         * for Identity Assurance 1.0.
305         *
306         * @param verifiedClaimsList List of the verified claims sets,
307         *                           {@code null} if not specified or parsing
308         *                           failed.
309         */
310        public void setVerifiedClaims(final List<VerifiedClaimsSet> verifiedClaimsList) {
311                
312                if (verifiedClaimsList != null) {
313                        List<JSONObject> jsonObjects = new LinkedList<>();
314                        for (VerifiedClaimsSet verifiedClaims: verifiedClaimsList) {
315                                if (verifiedClaims != null) {
316                                        jsonObjects.add(verifiedClaims.toJSONObject());
317                                }
318                        }
319                        setClaim(VERIFIED_CLAIMS_CLAIM_NAME, jsonObjects);
320                } else {
321                        setClaim(VERIFIED_CLAIMS_CLAIM_NAME, null);
322                }
323        }
324        
325        
326        /**
327         * Adds the specified aggregated claims provided by an external claims
328         * source.
329         *
330         * @param aggregatedClaims The aggregated claims instance, if
331         *                         {@code null} nothing will be added.
332         */
333        public void addAggregatedClaims(final AggregatedClaims aggregatedClaims) {
334                
335                if (aggregatedClaims == null) {
336                        return;
337                }
338                
339                aggregatedClaims.mergeInto(claims);
340        }
341        
342        
343        /**
344         * Gets the included aggregated claims provided by each external claims
345         * source.
346         *
347         * @return The aggregated claims, {@code null} if none are found.
348         */
349        public Set<AggregatedClaims> getAggregatedClaims() {
350        
351                Map<String,JSONObject> claimSources = ExternalClaimsUtils.getExternalClaimSources(claims);
352                
353                if (claimSources == null) {
354                        return null; // No external _claims_sources
355                }
356                
357                Set<AggregatedClaims> aggregatedClaimsSet = new HashSet<>();
358                
359                for (Map.Entry<String,JSONObject> en: claimSources.entrySet()) {
360                        
361                        String sourceID = en.getKey();
362                        JSONObject sourceSpec = en.getValue();
363                        
364                        Object jwtValue = sourceSpec.get("JWT");
365                        if (! (jwtValue instanceof String)) {
366                                continue; // skip
367                        }
368                        
369                        JWT claimsJWT;
370                        try {
371                                claimsJWT = JWTParser.parse((String)jwtValue);
372                        } catch (java.text.ParseException e) {
373                                continue; // invalid JWT, skip
374                        }
375                        
376                        Set<String> claimNames = ExternalClaimsUtils.getExternalClaimNamesForSource(claims, sourceID);
377                        
378                        if (claimNames.isEmpty()) {
379                                continue; // skip
380                        }
381                        
382                        aggregatedClaimsSet.add(new AggregatedClaims(sourceID, claimNames, claimsJWT));
383                }
384                
385                if (aggregatedClaimsSet.isEmpty()) {
386                        return null;
387                }
388                
389                return aggregatedClaimsSet;
390        }
391        
392        
393        /**
394         * Adds the specified distributed claims from an external claims source.
395         *
396         * @param distributedClaims The distributed claims instance, if
397         *                          {@code null} nothing will be added.
398         */
399        public void addDistributedClaims(final DistributedClaims distributedClaims) {
400                
401                if (distributedClaims == null) {
402                        return;
403                }
404                
405                distributedClaims.mergeInto(claims);
406        }
407        
408        
409        /**
410         * Gets the included distributed claims provided by each external
411         * claims source.
412         *
413         * @return The distributed claims, {@code null} if none are found.
414         */
415        public Set<DistributedClaims> getDistributedClaims() {
416                
417                Map<String,JSONObject> claimSources = ExternalClaimsUtils.getExternalClaimSources(claims);
418                
419                if (claimSources == null) {
420                        return null; // No external _claims_sources
421                }
422                
423                Set<DistributedClaims> distributedClaimsSet = new HashSet<>();
424                
425                for (Map.Entry<String,JSONObject> en: claimSources.entrySet()) {
426                        
427                        String sourceID = en.getKey();
428                        JSONObject sourceSpec = en.getValue();
429        
430                        Object endpointValue = sourceSpec.get("endpoint");
431                        if (! (endpointValue instanceof String)) {
432                                continue; // skip
433                        }
434                        
435                        URI endpoint;
436                        try {
437                                endpoint = new URI((String)endpointValue);
438                        } catch (URISyntaxException e) {
439                                continue; // invalid URI, skip
440                        }
441                        
442                        AccessToken accessToken = null;
443                        Object accessTokenValue = sourceSpec.get("access_token");
444                        if (accessTokenValue instanceof String) {
445                                accessToken = new TypelessAccessToken((String)accessTokenValue);
446                        }
447                        
448                        Set<String> claimNames = ExternalClaimsUtils.getExternalClaimNamesForSource(claims, sourceID);
449                        
450                        if (claimNames.isEmpty()) {
451                                continue; // skip
452                        }
453                        
454                        distributedClaimsSet.add(new DistributedClaims(sourceID, claimNames, endpoint, accessToken));
455                }
456                
457                if (distributedClaimsSet.isEmpty()) {
458                        return null;
459                }
460                
461                return distributedClaimsSet;
462        }
463        
464        
465        /**
466         * Parses a UserInfo claims set from the specified JSON object string.
467         *
468         * @param json The JSON object string to parse. Must not be
469         *             {@code null}.
470         *
471         * @return The UserInfo claims set.
472         *
473         * @throws ParseException If parsing failed.
474         */
475        public static UserInfo parse(final String json)
476                throws ParseException {
477
478                JSONObject jsonObject = JSONObjectUtils.parse(json);
479
480                try {
481                        return new UserInfo(jsonObject);
482
483                } catch (IllegalArgumentException e) {
484
485                        throw new ParseException(e.getMessage(), e);
486                }
487        }
488}