From 7fb7dc7b406a6c5702ccf851d6d0f2e1d8896786 Mon Sep 17 00:00:00 2001 From: Andrew Lees <32634907+Andrew-Lees11@users.noreply.github.com> Date: Mon, 18 Jun 2018 16:10:45 +0100 Subject: [PATCH] feat: Multiple Type-Safe Authentication Methods (#68) --- README.md | 38 ++- .../MultiTypeSafeCredentials.swift | 101 +++++++ Sources/Credentials/TypeSafeCredentials.swift | 13 +- .../CredentialsTests/DummyTypeSafeTypes.swift | 111 ++++++++ .../CredentialsTests/TestTypeSafeBasic.swift | 106 ++++++++ .../TestTypeSafeMultiCredentials.swift | 250 ++++++++++++++++++ Tests/LinuxMain.swift | 4 +- 7 files changed, 615 insertions(+), 8 deletions(-) create mode 100644 Sources/Credentials/MultiTypeSafeCredentials.swift create mode 100644 Tests/CredentialsTests/DummyTypeSafeTypes.swift create mode 100644 Tests/CredentialsTests/TestTypeSafeBasic.swift create mode 100644 Tests/CredentialsTests/TestTypeSafeMultiCredentials.swift diff --git a/README.md b/README.md index 5756ed7..f41f14c 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,6 @@ Kitura-Credentials is an authentication middleware for Kitura. Kitura-Credential Plugins can range from a simple password based authentication or delegated authentication using OAuth (via Facebook OAuth provider, etc.), or federated authentication using OpenID. - There are two main authentication schemes supported by Kitura-Credentials: redirecting and non-redirecting. Redirecting scheme is used for example in OAuth2 Authorization Code flow authentication, where the users, that are not logged in, are redirected to a login page. All other types of authentication are non-redirecting, i.e., unauthorized requests are rejected (usually with 401 Unauthorized HTTP status code). An example of non-redirecting authentication is delegated authentication using OAuth access token (also called bearer token) that was independently acquired (say by a mobile app or other client of the Kitura based backend). Kitura-Credentials middleware checks if the request belongs to a session. If so and the user is logged in, it updates request's user profile and propagates the request. Otherwise, it loops through the non-redirecting plugins in the order they were registered until a matching plugin is found. The plugin either succeeds to authenticate the request (in that case user profile information is returned) or fails. If a matching plugin is found but it fails to authenticate the request, HTTP status code in the router response is set to Unauthorized (401), or to the code returned from the plugin along with HTTP headers, and the request is not propagated. If no matching plugin is found, in case the request belongs to a session and a redirecting plugin exists, the request is redirected. Otherwise, HTTP status code in the router response is set to Unauthorized (401), or to the first code returned from the plugins along with HTTP headers, and the request is not propagated. In case of successful authentication, request's user profile is set with user profile information received from the authenticating plugin. @@ -50,6 +49,43 @@ The latest version of Kitura-Credentials requires **Swift 4.0** or newer. You ca ## Example +### Codable routing + +Within Codable routes, you implement a single credentials plugin by defining a Swift type that conforms to the plugins implementation of `TypeSafeCredentials`. This can then be applied to a codable route by defining it in the route signiture: + +```swift +router.get("/authenticated") { (userProfile: BasicAuthedUser, respondWith: (BasicAuthedUser?, RequestError?) -> Void) in + print("authenticated \(userProfile.id) using \(userProfile.provider)") + respondWith(userProfile, nil) +} +``` + +To apply multiple authentication methods to a route, you define a type which conforms to `TypeSafeMultiCredentials` and add it to your codable route signiture. The type must define an array of `TypeSafeCredentials` types, that will be queried in order, to attempt to authenticate a user. It must also define an initialiser that creates an instance of self from an instance of the `TypeSafeCredentials` type. + +If a user can authenticate with either HTTP basic or a token, and has defined the types `BasicAuthedUser` and `TokenAuthedUser`, then an implementation could be as follows: + +```swift +public struct MultiAuthedUser : TypeSafeMultiCredentials { + + public let id: String + public let provider: String + + public static var authenticationMethods: [TypeSafeCredentials.Type] = [BasicAuthedUser.self, TokenAuthedUser.self] + + public init(successfulAuth: TypeSafeCredentials) { + self.id = successfulAuth.id + self.provider = successfulAuth.provider + } +} + +router.get("/multiauth") { (userProfile: MultiAuthedUser, respondWith: (MultiAuthedUser?, RequestError?) -> Void) in + print("authenticated \(userProfile.id) using \(userProfile.provider)") + respondWith(userProfile, nil) +} +``` + +### Raw routing + For OAuth2 Authorization Code flow authentication example please see [Kitura-Credentials-Sample](https://github.com/IBM-Swift/Kitura-Credentials-Sample).
diff --git a/Sources/Credentials/MultiTypeSafeCredentials.swift b/Sources/Credentials/MultiTypeSafeCredentials.swift new file mode 100644 index 0000000..ff5189a --- /dev/null +++ b/Sources/Credentials/MultiTypeSafeCredentials.swift @@ -0,0 +1,101 @@ +/** + * Copyright IBM Corporation 2018 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + **/ + +import Kitura +import LoggerAPI + +import Foundation + +// MARK TypeSafeMultiCredentials + +/** + A `TypeSafeMiddleware` for authenticating users using multiple authentication methods. A type conforming to `TypeSafeMultiCredentials` must implement a static array of `TypeSafeCredentials` types and an initializer, which takes a `TypeSafeCredentials` instance. The route will attempt to authenticate by iterating through this array of `TypeSafeCredentials` until an authentication succeeds. This returns an instance of the succesful `TypeSafeCredentials` which is used to initialize the `TypeSafeMultiCredentials` instance. If a plugin fails or you reach then end of your `TypeSafeCredentials array` an unauthorized response is sent. The type conforming to `TypeSafeMultiCredentials` can then be used as a middleware in codable routes. + ### Usage Example: ### + ```swift + public final class AuthedUser: TypeSafeMultiCredentials { + + public let id: String + public let provider: String + public let name: String? + + } extension TypeSafeMultiCredentials { + static let authenticationMethods: [TypeSafeCredentials.Type] = [MyBasicAuth.self, GoogleTokenProfile.self] + + init(successfulAuth: TypeSafeCredentials) { + self.id = successfulAuth.id + self.provider = successfulAuth.provider + } + } + + router.get("/protected") { (authedUser: AuthedUser, respondWith: (AuthedUser?, RequestError?) -> Void) in + print("user: \(authedUser.id) successfully authenticated using: \(authedUser.provider)") + respondWith(authedUser, nil) + } + ``` + */ +public protocol TypeSafeMultiCredentials: TypeSafeCredentials { + + /// An array of authentication types that conform to `TypeSafeCredentials`. The `authenticate` function for each type will be called in order and, on successfully authenticating, will call `init` using the `TypeSafeCredentials` instance. + static var authenticationMethods: [TypeSafeCredentials.Type] { get } + + /** + This initalizer creates an instance of the type conforming to `TypeSafeMultiCredentials` from a successfully authenticated `TypeSafeCredentials` instance. + ```swift + ### Usage Example: ### + init(successfulAuth: TypeSafeCredentials) { + self.id = successfulAuth.id + self.provider = successfulAuth.provider + switch(successAuth.self) { + case let googleProfile as GoogleTokenProfile: + self.name = googleProfile.name + default: + self.name = nil + } + } + ``` + */ + init(successfulAuth: TypeSafeCredentials) +} + +extension TypeSafeMultiCredentials { + + /// Static function that attempts to create an instance of Self by iterating through an array `TypeSafeCredentials` types and calling `authenticate`. On a successful authentication, an instance of Self is initialized from the `TypeSafeCredentials` instance and returned so it can be used by a `TypeSafeMiddleware` route. On a failed authentication, an unauthorized response is sent immediately. If the authentication header isn't recognised, authenticate is called on the next `TypeSafeCredentials` type. + /// - Parameter request: The `RouterRequest` object used to get information + /// about the request. + /// - Parameter response: The `RouterResponse` object used to respond to the + /// request. + /// - Parameter onSuccess: The closure to invoke in the case of successful authentication. + /// - Parameter onFailure: The closure to invoke in the case of an authentication failure. + /// - Parameter onSkip: The closure to invoke when the plugin doesn't recognize the + /// authentication data in the request. + public static func authenticate(request: RouterRequest, response: RouterResponse, + onSuccess: @escaping (Self) -> Void, + onFailure: @escaping (HTTPStatusCode?, [String : String]?) -> Void, + onSkip: @escaping (HTTPStatusCode?, [String : String]?) -> Void) { + for authentication in authenticationMethods { + authentication.authenticate(request: request, response: response, + onSuccess: { (successfulAuth) in + return onSuccess(Self(successfulAuth: successfulAuth)) + }, onFailure: { (statusCode, _) in + return onFailure(statusCode ?? .unauthorized, nil) + }, onSkip: { (_, _) in + // Do nothing if skipping authentication + }) + } + onSkip(.unauthorized, nil) + } + +} diff --git a/Sources/Credentials/TypeSafeCredentials.swift b/Sources/Credentials/TypeSafeCredentials.swift index 13a82c7..1b258c0 100644 --- a/Sources/Credentials/TypeSafeCredentials.swift +++ b/Sources/Credentials/TypeSafeCredentials.swift @@ -33,14 +33,15 @@ import Foundation public static func authenticate(request: RouterRequest, response: RouterResponse, onSuccess: @escaping (TypeSafeHTTPBasic) -> Void, onFailure: @escaping (HTTPStatusCode?, [String : String]?) -> Void, onSkip: @escaping (HTTPStatusCode?, [String : String]?) -> Void { - if let user = request.urlURL.user, let password = request.urlURL.password { - if users[user] == password { - return onSuccess(UserHTTPBasic(id: user)) + if let user = request.urlURL.user, let password = request.urlURL.password { + if users[user] == password { + return onSuccess(UserHTTPBasic(id: user)) + } else { + return onFailure() + } } else { - return onFailure() + return onSkip() } - } else { - return onSkip() } } ``` diff --git a/Tests/CredentialsTests/DummyTypeSafeTypes.swift b/Tests/CredentialsTests/DummyTypeSafeTypes.swift new file mode 100644 index 0000000..8932971 --- /dev/null +++ b/Tests/CredentialsTests/DummyTypeSafeTypes.swift @@ -0,0 +1,111 @@ +/** + * Copyright IBM Corporation 2018 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + **/ + +import Kitura +import KituraNet +import LoggerAPI +@testable import Credentials +import Foundation + +public struct TypeSafeBasic : TypeSafeCredentials { + + public let id: String + public let provider: String = "HTTPBasic" + private static let users = ["John" : "123", "Doe" : "456"] + + public static func authenticate(request: RouterRequest, response: RouterResponse, onSuccess: @escaping (TypeSafeBasic) -> Void, onFailure: @escaping (HTTPStatusCode?, [String : String]?) -> Void, onSkip: @escaping (HTTPStatusCode?, [String : String]?) -> Void) { + + guard let authorizationHeader = request.headers["Authorization"] else { + return onSkip(.unauthorized, nil) + } + let authorizationHeaderComponents = authorizationHeader.components(separatedBy: " ") + guard authorizationHeaderComponents.count == 2, + authorizationHeaderComponents[0] == "Basic", + let decodedData = Data(base64Encoded: authorizationHeaderComponents[1], options: Data.Base64DecodingOptions(rawValue: 0)), + let userAuthorization = String(data: decodedData, encoding: .utf8) + else { + return onSkip(.unauthorized, nil) + } + let credentials = userAuthorization.components(separatedBy: ":") + guard credentials.count >= 2 else { + return onFailure(.badRequest, nil) + } + + let userid = credentials[0] + let password = credentials[1] + + if users[userid] == password { + return onSuccess(TypeSafeBasic(id: userid)) + } else { + return onFailure(.unauthorized, nil) + } + } +} + +public struct TypeSafeToken : TypeSafeCredentials { + + public let id: String + public let provider: String = "DummyToken" + private static let users = ["John" : "123", "Doe" : "456"] + + public static func authenticate(request: RouterRequest, response: RouterResponse, onSuccess: @escaping (TypeSafeToken) -> Void, onFailure: @escaping (HTTPStatusCode?, [String : String]?) -> Void, onSkip: @escaping (HTTPStatusCode?, [String : String]?) -> Void) { + + guard let type = request.headers["X-token-type"], type == "DummyToken" else { + return onSkip(nil, nil) + } + guard let token = request.headers["access_token"], token == "dummyToken123" else { + return onFailure(nil, nil) + } + + let userProfile = TypeSafeToken(id: token) + onSuccess(userProfile) + } +} + +public struct MultiTypeSafeOnlyBasic : TypeSafeMultiCredentials { + + public let id: String + public let provider: String + + public static var authenticationMethods: [TypeSafeCredentials.Type] = [TypeSafeBasic.self] + + public init(successfulAuth: TypeSafeCredentials) { + self.id = successfulAuth.id + self.provider = successfulAuth.provider + } +} + +public struct MultiTypeSafeTokenBasic : TypeSafeMultiCredentials { + + public let id: String + public let provider: String + + public static var authenticationMethods: [TypeSafeCredentials.Type] = [TypeSafeBasic.self, TypeSafeToken.self] + + public init(successfulAuth: TypeSafeCredentials) { + self.id = successfulAuth.id + self.provider = successfulAuth.provider + } +} + +public struct User: Codable, Equatable { + let name: String + let provider: String + + public static func == (lhs: User, rhs: User) -> Bool { + return lhs.name == rhs.name && lhs.provider == rhs.provider + } +} diff --git a/Tests/CredentialsTests/TestTypeSafeBasic.swift b/Tests/CredentialsTests/TestTypeSafeBasic.swift new file mode 100644 index 0000000..5a0bcd7 --- /dev/null +++ b/Tests/CredentialsTests/TestTypeSafeBasic.swift @@ -0,0 +1,106 @@ +/** + * Copyright IBM Corporation 2018 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + **/ + +import Foundation +import XCTest + +import Kitura +import KituraNet + +@testable import Credentials + +class TestTypeSafeBasic : XCTestCase { + + static var allTests : [(String, (TestTypeSafeBasic) -> () throws -> Void)] { + return [ + ("testTypeSafeNoCredentials", testTypeSafeNoCredentials), + ("testTypeSafeBadCredentials", testTypeSafeBadCredentials), + ("testTypeSafeBasic", testTypeSafeBasic), + ] + } + + let host = "127.0.0.1" + + let router = TestTypeSafeBasic.setupTypeSafeRouter() + + func testTypeSafeNoCredentials() { + performServerTest(router: router) { expectation in + self.performRequest(method: "get", host: self.host, path: "/private/typesafebasic", callback: {response in + XCTAssertNotNil(response, "ERROR!!! ClientRequest response object was nil") + XCTAssertEqual(response?.statusCode, HTTPStatusCode.unauthorized, "HTTP Status code was \(String(describing: response?.statusCode))") + expectation.fulfill() + }) + } + } + + func testTypeSafeBadCredentials() { + + performServerTest(router: router) { expectation in + self.performRequest(method: "get", path:"/private/typesafebasic", callback: {response in + XCTAssertNotNil(response, "ERROR!!! ClientRequest response object was nil") + XCTAssertEqual(response?.statusCode, HTTPStatusCode.unauthorized, "HTTP Status code was \(String(describing: response?.statusCode))") + expectation.fulfill() + }, headers: ["Authorization" : "Basic QWxhZGRpbjpPcGVuU2VzYW1l"]) + } + + performServerTest(router: router) { expectation in + self.performRequest(method: "get", path:"/private/typesafebasic", callback: {response in + XCTAssertNotNil(response, "ERROR!!! ClientRequest response object was nil") + XCTAssertEqual(response?.statusCode, HTTPStatusCode.unauthorized, "HTTP Status code was \(String(describing: response?.statusCode))") + expectation.fulfill() + }, headers: ["Authorization" : "Basic"]) + } + } + + func testTypeSafeBasic() { + + performServerTest(router: router) { expectation in + self.performRequest(method: "get", path:"/private/typesafebasic", callback: {response in + XCTAssertNotNil(response, "ERROR!!! ClientRequest response object was nil") + XCTAssertEqual(response?.statusCode, HTTPStatusCode.OK, "HTTP Status code was \(String(describing: response?.statusCode))") + do { + guard let stringBody = try response?.readString(), + let jsonData = stringBody.data(using: .utf8) + else { + return XCTFail("Did not receive a JSON body") + } + let decoder = JSONDecoder() + let body = try decoder.decode(User.self, from: jsonData) + XCTAssertEqual(body, User(name: "John", provider: "HTTPBasic")) + } catch { + XCTFail("No response body") + } + expectation.fulfill() + // Basic Sm9objoxMjM= is "John" : "123" base64 encoded. + }, headers: ["Authorization" : "Basic Sm9objoxMjM="]) + } + } + + static func setupTypeSafeRouter() -> Router { + let router = Router() + + router.get("/private/typesafebasic") { (authedUser: TypeSafeBasic, respondWith: (User?, RequestError?) -> Void) in + let user = User(name: authedUser.id, provider: authedUser.provider) + respondWith(user, nil) + } + + return router + } + + + + +} diff --git a/Tests/CredentialsTests/TestTypeSafeMultiCredentials.swift b/Tests/CredentialsTests/TestTypeSafeMultiCredentials.swift new file mode 100644 index 0000000..6c146e7 --- /dev/null +++ b/Tests/CredentialsTests/TestTypeSafeMultiCredentials.swift @@ -0,0 +1,250 @@ +/** + * Copyright IBM Corporation 2018 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + **/ + +import Foundation +import XCTest + +import Kitura +import KituraNet + +@testable import Credentials + +class TestTypeSafeMultiCredentials : XCTestCase { + + static var allTests : [(String, (TestTypeSafeMultiCredentials) -> () throws -> Void)] { + return [ + ("testSingleNoCredentials", testSingleNoCredentials), + ("testSingleBadCredentials", testSingleBadCredentials), + ("testSingleBasic", testSingleBasic), + ] + } + + let host = "127.0.0.1" + + let router = TestTypeSafeMultiCredentials.setupTypeSafeRouter() + + func testSingleNoCredentials() { + performServerTest(router: router) { expectation in + self.performRequest(method: "get", host: self.host, path: "/private/onlybasic", callback: {response in + XCTAssertNotNil(response, "ERROR!!! ClientRequest response object was nil") + XCTAssertEqual(response?.statusCode, HTTPStatusCode.unauthorized, "HTTP Status code was \(String(describing: response?.statusCode))") + expectation.fulfill() + }) + } + } + + func testSingleBadCredentials() { + + performServerTest(router: router) { expectation in + self.performRequest(method: "get", path:"/private/onlybasic", callback: {response in + XCTAssertNotNil(response, "ERROR!!! ClientRequest response object was nil") + XCTAssertEqual(response?.statusCode, HTTPStatusCode.unauthorized, "HTTP Status code was \(String(describing: response?.statusCode))") + expectation.fulfill() + }, headers: ["Authorization" : "Basic QWxhZGRpbjpPcGVuU2VzYW1l"]) + } + + performServerTest(router: router) { expectation in + self.performRequest(method: "get", path:"/private/onlybasic", callback: {response in + XCTAssertNotNil(response, "ERROR!!! ClientRequest response object was nil") + XCTAssertEqual(response?.statusCode, HTTPStatusCode.unauthorized, "HTTP Status code was \(String(describing: response?.statusCode))") + expectation.fulfill() + }, headers: ["Authorization" : "Basic"]) + } + } + + func testSingleBasic() { + + performServerTest(router: router) { expectation in + self.performRequest(method: "get", path:"/private/onlybasic", callback: {response in + XCTAssertNotNil(response, "ERROR!!! ClientRequest response object was nil") + XCTAssertEqual(response?.statusCode, HTTPStatusCode.OK, "HTTP Status code was \(String(describing: response?.statusCode))") + do { + guard let stringBody = try response?.readString(), + let jsonData = stringBody.data(using: .utf8) + else { + return XCTFail("Did not receive a JSON body") + } + let decoder = JSONDecoder() + let body = try decoder.decode(User.self, from: jsonData) + XCTAssertEqual(body, User(name: "John", provider: "HTTPBasic")) + } catch { + XCTFail("No response body") + } + expectation.fulfill() + // Basic Sm9objoxMjM= is "John" : "123" base64 encoded. + }, headers: ["Authorization" : "Basic Sm9objoxMjM="]) + } + } + + func testTwoNoCredentials() { + performServerTest(router: router) { expectation in + self.performRequest(method: "get", host: self.host, path: "/private/tokenbasic", callback: {response in + XCTAssertNotNil(response, "ERROR!!! ClientRequest response object was nil") + XCTAssertEqual(response?.statusCode, HTTPStatusCode.unauthorized, "HTTP Status code was \(String(describing: response?.statusCode))") + expectation.fulfill() + }) + } + } + + func testTwoBadCredentials() { + + performServerTest(router: router) { expectation in + self.performRequest(method: "get", path:"/private/tokenbasic", callback: {response in + XCTAssertNotNil(response, "ERROR!!! ClientRequest response object was nil") + XCTAssertEqual(response?.statusCode, HTTPStatusCode.unauthorized, "HTTP Status code was \(String(describing: response?.statusCode))") + expectation.fulfill() + }, headers: ["Authorization" : "Basic QWxhZGRpbjpPcGVuU2VzYW1l", ]) + } + + performServerTest(router: router) { expectation in + self.performRequest(method: "get", path:"/private/tokenbasic", callback: {response in + XCTAssertNotNil(response, "ERROR!!! ClientRequest response object was nil") + XCTAssertEqual(response?.statusCode, HTTPStatusCode.unauthorized, "HTTP Status code was \(String(describing: response?.statusCode))") + expectation.fulfill() + }, headers: ["X-token-type" : "DummyToken", "access_token" : "WrongToken"]) + } + + // Basic authentication fails and so route returns unauthorized immediately + performServerTest(router: router) { expectation in + self.performRequest(method: "get", path:"/private/tokenbasic", callback: {response in + XCTAssertNotNil(response, "ERROR!!! ClientRequest response object was nil") + XCTAssertEqual(response?.statusCode, HTTPStatusCode.unauthorized, "HTTP Status code was \(String(describing: response?.statusCode))") + expectation.fulfill() + }, headers: ["Authorization" : "Basic QWxhZGRpbjpPcGVuU2VzYW1l", "X-token-type" : "DummyToken", "access_token" : "dummyToken123"]) + } + } + + func testTokenBasic() { + + // Authenticate using MultiTypeSafeTokenBasic with basic authentiction + performServerTest(router: router) { expectation in + self.performRequest(method: "get", path:"/private/tokenbasic", callback: {response in + XCTAssertNotNil(response, "ERROR!!! ClientRequest response object was nil") + XCTAssertEqual(response?.statusCode, HTTPStatusCode.OK, "HTTP Status code was \(String(describing: response?.statusCode))") + do { + guard let stringBody = try response?.readString(), + let jsonData = stringBody.data(using: .utf8) + else { + return XCTFail("Did not receive a JSON body") + } + let decoder = JSONDecoder() + let body = try decoder.decode(User.self, from: jsonData) + XCTAssertEqual(body, User(name: "John", provider: "HTTPBasic")) + } catch { + XCTFail("No response body") + } + expectation.fulfill() + // Basic Sm9objoxMjM= is "John" : "123" base64 encoded. + }, headers: ["Authorization" : "Basic Sm9objoxMjM="]) + } + + // Authenticate using MultiTypeSafeTokenBasic with token authentiction + performServerTest(router: router) { expectation in + self.performRequest(method: "get", path:"/private/tokenbasic", callback: {response in + XCTAssertNotNil(response, "ERROR!!! ClientRequest response object was nil") + XCTAssertEqual(response?.statusCode, HTTPStatusCode.OK, "HTTP Status code was \(String(describing: response?.statusCode))") + do { + guard let stringBody = try response?.readString(), + let jsonData = stringBody.data(using: .utf8) + else { + return XCTFail("Did not receive a JSON body") + } + let decoder = JSONDecoder() + let body = try decoder.decode(User.self, from: jsonData) + XCTAssertEqual(body, User(name: "dummyToken123", provider: "DummyToken")) + } catch { + XCTFail("No response body") + } + expectation.fulfill() + // Basic Sm9objoxMjM= is "John" : "123" base64 encoded. + }, headers: ["X-token-type" : "DummyToken", "access_token" : "dummyToken123"]) + } + + // Authenticate using MultiTypeSafeTokenBasic with both provided + // meaning basic authentication is used since it is first in the array + performServerTest(router: router) { expectation in + self.performRequest(method: "get", path:"/private/tokenbasic", callback: {response in + XCTAssertNotNil(response, "ERROR!!! ClientRequest response object was nil") + XCTAssertEqual(response?.statusCode, HTTPStatusCode.OK, "HTTP Status code was \(String(describing: response?.statusCode))") + do { + guard let stringBody = try response?.readString(), + let jsonData = stringBody.data(using: .utf8) + else { + return XCTFail("Did not receive a JSON body") + } + let decoder = JSONDecoder() + let body = try decoder.decode(User.self, from: jsonData) + XCTAssertEqual(body, User(name: "John", provider: "HTTPBasic")) + } catch { + XCTFail("No response body") + } + expectation.fulfill() + // Basic Sm9objoxMjM= is "John" : "123" base64 encoded. + }, headers: ["Authorization" : "Basic Sm9objoxMjM=", "X-token-type" : "DummyToken", "access_token" : "dummyToken123"]) + } + + // Authenticate using MultiTypeSafeTokenBasic with both provided but token is incorrect + // meaning basic authentication is used since it is first in the array + performServerTest(router: router) { expectation in + self.performRequest(method: "get", path:"/private/tokenbasic", callback: {response in + XCTAssertNotNil(response, "ERROR!!! ClientRequest response object was nil") + XCTAssertEqual(response?.statusCode, HTTPStatusCode.OK, "HTTP Status code was \(String(describing: response?.statusCode))") + do { + guard let stringBody = try response?.readString(), + let jsonData = stringBody.data(using: .utf8) + else { + return XCTFail("Did not receive a JSON body") + } + let decoder = JSONDecoder() + let body = try decoder.decode(User.self, from: jsonData) + XCTAssertEqual(body, User(name: "John", provider: "HTTPBasic")) + } catch { + XCTFail("No response body") + } + expectation.fulfill() + // Basic Sm9objoxMjM= is "John" : "123" base64 encoded. + }, headers: ["Authorization" : "Basic Sm9objoxMjM=", "X-token-type" : "DummyToken", "access_token" : "WrongToken"]) + } + + } + + static func setupTypeSafeRouter() -> Router { + let router = Router() + + router.get("/private/onlybasic") { (authedUser: MultiTypeSafeOnlyBasic, respondWith: (User?, RequestError?) -> Void) in + let user = User(name: authedUser.id, provider: authedUser.provider) + respondWith(user, nil) + } + + router.get("/private/tokenbasic") { (authedUser: MultiTypeSafeTokenBasic, respondWith: (User?, RequestError?) -> Void) in + let user = User(name: authedUser.id, provider: authedUser.provider) + respondWith(user, nil) + } + + return router + } + + struct User: Codable, Equatable { + let name: String + let provider: String + + static func == (lhs: User, rhs: User) -> Bool { + return lhs.name == rhs.name && lhs.provider == rhs.provider + } + } + + +} diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift index 4ebcf57..d40ec61 100644 --- a/Tests/LinuxMain.swift +++ b/Tests/LinuxMain.swift @@ -22,5 +22,7 @@ import XCTest XCTMain([ testCase(TestToken.allTests), testCase(TestSession.allTests), - testCase(TestUnauthorizedSession.allTests) + testCase(TestUnauthorizedSession.allTests), + testCase(TestTypeSafeBasic.allTests), + testCase(TestTypeSafeMultiCredentials.allTests) ])