Skip to content

Commit

Permalink
feat: TypeSafeHTTPBasic (#49)
Browse files Browse the repository at this point in the history
  • Loading branch information
Andrew-Lees11 authored and ianpartridge committed Jun 5, 2018
1 parent f9328fb commit cb3445b
Show file tree
Hide file tree
Showing 5 changed files with 304 additions and 2 deletions.
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ let package = Package(
)
],
dependencies: [
.package(url: "https://github.com/IBM-Swift/Kitura-Credentials.git", from: "2.0.0"),
.package(url: "https://github.com/IBM-Swift/Kitura-Credentials.git", from: "2.2.0"),
],
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
Expand Down
34 changes: 34 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,40 @@ CredentialsHTTPDigest initialization is similar to CredentialsHTTPBasic. In addi

## Example

### Codable routing

First create a struct or final class that conforms to `TypeSafeHTTPBasic`,
adding any instance variables which you will initialise in verifyPassword:

```swift
import CredentialsHTTP

public struct MyBasicAuth: TypeSafeHTTPBasic {

public let id: String

static let users = ["John" : "12345", "Mary" : "qwerasdf"]

public static func verifyPassword(username: String, password: String, callback: @escaping (MyBasicAuth?) -> Void) {
if let storedPassword = users[username], storedPassword == password {
callback(MyBasicAuth(id: username))
} else {
callback(nil)
}
}
}
```

Add authentication to routes by adding your `TypeSafeHTTPBasic` object, as a `TypeSafeMiddleware`, to your codable routes:

```swift
router.get("/authedFruits") { (userProfile: MyBasicAuth, respondWith: (MyBasicAuth?, RequestError?) -> Void) in
print("authenticated \(userProfile.id) using \(userProfile.provider)")
respondWith(userProfile, nil)
}
```

### Raw routing
This example shows how to use this plugin to authenticate requests with HTTP Basic authentication. HTTP Digest authentication is similar.
<br>

Expand Down
126 changes: 126 additions & 0 deletions Sources/CredentialsHTTP/TypeSafeHTTPBasic.swift
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 + "\""])
}
}
}
}
141 changes: 141 additions & 0 deletions Tests/CredentialsHTTPTests/TestTypeSafeBasic.swift
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
}
}


}
3 changes: 2 additions & 1 deletion Tests/LinuxMain.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,6 @@ import XCTest

XCTMain([
testCase(TestBasic.allTests),
testCase(TestDigest.allTests)
testCase(TestDigest.allTests),
testCase(TestTypeSafeBasic.allTests)
])

0 comments on commit cb3445b

Please sign in to comment.