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