// Copyright 2013, 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.api.ads.common.lib.auth;
import com.google.api.ads.adwords.lib.utils.AdWordsInternals;
import com.google.api.ads.common.lib.conf.ConfigurationHelper;
import com.google.api.ads.common.lib.conf.ConfigurationLoadException;
import com.google.api.ads.common.lib.exception.OAuthException;
import com.google.api.ads.common.lib.exception.ValidationException;
import com.google.api.ads.common.lib.utils.Internals;
import com.google.api.ads.dfp.lib.utils.DfpInternals;
import com.google.api.client.auth.oauth2.Credential;
import com.google.api.client.googleapis.auth.oauth2.GoogleCredential;
import com.google.api.client.googleapis.auth.oauth2.GoogleOAuthConstants;
import com.google.api.client.http.HttpTransport;
import com.google.api.client.http.javanet.NetHttpTransport;
import com.google.api.client.json.jackson2.JacksonFactory;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.common.collect.Lists;
import com.google.common.io.Files;
import org.apache.commons.configuration.Configuration;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.List;
import javax.annotation.Nullable;
/**
* OfflineCredentials offline OAuth2 provider.<br>
* <br>
* Example usage:
* <pre><code>
* Credential credential = new OfflineCredentials.Builder()
* .forApi(OfflineCredentials.Api.ADWORDS)
* .fromFile()
* .build()
* .generateCredential();
* </code></pre>
*
* Generate a refresh token or service account key file and place
* it in your ads.properties file to be read by this utility.
*/
public class OfflineCredentials {
/**
* Enum representing the API that OfflineCredentials can be used for.
*/
public static enum Api {
ADWORDS("api.adwords.", AdWordsInternals.getInstance(),
"https://www.googleapis.com/auth/adwords"),
DFP("api.dfp.", DfpInternals.getInstance(), "https://www.googleapis.com/auth/dfp");
private final String propKeyPrefix;
private final Internals internals;
private final String scope;
private Api(String propKeyPrefix, Internals internals, String scope) {
this.propKeyPrefix =
Preconditions.checkNotNull(propKeyPrefix, "Null property key prefix for: %s", this);
this.internals = Preconditions.checkNotNull(internals, "Null internals for: %s", this);
this.scope = Preconditions.checkNotNull(scope, "Null scope for: %s", this);
}
/**
* Gets the property key prefix.
*/
public String getPropKeyPrefix() {
return propKeyPrefix;
}
/**
* Gets the internals;
*/
Internals getInternals() {
return internals;
}
}
private final HttpTransport httpTransport;
private final String refreshToken;
private final String clientId;
private final String clientSecret;
private final OAuth2Helper oAuth2Helper;
private final String tokenServerUrl;
private final String jsonKeyFilePath;
private final List<String> scopes;
/**
* Constructor.
*
* @param builder the builder for OfflineCredentials
*/
private OfflineCredentials(ForApiBuilder builder) {
this.httpTransport = builder.httpTransport;
this.refreshToken = builder.refreshToken;
this.clientId = builder.clientId;
this.clientSecret = builder.clientSecret;
this.oAuth2Helper = builder.oAuth2Helper;
this.tokenServerUrl = builder.tokenServerUrl;
this.jsonKeyFilePath = builder.jsonKeyFilePath;
this.scopes = builder.scopes;
}
/**
* Gets the {@link HttpTransport} that will be used when
* generating a {@link Credential}.
*/
public HttpTransport getHttpTransport() {
return httpTransport;
}
/**
* Gets the refresh token that will be used to
* generate a {@link Credential}.
*/
public String getRefreshToken() {
return refreshToken;
}
/**
* Gets the client ID that will be used to
* generate a {@link Credential}.
*/
public String getClientId() {
return clientId;
}
/**
* Gets the client secret that will be used to
* generate a {@link Credential}.
*/
public String getClientSecret() {
return clientSecret;
}
/**
* Gets the file path to a JSON key file that will be used to
* generate a service account {@link Credential}.
*/
public String getJsonKeyFilePath() {
return jsonKeyFilePath;
}
/**
* Generates a new offline credential and immediately refreshes it.
*
* @return a newly refreshed offline credential.
* @throws OAuthException if the credential could not be refreshed.
*/
public Credential generateCredential() throws OAuthException {
GoogleCredential credential = Strings.isNullOrEmpty(this.jsonKeyFilePath)
? generateCredentialFromClientSecrets()
: generateCredentialFromKeyFile();
try {
if (!oAuth2Helper.callRefreshToken(credential)) {
throw new OAuthException(
"Credential could not be refreshed. A newly generated refresh token or "
+ "secret key may be required.");
}
} catch (IOException e) {
throw new OAuthException("Credential could not be refreshed.", e);
}
return credential;
}
private GoogleCredential generateCredentialFromKeyFile() throws OAuthException {
try {
File jsonKeyFile = new File(jsonKeyFilePath);
return GoogleCredential.fromStream(
Files.asByteSource(jsonKeyFile).openStream(),
httpTransport,
new JacksonFactory())
.createScoped(this.scopes);
} catch (IOException e) {
throw new OAuthException("Service account key file could not be loaded.", e);
}
}
private GoogleCredential generateCredentialFromClientSecrets() {
GoogleCredential credential = new GoogleCredential.Builder()
.setTransport(httpTransport)
.setJsonFactory(new JacksonFactory())
.setClientSecrets(clientId, clientSecret)
.setTokenServerEncodedUrl(tokenServerUrl)
.build();
credential.setRefreshToken(refreshToken);
return credential;
}
/**
* Pre-builder for OfflineCredentials.
*/
public static class Builder {
@Nullable
private OAuth2Helper oAuth2Helper;
/**
* Default constructor.
*/
public Builder() {
this(null);
}
@VisibleForTesting
Builder(@Nullable OAuth2Helper oAuth2Helper) {
this.oAuth2Helper = oAuth2Helper;
}
/**
* Specifies which {@link Api} should this {@code OfflineCredentials} be
* used for. Should be called first before any other builder methods.
*/
public ForApiBuilder forApi(Api api) {
defaultOptionals(api);
return new ForApiBuilder(api, oAuth2Helper);
}
private void defaultOptionals(Api api) {
if (oAuth2Helper == null) {
oAuth2Helper = api.getInternals().getOAuth2Helper();
}
}
}
/**
* Builder for OfflineCredentials.
*/
public static class ForApiBuilder implements
com.google.api.ads.common.lib.utils.Builder<OfflineCredentials> {
// Use thread-safe global instance as default.
@VisibleForTesting
static final HttpTransport DEFAULT_HTTP_TRANSPORT = new NetHttpTransport();
private HttpTransport httpTransport;
private String refreshToken;
private String clientId;
private String clientSecret;
private String configFilePath;
private String tokenServerUrl;
private String jsonKeyFilePath;
private List<String> scopes;
private final ConfigurationHelper configHelper;
private final Api api;
private final OAuth2Helper oAuth2Helper;
/**
* Private constructor.
*
* @param api the API for the builder
* @param oAuth2Helper the OAuth2 helper
*/
private ForApiBuilder(Api api, OAuth2Helper oAuth2Helper) {
this(new ConfigurationHelper(), api, oAuth2Helper);
}
@VisibleForTesting
ForApiBuilder(ConfigurationHelper configHelper, Api api, OAuth2Helper oAuth2Helper) {
this.configHelper = configHelper;
this.api = api;
this.oAuth2Helper = oAuth2Helper;
}
@Override
public ForApiBuilder fromFile(String path) throws ConfigurationLoadException {
return from(configHelper.fromFile(path), path.toString());
}
@Override
public ForApiBuilder fromFile(File path) throws ConfigurationLoadException {
return from(configHelper.fromFile(path), path.getAbsolutePath());
}
@Override
public ForApiBuilder fromFile(URL path) throws ConfigurationLoadException {
return from(configHelper.fromFile(path), path.toString());
}
@Override
public ForApiBuilder fromFile() throws ConfigurationLoadException {
return fromFile(com.google.api.ads.common.lib.utils.Builder.DEFAULT_CONFIGURATION_FILENAME);
}
/**
* Reads properties from the provided {@link Configuration} object
* <br><br>
* Understands the following properties suffixes:
* <br><br>
* <ul>
* <li>refreshToken</li>
* <li>clientId</li>
* <li>clientSecret</li>
* <li>jsonKeyFilePath</li>
* </ul><br>
* For example, the AdWords OAuth2 refresh token can be read from:
* <code>api.adwords.refreshToken</code>
*
* @param config the configuration
* @return Builder populated from the Configuration
*/
@Override
public ForApiBuilder from(Configuration config) {
this.refreshToken = config.getString(getPropertyKey("refreshToken"), null);
this.clientId = config.getString(getPropertyKey("clientId"), null);
this.clientSecret = config.getString(getPropertyKey("clientSecret"), null);
this.jsonKeyFilePath = config.getString(getPropertyKey("jsonKeyFilePath"), null);
return this;
}
/**
* Reads properties from the provided {@link Configuration} object
* <br><br>
* Understands the following properties suffixes:
* <br><br>
* <ul>
* <li>refreshToken</li>
* <li>clientId</li>
* <li>clientSecret</li>
* <li>jsonKeyFilePath</li>
* </ul><br>
* For example, the AdWords OAuth2 refresh token can be read from:
* <code>api.adwords.refreshToken</code>
*
* @param config the configuration
* @param filePath the file path of the configuration
* @return Builder populated from the Configuration
*/
ForApiBuilder from(Configuration config, String filePath) {
from(config);
this.configFilePath = filePath;
return this;
}
/**
* Sets the client ID & secret to create the OAuth2 Credential with. If you
* do not have a client ID or secret, please create one in the API console:
* https://console.developers.google.com/project
*/
public ForApiBuilder withClientSecrets(String clientId, String clientSecret) {
this.clientId = clientId;
this.clientSecret = clientSecret;
return this;
}
/**
* Sets the refresh token to create the OAuth2 Credential with. If you need
* to create one, see the GetRefreshToken example.
*/
public ForApiBuilder withRefreshToken(String refreshToken) {
this.refreshToken = refreshToken;
return this;
}
/**
* Sets the path to a JSON key file for authenticating with a service account.
* If you do not have one, please create it in the API console:
* https://console.developers.google.com/apis/credentials
*/
public ForApiBuilder withJsonKeyFilePath(String jsonKeyFilePath) {
this.jsonKeyFilePath = jsonKeyFilePath;
return this;
}
/**
* Optionally sets scopes for authenticating with a service account. By default,
* the scope will only be for the set {@link Api}. If you are using a refresh token,
* the scope is set at the time the refresh token is generated, and this function is
* a no-op.
*/
public ForApiBuilder withScopes(List<String> scopes) {
this.scopes = scopes;
return this;
}
/**
* Sets the {@link HttpTransport} to be used to make the request. By
* default, {@link NetHttpTransport} will be used, but due to some
* environment restrictions, you may want to use a different transport,
* such as {@code UrlFetchTransport} for AppEngine.
*/
public ForApiBuilder withHttpTransport(HttpTransport httpTransport) {
this.httpTransport = httpTransport;
return this;
}
/**
* Sets the token server URL. This is a no-op when using a service account key file.
* Set the token server URL in the key file instead. Not required and defaults to
* https://accounts.google.com/o/oauth2/token
*/
public ForApiBuilder withTokenUrlServer(String tokenServerUrl) {
this.tokenServerUrl = tokenServerUrl;
return this;
}
/**
* Validates the {@code OfflineCredentials} object.
* @throws ValidationException if the {@code OfflineCredentials} did not
* validate
*/
private void validate() throws ValidationException {
if (!Strings.isNullOrEmpty(this.jsonKeyFilePath)) {
// Make sure only one OAuth format is specified.
boolean otherOAuthPropsSet =
!Strings.isNullOrEmpty(this.clientId)
|| !Strings.isNullOrEmpty(this.clientSecret)
|| !Strings.isNullOrEmpty(refreshToken);
if (otherOAuthPropsSet) {
throw new ValidationException("Multiple OAuth formats set. Please specify either "
+ "a service account key file or a client ID and secret, not both.",
this.configFilePath != null ? generateFilePathWarning("jsonKeyFilePath") : ".");
}
// Key file has everything we need, so skip the remaining validation.
return;
}
if (Strings.isNullOrEmpty(this.clientId)) {
throw new ValidationException(String.format("Client ID must be set%s\n"
+ "If you do not have a client ID or secret, please create one in the API console: "
+ "https://console.developers.google.com/project",
this.configFilePath != null ? generateFilePathWarning("clientId") : "."),
"clientId");
}
if (Strings.isNullOrEmpty(this.clientSecret)) {
throw new ValidationException(String.format("Client secret must be set%s\n"
+ "If you do not have a client ID or secret, please create one in the API console: "
+ "https://console.developers.google.com/project",
this.configFilePath != null ? generateFilePathWarning("clientSecret") : "."),
"clientSecret");
}
if (Strings.isNullOrEmpty(this.refreshToken)) {
throw new ValidationException(String.format("A refresh token must be set%s\n"
+ "It is required for offline credentials. If you need to create one, see the "
+ "GetRefreshToken example.",
this.configFilePath != null ? generateFilePathWarning("refreshToken") : "."),
"refreshToken");
}
}
/**
* Generates a file path warning for the key.
*/
private String generateFilePathWarning(String key) {
return String.format(" as %s in %s.", getPropertyKey(key), this.configFilePath);
}
/**
* Fills in defaults if {@code null}.
*/
private void defaultOptionals() {
if (this.httpTransport == null) {
this.httpTransport = DEFAULT_HTTP_TRANSPORT;
}
if (this.tokenServerUrl == null) {
this.tokenServerUrl = GoogleOAuthConstants.TOKEN_SERVER_URL;
}
if (this.scopes == null) {
this.scopes = Lists.newArrayList(this.api.scope);
}
}
@Override
public OfflineCredentials build() throws ValidationException {
defaultOptionals();
validate();
return new OfflineCredentials(this);
}
/**
* Adds the correct property key prefix to match the API.
*
* @param suffix the property suffix
* @return property value for key
*/
private String getPropertyKey(String suffix) {
return api.getPropKeyPrefix() + suffix;
}
}
}