/
authn.go
181 lines (152 loc) · 5.36 KB
/
authn.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
package yubikey
import (
"errors"
"net/http"
"github.com/gin-gonic/gin"
"github.com/go-webauthn/webauthn/protocol"
"github.com/go-webauthn/webauthn/webauthn"
"github.com/rs/zerolog/log"
)
type RegistrationForm struct {
Email string `json:"email"`
Name string `json:"name"`
}
func (s *Server) BeginRegistration(c *gin.Context) {
form := &RegistrationForm{}
if err := c.BindJSON(form); err != nil {
log.Error().Err(err).Msg("could not bind registration form")
c.JSON(http.StatusBadRequest, gin.H{"error": "could not bind registration form"})
return
}
if form.Email == "" || form.Name == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "missing either name or email address"})
return
}
// Find or create a new user
user, err := s.users.NewUser(form.Name, form.Email)
if err != nil {
if errors.Is(err, ErrUserAlreadyExists) {
if user, err = s.users.GetUser(form.Email); err != nil {
log.Warn().Err(err).Msg("could not retrieve an existing user")
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
} else {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
}
// Ensure the same authenticator cannot be registered twice
registerOptions := func(credCreationOpts *protocol.PublicKeyCredentialCreationOptions) {
credCreationOpts.CredentialExcludeList = user.CredentialExcludeList()
}
opts, session, err := s.authn.BeginRegistration(user, registerOptions)
if err != nil {
log.Error().Err(err).Msg("could not begin webauthn registration")
c.JSON(http.StatusInternalServerError, gin.H{"error": "could not begin webauthn registration"})
return
}
// Session values must be stored
if err := s.sessions.SaveWebauthnSession("registration", session, c.Request, c.Writer); err != nil {
log.Error().Err(err).Msg("could not save session data")
c.JSON(http.StatusInternalServerError, gin.H{"error": "could not begin webauthn registration"})
return
}
// Return the options as JSON
c.JSON(http.StatusOK, opts)
}
func (s *Server) FinishRegistration(c *gin.Context) {
var (
user *User
session webauthn.SessionData
err error
)
// Load the session data
if session, err = s.sessions.GetWebauthnSession("registration", c.Request); err != nil {
log.Warn().Err(err).Msg("could not get session data from request")
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Load the user from the session user ID
// TODO: do we have to sidechannel this information for security?
// e.g. the example uses the username in a param rather than from the session
if user, err = s.users.Lookup(session.UserID); err != nil {
log.Warn().Err(err).Msg("could not lookup user from session data")
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
var credential *webauthn.Credential
if credential, err = s.authn.FinishRegistration(user, session, c.Request); err != nil {
log.Warn().Err(err).Msg("could not finish registration")
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Check to make sure the credential is not already assigned to a user.
if s.users.CredentialExists(credential) {
c.JSON(http.StatusBadRequest, gin.H{"error": "credential already assigned"})
return
}
// Add the credential to the user and return the response
user.AddCredential(*credential)
c.JSON(http.StatusOK, gin.H{"message": "registration successful"})
}
type LoginForm struct {
Email string `json:"email"`
}
func (s *Server) BeginLogin(c *gin.Context) {
form := &LoginForm{}
if err := c.BindJSON(form); err != nil {
log.Error().Err(err).Msg("could not bind login form")
c.JSON(http.StatusBadRequest, gin.H{"error": "could not bind login form"})
return
}
if form.Email == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "email is required to login"})
return
}
// Look up user
user, err := s.users.GetUser(form.Email)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
opts, session, err := s.authn.BeginLogin(user)
if err != nil {
log.Error().Err(err).Msg("could not begin webauthn login")
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// Session values must be stored
if err := s.sessions.SaveWebauthnSession("authentication", session, c.Request, c.Writer); err != nil {
log.Error().Err(err).Msg("could not save session data")
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, opts)
}
func (s *Server) FinishLogin(c *gin.Context) {
var (
user *User
session webauthn.SessionData
err error
)
// Load the session data
if session, err = s.sessions.GetWebauthnSession("authentication", c.Request); err != nil {
log.Warn().Err(err).Msg("could not get session data from request")
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Load the user from the session user ID
// TODO: do we have to sidechannel this information for security?
if user, err = s.users.Lookup(session.UserID); err != nil {
log.Warn().Err(err).Msg("could not lookup user from session data")
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if _, err = s.authn.FinishLogin(user, session, c.Request); err != nil {
log.Warn().Err(err).Msg("could not finish registration")
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "login successful"})
}