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.validators;
019
020
021import java.util.List;
022
023import com.nimbusds.jose.proc.SecurityContext;
024import com.nimbusds.jwt.JWTClaimsSet;
025import com.nimbusds.jwt.proc.BadJWTException;
026import com.nimbusds.jwt.proc.JWTClaimsSetVerifier;
027import com.nimbusds.oauth2.sdk.ParseException;
028import com.nimbusds.oauth2.sdk.id.ClientID;
029import com.nimbusds.oauth2.sdk.id.Issuer;
030import com.nimbusds.oauth2.sdk.util.JSONObjectUtils;
031import com.nimbusds.openid.connect.sdk.claims.LogoutTokenClaimsSet;
032import net.jcip.annotations.ThreadSafe;
033import net.minidev.json.JSONObject;
034
035
036/**
037 * ID token claims verifier.
038 *
039 * <p>Related specifications:
040 *
041 * <ul>
042 *     <li>OpenID Connect Back-Channel Logout 1.0, section 2.6 (draft 04).
043 * </ul>
044 */
045@ThreadSafe
046public class LogoutTokenClaimsVerifier implements JWTClaimsSetVerifier {
047        
048        
049        /**
050         * The expected logout token issuer.
051         */
052        private final Issuer expectedIssuer;
053
054
055        /**
056         * The requesting client.
057         */
058        private final ClientID expectedClientID;
059        
060        
061        /**
062         * Creates a new logout token claims verifier.
063         *
064         * @param issuer   The expected ID token issuer. Must not be
065         *                 {@code null}.
066         * @param clientID The client ID. Must not be {@code null}. or positive
067         *                 integer.
068         */
069        public LogoutTokenClaimsVerifier(final Issuer issuer,
070                                         final ClientID clientID) {
071                
072                if (issuer == null) {
073                        throw new IllegalArgumentException("The expected ID token issuer must not be null");
074                }
075                this.expectedIssuer = issuer;
076                
077                if (clientID == null) {
078                        throw new IllegalArgumentException("The client ID must not be null");
079                }
080                this.expectedClientID = clientID;
081        }
082        
083        
084        /**
085         * Returns the expected ID token issuer.
086         *
087         * @return The ID token issuer.
088         */
089        public Issuer getExpectedIssuer() {
090                
091                return expectedIssuer;
092        }
093        
094        
095        /**
096         * Returns the client ID for verifying the ID token audience.
097         *
098         * @return The client ID.
099         */
100        public ClientID getClientID() {
101                
102                return expectedClientID;
103        }
104        
105        
106        @Override
107        public void verify(final JWTClaimsSet claimsSet, final SecurityContext ctx)
108                throws BadJWTException {
109                
110                // See http://openid.net/specs/openid-connect-backchannel-1_0-ID1.html#Validation
111                
112                // Check event type
113                try {
114                        JSONObject events = claimsSet.getJSONObjectClaim("events");
115                        
116                        if (events == null) {
117                                throw new BadJWTException("Missing JWT events (events) claim");
118                        }
119                        
120                        if (JSONObjectUtils.getJSONObject(events, LogoutTokenClaimsSet.EVENT_TYPE, null) == null) {
121                                throw new BadJWTException("Missing event type, required " + LogoutTokenClaimsSet.EVENT_TYPE);
122                        }
123                        
124                } catch (java.text.ParseException | ParseException e) {
125                        throw new BadJWTException("Invalid JWT events (events) claim");
126                }
127                
128                
129                // Check required claims, match them with expected where needed
130                
131                final String tokenIssuer = claimsSet.getIssuer();
132                
133                if (tokenIssuer == null) {
134                        throw BadJWTExceptions.MISSING_ISS_CLAIM_EXCEPTION;
135                }
136                
137                if (! getExpectedIssuer().getValue().equals(tokenIssuer)) {
138                        throw new BadJWTException("Unexpected JWT issuer: " + tokenIssuer);
139                }
140                
141                final List<String> tokenAudience = claimsSet.getAudience();
142                
143                if (tokenAudience == null || tokenAudience.isEmpty()) {
144                        throw BadJWTExceptions.MISSING_AUD_CLAIM_EXCEPTION;
145                }
146                
147                if (! tokenAudience.contains(expectedClientID.getValue())) {
148                        throw new BadJWTException("Unexpected JWT audience: " + tokenAudience);
149                }
150                
151                
152                if (claimsSet.getIssueTime() == null) {
153                        throw BadJWTExceptions.MISSING_IAT_CLAIM_EXCEPTION;
154                }
155                
156                if (claimsSet.getJWTID() == null) {
157                        throw new BadJWTException("Missing JWT ID (jti) claim");
158                }
159                
160                
161                // Either sub or sid must be present
162                try {
163                        if (claimsSet.getSubject() == null && claimsSet.getStringClaim("sid") == null) {
164                                throw new BadJWTException("Missing subject (sub) and / or session ID (sid) claim(s)");
165                        }
166                        
167                } catch (java.text.ParseException e) {
168                        throw new BadJWTException("Invalid session ID (sid) claim");
169                }
170                
171                // Nonce illegal
172                if (claimsSet.getClaim("nonce") != null) {
173                        throw new BadJWTException("Found illegal nonce (nonce) claim");
174                }
175        }
176}