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}