Skip to content

Commit

Permalink
feat: new SDK core integration (#114)
Browse files Browse the repository at this point in the history
Co-authored-by: mohnoor94 <[email protected]>
Co-authored-by: Omar Aljarrah <[email protected]>
  • Loading branch information
3 people authored Dec 3, 2024
1 parent c8b1b83 commit 3fcec30
Show file tree
Hide file tree
Showing 121 changed files with 3,704 additions and 3,560 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ Make sure you have **Java 8** or higher.
```groovy
// gradle.build
dependencies {
implementation 'com.expediagroup:lodging-connectivity-sdk:1.0.7-SNAPSHOT'
implementation 'com.expediagroup:lodging-connectivity-sdk:1.0.6-SNAPSHOT'
}
```

Expand All @@ -29,7 +29,7 @@ dependencies {
<dependency>
<groupId>com.expediagroup</groupId>
<artifactId>lodging-connectivity-sdk</artifactId>
<version>1.0.7-SNAPSHOT</version>
<version>1.0.6-SNAPSHOT</version>
</dependency>
```

Expand Down
11 changes: 1 addition & 10 deletions apollo-compiler-plugin/build.gradle
Original file line number Diff line number Diff line change
@@ -1,12 +1,3 @@
plugins {
id 'org.jetbrains.kotlin.jvm' version '2.0.21'
}

kotlin {
jvmToolchain(8)
}

dependencies {
// Add apollo-compiler as a dependency
implementation("com.apollographql.apollo:apollo-compiler:4.1.0")
implementation 'com.apollographql.apollo:apollo-compiler:4.1.0'
}
10 changes: 10 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
@@ -1,12 +1,22 @@
plugins {
id 'java'
id 'org.jetbrains.kotlin.jvm' version '2.0.21'
}

subprojects {
/* Shared plugins between all modules */
apply plugin: 'java'
apply plugin: 'org.jetbrains.kotlin.jvm'

/* Shared repositories between all modules */
repositories {
mavenCentral()
}

kotlin {
jvmToolchain(8)
}

java {
withSourcesJar()
sourceCompatibility = JavaVersion.VERSION_1_8
Expand Down
37 changes: 16 additions & 21 deletions code/build.gradle
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
plugins {
id 'org.jetbrains.kotlin.jvm' version '2.0.21'
id 'org.jetbrains.dokka' version '1.9.20'
id 'com.apollographql.apollo' version '4.1.0'

Expand All @@ -8,8 +7,14 @@ plugins {
id 'signing'
}

kotlin {
jvmToolchain(8)
jar {
manifest {
attributes(
"version": version,
"artifactId": rootProject.name,
"userAgentPrefix": "expediagroup-sdk-java-${rootProject.name.replaceFirst("-sdk", "")}"
)
}
}

dependencies {
Expand All @@ -19,29 +24,19 @@ dependencies {
/* Dokka */
dokkaHtmlPlugin 'org.jetbrains.dokka:versioning-plugin:1.9.20'

/* Apollo Kotlin */
api 'com.apollographql.apollo:apollo-runtime:4.1.0'
/* Apollo */
api 'com.apollographql.java:client:0.0.2'
implementation 'com.apollographql.adapters:apollo-adapters-core:0.0.4'
implementation 'com.apollographql.ktor:apollo-engine-ktor:0.0.2'


/* EG SDK Core */
implementation 'io.ktor:ktor-client-core:2.3.13'
implementation 'io.ktor:ktor-client-auth-jvm:2.3.13'
implementation 'io.ktor:ktor-http:2.3.13'
implementation 'io.ktor:ktor-client-okhttp:2.3.13'
implementation 'io.ktor:ktor-client-content-negotiation-jvm:2.3.13'
implementation 'io.ktor:ktor-serialization-jackson-jvm:2.3.13'
implementation 'io.ktor:ktor-client-logging-jvm:2.3.13'
implementation 'io.ktor:ktor-client-encoding:2.3.13'
implementation 'com.fasterxml.jackson.core:jackson-databind:2.18.2'
implementation 'org.jetbrains.kotlinx:atomicfu-jvm:0.26.0'
implementation 'javax.validation:validation-api:2.0.1.Final'
implementation 'org.hibernate.validator:hibernate-validator:6.2.5.Final'
implementation 'jakarta.validation:jakarta.validation-api:2.0.2'
implementation 'com.ebay.ejmask:ejmask-api:1.0.3'
implementation 'com.ebay.ejmask:ejmask-extensions:1.0.3'
implementation 'com.fasterxml.jackson.core:jackson-databind:2.18.1'
implementation 'com.fasterxml.jackson.module:jackson-module-kotlin:2.18.1'
implementation 'org.slf4j:slf4j-api:2.0.16'
}

apply from: "tasks-gradle/apollo.gradle"
apply from: "tasks-gradle/publishing.gradle"
apply from: "tasks-gradle/dokka.gradle"


Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/*
* Copyright (C) 2024 Expedia, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.expediagroup.sdk.core.authentication.bearer

import com.expediagroup.sdk.core.http.Request
import com.expediagroup.sdk.core.http.Response
import com.expediagroup.sdk.core.interceptor.Interceptor
import com.expediagroup.sdk.core.model.exception.service.ExpediaGroupAuthException
import java.io.IOException

/**
* An interceptor that handles bearer token-based authentication for HTTP requests.
*
* This interceptor ensures that an up-to-date bearer token is added to the `Authorization` header of each HTTP request.
* It manages token expiration and re-authentication automatically. If the token is about to expire, the interceptor
* synchronously refreshes it before proceeding with the request.
*
* @param authManager The [BearerAuthenticationManager] used for making authentication requests execution and parsing.
*/
class BearerAuthenticationInterceptor(
private val authManager: BearerAuthenticationManager
) : Interceptor {

private val lock = Any()

/**
* Intercepts the HTTP request, adding a bearer token to the `Authorization` header.
*
* This method checks if the token needs to be refreshed and does so if necessary. It excludes
* requests targeting the `authUrl` from this behavior to avoid recursive authentication requests.
*
* @param chain The [Interceptor.Chain] responsible for managing the request and its progression.
* @return The [Response] resulting from the executed request.
* @throws ExpediaGroupAuthException If authentication fails due to invalid credentials or server errors
*/
@Throws(ExpediaGroupAuthException::class)
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()

if (isAuthenticationRequest(request)) {
return chain.proceed(request)
}

ensureValidAuthentication()

val authorizedRequest = request.newBuilder()
.addHeader("Authorization", authManager.getAuthorizationHeaderValue())
.build()

return chain.proceed(authorizedRequest)
}

/**
* Checks if the given request is for authentication.
*/
private fun isAuthenticationRequest(request: Request): Boolean = request.url.toString() == authManager.authUrl

/**
* Ensures there is a valid authentication token available.
* If needed, authenticates under a synchronization lock to prevent multiple simultaneous authentications.
*
* @throws ExpediaGroupAuthException If authentication fails
*/
@Throws(ExpediaGroupAuthException::class)
private fun ensureValidAuthentication() {
try {
if (authManager.isTokenAboutToExpire()) {
synchronized(lock) {
if (authManager.isTokenAboutToExpire()) {
authManager.authenticate()
}
}
}
} catch (e: IOException) {
throw ExpediaGroupAuthException("Failed to authenticate", e)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
/*
* Copyright (C) 2024 Expedia, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.expediagroup.sdk.core.authentication.bearer

import com.expediagroup.sdk.core.authentication.common.AuthenticationManager
import com.expediagroup.sdk.core.authentication.common.Credentials
import com.expediagroup.sdk.core.client.Transport
import com.expediagroup.sdk.core.http.MediaType
import com.expediagroup.sdk.core.http.Request
import com.expediagroup.sdk.core.http.RequestBody
import com.expediagroup.sdk.core.http.Response
import com.expediagroup.sdk.core.logging.common.LoggerDecorator
import com.expediagroup.sdk.core.logging.common.RequestLogger
import com.expediagroup.sdk.core.logging.common.ResponseLogger
import com.expediagroup.sdk.core.model.exception.client.ExpediaGroupResponseParsingException
import com.expediagroup.sdk.core.model.exception.service.ExpediaGroupAuthException
import com.expediagroup.sdk.core.model.exception.service.ExpediaGroupNetworkException
import org.slf4j.LoggerFactory

/**
* Manages bearer token authentication for HTTP requests.
*
* The `BearerAuthenticationManager` handles the lifecycle of bearer tokens, including retrieval, storage,
* and validation. It interacts with an authentication server to fetch tokens using client credentials,
* ensures tokens are refreshed when necessary, and provides them in the required format for authorization headers.
*
* @param transport The [Transport] used to execute authentication requests.
* @param authUrl The URL of the authentication server's endpoint to obtain bearer tokens.
* @param credentials The [Credentials] containing the client key and secret used for authentication.
*/
class BearerAuthenticationManager(
val authUrl: String,
private val transport: Transport,
private val credentials: Credentials
) : AuthenticationManager {

@Volatile
private var bearerTokenStorage = BearerTokenStorage.empty

/**
* Initiates authentication to obtain a new bearer token.
*
* This method sends a request to the authentication server, parses the response, and
* stores the token for future use.
*
* @throws ExpediaGroupAuthException If the authentication request fails.
* @throws ExpediaGroupResponseParsingException If the response cannot be parsed.
*/
@Throws(ExpediaGroupAuthException::class, ExpediaGroupResponseParsingException::class)
override fun authenticate() {
clearAuthentication()
.let {
buildAuthenticationRequest().also {
RequestLogger.log(logger, it, "Authentication")
}
}.let {
executeAuthenticationRequest(it).also {
ResponseLogger.log(logger, it, "Authentication")
}
}.let {
TokenResponse.parse(it)
}.also {
storeToken(it)
}
}

/**
* Checks if the current bearer token is about to expire and needs renewal.
*
* @return `true` if the token is near expiration, `false` otherwise.
*/
fun isTokenAboutToExpire(): Boolean = run {
bearerTokenStorage.isAboutToExpire()
}

/**
* Clears the stored authentication token.
*
* This method resets the internal token storage, effectively invalidating the current session.
*/
override fun clearAuthentication() = run {
bearerTokenStorage = BearerTokenStorage.empty
}

/**
* Retrieves the stored token formatted as an `Authorization` header value.
*
* @return The token in the format `Bearer <token>` for use in HTTP headers.
*/
fun getAuthorizationHeaderValue(): String = run {
bearerTokenStorage.getAuthorizationHeaderValue()
}

/**
* Creates an HTTP request to fetch a new bearer token from the authentication server.
*
* @return A [Request] object configured with the necessary headers and parameters.
*/
private fun buildAuthenticationRequest(): Request = run {
Request.Builder()
.url(authUrl)
.method("POST", RequestBody.create(mapOf("grant_type" to "client_credentials")))
.header("Authorization", credentials.encodeBasic())
.header("Content-Type", MediaType.APPLICATION_FORM_URLENCODED.toString())
.build()
}

/**
* Executes the authentication request and validates the response.
*
* @param request The [Request] object to be executed.
* @return The [Response] received from the server.
* @throws ExpediaGroupAuthException If the server responds with an error.
*/
@Throws(ExpediaGroupAuthException::class, ExpediaGroupNetworkException::class)
private fun executeAuthenticationRequest(request: Request): Response = run {
transport.execute(request).apply {
if (!this.isSuccessful) {
throw ExpediaGroupAuthException(this.code, "Authentication failed")
}
}
}

/**
* Stores the retrieved token in internal storage for subsequent use.
*
* @param tokenResponse The [TokenResponse] containing the token and its expiration time.
*/
private fun storeToken(tokenResponse: TokenResponse) = run {
bearerTokenStorage = BearerTokenStorage.create(
accessToken = tokenResponse.accessToken,
expiresIn = tokenResponse.expiresIn
)
}

private companion object {
private val logger = LoggerDecorator(LoggerFactory.getLogger(this::class.java.enclosingClass))
}
}
Loading

0 comments on commit 3fcec30

Please sign in to comment.