-
Notifications
You must be signed in to change notification settings - Fork 19
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Multiple Type-Safe Authentication Methods (#68)
- Loading branch information
1 parent
4459dbf
commit 7fb7dc7
Showing
7 changed files
with
615 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} | ||
|
||
|
||
|
||
|
||
} |
Oops, something went wrong.