- User Management: Handle user information and credentials.
- WebAuthn Integration: Easily integrate with WebAuthn for authentication.
- Session Management: Manage user sessions securely.
- Middleware Support: Middleware for authenticated routes.
Note
In general, this library is built on top of two open-source solutions:
- Golang WebAuthn Library β https://github.com/go-webauthn/webauthn
- JS/TS SimpleWebAuthn client β https://github.com/MasterKale/SimpleWebAuthn
To get started, you need to have Go installed on your machine. If you don't have it installed, you can download it from here.
Install the library using go get
:
go get github.com/egregors/passkey
To add a passkey service to your application, you need to do a few simple steps:
package passkey
import "github.com/go-webauthn/webauthn/webauthn"
// 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)
}
// 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)
}
Your User
model also should implement User
interface:
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.
The whole example is in _example
directory.
package main
import (
"embed"
"fmt"
"io/fs"
"net/http"
"net/url"
"time"
"github.com/go-webauthn/webauthn/webauthn"
"github.com/egregors/passkey"
"github.com/egregors/passkey/log"
)
//go:embed web/*
var webFiles embed.FS
const userKey = "pkUser"
func main() {
proto := getEnv("PROTO", "http") // "http" | "https"
sub := getEnv("SUB", "") // "" | "login."
host := getEnv("HOST", "localhost") // "localhost" | "example.com"
originPort := getEnv("ORIGIN_PORT", ":8080") // ":8080" | "" if you use reverse proxy it should be the most "external" port
serverPort := getEnv("SERVER_PORT", ":8080") // ":8080"
origin := fmt.Sprintf("%s://%s%s%s", proto, sub, host, originPort)
l := log.NewLogger()
pkey, err := passkey.New(
passkey.Config{
WebauthnConfig: &webauthn.Config{
RPDisplayName: "Passkey Example", // Display Name for your site
RPID: host, // Generally the FQDN for your site
RPOrigins: []string{origin}, // The origin URLs allowed for WebAuthn
},
UserStore: NewUserStore(),
AuthSessionStore: NewSessionStore[webauthn.SessionData](),
UserSessionStore: NewSessionStore[passkey.UserSessionData](),
},
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 {
panic(err)
}
mux := http.NewServeMux()
// mount the passkey routes
pkey.MountRoutes(mux, "/api/")
// public routes
web, _ := fs.Sub(webFiles, "web")
mux.Handle("/", http.FileServer(http.FS(web)))
mux.HandleFunc("/logout", func(w http.ResponseWriter, r *http.Request) {
pkey.Logout(w, r)
http.Redirect(w, r, "/", http.StatusSeeOther)
})
// private routes
privateMux := http.NewServeMux()
privateMux.HandleFunc("/", privateHandler())
// wrap the privateMux with the Auth middleware
withAuth := pkey.Auth(
userKey,
nil,
passkey.RedirectUnauthorized(url.URL{Path: "/"}),
)
mux.Handle("/private", withAuth(privateMux))
// start the server
l.Infof("Listening on %s\n", origin)
if err := http.ListenAndServe(serverPort, mux); err != nil { //nolint:gosec
panic(err)
}
}
You can optionally provide a logger to the New
function using the WithLogger
option.
Full list of options:
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 |
You need a client-side library that can be used to interact with the server-side library. In example app we use
SimpleWebAuthn library (check _example/web
directory).
The library comes with an example application that demonstrates how to use it. To run the example application just run the following command:
# go run local example app on http://localhost:8080 (no ssl)
make run
or
# run example app in docker container on https://localhost (with self-signed certificate)
make up
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. |
The library provides a middleware function that can be used to protect routes that require authentication.
Auth(userIDKey string, onSuccess, onFail http.HandlerFunc) func (next http.Handler) http.Handler
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.UserIDFromCtx
.
passkey
contains a helper function:
Helper | Description |
---|---|
Unauthorized | Returns a 401 Unauthorized response when the user is not authenticated. |
RedirectUnauthorized(target) | Redirects the user to a given URL when they are not authenticated. |
UserFromContext | Get userID from context |
You can use it to protect routes that require authentication:
package main
import (
"net/url"
"github.com/egregors/passkey"
)
func main() {
pkey, err := passkey.New(...)
check(err)
withAuth := pkey.Auth(
"pkUser",
nil,
passkey.RedirectUnauthorized(url.URL{Path: "/"}),
)
mux.Handle("/private", withAuth(privateMux))
}
To common dev task just use make
:
β passkey git:(main) make help
Usage: make [task]
task help
------ ----
lint Lint the files
test Run unittests
run Run example project
up Run example project with local SSL (self-signed certificate)
gen Generate mocks
update-go-deps Updating Go dependencies
help Show help message
Use mockery to generate mocks for interfaces.A
Q: I'm getting an error "named cookie not present" when I try to interact with passkey running on localhost
from
macOS Safari.
A: This is a known issue with Safari and localhost. You can work around it by using the WithInsecureCookie
option when
creating a new Passkey
instance. This will set the Secure
flag on the session cookie to false
.
Q: I'm getting an error "Error validating origin" when I try to interact with passkey.
A: This error occurs when the origin URL is not included in the RPOrigins
list. Make sure that the origin URL is
included in the RPOrigins
list when creating a new Passkey
instance.
Q: I'm getting an error "WebAuthn in not supported in this browser" when I try to interact with passkey on localhost
from iOS Safari.
A: Mobile Safari no not store insecure cookies. To play around with the example app on iOS Safari, you should run in
with self-signed certificate. Use make up
to run the example app in docker container with self-signed certificate.
Bug reports, bug fixes and new features are always welcome. Please open issues and submit pull requests for any new code.
This project is licensed under the MIT License - see the LICENSE file for details.