diff --git a/README.md b/README.md index c5d7c7b1ee58..76dff042d27d 100644 --- a/README.md +++ b/README.md @@ -107,7 +107,7 @@ First, ensure that the necessary Google Cloud APIs are enabled for your project. Next, choose a method for authenticating API requests from within your project: 1. When using `gcloud-java` libraries from within Compute/App Engine, no additional authentication steps are necessary. -2. When using `gcloud-java` libraries elsewhere, there are two options: +2. When using `gcloud-java` libraries elsewhere, there are three options: * [Generate a JSON service account key](https://cloud.google.com/storage/docs/authentication?hl=en#service_accounts). After downloading that key, you must do one of the following: * Define the environment variable GOOGLE_APPLICATION_CREDENTIALS to be the location of the key. For example: ```bash @@ -116,11 +116,18 @@ Next, choose a method for authenticating API requests from within your project: * Supply the JSON credentials file when building the service options. For example, this Storage object has the necessary permissions to interact with your Google Cloud Storage data: ```java Storage storage = StorageOptions.builder() - .authCredentials(AuthCredentials.createForJson(new FileInputStream("/path/to/my/key.json")) + .authCredentials(AuthCredentials.createForJson(new FileInputStream("/path/to/my/key.json")) + .build() + .service(); + ``` + * If running locally for development/testing, you can use Google Cloud SDK. Download the SDK if you haven't already, then login using the SDK (`gcloud auth login` in command line). Be sure to set your project ID as described above. + * If you already have an OAuth2 access token, you can use it to authenticate (notice that in this case the access token will not be automatically refreshed): + ```java + Storage storage = StorageOptions.builder() + .authCredentials(AuthCredentials.createFor("your_access_token")) .build() .service(); - ``` - * If running locally for development/testing, you can use use Google Cloud SDK. Download the SDK if you haven't already, then login using the SDK (`gcloud auth login` in command line). Be sure to set your project ID as described above. + ``` `gcloud-java` looks for credentials in the following order, stopping once it finds credentials: diff --git a/gcloud-java-core/src/main/java/com/google/cloud/AuthCredentials.java b/gcloud-java-core/src/main/java/com/google/cloud/AuthCredentials.java index ec5a631f5f54..792831bb9edc 100644 --- a/gcloud-java-core/src/main/java/com/google/cloud/AuthCredentials.java +++ b/gcloud-java-core/src/main/java/com/google/cloud/AuthCredentials.java @@ -341,6 +341,67 @@ public RestorableState capture() { } } + /** + * Represents OAuth2 credentials. These credentials can be created given an OAuth2 access token. + * The access token will not be automatically refreshed. + */ + public static class OAuth2AuthCredentials extends AuthCredentials { + + private final GoogleCredentials credentials; + private final String accessToken; + private final Date expirationTime; + + private static class OAuth2AuthCredentialsState + implements RestorableState, Serializable { + + private static final long serialVersionUID = -7760693952274496205L; + + private final String accessToken; + private final Date expirationTime; + + private OAuth2AuthCredentialsState(String accessToken, Date expirationTime) { + this.accessToken = accessToken; + this.expirationTime = expirationTime; + } + + @Override + public AuthCredentials restore() { + return new OAuth2AuthCredentials(accessToken, expirationTime); + } + + @Override + public int hashCode() { + return Objects.hash(accessToken, expirationTime); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof OAuth2AuthCredentialsState)) { + return false; + } + OAuth2AuthCredentialsState other = (OAuth2AuthCredentialsState) obj; + return Objects.equals(accessToken, other.accessToken) + && Objects.equals(expirationTime, other.expirationTime); + } + } + + OAuth2AuthCredentials(String accessToken, Date expirationTime) { + this.accessToken = checkNotNull(accessToken); + this.expirationTime = expirationTime; + this.credentials = new GoogleCredentials(new AccessToken(accessToken, expirationTime)); + } + + @Override + public GoogleCredentials credentials() { + return credentials; + } + + @Override + public RestorableState capture() { + return new OAuth2AuthCredentialsState(accessToken, expirationTime); + } + } + /** * A placeholder for credentials to signify that requests sent to the server should not be * authenticated. This is typically useful when using the local service emulators, such as @@ -428,6 +489,28 @@ public static ServiceAccountAuthCredentials createFor(String account, PrivateKey return new ServiceAccountAuthCredentials(account, privateKey); } + /** + * Creates OAuth2 Credentials given the string representation of an access token. The access token + * will not be automatically refreshed. + * + * @param accessToken string representation of an access token + * @return the credentials instance + */ + public static OAuth2AuthCredentials createFor(String accessToken) { + return createFor(accessToken, (Date) null); + } + + /** + * Creates OAuth2 Credentials given the string representation of an access token and its + * expiration time. The access token will not be automatically refreshed. + * + * @param accessToken string representation of an access token + * @return the credentials instance + */ + public static OAuth2AuthCredentials createFor(String accessToken, Date expirationTime) { + return new OAuth2AuthCredentials(accessToken, expirationTime); + } + /** * Creates a placeholder denoting that no credentials should be used. This is typically useful * when using the local service emulators, such as {@code LocalDatastoreHelper} and diff --git a/gcloud-java-core/src/test/java/com/google/cloud/AuthCredentialsTest.java b/gcloud-java-core/src/test/java/com/google/cloud/AuthCredentialsTest.java new file mode 100644 index 000000000000..9074c2d103a6 --- /dev/null +++ b/gcloud-java-core/src/test/java/com/google/cloud/AuthCredentialsTest.java @@ -0,0 +1,128 @@ +/* + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud; + +import static com.google.common.base.Charsets.UTF_8; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; + +import com.google.auth.oauth2.AccessToken; +import com.google.auth.oauth2.ServiceAccountCredentials; +import com.google.cloud.AuthCredentials.OAuth2AuthCredentials; +import com.google.cloud.AuthCredentials.ServiceAccountAuthCredentials; +import com.google.common.io.BaseEncoding; + +import org.junit.BeforeClass; +import org.junit.Test; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.security.InvalidKeyException; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.Signature; +import java.security.SignatureException; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.PKCS8EncodedKeySpec; +import java.util.Date; + +public class AuthCredentialsTest { + + private static final String ACCESS_TOKEN = "accessToken"; + private static final Date EXPIRATION_DATE = new Date(); + private static final String SERVICE_ACCOUNT = "someclientid@developer.gserviceaccount.com"; + private static final String PRIVATE_KEY_STRING = "MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoG" + + "BAL2xolH1zrISQ8+GzOV29BNjjzq4/HIP8Psd1+cZb81vDklSF+95wB250MSE0BDc81pvIMwj5OmIfLg1NY6uB1xav" + + "OPpVdx1z664AGc/BEJ1zInXGXaQ6s+SxGenVq40Yws57gikQGMZjttpf1Qbz4DjkxsbRoeaRHn06n9pH1ejAgMBAAE" + + "CgYEAkWcm0AJF5LMhbWKbjkxm/LG06UNApkHX6vTOOOODkonM/qDBnhvKCj8Tan+PaU2j7679Cd19qxCm4SBQJET7e" + + "BhqLD9L2j9y0h2YUQnLbISaqUS1/EXcr2C1Lf9VCEn1y/GYuDYqs85rGoQ4ZYfM9ClROSq86fH+cbIIssqJqukCQQD" + + "18LjfJz/ichFeli5/l1jaFid2XoCH3T6TVuuysszVx68fh60gSIxEF/0X2xB+wuPxTP4IQ+t8tD/ktd232oWXAkEAx" + + "XPych2QBHePk9/lek4tOkKBgfnDzex7S/pI0G1vpB3VmzBbCsokn9lpOv7JV8071GDlW/7R6jlLfpQy3hN31QJAE10" + + "osSk99m5Uv8XDU3hvHnywDrnSFOBulNs7I47AYfSe7TSZhPkxUgsxejddTR27JLyTI8N1PxRSE4feNSOXcQJAMMKJR" + + "JT4U6IS2rmXubREhvaVdLtxFxEnAYQ1JwNfZm/XqBMw6GEy2iaeTetNXVlZRQEIoscyn1y2v/No/F5iYQJBAKBOGAS" + + "oQcBjGTOg/H/SfcE8QVNsKEpthRrs6CkpT80aZ/AV+ksfoIf2zw2M3mAHfrO+TBLdz4sicuFQvlN9SEc="; + private static final String JSON_KEY = "{\n" + + " \"private_key_id\": \"somekeyid\",\n" + + " \"private_key\": \"-----BEGIN PRIVATE KEY-----\\n" + PRIVATE_KEY_STRING + + "\\n-----END PRIVATE KEY-----\\n\",\n" + + " \"client_email\": \"someclientid@developer.gserviceaccount.com\",\n" + + " \"client_id\": \"someclientid.apps.googleusercontent.com\",\n" + + " \"type\": \"service_account\"\n" + + "}"; + private static final AuthCredentials NO_AUTH_CREDENTIALS = AuthCredentials.noAuth(); + private static final OAuth2AuthCredentials OAUTH2_AUTH_CREDENTIALS = + AuthCredentials.createFor(ACCESS_TOKEN, EXPIRATION_DATE); + private static final byte[] BYTES_TO_SIGN = PRIVATE_KEY_STRING.getBytes(UTF_8); + + private static PrivateKey privateKey; + private static byte[] signedBytes; + + @BeforeClass + public static void beforeClass() throws NoSuchAlgorithmException, InvalidKeySpecException, + InvalidKeyException, SignatureException { + KeyFactory keyFactory = KeyFactory.getInstance("RSA"); + privateKey = keyFactory.generatePrivate( + new PKCS8EncodedKeySpec(BaseEncoding.base64().decode(PRIVATE_KEY_STRING))); + Signature signature = Signature.getInstance("SHA256withRSA"); + signature.initSign(privateKey); + signature.update(BYTES_TO_SIGN); + signedBytes = signature.sign(); + } + + @Test + public void testNoAuthCredentials() { + assertSame(NO_AUTH_CREDENTIALS, AuthCredentials.noAuth()); + assertNull(NO_AUTH_CREDENTIALS.credentials()); + } + + @Test + public void testOAuth2AuthCredentials() { + AccessToken accessToken = OAUTH2_AUTH_CREDENTIALS.credentials().getAccessToken(); + assertEquals(ACCESS_TOKEN, accessToken.getTokenValue()); + assertEquals(EXPIRATION_DATE, accessToken.getExpirationTime()); + OAuth2AuthCredentials oAuth2AuthCredentials = + AuthCredentials.createFor(ACCESS_TOKEN); + accessToken = oAuth2AuthCredentials.credentials().getAccessToken(); + assertEquals(ACCESS_TOKEN, accessToken.getTokenValue()); + assertNull(accessToken.getExpirationTime()); + } + + @Test + public void testServiceAccountFromJson() throws IOException, SignatureException { + ServiceAccountAuthCredentials serviceAccountAuthCredentials = + AuthCredentials.createForJson(new ByteArrayInputStream(JSON_KEY.getBytes())); + ServiceAccountCredentials credentials = serviceAccountAuthCredentials.credentials(); + assertEquals(SERVICE_ACCOUNT, serviceAccountAuthCredentials.account()); + assertEquals(SERVICE_ACCOUNT, credentials.getClientEmail()); + assertEquals(privateKey, credentials.getPrivateKey()); + assertArrayEquals(signedBytes, serviceAccountAuthCredentials.sign(BYTES_TO_SIGN)); + } + + @Test + public void testServiceAccountFromKey() throws IOException, SignatureException { + ServiceAccountAuthCredentials serviceAccountAuthCredentials = + AuthCredentials.createFor(SERVICE_ACCOUNT, privateKey); + ServiceAccountCredentials credentials = serviceAccountAuthCredentials.credentials(); + assertEquals(SERVICE_ACCOUNT, serviceAccountAuthCredentials.account()); + assertEquals(SERVICE_ACCOUNT, credentials.getClientEmail()); + assertEquals(privateKey, credentials.getPrivateKey()); + assertArrayEquals(signedBytes, serviceAccountAuthCredentials.sign(BYTES_TO_SIGN)); + } +} diff --git a/gcloud-java-core/src/test/java/com/google/cloud/SerializationTest.java b/gcloud-java-core/src/test/java/com/google/cloud/SerializationTest.java index 75347b250227..7fa778a524eb 100644 --- a/gcloud-java-core/src/test/java/com/google/cloud/SerializationTest.java +++ b/gcloud-java-core/src/test/java/com/google/cloud/SerializationTest.java @@ -22,6 +22,7 @@ import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.Serializable; +import java.util.Date; public class SerializationTest extends BaseSerializationTest { @@ -94,7 +95,8 @@ protected Serializable[] serializableObjects() { protected Restorable[] restorableObjects() { try { return new Restorable[]{AuthCredentials.createForAppEngine(), AuthCredentials.noAuth(), - AuthCredentials.createForJson(new ByteArrayInputStream(JSON_KEY.getBytes()))}; + AuthCredentials.createForJson(new ByteArrayInputStream(JSON_KEY.getBytes())), + AuthCredentials.createFor("accessToken", new Date())}; } catch (IOException ex) { // never reached throw new RuntimeException(ex);