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 com.nimbusds.jwt.*;
022import com.nimbusds.oauth2.sdk.AuthorizationResponse;
023import com.nimbusds.oauth2.sdk.ParseException;
024import com.nimbusds.oauth2.sdk.ResponseMode;
025import com.nimbusds.oauth2.sdk.as.AuthorizationServerMetadata;
026import com.nimbusds.oauth2.sdk.id.ClientID;
027import com.nimbusds.oauth2.sdk.id.Issuer;
028import com.nimbusds.oauth2.sdk.util.CollectionUtils;
029import com.nimbusds.oauth2.sdk.util.MultivaluedMapUtils;
030
031import java.util.*;
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                JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder()
101                        .issuer(iss.getValue())
102                        .audience(aud.getValue())
103                        .expirationTime(Objects.requireNonNull(exp));
104                
105                for (Map.Entry<String, ?> en: MultivaluedMapUtils.toSingleValuedMap(response.toParameters()).entrySet()) {
106                        
107                        if ("response".equals(en.getKey())) {
108                                continue; // own JARM parameter, skip
109                        }
110                        
111                        if ("iss".equals(en.getKey())) {
112                                if (! iss.getValue().equals(en.getValue())) {
113                                        throw new IllegalArgumentException("Authorization response iss doesn't match JWT iss claim: " + en.getValue());
114                                }
115                        }
116                        
117                        builder = builder.claim(en.getKey(), en.getValue() + ""); // force string
118                }
119                
120                return builder.build();
121        }
122        
123        
124        /**
125         * Returns a multi-valued map representation of the specified JWT
126         * claims set.
127         *
128         * @param jwtClaimsSet The JWT claims set. Must not be {@code null}.
129         *
130         * @return The multi-valued map.
131         */
132        public static Map<String,List<String>> toMultiValuedStringParameters(final JWTClaimsSet jwtClaimsSet) {
133                
134                Map<String,List<String>> params = new HashMap<>();
135                
136                for (Map.Entry<String,Object> en: jwtClaimsSet.getClaims().entrySet()) {
137                        params.put(en.getKey(), Collections.singletonList(en.getValue() + ""));
138                }
139                
140                return params;
141        }
142        
143        
144        /**
145         * Returns {@code true} if the specified JWT-secured authorisation
146         * response implies an error response. Note that the JWT is not
147         * validated in any way!
148         *
149         * @param jwtString The JWT-secured authorisation response string. Must
150         *                  not be {@code null}.
151         *
152         * @return {@code true} if an error is implied by the presence of the
153         *         {@code error} claim, else {@code false} (also for encrypted
154         *         JWTs which payload cannot be inspected without decrypting
155         *         first).
156         *
157         * @throws ParseException If the JWT is invalid or plain (unsecured).
158         */
159        public static boolean impliesAuthorizationErrorResponse(final String jwtString)
160                throws ParseException  {
161                
162                try {
163                        return impliesAuthorizationErrorResponse(JWTParser.parse(jwtString));
164                } catch (java.text.ParseException e) {
165                        throw new ParseException("Invalid JWT-secured authorization response: " + e.getMessage(), e);
166                }
167        }
168        
169        
170        /**
171         * Returns {@code true} if the specified JWT-secured authorisation
172         * response implies an error response. Note that the JWT is not
173         * validated in any way!
174         *
175         * @param jwt The JWT-secured authorisation response. Must not be
176         *            {@code null}.
177         *
178         * @return {@code true} if an error is implied by the presence of the
179         *         {@code error} claim, else {@code false} (also for encrypted
180         *         JWTs which payload cannot be inspected without decrypting
181         *         first).
182         *
183         * @throws ParseException If the JWT is plain (unsecured).
184         */
185        public static boolean impliesAuthorizationErrorResponse(final JWT jwt)
186                throws ParseException  {
187                
188                if (jwt instanceof PlainJWT) {
189                        throw new ParseException("Invalid JWT-secured authorization response: The JWT must not be plain (unsecured)");
190                }
191                
192                if (jwt instanceof EncryptedJWT) {
193                        // Cannot peek into payload
194                        return false;
195                }
196                
197                if (jwt instanceof SignedJWT) {
198                        
199                        SignedJWT signedJWT = (SignedJWT)jwt;
200                        
201                        try {
202                                return signedJWT.getJWTClaimsSet().getStringClaim("error") != null;
203                        } catch (java.text.ParseException e) {
204                                throw new ParseException("Invalid JWT claims set: " + e.getMessage());
205                        }
206                }
207                
208                throw new ParseException("Unexpected JWT type");
209        }
210        
211        
212        /**
213         * Prevents public instantiation.
214         */
215        private JARMUtils() {}
216}