1 /* 2 * Copyright 2015, Google Inc. All rights reserved. 3 * 4 * Redistribution and use in source and binary forms, with or without 5 * modification, are permitted provided that the following conditions are 6 * met: 7 * 8 * * Redistributions of source code must retain the above copyright 9 * notice, this list of conditions and the following disclaimer. 10 * * Redistributions in binary form must reproduce the above 11 * copyright notice, this list of conditions and the following disclaimer 12 * in the documentation and/or other materials provided with the 13 * distribution. 14 * 15 * * Neither the name of Google Inc. nor the names of its 16 * contributors may be used to endorse or promote products derived from 17 * this software without specific prior written permission. 18 * 19 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 20 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 21 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 22 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 23 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 24 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 25 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 26 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 27 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 28 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 */ 31 32 package com.google.auth.oauth2; 33 34 import com.google.api.client.http.HttpHeaders; 35 import com.google.api.client.http.HttpTransport; 36 import com.google.api.client.http.javanet.NetHttpTransport; 37 import com.google.api.client.json.GenericJson; 38 import com.google.api.client.json.JsonFactory; 39 import com.google.api.client.json.JsonObjectParser; 40 import com.google.api.client.json.gson.GsonFactory; 41 import com.google.api.client.util.PemReader; 42 import com.google.api.client.util.PemReader.Section; 43 import com.google.api.client.util.SecurityUtils; 44 import com.google.auth.http.AuthHttpConstants; 45 import com.google.auth.http.HttpTransportFactory; 46 import com.google.common.io.ByteStreams; 47 import java.io.ByteArrayInputStream; 48 import java.io.File; 49 import java.io.FileOutputStream; 50 import java.io.IOException; 51 import java.io.InputStream; 52 import java.io.OutputStream; 53 import java.io.Reader; 54 import java.io.StringReader; 55 import java.math.BigDecimal; 56 import java.net.URI; 57 import java.nio.charset.StandardCharsets; 58 import java.security.KeyFactory; 59 import java.security.NoSuchAlgorithmException; 60 import java.security.PrivateKey; 61 import java.security.spec.InvalidKeySpecException; 62 import java.security.spec.PKCS8EncodedKeySpec; 63 import java.util.Arrays; 64 import java.util.Collection; 65 import java.util.HashSet; 66 import java.util.List; 67 import java.util.Map; 68 import java.util.Set; 69 70 /** Internal utilities for the com.google.auth.oauth2 namespace. */ 71 class OAuth2Utils { 72 static final String SIGNATURE_ALGORITHM = "SHA256withRSA"; 73 74 static final String TOKEN_TYPE_ACCESS_TOKEN = "urn:ietf:params:oauth:token-type:access_token"; 75 static final String TOKEN_TYPE_TOKEN_EXCHANGE = "urn:ietf:params:oauth:token-type:token-exchange"; 76 static final String GRANT_TYPE_JWT_BEARER = "urn:ietf:params:oauth:grant-type:jwt-bearer"; 77 78 static final URI TOKEN_SERVER_URI = URI.create("https://oauth2.googleapis.com/token"); 79 static final URI TOKEN_REVOKE_URI = URI.create("https://oauth2.googleapis.com/revoke"); 80 static final URI USER_AUTH_URI = URI.create("https://accounts.google.com/o/oauth2/auth"); 81 82 static final HttpTransport HTTP_TRANSPORT = new NetHttpTransport(); 83 84 static final HttpTransportFactory HTTP_TRANSPORT_FACTORY = new DefaultHttpTransportFactory(); 85 86 static final JsonFactory JSON_FACTORY = GsonFactory.getDefaultInstance(); 87 88 private static String VALUE_NOT_FOUND_MESSAGE = "%sExpected value %s not found."; 89 private static String VALUE_WRONG_TYPE_MESSAGE = "%sExpected %s value %s of wrong type."; 90 91 static final String BEARER_PREFIX = AuthHttpConstants.BEARER + " "; 92 93 static final String TOKEN_RESPONSE_SCOPE = "scope"; 94 95 // Includes expected server errors from Google token endpoint 96 // Other 5xx codes are either not used or retries are unlikely to succeed 97 public static final Set<Integer> TOKEN_ENDPOINT_RETRYABLE_STATUS_CODES = 98 new HashSet<>(Arrays.asList(500, 503, 408, 429)); 99 100 static class DefaultHttpTransportFactory implements HttpTransportFactory { 101 102 @Override create()103 public HttpTransport create() { 104 return HTTP_TRANSPORT; 105 } 106 } 107 108 /** 109 * Returns whether the headers contain the specified value as one of the entries in the specified 110 * header. 111 */ headersContainValue(HttpHeaders headers, String headerName, String value)112 static boolean headersContainValue(HttpHeaders headers, String headerName, String value) { 113 Object values = headers.get(headerName); 114 if (values instanceof Collection) { 115 @SuppressWarnings("unchecked") 116 Collection<Object> valuesCollection = (Collection<Object>) values; 117 return valuesCollection.contains(value); 118 } 119 return false; 120 } 121 122 /** Parses the specified JSON text. */ parseJson(String json)123 static GenericJson parseJson(String json) throws IOException { 124 JsonObjectParser parser = new JsonObjectParser(OAuth2Utils.JSON_FACTORY); 125 InputStream stateStream = new ByteArrayInputStream(json.getBytes(StandardCharsets.UTF_8)); 126 GenericJson stateJson = 127 parser.parseAndClose(stateStream, StandardCharsets.UTF_8, GenericJson.class); 128 return stateJson; 129 } 130 131 /** Return the specified string from JSON or throw a helpful error message. */ validateString(Map<String, Object> map, String key, String errorPrefix)132 static String validateString(Map<String, Object> map, String key, String errorPrefix) 133 throws IOException { 134 Object value = map.get(key); 135 if (value == null) { 136 throw new IOException(String.format(VALUE_NOT_FOUND_MESSAGE, errorPrefix, key)); 137 } 138 if (!(value instanceof String)) { 139 throw new IOException(String.format(VALUE_WRONG_TYPE_MESSAGE, errorPrefix, "string", key)); 140 } 141 return (String) value; 142 } 143 144 /** 145 * Saves the end user credentials into the given file path. 146 * 147 * @param credentials InputStream containing user credentials in JSON format 148 * @param filePath Path to file where to store the credentials 149 * @throws IOException An error saving the credentials. 150 */ writeInputStreamToFile(InputStream credentials, String filePath)151 static void writeInputStreamToFile(InputStream credentials, String filePath) throws IOException { 152 final OutputStream outputStream = new FileOutputStream(new File(filePath)); 153 try { 154 ByteStreams.copy(credentials, outputStream); 155 } finally { 156 outputStream.close(); 157 } 158 } 159 160 /** Return the specified optional string from JSON or throw a helpful error message. */ validateOptionalString(Map<String, Object> map, String key, String errorPrefix)161 static String validateOptionalString(Map<String, Object> map, String key, String errorPrefix) 162 throws IOException { 163 Object value = map.get(key); 164 if (value == null) { 165 return null; 166 } 167 if (!(value instanceof String)) { 168 throw new IOException(String.format(VALUE_WRONG_TYPE_MESSAGE, errorPrefix, "string", key)); 169 } 170 return (String) value; 171 } 172 173 /** Return the specified list of strings from JSON or throw a helpful error message. */ 174 @SuppressWarnings("unchecked") validateOptionalListString( Map<String, Object> map, String key, String errorPrefix)175 static List<String> validateOptionalListString( 176 Map<String, Object> map, String key, String errorPrefix) throws IOException { 177 Object value = map.get(key); 178 if (value == null) { 179 return null; 180 } 181 if (!(value instanceof List)) { 182 throw new IOException( 183 String.format(VALUE_WRONG_TYPE_MESSAGE, errorPrefix, "List<String>", key)); 184 } 185 return (List<String>) value; 186 } 187 188 /** Return the specified integer from JSON or throw a helpful error message. */ validateInt32(Map<String, Object> map, String key, String errorPrefix)189 static int validateInt32(Map<String, Object> map, String key, String errorPrefix) 190 throws IOException { 191 Object value = map.get(key); 192 if (value == null) { 193 throw new IOException(String.format(VALUE_NOT_FOUND_MESSAGE, errorPrefix, key)); 194 } 195 if (value instanceof BigDecimal) { 196 BigDecimal bigDecimalValue = (BigDecimal) value; 197 return bigDecimalValue.intValueExact(); 198 } 199 if (!(value instanceof Integer)) { 200 throw new IOException(String.format(VALUE_WRONG_TYPE_MESSAGE, errorPrefix, "integer", key)); 201 } 202 return (Integer) value; 203 } 204 205 /** Return the specified long from JSON or throw a helpful error message. */ validateLong(Map<String, Object> map, String key, String errorPrefix)206 static long validateLong(Map<String, Object> map, String key, String errorPrefix) 207 throws IOException { 208 Object value = map.get(key); 209 if (value == null) { 210 throw new IOException(String.format(VALUE_NOT_FOUND_MESSAGE, errorPrefix, key)); 211 } 212 if (value instanceof BigDecimal) { 213 BigDecimal bigDecimalValue = (BigDecimal) value; 214 return bigDecimalValue.longValueExact(); 215 } 216 if (!(value instanceof Long)) { 217 throw new IOException(String.format(VALUE_WRONG_TYPE_MESSAGE, errorPrefix, "long", key)); 218 } 219 return (Long) value; 220 } 221 222 /** Return the specified map from JSON or throw a helpful error message. */ 223 @SuppressWarnings({"unchecked", "rawtypes"}) validateMap(Map<String, Object> map, String key, String errorPrefix)224 static Map<String, Object> validateMap(Map<String, Object> map, String key, String errorPrefix) 225 throws IOException { 226 Object value = map.get(key); 227 if (value == null) { 228 throw new IOException(String.format(VALUE_NOT_FOUND_MESSAGE, errorPrefix, key)); 229 } 230 if (!(value instanceof Map)) { 231 throw new IOException(String.format(VALUE_WRONG_TYPE_MESSAGE, errorPrefix, "Map", key)); 232 } 233 return (Map) value; 234 } 235 236 /** Helper to convert from a PKCS#8 String to an RSA private key */ privateKeyFromPkcs8(String privateKeyPkcs8)237 static PrivateKey privateKeyFromPkcs8(String privateKeyPkcs8) throws IOException { 238 Reader reader = new StringReader(privateKeyPkcs8); 239 Section section = PemReader.readFirstSectionAndClose(reader, "PRIVATE KEY"); 240 if (section == null) { 241 throw new IOException("Invalid PKCS#8 data."); 242 } 243 byte[] bytes = section.getBase64DecodedBytes(); 244 PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(bytes); 245 Exception unexpectedException; 246 try { 247 KeyFactory keyFactory = SecurityUtils.getRsaKeyFactory(); 248 return keyFactory.generatePrivate(keySpec); 249 } catch (NoSuchAlgorithmException | InvalidKeySpecException exception) { 250 unexpectedException = exception; 251 } 252 throw new IOException("Unexpected exception reading PKCS#8 data", unexpectedException); 253 } 254 OAuth2Utils()255 private OAuth2Utils() {} 256 } 257