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 try { 095 return JSONObjectUtils.getNonNegativeLong(jsonObject, "expires_in"); 096 } catch (ParseException e) { 097 throw new ParseException("Invalid expires_in"); 098 } 099 } else { 100 String lifetimeStr = JSONObjectUtils.getNonBlankString(jsonObject, "expires_in"); 101 try { 102 return Long.parseLong(lifetimeStr); 103 } catch (NumberFormatException e) { 104 throw new ParseException("Invalid expires_in"); 105 } 106 } 107 } 108 109 return 0L; 110 } 111 112 113 /** 114 * Parses a {@code scope} parameter from a JSON object. 115 * 116 * @param jsonObject The JSON object. Must not be {@code null}. 117 * 118 * @return The scope, {@code null} if not specified. 119 * 120 * @throws ParseException If parsing failed. 121 */ 122 public static Scope parseScopeFromJSONObject(final JSONObject jsonObject) 123 throws ParseException { 124 125 return Scope.parse(JSONObjectUtils.getString(jsonObject, "scope", null)); 126 } 127 128 129 /** 130 * Parses an {@code authorization_details} parameter from a JSON 131 * object. 132 * 133 * @param jsonObject The JSON object. Must not be {@code null}. 134 * 135 * @return The authorisation details, {@code null} if not specified. 136 * 137 * @throws ParseException If parsing failed. 138 */ 139 public static List<AuthorizationDetail> parseAuthorizationDetailsFromJSONObject(final JSONObject jsonObject) 140 throws ParseException { 141 142 JSONArray jsonArray = JSONObjectUtils.getJSONArray(jsonObject, "authorization_details", null); 143 144 if (jsonArray == null) { 145 return null; 146 } 147 148 return AuthorizationDetail.parseList(JSONArrayUtils.toJSONObjectList(jsonArray)); 149 } 150 151 152 /** 153 * Parses an {@code issued_token_type} parameter from a JSON object. 154 * 155 * @param jsonObject The JSON object. Must not be {@code null}. 156 * 157 * @return The issued token type, {@code null} if not specified. 158 * 159 * @throws ParseException If parsing failed. 160 */ 161 public static TokenTypeURI parseIssuedTokenTypeFromJSONObject(final JSONObject jsonObject) 162 throws ParseException { 163 164 String issuedTokenTypeString = JSONObjectUtils.getString(jsonObject, "issued_token_type", null); 165 166 if (issuedTokenTypeString == null) { 167 return null; 168 } 169 170 try { 171 return TokenTypeURI.parse(issuedTokenTypeString); 172 } catch (ParseException e) { 173 throw new ParseException("Invalid issued_token_type", e); 174 } 175 } 176 177 178 private static class GenericTokenSchemeError extends TokenSchemeError { 179 180 private static final long serialVersionUID = -8049139536364886132L; 181 182 public GenericTokenSchemeError(final AccessTokenType scheme, 183 final String code, 184 final String description, 185 final int httpStatusCode) { 186 super(scheme, code, description, httpStatusCode, null, null, null); 187 } 188 189 @Override 190 public TokenSchemeError setDescription(String description) { 191 return this; 192 } 193 194 @Override 195 public TokenSchemeError appendDescription(String text) { 196 return this; 197 } 198 199 @Override 200 public TokenSchemeError setHTTPStatusCode(int httpStatusCode) { 201 return this; 202 } 203 204 @Override 205 public TokenSchemeError setURI(URI uri) { 206 return this; 207 } 208 209 @Override 210 public TokenSchemeError setRealm(String realm) { 211 return this; 212 } 213 214 @Override 215 public TokenSchemeError setScope(Scope scope) { 216 return this; 217 } 218 } 219 220 221 private static TokenSchemeError getTypedMissingTokenError(final AccessTokenType type) { 222 if (AccessTokenType.BEARER.equals(type)) { 223 return BearerTokenError.MISSING_TOKEN; 224 } else if (AccessTokenType.DPOP.equals(type)) { 225 return DPoPTokenError.MISSING_TOKEN; 226 } else { 227 return new GenericTokenSchemeError(type, null, null, HTTPResponse.SC_UNAUTHORIZED); 228 } 229 } 230 231 232 private static TokenSchemeError getTypedInvalidRequestError(final AccessTokenType type) { 233 if (AccessTokenType.BEARER.equals(type)) { 234 return BearerTokenError.INVALID_REQUEST; 235 } else if (AccessTokenType.DPOP.equals(type)) { 236 return DPoPTokenError.INVALID_REQUEST; 237 } else { 238 return new GenericTokenSchemeError(type, "invalid_request", "Invalid request", HTTPResponse.SC_BAD_REQUEST); 239 } 240 } 241 242 243 /** 244 * Parses an access token value from an {@code Authorization} HTTP 245 * request header. 246 * 247 * @param header The {@code Authorization} header value, {@code null} 248 * if not specified. 249 * @param type The expected access token type, such as 250 * {@link AccessTokenType#BEARER} or 251 * {@link AccessTokenType#DPOP}. Must not be 252 * {@code null}. 253 * 254 * @return The access token value. 255 * 256 * @throws ParseException If parsing failed. 257 */ 258 public static String parseValueFromAuthorizationHeader(final String header, 259 final AccessTokenType type) 260 throws ParseException { 261 262 if (StringUtils.isBlank(header)) { 263 TokenSchemeError schemeError = getTypedMissingTokenError(type); 264 throw new ParseException("Missing HTTP Authorization header", schemeError); 265 } 266 267 String[] parts = header.split("\\s", 2); 268 269 if (parts.length != 2) { 270 TokenSchemeError schemeError = getTypedInvalidRequestError(type); 271 throw new ParseException("Invalid HTTP Authorization header value", schemeError); 272 } 273 274 if (! parts[0].equalsIgnoreCase(type.getValue())) { 275 TokenSchemeError schemeError = getTypedInvalidRequestError(type); 276 throw new ParseException("Token type must be " + type, schemeError); 277 } 278 279 if (StringUtils.isBlank(parts[1])) { 280 TokenSchemeError schemeError = getTypedInvalidRequestError(type); 281 throw new ParseException("Invalid HTTP Authorization header value: Missing token", schemeError); 282 } 283 284 return parts[1].trim(); 285 } 286 287 288 /** 289 * Parses an {@code access_token} values from a query or form 290 * parameters. 291 * 292 * @param parameters The parameters. Must not be {@code null}. 293 * @param type The expected access token type, such as 294 * {@link AccessTokenType#BEARER} or 295 * {@link AccessTokenType#DPOP}. Must not be 296 * {@code null}. 297 * 298 * @return The access token value. 299 * 300 * @throws ParseException If parsing failed. 301 */ 302 public static String parseValueFromQueryParameters(final Map<String, List<String>> parameters, 303 final AccessTokenType type) 304 throws ParseException { 305 306 if (! parameters.containsKey("access_token")) { 307 TokenSchemeError schemeError = getTypedMissingTokenError(type); 308 throw new ParseException("Missing access token parameter", schemeError); 309 } 310 311 String accessTokenValue = MultivaluedMapUtils.getFirstValue(parameters, "access_token"); 312 313 if (StringUtils.isBlank(accessTokenValue)) { 314 TokenSchemeError schemeError = getTypedInvalidRequestError(type); 315 throw new ParseException("Blank / empty access token", schemeError); 316 } 317 318 return accessTokenValue; 319 } 320 321 322 /** 323 * Parses an {@code access_token} value from a query or form 324 * parameters. 325 * 326 * @param parameters The query parameters. Must not be {@code null}. 327 * 328 * @return The access token value. 329 * 330 * @throws ParseException If parsing failed. 331 */ 332 public static String parseValueFromQueryParameters(final Map<String, List<String>> parameters) 333 throws ParseException { 334 335 String accessTokenValue = MultivaluedMapUtils.getFirstValue(parameters, "access_token"); 336 337 if (StringUtils.isBlank(accessTokenValue)) { 338 throw new ParseException("Missing access token"); 339 } 340 341 return accessTokenValue; 342 } 343 344 345 /** 346 * Determines the access token type from an {@code Authorization} HTTP 347 * request header. 348 * 349 * @param header The {@code Authorization} header value. Must not be 350 * {@code null}. 351 * 352 * @return The access token type. 353 * 354 * @throws ParseException If parsing failed. 355 */ 356 public static AccessTokenType determineAccessTokenTypeFromAuthorizationHeader(final String header) 357 throws ParseException { 358 359 String[] parts = header.split("\\s", 2); 360 361 if (parts.length < 2 || StringUtils.isBlank(parts[0]) || StringUtils.isBlank(parts[1])) { 362 throw new ParseException("Invalid Authorization header"); 363 } 364 365 if (parts[0].equalsIgnoreCase(AccessTokenType.BEARER.getValue())) { 366 return AccessTokenType.BEARER; 367 } 368 369 if (parts[0].equalsIgnoreCase(AccessTokenType.DPOP.getValue())) { 370 return AccessTokenType.DPOP; 371 } 372 373 return new AccessTokenType(parts[0]); 374 } 375 376 377 private AccessTokenParseUtils() {} 378}