diff options
Diffstat (limited to 'app/controllers/login.go')
-rw-r--r-- | app/controllers/login.go | 117 |
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 @@ | |||
1 | package controllers | ||
2 | |||
3 | import ( | ||
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 | |||
18 | type 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 | |||
27 | func (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 | |||
48 | func (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 | } | ||