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 +}()