-
Notifications
You must be signed in to change notification settings - Fork 13
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
f9328fb
commit cb3445b
Showing
5 changed files
with
304 additions
and
2 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
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,126 @@ | ||
/** | ||
* 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 Credentials | ||
|
||
import Foundation | ||
|
||
/** | ||
A `TypeSafeCredentials` plugin for HTTP basic authentication. | ||
This protocol will be implemented by a Swift object defined by the user. | ||
The plugin must implement a `verifyPassword` function which takes a username and password as input | ||
and returns an instance of `Self` on success or `nil` on failure. | ||
This instance must contain the authentication `provider` (defaults to "HTTPBasic") and an `id`, uniquely identifying the user. | ||
The users object can then be used in TypeSafeMiddlware routes to authenticate with HTTP basic. | ||
### Usage Example: ### | ||
```swift | ||
public struct MyHTTPBasic: TypeSafeHTTPBasic { | ||
public var id: String | ||
static let users = ["John" : "12345", "Mary" : "qwerasdf"] | ||
public static let realm = "Login message" | ||
public static func verifyPassword(username: String, password: String, callback: @escaping (TestHTTPBasic?) -> Void) { | ||
if let storedPassword = users[username], storedPassword == password { | ||
callback(TestHTTPBasic(id: username)) | ||
} else { | ||
callback(nil) | ||
} | ||
} | ||
} | ||
struct User: Codable { | ||
let name: String | ||
} | ||
router.get("/authedFruits") { (authedUser: MyHTTPBasic, respondWith: (User?, RequestError?) -> Void) in | ||
let user = User(name: authedUser.id) | ||
respondWith(user, nil) | ||
} | ||
``` | ||
*/ | ||
public protocol TypeSafeHTTPBasic : TypeSafeCredentials { | ||
|
||
/// The realm for which these credentials are valid (defaults to "User") | ||
static var realm: String { get } | ||
|
||
/// The function that takes a username, a password and a callback which accepts a TypeSafeHTTPBasic instance on success or nil on failure. | ||
static func verifyPassword(username: String, password: String, callback: @escaping (Self?) -> Void) -> Void | ||
|
||
} | ||
|
||
extension TypeSafeHTTPBasic { | ||
|
||
/// The name of the authentication provider (defaults to "HTTPBasic") | ||
public var provider: String { | ||
return "HTTPBasic" | ||
} | ||
|
||
/// The realm for which these credentials are valid (defaults to "User") | ||
public static var realm: String { | ||
return "User" | ||
} | ||
|
||
/// Authenticate incoming request using HTTP Basic authentication. | ||
/// | ||
/// - 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) { | ||
|
||
let userid: String | ||
let password: String | ||
if let requestUser = request.urlURL.user, let requestPassword = request.urlURL.password { | ||
userid = requestUser | ||
password = requestPassword | ||
} else { | ||
guard let authorizationHeader = request.headers["Authorization"] else { | ||
return onSkip(.unauthorized, ["WWW-Authenticate" : "Basic realm=\"" + realm + "\""]) | ||
} | ||
|
||
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, ["WWW-Authenticate" : "Basic realm=\"" + realm + "\""]) | ||
} | ||
let credentials = userAuthorization.components(separatedBy: ":") | ||
guard credentials.count >= 2 else { | ||
return onFailure(.badRequest, nil) | ||
} | ||
userid = credentials[0] | ||
password = credentials[1] | ||
} | ||
|
||
verifyPassword(username: userid, password: password) { selfInstance in | ||
if let selfInstance = selfInstance { | ||
onSuccess(selfInstance) | ||
} else { | ||
onFailure(.unauthorized, ["WWW-Authenticate" : "Basic realm=\"" + self.realm + "\""]) | ||
} | ||
} | ||
} | ||
} |
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,141 @@ | ||
/** | ||
* Copyright IBM Corporation 2016 | ||
* | ||
* 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 | ||
import Credentials | ||
|
||
@testable import CredentialsHTTP | ||
|
||
class TestTypeSafeBasic : XCTestCase { | ||
|
||
static var allTests : [(String, (TestTypeSafeBasic) -> () throws -> Void)] { | ||
return [ | ||
("testTypeSafeNoCredentials", testTypeSafeNoCredentials), | ||
("testTypeSafeBadCredentials", testTypeSafeBadCredentials), | ||
("testTypeSafeBasic", testTypeSafeBasic), | ||
] | ||
} | ||
|
||
override func setUp() { | ||
doSetUp() | ||
} | ||
|
||
override func tearDown() { | ||
doTearDown() | ||
} | ||
|
||
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))") | ||
XCTAssertEqual(response?.headers["WWW-Authenticate"]?.first, "Basic realm=\"test\"") | ||
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))") | ||
XCTAssertEqual(response?.headers["WWW-Authenticate"]?.first, "Basic realm=\"test\"") | ||
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))") | ||
XCTAssertEqual(response?.headers["WWW-Authenticate"]?.first, "Basic realm=\"test\"") | ||
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: "Mary", provider: "HTTPBasic")) | ||
} catch { | ||
XCTFail("No response body") | ||
} | ||
expectation.fulfill() | ||
}, headers: ["Authorization" : "Basic TWFyeTpxd2VyYXNkZg=="]) | ||
} | ||
} | ||
|
||
static func setupTypeSafeRouter() -> Router { | ||
let router = Router() | ||
|
||
router.get("/private/typesafebasic") { (authedUser: TestHTTPBasic, respondWith: (User?, RequestError?) -> Void) in | ||
let user = User(name: authedUser.id, provider: authedUser.provider) | ||
respondWith(user, nil) | ||
} | ||
|
||
return router | ||
} | ||
|
||
public struct TestHTTPBasic: TypeSafeHTTPBasic { | ||
|
||
public let id: String | ||
|
||
static let users = ["John" : "12345", "Mary" : "qwerasdf"] | ||
|
||
public static let realm = "test" | ||
|
||
public static func verifyPassword(username: String, password: String, callback: @escaping (TestHTTPBasic?) -> Void) { | ||
if let storedPassword = users[username], storedPassword == password { | ||
callback(TestHTTPBasic(id: username)) | ||
} else { | ||
callback(nil) | ||
} | ||
} | ||
} | ||
|
||
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 | ||
} | ||
} | ||
|
||
|
||
} |
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