Skip to content

Commit

Permalink
Merge pull request #36 from egregors/#27_ifaces-rework
Browse files Browse the repository at this point in the history
[next]: ifaces rework, divide user.id and user.name, add itself logger
close #1 close #31 close #27
  • Loading branch information
egregors authored Nov 28, 2024
2 parents 3ba5c44 + 54ba2f2 commit 1ccc47f
Show file tree
Hide file tree
Showing 35 changed files with 6,388 additions and 729 deletions.
2 changes: 1 addition & 1 deletion .golangci.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
run:
go: "1.21"
go: "1.23"
timeout: 5m
output:
format: tab
Expand Down
8 changes: 7 additions & 1 deletion .mockery.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
with-expecter: True
issue-845-fix: True
resolve-type-alias: False
inpackage: True
dir: "{{.InterfaceDir}}"
mockname: "Mock{{.InterfaceName}}"
Expand All @@ -8,6 +10,10 @@ packages:
github.com/egregors/passkey:
interfaces:
Logger:
User:
UserStore:
SessionStore:
User:
WebAuthnInterface:
github.com/egregors/passkey/deps:
interfaces:
WebAuthnInterface:
131 changes: 71 additions & 60 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,35 +15,32 @@
## Table of Contents

<!-- TOC -->

* [Table of Contents](#table-of-contents)
* [Features](#features)
* [Installation](#installation)
* [Usage](#usage)
* [Table of Contents](#table-of-contents)
* [Features](#features)
* [Installation](#installation)
* [Usage](#usage)
* [Library Usage](#library-usage)
* [Implement the `UserStore` and
`SessionStore` interfaces](#implement-the-userstore-and-sessionstore-interfaces)
* [Create a new `Passkey` instance and mount the routes](#create-a-new-passkey-instance-and-mount-the-routes)
* [Client-side](#client-side)
* [Implement storages: `UserStore` and two `SessionStore` one for WebAuthn and one for general session](#implement-storages-userstore-and-two-sessionstore-one-for-webauthn-and-one-for-general-session)
* [Create a new `Passkey` instance and mount the routes](#create-a-new-passkey-instance-and-mount-the-routes)
* [Client-side](#client-side)
* [Example Application](#example-application)
* [API](#api)
* [API](#api)
* [Middleware](#middleware)
* [Development](#development)
* [Development](#development)
* [Common tasks](#common-tasks)
* [Mocks](#mocks)
* [Troubleshooting](#troubleshooting)
* [FAQ](#faq)
* [Contributing](#contributing)
* [License](#license)

* [FAQ](#faq)
* [Contributing](#contributing)
* [License](#license)
<!-- TOC -->

## Features

- **User Management**: Handle user information and credentials.
- **WebAuthn Integration**: Easily integrate with WebAuthn for authentication.
- **Session Management**: Manage user sessions securely.
- **Middleware Support**: Implement middleware for authenticated routes.
- **Middleware Support**: Middleware for authenticated routes.

> [!WARNING]
> Stable version is not released yet. The API and the lib are under development.
Expand Down Expand Up @@ -74,61 +71,73 @@ go get github.com/egregors/passkey

## Usage

## Terminology

- **WebAuthn Credential ID** – is a unique ID generated by the authenticator (e.g. your smartphone, laptop or hardware
security key like YubiKey) during the registration (sign-up) process of a new credential (passkey).
- **WebAuthn User ID (user.id)** – is an ID specified by the relying party (RP) to represent a user account within their
system. In the context of passkeys, the User ID (user.id) plays a crucial role in linking a particular user with their
credentials (passkeys).

### Library Usage

To add a passkey service to your application, you need to do two things:
To add a passkey service to your application, you need to do a few simple steps:

#### Implement the `UserStore` and `SessionStore` interfaces
#### Implement storages: `UserStore` and two `SessionStore` one for WebAuthn and one for general session

```go
package passkey

import "github.com/go-webauthn/webauthn/webauthn"

type User interface {
webauthn.User
PutCredential(webauthn.Credential)
// UserStore is a persistent storage for users and credentials
type UserStore interface {
Create(username string) (User, error)
Update(User) error

Get(userID []byte) (User, error)
GetByName(username string) (User, error)
}

type UserStore interface {
GetOrCreateUser(UserID string) User
SaveUser(User)
// SessionStore is a storage for session data
type SessionStore[T webauthn.SessionData | UserSessionData] interface {
Create(data T) (string, error)
Delete(token string)

Get(token string) (*T, bool)
}

type SessionStore interface {
GenSessionID() (string, error)
GetSession(token string) (*webauthn.SessionData, bool)
SaveSession(token string, data *webauthn.SessionData)
DeleteSession(token string)
```

Your `User` model also should implement `User` interface:

```go
package main

import "github.com/go-webauthn/webauthn/webauthn"

// User is a user with webauthn credentials
type User interface {
webauthn.User
PutCredential(webauthn.Credential)
}

```

This interface is an extension of the `webauthn.User` interface. It adds a `PutCredential` method that allows you to
store a credential in the user object.

#### Create a new `Passkey` instance and mount the routes

The whole example is in `_example` directory.

```go
package main

import (
"embed"
"fmt"
"html/template"
"io/fs"
"net/http"
"net/url"
"os"
"time"

"github.com/egregors/passkey"
"github.com/go-webauthn/webauthn/webauthn"

"github.com/egregors/passkey"
"github.com/egregors/passkey/log"
)

//go:embed web/*
Expand All @@ -145,7 +154,7 @@ func main() {

origin := fmt.Sprintf("%s://%s%s%s", proto, sub, host, originPort)

storage := NewStorage()
l := log.NewLogger()

pkey, err := passkey.New(
passkey.Config{
Expand All @@ -154,12 +163,13 @@ func main() {
RPID: host, // Generally the FQDN for your site
RPOrigins: []string{origin}, // The origin URLs allowed for WebAuthn
},
UserStore: storage,
SessionStore: storage,
SessionMaxAge: 24 * time.Hour,
UserStore: NewUserStore(),
AuthSessionStore: NewSessionStore[webauthn.SessionData](),
UserSessionStore: NewSessionStore[passkey.UserSessionData](),
},
passkey.WithLogger(NewLogger()),
passkey.WithCookieMaxAge(60*time.Minute),
passkey.WithLogger(l),
passkey.WithUserSessionMaxAge(60*time.Minute),
passkey.WithSessionCookieNamePrefix("passkeyDemo"),
passkey.WithInsecureCookie(), // In order to support Safari on localhost. Do not use in production.
)
if err != nil {
Expand Down Expand Up @@ -192,8 +202,8 @@ func main() {
mux.Handle("/private", withAuth(privateMux))

// start the server
fmt.Printf("Listening on %s\n", origin)
if err := http.ListenAndServe(serverPort, mux); err != nil {
l.Infof("Listening on %s\n", origin)
if err := http.ListenAndServe(serverPort, mux); err != nil { //nolint:gosec
panic(err)
}
}
Expand All @@ -204,12 +214,12 @@ You can optionally provide a logger to the `New` function using the `WithLogger`

Full list of options:

| Name | Default | Description |
|-----------------------|----------------------------------------|----------------------------------------|
| WithLogger | NullLogger | Provide custom logger |
| WithInsecureCookie | Disabled (cookie is secure by default) | Sets Cookie.Secure to false |
| WithSessionCookieName | `sid` | Sets the name of the session cookie |
| WithCookieMaxAge | 60 minutes | Sets the max age of the session cookie |
| Name | Default | Description |
|-----------------------------|----------------------------------------|------------------------------------------------------|
| WithLogger | NullLogger | Provide custom logger |
| WithInsecureCookie | Disabled (cookie is secure by default) | Sets Cookie.Secure to false |
| WithSessionCookieNamePrefix | `pk` | Sets the name prefix of the session and user cookies |
| WithUserSessionMaxAge | 60 minutes | Sets the max age of the user session cookie |

#### Client-side

Expand All @@ -235,11 +245,12 @@ make up

## API

| Method | Description |
|---------------------------------------------------------------------------------------------------|-----------------------------------------------------------|
| `New(cfg Config, opts ...Option) (*Passkey, error)` | Creates a new Passkey instance. |
| `MountRoutes(mux *http.ServeMux, path string)` | Mounts the Passkey routes onto a given HTTP multiplexer. |
| `Auth(userIDKey string, onSuccess, onFail http.HandlerFunc) func(next http.Handler) http.Handler` | Middleware to protect routes that require authentication. |
| Method | Description |
|---------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------|
| `New(cfg Config, opts ...Option) (*Passkey, error)` | Creates a new Passkey instance. |
| `MountRoutes(mux *http.ServeMux, path string)` | Mounts the Passkey routes onto a given HTTP multiplexer. |
| `Auth(userIDKey string, onSuccess, onFail http.HandlerFunc) func(next http.Handler) http.Handler` | Middleware to protect routes that require authentication. |
| `UserIDFromCtx(ctx context.Context, pkUserKey string) ([]byte, bool)` | Returns the user ID from the request context. If the userID is not found, it returns nil and false. |

### Middleware

Expand All @@ -251,7 +262,7 @@ Auth(userIDKey string, onSuccess, onFail http.HandlerFunc) func (next http.Handl

It takes key for context and two callback functions that are called when the user is authenticated or not.
You can use the context key to retrieve the authenticated userID from the request context
with `passkey.UserFromContext`.
with `passkey.UserIDFromCtx`.

`passkey` contains a helper function:

Expand Down
12 changes: 6 additions & 6 deletions _example/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
FROM golang:1.22-alpine AS builder
# Description: Dockerfile for building the example application
# It should be call from the root directory of the project because of the context
FROM golang:1.23-alpine AS builder

WORKDIR /app

COPY . .

RUN go mod init passkey_demo && go mod tidy

RUN go build -o main ./*.go
RUN go build -o ./_example/main ./_example/*.go

FROM scratch

COPY --from=builder /app/main /main
COPY --from=builder /app/web /web
COPY --from=builder /app/_example/main /main
COPY --from=builder /app/_example/web /web

EXPOSE 8080

Expand Down
8 changes: 4 additions & 4 deletions _example/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
version: '3.9'

services:
traefik:
image: traefik:v2.9
Expand All @@ -17,14 +15,16 @@ services:
- "443:443"

app:
build: .
build:
context: ../
dockerfile: _example/Dockerfile
image: app
environment:
- PROTO=https
- ORIGIN_PORT=
labels:
- "traefik.enable=true"
- "traefik.http.routers.app.tls=true"
- "traefik.http.routers.app.rule=Host(`localhost`, `192.168.8.151`)"
- "traefik.http.routers.app.rule=Host(`localhost`)"
- "traefik.http.routers.app.entrypoints=websecure"
- "traefik.http.services.app.loadbalancer.server.port=8080"
27 changes: 27 additions & 0 deletions _example/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
module example

go 1.23.3

require (
github.com/egregors/passkey v0.0.0
github.com/go-webauthn/webauthn v0.11.2
github.com/google/uuid v1.6.0
)

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
github.com/go-webauthn/x v0.1.14 // indirect
github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
github.com/google/go-tpm v0.9.1 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/stretchr/testify v1.9.0 // indirect
github.com/x448/float16 v0.8.4 // indirect
golang.org/x/crypto v0.26.0 // indirect
golang.org/x/sys v0.23.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

replace github.com/egregors/passkey => ../
32 changes: 32 additions & 0 deletions _example/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
github.com/go-webauthn/webauthn v0.11.2 h1:Fgx0/wlmkClTKlnOsdOQ+K5HcHDsDcYIvtYmfhEOSUc=
github.com/go-webauthn/webauthn v0.11.2/go.mod h1:aOtudaF94pM71g3jRwTYYwQTG1KyTILTcZqN1srkmD0=
github.com/go-webauthn/x v0.1.14 h1:1wrB8jzXAofojJPAaRxnZhRgagvLGnLjhCAwg3kTpT0=
github.com/go-webauthn/x v0.1.14/go.mod h1:UuVvFZ8/NbOnkDz3y1NaxtUN87pmtpC1PQ+/5BBQRdc=
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/google/go-tpm v0.9.1 h1:0pGc4X//bAlmZzMKf8iz6IsDo1nYTbYJ6FZN/rg4zdM=
github.com/google/go-tpm v0.9.1/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM=
golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
Loading

0 comments on commit 1ccc47f

Please sign in to comment.