- 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.
Warning
Stable version is not released yet. The API and the lib are under development.
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
- 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).
To add a passkey service to your application, you need to do two things:
package passkey
import "github.com/go-webauthn/webauthn/webauthn"
type User interface {
webauthn.User
PutCredential(webauthn.Credential)
}
type UserStore interface {
GetOrCreateUser(UserID string) User
SaveUser(User)
}
type SessionStore interface {
GenSessionID() (string, error)
GetSession(token string) (*webauthn.SessionData, bool)
SaveSession(token string, data *webauthn.SessionData)
DeleteSession(token string)
}
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"
)
//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)
storage := NewStorage()
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: storage,
SessionStore: storage,
SessionMaxAge: 24 * time.Hour,
},
passkey.WithLogger(NewLogger()),
passkey.WithCookieMaxAge(60*time.Minute),
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
fmt.Printf("Listening on %s\n", origin)
if err := http.ListenAndServe(serverPort, mux); err != nil {
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 |
WithSessionCookieName | sid |
Sets the name of the session cookie |
WithCookieMaxAge | 60 minutes | Sets the max age of the 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. |
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.UserFromContext
.
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.