Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
/*
* Copyright 2020 Google Inc.
*
* 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.client.auth.oauth2;

import com.google.api.client.http.HttpExecuteInterceptor;
import com.google.api.client.http.HttpRequest;
import com.google.api.client.http.HttpRequestInitializer;
import com.google.api.client.http.UrlEncodedContent;
import com.google.api.client.util.Data;
import com.google.api.client.util.Preconditions;

import java.io.IOException;
import java.util.Map;

/**
* Client credentials specified as URL-encoded parameters in the HTTP request body as specified in
* <a href="https://tools.ietf.org/html/rfc7523">JSON Web Token (JWT) Profile
* for OAuth 2.0 Client Authentication and Authorization Grants</a>
*
Comment thread
junying1 marked this conversation as resolved.
Comment thread
junying1 marked this conversation as resolved.
Comment thread
junying1 marked this conversation as resolved.
Comment thread
junying1 marked this conversation as resolved.
Comment on lines +23 to +31
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
import java.io.IOException;
import java.util.Map;
/**
* Client credentials specified as URL-encoded parameters in the HTTP request body as specified in
* <a href="https://tools.ietf.org/html/rfc7523">JSON Web Token (JWT) Profile
* for OAuth 2.0 Client Authentication and Authorization Grants</a>
*
* <a href="https://tools.ietf.org/html/rfc7523">JSON Web Token (JWT) Profile for OAuth 2.0 Client
* Authentication and Authorization Grants</a>
* <p>To use JWT authentication, grant_type must be "client_credentials". If
* AuthorizationCodeTokenRequest.setGrantType() is called, set it to
* JWTAuthentication.GRANT_TYPE_CLIENT_CREDENTIALS. It can also be left uncalled. Setting it to any
* other value causes an IllegalArgumentException.

* <p>This implementation assumes that the {@link HttpRequest#getContent()} is {@code null} or an
* instance of {@link UrlEncodedContent}. This is used as the client authentication in {@link
* TokenRequest#setClientAuthentication(HttpExecuteInterceptor)}.
*
* <p>
* To use JWT authentication, grant_type must be "client_credentials".
* If AuthorizationCodeTokenRequest.setGrantType() is called, set it to
* JWTAuthentication.GRANT_TYPE_CLIENT_CREDENTIALS. It can also be left
* uncalled. Setting it to any other value causes an IllegalArgumentException.
* </p>
*
* <p>Sample usage:
*
* <pre>
* static void requestAccessToken() throws IOException {
Comment thread
junying1 marked this conversation as resolved.
* try {
* TokenResponse response = new AuthorizationCodeTokenRequest(new NetHttpTransport(),
* new GsonFactory(), new GenericUrl("https://server.example.com/token")
* .setGrantType(JWTAuthentication.GRANT_TYPE_CLIENT_CREDENTIALS)
* .setClientAuthentication(
* new JWTAuthentication("eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzM4NCJ9...")).execute();
* System.out.println("Access token: " + response.getAccessToken());
* } catch (TokenResponseException e) {
* if (e.getDetails() != null) {
* System.err.println("Error: " + e.getDetails().getError());
* if (e.getDetails().getErrorDescription() != null) {
* System.err.println(e.getDetails().getErrorDescription());
* }
* if (e.getDetails().getErrorUri() != null) {
* System.err.println(e.getDetails().getErrorUri());
* }
* } else {
* System.err.println(e.getMessage());
* }
* }
* }
* </pre>
*
* <p>Implementation is immutable and thread-safe.
*
* @author Jun Ying
*/

public class JWTAuthentication
implements HttpRequestInitializer, HttpExecuteInterceptor {

public static final String GRANT_TYPE_KEY = "grant_type";

/** Predefined value for grant_type when using JWT **/
public static final String GRANT_TYPE_CLIENT_CREDENTIALS = "client_credentials";

Comment on lines +74 to +82
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
public class JWTAuthentication
implements HttpRequestInitializer, HttpExecuteInterceptor {
public static final String GRANT_TYPE_KEY = "grant_type";
/** Predefined value for grant_type when using JWT **/
public static final String GRANT_TYPE_CLIENT_CREDENTIALS = "client_credentials";
public class JWTAuthentication implements HttpRequestInitializer, HttpExecuteInterceptor {
/** Predefined value for grant_type when using JWT * */
/** Predefined value for client_assertion_type when using JWT * */
public static final String CLIENT_ASSERTION_TYPE =
"urn:ietf:params:oauth:client-assertion-type:jwt-bearer";
/** @param jwt JWT used for authentication */

public static final String CLIENT_ASSERTION_TYPE_KEY = "client_assertion_type";

/** Predefined value for client_assertion_type when using JWT **/
public static final String CLIENT_ASSERTION_TYPE = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer";

public static final String CLIENT_ASSERTION_KEY = "client_assertion";

/** JWT for authentication. */
private final String jwt;

/**
* @param jwt JWT used for authentication
*/
public JWTAuthentication(String jwt) {
this.jwt = Preconditions.checkNotNull(jwt);
}

public void initialize(HttpRequest request) throws IOException {
request.setInterceptor(this);
}

public void intercept(HttpRequest request) {
Map<String, Object> data = Data.mapOf(UrlEncodedContent.getContent(request).getData());
if (!data.containsKey(GRANT_TYPE_KEY)) {
data.put(GRANT_TYPE_KEY, GRANT_TYPE_CLIENT_CREDENTIALS);
} else {
String grantType = (String) data.get(GRANT_TYPE_KEY);
if (!grantType.equals(GRANT_TYPE_CLIENT_CREDENTIALS)) {
throw new IllegalArgumentException(GRANT_TYPE_KEY
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
throw new IllegalArgumentException(GRANT_TYPE_KEY
throw new IllegalArgumentException(
GRANT_TYPE_KEY

+ " must be "
+ GRANT_TYPE_CLIENT_CREDENTIALS
+ ", not "
+ grantType
+ ".");
}
}
data.put(CLIENT_ASSERTION_TYPE_KEY, CLIENT_ASSERTION_TYPE);
data.put(CLIENT_ASSERTION_KEY, jwt);
}

/** Returns the JWT. */
public final String getJWT() {
return jwt;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/*
* Copyright 2020 Google Inc.
*
* 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.client.auth.oauth2;

import com.google.api.client.http.GenericUrl;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
import com.google.api.client.http.GenericUrl;
import static org.junit.Assert.assertThrows;
import com.google.api.client.http.GenericUrl;

import com.google.api.client.http.HttpRequest;
import com.google.api.client.http.UrlEncodedContent;
import com.google.api.client.json.jackson2.JacksonFactory;
Comment thread
junying1 marked this conversation as resolved.
Comment thread
junying1 marked this conversation as resolved.
Comment thread
junying1 marked this conversation as resolved.
Comment thread
junying1 marked this conversation as resolved.
import com.google.api.client.testing.http.HttpTesting;
import com.google.api.client.testing.http.MockHttpTransport;
import java.util.Map;
import junit.framework.TestCase;
import static org.junit.Assert.assertThrows;
Comment thread
junying1 marked this conversation as resolved.
Comment on lines +24 to +25
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
import junit.framework.TestCase;
import static org.junit.Assert.assertThrows;
import junit.framework.TestCase;

import org.junit.function.ThrowingRunnable;

/**
* Tests {@link JWTAuthentication}.
*
* @author Jun Ying
*/
public class JWTAuthenticationTest extends TestCase {

private static final String JWT = "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzM4NCJ9";

public void test() throws Exception {
TokenRequest request =
new ClientCredentialsTokenRequest(new MockHttpTransport(), new JacksonFactory(),
new GenericUrl(HttpTesting.SIMPLE_GENERIC_URL.toString()));

JWTAuthentication auth =
Comment thread
junying1 marked this conversation as resolved.
Comment thread
junying1 marked this conversation as resolved.
Comment thread
junying1 marked this conversation as resolved.
Comment thread
junying1 marked this conversation as resolved.
Comment on lines +39 to +42
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
new ClientCredentialsTokenRequest(new MockHttpTransport(), new JacksonFactory(),
new GenericUrl(HttpTesting.SIMPLE_GENERIC_URL.toString()));
JWTAuthentication auth =
new ClientCredentialsTokenRequest(
new MockHttpTransport(),
new JacksonFactory(),
new GenericUrl(HttpTesting.SIMPLE_GENERIC_URL.toString()));
JWTAuthentication auth = new JWTAuthentication(JWT);

new JWTAuthentication(JWT);

assertEquals(JWT, auth.getJWT());

request.setGrantType(JWTAuthentication.GRANT_TYPE_CLIENT_CREDENTIALS);

request.setClientAuthentication(auth);

HttpRequest httpRequest = request.executeUnparsed().getRequest();
auth.intercept(httpRequest);
UrlEncodedContent content = (UrlEncodedContent) httpRequest.getContent();
@SuppressWarnings("unchecked")
Map<String, ?> data = (Map<String, ?>) content.getData();
assertEquals(JWT, data.get("client_assertion"));
assertEquals(JWTAuthentication.GRANT_TYPE_CLIENT_CREDENTIALS, data.get("grant_type"));
}

public void testNoGrantType() throws Exception {
HttpRequest request =
new MockHttpTransport()
.createRequestFactory()
.buildGetRequest(HttpTesting.SIMPLE_GENERIC_URL);
JWTAuthentication auth =
new JWTAuthentication(JWT);
Comment thread
junying1 marked this conversation as resolved.
Comment thread
junying1 marked this conversation as resolved.
Comment thread
junying1 marked this conversation as resolved.
Comment thread
junying1 marked this conversation as resolved.
Comment on lines +65 to +66
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
JWTAuthentication auth =
new JWTAuthentication(JWT);
JWTAuthentication auth = new JWTAuthentication(JWT);

assertEquals(JWT, auth.getJWT());
auth.intercept(request);
UrlEncodedContent content = (UrlEncodedContent) request.getContent();
@SuppressWarnings("unchecked")
Map<String, ?> data = (Map<String, ?>) content.getData();
assertEquals(JWT, data.get("client_assertion"));
assertEquals(JWTAuthentication.GRANT_TYPE_CLIENT_CREDENTIALS, data.get("grant_type"));
}

public void testInvalidGrantType() {
final TokenRequest request =
new ClientCredentialsTokenRequest(new MockHttpTransport(), new JacksonFactory(),
new GenericUrl(HttpTesting.SIMPLE_GENERIC_URL.toString()));

JWTAuthentication auth =
Comment thread
junying1 marked this conversation as resolved.
Comment thread
junying1 marked this conversation as resolved.
Comment thread
junying1 marked this conversation as resolved.
Comment thread
junying1 marked this conversation as resolved.
Comment on lines +78 to +81
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
new ClientCredentialsTokenRequest(new MockHttpTransport(), new JacksonFactory(),
new GenericUrl(HttpTesting.SIMPLE_GENERIC_URL.toString()));
JWTAuthentication auth =
new ClientCredentialsTokenRequest(
new MockHttpTransport(),
new JacksonFactory(),
new GenericUrl(HttpTesting.SIMPLE_GENERIC_URL.toString()));
JWTAuthentication auth = new JWTAuthentication(JWT);

new JWTAuthentication(JWT);

assertEquals(JWT, auth.getJWT());

request.setGrantType("invalid");

request.setClientAuthentication(auth);


assertThrows(IllegalArgumentException.class, new ThrowingRunnable() {
@Override
public void run() throws Throwable {
request.executeUnparsed();
}
});
}

public void test_noJWT() {
assertThrows(RuntimeException.class, new ThrowingRunnable() {
@Override
public void run() {
Comment thread
junying1 marked this conversation as resolved.
Comment thread
junying1 marked this conversation as resolved.
Comment thread
junying1 marked this conversation as resolved.
Comment thread
junying1 marked this conversation as resolved.
Comment on lines +90 to +102
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
assertThrows(IllegalArgumentException.class, new ThrowingRunnable() {
@Override
public void run() throws Throwable {
request.executeUnparsed();
}
});
}
public void test_noJWT() {
assertThrows(RuntimeException.class, new ThrowingRunnable() {
@Override
public void run() {
assertThrows(
IllegalArgumentException.class,
new ThrowingRunnable() {
@Override
public void run() throws Throwable {
request.executeUnparsed();
}
});
assertThrows(
RuntimeException.class,
new ThrowingRunnable() {
@Override
public void run() {
JWTAuthentication auth = new JWTAuthentication(null);
}
});

JWTAuthentication auth = new JWTAuthentication(null);
}
});
}
}