Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Authentication framework #21

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ let package = Package(
.package(url: "https://github.com/apple/swift-nio-ssl.git", from: "2.4.0"),
.package(url: "https://github.com/swift-server/async-http-client.git", from: "1.2.0"),
.package(url: "https://github.com/swift-server/swift-service-lifecycle.git", from: "1.0.0-alpha.6"),
.package(url: "https://github.com/swift-extras/swift-extras-base64.git", from: "0.5.0"),
],
targets: [
.target(name: "CURLParser", dependencies: []),
Expand All @@ -45,6 +46,7 @@ let package = Package(
.product(name: "Logging", package: "swift-log"),
.product(name: "NIO", package: "swift-nio"),
.product(name: "NIOHTTP1", package: "swift-nio"),
.product(name: "ExtrasBase64", package: "swift-extras-base64"),
]),
.target(name: "HummingbirdFiles", dependencies: [
.byName(name: "Hummingbird"),
Expand Down
13 changes: 13 additions & 0 deletions Sources/Hummingbird/Authentication/Authenticator.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import NIO

public protocol HBAuthenticator: HBMiddleware {
func authenticate(request: HBRequest) -> EventLoopFuture<Void>
}

extension HBAuthenticator {
public func apply(to request: HBRequest, next: HBResponder) -> EventLoopFuture<HBResponse> {
authenticate(request: request).flatMap {
next.respond(to: request)
}
}
}
26 changes: 26 additions & 0 deletions Sources/Hummingbird/Authentication/BasicAuthentication.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import ExtrasBase64

public struct BasicAuthentication {
public let username: String
public let password: String
}

extension HBRequest.Auth {
public var basic: BasicAuthentication? {
// check for authorization header
guard let authorization = request.headers["Authorization"].first else { return nil }
// check for basic prefix
guard authorization.hasPrefix("Basic ") else { return nil }
// extract base64 data
let base64 = String(authorization.dropFirst("Basic ".count))
// decode base64
guard let data = try? base64.base64decoded() else { return nil }
// create string from data
let usernamePassword = String(decoding: data, as: Unicode.UTF8.self)
// split string
let split = usernamePassword.split(separator: ":", maxSplits: 1)
// need two splits
guard split.count == 2 else { return nil }
return .init(username: String(split[0]), password: String(split[1]))
}
}
14 changes: 14 additions & 0 deletions Sources/Hummingbird/Authentication/BearerAuthentication.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
public struct BearerAuthentication {
public let token: String
}

extension HBRequest.Auth {
public var bearer: BearerAuthentication? {
// check for authorization header
guard let authorization = request.headers["Authorization"].first else { return nil }
// check for bearer prefix
guard authorization.hasPrefix("Bearer ") else { return nil }
// return token
return .init(token: String(authorization.dropFirst("Bearer ".count)))
}
}
39 changes: 39 additions & 0 deletions Sources/Hummingbird/Authentication/Request+Auth.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
extension HBRequest {
public struct Auth {
/// Login with type
/// - Parameter auth: authentication details
public func login<Auth>(_ auth: Auth) {
var logins = self.loginCache ?? [:]
logins[ObjectIdentifier(Auth.self)] = auth
self.request.extensions.set(\.auth.loginCache, value: logins)
}

/// Logout type
/// - Parameter auth: authentication type
public func logout<Auth>(_: Auth.Type) {
if var logins = self.loginCache {
logins[ObjectIdentifier(Auth.self)] = nil
self.request.extensions.set(\.auth.loginCache, value: logins)
}
}

/// Return authenticated type
/// - Parameter auth: Type required
public func get<Auth>(_: Auth.Type) -> Auth? {
return self.loginCache?[ObjectIdentifier(Auth.self)] as? Auth
}

/// Return if request is authenticated with type
/// - Parameter auth: Authentication type
public func has<Auth>(_: Auth.Type) -> Bool {
return self.loginCache?[ObjectIdentifier(Auth.self)] != nil
}

var loginCache: [ObjectIdentifier: Any]? { self.request.extensions.get(\.auth.loginCache) }

let request: HBRequest
}

/// Authentication object
public var auth: Auth { return .init(request: self) }
}
75 changes: 75 additions & 0 deletions Tests/HummingbirdTests/AuthenticationTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import ExtrasBase64
import Hummingbird
import XCTest

class AuthenticationTests: XCTestCase {
func testBasicAuthentication() {
let app = HBApplication(testing: .embedded, configuration: .init(maxUploadSize: 65536))
app.router.get("/authenticate") { request -> [String] in
guard let basic = request.auth.basic else { throw HBHTTPError(.unauthorized) }
return [basic.username, basic.password]
}
app.XCTStart()
defer { app.XCTStop() }

let basic = "adamfowler:testpassword"
let basicHeader = "Basic \(String(base64Encoding: basic.utf8))"
app.XCTExecute(uri: "/authenticate", method: .GET, headers: ["Authorization": basicHeader]) { response in
let body = try XCTUnwrap(response.body)
XCTAssertEqual(String(buffer: body), #"["adamfowler", "testpassword"]"#)
}
}

func testBearerAuthentication() {
let app = HBApplication(testing: .embedded, configuration: .init(maxUploadSize: 65536))
app.router.get("/authenticate") { request -> String? in
return request.auth.bearer?.token
}
app.XCTStart()
defer { app.XCTStop() }

app.XCTExecute(
uri: "/authenticate",
method: .GET,
headers: ["Authorization": "Bearer jh345jjefgi34rj"]
) { response in
let body = try XCTUnwrap(response.body)
XCTAssertEqual(String(buffer: body), "jh345jjefgi34rj")
}
}

func testAuthenticator() {
struct MyAuthenticator: HBAuthenticator {
func authenticate(request: HBRequest) -> EventLoopFuture<Void> {
guard let basic = request.auth.basic else { return request.success(()) }
if basic.username == "adamfowler", basic.password == "password" {
request.auth.login(basic)
}
return request.success(())
}
}
let app = HBApplication(testing: .embedded, configuration: .init(maxUploadSize: 65536))
app.middleware.add(MyAuthenticator())
app.router.get("/authenticate") { request -> HTTPResponseStatus in
return request.auth.has(BasicAuthentication.self) ? .ok : .unauthorized
}
app.XCTStart()
defer { app.XCTStop() }

do {
let basic = "adamfowler:nopassword"
let basicHeader = "Basic \(String(base64Encoding: basic.utf8))"
app.XCTExecute(uri: "/authenticate", method: .GET, headers: ["Authorization": basicHeader]) { response in
XCTAssertEqual(response.status, .unauthorized)
}
}

do {
let basic = "adamfowler:password"
let basicHeader = "Basic \(String(base64Encoding: basic.utf8))"
app.XCTExecute(uri: "/authenticate", method: .GET, headers: ["Authorization": basicHeader]) { response in
XCTAssertEqual(response.status, .ok)
}
}
}
}