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.oauth2.sdk.jarm;
019
020
021import java.util.*;
022
023import com.nimbusds.jwt.*;
024import com.nimbusds.oauth2.sdk.AuthorizationResponse;
025import com.nimbusds.oauth2.sdk.ParseException;
026import com.nimbusds.oauth2.sdk.ResponseMode;
027import com.nimbusds.oauth2.sdk.as.AuthorizationServerMetadata;
028import com.nimbusds.oauth2.sdk.id.ClientID;
029import com.nimbusds.oauth2.sdk.id.Issuer;
030import com.nimbusds.oauth2.sdk.util.CollectionUtils;
031import com.nimbusds.oauth2.sdk.util.MultivaluedMapUtils;
032
033
034/**
035 * JWT Secured Authorization Response Mode for OAuth 2.0 (JARM) utilities.
036 */
037public final class JARMUtils {
038        
039        
040        /**
041         * The JARM response modes.
042         */
043        public static final Set<ResponseMode> RESPONSE_MODES = new HashSet<>(Arrays.asList(
044                ResponseMode.JWT,
045                ResponseMode.QUERY_JWT,
046                ResponseMode.FRAGMENT_JWT,
047                ResponseMode.FORM_POST_JWT
048        ));
049        
050        
051        /**
052         * Returns {@code true} if JARM is supported for the specified OpenID
053         * provider / Authorisation server metadata.
054         *
055         * @param asMetadata The OpenID provider / Authorisation server
056         *                   metadata. Must not be {@code null}.
057         *
058         * @return {@code true} if JARM is supported, else {@code false}.
059         */
060        public static boolean supportsJARM(final AuthorizationServerMetadata asMetadata) {
061                
062                if (CollectionUtils.isEmpty(asMetadata.getAuthorizationJWSAlgs())) {
063                        return false;
064                }
065                
066                if (CollectionUtils.isEmpty(asMetadata.getResponseModes())) {
067                        return false;
068                }
069                
070                for (ResponseMode responseMode: JARMUtils.RESPONSE_MODES) {
071                        if (asMetadata.getResponseModes().contains(responseMode)) {
072                                return true;
073                        }
074                }
075                
076                return false;
077        }
078        
079        
080        /**
081         * Creates a JSON Web Token (JWT) claims set for the specified
082         * authorisation success response.
083         *
084         * @param iss      The OAuth 2.0 authorisation server issuer. Must not
085         *                 be {@code null}.
086         * @param aud      The client ID. Must not be {@code null}.
087         * @param exp      The JWT expiration time. Must not be {@code null}.
088         * @param response The plain authorisation response to use its
089         *                 parameters. If it specifies an {@code iss} (issuer)
090         *                 parameter its value must match the JWT {@code iss}
091         *                 claim. Must not be {@code null}.
092         *
093         * @return The JWT claims set.
094         */
095        public static JWTClaimsSet toJWTClaimsSet(final Issuer iss,
096                                                  final ClientID aud,
097                                                  final Date exp,
098                                                  final AuthorizationResponse response) {
099        
100                if (exp == null) {
101                        throw new IllegalArgumentException("The expiration time must not be null");
102                }
103                
104                JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder()
105                        .issuer(iss.getValue())
106                        .audience(aud.getValue())
107                        .expirationTime(exp);
108                
109                for (Map.Entry<String, ?> en: MultivaluedMapUtils.toSingleValuedMap(response.toParameters()).entrySet()) {
110                        
111                        if ("response".equals(en.getKey())) {
112                                continue; // own JARM parameter, skip
113                        }
114                        
115                        if ("iss".equals(en.getKey())) {
116                                if (! iss.getValue().equals(en.getValue())) {
117                                        throw new IllegalArgumentException("Authorization response iss doesn't match JWT iss claim: " + en.getValue());
118                                }
119                        }
120                        
121                        builder = builder.claim(en.getKey(), en.getValue() + ""); // force string
122                }
123                
124                return builder.build();
125        }
126        
127        
128        /**
129         * Returns a multi-valued map representation of the specified JWT
130         * claims set.
131         *
132         * @param jwtClaimsSet The JWT claims set. Must not be {@code null}.
133         *
134         * @return The multi-valued map.
135         */
136        public static Map<String,List<String>> toMultiValuedStringParameters(final JWTClaimsSet jwtClaimsSet) {
137                
138                Map<String,List<String>> params = new HashMap<>();
139                
140                for (Map.Entry<String,Object> en: jwtClaimsSet.getClaims().entrySet()) {
141                        params.put(en.getKey(), Collections.singletonList(en.getValue() + ""));
142                }
143                
144                return params;
145        }
146        
147        
148        /**
149         * Returns {@code true} if the specified JWT-secured authorisation
150         * response implies an error response. Note that the JWT is not
151         * validated in any way!
152         *
153         * @param jwtString The JWT-secured authorisation response string. Must
154         *                  not be {@code null}.
155         *
156         * @return {@code true} if an error is implied by the presence of the
157         *         {@code error} claim, else {@code false} (also for encrypted
158         *         JWTs which payload cannot be inspected without decrypting
159         *         first).
160         *
161         * @throws ParseException If the JWT is invalid or plain (unsecured).
162         */
163        public static boolean impliesAuthorizationErrorResponse(final String jwtString)
164                throws ParseException  {
165                
166                try {
167                        return impliesAuthorizationErrorResponse(JWTParser.parse(jwtString));
168                } catch (java.text.ParseException e) {
169                        throw new ParseException("Invalid JWT-secured authorization response: " + e.getMessage(), e);
170                }
171        }
172        
173        
174        /**
175         * Returns {@code true} if the specified JWT-secured authorisation
176         * response implies an error response. Note that the JWT is not
177         * validated in any way!
178         *
179         * @param jwt The JWT-secured authorisation response. Must not be
180         *            {@code null}.
181         *
182         * @return {@code true} if an error is implied by the presence of the
183         *         {@code error} claim, else {@code false} (also for encrypted
184         *         JWTs which payload cannot be inspected without decrypting
185         *         first).
186         *
187         * @throws ParseException If the JWT is plain (unsecured).
188         */
189        public static boolean impliesAuthorizationErrorResponse(final JWT jwt)
190                throws ParseException  {
191                
192                if (jwt instanceof PlainJWT) {
193                        throw new ParseException("Invalid JWT-secured authorization response: The JWT must not be plain (unsecured)");
194                }
195                
196                if (jwt instanceof EncryptedJWT) {
197                        // Cannot peek into payload
198                        return false;
199                }
200                
201                if (jwt instanceof SignedJWT) {
202                        
203                        SignedJWT signedJWT = (SignedJWT)jwt;
204                        
205                        try {
206                                return signedJWT.getJWTClaimsSet().getStringClaim("error") != null;
207                        } catch (java.text.ParseException e) {
208                                throw new ParseException("Invalid JWT claims set: " + e.getMessage());
209                        }
210                }
211                
212                throw new ParseException("Unexpected JWT type");
213        }
214        
215        
216        /**
217         * Prevents public instantiation.
218         */
219        private JARMUtils() {}
220}