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}