From ee7a6ae60a5c64f06de7309671b4cc355db002dd Mon Sep 17 00:00:00 2001 From: gkatzioura Date: Thu, 5 Oct 2023 08:19:18 +0100 Subject: [PATCH 1/2] 1296: Added reactive binding --- pom.xml | 1 + .../auth/oauth2/CacheableTokenProvider.java | 67 +++++++++++++ .../oauth2/ComputeEngineTokenProvider.java | 75 ++++++++++++++ .../com/google/auth/oauth2/Constants.java | 28 ++++++ .../auth/oauth2/ReactiveTokenProvider.java | 54 ++++++++++ .../google/auth/oauth2/RefreshThreshold.java | 40 ++++++++ .../oauth2/ServiceAccountTokenProvider.java | 95 ++++++++++++++++++ .../oauth2/UserCredentialsTokenProvider.java | 95 ++++++++++++++++++ .../oauth2/CacheableTokenProviderTest.java | 70 +++++++++++++ .../ComputeEngineTokenProviderTest.java | 87 ++++++++++++++++ .../oauth2/ReactiveTokenProviderTest.java | 65 ++++++++++++ .../auth/oauth2/RefreshThresholdTest.java | 45 +++++++++ .../ServiceAccountTokenProviderTest.java | 99 +++++++++++++++++++ .../google/auth/oauth2/TokenProviderBase.java | 62 ++++++++++++ .../UserCredentialsTokenProviderTest.java | 90 +++++++++++++++++ reactor/javatests/fake-credential-key.json | 12 +++ reactor/pom.xml | 92 +++++++++++++++++ 17 files changed, 1077 insertions(+) create mode 100644 reactor/java/com/google/auth/oauth2/CacheableTokenProvider.java create mode 100644 reactor/java/com/google/auth/oauth2/ComputeEngineTokenProvider.java create mode 100644 reactor/java/com/google/auth/oauth2/Constants.java create mode 100644 reactor/java/com/google/auth/oauth2/ReactiveTokenProvider.java create mode 100644 reactor/java/com/google/auth/oauth2/RefreshThreshold.java create mode 100644 reactor/java/com/google/auth/oauth2/ServiceAccountTokenProvider.java create mode 100644 reactor/java/com/google/auth/oauth2/UserCredentialsTokenProvider.java create mode 100644 reactor/javatests/com/google/auth/oauth2/CacheableTokenProviderTest.java create mode 100644 reactor/javatests/com/google/auth/oauth2/ComputeEngineTokenProviderTest.java create mode 100644 reactor/javatests/com/google/auth/oauth2/ReactiveTokenProviderTest.java create mode 100644 reactor/javatests/com/google/auth/oauth2/RefreshThresholdTest.java create mode 100644 reactor/javatests/com/google/auth/oauth2/ServiceAccountTokenProviderTest.java create mode 100644 reactor/javatests/com/google/auth/oauth2/TokenProviderBase.java create mode 100644 reactor/javatests/com/google/auth/oauth2/UserCredentialsTokenProviderTest.java create mode 100644 reactor/javatests/fake-credential-key.json create mode 100644 reactor/pom.xml diff --git a/pom.xml b/pom.xml index 255ef7bfd..766c2baea 100644 --- a/pom.xml +++ b/pom.xml @@ -51,6 +51,7 @@ credentials oauth2_http appengine + reactor bom diff --git a/reactor/java/com/google/auth/oauth2/CacheableTokenProvider.java b/reactor/java/com/google/auth/oauth2/CacheableTokenProvider.java new file mode 100644 index 000000000..75bcc9595 --- /dev/null +++ b/reactor/java/com/google/auth/oauth2/CacheableTokenProvider.java @@ -0,0 +1,67 @@ +/* + * Copyright 2017-2023 the original author or authors. + * + * 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 + * + * https://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.auth.oauth2; + +import java.util.concurrent.atomic.AtomicReference; +import reactor.core.publisher.Mono; + +public class CacheableTokenProvider implements ReactiveTokenProvider { + + private ReactiveTokenProvider reactiveTokenProvider; + + private AtomicReference> atomicReference; + + private final RefreshThreshold refreshThreshold; + + public CacheableTokenProvider(ReactiveTokenProvider reactiveTokenProvider) { + this(reactiveTokenProvider, new RefreshThreshold()); + } + + public CacheableTokenProvider(ReactiveTokenProvider reactiveTokenProvider, + RefreshThreshold refreshThreshold) { + this.reactiveTokenProvider = reactiveTokenProvider; + this.atomicReference = new AtomicReference<>(cachedRetrieval()); + this.refreshThreshold = refreshThreshold; + } + + @Override + public Mono retrieve() { + Mono currentInstance = atomicReference.get(); + return currentInstance.flatMap(t -> createNewIfCloseToExpiration(currentInstance, t)); + } + + private Mono createNewIfCloseToExpiration(Mono currentInstance, + AccessToken t) { + boolean expiresSoon = refreshThreshold.over(t); + if (expiresSoon) { + return refreshOnExpiration(currentInstance); + } else { + return Mono.just(t); + } + } + + private Mono refreshOnExpiration(Mono expected) { + Mono toExecute = cachedRetrieval(); + atomicReference.compareAndSet(expected, toExecute); + return atomicReference.get(); + } + + Mono cachedRetrieval() { + return reactiveTokenProvider.retrieve().cache(); + } + +} \ No newline at end of file diff --git a/reactor/java/com/google/auth/oauth2/ComputeEngineTokenProvider.java b/reactor/java/com/google/auth/oauth2/ComputeEngineTokenProvider.java new file mode 100644 index 000000000..4d12e7add --- /dev/null +++ b/reactor/java/com/google/auth/oauth2/ComputeEngineTokenProvider.java @@ -0,0 +1,75 @@ +/* + * Copyright 2017-2023 the original author or authors. + * + * 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 + * + * https://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.auth.oauth2; + +import static com.google.auth.oauth2.Constants.ACCESS_TOKEN; +import static com.google.auth.oauth2.Constants.ERROR_PARSING_TOKEN_REFRESH_RESPONSE; +import static com.google.auth.oauth2.Constants.EXPIRES_IN; + +import com.google.api.client.util.GenericData; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; + +import java.io.IOException; +import java.util.Date; + + +public class ComputeEngineTokenProvider implements ReactiveTokenProvider { + + + private final WebClient webClient; + private final ComputeEngineCredentials computeEngineCredentials; + + private final String tokenUrl; + + public ComputeEngineTokenProvider(WebClient webClient, + ComputeEngineCredentials computeEngineCredentials) { + this.webClient = webClient; + this.computeEngineCredentials = computeEngineCredentials; + tokenUrl = computeEngineCredentials.createTokenUrlWithScopes(); + } + + public ComputeEngineTokenProvider(WebClient webClient, + ComputeEngineCredentials computeEngineCredentials, String tokenUrl) { + this.webClient = webClient; + this.computeEngineCredentials = computeEngineCredentials; + this.tokenUrl = tokenUrl; + } + + @Override + public Mono retrieve() { + return webClient.get().uri(tokenUrl) + .header("Metadata-Flavor", "Google") + .retrieve() + .bodyToMono(GenericData.class) + .flatMap(gd -> { + try { + String tokenValue = OAuth2Utils.validateString(gd, ACCESS_TOKEN, + ERROR_PARSING_TOKEN_REFRESH_RESPONSE); + int expiresInSeconds = OAuth2Utils.validateInt32(gd, EXPIRES_IN, + ERROR_PARSING_TOKEN_REFRESH_RESPONSE); + long expiresAtMilliseconds = + computeEngineCredentials.clock.currentTimeMillis() + (expiresInSeconds * 1000L); + AccessToken accessToken = new AccessToken(tokenValue, new Date(expiresAtMilliseconds)); + return Mono.just(accessToken); + } catch (IOException e) { + return Mono.error(e); + } + }); + } + +} \ No newline at end of file diff --git a/reactor/java/com/google/auth/oauth2/Constants.java b/reactor/java/com/google/auth/oauth2/Constants.java new file mode 100644 index 000000000..67f1d45d8 --- /dev/null +++ b/reactor/java/com/google/auth/oauth2/Constants.java @@ -0,0 +1,28 @@ +/* + * Copyright 2017-2023 the original author or authors. + * + * 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 + * + * https://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.auth.oauth2; + +public class Constants { + + static final String ACCESS_TOKEN = "access_token"; + static final String EXPIRES_IN = "expires_in"; + static final String ERROR_PARSING_TOKEN_REFRESH_RESPONSE = "Error parsing token refresh response. "; + + private Constants() { + } + +} \ No newline at end of file diff --git a/reactor/java/com/google/auth/oauth2/ReactiveTokenProvider.java b/reactor/java/com/google/auth/oauth2/ReactiveTokenProvider.java new file mode 100644 index 000000000..0c72825f6 --- /dev/null +++ b/reactor/java/com/google/auth/oauth2/ReactiveTokenProvider.java @@ -0,0 +1,54 @@ +/* + * Copyright 2017-2023 the original author or authors. + * + * 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 + * + * https://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.auth.oauth2; + +import com.google.auth.Credentials; +import com.google.auth.oauth2.AccessToken; +import com.google.auth.oauth2.ComputeEngineCredentials; +import com.google.auth.oauth2.ServiceAccountCredentials; +import com.google.auth.oauth2.UserCredentials; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; + +public interface ReactiveTokenProvider { + + Mono retrieve(); + + static ReactiveTokenProvider createCacheable(Credentials credentials) { + ReactiveTokenProvider reactiveTokenProvider = create(credentials); + return new CacheableTokenProvider(reactiveTokenProvider); + } + + static ReactiveTokenProvider create(Credentials credentials) { + WebClient webClient = WebClient.builder().build(); + return create(credentials, webClient); + } + + static ReactiveTokenProvider create(Credentials credentials, WebClient webClient) { + if (credentials instanceof UserCredentials) { + return new UserCredentialsTokenProvider(webClient, (UserCredentials) credentials); + } else if (credentials instanceof ServiceAccountCredentials) { + return new ServiceAccountTokenProvider(webClient, (ServiceAccountCredentials) credentials); + } else if (credentials instanceof ComputeEngineCredentials) { + return new ComputeEngineTokenProvider(webClient, (ComputeEngineCredentials) credentials); + } else { + throw new UnsupportedOperationException( + "Unsupported credentials type. UserCredentials,ServiceAccountCredentials,ComputeEngineCredentials are supported"); + } + } + +} diff --git a/reactor/java/com/google/auth/oauth2/RefreshThreshold.java b/reactor/java/com/google/auth/oauth2/RefreshThreshold.java new file mode 100644 index 000000000..4bbffa563 --- /dev/null +++ b/reactor/java/com/google/auth/oauth2/RefreshThreshold.java @@ -0,0 +1,40 @@ +/* + * Copyright 2017-2023 the original author or authors. + * + * 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 + * + * https://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.auth.oauth2; + +import com.google.auth.oauth2.AccessToken; + +class RefreshThreshold { + + static final int DEFAULT_MILLIS_BEFORE_EXPIRATION = 60 * 1000; + private final int millisBeforeExpiration; + + public RefreshThreshold() { + this(DEFAULT_MILLIS_BEFORE_EXPIRATION); + } + + public RefreshThreshold(int millisBeforeExpiration) { + this.millisBeforeExpiration = millisBeforeExpiration; + } + + boolean over(AccessToken accessToken) { + long currentMillis = System.currentTimeMillis(); + long expirationMillis = accessToken.getExpirationTime().getTime(); + return currentMillis > expirationMillis - millisBeforeExpiration; + } + +} \ No newline at end of file diff --git a/reactor/java/com/google/auth/oauth2/ServiceAccountTokenProvider.java b/reactor/java/com/google/auth/oauth2/ServiceAccountTokenProvider.java new file mode 100644 index 000000000..8b54c1565 --- /dev/null +++ b/reactor/java/com/google/auth/oauth2/ServiceAccountTokenProvider.java @@ -0,0 +1,95 @@ +/* + * Copyright 2017-2023 the original author or authors. + * + * 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 + * + * https://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.auth.oauth2; + +import com.google.api.client.json.JsonFactory; +import com.google.api.client.util.GenericData; +import java.io.IOException; +import java.util.Date; +import org.springframework.http.MediaType; +import org.springframework.web.reactive.function.BodyInserters; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; + +public class ServiceAccountTokenProvider implements ReactiveTokenProvider { + + private static final String GRANT_TYPE = "grant_type"; + private static final String ASSERTION = "assertion"; + private final WebClient webClient; + private final ServiceAccountCredentials serviceAccountCredentials; + + private final String tokenUrl; + + public ServiceAccountTokenProvider(WebClient webClient, + ServiceAccountCredentials serviceAccountCredentials) { + this.webClient = webClient; + this.serviceAccountCredentials = serviceAccountCredentials; + tokenUrl = OAuth2Utils.TOKEN_SERVER_URI.toString(); + } + + public ServiceAccountTokenProvider(WebClient webClient, + ServiceAccountCredentials serviceAccountCredentials, String tokenUrl) { + this.webClient = webClient; + this.serviceAccountCredentials = serviceAccountCredentials; + this.tokenUrl = tokenUrl; + } + + @Override + public Mono retrieve() { + JsonFactory jsonFactory = OAuth2Utils.JSON_FACTORY; + long currentTime = serviceAccountCredentials.clock.currentTimeMillis(); + + try { + String assertion = serviceAccountCredentials.createAssertion(jsonFactory, currentTime); + + return webClient.post() + .uri(tokenUrl) + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .body(refreshForm(assertion)) + .retrieve() + .bodyToMono(GenericData.class) + .flatMap(gd -> { + try { + AccessToken accessToken = getAccessToken(gd); + return Mono.just(accessToken); + } catch (IOException e) { + return Mono.error(e); + } + }); + + } catch (IOException e) { + return Mono.error(e); + } + } + + private AccessToken getAccessToken(GenericData gd) throws IOException { + String tokenValue = OAuth2Utils.validateString(gd, "access_token", + "Error parsing token refresh response. "); + int expiresInSeconds = OAuth2Utils.validateInt32(gd, "expires_in", + "Error parsing token refresh response. "); + long expiresAtMilliseconds = + serviceAccountCredentials.clock.currentTimeMillis() + (long) expiresInSeconds * 1000L; + AccessToken accessToken = new AccessToken(tokenValue, new Date(expiresAtMilliseconds)); + return accessToken; + } + + private static BodyInserters.FormInserter refreshForm(String assertion) { + return BodyInserters.fromFormData(GRANT_TYPE, OAuth2Utils.GRANT_TYPE_JWT_BEARER) + .with(ASSERTION, assertion); + } + +} \ No newline at end of file diff --git a/reactor/java/com/google/auth/oauth2/UserCredentialsTokenProvider.java b/reactor/java/com/google/auth/oauth2/UserCredentialsTokenProvider.java new file mode 100644 index 000000000..9fd94cbd3 --- /dev/null +++ b/reactor/java/com/google/auth/oauth2/UserCredentialsTokenProvider.java @@ -0,0 +1,95 @@ +/* + * Copyright 2017-2023 the original author or authors. + * + * 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 + * + * https://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.auth.oauth2; + +import com.google.api.client.util.GenericData; +import com.google.cloud.spring.core.ReactiveTokenProvider; +import java.io.IOException; +import java.util.Date; +import org.springframework.http.MediaType; +import org.springframework.web.reactive.function.BodyInserters; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; + +public class UserCredentialsTokenProvider implements ReactiveTokenProvider { + + private final WebClient webClient; + private final UserCredentials userCredentials; + + private final String tokenUrl; + + + public UserCredentialsTokenProvider(WebClient webClient, UserCredentials userCredentials) { + this.webClient = webClient; + this.userCredentials = userCredentials; + this.tokenUrl = OAuth2Utils.TOKEN_SERVER_URI.toString(); + } + + public UserCredentialsTokenProvider(WebClient webClient, UserCredentials userCredentials, + String tokenUrl) { + this.webClient = webClient; + this.userCredentials = userCredentials; + this.tokenUrl = tokenUrl; + } + + @Override + public Mono retrieve() { + if (userCredentials.getRefreshToken() == null) { + throw new IllegalStateException( + "UserCredentials instance cannot refresh because there is no refresh token."); + } else { + GenericData tokenRequest = new GenericData(); + tokenRequest.set("client_id", this.userCredentials.getClientId()); + tokenRequest.set("client_secret", this.userCredentials.getClientSecret()); + tokenRequest.set("refresh_token", this.userCredentials.getRefreshToken()); + tokenRequest.set("grant_type", "refresh_token"); + + return webClient.post().uri(tokenUrl) + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .body(BodyInserters.fromFormData("client_id", this.userCredentials.getClientId()) + .with("client_secret", this.userCredentials.getClientSecret()) + .with("refresh_token", this.userCredentials.getRefreshToken()) + .with("grant_type", "refresh_token") + ).retrieve().bodyToMono(GenericData.class) + .flatMap(gd -> { + try { + AccessToken accessToken = toAccessToken(gd); + return Mono.just(accessToken); + } catch (IOException e) { + return Mono.error(e); + } + }); + } + + } + + private AccessToken toAccessToken(GenericData gd) throws IOException { + String accessTokenValue = OAuth2Utils.validateString(gd, "access_token", + "Error parsing token refresh response. "); + int expiresInSeconds = OAuth2Utils.validateInt32(gd, "expires_in", + "Error parsing token refresh response. "); + long expiresAtMilliseconds = + userCredentials.clock.currentTimeMillis() + (long) (expiresInSeconds * 1000); + String scopes = OAuth2Utils.validateOptionalString(gd, "scope", + "Error parsing token refresh response. "); + AccessToken accessToken = AccessToken.newBuilder() + .setExpirationTime(new Date(expiresAtMilliseconds)).setTokenValue(accessTokenValue) + .setScopes(scopes).build(); + return accessToken; + } + +} \ No newline at end of file diff --git a/reactor/javatests/com/google/auth/oauth2/CacheableTokenProviderTest.java b/reactor/javatests/com/google/auth/oauth2/CacheableTokenProviderTest.java new file mode 100644 index 000000000..18289dc7f --- /dev/null +++ b/reactor/javatests/com/google/auth/oauth2/CacheableTokenProviderTest.java @@ -0,0 +1,70 @@ +/* + * Copyright 2017-2023 the original author or authors. + * + * 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 + * + * https://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.auth.oauth2; + + +import java.util.Date; +import java.util.UUID; + +import org.junit.Test; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +public class CacheableTokenProviderTest { + + @Test + void testRetrieveCache() { + ReactiveTokenProvider reactiveTokenProvider = () -> Mono.fromSupplier(() -> { + String token = UUID.randomUUID().toString(); + Date date = new Date(); + return new AccessToken(token, date); + }); + + ReactiveTokenProvider cacheableTokenProvider = new CacheableTokenProvider( + reactiveTokenProvider); + + StepVerifier.create( + Mono.zip(cacheableTokenProvider.retrieve(), cacheableTokenProvider.retrieve()) + .map(tuple -> tuple.getT1().equals(tuple.getT2()))) + .expectNext(true) + .verifyComplete(); + } + + @Test + void testRetrieveExpired() throws InterruptedException { + ReactiveTokenProvider reactiveTokenProvider = () -> Mono.fromSupplier(() -> { + String token = UUID.randomUUID().toString(); + return new AccessToken(token, new Date(System.currentTimeMillis() + 4 * 1000)); + }); + + ReactiveTokenProvider cacheableTokenProvider = new CacheableTokenProvider( + reactiveTokenProvider); + + Mono firstMono = cacheableTokenProvider.retrieve(); + + AccessToken accessToken = firstMono.block(); + + Thread.sleep(1000L); + Mono secondMono = cacheableTokenProvider.retrieve(); + + StepVerifier.create(secondMono.map(at -> at.equals(accessToken))) + .expectNext(false) + .verifyComplete(); + } + + +} \ No newline at end of file diff --git a/reactor/javatests/com/google/auth/oauth2/ComputeEngineTokenProviderTest.java b/reactor/javatests/com/google/auth/oauth2/ComputeEngineTokenProviderTest.java new file mode 100644 index 000000000..0528b737f --- /dev/null +++ b/reactor/javatests/com/google/auth/oauth2/ComputeEngineTokenProviderTest.java @@ -0,0 +1,87 @@ +/* + * Copyright 2017-2023 the original author or authors. + * + * 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 + * + * https://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.auth.oauth2; + + +import java.io.IOException; +import java.net.URISyntaxException; +import okhttp3.mockwebserver.MockWebServer; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.test.StepVerifier; + +import static com.google.auth.oauth2.TokenProviderBase.*; + +public class ComputeEngineTokenProviderTest { + + private static final Long SECONDS = 3600L; + private static final String ACCESS_TOKEN = "ya29.a0AfH6SMAa-dKy_..."; + + private static String COMPUTE_ENGINE_TOKEN = + "{\"access_token\":\"" + ACCESS_TOKEN + "\",\"expires_in\":" + SECONDS + + ",\"token_type\":\"Bearer\"}"; + private static String COMPUTE_ENGINE_TOKEN_BAD_RESPONSE = + "{\"token\":\"" + ACCESS_TOKEN + "\",\"in\":" + SECONDS + ",\"token_type\":\"Bearer\"}"; + + private MockWebServer mockWebServer; + + private WebClient webClient; + + private String tokenUri; + + @Before + public void setUp() throws IOException, URISyntaxException { + mockWebServer = new MockWebServer(); + mockWebServer.start(); + + webClient = WebClient.builder().build(); + tokenUri = mockWebServer.url("/").toString(); + } + + @Test + public void testRetrieve() { + mockWebServer.enqueue(successfulResponse(COMPUTE_ENGINE_TOKEN)); + ComputeEngineCredentials computeEngineCredentials = ComputeEngineCredentials.create(); + ReactiveTokenProvider tokenProvider = new ComputeEngineTokenProvider(webClient, + computeEngineCredentials, tokenUri); + Long expirationWindowStart = addExpiration(System.currentTimeMillis()); + StepVerifier.create(tokenProvider.retrieve()) + .expectNextMatches(at -> expectedToken(expirationWindowStart, at)) + .expectNext() + .verifyComplete(); + } + + + @Test + public void testRetrieveErrorParsingResponse() { + mockWebServer.enqueue(successfulResponse(COMPUTE_ENGINE_TOKEN_BAD_RESPONSE)); + ComputeEngineCredentials computeEngineCredentials = ComputeEngineCredentials.create(); + ReactiveTokenProvider tokenProvider = new ComputeEngineTokenProvider(webClient, + computeEngineCredentials, tokenUri); + StepVerifier.create(tokenProvider.retrieve()) + .expectNext() + .verifyErrorMatches(TokenProviderBase::tokenParseError); + } + + @After + public void tearDown() throws IOException { + mockWebServer.shutdown(); + } + +} \ No newline at end of file diff --git a/reactor/javatests/com/google/auth/oauth2/ReactiveTokenProviderTest.java b/reactor/javatests/com/google/auth/oauth2/ReactiveTokenProviderTest.java new file mode 100644 index 000000000..223bff07f --- /dev/null +++ b/reactor/javatests/com/google/auth/oauth2/ReactiveTokenProviderTest.java @@ -0,0 +1,65 @@ + +/* + * Copyright 2017-2023 the original author or authors. + * + * 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 + * + * https://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.auth.oauth2; + + +import java.io.IOException; + +import org.junit.Test; +import org.springframework.core.io.ClassPathResource; + +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; + + +public class ReactiveTokenProviderTest { + + @Test + public void testCreateCacheable() throws IOException { + ClassPathResource classPathResource = new ClassPathResource("fake-credential-key.json"); + ServiceAccountCredentials serviceAccountCredentials = ServiceAccountCredentials.fromStream( + classPathResource.getInputStream()); + ReactiveTokenProvider reactiveTokenProvider = ReactiveTokenProvider.createCacheable( + serviceAccountCredentials); + assertTrue(reactiveTokenProvider instanceof CacheableTokenProvider); + } + + @Test + public void testCreateUserCredentialsTokenProvider() { + UserCredentials userCredentials = mock(UserCredentials.class); + ReactiveTokenProvider reactiveTokenProvider = ReactiveTokenProvider.create(userCredentials); + assertTrue(reactiveTokenProvider instanceof UserCredentialsTokenProvider); + } + + @Test + public void testCreateServiceAccountTokenProvider() { + ServiceAccountCredentials serviceAccountCredentials = mock(ServiceAccountCredentials.class); + ReactiveTokenProvider reactiveTokenProvider = ReactiveTokenProvider.create( + serviceAccountCredentials); + assertTrue(reactiveTokenProvider instanceof ServiceAccountTokenProvider); + } + + @Test + public void testCreateComputeEngineTokenProvider() { + ComputeEngineCredentials computeEngineCredentials = mock(ComputeEngineCredentials.class); + ReactiveTokenProvider reactiveTokenProvider = ReactiveTokenProvider.create( + computeEngineCredentials); + assertTrue(reactiveTokenProvider instanceof ComputeEngineTokenProvider); + } + +} \ No newline at end of file diff --git a/reactor/javatests/com/google/auth/oauth2/RefreshThresholdTest.java b/reactor/javatests/com/google/auth/oauth2/RefreshThresholdTest.java new file mode 100644 index 000000000..de2cf057a --- /dev/null +++ b/reactor/javatests/com/google/auth/oauth2/RefreshThresholdTest.java @@ -0,0 +1,45 @@ +/* + * Copyright 2017-2023 the original author or authors. + * + * 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 + * + * https://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.auth.oauth2; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import com.google.auth.oauth2.AccessToken; +import org.junit.Test; + +import java.util.Date; + +public class RefreshThresholdTest { + + @Test + public void testCloseToExpirationTrue() { + long currentDate = System.currentTimeMillis(); + RefreshThreshold refreshThreshold = new RefreshThreshold(10_000); + AccessToken accessToken = new AccessToken("token", new Date(currentDate + 9_500)); + assertTrue(refreshThreshold.over(accessToken)); + } + + @Test + public void testCloseToExpirationFalse() { + long currentDate = System.currentTimeMillis(); + RefreshThreshold refreshThreshold = new RefreshThreshold(10_000); + AccessToken accessToken = new AccessToken("token", new Date(currentDate + 20_000)); + assertFalse(refreshThreshold.over(accessToken)); + } + +} diff --git a/reactor/javatests/com/google/auth/oauth2/ServiceAccountTokenProviderTest.java b/reactor/javatests/com/google/auth/oauth2/ServiceAccountTokenProviderTest.java new file mode 100644 index 000000000..64155df1b --- /dev/null +++ b/reactor/javatests/com/google/auth/oauth2/ServiceAccountTokenProviderTest.java @@ -0,0 +1,99 @@ +/* + * Copyright 2017-2023 the original author or authors. + * + * 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 + * + * https://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.auth.oauth2; + +import static com.google.auth.oauth2.TokenProviderBase.ACCESS_TOKEN; +import static com.google.auth.oauth2.TokenProviderBase.SECONDS; +import static com.google.auth.oauth2.TokenProviderBase.addExpiration; +import static com.google.auth.oauth2.TokenProviderBase.expectedToken; +import static com.google.auth.oauth2.TokenProviderBase.successfulResponse; + +import java.io.IOException; +import java.net.URISyntaxException; +import okhttp3.mockwebserver.MockWebServer; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.springframework.core.io.ClassPathResource; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.test.StepVerifier; + +public class ServiceAccountTokenProviderTest { + + private static final ClassPathResource SA_RESOURCE = new ClassPathResource( + "fake-credential-key.json"); + + private static final String SERVICE_ACCOUNT_TOKEN = "{\n" + + " \"access_token\": \"" + ACCESS_TOKEN + "\",\n" + + " \"expires_in\": " + SECONDS + ",\n" + + " \"scope\": \"https://www.googleapis.com/auth/drive.readonly\",\n" + + " \"token_type\": \"Bearer\"\n" + + "}"; + + private static final String SERVICE_ACCOUNT_TOKEN_BAD_RESPONSE = "{\n" + + " \"token\": \"" + ACCESS_TOKEN + "\",\n" + + " \"in\": " + SECONDS + ",\n" + + " \"scope\": \"https://www.googleapis.com/auth/drive.readonly\",\n" + + " \"token_type\": \"Bearer\"\n" + + "}"; + + private MockWebServer mockWebServer; + + private WebClient webClient; + + private String tokenUrl; + + @Before + void setUp() throws IOException, URISyntaxException { + mockWebServer = new MockWebServer(); + mockWebServer.start(); + + webClient = WebClient.builder().build(); + tokenUrl = mockWebServer.url("/").toString(); + } + + @Test + void testRetrieve() throws IOException { + mockWebServer.enqueue(successfulResponse(SERVICE_ACCOUNT_TOKEN)); + ServiceAccountCredentials serviceAccountCredentials = ServiceAccountCredentials.fromStream( + SA_RESOURCE.getInputStream()); + ReactiveTokenProvider tokenProvider = new ServiceAccountTokenProvider(webClient, + serviceAccountCredentials, tokenUrl); + Long expirationWindowStart = addExpiration(System.currentTimeMillis()); + StepVerifier.create(tokenProvider.retrieve()) + .expectNextMatches(at -> expectedToken(expirationWindowStart, at)) + .verifyComplete(); + } + + @Test + void testRetrieveErrorParsingResponse() throws IOException { + mockWebServer.enqueue(successfulResponse(SERVICE_ACCOUNT_TOKEN_BAD_RESPONSE)); + ServiceAccountCredentials serviceAccountCredentials = ServiceAccountCredentials.fromStream( + SA_RESOURCE.getInputStream()); + ReactiveTokenProvider tokenProvider = new ServiceAccountTokenProvider(webClient, + serviceAccountCredentials, tokenUrl); + StepVerifier.create(tokenProvider.retrieve()) + .expectNext() + .verifyErrorMatches(TokenProviderBase::tokenParseError); + } + + @After + void tearDown() throws IOException { + mockWebServer.shutdown(); + } + +} \ No newline at end of file diff --git a/reactor/javatests/com/google/auth/oauth2/TokenProviderBase.java b/reactor/javatests/com/google/auth/oauth2/TokenProviderBase.java new file mode 100644 index 000000000..fdac12154 --- /dev/null +++ b/reactor/javatests/com/google/auth/oauth2/TokenProviderBase.java @@ -0,0 +1,62 @@ +/* + * Copyright 2017-2023 the original author or authors. + * + * 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 + * + * https://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.auth.oauth2; + +import static com.google.auth.oauth2.Constants.ERROR_PARSING_TOKEN_REFRESH_RESPONSE; + +import java.io.IOException; +import okhttp3.mockwebserver.MockResponse; + +public class TokenProviderBase { + + static final Long SECONDS = 3600L; + static final String ACCESS_TOKEN = "ya29.a0AfH6SMAa-dKy_..."; + + private TokenProviderBase() { + } + + static MockResponse successfulResponse(String response) { + return new MockResponse() + .setHeader("Content-Type", "application/json") + .setResponseCode(200) + .setBody(response); + } + + static long addExpiration(Long millis) { + return millis + (SECONDS * 1000L); + } + + static boolean expectedToken(Long expirationWindowStart, AccessToken at) { + boolean isExpirationWithinWindow = isExpirationWithinWindow(expirationWindowStart, at); + return isExpirationWithinWindow + && at.getTokenValue().equals(ACCESS_TOKEN); + } + + static boolean tokenParseError(Throwable e) { + return e instanceof IOException + && e.getMessage().contains(ERROR_PARSING_TOKEN_REFRESH_RESPONSE); + } + + static boolean isExpirationWithinWindow(Long expirationWindowStart, AccessToken at) { + Long expirationWindowEnd = addExpiration(System.currentTimeMillis()); + Long expiration = at.getExpirationTimeMillis(); + boolean expirationWithinLimits = + expiration <= expirationWindowEnd && expiration >= expirationWindowStart; + return expirationWithinLimits; + } + +} \ No newline at end of file diff --git a/reactor/javatests/com/google/auth/oauth2/UserCredentialsTokenProviderTest.java b/reactor/javatests/com/google/auth/oauth2/UserCredentialsTokenProviderTest.java new file mode 100644 index 000000000..2b539fb8c --- /dev/null +++ b/reactor/javatests/com/google/auth/oauth2/UserCredentialsTokenProviderTest.java @@ -0,0 +1,90 @@ +/* + * Copyright 2017-2023 the original author or authors. + * + * 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 + * + * https://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.auth.oauth2; + +import static com.google.auth.oauth2.TokenProviderBase.ACCESS_TOKEN; +import static com.google.auth.oauth2.TokenProviderBase.successfulResponse; + +import java.io.IOException; +import java.net.URISyntaxException; +import okhttp3.mockwebserver.MockWebServer; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.test.StepVerifier; + +public class UserCredentialsTokenProviderTest { + + private static String USER_CREDENTIALS_TOKEN = + "{\"access_token\":\"" + ACCESS_TOKEN + + "\",\"token_type\":\"Bearer\",\"expires_in\":3599,\"refresh_token\":\"test_refresh_token\",\"scope\":\"https://www.googleapis.com/auth/cloud-platform\"}"; + + private static String USER_CREDENTIALS_TOKEN_BAD_RESPONSE = + "{\"token\":\"" + ACCESS_TOKEN + + "\",\"token_type\":\"Bearer\",\"in\":3599,\"refresh_token\":\"test_refresh_token\",\"scope\":\"https://www.googleapis.com/auth/cloud-platform\"}"; + + private MockWebServer mockWebServer; + + private WebClient webClient; + + private String tokenUrl; + + @Before + public void setUp() throws IOException, URISyntaxException { + mockWebServer = new MockWebServer(); + mockWebServer.start(); + webClient = WebClient.builder().build(); + tokenUrl = mockWebServer.url("/").toString(); + } + + @Test + public void testRetrieve() throws Exception { + mockWebServer.enqueue(successfulResponse(USER_CREDENTIALS_TOKEN)); + UserCredentials userCredentials = createUserCredentials(); + ReactiveTokenProvider tokenProvider = new UserCredentialsTokenProvider(webClient, + userCredentials, tokenUrl); + StepVerifier.create(tokenProvider.retrieve().map(AccessToken::getTokenValue)) + .expectNext(ACCESS_TOKEN) + .verifyComplete(); + } + + @Test + public void testRetrieveErrorParsingResponse() { + mockWebServer.enqueue(successfulResponse(USER_CREDENTIALS_TOKEN_BAD_RESPONSE)); + UserCredentials userCredentials = createUserCredentials(); + ReactiveTokenProvider tokenProvider = new UserCredentialsTokenProvider(webClient, + userCredentials, tokenUrl); + StepVerifier.create(tokenProvider.retrieve()) + .expectNext() + .verifyErrorMatches(TokenProviderBase::tokenParseError); + } + + @After + public void tearDown() throws IOException { + mockWebServer.shutdown(); + } + + private static UserCredentials createUserCredentials() { + return UserCredentials.newBuilder() + .setClientId("test_client_id") + .setClientSecret("test_client_secret") + .setRefreshToken("refresh_token") + .build(); + } + +} \ No newline at end of file diff --git a/reactor/javatests/fake-credential-key.json b/reactor/javatests/fake-credential-key.json new file mode 100644 index 000000000..56b44a223 --- /dev/null +++ b/reactor/javatests/fake-credential-key.json @@ -0,0 +1,12 @@ +{ + "type": "service_account", + "project_id": "fake-project-id", + "private_key_id": "fake-key", + "private_key": "-----BEGIN PRIVATE KEY-----\nMIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBANJrTOsRMUr/Mkvq\nVLfTkwASU3YmQEioDD/7kmrGLHzQDHl8H/d4AdnGJ9i6XXQTJnfZScIsEfzXrK/B\n3Zavpf3u41VJR9n8VqTNtlR9aCic+h4H7vhZAKD2HvkI1KW69ST+WYZNeZpAU5Z/\nzF1ndrmkJCu70usGX1Pzbz3SnnBvAgMBAAECgYAgzbuTFf4STBCiRyjn86MCKtk6\nHSJ+cTxfqS+dV8HNv32CXvh40wuu0LabkgpJs0aW/pgCHm67dUAlslqCSGXfFUCJ\n46doBP7B5Fh1uNUQNFRGxHXNKKhXX2b1si5Py5zRtM4KP0Of61ExRO+Z5MODwFad\n9kSmDShyAC02z7/AUQJBAO3pFt65VHsdE5JRg376195PbQaRtDHeTFkdHVtKSFYF\nQI5GsAOy8OgLXHPzsl+ot2bbiHFMfptAD8nBXRogwScCQQDiaxgACPrO56lCJnMZ\nxM+etTzZ3IL1G/co6cbNb9sagSkgYDwPJcVCThNBhSjncmPcwfpW6aNr/JWa7Z8M\nkNN5AkAzzmEDiPnjgTZk00k+GmNtboBAQPQrM8wOT6+31FoiGSywjqX/eDTLYsX0\nHeoGuJePV1jDyzN6nR2TAn9ClEVbAkEAgKrxYZu4w/ncMu5cvIken4dJBFmOxjHV\nPBfV1Qs6zQ4XTAHEP6tsNOjfgn1kqFpWK67ET73IE+bfMcLVfrOSqQJAFkDMY8B7\nG88fVmSVUnh5NHFP+oE5W5GjuMfnWi5VWfgJORyhzVB2zX1LhEOOy8RRyfUAfq1Q\nrz7IoJTmHNfn9A==\n-----END PRIVATE KEY-----\n", + "client_email": "test@fake-project.iam.gserviceaccount.com", + "client_id": "45678", + "auth_uri": "https://fake.auth.url.com/o/oauth2/auth", + "token_uri": "https://fake.token.url.com/token", + "auth_provider_x509_cert_url": "https://fake.auth.provider.cert.url.com/oauth2/v1/certs", + "client_x509_cert_url": "https://www.fake.client.cert.com/robot/v1/metadata/x509/test@fake-project.iam.gserviceaccount.com" +} \ No newline at end of file diff --git a/reactor/pom.xml b/reactor/pom.xml new file mode 100644 index 000000000..bcd85e0bf --- /dev/null +++ b/reactor/pom.xml @@ -0,0 +1,92 @@ + + + 4.0.0 + + com.google.auth + google-auth-library-parent + 1.19.1-SNAPSHOT + ../pom.xml + + + google-auth-library-reactor + Google Auth Library for Java - Reactor Binding + + + + ossrh + https://google.oss.sonatype.org/content/repositories/snapshots + + + + + java + javatests + + + javatests + + + + + org.sonatype.plugins + nexus-staging-maven-plugin + + + org.apache.maven.plugins + maven-source-plugin + + + org.apache.maven.plugins + maven-javadoc-plugin + + + org.apache.maven.plugins + maven-jar-plugin + + + + com.google.auth + + + + + + + + + + org.springframework.boot + spring-boot-starter-webflux + 2.7.8 + true + + + com.google.auth + google-auth-library-oauth2-http + + + io.projectreactor + reactor-test + test + 3.5.10 + + + com.squareup.okhttp3 + mockwebserver + 4.10.0 + test + + + junit + junit + test + + + org.mockito + mockito-core + 4.11.0 + test + + + + From df160777ec439e5eabb02aea11b6c9a722c7cc2b Mon Sep 17 00:00:00 2001 From: gkatzioura Date: Thu, 5 Oct 2023 08:27:02 +0100 Subject: [PATCH 2/2] 1296: Applied format --- .../auth/oauth2/CacheableTokenProvider.java | 69 +++++---- .../oauth2/ComputeEngineTokenProvider.java | 87 +++++------ .../com/google/auth/oauth2/Constants.java | 13 +- .../auth/oauth2/ReactiveTokenProvider.java | 51 +++---- .../google/auth/oauth2/RefreshThreshold.java | 31 ++-- .../oauth2/ServiceAccountTokenProvider.java | 113 +++++++-------- .../oauth2/UserCredentialsTokenProvider.java | 124 ++++++++-------- .../oauth2/CacheableTokenProviderTest.java | 90 ++++++------ .../ComputeEngineTokenProviderTest.java | 114 +++++++-------- .../oauth2/ReactiveTokenProviderTest.java | 75 +++++----- .../auth/oauth2/RefreshThresholdTest.java | 35 +++-- .../ServiceAccountTokenProviderTest.java | 135 ++++++++++-------- .../google/auth/oauth2/TokenProviderBase.java | 71 +++++---- .../UserCredentialsTokenProviderTest.java | 99 ++++++------- 14 files changed, 552 insertions(+), 555 deletions(-) diff --git a/reactor/java/com/google/auth/oauth2/CacheableTokenProvider.java b/reactor/java/com/google/auth/oauth2/CacheableTokenProvider.java index 75bcc9595..98e06f4ea 100644 --- a/reactor/java/com/google/auth/oauth2/CacheableTokenProvider.java +++ b/reactor/java/com/google/auth/oauth2/CacheableTokenProvider.java @@ -21,47 +21,46 @@ public class CacheableTokenProvider implements ReactiveTokenProvider { - private ReactiveTokenProvider reactiveTokenProvider; + private ReactiveTokenProvider reactiveTokenProvider; - private AtomicReference> atomicReference; + private AtomicReference> atomicReference; - private final RefreshThreshold refreshThreshold; + private final RefreshThreshold refreshThreshold; - public CacheableTokenProvider(ReactiveTokenProvider reactiveTokenProvider) { - this(reactiveTokenProvider, new RefreshThreshold()); - } - - public CacheableTokenProvider(ReactiveTokenProvider reactiveTokenProvider, - RefreshThreshold refreshThreshold) { - this.reactiveTokenProvider = reactiveTokenProvider; - this.atomicReference = new AtomicReference<>(cachedRetrieval()); - this.refreshThreshold = refreshThreshold; - } + public CacheableTokenProvider(ReactiveTokenProvider reactiveTokenProvider) { + this(reactiveTokenProvider, new RefreshThreshold()); + } - @Override - public Mono retrieve() { - Mono currentInstance = atomicReference.get(); - return currentInstance.flatMap(t -> createNewIfCloseToExpiration(currentInstance, t)); - } + public CacheableTokenProvider( + ReactiveTokenProvider reactiveTokenProvider, RefreshThreshold refreshThreshold) { + this.reactiveTokenProvider = reactiveTokenProvider; + this.atomicReference = new AtomicReference<>(cachedRetrieval()); + this.refreshThreshold = refreshThreshold; + } - private Mono createNewIfCloseToExpiration(Mono currentInstance, - AccessToken t) { - boolean expiresSoon = refreshThreshold.over(t); - if (expiresSoon) { - return refreshOnExpiration(currentInstance); - } else { - return Mono.just(t); - } - } + @Override + public Mono retrieve() { + Mono currentInstance = atomicReference.get(); + return currentInstance.flatMap(t -> createNewIfCloseToExpiration(currentInstance, t)); + } - private Mono refreshOnExpiration(Mono expected) { - Mono toExecute = cachedRetrieval(); - atomicReference.compareAndSet(expected, toExecute); - return atomicReference.get(); + private Mono createNewIfCloseToExpiration( + Mono currentInstance, AccessToken t) { + boolean expiresSoon = refreshThreshold.over(t); + if (expiresSoon) { + return refreshOnExpiration(currentInstance); + } else { + return Mono.just(t); } + } - Mono cachedRetrieval() { - return reactiveTokenProvider.retrieve().cache(); - } + private Mono refreshOnExpiration(Mono expected) { + Mono toExecute = cachedRetrieval(); + atomicReference.compareAndSet(expected, toExecute); + return atomicReference.get(); + } -} \ No newline at end of file + Mono cachedRetrieval() { + return reactiveTokenProvider.retrieve().cache(); + } +} diff --git a/reactor/java/com/google/auth/oauth2/ComputeEngineTokenProvider.java b/reactor/java/com/google/auth/oauth2/ComputeEngineTokenProvider.java index 4d12e7add..9855d5689 100644 --- a/reactor/java/com/google/auth/oauth2/ComputeEngineTokenProvider.java +++ b/reactor/java/com/google/auth/oauth2/ComputeEngineTokenProvider.java @@ -21,55 +21,56 @@ import static com.google.auth.oauth2.Constants.EXPIRES_IN; import com.google.api.client.util.GenericData; -import org.springframework.web.reactive.function.client.WebClient; -import reactor.core.publisher.Mono; - import java.io.IOException; import java.util.Date; - +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; public class ComputeEngineTokenProvider implements ReactiveTokenProvider { + private final WebClient webClient; + private final ComputeEngineCredentials computeEngineCredentials; - private final WebClient webClient; - private final ComputeEngineCredentials computeEngineCredentials; - - private final String tokenUrl; - - public ComputeEngineTokenProvider(WebClient webClient, - ComputeEngineCredentials computeEngineCredentials) { - this.webClient = webClient; - this.computeEngineCredentials = computeEngineCredentials; - tokenUrl = computeEngineCredentials.createTokenUrlWithScopes(); - } + private final String tokenUrl; - public ComputeEngineTokenProvider(WebClient webClient, - ComputeEngineCredentials computeEngineCredentials, String tokenUrl) { - this.webClient = webClient; - this.computeEngineCredentials = computeEngineCredentials; - this.tokenUrl = tokenUrl; - } + public ComputeEngineTokenProvider( + WebClient webClient, ComputeEngineCredentials computeEngineCredentials) { + this.webClient = webClient; + this.computeEngineCredentials = computeEngineCredentials; + tokenUrl = computeEngineCredentials.createTokenUrlWithScopes(); + } - @Override - public Mono retrieve() { - return webClient.get().uri(tokenUrl) - .header("Metadata-Flavor", "Google") - .retrieve() - .bodyToMono(GenericData.class) - .flatMap(gd -> { - try { - String tokenValue = OAuth2Utils.validateString(gd, ACCESS_TOKEN, - ERROR_PARSING_TOKEN_REFRESH_RESPONSE); - int expiresInSeconds = OAuth2Utils.validateInt32(gd, EXPIRES_IN, - ERROR_PARSING_TOKEN_REFRESH_RESPONSE); - long expiresAtMilliseconds = - computeEngineCredentials.clock.currentTimeMillis() + (expiresInSeconds * 1000L); - AccessToken accessToken = new AccessToken(tokenValue, new Date(expiresAtMilliseconds)); - return Mono.just(accessToken); - } catch (IOException e) { - return Mono.error(e); - } - }); - } + public ComputeEngineTokenProvider( + WebClient webClient, ComputeEngineCredentials computeEngineCredentials, String tokenUrl) { + this.webClient = webClient; + this.computeEngineCredentials = computeEngineCredentials; + this.tokenUrl = tokenUrl; + } -} \ No newline at end of file + @Override + public Mono retrieve() { + return webClient + .get() + .uri(tokenUrl) + .header("Metadata-Flavor", "Google") + .retrieve() + .bodyToMono(GenericData.class) + .flatMap( + gd -> { + try { + String tokenValue = + OAuth2Utils.validateString( + gd, ACCESS_TOKEN, ERROR_PARSING_TOKEN_REFRESH_RESPONSE); + int expiresInSeconds = + OAuth2Utils.validateInt32(gd, EXPIRES_IN, ERROR_PARSING_TOKEN_REFRESH_RESPONSE); + long expiresAtMilliseconds = + computeEngineCredentials.clock.currentTimeMillis() + (expiresInSeconds * 1000L); + AccessToken accessToken = + new AccessToken(tokenValue, new Date(expiresAtMilliseconds)); + return Mono.just(accessToken); + } catch (IOException e) { + return Mono.error(e); + } + }); + } +} diff --git a/reactor/java/com/google/auth/oauth2/Constants.java b/reactor/java/com/google/auth/oauth2/Constants.java index 67f1d45d8..b961505a5 100644 --- a/reactor/java/com/google/auth/oauth2/Constants.java +++ b/reactor/java/com/google/auth/oauth2/Constants.java @@ -18,11 +18,10 @@ public class Constants { - static final String ACCESS_TOKEN = "access_token"; - static final String EXPIRES_IN = "expires_in"; - static final String ERROR_PARSING_TOKEN_REFRESH_RESPONSE = "Error parsing token refresh response. "; + static final String ACCESS_TOKEN = "access_token"; + static final String EXPIRES_IN = "expires_in"; + static final String ERROR_PARSING_TOKEN_REFRESH_RESPONSE = + "Error parsing token refresh response. "; - private Constants() { - } - -} \ No newline at end of file + private Constants() {} +} diff --git a/reactor/java/com/google/auth/oauth2/ReactiveTokenProvider.java b/reactor/java/com/google/auth/oauth2/ReactiveTokenProvider.java index 0c72825f6..468598768 100644 --- a/reactor/java/com/google/auth/oauth2/ReactiveTokenProvider.java +++ b/reactor/java/com/google/auth/oauth2/ReactiveTokenProvider.java @@ -17,38 +17,33 @@ package com.google.auth.oauth2; import com.google.auth.Credentials; -import com.google.auth.oauth2.AccessToken; -import com.google.auth.oauth2.ComputeEngineCredentials; -import com.google.auth.oauth2.ServiceAccountCredentials; -import com.google.auth.oauth2.UserCredentials; import org.springframework.web.reactive.function.client.WebClient; import reactor.core.publisher.Mono; public interface ReactiveTokenProvider { - Mono retrieve(); - - static ReactiveTokenProvider createCacheable(Credentials credentials) { - ReactiveTokenProvider reactiveTokenProvider = create(credentials); - return new CacheableTokenProvider(reactiveTokenProvider); - } - - static ReactiveTokenProvider create(Credentials credentials) { - WebClient webClient = WebClient.builder().build(); - return create(credentials, webClient); + Mono retrieve(); + + static ReactiveTokenProvider createCacheable(Credentials credentials) { + ReactiveTokenProvider reactiveTokenProvider = create(credentials); + return new CacheableTokenProvider(reactiveTokenProvider); + } + + static ReactiveTokenProvider create(Credentials credentials) { + WebClient webClient = WebClient.builder().build(); + return create(credentials, webClient); + } + + static ReactiveTokenProvider create(Credentials credentials, WebClient webClient) { + if (credentials instanceof UserCredentials) { + return new UserCredentialsTokenProvider(webClient, (UserCredentials) credentials); + } else if (credentials instanceof ServiceAccountCredentials) { + return new ServiceAccountTokenProvider(webClient, (ServiceAccountCredentials) credentials); + } else if (credentials instanceof ComputeEngineCredentials) { + return new ComputeEngineTokenProvider(webClient, (ComputeEngineCredentials) credentials); + } else { + throw new UnsupportedOperationException( + "Unsupported credentials type. UserCredentials,ServiceAccountCredentials,ComputeEngineCredentials are supported"); } - - static ReactiveTokenProvider create(Credentials credentials, WebClient webClient) { - if (credentials instanceof UserCredentials) { - return new UserCredentialsTokenProvider(webClient, (UserCredentials) credentials); - } else if (credentials instanceof ServiceAccountCredentials) { - return new ServiceAccountTokenProvider(webClient, (ServiceAccountCredentials) credentials); - } else if (credentials instanceof ComputeEngineCredentials) { - return new ComputeEngineTokenProvider(webClient, (ComputeEngineCredentials) credentials); - } else { - throw new UnsupportedOperationException( - "Unsupported credentials type. UserCredentials,ServiceAccountCredentials,ComputeEngineCredentials are supported"); - } - } - + } } diff --git a/reactor/java/com/google/auth/oauth2/RefreshThreshold.java b/reactor/java/com/google/auth/oauth2/RefreshThreshold.java index 4bbffa563..b24da954c 100644 --- a/reactor/java/com/google/auth/oauth2/RefreshThreshold.java +++ b/reactor/java/com/google/auth/oauth2/RefreshThreshold.java @@ -16,25 +16,22 @@ package com.google.auth.oauth2; -import com.google.auth.oauth2.AccessToken; - class RefreshThreshold { - static final int DEFAULT_MILLIS_BEFORE_EXPIRATION = 60 * 1000; - private final int millisBeforeExpiration; - - public RefreshThreshold() { - this(DEFAULT_MILLIS_BEFORE_EXPIRATION); - } + static final int DEFAULT_MILLIS_BEFORE_EXPIRATION = 60 * 1000; + private final int millisBeforeExpiration; - public RefreshThreshold(int millisBeforeExpiration) { - this.millisBeforeExpiration = millisBeforeExpiration; - } + public RefreshThreshold() { + this(DEFAULT_MILLIS_BEFORE_EXPIRATION); + } - boolean over(AccessToken accessToken) { - long currentMillis = System.currentTimeMillis(); - long expirationMillis = accessToken.getExpirationTime().getTime(); - return currentMillis > expirationMillis - millisBeforeExpiration; - } + public RefreshThreshold(int millisBeforeExpiration) { + this.millisBeforeExpiration = millisBeforeExpiration; + } -} \ No newline at end of file + boolean over(AccessToken accessToken) { + long currentMillis = System.currentTimeMillis(); + long expirationMillis = accessToken.getExpirationTime().getTime(); + return currentMillis > expirationMillis - millisBeforeExpiration; + } +} diff --git a/reactor/java/com/google/auth/oauth2/ServiceAccountTokenProvider.java b/reactor/java/com/google/auth/oauth2/ServiceAccountTokenProvider.java index 8b54c1565..e7182e9d6 100644 --- a/reactor/java/com/google/auth/oauth2/ServiceAccountTokenProvider.java +++ b/reactor/java/com/google/auth/oauth2/ServiceAccountTokenProvider.java @@ -27,69 +27,70 @@ public class ServiceAccountTokenProvider implements ReactiveTokenProvider { - private static final String GRANT_TYPE = "grant_type"; - private static final String ASSERTION = "assertion"; - private final WebClient webClient; - private final ServiceAccountCredentials serviceAccountCredentials; + private static final String GRANT_TYPE = "grant_type"; + private static final String ASSERTION = "assertion"; + private final WebClient webClient; + private final ServiceAccountCredentials serviceAccountCredentials; - private final String tokenUrl; + private final String tokenUrl; - public ServiceAccountTokenProvider(WebClient webClient, - ServiceAccountCredentials serviceAccountCredentials) { - this.webClient = webClient; - this.serviceAccountCredentials = serviceAccountCredentials; - tokenUrl = OAuth2Utils.TOKEN_SERVER_URI.toString(); - } + public ServiceAccountTokenProvider( + WebClient webClient, ServiceAccountCredentials serviceAccountCredentials) { + this.webClient = webClient; + this.serviceAccountCredentials = serviceAccountCredentials; + tokenUrl = OAuth2Utils.TOKEN_SERVER_URI.toString(); + } - public ServiceAccountTokenProvider(WebClient webClient, - ServiceAccountCredentials serviceAccountCredentials, String tokenUrl) { - this.webClient = webClient; - this.serviceAccountCredentials = serviceAccountCredentials; - this.tokenUrl = tokenUrl; - } + public ServiceAccountTokenProvider( + WebClient webClient, ServiceAccountCredentials serviceAccountCredentials, String tokenUrl) { + this.webClient = webClient; + this.serviceAccountCredentials = serviceAccountCredentials; + this.tokenUrl = tokenUrl; + } - @Override - public Mono retrieve() { - JsonFactory jsonFactory = OAuth2Utils.JSON_FACTORY; - long currentTime = serviceAccountCredentials.clock.currentTimeMillis(); + @Override + public Mono retrieve() { + JsonFactory jsonFactory = OAuth2Utils.JSON_FACTORY; + long currentTime = serviceAccountCredentials.clock.currentTimeMillis(); - try { - String assertion = serviceAccountCredentials.createAssertion(jsonFactory, currentTime); + try { + String assertion = serviceAccountCredentials.createAssertion(jsonFactory, currentTime); - return webClient.post() - .uri(tokenUrl) - .contentType(MediaType.APPLICATION_FORM_URLENCODED) - .body(refreshForm(assertion)) - .retrieve() - .bodyToMono(GenericData.class) - .flatMap(gd -> { - try { - AccessToken accessToken = getAccessToken(gd); - return Mono.just(accessToken); - } catch (IOException e) { - return Mono.error(e); - } - }); + return webClient + .post() + .uri(tokenUrl) + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .body(refreshForm(assertion)) + .retrieve() + .bodyToMono(GenericData.class) + .flatMap( + gd -> { + try { + AccessToken accessToken = getAccessToken(gd); + return Mono.just(accessToken); + } catch (IOException e) { + return Mono.error(e); + } + }); - } catch (IOException e) { - return Mono.error(e); - } + } catch (IOException e) { + return Mono.error(e); } + } - private AccessToken getAccessToken(GenericData gd) throws IOException { - String tokenValue = OAuth2Utils.validateString(gd, "access_token", - "Error parsing token refresh response. "); - int expiresInSeconds = OAuth2Utils.validateInt32(gd, "expires_in", - "Error parsing token refresh response. "); - long expiresAtMilliseconds = - serviceAccountCredentials.clock.currentTimeMillis() + (long) expiresInSeconds * 1000L; - AccessToken accessToken = new AccessToken(tokenValue, new Date(expiresAtMilliseconds)); - return accessToken; - } - - private static BodyInserters.FormInserter refreshForm(String assertion) { - return BodyInserters.fromFormData(GRANT_TYPE, OAuth2Utils.GRANT_TYPE_JWT_BEARER) - .with(ASSERTION, assertion); - } + private AccessToken getAccessToken(GenericData gd) throws IOException { + String tokenValue = + OAuth2Utils.validateString(gd, "access_token", "Error parsing token refresh response. "); + int expiresInSeconds = + OAuth2Utils.validateInt32(gd, "expires_in", "Error parsing token refresh response. "); + long expiresAtMilliseconds = + serviceAccountCredentials.clock.currentTimeMillis() + (long) expiresInSeconds * 1000L; + AccessToken accessToken = new AccessToken(tokenValue, new Date(expiresAtMilliseconds)); + return accessToken; + } -} \ No newline at end of file + private static BodyInserters.FormInserter refreshForm(String assertion) { + return BodyInserters.fromFormData(GRANT_TYPE, OAuth2Utils.GRANT_TYPE_JWT_BEARER) + .with(ASSERTION, assertion); + } +} diff --git a/reactor/java/com/google/auth/oauth2/UserCredentialsTokenProvider.java b/reactor/java/com/google/auth/oauth2/UserCredentialsTokenProvider.java index 9fd94cbd3..cd1a17920 100644 --- a/reactor/java/com/google/auth/oauth2/UserCredentialsTokenProvider.java +++ b/reactor/java/com/google/auth/oauth2/UserCredentialsTokenProvider.java @@ -17,7 +17,6 @@ package com.google.auth.oauth2; import com.google.api.client.util.GenericData; -import com.google.cloud.spring.core.ReactiveTokenProvider; import java.io.IOException; import java.util.Date; import org.springframework.http.MediaType; @@ -27,69 +26,74 @@ public class UserCredentialsTokenProvider implements ReactiveTokenProvider { - private final WebClient webClient; - private final UserCredentials userCredentials; + private final WebClient webClient; + private final UserCredentials userCredentials; - private final String tokenUrl; + private final String tokenUrl; + public UserCredentialsTokenProvider(WebClient webClient, UserCredentials userCredentials) { + this.webClient = webClient; + this.userCredentials = userCredentials; + this.tokenUrl = OAuth2Utils.TOKEN_SERVER_URI.toString(); + } - public UserCredentialsTokenProvider(WebClient webClient, UserCredentials userCredentials) { - this.webClient = webClient; - this.userCredentials = userCredentials; - this.tokenUrl = OAuth2Utils.TOKEN_SERVER_URI.toString(); - } - - public UserCredentialsTokenProvider(WebClient webClient, UserCredentials userCredentials, - String tokenUrl) { - this.webClient = webClient; - this.userCredentials = userCredentials; - this.tokenUrl = tokenUrl; - } + public UserCredentialsTokenProvider( + WebClient webClient, UserCredentials userCredentials, String tokenUrl) { + this.webClient = webClient; + this.userCredentials = userCredentials; + this.tokenUrl = tokenUrl; + } - @Override - public Mono retrieve() { - if (userCredentials.getRefreshToken() == null) { - throw new IllegalStateException( - "UserCredentials instance cannot refresh because there is no refresh token."); - } else { - GenericData tokenRequest = new GenericData(); - tokenRequest.set("client_id", this.userCredentials.getClientId()); - tokenRequest.set("client_secret", this.userCredentials.getClientSecret()); - tokenRequest.set("refresh_token", this.userCredentials.getRefreshToken()); - tokenRequest.set("grant_type", "refresh_token"); - - return webClient.post().uri(tokenUrl) - .contentType(MediaType.APPLICATION_FORM_URLENCODED) - .body(BodyInserters.fromFormData("client_id", this.userCredentials.getClientId()) - .with("client_secret", this.userCredentials.getClientSecret()) - .with("refresh_token", this.userCredentials.getRefreshToken()) - .with("grant_type", "refresh_token") - ).retrieve().bodyToMono(GenericData.class) - .flatMap(gd -> { - try { - AccessToken accessToken = toAccessToken(gd); - return Mono.just(accessToken); - } catch (IOException e) { - return Mono.error(e); - } - }); - } - - } + @Override + public Mono retrieve() { + if (userCredentials.getRefreshToken() == null) { + throw new IllegalStateException( + "UserCredentials instance cannot refresh because there is no refresh token."); + } else { + GenericData tokenRequest = new GenericData(); + tokenRequest.set("client_id", this.userCredentials.getClientId()); + tokenRequest.set("client_secret", this.userCredentials.getClientSecret()); + tokenRequest.set("refresh_token", this.userCredentials.getRefreshToken()); + tokenRequest.set("grant_type", "refresh_token"); - private AccessToken toAccessToken(GenericData gd) throws IOException { - String accessTokenValue = OAuth2Utils.validateString(gd, "access_token", - "Error parsing token refresh response. "); - int expiresInSeconds = OAuth2Utils.validateInt32(gd, "expires_in", - "Error parsing token refresh response. "); - long expiresAtMilliseconds = - userCredentials.clock.currentTimeMillis() + (long) (expiresInSeconds * 1000); - String scopes = OAuth2Utils.validateOptionalString(gd, "scope", - "Error parsing token refresh response. "); - AccessToken accessToken = AccessToken.newBuilder() - .setExpirationTime(new Date(expiresAtMilliseconds)).setTokenValue(accessTokenValue) - .setScopes(scopes).build(); - return accessToken; + return webClient + .post() + .uri(tokenUrl) + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .body( + BodyInserters.fromFormData("client_id", this.userCredentials.getClientId()) + .with("client_secret", this.userCredentials.getClientSecret()) + .with("refresh_token", this.userCredentials.getRefreshToken()) + .with("grant_type", "refresh_token")) + .retrieve() + .bodyToMono(GenericData.class) + .flatMap( + gd -> { + try { + AccessToken accessToken = toAccessToken(gd); + return Mono.just(accessToken); + } catch (IOException e) { + return Mono.error(e); + } + }); } + } -} \ No newline at end of file + private AccessToken toAccessToken(GenericData gd) throws IOException { + String accessTokenValue = + OAuth2Utils.validateString(gd, "access_token", "Error parsing token refresh response. "); + int expiresInSeconds = + OAuth2Utils.validateInt32(gd, "expires_in", "Error parsing token refresh response. "); + long expiresAtMilliseconds = + userCredentials.clock.currentTimeMillis() + (long) (expiresInSeconds * 1000); + String scopes = + OAuth2Utils.validateOptionalString(gd, "scope", "Error parsing token refresh response. "); + AccessToken accessToken = + AccessToken.newBuilder() + .setExpirationTime(new Date(expiresAtMilliseconds)) + .setTokenValue(accessTokenValue) + .setScopes(scopes) + .build(); + return accessToken; + } +} diff --git a/reactor/javatests/com/google/auth/oauth2/CacheableTokenProviderTest.java b/reactor/javatests/com/google/auth/oauth2/CacheableTokenProviderTest.java index 18289dc7f..9689ab6c1 100644 --- a/reactor/javatests/com/google/auth/oauth2/CacheableTokenProviderTest.java +++ b/reactor/javatests/com/google/auth/oauth2/CacheableTokenProviderTest.java @@ -16,55 +16,57 @@ package com.google.auth.oauth2; - import java.util.Date; import java.util.UUID; - import org.junit.Test; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; public class CacheableTokenProviderTest { - @Test - void testRetrieveCache() { - ReactiveTokenProvider reactiveTokenProvider = () -> Mono.fromSupplier(() -> { - String token = UUID.randomUUID().toString(); - Date date = new Date(); - return new AccessToken(token, date); - }); - - ReactiveTokenProvider cacheableTokenProvider = new CacheableTokenProvider( - reactiveTokenProvider); - - StepVerifier.create( - Mono.zip(cacheableTokenProvider.retrieve(), cacheableTokenProvider.retrieve()) - .map(tuple -> tuple.getT1().equals(tuple.getT2()))) - .expectNext(true) - .verifyComplete(); - } - - @Test - void testRetrieveExpired() throws InterruptedException { - ReactiveTokenProvider reactiveTokenProvider = () -> Mono.fromSupplier(() -> { - String token = UUID.randomUUID().toString(); - return new AccessToken(token, new Date(System.currentTimeMillis() + 4 * 1000)); - }); - - ReactiveTokenProvider cacheableTokenProvider = new CacheableTokenProvider( - reactiveTokenProvider); - - Mono firstMono = cacheableTokenProvider.retrieve(); - - AccessToken accessToken = firstMono.block(); - - Thread.sleep(1000L); - Mono secondMono = cacheableTokenProvider.retrieve(); - - StepVerifier.create(secondMono.map(at -> at.equals(accessToken))) - .expectNext(false) - .verifyComplete(); - } - - -} \ No newline at end of file + @Test + void testRetrieveCache() { + ReactiveTokenProvider reactiveTokenProvider = + () -> + Mono.fromSupplier( + () -> { + String token = UUID.randomUUID().toString(); + Date date = new Date(); + return new AccessToken(token, date); + }); + + ReactiveTokenProvider cacheableTokenProvider = + new CacheableTokenProvider(reactiveTokenProvider); + + StepVerifier.create( + Mono.zip(cacheableTokenProvider.retrieve(), cacheableTokenProvider.retrieve()) + .map(tuple -> tuple.getT1().equals(tuple.getT2()))) + .expectNext(true) + .verifyComplete(); + } + + @Test + void testRetrieveExpired() throws InterruptedException { + ReactiveTokenProvider reactiveTokenProvider = + () -> + Mono.fromSupplier( + () -> { + String token = UUID.randomUUID().toString(); + return new AccessToken(token, new Date(System.currentTimeMillis() + 4 * 1000)); + }); + + ReactiveTokenProvider cacheableTokenProvider = + new CacheableTokenProvider(reactiveTokenProvider); + + Mono firstMono = cacheableTokenProvider.retrieve(); + + AccessToken accessToken = firstMono.block(); + + Thread.sleep(1000L); + Mono secondMono = cacheableTokenProvider.retrieve(); + + StepVerifier.create(secondMono.map(at -> at.equals(accessToken))) + .expectNext(false) + .verifyComplete(); + } +} diff --git a/reactor/javatests/com/google/auth/oauth2/ComputeEngineTokenProviderTest.java b/reactor/javatests/com/google/auth/oauth2/ComputeEngineTokenProviderTest.java index 0528b737f..4d3cd0ca8 100644 --- a/reactor/javatests/com/google/auth/oauth2/ComputeEngineTokenProviderTest.java +++ b/reactor/javatests/com/google/auth/oauth2/ComputeEngineTokenProviderTest.java @@ -16,6 +16,7 @@ package com.google.auth.oauth2; +import static com.google.auth.oauth2.TokenProviderBase.*; import java.io.IOException; import java.net.URISyntaxException; @@ -26,62 +27,61 @@ import org.springframework.web.reactive.function.client.WebClient; import reactor.test.StepVerifier; -import static com.google.auth.oauth2.TokenProviderBase.*; - public class ComputeEngineTokenProviderTest { - private static final Long SECONDS = 3600L; - private static final String ACCESS_TOKEN = "ya29.a0AfH6SMAa-dKy_..."; - - private static String COMPUTE_ENGINE_TOKEN = - "{\"access_token\":\"" + ACCESS_TOKEN + "\",\"expires_in\":" + SECONDS - + ",\"token_type\":\"Bearer\"}"; - private static String COMPUTE_ENGINE_TOKEN_BAD_RESPONSE = - "{\"token\":\"" + ACCESS_TOKEN + "\",\"in\":" + SECONDS + ",\"token_type\":\"Bearer\"}"; - - private MockWebServer mockWebServer; - - private WebClient webClient; - - private String tokenUri; - - @Before - public void setUp() throws IOException, URISyntaxException { - mockWebServer = new MockWebServer(); - mockWebServer.start(); - - webClient = WebClient.builder().build(); - tokenUri = mockWebServer.url("/").toString(); - } - - @Test - public void testRetrieve() { - mockWebServer.enqueue(successfulResponse(COMPUTE_ENGINE_TOKEN)); - ComputeEngineCredentials computeEngineCredentials = ComputeEngineCredentials.create(); - ReactiveTokenProvider tokenProvider = new ComputeEngineTokenProvider(webClient, - computeEngineCredentials, tokenUri); - Long expirationWindowStart = addExpiration(System.currentTimeMillis()); - StepVerifier.create(tokenProvider.retrieve()) - .expectNextMatches(at -> expectedToken(expirationWindowStart, at)) - .expectNext() - .verifyComplete(); - } - - - @Test - public void testRetrieveErrorParsingResponse() { - mockWebServer.enqueue(successfulResponse(COMPUTE_ENGINE_TOKEN_BAD_RESPONSE)); - ComputeEngineCredentials computeEngineCredentials = ComputeEngineCredentials.create(); - ReactiveTokenProvider tokenProvider = new ComputeEngineTokenProvider(webClient, - computeEngineCredentials, tokenUri); - StepVerifier.create(tokenProvider.retrieve()) - .expectNext() - .verifyErrorMatches(TokenProviderBase::tokenParseError); - } - - @After - public void tearDown() throws IOException { - mockWebServer.shutdown(); - } - -} \ No newline at end of file + private static final Long SECONDS = 3600L; + private static final String ACCESS_TOKEN = "ya29.a0AfH6SMAa-dKy_..."; + + private static String COMPUTE_ENGINE_TOKEN = + "{\"access_token\":\"" + + ACCESS_TOKEN + + "\",\"expires_in\":" + + SECONDS + + ",\"token_type\":\"Bearer\"}"; + private static String COMPUTE_ENGINE_TOKEN_BAD_RESPONSE = + "{\"token\":\"" + ACCESS_TOKEN + "\",\"in\":" + SECONDS + ",\"token_type\":\"Bearer\"}"; + + private MockWebServer mockWebServer; + + private WebClient webClient; + + private String tokenUri; + + @Before + public void setUp() throws IOException, URISyntaxException { + mockWebServer = new MockWebServer(); + mockWebServer.start(); + + webClient = WebClient.builder().build(); + tokenUri = mockWebServer.url("/").toString(); + } + + @Test + public void testRetrieve() { + mockWebServer.enqueue(successfulResponse(COMPUTE_ENGINE_TOKEN)); + ComputeEngineCredentials computeEngineCredentials = ComputeEngineCredentials.create(); + ReactiveTokenProvider tokenProvider = + new ComputeEngineTokenProvider(webClient, computeEngineCredentials, tokenUri); + Long expirationWindowStart = addExpiration(System.currentTimeMillis()); + StepVerifier.create(tokenProvider.retrieve()) + .expectNextMatches(at -> expectedToken(expirationWindowStart, at)) + .expectNext() + .verifyComplete(); + } + + @Test + public void testRetrieveErrorParsingResponse() { + mockWebServer.enqueue(successfulResponse(COMPUTE_ENGINE_TOKEN_BAD_RESPONSE)); + ComputeEngineCredentials computeEngineCredentials = ComputeEngineCredentials.create(); + ReactiveTokenProvider tokenProvider = + new ComputeEngineTokenProvider(webClient, computeEngineCredentials, tokenUri); + StepVerifier.create(tokenProvider.retrieve()) + .expectNext() + .verifyErrorMatches(TokenProviderBase::tokenParseError); + } + + @After + public void tearDown() throws IOException { + mockWebServer.shutdown(); + } +} diff --git a/reactor/javatests/com/google/auth/oauth2/ReactiveTokenProviderTest.java b/reactor/javatests/com/google/auth/oauth2/ReactiveTokenProviderTest.java index 223bff07f..43f61ed9a 100644 --- a/reactor/javatests/com/google/auth/oauth2/ReactiveTokenProviderTest.java +++ b/reactor/javatests/com/google/auth/oauth2/ReactiveTokenProviderTest.java @@ -1,4 +1,3 @@ - /* * Copyright 2017-2023 the original author or authors. * @@ -17,49 +16,45 @@ package com.google.auth.oauth2; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; import java.io.IOException; - import org.junit.Test; import org.springframework.core.io.ClassPathResource; -import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.mock; - - public class ReactiveTokenProviderTest { - @Test - public void testCreateCacheable() throws IOException { - ClassPathResource classPathResource = new ClassPathResource("fake-credential-key.json"); - ServiceAccountCredentials serviceAccountCredentials = ServiceAccountCredentials.fromStream( - classPathResource.getInputStream()); - ReactiveTokenProvider reactiveTokenProvider = ReactiveTokenProvider.createCacheable( - serviceAccountCredentials); - assertTrue(reactiveTokenProvider instanceof CacheableTokenProvider); - } - - @Test - public void testCreateUserCredentialsTokenProvider() { - UserCredentials userCredentials = mock(UserCredentials.class); - ReactiveTokenProvider reactiveTokenProvider = ReactiveTokenProvider.create(userCredentials); - assertTrue(reactiveTokenProvider instanceof UserCredentialsTokenProvider); - } - - @Test - public void testCreateServiceAccountTokenProvider() { - ServiceAccountCredentials serviceAccountCredentials = mock(ServiceAccountCredentials.class); - ReactiveTokenProvider reactiveTokenProvider = ReactiveTokenProvider.create( - serviceAccountCredentials); - assertTrue(reactiveTokenProvider instanceof ServiceAccountTokenProvider); - } - - @Test - public void testCreateComputeEngineTokenProvider() { - ComputeEngineCredentials computeEngineCredentials = mock(ComputeEngineCredentials.class); - ReactiveTokenProvider reactiveTokenProvider = ReactiveTokenProvider.create( - computeEngineCredentials); - assertTrue(reactiveTokenProvider instanceof ComputeEngineTokenProvider); - } - -} \ No newline at end of file + @Test + public void testCreateCacheable() throws IOException { + ClassPathResource classPathResource = new ClassPathResource("fake-credential-key.json"); + ServiceAccountCredentials serviceAccountCredentials = + ServiceAccountCredentials.fromStream(classPathResource.getInputStream()); + ReactiveTokenProvider reactiveTokenProvider = + ReactiveTokenProvider.createCacheable(serviceAccountCredentials); + assertTrue(reactiveTokenProvider instanceof CacheableTokenProvider); + } + + @Test + public void testCreateUserCredentialsTokenProvider() { + UserCredentials userCredentials = mock(UserCredentials.class); + ReactiveTokenProvider reactiveTokenProvider = ReactiveTokenProvider.create(userCredentials); + assertTrue(reactiveTokenProvider instanceof UserCredentialsTokenProvider); + } + + @Test + public void testCreateServiceAccountTokenProvider() { + ServiceAccountCredentials serviceAccountCredentials = mock(ServiceAccountCredentials.class); + ReactiveTokenProvider reactiveTokenProvider = + ReactiveTokenProvider.create(serviceAccountCredentials); + assertTrue(reactiveTokenProvider instanceof ServiceAccountTokenProvider); + } + + @Test + public void testCreateComputeEngineTokenProvider() { + ComputeEngineCredentials computeEngineCredentials = mock(ComputeEngineCredentials.class); + ReactiveTokenProvider reactiveTokenProvider = + ReactiveTokenProvider.create(computeEngineCredentials); + assertTrue(reactiveTokenProvider instanceof ComputeEngineTokenProvider); + } +} diff --git a/reactor/javatests/com/google/auth/oauth2/RefreshThresholdTest.java b/reactor/javatests/com/google/auth/oauth2/RefreshThresholdTest.java index de2cf057a..c8c4246d4 100644 --- a/reactor/javatests/com/google/auth/oauth2/RefreshThresholdTest.java +++ b/reactor/javatests/com/google/auth/oauth2/RefreshThresholdTest.java @@ -19,27 +19,24 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; -import com.google.auth.oauth2.AccessToken; -import org.junit.Test; - import java.util.Date; +import org.junit.Test; public class RefreshThresholdTest { - @Test - public void testCloseToExpirationTrue() { - long currentDate = System.currentTimeMillis(); - RefreshThreshold refreshThreshold = new RefreshThreshold(10_000); - AccessToken accessToken = new AccessToken("token", new Date(currentDate + 9_500)); - assertTrue(refreshThreshold.over(accessToken)); - } - - @Test - public void testCloseToExpirationFalse() { - long currentDate = System.currentTimeMillis(); - RefreshThreshold refreshThreshold = new RefreshThreshold(10_000); - AccessToken accessToken = new AccessToken("token", new Date(currentDate + 20_000)); - assertFalse(refreshThreshold.over(accessToken)); - } - + @Test + public void testCloseToExpirationTrue() { + long currentDate = System.currentTimeMillis(); + RefreshThreshold refreshThreshold = new RefreshThreshold(10_000); + AccessToken accessToken = new AccessToken("token", new Date(currentDate + 9_500)); + assertTrue(refreshThreshold.over(accessToken)); + } + + @Test + public void testCloseToExpirationFalse() { + long currentDate = System.currentTimeMillis(); + RefreshThreshold refreshThreshold = new RefreshThreshold(10_000); + AccessToken accessToken = new AccessToken("token", new Date(currentDate + 20_000)); + assertFalse(refreshThreshold.over(accessToken)); + } } diff --git a/reactor/javatests/com/google/auth/oauth2/ServiceAccountTokenProviderTest.java b/reactor/javatests/com/google/auth/oauth2/ServiceAccountTokenProviderTest.java index 64155df1b..21cc1fb9a 100644 --- a/reactor/javatests/com/google/auth/oauth2/ServiceAccountTokenProviderTest.java +++ b/reactor/javatests/com/google/auth/oauth2/ServiceAccountTokenProviderTest.java @@ -34,66 +34,75 @@ public class ServiceAccountTokenProviderTest { - private static final ClassPathResource SA_RESOURCE = new ClassPathResource( - "fake-credential-key.json"); - - private static final String SERVICE_ACCOUNT_TOKEN = "{\n" - + " \"access_token\": \"" + ACCESS_TOKEN + "\",\n" - + " \"expires_in\": " + SECONDS + ",\n" - + " \"scope\": \"https://www.googleapis.com/auth/drive.readonly\",\n" - + " \"token_type\": \"Bearer\"\n" - + "}"; - - private static final String SERVICE_ACCOUNT_TOKEN_BAD_RESPONSE = "{\n" - + " \"token\": \"" + ACCESS_TOKEN + "\",\n" - + " \"in\": " + SECONDS + ",\n" - + " \"scope\": \"https://www.googleapis.com/auth/drive.readonly\",\n" - + " \"token_type\": \"Bearer\"\n" - + "}"; - - private MockWebServer mockWebServer; - - private WebClient webClient; - - private String tokenUrl; - - @Before - void setUp() throws IOException, URISyntaxException { - mockWebServer = new MockWebServer(); - mockWebServer.start(); - - webClient = WebClient.builder().build(); - tokenUrl = mockWebServer.url("/").toString(); - } - - @Test - void testRetrieve() throws IOException { - mockWebServer.enqueue(successfulResponse(SERVICE_ACCOUNT_TOKEN)); - ServiceAccountCredentials serviceAccountCredentials = ServiceAccountCredentials.fromStream( - SA_RESOURCE.getInputStream()); - ReactiveTokenProvider tokenProvider = new ServiceAccountTokenProvider(webClient, - serviceAccountCredentials, tokenUrl); - Long expirationWindowStart = addExpiration(System.currentTimeMillis()); - StepVerifier.create(tokenProvider.retrieve()) - .expectNextMatches(at -> expectedToken(expirationWindowStart, at)) - .verifyComplete(); - } - - @Test - void testRetrieveErrorParsingResponse() throws IOException { - mockWebServer.enqueue(successfulResponse(SERVICE_ACCOUNT_TOKEN_BAD_RESPONSE)); - ServiceAccountCredentials serviceAccountCredentials = ServiceAccountCredentials.fromStream( - SA_RESOURCE.getInputStream()); - ReactiveTokenProvider tokenProvider = new ServiceAccountTokenProvider(webClient, - serviceAccountCredentials, tokenUrl); - StepVerifier.create(tokenProvider.retrieve()) - .expectNext() - .verifyErrorMatches(TokenProviderBase::tokenParseError); - } - - @After - void tearDown() throws IOException { - mockWebServer.shutdown(); - } - -} \ No newline at end of file + private static final ClassPathResource SA_RESOURCE = + new ClassPathResource("fake-credential-key.json"); + + private static final String SERVICE_ACCOUNT_TOKEN = + "{\n" + + " \"access_token\": \"" + + ACCESS_TOKEN + + "\",\n" + + " \"expires_in\": " + + SECONDS + + ",\n" + + " \"scope\": \"https://www.googleapis.com/auth/drive.readonly\",\n" + + " \"token_type\": \"Bearer\"\n" + + "}"; + + private static final String SERVICE_ACCOUNT_TOKEN_BAD_RESPONSE = + "{\n" + + " \"token\": \"" + + ACCESS_TOKEN + + "\",\n" + + " \"in\": " + + SECONDS + + ",\n" + + " \"scope\": \"https://www.googleapis.com/auth/drive.readonly\",\n" + + " \"token_type\": \"Bearer\"\n" + + "}"; + + private MockWebServer mockWebServer; + + private WebClient webClient; + + private String tokenUrl; + + @Before + void setUp() throws IOException, URISyntaxException { + mockWebServer = new MockWebServer(); + mockWebServer.start(); + + webClient = WebClient.builder().build(); + tokenUrl = mockWebServer.url("/").toString(); + } + + @Test + void testRetrieve() throws IOException { + mockWebServer.enqueue(successfulResponse(SERVICE_ACCOUNT_TOKEN)); + ServiceAccountCredentials serviceAccountCredentials = + ServiceAccountCredentials.fromStream(SA_RESOURCE.getInputStream()); + ReactiveTokenProvider tokenProvider = + new ServiceAccountTokenProvider(webClient, serviceAccountCredentials, tokenUrl); + Long expirationWindowStart = addExpiration(System.currentTimeMillis()); + StepVerifier.create(tokenProvider.retrieve()) + .expectNextMatches(at -> expectedToken(expirationWindowStart, at)) + .verifyComplete(); + } + + @Test + void testRetrieveErrorParsingResponse() throws IOException { + mockWebServer.enqueue(successfulResponse(SERVICE_ACCOUNT_TOKEN_BAD_RESPONSE)); + ServiceAccountCredentials serviceAccountCredentials = + ServiceAccountCredentials.fromStream(SA_RESOURCE.getInputStream()); + ReactiveTokenProvider tokenProvider = + new ServiceAccountTokenProvider(webClient, serviceAccountCredentials, tokenUrl); + StepVerifier.create(tokenProvider.retrieve()) + .expectNext() + .verifyErrorMatches(TokenProviderBase::tokenParseError); + } + + @After + void tearDown() throws IOException { + mockWebServer.shutdown(); + } +} diff --git a/reactor/javatests/com/google/auth/oauth2/TokenProviderBase.java b/reactor/javatests/com/google/auth/oauth2/TokenProviderBase.java index fdac12154..729ac9f24 100644 --- a/reactor/javatests/com/google/auth/oauth2/TokenProviderBase.java +++ b/reactor/javatests/com/google/auth/oauth2/TokenProviderBase.java @@ -23,40 +23,37 @@ public class TokenProviderBase { - static final Long SECONDS = 3600L; - static final String ACCESS_TOKEN = "ya29.a0AfH6SMAa-dKy_..."; - - private TokenProviderBase() { - } - - static MockResponse successfulResponse(String response) { - return new MockResponse() - .setHeader("Content-Type", "application/json") - .setResponseCode(200) - .setBody(response); - } - - static long addExpiration(Long millis) { - return millis + (SECONDS * 1000L); - } - - static boolean expectedToken(Long expirationWindowStart, AccessToken at) { - boolean isExpirationWithinWindow = isExpirationWithinWindow(expirationWindowStart, at); - return isExpirationWithinWindow - && at.getTokenValue().equals(ACCESS_TOKEN); - } - - static boolean tokenParseError(Throwable e) { - return e instanceof IOException - && e.getMessage().contains(ERROR_PARSING_TOKEN_REFRESH_RESPONSE); - } - - static boolean isExpirationWithinWindow(Long expirationWindowStart, AccessToken at) { - Long expirationWindowEnd = addExpiration(System.currentTimeMillis()); - Long expiration = at.getExpirationTimeMillis(); - boolean expirationWithinLimits = - expiration <= expirationWindowEnd && expiration >= expirationWindowStart; - return expirationWithinLimits; - } - -} \ No newline at end of file + static final Long SECONDS = 3600L; + static final String ACCESS_TOKEN = "ya29.a0AfH6SMAa-dKy_..."; + + private TokenProviderBase() {} + + static MockResponse successfulResponse(String response) { + return new MockResponse() + .setHeader("Content-Type", "application/json") + .setResponseCode(200) + .setBody(response); + } + + static long addExpiration(Long millis) { + return millis + (SECONDS * 1000L); + } + + static boolean expectedToken(Long expirationWindowStart, AccessToken at) { + boolean isExpirationWithinWindow = isExpirationWithinWindow(expirationWindowStart, at); + return isExpirationWithinWindow && at.getTokenValue().equals(ACCESS_TOKEN); + } + + static boolean tokenParseError(Throwable e) { + return e instanceof IOException + && e.getMessage().contains(ERROR_PARSING_TOKEN_REFRESH_RESPONSE); + } + + static boolean isExpirationWithinWindow(Long expirationWindowStart, AccessToken at) { + Long expirationWindowEnd = addExpiration(System.currentTimeMillis()); + Long expiration = at.getExpirationTimeMillis(); + boolean expirationWithinLimits = + expiration <= expirationWindowEnd && expiration >= expirationWindowStart; + return expirationWithinLimits; + } +} diff --git a/reactor/javatests/com/google/auth/oauth2/UserCredentialsTokenProviderTest.java b/reactor/javatests/com/google/auth/oauth2/UserCredentialsTokenProviderTest.java index 2b539fb8c..b24788e44 100644 --- a/reactor/javatests/com/google/auth/oauth2/UserCredentialsTokenProviderTest.java +++ b/reactor/javatests/com/google/auth/oauth2/UserCredentialsTokenProviderTest.java @@ -30,61 +30,62 @@ public class UserCredentialsTokenProviderTest { - private static String USER_CREDENTIALS_TOKEN = - "{\"access_token\":\"" + ACCESS_TOKEN - + "\",\"token_type\":\"Bearer\",\"expires_in\":3599,\"refresh_token\":\"test_refresh_token\",\"scope\":\"https://www.googleapis.com/auth/cloud-platform\"}"; + private static String USER_CREDENTIALS_TOKEN = + "{\"access_token\":\"" + + ACCESS_TOKEN + + "\",\"token_type\":\"Bearer\",\"expires_in\":3599,\"refresh_token\":\"test_refresh_token\",\"scope\":\"https://www.googleapis.com/auth/cloud-platform\"}"; - private static String USER_CREDENTIALS_TOKEN_BAD_RESPONSE = - "{\"token\":\"" + ACCESS_TOKEN - + "\",\"token_type\":\"Bearer\",\"in\":3599,\"refresh_token\":\"test_refresh_token\",\"scope\":\"https://www.googleapis.com/auth/cloud-platform\"}"; + private static String USER_CREDENTIALS_TOKEN_BAD_RESPONSE = + "{\"token\":\"" + + ACCESS_TOKEN + + "\",\"token_type\":\"Bearer\",\"in\":3599,\"refresh_token\":\"test_refresh_token\",\"scope\":\"https://www.googleapis.com/auth/cloud-platform\"}"; - private MockWebServer mockWebServer; + private MockWebServer mockWebServer; - private WebClient webClient; + private WebClient webClient; - private String tokenUrl; + private String tokenUrl; - @Before - public void setUp() throws IOException, URISyntaxException { - mockWebServer = new MockWebServer(); - mockWebServer.start(); - webClient = WebClient.builder().build(); - tokenUrl = mockWebServer.url("/").toString(); - } + @Before + public void setUp() throws IOException, URISyntaxException { + mockWebServer = new MockWebServer(); + mockWebServer.start(); + webClient = WebClient.builder().build(); + tokenUrl = mockWebServer.url("/").toString(); + } - @Test - public void testRetrieve() throws Exception { - mockWebServer.enqueue(successfulResponse(USER_CREDENTIALS_TOKEN)); - UserCredentials userCredentials = createUserCredentials(); - ReactiveTokenProvider tokenProvider = new UserCredentialsTokenProvider(webClient, - userCredentials, tokenUrl); - StepVerifier.create(tokenProvider.retrieve().map(AccessToken::getTokenValue)) - .expectNext(ACCESS_TOKEN) - .verifyComplete(); - } + @Test + public void testRetrieve() throws Exception { + mockWebServer.enqueue(successfulResponse(USER_CREDENTIALS_TOKEN)); + UserCredentials userCredentials = createUserCredentials(); + ReactiveTokenProvider tokenProvider = + new UserCredentialsTokenProvider(webClient, userCredentials, tokenUrl); + StepVerifier.create(tokenProvider.retrieve().map(AccessToken::getTokenValue)) + .expectNext(ACCESS_TOKEN) + .verifyComplete(); + } - @Test - public void testRetrieveErrorParsingResponse() { - mockWebServer.enqueue(successfulResponse(USER_CREDENTIALS_TOKEN_BAD_RESPONSE)); - UserCredentials userCredentials = createUserCredentials(); - ReactiveTokenProvider tokenProvider = new UserCredentialsTokenProvider(webClient, - userCredentials, tokenUrl); - StepVerifier.create(tokenProvider.retrieve()) - .expectNext() - .verifyErrorMatches(TokenProviderBase::tokenParseError); - } + @Test + public void testRetrieveErrorParsingResponse() { + mockWebServer.enqueue(successfulResponse(USER_CREDENTIALS_TOKEN_BAD_RESPONSE)); + UserCredentials userCredentials = createUserCredentials(); + ReactiveTokenProvider tokenProvider = + new UserCredentialsTokenProvider(webClient, userCredentials, tokenUrl); + StepVerifier.create(tokenProvider.retrieve()) + .expectNext() + .verifyErrorMatches(TokenProviderBase::tokenParseError); + } - @After - public void tearDown() throws IOException { - mockWebServer.shutdown(); - } + @After + public void tearDown() throws IOException { + mockWebServer.shutdown(); + } - private static UserCredentials createUserCredentials() { - return UserCredentials.newBuilder() - .setClientId("test_client_id") - .setClientSecret("test_client_secret") - .setRefreshToken("refresh_token") - .build(); - } - -} \ No newline at end of file + private static UserCredentials createUserCredentials() { + return UserCredentials.newBuilder() + .setClientId("test_client_id") + .setClientSecret("test_client_secret") + .setRefreshToken("refresh_token") + .build(); + } +}