Skip to content

Commit

Permalink
Merge pull request #11 from makeen-project/main
Browse files Browse the repository at this point in the history
Added authentication with API key and log added for generating credential failure
  • Loading branch information
mbalfour-amzn authored Sep 24, 2024
2 parents da146ef + 8648de5 commit 608a286
Show file tree
Hide file tree
Showing 15 changed files with 540 additions and 110 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/test-android.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ jobs:
./gradlew testDebugUnitTest
- name: Upload test results
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v3
if: always()
with:
name: test-results
Expand Down
22 changes: 16 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Add the following lines to the dependencies section of your build.gradle file in

```
implementation("software.amazon.location:auth:0.2.5")
implementation("aws.sdk.kotlin:location:1.2.21")
implementation("aws.sdk.kotlin:location:1.3.29")
implementation("org.maplibre.gl:android-sdk:11.0.0-pre5")
implementation("com.squareup.okhttp3:okhttp:4.12.0")
```
Expand All @@ -34,21 +34,30 @@ import okhttp3.OkHttpClient
You can create an AuthHelper and use it with the AWS Kotlin SDK:

```
// Create a credentail provider using Identity Pool Id with AuthHelper
private fun exampleCognitoLogin() {
// Create a credential provider using Identity Pool Id with AuthHelper
private suspend fun exampleCognitoLogin() {
var authHelper = AuthHelper(applicationContext)
var locationCredentialsProvider : LocationCredentialsProvider = authHelper.authenticateWithCognitoIdentityPool("My-Cognito-Identity-Pool-Id")
var locationCredentialsProvider : LocationCredentialsProvider = authHelper.authenticateWithCognitoIdentityPool("MY-COGNITO-IDENTITY-POOL-ID")
var locationClient = locationCredentialsProvider?.getLocationClient()
}
OR
// Create a credentail provider using custom credential provider with AuthHelper
private fun exampleCustomCredentialLogin() {
// Create a credential provider using custom credential provider with AuthHelper
private suspend fun exampleCustomCredentialLogin() {
var authHelper = AuthHelper(applicationContext)
var locationCredentialsProvider : LocationCredentialsProvider = authHelper.authenticateWithCredentialsProvider("MY-AWS-REGION", MY-CUSTOM-CREDENTIAL-PROVIDER)
var locationClient = locationCredentialsProvider?.getLocationClient()
}
OR
// Create a credential provider using Api key with AuthHelper
private suspend fun exampleApiKeyLogin() {
var authHelper = AuthHelper(applicationContext)
var locationCredentialsProvider : LocationCredentialsProvider = authHelper.authenticateWithApiKey("MY-API-KEY", "MY-AWS-REGION")
var locationClient = locationCredentialsProvider?.getLocationClient()
}
```
You can use the LocationCredentialsProvider to load the maplibre map. Here is an example of that:

Expand All @@ -57,6 +66,7 @@ HttpRequestUtil.setOkHttpClient(
OkHttpClient.Builder()
.addInterceptor(
AwsSignerInterceptor(
applicationContext,
"geo",
"MY-AWS-REGION",
locationCredentialsProvider
Expand Down
16 changes: 8 additions & 8 deletions library/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -83,18 +83,18 @@ android {
}

dependencies {
implementation("androidx.core:core-ktx:1.12.0")
implementation("androidx.appcompat:appcompat:1.6.1")
implementation("com.google.android.material:material:1.10.0")
implementation("com.squareup.okhttp3:okhttp:4.12.0")
implementation("androidx.core:core-ktx:1.13.1")
implementation("androidx.appcompat:appcompat:1.7.0")
implementation("com.google.android.material:material:1.12.0")
implementation("androidx.security:security-crypto:1.1.0-alpha06")
implementation("aws.sdk.kotlin:cognitoidentity:1.2.21")
implementation("aws.sdk.kotlin:location:1.2.21")
implementation("aws.sdk.kotlin:cognitoidentity:1.3.29")
implementation("aws.sdk.kotlin:location:1.3.29")
implementation("com.squareup.okhttp3:okhttp:4.12.0")

testImplementation("junit:junit:4.13.2")
testImplementation("org.jetbrains.kotlin:kotlin-test:1.9.20")
testImplementation("io.mockk:mockk:1.13.10")

androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
androidTestImplementation("androidx.test.ext:junit:1.2.1")
androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package software.amazon.location.auth

import android.content.Context
import software.amazon.location.auth.utils.Constants.API_KEY

/**
* Provides API key credentials for accessing services and manages their storage.
*/
class ApiKeyCredentialsProvider {
private var securePreferences: EncryptedSharedPreferences? = null

/**
* Initializes the provider and saves the provided API key.
* @param context The application context.
* @param apiKey The API key to save.
*/
constructor(context: Context, apiKey: String) {
initialize(context)
saveCredentials(apiKey)
}

/**
* Initializes the provider and retrieves the API key from the cache.
* @param context The application context.
* @throws Exception If no credentials are found in the cache.
*/
constructor(context: Context) {
initialize(context)
val apiKey = getCachedCredentials()
if (apiKey === null) throw Exception("No credentials found")
}

private fun initialize(context: Context) {
securePreferences = EncryptedSharedPreferences(context, PREFS_NAME)
securePreferences?.initEncryptedSharedPreferences()
}

private fun saveCredentials(apiKey: String) {
if (securePreferences === null) throw Exception("Not initialized")
securePreferences!!.put(API_KEY, apiKey)
}

/**
* Retrieves the cached API key credentials.
* @return The API key or null if not found.
* @throws Exception If the AWSKeyValueStore is not initialized.
*/
fun getCachedCredentials(): String? {
if (securePreferences === null) throw Exception("Not initialized")
return securePreferences!!.get(API_KEY)
}

/**
* Clears the stored credentials.
*/
fun clearCredentials() {
securePreferences?.remove(API_KEY)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package software.amazon.location.auth

import aws.smithy.kotlin.runtime.client.ProtocolRequestInterceptorContext
import aws.smithy.kotlin.runtime.http.interceptors.HttpInterceptor
import aws.smithy.kotlin.runtime.http.request.HttpRequest
import aws.smithy.kotlin.runtime.http.request.toBuilder
import aws.smithy.kotlin.runtime.http.request.url
import aws.smithy.kotlin.runtime.net.url.Url
import software.amazon.location.auth.utils.Constants.QUERY_PARAM_KEY

class ApiKeyInterceptor(
private val apiKey: String,
) : HttpInterceptor {
override suspend fun modifyBeforeSigning(context: ProtocolRequestInterceptorContext<Any, HttpRequest>): HttpRequest {
val req = context.protocolRequest.toBuilder()

if (!context.protocolRequest.url.toString().contains("$QUERY_PARAM_KEY=")) {
req.url(Url.parse(context.protocolRequest.url.toString()+"?$QUERY_PARAM_KEY=$apiKey"))
return req.build()
} else {
return super.modifyBeforeSigning(context)
}
}
}
18 changes: 18 additions & 0 deletions library/src/main/java/software/amazon/location/auth/AuthHelper.kt
Original file line number Diff line number Diff line change
Expand Up @@ -118,4 +118,22 @@ class AuthHelper(private val context: Context) {
locationCredentialsProvider
}
}

/**
* Authenticates using an API key.
* @param apiKey The API key for authentication.
* @param region The AWS region as a string.
* @return A LocationCredentialsProvider instance.
*/
suspend fun authenticateWithApiKey(apiKey: String, region: String): LocationCredentialsProvider {
return withContext(Dispatchers.IO) {
val locationCredentialsProvider = LocationCredentialsProvider(
context,
AwsRegions.fromName(region),
apiKey,
)
locationCredentialsProvider.initializeLocationClient()
locationCredentialsProvider
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package software.amazon.location.auth


import android.content.Context
import java.net.URL
import java.nio.charset.StandardCharsets
import java.security.MessageDigest
Expand All @@ -13,64 +14,95 @@ import kotlinx.coroutines.runBlocking
import okhttp3.Interceptor
import okhttp3.Response
import software.amazon.location.auth.utils.Constants
import software.amazon.location.auth.utils.Constants.API_KEY
import software.amazon.location.auth.utils.Constants.HEADER_HOST
import software.amazon.location.auth.utils.Constants.HEADER_X_AMZ_CONTENT_SHA256
import software.amazon.location.auth.utils.Constants.HEADER_X_AMZ_DATE
import software.amazon.location.auth.utils.Constants.HEADER_X_AMZ_SECURITY_TOKEN
import software.amazon.location.auth.utils.Constants.METHOD
import software.amazon.location.auth.utils.Constants.QUERY_PARAM_KEY
import software.amazon.location.auth.utils.Constants.TIME_PATTERN
import software.amazon.location.auth.utils.HASHING_ALGORITHM
import software.amazon.location.auth.utils.awsAuthorizationHeader

class AwsSignerInterceptor(
private val context: Context,
private val serviceName: String,
private val region: String,
private val credentialsProvider: LocationCredentialsProvider?
) : Interceptor {

private val sdfMap = HashMap<String, SimpleDateFormat>()

private var securePreferences: EncryptedSharedPreferences?= null
override fun intercept(chain: Interceptor.Chain): Response {
val originalRequest = chain.request()
if (!originalRequest.url.host.contains("amazonaws.com") || credentialsProvider?.getCredentialsProvider() == null) {
return chain.proceed(originalRequest)
}
runBlocking {
if (!credentialsProvider.isCredentialsValid()) {
credentialsProvider.verifyAndRefreshCredentials()
}
if (securePreferences == null){
securePreferences = initPreference(context)
}
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())
val timeStamp = getTimeStamp(dateMilli)
val payloadHash = sha256Hex((originalRequest.body ?: "").toString())

val modifiedRequest =
originalRequest.newBuilder()
.header(HEADER_X_AMZ_DATE, timeStamp)
.header(HEADER_HOST, host)
.header(HEADER_X_AMZ_SECURITY_TOKEN, sessionToken)
.header(HEADER_X_AMZ_CONTENT_SHA256, payloadHash)
val method = securePreferences?.get(METHOD)
if (method === null) throw Exception("No credentials found")
if (method == "apiKey") {
val originalHttpUrl = originalRequest.url
val hasKey = originalHttpUrl.queryParameter(QUERY_PARAM_KEY) != null
val newHttpUrl = if (!hasKey) {
val apiKey = securePreferences?.get(API_KEY)
originalHttpUrl.newBuilder()
.addQueryParameter(QUERY_PARAM_KEY, apiKey)
.build()
} else {
originalHttpUrl
}
val newRequest = originalRequest.newBuilder()
.url(newHttpUrl)
.build()

return chain.proceed(newRequest)
} else {
runBlocking {
if (!credentialsProvider.isCredentialsValid()) {
credentialsProvider.verifyAndRefreshCredentials()
}
}
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())
val timeStamp = getTimeStamp(dateMilli)
val payloadHash = sha256Hex((originalRequest.body ?: "").toString())

val finalRequest = modifiedRequest.newBuilder()
.header(
Constants.HEADER_AUTHORIZATION,
modifiedRequest.awsAuthorizationHeader(
accessKeyId,
secretKey,
region,
serviceName,
timeStamp
val modifiedRequest =
originalRequest.newBuilder()
.header(HEADER_X_AMZ_DATE, timeStamp)
.header(HEADER_HOST, host)
.header(HEADER_X_AMZ_SECURITY_TOKEN, sessionToken)
.header(HEADER_X_AMZ_CONTENT_SHA256, payloadHash)
.build()

val finalRequest = modifiedRequest.newBuilder()
.header(
Constants.HEADER_AUTHORIZATION,
modifiedRequest.awsAuthorizationHeader(
accessKeyId,
secretKey,
region,
serviceName,
timeStamp
)
)
)
.build()
return chain.proceed(finalRequest)
.build()
return chain.proceed(finalRequest)
}
return chain.proceed(originalRequest)
}
return chain.proceed(originalRequest)
}

fun initPreference(context: Context): EncryptedSharedPreferences {
return EncryptedSharedPreferences(context, PREFS_NAME).apply { initEncryptedSharedPreferences() }
}

private fun extractHostHeader(urlString: String): String {
Expand Down
Loading

0 comments on commit 608a286

Please sign in to comment.