Skip to content

Commit

Permalink
Update webauthn
Browse files Browse the repository at this point in the history
  • Loading branch information
adam-fowler committed Mar 13, 2024
1 parent eb1741f commit 72b9932
Show file tree
Hide file tree
Showing 8 changed files with 60 additions and 60 deletions.
18 changes: 9 additions & 9 deletions webauthn/Sources/App/Application+build.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,11 @@ public protocol AppArguments {
var privateKey: String { get }
}

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

let fluent = HBFluent(logger: logger)
let fluent = Fluent(logger: logger)
// add sqlite database
if arguments.inMemoryDatabase {
fluent.databases.use(.sqlite(.memory), as: .sqlite)
Expand All @@ -49,18 +49,18 @@ func buildApplication(_ arguments: AppArguments) async throws -> some HBApplicat
try await fluent.migrate()

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

// load mustache template library
let library = try await HBMustacheLibrary(directory: "templates")
let library = try await MustacheLibrary(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) {
let router = RouterBuilder(context: WebAuthnRequestContext.self) {
// add logging middleware
HBLogRequestsMiddleware(.info)
LogRequestsMiddleware(.info)
// add file middleware to server HTML files
HBFileMiddleware(searchForIndexHtml: true, logger: logger)
FileMiddleware(searchForIndexHtml: true, logger: logger)
// health check endpoint
Get("/health") { _, _ -> HTTPResponse.Status in
return .ok
Expand All @@ -85,7 +85,7 @@ func buildApplication(_ arguments: AppArguments) async throws -> some HBApplicat
}
}

var app = HBApplication(router: router)
var app = Application(router: router)
app.addServices(fluent, memoryPersist)
return app
}
20 changes: 10 additions & 10 deletions webauthn/Sources/App/Controllers/HTMLController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ import HummingbirdMustache
import HummingbirdRouter

/// Redirects to login page if no user has been authenticated
struct RedirectMiddleware<Context: HBAuthRequestContext>: HBMiddlewareProtocol {
struct RedirectMiddleware<Context: AuthRequestContext>: RouterMiddleware {
let to: String
func handle(_ request: HBRequest, context: Context, next: (HBRequest, Context) async throws -> HBResponse) async throws -> HBResponse {
func handle(_ request: Request, context: Context, next: (Request, Context) async throws -> Response) async throws -> Response {
// check if authenticated
if context.auth.has(AuthenticatedUser.self) {
return try await next(request, context)
Expand All @@ -36,14 +36,14 @@ struct RedirectMiddleware<Context: HBAuthRequestContext>: HBMiddlewareProtocol {
struct HTMLController {
typealias Context = WebAuthnRequestContext

let homeTemplate: HBMustacheTemplate
let fluent: HBFluent
let sessionStorage: HBSessionStorage
let homeTemplate: MustacheTemplate
let fluent: Fluent
let sessionStorage: SessionStorage

init(
mustacheLibrary: HBMustacheLibrary,
fluent: HBFluent,
sessionStorage: HBSessionStorage
mustacheLibrary: MustacheLibrary,
fluent: Fluent,
sessionStorage: SessionStorage
) {
// get the mustache templates from the library
guard let homeTemplate = mustacheLibrary.getTemplate(named: "home")
Expand All @@ -56,7 +56,7 @@ struct HTMLController {
}

// return Route for home page
var endpoints: some HBMiddlewareProtocol<Context> {
var endpoints: some RouterMiddleware<Context> {
Get("/") {
WebAuthnSessionAuthenticator(fluent: self.fluent, sessionStorage: self.sessionStorage)
RedirectMiddleware(to: "/login.html")
Expand All @@ -65,7 +65,7 @@ struct HTMLController {
}

/// Home page listing todos and with add todo UI
@Sendable func home(request: HBRequest, context: Context) async throws -> HTML {
@Sendable func home(request: Request, context: Context) async throws -> HTML {
// get user
let user = try context.auth.require(AuthenticatedUser.self)
// Render home template and return as HTML
Expand Down
42 changes: 21 additions & 21 deletions webauthn/Sources/App/Controllers/WebAuthnController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,11 @@ struct HBWebAuthnController {
typealias Context = WebAuthnRequestContext

let webauthn: WebAuthnManager
let fluent: HBFluent
let sessionStorage: HBSessionStorage
let fluent: Fluent
let sessionStorage: SessionStorage

// return RouteGroup with user login endpoints
var endpoints: some HBMiddlewareProtocol<Context> {
var endpoints: some RouterMiddleware<Context> {
RouteGroup("user") {
Post("signup", handler: self.signin)
Get("login", handler: self.beginAuthentication)
Expand All @@ -52,13 +52,13 @@ struct HBWebAuthnController {
let username: String
}

@Sendable func signin(request: HBRequest, context: Context) async throws -> HBResponse {
@Sendable func signin(request: Request, context: Context) async throws -> Response {
let input = try await request.decode(as: SignInInput.self, context: context)
guard try await User.query(on: self.fluent.db())
.filter(\.$username == input.username)
.first() == nil
else {
throw HBHTTPError(.conflict, message: "Username already taken.")
throw HTTPError(.conflict, message: "Username already taken.")
}
let user = User(username: input.username)
try await user.save(on: self.fluent.db())
Expand All @@ -67,15 +67,15 @@ struct HBWebAuthnController {
session: session,
expiresIn: .seconds(600)
)
var response = HBResponse.redirect(to: "/api/user/register/start", type: .temporary)
var response = Response.redirect(to: "/api/user/register/start", type: .temporary)
response.setCookie(cookie)
return response
}

/// Begin registering a User
@Sendable func beginRegistration(request: HBRequest, context: Context) async throws -> PublicKeyCredentialCreationOptions {
@Sendable func beginRegistration(request: Request, context: Context) async throws -> PublicKeyCredentialCreationOptions {
let authenticationSession = try context.auth.require(AuthenticationSession.self)
guard case .signedUp(let user) = authenticationSession else { throw HBHTTPError(.unauthorized) }
guard case .signedUp(let user) = authenticationSession else { throw HTTPError(.unauthorized) }
let options = self.webauthn.beginRegistration(user: user.publicKeyCredentialUserEntity)
let session = WebAuthnSession(from: .registering(
user: user,
Expand All @@ -86,10 +86,10 @@ struct HBWebAuthnController {
}

/// Finish registering a user
@Sendable func finishRegistration(request: HBRequest, context: Context) async throws -> HTTPResponse.Status {
@Sendable func finishRegistration(request: Request, context: Context) async throws -> HTTPResponse.Status {
let authenticationSession = try context.auth.require(AuthenticationSession.self)
let input = try await request.decode(as: RegistrationCredential.self, context: context)
guard case .registering(let user, let challenge) = authenticationSession else { throw HBHTTPError(.unauthorized) }
guard case .registering(let user, let challenge) = authenticationSession else { throw HTTPError(.unauthorized) }
do {
let credential = try await self.webauthn.finishRegistration(
challenge: challenge,
Expand All @@ -102,39 +102,39 @@ struct HBWebAuthnController {
try await WebAuthnCredential(credential: credential, userId: user.id).save(on: self.fluent.db())
} catch {
context.logger.error("\(error)")
throw HBHTTPError(.unauthorized)
throw HTTPError(.unauthorized)
}
context.logger.info("Registration success, id: \(input.id)")

return .ok
}

/// Begin Authenticating a user
@Sendable func beginAuthentication(_ request: HBRequest, context: Context) async throws -> HBEditedResponse<PublicKeyCredentialRequestOptions> {
@Sendable func beginAuthentication(_ request: Request, context: Context) async throws -> EditedResponse<PublicKeyCredentialRequestOptions> {
let options = try self.webauthn.beginAuthentication(timeout: 60000)
let session = WebAuthnSession(from: .authenticating(
challenge: options.challenge
))
let cookie = try await sessionStorage.save(session: session, expiresIn: .seconds(600))
var editedResponse = HBEditedResponse(response: options)
var editedResponse = EditedResponse(response: options)
editedResponse.setCookie(cookie)
return editedResponse
}

/// End Authenticating a user
@Sendable func finishAuthentication(request: HBRequest, context: Context) async throws -> HTTPResponse.Status {
@Sendable func finishAuthentication(request: Request, context: Context) async throws -> HTTPResponse.Status {
let authenticationSession = try context.auth.require(AuthenticationSession.self)
let input = try await request.decode(as: AuthenticationCredential.self, context: context)
guard case .authenticating(let challenge) = authenticationSession else { throw HBHTTPError(.unauthorized) }
guard case .authenticating(let challenge) = authenticationSession else { throw HTTPError(.unauthorized) }
let id = input.id.urlDecoded.asString()
guard let webAuthnCredential = try await WebAuthnCredential.query(on: fluent.db())
.filter(\.$id == id)
.with(\.$user)
.first()
else {
throw HBHTTPError(.unauthorized)
throw HTTPError(.unauthorized)
}
guard let decodedPublicKey = webAuthnCredential.publicKey.decoded else { throw HBHTTPError(.internalServerError) }
guard let decodedPublicKey = webAuthnCredential.publicKey.decoded else { throw HTTPError(.internalServerError) }
context.logger.info("Challenge: \(challenge)")
do {
_ = try self.webauthn.finishAuthentication(
Expand All @@ -145,7 +145,7 @@ struct HBWebAuthnController {
)
} catch {
context.logger.error("\(error)")
throw HBHTTPError(.unauthorized)
throw HTTPError(.unauthorized)
}
let session = try WebAuthnSession.authenticated(userId: webAuthnCredential.user.requireID())
try await self.sessionStorage.update(session: session, expiresIn: .seconds(24 * 60 * 60), request: request)
Expand All @@ -154,11 +154,11 @@ struct HBWebAuthnController {
}

/// Test authenticated
@Sendable func logout(_ request: HBRequest, context: Context) async throws -> HTTPResponse.Status {
@Sendable func logout(_ request: Request, context: Context) async throws -> HTTPResponse.Status {
try await self.sessionStorage.delete(request: request)
return .ok
}
}

extension PublicKeyCredentialCreationOptions: HBResponseEncodable {}
extension PublicKeyCredentialRequestOptions: HBResponseEncodable {}
extension PublicKeyCredentialCreationOptions: ResponseEncodable {}
extension PublicKeyCredentialRequestOptions: ResponseEncodable {}
4 changes: 2 additions & 2 deletions webauthn/Sources/App/Extensions/Fluent+Mustache.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import HummingbirdMustache
/// to access the wrappedValue. In the mustache template you would access this with
/// `{{wrappedValue(_myProperty)}}`. Note the `_` prefix on the property name. This is
/// required as this is how property wrappers appear in the Mirror reflection data.
extension FieldProperty: HBMustacheTransformable {
extension FieldProperty: MustacheTransformable {
public func transform(_ name: String) -> Any? {
switch name {
case "wrappedValue":
Expand All @@ -20,7 +20,7 @@ extension FieldProperty: HBMustacheTransformable {
/// to access the wrappedValue. In the mustache template you would access this with
/// `{{wrappedValue(_myID)}}`. Note the `_` prefix on the property name. This is
/// required as this is how property wrappers appear in the Mirror reflection data.
extension IDProperty: HBMustacheTransformable {
extension IDProperty: MustacheTransformable {
public func transform(_ name: String) -> Any? {
switch name {
case "wrappedValue":
Expand Down
4 changes: 2 additions & 2 deletions webauthn/Sources/App/Extensions/html.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@ import Hummingbird

/// Type wrapping HTML code. Will convert to HBResponse that includes the correct
/// content-type header
struct HTML: HBResponseGenerator {
struct HTML: ResponseGenerator {
let html: String

public func response(from request: HBRequest, context: some HBBaseRequestContext) throws -> HBResponse {
public func response(from request: Request, context: some BaseRequestContext) throws -> Response {
let buffer = context.allocator.buffer(string: self.html)
return .init(status: .ok, headers: [.contentType: "text/html"], body: .init(byteBuffer: buffer))
}
Expand Down
22 changes: 11 additions & 11 deletions webauthn/Sources/App/Middleware/WebAuthnAuthenticator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import WebAuthn

/// cannot conform fluent model `User` to `HBAuthenticatable` as it is not Sendable
/// so create a copy to store in login cache
struct AuthenticatedUser: HBAuthenticatable, Codable {
struct AuthenticatedUser: Authenticatable, Codable {
var id: UUID
var username: String

Expand All @@ -30,7 +30,7 @@ struct AuthenticatedUser: HBAuthenticatable, Codable {
}

/// Authentication state stored in login cache
enum AuthenticationSession: Sendable, Codable, HBAuthenticatable, HBResponseEncodable {
enum AuthenticationSession: Sendable, Codable, Authenticatable, ResponseEncodable {
case signedUp(user: AuthenticatedUser)
case registering(user: AuthenticatedUser, challenge: [UInt8])
case authenticating(challenge: [UInt8])
Expand Down Expand Up @@ -59,7 +59,7 @@ enum WebAuthnSession: Codable {
}

/// return authentication state from session object
func session(for request: HBRequest, fluent: HBFluent) async throws -> AuthenticationSession? {
func session(for request: Request, fluent: Fluent) async throws -> AuthenticationSession? {
switch self {
case .authenticating(let encodedChallenge):
guard let challenge = URLEncodedBase64(encodedChallenge).decodedBytes else { return nil }
Expand All @@ -79,28 +79,28 @@ enum WebAuthnSession: Codable {
}

/// Authenticator that will return current state of authentication
struct WebAuthnSessionStateAuthenticator<Context: HBAuthRequestContext>: HBSessionAuthenticator {
struct WebAuthnSessionStateAuthenticator<Context: AuthRequestContext>: SessionMiddleware {
typealias Session = WebAuthnSession
/// fluent reference
let fluent: HBFluent
let fluent: Fluent
/// container for session objects
let sessionStorage: HBSessionStorage
let sessionStorage: SessionStorage

func getValue(from session: Session, request: HBRequest, context: Context) async throws -> AuthenticationSession? {
func getValue(from session: Session, request: Request, context: Context) async throws -> AuthenticationSession? {
return try await session.session(for: request, fluent: self.fluent)
}
}

/// Authenticator that will return an authenticated user from a WebAuthnSession
struct WebAuthnSessionAuthenticator<Context: HBAuthRequestContext>: HBSessionAuthenticator {
struct WebAuthnSessionAuthenticator<Context: AuthRequestContext>: SessionMiddleware {
typealias Session = WebAuthnSession

/// fluent reference
let fluent: HBFluent
let fluent: Fluent
/// container for session objects
let sessionStorage: HBSessionStorage
let sessionStorage: SessionStorage

func getValue(from session: Session, request: HBRequest, context: Context) async throws -> AuthenticatedUser? {
func getValue(from session: Session, request: Request, context: Context) async throws -> AuthenticatedUser? {
guard case .authenticated(let userId) = session else { return nil }
if let user = try await User.find(userId, on: self.fluent.db()) {
return AuthenticatedUser(id: userId, username: user.username)
Expand Down
2 changes: 1 addition & 1 deletion webauthn/Sources/App/Models/User.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import HummingbirdAuth
import HummingbirdFluent
import WebAuthn

final class User: Model, HBResponseEncodable {
final class User: Model, ResponseEncodable {
static let schema = "user"

@ID(key: .id)
Expand Down
8 changes: 4 additions & 4 deletions webauthn/Sources/App/RequestContext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ import HummingbirdRouter
import Logging
import NIOCore

struct WebAuthnRequestContext: HBAuthRequestContext, HBRouterRequestContext {
var coreContext: HBCoreRequestContext
var auth: HBLoginCache
var routerContext: HBRouterBuilderContext
struct WebAuthnRequestContext: AuthRequestContext, RouterRequestContext {
var coreContext: CoreRequestContext
var auth: LoginCache
var routerContext: RouterBuilderContext

init(channel: Channel, logger: Logger) {
self.coreContext = .init(allocator: channel.allocator, logger: logger)
Expand Down

0 comments on commit 72b9932

Please sign in to comment.