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}