Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Implement JWT OAuth 2 Client Authentication as HttpExecuteInterceptor. #582

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
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>
*
junying1 marked this conversation as resolved.
Show resolved Hide resolved
junying1 marked this conversation as resolved.
Show resolved Hide resolved
junying1 marked this conversation as resolved.
Show resolved Hide resolved
junying1 marked this conversation as resolved.
Show resolved Hide resolved
Comment on lines +23 to +31

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 {
junying1 marked this conversation as resolved.
Show resolved Hide 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

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

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;

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;
junying1 marked this conversation as resolved.
Show resolved Hide resolved
junying1 marked this conversation as resolved.
Show resolved Hide resolved
junying1 marked this conversation as resolved.
Show resolved Hide resolved
junying1 marked this conversation as resolved.
Show resolved Hide 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;
junying1 marked this conversation as resolved.
Show resolved Hide resolved
Comment on lines +24 to +25

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 =
junying1 marked this conversation as resolved.
Show resolved Hide resolved
junying1 marked this conversation as resolved.
Show resolved Hide resolved
junying1 marked this conversation as resolved.
Show resolved Hide resolved
junying1 marked this conversation as resolved.
Show resolved Hide resolved
Comment on lines +39 to +42

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);
junying1 marked this conversation as resolved.
Show resolved Hide resolved
junying1 marked this conversation as resolved.
Show resolved Hide resolved
junying1 marked this conversation as resolved.
Show resolved Hide resolved
junying1 marked this conversation as resolved.
Show resolved Hide resolved
Comment on lines +65 to +66

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 =
junying1 marked this conversation as resolved.
Show resolved Hide resolved
junying1 marked this conversation as resolved.
Show resolved Hide resolved
junying1 marked this conversation as resolved.
Show resolved Hide resolved
junying1 marked this conversation as resolved.
Show resolved Hide resolved
Comment on lines +78 to +81

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() {
junying1 marked this conversation as resolved.
Show resolved Hide resolved
junying1 marked this conversation as resolved.
Show resolved Hide resolved
junying1 marked this conversation as resolved.
Show resolved Hide resolved
junying1 marked this conversation as resolved.
Show resolved Hide resolved
Comment on lines +90 to +102

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);
}
});
}
}