summaryrefslogtreecommitdiff
path: root/app/controllers/login.go
diff options
context:
space:
mode:
Diffstat (limited to 'app/controllers/login.go')
-rw-r--r--app/controllers/login.go117
1 files changed, 117 insertions, 0 deletions
diff --git a/app/controllers/login.go b/app/controllers/login.go
new file mode 100644
index 0000000..603eb20
--- /dev/null
+++ b/app/controllers/login.go
@@ -0,0 +1,117 @@
1package controllers
2
3import (
4 "bytes"
5 "encoding/json"
6 "io"
7 "net/http"
8 "time"
9
10 "code.crute.us/mcrute/golib/echo/session"
11 "code.crute.us/mcrute/ssh-proxy/app"
12 "code.crute.us/mcrute/ssh-proxy/app/models"
13 "github.com/go-webauthn/webauthn/protocol"
14 "github.com/go-webauthn/webauthn/webauthn"
15 "github.com/labstack/echo/v4"
16)
17
18type LoginController[T app.AppSession] struct {
19 Logger echo.Logger
20 Sessions session.Store[T]
21 Users models.UserStore
22 AuthSessions models.AuthSessionStore
23 Webauthn *webauthn.WebAuthn
24 SessionExpiration time.Duration
25}
26
27func (a *LoginController[T]) HandleStart(c echo.Context) error {
28 user, err := a.Users.Get(c.Request().Context(), c.Param("username"))
29 if err != nil {
30 a.Logger.Errorf("Error getting user: %s", err)
31 return c.NoContent(http.StatusNotFound)
32 }
33
34 request, sessionData, err := a.Webauthn.BeginLogin(user)
35 if err != nil {
36 a.Logger.Errorf("Error creating webauthn request: %s", err)
37 return c.NoContent(http.StatusInternalServerError)
38 }
39
40 session := a.Sessions.Get(c)
41 s := session.Self()
42 s.WebauthnSession = sessionData
43 a.Sessions.Update(c, session)
44
45 return c.JSON(http.StatusOK, request)
46}
47
48func (a *LoginController[T]) HandleFinish(c echo.Context) error {
49 ctx := c.Request().Context()
50
51 body, err := io.ReadAll(c.Request().Body)
52 if err != nil {
53 a.Logger.Errorf("Error reading request body:", err)
54 return c.NoContent(http.StatusInternalServerError)
55 }
56
57 user, err := a.Users.Get(ctx, c.Param("username"))
58 if err != nil {
59 a.Logger.Errorf("Error getting user: %s", err)
60 return c.NoContent(http.StatusNotFound)
61 }
62
63 response, err := protocol.ParseCredentialRequestResponseBody(bytes.NewBuffer(body))
64 if err != nil {
65 a.Logger.Errorf("Error parsing credential response: %s", err)
66 return c.NoContent(http.StatusBadRequest)
67 }
68
69 session := a.Sessions.Get(c)
70 s := session.Self()
71
72 if s.WebauthnSession == nil {
73 a.Logger.Errorf("Webauthn session is not set")
74 return c.NoContent(http.StatusBadRequest)
75 }
76
77 if _, err := a.Webauthn.ValidateLogin(user, *s.WebauthnSession, response); err != nil {
78 a.Logger.Errorf("Error validating login: %s", err)
79 return c.NoContent(http.StatusBadRequest)
80 }
81
82 // Don't check the clone warning or the auth count because these are
83 // meaningless for Passkeys since they are synced across devices
84 // (presumably securely). This would only matter for hard tokens like
85 // Yubikeys and since we're also allowing Passkey support there is no
86 // need to be more strict for that class of device.
87
88 var code struct {
89 Code string `json:"code"`
90 }
91 if err := json.Unmarshal(body, &code); err != nil {
92 a.Logger.Errorf("Error decoding json body")
93 return c.NoContent(http.StatusBadRequest)
94 }
95
96 authSession, err := a.AuthSessions.GetByUserCode(ctx, code.Code)
97 if err != nil {
98 a.Logger.Errorf("No auth session exists")
99 return c.NoContent(http.StatusUnauthorized)
100 }
101
102 if authSession.AccessCode != "" {
103 a.Logger.Errorf("Session is already authenticated")
104 return c.NoContent(http.StatusUnauthorized)
105 }
106
107 authSession.GenerateAccessCode()
108 authSession.UserId = user.Username
109 authSession.Expires = time.Now().Add(a.SessionExpiration)
110
111 if err := a.AuthSessions.Upsert(ctx, authSession); err != nil {
112 a.Logger.Errorf("Error saving auth session")
113 return c.NoContent(http.StatusInternalServerError)
114 }
115
116 return c.NoContent(http.StatusOK)
117}