001 package com.nimbusds.oauth2.sdk.token; 002 003 004 import net.jcip.annotations.Immutable; 005 006 import net.minidev.json.JSONObject; 007 008 import com.nimbusds.oauth2.sdk.ParseException; 009 import com.nimbusds.oauth2.sdk.Scope; 010 import com.nimbusds.oauth2.sdk.http.HTTPRequest; 011 import com.nimbusds.oauth2.sdk.util.JSONObjectUtils; 012 import java.util.Map; 013 014 015 /** 016 * Bearer access token. This class is immutable. 017 * 018 * <p>Example bearer access token serialised to JSON: 019 * 020 * <pre> 021 * { 022 * "access_token" : "2YotnFZFEjr1zCsicMWpAA", 023 * "token_type" : "bearer", 024 * "expires_in" : 3600, 025 * "scope" : "read write" 026 * } 027 * </pre> 028 * 029 * <p>The above example token serialised to a HTTP Authorization header: 030 * 031 * <pre> 032 * Authorization: Bearer 2YotnFZFEjr1zCsicMWpAA 033 * </pre> 034 * 035 * <p>Related specifications: 036 * 037 * <ul> 038 * <li>OAuth 2.0 (RFC 6749), sections 1.4 and 5.1. 039 * <li>OAuth 2.0 Bearer Token Usage (RFC 6750). 040 * </ul> 041 * 042 * @author Vladimir Dzhuvinov 043 */ 044 @Immutable 045 public final class BearerAccessToken extends AccessToken { 046 047 048 /** 049 * Creates a new minimal bearer access token with a randomly generated 050 * value. The value will be made up of 32 mixed-case alphanumeric ASCII 051 * characters. The optional lifetime and scope are left undefined. 052 */ 053 public BearerAccessToken() { 054 055 this(32); 056 } 057 058 059 /** 060 * Creates a new minimal bearer access token with a randomly generated 061 * value of the specified length. The value will be made up of 062 * mixed-case alphanumeric ASCII characters. The optional lifetime and 063 * scope are left undefined. 064 * 065 * @param length The number of characters. Must be a positive integer. 066 */ 067 public BearerAccessToken(final int length) { 068 069 this(length, 0l, null); 070 } 071 072 073 /** 074 * Creates a new bearer access token with a randomly generated value 075 * and the specified optional lifetime and scope. The value will be 076 * made up of 32 mixed-case alphanumeric ASCII characters. 077 * 078 * @param lifetime The lifetime in seconds, 0 if not specified. 079 * @param scope The scope, {@code null} if not specified. 080 */ 081 public BearerAccessToken(final long lifetime, final Scope scope) { 082 083 this(32, lifetime, scope); 084 } 085 086 087 /** 088 * Creates a new bearer access token with a randomly generated value of 089 * the specified length and optional lifetime and scope. The value will 090 * be made up of mixed-case alphanumeric ASCII characters. 091 * 092 * @param length The number of characters. Must be a positive 093 * integer. 094 * @param lifetime The lifetime in seconds, 0 if not specified. 095 * @param scope The scope, {@code null} if not specified. 096 */ 097 public BearerAccessToken(final int length, final long lifetime, final Scope scope) { 098 099 super(AccessTokenType.BEARER, length, lifetime, scope); 100 } 101 102 103 /** 104 * Creates a new minimal bearer access token with the specified value. 105 * The optional lifetime and scope are left undefined. 106 * 107 * @param value The access token value. Must not be {@code null} or 108 * empty string. 109 */ 110 public BearerAccessToken(final String value) { 111 112 this(value, 0l, null); 113 } 114 115 116 /** 117 * Creates a new bearer access token with the specified value and 118 * optional lifetime and scope. 119 * 120 * @param value The access token value. Must not be {@code null} or 121 * empty string. 122 * @param lifetime The lifetime in seconds, 0 if not specified. 123 * @param scope The scope, {@code null} if not specified. 124 */ 125 public BearerAccessToken(final String value, final long lifetime, final Scope scope) { 126 127 super(AccessTokenType.BEARER, value, lifetime, scope); 128 } 129 130 131 /** 132 * Returns the HTTP Authorization header value for this bearer access 133 * token. 134 * 135 * <p>Example: 136 * 137 * <pre> 138 * Authorization: Bearer eyJhbGciOiJIUzI1NiJ9 139 * </pre> 140 * 141 * @return The HTTP Authorization header. 142 */ 143 @Override 144 public String toAuthorizationHeader(){ 145 146 return "Bearer " + getValue(); 147 } 148 149 150 @Override 151 public boolean equals(final Object object) { 152 153 return object != null && 154 object instanceof BearerAccessToken && 155 this.toString().equals(object.toString()); 156 } 157 158 159 /** 160 * Parses a bearer access token from a JSON object access token 161 * response. 162 * 163 * @param jsonObject The JSON object to parse. Must not be 164 * {@code null}. 165 * 166 * @return The bearer access token. 167 * 168 * @throws ParseException If the JSON object couldn't be parsed to a 169 * bearer access token. 170 */ 171 public static BearerAccessToken parse(final JSONObject jsonObject) 172 throws ParseException { 173 174 // Parse and verify type 175 AccessTokenType tokenType = new AccessTokenType(JSONObjectUtils.getString(jsonObject, "token_type")); 176 177 if (! tokenType.equals(AccessTokenType.BEARER)) 178 throw new ParseException("Token type must be \"Bearer\""); 179 180 181 // Parse value 182 String accessTokenValue = JSONObjectUtils.getString(jsonObject, "access_token"); 183 184 185 // Parse lifetime 186 long lifetime = 0; 187 188 if (jsonObject.containsKey("expires_in")) { 189 190 // Lifetime can be a JSON number or string 191 192 if (jsonObject.get("expires_in") instanceof Number) { 193 194 lifetime = JSONObjectUtils.getLong(jsonObject, "expires_in"); 195 } 196 else { 197 String lifetimeStr = JSONObjectUtils.getString(jsonObject, "expires_in"); 198 199 try { 200 lifetime = new Long(lifetimeStr); 201 202 } catch (NumberFormatException e) { 203 204 throw new ParseException("Invalid \"expires_in\" parameter, must be integer"); 205 } 206 } 207 } 208 209 210 // Parse scope 211 Scope scope = null; 212 213 if (jsonObject.containsKey("scope")) 214 scope = Scope.parse(JSONObjectUtils.getString(jsonObject, "scope")); 215 216 217 return new BearerAccessToken(accessTokenValue, lifetime, scope); 218 } 219 220 221 /** 222 * Parses an HTTP Authorization header for a bearer access token. 223 * 224 * @param header The HTTP Authorization header value to parse. Must not 225 * be {@code null}. 226 * 227 * @return The bearer access token. 228 * 229 * @throws ParseException If the HTTP Authorization header value 230 * couldn't be parsed to a bearer access token. 231 */ 232 public static BearerAccessToken parse(final String header) 233 throws ParseException { 234 235 String[] parts = header.split("\\s", 2); 236 237 if (parts.length != 2) 238 throw new ParseException("Invalid HTTP Authorization header value"); 239 240 if (! parts[0].equals("Bearer")) 241 throw new ParseException("Token type must be \"Bearer\""); 242 243 try { 244 return new BearerAccessToken(parts[1]); 245 246 } catch (IllegalArgumentException e) { 247 248 throw new ParseException(e.getMessage()); 249 } 250 } 251 252 253 /** 254 * Parses an HTTP request for a bearer access token. 255 * 256 * @param request The HTTP request to parse. Must be GET or POST, and 257 * not {@code null}. 258 * 259 * @return The bearer access token. 260 * 261 * @throws ParseException If a bearer access token wasn't found in the 262 * HTTP request. 263 */ 264 public static BearerAccessToken parse(final HTTPRequest request) 265 throws ParseException { 266 267 if (request.getMethod().equals(HTTPRequest.Method.GET)) { 268 269 String authzHeader = request.getAuthorization(); 270 271 if (authzHeader != null) { 272 273 return parse(authzHeader); 274 } 275 276 Map<String,String> params = request.getQueryParameters(); 277 278 if (params.get("access_token") != null) { 279 280 return parse(params.get("access_token")); 281 } 282 283 throw new ParseException("Missing Bearer access token"); 284 285 } else if (request.getMethod().equals(HTTPRequest.Method.POST)) { 286 287 Map<String,String> params = request.getQueryParameters(); 288 289 if (params.get("access_token") != null) { 290 291 return parse(params.get("access_token")); 292 } 293 294 throw new ParseException("Missing Bearer access token"); 295 296 } else { 297 throw new ParseException("Unexpected HTTP method: " + request.getMethod()); 298 } 299 } 300 }