diff --git a/README.md b/README.md index e6f803c..61838e1 100644 --- a/README.md +++ b/README.md @@ -15,27 +15,24 @@ ## Table of Contents - -* [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) ## Features @@ -43,7 +40,7 @@ - **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. @@ -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/* @@ -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{ @@ -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 { @@ -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) } } @@ -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 @@ -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 @@ -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: diff --git a/middleware.go b/middleware.go index f7495ed..b5ae4f4 100644 --- a/middleware.go +++ b/middleware.go @@ -65,7 +65,7 @@ func RedirectUnauthorized(target url.URL) http.HandlerFunc { } } -// UserIDFromCtx returns the user ID from the request context. If the userID is not found, it returns an empty string. +// UserIDFromCtx returns the user ID from the request context. If the userID is not found, it returns nil and false. func UserIDFromCtx(ctx context.Context, pkUserKey string) ([]byte, bool) { if ctx.Value(pkUserKey) == nil { return nil, false