From 91d8e8a3c1e830aa692b50f7159da9e007f3522d Mon Sep 17 00:00:00 2001 From: Rob Bygrave Date: Wed, 18 Dec 2024 09:39:13 +1300 Subject: [PATCH] Add AWS CognitoAuthTokenProvider module (#536) * Add AWS CognitoAuthTokenProvider module An HttpClient AuthTokenProvider for AWS Cognito * Packaging pom on aws-cognito * Release aws-cognito/http-client-authtoken independently --- aws-cognito/http-client-authtoken/pom.xml | 45 +++++++++ .../cognito/AmzCognitoAuthTokenProvider.java | 93 +++++++++++++++++++ .../cognito/CognitoAuthTokenProvider.java | 65 +++++++++++++ .../src/main/java/module-info.java | 7 ++ .../cognito/CognitoAuthTokenProviderTest.java | 49 ++++++++++ aws-cognito/pom.xml | 15 +++ pom.xml | 2 + 7 files changed, 276 insertions(+) create mode 100644 aws-cognito/http-client-authtoken/pom.xml create mode 100644 aws-cognito/http-client-authtoken/src/main/java/io/avaje/aws/client/cognito/AmzCognitoAuthTokenProvider.java create mode 100644 aws-cognito/http-client-authtoken/src/main/java/io/avaje/aws/client/cognito/CognitoAuthTokenProvider.java create mode 100644 aws-cognito/http-client-authtoken/src/main/java/module-info.java create mode 100644 aws-cognito/http-client-authtoken/src/test/java/io/avaje/aws/client/cognito/CognitoAuthTokenProviderTest.java create mode 100644 aws-cognito/pom.xml diff --git a/aws-cognito/http-client-authtoken/pom.xml b/aws-cognito/http-client-authtoken/pom.xml new file mode 100644 index 00000000..897acf4e --- /dev/null +++ b/aws-cognito/http-client-authtoken/pom.xml @@ -0,0 +1,45 @@ + + + 4.0.0 + + org.avaje + java11-oss + 4.5 + + + io.avaje.aws + avaje-cognito-client-token + 1.0-RC1 + + + + io.avaje + avaje-http-client + 2.8 + + + + io.avaje + avaje-json-core + 3.0-RC5 + + + + + io.avaje + avaje-json-node + 3.0-RC5 + test + + + + io.avaje + junit + 1.5 + test + + + + diff --git a/aws-cognito/http-client-authtoken/src/main/java/io/avaje/aws/client/cognito/AmzCognitoAuthTokenProvider.java b/aws-cognito/http-client-authtoken/src/main/java/io/avaje/aws/client/cognito/AmzCognitoAuthTokenProvider.java new file mode 100644 index 00000000..359fbfca --- /dev/null +++ b/aws-cognito/http-client-authtoken/src/main/java/io/avaje/aws/client/cognito/AmzCognitoAuthTokenProvider.java @@ -0,0 +1,93 @@ +package io.avaje.aws.client.cognito; + +import io.avaje.http.client.AuthToken; +import io.avaje.http.client.AuthTokenProvider; +import io.avaje.http.client.BasicAuthIntercept; +import io.avaje.http.client.HttpClientRequest; +import io.avaje.json.simple.SimpleMapper; + +import java.net.http.HttpResponse; +import java.time.Instant; + +final class AmzCognitoAuthTokenProvider implements CognitoAuthTokenProvider.Builder { + + private String url; + private String clientId; + private String clientSecret; + private String scope; + + @Override + public CognitoAuthTokenProvider.Builder url(String url) { + this.url = url; + return this; + } + + @Override + public CognitoAuthTokenProvider.Builder clientId(String clientId) { + this.clientId = clientId; + return this; + } + + @Override + public CognitoAuthTokenProvider.Builder clientSecret(String clientSecret) { + this.clientSecret = clientSecret; + return this; + } + + @Override + public CognitoAuthTokenProvider.Builder scope(String scope) { + this.scope = scope; + return this; + } + + @Override + public AuthTokenProvider build() { + return new Provider(url, clientId, clientSecret, scope); + } + + private static final class Provider implements AuthTokenProvider { + + private static final SimpleMapper MAPPER = SimpleMapper.builder().build(); + + private final String url; + private final String clientId; + private final String scope; + private final String authHeader; + + public Provider(String url, String clientId, String clientSecret, String scope) { + this.url = url; + this.clientId = clientId; + this.scope = scope; + this.authHeader = "Basic " + BasicAuthIntercept.encode(clientId, clientSecret); + } + + @Override + public AuthToken obtainToken(HttpClientRequest request) { + HttpResponse res = request + .url(url) + .header("Authorization", authHeader) + .formParam("grant_type", "client_credentials") + .formParam("client_id", clientId) + .formParam("scope", scope) + .POST() + .asString(); + + if (res.statusCode() != 200) { + throw new IllegalStateException("Error response getting access token statusCode:" + res.statusCode() + " res:" + res); + } + return decodeAuthToken(res.body()); + } + + private AuthToken decodeAuthToken(String responseBody) { + final var responseMap = MAPPER.fromJsonObject(responseBody); + final var accessToken = (String) responseMap.get("access_token"); + final var expiresIn = (Long) responseMap.get("expires_in"); + + var validUntil = Instant.now() + .plusSeconds(expiresIn) + .minusSeconds(60); + + return AuthToken.of(accessToken, validUntil); + } + } +} diff --git a/aws-cognito/http-client-authtoken/src/main/java/io/avaje/aws/client/cognito/CognitoAuthTokenProvider.java b/aws-cognito/http-client-authtoken/src/main/java/io/avaje/aws/client/cognito/CognitoAuthTokenProvider.java new file mode 100644 index 00000000..e0f37c10 --- /dev/null +++ b/aws-cognito/http-client-authtoken/src/main/java/io/avaje/aws/client/cognito/CognitoAuthTokenProvider.java @@ -0,0 +1,65 @@ +package io.avaje.aws.client.cognito; + +import io.avaje.http.client.AuthTokenProvider; + +/** + * AuthTokenProvider for AWS Cognito providing Bearer access tokens. + * + *
{@code
+ *
+ *     AuthTokenProvider authTokenProvider = CognitoAuthTokenProvider.builder()
+ *       .url("https://foo.amazoncognito.com/oauth2/token")
+ *       .clientId("")
+ *       .clientSecret("")
+ *       .scope("default/default")
+ *       .build();
+ *
+ *     // specify the authTokenProvider on the HttpClient ...
+ *
+ *     HttpClient client = HttpClient.builder()
+ *       .authTokenProvider(authTokenProvider)
+ *       .baseUrl(myApplicationUrl)
+ *       .build();
+ *
+ * }
+ */ +public interface CognitoAuthTokenProvider extends AuthTokenProvider { + + /** + * Return a builder for the CognitoAuthTokenProvider. + */ + static Builder builder() { + return new AmzCognitoAuthTokenProvider(); + } + + /** + * The builder for the AWS Cognito AuthTokenProvider. + */ + interface Builder { + + /** + * Set the url used to obtain access tokens. + */ + Builder url(String url); + + /** + * Set the clientId. + */ + Builder clientId(String clientId); + + /** + * Set the clientSecret. + */ + Builder clientSecret(String clientSecret); + + /** + * Set the scope. + */ + Builder scope(String scope); + + /** + * Build and return the AuthTokenProvider. + */ + AuthTokenProvider build(); + } +} diff --git a/aws-cognito/http-client-authtoken/src/main/java/module-info.java b/aws-cognito/http-client-authtoken/src/main/java/module-info.java new file mode 100644 index 00000000..12a41680 --- /dev/null +++ b/aws-cognito/http-client-authtoken/src/main/java/module-info.java @@ -0,0 +1,7 @@ +module io.avaje.aws.client.cognito { + + exports io.avaje.aws.client.cognito; + + requires transitive io.avaje.http.client; + requires transitive io.avaje.json; +} diff --git a/aws-cognito/http-client-authtoken/src/test/java/io/avaje/aws/client/cognito/CognitoAuthTokenProviderTest.java b/aws-cognito/http-client-authtoken/src/test/java/io/avaje/aws/client/cognito/CognitoAuthTokenProviderTest.java new file mode 100644 index 00000000..6658d834 --- /dev/null +++ b/aws-cognito/http-client-authtoken/src/test/java/io/avaje/aws/client/cognito/CognitoAuthTokenProviderTest.java @@ -0,0 +1,49 @@ +package io.avaje.aws.client.cognito; + +import io.avaje.http.client.AuthTokenProvider; +import io.avaje.http.client.HttpClientRequest; +import io.avaje.http.client.HttpClientResponse; +import org.junit.jupiter.api.Test; + +import java.net.http.HttpResponse; + +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.*; + +@SuppressWarnings("unchecked") +class CognitoAuthTokenProviderTest { + + @Test + void obtainToken() { + final String url = "https://.amazoncognito.com/oauth2/token"; + final String clientId = ""; + final String clientSecret = ""; + + AuthTokenProvider authTokenProvider = CognitoAuthTokenProvider.builder() + .url(url) + .clientId(clientId) + .clientSecret(clientSecret) + .scope("default/default") + .build(); + + HttpResponse httpResponse = mock(HttpResponse.class); + when(httpResponse.statusCode()).thenReturn(200); + when(httpResponse.body()).thenReturn("{\"access_token\":\"1234\",\"expires_in\":3600}"); + + HttpClientResponse clientResponse = mock(HttpClientResponse.class); + when(clientResponse.asString()).thenReturn(httpResponse); + + HttpClientRequest httpClientRequest = mock(HttpClientRequest.class); + when(httpClientRequest.url(anyString())).thenReturn(httpClientRequest); + + when(httpClientRequest.header(anyString(), anyString())).thenReturn(httpClientRequest); + when(httpClientRequest.formParam(anyString(), anyString())).thenReturn(httpClientRequest); + when(httpClientRequest.POST()).thenReturn(clientResponse); + + // act + authTokenProvider.obtainToken(httpClientRequest); + + // verify the Authorization header has been obtained and set + verify(httpClientRequest).header("Authorization", "Basic PHNvbWV0aGluZz46PHNvbWV0aGluZz4="); + } +} diff --git a/aws-cognito/pom.xml b/aws-cognito/pom.xml new file mode 100644 index 00000000..466964dc --- /dev/null +++ b/aws-cognito/pom.xml @@ -0,0 +1,15 @@ + + + 4.0.0 + + org.avaje + java11-oss + 4.5 + + + aws-cognito + pom + + diff --git a/pom.xml b/pom.xml index 018e68b5..f0b2a7d1 100644 --- a/pom.xml +++ b/pom.xml @@ -75,6 +75,8 @@ test21 + + aws-cognito/http-client-authtoken htmx-nima htmx-nima-jstache http-generator-helidon