001/* 002 * oauth2-oidc-sdk 003 * 004 * Copyright 2012-2021, 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.token; 019 020 021import com.nimbusds.oauth2.sdk.ParseException; 022import com.nimbusds.oauth2.sdk.Scope; 023import com.nimbusds.oauth2.sdk.http.HTTPResponse; 024import com.nimbusds.oauth2.sdk.rar.AuthorizationDetail; 025import com.nimbusds.oauth2.sdk.util.JSONArrayUtils; 026import com.nimbusds.oauth2.sdk.util.JSONObjectUtils; 027import com.nimbusds.oauth2.sdk.util.MultivaluedMapUtils; 028import com.nimbusds.oauth2.sdk.util.StringUtils; 029import net.minidev.json.JSONArray; 030import net.minidev.json.JSONObject; 031 032import java.net.URI; 033import java.util.List; 034import java.util.Map; 035 036 037/** 038 * Access token parse utilities. 039 */ 040public class AccessTokenParseUtils { 041 042 043 /** 044 * Parses a {@code token_type} from a JSON object and ensures it 045 * matches the specified. 046 * 047 * @param jsonObject The JSON object. Must not be {@code null}. 048 * @param type The expected token type. Must not be {@code null}. 049 * 050 * @throws ParseException If parsing failed. 051 */ 052 public static void parseAndEnsureTypeFromJSONObject(final JSONObject jsonObject, final AccessTokenType type) 053 throws ParseException { 054 055 if (! new AccessTokenType(JSONObjectUtils.getNonBlankString(jsonObject, "token_type")).equals(type)) { 056 throw new ParseException("The token type must be " + type); 057 } 058 } 059 060 061 /** 062 * Parses an {code access_token} value from a JSON object. 063 * 064 * @param params The JSON object. Must not be {@code null}. 065 * 066 * @return The access token value. 067 * 068 * @throws ParseException If parsing failed. 069 */ 070 public static String parseValueFromJSONObject(final JSONObject params) 071 throws ParseException { 072 073 return JSONObjectUtils.getNonBlankString(params, "access_token"); 074 } 075 076 077 /** 078 * Parses an access token {@code expires_in} parameter from a JSON 079 * object. 080 * 081 * @param jsonObject The JSON object. Must not be {@code null}. 082 * 083 * @return The access token lifetime, in seconds, zero if not 084 * specified. 085 * 086 * @throws ParseException If parsing failed. 087 */ 088 public static long parseLifetimeFromJSONObject(final JSONObject jsonObject) 089 throws ParseException { 090 091 if (jsonObject.containsKey("expires_in")) { 092 // Lifetime can be a JSON number or string 093 if (jsonObject.get("expires_in") instanceof Number) { 094 return JSONObjectUtils.getLong(jsonObject, "expires_in"); 095 } else { 096 String lifetimeStr = JSONObjectUtils.getNonBlankString(jsonObject, "expires_in"); 097 try { 098 return Long.parseLong(lifetimeStr); 099 } catch (NumberFormatException e) { 100 throw new ParseException("expires_in must be an integer"); 101 } 102 } 103 } 104 105 return 0L; 106 } 107 108 109 /** 110 * Parses a {@code scope} parameter from a JSON object. 111 * 112 * @param jsonObject The JSON object. Must not be {@code null}. 113 * 114 * @return The scope, {@code null} if not specified. 115 * 116 * @throws ParseException If parsing failed. 117 */ 118 public static Scope parseScopeFromJSONObject(final JSONObject jsonObject) 119 throws ParseException { 120 121 return Scope.parse(JSONObjectUtils.getString(jsonObject, "scope", null)); 122 } 123 124 125 /** 126 * Parses an {@code authorization_details} parameter from a JSON 127 * object. 128 * 129 * @param jsonObject The JSON object. Must not be {@code null}. 130 * 131 * @return The authorisation details, {@code null} if not specified. 132 * 133 * @throws ParseException If parsing failed. 134 */ 135 public static List<AuthorizationDetail> parseAuthorizationDetailsFromJSONObject(final JSONObject jsonObject) 136 throws ParseException { 137 138 JSONArray jsonArray = JSONObjectUtils.getJSONArray(jsonObject, "authorization_details", null); 139 140 if (jsonArray == null) { 141 return null; 142 } 143 144 return AuthorizationDetail.parseList(JSONArrayUtils.toJSONObjectList(jsonArray)); 145 } 146 147 148 /** 149 * Parses an {@code issued_token_type} parameter from a JSON object. 150 * 151 * @param jsonObject The JSON object. Must not be {@code null}. 152 * 153 * @return The issued token type, {@code null} if not specified. 154 * 155 * @throws ParseException If parsing failed. 156 */ 157 public static TokenTypeURI parseIssuedTokenTypeFromJSONObject(final JSONObject jsonObject) 158 throws ParseException { 159 160 String issuedTokenTypeString = JSONObjectUtils.getString(jsonObject, "issued_token_type", null); 161 162 if (issuedTokenTypeString == null) { 163 return null; 164 } 165 166 try { 167 return TokenTypeURI.parse(issuedTokenTypeString); 168 } catch (ParseException e) { 169 throw new ParseException("Invalid issued_token_type", e); 170 } 171 } 172 173 174 private static class GenericTokenSchemeError extends TokenSchemeError { 175 176 private static final long serialVersionUID = -8049139536364886132L; 177 178 public GenericTokenSchemeError(final AccessTokenType scheme, 179 final String code, 180 final String description, 181 final int httpStatusCode) { 182 super(scheme, code, description, httpStatusCode, null, null, null); 183 } 184 185 @Override 186 public TokenSchemeError setDescription(String description) { 187 return this; 188 } 189 190 @Override 191 public TokenSchemeError appendDescription(String text) { 192 return this; 193 } 194 195 @Override 196 public TokenSchemeError setHTTPStatusCode(int httpStatusCode) { 197 return this; 198 } 199 200 @Override 201 public TokenSchemeError setURI(URI uri) { 202 return this; 203 } 204 205 @Override 206 public TokenSchemeError setRealm(String realm) { 207 return this; 208 } 209 210 @Override 211 public TokenSchemeError setScope(Scope scope) { 212 return this; 213 } 214 } 215 216 217 private static TokenSchemeError getTypedMissingTokenError(final AccessTokenType type) { 218 if (AccessTokenType.BEARER.equals(type)) { 219 return BearerTokenError.MISSING_TOKEN; 220 } else if (AccessTokenType.DPOP.equals(type)) { 221 return DPoPTokenError.MISSING_TOKEN; 222 } else { 223 return new GenericTokenSchemeError(type, null, null, HTTPResponse.SC_UNAUTHORIZED); 224 } 225 } 226 227 228 private static TokenSchemeError getTypedInvalidRequestError(final AccessTokenType type) { 229 if (AccessTokenType.BEARER.equals(type)) { 230 return BearerTokenError.INVALID_REQUEST; 231 } else if (AccessTokenType.DPOP.equals(type)) { 232 return DPoPTokenError.INVALID_REQUEST; 233 } else { 234 return new GenericTokenSchemeError(type, "invalid_request", "Invalid request", HTTPResponse.SC_BAD_REQUEST); 235 } 236 } 237 238 239 /** 240 * Parses an access token value from an {@code Authorization} HTTP 241 * request header. 242 * 243 * @param header The {@code Authorization} header value, {@code null} 244 * if not specified. 245 * @param type The expected access token type, such as 246 * {@link AccessTokenType#BEARER} or 247 * {@link AccessTokenType#DPOP}. Must not be 248 * {@code null}. 249 * 250 * @return The access token value. 251 * 252 * @throws ParseException If parsing failed. 253 */ 254 public static String parseValueFromAuthorizationHeader(final String header, 255 final AccessTokenType type) 256 throws ParseException { 257 258 if (StringUtils.isBlank(header)) { 259 TokenSchemeError schemeError = getTypedMissingTokenError(type); 260 throw new ParseException("Missing HTTP Authorization header", schemeError); 261 } 262 263 String[] parts = header.split("\\s", 2); 264 265 if (parts.length != 2) { 266 TokenSchemeError schemeError = getTypedInvalidRequestError(type); 267 throw new ParseException("Invalid HTTP Authorization header value", schemeError); 268 } 269 270 if (! parts[0].equalsIgnoreCase(type.getValue())) { 271 TokenSchemeError schemeError = getTypedInvalidRequestError(type); 272 throw new ParseException("Token type must be " + type, schemeError); 273 } 274 275 if (StringUtils.isBlank(parts[1])) { 276 TokenSchemeError schemeError = getTypedInvalidRequestError(type); 277 throw new ParseException("Invalid HTTP Authorization header value: Missing token", schemeError); 278 } 279 280 return parts[1]; 281 } 282 283 284 /** 285 * Parses an {@code access_token} values from a query or form 286 * parameters. 287 * 288 * @param parameters The parameters. Must not be {@code null}. 289 * @param type The expected access token type, such as 290 * {@link AccessTokenType#BEARER} or 291 * {@link AccessTokenType#DPOP}. Must not be 292 * {@code null}. 293 * 294 * @return The access token value. 295 * 296 * @throws ParseException If parsing failed. 297 */ 298 public static String parseValueFromQueryParameters(final Map<String, List<String>> parameters, 299 final AccessTokenType type) 300 throws ParseException { 301 302 if (! parameters.containsKey("access_token")) { 303 TokenSchemeError schemeError = getTypedMissingTokenError(type); 304 throw new ParseException("Missing access token parameter", schemeError); 305 } 306 307 String accessTokenValue = MultivaluedMapUtils.getFirstValue(parameters, "access_token"); 308 309 if (StringUtils.isBlank(accessTokenValue)) { 310 TokenSchemeError schemeError = getTypedInvalidRequestError(type); 311 throw new ParseException("Blank / empty access token", schemeError); 312 } 313 314 return accessTokenValue; 315 } 316 317 318 /** 319 * Parses an {@code access_token} value from a query or form 320 * parameters. 321 * 322 * @param parameters The query parameters. Must not be {@code null}. 323 * 324 * @return The access token value. 325 * 326 * @throws ParseException If parsing failed. 327 */ 328 public static String parseValueFromQueryParameters(final Map<String, List<String>> parameters) 329 throws ParseException { 330 331 String accessTokenValue = MultivaluedMapUtils.getFirstValue(parameters, "access_token"); 332 333 if (StringUtils.isBlank(accessTokenValue)) { 334 throw new ParseException("Missing access token"); 335 } 336 337 return accessTokenValue; 338 } 339 340 341 /** 342 * Determines the access token type from an {@code Authorization} HTTP 343 * request header. 344 * 345 * @param header The {@code Authorization} header value. Must not be 346 * {@code null}. 347 * 348 * @return The access token type. 349 * 350 * @throws ParseException If parsing failed. 351 */ 352 public static AccessTokenType determineAccessTokenTypeFromAuthorizationHeader(final String header) 353 throws ParseException { 354 355 String[] parts = header.split("\\s", 2); 356 357 if (parts.length < 2 || StringUtils.isBlank(parts[0]) || StringUtils.isBlank(parts[1])) { 358 throw new ParseException("Invalid Authorization header"); 359 } 360 361 if (parts[0].equalsIgnoreCase(AccessTokenType.BEARER.getValue())) { 362 return AccessTokenType.BEARER; 363 } 364 365 if (parts[0].equalsIgnoreCase(AccessTokenType.DPOP.getValue())) { 366 return AccessTokenType.DPOP; 367 } 368 369 return new AccessTokenType(parts[0]); 370 } 371 372 373 private AccessTokenParseUtils() {} 374}