Skip to content

Commit

Permalink
feat: add AuthTokenGenerator (#1212)
Browse files Browse the repository at this point in the history
  • Loading branch information
lauzadis authored Jan 10, 2025
1 parent 003633b commit 5f5ec8f
Show file tree
Hide file tree
Showing 3 changed files with 125 additions and 0 deletions.
10 changes: 10 additions & 0 deletions runtime/auth/aws-signing-common/api/aws-signing-common.api
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
public final class aws/smithy/kotlin/runtime/auth/awssigning/AuthTokenGenerator {
public fun <init> (Ljava/lang/String;Laws/smithy/kotlin/runtime/auth/awscredentials/CredentialsProvider;Laws/smithy/kotlin/runtime/auth/awssigning/AwsSigner;Laws/smithy/kotlin/runtime/time/Clock;)V
public synthetic fun <init> (Ljava/lang/String;Laws/smithy/kotlin/runtime/auth/awscredentials/CredentialsProvider;Laws/smithy/kotlin/runtime/auth/awssigning/AwsSigner;Laws/smithy/kotlin/runtime/time/Clock;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun generateAuthToken-exY8QGI (Laws/smithy/kotlin/runtime/net/url/Url;Ljava/lang/String;JLkotlin/coroutines/Continuation;)Ljava/lang/Object;
public final fun getClock ()Laws/smithy/kotlin/runtime/time/Clock;
public final fun getCredentialsProvider ()Laws/smithy/kotlin/runtime/auth/awscredentials/CredentialsProvider;
public final fun getService ()Ljava/lang/String;
public final fun getSigner ()Laws/smithy/kotlin/runtime/auth/awssigning/AwsSigner;
}

public final class aws/smithy/kotlin/runtime/auth/awssigning/AwsChunkedByteReadChannel : aws/smithy/kotlin/runtime/io/SdkByteReadChannel {
public fun <init> (Laws/smithy/kotlin/runtime/io/SdkByteReadChannel;Laws/smithy/kotlin/runtime/auth/awssigning/AwsSigner;Laws/smithy/kotlin/runtime/auth/awssigning/AwsSigningConfig;[BLaws/smithy/kotlin/runtime/http/DeferredHeaders;)V
public synthetic fun <init> (Laws/smithy/kotlin/runtime/io/SdkByteReadChannel;Laws/smithy/kotlin/runtime/auth/awssigning/AwsSigner;Laws/smithy/kotlin/runtime/auth/awssigning/AwsSigningConfig;[BLaws/smithy/kotlin/runtime/http/DeferredHeaders;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package aws.smithy.kotlin.runtime.auth.awssigning

import aws.smithy.kotlin.runtime.auth.awscredentials.CredentialsProvider
import aws.smithy.kotlin.runtime.auth.awssigning.AwsSigningConfig.Companion.invoke
import aws.smithy.kotlin.runtime.http.HttpMethod
import aws.smithy.kotlin.runtime.http.request.HttpRequest
import aws.smithy.kotlin.runtime.net.url.Url
import aws.smithy.kotlin.runtime.time.Clock
import kotlin.time.Duration

/**
* Generates an authentication token, which is a SigV4-signed URL with the HTTP scheme removed.
* @param service The name of the service the token is being generated for
* @param credentialsProvider The [CredentialsProvider] which will provide credentials to use when generating the auth token
* @param signer The [AwsSigner] implementation to use when creating the authentication token
* @param clock The [Clock] implementation to use
*/
public class AuthTokenGenerator(
public val service: String,
public val credentialsProvider: CredentialsProvider,
public val signer: AwsSigner,
public val clock: Clock = Clock.System,
) {
private fun Url.trimScheme(): String = toString().removePrefix(scheme.protocolName).removePrefix("://")

public suspend fun generateAuthToken(endpoint: Url, region: String, expiration: Duration): String {
val req = HttpRequest(HttpMethod.GET, endpoint)

val config = AwsSigningConfig {
credentials = credentialsProvider.resolve()
this.region = region
service = this@AuthTokenGenerator.service
signingDate = clock.now()
expiresAfter = expiration
signatureType = AwsSignatureType.HTTP_REQUEST_VIA_QUERY_PARAMS
}

return signer.sign(req, config).output.url.trimScheme()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package aws.smithy.kotlin.runtime.auth.awssigning

import aws.smithy.kotlin.runtime.auth.awscredentials.Credentials
import aws.smithy.kotlin.runtime.auth.awscredentials.CredentialsProvider
import aws.smithy.kotlin.runtime.collections.Attributes
import aws.smithy.kotlin.runtime.http.Headers
import aws.smithy.kotlin.runtime.http.request.HttpRequest
import aws.smithy.kotlin.runtime.http.request.toBuilder
import aws.smithy.kotlin.runtime.net.Host
import aws.smithy.kotlin.runtime.net.url.Url
import aws.smithy.kotlin.runtime.time.Instant
import aws.smithy.kotlin.runtime.time.ManualClock
import kotlinx.coroutines.test.runTest
import kotlin.test.Test
import kotlin.test.assertContains
import kotlin.test.assertTrue
import kotlin.time.Duration.Companion.seconds
import kotlin.time.DurationUnit

class AuthTokenGeneratorTest {
@Test
fun testGenerateAuthToken() = runTest {
val credentials = Credentials("akid", "secret")

val credentialsProvider = object : CredentialsProvider {
var credentialsResolved = false
override suspend fun resolve(attributes: Attributes): Credentials {
credentialsResolved = true
return credentials
}
}

val clock = ManualClock(Instant.fromEpochSeconds(0))

val generator = AuthTokenGenerator("foo", credentialsProvider, TEST_SIGNER, clock = clock)

val endpoint = Url { host = Host.parse("foo.bar.us-east-1.baz") }
val token = generator.generateAuthToken(endpoint, "us-east-1", 333.seconds)

assertContains(token, "foo.bar.us-east-1.baz")
assertContains(token, "X-Amz-Credential=signature") // test custom signer was invoked
assertContains(token, "X-Amz-Expires=333") // expiration
assertContains(token, "X-Amz-SigningDate=0") // clock

assertTrue(credentialsProvider.credentialsResolved)
}
}

private val TEST_SIGNER = object : AwsSigner {
override suspend fun sign(
request: HttpRequest,
config: AwsSigningConfig,
): AwsSigningResult<HttpRequest> {
val builder = request.toBuilder()
builder.url.parameters.decodedParameters.apply {
put("X-Amz-Credential", "signature")
put("X-Amz-Expires", (config.expiresAfter?.toLong(DurationUnit.SECONDS) ?: 900).toString())
put("X-Amz-SigningDate", config.signingDate.epochSeconds.toString())
}

return AwsSigningResult<HttpRequest>(builder.build(), "signature".encodeToByteArray())
}

override suspend fun signChunk(chunkBody: ByteArray, prevSignature: ByteArray, config: AwsSigningConfig): AwsSigningResult<Unit> = throw IllegalStateException("signChunk unexpectedly invoked")

override suspend fun signChunkTrailer(trailingHeaders: Headers, prevSignature: ByteArray, config: AwsSigningConfig): AwsSigningResult<Unit> = throw IllegalStateException("signChunkTrailer unexpectedly invoked")
}

0 comments on commit 5f5ec8f

Please sign in to comment.