forked from hailey.at/cocoon
An atproto PDS written in Go

totp

+2
go.mod
··· 24 24 github.com/labstack/echo/v4 v4.13.3 25 25 github.com/lestrrat-go/jwx/v2 v2.0.12 26 26 github.com/multiformats/go-multihash v0.2.3 27 + github.com/pquerna/otp v1.5.0 27 28 github.com/samber/slog-echo v1.16.1 28 29 github.com/urfave/cli/v2 v2.27.6 29 30 github.com/whyrusleeping/cbor-gen v0.2.1-0.20241030202151-b7a6831be65e ··· 37 38 github.com/Azure/go-autorest v14.2.0+incompatible // indirect 38 39 github.com/RussellLuo/slidingwindow v0.0.0-20200528002341-535bb99d338b // indirect 39 40 github.com/beorn7/perks v1.0.1 // indirect 41 + github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect 40 42 github.com/carlmjohnson/versioninfo v0.22.5 // indirect 41 43 github.com/cespare/xxhash/v2 v2.3.0 // indirect 42 44 github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect
+4
go.sum
··· 20 20 github.com/bluesky-social/indigo v0.0.0-20250414202759-826fcdeaa36b/go.mod h1:yjdhLA1LkK8VDS/WPUoYPo25/Hq/8rX38Ftr67EsqKY= 21 21 github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY= 22 22 github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= 23 + github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI= 24 + github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= 23 25 github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 h1:R8vQdOQdZ9Y3SkEwmHoWBmX1DNXhXZqlTpq6s4tyJGc= 24 26 github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= 25 27 github.com/carlmjohnson/versioninfo v0.22.5 h1:O00sjOLUAFxYQjlN/bzYTuZiS0y6fWDQjMRvwtKgwwc= ··· 289 291 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 290 292 github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f h1:VXTQfuJj9vKR4TCkEuWIckKvdHFeJH/huIFJ9/cXOB0= 291 293 github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f/go.mod h1:/zvteZs/GwLtCgZ4BL6CBsk9IKIlexP43ObX9AxTqTw= 294 + github.com/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs= 295 + github.com/pquerna/otp v1.5.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg= 292 296 github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= 293 297 github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= 294 298 github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
+9
models/models.go
··· 7 7 "github.com/bluesky-social/indigo/atproto/crypto" 8 8 ) 9 9 10 + type TwoFactorType string 11 + 12 + var ( 13 + TwoFactorTypeNone = TwoFactorType("none") 14 + TwoFactorTypeTotp = TwoFactorType("totp") 15 + ) 16 + 10 17 type Repo struct { 11 18 Did string `gorm:"primaryKey"` 12 19 CreatedAt time.Time ··· 23 30 Rev string 24 31 Root []byte 25 32 Preferences []byte 33 + TwoFactorType TwoFactorType `gorm:"default:none"` 34 + TotpSecret *string 26 35 } 27 36 28 37 func (r *Repo) SignFor(ctx context.Context, did string, msg []byte) ([]byte, error) {
+100
server/handle_account_totp_enroll.go
··· 1 + package server 2 + 3 + import ( 4 + "bytes" 5 + "encoding/base64" 6 + "fmt" 7 + "image/png" 8 + 9 + "github.com/haileyok/cocoon/internal/helpers" 10 + "github.com/haileyok/cocoon/models" 11 + "github.com/labstack/echo/v4" 12 + "github.com/pquerna/otp/totp" 13 + ) 14 + 15 + func (s *Server) handleAccountTotpEnrollGet(e echo.Context) error { 16 + urepo, sess, err := s.getSessionRepoOrErr(e) 17 + if err != nil { 18 + return e.Redirect(303, "/account/signin") 19 + } 20 + 21 + if urepo.TwoFactorType == models.TwoFactorTypeTotp { 22 + sess.AddFlash("You have already enabled TOTP", "error") 23 + sess.Save(e.Request(), e.Response()) 24 + return e.Redirect(303, "/account") 25 + } else if urepo.TwoFactorType != models.TwoFactorTypeNone { 26 + sess.AddFlash("You have already have another 2FA method enabled", "error") 27 + sess.Save(e.Request(), e.Response()) 28 + return e.Redirect(303, "/account") 29 + } 30 + 31 + secret, err := totp.Generate(totp.GenerateOpts{ 32 + Issuer: s.config.Hostname, 33 + AccountName: urepo.Repo.Did, 34 + }) 35 + if err != nil { 36 + s.logger.Error("error generating totp secret", "error", err) 37 + return helpers.ServerError(e, nil) 38 + } 39 + 40 + sess.Values["totp-secret"] = secret 41 + if err := sess.Save(e.Request(), e.Response()); err != nil { 42 + s.logger.Error("error saving session", "error", err) 43 + 44 + return helpers.ServerError(e, nil) 45 + } 46 + 47 + var buf bytes.Buffer 48 + img, err := secret.Image(200, 200) 49 + if err != nil { 50 + s.logger.Error("error generating image from secret", "error", err) 51 + return helpers.ServerError(e, nil) 52 + } 53 + png.Encode(&buf, img) 54 + 55 + b64img := fmt.Sprintf("data:image/png;base64,%s", base64.StdEncoding.EncodeToString(buf.Bytes())) 56 + 57 + return e.Render(200, "totp_enroll.html", map[string]any{ 58 + "flashes": getFlashesFromSession(e, sess), 59 + "Image": b64img, 60 + }) 61 + } 62 + 63 + type TotpEnrollRequest struct { 64 + Code string `form:"code"` 65 + } 66 + 67 + func (s *Server) handleAccountTotpEnrollPost(e echo.Context) error { 68 + urepo, sess, err := s.getSessionRepoOrErr(e) 69 + if err != nil { 70 + return e.Redirect(303, "/account/signin") 71 + } 72 + 73 + var req TotpEnrollRequest 74 + if err := e.Bind(&req); err != nil { 75 + s.logger.Error("error binding request for enroll totp", "error", err) 76 + return helpers.ServerError(e, nil) 77 + } 78 + 79 + secret, ok := sess.Values["totp-secret"].(string) 80 + if !ok { 81 + return helpers.InputError(e, nil) 82 + } 83 + 84 + if !totp.Validate(req.Code, secret) { 85 + sess.AddFlash("The provided code was not valid.", "error") 86 + sess.Save(e.Request(), e.Response()) 87 + return e.Redirect(303, "/account/totp-enroll") 88 + } 89 + 90 + if err := s.db.Exec("UPDATE repos SET two_factor_type = ?, totp_secret = ? WHERE did = ?", nil, models.TwoFactorTypeTotp, secret, urepo.Repo.Did).Error; err != nil { 91 + s.logger.Error("error updating database with totp token", "error", err) 92 + return helpers.ServerError(e, nil) 93 + } 94 + 95 + sess.AddFlash("You have successfully enrolled in TOTP!", "success") 96 + delete(sess.Values, "totp-secret") 97 + sess.Save(e.Request(), e.Response()) 98 + 99 + return e.Redirect(303, "/account") 100 + }
+2
server/server.go
··· 690 690 s.echo.GET("/account/signin", s.handleAccountSigninGet) 691 691 s.echo.POST("/account/signin", s.handleAccountSigninPost) 692 692 s.echo.GET("/account/signout", s.handleAccountSignout) 693 + s.echo.GET("/account/totp-enroll", s.handleAccountTotpEnrollGet) 694 + s.echo.POST("/account/totp-enroll", s.handleAccountTotpEnrollPost) 693 695 694 696 // oauth account 695 697 s.echo.GET("/oauth/jwks", s.handleOauthJwks)
+5
server/static/style.css
··· 81 81 .alert-danger { 82 82 background-color: var(--danger); 83 83 } 84 + 85 + .totp-image { 86 + height: 200; 87 + width: 200; 88 + }
+30
server/templates/totp_enroll.html
··· 1 + <!doctype html> 2 + <html lang="en"> 3 + <head> 4 + <meta charset="utf-8" /> 5 + <meta name="viewport" content="width=device-width, initial-scale=1" /> 6 + <meta name="color-scheme" content="light dark" /> 7 + <link rel="stylesheet" href="/static/pico.css" /> 8 + <link rel="stylesheet" href="/static/style.css" /> 9 + <title>TOTP Enrollment</title> 10 + </head> 11 + <body class="centered-body"> 12 + <main class="container base-container box-shadow-container login-container"> 13 + <h2>TOTP Enrollment</h2> 14 + <p> 15 + Enroll in TOTP by adding the below secret to your TOTP manager and 16 + verifying the code. 17 + </p> 18 + {{ if .flashes.errors }} 19 + <div class="alert alert-danger margin-bottom-xs"> 20 + <p>{{ index .flashes.errors 0 }}</p> 21 + </div> 22 + <img src="{{ .Image }}" class="totp-image" /> 23 + {{ end }} 24 + <form action="/account/totp-enroll" method="post"> 25 + <input name="code" id="code" placeholder="Code" /> 26 + <button class="primary" type="submit" value="Login">Enroll</button> 27 + </form> 28 + </main> 29 + </body> 30 + </html>