Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions Sources/BedrockAuthentication.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,14 @@ import SmithyIdentity
/// - `webIdentity`: Use a web identity token (JWT) to assume an IAM role. This is useful for applications running on iOS, tvOS or macOS where you cannot use the AWS CLI. Typically, the application authenticates the user with an external Identity provider (such as Sign In with Apple or Login With Google) and receives a JWT token. The application then uses this token to assume an IAM role and receive temporary AWS credentials. Some additional configuration is required on your AWS account to allow this. See https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_providers_oidc.html for more information. If you use Sign In With Apple, read https://docs.aws.amazon.com/sdk-for-swift/latest/developer-guide/apple-integration.html for more information.
/// Because `webidentity` is often used by application presenting a user interface. This method of authentication allows you to pass an optional closure that will be called when the credentials are retrieved. This is useful for updating the UI or notifying the user. The closure is called on the main (UI) thread.
/// - `static`: Use static AWS credentials. We strongly recommend to not use this option in production. This might be useful in some rare cases when testing and debugging.
/// - `apiKey`: Use an API key to authenticate. This is useful for applications that do not require full AWS credentials and only need to access specific APIs. The API key is passed as a string. API Keys are generated in the AWS console.
public enum BedrockAuthentication: Sendable, CustomStringConvertible {
case `default`
case profile(profileName: String = "default")
case sso(profileName: String = "default")
case webIdentity(token: String, roleARN: String, region: Region, notification: @Sendable () -> Void = {})
case `static`(accessKey: String, secretKey: String, sessionToken: String)
case apiKey(key: String)

public var description: String {
switch self {
Expand All @@ -43,6 +45,8 @@ public enum BedrockAuthentication: Sendable, CustomStringConvertible {
return "webIdentity: \(redactingSecret(secret: token)), roleARN: \(roleARN), region: \(region)"
case .static(let accessKey, let secretKey, _):
return "static: \(accessKey), secretKey: \(redactingSecret(secret: secretKey))"
case .apiKey(let key):
return "apiKey: \(redactingSecret(secret: key))"
}
}
private func redactingSecret(secret: String) -> String {
Expand All @@ -59,7 +63,8 @@ public enum BedrockAuthentication: Sendable, CustomStringConvertible {
) async throws -> (any SmithyIdentity.AWSCredentialIdentityResolver)? {

switch self {
case .default:
case .default,
.apiKey(_):
return nil
case .profile(let profileName):
return try? ProfileAWSCredentialIdentityResolver(profileName: profileName)
Expand All @@ -74,7 +79,7 @@ public enum BedrockAuthentication: Sendable, CustomStringConvertible {
notify: notification
)
case .static(let accessKey, let secretKey, let sessionToken):
logger.warning("Using static AWS credentials. This is not recommended for production.")
logger.info("Using static AWS credentials. This is not recommended for production.")
let creds = AWSCredentialIdentity(accessKey: accessKey, secret: secretKey, sessionToken: sessionToken)
return StaticAWSCredentialIdentityResolver(creds)
}
Expand Down
67 changes: 51 additions & 16 deletions Sources/BedrockService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,13 @@ import AwsCommonRuntimeKit
import Foundation
import Logging

// for setenv and unsetenv functions
#if os(Linux)
import Glibc
#else
import Darwin.C
#endif

public struct BedrockService: Sendable {
package let region: Region
package let logger: Logging.Logger
Expand Down Expand Up @@ -106,21 +113,18 @@ public struct BedrockService: Sendable {
/// - authentication: The authentication type to use
/// - Returns: Configured BedrockClientProtocol instance
/// - Throws: Error if client creation fails
static private func createBedrockClient(
internal static func createBedrockClient(
region: Region,
authentication: BedrockAuthentication,
logger: Logging.Logger
) async throws
-> BedrockClientProtocol
-> BedrockClient
{
let config = try await BedrockClient.BedrockClientConfiguration(
region: region.rawValue
)
if let awsCredentialIdentityResolver = try? await authentication.getAWSCredentialIdentityResolver(
let config: BedrockClient.BedrockClientConfiguration = try await prepareConfig(
region: region,
authentication: authentication,
logger: logger
) {
config.awsCredentialIdentityResolver = awsCredentialIdentityResolver
}
)
return BedrockClient(config: config)
}

Expand All @@ -130,24 +134,55 @@ public struct BedrockService: Sendable {
/// - authentication: The authentication type to use
/// - Returns: Configured BedrockRuntimeClientProtocol instance
/// - Throws: Error if client creation fails
static private func createBedrockRuntimeClient(
internal static func createBedrockRuntimeClient(
region: Region,
authentication: BedrockAuthentication,
logger: Logging.Logger
)
async throws
-> BedrockRuntimeClientProtocol
-> BedrockRuntimeClient
{
let config =
try await BedrockRuntimeClient.BedrockRuntimeClientConfiguration(
region: region.rawValue
)
let config: BedrockRuntimeClient.BedrockRuntimeClientConfiguration = try await prepareConfig(
region: region,
authentication: authentication,
logger: logger
)
return BedrockRuntimeClient(config: config)
}

/// Generic function to create client configuration and avoid duplication code.
internal static func prepareConfig<C: BedrockConfigProtocol>(
Copy link

Copilot AI Jul 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The prepareConfig function bundles initialization, credential resolver setup, API-key header injection, and environment cleanup. Consider splitting these responsibilities into focused helper methods or adding detailed comments to clarify each step and improve readability and testability.

Copilot uses AI. Check for mistakes.
region: Region,
authentication: BedrockAuthentication,
logger: Logging.Logger
) async throws -> C {
var config: C = try await .init()

config.region = region.rawValue

// support profile, SSO, web identity and static authentication
if let awsCredentialIdentityResolver = try? await authentication.getAWSCredentialIdentityResolver(
logger: logger
) {
config.awsCredentialIdentityResolver = awsCredentialIdentityResolver
}
return BedrockRuntimeClient(config: config)

// support API keys
if case .apiKey(let key) = authentication {
config.httpClientConfiguration.defaultHeaders.add(
name: "Authorization",
value: "Bearer \(key)"
)
logger.trace("Using API Key for authentication")
} else {
logger.trace("Using AWS credentials for authentication")
}

//We uncheck AWS_BEARER_TOKEN_BEDROCK to avoid conflict with future AWS SDK version
//see https://docs.aws.amazon.com/bedrock/latest/userguide/getting-started-api-keys.html
unsetenv("AWS_BEARER_TOKEN_BEDROCK")
Copy link

Copilot AI Jul 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Unsetting the environment variable globally may have unintended side effects for other concurrent code. Consider scoping this change or documenting its impact on other AWS SDK behavior.

Suggested change
unsetenv("AWS_BEARER_TOKEN_BEDROCK")
// Temporarily remove AWS_BEARER_TOKEN_BEDROCK from the environment for this configuration
var environment = ProcessInfo.processInfo.environment
environment["AWS_BEARER_TOKEN_BEDROCK"] = nil
// Pass the modified environment to the relevant configuration or process if needed

Copilot uses AI. Check for mistakes.

return config
}

func handleCommonError(_ error: Error, context: String) throws -> Never {
Expand Down
30 changes: 30 additions & 0 deletions Sources/Protocols/BedrockConfigProtocol.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift Bedrock Library open source project
//
// Copyright (c) 2025 Amazon.com, Inc. or its affiliates
// and the Swift Bedrock Library project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of Swift Bedrock Library project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

import AWSBedrock
import AWSBedrockRuntime
import ClientRuntime
import SmithyIdentity

protocol BedrockConfigProtocol {
init() async throws
var awsCredentialIdentityResolver: any SmithyIdentity.AWSCredentialIdentityResolver { get set }
var httpClientConfiguration: ClientRuntime.HttpClientConfiguration { get set }
var region: String? { get set }
}
extension BedrockClient.BedrockClientConfiguration: @retroactive @unchecked Sendable, BedrockConfigProtocol {}
extension BedrockRuntimeClient.BedrockRuntimeClientConfiguration: @retroactive @unchecked Sendable,
BedrockConfigProtocol
{}
56 changes: 56 additions & 0 deletions Tests/AuthenticationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@
//
//===----------------------------------------------------------------------===//

import AWSBedrock
import AwsCommonRuntimeKit
import Logging
import Testing

@testable import BedrockService
Expand All @@ -35,6 +37,7 @@ extension BedrockServiceTests {
region: .useast1,
notification: {}
),
BedrockAuthentication.apiKey(key: "MY_SECRET_API_KEY"),
]
)
func authNoLeaks(auth: BedrockAuthentication) {
Expand Down Expand Up @@ -62,4 +65,57 @@ extension BedrockServiceTests {
// let _ = try await bedrock.listModels()
// }
// }

@Test("Authentication: API Key authentication adds HTTP Header to the request")
func apiKeyAuthentication() async throws {
// given
let testApiKey = "test-api-key-12345"
let auth = BedrockAuthentication.apiKey(key: testApiKey)

// when
// create bedrock configuration with API Key authentication
let config: BedrockClient.BedrockClientConfiguration = try await BedrockService.prepareConfig(
region: .useast1,
authentication: auth,
logger: Logger(label: "test.logger"),
)

// then
#expect(config.region == Region.useast1.rawValue) // default region
#expect(
config.httpClientConfiguration.defaultHeaders.value(for: "Authorization") == "Bearer test-api-key-12345"
)

}

@Test("Authentication: API Key returns nil credential resolver")
func apiKeyCredentialResolver() async throws {
// given
let testApiKey = "test-api-key-12345"
let auth = BedrockAuthentication.apiKey(key: testApiKey)
let logger = Logger(label: "test.logger")

// when
let resolver = try await auth.getAWSCredentialIdentityResolver(logger: logger)

// then
#expect(resolver == nil, "API Key authentication should return nil credential resolver")
}

@Test("Authentication: API Key description doesn't leak full key")
func apiKeyDescription() {
// given
let testApiKey = "test-api-key-12345-very-long-key"
let auth = BedrockAuthentication.apiKey(key: testApiKey)

// when
let description = auth.description

// then
#expect(description.contains("apiKey:"))
#expect(description.contains("tes...")) // should show first 3 characters
#expect(description.contains("*** shuuut, it's a secret ***"))
#expect(!description.contains("12345")) // should not contain the full key
#expect(!description.contains("very-long-key")) // should not contain the full key
}
}