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