From acf889cd6a3bb79f7832787c0446ce2e7dba38d3 Mon Sep 17 00:00:00 2001 From: Francesco Paolo Severino Date: Sun, 10 Nov 2024 13:49:36 +0100 Subject: [PATCH 01/37] Update to Swift 5.10 --- .github/workflows/test.yml | 56 ++++--------------- Package.swift | 40 ++++++++----- Sources/ImperialAuth0/Auth0.swift | 8 +-- Sources/ImperialAuth0/Auth0Router.swift | 8 +-- .../Helpers/Optional+Imperial.swift | 2 +- .../Middleware/ImperialMiddleware.swift | 2 +- .../Routing/FederatedServiceRouter.swift | 12 ++-- Sources/ImperialCore/ServiceRegister.swift | 2 +- .../Services/FederatedService.swift | 6 +- Sources/ImperialDiscord/Discord.swift | 8 +-- Sources/ImperialDiscord/DiscordRouter.swift | 8 +-- Sources/ImperialDropbox/Dropbox.swift | 8 +-- Sources/ImperialDropbox/DropboxRouter.swift | 8 +-- Sources/ImperialFacebook/Facebook.swift | 8 +-- Sources/ImperialFacebook/FacebookRouter.swift | 8 +-- Sources/ImperialGitHub/GitHub.swift | 8 +-- Sources/ImperialGitHub/GitHubRouter.swift | 8 +-- Sources/ImperialGitlab/Gitlab.swift | 8 +-- Sources/ImperialGitlab/GitlabRouter.swift | 8 +-- Sources/ImperialGoogle/JWT/GoogleJWT.swift | 8 +-- .../ImperialGoogle/JWT/GoogleJWTRouter.swift | 8 +-- Sources/ImperialGoogle/Standard/Google.swift | 8 +-- .../Standard/GoogleRouter.swift | 8 +-- Sources/ImperialKeycloak/Keycloak.swift | 8 +-- Sources/ImperialKeycloak/KeycloakRouter.swift | 8 +-- Sources/ImperialMicrosoft/Microsoft.swift | 8 +-- .../ImperialMicrosoft/MicrosoftRouter.swift | 8 +-- Sources/ImperialShopify/Shopify.swift | 8 +-- Sources/ImperialShopify/ShopifyRouter.swift | 8 +-- Tests/ImperialTests/ImperialTests.swift | 4 -- Tests/LinuxMain.swift | 6 -- 31 files changed, 136 insertions(+), 170 deletions(-) delete mode 100644 Tests/LinuxMain.swift diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 21ae4a5c..bf05755d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,49 +1,13 @@ name: test +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true on: - push: - branches: [ main ] - pull_request: - branches: [ main ] - + pull_request: { types: [opened, reopened, synchronize, ready_for_review] } + push: { branches: [ main ] } + jobs: - macos: - strategy: - fail-fast: false - matrix: - xcode: [latest, latest-stable] - runs-on: macos-latest - steps: - - name: Select latest available Xcode - uses: maxim-lobanov/setup-xcode@v1.1 - with: { 'xcode-version': '${{ matrix.xcode }}' } - - name: Check out code - uses: actions/checkout@v2 - - name: Run tests with Thread Sanitizer - run: swift test --enable-test-discovery --sanitize=thread - linux: - strategy: - fail-fast: false - matrix: - swiftver: - - swift:5.2 - - swift:5.3 - swiftos: - - xenial - - bionic - - focal - - centos7 - - centos8 - - amazonlinux2 - container: ${{ format('{0}-{1}', matrix.swiftver, matrix.swiftos) }} - runs-on: ubuntu-latest - steps: - - name: SPM is incompatible with CentOS 7 - if: ${{ matrix.swiftos == 'centos7' }} - run: | - yum install -y make libcurl-devel - git clone https://github.com/git/git -bv2.28.0 --depth 1 && cd git - make prefix=/usr -j all install NO_OPENSSL=1 NO_EXPAT=1 NO_TCLTK=1 NO_GETTEXT=1 NO_PERL=1 - - name: Check out code - uses: actions/checkout@v2 - - name: Run tests with Thread Sanitizer - run: swift test --enable-test-discovery --sanitize=thread + unit-tests: + uses: vapor/ci/.github/workflows/run-unit-tests.yml@main + secrets: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} \ No newline at end of file diff --git a/Package.swift b/Package.swift index d6bbb271..65bb98ff 100755 --- a/Package.swift +++ b/Package.swift @@ -1,10 +1,10 @@ -// swift-tools-version:5.2 +// swift-tools-version:5.10 import PackageDescription let package = Package( name: "Imperial", platforms: [ - .macOS(.v10_15) + .macOS(.v14) ], products: [ .library(name: "ImperialCore", targets: ["ImperialCore"]), @@ -42,18 +42,30 @@ let package = Package( dependencies: [ .product(name: "Vapor", package: "vapor"), .product(name: "JWTKit", package: "jwt-kit"), - ] + ], + swiftSettings: swiftSettings ), - .target(name: "ImperialAuth0", dependencies: ["ImperialCore"]), - .target(name: "ImperialDiscord", dependencies: ["ImperialCore"]), - .target(name: "ImperialDropbox", dependencies: ["ImperialCore"]), - .target(name: "ImperialFacebook", dependencies: ["ImperialCore"]), - .target(name: "ImperialGitHub", dependencies: ["ImperialCore"]), - .target(name: "ImperialGitlab", dependencies: ["ImperialCore"]), - .target(name: "ImperialGoogle", dependencies: ["ImperialCore"]), - .target(name: "ImperialKeycloak", dependencies: ["ImperialCore"]), - .target(name: "ImperialMicrosoft", dependencies: ["ImperialCore"]), - .target(name: "ImperialShopify", dependencies: ["ImperialCore"]), - .testTarget(name: "ImperialTests", dependencies: ["ImperialCore", "ImperialShopify"]), + .target(name: "ImperialAuth0", dependencies: ["ImperialCore"], swiftSettings: swiftSettings), + .target(name: "ImperialDiscord", dependencies: ["ImperialCore"], swiftSettings: swiftSettings), + .target(name: "ImperialDropbox", dependencies: ["ImperialCore"], swiftSettings: swiftSettings), + .target(name: "ImperialFacebook", dependencies: ["ImperialCore"], swiftSettings: swiftSettings), + .target(name: "ImperialGitHub", dependencies: ["ImperialCore"], swiftSettings: swiftSettings), + .target(name: "ImperialGitlab", dependencies: ["ImperialCore"], swiftSettings: swiftSettings), + .target(name: "ImperialGoogle", dependencies: ["ImperialCore"], swiftSettings: swiftSettings), + .target(name: "ImperialKeycloak", dependencies: ["ImperialCore"], swiftSettings: swiftSettings), + .target(name: "ImperialMicrosoft", dependencies: ["ImperialCore"], swiftSettings: swiftSettings), + .target(name: "ImperialShopify", dependencies: ["ImperialCore"], swiftSettings: swiftSettings), + .testTarget(name: "ImperialTests", dependencies: ["ImperialCore", "ImperialShopify"], swiftSettings: swiftSettings), ] ) + +var swiftSettings: [SwiftSetting] { + [ + .enableUpcomingFeature("ExistentialAny"), + .enableUpcomingFeature("ConciseMagicFile"), + .enableUpcomingFeature("ForwardTrailingClosures"), + .enableUpcomingFeature("DisableOutwardActorInference"), + .enableUpcomingFeature("StrictConcurrency"), + .enableExperimentalFeature("StrictConcurrency=complete"), + ] +} \ No newline at end of file diff --git a/Sources/ImperialAuth0/Auth0.swift b/Sources/ImperialAuth0/Auth0.swift index b9c07945..29247edb 100644 --- a/Sources/ImperialAuth0/Auth0.swift +++ b/Sources/ImperialAuth0/Auth0.swift @@ -2,17 +2,17 @@ import Vapor public class Auth0: FederatedService { - public var tokens: FederatedServiceTokens - public var router: FederatedServiceRouter + public var tokens: any FederatedServiceTokens + public var router: any FederatedServiceRouter @discardableResult public required init( - routes: RoutesBuilder, + routes: any RoutesBuilder, authenticate: String, authenticateCallback: ((Request) throws -> (EventLoopFuture))?, callback: String, scope: [String] = [], - completion: @escaping (Request, String) throws -> (EventLoopFuture) + completion: @escaping (Request, String) throws -> (EventLoopFuture) ) throws { self.router = try Auth0Router(callback: callback, completion: completion) self.tokens = self.router.tokens diff --git a/Sources/ImperialAuth0/Auth0Router.swift b/Sources/ImperialAuth0/Auth0Router.swift index d8064081..75afa0e3 100644 --- a/Sources/ImperialAuth0/Auth0Router.swift +++ b/Sources/ImperialAuth0/Auth0Router.swift @@ -4,8 +4,8 @@ import Foundation public class Auth0Router: FederatedServiceRouter { public let baseURL: String - public let tokens: FederatedServiceTokens - public let callbackCompletion: (Request, String) throws -> (EventLoopFuture) + public let tokens: any FederatedServiceTokens + public let callbackCompletion: (Request, String) throws -> (EventLoopFuture) public var scope: [String] = [ ] public var requiredScopes = [ "openid" ] public let callbackURL: String @@ -17,7 +17,7 @@ public class Auth0Router: FederatedServiceRouter { return self.baseURL.finished(with: "/") + path } - public required init(callback: String, completion: @escaping (Request, String) throws -> (EventLoopFuture)) throws { + public required init(callback: String, completion: @escaping (Request, String) throws -> (EventLoopFuture)) throws { let auth = try Auth0Auth() self.tokens = auth self.baseURL = "https://\(auth.domain)" @@ -45,7 +45,7 @@ public class Auth0Router: FederatedServiceRouter { return rtn } - public func callbackBody(with code: String) -> ResponseEncodable { + public func callbackBody(with code: String) -> any ResponseEncodable { Auth0CallbackBody(clientId: self.tokens.clientID, clientSecret: self.tokens.clientSecret, code: code, diff --git a/Sources/ImperialCore/Helpers/Optional+Imperial.swift b/Sources/ImperialCore/Helpers/Optional+Imperial.swift index b4e82a67..5d3032d3 100644 --- a/Sources/ImperialCore/Helpers/Optional+Imperial.swift +++ b/Sources/ImperialCore/Helpers/Optional+Imperial.swift @@ -5,7 +5,7 @@ extension Optional { /// - Parameter error: The error to throw if the optional is `nil`. /// - Returns: The value contained in the optional. /// - Throws: The error passed in if the optional is `nil`. - public func value(or error: Error) throws -> Wrapped { + public func value(or error: any Error) throws -> Wrapped { switch self { case let .some(value): return value case .none: throw error diff --git a/Sources/ImperialCore/Middleware/ImperialMiddleware.swift b/Sources/ImperialCore/Middleware/ImperialMiddleware.swift index acb0ea1d..d03f9ef6 100644 --- a/Sources/ImperialCore/Middleware/ImperialMiddleware.swift +++ b/Sources/ImperialCore/Middleware/ImperialMiddleware.swift @@ -15,7 +15,7 @@ public class ImperialMiddleware: Middleware { /// Checks that the request contains an access token. If it does, let the request through. If not, redirect the user to the `redirectPath`. /// If the `redirectPath` is `nil`, then throw the error from getting the access token (Abort.unauthorized). - public func respond(to request: Request, chainingTo next: Responder) -> EventLoopFuture { + public func respond(to request: Request, chainingTo next: any Responder) -> EventLoopFuture { do { _ = try request.accessToken() return next.respond(to: request) diff --git a/Sources/ImperialCore/Routing/FederatedServiceRouter.swift b/Sources/ImperialCore/Routing/FederatedServiceRouter.swift index 623a460a..415063bc 100644 --- a/Sources/ImperialCore/Routing/FederatedServiceRouter.swift +++ b/Sources/ImperialCore/Routing/FederatedServiceRouter.swift @@ -6,11 +6,11 @@ import Vapor public protocol FederatedServiceRouter { /// A class that gets the client ID and secret from environment variables. - var tokens: FederatedServiceTokens { get } + var tokens: any FederatedServiceTokens { get } /// The callback that is fired after the access token is fetched from the OAuth provider. /// The response that is returned from this callback is also returned from the callback route. - var callbackCompletion: (Request, String) throws -> (EventLoopFuture) { get } + var callbackCompletion: (Request, String) throws -> (EventLoopFuture) { get } /// The scopes to get permission for when getting the access token. /// Usage of this property varies by provider. @@ -43,7 +43,7 @@ public protocol FederatedServiceRouter { /// - callback: The callback URL that the OAuth provider will redirect to after authenticating the user. /// - completion: The completion handler that will be fired at the end of the `callback` route. The access token is passed into it. /// - Throws: Any errors that could occur in the implementation. - init(callback: String, completion: @escaping (Request, String) throws -> (EventLoopFuture)) throws + init(callback: String, completion: @escaping (Request, String) throws -> (EventLoopFuture)) throws /// Configures the `authenticate` and `callback` routes with the droplet. /// @@ -51,7 +51,7 @@ public protocol FederatedServiceRouter { /// - authURL: The URL for the route that will redirect the user to the OAuth provider. /// - authenticateCallback: Execute custom code within the authenticate closure before redirection. /// - Throws: N/A - func configureRoutes(withAuthURL authURL: String, authenticateCallback: ((Request) throws -> (EventLoopFuture))?, on router: RoutesBuilder) throws + func configureRoutes(withAuthURL authURL: String, authenticateCallback: ((Request) throws -> (EventLoopFuture))?, on router: any RoutesBuilder) throws /// Gets an access token from an OAuth provider. /// This method is the main body of the `callback` handler. @@ -61,7 +61,7 @@ public protocol FederatedServiceRouter { func fetchToken(from request: Request) throws -> EventLoopFuture /// Creates CallbackBody with authorization code - func callbackBody(with code: String) -> ResponseEncodable + func callbackBody(with code: String) -> any ResponseEncodable /// The route that the OAuth provider calls when the user has been authenticated. /// @@ -77,7 +77,7 @@ extension FederatedServiceRouter { public var errorKey: String { "error" } public var callbackHeaders: HTTPHeaders { [:] } - public func configureRoutes(withAuthURL authURL: String, authenticateCallback: ((Request) throws -> (EventLoopFuture))?, on router: RoutesBuilder) throws { + public func configureRoutes(withAuthURL authURL: String, authenticateCallback: ((Request) throws -> (EventLoopFuture))?, on router: any RoutesBuilder) throws { router.get(callbackURL.pathComponents, use: callback) router.get(authURL.pathComponents) { req -> EventLoopFuture in let redirect: Response = req.redirect(to: try self.authURL(req)) diff --git a/Sources/ImperialCore/ServiceRegister.swift b/Sources/ImperialCore/ServiceRegister.swift index 3de41f1d..b53cf40c 100644 --- a/Sources/ImperialCore/ServiceRegister.swift +++ b/Sources/ImperialCore/ServiceRegister.swift @@ -20,7 +20,7 @@ extension RoutesBuilder { authenticateCallback: ((Request) throws -> (EventLoopFuture))? = nil, callback: String, scope: [String] = [], - completion: @escaping (Request, String) throws -> EventLoopFuture + completion: @escaping (Request, String) throws -> EventLoopFuture ) throws where OAuthProvider: FederatedService { _ = try OAuthProvider( routes: self, diff --git a/Sources/ImperialCore/Services/FederatedService.swift b/Sources/ImperialCore/Services/FederatedService.swift index 12e4d7ef..388fe703 100644 --- a/Sources/ImperialCore/Services/FederatedService.swift +++ b/Sources/ImperialCore/Services/FederatedService.swift @@ -28,10 +28,10 @@ public class Service: FederatedService { public protocol FederatedService { /// The service's token model for getting the client ID and secret. - var tokens: FederatedServiceTokens { get } + var tokens: any FederatedServiceTokens { get } /// The service's router for handling the request for the access token. - var router: FederatedServiceRouter { get } + var router: any FederatedServiceRouter { get } /// Creates a service for getting an access token from an OAuth provider. /// @@ -42,5 +42,5 @@ public protocol FederatedService { /// - scope: The scopes to send to the provider to request access to. /// - completion: The completion handler that will fire at the end of the callback route. The access token is passed into the callback and the response that is returned will be returned from the callback route. This will usually be a redirect back to the app. /// - Throws: Any errors that occur in the implementation. - init(routes: RoutesBuilder, authenticate: String, authenticateCallback: ((Request) throws -> (EventLoopFuture))?, callback: String, scope: [String], completion: @escaping (Request, String) throws -> (EventLoopFuture)) throws + init(routes: any RoutesBuilder, authenticate: String, authenticateCallback: ((Request) throws -> (EventLoopFuture))?, callback: String, scope: [String], completion: @escaping (Request, String) throws -> (EventLoopFuture)) throws } diff --git a/Sources/ImperialDiscord/Discord.swift b/Sources/ImperialDiscord/Discord.swift index fa2db848..0c5e50ce 100644 --- a/Sources/ImperialDiscord/Discord.swift +++ b/Sources/ImperialDiscord/Discord.swift @@ -2,17 +2,17 @@ import Vapor public class Discord: FederatedService { - public var tokens: FederatedServiceTokens - public var router: FederatedServiceRouter + public var tokens: any FederatedServiceTokens + public var router: any FederatedServiceRouter @discardableResult public required init( - routes: RoutesBuilder, + routes: any RoutesBuilder, authenticate: String, authenticateCallback: ((Request) throws -> (EventLoopFuture))?, callback: String, scope: [String] = [], - completion: @escaping (Request, String) throws -> (EventLoopFuture) + completion: @escaping (Request, String) throws -> (EventLoopFuture) ) throws { self.router = try DiscordRouter(callback: callback, completion: completion) self.tokens = self.router.tokens diff --git a/Sources/ImperialDiscord/DiscordRouter.swift b/Sources/ImperialDiscord/DiscordRouter.swift index 6932a865..060c3a37 100644 --- a/Sources/ImperialDiscord/DiscordRouter.swift +++ b/Sources/ImperialDiscord/DiscordRouter.swift @@ -5,15 +5,15 @@ public class DiscordRouter: FederatedServiceRouter { public static var baseURL: String = "https://discord.com/" public static var callbackURL: String = "callback" - public let tokens: FederatedServiceTokens - public let callbackCompletion: (Request, String) throws -> (EventLoopFuture) + public let tokens: any FederatedServiceTokens + public let callbackCompletion: (Request, String) throws -> (EventLoopFuture) public var scope: [String] = [] public let callbackURL: String public let accessTokenURL: String = "\(DiscordRouter.baseURL.finished(with: "/"))api/oauth2/token" public let service: OAuthService = .discord public let callbackHeaders = HTTPHeaders([("Content-Type", "application/x-www-form-urlencoded")]) - public required init(callback: String, completion: @escaping (Request, String) throws -> (EventLoopFuture)) throws { + public required init(callback: String, completion: @escaping (Request, String) throws -> (EventLoopFuture)) throws { self.tokens = try DiscordAuth() self.callbackURL = callback self.callbackCompletion = completion @@ -39,7 +39,7 @@ public class DiscordRouter: FederatedServiceRouter { return url.absoluteString } - public func callbackBody(with code: String) -> ResponseEncodable { + public func callbackBody(with code: String) -> any ResponseEncodable { return DiscordCallbackBody( clientId: tokens.clientID, clientSecret: tokens.clientSecret, diff --git a/Sources/ImperialDropbox/Dropbox.swift b/Sources/ImperialDropbox/Dropbox.swift index 667f7d94..62152e59 100644 --- a/Sources/ImperialDropbox/Dropbox.swift +++ b/Sources/ImperialDropbox/Dropbox.swift @@ -2,17 +2,17 @@ import Vapor public class Dropbox: FederatedService { - public var tokens: FederatedServiceTokens - public var router: FederatedServiceRouter + public var tokens: any FederatedServiceTokens + public var router: any FederatedServiceRouter @discardableResult public required init( - routes: RoutesBuilder, + routes: any RoutesBuilder, authenticate: String, authenticateCallback: ((Request) throws -> (EventLoopFuture))?, callback: String, scope: [String] = [], - completion: @escaping (Request, String) throws -> (EventLoopFuture) + completion: @escaping (Request, String) throws -> (EventLoopFuture) ) throws { self.router = try DropboxRouter(callback: callback, completion: completion) self.tokens = self.router.tokens diff --git a/Sources/ImperialDropbox/DropboxRouter.swift b/Sources/ImperialDropbox/DropboxRouter.swift index 78d6ab88..0f83e259 100644 --- a/Sources/ImperialDropbox/DropboxRouter.swift +++ b/Sources/ImperialDropbox/DropboxRouter.swift @@ -2,8 +2,8 @@ import Vapor import Foundation public class DropboxRouter: FederatedServiceRouter { - public let tokens: FederatedServiceTokens - public let callbackCompletion: (Request, String) throws -> (EventLoopFuture) + public let tokens: any FederatedServiceTokens + public let callbackCompletion: (Request, String) throws -> (EventLoopFuture) public var scope: [String] = [] public let callbackURL: String public let accessTokenURL: String = "https://api.dropboxapi.com/oauth2/token" @@ -17,7 +17,7 @@ public class DropboxRouter: FederatedServiceRouter { public let service: OAuthService = .dropbox - public required init(callback: String, completion: @escaping (Request, String) throws -> (EventLoopFuture)) throws { + public required init(callback: String, completion: @escaping (Request, String) throws -> (EventLoopFuture)) throws { self.tokens = try DropboxAuth() self.callbackURL = callback self.callbackCompletion = completion @@ -42,7 +42,7 @@ public class DropboxRouter: FederatedServiceRouter { return url.absoluteString } - public func callbackBody(with code: String) -> ResponseEncodable { + public func callbackBody(with code: String) -> any ResponseEncodable { DropboxCallbackBody(code: code, redirectURI: callbackURL) } diff --git a/Sources/ImperialFacebook/Facebook.swift b/Sources/ImperialFacebook/Facebook.swift index 41fdde79..eb497aa8 100644 --- a/Sources/ImperialFacebook/Facebook.swift +++ b/Sources/ImperialFacebook/Facebook.swift @@ -2,17 +2,17 @@ import Vapor public class Facebook: FederatedService { - public var tokens: FederatedServiceTokens - public var router: FederatedServiceRouter + public var tokens: any FederatedServiceTokens + public var router: any FederatedServiceRouter @discardableResult public required init( - routes: RoutesBuilder, + routes: any RoutesBuilder, authenticate: String, authenticateCallback: ((Request) throws -> (EventLoopFuture))?, callback: String, scope: [String] = [], - completion: @escaping (Request, String) throws -> (EventLoopFuture) + completion: @escaping (Request, String) throws -> (EventLoopFuture) ) throws { self.router = try FacebookRouter(callback: callback, completion: completion) self.tokens = self.router.tokens diff --git a/Sources/ImperialFacebook/FacebookRouter.swift b/Sources/ImperialFacebook/FacebookRouter.swift index 7f404ab5..630d6879 100644 --- a/Sources/ImperialFacebook/FacebookRouter.swift +++ b/Sources/ImperialFacebook/FacebookRouter.swift @@ -3,8 +3,8 @@ import Foundation public class FacebookRouter: FederatedServiceRouter { - public let tokens: FederatedServiceTokens - public let callbackCompletion: (Request, String) throws -> (EventLoopFuture) + public let tokens: any FederatedServiceTokens + public let callbackCompletion: (Request, String) throws -> (EventLoopFuture) public var scope: [String] = [] public let callbackURL: String public var accessTokenURL: String = "https://graph.facebook.com/v3.2/oauth/access_token" @@ -29,13 +29,13 @@ public class FacebookRouter: FederatedServiceRouter { return url.absoluteString } - public required init(callback: String, completion: @escaping (Request, String) throws -> (EventLoopFuture)) throws { + public required init(callback: String, completion: @escaping (Request, String) throws -> (EventLoopFuture)) throws { self.tokens = try FacebookAuth() self.callbackURL = callback self.callbackCompletion = completion } - public func callbackBody(with code: String) -> ResponseEncodable { + public func callbackBody(with code: String) -> any ResponseEncodable { FacebookCallbackBody(code: code, clientId: tokens.clientID, clientSecret: tokens.clientSecret, diff --git a/Sources/ImperialGitHub/GitHub.swift b/Sources/ImperialGitHub/GitHub.swift index ee511d48..33c87a3b 100644 --- a/Sources/ImperialGitHub/GitHub.swift +++ b/Sources/ImperialGitHub/GitHub.swift @@ -2,17 +2,17 @@ import Vapor public class GitHub: FederatedService { - public var tokens: FederatedServiceTokens - public var router: FederatedServiceRouter + public var tokens: any FederatedServiceTokens + public var router: any FederatedServiceRouter @discardableResult public required init( - routes: RoutesBuilder, + routes: any RoutesBuilder, authenticate: String, authenticateCallback: ((Request) throws -> (EventLoopFuture))?, callback: String, scope: [String] = [], - completion: @escaping (Request, String) throws -> (EventLoopFuture) + completion: @escaping (Request, String) throws -> (EventLoopFuture) ) throws { self.router = try GitHubRouter(callback: callback, completion: completion) self.tokens = self.router.tokens diff --git a/Sources/ImperialGitHub/GitHubRouter.swift b/Sources/ImperialGitHub/GitHubRouter.swift index a1a53d02..731bb367 100644 --- a/Sources/ImperialGitHub/GitHubRouter.swift +++ b/Sources/ImperialGitHub/GitHubRouter.swift @@ -4,8 +4,8 @@ import Foundation public class GitHubRouter: FederatedServiceRouter { public static var baseURL: String = "https://github.com/" - public let tokens: FederatedServiceTokens - public let callbackCompletion: (Request, String) throws -> (EventLoopFuture) + public let tokens: any FederatedServiceTokens + public let callbackCompletion: (Request, String) throws -> (EventLoopFuture) public var scope: [String] = [] public let callbackURL: String public let accessTokenURL: String = "\(GitHubRouter.baseURL.finished(with: "/"))login/oauth/access_token" @@ -16,7 +16,7 @@ public class GitHubRouter: FederatedServiceRouter { return headers }() - public required init(callback: String, completion: @escaping (Request, String) throws -> (EventLoopFuture)) throws { + public required init(callback: String, completion: @escaping (Request, String) throws -> (EventLoopFuture)) throws { self.tokens = try GitHubAuth() self.callbackURL = callback self.callbackCompletion = completion @@ -40,7 +40,7 @@ public class GitHubRouter: FederatedServiceRouter { return url.absoluteString } - public func callbackBody(with code: String) -> ResponseEncodable { + public func callbackBody(with code: String) -> any ResponseEncodable { GitHubCallbackBody(clientId: tokens.clientID, clientSecret: tokens.clientSecret, code: code) diff --git a/Sources/ImperialGitlab/Gitlab.swift b/Sources/ImperialGitlab/Gitlab.swift index 62b88efb..9a6886f6 100644 --- a/Sources/ImperialGitlab/Gitlab.swift +++ b/Sources/ImperialGitlab/Gitlab.swift @@ -2,17 +2,17 @@ import Vapor public class Gitlab: FederatedService { - public var tokens: FederatedServiceTokens - public var router: FederatedServiceRouter + public var tokens: any FederatedServiceTokens + public var router: any FederatedServiceRouter @discardableResult public required init( - routes: RoutesBuilder, + routes: any RoutesBuilder, authenticate: String, authenticateCallback: ((Request) throws -> (EventLoopFuture))?, callback: String, scope: [String] = [], - completion: @escaping (Request, String) throws -> (EventLoopFuture) + completion: @escaping (Request, String) throws -> (EventLoopFuture) ) throws { self.router = try GitlabRouter(callback: callback, completion: completion) self.tokens = self.router.tokens diff --git a/Sources/ImperialGitlab/GitlabRouter.swift b/Sources/ImperialGitlab/GitlabRouter.swift index de35b259..7d417169 100644 --- a/Sources/ImperialGitlab/GitlabRouter.swift +++ b/Sources/ImperialGitlab/GitlabRouter.swift @@ -5,14 +5,14 @@ public class GitlabRouter: FederatedServiceRouter { public static var baseURL: String = "https://gitlab.com/" public static var callbackURL: String = "callback" - public let tokens: FederatedServiceTokens - public let callbackCompletion: (Request, String) throws -> (EventLoopFuture) + public let tokens: any FederatedServiceTokens + public let callbackCompletion: (Request, String) throws -> (EventLoopFuture) public var scope: [String] = [] public let callbackURL: String public let accessTokenURL: String = "\(GitlabRouter.baseURL.finished(with: "/"))oauth/token" public let service: OAuthService = .gitlab - public required init(callback: String, completion: @escaping (Request, String) throws -> (EventLoopFuture)) throws { + public required init(callback: String, completion: @escaping (Request, String) throws -> (EventLoopFuture)) throws { self.tokens = try GitlabAuth() self.callbackURL = callback self.callbackCompletion = completion @@ -37,7 +37,7 @@ public class GitlabRouter: FederatedServiceRouter { return url.absoluteString } - public func callbackBody(with code: String) -> ResponseEncodable { + public func callbackBody(with code: String) -> any ResponseEncodable { GitlabCallbackBody(clientId: tokens.clientID, clientSecret: tokens.clientSecret, code: code, diff --git a/Sources/ImperialGoogle/JWT/GoogleJWT.swift b/Sources/ImperialGoogle/JWT/GoogleJWT.swift index f610d293..41a472dd 100644 --- a/Sources/ImperialGoogle/JWT/GoogleJWT.swift +++ b/Sources/ImperialGoogle/JWT/GoogleJWT.swift @@ -1,17 +1,17 @@ import Vapor public class GoogleJWT: FederatedService { - public var tokens: FederatedServiceTokens - public var router: FederatedServiceRouter + public var tokens: any FederatedServiceTokens + public var router: any FederatedServiceRouter @discardableResult public required init( - routes: RoutesBuilder, + routes: any RoutesBuilder, authenticate: String, authenticateCallback: ((Request) throws -> (EventLoopFuture))?, callback: String, scope: [String] = [], - completion: @escaping (Request, String) throws -> (EventLoopFuture) + completion: @escaping (Request, String) throws -> (EventLoopFuture) ) throws { self.router = try GoogleJWTRouter(callback: callback, completion: completion) self.tokens = self.router.tokens diff --git a/Sources/ImperialGoogle/JWT/GoogleJWTRouter.swift b/Sources/ImperialGoogle/JWT/GoogleJWTRouter.swift index 3664067a..f002569f 100644 --- a/Sources/ImperialGoogle/JWT/GoogleJWTRouter.swift +++ b/Sources/ImperialGoogle/JWT/GoogleJWTRouter.swift @@ -5,8 +5,8 @@ import JWTKit public final class GoogleJWTRouter: FederatedServiceRouter { - public var tokens: FederatedServiceTokens - public var callbackCompletion: (Request, String) throws -> (EventLoopFuture) + public var tokens: any FederatedServiceTokens + public var callbackCompletion: (Request, String) throws -> (EventLoopFuture) public var scope: [String] = [] public var callbackURL: String public var accessTokenURL: String = "https://www.googleapis.com/oauth2/v4/token" @@ -18,7 +18,7 @@ public final class GoogleJWTRouter: FederatedServiceRouter { return headers }() - public init(callback: String, completion: @escaping (Request, String) throws -> (EventLoopFuture)) throws { + public init(callback: String, completion: @escaping (Request, String) throws -> (EventLoopFuture)) throws { self.tokens = try GoogleJWTAuth() self.callbackURL = callback self.authURL = callback @@ -29,7 +29,7 @@ public final class GoogleJWTRouter: FederatedServiceRouter { return authURL } - public func callbackBody(with code: String) -> ResponseEncodable { + public func callbackBody(with code: String) -> any ResponseEncodable { return "grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion=\(code)" } diff --git a/Sources/ImperialGoogle/Standard/Google.swift b/Sources/ImperialGoogle/Standard/Google.swift index 1589f3a5..a59101e1 100644 --- a/Sources/ImperialGoogle/Standard/Google.swift +++ b/Sources/ImperialGoogle/Standard/Google.swift @@ -2,17 +2,17 @@ import Vapor public class Google: FederatedService { - public var tokens: FederatedServiceTokens - public var router: FederatedServiceRouter + public var tokens: any FederatedServiceTokens + public var router: any FederatedServiceRouter @discardableResult public required init( - routes: RoutesBuilder, + routes: any RoutesBuilder, authenticate: String, authenticateCallback: ((Request) throws -> (EventLoopFuture))?, callback: String, scope: [String] = [], - completion: @escaping (Request, String) throws -> (EventLoopFuture) + completion: @escaping (Request, String) throws -> (EventLoopFuture) ) throws { self.router = try GoogleRouter(callback: callback, completion: completion) self.tokens = self.router.tokens diff --git a/Sources/ImperialGoogle/Standard/GoogleRouter.swift b/Sources/ImperialGoogle/Standard/GoogleRouter.swift index 56b70519..a08040e3 100644 --- a/Sources/ImperialGoogle/Standard/GoogleRouter.swift +++ b/Sources/ImperialGoogle/Standard/GoogleRouter.swift @@ -2,8 +2,8 @@ import Vapor import Foundation public class GoogleRouter: FederatedServiceRouter { - public let tokens: FederatedServiceTokens - public let callbackCompletion: (Request, String) throws -> (EventLoopFuture) + public let tokens: any FederatedServiceTokens + public let callbackCompletion: (Request, String) throws -> (EventLoopFuture) public var scope: [String] = [] public let callbackURL: String public let accessTokenURL: String = "https://www.googleapis.com/oauth2/v4/token" @@ -14,7 +14,7 @@ public class GoogleRouter: FederatedServiceRouter { return headers }() - public required init(callback: String, completion: @escaping (Request, String) throws -> (EventLoopFuture)) throws { + public required init(callback: String, completion: @escaping (Request, String) throws -> (EventLoopFuture)) throws { self.tokens = try GoogleAuth() self.callbackURL = callback self.callbackCompletion = completion @@ -39,7 +39,7 @@ public class GoogleRouter: FederatedServiceRouter { return url.absoluteString } - public func callbackBody(with code: String) -> ResponseEncodable { + public func callbackBody(with code: String) -> any ResponseEncodable { GoogleCallbackBody(code: code, clientId: tokens.clientID, clientSecret: tokens.clientSecret, diff --git a/Sources/ImperialKeycloak/Keycloak.swift b/Sources/ImperialKeycloak/Keycloak.swift index a5432fae..6001df9a 100644 --- a/Sources/ImperialKeycloak/Keycloak.swift +++ b/Sources/ImperialKeycloak/Keycloak.swift @@ -2,17 +2,17 @@ import Vapor public class Keycloak: FederatedService { - public var tokens: FederatedServiceTokens - public var router: FederatedServiceRouter + public var tokens: any FederatedServiceTokens + public var router: any FederatedServiceRouter @discardableResult public required init( - routes: RoutesBuilder, + routes: any RoutesBuilder, authenticate: String, authenticateCallback: ((Request) throws -> (EventLoopFuture))?, callback: String, scope: [String] = [], - completion: @escaping (Request, String) throws -> (EventLoopFuture) + completion: @escaping (Request, String) throws -> (EventLoopFuture) ) throws { self.router = try KeycloakRouter(callback: callback, completion: completion) self.tokens = self.router.tokens diff --git a/Sources/ImperialKeycloak/KeycloakRouter.swift b/Sources/ImperialKeycloak/KeycloakRouter.swift index e1fc8c5e..41a226b5 100644 --- a/Sources/ImperialKeycloak/KeycloakRouter.swift +++ b/Sources/ImperialKeycloak/KeycloakRouter.swift @@ -2,15 +2,15 @@ import Vapor import Foundation public class KeycloakRouter: FederatedServiceRouter { - public let tokens: FederatedServiceTokens + public let tokens: any FederatedServiceTokens public let keycloakTokens: KeycloakAuth - public let callbackCompletion: (Request, String) throws -> (EventLoopFuture) + public let callbackCompletion: (Request, String) throws -> (EventLoopFuture) public var scope: [String] = [] public let callbackURL: String public let accessTokenURL: String public let service: OAuthService = .keycloak - public required init(callback: String, completion: @escaping (Request, String) throws -> (EventLoopFuture)) throws { + public required init(callback: String, completion: @escaping (Request, String) throws -> (EventLoopFuture)) throws { self.tokens = try KeycloakAuth() self.keycloakTokens = self.tokens as! KeycloakAuth self.accessTokenURL = keycloakTokens.accessTokenURL @@ -26,7 +26,7 @@ public class KeycloakRouter: FederatedServiceRouter { "response_type=code" } - public func callbackBody(with code: String) -> ResponseEncodable { + public func callbackBody(with code: String) -> any ResponseEncodable { KeycloakCallbackBody(code: code, clientId: tokens.clientID, clientSecret: tokens.clientSecret, diff --git a/Sources/ImperialMicrosoft/Microsoft.swift b/Sources/ImperialMicrosoft/Microsoft.swift index 5621922e..caf8fb5c 100644 --- a/Sources/ImperialMicrosoft/Microsoft.swift +++ b/Sources/ImperialMicrosoft/Microsoft.swift @@ -2,17 +2,17 @@ import Vapor public class Microsoft: FederatedService { - public var tokens: FederatedServiceTokens - public var router: FederatedServiceRouter + public var tokens: any FederatedServiceTokens + public var router: any FederatedServiceRouter @discardableResult public required init( - routes: RoutesBuilder, + routes: any RoutesBuilder, authenticate: String, authenticateCallback: ((Request) throws -> (EventLoopFuture))?, callback: String, scope: [String] = [], - completion: @escaping (Request, String)throws -> (EventLoopFuture) + completion: @escaping (Request, String)throws -> (EventLoopFuture) ) throws { self.router = try MicrosoftRouter(callback: callback, completion: completion) self.tokens = self.router.tokens diff --git a/Sources/ImperialMicrosoft/MicrosoftRouter.swift b/Sources/ImperialMicrosoft/MicrosoftRouter.swift index 1d05d6a9..e7c96462 100644 --- a/Sources/ImperialMicrosoft/MicrosoftRouter.swift +++ b/Sources/ImperialMicrosoft/MicrosoftRouter.swift @@ -5,8 +5,8 @@ public class MicrosoftRouter: FederatedServiceRouter { public static var tenantIDEnvKey: String = "MICROSOFT_TENANT_ID" - public let tokens: FederatedServiceTokens - public let callbackCompletion: (Request, String)throws -> (EventLoopFuture) + public let tokens: any FederatedServiceTokens + public let callbackCompletion: (Request, String)throws -> (EventLoopFuture) public var scope: [String] = [] public let callbackURL: String public var tenantID: String { Environment.get(MicrosoftRouter.tenantIDEnvKey) ?? "common" } @@ -16,7 +16,7 @@ public class MicrosoftRouter: FederatedServiceRouter { public required init( callback: String, - completion: @escaping (Request, String) throws -> (EventLoopFuture) + completion: @escaping (Request, String) throws -> (EventLoopFuture) ) throws { self.tokens = try MicrosoftAuth() self.callbackURL = callback @@ -44,7 +44,7 @@ public class MicrosoftRouter: FederatedServiceRouter { return url.absoluteString } - public func callbackBody(with code: String) -> ResponseEncodable { + public func callbackBody(with code: String) -> any ResponseEncodable { MicrosoftCallbackBody(code: code, clientId: tokens.clientID, clientSecret: tokens.clientSecret, diff --git a/Sources/ImperialShopify/Shopify.swift b/Sources/ImperialShopify/Shopify.swift index e3774709..59cb7e4c 100644 --- a/Sources/ImperialShopify/Shopify.swift +++ b/Sources/ImperialShopify/Shopify.swift @@ -3,18 +3,18 @@ import Vapor public final class Shopify: FederatedService { - public var tokens: FederatedServiceTokens { self.router.tokens } - public var router: FederatedServiceRouter { self.shopifyRouter } + public var tokens: any FederatedServiceTokens { self.router.tokens } + public var router: any FederatedServiceRouter { self.shopifyRouter } public var shopifyRouter: ShopifyRouter public init( - routes: RoutesBuilder, + routes: any RoutesBuilder, authenticate: String, authenticateCallback: ((Request) throws -> (EventLoopFuture))?, callback: String, scope: [String], - completion: @escaping (Request, String) throws -> (EventLoopFuture) + completion: @escaping (Request, String) throws -> (EventLoopFuture) ) throws { self.shopifyRouter = try ShopifyRouter(callback: callback, completion: completion) self.shopifyRouter.scope = scope diff --git a/Sources/ImperialShopify/ShopifyRouter.swift b/Sources/ImperialShopify/ShopifyRouter.swift index 5e7f4a3e..a1be9df6 100644 --- a/Sources/ImperialShopify/ShopifyRouter.swift +++ b/Sources/ImperialShopify/ShopifyRouter.swift @@ -2,14 +2,14 @@ import Vapor public class ShopifyRouter: FederatedServiceRouter { - public let tokens: FederatedServiceTokens - public let callbackCompletion: (Request, String) throws -> (EventLoopFuture) + public let tokens: any FederatedServiceTokens + public let callbackCompletion: (Request, String) throws -> (EventLoopFuture) public var scope: [String] = [] public let callbackURL: String public var accessTokenURL: String = "" public let service: OAuthService = .shopify - required public init(callback: String, completion: @escaping (Request, String) throws -> (EventLoopFuture)) throws { + required public init(callback: String, completion: @escaping (Request, String) throws -> (EventLoopFuture)) throws { self.tokens = try ShopifyAuth() self.callbackURL = callback self.callbackCompletion = completion @@ -25,7 +25,7 @@ public class ShopifyRouter: FederatedServiceRouter { return try authURLFrom(shop, nonce: nonce).absoluteString } - public func callbackBody(with code: String) -> ResponseEncodable { + public func callbackBody(with code: String) -> any ResponseEncodable { ShopifyCallbackBody(code: code, clientId: tokens.clientID, clientSecret: tokens.clientSecret) diff --git a/Tests/ImperialTests/ImperialTests.swift b/Tests/ImperialTests/ImperialTests.swift index 65eb1254..6f3d3194 100644 --- a/Tests/ImperialTests/ImperialTests.swift +++ b/Tests/ImperialTests/ImperialTests.swift @@ -3,8 +3,4 @@ import XCTest class ImperialTests: XCTestCase { func testExists() {} - - static var allTests: [(String, (ImperialTests) -> () -> ())] = [ - ("testExists", testExists) - ] } diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift deleted file mode 100644 index b40e7908..00000000 --- a/Tests/LinuxMain.swift +++ /dev/null @@ -1,6 +0,0 @@ -import XCTest -@testable import ImperialTests - -XCTMain([ - testCase(ImperialTests.allTests), -]) From 0b906255894118cf8950068e007f9f4d0a34fc1a Mon Sep 17 00:00:00 2001 From: Francesco Paolo Severino Date: Sun, 10 Nov 2024 16:35:44 +0100 Subject: [PATCH 02/37] Update ImperialCore --- .../Helpers/Request+Imperial.swift | 12 ++-- .../Middleware/ImperialMiddleware.swift | 15 +++-- .../Model/FederatedCreatable.swift | 8 +-- .../Routing/FederatedServiceRouter.swift | 55 ++++++++----------- Sources/ImperialCore/ServiceRegister.swift | 9 ++- .../Services/FederatedService.swift | 2 +- 6 files changed, 43 insertions(+), 58 deletions(-) diff --git a/Sources/ImperialCore/Helpers/Request+Imperial.swift b/Sources/ImperialCore/Helpers/Request+Imperial.swift index d34a1c0c..fb2a07a6 100644 --- a/Sources/ImperialCore/Helpers/Request+Imperial.swift +++ b/Sources/ImperialCore/Helpers/Request+Imperial.swift @@ -10,18 +10,16 @@ extension Request { /// - service: The service to get the data from. /// - Returns: An instance of the type passed in. /// - Throws: Errors from trying to get the access token from the request. - func create(_ model: Model.Type, with service: OAuthService, on req: Request) throws -> EventLoopFuture { + func create(_ model: Model.Type, with service: OAuthService, on req: Request) async throws -> Model { let url = try service[model.serviceKey].value(or: ServiceError.noServiceEndpoint(model.serviceKey)) let token = try service.tokenPrefix + req .accessToken() - return req.client.get(URI(string: url), headers: ["Authorization": token]).flatMap { response in - return try! model.create(from: response).flatMapThrowing { instance in - try self.session.set("imperial-\(model)", to: instance) - return instance - } - } + let response = try await req.client.get(URI(string: url), headers: ["Authorization": token]) + let instance = try await model.init(from: response) + try self.session.set("imperial-\(model)", to: instance) + return instance } /// Gets an instance of a `FederatedCreatable` type that is stored in the request. diff --git a/Sources/ImperialCore/Middleware/ImperialMiddleware.swift b/Sources/ImperialCore/Middleware/ImperialMiddleware.swift index d03f9ef6..8cbb0c68 100644 --- a/Sources/ImperialCore/Middleware/ImperialMiddleware.swift +++ b/Sources/ImperialCore/Middleware/ImperialMiddleware.swift @@ -1,7 +1,7 @@ import Vapor /// Protects routes from users without an access token. -public class ImperialMiddleware: Middleware { +public struct ImperialMiddleware: AsyncMiddleware { /// The path to redirect the user to if they are not authenticated. let redirectPath: String? @@ -15,18 +15,17 @@ public class ImperialMiddleware: Middleware { /// Checks that the request contains an access token. If it does, let the request through. If not, redirect the user to the `redirectPath`. /// If the `redirectPath` is `nil`, then throw the error from getting the access token (Abort.unauthorized). - public func respond(to request: Request, chainingTo next: any Responder) -> EventLoopFuture { + public func respond(to request: Request, chainingTo next: any AsyncResponder) async throws -> Response { do { _ = try request.accessToken() - return next.respond(to: request) + return try await next.respond(to: request) } catch let error as Abort where error.status == .unauthorized { - guard let redirectPath = redirectPath else { - return request.eventLoop.makeFailedFuture(error) + guard let redirectPath else { + throw error } - let redirect: Response = request.redirect(to: redirectPath) - return request.eventLoop.makeSucceededFuture(redirect) + return request.redirect(to: redirectPath) } catch let error { - return request.eventLoop.makeFailedFuture(error) + throw error } } } diff --git a/Sources/ImperialCore/Model/FederatedCreatable.swift b/Sources/ImperialCore/Model/FederatedCreatable.swift index 39ec4b12..ee233a85 100644 --- a/Sources/ImperialCore/Model/FederatedCreatable.swift +++ b/Sources/ImperialCore/Model/FederatedCreatable.swift @@ -12,13 +12,13 @@ public protocol FederatedCreatable: Codable { /// - Parameter response: The JSON in the response from the `dataUri`. /// - Returns: An instence of the type that conforms to this protocol. /// - Throws: Any errors that could be thrown inside the method. - static func create(from response: ClientResponse) throws -> EventLoopFuture + init(from response: ClientResponse) async throws } extension FederatedCreatable { - - static func create(from response: ClientResponse) throws -> Self { - return try response.content.decode(Self.self) + + init(from response: ClientResponse) async throws { + self = try response.content.decode(Self.self) } } diff --git a/Sources/ImperialCore/Routing/FederatedServiceRouter.swift b/Sources/ImperialCore/Routing/FederatedServiceRouter.swift index 415063bc..c56bf1bf 100644 --- a/Sources/ImperialCore/Routing/FederatedServiceRouter.swift +++ b/Sources/ImperialCore/Routing/FederatedServiceRouter.swift @@ -10,7 +10,7 @@ public protocol FederatedServiceRouter { /// The callback that is fired after the access token is fetched from the OAuth provider. /// The response that is returned from this callback is also returned from the callback route. - var callbackCompletion: (Request, String) throws -> (EventLoopFuture) { get } + var callbackCompletion: (Request, String) async throws -> any AsyncResponseEncodable { get } /// The scopes to get permission for when getting the access token. /// Usage of this property varies by provider. @@ -43,7 +43,7 @@ public protocol FederatedServiceRouter { /// - callback: The callback URL that the OAuth provider will redirect to after authenticating the user. /// - completion: The completion handler that will be fired at the end of the `callback` route. The access token is passed into it. /// - Throws: Any errors that could occur in the implementation. - init(callback: String, completion: @escaping (Request, String) throws -> (EventLoopFuture)) throws + init(callback: String, completion: @escaping (Request, String) async throws -> any AsyncResponseEncodable) throws /// Configures the `authenticate` and `callback` routes with the droplet. /// @@ -51,24 +51,24 @@ public protocol FederatedServiceRouter { /// - authURL: The URL for the route that will redirect the user to the OAuth provider. /// - authenticateCallback: Execute custom code within the authenticate closure before redirection. /// - Throws: N/A - func configureRoutes(withAuthURL authURL: String, authenticateCallback: ((Request) throws -> (EventLoopFuture))?, on router: any RoutesBuilder) throws + func configureRoutes(withAuthURL authURL: String, authenticateCallback: ((Request) async throws -> Void)?, on router: any RoutesBuilder) throws /// Gets an access token from an OAuth provider. /// This method is the main body of the `callback` handler. /// /// - Parameters: request: The request for the route /// this method is called in. - func fetchToken(from request: Request) throws -> EventLoopFuture + func fetchToken(from request: Request) async throws -> String /// Creates CallbackBody with authorization code - func callbackBody(with code: String) -> any ResponseEncodable + func callbackBody(with code: String) -> any AsyncResponseEncodable /// The route that the OAuth provider calls when the user has been authenticated. /// /// - Parameter request: The request from the OAuth provider. /// - Returns: A response that should redirect the user back to the app. /// - Throws: An errors that occur in the implementation code. - func callback(_ request: Request) throws -> EventLoopFuture + func callback(_ request: Request) async throws -> Response } extension FederatedServiceRouter { @@ -77,20 +77,19 @@ extension FederatedServiceRouter { public var errorKey: String { "error" } public var callbackHeaders: HTTPHeaders { [:] } - public func configureRoutes(withAuthURL authURL: String, authenticateCallback: ((Request) throws -> (EventLoopFuture))?, on router: any RoutesBuilder) throws { + public func configureRoutes(withAuthURL authURL: String, authenticateCallback: ((Request) async throws -> Void)?, on router: any RoutesBuilder) throws { router.get(callbackURL.pathComponents, use: callback) - router.get(authURL.pathComponents) { req -> EventLoopFuture in + router.get(authURL.pathComponents) { req async throws -> Response in let redirect: Response = req.redirect(to: try self.authURL(req)) - guard let authenticateCallback = authenticateCallback else { - return req.eventLoop.makeSucceededFuture(redirect) - } - return try authenticateCallback(req).map { + guard let authenticateCallback else { return redirect } + try await authenticateCallback(req) + return redirect } } - public func fetchToken(from request: Request) throws -> EventLoopFuture { + public func fetchToken(from request: Request) async throws -> String { let code: String if let queryCode: String = try request.query.get(at: codeKey) { code = queryCode @@ -103,28 +102,18 @@ extension FederatedServiceRouter { let body = callbackBody(with: code) let url = URI(string: accessTokenURL) - return body.encodeResponse(for: request) - .map { $0.body.buffer } - .flatMap { buffer in - return request.client.post(url, headers: self.callbackHeaders) { $0.body = buffer } - }.flatMapThrowing { response in - return try response.content.get(String.self, at: ["access_token"]) - } + let buffer = try await body.encodeResponse(for: request).body.buffer + let response = try await request.client.post(url, headers: self.callbackHeaders) { $0.body = buffer } + return try response.content.get(String.self, at: ["access_token"]) } - public func callback(_ request: Request) throws -> EventLoopFuture { - return try self.fetchToken(from: request).flatMap { accessToken in - let session = request.session - do { - try session.setAccessToken(accessToken) - try session.set("access_token_service", to: self.service) - return try self.callbackCompletion(request, accessToken).flatMap { response in - return response.encodeResponse(for: request) - } - } catch { - return request.eventLoop.makeFailedFuture(error) - } - } + public func callback(_ request: Request) async throws -> Response { + let accessToken = try await self.fetchToken(from: request) + let session = request.session + try session.setAccessToken(accessToken) + try session.set("access_token_service", to: self.service) + let response = try await self.callbackCompletion(request, accessToken) + return try await response.encodeResponse(for: request) } } diff --git a/Sources/ImperialCore/ServiceRegister.swift b/Sources/ImperialCore/ServiceRegister.swift index b53cf40c..273a4e1c 100644 --- a/Sources/ImperialCore/ServiceRegister.swift +++ b/Sources/ImperialCore/ServiceRegister.swift @@ -17,10 +17,10 @@ extension RoutesBuilder { public func oAuth( from provider: OAuthProvider.Type, authenticate authUrl: String, - authenticateCallback: ((Request) throws -> (EventLoopFuture))? = nil, + authenticateCallback: ((Request) async throws -> Void)? = nil, callback: String, scope: [String] = [], - completion: @escaping (Request, String) throws -> EventLoopFuture + completion: @escaping (Request, String) async throws -> any AsyncResponseEncodable ) throws where OAuthProvider: FederatedService { _ = try OAuthProvider( routes: self, @@ -46,14 +46,13 @@ extension RoutesBuilder { public func oAuth( from provider: OAuthProvider.Type, authenticate authUrl: String, - authenticateCallback: ((Request) throws -> (EventLoopFuture))? = nil, + authenticateCallback: ((Request) async throws -> Void)? = nil, callback: String, scope: [String] = [], redirect redirectURL: String ) throws where OAuthProvider: FederatedService { try self.oAuth(from: OAuthProvider.self, authenticate: authUrl, authenticateCallback: authenticateCallback, callback: callback, scope: scope) { (request, _) in - let redirect: Response = request.redirect(to: redirectURL) - return request.eventLoop.makeSucceededFuture(redirect) + return request.redirect(to: redirectURL) } } } diff --git a/Sources/ImperialCore/Services/FederatedService.swift b/Sources/ImperialCore/Services/FederatedService.swift index 388fe703..5acb8406 100644 --- a/Sources/ImperialCore/Services/FederatedService.swift +++ b/Sources/ImperialCore/Services/FederatedService.swift @@ -42,5 +42,5 @@ public protocol FederatedService { /// - scope: The scopes to send to the provider to request access to. /// - completion: The completion handler that will fire at the end of the callback route. The access token is passed into the callback and the response that is returned will be returned from the callback route. This will usually be a redirect back to the app. /// - Throws: Any errors that occur in the implementation. - init(routes: any RoutesBuilder, authenticate: String, authenticateCallback: ((Request) throws -> (EventLoopFuture))?, callback: String, scope: [String], completion: @escaping (Request, String) throws -> (EventLoopFuture)) throws + init(routes: any RoutesBuilder, authenticate: String, authenticateCallback: ((Request) async throws -> Void)?, callback: String, scope: [String], completion: @escaping (Request, String) async throws -> any AsyncResponseEncodable) throws } From 512cb8078a638d5c9834fc9184bb838d9a224a69 Mon Sep 17 00:00:00 2001 From: Francesco Paolo Severino Date: Sun, 10 Nov 2024 17:15:18 +0100 Subject: [PATCH 03/37] Update Services --- Sources/ImperialAuth0/Auth0.swift | 4 +- Sources/ImperialAuth0/Auth0Router.swift | 6 +-- Sources/ImperialDiscord/Discord.swift | 4 +- Sources/ImperialDiscord/DiscordRouter.swift | 6 +-- Sources/ImperialDropbox/Dropbox.swift | 4 +- Sources/ImperialDropbox/DropboxRouter.swift | 6 +-- Sources/ImperialFacebook/Facebook.swift | 4 +- Sources/ImperialFacebook/FacebookRouter.swift | 6 +-- Sources/ImperialGitHub/GitHub.swift | 4 +- Sources/ImperialGitHub/GitHubRouter.swift | 6 +-- Sources/ImperialGitlab/Gitlab.swift | 4 +- Sources/ImperialGitlab/GitlabRouter.swift | 6 +-- Sources/ImperialGoogle/JWT/GoogleJWT.swift | 4 +- .../ImperialGoogle/JWT/GoogleJWTRouter.swift | 23 ++++------ Sources/ImperialGoogle/Standard/Google.swift | 4 +- .../Standard/GoogleRouter.swift | 6 +-- Sources/ImperialKeycloak/Keycloak.swift | 4 +- Sources/ImperialKeycloak/KeycloakRouter.swift | 6 +-- Sources/ImperialMicrosoft/Microsoft.swift | 4 +- .../ImperialMicrosoft/MicrosoftRouter.swift | 6 +-- Sources/ImperialShopify/Shopify.swift | 4 +- Sources/ImperialShopify/ShopifyRouter.swift | 43 +++++++------------ 22 files changed, 74 insertions(+), 90 deletions(-) diff --git a/Sources/ImperialAuth0/Auth0.swift b/Sources/ImperialAuth0/Auth0.swift index 29247edb..82081e30 100644 --- a/Sources/ImperialAuth0/Auth0.swift +++ b/Sources/ImperialAuth0/Auth0.swift @@ -9,10 +9,10 @@ public class Auth0: FederatedService { public required init( routes: any RoutesBuilder, authenticate: String, - authenticateCallback: ((Request) throws -> (EventLoopFuture))?, + authenticateCallback: ((Request) async throws -> Void)?, callback: String, scope: [String] = [], - completion: @escaping (Request, String) throws -> (EventLoopFuture) + completion: @escaping (Request, String) async throws -> any AsyncResponseEncodable ) throws { self.router = try Auth0Router(callback: callback, completion: completion) self.tokens = self.router.tokens diff --git a/Sources/ImperialAuth0/Auth0Router.swift b/Sources/ImperialAuth0/Auth0Router.swift index 75afa0e3..93be8fbc 100644 --- a/Sources/ImperialAuth0/Auth0Router.swift +++ b/Sources/ImperialAuth0/Auth0Router.swift @@ -5,7 +5,7 @@ public class Auth0Router: FederatedServiceRouter { public let baseURL: String public let tokens: any FederatedServiceTokens - public let callbackCompletion: (Request, String) throws -> (EventLoopFuture) + public let callbackCompletion: (Request, String) async throws -> any AsyncResponseEncodable public var scope: [String] = [ ] public var requiredScopes = [ "openid" ] public let callbackURL: String @@ -17,7 +17,7 @@ public class Auth0Router: FederatedServiceRouter { return self.baseURL.finished(with: "/") + path } - public required init(callback: String, completion: @escaping (Request, String) throws -> (EventLoopFuture)) throws { + public required init(callback: String, completion: @escaping (Request, String) async throws -> any AsyncResponseEncodable) throws { let auth = try Auth0Auth() self.tokens = auth self.baseURL = "https://\(auth.domain)" @@ -45,7 +45,7 @@ public class Auth0Router: FederatedServiceRouter { return rtn } - public func callbackBody(with code: String) -> any ResponseEncodable { + public func callbackBody(with code: String) -> any AsyncResponseEncodable { Auth0CallbackBody(clientId: self.tokens.clientID, clientSecret: self.tokens.clientSecret, code: code, diff --git a/Sources/ImperialDiscord/Discord.swift b/Sources/ImperialDiscord/Discord.swift index 0c5e50ce..b503e93c 100644 --- a/Sources/ImperialDiscord/Discord.swift +++ b/Sources/ImperialDiscord/Discord.swift @@ -9,10 +9,10 @@ public class Discord: FederatedService { public required init( routes: any RoutesBuilder, authenticate: String, - authenticateCallback: ((Request) throws -> (EventLoopFuture))?, + authenticateCallback: ((Request) async throws -> Void)?, callback: String, scope: [String] = [], - completion: @escaping (Request, String) throws -> (EventLoopFuture) + completion: @escaping (Request, String) async throws -> any AsyncResponseEncodable ) throws { self.router = try DiscordRouter(callback: callback, completion: completion) self.tokens = self.router.tokens diff --git a/Sources/ImperialDiscord/DiscordRouter.swift b/Sources/ImperialDiscord/DiscordRouter.swift index 060c3a37..d728807a 100644 --- a/Sources/ImperialDiscord/DiscordRouter.swift +++ b/Sources/ImperialDiscord/DiscordRouter.swift @@ -6,14 +6,14 @@ public class DiscordRouter: FederatedServiceRouter { public static var baseURL: String = "https://discord.com/" public static var callbackURL: String = "callback" public let tokens: any FederatedServiceTokens - public let callbackCompletion: (Request, String) throws -> (EventLoopFuture) + public let callbackCompletion: (Request, String) async throws -> any AsyncResponseEncodable public var scope: [String] = [] public let callbackURL: String public let accessTokenURL: String = "\(DiscordRouter.baseURL.finished(with: "/"))api/oauth2/token" public let service: OAuthService = .discord public let callbackHeaders = HTTPHeaders([("Content-Type", "application/x-www-form-urlencoded")]) - public required init(callback: String, completion: @escaping (Request, String) throws -> (EventLoopFuture)) throws { + public required init(callback: String, completion: @escaping (Request, String) async throws -> any AsyncResponseEncodable) throws { self.tokens = try DiscordAuth() self.callbackURL = callback self.callbackCompletion = completion @@ -39,7 +39,7 @@ public class DiscordRouter: FederatedServiceRouter { return url.absoluteString } - public func callbackBody(with code: String) -> any ResponseEncodable { + public func callbackBody(with code: String) -> any AsyncResponseEncodable { return DiscordCallbackBody( clientId: tokens.clientID, clientSecret: tokens.clientSecret, diff --git a/Sources/ImperialDropbox/Dropbox.swift b/Sources/ImperialDropbox/Dropbox.swift index 62152e59..1806edaf 100644 --- a/Sources/ImperialDropbox/Dropbox.swift +++ b/Sources/ImperialDropbox/Dropbox.swift @@ -9,10 +9,10 @@ public class Dropbox: FederatedService { public required init( routes: any RoutesBuilder, authenticate: String, - authenticateCallback: ((Request) throws -> (EventLoopFuture))?, + authenticateCallback: ((Request) async throws -> Void)?, callback: String, scope: [String] = [], - completion: @escaping (Request, String) throws -> (EventLoopFuture) + completion: @escaping (Request, String) async throws -> any AsyncResponseEncodable ) throws { self.router = try DropboxRouter(callback: callback, completion: completion) self.tokens = self.router.tokens diff --git a/Sources/ImperialDropbox/DropboxRouter.swift b/Sources/ImperialDropbox/DropboxRouter.swift index 0f83e259..e1de7e36 100644 --- a/Sources/ImperialDropbox/DropboxRouter.swift +++ b/Sources/ImperialDropbox/DropboxRouter.swift @@ -3,7 +3,7 @@ import Foundation public class DropboxRouter: FederatedServiceRouter { public let tokens: any FederatedServiceTokens - public let callbackCompletion: (Request, String) throws -> (EventLoopFuture) + public let callbackCompletion: (Request, String) async throws -> any AsyncResponseEncodable public var scope: [String] = [] public let callbackURL: String public let accessTokenURL: String = "https://api.dropboxapi.com/oauth2/token" @@ -17,7 +17,7 @@ public class DropboxRouter: FederatedServiceRouter { public let service: OAuthService = .dropbox - public required init(callback: String, completion: @escaping (Request, String) throws -> (EventLoopFuture)) throws { + public required init(callback: String, completion: @escaping (Request, String) async throws -> any AsyncResponseEncodable) throws { self.tokens = try DropboxAuth() self.callbackURL = callback self.callbackCompletion = completion @@ -42,7 +42,7 @@ public class DropboxRouter: FederatedServiceRouter { return url.absoluteString } - public func callbackBody(with code: String) -> any ResponseEncodable { + public func callbackBody(with code: String) -> any AsyncResponseEncodable { DropboxCallbackBody(code: code, redirectURI: callbackURL) } diff --git a/Sources/ImperialFacebook/Facebook.swift b/Sources/ImperialFacebook/Facebook.swift index eb497aa8..750ed065 100644 --- a/Sources/ImperialFacebook/Facebook.swift +++ b/Sources/ImperialFacebook/Facebook.swift @@ -9,10 +9,10 @@ public class Facebook: FederatedService { public required init( routes: any RoutesBuilder, authenticate: String, - authenticateCallback: ((Request) throws -> (EventLoopFuture))?, + authenticateCallback: ((Request) async throws -> Void)?, callback: String, scope: [String] = [], - completion: @escaping (Request, String) throws -> (EventLoopFuture) + completion: @escaping (Request, String) async throws -> any AsyncResponseEncodable ) throws { self.router = try FacebookRouter(callback: callback, completion: completion) self.tokens = self.router.tokens diff --git a/Sources/ImperialFacebook/FacebookRouter.swift b/Sources/ImperialFacebook/FacebookRouter.swift index 630d6879..7672f714 100644 --- a/Sources/ImperialFacebook/FacebookRouter.swift +++ b/Sources/ImperialFacebook/FacebookRouter.swift @@ -4,7 +4,7 @@ import Foundation public class FacebookRouter: FederatedServiceRouter { public let tokens: any FederatedServiceTokens - public let callbackCompletion: (Request, String) throws -> (EventLoopFuture) + public let callbackCompletion: (Request, String) async throws -> any AsyncResponseEncodable public var scope: [String] = [] public let callbackURL: String public var accessTokenURL: String = "https://graph.facebook.com/v3.2/oauth/access_token" @@ -29,13 +29,13 @@ public class FacebookRouter: FederatedServiceRouter { return url.absoluteString } - public required init(callback: String, completion: @escaping (Request, String) throws -> (EventLoopFuture)) throws { + public required init(callback: String, completion: @escaping (Request, String) async throws -> any AsyncResponseEncodable) throws { self.tokens = try FacebookAuth() self.callbackURL = callback self.callbackCompletion = completion } - public func callbackBody(with code: String) -> any ResponseEncodable { + public func callbackBody(with code: String) -> any AsyncResponseEncodable { FacebookCallbackBody(code: code, clientId: tokens.clientID, clientSecret: tokens.clientSecret, diff --git a/Sources/ImperialGitHub/GitHub.swift b/Sources/ImperialGitHub/GitHub.swift index 33c87a3b..8da63f9e 100644 --- a/Sources/ImperialGitHub/GitHub.swift +++ b/Sources/ImperialGitHub/GitHub.swift @@ -9,10 +9,10 @@ public class GitHub: FederatedService { public required init( routes: any RoutesBuilder, authenticate: String, - authenticateCallback: ((Request) throws -> (EventLoopFuture))?, + authenticateCallback: ((Request) async throws -> Void)?, callback: String, scope: [String] = [], - completion: @escaping (Request, String) throws -> (EventLoopFuture) + completion: @escaping (Request, String) async throws -> any AsyncResponseEncodable ) throws { self.router = try GitHubRouter(callback: callback, completion: completion) self.tokens = self.router.tokens diff --git a/Sources/ImperialGitHub/GitHubRouter.swift b/Sources/ImperialGitHub/GitHubRouter.swift index 731bb367..165394a2 100644 --- a/Sources/ImperialGitHub/GitHubRouter.swift +++ b/Sources/ImperialGitHub/GitHubRouter.swift @@ -5,7 +5,7 @@ public class GitHubRouter: FederatedServiceRouter { public static var baseURL: String = "https://github.com/" public let tokens: any FederatedServiceTokens - public let callbackCompletion: (Request, String) throws -> (EventLoopFuture) + public let callbackCompletion: (Request, String) async throws -> any AsyncResponseEncodable public var scope: [String] = [] public let callbackURL: String public let accessTokenURL: String = "\(GitHubRouter.baseURL.finished(with: "/"))login/oauth/access_token" @@ -16,7 +16,7 @@ public class GitHubRouter: FederatedServiceRouter { return headers }() - public required init(callback: String, completion: @escaping (Request, String) throws -> (EventLoopFuture)) throws { + public required init(callback: String, completion: @escaping (Request, String) async throws -> any AsyncResponseEncodable) throws { self.tokens = try GitHubAuth() self.callbackURL = callback self.callbackCompletion = completion @@ -40,7 +40,7 @@ public class GitHubRouter: FederatedServiceRouter { return url.absoluteString } - public func callbackBody(with code: String) -> any ResponseEncodable { + public func callbackBody(with code: String) -> any AsyncResponseEncodable { GitHubCallbackBody(clientId: tokens.clientID, clientSecret: tokens.clientSecret, code: code) diff --git a/Sources/ImperialGitlab/Gitlab.swift b/Sources/ImperialGitlab/Gitlab.swift index 9a6886f6..e199422c 100644 --- a/Sources/ImperialGitlab/Gitlab.swift +++ b/Sources/ImperialGitlab/Gitlab.swift @@ -9,10 +9,10 @@ public class Gitlab: FederatedService { public required init( routes: any RoutesBuilder, authenticate: String, - authenticateCallback: ((Request) throws -> (EventLoopFuture))?, + authenticateCallback: ((Request) async throws -> Void)?, callback: String, scope: [String] = [], - completion: @escaping (Request, String) throws -> (EventLoopFuture) + completion: @escaping (Request, String) async throws -> any AsyncResponseEncodable ) throws { self.router = try GitlabRouter(callback: callback, completion: completion) self.tokens = self.router.tokens diff --git a/Sources/ImperialGitlab/GitlabRouter.swift b/Sources/ImperialGitlab/GitlabRouter.swift index 7d417169..df4a3e1e 100644 --- a/Sources/ImperialGitlab/GitlabRouter.swift +++ b/Sources/ImperialGitlab/GitlabRouter.swift @@ -6,13 +6,13 @@ public class GitlabRouter: FederatedServiceRouter { public static var baseURL: String = "https://gitlab.com/" public static var callbackURL: String = "callback" public let tokens: any FederatedServiceTokens - public let callbackCompletion: (Request, String) throws -> (EventLoopFuture) + public let callbackCompletion: (Request, String) async throws -> any AsyncResponseEncodable public var scope: [String] = [] public let callbackURL: String public let accessTokenURL: String = "\(GitlabRouter.baseURL.finished(with: "/"))oauth/token" public let service: OAuthService = .gitlab - public required init(callback: String, completion: @escaping (Request, String) throws -> (EventLoopFuture)) throws { + public required init(callback: String, completion: @escaping (Request, String) async throws -> any AsyncResponseEncodable) throws { self.tokens = try GitlabAuth() self.callbackURL = callback self.callbackCompletion = completion @@ -37,7 +37,7 @@ public class GitlabRouter: FederatedServiceRouter { return url.absoluteString } - public func callbackBody(with code: String) -> any ResponseEncodable { + public func callbackBody(with code: String) -> any AsyncResponseEncodable { GitlabCallbackBody(clientId: tokens.clientID, clientSecret: tokens.clientSecret, code: code, diff --git a/Sources/ImperialGoogle/JWT/GoogleJWT.swift b/Sources/ImperialGoogle/JWT/GoogleJWT.swift index 41a472dd..2299ab15 100644 --- a/Sources/ImperialGoogle/JWT/GoogleJWT.swift +++ b/Sources/ImperialGoogle/JWT/GoogleJWT.swift @@ -8,10 +8,10 @@ public class GoogleJWT: FederatedService { public required init( routes: any RoutesBuilder, authenticate: String, - authenticateCallback: ((Request) throws -> (EventLoopFuture))?, + authenticateCallback: ((Request) async throws -> Void)?, callback: String, scope: [String] = [], - completion: @escaping (Request, String) throws -> (EventLoopFuture) + completion: @escaping (Request, String) async throws -> any AsyncResponseEncodable ) throws { self.router = try GoogleJWTRouter(callback: callback, completion: completion) self.tokens = self.router.tokens diff --git a/Sources/ImperialGoogle/JWT/GoogleJWTRouter.swift b/Sources/ImperialGoogle/JWT/GoogleJWTRouter.swift index f002569f..a15363b5 100644 --- a/Sources/ImperialGoogle/JWT/GoogleJWTRouter.swift +++ b/Sources/ImperialGoogle/JWT/GoogleJWTRouter.swift @@ -6,7 +6,7 @@ import JWTKit public final class GoogleJWTRouter: FederatedServiceRouter { public var tokens: any FederatedServiceTokens - public var callbackCompletion: (Request, String) throws -> (EventLoopFuture) + public var callbackCompletion: (Request, String) async throws -> any AsyncResponseEncodable public var scope: [String] = [] public var callbackURL: String public var accessTokenURL: String = "https://www.googleapis.com/oauth2/v4/token" @@ -18,7 +18,7 @@ public final class GoogleJWTRouter: FederatedServiceRouter { return headers }() - public init(callback: String, completion: @escaping (Request, String) throws -> (EventLoopFuture)) throws { + public init(callback: String, completion: @escaping (Request, String) async throws -> any AsyncResponseEncodable) throws { self.tokens = try GoogleJWTAuth() self.callbackURL = callback self.authURL = callback @@ -29,27 +29,22 @@ public final class GoogleJWTRouter: FederatedServiceRouter { return authURL } - public func callbackBody(with code: String) -> any ResponseEncodable { + public func callbackBody(with code: String) -> any AsyncResponseEncodable { return "grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion=\(code)" } - public func fetchToken(from request: Request) throws -> EventLoopFuture { + public func fetchToken(from request: Request) async throws -> String { let token = try self.jwt() let body = callbackBody(with: token) let url = URI(string: self.accessTokenURL) - return body.encodeResponse(for: request) - .map { $0.body.buffer } - .flatMap { buffer in - return request.client.post(url, headers: self.callbackHeaders) { $0.body = buffer } - }.flatMapThrowing { response in - return try response.content.get(GoogleJWTResponse.self) - }.map { $0.accessToken } + let buffer = try await body.encodeResponse(for: request).body.buffer + let response = try await request.client.post(url, headers: self.callbackHeaders) { $0.body = buffer } + return try response.content.get(GoogleJWTResponse.self).accessToken } - public func authenticate(_ request: Request) throws -> EventLoopFuture { - let redirect: Response = request.redirect(to: self.callbackURL) - return request.eventLoop.makeSucceededFuture(redirect) + public func authenticate(_ request: Request) async throws -> Response { + request.redirect(to: self.callbackURL) } public func jwt() throws -> String { diff --git a/Sources/ImperialGoogle/Standard/Google.swift b/Sources/ImperialGoogle/Standard/Google.swift index a59101e1..2a232bdd 100644 --- a/Sources/ImperialGoogle/Standard/Google.swift +++ b/Sources/ImperialGoogle/Standard/Google.swift @@ -9,10 +9,10 @@ public class Google: FederatedService { public required init( routes: any RoutesBuilder, authenticate: String, - authenticateCallback: ((Request) throws -> (EventLoopFuture))?, + authenticateCallback: ((Request) async throws -> Void)?, callback: String, scope: [String] = [], - completion: @escaping (Request, String) throws -> (EventLoopFuture) + completion: @escaping (Request, String) async throws -> any AsyncResponseEncodable ) throws { self.router = try GoogleRouter(callback: callback, completion: completion) self.tokens = self.router.tokens diff --git a/Sources/ImperialGoogle/Standard/GoogleRouter.swift b/Sources/ImperialGoogle/Standard/GoogleRouter.swift index a08040e3..30a8ec02 100644 --- a/Sources/ImperialGoogle/Standard/GoogleRouter.swift +++ b/Sources/ImperialGoogle/Standard/GoogleRouter.swift @@ -3,7 +3,7 @@ import Foundation public class GoogleRouter: FederatedServiceRouter { public let tokens: any FederatedServiceTokens - public let callbackCompletion: (Request, String) throws -> (EventLoopFuture) + public let callbackCompletion: (Request, String) async throws -> any AsyncResponseEncodable public var scope: [String] = [] public let callbackURL: String public let accessTokenURL: String = "https://www.googleapis.com/oauth2/v4/token" @@ -14,7 +14,7 @@ public class GoogleRouter: FederatedServiceRouter { return headers }() - public required init(callback: String, completion: @escaping (Request, String) throws -> (EventLoopFuture)) throws { + public required init(callback: String, completion: @escaping (Request, String) async throws -> any AsyncResponseEncodable) throws { self.tokens = try GoogleAuth() self.callbackURL = callback self.callbackCompletion = completion @@ -39,7 +39,7 @@ public class GoogleRouter: FederatedServiceRouter { return url.absoluteString } - public func callbackBody(with code: String) -> any ResponseEncodable { + public func callbackBody(with code: String) -> any AsyncResponseEncodable { GoogleCallbackBody(code: code, clientId: tokens.clientID, clientSecret: tokens.clientSecret, diff --git a/Sources/ImperialKeycloak/Keycloak.swift b/Sources/ImperialKeycloak/Keycloak.swift index 6001df9a..22a03020 100644 --- a/Sources/ImperialKeycloak/Keycloak.swift +++ b/Sources/ImperialKeycloak/Keycloak.swift @@ -9,10 +9,10 @@ public class Keycloak: FederatedService { public required init( routes: any RoutesBuilder, authenticate: String, - authenticateCallback: ((Request) throws -> (EventLoopFuture))?, + authenticateCallback: ((Request) async throws -> Void)?, callback: String, scope: [String] = [], - completion: @escaping (Request, String) throws -> (EventLoopFuture) + completion: @escaping (Request, String) async throws -> any AsyncResponseEncodable ) throws { self.router = try KeycloakRouter(callback: callback, completion: completion) self.tokens = self.router.tokens diff --git a/Sources/ImperialKeycloak/KeycloakRouter.swift b/Sources/ImperialKeycloak/KeycloakRouter.swift index 41a226b5..aaa7d575 100644 --- a/Sources/ImperialKeycloak/KeycloakRouter.swift +++ b/Sources/ImperialKeycloak/KeycloakRouter.swift @@ -4,13 +4,13 @@ import Foundation public class KeycloakRouter: FederatedServiceRouter { public let tokens: any FederatedServiceTokens public let keycloakTokens: KeycloakAuth - public let callbackCompletion: (Request, String) throws -> (EventLoopFuture) + public let callbackCompletion: (Request, String) async throws -> any AsyncResponseEncodable public var scope: [String] = [] public let callbackURL: String public let accessTokenURL: String public let service: OAuthService = .keycloak - public required init(callback: String, completion: @escaping (Request, String) throws -> (EventLoopFuture)) throws { + public required init(callback: String, completion: @escaping (Request, String) async throws -> any AsyncResponseEncodable) throws { self.tokens = try KeycloakAuth() self.keycloakTokens = self.tokens as! KeycloakAuth self.accessTokenURL = keycloakTokens.accessTokenURL @@ -26,7 +26,7 @@ public class KeycloakRouter: FederatedServiceRouter { "response_type=code" } - public func callbackBody(with code: String) -> any ResponseEncodable { + public func callbackBody(with code: String) -> any AsyncResponseEncodable { KeycloakCallbackBody(code: code, clientId: tokens.clientID, clientSecret: tokens.clientSecret, diff --git a/Sources/ImperialMicrosoft/Microsoft.swift b/Sources/ImperialMicrosoft/Microsoft.swift index caf8fb5c..de032405 100644 --- a/Sources/ImperialMicrosoft/Microsoft.swift +++ b/Sources/ImperialMicrosoft/Microsoft.swift @@ -9,10 +9,10 @@ public class Microsoft: FederatedService { public required init( routes: any RoutesBuilder, authenticate: String, - authenticateCallback: ((Request) throws -> (EventLoopFuture))?, + authenticateCallback: ((Request) async throws -> Void)?, callback: String, scope: [String] = [], - completion: @escaping (Request, String)throws -> (EventLoopFuture) + completion: @escaping (Request, String) async throws -> any AsyncResponseEncodable ) throws { self.router = try MicrosoftRouter(callback: callback, completion: completion) self.tokens = self.router.tokens diff --git a/Sources/ImperialMicrosoft/MicrosoftRouter.swift b/Sources/ImperialMicrosoft/MicrosoftRouter.swift index e7c96462..4fecda3b 100644 --- a/Sources/ImperialMicrosoft/MicrosoftRouter.swift +++ b/Sources/ImperialMicrosoft/MicrosoftRouter.swift @@ -6,7 +6,7 @@ public class MicrosoftRouter: FederatedServiceRouter { public static var tenantIDEnvKey: String = "MICROSOFT_TENANT_ID" public let tokens: any FederatedServiceTokens - public let callbackCompletion: (Request, String)throws -> (EventLoopFuture) + public let callbackCompletion: (Request, String) async throws -> any AsyncResponseEncodable public var scope: [String] = [] public let callbackURL: String public var tenantID: String { Environment.get(MicrosoftRouter.tenantIDEnvKey) ?? "common" } @@ -16,7 +16,7 @@ public class MicrosoftRouter: FederatedServiceRouter { public required init( callback: String, - completion: @escaping (Request, String) throws -> (EventLoopFuture) + completion: @escaping (Request, String) async throws -> any AsyncResponseEncodable ) throws { self.tokens = try MicrosoftAuth() self.callbackURL = callback @@ -44,7 +44,7 @@ public class MicrosoftRouter: FederatedServiceRouter { return url.absoluteString } - public func callbackBody(with code: String) -> any ResponseEncodable { + public func callbackBody(with code: String) -> any AsyncResponseEncodable { MicrosoftCallbackBody(code: code, clientId: tokens.clientID, clientSecret: tokens.clientSecret, diff --git a/Sources/ImperialShopify/Shopify.swift b/Sources/ImperialShopify/Shopify.swift index 59cb7e4c..81a874d3 100644 --- a/Sources/ImperialShopify/Shopify.swift +++ b/Sources/ImperialShopify/Shopify.swift @@ -11,10 +11,10 @@ public final class Shopify: FederatedService { public init( routes: any RoutesBuilder, authenticate: String, - authenticateCallback: ((Request) throws -> (EventLoopFuture))?, + authenticateCallback: ((Request) async throws -> Void)?, callback: String, scope: [String], - completion: @escaping (Request, String) throws -> (EventLoopFuture) + completion: @escaping (Request, String) async throws -> any AsyncResponseEncodable ) throws { self.shopifyRouter = try ShopifyRouter(callback: callback, completion: completion) self.shopifyRouter.scope = scope diff --git a/Sources/ImperialShopify/ShopifyRouter.swift b/Sources/ImperialShopify/ShopifyRouter.swift index a1be9df6..5db49d81 100644 --- a/Sources/ImperialShopify/ShopifyRouter.swift +++ b/Sources/ImperialShopify/ShopifyRouter.swift @@ -3,13 +3,13 @@ import Vapor public class ShopifyRouter: FederatedServiceRouter { public let tokens: any FederatedServiceTokens - public let callbackCompletion: (Request, String) throws -> (EventLoopFuture) + public let callbackCompletion: (Request, String) async throws -> any AsyncResponseEncodable public var scope: [String] = [] public let callbackURL: String public var accessTokenURL: String = "" public let service: OAuthService = .shopify - required public init(callback: String, completion: @escaping (Request, String) throws -> (EventLoopFuture)) throws { + required public init(callback: String, completion: @escaping (Request, String) async throws -> any AsyncResponseEncodable) throws { self.tokens = try ShopifyAuth() self.callbackURL = callback self.callbackCompletion = completion @@ -25,7 +25,7 @@ public class ShopifyRouter: FederatedServiceRouter { return try authURLFrom(shop, nonce: nonce).absoluteString } - public func callbackBody(with code: String) -> any ResponseEncodable { + public func callbackBody(with code: String) -> any AsyncResponseEncodable { ShopifyCallbackBody(code: code, clientId: tokens.clientID, clientSecret: tokens.clientSecret) @@ -35,7 +35,7 @@ public class ShopifyRouter: FederatedServiceRouter { /// This method is the main body of the `callback` handler. /// /// - Parameters: request: The request for the route this method is called in. - public func fetchToken(from request: Request) throws -> EventLoopFuture { + public func fetchToken(from request: Request) async throws -> String { // Extract the parameters to verify guard let code = request.query[String.self, at: "code"], let shop = request.query[String.self, at: "shop"], @@ -52,13 +52,9 @@ public class ShopifyRouter: FederatedServiceRouter { // exchange code for access token let body = callbackBody(with: code) let url = URI(string: self.accessTokenURL) - return body.encodeResponse(for: request) - .map { $0.body.buffer } - .flatMap { buffer in - return request.client.post(url) { $0.body = buffer } - }.flatMapThrowing { response in - return try response.content.get(String.self, at: ["access_token"]) - } + let buffer = try await body.encodeResponse(for: request).body.buffer + let response = try await request.client.post(url) { $0.body = buffer } + return try response.content.get(String.self, at: ["access_token"]) } /// The route that the OAuth provider calls when the user has benn authenticated. @@ -66,22 +62,15 @@ public class ShopifyRouter: FederatedServiceRouter { /// - Parameter request: The request from the OAuth provider. /// - Returns: A response that should redirect the user back to the app. /// - Throws: Any errors that occur in the implementation code. - public func callback(_ request: Request) throws -> EventLoopFuture { - return try self.fetchToken(from: request).flatMap { accessToken in - let session = request.session - do { - guard let domain = request.query[String.self, at: "shop"] else { throw Abort(.badRequest) } - - try session.setAccessToken(accessToken) - try session.setShopDomain(domain) - try session.setNonce(nil) - return try self.callbackCompletion(request, accessToken).flatMap { response in - return response.encodeResponse(for: request) - } - } catch { - return request.eventLoop.makeFailedFuture(error) - } - } + public func callback(_ request: Request) async throws -> Response { + let accessToken = try await self.fetchToken(from: request) + let session = request.session + guard let domain = request.query[String.self, at: "shop"] else { throw Abort(.badRequest) } + try session.setAccessToken(accessToken) + try session.setShopDomain(domain) + try session.setNonce(nil) + let response = try await self.callbackCompletion(request, accessToken) + return try await response.encodeResponse(for: request) } private func authURLFrom(_ shop: String, nonce: String) throws -> URL { From 7b86214e496978db8d6a18858c613fd13eff5e31 Mon Sep 17 00:00:00 2001 From: Francesco Paolo Severino Date: Sun, 10 Nov 2024 17:31:29 +0100 Subject: [PATCH 04/37] Remove old Microsoft service --- .../Services/Microsoft/Microsoft.swift | 24 ------ .../Services/Microsoft/MicrosoftAuth.swift | 16 ---- .../Microsoft/MicrosoftCallbackBody.swift | 21 ----- .../Services/Microsoft/MicrosoftRouter.swift | 81 ------------------- .../Microsoft/Service+Microsoft.swift | 6 -- 5 files changed, 148 deletions(-) delete mode 100644 Sources/Imperial/Services/Microsoft/Microsoft.swift delete mode 100644 Sources/Imperial/Services/Microsoft/MicrosoftAuth.swift delete mode 100644 Sources/Imperial/Services/Microsoft/MicrosoftCallbackBody.swift delete mode 100644 Sources/Imperial/Services/Microsoft/MicrosoftRouter.swift delete mode 100644 Sources/Imperial/Services/Microsoft/Service+Microsoft.swift diff --git a/Sources/Imperial/Services/Microsoft/Microsoft.swift b/Sources/Imperial/Services/Microsoft/Microsoft.swift deleted file mode 100644 index 2279eaa2..00000000 --- a/Sources/Imperial/Services/Microsoft/Microsoft.swift +++ /dev/null @@ -1,24 +0,0 @@ -import Vapor - -public class Microsoft: FederatedService { - public var tokens: FederatedServiceTokens - public var router: FederatedServiceRouter - - @discardableResult - public required init( - router: Router, - authenticate: String, - authenticateCallback: ((Request)throws -> (Future))?, - callback: String, - scope: [String] = [], - completion: @escaping (Request, String)throws -> (Future) - ) throws { - self.router = try MicrosoftRouter(callback: callback, completion: completion) - self.tokens = self.router.tokens - - self.router.scope = scope - try self.router.configureRoutes(withAuthURL: authenticate, authenticateCallback: authenticateCallback, on: router) - - OAuthService.register(.microsoft) - } -} diff --git a/Sources/Imperial/Services/Microsoft/MicrosoftAuth.swift b/Sources/Imperial/Services/Microsoft/MicrosoftAuth.swift deleted file mode 100644 index 45d41b0c..00000000 --- a/Sources/Imperial/Services/Microsoft/MicrosoftAuth.swift +++ /dev/null @@ -1,16 +0,0 @@ -import Vapor - -public class MicrosoftAuth: FederatedServiceTokens { - public static var idEnvKey: String = "MICROSOFT_CLIENT_ID" - public static var secretEnvKey: String = "MICROSOFT_CLIENT_SECRET" - public var clientID: String - public var clientSecret: String - - public required init() throws { - let idError = ImperialError.missingEnvVar(MicrosoftAuth.idEnvKey) - let secretError = ImperialError.missingEnvVar(MicrosoftAuth.secretEnvKey) - - self.clientID = try Environment.get(MicrosoftAuth.idEnvKey).value(or: idError) - self.clientSecret = try Environment.get(MicrosoftAuth.secretEnvKey).value(or: secretError) - } -} diff --git a/Sources/Imperial/Services/Microsoft/MicrosoftCallbackBody.swift b/Sources/Imperial/Services/Microsoft/MicrosoftCallbackBody.swift deleted file mode 100644 index 4b6ec696..00000000 --- a/Sources/Imperial/Services/Microsoft/MicrosoftCallbackBody.swift +++ /dev/null @@ -1,21 +0,0 @@ -import Vapor - -struct MicrosoftCallbackBody: Content { - let code: String - let clientId: String - let clientSecret: String - let redirectURI: String - let scope: String - let grantType: String = "authorization_code" - - static var defaultContentType: MediaType = .urlEncodedForm - - enum CodingKeys: String, CodingKey { - case code = "code" - case clientId = "client_id" - case clientSecret = "client_secret" - case redirectURI = "redirect_uri" - case grantType = "grant_type" - case scope = "scope" - } -} diff --git a/Sources/Imperial/Services/Microsoft/MicrosoftRouter.swift b/Sources/Imperial/Services/Microsoft/MicrosoftRouter.swift deleted file mode 100644 index 2e6ec451..00000000 --- a/Sources/Imperial/Services/Microsoft/MicrosoftRouter.swift +++ /dev/null @@ -1,81 +0,0 @@ -import Vapor -import Foundation - -public class MicrosoftRouter: FederatedServiceRouter { - public static var tenantIDEnvKey: String = "MICROSOFT_TENANT_ID" - - public let tokens: FederatedServiceTokens - public let callbackCompletion: (Request, String)throws -> (Future) - public var scope: [String] = [] - public let callbackURL: String - public var tenantID: String { Environment.get(MicrosoftRouter.tenantIDEnvKey) ?? "common" } - public var accessTokenURL: String { "https://login.microsoftonline.com/\(self.tenantID)/oauth2/v2.0/token" } - - public required init( - callback: String, - completion: @escaping (Request, String) throws -> (Future) - ) throws { - self.tokens = try MicrosoftAuth() - self.callbackURL = callback - self.callbackCompletion = completion - } - - public func authURL(_ request: Request) throws -> String { - return "https://login.microsoftonline.com/\(self.tenantID)/oauth2/v2.0/authorize?" - + "client_id=\(self.tokens.clientID)&" - + "response_type=code&" - + "redirect_uri=\(self.callbackURL)&" - + "response_mode=query&" - + "scope=\(scope.joined(separator: "%20"))&" - + "prompt=consent" - } - - public func fetchToken(from request: Request)throws -> Future { - let code: String - - if let queryCode: String = try request.query.get(at: "code") { - code = queryCode - } else if let error: String = try request.query.get(at: "error_description") { - throw Abort(.badRequest, reason: error) - } else { - throw Abort(.badRequest, reason: "Missing 'code' key in URL query") - } - - let body = MicrosoftCallbackBody( - code: code, - clientId: self.tokens.clientID, - clientSecret: self.tokens.clientSecret, - redirectURI: self.callbackURL, - scope: scope.joined(separator: "%20") - ) - - return try body.encode(using: request).flatMap(to: Response.self) { request in - guard let url = URL(string: self.accessTokenURL) else { - throw Abort( - .internalServerError, - reason: "Unable to convert String '\(self.accessTokenURL)' to URL" - ) - } - - request.http.method = .POST - request.http.url = url - - return try request.make(Client.self).send(request) - }.flatMap(to: String.self) { response in - return response.content.get(String.self, at: ["access_token"]) - } - } - - public func callback(_ request: Request)throws -> Future { - return try self.fetchToken(from: request).flatMap(to: ResponseEncodable.self) { accessToken in - let session = try request.session() - - session.setAccessToken(accessToken) - try session.set("access_token_service", to: OAuthService.microsoft) - - return try self.callbackCompletion(request, accessToken) - }.flatMap(to: Response.self) { response in - return try response.encode(for: request) - } - } -} diff --git a/Sources/Imperial/Services/Microsoft/Service+Microsoft.swift b/Sources/Imperial/Services/Microsoft/Service+Microsoft.swift deleted file mode 100644 index 9341deef..00000000 --- a/Sources/Imperial/Services/Microsoft/Service+Microsoft.swift +++ /dev/null @@ -1,6 +0,0 @@ -extension OAuthService { - public static let microsoft = OAuthService.init( - name: "microsoft", - endpoints: [:] - ) -} From 954b706f6f1184e762424214a2203da0b866d9b9 Mon Sep 17 00:00:00 2001 From: Francesco Paolo Severino Date: Sun, 10 Nov 2024 17:39:06 +0100 Subject: [PATCH 05/37] Update to Swift 6 --- Package.swift | 11 ++++----- Tests/ImperialTests/ImperialTests.swift | 7 +++--- Tests/ImperialTests/ShopifyTests.swift | 32 ++++++++++++------------- 3 files changed, 24 insertions(+), 26 deletions(-) diff --git a/Package.swift b/Package.swift index 65bb98ff..7076f102 100755 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.10 +// swift-tools-version:6.0 import PackageDescription let package = Package( @@ -56,16 +56,13 @@ let package = Package( .target(name: "ImperialMicrosoft", dependencies: ["ImperialCore"], swiftSettings: swiftSettings), .target(name: "ImperialShopify", dependencies: ["ImperialCore"], swiftSettings: swiftSettings), .testTarget(name: "ImperialTests", dependencies: ["ImperialCore", "ImperialShopify"], swiftSettings: swiftSettings), - ] + ], + swiftLanguageModes: [.v5] ) var swiftSettings: [SwiftSetting] { [ .enableUpcomingFeature("ExistentialAny"), - .enableUpcomingFeature("ConciseMagicFile"), - .enableUpcomingFeature("ForwardTrailingClosures"), - .enableUpcomingFeature("DisableOutwardActorInference"), - .enableUpcomingFeature("StrictConcurrency"), - .enableExperimentalFeature("StrictConcurrency=complete"), + .enableUpcomingFeature("FullTypedThrows"), ] } \ No newline at end of file diff --git a/Tests/ImperialTests/ImperialTests.swift b/Tests/ImperialTests/ImperialTests.swift index 6f3d3194..8ccde3d2 100644 --- a/Tests/ImperialTests/ImperialTests.swift +++ b/Tests/ImperialTests/ImperialTests.swift @@ -1,6 +1,7 @@ -import XCTest +import Testing + @testable import ImperialCore -class ImperialTests: XCTestCase { - func testExists() {} +struct ImperialTests { + @Test func empty() {} } diff --git a/Tests/ImperialTests/ShopifyTests.swift b/Tests/ImperialTests/ShopifyTests.swift index 28ffd31c..dbde17b5 100644 --- a/Tests/ImperialTests/ShopifyTests.swift +++ b/Tests/ImperialTests/ShopifyTests.swift @@ -1,40 +1,40 @@ +import Foundation +import Testing + @testable import ImperialShopify -import XCTest -final class ShopifyTests: XCTestCase { - - func testDomainCheck() throws { - +@Suite("Shopify Tests") +struct ShopifyTests { + @Test("Valid Shopify Domain") func domainCheck() throws { let domain = "davidmuzi.myshopify.com" - XCTAssertTrue(URL(string: domain)!.isValidShopifyDomain()) + #expect(URL(string: domain)!.isValidShopifyDomain()) let domain2 = "d4m3.myshopify.com" - XCTAssertTrue(URL(string: domain2)!.isValidShopifyDomain()) + #expect(URL(string: domain2)!.isValidShopifyDomain()) let domain3 = "david-muzi.myshopify.com" - XCTAssertTrue(URL(string: domain3)!.isValidShopifyDomain()) + #expect(URL(string: domain3)!.isValidShopifyDomain()) let domain4 = "david.muzi.myshopify.com" - XCTAssertTrue(URL(string: domain4)!.isValidShopifyDomain()) + #expect(URL(string: domain4)!.isValidShopifyDomain()) let domain5 = "david#muzi.myshopify.com" - XCTAssertFalse(URL(string: domain5)!.isValidShopifyDomain()) + #expect(!URL(string: domain5)!.isValidShopifyDomain()) let domain6 = "davidmuzi.myshopify.com.ca" - XCTAssertFalse(URL(string: domain6)!.isValidShopifyDomain()) + #expect(!URL(string: domain6)!.isValidShopifyDomain()) let domain7 = "davidmuzi.square.com" - XCTAssertFalse(URL(string: domain7)!.isValidShopifyDomain()) + #expect(!URL(string: domain7)!.isValidShopifyDomain()) let domain8 = "david*muzi.shopify.ca" - XCTAssertFalse(URL(string: domain8)!.isValidShopifyDomain()) + #expect(!URL(string: domain8)!.isValidShopifyDomain()) } - func testHMACValidation() throws { - + @Test("HMAC Validation") func hmacValidation() throws { let url = URL(string: "https://domain.com/?code=0907a61c0c8d55e99db179b68161bc00&hmac=700e2dadb827fcc8609e9d5ce208b2e9cdaab9df07390d2cbca10d7c328fc4bf&shop=some-shop.myshopify.com&state=0.6784241404160823×tamp=1337178173")! let hmac = url.generateHMAC(key: "hush") - XCTAssertEqual(hmac, "700e2dadb827fcc8609e9d5ce208b2e9cdaab9df07390d2cbca10d7c328fc4bf") + #expect(hmac == "700e2dadb827fcc8609e9d5ce208b2e9cdaab9df07390d2cbca10d7c328fc4bf") } } From 7efd4cb83e9edcdac6add5c90cf516d588085d6b Mon Sep 17 00:00:00 2001 From: Francesco Paolo Severino Date: Sun, 10 Nov 2024 21:00:53 +0100 Subject: [PATCH 06/37] Revert to macOS 13 --- Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index 7076f102..71c0f0aa 100755 --- a/Package.swift +++ b/Package.swift @@ -4,7 +4,7 @@ import PackageDescription let package = Package( name: "Imperial", platforms: [ - .macOS(.v14) + .macOS(.v13) ], products: [ .library(name: "ImperialCore", targets: ["ImperialCore"]), From 13bcee81cb4146026265555d1196248693b97d51 Mon Sep 17 00:00:00 2001 From: Francesco Paolo Severino <96546612+fpseverino@users.noreply.github.com> Date: Thu, 14 Nov 2024 18:55:03 +0100 Subject: [PATCH 07/37] Update Keycloak.swift Co-authored-by: Paul Toffoloni <69189821+ptoffy@users.noreply.github.com> --- Sources/ImperialKeycloak/Keycloak.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ImperialKeycloak/Keycloak.swift b/Sources/ImperialKeycloak/Keycloak.swift index 22a03020..68de1fc1 100644 --- a/Sources/ImperialKeycloak/Keycloak.swift +++ b/Sources/ImperialKeycloak/Keycloak.swift @@ -7,7 +7,7 @@ public class Keycloak: FederatedService { @discardableResult public required init( - routes: any RoutesBuilder, + routes: some RoutesBuilder, authenticate: String, authenticateCallback: ((Request) async throws -> Void)?, callback: String, From f3d5d1f6017ad346c2970729ca654771b4336548 Mon Sep 17 00:00:00 2001 From: Francesco Paolo Severino <96546612+fpseverino@users.noreply.github.com> Date: Thu, 14 Nov 2024 18:55:11 +0100 Subject: [PATCH 08/37] Update Dropbox.swift Co-authored-by: Paul Toffoloni <69189821+ptoffy@users.noreply.github.com> --- Sources/ImperialDropbox/Dropbox.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ImperialDropbox/Dropbox.swift b/Sources/ImperialDropbox/Dropbox.swift index 1806edaf..8001bc17 100644 --- a/Sources/ImperialDropbox/Dropbox.swift +++ b/Sources/ImperialDropbox/Dropbox.swift @@ -7,7 +7,7 @@ public class Dropbox: FederatedService { @discardableResult public required init( - routes: any RoutesBuilder, + routes: some RoutesBuilder, authenticate: String, authenticateCallback: ((Request) async throws -> Void)?, callback: String, From 7126c1fb640cbe6585158b32c1dc381372ee99c4 Mon Sep 17 00:00:00 2001 From: Francesco Paolo Severino <96546612+fpseverino@users.noreply.github.com> Date: Thu, 14 Nov 2024 18:55:17 +0100 Subject: [PATCH 09/37] Update Discord.swift Co-authored-by: Paul Toffoloni <69189821+ptoffy@users.noreply.github.com> --- Sources/ImperialDiscord/Discord.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ImperialDiscord/Discord.swift b/Sources/ImperialDiscord/Discord.swift index b503e93c..2c5f343e 100644 --- a/Sources/ImperialDiscord/Discord.swift +++ b/Sources/ImperialDiscord/Discord.swift @@ -7,7 +7,7 @@ public class Discord: FederatedService { @discardableResult public required init( - routes: any RoutesBuilder, + routes: some RoutesBuilder, authenticate: String, authenticateCallback: ((Request) async throws -> Void)?, callback: String, From 1daa432e3097e523de3e5a70dd4f14a7ff04444d Mon Sep 17 00:00:00 2001 From: Francesco Paolo Severino <96546612+fpseverino@users.noreply.github.com> Date: Thu, 14 Nov 2024 18:55:23 +0100 Subject: [PATCH 10/37] Update FederatedService.swift Co-authored-by: Paul Toffoloni <69189821+ptoffy@users.noreply.github.com> --- Sources/ImperialCore/Services/FederatedService.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ImperialCore/Services/FederatedService.swift b/Sources/ImperialCore/Services/FederatedService.swift index 5acb8406..c3ef06e7 100644 --- a/Sources/ImperialCore/Services/FederatedService.swift +++ b/Sources/ImperialCore/Services/FederatedService.swift @@ -42,5 +42,5 @@ public protocol FederatedService { /// - scope: The scopes to send to the provider to request access to. /// - completion: The completion handler that will fire at the end of the callback route. The access token is passed into the callback and the response that is returned will be returned from the callback route. This will usually be a redirect back to the app. /// - Throws: Any errors that occur in the implementation. - init(routes: any RoutesBuilder, authenticate: String, authenticateCallback: ((Request) async throws -> Void)?, callback: String, scope: [String], completion: @escaping (Request, String) async throws -> any AsyncResponseEncodable) throws + init(routes: some RoutesBuilder, authenticate: String, authenticateCallback: ((Request) async throws -> Void)?, callback: String, scope: [String], completion: @escaping (Request, String) async throws -> any AsyncResponseEncodable) throws } From 406a65d89ee0f7494d80103e319f805b547e6005 Mon Sep 17 00:00:00 2001 From: Francesco Paolo Severino <96546612+fpseverino@users.noreply.github.com> Date: Thu, 14 Nov 2024 18:55:30 +0100 Subject: [PATCH 11/37] Update FederatedServiceRouter.swift Co-authored-by: Paul Toffoloni <69189821+ptoffy@users.noreply.github.com> --- Sources/ImperialCore/Routing/FederatedServiceRouter.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ImperialCore/Routing/FederatedServiceRouter.swift b/Sources/ImperialCore/Routing/FederatedServiceRouter.swift index c56bf1bf..2dd268fa 100644 --- a/Sources/ImperialCore/Routing/FederatedServiceRouter.swift +++ b/Sources/ImperialCore/Routing/FederatedServiceRouter.swift @@ -77,7 +77,7 @@ extension FederatedServiceRouter { public var errorKey: String { "error" } public var callbackHeaders: HTTPHeaders { [:] } - public func configureRoutes(withAuthURL authURL: String, authenticateCallback: ((Request) async throws -> Void)?, on router: any RoutesBuilder) throws { + public func configureRoutes(withAuthURL authURL: String, authenticateCallback: ((Request) async throws -> Void)?, on router: some RoutesBuilder) throws { router.get(callbackURL.pathComponents, use: callback) router.get(authURL.pathComponents) { req async throws -> Response in let redirect: Response = req.redirect(to: try self.authURL(req)) From 967b3e5ecf50ccb3791fe7d54bce4a71ca3fd380 Mon Sep 17 00:00:00 2001 From: Francesco Paolo Severino <96546612+fpseverino@users.noreply.github.com> Date: Thu, 14 Nov 2024 18:55:38 +0100 Subject: [PATCH 12/37] Update FederatedServiceRouter.swift Co-authored-by: Paul Toffoloni <69189821+ptoffy@users.noreply.github.com> --- Sources/ImperialCore/Routing/FederatedServiceRouter.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ImperialCore/Routing/FederatedServiceRouter.swift b/Sources/ImperialCore/Routing/FederatedServiceRouter.swift index 2dd268fa..b2970785 100644 --- a/Sources/ImperialCore/Routing/FederatedServiceRouter.swift +++ b/Sources/ImperialCore/Routing/FederatedServiceRouter.swift @@ -51,7 +51,7 @@ public protocol FederatedServiceRouter { /// - authURL: The URL for the route that will redirect the user to the OAuth provider. /// - authenticateCallback: Execute custom code within the authenticate closure before redirection. /// - Throws: N/A - func configureRoutes(withAuthURL authURL: String, authenticateCallback: ((Request) async throws -> Void)?, on router: any RoutesBuilder) throws + func configureRoutes(withAuthURL authURL: String, authenticateCallback: ((Request) async throws -> Void)?, on router: some RoutesBuilder) throws /// Gets an access token from an OAuth provider. /// This method is the main body of the `callback` handler. From 9933bc0f8cf7e0c456d47eb645c0797b82ab489b Mon Sep 17 00:00:00 2001 From: Francesco Paolo Severino <96546612+fpseverino@users.noreply.github.com> Date: Thu, 14 Nov 2024 18:55:44 +0100 Subject: [PATCH 13/37] Update Auth0.swift Co-authored-by: Paul Toffoloni <69189821+ptoffy@users.noreply.github.com> --- Sources/ImperialAuth0/Auth0.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ImperialAuth0/Auth0.swift b/Sources/ImperialAuth0/Auth0.swift index 82081e30..879d90bd 100644 --- a/Sources/ImperialAuth0/Auth0.swift +++ b/Sources/ImperialAuth0/Auth0.swift @@ -7,7 +7,7 @@ public class Auth0: FederatedService { @discardableResult public required init( - routes: any RoutesBuilder, + routes: some RoutesBuilder, authenticate: String, authenticateCallback: ((Request) async throws -> Void)?, callback: String, From 7a85b7dc1d33523dc2ca859c1c82446c2bdefadd Mon Sep 17 00:00:00 2001 From: Francesco Paolo Severino Date: Thu, 14 Nov 2024 19:14:50 +0100 Subject: [PATCH 14/37] Conform all services --- Sources/ImperialFacebook/Facebook.swift | 2 +- Sources/ImperialGitHub/GitHub.swift | 2 +- Sources/ImperialGitlab/Gitlab.swift | 2 +- Sources/ImperialGoogle/JWT/GoogleJWT.swift | 2 +- Sources/ImperialGoogle/Standard/Google.swift | 2 +- Sources/ImperialMicrosoft/Microsoft.swift | 2 +- Sources/ImperialShopify/Shopify.swift | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Sources/ImperialFacebook/Facebook.swift b/Sources/ImperialFacebook/Facebook.swift index 750ed065..e1e64b56 100644 --- a/Sources/ImperialFacebook/Facebook.swift +++ b/Sources/ImperialFacebook/Facebook.swift @@ -7,7 +7,7 @@ public class Facebook: FederatedService { @discardableResult public required init( - routes: any RoutesBuilder, + routes: some RoutesBuilder, authenticate: String, authenticateCallback: ((Request) async throws -> Void)?, callback: String, diff --git a/Sources/ImperialGitHub/GitHub.swift b/Sources/ImperialGitHub/GitHub.swift index 8da63f9e..1d0494b4 100644 --- a/Sources/ImperialGitHub/GitHub.swift +++ b/Sources/ImperialGitHub/GitHub.swift @@ -7,7 +7,7 @@ public class GitHub: FederatedService { @discardableResult public required init( - routes: any RoutesBuilder, + routes: some RoutesBuilder, authenticate: String, authenticateCallback: ((Request) async throws -> Void)?, callback: String, diff --git a/Sources/ImperialGitlab/Gitlab.swift b/Sources/ImperialGitlab/Gitlab.swift index e199422c..f9c23d31 100644 --- a/Sources/ImperialGitlab/Gitlab.swift +++ b/Sources/ImperialGitlab/Gitlab.swift @@ -7,7 +7,7 @@ public class Gitlab: FederatedService { @discardableResult public required init( - routes: any RoutesBuilder, + routes: some RoutesBuilder, authenticate: String, authenticateCallback: ((Request) async throws -> Void)?, callback: String, diff --git a/Sources/ImperialGoogle/JWT/GoogleJWT.swift b/Sources/ImperialGoogle/JWT/GoogleJWT.swift index 2299ab15..709db8b2 100644 --- a/Sources/ImperialGoogle/JWT/GoogleJWT.swift +++ b/Sources/ImperialGoogle/JWT/GoogleJWT.swift @@ -6,7 +6,7 @@ public class GoogleJWT: FederatedService { @discardableResult public required init( - routes: any RoutesBuilder, + routes: some RoutesBuilder, authenticate: String, authenticateCallback: ((Request) async throws -> Void)?, callback: String, diff --git a/Sources/ImperialGoogle/Standard/Google.swift b/Sources/ImperialGoogle/Standard/Google.swift index 2a232bdd..67d3a06a 100644 --- a/Sources/ImperialGoogle/Standard/Google.swift +++ b/Sources/ImperialGoogle/Standard/Google.swift @@ -7,7 +7,7 @@ public class Google: FederatedService { @discardableResult public required init( - routes: any RoutesBuilder, + routes: some RoutesBuilder, authenticate: String, authenticateCallback: ((Request) async throws -> Void)?, callback: String, diff --git a/Sources/ImperialMicrosoft/Microsoft.swift b/Sources/ImperialMicrosoft/Microsoft.swift index de032405..a6b5998f 100644 --- a/Sources/ImperialMicrosoft/Microsoft.swift +++ b/Sources/ImperialMicrosoft/Microsoft.swift @@ -7,7 +7,7 @@ public class Microsoft: FederatedService { @discardableResult public required init( - routes: any RoutesBuilder, + routes: some RoutesBuilder, authenticate: String, authenticateCallback: ((Request) async throws -> Void)?, callback: String, diff --git a/Sources/ImperialShopify/Shopify.swift b/Sources/ImperialShopify/Shopify.swift index 81a874d3..6d30576e 100644 --- a/Sources/ImperialShopify/Shopify.swift +++ b/Sources/ImperialShopify/Shopify.swift @@ -9,7 +9,7 @@ public final class Shopify: FederatedService { public var shopifyRouter: ShopifyRouter public init( - routes: any RoutesBuilder, + routes: some RoutesBuilder, authenticate: String, authenticateCallback: ((Request) async throws -> Void)?, callback: String, From 4812c42179c166d21e9be861bf0233ebbe88414a Mon Sep 17 00:00:00 2001 From: Francesco Paolo Severino Date: Thu, 14 Nov 2024 19:29:06 +0100 Subject: [PATCH 15/37] Remove `Optional+Imperial` --- Sources/ImperialAuth0/Auth0Auth.swift | 21 +++++++++----- .../Helpers/Optional+Imperial.swift | 14 ---------- .../Helpers/Request+Imperial.swift | 4 ++- .../ImperialCore/Services/OAuthService.swift | 5 +++- Sources/ImperialDiscord/DiscordAuth.swift | 12 +++++--- Sources/ImperialDropbox/DropboxAuth.swift | 14 ++++++---- Sources/ImperialFacebook/FacebookAuth.swift | 12 +++++--- Sources/ImperialGitHub/GitHubAuth.swift | 14 ++++++---- Sources/ImperialGitlab/GitlabAuth.swift | 14 ++++++---- .../ImperialGoogle/JWT/GoogleJWTAuth.swift | 14 ++++++---- .../ImperialGoogle/Standard/GoogleAuth.swift | 14 ++++++---- Sources/ImperialKeycloak/KeycloakAuth.swift | 28 +++++++++++++------ Sources/ImperialMicrosoft/MicrosoftAuth.swift | 14 ++++++---- Sources/ImperialShopify/ShopifyAuth.swift | 14 ++++++---- 14 files changed, 119 insertions(+), 75 deletions(-) delete mode 100644 Sources/ImperialCore/Helpers/Optional+Imperial.swift diff --git a/Sources/ImperialAuth0/Auth0Auth.swift b/Sources/ImperialAuth0/Auth0Auth.swift index f6dc0c6a..2d4e5fe0 100644 --- a/Sources/ImperialAuth0/Auth0Auth.swift +++ b/Sources/ImperialAuth0/Auth0Auth.swift @@ -9,12 +9,19 @@ public class Auth0Auth: FederatedServiceTokens { public var clientSecret: String public required init() throws { - let domainError = ImperialError.missingEnvVar(Auth0Auth.domain) - let idError = ImperialError.missingEnvVar(Auth0Auth.idEnvKey) - let secretError = ImperialError.missingEnvVar(Auth0Auth.secretEnvKey) - - self.domain = try Environment.get(Auth0Auth.domain).value(or: domainError) - self.clientID = try Environment.get(Auth0Auth.idEnvKey).value(or: idError) - self.clientSecret = try Environment.get(Auth0Auth.secretEnvKey).value(or: secretError) + guard let domain = Environment.get(Auth0Auth.domain) else { + throw ImperialError.missingEnvVar(Auth0Auth.domain) + } + self.domain = domain + + guard let clientID = Environment.get(Auth0Auth.idEnvKey) else { + throw ImperialError.missingEnvVar(Auth0Auth.idEnvKey) + } + self.clientID = clientID + + guard let clientSecret = Environment.get(Auth0Auth.secretEnvKey) else { + throw ImperialError.missingEnvVar(Auth0Auth.secretEnvKey) + } + self.clientSecret = clientSecret } } diff --git a/Sources/ImperialCore/Helpers/Optional+Imperial.swift b/Sources/ImperialCore/Helpers/Optional+Imperial.swift deleted file mode 100644 index 5d3032d3..00000000 --- a/Sources/ImperialCore/Helpers/Optional+Imperial.swift +++ /dev/null @@ -1,14 +0,0 @@ -extension Optional { - - /// Gets the value contained in an optional. - /// - /// - Parameter error: The error to throw if the optional is `nil`. - /// - Returns: The value contained in the optional. - /// - Throws: The error passed in if the optional is `nil`. - public func value(or error: any Error) throws -> Wrapped { - switch self { - case let .some(value): return value - case .none: throw error - } - } -} diff --git a/Sources/ImperialCore/Helpers/Request+Imperial.swift b/Sources/ImperialCore/Helpers/Request+Imperial.swift index fb2a07a6..d31721e7 100644 --- a/Sources/ImperialCore/Helpers/Request+Imperial.swift +++ b/Sources/ImperialCore/Helpers/Request+Imperial.swift @@ -11,7 +11,9 @@ extension Request { /// - Returns: An instance of the type passed in. /// - Throws: Errors from trying to get the access token from the request. func create(_ model: Model.Type, with service: OAuthService, on req: Request) async throws -> Model { - let url = try service[model.serviceKey].value(or: ServiceError.noServiceEndpoint(model.serviceKey)) + guard let url = service[model.serviceKey] else { + throw ServiceError.noServiceEndpoint(model.serviceKey) + } let token = try service.tokenPrefix + req .accessToken() diff --git a/Sources/ImperialCore/Services/OAuthService.swift b/Sources/ImperialCore/Services/OAuthService.swift index 8c3a026c..bdc9869d 100644 --- a/Sources/ImperialCore/Services/OAuthService.swift +++ b/Sources/ImperialCore/Services/OAuthService.swift @@ -66,7 +66,10 @@ public struct OAuthService: Codable, Content { /// - Returns: The service that matches the name passed in. /// - Throws: `ImperialError.noServiceFound` if no service is found with the name passed in. public static func get(service name: String) throws -> OAuthService { - return try self.services[name].value(or: ServiceError.noServiceFound(name)) + guard let service = self.services[name] else { + throw ServiceError.noServiceFound(name) + } + return service } } diff --git a/Sources/ImperialDiscord/DiscordAuth.swift b/Sources/ImperialDiscord/DiscordAuth.swift index 59cba99f..4cf47bdb 100644 --- a/Sources/ImperialDiscord/DiscordAuth.swift +++ b/Sources/ImperialDiscord/DiscordAuth.swift @@ -7,10 +7,14 @@ public class DiscordAuth: FederatedServiceTokens { public var clientSecret: String public required init() throws { - let idError = ImperialError.missingEnvVar(DiscordAuth.idEnvKey) - let secretError = ImperialError.missingEnvVar(DiscordAuth.secretEnvKey) + guard let clientID = Environment.get(DiscordAuth.idEnvKey) else { + throw ImperialError.missingEnvVar(DiscordAuth.idEnvKey) + } + self.clientID = clientID - self.clientID = try Environment.get(DiscordAuth.idEnvKey).value(or: idError) - self.clientSecret = try Environment.get(DiscordAuth.secretEnvKey).value(or: secretError) + guard let clientSecret = Environment.get(DiscordAuth.secretEnvKey) else { + throw ImperialError.missingEnvVar(DiscordAuth.secretEnvKey) + } + self.clientSecret = clientSecret } } diff --git a/Sources/ImperialDropbox/DropboxAuth.swift b/Sources/ImperialDropbox/DropboxAuth.swift index e5222a8a..d3f228d1 100644 --- a/Sources/ImperialDropbox/DropboxAuth.swift +++ b/Sources/ImperialDropbox/DropboxAuth.swift @@ -7,10 +7,14 @@ public class DropboxAuth: FederatedServiceTokens { public var clientSecret: String public required init() throws { - let idError = ImperialError.missingEnvVar(DropboxAuth.idEnvKey) - let secretError = ImperialError.missingEnvVar(DropboxAuth.secretEnvKey) - - self.clientID = try Environment.get(DropboxAuth.idEnvKey).value(or: idError) - self.clientSecret = try Environment.get(DropboxAuth.secretEnvKey).value(or: secretError) + guard let clientID = Environment.get(DropboxAuth.idEnvKey) else { + throw ImperialError.missingEnvVar(DropboxAuth.idEnvKey) + } + self.clientID = clientID + + guard let clientSecret = Environment.get(DropboxAuth.secretEnvKey) else { + throw ImperialError.missingEnvVar(DropboxAuth.secretEnvKey) + } + self.clientSecret = clientSecret } } diff --git a/Sources/ImperialFacebook/FacebookAuth.swift b/Sources/ImperialFacebook/FacebookAuth.swift index 93bdfce0..25936909 100644 --- a/Sources/ImperialFacebook/FacebookAuth.swift +++ b/Sources/ImperialFacebook/FacebookAuth.swift @@ -7,10 +7,14 @@ public class FacebookAuth: FederatedServiceTokens { public var clientSecret: String public required init() throws { - let idError = ImperialError.missingEnvVar(FacebookAuth.idEnvKey) - let secretError = ImperialError.missingEnvVar(FacebookAuth.secretEnvKey) + guard let clientID = Environment.get(FacebookAuth.idEnvKey) else { + throw ImperialError.missingEnvVar(FacebookAuth.idEnvKey) + } + self.clientID = clientID - self.clientID = try Environment.get(FacebookAuth.idEnvKey).value(or: idError) - self.clientSecret = try Environment.get(FacebookAuth.secretEnvKey).value(or: secretError) + guard let clientSecret = Environment.get(FacebookAuth.secretEnvKey) else { + throw ImperialError.missingEnvVar(FacebookAuth.secretEnvKey) + } + self.clientSecret = clientSecret } } diff --git a/Sources/ImperialGitHub/GitHubAuth.swift b/Sources/ImperialGitHub/GitHubAuth.swift index 079dee00..6a3daa70 100644 --- a/Sources/ImperialGitHub/GitHubAuth.swift +++ b/Sources/ImperialGitHub/GitHubAuth.swift @@ -7,10 +7,14 @@ public class GitHubAuth: FederatedServiceTokens { public var clientSecret: String public required init() throws { - let idError = ImperialError.missingEnvVar(GitHubAuth.idEnvKey) - let secretError = ImperialError.missingEnvVar(GitHubAuth.secretEnvKey) - - self.clientID = try Environment.get(GitHubAuth.idEnvKey).value(or: idError) - self.clientSecret = try Environment.get(GitHubAuth.secretEnvKey).value(or: secretError) + guard let clientID = Environment.get(GitHubAuth.idEnvKey) else { + throw ImperialError.missingEnvVar(GitHubAuth.idEnvKey) + } + self.clientID = clientID + + guard let clientSecret = Environment.get(GitHubAuth.secretEnvKey) else { + throw ImperialError.missingEnvVar(GitHubAuth.secretEnvKey) + } + self.clientSecret = clientSecret } } diff --git a/Sources/ImperialGitlab/GitlabAuth.swift b/Sources/ImperialGitlab/GitlabAuth.swift index 0f91ec3a..cd81b987 100644 --- a/Sources/ImperialGitlab/GitlabAuth.swift +++ b/Sources/ImperialGitlab/GitlabAuth.swift @@ -7,10 +7,14 @@ public class GitlabAuth: FederatedServiceTokens { public var clientSecret: String public required init() throws { - let idError = ImperialError.missingEnvVar(GitlabAuth.idEnvKey) - let secretError = ImperialError.missingEnvVar(GitlabAuth.secretEnvKey) - - self.clientID = try Environment.get(GitlabAuth.idEnvKey).value(or: idError) - self.clientSecret = try Environment.get(GitlabAuth.secretEnvKey).value(or: secretError) + guard let clientID = Environment.get(GitlabAuth.idEnvKey) else { + throw ImperialError.missingEnvVar(GitlabAuth.idEnvKey) + } + self.clientID = clientID + + guard let clientSecret = Environment.get(GitlabAuth.secretEnvKey) else { + throw ImperialError.missingEnvVar(GitlabAuth.secretEnvKey) + } + self.clientSecret = clientSecret } } diff --git a/Sources/ImperialGoogle/JWT/GoogleJWTAuth.swift b/Sources/ImperialGoogle/JWT/GoogleJWTAuth.swift index 48c2f36e..f906365b 100644 --- a/Sources/ImperialGoogle/JWT/GoogleJWTAuth.swift +++ b/Sources/ImperialGoogle/JWT/GoogleJWTAuth.swift @@ -7,10 +7,14 @@ public class GoogleJWTAuth: FederatedServiceTokens { public var clientSecret: String public required init() throws { - let idError = ImperialError.missingEnvVar(GoogleJWTAuth.idEnvKey) - let secretError = ImperialError.missingEnvVar(GoogleJWTAuth.secretEnvKey) - - self.clientID = try Environment.get(GoogleJWTAuth.idEnvKey).value(or: idError) - self.clientSecret = try Environment.get(GoogleJWTAuth.secretEnvKey).value(or: secretError) + guard let clientID = Environment.get(GoogleJWTAuth.idEnvKey) else { + throw ImperialError.missingEnvVar(GoogleJWTAuth.idEnvKey) + } + self.clientID = clientID + + guard let clientSecret = Environment.get(GoogleJWTAuth.secretEnvKey) else { + throw ImperialError.missingEnvVar(GoogleJWTAuth.secretEnvKey) + } + self.clientSecret = clientSecret } } diff --git a/Sources/ImperialGoogle/Standard/GoogleAuth.swift b/Sources/ImperialGoogle/Standard/GoogleAuth.swift index 8f7680a0..1ee30a62 100644 --- a/Sources/ImperialGoogle/Standard/GoogleAuth.swift +++ b/Sources/ImperialGoogle/Standard/GoogleAuth.swift @@ -7,10 +7,14 @@ public class GoogleAuth: FederatedServiceTokens { public var clientSecret: String public required init() throws { - let idError = ImperialError.missingEnvVar(GoogleAuth.idEnvKey) - let secretError = ImperialError.missingEnvVar(GoogleAuth.secretEnvKey) - - self.clientID = try Environment.get(GoogleAuth.idEnvKey).value(or: idError) - self.clientSecret = try Environment.get(GoogleAuth.secretEnvKey).value(or: secretError) + guard let clientID = Environment.get(GoogleAuth.idEnvKey) else { + throw ImperialError.missingEnvVar(GoogleAuth.idEnvKey) + } + self.clientID = clientID + + guard let clientSecret = Environment.get(GoogleAuth.secretEnvKey) else { + throw ImperialError.missingEnvVar(GoogleAuth.secretEnvKey) + } + self.clientSecret = clientSecret } } diff --git a/Sources/ImperialKeycloak/KeycloakAuth.swift b/Sources/ImperialKeycloak/KeycloakAuth.swift index e36dad5b..48c078b4 100644 --- a/Sources/ImperialKeycloak/KeycloakAuth.swift +++ b/Sources/ImperialKeycloak/KeycloakAuth.swift @@ -11,14 +11,24 @@ public class KeycloakAuth: FederatedServiceTokens { public var authURL: String public required init() throws { - let idError = ImperialError.missingEnvVar(KeycloakAuth.idEnvKey) - let secretError = ImperialError.missingEnvVar(KeycloakAuth.secretEnvKey) - let tokenError = ImperialError.missingEnvVar(KeycloakAuth.accessTokenEnvURL) - let authError = ImperialError.missingEnvVar(KeycloakAuth.authEnvURL) - - self.clientID = try Environment.get(KeycloakAuth.idEnvKey).value(or: idError) - self.clientSecret = try Environment.get(KeycloakAuth.secretEnvKey).value(or: secretError) - self.accessTokenURL = try Environment.get(KeycloakAuth.accessTokenEnvURL).value(or: tokenError) - self.authURL = try Environment.get(KeycloakAuth.authEnvURL).value(or: authError) + guard let clientID = Environment.get(KeycloakAuth.idEnvKey) else { + throw ImperialError.missingEnvVar(KeycloakAuth.idEnvKey) + } + self.clientID = clientID + + guard let clientSecret = Environment.get(KeycloakAuth.secretEnvKey) else { + throw ImperialError.missingEnvVar(KeycloakAuth.secretEnvKey) + } + self.clientSecret = clientSecret + + guard let accessTokenURL = Environment.get(KeycloakAuth.accessTokenEnvURL) else { + throw ImperialError.missingEnvVar(KeycloakAuth.accessTokenEnvURL) + } + self.accessTokenURL = accessTokenURL + + guard let authURL = Environment.get(KeycloakAuth.authEnvURL) else { + throw ImperialError.missingEnvVar(KeycloakAuth.authEnvURL) + } + self.authURL = authURL } } diff --git a/Sources/ImperialMicrosoft/MicrosoftAuth.swift b/Sources/ImperialMicrosoft/MicrosoftAuth.swift index 45d41b0c..11c7f5a1 100644 --- a/Sources/ImperialMicrosoft/MicrosoftAuth.swift +++ b/Sources/ImperialMicrosoft/MicrosoftAuth.swift @@ -7,10 +7,14 @@ public class MicrosoftAuth: FederatedServiceTokens { public var clientSecret: String public required init() throws { - let idError = ImperialError.missingEnvVar(MicrosoftAuth.idEnvKey) - let secretError = ImperialError.missingEnvVar(MicrosoftAuth.secretEnvKey) - - self.clientID = try Environment.get(MicrosoftAuth.idEnvKey).value(or: idError) - self.clientSecret = try Environment.get(MicrosoftAuth.secretEnvKey).value(or: secretError) + guard let clientID = Environment.get(MicrosoftAuth.idEnvKey) else { + throw ImperialError.missingEnvVar(MicrosoftAuth.idEnvKey) + } + self.clientID = clientID + + guard let clientSecret = Environment.get(MicrosoftAuth.secretEnvKey) else { + throw ImperialError.missingEnvVar(MicrosoftAuth.secretEnvKey) + } + self.clientSecret = clientSecret } } diff --git a/Sources/ImperialShopify/ShopifyAuth.swift b/Sources/ImperialShopify/ShopifyAuth.swift index b2bc4350..cc944f4c 100644 --- a/Sources/ImperialShopify/ShopifyAuth.swift +++ b/Sources/ImperialShopify/ShopifyAuth.swift @@ -7,10 +7,14 @@ public class ShopifyAuth: FederatedServiceTokens { public var clientSecret: String public required init() throws { - let idError = ImperialError.missingEnvVar(ShopifyAuth.idEnvKey) - let secretError = ImperialError.missingEnvVar(ShopifyAuth.secretEnvKey) - - self.clientID = try Environment.get(ShopifyAuth.idEnvKey).value(or: idError) - self.clientSecret = try Environment.get(ShopifyAuth.secretEnvKey).value(or: secretError) + guard let clientID = Environment.get(ShopifyAuth.idEnvKey) else { + throw ImperialError.missingEnvVar(ShopifyAuth.idEnvKey) + } + self.clientID = clientID + + guard let clientSecret = Environment.get(ShopifyAuth.secretEnvKey) else { + throw ImperialError.missingEnvVar(ShopifyAuth.secretEnvKey) + } + self.clientSecret = clientSecret } } From 2bb84220e559475baa5e421dd7124c47197d97b5 Mon Sep 17 00:00:00 2001 From: Francesco Paolo Severino Date: Thu, 14 Nov 2024 19:38:56 +0100 Subject: [PATCH 16/37] Change existential return types to opaque (where possible) --- Sources/ImperialAuth0/Auth0.swift | 2 +- Sources/ImperialAuth0/Auth0Router.swift | 2 +- Sources/ImperialCore/Routing/FederatedServiceRouter.swift | 2 +- Sources/ImperialCore/ServiceRegister.swift | 2 +- Sources/ImperialCore/Services/FederatedService.swift | 2 +- Sources/ImperialDiscord/Discord.swift | 2 +- Sources/ImperialDiscord/DiscordRouter.swift | 2 +- Sources/ImperialDropbox/Dropbox.swift | 2 +- Sources/ImperialDropbox/DropboxRouter.swift | 2 +- Sources/ImperialFacebook/Facebook.swift | 2 +- Sources/ImperialFacebook/FacebookRouter.swift | 2 +- Sources/ImperialGitHub/GitHub.swift | 2 +- Sources/ImperialGitHub/GitHubRouter.swift | 2 +- Sources/ImperialGitlab/Gitlab.swift | 2 +- Sources/ImperialGitlab/GitlabRouter.swift | 2 +- Sources/ImperialGoogle/JWT/GoogleJWT.swift | 2 +- Sources/ImperialGoogle/JWT/GoogleJWTRouter.swift | 2 +- Sources/ImperialGoogle/Standard/Google.swift | 2 +- Sources/ImperialGoogle/Standard/GoogleRouter.swift | 2 +- Sources/ImperialKeycloak/Keycloak.swift | 2 +- Sources/ImperialKeycloak/KeycloakRouter.swift | 2 +- Sources/ImperialMicrosoft/Microsoft.swift | 2 +- Sources/ImperialMicrosoft/MicrosoftRouter.swift | 2 +- Sources/ImperialShopify/Shopify.swift | 2 +- Sources/ImperialShopify/ShopifyRouter.swift | 2 +- 25 files changed, 25 insertions(+), 25 deletions(-) diff --git a/Sources/ImperialAuth0/Auth0.swift b/Sources/ImperialAuth0/Auth0.swift index 879d90bd..52750af3 100644 --- a/Sources/ImperialAuth0/Auth0.swift +++ b/Sources/ImperialAuth0/Auth0.swift @@ -12,7 +12,7 @@ public class Auth0: FederatedService { authenticateCallback: ((Request) async throws -> Void)?, callback: String, scope: [String] = [], - completion: @escaping (Request, String) async throws -> any AsyncResponseEncodable + completion: @escaping (Request, String) async throws -> some AsyncResponseEncodable ) throws { self.router = try Auth0Router(callback: callback, completion: completion) self.tokens = self.router.tokens diff --git a/Sources/ImperialAuth0/Auth0Router.swift b/Sources/ImperialAuth0/Auth0Router.swift index 93be8fbc..1a9db3ca 100644 --- a/Sources/ImperialAuth0/Auth0Router.swift +++ b/Sources/ImperialAuth0/Auth0Router.swift @@ -17,7 +17,7 @@ public class Auth0Router: FederatedServiceRouter { return self.baseURL.finished(with: "/") + path } - public required init(callback: String, completion: @escaping (Request, String) async throws -> any AsyncResponseEncodable) throws { + public required init(callback: String, completion: @escaping (Request, String) async throws -> some AsyncResponseEncodable) throws { let auth = try Auth0Auth() self.tokens = auth self.baseURL = "https://\(auth.domain)" diff --git a/Sources/ImperialCore/Routing/FederatedServiceRouter.swift b/Sources/ImperialCore/Routing/FederatedServiceRouter.swift index b2970785..14d495ec 100644 --- a/Sources/ImperialCore/Routing/FederatedServiceRouter.swift +++ b/Sources/ImperialCore/Routing/FederatedServiceRouter.swift @@ -43,7 +43,7 @@ public protocol FederatedServiceRouter { /// - callback: The callback URL that the OAuth provider will redirect to after authenticating the user. /// - completion: The completion handler that will be fired at the end of the `callback` route. The access token is passed into it. /// - Throws: Any errors that could occur in the implementation. - init(callback: String, completion: @escaping (Request, String) async throws -> any AsyncResponseEncodable) throws + init(callback: String, completion: @escaping (Request, String) async throws -> some AsyncResponseEncodable) throws /// Configures the `authenticate` and `callback` routes with the droplet. /// diff --git a/Sources/ImperialCore/ServiceRegister.swift b/Sources/ImperialCore/ServiceRegister.swift index 273a4e1c..4c77619d 100644 --- a/Sources/ImperialCore/ServiceRegister.swift +++ b/Sources/ImperialCore/ServiceRegister.swift @@ -20,7 +20,7 @@ extension RoutesBuilder { authenticateCallback: ((Request) async throws -> Void)? = nil, callback: String, scope: [String] = [], - completion: @escaping (Request, String) async throws -> any AsyncResponseEncodable + completion: @escaping (Request, String) async throws -> some AsyncResponseEncodable ) throws where OAuthProvider: FederatedService { _ = try OAuthProvider( routes: self, diff --git a/Sources/ImperialCore/Services/FederatedService.swift b/Sources/ImperialCore/Services/FederatedService.swift index c3ef06e7..5b572916 100644 --- a/Sources/ImperialCore/Services/FederatedService.swift +++ b/Sources/ImperialCore/Services/FederatedService.swift @@ -42,5 +42,5 @@ public protocol FederatedService { /// - scope: The scopes to send to the provider to request access to. /// - completion: The completion handler that will fire at the end of the callback route. The access token is passed into the callback and the response that is returned will be returned from the callback route. This will usually be a redirect back to the app. /// - Throws: Any errors that occur in the implementation. - init(routes: some RoutesBuilder, authenticate: String, authenticateCallback: ((Request) async throws -> Void)?, callback: String, scope: [String], completion: @escaping (Request, String) async throws -> any AsyncResponseEncodable) throws + init(routes: some RoutesBuilder, authenticate: String, authenticateCallback: ((Request) async throws -> Void)?, callback: String, scope: [String], completion: @escaping (Request, String) async throws -> some AsyncResponseEncodable) throws } diff --git a/Sources/ImperialDiscord/Discord.swift b/Sources/ImperialDiscord/Discord.swift index 2c5f343e..a93ed3b3 100644 --- a/Sources/ImperialDiscord/Discord.swift +++ b/Sources/ImperialDiscord/Discord.swift @@ -12,7 +12,7 @@ public class Discord: FederatedService { authenticateCallback: ((Request) async throws -> Void)?, callback: String, scope: [String] = [], - completion: @escaping (Request, String) async throws -> any AsyncResponseEncodable + completion: @escaping (Request, String) async throws -> some AsyncResponseEncodable ) throws { self.router = try DiscordRouter(callback: callback, completion: completion) self.tokens = self.router.tokens diff --git a/Sources/ImperialDiscord/DiscordRouter.swift b/Sources/ImperialDiscord/DiscordRouter.swift index d728807a..f3667eca 100644 --- a/Sources/ImperialDiscord/DiscordRouter.swift +++ b/Sources/ImperialDiscord/DiscordRouter.swift @@ -13,7 +13,7 @@ public class DiscordRouter: FederatedServiceRouter { public let service: OAuthService = .discord public let callbackHeaders = HTTPHeaders([("Content-Type", "application/x-www-form-urlencoded")]) - public required init(callback: String, completion: @escaping (Request, String) async throws -> any AsyncResponseEncodable) throws { + public required init(callback: String, completion: @escaping (Request, String) async throws -> some AsyncResponseEncodable) throws { self.tokens = try DiscordAuth() self.callbackURL = callback self.callbackCompletion = completion diff --git a/Sources/ImperialDropbox/Dropbox.swift b/Sources/ImperialDropbox/Dropbox.swift index 8001bc17..4d872c17 100644 --- a/Sources/ImperialDropbox/Dropbox.swift +++ b/Sources/ImperialDropbox/Dropbox.swift @@ -12,7 +12,7 @@ public class Dropbox: FederatedService { authenticateCallback: ((Request) async throws -> Void)?, callback: String, scope: [String] = [], - completion: @escaping (Request, String) async throws -> any AsyncResponseEncodable + completion: @escaping (Request, String) async throws -> some AsyncResponseEncodable ) throws { self.router = try DropboxRouter(callback: callback, completion: completion) self.tokens = self.router.tokens diff --git a/Sources/ImperialDropbox/DropboxRouter.swift b/Sources/ImperialDropbox/DropboxRouter.swift index e1de7e36..1d713082 100644 --- a/Sources/ImperialDropbox/DropboxRouter.swift +++ b/Sources/ImperialDropbox/DropboxRouter.swift @@ -17,7 +17,7 @@ public class DropboxRouter: FederatedServiceRouter { public let service: OAuthService = .dropbox - public required init(callback: String, completion: @escaping (Request, String) async throws -> any AsyncResponseEncodable) throws { + public required init(callback: String, completion: @escaping (Request, String) async throws -> some AsyncResponseEncodable) throws { self.tokens = try DropboxAuth() self.callbackURL = callback self.callbackCompletion = completion diff --git a/Sources/ImperialFacebook/Facebook.swift b/Sources/ImperialFacebook/Facebook.swift index e1e64b56..326e670e 100644 --- a/Sources/ImperialFacebook/Facebook.swift +++ b/Sources/ImperialFacebook/Facebook.swift @@ -12,7 +12,7 @@ public class Facebook: FederatedService { authenticateCallback: ((Request) async throws -> Void)?, callback: String, scope: [String] = [], - completion: @escaping (Request, String) async throws -> any AsyncResponseEncodable + completion: @escaping (Request, String) async throws -> some AsyncResponseEncodable ) throws { self.router = try FacebookRouter(callback: callback, completion: completion) self.tokens = self.router.tokens diff --git a/Sources/ImperialFacebook/FacebookRouter.swift b/Sources/ImperialFacebook/FacebookRouter.swift index 7672f714..ac38b969 100644 --- a/Sources/ImperialFacebook/FacebookRouter.swift +++ b/Sources/ImperialFacebook/FacebookRouter.swift @@ -29,7 +29,7 @@ public class FacebookRouter: FederatedServiceRouter { return url.absoluteString } - public required init(callback: String, completion: @escaping (Request, String) async throws -> any AsyncResponseEncodable) throws { + public required init(callback: String, completion: @escaping (Request, String) async throws -> some AsyncResponseEncodable) throws { self.tokens = try FacebookAuth() self.callbackURL = callback self.callbackCompletion = completion diff --git a/Sources/ImperialGitHub/GitHub.swift b/Sources/ImperialGitHub/GitHub.swift index 1d0494b4..40dd0f98 100644 --- a/Sources/ImperialGitHub/GitHub.swift +++ b/Sources/ImperialGitHub/GitHub.swift @@ -12,7 +12,7 @@ public class GitHub: FederatedService { authenticateCallback: ((Request) async throws -> Void)?, callback: String, scope: [String] = [], - completion: @escaping (Request, String) async throws -> any AsyncResponseEncodable + completion: @escaping (Request, String) async throws -> some AsyncResponseEncodable ) throws { self.router = try GitHubRouter(callback: callback, completion: completion) self.tokens = self.router.tokens diff --git a/Sources/ImperialGitHub/GitHubRouter.swift b/Sources/ImperialGitHub/GitHubRouter.swift index 165394a2..e42e8101 100644 --- a/Sources/ImperialGitHub/GitHubRouter.swift +++ b/Sources/ImperialGitHub/GitHubRouter.swift @@ -16,7 +16,7 @@ public class GitHubRouter: FederatedServiceRouter { return headers }() - public required init(callback: String, completion: @escaping (Request, String) async throws -> any AsyncResponseEncodable) throws { + public required init(callback: String, completion: @escaping (Request, String) async throws -> some AsyncResponseEncodable) throws { self.tokens = try GitHubAuth() self.callbackURL = callback self.callbackCompletion = completion diff --git a/Sources/ImperialGitlab/Gitlab.swift b/Sources/ImperialGitlab/Gitlab.swift index f9c23d31..97c548fc 100644 --- a/Sources/ImperialGitlab/Gitlab.swift +++ b/Sources/ImperialGitlab/Gitlab.swift @@ -12,7 +12,7 @@ public class Gitlab: FederatedService { authenticateCallback: ((Request) async throws -> Void)?, callback: String, scope: [String] = [], - completion: @escaping (Request, String) async throws -> any AsyncResponseEncodable + completion: @escaping (Request, String) async throws -> some AsyncResponseEncodable ) throws { self.router = try GitlabRouter(callback: callback, completion: completion) self.tokens = self.router.tokens diff --git a/Sources/ImperialGitlab/GitlabRouter.swift b/Sources/ImperialGitlab/GitlabRouter.swift index df4a3e1e..14034425 100644 --- a/Sources/ImperialGitlab/GitlabRouter.swift +++ b/Sources/ImperialGitlab/GitlabRouter.swift @@ -12,7 +12,7 @@ public class GitlabRouter: FederatedServiceRouter { public let accessTokenURL: String = "\(GitlabRouter.baseURL.finished(with: "/"))oauth/token" public let service: OAuthService = .gitlab - public required init(callback: String, completion: @escaping (Request, String) async throws -> any AsyncResponseEncodable) throws { + public required init(callback: String, completion: @escaping (Request, String) async throws -> some AsyncResponseEncodable) throws { self.tokens = try GitlabAuth() self.callbackURL = callback self.callbackCompletion = completion diff --git a/Sources/ImperialGoogle/JWT/GoogleJWT.swift b/Sources/ImperialGoogle/JWT/GoogleJWT.swift index 709db8b2..3ed8d048 100644 --- a/Sources/ImperialGoogle/JWT/GoogleJWT.swift +++ b/Sources/ImperialGoogle/JWT/GoogleJWT.swift @@ -11,7 +11,7 @@ public class GoogleJWT: FederatedService { authenticateCallback: ((Request) async throws -> Void)?, callback: String, scope: [String] = [], - completion: @escaping (Request, String) async throws -> any AsyncResponseEncodable + completion: @escaping (Request, String) async throws -> some AsyncResponseEncodable ) throws { self.router = try GoogleJWTRouter(callback: callback, completion: completion) self.tokens = self.router.tokens diff --git a/Sources/ImperialGoogle/JWT/GoogleJWTRouter.swift b/Sources/ImperialGoogle/JWT/GoogleJWTRouter.swift index a15363b5..e1db5a51 100644 --- a/Sources/ImperialGoogle/JWT/GoogleJWTRouter.swift +++ b/Sources/ImperialGoogle/JWT/GoogleJWTRouter.swift @@ -18,7 +18,7 @@ public final class GoogleJWTRouter: FederatedServiceRouter { return headers }() - public init(callback: String, completion: @escaping (Request, String) async throws -> any AsyncResponseEncodable) throws { + public init(callback: String, completion: @escaping (Request, String) async throws -> some AsyncResponseEncodable) throws { self.tokens = try GoogleJWTAuth() self.callbackURL = callback self.authURL = callback diff --git a/Sources/ImperialGoogle/Standard/Google.swift b/Sources/ImperialGoogle/Standard/Google.swift index 67d3a06a..a5f1ed31 100644 --- a/Sources/ImperialGoogle/Standard/Google.swift +++ b/Sources/ImperialGoogle/Standard/Google.swift @@ -12,7 +12,7 @@ public class Google: FederatedService { authenticateCallback: ((Request) async throws -> Void)?, callback: String, scope: [String] = [], - completion: @escaping (Request, String) async throws -> any AsyncResponseEncodable + completion: @escaping (Request, String) async throws -> some AsyncResponseEncodable ) throws { self.router = try GoogleRouter(callback: callback, completion: completion) self.tokens = self.router.tokens diff --git a/Sources/ImperialGoogle/Standard/GoogleRouter.swift b/Sources/ImperialGoogle/Standard/GoogleRouter.swift index 30a8ec02..8466b052 100644 --- a/Sources/ImperialGoogle/Standard/GoogleRouter.swift +++ b/Sources/ImperialGoogle/Standard/GoogleRouter.swift @@ -14,7 +14,7 @@ public class GoogleRouter: FederatedServiceRouter { return headers }() - public required init(callback: String, completion: @escaping (Request, String) async throws -> any AsyncResponseEncodable) throws { + public required init(callback: String, completion: @escaping (Request, String) async throws -> some AsyncResponseEncodable) throws { self.tokens = try GoogleAuth() self.callbackURL = callback self.callbackCompletion = completion diff --git a/Sources/ImperialKeycloak/Keycloak.swift b/Sources/ImperialKeycloak/Keycloak.swift index 68de1fc1..d7a8f1f4 100644 --- a/Sources/ImperialKeycloak/Keycloak.swift +++ b/Sources/ImperialKeycloak/Keycloak.swift @@ -12,7 +12,7 @@ public class Keycloak: FederatedService { authenticateCallback: ((Request) async throws -> Void)?, callback: String, scope: [String] = [], - completion: @escaping (Request, String) async throws -> any AsyncResponseEncodable + completion: @escaping (Request, String) async throws -> some AsyncResponseEncodable ) throws { self.router = try KeycloakRouter(callback: callback, completion: completion) self.tokens = self.router.tokens diff --git a/Sources/ImperialKeycloak/KeycloakRouter.swift b/Sources/ImperialKeycloak/KeycloakRouter.swift index aaa7d575..e9215a42 100644 --- a/Sources/ImperialKeycloak/KeycloakRouter.swift +++ b/Sources/ImperialKeycloak/KeycloakRouter.swift @@ -10,7 +10,7 @@ public class KeycloakRouter: FederatedServiceRouter { public let accessTokenURL: String public let service: OAuthService = .keycloak - public required init(callback: String, completion: @escaping (Request, String) async throws -> any AsyncResponseEncodable) throws { + public required init(callback: String, completion: @escaping (Request, String) async throws -> some AsyncResponseEncodable) throws { self.tokens = try KeycloakAuth() self.keycloakTokens = self.tokens as! KeycloakAuth self.accessTokenURL = keycloakTokens.accessTokenURL diff --git a/Sources/ImperialMicrosoft/Microsoft.swift b/Sources/ImperialMicrosoft/Microsoft.swift index a6b5998f..8a0eb32a 100644 --- a/Sources/ImperialMicrosoft/Microsoft.swift +++ b/Sources/ImperialMicrosoft/Microsoft.swift @@ -12,7 +12,7 @@ public class Microsoft: FederatedService { authenticateCallback: ((Request) async throws -> Void)?, callback: String, scope: [String] = [], - completion: @escaping (Request, String) async throws -> any AsyncResponseEncodable + completion: @escaping (Request, String) async throws -> some AsyncResponseEncodable ) throws { self.router = try MicrosoftRouter(callback: callback, completion: completion) self.tokens = self.router.tokens diff --git a/Sources/ImperialMicrosoft/MicrosoftRouter.swift b/Sources/ImperialMicrosoft/MicrosoftRouter.swift index 4fecda3b..90bb7d36 100644 --- a/Sources/ImperialMicrosoft/MicrosoftRouter.swift +++ b/Sources/ImperialMicrosoft/MicrosoftRouter.swift @@ -16,7 +16,7 @@ public class MicrosoftRouter: FederatedServiceRouter { public required init( callback: String, - completion: @escaping (Request, String) async throws -> any AsyncResponseEncodable + completion: @escaping (Request, String) async throws -> some AsyncResponseEncodable ) throws { self.tokens = try MicrosoftAuth() self.callbackURL = callback diff --git a/Sources/ImperialShopify/Shopify.swift b/Sources/ImperialShopify/Shopify.swift index 6d30576e..26d305a2 100644 --- a/Sources/ImperialShopify/Shopify.swift +++ b/Sources/ImperialShopify/Shopify.swift @@ -14,7 +14,7 @@ public final class Shopify: FederatedService { authenticateCallback: ((Request) async throws -> Void)?, callback: String, scope: [String], - completion: @escaping (Request, String) async throws -> any AsyncResponseEncodable + completion: @escaping (Request, String) async throws -> some AsyncResponseEncodable ) throws { self.shopifyRouter = try ShopifyRouter(callback: callback, completion: completion) self.shopifyRouter.scope = scope diff --git a/Sources/ImperialShopify/ShopifyRouter.swift b/Sources/ImperialShopify/ShopifyRouter.swift index 5db49d81..d4fbfaab 100644 --- a/Sources/ImperialShopify/ShopifyRouter.swift +++ b/Sources/ImperialShopify/ShopifyRouter.swift @@ -9,7 +9,7 @@ public class ShopifyRouter: FederatedServiceRouter { public var accessTokenURL: String = "" public let service: OAuthService = .shopify - required public init(callback: String, completion: @escaping (Request, String) async throws -> any AsyncResponseEncodable) throws { + required public init(callback: String, completion: @escaping (Request, String) async throws -> some AsyncResponseEncodable) throws { self.tokens = try ShopifyAuth() self.callbackURL = callback self.callbackCompletion = completion From 91abbb30b6b2721f93ec44b04b62699f95176904 Mon Sep 17 00:00:00 2001 From: Francesco Paolo Severino Date: Thu, 14 Nov 2024 20:05:01 +0100 Subject: [PATCH 17/37] Remove `FullTypedThrows` --- Package.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Package.swift b/Package.swift index 71c0f0aa..2c366a6a 100755 --- a/Package.swift +++ b/Package.swift @@ -63,6 +63,5 @@ let package = Package( var swiftSettings: [SwiftSetting] { [ .enableUpcomingFeature("ExistentialAny"), - .enableUpcomingFeature("FullTypedThrows"), ] } \ No newline at end of file From 45634e6b70dfaccd46af3084514da89a9fc511b9 Mon Sep 17 00:00:00 2001 From: Francesco Paolo Severino Date: Fri, 15 Nov 2024 17:15:20 +0100 Subject: [PATCH 18/37] Remove superfluous `String` extension --- Sources/ImperialCore/Helpers/String+Tools.swift | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/Sources/ImperialCore/Helpers/String+Tools.swift b/Sources/ImperialCore/Helpers/String+Tools.swift index 5fa6f7ae..36be85ef 100644 --- a/Sources/ImperialCore/Helpers/String+Tools.swift +++ b/Sources/ImperialCore/Helpers/String+Tools.swift @@ -1,18 +1,4 @@ import Foundation -import RoutingKit - -extension String { - var pathComponents: [PathComponent] { - var pathComponentArray = [PathComponent]() - if let components = URL(string: self)?.pathComponents { - for item in components where item != "/" { - pathComponentArray.append(PathComponent(stringLiteral: item)) - } - } - return pathComponentArray - } - -} extension String.UTF8View: DataProtocol { public var regions: CollectionOfOne> { Array(self).regions } From 88553a309a9d00578891732eca38ee0e4556fce1 Mon Sep 17 00:00:00 2001 From: Francesco Paolo Severino Date: Fri, 15 Nov 2024 17:47:45 +0100 Subject: [PATCH 19/37] Improve error types --- .../ImperialCore/Errors/ImperialError.swift | 78 ++++++++++++---- .../ImperialCore/Errors/ServiceError.swift | 90 ++++++++++++++++--- .../Helpers/Request+Imperial.swift | 2 - Tests/ImperialTests/ImperialTests.swift | 12 ++- Tests/ImperialTests/ShopifyTests.swift | 2 +- 5 files changed, 149 insertions(+), 35 deletions(-) diff --git a/Sources/ImperialCore/Errors/ImperialError.swift b/Sources/ImperialCore/Errors/ImperialError.swift index c7fa4de4..45cbee1a 100644 --- a/Sources/ImperialCore/Errors/ImperialError.swift +++ b/Sources/ImperialCore/Errors/ImperialError.swift @@ -1,25 +1,67 @@ /// Represents various errors that can occur when attempting to unwrap an optional value. -public enum ImperialError: Error, CustomStringConvertible { - +public struct ImperialError: Error, Sendable, Equatable { + public struct ErrorType: Sendable, Hashable, CustomStringConvertible, Equatable { + enum Base: String, Sendable, Equatable { + case missingEnvVar + } + + let base: Base + + private init(_ base: Base) { + self.base = base + } + + public static let missingEnvVar = Self(.missingEnvVar) + + public var description: String { + base.rawValue + } + } + + private struct Backing: Sendable, Equatable { + fileprivate let errorType: ErrorType + fileprivate let variable: String? + + init(errorType: ErrorType, variable: String? = nil) { + self.errorType = errorType + self.variable = variable + } + + static func == (lhs: ImperialError.Backing, rhs: ImperialError.Backing) -> Bool { + lhs.errorType == rhs.errorType + } + } + + private var backing: Backing + + public var errorType: ErrorType { backing.errorType } + public var variable: String? { backing.variable } + + private init(backing: Backing) { + self.backing = backing + } + /// Thrown when no environment varibale is found with a given name. - /// - warning: This error is never thrown; rather, the application will fatal error. - case missingEnvVar(String) - - /// Thrown when we attempt to create a `FederatedCreatable` model and there is - /// no JSON in the response from the the request to `dataUri`. - case missingJSONFromResponse(String) - - - - /// Thrown when `request.fetch` is called with a type that has not been run through `request.create`. - case typeNotInitialized(String) - + public static func missingEnvVar(_ variable: String) -> Self { + .init(backing: .init(errorType: .missingEnvVar, variable: variable)) + } + + public static func == (lhs: ImperialError, rhs: ImperialError) -> Bool { + lhs.backing == rhs.backing + } +} + +extension ImperialError: CustomStringConvertible { /// A human readable version of the error thrown. public var description: String { - switch self { - case let .missingEnvVar(variable): return "Missing enviroment variable '\(variable)'" - case let .missingJSONFromResponse(uri): return "Reponse returned from '\(uri)' does not contain JSON" - case let .typeNotInitialized(type): return "No instence of type '\(type)' has been created" + var result = #"ImperialError(errorType: \#(self.errorType)"# + + if let variable { + result.append(", missing enviroment variable: \(variable)") } + + result.append(")") + + return result } } diff --git a/Sources/ImperialCore/Errors/ServiceError.swift b/Sources/ImperialCore/Errors/ServiceError.swift index dfb6febd..dacce070 100644 --- a/Sources/ImperialCore/Errors/ServiceError.swift +++ b/Sources/ImperialCore/Errors/ServiceError.swift @@ -1,20 +1,84 @@ /// Represents an error that occurs during a service action. -public enum ServiceError: Error, CustomStringConvertible { - +public struct ServiceError: Error, Sendable, Equatable { + public struct ErrorType: Sendable, Hashable, CustomStringConvertible, Equatable { + enum Base: String, Sendable, Equatable { + case noServiceFound + case noServiceEndpoint + } + + let base: Base + + private init(_ base: Base) { + self.base = base + } + + public static let noServiceFound = Self(.noServiceFound) + public static let noServiceEndpoint = Self(.noServiceEndpoint) + + public var description: String { + base.rawValue + } + } + + private struct Backing: Sendable, Equatable { + fileprivate let errorType: ErrorType + fileprivate let name: String? + fileprivate let endpoint: String? + + init( + errorType: ErrorType, + name: String? = nil, + endpoint: String? = nil + ) { + self.errorType = errorType + self.name = name + self.endpoint = endpoint + } + + static func == (lhs: ServiceError.Backing, rhs: ServiceError.Backing) -> Bool { + lhs.errorType == rhs.errorType + } + } + + private var backing: Backing + + public var errorType: ErrorType { backing.errorType } + public var name: String? { backing.name } + public var endpoint: String? { backing.endpoint } + + private init(backing: Backing) { + self.backing = backing + } + /// Thrown when no service is registered with a given name. - case noServiceFound(String) - - /// Thrown when no `FederatedSewrvice` type is found whgen creating a `Service` from JSON. - case noExistingService(String) - + public static func noServiceFound(_ name: String) -> Self { + .init(backing: .init(errorType: .noServiceFound, name: name)) + } + /// Thrown when a `FederatedCreatable` type has a `serviceKey` that does not match any available endpoints in the service. - case noServiceEndpoint(String) - + public static func noServiceEndpoint(_ endpoint: String) -> Self { + .init(backing: .init(errorType: .noServiceEndpoint, endpoint: endpoint)) + } + + public static func == (lhs: ServiceError, rhs: ServiceError) -> Bool { + lhs.backing == rhs.backing + } +} + +extension ServiceError: CustomStringConvertible { public var description: String { - switch self { - case let .noServiceFound(name): return "No service was found with the name '\(name)'" - case let .noExistingService(name): return "No service exists with the name '\(name)'" - case let .noServiceEndpoint(endpoint): return "Service does not have available endpoint for key '\(endpoint)'" + var result = #"ServiceError(errorType: \#(self.errorType)"# + + if let name { + result.append(", no service was found with the name: \(name)") + } + + if let endpoint { + result.append(", service does not have available endpoint for key: \(endpoint)") } + + result.append(")") + + return result } } diff --git a/Sources/ImperialCore/Helpers/Request+Imperial.swift b/Sources/ImperialCore/Helpers/Request+Imperial.swift index d31721e7..c621d793 100644 --- a/Sources/ImperialCore/Helpers/Request+Imperial.swift +++ b/Sources/ImperialCore/Helpers/Request+Imperial.swift @@ -29,8 +29,6 @@ extension Request { /// - Parameters: /// - model: A type that conforms to `FederatedCreatable`. /// - Returns: An instance of the type passed in that has been stored in the request. - /// - Throws: - /// - `ImperialError.typeNotInitialized`: If there is no value stored in the request for the type passed in. func fetch(_ model: T.Type) throws -> T { return try session.get("imperial-\(model)", as: T.self) } diff --git a/Tests/ImperialTests/ImperialTests.swift b/Tests/ImperialTests/ImperialTests.swift index 8ccde3d2..b0253317 100644 --- a/Tests/ImperialTests/ImperialTests.swift +++ b/Tests/ImperialTests/ImperialTests.swift @@ -2,6 +2,16 @@ import Testing @testable import ImperialCore +@Suite("ImperialCore Tests") struct ImperialTests { - @Test func empty() {} + @Test("ImperialError & ServiceError") + func errors() { + #expect(ImperialError.missingEnvVar("test").description == "ImperialError(errorType: missingEnvVar, missing enviroment variable: test)") + #expect(ImperialError.missingEnvVar("foo") == ImperialError.missingEnvVar("bar")) + + #expect(ServiceError.noServiceFound("test").description == "ServiceError(errorType: noServiceFound, no service was found with the name: test)") + #expect(ServiceError.noServiceEndpoint("test").description == "ServiceError(errorType: noServiceEndpoint, service does not have available endpoint for key: test)") + #expect(ServiceError.noServiceFound("foo") == ServiceError.noServiceFound("bar")) + #expect(ServiceError.noServiceEndpoint("foo") == ServiceError.noServiceEndpoint("bar")) + } } diff --git a/Tests/ImperialTests/ShopifyTests.swift b/Tests/ImperialTests/ShopifyTests.swift index dbde17b5..fa751327 100644 --- a/Tests/ImperialTests/ShopifyTests.swift +++ b/Tests/ImperialTests/ShopifyTests.swift @@ -3,7 +3,7 @@ import Testing @testable import ImperialShopify -@Suite("Shopify Tests") +@Suite("ImperialShopify Tests") struct ShopifyTests { @Test("Valid Shopify Domain") func domainCheck() throws { let domain = "davidmuzi.myshopify.com" From c824ad44a7b9ea32d5a4f0a94af9b246ba768d1b Mon Sep 17 00:00:00 2001 From: Francesco Paolo Severino Date: Sat, 16 Nov 2024 12:06:17 +0100 Subject: [PATCH 20/37] Introduce basic testing --- .env.testing | 23 ++++++ Package.swift | 16 ++++- Tests/ImperialTests/ImperialTests.swift | 96 +++++++++++++++++++++++++ Tests/ImperialTests/ShopifyTests.swift | 15 ++++ Tests/ImperialTests/withApp.swift | 24 +++++++ 5 files changed, 173 insertions(+), 1 deletion(-) create mode 100644 .env.testing create mode 100644 Tests/ImperialTests/withApp.swift diff --git a/.env.testing b/.env.testing new file mode 100644 index 00000000..efe045f8 --- /dev/null +++ b/.env.testing @@ -0,0 +1,23 @@ +GITHUB_CLIENT_ID=test +GITHUB_CLIENT_SECRET=test + +GOOGLE_CLIENT_ID=test +GOOGLE_CLIENT_SECRET=test + +SHOPIFY_CLIENT_ID=test +SHOPIFY_CLIENT_SECRET=test + +FACEBOOK_CLIENT_ID=test +FACEBOOK_CLIENT_SECRET=test + +KEYCLOAK_CLIENT_ID=test +KEYCLOAK_CLIENT_SECRET=test +KEYCLOAK_ACCESS_TOKEN_URL=test +KEYCLOAK_AUTH_URL=test + +DISCORD_CLIENT_ID=test +DISCORD_CLIENT_SECRET=test + +AUTH0_DOMAIN=test +AUTH0_CLIENT_ID=test +AUTH0_CLIENT_SECRET=test \ No newline at end of file diff --git a/Package.swift b/Package.swift index 2c366a6a..9da6072d 100755 --- a/Package.swift +++ b/Package.swift @@ -55,7 +55,21 @@ let package = Package( .target(name: "ImperialKeycloak", dependencies: ["ImperialCore"], swiftSettings: swiftSettings), .target(name: "ImperialMicrosoft", dependencies: ["ImperialCore"], swiftSettings: swiftSettings), .target(name: "ImperialShopify", dependencies: ["ImperialCore"], swiftSettings: swiftSettings), - .testTarget(name: "ImperialTests", dependencies: ["ImperialCore", "ImperialShopify"], swiftSettings: swiftSettings), + .testTarget( + name: "ImperialTests", + dependencies: [ + .target(name: "ImperialCore"), + .target(name: "ImperialGitHub"), + .target(name: "ImperialGoogle"), + .target(name: "ImperialShopify"), + .target(name: "ImperialFacebook"), + .target(name: "ImperialKeycloak"), + .target(name: "ImperialDiscord"), + .target(name: "ImperialAuth0"), + .product(name: "XCTVapor", package: "vapor"), + ], + swiftSettings: swiftSettings + ), ], swiftLanguageModes: [.v5] ) diff --git a/Tests/ImperialTests/ImperialTests.swift b/Tests/ImperialTests/ImperialTests.swift index b0253317..757b020c 100644 --- a/Tests/ImperialTests/ImperialTests.swift +++ b/Tests/ImperialTests/ImperialTests.swift @@ -1,3 +1,9 @@ +import ImperialGitHub +import ImperialGoogle +import ImperialFacebook +import ImperialKeycloak +import ImperialDiscord +import ImperialAuth0 import Testing @testable import ImperialCore @@ -14,4 +20,94 @@ struct ImperialTests { #expect(ServiceError.noServiceFound("foo") == ServiceError.noServiceFound("bar")) #expect(ServiceError.noServiceEndpoint("foo") == ServiceError.noServiceEndpoint("bar")) } + + @Test("GitHub Route") + func githubRoute() async throws { + try await withApp { app in + try await app.test(.GET, "/github", afterResponse: { res async throws in + #expect(res.status == .notFound) + }) + + try app.oAuth(from: GitHub.self, authenticate: "github", callback: "gh-auth-complete", redirect: "/") + + try await app.test(.GET, "/github", afterResponse: { res async throws in + #expect(res.status != .notFound) + }) + } + } + + @Test("Google Route") + func googleRoute() async throws { + try await withApp { app in + try await app.test(.GET, "/google", afterResponse: { res async throws in + #expect(res.status == .notFound) + }) + + try app.oAuth(from: Google.self, authenticate: "google", callback: "google-auth-complete", redirect: "/") + + try await app.test(.GET, "/google", afterResponse: { res async throws in + #expect(res.status != .notFound) + }) + } + } + + @Test("Facebook Route") + func facebookRoute() async throws { + try await withApp { app in + try await app.test(.GET, "/facebook", afterResponse: { res async throws in + #expect(res.status == .notFound) + }) + + try app.oAuth(from: Facebook.self, authenticate: "facebook", callback: "facebook-auth-complete", redirect: "/") + + try await app.test(.GET, "/facebook", afterResponse: { res async throws in + #expect(res.status != .notFound) + }) + } + } + + @Test("Keycloak Route") + func keycloakRoute() async throws { + try await withApp { app in + try await app.test(.GET, "/keycloak", afterResponse: { res async throws in + #expect(res.status == .notFound) + }) + + try app.oAuth(from: Keycloak.self, authenticate: "keycloak", callback: "keycloak-auth-complete", redirect: "/") + + try await app.test(.GET, "/keycloak", afterResponse: { res async throws in + #expect(res.status != .notFound) + }) + } + } + + @Test("Discord Route") + func discordRoute() async throws { + try await withApp { app in + try await app.test(.GET, "/discord", afterResponse: { res async throws in + #expect(res.status == .notFound) + }) + + try app.oAuth(from: Discord.self, authenticate: "discord", callback: "discord-auth-complete", redirect: "/") + + try await app.test(.GET, "/discord", afterResponse: { res async throws in + #expect(res.status != .notFound) + }) + } + } + + @Test("Auth0 Route") + func auth0Route() async throws { + try await withApp { app in + try await app.test(.GET, "/auth0", afterResponse: { res async throws in + #expect(res.status == .notFound) + }) + + try app.oAuth(from: Auth0.self, authenticate: "auth0", callback: "auth0-auth-complete", redirect: "/") + + try await app.test(.GET, "/auth0", afterResponse: { res async throws in + #expect(res.status != .notFound) + }) + } + } } diff --git a/Tests/ImperialTests/ShopifyTests.swift b/Tests/ImperialTests/ShopifyTests.swift index fa751327..507a9a5c 100644 --- a/Tests/ImperialTests/ShopifyTests.swift +++ b/Tests/ImperialTests/ShopifyTests.swift @@ -1,10 +1,25 @@ import Foundation import Testing +import XCTVapor @testable import ImperialShopify @Suite("ImperialShopify Tests") struct ShopifyTests { + @Test("Shopify Route") func shopifyRoute() async throws { + try await withApp { app in + try await app.test(.GET, "/shopify", afterResponse: { res async throws in + #expect(res.status == .notFound) + }) + + try app.oAuth(from: Shopify.self, authenticate: "shopify", callback: "shopify-auth-complete", redirect: "/") + + try await app.test(.GET, "/shopify", afterResponse: { res async throws in + #expect(res.status != .notFound) + }) + } + } + @Test("Valid Shopify Domain") func domainCheck() throws { let domain = "davidmuzi.myshopify.com" #expect(URL(string: domain)!.isValidShopifyDomain()) diff --git a/Tests/ImperialTests/withApp.swift b/Tests/ImperialTests/withApp.swift new file mode 100644 index 00000000..9ac634f4 --- /dev/null +++ b/Tests/ImperialTests/withApp.swift @@ -0,0 +1,24 @@ +import Testing +import Vapor + +func withApp(_ test: (Application) async throws -> ()) async throws { + let app = try await Application.make(.testing) + try #require(isLoggingConfigured) + do { + app.middleware.use(app.sessions.middleware) + try await test(app) + } catch { + try await app.asyncShutdown() + throw error + } + try await app.asyncShutdown() +} + +let isLoggingConfigured: Bool = { + LoggingSystem.bootstrap { label in + var handler = StreamLogHandler.standardOutput(label: label) + handler.logLevel = .debug + return handler + } + return true +}() \ No newline at end of file From f3e2e60d494dd36c8d26d43a752212c6c4416b1a Mon Sep 17 00:00:00 2001 From: Francesco Paolo Severino Date: Sat, 16 Nov 2024 13:13:04 +0100 Subject: [PATCH 21/37] Improve `OAuthService` sendability --- Sources/ImperialAuth0/Auth0.swift | 2 +- .../ImperialCore/Services/OAuthService.swift | 56 +++++-------------- Sources/ImperialDiscord/Discord.swift | 2 +- Sources/ImperialDropbox/Dropbox.swift | 2 +- Sources/ImperialFacebook/Facebook.swift | 2 +- Sources/ImperialGitHub/GitHub.swift | 2 +- Sources/ImperialGitlab/Gitlab.swift | 2 +- Sources/ImperialGoogle/JWT/GoogleJWT.swift | 2 +- Sources/ImperialGoogle/Standard/Google.swift | 2 +- Sources/ImperialKeycloak/Keycloak.swift | 2 +- Sources/ImperialMicrosoft/Microsoft.swift | 2 +- Sources/ImperialShopify/Shopify.swift | 2 +- 12 files changed, 26 insertions(+), 52 deletions(-) diff --git a/Sources/ImperialAuth0/Auth0.swift b/Sources/ImperialAuth0/Auth0.swift index 52750af3..3b38bf0a 100644 --- a/Sources/ImperialAuth0/Auth0.swift +++ b/Sources/ImperialAuth0/Auth0.swift @@ -20,6 +20,6 @@ public class Auth0: FederatedService { self.router.scope = scope try self.router.configureRoutes(withAuthURL: authenticate, authenticateCallback: authenticateCallback, on: routes) - OAuthService.register(.auth0) + OAuthService.services[OAuthService.auth0.name] = .auth0 } } diff --git a/Sources/ImperialCore/Services/OAuthService.swift b/Sources/ImperialCore/Services/OAuthService.swift index bdc9869d..7969321f 100644 --- a/Sources/ImperialCore/Services/OAuthService.swift +++ b/Sources/ImperialCore/Services/OAuthService.swift @@ -1,16 +1,23 @@ -import class NIO.ThreadSpecificVariable +import NIOConcurrencyHelpers import Vapor -fileprivate var services: ThreadSpecificVariable = .init(value: .init()) - /// Represents a service that interacts with an OAuth provider. -public struct OAuthService: Codable, Content { +public struct OAuthService: Codable, Content, Sendable { + static private let servicesBox: NIOLockedValueBox<[String: ImperialCore.OAuthService]> = .init([:]) /// The services that are available for use in the application. /// Services are added and fetched with the `Service.register` and `.get` static methods. - private static var services: OAuthServiceContainer { - get { ImperialCore.services.currentValue! } - set { ImperialCore.services.currentValue = newValue } + public package(set) static var services: [String: ImperialCore.OAuthService] { + get { + self.servicesBox.withLockedValue { services in + services + } + } + set { + self.servicesBox.withLockedValue { services in + services = newValue + } + } } @@ -43,7 +50,7 @@ public struct OAuthService: Codable, Content { } /// Syntax sugar for getting or setting one of the service's endpoints. - public subscript (key: String) -> String? { + public subscript(key: String) -> String? { get { return endpoints[key] } @@ -51,37 +58,4 @@ public struct OAuthService: Codable, Content { endpoints[key] = newValue } } - - /// Registers a service as available for use. - /// - /// - Parameter service: The service to register. - public static func register(_ service: OAuthService) { - #warning("It would be nice if this method could be internal") - self.services[service.name] = service - } - - /// Gets a service if it is available for use. - /// - /// - Parameter name: The name of the service to fetch. - /// - Returns: The service that matches the name passed in. - /// - Throws: `ImperialError.noServiceFound` if no service is found with the name passed in. - public static func get(service name: String) throws -> OAuthService { - guard let service = self.services[name] else { - throw ServiceError.noServiceFound(name) - } - return service - } -} - -private final class OAuthServiceContainer { - var services: [String: OAuthService] - - init() { - self.services = [:] - } - - subscript(service: String) -> OAuthService? { - get { self.services[service] } - set { self.services[service] = newValue } - } } diff --git a/Sources/ImperialDiscord/Discord.swift b/Sources/ImperialDiscord/Discord.swift index a93ed3b3..8e200a42 100644 --- a/Sources/ImperialDiscord/Discord.swift +++ b/Sources/ImperialDiscord/Discord.swift @@ -20,6 +20,6 @@ public class Discord: FederatedService { self.router.scope = scope try self.router.configureRoutes(withAuthURL: authenticate, authenticateCallback: authenticateCallback, on: routes) - OAuthService.register(.discord) + OAuthService.services[OAuthService.discord.name] = .discord } } diff --git a/Sources/ImperialDropbox/Dropbox.swift b/Sources/ImperialDropbox/Dropbox.swift index 4d872c17..43e6090a 100644 --- a/Sources/ImperialDropbox/Dropbox.swift +++ b/Sources/ImperialDropbox/Dropbox.swift @@ -20,6 +20,6 @@ public class Dropbox: FederatedService { self.router.scope = scope try self.router.configureRoutes(withAuthURL: authenticate, authenticateCallback: authenticateCallback, on: routes) - OAuthService.register(.dropbox) + OAuthService.services[OAuthService.dropbox.name] = .dropbox } } diff --git a/Sources/ImperialFacebook/Facebook.swift b/Sources/ImperialFacebook/Facebook.swift index 326e670e..7fac3dc1 100644 --- a/Sources/ImperialFacebook/Facebook.swift +++ b/Sources/ImperialFacebook/Facebook.swift @@ -20,7 +20,7 @@ public class Facebook: FederatedService { self.router.scope = scope try self.router.configureRoutes(withAuthURL: authenticate, authenticateCallback: authenticateCallback, on: routes) - OAuthService.register(.facebook) + OAuthService.services[OAuthService.facebook.name] = .facebook } } diff --git a/Sources/ImperialGitHub/GitHub.swift b/Sources/ImperialGitHub/GitHub.swift index 40dd0f98..1add1124 100644 --- a/Sources/ImperialGitHub/GitHub.swift +++ b/Sources/ImperialGitHub/GitHub.swift @@ -20,7 +20,7 @@ public class GitHub: FederatedService { self.router.scope = scope try self.router.configureRoutes(withAuthURL: authenticate, authenticateCallback: authenticateCallback, on: routes) - OAuthService.register(.github) + OAuthService.services[OAuthService.github.name] = .github } } diff --git a/Sources/ImperialGitlab/Gitlab.swift b/Sources/ImperialGitlab/Gitlab.swift index 97c548fc..08ff7acf 100644 --- a/Sources/ImperialGitlab/Gitlab.swift +++ b/Sources/ImperialGitlab/Gitlab.swift @@ -20,7 +20,7 @@ public class Gitlab: FederatedService { self.router.scope = scope try self.router.configureRoutes(withAuthURL: authenticate, authenticateCallback: authenticateCallback, on: routes) - OAuthService.register(.gitlab) + OAuthService.services[OAuthService.gitlab.name] = .gitlab } } diff --git a/Sources/ImperialGoogle/JWT/GoogleJWT.swift b/Sources/ImperialGoogle/JWT/GoogleJWT.swift index 3ed8d048..838bee67 100644 --- a/Sources/ImperialGoogle/JWT/GoogleJWT.swift +++ b/Sources/ImperialGoogle/JWT/GoogleJWT.swift @@ -19,6 +19,6 @@ public class GoogleJWT: FederatedService { self.router.scope = scope try self.router.configureRoutes(withAuthURL: authenticate, authenticateCallback: authenticateCallback, on: routes) - OAuthService.register(.google) + OAuthService.services[OAuthService.googleJWT.name] = .googleJWT } } diff --git a/Sources/ImperialGoogle/Standard/Google.swift b/Sources/ImperialGoogle/Standard/Google.swift index a5f1ed31..aa8daa4d 100644 --- a/Sources/ImperialGoogle/Standard/Google.swift +++ b/Sources/ImperialGoogle/Standard/Google.swift @@ -20,6 +20,6 @@ public class Google: FederatedService { self.router.scope = scope try self.router.configureRoutes(withAuthURL: authenticate, authenticateCallback: authenticateCallback, on: routes) - OAuthService.register(.google) + OAuthService.services[OAuthService.google.name] = .google } } diff --git a/Sources/ImperialKeycloak/Keycloak.swift b/Sources/ImperialKeycloak/Keycloak.swift index d7a8f1f4..c9a9e3b8 100644 --- a/Sources/ImperialKeycloak/Keycloak.swift +++ b/Sources/ImperialKeycloak/Keycloak.swift @@ -20,6 +20,6 @@ public class Keycloak: FederatedService { self.router.scope = scope try self.router.configureRoutes(withAuthURL: authenticate, authenticateCallback: authenticateCallback, on: routes) - OAuthService.register(.keycloak) + OAuthService.services[OAuthService.keycloak.name] = .keycloak } } diff --git a/Sources/ImperialMicrosoft/Microsoft.swift b/Sources/ImperialMicrosoft/Microsoft.swift index 8a0eb32a..fddd854b 100644 --- a/Sources/ImperialMicrosoft/Microsoft.swift +++ b/Sources/ImperialMicrosoft/Microsoft.swift @@ -20,6 +20,6 @@ public class Microsoft: FederatedService { self.router.scope = scope try self.router.configureRoutes(withAuthURL: authenticate, authenticateCallback: authenticateCallback, on: routes) - OAuthService.register(.microsoft) + OAuthService.services[OAuthService.microsoft.name] = .microsoft } } diff --git a/Sources/ImperialShopify/Shopify.swift b/Sources/ImperialShopify/Shopify.swift index 26d305a2..c90ce2ee 100644 --- a/Sources/ImperialShopify/Shopify.swift +++ b/Sources/ImperialShopify/Shopify.swift @@ -25,7 +25,7 @@ public final class Shopify: FederatedService { on: routes ) - OAuthService.register(.shopify) + OAuthService.services[OAuthService.shopify.name] = .shopify } } From c9199fc56f03879f68ab9819bfdeff239871c3a3 Mon Sep 17 00:00:00 2001 From: Francesco Paolo Severino Date: Sat, 16 Nov 2024 13:30:01 +0100 Subject: [PATCH 22/37] Add more basic tests --- .env.testing | 30 ++-- Package.swift | 11 +- .../ImperialCore/Errors/ServiceError.swift | 15 -- Tests/ImperialTests/ImperialTests.swift | 128 +++++++++++++----- 4 files changed, 123 insertions(+), 61 deletions(-) diff --git a/.env.testing b/.env.testing index efe045f8..a48a0841 100644 --- a/.env.testing +++ b/.env.testing @@ -1,23 +1,35 @@ +AUTH0_DOMAIN=test +AUTH0_CLIENT_ID=test +AUTH0_CLIENT_SECRET=test + +DISCORD_CLIENT_ID=test +DISCORD_CLIENT_SECRET=test + +DROPBOX_CLIENT_ID=test +DROPBOX_CLIENT_SECRET=test + +FACEBOOK_CLIENT_ID=test +FACEBOOK_CLIENT_SECRET=test + GITHUB_CLIENT_ID=test GITHUB_CLIENT_SECRET=test +GITLAB_CLIENT_ID=test +GITLAB_CLIENT_SECRET=test + GOOGLE_CLIENT_ID=test GOOGLE_CLIENT_SECRET=test +GOOGLEJWT_CLIENT_EMAIL=test +GOOGLEJWT_CLIENT_SECRET=test + SHOPIFY_CLIENT_ID=test SHOPIFY_CLIENT_SECRET=test -FACEBOOK_CLIENT_ID=test -FACEBOOK_CLIENT_SECRET=test - KEYCLOAK_CLIENT_ID=test KEYCLOAK_CLIENT_SECRET=test KEYCLOAK_ACCESS_TOKEN_URL=test KEYCLOAK_AUTH_URL=test -DISCORD_CLIENT_ID=test -DISCORD_CLIENT_SECRET=test - -AUTH0_DOMAIN=test -AUTH0_CLIENT_ID=test -AUTH0_CLIENT_SECRET=test \ No newline at end of file +MICROSOFT_CLIENT_ID=test +MICROSOFT_CLIENT_SECRET=test \ No newline at end of file diff --git a/Package.swift b/Package.swift index 9da6072d..ed230455 100755 --- a/Package.swift +++ b/Package.swift @@ -59,13 +59,16 @@ let package = Package( name: "ImperialTests", dependencies: [ .target(name: "ImperialCore"), + .target(name: "ImperialAuth0"), + .target(name: "ImperialDiscord"), + .target(name: "ImperialDropbox"), + .target(name: "ImperialFacebook"), .target(name: "ImperialGitHub"), + .target(name: "ImperialGitlab"), .target(name: "ImperialGoogle"), - .target(name: "ImperialShopify"), - .target(name: "ImperialFacebook"), .target(name: "ImperialKeycloak"), - .target(name: "ImperialDiscord"), - .target(name: "ImperialAuth0"), + .target(name: "ImperialMicrosoft"), + .target(name: "ImperialShopify"), .product(name: "XCTVapor", package: "vapor"), ], swiftSettings: swiftSettings diff --git a/Sources/ImperialCore/Errors/ServiceError.swift b/Sources/ImperialCore/Errors/ServiceError.swift index dacce070..344bdff7 100644 --- a/Sources/ImperialCore/Errors/ServiceError.swift +++ b/Sources/ImperialCore/Errors/ServiceError.swift @@ -2,7 +2,6 @@ public struct ServiceError: Error, Sendable, Equatable { public struct ErrorType: Sendable, Hashable, CustomStringConvertible, Equatable { enum Base: String, Sendable, Equatable { - case noServiceFound case noServiceEndpoint } @@ -12,7 +11,6 @@ public struct ServiceError: Error, Sendable, Equatable { self.base = base } - public static let noServiceFound = Self(.noServiceFound) public static let noServiceEndpoint = Self(.noServiceEndpoint) public var description: String { @@ -22,16 +20,13 @@ public struct ServiceError: Error, Sendable, Equatable { private struct Backing: Sendable, Equatable { fileprivate let errorType: ErrorType - fileprivate let name: String? fileprivate let endpoint: String? init( errorType: ErrorType, - name: String? = nil, endpoint: String? = nil ) { self.errorType = errorType - self.name = name self.endpoint = endpoint } @@ -43,18 +38,12 @@ public struct ServiceError: Error, Sendable, Equatable { private var backing: Backing public var errorType: ErrorType { backing.errorType } - public var name: String? { backing.name } public var endpoint: String? { backing.endpoint } private init(backing: Backing) { self.backing = backing } - /// Thrown when no service is registered with a given name. - public static func noServiceFound(_ name: String) -> Self { - .init(backing: .init(errorType: .noServiceFound, name: name)) - } - /// Thrown when a `FederatedCreatable` type has a `serviceKey` that does not match any available endpoints in the service. public static func noServiceEndpoint(_ endpoint: String) -> Self { .init(backing: .init(errorType: .noServiceEndpoint, endpoint: endpoint)) @@ -69,10 +58,6 @@ extension ServiceError: CustomStringConvertible { public var description: String { var result = #"ServiceError(errorType: \#(self.errorType)"# - if let name { - result.append(", no service was found with the name: \(name)") - } - if let endpoint { result.append(", service does not have available endpoint for key: \(endpoint)") } diff --git a/Tests/ImperialTests/ImperialTests.swift b/Tests/ImperialTests/ImperialTests.swift index 757b020c..1539526c 100644 --- a/Tests/ImperialTests/ImperialTests.swift +++ b/Tests/ImperialTests/ImperialTests.swift @@ -1,24 +1,77 @@ +import ImperialAuth0 +import ImperialDiscord +import ImperialDropbox +import ImperialFacebook import ImperialGitHub +import ImperialGitlab import ImperialGoogle -import ImperialFacebook import ImperialKeycloak -import ImperialDiscord -import ImperialAuth0 +import ImperialMicrosoft import Testing +import XCTVapor @testable import ImperialCore @Suite("ImperialCore Tests") struct ImperialTests { - @Test("ImperialError & ServiceError") - func errors() { - #expect(ImperialError.missingEnvVar("test").description == "ImperialError(errorType: missingEnvVar, missing enviroment variable: test)") - #expect(ImperialError.missingEnvVar("foo") == ImperialError.missingEnvVar("bar")) + @Test("Auth0 Route") + func auth0Route() async throws { + try await withApp { app in + try await app.test(.GET, "/auth0", afterResponse: { res async throws in + #expect(res.status == .notFound) + }) - #expect(ServiceError.noServiceFound("test").description == "ServiceError(errorType: noServiceFound, no service was found with the name: test)") - #expect(ServiceError.noServiceEndpoint("test").description == "ServiceError(errorType: noServiceEndpoint, service does not have available endpoint for key: test)") - #expect(ServiceError.noServiceFound("foo") == ServiceError.noServiceFound("bar")) - #expect(ServiceError.noServiceEndpoint("foo") == ServiceError.noServiceEndpoint("bar")) + try app.oAuth(from: Auth0.self, authenticate: "auth0", callback: "auth0-auth-complete", redirect: "/") + + try await app.test(.GET, "/auth0", afterResponse: { res async throws in + #expect(res.status != .notFound) + }) + } + } + + @Test("Discord Route") + func discordRoute() async throws { + try await withApp { app in + try await app.test(.GET, "/discord", afterResponse: { res async throws in + #expect(res.status == .notFound) + }) + + try app.oAuth(from: Discord.self, authenticate: "discord", callback: "discord-auth-complete", redirect: "/") + + try await app.test(.GET, "/discord", afterResponse: { res async throws in + #expect(res.status != .notFound) + }) + } + } + + @Test("Dropbox Route") + func dropboxRoute() async throws { + try await withApp { app in + try await app.test(.GET, "/dropbox", afterResponse: { res async throws in + #expect(res.status == .notFound) + }) + + try app.oAuth(from: Dropbox.self, authenticate: "dropbox", callback: "dropbox-auth-complete", redirect: "/") + + try await app.test(.GET, "/dropbox", afterResponse: { res async throws in + #expect(res.status != .notFound) + }) + } + } + + @Test("Facebook Route") + func facebookRoute() async throws { + try await withApp { app in + try await app.test(.GET, "/facebook", afterResponse: { res async throws in + #expect(res.status == .notFound) + }) + + try app.oAuth(from: Facebook.self, authenticate: "facebook", callback: "facebook-auth-complete", redirect: "/") + + try await app.test(.GET, "/facebook", afterResponse: { res async throws in + #expect(res.status != .notFound) + }) + } } @Test("GitHub Route") @@ -36,6 +89,21 @@ struct ImperialTests { } } + @Test("Gitlab Route") + func gitlabRoute() async throws { + try await withApp { app in + try await app.test(.GET, "/gitlab", afterResponse: { res async throws in + #expect(res.status == .notFound) + }) + + try app.oAuth(from: Gitlab.self, authenticate: "gitlab", callback: "gitlab-auth-complete", redirect: "/") + + try await app.test(.GET, "/gitlab", afterResponse: { res async throws in + #expect(res.status != .notFound) + }) + } + } + @Test("Google Route") func googleRoute() async throws { try await withApp { app in @@ -51,16 +119,16 @@ struct ImperialTests { } } - @Test("Facebook Route") - func facebookRoute() async throws { + @Test("Google JWT Route") + func googleJWTRoute() async throws { try await withApp { app in - try await app.test(.GET, "/facebook", afterResponse: { res async throws in + try await app.test(.GET, "/googleJWT", afterResponse: { res async throws in #expect(res.status == .notFound) }) - try app.oAuth(from: Facebook.self, authenticate: "facebook", callback: "facebook-auth-complete", redirect: "/") + try app.oAuth(from: GoogleJWT.self, authenticate: "googleJWT", callback: "googleJWT-auth-complete", redirect: "/") - try await app.test(.GET, "/facebook", afterResponse: { res async throws in + try await app.test(.GET, "/googleJWT", afterResponse: { res async throws in #expect(res.status != .notFound) }) } @@ -81,33 +149,27 @@ struct ImperialTests { } } - @Test("Discord Route") - func discordRoute() async throws { + @Test("Microsoft Route") + func microsoftRoute() async throws { try await withApp { app in - try await app.test(.GET, "/discord", afterResponse: { res async throws in + try await app.test(.GET, "/microsoft", afterResponse: { res async throws in #expect(res.status == .notFound) }) - try app.oAuth(from: Discord.self, authenticate: "discord", callback: "discord-auth-complete", redirect: "/") + try app.oAuth(from: Microsoft.self, authenticate: "microsoft", callback: "microsoft-auth-complete", redirect: "/") - try await app.test(.GET, "/discord", afterResponse: { res async throws in + try await app.test(.GET, "/microsoft", afterResponse: { res async throws in #expect(res.status != .notFound) }) } } - @Test("Auth0 Route") - func auth0Route() async throws { - try await withApp { app in - try await app.test(.GET, "/auth0", afterResponse: { res async throws in - #expect(res.status == .notFound) - }) - - try app.oAuth(from: Auth0.self, authenticate: "auth0", callback: "auth0-auth-complete", redirect: "/") + @Test("ImperialError & ServiceError") + func errors() { + #expect(ImperialError.missingEnvVar("test").description == "ImperialError(errorType: missingEnvVar, missing enviroment variable: test)") + #expect(ImperialError.missingEnvVar("foo") == ImperialError.missingEnvVar("bar")) - try await app.test(.GET, "/auth0", afterResponse: { res async throws in - #expect(res.status != .notFound) - }) - } + #expect(ServiceError.noServiceEndpoint("test").description == "ServiceError(errorType: noServiceEndpoint, service does not have available endpoint for key: test)") + #expect(ServiceError.noServiceEndpoint("foo") == ServiceError.noServiceEndpoint("bar")) } } From 9732bbf763a1b75a4de851dc70ec6a6d356e2c0f Mon Sep 17 00:00:00 2001 From: Francesco Paolo Severino Date: Sun, 17 Nov 2024 17:22:52 +0100 Subject: [PATCH 23/37] Make a lot of stuff `Sendable` --- Sources/ImperialAuth0/Auth0.swift | 4 ++-- Sources/ImperialAuth0/Auth0Auth.swift | 8 ++++---- Sources/ImperialAuth0/Auth0Router.swift | 7 +++---- .../Authenticatable/FederatedServiceTokens.swift | 7 +++---- .../Routing/FederatedServiceRouter.swift | 14 ++++++-------- Sources/ImperialCore/ServiceRegister.swift | 6 +++--- .../ImperialCore/Services/FederatedService.swift | 2 +- Sources/ImperialDiscord/Discord.swift | 4 ++-- Sources/ImperialDiscord/DiscordAuth.swift | 6 +++--- Sources/ImperialDiscord/DiscordRouter.swift | 7 +++---- Sources/ImperialDropbox/Dropbox.swift | 4 ++-- Sources/ImperialDropbox/DropboxAuth.swift | 6 +++--- Sources/ImperialDropbox/DropboxRouter.swift | 6 +++--- Sources/ImperialFacebook/Facebook.swift | 4 ++-- Sources/ImperialFacebook/FacebookAuth.swift | 6 +++--- Sources/ImperialFacebook/FacebookRouter.swift | 7 +++---- Sources/ImperialGitHub/GitHub.swift | 4 ++-- Sources/ImperialGitHub/GitHubAuth.swift | 6 +++--- Sources/ImperialGitHub/GitHubRouter.swift | 7 +++---- Sources/ImperialGitlab/Gitlab.swift | 4 ++-- Sources/ImperialGitlab/GitlabAuth.swift | 6 +++--- Sources/ImperialGitlab/GitlabRouter.swift | 7 +++---- Sources/ImperialGoogle/JWT/GoogleJWT.swift | 4 ++-- Sources/ImperialGoogle/JWT/GoogleJWTAuth.swift | 6 +++--- Sources/ImperialGoogle/JWT/GoogleJWTRouter.swift | 4 ++-- Sources/ImperialGoogle/Standard/Google.swift | 4 ++-- Sources/ImperialGoogle/Standard/GoogleAuth.swift | 6 +++--- Sources/ImperialGoogle/Standard/GoogleRouter.swift | 6 +++--- Sources/ImperialKeycloak/Keycloak.swift | 4 ++-- Sources/ImperialKeycloak/KeycloakAuth.swift | 10 +++++----- Sources/ImperialKeycloak/KeycloakRouter.swift | 6 +++--- Sources/ImperialMicrosoft/Microsoft.swift | 4 ++-- Sources/ImperialMicrosoft/MicrosoftAuth.swift | 6 +++--- Sources/ImperialMicrosoft/MicrosoftRouter.swift | 7 +++---- Sources/ImperialShopify/Shopify.swift | 4 ++-- Sources/ImperialShopify/ShopifyAuth.swift | 6 +++--- Sources/ImperialShopify/ShopifyRouter.swift | 7 +++---- 37 files changed, 103 insertions(+), 113 deletions(-) diff --git a/Sources/ImperialAuth0/Auth0.swift b/Sources/ImperialAuth0/Auth0.swift index 3b38bf0a..91ff0dcb 100644 --- a/Sources/ImperialAuth0/Auth0.swift +++ b/Sources/ImperialAuth0/Auth0.swift @@ -9,10 +9,10 @@ public class Auth0: FederatedService { public required init( routes: some RoutesBuilder, authenticate: String, - authenticateCallback: ((Request) async throws -> Void)?, + authenticateCallback: (@Sendable (Request) async throws -> Void)?, callback: String, scope: [String] = [], - completion: @escaping (Request, String) async throws -> some AsyncResponseEncodable + completion: @escaping @Sendable (Request, String) async throws -> some AsyncResponseEncodable ) throws { self.router = try Auth0Router(callback: callback, completion: completion) self.tokens = self.router.tokens diff --git a/Sources/ImperialAuth0/Auth0Auth.swift b/Sources/ImperialAuth0/Auth0Auth.swift index 2d4e5fe0..8c52559d 100644 --- a/Sources/ImperialAuth0/Auth0Auth.swift +++ b/Sources/ImperialAuth0/Auth0Auth.swift @@ -1,12 +1,12 @@ import Vapor -public class Auth0Auth: FederatedServiceTokens { +final public class Auth0Auth: FederatedServiceTokens { public static var domain: String = "AUTH0_DOMAIN" public static var idEnvKey: String = "AUTH0_CLIENT_ID" public static var secretEnvKey: String = "AUTH0_CLIENT_SECRET" - public var domain: String - public var clientID: String - public var clientSecret: String + public let domain: String + public let clientID: String + public let clientSecret: String public required init() throws { guard let domain = Environment.get(Auth0Auth.domain) else { diff --git a/Sources/ImperialAuth0/Auth0Router.swift b/Sources/ImperialAuth0/Auth0Router.swift index 1a9db3ca..045564d8 100644 --- a/Sources/ImperialAuth0/Auth0Router.swift +++ b/Sources/ImperialAuth0/Auth0Router.swift @@ -1,11 +1,10 @@ import Vapor import Foundation -public class Auth0Router: FederatedServiceRouter { - +final public class Auth0Router: FederatedServiceRouter { public let baseURL: String public let tokens: any FederatedServiceTokens - public let callbackCompletion: (Request, String) async throws -> any AsyncResponseEncodable + public let callbackCompletion: @Sendable (Request, String) async throws -> any AsyncResponseEncodable public var scope: [String] = [ ] public var requiredScopes = [ "openid" ] public let callbackURL: String @@ -17,7 +16,7 @@ public class Auth0Router: FederatedServiceRouter { return self.baseURL.finished(with: "/") + path } - public required init(callback: String, completion: @escaping (Request, String) async throws -> some AsyncResponseEncodable) throws { + public required init(callback: String, completion: @escaping @Sendable (Request, String) async throws -> some AsyncResponseEncodable) throws { let auth = try Auth0Auth() self.tokens = auth self.baseURL = "https://\(auth.domain)" diff --git a/Sources/ImperialCore/Authenticatable/FederatedServiceTokens.swift b/Sources/ImperialCore/Authenticatable/FederatedServiceTokens.swift index 2a711e7f..deefc4d5 100644 --- a/Sources/ImperialCore/Authenticatable/FederatedServiceTokens.swift +++ b/Sources/ImperialCore/Authenticatable/FederatedServiceTokens.swift @@ -36,13 +36,12 @@ from environment variables and stores them. } ``` */ -public protocol FederatedServiceTokens { - +public protocol FederatedServiceTokens: Sendable { /// The name of the environment variable that has the client ID. - static var idEnvKey: String { get set } + static var idEnvKey: String { get } /// The client ID for the OAuth provider that the service is connected to. - var clientID: String { get set } + var clientID: String { get } /// The name of the environment variable that has the client secret. static var secretEnvKey: String { get } diff --git a/Sources/ImperialCore/Routing/FederatedServiceRouter.swift b/Sources/ImperialCore/Routing/FederatedServiceRouter.swift index 14d495ec..b5d46599 100644 --- a/Sources/ImperialCore/Routing/FederatedServiceRouter.swift +++ b/Sources/ImperialCore/Routing/FederatedServiceRouter.swift @@ -3,14 +3,13 @@ import Vapor /// Defines a type that implements the routing to get an access token from an OAuth provider. /// See implementations in the `Services/(Google|GitHub)/$0Router.swift` files -public protocol FederatedServiceRouter { - +public protocol FederatedServiceRouter: Sendable { /// A class that gets the client ID and secret from environment variables. var tokens: any FederatedServiceTokens { get } /// The callback that is fired after the access token is fetched from the OAuth provider. /// The response that is returned from this callback is also returned from the callback route. - var callbackCompletion: (Request, String) async throws -> any AsyncResponseEncodable { get } + var callbackCompletion: @Sendable (Request, String) async throws -> any AsyncResponseEncodable { get } /// The scopes to get permission for when getting the access token. /// Usage of this property varies by provider. @@ -43,7 +42,7 @@ public protocol FederatedServiceRouter { /// - callback: The callback URL that the OAuth provider will redirect to after authenticating the user. /// - completion: The completion handler that will be fired at the end of the `callback` route. The access token is passed into it. /// - Throws: Any errors that could occur in the implementation. - init(callback: String, completion: @escaping (Request, String) async throws -> some AsyncResponseEncodable) throws + init(callback: String, completion: @escaping @Sendable (Request, String) async throws -> some AsyncResponseEncodable) throws /// Configures the `authenticate` and `callback` routes with the droplet. /// @@ -51,7 +50,7 @@ public protocol FederatedServiceRouter { /// - authURL: The URL for the route that will redirect the user to the OAuth provider. /// - authenticateCallback: Execute custom code within the authenticate closure before redirection. /// - Throws: N/A - func configureRoutes(withAuthURL authURL: String, authenticateCallback: ((Request) async throws -> Void)?, on router: some RoutesBuilder) throws + func configureRoutes(withAuthURL authURL: String, authenticateCallback: (@Sendable (Request) async throws -> Void)?, on router: some RoutesBuilder) throws /// Gets an access token from an OAuth provider. /// This method is the main body of the `callback` handler. @@ -68,16 +67,15 @@ public protocol FederatedServiceRouter { /// - Parameter request: The request from the OAuth provider. /// - Returns: A response that should redirect the user back to the app. /// - Throws: An errors that occur in the implementation code. - func callback(_ request: Request) async throws -> Response + @Sendable func callback(_ request: Request) async throws -> Response } extension FederatedServiceRouter { - public var codeKey: String { "code" } public var errorKey: String { "error" } public var callbackHeaders: HTTPHeaders { [:] } - public func configureRoutes(withAuthURL authURL: String, authenticateCallback: ((Request) async throws -> Void)?, on router: some RoutesBuilder) throws { + public func configureRoutes(withAuthURL authURL: String, authenticateCallback: (@Sendable (Request) async throws -> Void)?, on router: some RoutesBuilder) throws { router.get(callbackURL.pathComponents, use: callback) router.get(authURL.pathComponents) { req async throws -> Response in let redirect: Response = req.redirect(to: try self.authURL(req)) diff --git a/Sources/ImperialCore/ServiceRegister.swift b/Sources/ImperialCore/ServiceRegister.swift index 4c77619d..8f643123 100644 --- a/Sources/ImperialCore/ServiceRegister.swift +++ b/Sources/ImperialCore/ServiceRegister.swift @@ -17,10 +17,10 @@ extension RoutesBuilder { public func oAuth( from provider: OAuthProvider.Type, authenticate authUrl: String, - authenticateCallback: ((Request) async throws -> Void)? = nil, + authenticateCallback: (@Sendable (Request) async throws -> Void)? = nil, callback: String, scope: [String] = [], - completion: @escaping (Request, String) async throws -> some AsyncResponseEncodable + completion: @escaping @Sendable (Request, String) async throws -> some AsyncResponseEncodable ) throws where OAuthProvider: FederatedService { _ = try OAuthProvider( routes: self, @@ -46,7 +46,7 @@ extension RoutesBuilder { public func oAuth( from provider: OAuthProvider.Type, authenticate authUrl: String, - authenticateCallback: ((Request) async throws -> Void)? = nil, + authenticateCallback: (@Sendable (Request) async throws -> Void)? = nil, callback: String, scope: [String] = [], redirect redirectURL: String diff --git a/Sources/ImperialCore/Services/FederatedService.swift b/Sources/ImperialCore/Services/FederatedService.swift index 5b572916..3066f019 100644 --- a/Sources/ImperialCore/Services/FederatedService.swift +++ b/Sources/ImperialCore/Services/FederatedService.swift @@ -42,5 +42,5 @@ public protocol FederatedService { /// - scope: The scopes to send to the provider to request access to. /// - completion: The completion handler that will fire at the end of the callback route. The access token is passed into the callback and the response that is returned will be returned from the callback route. This will usually be a redirect back to the app. /// - Throws: Any errors that occur in the implementation. - init(routes: some RoutesBuilder, authenticate: String, authenticateCallback: ((Request) async throws -> Void)?, callback: String, scope: [String], completion: @escaping (Request, String) async throws -> some AsyncResponseEncodable) throws + init(routes: some RoutesBuilder, authenticate: String, authenticateCallback: (@Sendable (Request) async throws -> Void)?, callback: String, scope: [String], completion: @escaping @Sendable (Request, String) async throws -> some AsyncResponseEncodable) throws } diff --git a/Sources/ImperialDiscord/Discord.swift b/Sources/ImperialDiscord/Discord.swift index 8e200a42..0a9e9d0c 100644 --- a/Sources/ImperialDiscord/Discord.swift +++ b/Sources/ImperialDiscord/Discord.swift @@ -9,10 +9,10 @@ public class Discord: FederatedService { public required init( routes: some RoutesBuilder, authenticate: String, - authenticateCallback: ((Request) async throws -> Void)?, + authenticateCallback: (@Sendable (Request) async throws -> Void)?, callback: String, scope: [String] = [], - completion: @escaping (Request, String) async throws -> some AsyncResponseEncodable + completion: @escaping @Sendable (Request, String) async throws -> some AsyncResponseEncodable ) throws { self.router = try DiscordRouter(callback: callback, completion: completion) self.tokens = self.router.tokens diff --git a/Sources/ImperialDiscord/DiscordAuth.swift b/Sources/ImperialDiscord/DiscordAuth.swift index 4cf47bdb..0db2d94b 100644 --- a/Sources/ImperialDiscord/DiscordAuth.swift +++ b/Sources/ImperialDiscord/DiscordAuth.swift @@ -1,10 +1,10 @@ import Vapor -public class DiscordAuth: FederatedServiceTokens { +final public class DiscordAuth: FederatedServiceTokens { public static var idEnvKey: String = "DISCORD_CLIENT_ID" public static var secretEnvKey: String = "DISCORD_CLIENT_SECRET" - public var clientID: String - public var clientSecret: String + public let clientID: String + public let clientSecret: String public required init() throws { guard let clientID = Environment.get(DiscordAuth.idEnvKey) else { diff --git a/Sources/ImperialDiscord/DiscordRouter.swift b/Sources/ImperialDiscord/DiscordRouter.swift index f3667eca..64af1452 100644 --- a/Sources/ImperialDiscord/DiscordRouter.swift +++ b/Sources/ImperialDiscord/DiscordRouter.swift @@ -1,19 +1,18 @@ import Vapor import Foundation -public class DiscordRouter: FederatedServiceRouter { - +final public class DiscordRouter: FederatedServiceRouter { public static var baseURL: String = "https://discord.com/" public static var callbackURL: String = "callback" public let tokens: any FederatedServiceTokens - public let callbackCompletion: (Request, String) async throws -> any AsyncResponseEncodable + public let callbackCompletion: @Sendable (Request, String) async throws -> any AsyncResponseEncodable public var scope: [String] = [] public let callbackURL: String public let accessTokenURL: String = "\(DiscordRouter.baseURL.finished(with: "/"))api/oauth2/token" public let service: OAuthService = .discord public let callbackHeaders = HTTPHeaders([("Content-Type", "application/x-www-form-urlencoded")]) - public required init(callback: String, completion: @escaping (Request, String) async throws -> some AsyncResponseEncodable) throws { + public required init(callback: String, completion: @escaping @Sendable (Request, String) async throws -> some AsyncResponseEncodable) throws { self.tokens = try DiscordAuth() self.callbackURL = callback self.callbackCompletion = completion diff --git a/Sources/ImperialDropbox/Dropbox.swift b/Sources/ImperialDropbox/Dropbox.swift index 43e6090a..bd6e4ee9 100644 --- a/Sources/ImperialDropbox/Dropbox.swift +++ b/Sources/ImperialDropbox/Dropbox.swift @@ -9,10 +9,10 @@ public class Dropbox: FederatedService { public required init( routes: some RoutesBuilder, authenticate: String, - authenticateCallback: ((Request) async throws -> Void)?, + authenticateCallback: (@Sendable (Request) async throws -> Void)?, callback: String, scope: [String] = [], - completion: @escaping (Request, String) async throws -> some AsyncResponseEncodable + completion: @escaping @Sendable (Request, String) async throws -> some AsyncResponseEncodable ) throws { self.router = try DropboxRouter(callback: callback, completion: completion) self.tokens = self.router.tokens diff --git a/Sources/ImperialDropbox/DropboxAuth.swift b/Sources/ImperialDropbox/DropboxAuth.swift index d3f228d1..99a07cf8 100644 --- a/Sources/ImperialDropbox/DropboxAuth.swift +++ b/Sources/ImperialDropbox/DropboxAuth.swift @@ -1,10 +1,10 @@ import Vapor -public class DropboxAuth: FederatedServiceTokens { +final public class DropboxAuth: FederatedServiceTokens { public static var idEnvKey: String = "DROPBOX_CLIENT_ID" public static var secretEnvKey: String = "DROPBOX_CLIENT_SECRET" - public var clientID: String - public var clientSecret: String + public let clientID: String + public let clientSecret: String public required init() throws { guard let clientID = Environment.get(DropboxAuth.idEnvKey) else { diff --git a/Sources/ImperialDropbox/DropboxRouter.swift b/Sources/ImperialDropbox/DropboxRouter.swift index 1d713082..5fc259b4 100644 --- a/Sources/ImperialDropbox/DropboxRouter.swift +++ b/Sources/ImperialDropbox/DropboxRouter.swift @@ -1,9 +1,9 @@ import Vapor import Foundation -public class DropboxRouter: FederatedServiceRouter { +final public class DropboxRouter: FederatedServiceRouter { public let tokens: any FederatedServiceTokens - public let callbackCompletion: (Request, String) async throws -> any AsyncResponseEncodable + public let callbackCompletion: @Sendable (Request, String) async throws -> any AsyncResponseEncodable public var scope: [String] = [] public let callbackURL: String public let accessTokenURL: String = "https://api.dropboxapi.com/oauth2/token" @@ -17,7 +17,7 @@ public class DropboxRouter: FederatedServiceRouter { public let service: OAuthService = .dropbox - public required init(callback: String, completion: @escaping (Request, String) async throws -> some AsyncResponseEncodable) throws { + public required init(callback: String, completion: @escaping @Sendable (Request, String) async throws -> some AsyncResponseEncodable) throws { self.tokens = try DropboxAuth() self.callbackURL = callback self.callbackCompletion = completion diff --git a/Sources/ImperialFacebook/Facebook.swift b/Sources/ImperialFacebook/Facebook.swift index 7fac3dc1..04cf0522 100644 --- a/Sources/ImperialFacebook/Facebook.swift +++ b/Sources/ImperialFacebook/Facebook.swift @@ -9,10 +9,10 @@ public class Facebook: FederatedService { public required init( routes: some RoutesBuilder, authenticate: String, - authenticateCallback: ((Request) async throws -> Void)?, + authenticateCallback: (@Sendable (Request) async throws -> Void)?, callback: String, scope: [String] = [], - completion: @escaping (Request, String) async throws -> some AsyncResponseEncodable + completion: @escaping @Sendable (Request, String) async throws -> some AsyncResponseEncodable ) throws { self.router = try FacebookRouter(callback: callback, completion: completion) self.tokens = self.router.tokens diff --git a/Sources/ImperialFacebook/FacebookAuth.swift b/Sources/ImperialFacebook/FacebookAuth.swift index 25936909..a8e5d8c8 100644 --- a/Sources/ImperialFacebook/FacebookAuth.swift +++ b/Sources/ImperialFacebook/FacebookAuth.swift @@ -1,10 +1,10 @@ import Vapor -public class FacebookAuth: FederatedServiceTokens { +final public class FacebookAuth: FederatedServiceTokens { public static var idEnvKey: String = "FACEBOOK_CLIENT_ID" public static var secretEnvKey: String = "FACEBOOK_CLIENT_SECRET" - public var clientID: String - public var clientSecret: String + public let clientID: String + public let clientSecret: String public required init() throws { guard let clientID = Environment.get(FacebookAuth.idEnvKey) else { diff --git a/Sources/ImperialFacebook/FacebookRouter.swift b/Sources/ImperialFacebook/FacebookRouter.swift index ac38b969..1811c1e3 100644 --- a/Sources/ImperialFacebook/FacebookRouter.swift +++ b/Sources/ImperialFacebook/FacebookRouter.swift @@ -1,10 +1,9 @@ import Vapor import Foundation -public class FacebookRouter: FederatedServiceRouter { - +final public class FacebookRouter: FederatedServiceRouter { public let tokens: any FederatedServiceTokens - public let callbackCompletion: (Request, String) async throws -> any AsyncResponseEncodable + public let callbackCompletion: @Sendable (Request, String) async throws -> any AsyncResponseEncodable public var scope: [String] = [] public let callbackURL: String public var accessTokenURL: String = "https://graph.facebook.com/v3.2/oauth/access_token" @@ -29,7 +28,7 @@ public class FacebookRouter: FederatedServiceRouter { return url.absoluteString } - public required init(callback: String, completion: @escaping (Request, String) async throws -> some AsyncResponseEncodable) throws { + public required init(callback: String, completion: @escaping @Sendable (Request, String) async throws -> some AsyncResponseEncodable) throws { self.tokens = try FacebookAuth() self.callbackURL = callback self.callbackCompletion = completion diff --git a/Sources/ImperialGitHub/GitHub.swift b/Sources/ImperialGitHub/GitHub.swift index 1add1124..26bf3991 100644 --- a/Sources/ImperialGitHub/GitHub.swift +++ b/Sources/ImperialGitHub/GitHub.swift @@ -9,10 +9,10 @@ public class GitHub: FederatedService { public required init( routes: some RoutesBuilder, authenticate: String, - authenticateCallback: ((Request) async throws -> Void)?, + authenticateCallback: (@Sendable (Request) async throws -> Void)?, callback: String, scope: [String] = [], - completion: @escaping (Request, String) async throws -> some AsyncResponseEncodable + completion: @escaping @Sendable (Request, String) async throws -> some AsyncResponseEncodable ) throws { self.router = try GitHubRouter(callback: callback, completion: completion) self.tokens = self.router.tokens diff --git a/Sources/ImperialGitHub/GitHubAuth.swift b/Sources/ImperialGitHub/GitHubAuth.swift index 6a3daa70..589c3bd4 100644 --- a/Sources/ImperialGitHub/GitHubAuth.swift +++ b/Sources/ImperialGitHub/GitHubAuth.swift @@ -1,10 +1,10 @@ import Vapor -public class GitHubAuth: FederatedServiceTokens { +final public class GitHubAuth: FederatedServiceTokens { public static var idEnvKey: String = "GITHUB_CLIENT_ID" public static var secretEnvKey: String = "GITHUB_CLIENT_SECRET" - public var clientID: String - public var clientSecret: String + public let clientID: String + public let clientSecret: String public required init() throws { guard let clientID = Environment.get(GitHubAuth.idEnvKey) else { diff --git a/Sources/ImperialGitHub/GitHubRouter.swift b/Sources/ImperialGitHub/GitHubRouter.swift index e42e8101..9691745d 100644 --- a/Sources/ImperialGitHub/GitHubRouter.swift +++ b/Sources/ImperialGitHub/GitHubRouter.swift @@ -1,11 +1,10 @@ import Vapor import Foundation -public class GitHubRouter: FederatedServiceRouter { - +final public class GitHubRouter: FederatedServiceRouter { public static var baseURL: String = "https://github.com/" public let tokens: any FederatedServiceTokens - public let callbackCompletion: (Request, String) async throws -> any AsyncResponseEncodable + public let callbackCompletion: @Sendable (Request, String) async throws -> any AsyncResponseEncodable public var scope: [String] = [] public let callbackURL: String public let accessTokenURL: String = "\(GitHubRouter.baseURL.finished(with: "/"))login/oauth/access_token" @@ -16,7 +15,7 @@ public class GitHubRouter: FederatedServiceRouter { return headers }() - public required init(callback: String, completion: @escaping (Request, String) async throws -> some AsyncResponseEncodable) throws { + public required init(callback: String, completion: @escaping @Sendable (Request, String) async throws -> some AsyncResponseEncodable) throws { self.tokens = try GitHubAuth() self.callbackURL = callback self.callbackCompletion = completion diff --git a/Sources/ImperialGitlab/Gitlab.swift b/Sources/ImperialGitlab/Gitlab.swift index 08ff7acf..e749de7c 100644 --- a/Sources/ImperialGitlab/Gitlab.swift +++ b/Sources/ImperialGitlab/Gitlab.swift @@ -9,10 +9,10 @@ public class Gitlab: FederatedService { public required init( routes: some RoutesBuilder, authenticate: String, - authenticateCallback: ((Request) async throws -> Void)?, + authenticateCallback: (@Sendable (Request) async throws -> Void)?, callback: String, scope: [String] = [], - completion: @escaping (Request, String) async throws -> some AsyncResponseEncodable + completion: @escaping @Sendable (Request, String) async throws -> some AsyncResponseEncodable ) throws { self.router = try GitlabRouter(callback: callback, completion: completion) self.tokens = self.router.tokens diff --git a/Sources/ImperialGitlab/GitlabAuth.swift b/Sources/ImperialGitlab/GitlabAuth.swift index cd81b987..881a9bc3 100644 --- a/Sources/ImperialGitlab/GitlabAuth.swift +++ b/Sources/ImperialGitlab/GitlabAuth.swift @@ -1,10 +1,10 @@ import Vapor -public class GitlabAuth: FederatedServiceTokens { +final public class GitlabAuth: FederatedServiceTokens { public static var idEnvKey: String = "GITLAB_CLIENT_ID" public static var secretEnvKey: String = "GITLAB_CLIENT_SECRET" - public var clientID: String - public var clientSecret: String + public let clientID: String + public let clientSecret: String public required init() throws { guard let clientID = Environment.get(GitlabAuth.idEnvKey) else { diff --git a/Sources/ImperialGitlab/GitlabRouter.swift b/Sources/ImperialGitlab/GitlabRouter.swift index 14034425..09784033 100644 --- a/Sources/ImperialGitlab/GitlabRouter.swift +++ b/Sources/ImperialGitlab/GitlabRouter.swift @@ -1,18 +1,17 @@ import Vapor import Foundation -public class GitlabRouter: FederatedServiceRouter { - +final public class GitlabRouter: FederatedServiceRouter { public static var baseURL: String = "https://gitlab.com/" public static var callbackURL: String = "callback" public let tokens: any FederatedServiceTokens - public let callbackCompletion: (Request, String) async throws -> any AsyncResponseEncodable + public let callbackCompletion: @Sendable (Request, String) async throws -> any AsyncResponseEncodable public var scope: [String] = [] public let callbackURL: String public let accessTokenURL: String = "\(GitlabRouter.baseURL.finished(with: "/"))oauth/token" public let service: OAuthService = .gitlab - public required init(callback: String, completion: @escaping (Request, String) async throws -> some AsyncResponseEncodable) throws { + public required init(callback: String, completion: @escaping @Sendable (Request, String) async throws -> some AsyncResponseEncodable) throws { self.tokens = try GitlabAuth() self.callbackURL = callback self.callbackCompletion = completion diff --git a/Sources/ImperialGoogle/JWT/GoogleJWT.swift b/Sources/ImperialGoogle/JWT/GoogleJWT.swift index 838bee67..b8d497e1 100644 --- a/Sources/ImperialGoogle/JWT/GoogleJWT.swift +++ b/Sources/ImperialGoogle/JWT/GoogleJWT.swift @@ -8,10 +8,10 @@ public class GoogleJWT: FederatedService { public required init( routes: some RoutesBuilder, authenticate: String, - authenticateCallback: ((Request) async throws -> Void)?, + authenticateCallback: (@Sendable (Request) async throws -> Void)?, callback: String, scope: [String] = [], - completion: @escaping (Request, String) async throws -> some AsyncResponseEncodable + completion: @escaping @Sendable (Request, String) async throws -> some AsyncResponseEncodable ) throws { self.router = try GoogleJWTRouter(callback: callback, completion: completion) self.tokens = self.router.tokens diff --git a/Sources/ImperialGoogle/JWT/GoogleJWTAuth.swift b/Sources/ImperialGoogle/JWT/GoogleJWTAuth.swift index f906365b..b20a711e 100644 --- a/Sources/ImperialGoogle/JWT/GoogleJWTAuth.swift +++ b/Sources/ImperialGoogle/JWT/GoogleJWTAuth.swift @@ -1,10 +1,10 @@ import Vapor -public class GoogleJWTAuth: FederatedServiceTokens { +final public class GoogleJWTAuth: FederatedServiceTokens { public static var idEnvKey: String = "GOOGLEJWT_CLIENT_EMAIL" public static var secretEnvKey: String = "GOOGLEJWT_CLIENT_SECRET" - public var clientID: String - public var clientSecret: String + public let clientID: String + public let clientSecret: String public required init() throws { guard let clientID = Environment.get(GoogleJWTAuth.idEnvKey) else { diff --git a/Sources/ImperialGoogle/JWT/GoogleJWTRouter.swift b/Sources/ImperialGoogle/JWT/GoogleJWTRouter.swift index e1db5a51..ed6847cf 100644 --- a/Sources/ImperialGoogle/JWT/GoogleJWTRouter.swift +++ b/Sources/ImperialGoogle/JWT/GoogleJWTRouter.swift @@ -6,7 +6,7 @@ import JWTKit public final class GoogleJWTRouter: FederatedServiceRouter { public var tokens: any FederatedServiceTokens - public var callbackCompletion: (Request, String) async throws -> any AsyncResponseEncodable + public var callbackCompletion: @Sendable (Request, String) async throws -> any AsyncResponseEncodable public var scope: [String] = [] public var callbackURL: String public var accessTokenURL: String = "https://www.googleapis.com/oauth2/v4/token" @@ -18,7 +18,7 @@ public final class GoogleJWTRouter: FederatedServiceRouter { return headers }() - public init(callback: String, completion: @escaping (Request, String) async throws -> some AsyncResponseEncodable) throws { + public init(callback: String, completion: @escaping @Sendable (Request, String) async throws -> some AsyncResponseEncodable) throws { self.tokens = try GoogleJWTAuth() self.callbackURL = callback self.authURL = callback diff --git a/Sources/ImperialGoogle/Standard/Google.swift b/Sources/ImperialGoogle/Standard/Google.swift index aa8daa4d..de03f300 100644 --- a/Sources/ImperialGoogle/Standard/Google.swift +++ b/Sources/ImperialGoogle/Standard/Google.swift @@ -9,10 +9,10 @@ public class Google: FederatedService { public required init( routes: some RoutesBuilder, authenticate: String, - authenticateCallback: ((Request) async throws -> Void)?, + authenticateCallback: (@Sendable (Request) async throws -> Void)?, callback: String, scope: [String] = [], - completion: @escaping (Request, String) async throws -> some AsyncResponseEncodable + completion: @escaping @Sendable (Request, String) async throws -> some AsyncResponseEncodable ) throws { self.router = try GoogleRouter(callback: callback, completion: completion) self.tokens = self.router.tokens diff --git a/Sources/ImperialGoogle/Standard/GoogleAuth.swift b/Sources/ImperialGoogle/Standard/GoogleAuth.swift index 1ee30a62..96613a85 100644 --- a/Sources/ImperialGoogle/Standard/GoogleAuth.swift +++ b/Sources/ImperialGoogle/Standard/GoogleAuth.swift @@ -1,10 +1,10 @@ import Vapor -public class GoogleAuth: FederatedServiceTokens { +final public class GoogleAuth: FederatedServiceTokens { public static var idEnvKey: String = "GOOGLE_CLIENT_ID" public static var secretEnvKey: String = "GOOGLE_CLIENT_SECRET" - public var clientID: String - public var clientSecret: String + public let clientID: String + public let clientSecret: String public required init() throws { guard let clientID = Environment.get(GoogleAuth.idEnvKey) else { diff --git a/Sources/ImperialGoogle/Standard/GoogleRouter.swift b/Sources/ImperialGoogle/Standard/GoogleRouter.swift index 8466b052..2754bf5a 100644 --- a/Sources/ImperialGoogle/Standard/GoogleRouter.swift +++ b/Sources/ImperialGoogle/Standard/GoogleRouter.swift @@ -1,9 +1,9 @@ import Vapor import Foundation -public class GoogleRouter: FederatedServiceRouter { +final public class GoogleRouter: FederatedServiceRouter { public let tokens: any FederatedServiceTokens - public let callbackCompletion: (Request, String) async throws -> any AsyncResponseEncodable + public let callbackCompletion: @Sendable (Request, String) async throws -> any AsyncResponseEncodable public var scope: [String] = [] public let callbackURL: String public let accessTokenURL: String = "https://www.googleapis.com/oauth2/v4/token" @@ -14,7 +14,7 @@ public class GoogleRouter: FederatedServiceRouter { return headers }() - public required init(callback: String, completion: @escaping (Request, String) async throws -> some AsyncResponseEncodable) throws { + public required init(callback: String, completion: @escaping @Sendable (Request, String) async throws -> some AsyncResponseEncodable) throws { self.tokens = try GoogleAuth() self.callbackURL = callback self.callbackCompletion = completion diff --git a/Sources/ImperialKeycloak/Keycloak.swift b/Sources/ImperialKeycloak/Keycloak.swift index c9a9e3b8..11e798c6 100644 --- a/Sources/ImperialKeycloak/Keycloak.swift +++ b/Sources/ImperialKeycloak/Keycloak.swift @@ -9,10 +9,10 @@ public class Keycloak: FederatedService { public required init( routes: some RoutesBuilder, authenticate: String, - authenticateCallback: ((Request) async throws -> Void)?, + authenticateCallback: (@Sendable (Request) async throws -> Void)?, callback: String, scope: [String] = [], - completion: @escaping (Request, String) async throws -> some AsyncResponseEncodable + completion: @escaping @Sendable (Request, String) async throws -> some AsyncResponseEncodable ) throws { self.router = try KeycloakRouter(callback: callback, completion: completion) self.tokens = self.router.tokens diff --git a/Sources/ImperialKeycloak/KeycloakAuth.swift b/Sources/ImperialKeycloak/KeycloakAuth.swift index 48c078b4..0484ef72 100644 --- a/Sources/ImperialKeycloak/KeycloakAuth.swift +++ b/Sources/ImperialKeycloak/KeycloakAuth.swift @@ -1,14 +1,14 @@ import Vapor -public class KeycloakAuth: FederatedServiceTokens { +final public class KeycloakAuth: FederatedServiceTokens { public static var idEnvKey: String = "KEYCLOAK_CLIENT_ID" public static var secretEnvKey: String = "KEYCLOAK_CLIENT_SECRET" public static var accessTokenEnvURL: String = "KEYCLOAK_ACCESS_TOKEN_URL" public static var authEnvURL: String = "KEYCLOAK_AUTH_URL" - public var clientID: String - public var clientSecret: String - public var accessTokenURL: String - public var authURL: String + public let clientID: String + public let clientSecret: String + public let accessTokenURL: String + public let authURL: String public required init() throws { guard let clientID = Environment.get(KeycloakAuth.idEnvKey) else { diff --git a/Sources/ImperialKeycloak/KeycloakRouter.swift b/Sources/ImperialKeycloak/KeycloakRouter.swift index e9215a42..5c266e86 100644 --- a/Sources/ImperialKeycloak/KeycloakRouter.swift +++ b/Sources/ImperialKeycloak/KeycloakRouter.swift @@ -1,16 +1,16 @@ import Vapor import Foundation -public class KeycloakRouter: FederatedServiceRouter { +final public class KeycloakRouter: FederatedServiceRouter { public let tokens: any FederatedServiceTokens public let keycloakTokens: KeycloakAuth - public let callbackCompletion: (Request, String) async throws -> any AsyncResponseEncodable + public let callbackCompletion: @Sendable (Request, String) async throws -> any AsyncResponseEncodable public var scope: [String] = [] public let callbackURL: String public let accessTokenURL: String public let service: OAuthService = .keycloak - public required init(callback: String, completion: @escaping (Request, String) async throws -> some AsyncResponseEncodable) throws { + public required init(callback: String, completion: @escaping @Sendable (Request, String) async throws -> some AsyncResponseEncodable) throws { self.tokens = try KeycloakAuth() self.keycloakTokens = self.tokens as! KeycloakAuth self.accessTokenURL = keycloakTokens.accessTokenURL diff --git a/Sources/ImperialMicrosoft/Microsoft.swift b/Sources/ImperialMicrosoft/Microsoft.swift index fddd854b..5688bc97 100644 --- a/Sources/ImperialMicrosoft/Microsoft.swift +++ b/Sources/ImperialMicrosoft/Microsoft.swift @@ -9,10 +9,10 @@ public class Microsoft: FederatedService { public required init( routes: some RoutesBuilder, authenticate: String, - authenticateCallback: ((Request) async throws -> Void)?, + authenticateCallback: (@Sendable (Request) async throws -> Void)?, callback: String, scope: [String] = [], - completion: @escaping (Request, String) async throws -> some AsyncResponseEncodable + completion: @escaping @Sendable (Request, String) async throws -> some AsyncResponseEncodable ) throws { self.router = try MicrosoftRouter(callback: callback, completion: completion) self.tokens = self.router.tokens diff --git a/Sources/ImperialMicrosoft/MicrosoftAuth.swift b/Sources/ImperialMicrosoft/MicrosoftAuth.swift index 11c7f5a1..31932823 100644 --- a/Sources/ImperialMicrosoft/MicrosoftAuth.swift +++ b/Sources/ImperialMicrosoft/MicrosoftAuth.swift @@ -1,10 +1,10 @@ import Vapor -public class MicrosoftAuth: FederatedServiceTokens { +final public class MicrosoftAuth: FederatedServiceTokens { public static var idEnvKey: String = "MICROSOFT_CLIENT_ID" public static var secretEnvKey: String = "MICROSOFT_CLIENT_SECRET" - public var clientID: String - public var clientSecret: String + public let clientID: String + public let clientSecret: String public required init() throws { guard let clientID = Environment.get(MicrosoftAuth.idEnvKey) else { diff --git a/Sources/ImperialMicrosoft/MicrosoftRouter.swift b/Sources/ImperialMicrosoft/MicrosoftRouter.swift index 90bb7d36..8490b47e 100644 --- a/Sources/ImperialMicrosoft/MicrosoftRouter.swift +++ b/Sources/ImperialMicrosoft/MicrosoftRouter.swift @@ -1,12 +1,11 @@ import Vapor import Foundation -public class MicrosoftRouter: FederatedServiceRouter { - +final public class MicrosoftRouter: FederatedServiceRouter { public static var tenantIDEnvKey: String = "MICROSOFT_TENANT_ID" public let tokens: any FederatedServiceTokens - public let callbackCompletion: (Request, String) async throws -> any AsyncResponseEncodable + public let callbackCompletion: @Sendable (Request, String) async throws -> any AsyncResponseEncodable public var scope: [String] = [] public let callbackURL: String public var tenantID: String { Environment.get(MicrosoftRouter.tenantIDEnvKey) ?? "common" } @@ -16,7 +15,7 @@ public class MicrosoftRouter: FederatedServiceRouter { public required init( callback: String, - completion: @escaping (Request, String) async throws -> some AsyncResponseEncodable + completion: @escaping @Sendable (Request, String) async throws -> some AsyncResponseEncodable ) throws { self.tokens = try MicrosoftAuth() self.callbackURL = callback diff --git a/Sources/ImperialShopify/Shopify.swift b/Sources/ImperialShopify/Shopify.swift index c90ce2ee..ead3086b 100644 --- a/Sources/ImperialShopify/Shopify.swift +++ b/Sources/ImperialShopify/Shopify.swift @@ -11,10 +11,10 @@ public final class Shopify: FederatedService { public init( routes: some RoutesBuilder, authenticate: String, - authenticateCallback: ((Request) async throws -> Void)?, + authenticateCallback: (@Sendable (Request) async throws -> Void)?, callback: String, scope: [String], - completion: @escaping (Request, String) async throws -> some AsyncResponseEncodable + completion: @escaping @Sendable (Request, String) async throws -> some AsyncResponseEncodable ) throws { self.shopifyRouter = try ShopifyRouter(callback: callback, completion: completion) self.shopifyRouter.scope = scope diff --git a/Sources/ImperialShopify/ShopifyAuth.swift b/Sources/ImperialShopify/ShopifyAuth.swift index cc944f4c..9e50424c 100644 --- a/Sources/ImperialShopify/ShopifyAuth.swift +++ b/Sources/ImperialShopify/ShopifyAuth.swift @@ -1,10 +1,10 @@ import Vapor -public class ShopifyAuth: FederatedServiceTokens { +final public class ShopifyAuth: FederatedServiceTokens { public static var idEnvKey: String = "SHOPIFY_CLIENT_ID" public static var secretEnvKey: String = "SHOPIFY_CLIENT_SECRET" - public var clientID: String - public var clientSecret: String + public let clientID: String + public let clientSecret: String public required init() throws { guard let clientID = Environment.get(ShopifyAuth.idEnvKey) else { diff --git a/Sources/ImperialShopify/ShopifyRouter.swift b/Sources/ImperialShopify/ShopifyRouter.swift index d4fbfaab..b3eff38f 100644 --- a/Sources/ImperialShopify/ShopifyRouter.swift +++ b/Sources/ImperialShopify/ShopifyRouter.swift @@ -1,15 +1,14 @@ import Vapor -public class ShopifyRouter: FederatedServiceRouter { - +final public class ShopifyRouter: FederatedServiceRouter { public let tokens: any FederatedServiceTokens - public let callbackCompletion: (Request, String) async throws -> any AsyncResponseEncodable + public let callbackCompletion: @Sendable (Request, String) async throws -> any AsyncResponseEncodable public var scope: [String] = [] public let callbackURL: String public var accessTokenURL: String = "" public let service: OAuthService = .shopify - required public init(callback: String, completion: @escaping (Request, String) async throws -> some AsyncResponseEncodable) throws { + required public init(callback: String, completion: @escaping @Sendable (Request, String) async throws -> some AsyncResponseEncodable) throws { self.tokens = try ShopifyAuth() self.callbackURL = callback self.callbackCompletion = completion From a11f7c8c86cddb2102710fa145302ab5f220b443 Mon Sep 17 00:00:00 2001 From: Francesco Paolo Severino Date: Sun, 17 Nov 2024 18:05:18 +0100 Subject: [PATCH 24/37] Remove a lot of `Sendable` warnings --- Sources/ImperialAuth0/Auth0.swift | 3 +-- Sources/ImperialAuth0/Auth0Router.swift | 9 +++++---- .../Routing/FederatedServiceRouter.swift | 9 +++++++-- Sources/ImperialDiscord/Discord.swift | 3 +-- Sources/ImperialDiscord/DiscordRouter.swift | 5 +++-- Sources/ImperialDropbox/Dropbox.swift | 3 +-- Sources/ImperialDropbox/DropboxRouter.swift | 5 +++-- Sources/ImperialFacebook/Facebook.swift | 3 +-- Sources/ImperialFacebook/FacebookRouter.swift | 7 ++++--- Sources/ImperialGitHub/GitHub.swift | 3 +-- Sources/ImperialGitHub/GitHubRouter.swift | 7 ++++--- Sources/ImperialGitlab/Gitlab.swift | 3 +-- Sources/ImperialGitlab/GitlabRouter.swift | 7 ++++--- Sources/ImperialGoogle/JWT/GoogleJWT.swift | 3 +-- Sources/ImperialGoogle/JWT/GoogleJWTRouter.swift | 16 ++++++++-------- Sources/ImperialGoogle/Standard/Google.swift | 3 +-- .../ImperialGoogle/Standard/GoogleRouter.swift | 5 +++-- Sources/ImperialKeycloak/Keycloak.swift | 3 +-- Sources/ImperialKeycloak/KeycloakRouter.swift | 5 +++-- Sources/ImperialMicrosoft/Microsoft.swift | 3 +-- Sources/ImperialMicrosoft/MicrosoftRouter.swift | 4 +++- Sources/ImperialShopify/Shopify.swift | 3 +-- Sources/ImperialShopify/ShopifyRouter.swift | 5 +++-- 23 files changed, 61 insertions(+), 56 deletions(-) diff --git a/Sources/ImperialAuth0/Auth0.swift b/Sources/ImperialAuth0/Auth0.swift index 91ff0dcb..0a52b444 100644 --- a/Sources/ImperialAuth0/Auth0.swift +++ b/Sources/ImperialAuth0/Auth0.swift @@ -14,10 +14,9 @@ public class Auth0: FederatedService { scope: [String] = [], completion: @escaping @Sendable (Request, String) async throws -> some AsyncResponseEncodable ) throws { - self.router = try Auth0Router(callback: callback, completion: completion) + self.router = try Auth0Router(callback: callback, scope: scope, completion: completion) self.tokens = self.router.tokens - self.router.scope = scope try self.router.configureRoutes(withAuthURL: authenticate, authenticateCallback: authenticateCallback, on: routes) OAuthService.services[OAuthService.auth0.name] = .auth0 diff --git a/Sources/ImperialAuth0/Auth0Router.swift b/Sources/ImperialAuth0/Auth0Router.swift index 045564d8..e4eacd7b 100644 --- a/Sources/ImperialAuth0/Auth0Router.swift +++ b/Sources/ImperialAuth0/Auth0Router.swift @@ -5,24 +5,25 @@ final public class Auth0Router: FederatedServiceRouter { public let baseURL: String public let tokens: any FederatedServiceTokens public let callbackCompletion: @Sendable (Request, String) async throws -> any AsyncResponseEncodable - public var scope: [String] = [ ] - public var requiredScopes = [ "openid" ] + public let scope: [String] + public let requiredScopes = ["openid"] public let callbackURL: String public let accessTokenURL: String - public var service: OAuthService = .auth0 + public let service: OAuthService = .auth0 public let callbackHeaders = HTTPHeaders([("Content-Type", "application/x-www-form-urlencoded")]) private func providerUrl(path: String) -> String { return self.baseURL.finished(with: "/") + path } - public required init(callback: String, completion: @escaping @Sendable (Request, String) async throws -> some AsyncResponseEncodable) throws { + public required init(callback: String, scope: [String], completion: @escaping @Sendable (Request, String) async throws -> some AsyncResponseEncodable) throws { let auth = try Auth0Auth() self.tokens = auth self.baseURL = "https://\(auth.domain)" self.accessTokenURL = baseURL.finished(with: "/") + "oauth/token" self.callbackURL = callback self.callbackCompletion = completion + self.scope = scope } public func authURL(_ request: Request) throws -> String { diff --git a/Sources/ImperialCore/Routing/FederatedServiceRouter.swift b/Sources/ImperialCore/Routing/FederatedServiceRouter.swift index b5d46599..ba9d5e2c 100644 --- a/Sources/ImperialCore/Routing/FederatedServiceRouter.swift +++ b/Sources/ImperialCore/Routing/FederatedServiceRouter.swift @@ -13,7 +13,7 @@ public protocol FederatedServiceRouter: Sendable { /// The scopes to get permission for when getting the access token. /// Usage of this property varies by provider. - var scope: [String] { get set } + var scope: [String] { get } /// The key to acess the code URL query parameter var codeKey: String { get } @@ -40,9 +40,14 @@ public protocol FederatedServiceRouter: Sendable { /// /// - Parameters: /// - callback: The callback URL that the OAuth provider will redirect to after authenticating the user. + /// - scope: The scopes to get access to on authentication. /// - completion: The completion handler that will be fired at the end of the `callback` route. The access token is passed into it. /// - Throws: Any errors that could occur in the implementation. - init(callback: String, completion: @escaping @Sendable (Request, String) async throws -> some AsyncResponseEncodable) throws + init( + callback: String, + scope: [String], + completion: @escaping @Sendable (Request, String) async throws -> some AsyncResponseEncodable + ) throws /// Configures the `authenticate` and `callback` routes with the droplet. /// diff --git a/Sources/ImperialDiscord/Discord.swift b/Sources/ImperialDiscord/Discord.swift index 0a9e9d0c..f74e0741 100644 --- a/Sources/ImperialDiscord/Discord.swift +++ b/Sources/ImperialDiscord/Discord.swift @@ -14,10 +14,9 @@ public class Discord: FederatedService { scope: [String] = [], completion: @escaping @Sendable (Request, String) async throws -> some AsyncResponseEncodable ) throws { - self.router = try DiscordRouter(callback: callback, completion: completion) + self.router = try DiscordRouter(callback: callback, scope: scope, completion: completion) self.tokens = self.router.tokens - self.router.scope = scope try self.router.configureRoutes(withAuthURL: authenticate, authenticateCallback: authenticateCallback, on: routes) OAuthService.services[OAuthService.discord.name] = .discord diff --git a/Sources/ImperialDiscord/DiscordRouter.swift b/Sources/ImperialDiscord/DiscordRouter.swift index 64af1452..7d84f6fe 100644 --- a/Sources/ImperialDiscord/DiscordRouter.swift +++ b/Sources/ImperialDiscord/DiscordRouter.swift @@ -6,16 +6,17 @@ final public class DiscordRouter: FederatedServiceRouter { public static var callbackURL: String = "callback" public let tokens: any FederatedServiceTokens public let callbackCompletion: @Sendable (Request, String) async throws -> any AsyncResponseEncodable - public var scope: [String] = [] + public let scope: [String] public let callbackURL: String public let accessTokenURL: String = "\(DiscordRouter.baseURL.finished(with: "/"))api/oauth2/token" public let service: OAuthService = .discord public let callbackHeaders = HTTPHeaders([("Content-Type", "application/x-www-form-urlencoded")]) - public required init(callback: String, completion: @escaping @Sendable (Request, String) async throws -> some AsyncResponseEncodable) throws { + public required init(callback: String, scope: [String], completion: @escaping @Sendable (Request, String) async throws -> some AsyncResponseEncodable) throws { self.tokens = try DiscordAuth() self.callbackURL = callback self.callbackCompletion = completion + self.scope = scope } public func authURL(_ request: Request) throws -> String { diff --git a/Sources/ImperialDropbox/Dropbox.swift b/Sources/ImperialDropbox/Dropbox.swift index bd6e4ee9..4ac7f959 100644 --- a/Sources/ImperialDropbox/Dropbox.swift +++ b/Sources/ImperialDropbox/Dropbox.swift @@ -14,10 +14,9 @@ public class Dropbox: FederatedService { scope: [String] = [], completion: @escaping @Sendable (Request, String) async throws -> some AsyncResponseEncodable ) throws { - self.router = try DropboxRouter(callback: callback, completion: completion) + self.router = try DropboxRouter(callback: callback, scope: scope, completion: completion) self.tokens = self.router.tokens - self.router.scope = scope try self.router.configureRoutes(withAuthURL: authenticate, authenticateCallback: authenticateCallback, on: routes) OAuthService.services[OAuthService.dropbox.name] = .dropbox diff --git a/Sources/ImperialDropbox/DropboxRouter.swift b/Sources/ImperialDropbox/DropboxRouter.swift index 5fc259b4..0dacd439 100644 --- a/Sources/ImperialDropbox/DropboxRouter.swift +++ b/Sources/ImperialDropbox/DropboxRouter.swift @@ -4,7 +4,7 @@ import Foundation final public class DropboxRouter: FederatedServiceRouter { public let tokens: any FederatedServiceTokens public let callbackCompletion: @Sendable (Request, String) async throws -> any AsyncResponseEncodable - public var scope: [String] = [] + public let scope: [String] public let callbackURL: String public let accessTokenURL: String = "https://api.dropboxapi.com/oauth2/token" @@ -17,10 +17,11 @@ final public class DropboxRouter: FederatedServiceRouter { public let service: OAuthService = .dropbox - public required init(callback: String, completion: @escaping @Sendable (Request, String) async throws -> some AsyncResponseEncodable) throws { + public required init(callback: String, scope: [String], completion: @escaping @Sendable (Request, String) async throws -> some AsyncResponseEncodable) throws { self.tokens = try DropboxAuth() self.callbackURL = callback self.callbackCompletion = completion + self.scope = scope } public func authURL(_ request: Request) throws -> String { diff --git a/Sources/ImperialFacebook/Facebook.swift b/Sources/ImperialFacebook/Facebook.swift index 04cf0522..0e0c9bac 100644 --- a/Sources/ImperialFacebook/Facebook.swift +++ b/Sources/ImperialFacebook/Facebook.swift @@ -14,10 +14,9 @@ public class Facebook: FederatedService { scope: [String] = [], completion: @escaping @Sendable (Request, String) async throws -> some AsyncResponseEncodable ) throws { - self.router = try FacebookRouter(callback: callback, completion: completion) + self.router = try FacebookRouter(callback: callback, scope: scope, completion: completion) self.tokens = self.router.tokens - self.router.scope = scope try self.router.configureRoutes(withAuthURL: authenticate, authenticateCallback: authenticateCallback, on: routes) OAuthService.services[OAuthService.facebook.name] = .facebook diff --git a/Sources/ImperialFacebook/FacebookRouter.swift b/Sources/ImperialFacebook/FacebookRouter.swift index 1811c1e3..2c6b7a52 100644 --- a/Sources/ImperialFacebook/FacebookRouter.swift +++ b/Sources/ImperialFacebook/FacebookRouter.swift @@ -4,9 +4,9 @@ import Foundation final public class FacebookRouter: FederatedServiceRouter { public let tokens: any FederatedServiceTokens public let callbackCompletion: @Sendable (Request, String) async throws -> any AsyncResponseEncodable - public var scope: [String] = [] + public let scope: [String] public let callbackURL: String - public var accessTokenURL: String = "https://graph.facebook.com/v3.2/oauth/access_token" + public let accessTokenURL: String = "https://graph.facebook.com/v3.2/oauth/access_token" public let service: OAuthService = .facebook public func authURL(_ request: Request) throws -> String { @@ -28,10 +28,11 @@ final public class FacebookRouter: FederatedServiceRouter { return url.absoluteString } - public required init(callback: String, completion: @escaping @Sendable (Request, String) async throws -> some AsyncResponseEncodable) throws { + public required init(callback: String, scope: [String], completion: @escaping @Sendable (Request, String) async throws -> some AsyncResponseEncodable) throws { self.tokens = try FacebookAuth() self.callbackURL = callback self.callbackCompletion = completion + self.scope = scope } public func callbackBody(with code: String) -> any AsyncResponseEncodable { diff --git a/Sources/ImperialGitHub/GitHub.swift b/Sources/ImperialGitHub/GitHub.swift index 26bf3991..01b3f44b 100644 --- a/Sources/ImperialGitHub/GitHub.swift +++ b/Sources/ImperialGitHub/GitHub.swift @@ -14,10 +14,9 @@ public class GitHub: FederatedService { scope: [String] = [], completion: @escaping @Sendable (Request, String) async throws -> some AsyncResponseEncodable ) throws { - self.router = try GitHubRouter(callback: callback, completion: completion) + self.router = try GitHubRouter(callback: callback, scope: scope, completion: completion) self.tokens = self.router.tokens - self.router.scope = scope try self.router.configureRoutes(withAuthURL: authenticate, authenticateCallback: authenticateCallback, on: routes) OAuthService.services[OAuthService.github.name] = .github diff --git a/Sources/ImperialGitHub/GitHubRouter.swift b/Sources/ImperialGitHub/GitHubRouter.swift index 9691745d..cb7c717c 100644 --- a/Sources/ImperialGitHub/GitHubRouter.swift +++ b/Sources/ImperialGitHub/GitHubRouter.swift @@ -2,10 +2,10 @@ import Vapor import Foundation final public class GitHubRouter: FederatedServiceRouter { - public static var baseURL: String = "https://github.com/" + public static let baseURL: String = "https://github.com/" public let tokens: any FederatedServiceTokens public let callbackCompletion: @Sendable (Request, String) async throws -> any AsyncResponseEncodable - public var scope: [String] = [] + public let scope: [String] public let callbackURL: String public let accessTokenURL: String = "\(GitHubRouter.baseURL.finished(with: "/"))login/oauth/access_token" public let service: OAuthService = .github @@ -15,10 +15,11 @@ final public class GitHubRouter: FederatedServiceRouter { return headers }() - public required init(callback: String, completion: @escaping @Sendable (Request, String) async throws -> some AsyncResponseEncodable) throws { + public required init(callback: String, scope: [String], completion: @escaping @Sendable (Request, String) async throws -> some AsyncResponseEncodable) throws { self.tokens = try GitHubAuth() self.callbackURL = callback self.callbackCompletion = completion + self.scope = scope } public func authURL(_ request: Request) throws -> String { diff --git a/Sources/ImperialGitlab/Gitlab.swift b/Sources/ImperialGitlab/Gitlab.swift index e749de7c..a1108e34 100644 --- a/Sources/ImperialGitlab/Gitlab.swift +++ b/Sources/ImperialGitlab/Gitlab.swift @@ -14,10 +14,9 @@ public class Gitlab: FederatedService { scope: [String] = [], completion: @escaping @Sendable (Request, String) async throws -> some AsyncResponseEncodable ) throws { - self.router = try GitlabRouter(callback: callback, completion: completion) + self.router = try GitlabRouter(callback: callback, scope: scope, completion: completion) self.tokens = self.router.tokens - self.router.scope = scope try self.router.configureRoutes(withAuthURL: authenticate, authenticateCallback: authenticateCallback, on: routes) OAuthService.services[OAuthService.gitlab.name] = .gitlab diff --git a/Sources/ImperialGitlab/GitlabRouter.swift b/Sources/ImperialGitlab/GitlabRouter.swift index 09784033..5754b265 100644 --- a/Sources/ImperialGitlab/GitlabRouter.swift +++ b/Sources/ImperialGitlab/GitlabRouter.swift @@ -2,19 +2,20 @@ import Vapor import Foundation final public class GitlabRouter: FederatedServiceRouter { - public static var baseURL: String = "https://gitlab.com/" + public static let baseURL: String = "https://gitlab.com/" public static var callbackURL: String = "callback" public let tokens: any FederatedServiceTokens public let callbackCompletion: @Sendable (Request, String) async throws -> any AsyncResponseEncodable - public var scope: [String] = [] + public let scope: [String] public let callbackURL: String public let accessTokenURL: String = "\(GitlabRouter.baseURL.finished(with: "/"))oauth/token" public let service: OAuthService = .gitlab - public required init(callback: String, completion: @escaping @Sendable (Request, String) async throws -> some AsyncResponseEncodable) throws { + public required init(callback: String, scope: [String], completion: @escaping @Sendable (Request, String) async throws -> some AsyncResponseEncodable) throws { self.tokens = try GitlabAuth() self.callbackURL = callback self.callbackCompletion = completion + self.scope = scope } public func authURL(_ request: Request) throws -> String { diff --git a/Sources/ImperialGoogle/JWT/GoogleJWT.swift b/Sources/ImperialGoogle/JWT/GoogleJWT.swift index b8d497e1..ba78740d 100644 --- a/Sources/ImperialGoogle/JWT/GoogleJWT.swift +++ b/Sources/ImperialGoogle/JWT/GoogleJWT.swift @@ -13,10 +13,9 @@ public class GoogleJWT: FederatedService { scope: [String] = [], completion: @escaping @Sendable (Request, String) async throws -> some AsyncResponseEncodable ) throws { - self.router = try GoogleJWTRouter(callback: callback, completion: completion) + self.router = try GoogleJWTRouter(callback: callback, scope: scope, completion: completion) self.tokens = self.router.tokens - self.router.scope = scope try self.router.configureRoutes(withAuthURL: authenticate, authenticateCallback: authenticateCallback, on: routes) OAuthService.services[OAuthService.googleJWT.name] = .googleJWT diff --git a/Sources/ImperialGoogle/JWT/GoogleJWTRouter.swift b/Sources/ImperialGoogle/JWT/GoogleJWTRouter.swift index ed6847cf..c789459e 100644 --- a/Sources/ImperialGoogle/JWT/GoogleJWTRouter.swift +++ b/Sources/ImperialGoogle/JWT/GoogleJWTRouter.swift @@ -4,13 +4,12 @@ import Vapor import JWTKit public final class GoogleJWTRouter: FederatedServiceRouter { - - public var tokens: any FederatedServiceTokens - public var callbackCompletion: @Sendable (Request, String) async throws -> any AsyncResponseEncodable - public var scope: [String] = [] - public var callbackURL: String - public var accessTokenURL: String = "https://www.googleapis.com/oauth2/v4/token" - public var authURL: String + public let tokens: any FederatedServiceTokens + public let callbackCompletion: @Sendable (Request, String) async throws -> any AsyncResponseEncodable + public let scope: [String] + public let callbackURL: String + public let accessTokenURL: String = "https://www.googleapis.com/oauth2/v4/token" + public let authURL: String public let service: OAuthService = .googleJWT public let callbackHeaders: HTTPHeaders = { var headers = HTTPHeaders() @@ -18,11 +17,12 @@ public final class GoogleJWTRouter: FederatedServiceRouter { return headers }() - public init(callback: String, completion: @escaping @Sendable (Request, String) async throws -> some AsyncResponseEncodable) throws { + public init(callback: String, scope: [String], completion: @escaping @Sendable (Request, String) async throws -> some AsyncResponseEncodable) throws { self.tokens = try GoogleJWTAuth() self.callbackURL = callback self.authURL = callback self.callbackCompletion = completion + self.scope = scope } public func authURL(_ request: Request) throws -> String { diff --git a/Sources/ImperialGoogle/Standard/Google.swift b/Sources/ImperialGoogle/Standard/Google.swift index de03f300..4d9607b2 100644 --- a/Sources/ImperialGoogle/Standard/Google.swift +++ b/Sources/ImperialGoogle/Standard/Google.swift @@ -14,10 +14,9 @@ public class Google: FederatedService { scope: [String] = [], completion: @escaping @Sendable (Request, String) async throws -> some AsyncResponseEncodable ) throws { - self.router = try GoogleRouter(callback: callback, completion: completion) + self.router = try GoogleRouter(callback: callback, scope: scope, completion: completion) self.tokens = self.router.tokens - self.router.scope = scope try self.router.configureRoutes(withAuthURL: authenticate, authenticateCallback: authenticateCallback, on: routes) OAuthService.services[OAuthService.google.name] = .google diff --git a/Sources/ImperialGoogle/Standard/GoogleRouter.swift b/Sources/ImperialGoogle/Standard/GoogleRouter.swift index 2754bf5a..206f56d1 100644 --- a/Sources/ImperialGoogle/Standard/GoogleRouter.swift +++ b/Sources/ImperialGoogle/Standard/GoogleRouter.swift @@ -4,7 +4,7 @@ import Foundation final public class GoogleRouter: FederatedServiceRouter { public let tokens: any FederatedServiceTokens public let callbackCompletion: @Sendable (Request, String) async throws -> any AsyncResponseEncodable - public var scope: [String] = [] + public let scope: [String] public let callbackURL: String public let accessTokenURL: String = "https://www.googleapis.com/oauth2/v4/token" public let service: OAuthService = .google @@ -14,10 +14,11 @@ final public class GoogleRouter: FederatedServiceRouter { return headers }() - public required init(callback: String, completion: @escaping @Sendable (Request, String) async throws -> some AsyncResponseEncodable) throws { + public required init(callback: String, scope: [String], completion: @escaping @Sendable (Request, String) async throws -> some AsyncResponseEncodable) throws { self.tokens = try GoogleAuth() self.callbackURL = callback self.callbackCompletion = completion + self.scope = scope } public func authURL(_ request: Request) throws -> String { diff --git a/Sources/ImperialKeycloak/Keycloak.swift b/Sources/ImperialKeycloak/Keycloak.swift index 11e798c6..77c5d9e3 100644 --- a/Sources/ImperialKeycloak/Keycloak.swift +++ b/Sources/ImperialKeycloak/Keycloak.swift @@ -14,10 +14,9 @@ public class Keycloak: FederatedService { scope: [String] = [], completion: @escaping @Sendable (Request, String) async throws -> some AsyncResponseEncodable ) throws { - self.router = try KeycloakRouter(callback: callback, completion: completion) + self.router = try KeycloakRouter(callback: callback, scope: scope, completion: completion) self.tokens = self.router.tokens - self.router.scope = scope try self.router.configureRoutes(withAuthURL: authenticate, authenticateCallback: authenticateCallback, on: routes) OAuthService.services[OAuthService.keycloak.name] = .keycloak diff --git a/Sources/ImperialKeycloak/KeycloakRouter.swift b/Sources/ImperialKeycloak/KeycloakRouter.swift index 5c266e86..81490e06 100644 --- a/Sources/ImperialKeycloak/KeycloakRouter.swift +++ b/Sources/ImperialKeycloak/KeycloakRouter.swift @@ -5,17 +5,18 @@ final public class KeycloakRouter: FederatedServiceRouter { public let tokens: any FederatedServiceTokens public let keycloakTokens: KeycloakAuth public let callbackCompletion: @Sendable (Request, String) async throws -> any AsyncResponseEncodable - public var scope: [String] = [] + public let scope: [String] public let callbackURL: String public let accessTokenURL: String public let service: OAuthService = .keycloak - public required init(callback: String, completion: @escaping @Sendable (Request, String) async throws -> some AsyncResponseEncodable) throws { + public required init(callback: String, scope: [String], completion: @escaping @Sendable (Request, String) async throws -> some AsyncResponseEncodable) throws { self.tokens = try KeycloakAuth() self.keycloakTokens = self.tokens as! KeycloakAuth self.accessTokenURL = keycloakTokens.accessTokenURL self.callbackURL = callback self.callbackCompletion = completion + self.scope = scope } public func authURL(_ request: Request) throws -> String { diff --git a/Sources/ImperialMicrosoft/Microsoft.swift b/Sources/ImperialMicrosoft/Microsoft.swift index 5688bc97..bb17a266 100644 --- a/Sources/ImperialMicrosoft/Microsoft.swift +++ b/Sources/ImperialMicrosoft/Microsoft.swift @@ -14,10 +14,9 @@ public class Microsoft: FederatedService { scope: [String] = [], completion: @escaping @Sendable (Request, String) async throws -> some AsyncResponseEncodable ) throws { - self.router = try MicrosoftRouter(callback: callback, completion: completion) + self.router = try MicrosoftRouter(callback: callback, scope: scope, completion: completion) self.tokens = self.router.tokens - self.router.scope = scope try self.router.configureRoutes(withAuthURL: authenticate, authenticateCallback: authenticateCallback, on: routes) OAuthService.services[OAuthService.microsoft.name] = .microsoft diff --git a/Sources/ImperialMicrosoft/MicrosoftRouter.swift b/Sources/ImperialMicrosoft/MicrosoftRouter.swift index 8490b47e..28dcea86 100644 --- a/Sources/ImperialMicrosoft/MicrosoftRouter.swift +++ b/Sources/ImperialMicrosoft/MicrosoftRouter.swift @@ -6,7 +6,7 @@ final public class MicrosoftRouter: FederatedServiceRouter { public let tokens: any FederatedServiceTokens public let callbackCompletion: @Sendable (Request, String) async throws -> any AsyncResponseEncodable - public var scope: [String] = [] + public let scope: [String] public let callbackURL: String public var tenantID: String { Environment.get(MicrosoftRouter.tenantIDEnvKey) ?? "common" } public var accessTokenURL: String { "https://login.microsoftonline.com/\(self.tenantID)/oauth2/v2.0/token" } @@ -15,11 +15,13 @@ final public class MicrosoftRouter: FederatedServiceRouter { public required init( callback: String, + scope: [String], completion: @escaping @Sendable (Request, String) async throws -> some AsyncResponseEncodable ) throws { self.tokens = try MicrosoftAuth() self.callbackURL = callback self.callbackCompletion = completion + self.scope = scope } public func authURL(_ request: Request) throws -> String { diff --git a/Sources/ImperialShopify/Shopify.swift b/Sources/ImperialShopify/Shopify.swift index ead3086b..064a34b7 100644 --- a/Sources/ImperialShopify/Shopify.swift +++ b/Sources/ImperialShopify/Shopify.swift @@ -16,8 +16,7 @@ public final class Shopify: FederatedService { scope: [String], completion: @escaping @Sendable (Request, String) async throws -> some AsyncResponseEncodable ) throws { - self.shopifyRouter = try ShopifyRouter(callback: callback, completion: completion) - self.shopifyRouter.scope = scope + self.shopifyRouter = try ShopifyRouter(callback: callback, scope: scope, completion: completion) try self.router.configureRoutes( withAuthURL: authenticate, diff --git a/Sources/ImperialShopify/ShopifyRouter.swift b/Sources/ImperialShopify/ShopifyRouter.swift index b3eff38f..3af91b16 100644 --- a/Sources/ImperialShopify/ShopifyRouter.swift +++ b/Sources/ImperialShopify/ShopifyRouter.swift @@ -3,15 +3,16 @@ import Vapor final public class ShopifyRouter: FederatedServiceRouter { public let tokens: any FederatedServiceTokens public let callbackCompletion: @Sendable (Request, String) async throws -> any AsyncResponseEncodable - public var scope: [String] = [] + public let scope: [String] public let callbackURL: String public var accessTokenURL: String = "" public let service: OAuthService = .shopify - required public init(callback: String, completion: @escaping @Sendable (Request, String) async throws -> some AsyncResponseEncodable) throws { + required public init(callback: String, scope: [String], completion: @escaping @Sendable (Request, String) async throws -> some AsyncResponseEncodable) throws { self.tokens = try ShopifyAuth() self.callbackURL = callback self.callbackCompletion = completion + self.scope = scope } public func authURL(_ request: Request) throws -> String { From d70b8eaf3053c2e0b21c14569583535fe9ba0441 Mon Sep 17 00:00:00 2001 From: Francesco Paolo Severino Date: Wed, 20 Nov 2024 21:27:47 +0100 Subject: [PATCH 25/37] Make `ShopifyRouter` fully `Sendable` --- Sources/ImperialShopify/ShopifyRouter.swift | 25 ++++++++------------- Sources/ImperialShopify/URL+Shopify.swift | 2 +- Tests/ImperialTests/ShopifyTests.swift | 16 ++++++------- 3 files changed, 18 insertions(+), 25 deletions(-) diff --git a/Sources/ImperialShopify/ShopifyRouter.swift b/Sources/ImperialShopify/ShopifyRouter.swift index 3af91b16..4c2488c2 100644 --- a/Sources/ImperialShopify/ShopifyRouter.swift +++ b/Sources/ImperialShopify/ShopifyRouter.swift @@ -5,7 +5,9 @@ final public class ShopifyRouter: FederatedServiceRouter { public let callbackCompletion: @Sendable (Request, String) async throws -> any AsyncResponseEncodable public let scope: [String] public let callbackURL: String - public var accessTokenURL: String = "" + // `accessTokenURL` used to be set inside `authURL` and read by `fetchToken` + // now `fetchToken` creates the `accessTokenURL` itself from the shop domain in the request + public let accessTokenURL: String = "" public let service: OAuthService = .shopify required public init(callback: String, scope: [String], completion: @escaping @Sendable (Request, String) async throws -> some AsyncResponseEncodable) throws { @@ -21,8 +23,10 @@ final public class ShopifyRouter: FederatedServiceRouter { let nonce = String(UUID().uuidString.prefix(6)) try request.session.setNonce(nonce) - accessTokenURL = try accessTokenURLFrom(shop) - return try authURLFrom(shop, nonce: nonce).absoluteString + return "https://\(shop)/admin/oauth/authorize?" + "client_id=\(tokens.clientID)&" + + "scope=\(scope.joined(separator: ","))&" + + "redirect_uri=\(callbackURL)&" + + "state=\(nonce)" } public func callbackBody(with code: String) -> any AsyncResponseEncodable { @@ -46,12 +50,12 @@ final public class ShopifyRouter: FederatedServiceRouter { let nonce = request.session.nonce() guard state == nonce else { throw Abort(.badRequest) } } - guard URL(string: shop)?.isValidShopifyDomain() == true else { throw Abort(.badRequest) } + guard URL(string: shop)?.isValidShopifyDomain == true else { throw Abort(.badRequest) } guard URL(string: request.url.string)?.generateHMAC(key: tokens.clientSecret) == hmac else { throw Abort(.badRequest) } // exchange code for access token let body = callbackBody(with: code) - let url = URI(string: self.accessTokenURL) + let url = URI(string: "https://\(shop)/admin/oauth/access_token") let buffer = try await body.encodeResponse(for: request).body.buffer let response = try await request.client.post(url) { $0.body = buffer } return try response.content.get(String.self, at: ["access_token"]) @@ -72,15 +76,4 @@ final public class ShopifyRouter: FederatedServiceRouter { let response = try await self.callbackCompletion(request, accessToken) return try await response.encodeResponse(for: request) } - - private func authURLFrom(_ shop: String, nonce: String) throws -> URL { - return URL(string: "https://\(shop)/admin/oauth/authorize?" + "client_id=\(tokens.clientID)&" + - "scope=\(scope.joined(separator: ","))&" + - "redirect_uri=\(callbackURL)&" + - "state=\(nonce)")! - } - - private func accessTokenURLFrom(_ shop: String) throws -> String { - return "https://\(shop)/admin/oauth/access_token" - } } diff --git a/Sources/ImperialShopify/URL+Shopify.swift b/Sources/ImperialShopify/URL+Shopify.swift index 2dd8d4bb..fdf2883c 100644 --- a/Sources/ImperialShopify/URL+Shopify.swift +++ b/Sources/ImperialShopify/URL+Shopify.swift @@ -13,7 +13,7 @@ extension URL { return hmac.hexEncodedString() } - func isValidShopifyDomain() -> Bool { + var isValidShopifyDomain: Bool { let domain = "myshopify.com" guard absoluteString.suffix(domain.count) == domain else { return false } diff --git a/Tests/ImperialTests/ShopifyTests.swift b/Tests/ImperialTests/ShopifyTests.swift index 507a9a5c..ae203041 100644 --- a/Tests/ImperialTests/ShopifyTests.swift +++ b/Tests/ImperialTests/ShopifyTests.swift @@ -22,28 +22,28 @@ struct ShopifyTests { @Test("Valid Shopify Domain") func domainCheck() throws { let domain = "davidmuzi.myshopify.com" - #expect(URL(string: domain)!.isValidShopifyDomain()) + #expect(URL(string: domain)!.isValidShopifyDomain) let domain2 = "d4m3.myshopify.com" - #expect(URL(string: domain2)!.isValidShopifyDomain()) + #expect(URL(string: domain2)!.isValidShopifyDomain) let domain3 = "david-muzi.myshopify.com" - #expect(URL(string: domain3)!.isValidShopifyDomain()) + #expect(URL(string: domain3)!.isValidShopifyDomain) let domain4 = "david.muzi.myshopify.com" - #expect(URL(string: domain4)!.isValidShopifyDomain()) + #expect(URL(string: domain4)!.isValidShopifyDomain) let domain5 = "david#muzi.myshopify.com" - #expect(!URL(string: domain5)!.isValidShopifyDomain()) + #expect(!URL(string: domain5)!.isValidShopifyDomain) let domain6 = "davidmuzi.myshopify.com.ca" - #expect(!URL(string: domain6)!.isValidShopifyDomain()) + #expect(!URL(string: domain6)!.isValidShopifyDomain) let domain7 = "davidmuzi.square.com" - #expect(!URL(string: domain7)!.isValidShopifyDomain()) + #expect(!URL(string: domain7)!.isValidShopifyDomain) let domain8 = "david*muzi.shopify.ca" - #expect(!URL(string: domain8)!.isValidShopifyDomain()) + #expect(!URL(string: domain8)!.isValidShopifyDomain) } @Test("HMAC Validation") func hmacValidation() throws { From b368cee10982e8d7352e11bb4fb66e972b32d1b2 Mon Sep 17 00:00:00 2001 From: Francesco Paolo Severino Date: Wed, 20 Nov 2024 21:54:21 +0100 Subject: [PATCH 26/37] Enable Swift 6 language mode --- Package.swift | 3 +-- Sources/ImperialAuth0/Auth0Auth.swift | 6 +++--- Sources/ImperialAuth0/Auth0CallbackBody.swift | 2 +- Sources/ImperialDiscord/DiscordAuth.swift | 4 ++-- Sources/ImperialDiscord/DiscordRouter.swift | 7 +++---- Sources/ImperialDropbox/DropboxAuth.swift | 4 ++-- Sources/ImperialDropbox/DropboxCallbackBody.swift | 2 +- Sources/ImperialFacebook/FacebookAuth.swift | 4 ++-- Sources/ImperialFacebook/FacebookCallbackBody.swift | 2 +- Sources/ImperialGitHub/GitHubAuth.swift | 4 ++-- Sources/ImperialGitlab/GitlabAuth.swift | 4 ++-- Sources/ImperialGitlab/GitlabRouter.swift | 5 ++--- Sources/ImperialGoogle/JWT/GoogleJWTAuth.swift | 4 ++-- Sources/ImperialGoogle/Standard/GoogleAuth.swift | 4 ++-- Sources/ImperialGoogle/Standard/GoogleCallbackBody.swift | 2 +- Sources/ImperialKeycloak/KeycloakAuth.swift | 8 ++++---- Sources/ImperialKeycloak/KeycloakCallbackBody.swift | 2 +- Sources/ImperialMicrosoft/MicrosoftAuth.swift | 4 ++-- Sources/ImperialMicrosoft/MicrosoftCallbackBody.swift | 2 +- Sources/ImperialMicrosoft/MicrosoftRouter.swift | 2 +- Sources/ImperialShopify/ShopifyAuth.swift | 4 ++-- Tests/ImperialTests/ImperialTests.swift | 2 +- 22 files changed, 39 insertions(+), 42 deletions(-) diff --git a/Package.swift b/Package.swift index ed230455..db6996da 100755 --- a/Package.swift +++ b/Package.swift @@ -73,8 +73,7 @@ let package = Package( ], swiftSettings: swiftSettings ), - ], - swiftLanguageModes: [.v5] + ] ) var swiftSettings: [SwiftSetting] { diff --git a/Sources/ImperialAuth0/Auth0Auth.swift b/Sources/ImperialAuth0/Auth0Auth.swift index 8c52559d..6e42287e 100644 --- a/Sources/ImperialAuth0/Auth0Auth.swift +++ b/Sources/ImperialAuth0/Auth0Auth.swift @@ -1,9 +1,9 @@ import Vapor final public class Auth0Auth: FederatedServiceTokens { - public static var domain: String = "AUTH0_DOMAIN" - public static var idEnvKey: String = "AUTH0_CLIENT_ID" - public static var secretEnvKey: String = "AUTH0_CLIENT_SECRET" + public static let domain: String = "AUTH0_DOMAIN" + public static let idEnvKey: String = "AUTH0_CLIENT_ID" + public static let secretEnvKey: String = "AUTH0_CLIENT_SECRET" public let domain: String public let clientID: String public let clientSecret: String diff --git a/Sources/ImperialAuth0/Auth0CallbackBody.swift b/Sources/ImperialAuth0/Auth0CallbackBody.swift index a098c160..03f7710f 100644 --- a/Sources/ImperialAuth0/Auth0CallbackBody.swift +++ b/Sources/ImperialAuth0/Auth0CallbackBody.swift @@ -7,7 +7,7 @@ struct Auth0CallbackBody: Content { let redirectURI: String let grantType: String = "authorization_code" - static var defaultContentType: HTTPMediaType = .urlEncodedForm + static let defaultContentType: HTTPMediaType = .urlEncodedForm enum CodingKeys: String, CodingKey { case clientId = "client_id" diff --git a/Sources/ImperialDiscord/DiscordAuth.swift b/Sources/ImperialDiscord/DiscordAuth.swift index 0db2d94b..12931230 100644 --- a/Sources/ImperialDiscord/DiscordAuth.swift +++ b/Sources/ImperialDiscord/DiscordAuth.swift @@ -1,8 +1,8 @@ import Vapor final public class DiscordAuth: FederatedServiceTokens { - public static var idEnvKey: String = "DISCORD_CLIENT_ID" - public static var secretEnvKey: String = "DISCORD_CLIENT_SECRET" + public static let idEnvKey: String = "DISCORD_CLIENT_ID" + public static let secretEnvKey: String = "DISCORD_CLIENT_SECRET" public let clientID: String public let clientSecret: String diff --git a/Sources/ImperialDiscord/DiscordRouter.swift b/Sources/ImperialDiscord/DiscordRouter.swift index 7d84f6fe..3199a99d 100644 --- a/Sources/ImperialDiscord/DiscordRouter.swift +++ b/Sources/ImperialDiscord/DiscordRouter.swift @@ -2,8 +2,7 @@ import Vapor import Foundation final public class DiscordRouter: FederatedServiceRouter { - public static var baseURL: String = "https://discord.com/" - public static var callbackURL: String = "callback" + public static let baseURL: String = "https://discord.com/" public let tokens: any FederatedServiceTokens public let callbackCompletion: @Sendable (Request, String) async throws -> any AsyncResponseEncodable public let scope: [String] @@ -27,7 +26,7 @@ final public class DiscordRouter: FederatedServiceRouter { components.path = "/api/oauth2/authorize" components.queryItems = [ clientIDItem, - .init(name: "redirect_uri", value: DiscordRouter.callbackURL), + .init(name: "redirect_uri", value: self.callbackURL), .init(name: "response_type", value: "code"), scopeItem ] @@ -45,7 +44,7 @@ final public class DiscordRouter: FederatedServiceRouter { clientSecret: tokens.clientSecret, grantType: "authorization_code", code: code, - redirectUri: DiscordRouter.callbackURL, + redirectUri: self.callbackURL, scope: scope.joined(separator: " ") ) } diff --git a/Sources/ImperialDropbox/DropboxAuth.swift b/Sources/ImperialDropbox/DropboxAuth.swift index 99a07cf8..864403d0 100644 --- a/Sources/ImperialDropbox/DropboxAuth.swift +++ b/Sources/ImperialDropbox/DropboxAuth.swift @@ -1,8 +1,8 @@ import Vapor final public class DropboxAuth: FederatedServiceTokens { - public static var idEnvKey: String = "DROPBOX_CLIENT_ID" - public static var secretEnvKey: String = "DROPBOX_CLIENT_SECRET" + public static let idEnvKey: String = "DROPBOX_CLIENT_ID" + public static let secretEnvKey: String = "DROPBOX_CLIENT_SECRET" public let clientID: String public let clientSecret: String diff --git a/Sources/ImperialDropbox/DropboxCallbackBody.swift b/Sources/ImperialDropbox/DropboxCallbackBody.swift index 02ee286a..2a110113 100644 --- a/Sources/ImperialDropbox/DropboxCallbackBody.swift +++ b/Sources/ImperialDropbox/DropboxCallbackBody.swift @@ -5,7 +5,7 @@ struct DropboxCallbackBody: Content { let redirectURI: String let grantType: String = "authorization_code" - static var defaultContentType: HTTPMediaType = .urlEncodedForm + static let defaultContentType: HTTPMediaType = .urlEncodedForm enum CodingKeys: String, CodingKey { case code diff --git a/Sources/ImperialFacebook/FacebookAuth.swift b/Sources/ImperialFacebook/FacebookAuth.swift index a8e5d8c8..43364e35 100644 --- a/Sources/ImperialFacebook/FacebookAuth.swift +++ b/Sources/ImperialFacebook/FacebookAuth.swift @@ -1,8 +1,8 @@ import Vapor final public class FacebookAuth: FederatedServiceTokens { - public static var idEnvKey: String = "FACEBOOK_CLIENT_ID" - public static var secretEnvKey: String = "FACEBOOK_CLIENT_SECRET" + public static let idEnvKey: String = "FACEBOOK_CLIENT_ID" + public static let secretEnvKey: String = "FACEBOOK_CLIENT_SECRET" public let clientID: String public let clientSecret: String diff --git a/Sources/ImperialFacebook/FacebookCallbackBody.swift b/Sources/ImperialFacebook/FacebookCallbackBody.swift index 8b73941a..1dbbae3f 100644 --- a/Sources/ImperialFacebook/FacebookCallbackBody.swift +++ b/Sources/ImperialFacebook/FacebookCallbackBody.swift @@ -7,7 +7,7 @@ struct FacebookCallbackBody: Content { let redirectURI: String let grantType: String = "authorization_code" - static var defaultContentType: HTTPMediaType = .urlEncodedForm + static let defaultContentType: HTTPMediaType = .urlEncodedForm enum CodingKeys: String, CodingKey { case code diff --git a/Sources/ImperialGitHub/GitHubAuth.swift b/Sources/ImperialGitHub/GitHubAuth.swift index 589c3bd4..e6a26b09 100644 --- a/Sources/ImperialGitHub/GitHubAuth.swift +++ b/Sources/ImperialGitHub/GitHubAuth.swift @@ -1,8 +1,8 @@ import Vapor final public class GitHubAuth: FederatedServiceTokens { - public static var idEnvKey: String = "GITHUB_CLIENT_ID" - public static var secretEnvKey: String = "GITHUB_CLIENT_SECRET" + public static let idEnvKey: String = "GITHUB_CLIENT_ID" + public static let secretEnvKey: String = "GITHUB_CLIENT_SECRET" public let clientID: String public let clientSecret: String diff --git a/Sources/ImperialGitlab/GitlabAuth.swift b/Sources/ImperialGitlab/GitlabAuth.swift index 881a9bc3..fe4151fe 100644 --- a/Sources/ImperialGitlab/GitlabAuth.swift +++ b/Sources/ImperialGitlab/GitlabAuth.swift @@ -1,8 +1,8 @@ import Vapor final public class GitlabAuth: FederatedServiceTokens { - public static var idEnvKey: String = "GITLAB_CLIENT_ID" - public static var secretEnvKey: String = "GITLAB_CLIENT_SECRET" + public static let idEnvKey: String = "GITLAB_CLIENT_ID" + public static let secretEnvKey: String = "GITLAB_CLIENT_SECRET" public let clientID: String public let clientSecret: String diff --git a/Sources/ImperialGitlab/GitlabRouter.swift b/Sources/ImperialGitlab/GitlabRouter.swift index 5754b265..872dacf7 100644 --- a/Sources/ImperialGitlab/GitlabRouter.swift +++ b/Sources/ImperialGitlab/GitlabRouter.swift @@ -3,7 +3,6 @@ import Foundation final public class GitlabRouter: FederatedServiceRouter { public static let baseURL: String = "https://gitlab.com/" - public static var callbackURL: String = "callback" public let tokens: any FederatedServiceTokens public let callbackCompletion: @Sendable (Request, String) async throws -> any AsyncResponseEncodable public let scope: [String] @@ -25,7 +24,7 @@ final public class GitlabRouter: FederatedServiceRouter { components.path = "/oauth/authorize" components.queryItems = [ clientIDItem, - .init(name: "redirect_uri", value: GitlabRouter.callbackURL), + .init(name: "redirect_uri", value: self.callbackURL), scopeItem, codeResponseTypeItem ] @@ -42,6 +41,6 @@ final public class GitlabRouter: FederatedServiceRouter { clientSecret: tokens.clientSecret, code: code, grantType: "authorization_code", - redirectUri: GitlabRouter.callbackURL) + redirectUri: self.callbackURL) } } diff --git a/Sources/ImperialGoogle/JWT/GoogleJWTAuth.swift b/Sources/ImperialGoogle/JWT/GoogleJWTAuth.swift index b20a711e..b78316f9 100644 --- a/Sources/ImperialGoogle/JWT/GoogleJWTAuth.swift +++ b/Sources/ImperialGoogle/JWT/GoogleJWTAuth.swift @@ -1,8 +1,8 @@ import Vapor final public class GoogleJWTAuth: FederatedServiceTokens { - public static var idEnvKey: String = "GOOGLEJWT_CLIENT_EMAIL" - public static var secretEnvKey: String = "GOOGLEJWT_CLIENT_SECRET" + public static let idEnvKey: String = "GOOGLEJWT_CLIENT_EMAIL" + public static let secretEnvKey: String = "GOOGLEJWT_CLIENT_SECRET" public let clientID: String public let clientSecret: String diff --git a/Sources/ImperialGoogle/Standard/GoogleAuth.swift b/Sources/ImperialGoogle/Standard/GoogleAuth.swift index 96613a85..2cbaa3ae 100644 --- a/Sources/ImperialGoogle/Standard/GoogleAuth.swift +++ b/Sources/ImperialGoogle/Standard/GoogleAuth.swift @@ -1,8 +1,8 @@ import Vapor final public class GoogleAuth: FederatedServiceTokens { - public static var idEnvKey: String = "GOOGLE_CLIENT_ID" - public static var secretEnvKey: String = "GOOGLE_CLIENT_SECRET" + public static let idEnvKey: String = "GOOGLE_CLIENT_ID" + public static let secretEnvKey: String = "GOOGLE_CLIENT_SECRET" public let clientID: String public let clientSecret: String diff --git a/Sources/ImperialGoogle/Standard/GoogleCallbackBody.swift b/Sources/ImperialGoogle/Standard/GoogleCallbackBody.swift index 15666d9d..f6870cad 100644 --- a/Sources/ImperialGoogle/Standard/GoogleCallbackBody.swift +++ b/Sources/ImperialGoogle/Standard/GoogleCallbackBody.swift @@ -7,7 +7,7 @@ struct GoogleCallbackBody: Content { let redirectURI: String let grantType: String = "authorization_code" - static var defaultContentType: HTTPMediaType = .urlEncodedForm + static let defaultContentType: HTTPMediaType = .urlEncodedForm enum CodingKeys: String, CodingKey { case code diff --git a/Sources/ImperialKeycloak/KeycloakAuth.swift b/Sources/ImperialKeycloak/KeycloakAuth.swift index 0484ef72..789a653c 100644 --- a/Sources/ImperialKeycloak/KeycloakAuth.swift +++ b/Sources/ImperialKeycloak/KeycloakAuth.swift @@ -1,10 +1,10 @@ import Vapor final public class KeycloakAuth: FederatedServiceTokens { - public static var idEnvKey: String = "KEYCLOAK_CLIENT_ID" - public static var secretEnvKey: String = "KEYCLOAK_CLIENT_SECRET" - public static var accessTokenEnvURL: String = "KEYCLOAK_ACCESS_TOKEN_URL" - public static var authEnvURL: String = "KEYCLOAK_AUTH_URL" + public static let idEnvKey: String = "KEYCLOAK_CLIENT_ID" + public static let secretEnvKey: String = "KEYCLOAK_CLIENT_SECRET" + public static let accessTokenEnvURL: String = "KEYCLOAK_ACCESS_TOKEN_URL" + public static let authEnvURL: String = "KEYCLOAK_AUTH_URL" public let clientID: String public let clientSecret: String public let accessTokenURL: String diff --git a/Sources/ImperialKeycloak/KeycloakCallbackBody.swift b/Sources/ImperialKeycloak/KeycloakCallbackBody.swift index e9defcbf..7ed0ef77 100644 --- a/Sources/ImperialKeycloak/KeycloakCallbackBody.swift +++ b/Sources/ImperialKeycloak/KeycloakCallbackBody.swift @@ -7,7 +7,7 @@ struct KeycloakCallbackBody: Content { let redirectURI: String let grantType: String = "authorization_code" - static var defaultContentType: HTTPMediaType = .urlEncodedForm + static let defaultContentType: HTTPMediaType = .urlEncodedForm enum CodingKeys: String, CodingKey { case code diff --git a/Sources/ImperialMicrosoft/MicrosoftAuth.swift b/Sources/ImperialMicrosoft/MicrosoftAuth.swift index 31932823..e0851a70 100644 --- a/Sources/ImperialMicrosoft/MicrosoftAuth.swift +++ b/Sources/ImperialMicrosoft/MicrosoftAuth.swift @@ -1,8 +1,8 @@ import Vapor final public class MicrosoftAuth: FederatedServiceTokens { - public static var idEnvKey: String = "MICROSOFT_CLIENT_ID" - public static var secretEnvKey: String = "MICROSOFT_CLIENT_SECRET" + public static let idEnvKey: String = "MICROSOFT_CLIENT_ID" + public static let secretEnvKey: String = "MICROSOFT_CLIENT_SECRET" public let clientID: String public let clientSecret: String diff --git a/Sources/ImperialMicrosoft/MicrosoftCallbackBody.swift b/Sources/ImperialMicrosoft/MicrosoftCallbackBody.swift index 12efd313..00820821 100644 --- a/Sources/ImperialMicrosoft/MicrosoftCallbackBody.swift +++ b/Sources/ImperialMicrosoft/MicrosoftCallbackBody.swift @@ -8,7 +8,7 @@ struct MicrosoftCallbackBody: Content { let scope: String let grantType: String = "authorization_code" - static var defaultContentType: HTTPMediaType = .urlEncodedForm + static let defaultContentType: HTTPMediaType = .urlEncodedForm enum CodingKeys: String, CodingKey { case code diff --git a/Sources/ImperialMicrosoft/MicrosoftRouter.swift b/Sources/ImperialMicrosoft/MicrosoftRouter.swift index 28dcea86..2eb219c2 100644 --- a/Sources/ImperialMicrosoft/MicrosoftRouter.swift +++ b/Sources/ImperialMicrosoft/MicrosoftRouter.swift @@ -2,7 +2,7 @@ import Vapor import Foundation final public class MicrosoftRouter: FederatedServiceRouter { - public static var tenantIDEnvKey: String = "MICROSOFT_TENANT_ID" + public static let tenantIDEnvKey: String = "MICROSOFT_TENANT_ID" public let tokens: any FederatedServiceTokens public let callbackCompletion: @Sendable (Request, String) async throws -> any AsyncResponseEncodable diff --git a/Sources/ImperialShopify/ShopifyAuth.swift b/Sources/ImperialShopify/ShopifyAuth.swift index 9e50424c..c405b261 100644 --- a/Sources/ImperialShopify/ShopifyAuth.swift +++ b/Sources/ImperialShopify/ShopifyAuth.swift @@ -1,8 +1,8 @@ import Vapor final public class ShopifyAuth: FederatedServiceTokens { - public static var idEnvKey: String = "SHOPIFY_CLIENT_ID" - public static var secretEnvKey: String = "SHOPIFY_CLIENT_SECRET" + public static let idEnvKey: String = "SHOPIFY_CLIENT_ID" + public static let secretEnvKey: String = "SHOPIFY_CLIENT_SECRET" public let clientID: String public let clientSecret: String diff --git a/Tests/ImperialTests/ImperialTests.swift b/Tests/ImperialTests/ImperialTests.swift index 1539526c..9e11b9c1 100644 --- a/Tests/ImperialTests/ImperialTests.swift +++ b/Tests/ImperialTests/ImperialTests.swift @@ -12,7 +12,7 @@ import XCTVapor @testable import ImperialCore -@Suite("ImperialCore Tests") +@Suite("Imperial Tests") struct ImperialTests { @Test("Auth0 Route") func auth0Route() async throws { From f1fef4842e10b85e2e8690859cf75d5343155bb6 Mon Sep 17 00:00:00 2001 From: Francesco Paolo Severino Date: Wed, 20 Nov 2024 22:07:45 +0100 Subject: [PATCH 27/37] Update to JWTKit v5 --- Package.swift | 2 +- .../ImperialGoogle/JWT/GoogleJWTPayload.swift | 2 +- .../ImperialGoogle/JWT/GoogleJWTRouter.swift | 30 ++++++++++--------- 3 files changed, 18 insertions(+), 16 deletions(-) diff --git a/Package.swift b/Package.swift index db6996da..63667a97 100755 --- a/Package.swift +++ b/Package.swift @@ -34,7 +34,7 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/vapor/vapor.git", from: "4.0.0"), - .package(url: "https://github.com/vapor/jwt-kit.git", from: "4.0.0") + .package(url: "https://github.com/vapor/jwt-kit.git", from: "5.0.0") ], targets: [ .target( diff --git a/Sources/ImperialGoogle/JWT/GoogleJWTPayload.swift b/Sources/ImperialGoogle/JWT/GoogleJWTPayload.swift index 66085608..ff9d270a 100755 --- a/Sources/ImperialGoogle/JWT/GoogleJWTPayload.swift +++ b/Sources/ImperialGoogle/JWT/GoogleJWTPayload.swift @@ -8,7 +8,7 @@ public struct GoogleJWTPayload: JWTPayload { public var iat: IssuedAtClaim public var exp: ExpirationClaim - public func verify(using signer: JWTSigner) throws { + public func verify(using key: some JWTAlgorithm) throws { try exp.verifyNotExpired() } } diff --git a/Sources/ImperialGoogle/JWT/GoogleJWTRouter.swift b/Sources/ImperialGoogle/JWT/GoogleJWTRouter.swift index c789459e..034235f1 100644 --- a/Sources/ImperialGoogle/JWT/GoogleJWTRouter.swift +++ b/Sources/ImperialGoogle/JWT/GoogleJWTRouter.swift @@ -34,7 +34,7 @@ public final class GoogleJWTRouter: FederatedServiceRouter { } public func fetchToken(from request: Request) async throws -> String { - let token = try self.jwt() + let token = try await self.jwt let body = callbackBody(with: token) let url = URI(string: self.accessTokenURL) @@ -47,18 +47,20 @@ public final class GoogleJWTRouter: FederatedServiceRouter { request.redirect(to: self.callbackURL) } - public func jwt() throws -> String { - let payload = GoogleJWTPayload( - iss: IssuerClaim(value: self.tokens.clientID), - scope: self.scope.joined(separator: " "), - aud: AudienceClaim(value: "https://www.googleapis.com/oauth2/v4/token"), - iat: IssuedAtClaim(value: Date()), - exp: ExpirationClaim(value: Date().addingTimeInterval(3600)) - ) - - let pk = try RSAKey.private(pem: self.tokens.clientSecret.utf8) - let signer = JWTSigner.rs256(key: pk) - let jwtData = try signer.sign(payload) - return jwtData + private var jwt: String { + get async throws { + let payload = GoogleJWTPayload( + iss: IssuerClaim(value: self.tokens.clientID), + scope: self.scope.joined(separator: " "), + aud: AudienceClaim(value: "https://www.googleapis.com/oauth2/v4/token"), + iat: IssuedAtClaim(value: Date()), + exp: ExpirationClaim(value: Date().addingTimeInterval(3600)) + ) + + let pk = try Insecure.RSA.PrivateKey(pem: self.tokens.clientSecret.utf8) + let keys = JWTKeyCollection() + await keys.add(rsa: pk, digestAlgorithm: .sha256) + return try await keys.sign(payload) + } } } From c17faff273e033bac19938af59ef0076fc8fe7e4 Mon Sep 17 00:00:00 2001 From: Francesco Paolo Severino Date: Wed, 20 Nov 2024 22:16:59 +0100 Subject: [PATCH 28/37] Add formatting and linting --- .github/workflows/test.yml | 2 + .swift-format | 70 ++++++++ Package.swift | 36 ++-- .../Services/DeviantArt/DeviantArt.swift | 4 +- .../DeviantArt/DeviantArtRouter.swift | 31 ++-- Sources/Imperial/Services/Imgur/Imgur.swift | 4 +- .../Imperial/Services/Imgur/ImgurRouter.swift | 22 ++- .../Imperial/Services/Mixcloud/Mixcloud.swift | 4 +- .../Services/Mixcloud/MixcloudRouter.swift | 32 ++-- Sources/ImperialAuth0/Auth0.swift | 6 +- Sources/ImperialAuth0/Auth0Auth.swift | 2 +- Sources/ImperialAuth0/Auth0Router.swift | 25 +-- .../FederatedServiceTokens.swift | 82 +++++---- .../ImperialCore/Errors/ServiceError.swift | 2 +- .../Helpers/Request+Imperial.swift | 12 +- .../Helpers/Sessions+Imperial.swift | 12 +- .../ImperialCore/Helpers/String+Tools.swift | 4 +- .../Middleware/ImperialMiddleware.swift | 6 +- .../Model/FederatedCreatable.swift | 6 +- .../Routing/FederatedServiceRouter.swift | 54 +++--- Sources/ImperialCore/ServiceRegister.swift | 8 +- .../Services/FederatedService.swift | 59 ++++--- .../ImperialCore/Services/OAuthService.swift | 9 +- Sources/ImperialDiscord/DiscordRouter.swift | 8 +- Sources/ImperialDiscord/Service+Discord.swift | 2 +- Sources/ImperialDropbox/Dropbox.swift | 6 +- Sources/ImperialDropbox/DropboxAuth.swift | 2 +- .../ImperialDropbox/DropboxCallbackBody.swift | 4 +- Sources/ImperialDropbox/DropboxRouter.swift | 29 +-- Sources/ImperialFacebook/Facebook.swift | 1 - Sources/ImperialFacebook/FacebookRouter.swift | 21 ++- Sources/ImperialGitHub/GitHub.swift | 1 - Sources/ImperialGitHub/GitHubAuth.swift | 2 +- .../ImperialGitHub/GitHubCallbackBody.swift | 2 +- Sources/ImperialGitHub/GitHubRouter.swift | 25 +-- Sources/ImperialGitlab/Gitlab.swift | 1 - Sources/ImperialGitlab/GitlabAuth.swift | 2 +- .../ImperialGitlab/GitlabCallbackBody.swift | 2 +- Sources/ImperialGitlab/GitlabRouter.swift | 29 +-- Sources/ImperialGoogle/JWT/GoogleJWT.swift | 6 +- .../ImperialGoogle/JWT/GoogleJWTAuth.swift | 2 +- .../ImperialGoogle/JWT/GoogleJWTPayload.swift | 2 +- .../JWT/GoogleJWTResponse.swift | 2 +- .../ImperialGoogle/JWT/GoogleJWTRouter.swift | 28 +-- Sources/ImperialGoogle/Standard/Google.swift | 6 +- .../ImperialGoogle/Standard/GoogleAuth.swift | 2 +- .../Standard/GoogleCallbackBody.swift | 4 +- .../Standard/GoogleRouter.swift | 27 +-- Sources/ImperialKeycloak/KeycloakAuth.swift | 2 +- .../KeycloakCallbackBody.swift | 4 +- Sources/ImperialKeycloak/KeycloakRouter.swift | 26 +-- Sources/ImperialMicrosoft/Microsoft.swift | 6 +- Sources/ImperialMicrosoft/MicrosoftAuth.swift | 2 +- .../MicrosoftCallbackBody.swift | 4 +- .../ImperialMicrosoft/MicrosoftRouter.swift | 23 +-- Sources/ImperialShopify/Service+Shopify.swift | 2 +- Sources/ImperialShopify/Session+Shopify.swift | 6 +- Sources/ImperialShopify/Shopify.swift | 1 - Sources/ImperialShopify/ShopifyAuth.swift | 30 ++-- .../ImperialShopify/ShopifyCallbackBody.swift | 18 +- Sources/ImperialShopify/ShopifyRouter.swift | 46 ++--- Sources/ImperialShopify/URL+Shopify.swift | 32 ++-- Tests/ImperialTests/ImperialTests.swift | 167 +++++++++++------- Tests/ImperialTests/ShopifyTests.swift | 85 +++++---- Tests/ImperialTests/withApp.swift | 4 +- 65 files changed, 657 insertions(+), 509 deletions(-) create mode 100644 .swift-format diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index bf05755d..7e451d25 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -9,5 +9,7 @@ on: jobs: unit-tests: uses: vapor/ci/.github/workflows/run-unit-tests.yml@main + with: + with_linting: true secrets: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} \ No newline at end of file diff --git a/.swift-format b/.swift-format new file mode 100644 index 00000000..47901d18 --- /dev/null +++ b/.swift-format @@ -0,0 +1,70 @@ +{ + "fileScopedDeclarationPrivacy": { + "accessLevel": "private" + }, + "indentation": { + "spaces": 4 + }, + "indentConditionalCompilationBlocks": true, + "indentSwitchCaseLabels": false, + "lineBreakAroundMultilineExpressionChainComponents": false, + "lineBreakBeforeControlFlowKeywords": false, + "lineBreakBeforeEachArgument": false, + "lineBreakBeforeEachGenericRequirement": false, + "lineLength": 140, + "maximumBlankLines": 1, + "multiElementCollectionTrailingCommas": true, + "noAssignmentInExpressions": { + "allowedFunctions": [ + "XCTAssertNoThrow" + ] + }, + "prioritizeKeepingFunctionOutputTogether": false, + "respectsExistingLineBreaks": true, + "rules": { + "AllPublicDeclarationsHaveDocumentation": false, + "AlwaysUseLiteralForEmptyCollectionInit": false, + "AlwaysUseLowerCamelCase": true, + "AmbiguousTrailingClosureOverload": true, + "BeginDocumentationCommentWithOneLineSummary": false, + "DoNotUseSemicolons": true, + "DontRepeatTypeInStaticProperties": true, + "FileScopedDeclarationPrivacy": true, + "FullyIndirectEnum": true, + "GroupNumericLiterals": true, + "IdentifiersMustBeASCII": true, + "NeverForceUnwrap": false, + "NeverUseForceTry": false, + "NeverUseImplicitlyUnwrappedOptionals": false, + "NoAccessLevelOnExtensionDeclaration": true, + "NoAssignmentInExpressions": true, + "NoBlockComments": true, + "NoCasesWithOnlyFallthrough": true, + "NoEmptyTrailingClosureParentheses": true, + "NoLabelsInCasePatterns": true, + "NoLeadingUnderscores": false, + "NoParensAroundConditions": true, + "NoPlaygroundLiterals": true, + "NoVoidReturnOnFunctionSignature": true, + "OmitExplicitReturns": false, + "OneCasePerLine": true, + "OneVariableDeclarationPerLine": true, + "OnlyOneTrailingClosureArgument": true, + "OrderedImports": true, + "ReplaceForEachWithForLoop": true, + "ReturnVoidInsteadOfEmptyTuple": true, + "TypeNamesShouldBeCapitalized": true, + "UseEarlyExits": false, + "UseExplicitNilCheckInConditions": true, + "UseLetInEveryBoundCaseVariable": true, + "UseShorthandTypeNames": true, + "UseSingleLinePropertyGetter": true, + "UseSynthesizedInitializer": true, + "UseTripleSlashForDocumentationComments": true, + "UseWhereClausesInForLoops": false, + "ValidateDocumentationComments": false + }, + "spacesAroundRangeFormationOperators": false, + "tabWidth": 8, + "version": 1 +} \ No newline at end of file diff --git a/Package.swift b/Package.swift index 63667a97..8a92abf6 100755 --- a/Package.swift +++ b/Package.swift @@ -4,7 +4,7 @@ import PackageDescription let package = Package( name: "Imperial", platforms: [ - .macOS(.v13) + .macOS(.v13) ], products: [ .library(name: "ImperialCore", targets: ["ImperialCore"]), @@ -18,23 +18,25 @@ let package = Package( .library(name: "ImperialKeycloak", targets: ["ImperialCore", "ImperialKeycloak"]), .library(name: "ImperialMicrosoft", targets: ["ImperialCore", "ImperialMicrosoft"]), .library(name: "ImperialShopify", targets: ["ImperialCore", "ImperialShopify"]), - .library(name: "Imperial", targets: [ - "ImperialCore", - "ImperialAuth0", - "ImperialDiscord", - "ImperialDropbox", - "ImperialFacebook", - "ImperialGitHub", - "ImperialGitlab", - "ImperialGoogle", - "ImperialKeycloak", - "ImperialMicrosoft", - "ImperialShopify" - ]), + .library( + name: "Imperial", + targets: [ + "ImperialCore", + "ImperialAuth0", + "ImperialDiscord", + "ImperialDropbox", + "ImperialFacebook", + "ImperialGitHub", + "ImperialGitlab", + "ImperialGoogle", + "ImperialKeycloak", + "ImperialMicrosoft", + "ImperialShopify", + ]), ], dependencies: [ .package(url: "https://github.com/vapor/vapor.git", from: "4.0.0"), - .package(url: "https://github.com/vapor/jwt-kit.git", from: "5.0.0") + .package(url: "https://github.com/vapor/jwt-kit.git", from: "5.0.0"), ], targets: [ .target( @@ -78,6 +80,6 @@ let package = Package( var swiftSettings: [SwiftSetting] { [ - .enableUpcomingFeature("ExistentialAny"), + .enableUpcomingFeature("ExistentialAny") ] -} \ No newline at end of file +} diff --git a/Sources/Imperial/Services/DeviantArt/DeviantArt.swift b/Sources/Imperial/Services/DeviantArt/DeviantArt.swift index 239a3de8..103becd5 100644 --- a/Sources/Imperial/Services/DeviantArt/DeviantArt.swift +++ b/Sources/Imperial/Services/DeviantArt/DeviantArt.swift @@ -8,10 +8,10 @@ public class DeviantArt: FederatedService { public required init( router: Router, authenticate: String, - authenticateCallback: ((Request)throws -> (Future))?, + authenticateCallback: ((Request) throws -> (Future))?, callback: String, scope: [String] = [], - completion: @escaping (Request, String)throws -> (Future) + completion: @escaping (Request, String) throws -> (Future) ) throws { self.router = try DeviantArtRouter(callback: callback, completion: completion) self.tokens = self.router.tokens diff --git a/Sources/Imperial/Services/DeviantArt/DeviantArtRouter.swift b/Sources/Imperial/Services/DeviantArt/DeviantArtRouter.swift index 66e8dd93..2235092d 100644 --- a/Sources/Imperial/Services/DeviantArt/DeviantArtRouter.swift +++ b/Sources/Imperial/Services/DeviantArt/DeviantArtRouter.swift @@ -1,33 +1,31 @@ -import Vapor import Foundation +import Vapor public class DeviantArtRouter: FederatedServiceRouter { public let tokens: FederatedServiceTokens - public let callbackCompletion: (Request, String)throws -> (Future) + public let callbackCompletion: (Request, String) throws -> (Future) public var scope: [String] = [] public var callbackURL: String public let accessTokenURL: String = "https://www.deviantart.com/oauth2/token" - public required init(callback: String, completion: @escaping (Request, String)throws -> (Future)) throws { + public required init(callback: String, completion: @escaping (Request, String) throws -> (Future)) throws { self.tokens = try DeviantArtAuth() self.callbackURL = callback self.callbackCompletion = completion } public func authURL(_ request: Request) throws -> String { - let scope : String + let scope: String if self.scope.count > 0 { - scope = "scope="+self.scope.joined(separator: " ")+"&" + scope = "scope=" + self.scope.joined(separator: " ") + "&" } else { scope = "" } - return "https://www.deviantart.com/oauth2/authorize?" + - "client_id=\(self.tokens.clientID)&" + - "redirect_uri=\(self.callbackURL)&\(scope)" + - "response_type=code" + return "https://www.deviantart.com/oauth2/authorize?" + "client_id=\(self.tokens.clientID)&" + + "redirect_uri=\(self.callbackURL)&\(scope)" + "response_type=code" } - public func fetchToken(from request: Request)throws -> Future { + public func fetchToken(from request: Request) throws -> Future { let code: String if let queryCode: String = try request.query.get(at: "code") { code = queryCode @@ -37,7 +35,8 @@ public class DeviantArtRouter: FederatedServiceRouter { throw Abort(.badRequest, reason: "Missing 'code' key in URL query") } - let body = DeviantArtCallbackBody(code: code, clientId: self.tokens.clientID, clientSecret: self.tokens.clientSecret, redirectURI: self.callbackURL) + let body = DeviantArtCallbackBody( + code: code, clientId: self.tokens.clientID, clientSecret: self.tokens.clientSecret, redirectURI: self.callbackURL) return try body.encode(using: request).flatMap(to: Response.self) { request in guard let url = URL(string: self.accessTokenURL) else { throw Abort(.internalServerError, reason: "Unable to convert String '\(self.accessTokenURL)' to URL") @@ -49,15 +48,15 @@ public class DeviantArtRouter: FederatedServiceRouter { let session = try request.session() return response.content.get(String.self, at: ["refresh_token"]) - .flatMap { refresh in - session.setRefreshToken(refresh) + .flatMap { refresh in + session.setRefreshToken(refresh) - return response.content.get(String.self, at: ["access_token"]) - } + return response.content.get(String.self, at: ["access_token"]) + } } } - public func callback(_ request: Request)throws -> Future { + public func callback(_ request: Request) throws -> Future { return try self.fetchToken(from: request).flatMap(to: ResponseEncodable.self) { accessToken in let session = try request.session() diff --git a/Sources/Imperial/Services/Imgur/Imgur.swift b/Sources/Imperial/Services/Imgur/Imgur.swift index 17e360ee..ff815c00 100644 --- a/Sources/Imperial/Services/Imgur/Imgur.swift +++ b/Sources/Imperial/Services/Imgur/Imgur.swift @@ -8,10 +8,10 @@ public class Imgur: FederatedService { public required init( router: Router, authenticate: String, - authenticateCallback: ((Request)throws -> (Future))?, + authenticateCallback: ((Request) throws -> (Future))?, callback: String, scope: [String] = [], - completion: @escaping (Request, String)throws -> (Future) + completion: @escaping (Request, String) throws -> (Future) ) throws { self.router = try ImgurRouter(callback: callback, completion: completion) self.tokens = self.router.tokens diff --git a/Sources/Imperial/Services/Imgur/ImgurRouter.swift b/Sources/Imperial/Services/Imgur/ImgurRouter.swift index 52e3756f..be0556cc 100644 --- a/Sources/Imperial/Services/Imgur/ImgurRouter.swift +++ b/Sources/Imperial/Services/Imgur/ImgurRouter.swift @@ -1,26 +1,24 @@ -import Vapor import Foundation +import Vapor public class ImgurRouter: FederatedServiceRouter { public let tokens: FederatedServiceTokens - public let callbackCompletion: (Request, String)throws -> (Future) + public let callbackCompletion: (Request, String) throws -> (Future) public var scope: [String] = [] public var callbackURL: String public let accessTokenURL: String = "https://api.imgur.com/oauth2/token" - public required init(callback: String, completion: @escaping (Request, String)throws -> (Future)) throws { + public required init(callback: String, completion: @escaping (Request, String) throws -> (Future)) throws { self.tokens = try ImgurAuth() self.callbackURL = callback self.callbackCompletion = completion } public func authURL(_ request: Request) throws -> String { - return "https://api.imgur.com/oauth2/authorize?" + - "client_id=\(self.tokens.clientID)&" + - "response_type=code" + return "https://api.imgur.com/oauth2/authorize?" + "client_id=\(self.tokens.clientID)&" + "response_type=code" } - public func fetchToken(from request: Request)throws -> Future { + public func fetchToken(from request: Request) throws -> Future { let code: String if let queryCode: String = try request.query.get(at: "code") { code = queryCode @@ -42,15 +40,15 @@ public class ImgurRouter: FederatedServiceRouter { let session = try request.session() return response.content.get(String.self, at: ["refresh_token"]) - .flatMap { refresh in - session.setRefreshToken(refresh) + .flatMap { refresh in + session.setRefreshToken(refresh) - return response.content.get(String.self, at: ["access_token"]) - } + return response.content.get(String.self, at: ["access_token"]) + } } } - public func callback(_ request: Request)throws -> Future { + public func callback(_ request: Request) throws -> Future { return try self.fetchToken(from: request).flatMap(to: ResponseEncodable.self) { accessToken in let session = try request.session() diff --git a/Sources/Imperial/Services/Mixcloud/Mixcloud.swift b/Sources/Imperial/Services/Mixcloud/Mixcloud.swift index 5396a325..a201b0bd 100644 --- a/Sources/Imperial/Services/Mixcloud/Mixcloud.swift +++ b/Sources/Imperial/Services/Mixcloud/Mixcloud.swift @@ -10,10 +10,10 @@ public class Mixcloud: FederatedService { public required init( router: Router, authenticate: String, - authenticateCallback: ((Request)throws -> (Future))?, + authenticateCallback: ((Request) throws -> (Future))?, callback: String, scope: [String] = [], - completion: @escaping (Request, String)throws -> (Future) + completion: @escaping (Request, String) throws -> (Future) ) throws { self.router = try MixcloudRouter(callback: callback, completion: completion) self.tokens = self.router.tokens diff --git a/Sources/Imperial/Services/Mixcloud/MixcloudRouter.swift b/Sources/Imperial/Services/Mixcloud/MixcloudRouter.swift index 5f832a75..6b6607d6 100644 --- a/Sources/Imperial/Services/Mixcloud/MixcloudRouter.swift +++ b/Sources/Imperial/Services/Mixcloud/MixcloudRouter.swift @@ -1,26 +1,24 @@ -import Vapor import Foundation +import Vapor public class MixcloudRouter: FederatedServiceRouter { public let tokens: FederatedServiceTokens - public let callbackCompletion: (Request, String)throws -> (Future) + public let callbackCompletion: (Request, String) throws -> (Future) public var scope: [String] = [] public var callbackURL: String public let accessTokenURL: String = "https://www.mixcloud.com/oauth/access_token" - public required init(callback: String, completion: @escaping (Request, String)throws -> (Future)) throws { + public required init(callback: String, completion: @escaping (Request, String) throws -> (Future)) throws { self.tokens = try MixcloudAuth() self.callbackURL = callback self.callbackCompletion = completion } public func authURL(_ request: Request) throws -> String { - return "https://www.mixcloud.com/oauth/authorize?" + - "client_id=\(self.tokens.clientID)&" + - "redirect_uri=\(self.callbackURL)" + return "https://www.mixcloud.com/oauth/authorize?" + "client_id=\(self.tokens.clientID)&" + "redirect_uri=\(self.callbackURL)" } - public func fetchToken(from request: Request)throws -> Future { + public func fetchToken(from request: Request) throws -> Future { let code: String if let queryCode: String = try request.query.get(at: "code") { code = queryCode @@ -30,17 +28,19 @@ public class MixcloudRouter: FederatedServiceRouter { throw Abort(.badRequest, reason: "Missing 'code' key in URL query") } - let body = MixcloudCallbackBody(code: code, clientId: self.tokens.clientID, clientSecret: self.tokens.clientSecret, redirectURI: self.callbackURL) - return try request - .client() - .get(self.accessTokenURL) { request in - try request.query.encode(body) - }.flatMap(to: String.self) { response in - return response.content.get(String.self, at: ["access_token"]) - } + let body = MixcloudCallbackBody( + code: code, clientId: self.tokens.clientID, clientSecret: self.tokens.clientSecret, redirectURI: self.callbackURL) + return + try request + .client() + .get(self.accessTokenURL) { request in + try request.query.encode(body) + }.flatMap(to: String.self) { response in + return response.content.get(String.self, at: ["access_token"]) + } } - public func callback(_ request: Request)throws -> Future { + public func callback(_ request: Request) throws -> Future { return try self.fetchToken(from: request).flatMap(to: ResponseEncodable.self) { accessToken in let session = try request.session() diff --git a/Sources/ImperialAuth0/Auth0.swift b/Sources/ImperialAuth0/Auth0.swift index 0a52b444..d35271c2 100644 --- a/Sources/ImperialAuth0/Auth0.swift +++ b/Sources/ImperialAuth0/Auth0.swift @@ -4,7 +4,7 @@ import Vapor public class Auth0: FederatedService { public var tokens: any FederatedServiceTokens public var router: any FederatedServiceRouter - + @discardableResult public required init( routes: some RoutesBuilder, @@ -16,9 +16,9 @@ public class Auth0: FederatedService { ) throws { self.router = try Auth0Router(callback: callback, scope: scope, completion: completion) self.tokens = self.router.tokens - + try self.router.configureRoutes(withAuthURL: authenticate, authenticateCallback: authenticateCallback, on: routes) - + OAuthService.services[OAuthService.auth0.name] = .auth0 } } diff --git a/Sources/ImperialAuth0/Auth0Auth.swift b/Sources/ImperialAuth0/Auth0Auth.swift index 6e42287e..933ad13c 100644 --- a/Sources/ImperialAuth0/Auth0Auth.swift +++ b/Sources/ImperialAuth0/Auth0Auth.swift @@ -7,7 +7,7 @@ final public class Auth0Auth: FederatedServiceTokens { public let domain: String public let clientID: String public let clientSecret: String - + public required init() throws { guard let domain = Environment.get(Auth0Auth.domain) else { throw ImperialError.missingEnvVar(Auth0Auth.domain) diff --git a/Sources/ImperialAuth0/Auth0Router.swift b/Sources/ImperialAuth0/Auth0Router.swift index e4eacd7b..37860913 100644 --- a/Sources/ImperialAuth0/Auth0Router.swift +++ b/Sources/ImperialAuth0/Auth0Router.swift @@ -1,5 +1,5 @@ -import Vapor import Foundation +import Vapor final public class Auth0Router: FederatedServiceRouter { public let baseURL: String @@ -15,8 +15,10 @@ final public class Auth0Router: FederatedServiceRouter { private func providerUrl(path: String) -> String { return self.baseURL.finished(with: "/") + path } - - public required init(callback: String, scope: [String], completion: @escaping @Sendable (Request, String) async throws -> some AsyncResponseEncodable) throws { + + public required init( + callback: String, scope: [String], completion: @escaping @Sendable (Request, String) async throws -> some AsyncResponseEncodable + ) throws { let auth = try Auth0Auth() self.tokens = auth self.baseURL = "https://\(auth.domain)" @@ -25,11 +27,11 @@ final public class Auth0Router: FederatedServiceRouter { self.callbackCompletion = completion self.scope = scope } - + public func authURL(_ request: Request) throws -> String { - let path="authorize" + let path = "authorize" - var params=[ + var params = [ "response_type=code", "client_id=\(self.tokens.clientID)", "redirect_uri=\(self.callbackURL)", @@ -38,7 +40,7 @@ final public class Auth0Router: FederatedServiceRouter { let allScopes = self.scope + self.requiredScopes let scopeString = allScopes.joined(separator: " ").addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) if let scopes = scopeString { - params += [ "scope=\(scopes)" ] + params += ["scope=\(scopes)"] } let rtn = self.providerUrl(path: path + "?" + params.joined(separator: "&")) @@ -46,9 +48,10 @@ final public class Auth0Router: FederatedServiceRouter { } public func callbackBody(with code: String) -> any AsyncResponseEncodable { - Auth0CallbackBody(clientId: self.tokens.clientID, - clientSecret: self.tokens.clientSecret, - code: code, - redirectURI: self.callbackURL) + Auth0CallbackBody( + clientId: self.tokens.clientID, + clientSecret: self.tokens.clientSecret, + code: code, + redirectURI: self.callbackURL) } } diff --git a/Sources/ImperialCore/Authenticatable/FederatedServiceTokens.swift b/Sources/ImperialCore/Authenticatable/FederatedServiceTokens.swift index deefc4d5..ac21c530 100644 --- a/Sources/ImperialCore/Authenticatable/FederatedServiceTokens.swift +++ b/Sources/ImperialCore/Authenticatable/FederatedServiceTokens.swift @@ -1,54 +1,52 @@ -/** -Represents a type that fetches the client id and secret -from environment variables and stores them. - - Usage: - - ```swift - public class GitHubAuth: FederatedServiceTokens { - public var idEnvKey: String = "GITHUB_CLIENT_ID" - public var secretEnvKey: String = "GITHUB_CLIENT_SECRET" - public var clientID: String - public var clientSecret: String - - public required init() throws { - let idError = ImperialError.missingEnvVar(idEnvKey) - let secretError = ImperialError.missingEnvVar(secretEnvKey) - - do { - guard let id = ImperialConfig.gitHubID else { - throw idError - } - self.clientID = id - } catch { - self.clientID = try Env.get(idEnvKey).value(or: idError) - } - - do { - guard let secret = ImperialConfig.gitHubSecret else { - throw secretError - } - self.clientSecret = secret - } catch { - self.clientSecret = try Env.get(secretEnvKey).value(or: secretError) - } - } - } - ``` - */ +/// Represents a type that fetches the client id and secret +/// from environment variables and stores them. +/// +/// Usage: +/// +/// ```swift +/// public class GitHubAuth: FederatedServiceTokens { +/// public var idEnvKey: String = "GITHUB_CLIENT_ID" +/// public var secretEnvKey: String = "GITHUB_CLIENT_SECRET" +/// public var clientID: String +/// public var clientSecret: String +/// +/// public required init() throws { +/// let idError = ImperialError.missingEnvVar(idEnvKey) +/// let secretError = ImperialError.missingEnvVar(secretEnvKey) +/// +/// do { +/// guard let id = ImperialConfig.gitHubID else { +/// throw idError +/// } +/// self.clientID = id +/// } catch { +/// self.clientID = try Env.get(idEnvKey).value(or: idError) +/// } +/// +/// do { +/// guard let secret = ImperialConfig.gitHubSecret else { +/// throw secretError +/// } +/// self.clientSecret = secret +/// } catch { +/// self.clientSecret = try Env.get(secretEnvKey).value(or: secretError) +/// } +/// } +/// } +/// ``` public protocol FederatedServiceTokens: Sendable { /// The name of the environment variable that has the client ID. static var idEnvKey: String { get } - + /// The client ID for the OAuth provider that the service is connected to. var clientID: String { get } - + /// The name of the environment variable that has the client secret. static var secretEnvKey: String { get } - + /// The client secret for the OAuth provider that the service is connected to. var clientSecret: String { get } - + /// Gets the client ID and secret from the environment variables and store them. init() throws } diff --git a/Sources/ImperialCore/Errors/ServiceError.swift b/Sources/ImperialCore/Errors/ServiceError.swift index 344bdff7..0d340fcb 100644 --- a/Sources/ImperialCore/Errors/ServiceError.swift +++ b/Sources/ImperialCore/Errors/ServiceError.swift @@ -63,7 +63,7 @@ extension ServiceError: CustomStringConvertible { } result.append(")") - + return result } } diff --git a/Sources/ImperialCore/Helpers/Request+Imperial.swift b/Sources/ImperialCore/Helpers/Request+Imperial.swift index c621d793..fc0cceb3 100644 --- a/Sources/ImperialCore/Helpers/Request+Imperial.swift +++ b/Sources/ImperialCore/Helpers/Request+Imperial.swift @@ -2,7 +2,7 @@ import Foundation import Vapor extension Request { - + /// Creates an instance of a `FederatedCreatable` type from JSON fetched from an OAuth provider's API. /// /// - Parameters: @@ -14,16 +14,18 @@ extension Request { guard let url = service[model.serviceKey] else { throw ServiceError.noServiceEndpoint(model.serviceKey) } - - let token = try service.tokenPrefix + req + + let token = + try service.tokenPrefix + + req .accessToken() - + let response = try await req.client.get(URI(string: url), headers: ["Authorization": token]) let instance = try await model.init(from: response) try self.session.set("imperial-\(model)", to: instance) return instance } - + /// Gets an instance of a `FederatedCreatable` type that is stored in the request. /// /// - Parameters: diff --git a/Sources/ImperialCore/Helpers/Sessions+Imperial.swift b/Sources/ImperialCore/Helpers/Sessions+Imperial.swift index 1409fb5d..39a4d379 100644 --- a/Sources/ImperialCore/Helpers/Sessions+Imperial.swift +++ b/Sources/ImperialCore/Helpers/Sessions+Imperial.swift @@ -1,7 +1,7 @@ import Vapor extension Request { - + /// Gets the access token from the current session. /// /// - Returns: The access token in the current session. @@ -18,13 +18,13 @@ extension Request { /// - Throws: /// - `Abort.unauthorized` if no refresh token exists. /// - `SessionsError.notConfigured` if session middlware is not configured yet. - public func refreshToken()throws -> String { + public func refreshToken() throws -> String { return try self.session.refreshToken() } } extension Session { - + /// Keys used to store and retrieve items from the session enum Keys { static let token = "access_token" @@ -41,7 +41,7 @@ extension Session { } return token } - + /// Sets the access token on the session. /// /// - Parameter token: the access token to store on the session @@ -53,7 +53,7 @@ extension Session { /// /// - Returns: The refresh token stored with the `refresh_token` key. /// - Throws: `Abort.unauthorized` if no refresh token exists. - public func refreshToken()throws -> String { + public func refreshToken() throws -> String { guard let token = self.data[Keys.refresh] else { if self.data[Keys.token] == nil { throw Abort(.unauthorized, reason: "User currently not authenticated") @@ -88,7 +88,7 @@ extension Session { } return try JSONDecoder().decode(T.self, from: Data(stored.utf8)) } - + /// Sets a key in the session to a codable object. /// /// - Parameters: diff --git a/Sources/ImperialCore/Helpers/String+Tools.swift b/Sources/ImperialCore/Helpers/String+Tools.swift index 36be85ef..3e780cb5 100644 --- a/Sources/ImperialCore/Helpers/String+Tools.swift +++ b/Sources/ImperialCore/Helpers/String+Tools.swift @@ -1,9 +1,9 @@ import Foundation extension String.UTF8View: DataProtocol { - public var regions: CollectionOfOne> { Array(self).regions } + public var regions: CollectionOfOne<[UInt8]> { Array(self).regions } } extension Substring.UTF8View: DataProtocol { - public var regions: CollectionOfOne> { Array(self).regions } + public var regions: CollectionOfOne<[UInt8]> { Array(self).regions } } diff --git a/Sources/ImperialCore/Middleware/ImperialMiddleware.swift b/Sources/ImperialCore/Middleware/ImperialMiddleware.swift index 8cbb0c68..cf59c6f3 100644 --- a/Sources/ImperialCore/Middleware/ImperialMiddleware.swift +++ b/Sources/ImperialCore/Middleware/ImperialMiddleware.swift @@ -2,17 +2,17 @@ import Vapor /// Protects routes from users without an access token. public struct ImperialMiddleware: AsyncMiddleware { - + /// The path to redirect the user to if they are not authenticated. let redirectPath: String? - + /// Creates an instance of `ImperialMiddleware` with the option of a redirect path. /// /// - Parameter redirect: The path to redirect a user to if they do not have an access token. public init(redirect: String? = nil) { self.redirectPath = redirect } - + /// Checks that the request contains an access token. If it does, let the request through. If not, redirect the user to the `redirectPath`. /// If the `redirectPath` is `nil`, then throw the error from getting the access token (Abort.unauthorized). public func respond(to request: Request, chainingTo next: any AsyncResponder) async throws -> Response { diff --git a/Sources/ImperialCore/Model/FederatedCreatable.swift b/Sources/ImperialCore/Model/FederatedCreatable.swift index ee233a85..ec6a1690 100644 --- a/Sources/ImperialCore/Model/FederatedCreatable.swift +++ b/Sources/ImperialCore/Model/FederatedCreatable.swift @@ -3,10 +3,10 @@ import Vapor /// Defines a type that can be created with federated login data. /// This type is used as a parameter in the `request.fetch` method public protocol FederatedCreatable: Codable { - + /// The key for the service's endpoint to use when `request.create` is called with the implimenting type. static var serviceKey: String { get } - + /// Creates an instance of the model with JSON. /// /// - Parameter response: The JSON in the response from the `dataUri`. @@ -20,5 +20,5 @@ extension FederatedCreatable { init(from response: ClientResponse) async throws { self = try response.content.decode(Self.self) } - + } diff --git a/Sources/ImperialCore/Routing/FederatedServiceRouter.swift b/Sources/ImperialCore/Routing/FederatedServiceRouter.swift index ba9d5e2c..f2768db1 100644 --- a/Sources/ImperialCore/Routing/FederatedServiceRouter.swift +++ b/Sources/ImperialCore/Routing/FederatedServiceRouter.swift @@ -6,36 +6,36 @@ import Vapor public protocol FederatedServiceRouter: Sendable { /// A class that gets the client ID and secret from environment variables. var tokens: any FederatedServiceTokens { get } - + /// The callback that is fired after the access token is fetched from the OAuth provider. /// The response that is returned from this callback is also returned from the callback route. var callbackCompletion: @Sendable (Request, String) async throws -> any AsyncResponseEncodable { get } - + /// The scopes to get permission for when getting the access token. /// Usage of this property varies by provider. var scope: [String] { get } - + /// The key to acess the code URL query parameter var codeKey: String { get } - + /// The key to acess the error URL query parameter var errorKey: String { get } - + /// The OAuthService associated with the router var service: OAuthService { get } - + /// The URL (or URI) for that route that the provider will fire when the user authenticates with the OAuth provider. var callbackURL: String { get } - + /// HTTPHeaders for the Callback request var callbackHeaders: HTTPHeaders { get } /// The URL on the app that will redirect to the `authURL` to get the access token from the OAuth provider. var accessTokenURL: String { get } - + /// The URL of the page that the user will be redirected to to get the access token. func authURL(_ request: Request) throws -> String - + /// Creates an instence of the type implementing the protocol. /// /// - Parameters: @@ -48,25 +48,27 @@ public protocol FederatedServiceRouter: Sendable { scope: [String], completion: @escaping @Sendable (Request, String) async throws -> some AsyncResponseEncodable ) throws - + /// Configures the `authenticate` and `callback` routes with the droplet. /// /// - Parameters: /// - authURL: The URL for the route that will redirect the user to the OAuth provider. /// - authenticateCallback: Execute custom code within the authenticate closure before redirection. /// - Throws: N/A - func configureRoutes(withAuthURL authURL: String, authenticateCallback: (@Sendable (Request) async throws -> Void)?, on router: some RoutesBuilder) throws - + func configureRoutes( + withAuthURL authURL: String, authenticateCallback: (@Sendable (Request) async throws -> Void)?, on router: some RoutesBuilder) + throws + /// Gets an access token from an OAuth provider. /// This method is the main body of the `callback` handler. /// /// - Parameters: request: The request for the route /// this method is called in. func fetchToken(from request: Request) async throws -> String - + /// Creates CallbackBody with authorization code func callbackBody(with code: String) -> any AsyncResponseEncodable - + /// The route that the OAuth provider calls when the user has been authenticated. /// /// - Parameter request: The request from the OAuth provider. @@ -79,10 +81,12 @@ extension FederatedServiceRouter { public var codeKey: String { "code" } public var errorKey: String { "error" } public var callbackHeaders: HTTPHeaders { [:] } - - public func configureRoutes(withAuthURL authURL: String, authenticateCallback: (@Sendable (Request) async throws -> Void)?, on router: some RoutesBuilder) throws { - router.get(callbackURL.pathComponents, use: callback) - router.get(authURL.pathComponents) { req async throws -> Response in + + public func configureRoutes( + withAuthURL authURL: String, authenticateCallback: (@Sendable (Request) async throws -> Void)?, on router: some RoutesBuilder + ) throws { + router.get(callbackURL.pathComponents, use: callback) + router.get(authURL.pathComponents) { req async throws -> Response in let redirect: Response = req.redirect(to: try self.authURL(req)) guard let authenticateCallback else { return redirect @@ -91,7 +95,7 @@ extension FederatedServiceRouter { return redirect } } - + public func fetchToken(from request: Request) async throws -> String { let code: String if let queryCode: String = try request.query.get(at: codeKey) { @@ -101,15 +105,15 @@ extension FederatedServiceRouter { } else { throw Abort(.badRequest, reason: "Missing 'code' key in URL query") } - + let body = callbackBody(with: code) let url = URI(string: accessTokenURL) - + let buffer = try await body.encodeResponse(for: request).body.buffer let response = try await request.client.post(url, headers: self.callbackHeaders) { $0.body = buffer } return try response.content.get(String.self, at: ["access_token"]) } - + public func callback(_ request: Request) async throws -> Response { let accessToken = try await self.fetchToken(from: request) let session = request.session @@ -125,15 +129,15 @@ extension FederatedServiceRouter { public var clientIDItem: URLQueryItem { .init(name: "client_id", value: tokens.clientID) } - + public var redirectURIItem: URLQueryItem { .init(name: "redirect_uri", value: callbackURL) } - + public var scopeItem: URLQueryItem { .init(name: "scope", value: scope.joined(separator: " ")) } - + public var codeResponseTypeItem: URLQueryItem { .init(name: "response_type", value: "code") } diff --git a/Sources/ImperialCore/ServiceRegister.swift b/Sources/ImperialCore/ServiceRegister.swift index 8f643123..3f383e74 100644 --- a/Sources/ImperialCore/ServiceRegister.swift +++ b/Sources/ImperialCore/ServiceRegister.swift @@ -1,7 +1,7 @@ import Vapor extension RoutesBuilder { - + /// Registers an OAuth provider's router with /// the parent route. /// @@ -31,7 +31,7 @@ extension RoutesBuilder { completion: completion ) } - + /// Registers an OAuth provider's router with /// the parent route and a redirection callback. /// @@ -51,7 +51,9 @@ extension RoutesBuilder { scope: [String] = [], redirect redirectURL: String ) throws where OAuthProvider: FederatedService { - try self.oAuth(from: OAuthProvider.self, authenticate: authUrl, authenticateCallback: authenticateCallback, callback: callback, scope: scope) { (request, _) in + try self.oAuth( + from: OAuthProvider.self, authenticate: authUrl, authenticateCallback: authenticateCallback, callback: callback, scope: scope + ) { (request, _) in return request.redirect(to: redirectURL) } } diff --git a/Sources/ImperialCore/Services/FederatedService.swift b/Sources/ImperialCore/Services/FederatedService.swift index 3066f019..e5ae4843 100644 --- a/Sources/ImperialCore/Services/FederatedService.swift +++ b/Sources/ImperialCore/Services/FederatedService.swift @@ -1,38 +1,36 @@ import Vapor -/** -Represents a connection to an OAuth provider to get an access token for authenticating a user. - -Usage: - -```swift -import HTTP - -public class Service: FederatedService { - public var tokens: FederatedServiceTokens - public var router: FederatedServiceRouter - - @discardableResult - public required init(authenticate: String, callback: String, scope: [String] = [], completion: @escaping (String) -> (ResponseRepresentable)) throws { - self.router = try ServiceRouter(callback: callback, completion: completion) - self.tokens = self.router.tokens - - self.router.scope = scope - try self.router.configureRoutes(withAuthURL: authenticate, authenticateCallback: authenticateCallback, on: router) - - Service.register(.service) - } -} -``` - */ +/// Represents a connection to an OAuth provider to get an access token for authenticating a user. +/// +/// Usage: +/// +/// ```swift +/// import HTTP +/// +/// public class Service: FederatedService { +/// public var tokens: FederatedServiceTokens +/// public var router: FederatedServiceRouter +/// +/// @discardableResult +/// public required init(authenticate: String, callback: String, scope: [String] = [], completion: @escaping (String) -> (ResponseRepresentable)) throws { +/// self.router = try ServiceRouter(callback: callback, completion: completion) +/// self.tokens = self.router.tokens +/// +/// self.router.scope = scope +/// try self.router.configureRoutes(withAuthURL: authenticate, authenticateCallback: authenticateCallback, on: router) +/// +/// Service.register(.service) +/// } +/// } +/// ``` public protocol FederatedService { - + /// The service's token model for getting the client ID and secret. var tokens: any FederatedServiceTokens { get } - + /// The service's router for handling the request for the access token. var router: any FederatedServiceRouter { get } - + /// Creates a service for getting an access token from an OAuth provider. /// /// - Parameters: @@ -42,5 +40,8 @@ public protocol FederatedService { /// - scope: The scopes to send to the provider to request access to. /// - completion: The completion handler that will fire at the end of the callback route. The access token is passed into the callback and the response that is returned will be returned from the callback route. This will usually be a redirect back to the app. /// - Throws: Any errors that occur in the implementation. - init(routes: some RoutesBuilder, authenticate: String, authenticateCallback: (@Sendable (Request) async throws -> Void)?, callback: String, scope: [String], completion: @escaping @Sendable (Request, String) async throws -> some AsyncResponseEncodable) throws + init( + routes: some RoutesBuilder, authenticate: String, authenticateCallback: (@Sendable (Request) async throws -> Void)?, + callback: String, scope: [String], completion: @escaping @Sendable (Request, String) async throws -> some AsyncResponseEncodable) + throws } diff --git a/Sources/ImperialCore/Services/OAuthService.swift b/Sources/ImperialCore/Services/OAuthService.swift index 7969321f..3d142d00 100644 --- a/Sources/ImperialCore/Services/OAuthService.swift +++ b/Sources/ImperialCore/Services/OAuthService.swift @@ -20,16 +20,15 @@ public struct OAuthService: Codable, Content, Sendable { } } - /// The name of the service, i.e. "google", "github", etc. public let name: String - + /// The prefix for the access token when it is used in a authorization header. Defaults to `'Bearer '`. public let tokenPrefix: String - + /// The endpoints for the provider's API to use for initializing `FederatedCreatable` types public var endpoints: [String: String] - + /// Defines an OAuth provider that is supported by Imperial. /// /// Providers are usually defined in extensions as static properties. @@ -48,7 +47,7 @@ public struct OAuthService: Codable, Content, Sendable { self.tokenPrefix = prefix ?? "Bearer " self.endpoints = endpoints } - + /// Syntax sugar for getting or setting one of the service's endpoints. public subscript(key: String) -> String? { get { diff --git a/Sources/ImperialDiscord/DiscordRouter.swift b/Sources/ImperialDiscord/DiscordRouter.swift index 3199a99d..2fd9efe3 100644 --- a/Sources/ImperialDiscord/DiscordRouter.swift +++ b/Sources/ImperialDiscord/DiscordRouter.swift @@ -1,5 +1,5 @@ -import Vapor import Foundation +import Vapor final public class DiscordRouter: FederatedServiceRouter { public static let baseURL: String = "https://discord.com/" @@ -11,7 +11,9 @@ final public class DiscordRouter: FederatedServiceRouter { public let service: OAuthService = .discord public let callbackHeaders = HTTPHeaders([("Content-Type", "application/x-www-form-urlencoded")]) - public required init(callback: String, scope: [String], completion: @escaping @Sendable (Request, String) async throws -> some AsyncResponseEncodable) throws { + public required init( + callback: String, scope: [String], completion: @escaping @Sendable (Request, String) async throws -> some AsyncResponseEncodable + ) throws { self.tokens = try DiscordAuth() self.callbackURL = callback self.callbackCompletion = completion @@ -28,7 +30,7 @@ final public class DiscordRouter: FederatedServiceRouter { clientIDItem, .init(name: "redirect_uri", value: self.callbackURL), .init(name: "response_type", value: "code"), - scopeItem + scopeItem, ] guard let url = components.url else { diff --git a/Sources/ImperialDiscord/Service+Discord.swift b/Sources/ImperialDiscord/Service+Discord.swift index 6109c5e6..b8762736 100644 --- a/Sources/ImperialDiscord/Service+Discord.swift +++ b/Sources/ImperialDiscord/Service+Discord.swift @@ -1,4 +1,4 @@ - extension OAuthService { +extension OAuthService { public static let discord = OAuthService.init( name: "discord", endpoints: [:] diff --git a/Sources/ImperialDropbox/Dropbox.swift b/Sources/ImperialDropbox/Dropbox.swift index 4ac7f959..c9a3fa12 100644 --- a/Sources/ImperialDropbox/Dropbox.swift +++ b/Sources/ImperialDropbox/Dropbox.swift @@ -4,7 +4,7 @@ import Vapor public class Dropbox: FederatedService { public var tokens: any FederatedServiceTokens public var router: any FederatedServiceRouter - + @discardableResult public required init( routes: some RoutesBuilder, @@ -16,9 +16,9 @@ public class Dropbox: FederatedService { ) throws { self.router = try DropboxRouter(callback: callback, scope: scope, completion: completion) self.tokens = self.router.tokens - + try self.router.configureRoutes(withAuthURL: authenticate, authenticateCallback: authenticateCallback, on: routes) - + OAuthService.services[OAuthService.dropbox.name] = .dropbox } } diff --git a/Sources/ImperialDropbox/DropboxAuth.swift b/Sources/ImperialDropbox/DropboxAuth.swift index 864403d0..03168fbc 100644 --- a/Sources/ImperialDropbox/DropboxAuth.swift +++ b/Sources/ImperialDropbox/DropboxAuth.swift @@ -5,7 +5,7 @@ final public class DropboxAuth: FederatedServiceTokens { public static let secretEnvKey: String = "DROPBOX_CLIENT_SECRET" public let clientID: String public let clientSecret: String - + public required init() throws { guard let clientID = Environment.get(DropboxAuth.idEnvKey) else { throw ImperialError.missingEnvVar(DropboxAuth.idEnvKey) diff --git a/Sources/ImperialDropbox/DropboxCallbackBody.swift b/Sources/ImperialDropbox/DropboxCallbackBody.swift index 2a110113..8be9159c 100644 --- a/Sources/ImperialDropbox/DropboxCallbackBody.swift +++ b/Sources/ImperialDropbox/DropboxCallbackBody.swift @@ -4,9 +4,9 @@ struct DropboxCallbackBody: Content { let code: String let redirectURI: String let grantType: String = "authorization_code" - + static let defaultContentType: HTTPMediaType = .urlEncodedForm - + enum CodingKeys: String, CodingKey { case code case redirectURI = "redirect_uri" diff --git a/Sources/ImperialDropbox/DropboxRouter.swift b/Sources/ImperialDropbox/DropboxRouter.swift index 0dacd439..dd5d905b 100644 --- a/Sources/ImperialDropbox/DropboxRouter.swift +++ b/Sources/ImperialDropbox/DropboxRouter.swift @@ -1,5 +1,5 @@ -import Vapor import Foundation +import Vapor final public class DropboxRouter: FederatedServiceRouter { public let tokens: any FederatedServiceTokens @@ -7,23 +7,25 @@ final public class DropboxRouter: FederatedServiceRouter { public let scope: [String] public let callbackURL: String public let accessTokenURL: String = "https://api.dropboxapi.com/oauth2/token" - + public var callbackHeaders: HTTPHeaders { var headers = HTTPHeaders() headers.basicAuthorization = .init(username: tokens.clientID, password: tokens.clientSecret) headers.contentType = .urlEncodedForm return headers } - + public let service: OAuthService = .dropbox - - public required init(callback: String, scope: [String], completion: @escaping @Sendable (Request, String) async throws -> some AsyncResponseEncodable) throws { + + public required init( + callback: String, scope: [String], completion: @escaping @Sendable (Request, String) async throws -> some AsyncResponseEncodable + ) throws { self.tokens = try DropboxAuth() self.callbackURL = callback self.callbackCompletion = completion self.scope = scope } - + public func authURL(_ request: Request) throws -> String { var components = URLComponents() components.scheme = "https" @@ -33,19 +35,20 @@ final public class DropboxRouter: FederatedServiceRouter { clientIDItem, redirectURIItem, scopeItem, - codeResponseTypeItem + codeResponseTypeItem, ] - + guard let url = components.url else { throw Abort(.internalServerError) } - + return url.absoluteString } - + public func callbackBody(with code: String) -> any AsyncResponseEncodable { - DropboxCallbackBody(code: code, - redirectURI: callbackURL) + DropboxCallbackBody( + code: code, + redirectURI: callbackURL) } - + } diff --git a/Sources/ImperialFacebook/Facebook.swift b/Sources/ImperialFacebook/Facebook.swift index 0e0c9bac..5d6d3539 100644 --- a/Sources/ImperialFacebook/Facebook.swift +++ b/Sources/ImperialFacebook/Facebook.swift @@ -22,4 +22,3 @@ public class Facebook: FederatedService { OAuthService.services[OAuthService.facebook.name] = .facebook } } - diff --git a/Sources/ImperialFacebook/FacebookRouter.swift b/Sources/ImperialFacebook/FacebookRouter.swift index 2c6b7a52..a7448adb 100644 --- a/Sources/ImperialFacebook/FacebookRouter.swift +++ b/Sources/ImperialFacebook/FacebookRouter.swift @@ -1,5 +1,5 @@ -import Vapor import Foundation +import Vapor final public class FacebookRouter: FederatedServiceRouter { public let tokens: any FederatedServiceTokens @@ -18,17 +18,19 @@ final public class FacebookRouter: FederatedServiceRouter { clientIDItem, redirectURIItem, scopeItem, - codeResponseTypeItem + codeResponseTypeItem, ] - + guard let url = components.url else { throw Abort(.internalServerError) } - + return url.absoluteString } - public required init(callback: String, scope: [String], completion: @escaping @Sendable (Request, String) async throws -> some AsyncResponseEncodable) throws { + public required init( + callback: String, scope: [String], completion: @escaping @Sendable (Request, String) async throws -> some AsyncResponseEncodable + ) throws { self.tokens = try FacebookAuth() self.callbackURL = callback self.callbackCompletion = completion @@ -36,10 +38,11 @@ final public class FacebookRouter: FederatedServiceRouter { } public func callbackBody(with code: String) -> any AsyncResponseEncodable { - FacebookCallbackBody(code: code, - clientId: tokens.clientID, - clientSecret: tokens.clientSecret, - redirectURI: callbackURL) + FacebookCallbackBody( + code: code, + clientId: tokens.clientID, + clientSecret: tokens.clientSecret, + redirectURI: callbackURL) } } diff --git a/Sources/ImperialGitHub/GitHub.swift b/Sources/ImperialGitHub/GitHub.swift index 01b3f44b..ac4889c5 100644 --- a/Sources/ImperialGitHub/GitHub.swift +++ b/Sources/ImperialGitHub/GitHub.swift @@ -22,4 +22,3 @@ public class GitHub: FederatedService { OAuthService.services[OAuthService.github.name] = .github } } - diff --git a/Sources/ImperialGitHub/GitHubAuth.swift b/Sources/ImperialGitHub/GitHubAuth.swift index e6a26b09..509063e7 100644 --- a/Sources/ImperialGitHub/GitHubAuth.swift +++ b/Sources/ImperialGitHub/GitHubAuth.swift @@ -5,7 +5,7 @@ final public class GitHubAuth: FederatedServiceTokens { public static let secretEnvKey: String = "GITHUB_CLIENT_SECRET" public let clientID: String public let clientSecret: String - + public required init() throws { guard let clientID = Environment.get(GitHubAuth.idEnvKey) else { throw ImperialError.missingEnvVar(GitHubAuth.idEnvKey) diff --git a/Sources/ImperialGitHub/GitHubCallbackBody.swift b/Sources/ImperialGitHub/GitHubCallbackBody.swift index e697eae4..f318a34a 100644 --- a/Sources/ImperialGitHub/GitHubCallbackBody.swift +++ b/Sources/ImperialGitHub/GitHubCallbackBody.swift @@ -4,7 +4,7 @@ struct GitHubCallbackBody: Content { let clientId: String let clientSecret: String let code: String - + enum CodingKeys: String, CodingKey { case clientId = "client_id" case clientSecret = "client_secret" diff --git a/Sources/ImperialGitHub/GitHubRouter.swift b/Sources/ImperialGitHub/GitHubRouter.swift index cb7c717c..1d1b32b1 100644 --- a/Sources/ImperialGitHub/GitHubRouter.swift +++ b/Sources/ImperialGitHub/GitHubRouter.swift @@ -1,5 +1,5 @@ -import Vapor import Foundation +import Vapor final public class GitHubRouter: FederatedServiceRouter { public static let baseURL: String = "https://github.com/" @@ -15,35 +15,38 @@ final public class GitHubRouter: FederatedServiceRouter { return headers }() - public required init(callback: String, scope: [String], completion: @escaping @Sendable (Request, String) async throws -> some AsyncResponseEncodable) throws { + public required init( + callback: String, scope: [String], completion: @escaping @Sendable (Request, String) async throws -> some AsyncResponseEncodable + ) throws { self.tokens = try GitHubAuth() self.callbackURL = callback self.callbackCompletion = completion self.scope = scope } - + public func authURL(_ request: Request) throws -> String { - + var components = URLComponents() components.scheme = "https" components.host = "github.com" components.path = "/login/oauth/authorize" components.queryItems = [ clientIDItem, - scopeItem + scopeItem, ] - + guard let url = components.url else { throw Abort(.internalServerError) } - + return url.absoluteString } - + public func callbackBody(with code: String) -> any AsyncResponseEncodable { - GitHubCallbackBody(clientId: tokens.clientID, - clientSecret: tokens.clientSecret, - code: code) + GitHubCallbackBody( + clientId: tokens.clientID, + clientSecret: tokens.clientSecret, + code: code) } } diff --git a/Sources/ImperialGitlab/Gitlab.swift b/Sources/ImperialGitlab/Gitlab.swift index a1108e34..f45a8145 100644 --- a/Sources/ImperialGitlab/Gitlab.swift +++ b/Sources/ImperialGitlab/Gitlab.swift @@ -22,4 +22,3 @@ public class Gitlab: FederatedService { OAuthService.services[OAuthService.gitlab.name] = .gitlab } } - diff --git a/Sources/ImperialGitlab/GitlabAuth.swift b/Sources/ImperialGitlab/GitlabAuth.swift index fe4151fe..01c33ff1 100644 --- a/Sources/ImperialGitlab/GitlabAuth.swift +++ b/Sources/ImperialGitlab/GitlabAuth.swift @@ -5,7 +5,7 @@ final public class GitlabAuth: FederatedServiceTokens { public static let secretEnvKey: String = "GITLAB_CLIENT_SECRET" public let clientID: String public let clientSecret: String - + public required init() throws { guard let clientID = Environment.get(GitlabAuth.idEnvKey) else { throw ImperialError.missingEnvVar(GitlabAuth.idEnvKey) diff --git a/Sources/ImperialGitlab/GitlabCallbackBody.swift b/Sources/ImperialGitlab/GitlabCallbackBody.swift index d5608d51..60ed6b5b 100644 --- a/Sources/ImperialGitlab/GitlabCallbackBody.swift +++ b/Sources/ImperialGitlab/GitlabCallbackBody.swift @@ -6,7 +6,7 @@ struct GitlabCallbackBody: Content { let code: String let grantType: String let redirectUri: String - + enum CodingKeys: String, CodingKey { case clientId = "client_id" case clientSecret = "client_secret" diff --git a/Sources/ImperialGitlab/GitlabRouter.swift b/Sources/ImperialGitlab/GitlabRouter.swift index 872dacf7..3cd04820 100644 --- a/Sources/ImperialGitlab/GitlabRouter.swift +++ b/Sources/ImperialGitlab/GitlabRouter.swift @@ -1,5 +1,5 @@ -import Vapor import Foundation +import Vapor final public class GitlabRouter: FederatedServiceRouter { public static let baseURL: String = "https://gitlab.com/" @@ -9,14 +9,16 @@ final public class GitlabRouter: FederatedServiceRouter { public let callbackURL: String public let accessTokenURL: String = "\(GitlabRouter.baseURL.finished(with: "/"))oauth/token" public let service: OAuthService = .gitlab - - public required init(callback: String, scope: [String], completion: @escaping @Sendable (Request, String) async throws -> some AsyncResponseEncodable) throws { + + public required init( + callback: String, scope: [String], completion: @escaping @Sendable (Request, String) async throws -> some AsyncResponseEncodable + ) throws { self.tokens = try GitlabAuth() self.callbackURL = callback self.callbackCompletion = completion self.scope = scope } - + public func authURL(_ request: Request) throws -> String { var components = URLComponents() components.scheme = "https" @@ -26,21 +28,22 @@ final public class GitlabRouter: FederatedServiceRouter { clientIDItem, .init(name: "redirect_uri", value: self.callbackURL), scopeItem, - codeResponseTypeItem + codeResponseTypeItem, ] - + guard let url = components.url else { throw Abort(.internalServerError) } - + return url.absoluteString } - + public func callbackBody(with code: String) -> any AsyncResponseEncodable { - GitlabCallbackBody(clientId: tokens.clientID, - clientSecret: tokens.clientSecret, - code: code, - grantType: "authorization_code", - redirectUri: self.callbackURL) + GitlabCallbackBody( + clientId: tokens.clientID, + clientSecret: tokens.clientSecret, + code: code, + grantType: "authorization_code", + redirectUri: self.callbackURL) } } diff --git a/Sources/ImperialGoogle/JWT/GoogleJWT.swift b/Sources/ImperialGoogle/JWT/GoogleJWT.swift index ba78740d..a48d0f61 100644 --- a/Sources/ImperialGoogle/JWT/GoogleJWT.swift +++ b/Sources/ImperialGoogle/JWT/GoogleJWT.swift @@ -3,7 +3,7 @@ import Vapor public class GoogleJWT: FederatedService { public var tokens: any FederatedServiceTokens public var router: any FederatedServiceRouter - + @discardableResult public required init( routes: some RoutesBuilder, @@ -15,9 +15,9 @@ public class GoogleJWT: FederatedService { ) throws { self.router = try GoogleJWTRouter(callback: callback, scope: scope, completion: completion) self.tokens = self.router.tokens - + try self.router.configureRoutes(withAuthURL: authenticate, authenticateCallback: authenticateCallback, on: routes) - + OAuthService.services[OAuthService.googleJWT.name] = .googleJWT } } diff --git a/Sources/ImperialGoogle/JWT/GoogleJWTAuth.swift b/Sources/ImperialGoogle/JWT/GoogleJWTAuth.swift index b78316f9..cfaf2b78 100644 --- a/Sources/ImperialGoogle/JWT/GoogleJWTAuth.swift +++ b/Sources/ImperialGoogle/JWT/GoogleJWTAuth.swift @@ -5,7 +5,7 @@ final public class GoogleJWTAuth: FederatedServiceTokens { public static let secretEnvKey: String = "GOOGLEJWT_CLIENT_SECRET" public let clientID: String public let clientSecret: String - + public required init() throws { guard let clientID = Environment.get(GoogleJWTAuth.idEnvKey) else { throw ImperialError.missingEnvVar(GoogleJWTAuth.idEnvKey) diff --git a/Sources/ImperialGoogle/JWT/GoogleJWTPayload.swift b/Sources/ImperialGoogle/JWT/GoogleJWTPayload.swift index ff9d270a..90dc29dd 100755 --- a/Sources/ImperialGoogle/JWT/GoogleJWTPayload.swift +++ b/Sources/ImperialGoogle/JWT/GoogleJWTPayload.swift @@ -1,5 +1,5 @@ -import Vapor import JWTKit +import Vapor public struct GoogleJWTPayload: JWTPayload { public var iss: IssuerClaim diff --git a/Sources/ImperialGoogle/JWT/GoogleJWTResponse.swift b/Sources/ImperialGoogle/JWT/GoogleJWTResponse.swift index dd9d6460..9bde541a 100644 --- a/Sources/ImperialGoogle/JWT/GoogleJWTResponse.swift +++ b/Sources/ImperialGoogle/JWT/GoogleJWTResponse.swift @@ -4,7 +4,7 @@ public struct GoogleJWTResponse: Content { public var accessToken: String public var tokenType: String public var expiresIn: Int - + public enum CodingKeys: String, CodingKey { case accessToken = "access_token" case tokenType = "token_type" diff --git a/Sources/ImperialGoogle/JWT/GoogleJWTRouter.swift b/Sources/ImperialGoogle/JWT/GoogleJWTRouter.swift index 034235f1..1afbf842 100644 --- a/Sources/ImperialGoogle/JWT/GoogleJWTRouter.swift +++ b/Sources/ImperialGoogle/JWT/GoogleJWTRouter.swift @@ -1,7 +1,7 @@ -import Foundation import Crypto -import Vapor +import Foundation import JWTKit +import Vapor public final class GoogleJWTRouter: FederatedServiceRouter { public let tokens: any FederatedServiceTokens @@ -16,37 +16,39 @@ public final class GoogleJWTRouter: FederatedServiceRouter { headers.contentType = .urlEncodedForm return headers }() - - public init(callback: String, scope: [String], completion: @escaping @Sendable (Request, String) async throws -> some AsyncResponseEncodable) throws { + + public init( + callback: String, scope: [String], completion: @escaping @Sendable (Request, String) async throws -> some AsyncResponseEncodable + ) throws { self.tokens = try GoogleJWTAuth() self.callbackURL = callback self.authURL = callback self.callbackCompletion = completion self.scope = scope } - + public func authURL(_ request: Request) throws -> String { return authURL } - + public func callbackBody(with code: String) -> any AsyncResponseEncodable { return "grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion=\(code)" } - + public func fetchToken(from request: Request) async throws -> String { let token = try await self.jwt - + let body = callbackBody(with: token) - let url = URI(string: self.accessTokenURL) - let buffer = try await body.encodeResponse(for: request).body.buffer + let url = URI(string: self.accessTokenURL) + let buffer = try await body.encodeResponse(for: request).body.buffer let response = try await request.client.post(url, headers: self.callbackHeaders) { $0.body = buffer } return try response.content.get(GoogleJWTResponse.self).accessToken } - + public func authenticate(_ request: Request) async throws -> Response { request.redirect(to: self.callbackURL) } - + private var jwt: String { get async throws { let payload = GoogleJWTPayload( @@ -56,7 +58,7 @@ public final class GoogleJWTRouter: FederatedServiceRouter { iat: IssuedAtClaim(value: Date()), exp: ExpirationClaim(value: Date().addingTimeInterval(3600)) ) - + let pk = try Insecure.RSA.PrivateKey(pem: self.tokens.clientSecret.utf8) let keys = JWTKeyCollection() await keys.add(rsa: pk, digestAlgorithm: .sha256) diff --git a/Sources/ImperialGoogle/Standard/Google.swift b/Sources/ImperialGoogle/Standard/Google.swift index 4d9607b2..7f7109c2 100644 --- a/Sources/ImperialGoogle/Standard/Google.swift +++ b/Sources/ImperialGoogle/Standard/Google.swift @@ -4,7 +4,7 @@ import Vapor public class Google: FederatedService { public var tokens: any FederatedServiceTokens public var router: any FederatedServiceRouter - + @discardableResult public required init( routes: some RoutesBuilder, @@ -16,9 +16,9 @@ public class Google: FederatedService { ) throws { self.router = try GoogleRouter(callback: callback, scope: scope, completion: completion) self.tokens = self.router.tokens - + try self.router.configureRoutes(withAuthURL: authenticate, authenticateCallback: authenticateCallback, on: routes) - + OAuthService.services[OAuthService.google.name] = .google } } diff --git a/Sources/ImperialGoogle/Standard/GoogleAuth.swift b/Sources/ImperialGoogle/Standard/GoogleAuth.swift index 2cbaa3ae..07cadfa8 100644 --- a/Sources/ImperialGoogle/Standard/GoogleAuth.swift +++ b/Sources/ImperialGoogle/Standard/GoogleAuth.swift @@ -5,7 +5,7 @@ final public class GoogleAuth: FederatedServiceTokens { public static let secretEnvKey: String = "GOOGLE_CLIENT_SECRET" public let clientID: String public let clientSecret: String - + public required init() throws { guard let clientID = Environment.get(GoogleAuth.idEnvKey) else { throw ImperialError.missingEnvVar(GoogleAuth.idEnvKey) diff --git a/Sources/ImperialGoogle/Standard/GoogleCallbackBody.swift b/Sources/ImperialGoogle/Standard/GoogleCallbackBody.swift index f6870cad..fb061935 100644 --- a/Sources/ImperialGoogle/Standard/GoogleCallbackBody.swift +++ b/Sources/ImperialGoogle/Standard/GoogleCallbackBody.swift @@ -6,9 +6,9 @@ struct GoogleCallbackBody: Content { let clientSecret: String let redirectURI: String let grantType: String = "authorization_code" - + static let defaultContentType: HTTPMediaType = .urlEncodedForm - + enum CodingKeys: String, CodingKey { case code case clientId = "client_id" diff --git a/Sources/ImperialGoogle/Standard/GoogleRouter.swift b/Sources/ImperialGoogle/Standard/GoogleRouter.swift index 206f56d1..b4641ffb 100644 --- a/Sources/ImperialGoogle/Standard/GoogleRouter.swift +++ b/Sources/ImperialGoogle/Standard/GoogleRouter.swift @@ -1,5 +1,5 @@ -import Vapor import Foundation +import Vapor final public class GoogleRouter: FederatedServiceRouter { public let tokens: any FederatedServiceTokens @@ -14,14 +14,16 @@ final public class GoogleRouter: FederatedServiceRouter { return headers }() - public required init(callback: String, scope: [String], completion: @escaping @Sendable (Request, String) async throws -> some AsyncResponseEncodable) throws { + public required init( + callback: String, scope: [String], completion: @escaping @Sendable (Request, String) async throws -> some AsyncResponseEncodable + ) throws { self.tokens = try GoogleAuth() self.callbackURL = callback self.callbackCompletion = completion self.scope = scope } - - public func authURL(_ request: Request) throws -> String { + + public func authURL(_ request: Request) throws -> String { var components = URLComponents() components.scheme = "https" components.host = "accounts.google.com" @@ -30,21 +32,22 @@ final public class GoogleRouter: FederatedServiceRouter { clientIDItem, redirectURIItem, scopeItem, - codeResponseTypeItem + codeResponseTypeItem, ] - + guard let url = components.url else { throw Abort(.internalServerError) } - + return url.absoluteString } - + public func callbackBody(with code: String) -> any AsyncResponseEncodable { - GoogleCallbackBody(code: code, - clientId: tokens.clientID, - clientSecret: tokens.clientSecret, - redirectURI: callbackURL) + GoogleCallbackBody( + code: code, + clientId: tokens.clientID, + clientSecret: tokens.clientSecret, + redirectURI: callbackURL) } } diff --git a/Sources/ImperialKeycloak/KeycloakAuth.swift b/Sources/ImperialKeycloak/KeycloakAuth.swift index 789a653c..0fb3af5a 100644 --- a/Sources/ImperialKeycloak/KeycloakAuth.swift +++ b/Sources/ImperialKeycloak/KeycloakAuth.swift @@ -9,7 +9,7 @@ final public class KeycloakAuth: FederatedServiceTokens { public let clientSecret: String public let accessTokenURL: String public let authURL: String - + public required init() throws { guard let clientID = Environment.get(KeycloakAuth.idEnvKey) else { throw ImperialError.missingEnvVar(KeycloakAuth.idEnvKey) diff --git a/Sources/ImperialKeycloak/KeycloakCallbackBody.swift b/Sources/ImperialKeycloak/KeycloakCallbackBody.swift index 7ed0ef77..5cac5465 100644 --- a/Sources/ImperialKeycloak/KeycloakCallbackBody.swift +++ b/Sources/ImperialKeycloak/KeycloakCallbackBody.swift @@ -6,9 +6,9 @@ struct KeycloakCallbackBody: Content { let clientSecret: String let redirectURI: String let grantType: String = "authorization_code" - + static let defaultContentType: HTTPMediaType = .urlEncodedForm - + enum CodingKeys: String, CodingKey { case code case clientId = "client_id" diff --git a/Sources/ImperialKeycloak/KeycloakRouter.swift b/Sources/ImperialKeycloak/KeycloakRouter.swift index 81490e06..ef32f136 100644 --- a/Sources/ImperialKeycloak/KeycloakRouter.swift +++ b/Sources/ImperialKeycloak/KeycloakRouter.swift @@ -1,5 +1,5 @@ -import Vapor import Foundation +import Vapor final public class KeycloakRouter: FederatedServiceRouter { public let tokens: any FederatedServiceTokens @@ -9,8 +9,10 @@ final public class KeycloakRouter: FederatedServiceRouter { public let callbackURL: String public let accessTokenURL: String public let service: OAuthService = .keycloak - - public required init(callback: String, scope: [String], completion: @escaping @Sendable (Request, String) async throws -> some AsyncResponseEncodable) throws { + + public required init( + callback: String, scope: [String], completion: @escaping @Sendable (Request, String) async throws -> some AsyncResponseEncodable + ) throws { self.tokens = try KeycloakAuth() self.keycloakTokens = self.tokens as! KeycloakAuth self.accessTokenURL = keycloakTokens.accessTokenURL @@ -20,17 +22,15 @@ final public class KeycloakRouter: FederatedServiceRouter { } public func authURL(_ request: Request) throws -> String { - return "\(keycloakTokens.authURL)/auth?" + - "client_id=\(self.tokens.clientID)&" + - "redirect_uri=\(self.callbackURL)&" + - "scope=\(scope.joined(separator: "%20"))&" + - "response_type=code" + return "\(keycloakTokens.authURL)/auth?" + "client_id=\(self.tokens.clientID)&" + "redirect_uri=\(self.callbackURL)&" + + "scope=\(scope.joined(separator: "%20"))&" + "response_type=code" } - + public func callbackBody(with code: String) -> any AsyncResponseEncodable { - KeycloakCallbackBody(code: code, - clientId: tokens.clientID, - clientSecret: tokens.clientSecret, - redirectURI: callbackURL) + KeycloakCallbackBody( + code: code, + clientId: tokens.clientID, + clientSecret: tokens.clientSecret, + redirectURI: callbackURL) } } diff --git a/Sources/ImperialMicrosoft/Microsoft.swift b/Sources/ImperialMicrosoft/Microsoft.swift index bb17a266..3dddf9ca 100644 --- a/Sources/ImperialMicrosoft/Microsoft.swift +++ b/Sources/ImperialMicrosoft/Microsoft.swift @@ -4,7 +4,7 @@ import Vapor public class Microsoft: FederatedService { public var tokens: any FederatedServiceTokens public var router: any FederatedServiceRouter - + @discardableResult public required init( routes: some RoutesBuilder, @@ -16,9 +16,9 @@ public class Microsoft: FederatedService { ) throws { self.router = try MicrosoftRouter(callback: callback, scope: scope, completion: completion) self.tokens = self.router.tokens - + try self.router.configureRoutes(withAuthURL: authenticate, authenticateCallback: authenticateCallback, on: routes) - + OAuthService.services[OAuthService.microsoft.name] = .microsoft } } diff --git a/Sources/ImperialMicrosoft/MicrosoftAuth.swift b/Sources/ImperialMicrosoft/MicrosoftAuth.swift index e0851a70..b737ac89 100644 --- a/Sources/ImperialMicrosoft/MicrosoftAuth.swift +++ b/Sources/ImperialMicrosoft/MicrosoftAuth.swift @@ -5,7 +5,7 @@ final public class MicrosoftAuth: FederatedServiceTokens { public static let secretEnvKey: String = "MICROSOFT_CLIENT_SECRET" public let clientID: String public let clientSecret: String - + public required init() throws { guard let clientID = Environment.get(MicrosoftAuth.idEnvKey) else { throw ImperialError.missingEnvVar(MicrosoftAuth.idEnvKey) diff --git a/Sources/ImperialMicrosoft/MicrosoftCallbackBody.swift b/Sources/ImperialMicrosoft/MicrosoftCallbackBody.swift index 00820821..2a5fe56a 100644 --- a/Sources/ImperialMicrosoft/MicrosoftCallbackBody.swift +++ b/Sources/ImperialMicrosoft/MicrosoftCallbackBody.swift @@ -7,9 +7,9 @@ struct MicrosoftCallbackBody: Content { let redirectURI: String let scope: String let grantType: String = "authorization_code" - + static let defaultContentType: HTTPMediaType = .urlEncodedForm - + enum CodingKeys: String, CodingKey { case code case clientId = "client_id" diff --git a/Sources/ImperialMicrosoft/MicrosoftRouter.swift b/Sources/ImperialMicrosoft/MicrosoftRouter.swift index 2eb219c2..14e19176 100644 --- a/Sources/ImperialMicrosoft/MicrosoftRouter.swift +++ b/Sources/ImperialMicrosoft/MicrosoftRouter.swift @@ -1,5 +1,5 @@ -import Vapor import Foundation +import Vapor final public class MicrosoftRouter: FederatedServiceRouter { public static let tenantIDEnvKey: String = "MICROSOFT_TENANT_ID" @@ -12,7 +12,7 @@ final public class MicrosoftRouter: FederatedServiceRouter { public var accessTokenURL: String { "https://login.microsoftonline.com/\(self.tenantID)/oauth2/v2.0/token" } public let service: OAuthService = .microsoft public let errorKey = "error_description" - + public required init( callback: String, scope: [String], @@ -37,20 +37,21 @@ final public class MicrosoftRouter: FederatedServiceRouter { .init(name: "response_mode", value: "query"), .init(name: "prompt", value: "consent"), ] - + guard let url = components.url else { throw Abort(.internalServerError) } - + return url.absoluteString } - + public func callbackBody(with code: String) -> any AsyncResponseEncodable { - MicrosoftCallbackBody(code: code, - clientId: tokens.clientID, - clientSecret: tokens.clientSecret, - redirectURI: callbackURL, - scope: scope.joined(separator: " ")) + MicrosoftCallbackBody( + code: code, + clientId: tokens.clientID, + clientSecret: tokens.clientSecret, + redirectURI: callbackURL, + scope: scope.joined(separator: " ")) } - + } diff --git a/Sources/ImperialShopify/Service+Shopify.swift b/Sources/ImperialShopify/Service+Shopify.swift index 43468538..e173bfee 100644 --- a/Sources/ImperialShopify/Service+Shopify.swift +++ b/Sources/ImperialShopify/Service+Shopify.swift @@ -1,3 +1,3 @@ extension OAuthService { - public static let shopify = OAuthService.init(name: "shopify", endpoints: [:]) + public static let shopify = OAuthService.init(name: "shopify", endpoints: [:]) } diff --git a/Sources/ImperialShopify/Session+Shopify.swift b/Sources/ImperialShopify/Session+Shopify.swift index 45369f30..d3444f41 100644 --- a/Sources/ImperialShopify/Session+Shopify.swift +++ b/Sources/ImperialShopify/Session+Shopify.swift @@ -10,15 +10,15 @@ extension Session { guard let domain = try? self.get(ShopifyKey.domain, as: String.self) else { throw Abort(.notFound) } return domain } - + func setShopDomain(_ domain: String) throws { try self.set(ShopifyKey.domain, to: domain) } - + func setNonce(_ nonce: String?) throws { try self.set(ShopifyKey.nonce, to: nonce) } - + func nonce() -> String? { return try? self.get(ShopifyKey.nonce, as: String.self) } diff --git a/Sources/ImperialShopify/Shopify.swift b/Sources/ImperialShopify/Shopify.swift index 064a34b7..79faa7e2 100644 --- a/Sources/ImperialShopify/Shopify.swift +++ b/Sources/ImperialShopify/Shopify.swift @@ -27,4 +27,3 @@ public final class Shopify: FederatedService { OAuthService.services[OAuthService.shopify.name] = .shopify } } - diff --git a/Sources/ImperialShopify/ShopifyAuth.swift b/Sources/ImperialShopify/ShopifyAuth.swift index c405b261..6004ae24 100644 --- a/Sources/ImperialShopify/ShopifyAuth.swift +++ b/Sources/ImperialShopify/ShopifyAuth.swift @@ -1,20 +1,20 @@ import Vapor final public class ShopifyAuth: FederatedServiceTokens { - public static let idEnvKey: String = "SHOPIFY_CLIENT_ID" - public static let secretEnvKey: String = "SHOPIFY_CLIENT_SECRET" - public let clientID: String - public let clientSecret: String - - public required init() throws { - guard let clientID = Environment.get(ShopifyAuth.idEnvKey) else { - throw ImperialError.missingEnvVar(ShopifyAuth.idEnvKey) - } - self.clientID = clientID + public static let idEnvKey: String = "SHOPIFY_CLIENT_ID" + public static let secretEnvKey: String = "SHOPIFY_CLIENT_SECRET" + public let clientID: String + public let clientSecret: String - guard let clientSecret = Environment.get(ShopifyAuth.secretEnvKey) else { - throw ImperialError.missingEnvVar(ShopifyAuth.secretEnvKey) - } - self.clientSecret = clientSecret - } + public required init() throws { + guard let clientID = Environment.get(ShopifyAuth.idEnvKey) else { + throw ImperialError.missingEnvVar(ShopifyAuth.idEnvKey) + } + self.clientID = clientID + + guard let clientSecret = Environment.get(ShopifyAuth.secretEnvKey) else { + throw ImperialError.missingEnvVar(ShopifyAuth.secretEnvKey) + } + self.clientSecret = clientSecret + } } diff --git a/Sources/ImperialShopify/ShopifyCallbackBody.swift b/Sources/ImperialShopify/ShopifyCallbackBody.swift index de11a327..7bb54679 100644 --- a/Sources/ImperialShopify/ShopifyCallbackBody.swift +++ b/Sources/ImperialShopify/ShopifyCallbackBody.swift @@ -1,13 +1,13 @@ import Vapor struct ShopifyCallbackBody: Content { - let code: String - let clientId: String - let clientSecret: String - - enum CodingKeys: String, CodingKey { - case code - case clientId = "client_id" - case clientSecret = "client_secret" - } + let code: String + let clientId: String + let clientSecret: String + + enum CodingKeys: String, CodingKey { + case code + case clientId = "client_id" + case clientSecret = "client_secret" + } } diff --git a/Sources/ImperialShopify/ShopifyRouter.swift b/Sources/ImperialShopify/ShopifyRouter.swift index 4c2488c2..0ca7e11d 100644 --- a/Sources/ImperialShopify/ShopifyRouter.swift +++ b/Sources/ImperialShopify/ShopifyRouter.swift @@ -9,32 +9,33 @@ final public class ShopifyRouter: FederatedServiceRouter { // now `fetchToken` creates the `accessTokenURL` itself from the shop domain in the request public let accessTokenURL: String = "" public let service: OAuthService = .shopify - - required public init(callback: String, scope: [String], completion: @escaping @Sendable (Request, String) async throws -> some AsyncResponseEncodable) throws { + + required public init( + callback: String, scope: [String], completion: @escaping @Sendable (Request, String) async throws -> some AsyncResponseEncodable + ) throws { self.tokens = try ShopifyAuth() self.callbackURL = callback self.callbackCompletion = completion self.scope = scope } - + public func authURL(_ request: Request) throws -> String { guard let shop = request.query[String.self, at: "shop"] else { throw Abort(.badRequest) } let nonce = String(UUID().uuidString.prefix(6)) try request.session.setNonce(nonce) - return "https://\(shop)/admin/oauth/authorize?" + "client_id=\(tokens.clientID)&" + - "scope=\(scope.joined(separator: ","))&" + - "redirect_uri=\(callbackURL)&" + - "state=\(nonce)" + return "https://\(shop)/admin/oauth/authorize?" + "client_id=\(tokens.clientID)&" + "scope=\(scope.joined(separator: ","))&" + + "redirect_uri=\(callbackURL)&" + "state=\(nonce)" } - + public func callbackBody(with code: String) -> any AsyncResponseEncodable { - ShopifyCallbackBody(code: code, - clientId: tokens.clientID, - clientSecret: tokens.clientSecret) + ShopifyCallbackBody( + code: code, + clientId: tokens.clientID, + clientSecret: tokens.clientSecret) } - + /// Gets an access token from an OAuth provider. /// This method is the main body of the `callback` handler. /// @@ -43,7 +44,8 @@ final public class ShopifyRouter: FederatedServiceRouter { // Extract the parameters to verify guard let code = request.query[String.self, at: "code"], let shop = request.query[String.self, at: "shop"], - let hmac = request.query[String.self, at: "hmac"] else { throw Abort(.badRequest) } + let hmac = request.query[String.self, at: "hmac"] + else { throw Abort(.badRequest) } // Verify the request if let state = request.query[String.self, at: "state"] { @@ -51,16 +53,16 @@ final public class ShopifyRouter: FederatedServiceRouter { guard state == nonce else { throw Abort(.badRequest) } } guard URL(string: shop)?.isValidShopifyDomain == true else { throw Abort(.badRequest) } - guard URL(string: request.url.string)?.generateHMAC(key: tokens.clientSecret) == hmac else { throw Abort(.badRequest) } + guard URL(string: request.url.string)?.generateHMAC(key: tokens.clientSecret) == hmac else { throw Abort(.badRequest) } // exchange code for access token let body = callbackBody(with: code) - let url = URI(string: "https://\(shop)/admin/oauth/access_token") - let buffer = try await body.encodeResponse(for: request).body.buffer + let url = URI(string: "https://\(shop)/admin/oauth/access_token") + let buffer = try await body.encodeResponse(for: request).body.buffer let response = try await request.client.post(url) { $0.body = buffer } return try response.content.get(String.self, at: ["access_token"]) } - + /// The route that the OAuth provider calls when the user has benn authenticated. /// /// - Parameter request: The request from the OAuth provider. @@ -70,10 +72,10 @@ final public class ShopifyRouter: FederatedServiceRouter { let accessToken = try await self.fetchToken(from: request) let session = request.session guard let domain = request.query[String.self, at: "shop"] else { throw Abort(.badRequest) } - try session.setAccessToken(accessToken) - try session.setShopDomain(domain) - try session.setNonce(nil) + try session.setAccessToken(accessToken) + try session.setShopDomain(domain) + try session.setNonce(nil) let response = try await self.callbackCompletion(request, accessToken) - return try await response.encodeResponse(for: request) - } + return try await response.encodeResponse(for: request) + } } diff --git a/Sources/ImperialShopify/URL+Shopify.swift b/Sources/ImperialShopify/URL+Shopify.swift index fdf2883c..1d0f5bcf 100644 --- a/Sources/ImperialShopify/URL+Shopify.swift +++ b/Sources/ImperialShopify/URL+Shopify.swift @@ -1,23 +1,23 @@ -import Foundation import Crypto +import Foundation extension URL { - - func generateHMAC(key: String) -> String { - let components = URLComponents(url: self, resolvingAgainstBaseURL: false)! - let params = components.queryItems!.filter { $0.name != "hmac" } - let queryItems = params.map { $0.name + "=" + $0.value! } - let queryString = queryItems.joined(separator: "&") - + + func generateHMAC(key: String) -> String { + let components = URLComponents(url: self, resolvingAgainstBaseURL: false)! + let params = components.queryItems!.filter { $0.name != "hmac" } + let queryItems = params.map { $0.name + "=" + $0.value! } + let queryString = queryItems.joined(separator: "&") + let hmac = HMAC.authenticationCode(for: queryString.utf8, using: .init(data: Array(key.utf8))) return hmac.hexEncodedString() - } + } + + var isValidShopifyDomain: Bool { + let domain = "myshopify.com" + + guard absoluteString.suffix(domain.count) == domain else { return false } - var isValidShopifyDomain: Bool { - let domain = "myshopify.com" - - guard absoluteString.suffix(domain.count) == domain else { return false } - - return absoluteString.range(of: "^[a-z0-9.-]+.myshopify.com$", options: .regularExpression) != nil - } + return absoluteString.range(of: "^[a-z0-9.-]+.myshopify.com$", options: .regularExpression) != nil + } } diff --git a/Tests/ImperialTests/ImperialTests.swift b/Tests/ImperialTests/ImperialTests.swift index 9e11b9c1..09b14c35 100644 --- a/Tests/ImperialTests/ImperialTests.swift +++ b/Tests/ImperialTests/ImperialTests.swift @@ -17,159 +17,202 @@ struct ImperialTests { @Test("Auth0 Route") func auth0Route() async throws { try await withApp { app in - try await app.test(.GET, "/auth0", afterResponse: { res async throws in - #expect(res.status == .notFound) - }) + try await app.test( + .GET, "/auth0", + afterResponse: { res async throws in + #expect(res.status == .notFound) + }) try app.oAuth(from: Auth0.self, authenticate: "auth0", callback: "auth0-auth-complete", redirect: "/") - try await app.test(.GET, "/auth0", afterResponse: { res async throws in - #expect(res.status != .notFound) - }) + try await app.test( + .GET, "/auth0", + afterResponse: { res async throws in + #expect(res.status != .notFound) + }) } } @Test("Discord Route") func discordRoute() async throws { try await withApp { app in - try await app.test(.GET, "/discord", afterResponse: { res async throws in - #expect(res.status == .notFound) - }) + try await app.test( + .GET, "/discord", + afterResponse: { res async throws in + #expect(res.status == .notFound) + }) try app.oAuth(from: Discord.self, authenticate: "discord", callback: "discord-auth-complete", redirect: "/") - try await app.test(.GET, "/discord", afterResponse: { res async throws in - #expect(res.status != .notFound) - }) + try await app.test( + .GET, "/discord", + afterResponse: { res async throws in + #expect(res.status != .notFound) + }) } } @Test("Dropbox Route") func dropboxRoute() async throws { try await withApp { app in - try await app.test(.GET, "/dropbox", afterResponse: { res async throws in - #expect(res.status == .notFound) - }) + try await app.test( + .GET, "/dropbox", + afterResponse: { res async throws in + #expect(res.status == .notFound) + }) try app.oAuth(from: Dropbox.self, authenticate: "dropbox", callback: "dropbox-auth-complete", redirect: "/") - try await app.test(.GET, "/dropbox", afterResponse: { res async throws in - #expect(res.status != .notFound) - }) + try await app.test( + .GET, "/dropbox", + afterResponse: { res async throws in + #expect(res.status != .notFound) + }) } } @Test("Facebook Route") func facebookRoute() async throws { try await withApp { app in - try await app.test(.GET, "/facebook", afterResponse: { res async throws in - #expect(res.status == .notFound) - }) + try await app.test( + .GET, "/facebook", + afterResponse: { res async throws in + #expect(res.status == .notFound) + }) try app.oAuth(from: Facebook.self, authenticate: "facebook", callback: "facebook-auth-complete", redirect: "/") - try await app.test(.GET, "/facebook", afterResponse: { res async throws in - #expect(res.status != .notFound) - }) + try await app.test( + .GET, "/facebook", + afterResponse: { res async throws in + #expect(res.status != .notFound) + }) } } @Test("GitHub Route") func githubRoute() async throws { try await withApp { app in - try await app.test(.GET, "/github", afterResponse: { res async throws in - #expect(res.status == .notFound) - }) + try await app.test( + .GET, "/github", + afterResponse: { res async throws in + #expect(res.status == .notFound) + }) try app.oAuth(from: GitHub.self, authenticate: "github", callback: "gh-auth-complete", redirect: "/") - try await app.test(.GET, "/github", afterResponse: { res async throws in - #expect(res.status != .notFound) - }) + try await app.test( + .GET, "/github", + afterResponse: { res async throws in + #expect(res.status != .notFound) + }) } } @Test("Gitlab Route") func gitlabRoute() async throws { try await withApp { app in - try await app.test(.GET, "/gitlab", afterResponse: { res async throws in - #expect(res.status == .notFound) - }) + try await app.test( + .GET, "/gitlab", + afterResponse: { res async throws in + #expect(res.status == .notFound) + }) try app.oAuth(from: Gitlab.self, authenticate: "gitlab", callback: "gitlab-auth-complete", redirect: "/") - try await app.test(.GET, "/gitlab", afterResponse: { res async throws in - #expect(res.status != .notFound) - }) + try await app.test( + .GET, "/gitlab", + afterResponse: { res async throws in + #expect(res.status != .notFound) + }) } } @Test("Google Route") func googleRoute() async throws { try await withApp { app in - try await app.test(.GET, "/google", afterResponse: { res async throws in - #expect(res.status == .notFound) - }) + try await app.test( + .GET, "/google", + afterResponse: { res async throws in + #expect(res.status == .notFound) + }) try app.oAuth(from: Google.self, authenticate: "google", callback: "google-auth-complete", redirect: "/") - try await app.test(.GET, "/google", afterResponse: { res async throws in - #expect(res.status != .notFound) - }) + try await app.test( + .GET, "/google", + afterResponse: { res async throws in + #expect(res.status != .notFound) + }) } } @Test("Google JWT Route") func googleJWTRoute() async throws { try await withApp { app in - try await app.test(.GET, "/googleJWT", afterResponse: { res async throws in - #expect(res.status == .notFound) - }) + try await app.test( + .GET, "/googleJWT", + afterResponse: { res async throws in + #expect(res.status == .notFound) + }) try app.oAuth(from: GoogleJWT.self, authenticate: "googleJWT", callback: "googleJWT-auth-complete", redirect: "/") - try await app.test(.GET, "/googleJWT", afterResponse: { res async throws in - #expect(res.status != .notFound) - }) + try await app.test( + .GET, "/googleJWT", + afterResponse: { res async throws in + #expect(res.status != .notFound) + }) } } @Test("Keycloak Route") func keycloakRoute() async throws { try await withApp { app in - try await app.test(.GET, "/keycloak", afterResponse: { res async throws in - #expect(res.status == .notFound) - }) + try await app.test( + .GET, "/keycloak", + afterResponse: { res async throws in + #expect(res.status == .notFound) + }) try app.oAuth(from: Keycloak.self, authenticate: "keycloak", callback: "keycloak-auth-complete", redirect: "/") - try await app.test(.GET, "/keycloak", afterResponse: { res async throws in - #expect(res.status != .notFound) - }) + try await app.test( + .GET, "/keycloak", + afterResponse: { res async throws in + #expect(res.status != .notFound) + }) } } @Test("Microsoft Route") func microsoftRoute() async throws { try await withApp { app in - try await app.test(.GET, "/microsoft", afterResponse: { res async throws in - #expect(res.status == .notFound) - }) + try await app.test( + .GET, "/microsoft", + afterResponse: { res async throws in + #expect(res.status == .notFound) + }) try app.oAuth(from: Microsoft.self, authenticate: "microsoft", callback: "microsoft-auth-complete", redirect: "/") - try await app.test(.GET, "/microsoft", afterResponse: { res async throws in - #expect(res.status != .notFound) - }) + try await app.test( + .GET, "/microsoft", + afterResponse: { res async throws in + #expect(res.status != .notFound) + }) } } @Test("ImperialError & ServiceError") func errors() { - #expect(ImperialError.missingEnvVar("test").description == "ImperialError(errorType: missingEnvVar, missing enviroment variable: test)") + #expect( + ImperialError.missingEnvVar("test").description == "ImperialError(errorType: missingEnvVar, missing enviroment variable: test)") #expect(ImperialError.missingEnvVar("foo") == ImperialError.missingEnvVar("bar")) - #expect(ServiceError.noServiceEndpoint("test").description == "ServiceError(errorType: noServiceEndpoint, service does not have available endpoint for key: test)") + #expect( + ServiceError.noServiceEndpoint("test").description + == "ServiceError(errorType: noServiceEndpoint, service does not have available endpoint for key: test)") #expect(ServiceError.noServiceEndpoint("foo") == ServiceError.noServiceEndpoint("bar")) } } diff --git a/Tests/ImperialTests/ShopifyTests.swift b/Tests/ImperialTests/ShopifyTests.swift index ae203041..c96fc63e 100644 --- a/Tests/ImperialTests/ShopifyTests.swift +++ b/Tests/ImperialTests/ShopifyTests.swift @@ -6,50 +6,57 @@ import XCTVapor @Suite("ImperialShopify Tests") struct ShopifyTests { - @Test("Shopify Route") func shopifyRoute() async throws { + @Test("Shopify Route") func shopifyRoute() async throws { try await withApp { app in - try await app.test(.GET, "/shopify", afterResponse: { res async throws in - #expect(res.status == .notFound) - }) + try await app.test( + .GET, "/shopify", + afterResponse: { res async throws in + #expect(res.status == .notFound) + }) try app.oAuth(from: Shopify.self, authenticate: "shopify", callback: "shopify-auth-complete", redirect: "/") - try await app.test(.GET, "/shopify", afterResponse: { res async throws in - #expect(res.status != .notFound) - }) + try await app.test( + .GET, "/shopify", + afterResponse: { res async throws in + #expect(res.status != .notFound) + }) } } - @Test("Valid Shopify Domain") func domainCheck() throws { - let domain = "davidmuzi.myshopify.com" - #expect(URL(string: domain)!.isValidShopifyDomain) - - let domain2 = "d4m3.myshopify.com" - #expect(URL(string: domain2)!.isValidShopifyDomain) - - let domain3 = "david-muzi.myshopify.com" - #expect(URL(string: domain3)!.isValidShopifyDomain) - - let domain4 = "david.muzi.myshopify.com" - #expect(URL(string: domain4)!.isValidShopifyDomain) - - let domain5 = "david#muzi.myshopify.com" - #expect(!URL(string: domain5)!.isValidShopifyDomain) - - let domain6 = "davidmuzi.myshopify.com.ca" - #expect(!URL(string: domain6)!.isValidShopifyDomain) - - let domain7 = "davidmuzi.square.com" - #expect(!URL(string: domain7)!.isValidShopifyDomain) - - let domain8 = "david*muzi.shopify.ca" - #expect(!URL(string: domain8)!.isValidShopifyDomain) - } - - @Test("HMAC Validation") func hmacValidation() throws { - let url = URL(string: "https://domain.com/?code=0907a61c0c8d55e99db179b68161bc00&hmac=700e2dadb827fcc8609e9d5ce208b2e9cdaab9df07390d2cbca10d7c328fc4bf&shop=some-shop.myshopify.com&state=0.6784241404160823×tamp=1337178173")! - - let hmac = url.generateHMAC(key: "hush") - #expect(hmac == "700e2dadb827fcc8609e9d5ce208b2e9cdaab9df07390d2cbca10d7c328fc4bf") - } + @Test("Valid Shopify Domain") func domainCheck() throws { + let domain = "davidmuzi.myshopify.com" + #expect(URL(string: domain)!.isValidShopifyDomain) + + let domain2 = "d4m3.myshopify.com" + #expect(URL(string: domain2)!.isValidShopifyDomain) + + let domain3 = "david-muzi.myshopify.com" + #expect(URL(string: domain3)!.isValidShopifyDomain) + + let domain4 = "david.muzi.myshopify.com" + #expect(URL(string: domain4)!.isValidShopifyDomain) + + let domain5 = "david#muzi.myshopify.com" + #expect(!URL(string: domain5)!.isValidShopifyDomain) + + let domain6 = "davidmuzi.myshopify.com.ca" + #expect(!URL(string: domain6)!.isValidShopifyDomain) + + let domain7 = "davidmuzi.square.com" + #expect(!URL(string: domain7)!.isValidShopifyDomain) + + let domain8 = "david*muzi.shopify.ca" + #expect(!URL(string: domain8)!.isValidShopifyDomain) + } + + @Test("HMAC Validation") func hmacValidation() throws { + let url = URL( + string: + "https://domain.com/?code=0907a61c0c8d55e99db179b68161bc00&hmac=700e2dadb827fcc8609e9d5ce208b2e9cdaab9df07390d2cbca10d7c328fc4bf&shop=some-shop.myshopify.com&state=0.6784241404160823×tamp=1337178173" + )! + + let hmac = url.generateHMAC(key: "hush") + #expect(hmac == "700e2dadb827fcc8609e9d5ce208b2e9cdaab9df07390d2cbca10d7c328fc4bf") + } } diff --git a/Tests/ImperialTests/withApp.swift b/Tests/ImperialTests/withApp.swift index 9ac634f4..312462fb 100644 --- a/Tests/ImperialTests/withApp.swift +++ b/Tests/ImperialTests/withApp.swift @@ -1,7 +1,7 @@ import Testing import Vapor -func withApp(_ test: (Application) async throws -> ()) async throws { +func withApp(_ test: (Application) async throws -> Void) async throws { let app = try await Application.make(.testing) try #require(isLoggingConfigured) do { @@ -21,4 +21,4 @@ let isLoggingConfigured: Bool = { return handler } return true -}() \ No newline at end of file +}() From 0d08b17ebcf81590ce0c2ae165953485f727713e Mon Sep 17 00:00:00 2001 From: Francesco Paolo Severino Date: Wed, 20 Nov 2024 22:35:53 +0100 Subject: [PATCH 29/37] Update README and add `.spi.yml` --- .spi.yml | 15 +++++++++++++++ README.md | 26 +++++++++++++++++++------- 2 files changed, 34 insertions(+), 7 deletions(-) create mode 100644 .spi.yml diff --git a/.spi.yml b/.spi.yml new file mode 100644 index 00000000..df63ab51 --- /dev/null +++ b/.spi.yml @@ -0,0 +1,15 @@ +version: 1 +builder: + configs: + - documentation_targets: + - ImperialCore + - ImperialAuth0 + - ImperialDiscord + - ImperialDropbox + - ImperialFacebook + - ImperialGitHub + - ImperialGitlab + - ImperialGoogle + - ImperialKeycloak + - ImperialMicrosoft + - ImperialShopify \ No newline at end of file diff --git a/README.md b/README.md index 3e405e95..e77e961c 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,24 @@ -# Imperial +
+ avatar +

Imperial

+ + Documentation + + Team Chat + MIT License + + Continuous Integration + + + + + + Swift 6.0+ + +
+
Imperial is a Federated Login service, allowing you to easily integrate your Vapor applications with OAuth providers to handle your apps authentication. -- [Usage Guides](https://github.com/vapor-community/Imperial/blob/master/docs) - ## Attribution Author(s): @calebkleveter, @rafiki270 - -## License - -All code contained in the Imperial package is under the [MIT](https://github.com/vapor-community/Imperial/blob/master/LICENSE) license agreement. From 7a59e3147dd3781f6b9ace2e3654c9d5ace40837 Mon Sep 17 00:00:00 2001 From: Francesco Paolo Severino Date: Fri, 22 Nov 2024 20:03:16 +0100 Subject: [PATCH 30/37] Adjust formatting and update documentation --- README.md | 3 -- .../DeviantArt/DeviantArtRouter.swift | 6 ++-- .../Helpers/Request+Imperial.swift | 5 +-- .../Services/FederatedService.swift | 34 ++++++++++++------- Sources/ImperialKeycloak/KeycloakRouter.swift | 7 ++-- Sources/ImperialShopify/ShopifyRouter.swift | 8 +++-- 6 files changed, 38 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index e77e961c..fe0871c4 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,3 @@
Imperial is a Federated Login service, allowing you to easily integrate your Vapor applications with OAuth providers to handle your apps authentication. - -## Attribution -Author(s): @calebkleveter, @rafiki270 diff --git a/Sources/Imperial/Services/DeviantArt/DeviantArtRouter.swift b/Sources/Imperial/Services/DeviantArt/DeviantArtRouter.swift index 2235092d..42e44296 100644 --- a/Sources/Imperial/Services/DeviantArt/DeviantArtRouter.swift +++ b/Sources/Imperial/Services/DeviantArt/DeviantArtRouter.swift @@ -21,8 +21,10 @@ public class DeviantArtRouter: FederatedServiceRouter { } else { scope = "" } - return "https://www.deviantart.com/oauth2/authorize?" + "client_id=\(self.tokens.clientID)&" - + "redirect_uri=\(self.callbackURL)&\(scope)" + "response_type=code" + return "https://www.deviantart.com/oauth2/authorize?" + + "client_id=\(self.tokens.clientID)&" + + "redirect_uri=\(self.callbackURL)&\(scope)" + + "response_type=code" } public func fetchToken(from request: Request) throws -> Future { diff --git a/Sources/ImperialCore/Helpers/Request+Imperial.swift b/Sources/ImperialCore/Helpers/Request+Imperial.swift index fc0cceb3..58cecbb0 100644 --- a/Sources/ImperialCore/Helpers/Request+Imperial.swift +++ b/Sources/ImperialCore/Helpers/Request+Imperial.swift @@ -15,10 +15,7 @@ extension Request { throw ServiceError.noServiceEndpoint(model.serviceKey) } - let token = - try service.tokenPrefix - + req - .accessToken() + let token = try service.tokenPrefix + req.accessToken() let response = try await req.client.get(URI(string: url), headers: ["Authorization": token]) let instance = try await model.init(from: response) diff --git a/Sources/ImperialCore/Services/FederatedService.swift b/Sources/ImperialCore/Services/FederatedService.swift index e5ae4843..b01dd0ca 100644 --- a/Sources/ImperialCore/Services/FederatedService.swift +++ b/Sources/ImperialCore/Services/FederatedService.swift @@ -5,26 +5,32 @@ import Vapor /// Usage: /// /// ```swift -/// import HTTP +/// import ImperialCore +/// import Vapor /// /// public class Service: FederatedService { -/// public var tokens: FederatedServiceTokens -/// public var router: FederatedServiceRouter +/// public var tokens: any FederatedServiceTokens +/// public var router: any FederatedServiceRouter /// /// @discardableResult -/// public required init(authenticate: String, callback: String, scope: [String] = [], completion: @escaping (String) -> (ResponseRepresentable)) throws { -/// self.router = try ServiceRouter(callback: callback, completion: completion) +/// public required init( +/// routes: some RoutesBuilder, +/// authenticate: String, +/// authenticateCallback: (@Sendable (Request) async throws -> Void)?, +/// callback: String, +/// scope: [String] = [], +/// completion: @escaping @Sendable (Request, String) async throws -> some AsyncResponseEncodable +/// ) throws { +/// self.router = try ServiceRouter(callback: callback, scope: scope, completion: completion) /// self.tokens = self.router.tokens /// -/// self.router.scope = scope -/// try self.router.configureRoutes(withAuthURL: authenticate, authenticateCallback: authenticateCallback, on: router) +/// try self.router.configureRoutes(withAuthURL: authenticate, authenticateCallback: authenticateCallback, on: routes) /// -/// Service.register(.service) +/// OAuthService.services[OAuthService.service.name] = .service /// } /// } /// ``` public protocol FederatedService { - /// The service's token model for getting the client ID and secret. var tokens: any FederatedServiceTokens { get } @@ -41,7 +47,11 @@ public protocol FederatedService { /// - completion: The completion handler that will fire at the end of the callback route. The access token is passed into the callback and the response that is returned will be returned from the callback route. This will usually be a redirect back to the app. /// - Throws: Any errors that occur in the implementation. init( - routes: some RoutesBuilder, authenticate: String, authenticateCallback: (@Sendable (Request) async throws -> Void)?, - callback: String, scope: [String], completion: @escaping @Sendable (Request, String) async throws -> some AsyncResponseEncodable) - throws + routes: some RoutesBuilder, + authenticate: String, + authenticateCallback: (@Sendable (Request) async throws -> Void)?, + callback: String, + scope: [String], + completion: @escaping @Sendable (Request, String) async throws -> some AsyncResponseEncodable + ) throws } diff --git a/Sources/ImperialKeycloak/KeycloakRouter.swift b/Sources/ImperialKeycloak/KeycloakRouter.swift index ef32f136..6eff574d 100644 --- a/Sources/ImperialKeycloak/KeycloakRouter.swift +++ b/Sources/ImperialKeycloak/KeycloakRouter.swift @@ -22,8 +22,11 @@ final public class KeycloakRouter: FederatedServiceRouter { } public func authURL(_ request: Request) throws -> String { - return "\(keycloakTokens.authURL)/auth?" + "client_id=\(self.tokens.clientID)&" + "redirect_uri=\(self.callbackURL)&" - + "scope=\(scope.joined(separator: "%20"))&" + "response_type=code" + return "\(keycloakTokens.authURL)/auth?" + + "client_id=\(self.tokens.clientID)&" + + "redirect_uri=\(self.callbackURL)&" + + "scope=\(scope.joined(separator: "%20"))&" + + "response_type=code" } public func callbackBody(with code: String) -> any AsyncResponseEncodable { diff --git a/Sources/ImperialShopify/ShopifyRouter.swift b/Sources/ImperialShopify/ShopifyRouter.swift index 0ca7e11d..ba4f75fa 100644 --- a/Sources/ImperialShopify/ShopifyRouter.swift +++ b/Sources/ImperialShopify/ShopifyRouter.swift @@ -7,6 +7,7 @@ final public class ShopifyRouter: FederatedServiceRouter { public let callbackURL: String // `accessTokenURL` used to be set inside `authURL` and read by `fetchToken` // now `fetchToken` creates the `accessTokenURL` itself from the shop domain in the request + // but the property is still required by the protocol, so it's set to an empty string public let accessTokenURL: String = "" public let service: OAuthService = .shopify @@ -25,8 +26,11 @@ final public class ShopifyRouter: FederatedServiceRouter { let nonce = String(UUID().uuidString.prefix(6)) try request.session.setNonce(nonce) - return "https://\(shop)/admin/oauth/authorize?" + "client_id=\(tokens.clientID)&" + "scope=\(scope.joined(separator: ","))&" - + "redirect_uri=\(callbackURL)&" + "state=\(nonce)" + return "https://\(shop)/admin/oauth/authorize?" + + "client_id=\(tokens.clientID)&" + + "scope=\(scope.joined(separator: ","))&" + + "redirect_uri=\(callbackURL)&" + + "state=\(nonce)" } public func callbackBody(with code: String) -> any AsyncResponseEncodable { From cb9f630f208669d75a33beb0c0876a9310818e9c Mon Sep 17 00:00:00 2001 From: Francesco Paolo Severino Date: Fri, 22 Nov 2024 22:27:57 +0100 Subject: [PATCH 31/37] Make error testing more type safe --- Tests/ImperialTests/ImperialTests.swift | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/Tests/ImperialTests/ImperialTests.swift b/Tests/ImperialTests/ImperialTests.swift index 09b14c35..887dbfe0 100644 --- a/Tests/ImperialTests/ImperialTests.swift +++ b/Tests/ImperialTests/ImperialTests.swift @@ -206,13 +206,20 @@ struct ImperialTests { @Test("ImperialError & ServiceError") func errors() { + let variable = "test" + let imperialError = ImperialError.missingEnvVar(variable) #expect( - ImperialError.missingEnvVar("test").description == "ImperialError(errorType: missingEnvVar, missing enviroment variable: test)") + imperialError.description + == "ImperialError(errorType: \(imperialError.errorType.base.rawValue), missing enviroment variable: \(variable))" + ) #expect(ImperialError.missingEnvVar("foo") == ImperialError.missingEnvVar("bar")) + let endpoint = "test" + let serviceError = ServiceError.noServiceEndpoint(endpoint) #expect( - ServiceError.noServiceEndpoint("test").description - == "ServiceError(errorType: noServiceEndpoint, service does not have available endpoint for key: test)") + serviceError.description + == "ServiceError(errorType: \(serviceError.errorType.base.rawValue), service does not have available endpoint for key: \(endpoint))" + ) #expect(ServiceError.noServiceEndpoint("foo") == ServiceError.noServiceEndpoint("bar")) } } From 0ab3d8a0f61ada39e159723524cfd9153d33be8c Mon Sep 17 00:00:00 2001 From: Francesco Paolo Severino <96546612+fpseverino@users.noreply.github.com> Date: Fri, 22 Nov 2024 22:35:03 +0100 Subject: [PATCH 32/37] Update Sources/ImperialShopify/ShopifyRouter.swift Co-authored-by: Vamsi Madduluri --- Sources/ImperialShopify/ShopifyRouter.swift | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/Sources/ImperialShopify/ShopifyRouter.swift b/Sources/ImperialShopify/ShopifyRouter.swift index ba4f75fa..74ca1201 100644 --- a/Sources/ImperialShopify/ShopifyRouter.swift +++ b/Sources/ImperialShopify/ShopifyRouter.swift @@ -26,11 +26,22 @@ final public class ShopifyRouter: FederatedServiceRouter { let nonce = String(UUID().uuidString.prefix(6)) try request.session.setNonce(nonce) - return "https://\(shop)/admin/oauth/authorize?" - + "client_id=\(tokens.clientID)&" - + "scope=\(scope.joined(separator: ","))&" - + "redirect_uri=\(callbackURL)&" - + "state=\(nonce)" + var components = URLComponents() + components.scheme = "https" + components.host = shop + components.path = "/admin/oauth/authorize" + components.queryItems = [ + clientIDItem, + .init(name: "scope", value: scope.joined(separator: ",")), + redirectURIItem, + .init(name: "state", value: nonce), + ] + + guard let url = components.url else { + throw Abort(.internalServerError) + } + + return url.absoluteString } public func callbackBody(with code: String) -> any AsyncResponseEncodable { From cc43d636441c86cb8d561d4ff7607451b5f42f0f Mon Sep 17 00:00:00 2001 From: Francesco Paolo Severino Date: Fri, 22 Nov 2024 23:09:41 +0100 Subject: [PATCH 33/37] Make all `FederatedService` `Sendable` --- Sources/ImperialAuth0/Auth0.swift | 6 +++--- Sources/ImperialCore/Services/FederatedService.swift | 2 +- Sources/ImperialDiscord/Discord.swift | 6 +++--- Sources/ImperialDropbox/Dropbox.swift | 6 +++--- Sources/ImperialFacebook/Facebook.swift | 6 +++--- Sources/ImperialGitHub/GitHub.swift | 6 +++--- Sources/ImperialGitlab/Gitlab.swift | 6 +++--- Sources/ImperialGoogle/JWT/GoogleJWT.swift | 6 +++--- Sources/ImperialGoogle/Standard/Google.swift | 6 +++--- Sources/ImperialKeycloak/Keycloak.swift | 6 +++--- Sources/ImperialMicrosoft/Microsoft.swift | 6 +++--- Sources/ImperialShopify/Shopify.swift | 3 +-- 12 files changed, 32 insertions(+), 33 deletions(-) diff --git a/Sources/ImperialAuth0/Auth0.swift b/Sources/ImperialAuth0/Auth0.swift index d35271c2..baebcddc 100644 --- a/Sources/ImperialAuth0/Auth0.swift +++ b/Sources/ImperialAuth0/Auth0.swift @@ -1,9 +1,9 @@ @_exported import ImperialCore import Vapor -public class Auth0: FederatedService { - public var tokens: any FederatedServiceTokens - public var router: any FederatedServiceRouter +final public class Auth0: FederatedService { + public let tokens: any FederatedServiceTokens + public let router: any FederatedServiceRouter @discardableResult public required init( diff --git a/Sources/ImperialCore/Services/FederatedService.swift b/Sources/ImperialCore/Services/FederatedService.swift index b01dd0ca..9eb3e170 100644 --- a/Sources/ImperialCore/Services/FederatedService.swift +++ b/Sources/ImperialCore/Services/FederatedService.swift @@ -30,7 +30,7 @@ import Vapor /// } /// } /// ``` -public protocol FederatedService { +public protocol FederatedService: Sendable { /// The service's token model for getting the client ID and secret. var tokens: any FederatedServiceTokens { get } diff --git a/Sources/ImperialDiscord/Discord.swift b/Sources/ImperialDiscord/Discord.swift index f74e0741..ab5ffc07 100644 --- a/Sources/ImperialDiscord/Discord.swift +++ b/Sources/ImperialDiscord/Discord.swift @@ -1,9 +1,9 @@ @_exported import ImperialCore import Vapor -public class Discord: FederatedService { - public var tokens: any FederatedServiceTokens - public var router: any FederatedServiceRouter +final public class Discord: FederatedService { + public let tokens: any FederatedServiceTokens + public let router: any FederatedServiceRouter @discardableResult public required init( diff --git a/Sources/ImperialDropbox/Dropbox.swift b/Sources/ImperialDropbox/Dropbox.swift index c9a3fa12..85ed1c53 100644 --- a/Sources/ImperialDropbox/Dropbox.swift +++ b/Sources/ImperialDropbox/Dropbox.swift @@ -1,9 +1,9 @@ @_exported import ImperialCore import Vapor -public class Dropbox: FederatedService { - public var tokens: any FederatedServiceTokens - public var router: any FederatedServiceRouter +final public class Dropbox: FederatedService { + public let tokens: any FederatedServiceTokens + public let router: any FederatedServiceRouter @discardableResult public required init( diff --git a/Sources/ImperialFacebook/Facebook.swift b/Sources/ImperialFacebook/Facebook.swift index 5d6d3539..19b0b84c 100644 --- a/Sources/ImperialFacebook/Facebook.swift +++ b/Sources/ImperialFacebook/Facebook.swift @@ -1,9 +1,9 @@ @_exported import ImperialCore import Vapor -public class Facebook: FederatedService { - public var tokens: any FederatedServiceTokens - public var router: any FederatedServiceRouter +final public class Facebook: FederatedService { + public let tokens: any FederatedServiceTokens + public let router: any FederatedServiceRouter @discardableResult public required init( diff --git a/Sources/ImperialGitHub/GitHub.swift b/Sources/ImperialGitHub/GitHub.swift index ac4889c5..8f2e891d 100644 --- a/Sources/ImperialGitHub/GitHub.swift +++ b/Sources/ImperialGitHub/GitHub.swift @@ -1,9 +1,9 @@ @_exported import ImperialCore import Vapor -public class GitHub: FederatedService { - public var tokens: any FederatedServiceTokens - public var router: any FederatedServiceRouter +final public class GitHub: FederatedService { + public let tokens: any FederatedServiceTokens + public let router: any FederatedServiceRouter @discardableResult public required init( diff --git a/Sources/ImperialGitlab/Gitlab.swift b/Sources/ImperialGitlab/Gitlab.swift index f45a8145..908db1a2 100644 --- a/Sources/ImperialGitlab/Gitlab.swift +++ b/Sources/ImperialGitlab/Gitlab.swift @@ -1,9 +1,9 @@ @_exported import ImperialCore import Vapor -public class Gitlab: FederatedService { - public var tokens: any FederatedServiceTokens - public var router: any FederatedServiceRouter +final public class Gitlab: FederatedService { + public let tokens: any FederatedServiceTokens + public let router: any FederatedServiceRouter @discardableResult public required init( diff --git a/Sources/ImperialGoogle/JWT/GoogleJWT.swift b/Sources/ImperialGoogle/JWT/GoogleJWT.swift index a48d0f61..201569e9 100644 --- a/Sources/ImperialGoogle/JWT/GoogleJWT.swift +++ b/Sources/ImperialGoogle/JWT/GoogleJWT.swift @@ -1,8 +1,8 @@ import Vapor -public class GoogleJWT: FederatedService { - public var tokens: any FederatedServiceTokens - public var router: any FederatedServiceRouter +final public class GoogleJWT: FederatedService { + public let tokens: any FederatedServiceTokens + public let router: any FederatedServiceRouter @discardableResult public required init( diff --git a/Sources/ImperialGoogle/Standard/Google.swift b/Sources/ImperialGoogle/Standard/Google.swift index 7f7109c2..edc498ca 100644 --- a/Sources/ImperialGoogle/Standard/Google.swift +++ b/Sources/ImperialGoogle/Standard/Google.swift @@ -1,9 +1,9 @@ @_exported import ImperialCore import Vapor -public class Google: FederatedService { - public var tokens: any FederatedServiceTokens - public var router: any FederatedServiceRouter +final public class Google: FederatedService { + public let tokens: any FederatedServiceTokens + public let router: any FederatedServiceRouter @discardableResult public required init( diff --git a/Sources/ImperialKeycloak/Keycloak.swift b/Sources/ImperialKeycloak/Keycloak.swift index 77c5d9e3..f6e5f70c 100644 --- a/Sources/ImperialKeycloak/Keycloak.swift +++ b/Sources/ImperialKeycloak/Keycloak.swift @@ -1,9 +1,9 @@ @_exported import ImperialCore import Vapor -public class Keycloak: FederatedService { - public var tokens: any FederatedServiceTokens - public var router: any FederatedServiceRouter +final public class Keycloak: FederatedService { + public let tokens: any FederatedServiceTokens + public let router: any FederatedServiceRouter @discardableResult public required init( diff --git a/Sources/ImperialMicrosoft/Microsoft.swift b/Sources/ImperialMicrosoft/Microsoft.swift index 3dddf9ca..bba59fa2 100644 --- a/Sources/ImperialMicrosoft/Microsoft.swift +++ b/Sources/ImperialMicrosoft/Microsoft.swift @@ -1,9 +1,9 @@ @_exported import ImperialCore import Vapor -public class Microsoft: FederatedService { - public var tokens: any FederatedServiceTokens - public var router: any FederatedServiceRouter +final public class Microsoft: FederatedService { + public let tokens: any FederatedServiceTokens + public let router: any FederatedServiceRouter @discardableResult public required init( diff --git a/Sources/ImperialShopify/Shopify.swift b/Sources/ImperialShopify/Shopify.swift index 79faa7e2..b7e835ea 100644 --- a/Sources/ImperialShopify/Shopify.swift +++ b/Sources/ImperialShopify/Shopify.swift @@ -2,11 +2,10 @@ import Vapor public final class Shopify: FederatedService { - public var tokens: any FederatedServiceTokens { self.router.tokens } public var router: any FederatedServiceRouter { self.shopifyRouter } - public var shopifyRouter: ShopifyRouter + public let shopifyRouter: ShopifyRouter public init( routes: some RoutesBuilder, From 87c761b8bc93a839d615cc1391298be001e4770c Mon Sep 17 00:00:00 2001 From: Francesco Paolo Severino Date: Fri, 22 Nov 2024 23:43:51 +0100 Subject: [PATCH 34/37] Refactor `Shopify` class to streamline token and router properties --- Sources/ImperialShopify/Shopify.swift | 9 ++++----- Sources/ImperialShopify/ShopifyRouter.swift | 3 ++- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Sources/ImperialShopify/Shopify.swift b/Sources/ImperialShopify/Shopify.swift index b7e835ea..01733f2b 100644 --- a/Sources/ImperialShopify/Shopify.swift +++ b/Sources/ImperialShopify/Shopify.swift @@ -2,10 +2,8 @@ import Vapor public final class Shopify: FederatedService { - public var tokens: any FederatedServiceTokens { self.router.tokens } - public var router: any FederatedServiceRouter { self.shopifyRouter } - - public let shopifyRouter: ShopifyRouter + public let tokens: any FederatedServiceTokens + public let router: any FederatedServiceRouter public init( routes: some RoutesBuilder, @@ -15,7 +13,8 @@ public final class Shopify: FederatedService { scope: [String], completion: @escaping @Sendable (Request, String) async throws -> some AsyncResponseEncodable ) throws { - self.shopifyRouter = try ShopifyRouter(callback: callback, scope: scope, completion: completion) + self.router = try ShopifyRouter(callback: callback, scope: scope, completion: completion) + self.tokens = self.router.tokens try self.router.configureRoutes( withAuthURL: authenticate, diff --git a/Sources/ImperialShopify/ShopifyRouter.swift b/Sources/ImperialShopify/ShopifyRouter.swift index 74ca1201..c6dc691c 100644 --- a/Sources/ImperialShopify/ShopifyRouter.swift +++ b/Sources/ImperialShopify/ShopifyRouter.swift @@ -48,7 +48,8 @@ final public class ShopifyRouter: FederatedServiceRouter { ShopifyCallbackBody( code: code, clientId: tokens.clientID, - clientSecret: tokens.clientSecret) + clientSecret: tokens.clientSecret + ) } /// Gets an access token from an OAuth provider. From 7d1550f93fd5191e51f16f86ffd27097736c12f6 Mon Sep 17 00:00:00 2001 From: Francesco Paolo Severino Date: Sat, 23 Nov 2024 00:08:12 +0100 Subject: [PATCH 35/37] Remove `baseURL` where unnecessary --- Sources/ImperialCore/Routing/FederatedServiceRouter.swift | 7 +++++-- Sources/ImperialDiscord/DiscordRouter.swift | 3 +-- Sources/ImperialGitHub/GitHubRouter.swift | 3 +-- Sources/ImperialGitlab/GitlabRouter.swift | 6 +++--- 4 files changed, 10 insertions(+), 9 deletions(-) diff --git a/Sources/ImperialCore/Routing/FederatedServiceRouter.swift b/Sources/ImperialCore/Routing/FederatedServiceRouter.swift index f2768db1..013326b9 100644 --- a/Sources/ImperialCore/Routing/FederatedServiceRouter.swift +++ b/Sources/ImperialCore/Routing/FederatedServiceRouter.swift @@ -54,10 +54,13 @@ public protocol FederatedServiceRouter: Sendable { /// - Parameters: /// - authURL: The URL for the route that will redirect the user to the OAuth provider. /// - authenticateCallback: Execute custom code within the authenticate closure before redirection. + /// - router: The router to configure the routes on. /// - Throws: N/A func configureRoutes( - withAuthURL authURL: String, authenticateCallback: (@Sendable (Request) async throws -> Void)?, on router: some RoutesBuilder) - throws + withAuthURL authURL: String, + authenticateCallback: (@Sendable (Request) async throws -> Void)?, + on router: some RoutesBuilder + ) throws /// Gets an access token from an OAuth provider. /// This method is the main body of the `callback` handler. diff --git a/Sources/ImperialDiscord/DiscordRouter.swift b/Sources/ImperialDiscord/DiscordRouter.swift index 2fd9efe3..a44033cd 100644 --- a/Sources/ImperialDiscord/DiscordRouter.swift +++ b/Sources/ImperialDiscord/DiscordRouter.swift @@ -2,12 +2,11 @@ import Foundation import Vapor final public class DiscordRouter: FederatedServiceRouter { - public static let baseURL: String = "https://discord.com/" public let tokens: any FederatedServiceTokens public let callbackCompletion: @Sendable (Request, String) async throws -> any AsyncResponseEncodable public let scope: [String] public let callbackURL: String - public let accessTokenURL: String = "\(DiscordRouter.baseURL.finished(with: "/"))api/oauth2/token" + public let accessTokenURL: String = "https://discord.com/api/oauth2/token" public let service: OAuthService = .discord public let callbackHeaders = HTTPHeaders([("Content-Type", "application/x-www-form-urlencoded")]) diff --git a/Sources/ImperialGitHub/GitHubRouter.swift b/Sources/ImperialGitHub/GitHubRouter.swift index 1d1b32b1..5d2f9c2d 100644 --- a/Sources/ImperialGitHub/GitHubRouter.swift +++ b/Sources/ImperialGitHub/GitHubRouter.swift @@ -2,12 +2,11 @@ import Foundation import Vapor final public class GitHubRouter: FederatedServiceRouter { - public static let baseURL: String = "https://github.com/" public let tokens: any FederatedServiceTokens public let callbackCompletion: @Sendable (Request, String) async throws -> any AsyncResponseEncodable public let scope: [String] public let callbackURL: String - public let accessTokenURL: String = "\(GitHubRouter.baseURL.finished(with: "/"))login/oauth/access_token" + public let accessTokenURL: String = "https://github.com/login/oauth/access_token" public let service: OAuthService = .github public let callbackHeaders: HTTPHeaders = { var headers = HTTPHeaders() diff --git a/Sources/ImperialGitlab/GitlabRouter.swift b/Sources/ImperialGitlab/GitlabRouter.swift index 3cd04820..d42cc635 100644 --- a/Sources/ImperialGitlab/GitlabRouter.swift +++ b/Sources/ImperialGitlab/GitlabRouter.swift @@ -2,12 +2,11 @@ import Foundation import Vapor final public class GitlabRouter: FederatedServiceRouter { - public static let baseURL: String = "https://gitlab.com/" public let tokens: any FederatedServiceTokens public let callbackCompletion: @Sendable (Request, String) async throws -> any AsyncResponseEncodable public let scope: [String] public let callbackURL: String - public let accessTokenURL: String = "\(GitlabRouter.baseURL.finished(with: "/"))oauth/token" + public let accessTokenURL: String = "https://gitlab.com/oauth/token" public let service: OAuthService = .gitlab public required init( @@ -44,6 +43,7 @@ final public class GitlabRouter: FederatedServiceRouter { clientSecret: tokens.clientSecret, code: code, grantType: "authorization_code", - redirectUri: self.callbackURL) + redirectUri: self.callbackURL + ) } } From 27611f732d14684d27391fe8d4e710937dae8629 Mon Sep 17 00:00:00 2001 From: Francesco Paolo Severino Date: Sat, 23 Nov 2024 00:22:19 +0100 Subject: [PATCH 36/37] Add OAuth routes inside `withApp` --- Tests/ImperialTests/ImperialTests.swift | 160 ++++++++++++------------ Tests/ImperialTests/ShopifyTests.swift | 16 +-- Tests/ImperialTests/withApp.swift | 7 +- 3 files changed, 94 insertions(+), 89 deletions(-) diff --git a/Tests/ImperialTests/ImperialTests.swift b/Tests/ImperialTests/ImperialTests.swift index 887dbfe0..0f7795c0 100644 --- a/Tests/ImperialTests/ImperialTests.swift +++ b/Tests/ImperialTests/ImperialTests.swift @@ -16,191 +16,191 @@ import XCTVapor struct ImperialTests { @Test("Auth0 Route") func auth0Route() async throws { - try await withApp { app in + try await withApp(service: Auth0.self) { app in try await app.test( - .GET, "/auth0", + .GET, "/service", afterResponse: { res async throws in - #expect(res.status == .notFound) - }) - - try app.oAuth(from: Auth0.self, authenticate: "auth0", callback: "auth0-auth-complete", redirect: "/") + #expect(res.status == .seeOther) + } + ) try await app.test( - .GET, "/auth0", + .GET, "/service-auth-complete", afterResponse: { res async throws in #expect(res.status != .notFound) - }) + } + ) } } @Test("Discord Route") func discordRoute() async throws { - try await withApp { app in + try await withApp(service: Discord.self) { app in try await app.test( - .GET, "/discord", + .GET, "/service", afterResponse: { res async throws in - #expect(res.status == .notFound) - }) - - try app.oAuth(from: Discord.self, authenticate: "discord", callback: "discord-auth-complete", redirect: "/") + #expect(res.status == .seeOther) + } + ) try await app.test( - .GET, "/discord", + .GET, "/service-auth-complete", afterResponse: { res async throws in #expect(res.status != .notFound) - }) + } + ) } } @Test("Dropbox Route") func dropboxRoute() async throws { - try await withApp { app in + try await withApp(service: Dropbox.self) { app in try await app.test( - .GET, "/dropbox", + .GET, "/service", afterResponse: { res async throws in - #expect(res.status == .notFound) - }) - - try app.oAuth(from: Dropbox.self, authenticate: "dropbox", callback: "dropbox-auth-complete", redirect: "/") + #expect(res.status == .seeOther) + } + ) try await app.test( - .GET, "/dropbox", + .GET, "/service-auth-complete", afterResponse: { res async throws in #expect(res.status != .notFound) - }) + } + ) } } @Test("Facebook Route") func facebookRoute() async throws { - try await withApp { app in + try await withApp(service: Facebook.self) { app in try await app.test( - .GET, "/facebook", + .GET, "/service", afterResponse: { res async throws in - #expect(res.status == .notFound) - }) - - try app.oAuth(from: Facebook.self, authenticate: "facebook", callback: "facebook-auth-complete", redirect: "/") + #expect(res.status == .seeOther) + } + ) try await app.test( - .GET, "/facebook", + .GET, "/service-auth-complete", afterResponse: { res async throws in #expect(res.status != .notFound) - }) + } + ) } } @Test("GitHub Route") func githubRoute() async throws { - try await withApp { app in + try await withApp(service: GitHub.self) { app in try await app.test( - .GET, "/github", + .GET, "/service", afterResponse: { res async throws in - #expect(res.status == .notFound) - }) - - try app.oAuth(from: GitHub.self, authenticate: "github", callback: "gh-auth-complete", redirect: "/") + #expect(res.status == .seeOther) + } + ) try await app.test( - .GET, "/github", + .GET, "/service-auth-complete", afterResponse: { res async throws in #expect(res.status != .notFound) - }) + } + ) } } @Test("Gitlab Route") func gitlabRoute() async throws { - try await withApp { app in + try await withApp(service: Gitlab.self) { app in try await app.test( - .GET, "/gitlab", + .GET, "/service", afterResponse: { res async throws in - #expect(res.status == .notFound) - }) - - try app.oAuth(from: Gitlab.self, authenticate: "gitlab", callback: "gitlab-auth-complete", redirect: "/") + #expect(res.status == .seeOther) + } + ) try await app.test( - .GET, "/gitlab", + .GET, "/service-auth-complete", afterResponse: { res async throws in #expect(res.status != .notFound) - }) + } + ) } } @Test("Google Route") func googleRoute() async throws { - try await withApp { app in + try await withApp(service: Google.self) { app in try await app.test( - .GET, "/google", + .GET, "/service", afterResponse: { res async throws in - #expect(res.status == .notFound) - }) - - try app.oAuth(from: Google.self, authenticate: "google", callback: "google-auth-complete", redirect: "/") + #expect(res.status == .seeOther) + } + ) try await app.test( - .GET, "/google", + .GET, "/service-auth-complete", afterResponse: { res async throws in #expect(res.status != .notFound) - }) + } + ) } } @Test("Google JWT Route") func googleJWTRoute() async throws { - try await withApp { app in + try await withApp(service: GoogleJWT.self) { app in try await app.test( - .GET, "/googleJWT", + .GET, "/service", afterResponse: { res async throws in - #expect(res.status == .notFound) - }) - - try app.oAuth(from: GoogleJWT.self, authenticate: "googleJWT", callback: "googleJWT-auth-complete", redirect: "/") + #expect(res.status == .seeOther) + } + ) try await app.test( - .GET, "/googleJWT", + .GET, "/service-auth-complete", afterResponse: { res async throws in #expect(res.status != .notFound) - }) + } + ) } } @Test("Keycloak Route") func keycloakRoute() async throws { - try await withApp { app in + try await withApp(service: Keycloak.self) { app in try await app.test( - .GET, "/keycloak", + .GET, "/service", afterResponse: { res async throws in - #expect(res.status == .notFound) - }) - - try app.oAuth(from: Keycloak.self, authenticate: "keycloak", callback: "keycloak-auth-complete", redirect: "/") + #expect(res.status == .seeOther) + } + ) try await app.test( - .GET, "/keycloak", + .GET, "/service-auth-complete", afterResponse: { res async throws in #expect(res.status != .notFound) - }) + } + ) } } @Test("Microsoft Route") func microsoftRoute() async throws { - try await withApp { app in + try await withApp(service: Microsoft.self) { app in try await app.test( - .GET, "/microsoft", + .GET, "/service", afterResponse: { res async throws in - #expect(res.status == .notFound) - }) - - try app.oAuth(from: Microsoft.self, authenticate: "microsoft", callback: "microsoft-auth-complete", redirect: "/") + #expect(res.status == .seeOther) + } + ) try await app.test( - .GET, "/microsoft", + .GET, "/service-auth-complete", afterResponse: { res async throws in #expect(res.status != .notFound) - }) + } + ) } } diff --git a/Tests/ImperialTests/ShopifyTests.swift b/Tests/ImperialTests/ShopifyTests.swift index c96fc63e..804ca6d2 100644 --- a/Tests/ImperialTests/ShopifyTests.swift +++ b/Tests/ImperialTests/ShopifyTests.swift @@ -7,20 +7,20 @@ import XCTVapor @Suite("ImperialShopify Tests") struct ShopifyTests { @Test("Shopify Route") func shopifyRoute() async throws { - try await withApp { app in + try await withApp(service: Shopify.self) { app in try await app.test( - .GET, "/shopify", + .GET, "/service", afterResponse: { res async throws in - #expect(res.status == .notFound) - }) - - try app.oAuth(from: Shopify.self, authenticate: "shopify", callback: "shopify-auth-complete", redirect: "/") + #expect(res.status != .notFound) + } + ) try await app.test( - .GET, "/shopify", + .GET, "/service-auth-complete", afterResponse: { res async throws in #expect(res.status != .notFound) - }) + } + ) } } diff --git a/Tests/ImperialTests/withApp.swift b/Tests/ImperialTests/withApp.swift index 312462fb..5152e81b 100644 --- a/Tests/ImperialTests/withApp.swift +++ b/Tests/ImperialTests/withApp.swift @@ -1,11 +1,16 @@ +import ImperialCore import Testing import Vapor -func withApp(_ test: (Application) async throws -> Void) async throws { +func withApp( + service: OAuthProvider.Type, + _ test: (Application) async throws -> Void +) async throws where OAuthProvider: FederatedService { let app = try await Application.make(.testing) try #require(isLoggingConfigured) do { app.middleware.use(app.sessions.middleware) + try app.oAuth(from: service.self, authenticate: "service", callback: "service-auth-complete", redirect: "/") try await test(app) } catch { try await app.asyncShutdown() From 69818e721465532957d8da1eef2201b4c7d25ce0 Mon Sep 17 00:00:00 2001 From: vamsii777 Date: Sat, 23 Nov 2024 12:46:20 +0530 Subject: [PATCH 37/37] Add ImperialX module for X (Twitter) OAuth integration - Add ImperialX library target and dependencies - Include swift-crypto package dependency Co-authored-by: Vamsi Madduluri --- Package.swift | 5 ++ Sources/ImperialX/Service+X.swift | 6 ++ Sources/ImperialX/Session+X.swift | 24 ++++++ Sources/ImperialX/X.swift | 24 ++++++ Sources/ImperialX/XAuth.swift | 20 +++++ Sources/ImperialX/XCallbackBody.swift | 21 +++++ Sources/ImperialX/XRouter.swift | 113 ++++++++++++++++++++++++++ 7 files changed, 213 insertions(+) create mode 100644 Sources/ImperialX/Service+X.swift create mode 100644 Sources/ImperialX/Session+X.swift create mode 100644 Sources/ImperialX/X.swift create mode 100644 Sources/ImperialX/XAuth.swift create mode 100644 Sources/ImperialX/XCallbackBody.swift create mode 100644 Sources/ImperialX/XRouter.swift diff --git a/Package.swift b/Package.swift index 8a92abf6..6d5f4211 100755 --- a/Package.swift +++ b/Package.swift @@ -18,6 +18,7 @@ let package = Package( .library(name: "ImperialKeycloak", targets: ["ImperialCore", "ImperialKeycloak"]), .library(name: "ImperialMicrosoft", targets: ["ImperialCore", "ImperialMicrosoft"]), .library(name: "ImperialShopify", targets: ["ImperialCore", "ImperialShopify"]), + .library(name: "ImperialX", targets: ["ImperialCore", "ImperialX"]), .library( name: "Imperial", targets: [ @@ -32,11 +33,13 @@ let package = Package( "ImperialKeycloak", "ImperialMicrosoft", "ImperialShopify", + "ImperialX", ]), ], dependencies: [ .package(url: "https://github.com/vapor/vapor.git", from: "4.0.0"), .package(url: "https://github.com/vapor/jwt-kit.git", from: "5.0.0"), + .package(url: "https://github.com/apple/swift-crypto.git", "3.8.0"..<"5.0.0"), ], targets: [ .target( @@ -57,6 +60,7 @@ let package = Package( .target(name: "ImperialKeycloak", dependencies: ["ImperialCore"], swiftSettings: swiftSettings), .target(name: "ImperialMicrosoft", dependencies: ["ImperialCore"], swiftSettings: swiftSettings), .target(name: "ImperialShopify", dependencies: ["ImperialCore"], swiftSettings: swiftSettings), + .target(name: "ImperialX", dependencies: ["ImperialCore", .product(name: "Crypto", package: "swift-crypto")], swiftSettings: swiftSettings), .testTarget( name: "ImperialTests", dependencies: [ @@ -71,6 +75,7 @@ let package = Package( .target(name: "ImperialKeycloak"), .target(name: "ImperialMicrosoft"), .target(name: "ImperialShopify"), + .target(name: "ImperialX"), .product(name: "XCTVapor", package: "vapor"), ], swiftSettings: swiftSettings diff --git a/Sources/ImperialX/Service+X.swift b/Sources/ImperialX/Service+X.swift new file mode 100644 index 00000000..d80519bb --- /dev/null +++ b/Sources/ImperialX/Service+X.swift @@ -0,0 +1,6 @@ +extension OAuthService { + public static let x = OAuthService( + name: "x", + endpoints: [:] + ) +} \ No newline at end of file diff --git a/Sources/ImperialX/Session+X.swift b/Sources/ImperialX/Session+X.swift new file mode 100644 index 00000000..1682b8c5 --- /dev/null +++ b/Sources/ImperialX/Session+X.swift @@ -0,0 +1,24 @@ +import Vapor + +extension Session { + enum XKey { + static let state = "x_state" + static let codeChallenge = "x_code_challenge" + } + + func setState(_ state: String?) throws { + try self.set(XKey.state, to: state) + } + + func state() -> String? { + return try? self.get(XKey.state, as: String.self) + } + + func setCodeChallenge(_ challenge: String?) throws { + try self.set(XKey.codeChallenge, to: challenge) + } + + func codeChallenge() -> String? { + return try? self.get(XKey.codeChallenge, as: String.self) + } +} \ No newline at end of file diff --git a/Sources/ImperialX/X.swift b/Sources/ImperialX/X.swift new file mode 100644 index 00000000..5f024960 --- /dev/null +++ b/Sources/ImperialX/X.swift @@ -0,0 +1,24 @@ +@_exported import ImperialCore +import Vapor + +final public class X: FederatedService { + public let tokens: any FederatedServiceTokens + public let router: any FederatedServiceRouter + + @discardableResult + public required init( + routes: some RoutesBuilder, + authenticate: String, + authenticateCallback: (@Sendable (Request) async throws -> Void)?, + callback: String, + scope: [String] = [], + completion: @escaping @Sendable (Request, String) async throws -> some AsyncResponseEncodable + ) throws { + self.router = try XRouter(callback: callback, scope: scope, completion: completion) + self.tokens = self.router.tokens + + try self.router.configureRoutes(withAuthURL: authenticate, authenticateCallback: authenticateCallback, on: routes) + + OAuthService.services[OAuthService.x.name] = .x + } +} \ No newline at end of file diff --git a/Sources/ImperialX/XAuth.swift b/Sources/ImperialX/XAuth.swift new file mode 100644 index 00000000..2132d239 --- /dev/null +++ b/Sources/ImperialX/XAuth.swift @@ -0,0 +1,20 @@ +import Vapor + +final public class XAuth: FederatedServiceTokens { + public static let idEnvKey: String = "X_CLIENT_ID" + public static let secretEnvKey: String = "X_CLIENT_SECRET" + public let clientID: String + public let clientSecret: String + + public required init() throws { + guard let clientID = Environment.get(XAuth.idEnvKey) else { + throw ImperialError.missingEnvVar(XAuth.idEnvKey) + } + self.clientID = clientID + + guard let clientSecret = Environment.get(XAuth.secretEnvKey) else { + throw ImperialError.missingEnvVar(XAuth.secretEnvKey) + } + self.clientSecret = clientSecret + } +} \ No newline at end of file diff --git a/Sources/ImperialX/XCallbackBody.swift b/Sources/ImperialX/XCallbackBody.swift new file mode 100644 index 00000000..9bcc1ba0 --- /dev/null +++ b/Sources/ImperialX/XCallbackBody.swift @@ -0,0 +1,21 @@ +import Vapor + +struct XCallbackBody: Content { + let code: String + let clientId: String + let clientSecret: String + let redirectURI: String + let codeVerifier: String + let grantType: String = "authorization_code" + + static let defaultContentType: HTTPMediaType = .urlEncodedForm + + enum CodingKeys: String, CodingKey { + case code + case clientId = "client_id" + case clientSecret = "client_secret" + case redirectURI = "redirect_uri" + case codeVerifier = "code_verifier" + case grantType = "grant_type" + } +} \ No newline at end of file diff --git a/Sources/ImperialX/XRouter.swift b/Sources/ImperialX/XRouter.swift new file mode 100644 index 00000000..ca878e9b --- /dev/null +++ b/Sources/ImperialX/XRouter.swift @@ -0,0 +1,113 @@ +import Foundation +import Vapor +import Crypto + +final public class XRouter: FederatedServiceRouter { + public let tokens: any FederatedServiceTokens + public let callbackCompletion: @Sendable (Request, String) async throws -> any AsyncResponseEncodable + public let scope: [String] + public let callbackURL: String + public let accessTokenURL: String = "https://api.twitter.com/2/oauth2/token" + public let service: OAuthService = .x + private let codeVerifier: String + + public required init( + callback: String, + scope: [String], + completion: @escaping @Sendable (Request, String) async throws -> some AsyncResponseEncodable + ) throws { + self.tokens = try XAuth() + self.callbackURL = callback + self.callbackCompletion = completion + self.scope = scope + self.codeVerifier = Data((0..<32).map { _ in UInt8.random(in: 33...126) }) + .base64EncodedString() + .replacingOccurrences(of: "+", with: "-") + .replacingOccurrences(of: "/", with: "_") + .replacingOccurrences(of: "=", with: "") + } + + public func authURL(_ request: Request) throws -> String { + var components = URLComponents() + components.scheme = "https" + components.host = "twitter.com" + components.path = "/i/oauth2/authorize" + + let state = String(UUID().uuidString.prefix(6)) + try request.session.setState(state) + + // Store code verifier + try request.session.setCodeChallenge(self.codeVerifier) + + // Generate code challenge using SHA256 + let hash = SHA256.hash(data: self.codeVerifier.data(using: .utf8)!) + let codeChallenge = Data(hash).base64EncodedString() + .replacingOccurrences(of: "+", with: "-") + .replacingOccurrences(of: "/", with: "_") + .replacingOccurrences(of: "=", with: "") + + components.queryItems = [ + clientIDItem, + redirectURIItem, + .init(name: "scope", value: scope.joined(separator: "%20")), + .init(name: "state", value: state), + codeResponseTypeItem, + .init(name: "code_challenge", value: codeChallenge), + .init(name: "code_challenge_method", value: "S256") + ] + + guard let url = components.url else { + throw Abort(.internalServerError) + } + + return url.absoluteString + } + + public func fetchToken(from request: Request) async throws -> String { + guard let code = request.query[String.self, at: "code"] else { + throw Abort(.badRequest) + } + + if let state = request.query[String.self, at: "state"] { + let xState = request.session.state() + guard state == xState else { throw Abort(.badRequest) } + } + + let body = callbackBody(with: code) + let url = URI(string: accessTokenURL) + let buffer = try await body.encodeResponse(for: request).body.buffer + let response = try await request.client.post(url) { $0.body = buffer } + return try response.content.get(String.self, at: ["access_token"]) + } + + // public func refreshAccessToken(using refreshToken: String, on request: Request) async throws -> String { + // let body: [String: String] = [ + // "grant_type": "refresh_token", + // "refresh_token": refreshToken, + // "client_id": tokens.clientID + // ] + // let url = URI(string: accessTokenURL) + + // let credentials = "\(tokens.clientID):\(tokens.clientSecret)" + // let base64Credentials = Data(credentials.utf8).base64EncodedString() + + // let response = try await request.client.post(url) { req in + // req.headers.contentType = .urlEncodedForm + // req.headers.add(name: .authorization, value: "Basic \(base64Credentials)") + // try req.content.encode(body, as: .urlEncodedForm) + // } + + // return try response.content.get(String.self, at: ["access_token"]) + // } + + + public func callbackBody(with code: String) -> any AsyncResponseEncodable { + XCallbackBody( + code: code, + clientId: tokens.clientID, + clientSecret: tokens.clientSecret, + redirectURI: callbackURL, + codeVerifier: self.codeVerifier + ) + } +}