Skip to content

Commit

Permalink
add Auth middleware helpers
Browse files Browse the repository at this point in the history
  • Loading branch information
egregors committed Jul 14, 2024
1 parent 9f0c317 commit f647015
Show file tree
Hide file tree
Showing 9 changed files with 496 additions and 45 deletions.
3 changes: 2 additions & 1 deletion .mockery.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ packages:
github.com/egregors/passkey:
interfaces:
Logger:
UserStore:
UserStore:
SessionStore:
57 changes: 38 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

<div align="center">
<h1>🔑 passkey</h1>

Expand Down Expand Up @@ -54,20 +53,20 @@ To add a passkey service to your application, you need to do two things:

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

type UserStore interface {
GetOrCreateUser(userName string) User
SaveUser(User)
GetOrCreateUser(userName 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 Down Expand Up @@ -154,23 +153,43 @@ This will start the example application on http://localhost:8080.

### Middleware

The `Auth` function provides middleware for adding passkey HTTP authentication to routes.
The library provides a middleware function that can be used to protect routes that require authentication.

```go
func Auth(sessionStore SessionStore) func (next http.Handler) http.Handler
func Auth(sessionStore SessionStore, onSuccess, onFail http.HandlerFunc) func (next http.Handler) http.Handler {
```
It takes two callback functions that are called when the user is authenticated or not.
`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. |
You can use it to protect routes that require authentication:
```go
mux := http.NewServeMux()
mux.HandleFunc("/private", 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)
mux.Handle("/private", withAuth(privateMux))
package main

import (
"net/url"

"github.com/egregors/passkey"
)

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

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

```
## Development
Expand Down
9 changes: 8 additions & 1 deletion _example/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package main
import (
"fmt"
"net/http"
"net/url"
"time"

"github.com/egregors/passkey"
Expand Down Expand Up @@ -45,7 +46,13 @@ func main() {
// render html from web/private.html
http.ServeFile(w, r, "./_example/web/private.html")
})
withAuth := passkey.Auth(storage)

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

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

fmt.Printf("Listening on %s\n", origin)
Expand Down
14 changes: 8 additions & 6 deletions _example/storage.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,20 +65,22 @@ func (s *Storage) GenSessionID() (string, error) {
return base64.URLEncoding.EncodeToString(b), nil
}

func (s *Storage) GetSession(token string) (webauthn.SessionData, bool) {
func (s *Storage) GetSession(token string) (*webauthn.SessionData, bool) {
s.sMu.RLock()
defer s.sMu.RUnlock()

val, ok := s.sessions[token]

return val, ok
if val, ok := s.sessions[token]; !ok {
return nil, false
} else {
return &val, true
}
}

func (s *Storage) SaveSession(token string, data webauthn.SessionData) {
func (s *Storage) SaveSession(token string, data *webauthn.SessionData) {
s.sMu.Lock()
defer s.sMu.Unlock()

s.sessions[token] = data
s.sessions[token] = *data
}

func (s *Storage) DeleteSession(token string) {
Expand Down
10 changes: 5 additions & 5 deletions handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ func (p *Passkey) beginRegistration(w http.ResponseWriter, r *http.Request) {
JSONResponse(w, fmt.Sprintf("can't generate session id: %s", err.Error()), http.StatusInternalServerError)
}

p.sessionStore.SaveSession(t, *session)
p.sessionStore.SaveSession(t, session)

http.SetCookie(w, &http.Cookie{
Name: sessionCookieName,
Expand Down Expand Up @@ -78,7 +78,7 @@ func (p *Passkey) finishRegistration(w http.ResponseWriter, r *http.Request) {
// TODO: username != user id? need to check
user := p.userStore.GetOrCreateUser(string(session.UserID)) // Get the user

credential, err := p.webAuthn.FinishRegistration(user, session, r)
credential, err := p.webAuthn.FinishRegistration(user, *session, r)
if err != nil {
msg := fmt.Sprintf("can't finish registration: %s", err.Error())
p.l.Errorf(msg)
Expand Down Expand Up @@ -130,7 +130,7 @@ func (p *Passkey) beginLogin(w http.ResponseWriter, r *http.Request) {

return
}
p.sessionStore.SaveSession(t, *session)
p.sessionStore.SaveSession(t, session)

http.SetCookie(w, &http.Cookie{
Name: sessionCookieName,
Expand Down Expand Up @@ -163,7 +163,7 @@ func (p *Passkey) finishLogin(w http.ResponseWriter, r *http.Request) {
// TODO: username != user id? need to check
user := p.userStore.GetOrCreateUser(string(session.UserID)) // Get the user

credential, err := p.webAuthn.FinishLogin(user, session, r)
credential, err := p.webAuthn.FinishLogin(user, *session, r)
if err != nil {
p.l.Errorf("can't finish login: %s", err.Error())
JSONResponse(w, fmt.Sprintf("can't finish login: %s", err.Error()), http.StatusBadRequest)
Expand Down Expand Up @@ -193,7 +193,7 @@ func (p *Passkey) finishLogin(w http.ResponseWriter, r *http.Request) {
return
}

p.sessionStore.SaveSession(t, webauthn.SessionData{
p.sessionStore.SaveSession(t, &webauthn.SessionData{
Expires: time.Now().Add(time.Hour),
})
http.SetCookie(w, &http.Cookie{
Expand Down
4 changes: 2 additions & 2 deletions ifaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ type UserStore interface {

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

Expand Down
42 changes: 31 additions & 11 deletions middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,37 +2,57 @@ package passkey

import (
"net/http"
"net/url"
"time"
)

// Auth implements a middleware handler for adding passkey http auth to a route.
//
// TODO:
// - cookie name should be configurable
// - fallback to a login page if not authenticated
// - fallback must be configurable as well
func Auth(sessionStore SessionStore) func(next http.Handler) http.Handler {
// It checks if the request has a valid session cookie and if the session is still valid.
// If the session is valid, the onSuccess handler is called and the next handler is executed.
// If the session is invalid, the onFail handler is called and the next handler is not executed.
func Auth(sessionStore SessionStore, onSuccess, onFail http.HandlerFunc) func(next http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// TODO: cookie name should be configurable
sid, err := r.Cookie("sid")
sid, err := r.Cookie(sessionCookieName)
if err != nil {
w.WriteHeader(http.StatusUnauthorized)
exec(onFail, w, r)

return
}

session, ok := sessionStore.GetSession(sid.Value)
if !ok {
w.WriteHeader(http.StatusUnauthorized)
exec(onFail, w, r)

return
}

if session.Expires.Before(time.Now()) {
w.WriteHeader(http.StatusUnauthorized)
exec(onFail, w, r)

return
}

exec(onSuccess, w, r)
next.ServeHTTP(w, r)
})
}
}

func exec(handlerFunc http.HandlerFunc, w http.ResponseWriter, r *http.Request) {
if handlerFunc != nil {
handlerFunc(w, r)
}
}

// Unauthorized writes a 401 Unauthorized status code to the response.
func Unauthorized(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusUnauthorized)
}

// RedirectUnauthorized redirects the user to the target URL with a 401 Unauthorized status code.
func RedirectUnauthorized(target url.URL) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, target.String(), http.StatusUnauthorized)
}
}
Loading

0 comments on commit f647015

Please sign in to comment.