Skip to content

Commit

Permalink
#9: add some options, including withInsecureCookie
Browse files Browse the repository at this point in the history
  • Loading branch information
egregors committed Jul 27, 2024
1 parent bab7432 commit 3cdcf70
Show file tree
Hide file tree
Showing 10 changed files with 198 additions and 107 deletions.
88 changes: 58 additions & 30 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,20 +70,20 @@ package passkey
import "github.com/go-webauthn/webauthn/webauthn"

type User interface {
webauthn.User
PutCredential(webauthn.Credential)
webauthn.User
PutCredential(webauthn.Credential)
}

type UserStore interface {
GetOrCreateUser(UserID string) User
SaveUser(User)
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)
GenSessionID() (string, error)
GetSession(token string) (*webauthn.SessionData, bool)
SaveSession(token string, data *webauthn.SessionData)
DeleteSession(token string)
}

```
Expand All @@ -96,6 +96,7 @@ package main
import (
"fmt"
"net/http"
"net/url"
"time"

"github.com/egregors/passkey"
Expand All @@ -119,44 +120,65 @@ func main() {
},
UserStore: storage,
SessionStore: storage,
SessionMaxAge: 60 * time.Minute,
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/")
pkey.MountStaticRoutes(mux, "/static/")

// public routes
mux.Handle("/", http.FileServer(http.Dir("./_example/web")))
mux.HandleFunc("/logout", func(w http.ResponseWriter, r *http.Request) {
pkey.Logout(w, r)
http.Redirect(w, r, "/", http.StatusSeeOther)
})
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("/", func(w http.ResponseWriter, r *http.Request) {
// render html from web/private.html
http.ServeFile(w, r, "./_example/web/private.html")
})
withAuth := passkey.Auth(storage)
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(port, 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 |

### Example Application

The library comes with an example application that demonstrates how to use the library. To run the example application,
navigate to the `_example` directory and run the following command:
The library comes with an example application that demonstrates how to use it. To run the example application
just run the following command:

```bash
make run
Expand All @@ -166,18 +188,19 @@ This will start the example application on http://localhost:8080.

## 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. |
| `MountStaticRoutes(mux *http.ServeMux, path string)` | Mounts the static routes onto a given HTTP multiplexer. |
| 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. |
| `MountStaticRoutes(mux *http.ServeMux, path string)` | Mounts the static 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. |

### Middleware

The library provides a middleware function that can be used to protect routes that require authentication.

```go
Auth(sessionStore SessionStore, userIDKey string, onSuccess, onFail http.HandlerFunc) func (next http.Handler) http.Handler
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.
Expand All @@ -204,16 +227,18 @@ import (
)

func main() {
// ...
withAuth := passkey.Auth(
storage,
pkey, err := passkey.New(...)
check(err)

withAuth := pkey.Auth(
"pkUser",
nil,
passkey.RedirectUnauthorized(url.URL{Path: "/"}),
)

mux.Handle("/private", withAuth(privateMux))
}

```

## Development
Expand Down Expand Up @@ -242,7 +267,10 @@ help Show help message

Use [mockery](https://github.com/vektra/mockery) to generate mocks for interfaces.

```
## Contributing

Bug reports, bug fixes and new features are always welcome. Please open issues and submit pull requests for any new
code.

## License

Expand Down
42 changes: 26 additions & 16 deletions _example/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,26 +30,50 @@ func main() {
},
UserStore: storage,
SessionStore: storage,
SessionMaxAge: 60 * time.Minute,
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/")
pkey.MountStaticRoutes(mux, "/static/")

// public routes
mux.Handle("/", http.FileServer(http.Dir("./_example/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("/", func(w http.ResponseWriter, r *http.Request) {
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(port, mux); err != nil {
panic(err)
}
}

func privateHandler() func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
// get the userID from the request context
userID, ok := passkey.UserFromContext(r.Context(), userKey)
if !ok {
Expand All @@ -76,19 +100,5 @@ func main() {

return
}
})

withAuth := passkey.Auth(
storage,
userKey,
nil,
passkey.RedirectUnauthorized(url.URL{Path: "/"}),
)

mux.Handle("/private", withAuth(privateMux))

fmt.Printf("Listening on %s\n", origin)
if err := http.ListenAndServe(port, mux); err != nil {
panic(err)
}
}
62 changes: 24 additions & 38 deletions handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,16 +43,7 @@ func (p *Passkey) beginRegistration(w http.ResponseWriter, r *http.Request) {
}

p.sessionStore.SaveSession(t, session)

http.SetCookie(w, &http.Cookie{
Name: sessionCookieName,
Value: t,
Path: "/", // TODO: it probably shouldn't be root
MaxAge: registerMaxAge,
Secure: true,
HttpOnly: true,
SameSite: http.SameSiteLaxMode, // TODO: SameSiteStrictMode maybe?
})
p.setSessionCookie(w, t)

// return the options generated with the session key
// options.publicKey contain our registration options
Expand All @@ -61,7 +52,7 @@ func (p *Passkey) beginRegistration(w http.ResponseWriter, r *http.Request) {

func (p *Passkey) finishRegistration(w http.ResponseWriter, r *http.Request) {
// Get the session key from cookie
sid, err := r.Cookie(sessionCookieName)
sid, err := r.Cookie(p.cookieSettings.Name)
if err != nil {
p.l.Errorf("can't get session id: %s", err.Error())
JSONResponse(w, fmt.Sprintf("can't get session id: %s", err.Error()), http.StatusBadRequest)
Expand All @@ -86,7 +77,7 @@ func (p *Passkey) finishRegistration(w http.ResponseWriter, r *http.Request) {
msg := fmt.Sprintf("can't finish registration: %s", err.Error())
p.l.Errorf(msg)

deleteCookie(w, sessionCookieName)
p.deleteSessionCookie(w)
JSONResponse(w, msg, http.StatusBadRequest)

return
Expand All @@ -97,7 +88,7 @@ func (p *Passkey) finishRegistration(w http.ResponseWriter, r *http.Request) {
p.userStore.SaveUser(user)

p.sessionStore.DeleteSession(sid.Value)
deleteCookie(w, sessionCookieName)
p.deleteSessionCookie(w)

p.l.Infof("finish registration")
JSONResponse(w, "Registration Success", http.StatusOK)
Expand All @@ -120,7 +111,7 @@ func (p *Passkey) beginLogin(w http.ResponseWriter, r *http.Request) {
msg := fmt.Sprintf("can't begin login: %s", err.Error())
p.l.Errorf(msg)
JSONResponse(w, msg, http.StatusBadRequest)
deleteCookie(w, sessionCookieName)
p.deleteSessionCookie(w)

return
}
Expand All @@ -134,16 +125,7 @@ func (p *Passkey) beginLogin(w http.ResponseWriter, r *http.Request) {
return
}
p.sessionStore.SaveSession(t, session)

http.SetCookie(w, &http.Cookie{
Name: sessionCookieName,
Value: t,
Path: "/", // TODO: it probably shouldn't be root
MaxAge: loginMaxAge,
Secure: true,
HttpOnly: true,
SameSite: http.SameSiteLaxMode, // TODO: SameSiteStrictMode maybe?
})
p.setSessionCookie(w, t)

// return the options generated with the session key
// options.publicKey contain our registration options
Expand All @@ -152,7 +134,7 @@ func (p *Passkey) beginLogin(w http.ResponseWriter, r *http.Request) {

func (p *Passkey) finishLogin(w http.ResponseWriter, r *http.Request) {
// Get the session key from cookie
sid, err := r.Cookie(sessionCookieName)
sid, err := r.Cookie(p.cookieSettings.Name)
if err != nil {
p.l.Errorf("can't get session id: %s", err.Error())
JSONResponse(w, fmt.Sprintf("can't get session id: %s", err.Error()), http.StatusBadRequest)
Expand Down Expand Up @@ -185,7 +167,7 @@ func (p *Passkey) finishLogin(w http.ResponseWriter, r *http.Request) {

// Delete the login session data
p.sessionStore.DeleteSession(sid.Value)
deleteCookie(w, sessionCookieName)
p.deleteSessionCookie(w)

// Add the new session cookie
t, err := p.sessionStore.GenSessionID()
Expand All @@ -201,15 +183,7 @@ func (p *Passkey) finishLogin(w http.ResponseWriter, r *http.Request) {
UserID: session.UserID,
Expires: time.Now().Add(p.cfg.SessionMaxAge),
})
http.SetCookie(w, &http.Cookie{
Name: sessionCookieName,
Value: t,
Path: "/",
MaxAge: int(p.sessionMaxAge.Seconds()),
Secure: true,
HttpOnly: true,
SameSite: http.SameSiteLaxMode, // TODO: SameSiteStrictMode maybe?
})
p.setSessionCookie(w, t)

p.l.Infof("finish login")
JSONResponse(w, "Login Success", http.StatusOK)
Expand Down Expand Up @@ -239,10 +213,22 @@ func JSONResponse(w http.ResponseWriter, data interface{}, status int) {
_ = json.NewEncoder(w).Encode(data)
}

// deleteCookie deletes a cookie
func deleteCookie(w http.ResponseWriter, name string) { //nolint:unparam // it's ok here
func (p *Passkey) setSessionCookie(w http.ResponseWriter, value string) {
http.SetCookie(w, &http.Cookie{
Name: p.cookieSettings.Name,
Value: value,
Path: p.cookieSettings.Path,
MaxAge: int(p.cookieSettings.MaxAge.Seconds()),
Secure: p.cookieSettings.Secure,
HttpOnly: p.cookieSettings.HttpOnly,
SameSite: p.cookieSettings.SameSite,
})
}

// deleteSessionCookie deletes a cookie
func (p *Passkey) deleteSessionCookie(w http.ResponseWriter) { //nolint:unparam // it's ok here
http.SetCookie(w, &http.Cookie{
Name: name,
Name: p.cookieSettings.Name,
Value: "",
Expires: time.Unix(0, 0),
MaxAge: -1,
Expand Down
3 changes: 2 additions & 1 deletion handlers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"net/http"
"net/http/httptest"
"testing"
"time"

"github.com/go-webauthn/webauthn/protocol"
"github.com/go-webauthn/webauthn/webauthn"
Expand Down Expand Up @@ -230,7 +231,7 @@ func TestPasskey_beginRegistration(t *testing.T) {
},
UserStore: tt.userStore(),
SessionStore: tt.sessionStore(),
SessionMaxAge: 69,
SessionMaxAge: 69 * time.Second,
},
)
assert.NoError(t, err)
Expand Down
Loading

0 comments on commit 3cdcf70

Please sign in to comment.