Skip to content

Commit

Permalink
Add session authenticator (#15)
Browse files Browse the repository at this point in the history
* Add session authenticator

* comments

* Add configurable session id storage

* formatting

* Have separate storage for session management

* Use branch main of hummingbird

* Use 1.0.0-alpha branches

* swift format

* fix tests

* Moved session tests,add header test
  • Loading branch information
adam-fowler authored Nov 17, 2022
1 parent 7d5df79 commit 77aff53
Show file tree
Hide file tree
Showing 10 changed files with 513 additions and 21 deletions.
4 changes: 2 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ let package = Package(
.library(name: "HummingbirdAuthXCT", targets: ["HummingbirdAuthXCT"]),
],
dependencies: [
.package(url: "/~https://github.com/hummingbird-project/hummingbird.git", from: "0.13.1"),
.package(url: "/~https://github.com/hummingbird-project/hummingbird.git", from: "1.0.0-alpha"),
.package(url: "/~https://github.com/apple/swift-crypto.git", "1.0.0"..<"3.0.0"),
.package(url: "/~https://github.com/apple/swift-nio.git", from: "2.33.0"),
.package(url: "/~https://github.com/swift-extras/swift-extras-base64.git", .upToNextMinor(from: "0.7.0")),
Expand All @@ -22,7 +22,7 @@ let package = Package(
.product(name: "Crypto", package: "swift-crypto"),
.product(name: "ExtrasBase64", package: "swift-extras-base64"),
.product(name: "Hummingbird", package: "hummingbird"),
.product(name: "_NIOConcurrency", package: "swift-nio"),
.product(name: "HummingbirdFoundation", package: "hummingbird"),
]),
.target(name: "HummingbirdAuthXCT", dependencies: [
.byName(name: "HummingbirdAuth"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,13 @@ import NIOCore

/// Async version of Middleware to check if a request is authenticated and then augment the request with
/// authentication data.
@available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *)
@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
public protocol HBAsyncAuthenticator: HBAuthenticator {
associatedtype Value = Value
func authenticate(request: HBRequest) async throws -> Value?
}

@available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *)
@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
extension HBAsyncAuthenticator {
public func authenticate(request: HBRequest) -> EventLoopFuture<Value?> {
let promise = request.eventLoop.makePromise(of: Value?.self)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Hummingbird server framework project
//
// Copyright (c) 2021-2021 the Hummingbird authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See hummingbird/CONTRIBUTORS.txt for the list of Hummingbird authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

#if compiler(>=5.5) && canImport(_Concurrency)

import Hummingbird
import NIOCore

/// Async version of session authenticator.
@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
public protocol HBAsyncSessionAuthenticator: HBAsyncAuthenticator {
associatedtype Value = Value
associatedtype Session: Codable

/// Convert Session object into authenticated user
/// - Parameters:
/// - from: session
/// - request: request being processed
/// - Returns: optional authenticated user
func getValue(from: Session, request: HBRequest) async throws -> Value?
}

extension HBAsyncSessionAuthenticator {
public func authenticate(request: HBRequest) async throws -> Value? {
let session: Session? = try await request.session.load()
guard let session = session else {
return nil
}
return try await getValue(from: session, request: request)
}
}

#endif // compiler(>=5.5) && canImport(_Concurrency)
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Hummingbird server framework project
//
// Copyright (c) 2021-2022 the Hummingbird authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See hummingbird/CONTRIBUTORS.txt for the list of Hummingbird authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

#if compiler(>=5.5) && canImport(_Concurrency)

import ExtrasBase64
import Foundation
import Hummingbird

@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
extension SessionManager {
/// save session
public func save<Session: Codable>(session: Session, expiresIn: TimeAmount) async throws {
let sessionId = Self.createSessionId()
// prefix with "hbs."
try await self.request.application.sessionStorage.driver.set(
key: "hbs.\(sessionId)",
value: session,
expires: expiresIn,
request: self.request
).get()
setId(sessionId)
}

/// load session
public func load<Session: Codable>(as: Session.Type = Session.self) async throws -> Session? {
guard let sessionId = getId() else { return nil }
// prefix with "hbs."
return try await self.request.application.sessionStorage.driver.get(
key: "hbs.\(sessionId)",
as: Session.self,
request: self.request
).get()
}
}

#endif // compiler(>=5.5) && canImport(_Concurrency)
48 changes: 48 additions & 0 deletions Sources/HummingbirdAuth/Sessions/Application+session.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Hummingbird server framework project
//
// Copyright (c) 2021-2022 the Hummingbird authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See hummingbird/CONTRIBUTORS.txt for the list of Hummingbird authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

import Hummingbird

extension HBApplication {
/// Accessor for session storage
public var sessionStorage: Persist {
self.extensions.get(
\.sessionStorage,
error: "To use session storage you need to set it up with `HBApplication.addSessions`."
)
}

/// Add session management to `HBApplication`.
/// - Parameters:
/// - storage: Factory struct that will create the persist driver for session storage
/// - sessionID: Where session id is stored in request/response
public func addSessions(
using storage: HBPersistDriverFactory,
sessionID: SessionManager.SessionIDStorage = .cookie("HB_SESSION_ID")
) {
SessionManager.sessionID = sessionID
self.extensions.set(\.sessionStorage, value: .init(storage, application: self)) { persist in
persist.driver.shutdown()
}
}

/// Add session management to `HBApplication` using default persist memory driver
/// - Parameter sessionID: Where session id is stored in request/response
public func addSessions(
sessionID: SessionManager.SessionIDStorage = .cookie("HB_SESSION_ID")
) {
SessionManager.sessionID = sessionID
self.extensions.set(\.sessionStorage, value: self.persist)
}
}
87 changes: 87 additions & 0 deletions Sources/HummingbirdAuth/Sessions/Request+session.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Hummingbird server framework project
//
// Copyright (c) 2021-2022 the Hummingbird authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See hummingbird/CONTRIBUTORS.txt for the list of Hummingbird authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

import ExtrasBase64
import Foundation
import Hummingbird
import HummingbirdFoundation

/// Manage session ids and associated data
public struct SessionManager {
internal static var sessionID: SessionIDStorage = .cookie("SESSION_ID")

// enum defining where to store a session id
public enum SessionIDStorage {
case cookie(String)
case header(String)
}

/// save session
public func save<Session: Codable>(session: Session, expiresIn: TimeAmount) -> EventLoopFuture<Void> {
let sessionId = Self.createSessionId()
// prefix with "hbs."
return self.request.application.sessionStorage.driver.set(
key: "hbs.\(sessionId)",
value: session,
expires: expiresIn,
request: self.request
).map { _ in setId(sessionId) }
}

/// load session
public func load<Session: Codable>(as: Session.Type = Session.self) -> EventLoopFuture<Session?> {
guard let sessionId = getId() else { return self.request.success(nil) }
// prefix with "hbs."
return self.request.application.sessionStorage.driver.get(
key: "hbs.\(sessionId)",
as: Session.self,
request: self.request
)
}

/// Get session id gets id from request
public func getId() -> String? {
switch Self.sessionID {
case .cookie(let cookie):
guard let sessionCookie = request.cookies[cookie]?.value else { return nil }
return String(sessionCookie)
case .header(let header):
guard let sessionHeader = request.headers[header].first else { return nil }
return sessionHeader
}
}

/// set session id on response
public func setId(_ id: String) {
switch Self.sessionID {
case .cookie(let cookie):
self.request.response.setCookie(.init(name: cookie, value: id))
case .header(let header):
self.request.response.headers.replaceOrAdd(name: header, value: id)
}
}

/// create a session id
public static func createSessionId() -> String {
let bytes: [UInt8] = (0..<32).map { _ in UInt8.random(in: 0...255) }
return String(base64Encoding: bytes)
}

let request: HBRequest
}

extension HBRequest {
/// access session info
public var session: SessionManager { return .init(request: self) }
}
43 changes: 43 additions & 0 deletions Sources/HummingbirdAuth/Sessions/SessionAuthenticator.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Hummingbird server framework project
//
// Copyright (c) 2021-2022 the Hummingbird authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See hummingbird/CONTRIBUTORS.txt for the list of Hummingbird authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

import Hummingbird

/// Session authenticator
public protocol HBSessionAuthenticator: HBAuthenticator {
/// authenticable value
associatedtype Value = Value
/// session object
associatedtype Session: Codable

/// Convert Session object into authenticated user
/// - Parameters:
/// - from: session
/// - request: request being processed
/// - Returns: Future holding optional authenticated user
func getValue(from: Session, request: HBRequest) -> EventLoopFuture<Value?>
}

extension HBSessionAuthenticator {
public func authenticate(request: HBRequest) -> EventLoopFuture<Value?> {
return request.session.load().flatMap { (session: Session?) in
// check if session exists.
guard let session = session else {
return request.success(nil)
}
// find authenticated user from session
return getValue(from: session, request: request)
}
}
}
Loading

0 comments on commit 77aff53

Please sign in to comment.