diff --git a/.gitignore b/.gitignore
index efea3d2..06de294 100644
--- a/.gitignore
+++ b/.gitignore
@@ -38,4 +38,4 @@ nb-configuration.xml
# Local environment
.env
-.env.json
+env.json
diff --git a/README.md b/README.md
index 3578c88..a55ad78 100644
--- a/README.md
+++ b/README.md
@@ -17,6 +17,9 @@ mvn clean package -Pnative
Running AWS SAM locally
```bash
-# Make sure you have your "sam/env.json" file in place
-sam local invoke --template sam/sam.native.yaml --event sam/event.json --env-vars sam/env.json
+sam local invoke --template sam/sam.native.yaml --env-vars sam/env.json --event sam/events/authorize.json
+```
+
+```bash
+sam local invoke --template sam/sam.native.yaml --env-vars sam/env.json --event sam/events/callback.json
```
diff --git a/pom.xml b/pom.xml
index 1310256..89ff68e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -27,13 +27,6 @@
pom
import
-
- io.quarkiverse.googlecloudservices
- quarkus-google-cloud-services-bom
- ${quarkus.google-cloud-services.version}
- pom
- import
-
@@ -81,22 +74,25 @@
org.jboss.logging
commons-logging-jboss-logging
-
-
- io.quarkiverse.googlecloudservices
- quarkus-google-cloud-secret-manager
+ io.quarkus
+ quarkus-config-yaml
+
+
com.google.apis
google-api-services-oauth2
${google-api-services-oauth2.version}
+
+ com.google.http-client
+ google-http-client
+
src/main/kotlin
- src/test/kotlin
${quarkus.platform.group-id}
@@ -208,6 +204,7 @@
+
native
@@ -221,8 +218,10 @@
native
true
quay.io/quarkus/ubi-quarkus-mandrel:22.2-java11
- 2g
+ 8g
+ -H:+RemoveSaturatedTypeFlows,\
+ -H:+PrintAnalysisStatistics,\
-H:ResourceConfigurationFiles=resources-config.json,\
-H:ReflectionConfigurationFiles=reflection-config.json,\
-H:EnableURLProtocols=http\,https,\
diff --git a/sam/env.json b/sam/env.json
deleted file mode 100644
index 42efa24..0000000
--- a/sam/env.json
+++ /dev/null
@@ -1,5 +0,0 @@
-{
- "function": {
- "GOOGLE_CLIENT_SECRET": "secret"
- }
-}
diff --git a/sam/env.json.example b/sam/env.json.example
index 42efa24..c4f11dd 100644
--- a/sam/env.json.example
+++ b/sam/env.json.example
@@ -1,5 +1,8 @@
{
- "function": {
- "GOOGLE_CLIENT_SECRET": "secret"
+ "GoogleAuthNative": {
+ "LOG_LEVEL": "DEBUG",
+ "GOOGLE_CLIENT_ID": "",
+ "GOOGLE_CLIENT_SECRET": "",
+ "GOOGLE_CLIENT_REDIRECT_URI": "http://localhost:8080/callback"
}
}
diff --git a/sam/event.json b/sam/events/authorize.json
similarity index 51%
rename from sam/event.json
rename to sam/events/authorize.json
index 89c26c0..6c97aff 100644
--- a/sam/event.json
+++ b/sam/events/authorize.json
@@ -1,4 +1,4 @@
{
- "path": "/",
+ "path": "/authorize",
"httpMethod": "GET"
}
diff --git a/sam/events/callback.json b/sam/events/callback.json
new file mode 100644
index 0000000..304d283
--- /dev/null
+++ b/sam/events/callback.json
@@ -0,0 +1,4 @@
+{
+ "path": "/callback?state=some-state&code=4%2F0ARtbsJq5sVmW60sVxVWpKemSD3uUpbYjkAEpDIEU7L9bXZ1xGB1UQR_UI5zaNEM-g_xJgw&scope=email+profile+openid+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.profile+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email&authuser=0&prompt=consent",
+ "httpMethod": "GET"
+}
diff --git a/sam/sam.native.yaml b/sam/sam.native.yaml
index 6975ccb..b5ba0d3 100644
--- a/sam/sam.native.yaml
+++ b/sam/sam.native.yaml
@@ -20,7 +20,10 @@ Resources:
Environment:
Variables:
DISABLE_SIGNAL_HANDLERS: true
- GOOGLE_CLIENT_SECRET: secret
+ LOG_LEVEL: null
+ GOOGLE_CLIENT_ID: null
+ GOOGLE_CLIENT_SECRET: null
+ GOOGLE_CLIENT_REDIRECT_URI: null
Events:
GetResource:
Type: Api
diff --git a/src/main/kotlin/com/acme/AuthUser.kt b/src/main/kotlin/com/acme/AuthUser.kt
new file mode 100644
index 0000000..246eef9
--- /dev/null
+++ b/src/main/kotlin/com/acme/AuthUser.kt
@@ -0,0 +1,10 @@
+package com.acme
+
+import com.google.api.services.oauth2.model.Userinfo
+import io.quarkus.runtime.annotations.RegisterForReflection
+
+@RegisterForReflection
+data class AuthUser(
+ val refreshToken: String,
+ val userInfo: Userinfo
+)
diff --git a/src/main/kotlin/com/acme/GoogleOAuthClient.kt b/src/main/kotlin/com/acme/GoogleOAuthClient.kt
new file mode 100644
index 0000000..b9cfe3d
--- /dev/null
+++ b/src/main/kotlin/com/acme/GoogleOAuthClient.kt
@@ -0,0 +1,109 @@
+package com.acme
+
+import com.google.api.client.auth.oauth2.BearerToken
+import com.google.api.client.auth.oauth2.Credential
+import com.google.api.client.googleapis.auth.oauth2.GoogleAuthorizationCodeFlow
+import com.google.api.client.googleapis.auth.oauth2.GoogleAuthorizationCodeRequestUrl
+import com.google.api.client.googleapis.auth.oauth2.GoogleAuthorizationCodeTokenRequest
+import com.google.api.client.googleapis.auth.oauth2.GoogleClientSecrets
+import com.google.api.client.http.apache.v2.ApacheHttpTransport
+import com.google.api.client.json.JsonFactory
+import com.google.api.client.json.gson.GsonFactory
+import com.google.api.services.oauth2.Oauth2
+import com.google.api.services.oauth2.Oauth2Scopes
+import java.net.URL
+import javax.enterprise.context.ApplicationScoped
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory
+
+@ApplicationScoped
+class GoogleOAuthClient(
+ private val properties: GoogleOAuthProperties
+) {
+
+ private val logger: Logger = LoggerFactory.getLogger(this.javaClass)
+
+ private val scopes = setOf(
+ // For basic user information
+ Oauth2Scopes.OPENID,
+ Oauth2Scopes.USERINFO_PROFILE,
+ Oauth2Scopes.USERINFO_EMAIL
+ )
+
+ private fun googleAuthorizationCodeFlow(): GoogleAuthorizationCodeFlow {
+ val jsonFactory: JsonFactory = GsonFactory.getDefaultInstance()
+ val details = GoogleClientSecrets.Details()
+
+ details.clientId = properties.clientId()
+ details.clientSecret = properties.clientSecret()
+
+ val clientSecrets = GoogleClientSecrets()
+ clientSecrets.installed = details
+
+ // Create builder
+ val builder = GoogleAuthorizationCodeFlow
+ .Builder(ApacheHttpTransport(), jsonFactory, clientSecrets, scopes)
+
+ return builder
+ .setAccessType("offline")
+ .setApprovalPrompt("force")
+ .build()
+ }
+
+ fun createAuthUrl(state: String): URL {
+ logger.debug("Creating a new authentication URL")
+ val flow = googleAuthorizationCodeFlow()
+
+ val codeRequestUrl: GoogleAuthorizationCodeRequestUrl = flow
+ .newAuthorizationUrl()
+ .setRedirectUri(properties.redirectUri())
+ .setState(state)
+
+ logger.info("Entries {}", codeRequestUrl.entries.size)
+ logger.info("Empty in native builds: {}", codeRequestUrl.buildRelativeUrl())
+
+ return codeRequestUrl.toURL()
+ }
+
+ fun authenticate(authorizationCode: String): AuthUser {
+ logger.debug("Fetching a new refresh token")
+ val flow = googleAuthorizationCodeFlow()
+
+ val codeTokenRequest: GoogleAuthorizationCodeTokenRequest = flow
+ .newTokenRequest(authorizationCode)
+ .setRedirectUri(properties.redirectUri())
+
+ logger.warn("Entries {}", codeTokenRequest.entries.size)
+ codeTokenRequest.entries.forEach { e ->
+ logger.info("${e.key} = ${e.value}")
+ }
+
+ logger.info("Let's execute the request")
+ val token = codeTokenRequest.execute()
+
+ // Let's save the refresh token in DynamoDB so that every instance will
+ // be able to reuse it
+ val httpTransport = ApacheHttpTransport()
+ val jsonFactory: JsonFactory = GsonFactory.getDefaultInstance()
+
+ val httpRequestInitializer = Credential.Builder(BearerToken.authorizationHeaderAccessMethod()).build()
+ httpRequestInitializer.accessToken = token.accessToken
+ logger.info("httpRequestInitializer.accessToken {}", httpRequestInitializer.accessToken)
+
+ val oauth2: Oauth2 = Oauth2
+ .Builder(httpTransport, jsonFactory, httpRequestInitializer)
+ .setApplicationName(properties.applicationName())
+ .build()
+
+ val get = oauth2
+ .userinfo()
+ .get()
+
+ val userInfo = get.execute()
+
+ return AuthUser(
+ refreshToken = token.refreshToken,
+ userInfo = userInfo
+ )
+ }
+}
diff --git a/src/main/kotlin/com/acme/GoogleOAuthProperties.kt b/src/main/kotlin/com/acme/GoogleOAuthProperties.kt
new file mode 100644
index 0000000..5093b7f
--- /dev/null
+++ b/src/main/kotlin/com/acme/GoogleOAuthProperties.kt
@@ -0,0 +1,17 @@
+package com.acme
+
+import io.quarkus.runtime.annotations.RegisterForReflection
+import io.smallrye.config.ConfigMapping
+
+@RegisterForReflection
+@ConfigMapping(prefix = GoogleOAuthProperties.PREFIX)
+interface GoogleOAuthProperties {
+ fun applicationName(): String
+ fun clientId(): String
+ fun clientSecret(): String
+ fun redirectUri(): String
+
+ companion object {
+ const val PREFIX = "pws-google"
+ }
+}
diff --git a/src/main/kotlin/com/acme/GreetingResource.kt b/src/main/kotlin/com/acme/GreetingResource.kt
deleted file mode 100644
index 2ed61d7..0000000
--- a/src/main/kotlin/com/acme/GreetingResource.kt
+++ /dev/null
@@ -1,63 +0,0 @@
-package com.acme
-
-import com.google.api.client.googleapis.auth.oauth2.GoogleAuthorizationCodeFlow
-import com.google.api.client.googleapis.auth.oauth2.GoogleClientSecrets
-import com.google.api.client.http.apache.v2.ApacheHttpTransport
-import com.google.api.client.json.JsonFactory
-import com.google.api.client.json.gson.GsonFactory
-import com.google.api.services.oauth2.Oauth2Scopes
-import javax.ws.rs.GET
-import javax.ws.rs.Path
-import javax.ws.rs.Produces
-import javax.ws.rs.core.MediaType
-import org.slf4j.Logger
-import org.slf4j.LoggerFactory
-
-
-@Path("/")
-class GreetingResource {
-
- private val logger: Logger = LoggerFactory.getLogger(this.javaClass)
-
- private val scopes = setOf(
- // For basic user information
- Oauth2Scopes.OPENID,
- Oauth2Scopes.USERINFO_PROFILE,
- Oauth2Scopes.USERINFO_EMAIL
- )
-
- @GET
- @Produces(MediaType.TEXT_PLAIN)
- fun get(): String {
- logger.info("Build auth url")
- val flow = googleAuthorizationCodeFlow()
-
- val codeRequestUrl = flow
- .newAuthorizationUrl()
- .setRedirectUri("http://localhost:3000")
- .setState("some-state")
-
- logger.debug("Why is this empty in native? {}", codeRequestUrl.entries.size)
-
- return codeRequestUrl.toURL().toString()
- }
-
- private fun googleAuthorizationCodeFlow(): GoogleAuthorizationCodeFlow {
- val jsonFactory: JsonFactory = GsonFactory.getDefaultInstance()
- val details = GoogleClientSecrets.Details()
-
- details.clientId = "some-id"
- details.clientSecret = "some-secret"
-
- val clientSecrets = GoogleClientSecrets()
- clientSecrets.installed = details
-
- val builder = GoogleAuthorizationCodeFlow
- .Builder(ApacheHttpTransport(), jsonFactory, clientSecrets, scopes)
-
- return builder
- .setAccessType("offline")
- .setApprovalPrompt("force")
- .build()
- }
-}
diff --git a/src/main/kotlin/com/acme/ReflectionConfig.kt b/src/main/kotlin/com/acme/ReflectionConfig.kt
new file mode 100644
index 0000000..b2f084a
--- /dev/null
+++ b/src/main/kotlin/com/acme/ReflectionConfig.kt
@@ -0,0 +1,54 @@
+package com.acme
+
+import io.quarkus.runtime.annotations.RegisterForReflection
+
+@Suppress("unused")
+@RegisterForReflection(
+ targets = [
+ // google-http-client
+ com.google.api.client.http.HttpHeaders::class,
+ com.google.api.client.json.rpc2.JsonRpcRequest::class,
+ com.google.api.client.json.webtoken.JsonWebSignature::class,
+ com.google.api.client.json.webtoken.JsonWebToken.Header::class,
+ // google-api-client
+ com.google.api.client.googleapis.auth.oauth2.GoogleAuthorizationCodeRequestUrl::class,
+ com.google.api.client.googleapis.auth.oauth2.GoogleBrowserClientRequestUrl::class,
+ com.google.api.client.googleapis.auth.oauth2.GoogleClientSecrets::class,
+ com.google.api.client.googleapis.auth.oauth2.GoogleClientSecrets.Details::class,
+ com.google.api.client.googleapis.auth.oauth2.GoogleIdToken.Payload::class,
+ com.google.api.client.googleapis.auth.oauth2.GoogleTokenResponse::class,
+ com.google.api.client.googleapis.json.GoogleJsonError.ErrorInfo::class,
+ com.google.api.client.googleapis.json.GoogleJsonError.Details::class,
+ com.google.api.client.googleapis.json.GoogleJsonError.ParameterViolations::class,
+ com.google.api.client.googleapis.json.GoogleJsonError::class,
+ com.google.api.client.googleapis.json.GoogleJsonErrorContainer::class,
+ com.google.api.client.googleapis.mtls.ContextAwareMetadataJson::class,
+ // google-api-services-oauth2-v2
+ com.google.api.services.oauth2.model.Userinfo::class,
+ com.google.api.services.oauth2.model.Tokeninfo::class,
+ com.google.api.services.oauth2.Oauth2.Tokeninfo::class,
+ com.google.api.services.oauth2.Oauth2.Userinfo.Get::class,
+ com.google.api.services.oauth2.Oauth2.Userinfo.V2.Me::class,
+ com.google.api.services.oauth2.Oauth2.Userinfo.V2.Me.Get::class,
+ // google-oauth-client
+ com.google.api.client.auth.oauth.OAuthAuthorizeTemporaryTokenUrl::class,
+ com.google.api.client.auth.oauth.OAuthCallbackUrl::class,
+ com.google.api.client.auth.oauth.OAuthCredentialsResponse::class,
+ com.google.api.client.auth.oauth2.AuthorizationCodeRequestUrl::class,
+ com.google.api.client.auth.oauth2.AuthorizationCodeResponseUrl::class,
+ com.google.api.client.auth.oauth2.AuthorizationCodeTokenRequest::class,
+ com.google.api.client.auth.oauth2.AuthorizationRequestUrl::class,
+ com.google.api.client.auth.oauth2.PasswordTokenRequest::class,
+ com.google.api.client.auth.oauth2.RefreshTokenRequest::class,
+ com.google.api.client.auth.oauth2.TokenErrorResponse::class,
+ com.google.api.client.auth.oauth2.TokenRequest::class,
+ com.google.api.client.auth.oauth2.TokenResponse::class,
+ com.google.api.client.auth.openidconnect.IdToken.Payload::class,
+ com.google.api.client.auth.openidconnect.IdTokenResponse::class
+ // @TODO: This must be fixed first
+ // https://github.com/googleapis/google-oauth-java-client/issues/947
+ // com.google.api.client.auth.openidconnect.IdTokenVerifier.PublicKeyLoader.JsonWebKeySet::class
+ // com.google.api.client.auth.openidconnect.IdTokenVerifier.PublicKeyLoader.JsonWebKey::class
+ ]
+)
+class ReflectionConfig
diff --git a/src/main/kotlin/com/acme/resources/AuthorizeResource.kt b/src/main/kotlin/com/acme/resources/AuthorizeResource.kt
new file mode 100644
index 0000000..cbccf5d
--- /dev/null
+++ b/src/main/kotlin/com/acme/resources/AuthorizeResource.kt
@@ -0,0 +1,23 @@
+package com.acme.resources
+
+import com.acme.GoogleOAuthClient
+import javax.ws.rs.GET
+import javax.ws.rs.Path
+import javax.ws.rs.Produces
+import javax.ws.rs.core.MediaType
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory
+
+
+@Path("/authorize")
+class AuthorizeResource(private val googleOAuthClient: GoogleOAuthClient) {
+
+ private val logger: Logger = LoggerFactory.getLogger(this.javaClass)
+
+ @GET
+ @Produces(MediaType.TEXT_PLAIN)
+ fun handle(): String {
+ logger.info("Build auth url")
+ return googleOAuthClient.createAuthUrl("some-state").toString()
+ }
+}
diff --git a/src/main/kotlin/com/acme/resources/CallbackResource.kt b/src/main/kotlin/com/acme/resources/CallbackResource.kt
new file mode 100644
index 0000000..706561d
--- /dev/null
+++ b/src/main/kotlin/com/acme/resources/CallbackResource.kt
@@ -0,0 +1,31 @@
+package com.acme.resources
+
+import com.acme.GoogleOAuthClient
+import javax.ws.rs.GET
+import javax.ws.rs.Path
+import javax.ws.rs.Produces
+import javax.ws.rs.QueryParam
+import javax.ws.rs.core.MediaType
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory
+
+
+@Path("/callback")
+class CallbackResource(
+ private val googleOAuthClient: GoogleOAuthClient
+) {
+
+ private val logger: Logger = LoggerFactory.getLogger(this.javaClass)
+
+ @GET
+ @Produces(MediaType.TEXT_PLAIN)
+ fun handle(
+ @QueryParam("code") code: String,
+ @QueryParam("state") state: String
+ ): String {
+ logger.info("Get refresh token")
+ val (refreshToken) = googleOAuthClient.authenticate(code)
+
+ return refreshToken
+ }
+}
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
deleted file mode 100644
index e69de29..0000000
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
new file mode 100644
index 0000000..c2a736b
--- /dev/null
+++ b/src/main/resources/application.yml
@@ -0,0 +1,9 @@
+pws-google:
+ application-name: "Some app"
+ client-id: ${GOOGLE_CLIENT_ID}
+ client-secret: ${GOOGLE_CLIENT_SECRET}
+ redirect-uri: ${GOOGLE_CLIENT_REDIRECT_URI}
+
+quarkus:
+ log:
+ level: ${LOG_LEVEL}
diff --git a/src/main/resources/reflection-config.json b/src/main/resources/reflection-config.json
index ed64df0..fe51488 100644
--- a/src/main/resources/reflection-config.json
+++ b/src/main/resources/reflection-config.json
@@ -1,9 +1 @@
-[
- {
- "name": "com.google.api.client.http.GenericUrl",
- "allPublicMethods": true,
- "allDeclaredFields": true,
- "allDeclaredMethods": true,
- "allDeclaredConstructors": true
- }
-]
+[]
diff --git a/src/main/resources/reflection-config.json.example b/src/main/resources/reflection-config.json.example
new file mode 100644
index 0000000..8ac2f58
--- /dev/null
+++ b/src/main/resources/reflection-config.json.example
@@ -0,0 +1,58 @@
+[
+ {
+ "name": "com.google.api.client.http.GenericUrl",
+ "allPublicMethods": true,
+ "allDeclaredFields": true,
+ "allDeclaredMethods": true,
+ "allDeclaredConstructors": true
+ },
+ {
+ "name": "com.google.api.client.auth.oauth2.TokenErrorResponse",
+ "allPublicMethods": true,
+ "allDeclaredFields": true,
+ "allDeclaredMethods": true,
+ "allDeclaredConstructors": true
+ },
+ {
+ "name": "com.google.api.client.googleapis.auth.oauth2.GoogleAuthorizationCodeTokenRequest",
+ "allPublicMethods": true,
+ "allDeclaredFields": true,
+ "allDeclaredMethods": true,
+ "allDeclaredConstructors": true
+ },
+ {
+ "name": "com.google.api.client.auth.oauth2.AuthorizationCodeTokenRequest",
+ "allPublicMethods": true,
+ "allDeclaredFields": true,
+ "allDeclaredMethods": true,
+ "allDeclaredConstructors": true
+ },
+ {
+ "name": ",com.google.api.client.auth.oauth2.TokenRequest",
+ "allPublicMethods": true,
+ "allDeclaredFields": true,
+ "allDeclaredMethods": true,
+ "allDeclaredConstructors": true
+ },
+ {
+ "name": "com.google.api.client.util.GenericData",
+ "allPublicMethods": true,
+ "allDeclaredFields": true,
+ "allDeclaredMethods": true,
+ "allDeclaredConstructors": true
+ },
+ {
+ "name": "com.google.api.client.util.GenericData.EntrySet",
+ "allPublicMethods": true,
+ "allDeclaredFields": true,
+ "allDeclaredMethods": true,
+ "allDeclaredConstructors": true
+ },
+ {
+ "name": "java.util.AbstractMap",
+ "allPublicMethods": true,
+ "allDeclaredFields": true,
+ "allDeclaredMethods": true,
+ "allDeclaredConstructors": true
+ }
+]
diff --git a/src/test/kotlin/com/acme/GreetingResourceIT.kt b/src/test/kotlin/com/acme/GreetingResourceIT.kt
deleted file mode 100644
index e393621..0000000
--- a/src/test/kotlin/com/acme/GreetingResourceIT.kt
+++ /dev/null
@@ -1,6 +0,0 @@
-package com.acme
-
-import io.quarkus.test.junit.QuarkusIntegrationTest
-
-@QuarkusIntegrationTest
-class GreetingResourceIT : GreetingResourceTest()
diff --git a/src/test/kotlin/com/acme/GreetingResourceTest.kt b/src/test/kotlin/com/acme/GreetingResourceTest.kt
deleted file mode 100644
index 68bd32b..0000000
--- a/src/test/kotlin/com/acme/GreetingResourceTest.kt
+++ /dev/null
@@ -1,19 +0,0 @@
-package com.acme
-
-import io.quarkus.test.junit.QuarkusTest
-import io.restassured.RestAssured.given
-import org.hamcrest.CoreMatchers.`is`
-import org.junit.jupiter.api.Test
-
-@QuarkusTest
-class GreetingResourceTest {
-
- @Test
- fun testHelloEndpoint() {
- given()
- .`when`().get("/")
- .then()
- .statusCode(200)
- .body(`is`("https://accounts.google.com/o/oauth2/auth?access_type=offline&approval_prompt=force&client_id=some-id&redirect_uri=http://localhost:3000&response_type=code&scope=openid%20https://www.googleapis.com/auth/userinfo.profile%20https://www.googleapis.com/auth/userinfo.email&state=some-state"))
- }
-}