From 4138f0e00ab6500bc7378cb14d8c099840f10fe2 Mon Sep 17 00:00:00 2001 From: Adam Fowler Date: Wed, 18 Sep 2024 17:08:54 +0100 Subject: [PATCH 1/4] Soto Cognito Authentication v5 fixes --- Package.swift | 10 +-- .../Request+Cognito+async.swift | 71 ------------------- .../Authenticators.swift | 32 +++++---- .../Request+Cognito.swift | 49 ++++++++----- 4 files changed, 56 insertions(+), 106 deletions(-) delete mode 100644 Sources/SotoCognitoAuthentication/AsyncAwaitSupport/Request+Cognito+async.swift diff --git a/Package.swift b/Package.swift index a9a9622..61f0249 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.2 +// swift-tools-version:5.10 //===----------------------------------------------------------------------===// // // This source file is part of the Soto for AWS open source project @@ -20,15 +20,15 @@ import PackageDescription let package = Package( name: "soto-cognito-authentication", platforms: [ - .macOS(.v10_15), - .iOS(.v13), - .tvOS(.v13), + .macOS(.v13), + .iOS(.v16), + .tvOS(.v16), ], products: [ .library(name: "SotoCognitoAuthentication", targets: ["SotoCognitoAuthentication"]), ], dependencies: [ - .package(url: "https://github.com/soto-project/soto-cognito-authentication-kit.git", from: "4.0.0"), + .package(url: "https://github.com/soto-project/soto-cognito-authentication-kit.git", from: "5.0.0-rc.3"), .package(url: "https://github.com/vapor/vapor.git", from: "4.0.0"), ], targets: [ diff --git a/Sources/SotoCognitoAuthentication/AsyncAwaitSupport/Request+Cognito+async.swift b/Sources/SotoCognitoAuthentication/AsyncAwaitSupport/Request+Cognito+async.swift deleted file mode 100644 index e253661..0000000 --- a/Sources/SotoCognitoAuthentication/AsyncAwaitSupport/Request+Cognito+async.swift +++ /dev/null @@ -1,71 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Soto for AWS open source project -// -// Copyright (c) 2020-2021 the Soto project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of Soto project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -#if compiler(>=5.5) && canImport(_Concurrency) - -import NIO -import Vapor - -@available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *) -extension Request.SotoCognito { - /// helper function that returns if request with bearer token is cognito access authenticated - /// - returns: - /// An access token object that contains the user name and id - public func authenticateAccess() async throws -> CognitoAccessToken { - guard let bearer = request.headers.bearerAuthorization else { - throw Abort(.unauthorized) - } - return try await request.application.cognito.authenticatable.authenticate(accessToken: bearer.token, on: request.eventLoop) - } - - /// helper function that returns if request with bearer token is cognito id authenticated and returns contents in the payload type - /// - returns: - /// The payload contained in the token. See `authenticate(idToken:on:)` for more details - public func authenticateId() async throws -> Payload { - guard let bearer = request.headers.bearerAuthorization else { - throw Abort(.unauthorized) - } - return try await request.application.cognito.authenticatable.authenticate(idToken: bearer.token, on: request.eventLoop) - } - - /// helper function that returns refreshed access and id tokens given a request containing the refresh token as a bearer token - /// - returns: - /// The payload contained in the token. See `authenticate(idToken:on:)` for more details - public func refresh(username: String) async throws -> CognitoAuthenticateResponse { - guard let bearer = request.headers.bearerAuthorization else { - throw Abort(.unauthorized) - } - return try await request.application.cognito.authenticatable.refresh( - username: username, - refreshToken: bearer.token, - context: request, - on: request.eventLoop - ) - } - - /// helper function that returns AWS credentials for a provided identity. The idToken is provided as a bearer token. - /// If you have setup to use an AWSCognito User pool to identify users then the idToken is the idToken returned from the `authenticate` function - /// - returns: - /// AWS credentials for signing request to AWS - public func awsCredentials() async throws -> CognitoIdentity.Credentials { - guard let bearer = request.headers.bearerAuthorization else { - throw Abort(.unauthorized) - } - let identifiable = request.application.cognito.identifiable - let identity = try await identifiable.getIdentityId(idToken: bearer.token, on: request.eventLoop) - return try await identifiable.getCredentialForIdentity(identityId: identity, idToken: bearer.token, on: self.request.eventLoop) - } -} - -#endif // compiler(>=5.5) && canImport(_Concurrency) diff --git a/Sources/SotoCognitoAuthentication/Authenticators.swift b/Sources/SotoCognitoAuthentication/Authenticators.swift index 2bba826..5556827 100644 --- a/Sources/SotoCognitoAuthentication/Authenticators.swift +++ b/Sources/SotoCognitoAuthentication/Authenticators.swift @@ -16,20 +16,26 @@ import NIO import SotoCognitoAuthenticationKit import Vapor +#if hasFeature(RetroactiveAttribute) +extension CognitoAuthenticateResponse: @retroactive Authenticatable {} +extension CognitoAccessToken: @retroactive Authenticatable {} +#else extension CognitoAuthenticateResponse: Authenticatable {} extension CognitoAccessToken: Authenticatable {} +#endif public typealias CognitoBasicAuthenticatable = CognitoAuthenticateResponse public typealias CognitoAccessAuthenticatable = CognitoAccessToken /// Authenticator for Cognito username and password -public struct CognitoBasicAuthenticator: BasicAuthenticator { +public struct CognitoBasicAuthenticator: AsyncBasicAuthenticator { public init() {} - public func authenticate(basic: BasicAuthorization, for request: Request) -> EventLoopFuture { - return request.application.cognito.authenticatable.authenticate(username: basic.username, password: basic.password, context: request, on: request.eventLoop).map { token in + public func authenticate(basic: BasicAuthorization, for request: Request) async throws { + do { + let token = try await request.application.cognito.authenticatable.authenticate(username: basic.username, password: basic.password, context: request) request.auth.login(token) - }.flatMapErrorThrowing { error in + } catch { switch error { case is AWSErrorType, is NIOConnectionError: // report connection errors with AWS, or unrecognised AWSErrorTypes @@ -42,13 +48,14 @@ public struct CognitoBasicAuthenticator: BasicAuthenticator { } /// Authenticator for Cognito access tokens -public struct CognitoAccessAuthenticator: BearerAuthenticator { +public struct CognitoAccessAuthenticator: AsyncBearerAuthenticator { public init() {} - public func authenticate(bearer: BearerAuthorization, for request: Request) -> EventLoopFuture { - return request.application.cognito.authenticatable.authenticate(accessToken: bearer.token, on: request.eventLoop).map { token in + public func authenticate(bearer: BearerAuthorization, for request: Request) async throws { + do { + let token = try await request.application.cognito.authenticatable.authenticate(accessToken: bearer.token) request.auth.login(token) - }.flatMapErrorThrowing { error in + } catch { switch error { case is NIOConnectionError: // loading of jwk may cause a connection error. We should report this @@ -63,13 +70,14 @@ public struct CognitoAccessAuthenticator: BearerAuthenticator { /// Authenticator for Cognito id tokens. Can use this to extract information from Id Token into Payload struct. The list of standard list of claims found in an id token are /// detailed in the [OpenID spec] (https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims) . Your `Payload` type needs /// to decode using these tags, plus the AWS specific "cognito:username" tag and any custom tags you have setup for the user pool. -public struct CognitoIdAuthenticator: BearerAuthenticator { +public struct CognitoIdAuthenticator: AsyncBearerAuthenticator { public init() {} - public func authenticate(bearer: BearerAuthorization, for request: Request) -> EventLoopFuture { - return request.application.cognito.authenticatable.authenticate(idToken: bearer.token, on: request.eventLoop).map { (payload: Payload) -> Void in + public func authenticate(bearer: BearerAuthorization, for request: Request) async throws { + do { + let payload: Payload = try await request.application.cognito.authenticatable.authenticate(idToken: bearer.token) request.auth.login(payload) - }.flatMapErrorThrowing { error in + } catch { switch error { case is NIOConnectionError: // loading of jwk may cause a connection error. We should report this diff --git a/Sources/SotoCognitoAuthentication/Request+Cognito.swift b/Sources/SotoCognitoAuthentication/Request+Cognito.swift index 7816e5f..e957808 100644 --- a/Sources/SotoCognitoAuthentication/Request+Cognito.swift +++ b/Sources/SotoCognitoAuthentication/Request+Cognito.swift @@ -12,11 +12,16 @@ // //===----------------------------------------------------------------------===// -import SotoCognitoAuthenticationKit +import NIO import Vapor +import SotoCognitoAuthenticationKit // extend AWSCognitoAuthenticateResponse so it can be returned from a Vapor route +#if hasFeature(RetroactiveAttribute) +extension CognitoAuthenticateResponse: @retroactive Content {} +#else extension CognitoAuthenticateResponse: Content {} +#endif public extension Request { var cognito: SotoCognito { @@ -27,46 +32,48 @@ public extension Request { /// helper function that returns if request with bearer token is cognito access authenticated /// - returns: /// An access token object that contains the user name and id - public func authenticateAccess() -> EventLoopFuture { + public func authenticateAccess() async throws -> CognitoAccessToken { guard let bearer = request.headers.bearerAuthorization else { - return self.request.eventLoop.makeFailedFuture(Abort(.unauthorized)) + throw Abort(.unauthorized) } - return self.request.application.cognito.authenticatable.authenticate(accessToken: bearer.token, on: self.request.eventLoop) + return try await request.application.cognito.authenticatable.authenticate(accessToken: bearer.token) } /// helper function that returns if request with bearer token is cognito id authenticated and returns contents in the payload type /// - returns: /// The payload contained in the token. See `authenticate(idToken:on:)` for more details - public func authenticateId() -> EventLoopFuture { + public func authenticateId() async throws -> Payload { guard let bearer = request.headers.bearerAuthorization else { - return self.request.eventLoop.makeFailedFuture(Abort(.unauthorized)) + throw Abort(.unauthorized) } - return self.request.application.cognito.authenticatable.authenticate(idToken: bearer.token, on: self.request.eventLoop) + return try await request.application.cognito.authenticatable.authenticate(idToken: bearer.token) } /// helper function that returns refreshed access and id tokens given a request containing the refresh token as a bearer token /// - returns: /// The payload contained in the token. See `authenticate(idToken:on:)` for more details - public func refresh(username: String) -> EventLoopFuture { + public func refresh(username: String) async throws -> CognitoAuthenticateResponse { guard let bearer = request.headers.bearerAuthorization else { - return self.request.eventLoop.makeFailedFuture(Abort(.unauthorized)) + throw Abort(.unauthorized) } - return self.request.application.cognito.authenticatable.refresh(username: username, refreshToken: bearer.token, context: self.request, on: self.request.eventLoop) + return try await request.application.cognito.authenticatable.refresh( + username: username, + refreshToken: bearer.token, + context: request + ) } /// helper function that returns AWS credentials for a provided identity. The idToken is provided as a bearer token. /// If you have setup to use an AWSCognito User pool to identify users then the idToken is the idToken returned from the `authenticate` function /// - returns: /// AWS credentials for signing request to AWS - public func awsCredentials() -> EventLoopFuture { + public func awsCredentials() async throws -> CognitoIdentity.Credentials { guard let bearer = request.headers.bearerAuthorization else { - return self.request.eventLoop.makeFailedFuture(Abort(.unauthorized)) + throw Abort(.unauthorized) } - let identifiable = self.request.application.cognito.identifiable - return identifiable.getIdentityId(idToken: bearer.token, on: self.request.eventLoop) - .flatMap { identity in - return identifiable.getCredentialForIdentity(identityId: identity, idToken: bearer.token, on: self.request.eventLoop) - } + let identifiable = request.application.cognito.identifiable + let identity = try await identifiable.getIdentityId(idToken: bearer.token) + return try await identifiable.getCredentialForIdentity(identityId: identity, idToken: bearer.token) } let request: Request @@ -74,7 +81,7 @@ public extension Request { } /// extend Vapor Request to provide Cognito context -extension Request: CognitoContextData { +extension Request { public var contextData: CognitoIdentityProvider.ContextDataType? { let host = headers["Host"].first ?? "localhost:8080" guard let remoteAddress = remoteAddress else { return nil } @@ -99,3 +106,9 @@ extension Request: CognitoContextData { return contextData } } + +#if hasFeature(RetroactiveAttribute) +extension Request: @retroactive CognitoContextData {} +#else +extension Request: CognitoContextData {} +#endif From 3104954dafd48eb0c2c63157cc6a191794bd646c Mon Sep 17 00:00:00 2001 From: Adam Fowler Date: Wed, 18 Sep 2024 17:10:05 +0100 Subject: [PATCH 2/4] Update CI --- .github/workflows/ci.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cb432be..7c37bab 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,7 +18,7 @@ jobs: runs-on: macOS-latest steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Build run: swift build @@ -26,13 +26,13 @@ jobs: strategy: matrix: os: [ubuntu-latest] - swift: ["swift:5.4", "swift:5.5", "swift:5.6"] + swift: ["swift:5.9", "swift:5.10", "swift:6.0"] runs-on: ${{ matrix.os }} container: image: ${{ matrix.swift }} steps: - name: Checkout - uses: actions/checkout@v1 + uses: actions/checkout@v4 with: fetch-depth: 1 - name: Build From 85e0b328253925421d200d0750e5a992956ba911 Mon Sep 17 00:00:00 2001 From: Adam Fowler Date: Wed, 18 Sep 2024 18:07:28 +0100 Subject: [PATCH 3/4] Remove swift 5.9 --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7c37bab..5c19331 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,7 +26,7 @@ jobs: strategy: matrix: os: [ubuntu-latest] - swift: ["swift:5.9", "swift:5.10", "swift:6.0"] + swift: ["swift:5.10", "swift:6.0"] runs-on: ${{ matrix.os }} container: image: ${{ matrix.swift }} From 2469aa20f5765a9e8e99ff70485968d02edf5266 Mon Sep 17 00:00:00 2001 From: Adam Fowler Date: Wed, 18 Sep 2024 18:14:43 +0100 Subject: [PATCH 4/4] format --- .../SotoCognitoAuthentication/Request+Cognito.swift | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Sources/SotoCognitoAuthentication/Request+Cognito.swift b/Sources/SotoCognitoAuthentication/Request+Cognito.swift index e957808..c42aaa5 100644 --- a/Sources/SotoCognitoAuthentication/Request+Cognito.swift +++ b/Sources/SotoCognitoAuthentication/Request+Cognito.swift @@ -13,8 +13,8 @@ //===----------------------------------------------------------------------===// import NIO -import Vapor import SotoCognitoAuthenticationKit +import Vapor // extend AWSCognitoAuthenticateResponse so it can be returned from a Vapor route #if hasFeature(RetroactiveAttribute) @@ -36,7 +36,7 @@ public extension Request { guard let bearer = request.headers.bearerAuthorization else { throw Abort(.unauthorized) } - return try await request.application.cognito.authenticatable.authenticate(accessToken: bearer.token) + return try await self.request.application.cognito.authenticatable.authenticate(accessToken: bearer.token) } /// helper function that returns if request with bearer token is cognito id authenticated and returns contents in the payload type @@ -46,7 +46,7 @@ public extension Request { guard let bearer = request.headers.bearerAuthorization else { throw Abort(.unauthorized) } - return try await request.application.cognito.authenticatable.authenticate(idToken: bearer.token) + return try await self.request.application.cognito.authenticatable.authenticate(idToken: bearer.token) } /// helper function that returns refreshed access and id tokens given a request containing the refresh token as a bearer token @@ -56,10 +56,10 @@ public extension Request { guard let bearer = request.headers.bearerAuthorization else { throw Abort(.unauthorized) } - return try await request.application.cognito.authenticatable.refresh( + return try await self.request.application.cognito.authenticatable.refresh( username: username, refreshToken: bearer.token, - context: request + context: self.request ) } @@ -71,7 +71,7 @@ public extension Request { guard let bearer = request.headers.bearerAuthorization else { throw Abort(.unauthorized) } - let identifiable = request.application.cognito.identifiable + let identifiable = self.request.application.cognito.identifiable let identity = try await identifiable.getIdentityId(idToken: bearer.token) return try await identifiable.getCredentialForIdentity(identityId: identity, idToken: bearer.token) }