Skip to content

Commit 5f5ec8f

Browse files
authored
feat: add AuthTokenGenerator (#1212)
1 parent 003633b commit 5f5ec8f

File tree

3 files changed

+125
-0
lines changed

3 files changed

+125
-0
lines changed

runtime/auth/aws-signing-common/api/aws-signing-common.api

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
1+
public final class aws/smithy/kotlin/runtime/auth/awssigning/AuthTokenGenerator {
2+
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
3+
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
4+
public final fun generateAuthToken-exY8QGI (Laws/smithy/kotlin/runtime/net/url/Url;Ljava/lang/String;JLkotlin/coroutines/Continuation;)Ljava/lang/Object;
5+
public final fun getClock ()Laws/smithy/kotlin/runtime/time/Clock;
6+
public final fun getCredentialsProvider ()Laws/smithy/kotlin/runtime/auth/awscredentials/CredentialsProvider;
7+
public final fun getService ()Ljava/lang/String;
8+
public final fun getSigner ()Laws/smithy/kotlin/runtime/auth/awssigning/AwsSigner;
9+
}
10+
111
public final class aws/smithy/kotlin/runtime/auth/awssigning/AwsChunkedByteReadChannel : aws/smithy/kotlin/runtime/io/SdkByteReadChannel {
212
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
313
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
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
package aws.smithy.kotlin.runtime.auth.awssigning
6+
7+
import aws.smithy.kotlin.runtime.auth.awscredentials.CredentialsProvider
8+
import aws.smithy.kotlin.runtime.auth.awssigning.AwsSigningConfig.Companion.invoke
9+
import aws.smithy.kotlin.runtime.http.HttpMethod
10+
import aws.smithy.kotlin.runtime.http.request.HttpRequest
11+
import aws.smithy.kotlin.runtime.net.url.Url
12+
import aws.smithy.kotlin.runtime.time.Clock
13+
import kotlin.time.Duration
14+
15+
/**
16+
* Generates an authentication token, which is a SigV4-signed URL with the HTTP scheme removed.
17+
* @param service The name of the service the token is being generated for
18+
* @param credentialsProvider The [CredentialsProvider] which will provide credentials to use when generating the auth token
19+
* @param signer The [AwsSigner] implementation to use when creating the authentication token
20+
* @param clock The [Clock] implementation to use
21+
*/
22+
public class AuthTokenGenerator(
23+
public val service: String,
24+
public val credentialsProvider: CredentialsProvider,
25+
public val signer: AwsSigner,
26+
public val clock: Clock = Clock.System,
27+
) {
28+
private fun Url.trimScheme(): String = toString().removePrefix(scheme.protocolName).removePrefix("://")
29+
30+
public suspend fun generateAuthToken(endpoint: Url, region: String, expiration: Duration): String {
31+
val req = HttpRequest(HttpMethod.GET, endpoint)
32+
33+
val config = AwsSigningConfig {
34+
credentials = credentialsProvider.resolve()
35+
this.region = region
36+
service = this@AuthTokenGenerator.service
37+
signingDate = clock.now()
38+
expiresAfter = expiration
39+
signatureType = AwsSignatureType.HTTP_REQUEST_VIA_QUERY_PARAMS
40+
}
41+
42+
return signer.sign(req, config).output.url.trimScheme()
43+
}
44+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
package aws.smithy.kotlin.runtime.auth.awssigning
6+
7+
import aws.smithy.kotlin.runtime.auth.awscredentials.Credentials
8+
import aws.smithy.kotlin.runtime.auth.awscredentials.CredentialsProvider
9+
import aws.smithy.kotlin.runtime.collections.Attributes
10+
import aws.smithy.kotlin.runtime.http.Headers
11+
import aws.smithy.kotlin.runtime.http.request.HttpRequest
12+
import aws.smithy.kotlin.runtime.http.request.toBuilder
13+
import aws.smithy.kotlin.runtime.net.Host
14+
import aws.smithy.kotlin.runtime.net.url.Url
15+
import aws.smithy.kotlin.runtime.time.Instant
16+
import aws.smithy.kotlin.runtime.time.ManualClock
17+
import kotlinx.coroutines.test.runTest
18+
import kotlin.test.Test
19+
import kotlin.test.assertContains
20+
import kotlin.test.assertTrue
21+
import kotlin.time.Duration.Companion.seconds
22+
import kotlin.time.DurationUnit
23+
24+
class AuthTokenGeneratorTest {
25+
@Test
26+
fun testGenerateAuthToken() = runTest {
27+
val credentials = Credentials("akid", "secret")
28+
29+
val credentialsProvider = object : CredentialsProvider {
30+
var credentialsResolved = false
31+
override suspend fun resolve(attributes: Attributes): Credentials {
32+
credentialsResolved = true
33+
return credentials
34+
}
35+
}
36+
37+
val clock = ManualClock(Instant.fromEpochSeconds(0))
38+
39+
val generator = AuthTokenGenerator("foo", credentialsProvider, TEST_SIGNER, clock = clock)
40+
41+
val endpoint = Url { host = Host.parse("foo.bar.us-east-1.baz") }
42+
val token = generator.generateAuthToken(endpoint, "us-east-1", 333.seconds)
43+
44+
assertContains(token, "foo.bar.us-east-1.baz")
45+
assertContains(token, "X-Amz-Credential=signature") // test custom signer was invoked
46+
assertContains(token, "X-Amz-Expires=333") // expiration
47+
assertContains(token, "X-Amz-SigningDate=0") // clock
48+
49+
assertTrue(credentialsProvider.credentialsResolved)
50+
}
51+
}
52+
53+
private val TEST_SIGNER = object : AwsSigner {
54+
override suspend fun sign(
55+
request: HttpRequest,
56+
config: AwsSigningConfig,
57+
): AwsSigningResult<HttpRequest> {
58+
val builder = request.toBuilder()
59+
builder.url.parameters.decodedParameters.apply {
60+
put("X-Amz-Credential", "signature")
61+
put("X-Amz-Expires", (config.expiresAfter?.toLong(DurationUnit.SECONDS) ?: 900).toString())
62+
put("X-Amz-SigningDate", config.signingDate.epochSeconds.toString())
63+
}
64+
65+
return AwsSigningResult<HttpRequest>(builder.build(), "signature".encodeToByteArray())
66+
}
67+
68+
override suspend fun signChunk(chunkBody: ByteArray, prevSignature: ByteArray, config: AwsSigningConfig): AwsSigningResult<Unit> = throw IllegalStateException("signChunk unexpectedly invoked")
69+
70+
override suspend fun signChunkTrailer(trailingHeaders: Headers, prevSignature: ByteArray, config: AwsSigningConfig): AwsSigningResult<Unit> = throw IllegalStateException("signChunkTrailer unexpectedly invoked")
71+
}

0 commit comments

Comments
 (0)