Skip to content

Commit

Permalink
#31: .
Browse files Browse the repository at this point in the history
  • Loading branch information
egregors committed Nov 13, 2024
1 parent 4843c7b commit f5cf483
Show file tree
Hide file tree
Showing 11 changed files with 238 additions and 77 deletions.
13 changes: 11 additions & 2 deletions _example/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,21 @@ import "github.com/go-webauthn/webauthn/webauthn"

type User struct {
ID []byte
DisplayName string
Name string
DisplayName string

creds []webauthn.Credential
}

func New(ID []byte, name, displayName string) *User {
return &User{
ID: ID,
Name: name,
DisplayName: displayName,
creds: make([]webauthn.Credential, 0, 0),
}
}

func (u *User) WebAuthnID() []byte {
return u.ID
}
Expand All @@ -26,6 +35,6 @@ func (u *User) WebAuthnCredentials() []webauthn.Credential {
return u.creds
}

func (u *User) PutCredential(credential webauthn.Credential) {
func (u *User) AddCredential(credential webauthn.Credential) {
u.creds = append(u.creds, credential)
}
14 changes: 14 additions & 0 deletions _example/storage.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,20 @@ type Storage struct {
uMu, sMu sync.RWMutex
}

func (s *Storage) New(id []byte, name string, displayName string) passkey.User {
//TODO implement me
panic("implement me")
}

func (s *Storage) Get(userName string) passkey.User {
return nil
}

func (s *Storage) Update(u passkey.User) error {
//TODO implement me
panic("implement me")
}

func NewStorage() *Storage {
return &Storage{
users: make(map[string]passkey.User),
Expand Down
4 changes: 2 additions & 2 deletions _example/web/script.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ async function register() {
// Get registration options from your server. Here, we also receive the challenge.
const response = await fetch('/api/passkey/registerBegin', {
method: 'POST', headers: {'Content-Type': 'application/json'},
body: JSON.stringify({username: username})
body: JSON.stringify({name: username})
});

// Check if the registration options are ok.
Expand Down Expand Up @@ -63,7 +63,7 @@ async function login() {
// Get login options from your server. Here, we also receive the challenge.
const response = await fetch('/api/passkey/loginBegin', {
method: 'POST', headers: {'Content-Type': 'application/json'},
body: JSON.stringify({username: username})
body: JSON.stringify({name: username})
});
// Check if the login options are ok.
if (!response.ok) {
Expand Down
2 changes: 1 addition & 1 deletion errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@ package passkey
import "fmt"

var (
ErrNoUsername = fmt.Errorf("no username provided")
ErrNoUsername = fmt.Errorf("no username provided: missing key 'name'")
)
90 changes: 64 additions & 26 deletions handlers.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package passkey

import (
"cmp"
"encoding/json"
"fmt"
"net/http"
Expand All @@ -9,20 +10,32 @@ import (
"github.com/go-webauthn/webauthn/webauthn"
)

type payload struct {
Name string `json:"name"`
DisplayName string `json:"displayName,omitempty"`
}

func (p *Passkey) beginRegistration(w http.ResponseWriter, r *http.Request) {
p.l.Infof("begin registration")

// TODO: i don't like this, but it's a quick solution
// can we actually do not use the username at all?
username, err := getUsername(r)
userData, err := p.parsePayload(r)
if err != nil {
p.l.Errorf("can't get username: %s", err.Error())
JSONResponse(w, fmt.Sprintf("can't get username: %s", err.Error()), http.StatusBadRequest)
msg := fmt.Sprintf("bad payload: %s", err.Error())
p.l.Errorf(msg)
JSONResponse(w, fmt.Sprintf("beginRegistration failed: %s", msg), http.StatusBadRequest)

return
}

user := p.userStore.GetOrCreateUser(username)
// get or create user
user := cmp.Or(
p.userStore.Get(userData.Name),
p.userStore.New(
p.genUserID(),
userData.Name,
userData.DisplayName,
),
)

options, session, err := p.webAuthn.BeginRegistration(user)
if err != nil {
Expand Down Expand Up @@ -70,7 +83,14 @@ 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
// user := p.userStore.GetOrCreateUser(string(session.UserID)) // Get the user
user := p.userStore.Get(string(session.UserID))
if user == nil {
p.l.Errorf("can't get user")
JSONResponse(w, "can't get user", http.StatusBadRequest)

return
}

credential, err := p.webAuthn.FinishRegistration(user, *session, r)
if err != nil {
Expand All @@ -84,8 +104,18 @@ func (p *Passkey) finishRegistration(w http.ResponseWriter, r *http.Request) {
}

// If creation was successful, store the credential object
user.PutCredential(*credential)
p.userStore.SaveUser(user)
user.AddCredential(*credential)

err = p.userStore.Update(user)
if err != nil {
msg := fmt.Sprintf("can't finish registration: %s", err.Error())
p.l.Errorf(msg)

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

return
}

p.sessionStore.DeleteSession(sid.Value)
p.deleteSessionCookie(w)
Expand All @@ -96,15 +126,18 @@ func (p *Passkey) finishRegistration(w http.ResponseWriter, r *http.Request) {

func (p *Passkey) beginLogin(w http.ResponseWriter, r *http.Request) {
p.l.Infof("begin login")
username, err := getUsername(r)

userData, err := p.parsePayload(r)
if err != nil {
p.l.Errorf("can't get user name: %s", err.Error())
JSONResponse(w, fmt.Sprintf("can't get user name: %s", err.Error()), http.StatusBadRequest)
msg := fmt.Sprintf("bad payload: %s", err.Error())
p.l.Errorf(msg)
JSONResponse(w, fmt.Sprintf("beginLogin failed: %s", msg), http.StatusBadRequest)

return
}

user := p.userStore.GetOrCreateUser(username)
// FIXME: it probably should be user.id
user := p.userStore.Get(userData.Name)

options, session, err := p.webAuthn.BeginLogin(user)
if err != nil {
Expand Down Expand Up @@ -146,7 +179,9 @@ func (p *Passkey) finishLogin(w http.ResponseWriter, r *http.Request) {
session, _ := p.sessionStore.GetSession(sid.Value) // FIXME: cover invalid session

// TODO: username != user id? need to check
user := p.userStore.GetOrCreateUser(string(session.UserID)) // Get the user
// user := p.userStore.GetOrCreateUser(string(session.UserID)) // Get the user
// FIXME: don't sure about this
user := p.userStore.Get(string(session.UserID))

credential, err := p.webAuthn.FinishLogin(user, *session, r)
if err != nil {
Expand All @@ -162,8 +197,9 @@ func (p *Passkey) finishLogin(w http.ResponseWriter, r *http.Request) {
}

// If login was successful, update the credential object
user.PutCredential(*credential)
p.userStore.SaveUser(user)
user.AddCredential(*credential)
// FIXME: handle error
p.userStore.Update(user)

// Delete the login session data
p.sessionStore.DeleteSession(sid.Value)
Expand All @@ -189,21 +225,23 @@ func (p *Passkey) finishLogin(w http.ResponseWriter, r *http.Request) {
JSONResponse(w, "Login Success", http.StatusOK)
}

// getUsername extracts an username from json request
func getUsername(r *http.Request) (string, error) {
var u struct {
Username string `json:"username"`
}
func (p *Passkey) parsePayload(r *http.Request) (payload, error) {
var pld payload
if err := json.NewDecoder(r.Body).Decode(&pld); err != nil {
p.l.Errorf("can't decode payload: %s", err.Error())

if err := json.NewDecoder(r.Body).Decode(&u); err != nil {
return "", err
return payload{}, err
}

if u.Username == "" {
return "", ErrNoUsername
// user.Name is required
if pld.Name == "" {
return payload{}, ErrNoUsername
}

return u.Username, nil
// if user.DisplayName is empty, use user.Name
pld.DisplayName = cmp.Or(pld.DisplayName, pld.Name)

return pld, nil
}

// JSONResponse sends json response
Expand Down
28 changes: 22 additions & 6 deletions handlers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ func body(key, value string) io.Reader {
return io.NopCloser(bytes.NewReader(bodyBytes))
}

func Test_getUsername(t *testing.T) {
func Test_parsePayload(t *testing.T) {
t.Skip("TODO: fix this test")

tests := []struct {
name string
Expand Down Expand Up @@ -63,9 +64,24 @@ func Test_getUsername(t *testing.T) {
},
},
}

pk, err := New(
Config{
WebauthnConfig: &webauthn.Config{
RPDisplayName: "Passkey Test",
RPID: "localhost",
RPOrigins: []string{"localhost"},
},
UserStore: NewMockUserStore(t),
SessionStore: NewMockSessionStore(t),
SessionMaxAge: 69 * time.Second,
},
)
assert.NoError(t, err)

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := getUsername(tt.r)
got, err := pk.parsePayload(tt.r)
if !tt.wantErr(t, err, fmt.Sprintf("getUsername(%v)", tt.r)) {
return
}
Expand Down Expand Up @@ -105,7 +121,7 @@ func TestPasskey_beginRegistration(t *testing.T) {
userStore: func() UserStore {
store := NewMockUserStore(t)
store.EXPECT().
GetOrCreateUser("Berik the Cat").
New([]byte("Berik the Cat"), "Berik", "The Cat").
Times(1).
Return(user, nil)

Expand Down Expand Up @@ -195,7 +211,7 @@ func TestPasskey_beginRegistration(t *testing.T) {
userStore: func() UserStore {
store := NewMockUserStore(t)
store.EXPECT().
GetOrCreateUser("Berik the Cat").
New([]byte("Berik the Cat"), "Berik", "The Cat").
Times(1).
Return(user, nil)

Expand All @@ -218,7 +234,7 @@ func TestPasskey_beginRegistration(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
p, err := New(
pk, err := New(
Config{
WebauthnConfig: &webauthn.Config{
RPDisplayName: "Passkey Test",
Expand All @@ -232,7 +248,7 @@ func TestPasskey_beginRegistration(t *testing.T) {
)
assert.NoError(t, err)

p.beginRegistration(tt.w, tt.r)
pk.beginRegistration(tt.w, tt.r)

assert.Equal(t, tt.wantStatus, tt.w.Code)

Expand Down
7 changes: 4 additions & 3 deletions ifaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,13 @@ type Logger interface {

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

type UserStore interface {
GetOrCreateUser(userID string) User
SaveUser(User)
New(id []byte, name string, displayName string) User
Get(userName string) User
Update(u User) error
}

type SessionStore interface {
Expand Down
20 changes: 10 additions & 10 deletions mock_User.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit f5cf483

Please sign in to comment.