A fork of the Cocoon PDS but being made more distributed.
11

Configure Feed

Select the types of activity you want to include in your feed.

totp

Hailey (Jul 4, 2025, 12:53 AM -0700) 4a6c9425 be005ba5

+152
+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>