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

Update webauthn example for 2.x.x #54

Merged
merged 6 commits into from
Jan 24, 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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Examples converted to Hummingbird 2.0
- [todos-mongokitten-openapi](https://github.com/hummingbird-project/hummingbird-examples/tree/2.x.x/todos-mongokitten-openapi) - Todos application, using MongoDB driver [MongoKitten](https://github.com/orlandos-nl/MongoKitten) and the [OpenAPI runtime](https://github.com/apple/swift-openapi-runtime).
- [todos-postgres-tutorial](https://github.com/hummingbird-project/hummingbird-examples/tree/2.x.x/todos-postgres-tutorial) - Todos application, based off [TodoBackend](http://todobackend.com) spec, using PostgresNIO. Sample code that goes along with the [Todos tutorial](https://hummingbird-project.github.io/hummingbird-docs/2.0/tutorials/todos).
- [upload](https://github.com/hummingbird-project/hummingbird-examples/tree/2.x.x/upload) - File uploading and downloading.
- [webauthn](https://github.com/hummingbird-project/hummingbird-examples/tree/2.x.x/webauthn) - Web app demonstrating WebAuthn(PassKey) authentication.

Examples still working with Hummingbird 1.0
- [auth-cognito](https://github.com/hummingbird-project/hummingbird-examples/tree/main/auth-cognito) - Authentication via AWS Cognito.
Expand All @@ -23,7 +24,6 @@ Examples still working with Hummingbird 1.0
- [todos-fluent](https://github.com/hummingbird-project/hummingbird-examples/tree/main/todos-fluent) - Todos application, based off [TodoBackend](http://todobackend.com) spec, using Fluent
- [todos-lambda](https://github.com/hummingbird-project/hummingbird-examples/tree/main/todos-lambda) - Todos application, based off [TodoBackend](http://todobackend.com) spec, using DynamoDB and running on AWS Lambda.
- [upload-s3](https://github.com/hummingbird-project/hummingbird-examples/tree/main/upload-s3) - File uploading and downloading using AWS S3 as backing store.
- [webauthn](https://github.com/hummingbird-project/hummingbird-examples/tree/main/webauthn) - Web app demonstrating WebAuthn(PassKey) authentication.
- [websocket-chat](https://github.com/hummingbird-project/hummingbird-examples/tree/main/websocket-chat) - Simple chat application using WebSockets.
- [websocket-echo](https://github.com/hummingbird-project/hummingbird-examples/tree/main/websocket-echo) - Simple WebSocket based echo server.

Expand Down
1 change: 0 additions & 1 deletion hello/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ let package = Package(
dependencies: [
.product(name: "ArgumentParser", package: "swift-argument-parser"),
.product(name: "Hummingbird", package: "hummingbird"),
.product(name: "HummingbirdFoundation", package: "hummingbird"),
],
swiftSettings: [
// Enable better optimizations when building in Release configuration. Despite the use of
Expand Down
14 changes: 7 additions & 7 deletions webauthn/Package.swift
Original file line number Diff line number Diff line change
@@ -1,19 +1,18 @@
// swift-tools-version:5.6
// swift-tools-version:5.9
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
name: "webauthn",
platforms: [.macOS(.v12)],
platforms: [.macOS(.v14)],
products: [
.executable(name: "App", targets: ["App"]),
],
dependencies: [
.package(url: "https://github.com/hummingbird-project/hummingbird.git", from: "1.2.0"),
.package(url: "https://github.com/hummingbird-project/hummingbird-core.git", from: "1.0.0"),
.package(url: "https://github.com/hummingbird-project/hummingbird-auth.git", from: "1.2.0"),
.package(url: "https://github.com/hummingbird-project/hummingbird-fluent.git", from: "1.0.0"),
.package(url: "https://github.com/hummingbird-project/hummingbird.git", from: "2.0.0-alpha.1"),
.package(url: "https://github.com/hummingbird-project/hummingbird-auth.git", from: "2.0.0-alpha.1"),
.package(url: "https://github.com/hummingbird-project/hummingbird-fluent.git", from: "2.0.0-alpha.1"),
.package(url: "https://github.com/hummingbird-project/hummingbird-mustache.git", from: "1.0.0"),
.package(url: "https://github.com/vapor/fluent-sqlite-driver.git", from: "4.0.0"),
.package(url: "https://github.com/swift-server/webauthn-swift.git", from: "1.0.0-alpha"),
Expand All @@ -26,10 +25,10 @@ let package = Package(
.product(name: "ArgumentParser", package: "swift-argument-parser"),
.product(name: "FluentSQLiteDriver", package: "fluent-sqlite-driver"),
.product(name: "Hummingbird", package: "hummingbird"),
.product(name: "HummingbirdRouter", package: "hummingbird"),
.product(name: "HummingbirdAuth", package: "hummingbird-auth"),
.product(name: "HummingbirdFluent", package: "hummingbird-fluent"),
.product(name: "HummingbirdMustache", package: "hummingbird-mustache"),
.product(name: "HummingbirdTLS", package: "hummingbird-core"),
.product(name: "WebAuthn", package: "webauthn-swift"),
],
swiftSettings: [
Expand All @@ -43,6 +42,7 @@ let package = Package(
name: "AppTests",
dependencies: [
.byName(name: "App"),
.product(name: "Hummingbird", package: "hummingbird"),
.product(name: "HummingbirdXCT", package: "hummingbird"),
]
),
Expand Down
2 changes: 1 addition & 1 deletion webauthn/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# WebAuthn

Application demostrating authentication via WebAuthn passkeys. This example uses the webAuthn library https://github.com/swift-server/webauthn-swift.
Application demostrating authentication via WebAuthn passkeys. This example uses the webAuthn library https://github.com/swift-server/webauthn-swift. The sample also uses the result builder router `HBRouterBuilder` from HummingbirdRouter

In your browser go to `localhost:8080`.
15 changes: 4 additions & 11 deletions webauthn/Sources/App/App.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import ArgumentParser
import Hummingbird

@main
struct App: ParsableCommand, AppArguments {
struct App: AsyncParsableCommand, AppArguments {
@Option(name: .shortAndLong)
var hostname: String = "127.0.0.1"

Expand All @@ -29,15 +29,8 @@ struct App: ParsableCommand, AppArguments {
var privateKey: String { "certs/server.key" }
var certificateChain: String { "certs/server.crt" }

func run() throws {
let app = HBApplication(
configuration: .init(
address: .hostname(self.hostname, port: self.port),
serverName: "Hummingbird"
)
)
try app.configure(self)
try app.start()
app.wait()
func run() async throws {
let app = try await buildApplication(self)
try await app.runService()
}
}
91 changes: 91 additions & 0 deletions webauthn/Sources/App/Application+build.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Hummingbird server framework project
//
// Copyright (c) 2021-2023 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 FluentSQLiteDriver
import Foundation
import Hummingbird
import HummingbirdAuth
import HummingbirdFluent
import HummingbirdMustache
import HummingbirdRouter
import WebAuthn

/// Application arguments protocol. We use a protocol so we can call
/// `HBApplication.configure` inside Tests as well as in the App executable.
/// Any variables added here also have to be added to `App` in App.swift and
/// `TestArguments` in AppTest.swift
public protocol AppArguments {
var hostname: String { get }
var port: Int { get }
var inMemoryDatabase: Bool { get }
var certificateChain: String { get }
var privateKey: String { get }
}

func buildApplication(_ arguments: AppArguments) async throws -> some HBApplicationProtocol {
var logger = Logger(label: "webauthn")
logger.logLevel = .debug

let fluent = HBFluent(logger: logger)
// add sqlite database
if arguments.inMemoryDatabase {
fluent.databases.use(.sqlite(.memory), as: .sqlite)
} else {
fluent.databases.use(.sqlite(.file("db.sqlite")), as: .sqlite)
}
await fluent.migrations.add(CreateUser())
await fluent.migrations.add(CreateWebAuthnCredential())
try await fluent.migrate()

// sessions are stored in memory
let memoryPersist = HBMemoryPersistDriver()
let sessionStorage = HBSessionStorage(memoryPersist)

// load mustache template library
let library = try HBMustacheLibrary(directory: "templates")
assert(library.getTemplate(named: "home") != nil, "Set your working directory to the root folder of this example to get it to work")

let router = HBRouterBuilder(context: WebAuthnRequestContext.self) {
// add logging middleware
HBLogRequestsMiddleware(.info)
// add file middleware to server HTML files
HBFileMiddleware(searchForIndexHtml: true, logger: logger)
// health check endpoint
Get("/health") { _, _ -> HTTPResponse.Status in
return .ok
}
HTMLController(
mustacheLibrary: library,
fluent: fluent,
sessionStorage: sessionStorage
).endpoints
RouteGroup("api") {
HBWebAuthnController(
webauthn: .init(
config: .init(
relyingPartyID: "localhost",
relyingPartyName: "Hummingbird WebAuthn example",
relyingPartyOrigin: "http://localhost:8080"
)
),
fluent: fluent,
sessionStorage: sessionStorage
).endpoints
}
}

var app = HBApplication(router: router)
app.addServices(fluent, memoryPersist)
return app
}
83 changes: 0 additions & 83 deletions webauthn/Sources/App/Application+configure.swift

This file was deleted.

44 changes: 30 additions & 14 deletions webauthn/Sources/App/Controllers/HTMLController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,45 +13,61 @@
//===----------------------------------------------------------------------===//

import Hummingbird
import HummingbirdAuth
import HummingbirdFluent
import HummingbirdMustache
import HummingbirdRouter

/// Redirects to login page if no user has been authenticated
struct RedirectMiddleware: HBMiddleware {
struct RedirectMiddleware<Context: HBAuthRequestContextProtocol>: HBMiddlewareProtocol {
let to: String
func apply(to request: HBRequest, next: HBResponder) -> EventLoopFuture<HBResponse> {
if request.authHas(User.self) {
return next.respond(to: request)
func handle(_ request: HBRequest, context: Context, next: (HBRequest, Context) async throws -> HBResponse) async throws -> HBResponse {
// check if authenticated
if context.auth.has(AuthenticatedUser.self) {
return try await next(request, context)
} else {
return request.eventLoop.makeSucceededFuture(.redirect(to: "\(self.to)?from=\(request.uri)", type: .found))
// if not authenticated then redirect to login page
return .redirect(to: "\(self.to)?from=\(request.uri)", type: .found)
}
}
}

/// Serves HTML pages
struct HTMLController {
typealias Context = WebAuthnRequestContext

let homeTemplate: HBMustacheTemplate
let fluent: HBFluent
let sessionStorage: HBSessionStorage

init(mustacheLibrary: HBMustacheLibrary) {
init(
mustacheLibrary: HBMustacheLibrary,
fluent: HBFluent,
sessionStorage: HBSessionStorage
) {
// get the mustache templates from the library
guard let homeTemplate = mustacheLibrary.getTemplate(named: "home")
else {
preconditionFailure("Failed to load mustache templates")
}
self.homeTemplate = homeTemplate
self.fluent = fluent
self.sessionStorage = sessionStorage
}

/// Add routes for webpages
func addRoutes(to router: HBRouterBuilder) {
router.group()
.add(middleware: WebAuthnSessionAuthenticator())
.add(middleware: RedirectMiddleware(to: "/login.html"))
.get("/", use: self.home)
// return Route for home page
var endpoints: some HBMiddlewareProtocol<Context> {
Get("/") {
WebAuthnSessionAuthenticator(fluent: self.fluent, sessionStorage: self.sessionStorage)
RedirectMiddleware(to: "/login.html")
self.home
}
}

/// Home page listing todos and with add todo UI
func home(request: HBRequest) async throws -> HTML {
@Sendable func home(request: HBRequest, context: Context) async throws -> HTML {
// get user
let user = try request.authRequire(User.self)
let user = try context.auth.require(AuthenticatedUser.self)
// Render home template and return as HTML
let object: [String: Any] = [
"name": user.username,
Expand Down
Loading