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

Get rid of LoginCache in favour of a stored property for an identity #67

Merged
merged 7 commits into from
Oct 13, 2024
Merged
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
17 changes: 10 additions & 7 deletions Sources/HummingbirdAuth/Authenticator/AuthRequestContext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,24 @@

import Hummingbird
import Logging
import NIOConcurrencyHelpers
import NIOCore

/// Protocol that all request contexts should conform to if they want to support
/// authentication middleware
public protocol AuthRequestContext: RequestContext {
/// Login cache
var auth: LoginCache { get set }
public protocol AuthRequestContext<Identity>: RequestContext {
associatedtype Identity: Sendable

/// The authenticated identity
var identity: Identity? { get set }
}

/// Implementation of a basic request context that supports authenticators
public struct BasicAuthRequestContext: AuthRequestContext, RequestContext {
public struct BasicAuthRequestContext<Identity: Sendable>: AuthRequestContext, RequestContext {
/// core context
public var coreContext: CoreRequestContextStorage
/// Login cache
public var auth: LoginCache
/// The authenticated identity
public var identity: Identity?

/// Initialize an `RequestContext`
/// - Parameters:
Expand All @@ -37,6 +40,6 @@ public struct BasicAuthRequestContext: AuthRequestContext, RequestContext {
/// - logger: Logger
public init(source: Source) {
self.coreContext = .init(source: source)
self.auth = .init()
self.identity = nil
}
}
32 changes: 14 additions & 18 deletions Sources/HummingbirdAuth/Authenticator/Authenticator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,6 @@
import Hummingbird
import NIOCore

/// Protocol for objects that can be returned by an `AuthenticatorMiddleware`.
public protocol Authenticatable: Sendable {}

/// Protocol for a middleware that checks if a request is authenticated.
///
/// Requires an `authenticate` function that returns authentication data when successful.
Expand All @@ -27,22 +24,21 @@ public protocol Authenticatable: Sendable {}
///
/// To use an authenticator middleware it is required that your request context conform to
/// ``AuthRequestContext`` so the middleware can attach authentication data to
/// ``AuthRequestContext/auth``.
/// ``AuthRequestContext/identity-swift.property``.
///
/// A simple username, password authenticator could be implemented as follows. If the
/// authenticator is successful it returns a `User` struct, otherwise it returns `nil`.
///
/// ```swift
/// struct BasicAuthenticator: AuthenticatorMiddleware {
/// func authenticate<Context: AuthRequestContext>(request: Request, context: Context) async throws -> User? {
/// struct BasicAuthenticator<Context: AuthRequestContext>: AuthenticatorMiddleware {
/// func authenticate(request: Request, context: Context) async throws -> User? {
/// // Basic authentication info in the "Authorization" header, is accessible
/// // via request.headers.basic
/// guard let basic = request.headers.basic else { return nil }
/// // check if user exists in the database and then verify the entered password
/// // against the one stored in the database. If it is correct then login in user
/// let user = try await database.getUserWithUsername(basic.username)
/// // did we find a user
/// guard let user = user else { return nil }
/// // check if user exists in the database
/// guard let user = try await database.getUserWithUsername(basic.username) else {
/// return nil
/// }
/// // verify password against password hash stored in database. If valid
/// // return the user. HummingbirdAuth provides an implementation of Bcrypt
/// // This should be run on the thread pool as it is a long process.
Expand All @@ -55,24 +51,24 @@ public protocol Authenticatable: Sendable {}
/// }
/// }
/// ```
public protocol AuthenticatorMiddleware: RouterMiddleware where Context: AuthRequestContext {
/// type to be authenticated
associatedtype Value: Authenticatable
public protocol AuthenticatorMiddleware: RouterMiddleware where Context: AuthRequestContext<Identity> {
/// Type to be authenticated
associatedtype Identity: Sendable
/// Called by middleware to see if request can authenticate.
///
/// Should return an authenticatable object if authenticated, return nil is not authenticated
/// Should return an object if authenticated, return nil is not authenticated
/// but want the request to be passed onto the next middleware or the router, or throw an error
/// if the request should not proceed any further
func authenticate(request: Request, context: Context) async throws -> Value?
func authenticate(request: Request, context: Context) async throws -> Identity?
}

extension AuthenticatorMiddleware {
/// Calls `authenticate` and if it returns a valid authenticatable object `login` with this object
/// Calls `authenticate` and if it returns a valid object `login` with this object
@inlinable
public func handle(_ request: Request, context: Context, next: (Request, Context) async throws -> Response) async throws -> Response {
if let authenticated = try await authenticate(request: request, context: context) {
var context = context
context.auth.login(authenticated)
context.identity = authenticated
return try await next(request, context)
} else {
return try await next(request, context)
Expand Down
12 changes: 8 additions & 4 deletions Sources/HummingbirdAuth/Authenticator/ClosureAuthenticator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,18 @@
import Hummingbird
import NIOCore

public struct ClosureAuthenticator<Context: AuthRequestContext, Value: Authenticatable>: AuthenticatorMiddleware {
let closure: @Sendable (Request, Context) async throws -> Value?
public struct ClosureAuthenticator<
Context: AuthRequestContext
>: AuthenticatorMiddleware {
public typealias Identity = Context.Identity

public init(_ closure: @escaping @Sendable (Request, Context) async throws -> Value?) {
let closure: @Sendable (Request, Context) async throws -> Context.Identity?

public init(_ closure: @escaping @Sendable (Request, Context) async throws -> Context.Identity?) {
self.closure = closure
}

public func authenticate(request: Request, context: Context) async throws -> Value? {
public func authenticate(request: Request, context: Context) async throws -> Context.Identity? {
return try await self.closure(request, context)
}
}
63 changes: 0 additions & 63 deletions Sources/HummingbirdAuth/Authenticator/LoginCache.swift

This file was deleted.

8 changes: 4 additions & 4 deletions Sources/HummingbirdAuth/Deprecated.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,13 @@
// prefix to the new ones.
@_documentation(visibility: internal) @available(*, unavailable, renamed: "AuthRequestContext")
public typealias HBAuthRequestContext = AuthRequestContext
@_documentation(visibility: internal) @available(*, unavailable, renamed: "Sendable")
public typealias Authenticatable = Sendable
@_documentation(visibility: internal) @available(*, unavailable, renamed: "BasicAuthRequestContext")
public typealias HBBasicAuthRequestContext = BasicAuthRequestContext
@_documentation(visibility: internal) @available(*, unavailable, renamed: "LoginCache")
public typealias HBLoginCache = LoginCache

@_documentation(visibility: internal) @available(*, unavailable, renamed: "Authenticatable")
public typealias HBAuthenticatable = Authenticatable
@_documentation(visibility: internal) @available(*, unavailable, renamed: "Sendable")
public typealias HBAuthenticatable = Sendable
@_documentation(visibility: internal) @available(*, unavailable, renamed: "AuthenticatorMiddleware")
public typealias HBAuthenticator = AuthenticatorMiddleware
@_documentation(visibility: internal) @available(*, unavailable, renamed: "SessionAuthenticator")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,13 @@
import Hummingbird

/// Middleware returning 401 for unauthenticated requests
public struct IsAuthenticatedMiddleware<Auth: Authenticatable, Context: AuthRequestContext>: RouterMiddleware {
public init(_: Auth.Type) {}
public struct IsAuthenticatedMiddleware<Context: AuthRequestContext>: RouterMiddleware {
public init() {}

public func handle(_ request: Request, context: Context, next: (Request, Context) async throws -> Response) async throws -> Response {
guard context.auth.has(Auth.self) else { throw HTTPError(.unauthorized) }
guard context.identity != nil else {
throw HTTPError(.unauthorized)
}
return try await next(request, context)
}
}
13 changes: 9 additions & 4 deletions Sources/HummingbirdAuth/Sessions/SessionAuthenticator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,12 @@ import Hummingbird
///
/// The `SessionAuthenticator` needs to have the ``SessionMiddleware`` before it in the middleware
/// chain to extract session information for the request
public struct SessionAuthenticator<Context: AuthRequestContext & SessionRequestContext, Repository: UserSessionRepository>: AuthenticatorMiddleware where Context.Session == Repository.Identifier {
public struct SessionAuthenticator<
Context: AuthRequestContext & SessionRequestContext,
Repository: UserSessionRepository<Context.Session, Context.Identity>
>: AuthenticatorMiddleware {
public typealias Identity = Context.Identity

/// User repository
public let users: Repository

Expand All @@ -34,10 +39,10 @@ public struct SessionAuthenticator<Context: AuthRequestContext & SessionRequestC
/// - Parameters:
/// - context: Request context type
/// - getUser: Closure returning user type from session id
public init<User: Authenticatable, Session>(
public init<Session>(
context: Context.Type = Context.self,
getUser: @escaping @Sendable (Session, UserRepositoryContext) async throws -> User?
) where Repository == UserSessionClosureRepository<Session, User> {
getUser: @escaping @Sendable (Session, UserRepositoryContext) async throws -> Context.Identity?
) where Repository == UserSessionClosureRepository<Session, Identity> {
self.users = UserSessionClosureRepository(getUser)
}

Expand Down
11 changes: 7 additions & 4 deletions Sources/HummingbirdAuth/Sessions/SessionContext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -122,11 +122,14 @@ public protocol SessionRequestContext<Session>: RequestContext {
}

/// Implementation of a basic request context that supports session storage and authenticators
public struct BasicSessionRequestContext<Session>: AuthRequestContext, SessionRequestContext, RequestContext where Session: Sendable & Codable {
public struct BasicSessionRequestContext<
Session,
Identity: Sendable
>: AuthRequestContext, SessionRequestContext, RequestContext where Session: Sendable & Codable {
/// core context
public var coreContext: CoreRequestContextStorage
/// Login cache
public var auth: LoginCache
/// The authenticated identity
public var identity: Identity?
/// Session
public let sessions: SessionContext<Session>

Expand All @@ -137,7 +140,7 @@ public struct BasicSessionRequestContext<Session>: AuthRequestContext, SessionRe
/// - logger: Logger
public init(source: Source) {
self.coreContext = .init(source: source)
self.auth = .init()
self.identity = nil
self.sessions = .init()
}
}
20 changes: 14 additions & 6 deletions Sources/HummingbirdAuth/Sessions/SessionMiddleware.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import Hummingbird
/// a new session will set the "Set-Cookie" header.
public struct SessionMiddleware<Context: SessionRequestContext>: RouterMiddleware {
/// Storage for session data
let sessionStorage: SessionStorage<SessionData<Context.Session>>
let sessionStorage: SessionStorage<Context.Session>

/// Default duration for a session token
let defaultSessionExpiration: Duration
Expand All @@ -43,7 +43,7 @@ public struct SessionMiddleware<Context: SessionRequestContext>: RouterMiddlewar
/// - Parameters:
/// - sessionStorage: Session storage
/// - defaultSessionExpiration: Default expiration for session data
public init(sessionStorage: SessionStorage<SessionData<Context.Session>>, defaultSessionExpiration: Duration = .seconds(60 * 60 * 12)) {
public init(sessionStorage: SessionStorage<Context.Session>, defaultSessionExpiration: Duration = .seconds(60 * 60 * 12)) {
self.sessionStorage = sessionStorage
self.defaultSessionExpiration = defaultSessionExpiration
}
Expand All @@ -52,18 +52,26 @@ public struct SessionMiddleware<Context: SessionRequestContext>: RouterMiddlewar
public func handle(_ request: Request, context: Context, next: (Request, Context) async throws -> Response) async throws -> Response {
let originalSessionData = try await sessionStorage.load(request: request)
if let originalSessionData {
context.sessions.sessionData = originalSessionData
context.sessions.sessionData = SessionData(
value: originalSessionData,
expiresIn: nil
)
}
var response = try await next(request, context)
let sessionData = context.sessions.sessionData
if let sessionData {
// if session has been edited then store new session
if sessionData.edited {
do {
try await self.sessionStorage.update(session: sessionData, expiresIn: sessionData.expiresIn, request: request)
} catch let error as SessionStorage<SessionData<Context.Session>>.Error where error == .sessionDoesNotExist {
try await self.sessionStorage
.update(
session: sessionData.object,
expiresIn: sessionData.expiresIn,
request: request
)
} catch let error as SessionStorage<Context.Session>.Error where error == .sessionDoesNotExist {
let cookie = try await self.sessionStorage.save(
session: sessionData,
session: sessionData.object,
expiresIn: sessionData.expiresIn ?? self.defaultSessionExpiration
)
// this is a new session so set the "Set-Cookie" header
Expand Down
4 changes: 2 additions & 2 deletions Sources/HummingbirdAuth/Sessions/UserSessionRepository.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public struct UserRepositoryContext {
/// Repository of users identified by a session object
public protocol UserSessionRepository<Identifier, User>: Sendable {
associatedtype Identifier: Codable
associatedtype User: Authenticatable
associatedtype User: Sendable

/// Get user from repository
/// - Parameters:
Expand All @@ -39,7 +39,7 @@ public protocol UserSessionRepository<Identifier, User>: Sendable {
}

/// Implementation of UserRepository that uses a closure
public struct UserSessionClosureRepository<Identifier: Codable, User: Authenticatable>: UserSessionRepository {
public struct UserSessionClosureRepository<Identifier: Codable, User: Sendable>: UserSessionRepository {
@usableFromInline
let getUserClosure: @Sendable (Identifier, UserRepositoryContext) async throws -> User?

Expand Down
14 changes: 10 additions & 4 deletions Sources/HummingbirdBasicAuth/BasicAuthenticator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,13 @@ import HummingbirdAuth
/// Basic password authenticator
///
/// Extract username and password from "Authorization" header and checks user exists and that the password is correct
public struct BasicAuthenticator<Context: AuthRequestContext, Repository: UserPasswordRepository, Verifier: PasswordHashVerifier>: AuthenticatorMiddleware {
public struct BasicAuthenticator<
Context: AuthRequestContext,
Repository: UserPasswordRepository,
Verifier: PasswordHashVerifier
>: AuthenticatorMiddleware where Context.Identity == Repository.User {
public typealias Identity = Context.Identity

public let users: Repository
public let passwordHashVerifier: Verifier

Expand All @@ -37,11 +43,11 @@ public struct BasicAuthenticator<Context: AuthRequestContext, Repository: UserPa
/// - passwordHashVerifier: password verifier
/// - context: Request context type
/// - getUser: Closure returning user type
public init<User: PasswordAuthenticatable>(
public init(
passwordHashVerifier: Verifier = BcryptPasswordVerifier(),
context: Context.Type = Context.self,
getUser: @escaping @Sendable (String, UserRepositoryContext) async throws -> User?
) where Repository == UserPasswordClosureRepository<User> {
getUser: @escaping @Sendable (String, UserRepositoryContext) async throws -> Context.Identity?
) where Identity: PasswordAuthenticatable, Repository == UserPasswordClosureRepository<Identity> {
self.users = .init(getUser)
self.passwordHashVerifier = passwordHashVerifier
}
Expand Down
2 changes: 1 addition & 1 deletion Sources/HummingbirdBasicAuth/UserPasswordRepository.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import HummingbirdAuth
import Logging

/// Protocol for password autheticatable object
public protocol PasswordAuthenticatable: Authenticatable {
public protocol PasswordAuthenticatable: Sendable {
var passwordHash: String? { get }
}

Expand Down
Loading
Loading