Skip to content

Commit

Permalink
Merge pull request aws-geospatial#9 from makeen-project/ALMS-133
Browse files Browse the repository at this point in the history
ALMS-141, ALMS-133
  • Loading branch information
olegfilimonov authored May 30, 2024
2 parents 962e7d3 + da02edb commit 8117ea4
Show file tree
Hide file tree
Showing 28 changed files with 314 additions and 553 deletions.
4 changes: 2 additions & 2 deletions library/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,8 @@ dependencies {
implementation("com.squareup.okhttp3:okhttp:4.12.0")
implementation("com.google.code.gson:gson:2.10.1")
implementation("androidx.security:security-crypto:1.1.0-alpha06")
implementation("com.squareup.retrofit2:retrofit:2.9.0")
implementation("com.squareup.retrofit2:converter-gson:2.9.0")
implementation("aws.sdk.kotlin:cognitoidentity:1.2.21")
implementation("aws.sdk.kotlin:location:1.2.21")

testImplementation("junit:junit:4.13.2")
testImplementation("org.jetbrains.kotlin:kotlin-test:1.9.20")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package software.amazon.location.auth


import aws.sdk.kotlin.services.location.LocationClient
import aws.sdk.kotlin.services.location.model.SearchPlaceIndexForPositionRequest
import aws.sdk.kotlin.services.location.model.SearchPlaceIndexForPositionResponse

/**
* Provides methods to interact with the Amazon Location service.
*
* @property locationClient An instance of LocationClient used for making requests to the Amazon Location service.
*/
class AmazonLocationClient(
private val locationClient: LocationClient
) {

/**
* Reverse geocodes a location specified by longitude and latitude coordinates.
*
* @param placeIndexName The name of the place index resource to use for the reverse geocoding request.
* @param longitude The longitude of the location to reverse geocode.
* @param latitude The latitude of the location to reverse geocode.
* @param mLanguage The language to use for the reverse geocoding results.
* @param mMaxResults The maximum number of results to return.
* @return A response containing the reverse geocoding results.
*/
suspend fun reverseGeocode(
placeIndexName: String,
longitude: Double,
latitude: Double,
mLanguage: String,
mMaxResults: Int
): SearchPlaceIndexForPositionResponse {
val request = SearchPlaceIndexForPositionRequest {
indexName = placeIndexName
position = listOf(longitude, latitude)
maxResults = mMaxResults
language = mLanguage
}

val response = locationClient.searchPlaceIndexForPosition(request)
return response
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,17 +31,17 @@ class AwsSignerInterceptor(

override fun intercept(chain: Interceptor.Chain): Response {
val originalRequest = chain.request()
if (!originalRequest.url.host.contains("amazonaws.com")) {
if (!originalRequest.url.host.contains("amazonaws.com") || credentialsProvider?.getCredentialsProvider() == null) {
return chain.proceed(originalRequest)
}
runBlocking {
if (credentialsProvider != null && !credentialsProvider.isCredentialsValid(credentialsProvider.getCredentialsProvider())) {
if (!credentialsProvider.isCredentialsValid(credentialsProvider.getCredentialsProvider()!!)) {
credentialsProvider.checkCredentials()
}
}
val accessKeyId = credentialsProvider?.getCredentialsProvider()?.accessKeyId
val secretKey = credentialsProvider?.getCredentialsProvider()?.secretKey
val sessionToken = credentialsProvider?.getCredentialsProvider()?.sessionToken
val accessKeyId = credentialsProvider.getCredentialsProvider()?.accessKeyId
val secretKey = credentialsProvider.getCredentialsProvider()?.secretKey
val sessionToken = credentialsProvider.getCredentialsProvider()?.sessionToken
if (!accessKeyId.isNullOrEmpty() && !secretKey.isNullOrEmpty() && !sessionToken.isNullOrEmpty() && region.isNotEmpty()) {
val dateMilli = Date().time
val host = extractHostHeader(originalRequest.url.toString())
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package software.amazon.location.auth

import android.content.Context
import software.amazon.location.auth.data.model.response.Credentials
import aws.sdk.kotlin.services.cognitoidentity.model.Credentials
import aws.smithy.kotlin.runtime.time.Instant
import aws.smithy.kotlin.runtime.time.epochMilliseconds
import aws.smithy.kotlin.runtime.time.fromEpochMilliseconds
import software.amazon.location.auth.utils.Constants.ACCESS_KEY_ID
import software.amazon.location.auth.utils.Constants.EXPIRATION
import software.amazon.location.auth.utils.Constants.SECRET_KEY
Expand Down Expand Up @@ -50,10 +53,11 @@ class CognitoCredentialsProvider {
*/
private fun saveCredentials(credentials: Credentials) {
if (securePreferences === null) throw Exception("Not initialized")
securePreferences?.put(ACCESS_KEY_ID, credentials.accessKeyId)
securePreferences?.put(SECRET_KEY, credentials.secretKey)
securePreferences?.put(SESSION_TOKEN, credentials.sessionToken)
securePreferences?.put(EXPIRATION, credentials.expiration.toString())
credentials.accessKeyId?.let { securePreferences?.put(ACCESS_KEY_ID, it) }
credentials.secretKey?.let { securePreferences?.put(SECRET_KEY, it) }
credentials.sessionToken?.let { securePreferences?.put(SESSION_TOKEN, it) }
credentials.expiration?.let { securePreferences?.put(EXPIRATION, it.epochMilliseconds.toString()) }

}

/**
Expand All @@ -62,12 +66,17 @@ class CognitoCredentialsProvider {
*/
fun getCachedCredentials(): Credentials? {
if (securePreferences === null) return null
val accessKeyId = securePreferences?.get(ACCESS_KEY_ID)
val secretKey = securePreferences?.get(SECRET_KEY)
val sessionToken = securePreferences?.get(SESSION_TOKEN)
val expiration = securePreferences?.get(EXPIRATION)
if (accessKeyId.isNullOrEmpty() || secretKey.isNullOrEmpty() || sessionToken.isNullOrEmpty() || expiration.isNullOrEmpty()) return null
return Credentials(accessKeyId, expiration.toDouble(), secretKey, sessionToken)
val mAccessKeyId = securePreferences?.get(ACCESS_KEY_ID)
val mSecretKey = securePreferences?.get(SECRET_KEY)
val mSessionToken = securePreferences?.get(SESSION_TOKEN)
val mExpiration = securePreferences?.get(EXPIRATION)
if (mAccessKeyId.isNullOrEmpty() || mSecretKey.isNullOrEmpty() || mSessionToken.isNullOrEmpty() || mExpiration.isNullOrEmpty()) return null
return Credentials.invoke {
accessKeyId = mAccessKeyId
secretKey = mSecretKey
sessionToken = mSessionToken
expiration = Instant.fromEpochMilliseconds(mExpiration.toLong())
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,4 +87,15 @@ class EncryptedSharedPreferences(private val context: Context, private val prefe
editor.remove(key)
editor.apply()
}

/**
* Checks if a particular key exists in the encrypted preferences.
* @param key The key to check for existence.
* @return True if the key exists, false otherwise.
* @throws Exception if preferences are not initialized.
*/
fun contains(key: String): Boolean {
if (sharedPreferences === null) throw Exception("SharedPreferences not initialized")
return sharedPreferences!!.contains(key)
}
}
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
package software.amazon.location.auth

import android.content.Context
import java.util.Date
import software.amazon.location.auth.data.model.response.Credentials
import software.amazon.location.auth.data.network.AwsRetrofitClient
import aws.sdk.kotlin.runtime.auth.credentials.StaticCredentialsProvider
import aws.sdk.kotlin.services.cognitoidentity.CognitoIdentityClient
import aws.sdk.kotlin.services.cognitoidentity.model.GetCredentialsForIdentityRequest
import aws.sdk.kotlin.services.cognitoidentity.model.GetIdRequest
import aws.sdk.kotlin.services.location.LocationClient
import aws.smithy.kotlin.runtime.auth.awscredentials.Credentials
import aws.smithy.kotlin.runtime.auth.awscredentials.CredentialsProvider
import aws.smithy.kotlin.runtime.time.Instant
import aws.smithy.kotlin.runtime.time.epochMilliseconds
import software.amazon.location.auth.utils.AwsRegions
import software.amazon.location.auth.utils.AwsRegions.Companion.DEFAULT_REGION
import software.amazon.location.auth.utils.CognitoCredentialsClient
import software.amazon.location.auth.utils.Constants.API_KEY
import software.amazon.location.auth.utils.Constants.BASE_URL
import software.amazon.location.auth.utils.Constants.IDENTITY_POOL_ID
import software.amazon.location.auth.utils.Constants.METHOD
import software.amazon.location.auth.utils.Constants.REGION
import software.amazon.location.auth.utils.Constants.SERVICE_NAME

const val PREFS_NAME = "software.amazon.location.auth"

Expand All @@ -24,6 +26,7 @@ class LocationCredentialsProvider {
private var cognitoCredentialsProvider: CognitoCredentialsProvider? = null
private var apiKeyProvider: ApiKeyCredentialsProvider? = null
private var securePreferences: EncryptedSharedPreferences
private var locationClient: LocationClient? = null

/**
* Initializes with Cognito credentials.
Expand Down Expand Up @@ -86,13 +89,11 @@ class LocationCredentialsProvider {
}

/**
* check AWS credentials.
* Checks AWS credentials availability and validity.
*
* This function retrieves the identity pool ID and region from a secure preferences
*
* The function first attempts to initialize the CognitoCredentialsProvider. If it fails
* or if there are no cached credentials or if the cached credentials are invalid, it
* generates new credentials.
* This function retrieves the identity pool ID and region from secure preferences. It then
* attempts to initialize the CognitoCredentialsProvider. If no credentials are found or if
* the cached credentials are invalid, new credentials are generated.
*
* @throws Exception if the identity pool ID or region is not found, or if credential generation fails.
*/
Expand All @@ -112,39 +113,91 @@ class LocationCredentialsProvider {
val credentials = cognitoCredentialsProvider?.getCachedCredentials()
credentials?.let {
if (!isCredentialsValid(it)) {
AwsRetrofitClient.clearApiService()
generateCredentials(region, identityPoolId)
} else {
initAwsRetrofitClient()
}
}
}
}

private fun initAwsRetrofitClient() {
val region = securePreferences.get(REGION) ?: DEFAULT_REGION.regionName
AwsRetrofitClient.init(getUrl(region), SERVICE_NAME, region, this)

/**
* Retrieves the LocationClient instance with configured AWS credentials.
*
* This function initializes and returns the LocationClient with the AWS region and
* credentials retrieved from secure preferences.
*
* @return An instance of LocationClient for interacting with the Amazon Location service.
* @throws Exception if the AWS region is not found in secure preferences.
*/
fun getLocationClient(): LocationClient? {
val identityPoolId = securePreferences.get(IDENTITY_POOL_ID)
val region = securePreferences.get(REGION)
if (identityPoolId === null || region === null) throw Exception("No credentials found")
if (locationClient == null) {
val credentialsProvider = createCredentialsProvider()
locationClient = LocationClient {
this.region = region
this.credentialsProvider = credentialsProvider
}
}
return locationClient
}

/**
* Creates a new instance of CredentialsProvider using the credentials obtained from the current provider.
*
* This function constructs a CredentialsProvider with the AWS credentials retrieved from the existing provider.
* It extracts the access key ID, secret access key, and session token from the current provider and initializes
* a StaticCredentialsProvider with these credentials.
*
* @return A new instance of CredentialsProvider initialized with the current AWS credentials.
* @throws Exception if credentials cannot be retrieved.
*/
private fun createCredentialsProvider(): CredentialsProvider {
if (getCredentialsProvider() == null || getCredentialsProvider()?.accessKeyId == null || getCredentialsProvider()?.secretKey == null) throw Exception(
"Failed to get credentials"
)
return StaticCredentialsProvider(
Credentials.invoke(
accessKeyId = getCredentialsProvider()?.accessKeyId!!,
secretAccessKey = getCredentialsProvider()?.secretKey!!,
sessionToken = getCredentialsProvider()?.sessionToken,
)
)
}

/**
* Generates new AWS credentials using the specified region and identity pool ID.
*
* This function uses CognitoCredentialsClient to fetch the identity ID and credentials,
* and then initializes the CognitoCredentialsProvider with the retrieved credentials.
* This function fetches the identity ID and credentials from Cognito, and then initializes
* the CognitoCredentialsProvider with the retrieved credentials.
*
* @param region The AWS region where the identity pool is located.
* @param identityPoolId The identity pool ID for Cognito.
* @throws Exception if the credential generation fails.
*/
private suspend fun generateCredentials(region: String, identityPoolId: String) {
val cognitoCredentialsHttpHelper = CognitoCredentialsClient(region)
val client = CognitoIdentityClient { this.region = region }
try {
val identityId = cognitoCredentialsHttpHelper.getIdentityId(identityPoolId)
val getIdResponse = client.getId(GetIdRequest { this.identityPoolId = identityPoolId })
val identityId =
getIdResponse.identityId ?: throw Exception("Failed to get identity ID")
if (identityId.isNotEmpty()) {
val credentials = cognitoCredentialsHttpHelper.getCredentials(identityId)
cognitoCredentialsProvider =
CognitoCredentialsProvider(context, credentials.credentials)
initAwsRetrofitClient()
val getCredentialsResponse =
client.getCredentialsForIdentity(GetCredentialsForIdentityRequest {
this.identityId = identityId
})

val credentials = getCredentialsResponse.credentials
?: throw Exception("Failed to get credentials")
if (credentials.accessKeyId == null || credentials.secretKey == null || credentials.sessionToken == null) throw Exception(
"Credentials generation failed"
)
cognitoCredentialsProvider = CognitoCredentialsProvider(
context,
credentials
)
locationClient = null
}
} catch (e: Exception) {
throw Exception("Credentials generation failed")
Expand All @@ -154,26 +207,23 @@ class LocationCredentialsProvider {
/**
* Checks if the provided credentials are still valid.
*
* This function compares the current date with the expiration date of the credentials.
*
* @param credentials The AWS credentials to validate.
* @return True if the credentials are valid (i.e., not expired), false otherwise.
*/
fun isCredentialsValid(credentials: Credentials): Boolean {
val expirationTime = credentials.expiration.toLong() * 1000
val expirationDate = Date(expirationTime)
val currentDate = Date()
return currentDate.before(expirationDate)
fun isCredentialsValid(credentials: aws.sdk.kotlin.services.cognitoidentity.model.Credentials): Boolean {
val currentTimeMillis = Instant.now().epochMilliseconds
val expirationTimeMillis = credentials.expiration?.epochMilliseconds ?: throw Exception("Failed to get credentials")
return currentTimeMillis < expirationTimeMillis
}

/**
* Retrieves the Cognito credentials.
* @return The Credentials instance.
* @throws Exception If the Cognito provider is not initialized.
*/
fun getCredentialsProvider(): Credentials {
fun getCredentialsProvider(): aws.sdk.kotlin.services.cognitoidentity.model.Credentials? {
if (cognitoCredentialsProvider === null) throw Exception("Cognito credentials not initialized")
return cognitoCredentialsProvider?.getCachedCredentials()!!
return cognitoCredentialsProvider?.getCachedCredentials()
}

/**
Expand All @@ -192,7 +242,7 @@ class LocationCredentialsProvider {
*/
suspend fun refresh() {
if (cognitoCredentialsProvider === null) throw Exception("Refresh is only supported for Cognito credentials. Make sure to use the cognito constructor.")
AwsRetrofitClient.clearApiService()
locationClient = null
cognitoCredentialsProvider?.clearCredentials()
checkCredentials()
}
Expand All @@ -205,9 +255,4 @@ class LocationCredentialsProvider {
if (cognitoCredentialsProvider === null) throw Exception("Clear is only supported for Cognito credentials. Make sure to use the cognito constructor.")
cognitoCredentialsProvider?.clearCredentials()
}

private fun getUrl(region: String): String {
val urlBuilder = StringBuilder(BASE_URL.format(region))
return urlBuilder.toString()
}
}

This file was deleted.

This file was deleted.

This file was deleted.

Loading

0 comments on commit 8117ea4

Please sign in to comment.