this repo has no description
1package main 2 3import ( 4 "context" 5 "fmt" 6 "html/template" 7 "io" 8 "log/slog" 9 "net/http" 10 "os" 11 12 "github.com/gorilla/sessions" 13 oauth "github.com/haileyok/atproto-oauth-golang" 14 _ "github.com/joho/godotenv/autoload" 15 "github.com/labstack/echo-contrib/session" 16 "github.com/labstack/echo/v4" 17 slogecho "github.com/samber/slog-echo" 18 "github.com/urfave/cli/v2" 19 "gorm.io/driver/sqlite" 20 "gorm.io/gorm" 21) 22 23var ( 24 ctx = context.Background() 25 serverAddr = os.Getenv("OAUTH_TEST_SERVER_ADDR") 26 serverUrlRoot = os.Getenv("OAUTH_TEST_SERVER_URL_ROOT") 27 staticFilePath = os.Getenv("OAUTH_TEST_SERVER_STATIC_PATH") 28 sessionSecret = os.Getenv("OAUTH_TEST_SESSION_SECRET") 29 serverMetadataUrl = fmt.Sprintf("%s/oauth/client-metadata.json", serverUrlRoot) 30 serverCallbackUrl = fmt.Sprintf("%s/callback", serverUrlRoot) 31 pdsUrl = os.Getenv("OAUTH_TEST_PDS_URL") 32 scope = "atproto transition:generic" 33) 34 35func main() { 36 app := &cli.App{ 37 Name: "atproto-goauth-demo-webserver", 38 Action: run, 39 } 40 41 if serverUrlRoot == "" { 42 panic(fmt.Errorf("no server url root set in env file")) 43 } 44 45 app.RunAndExitOnError() 46} 47 48type TestServer struct { 49 httpd *http.Server 50 e *echo.Echo 51 db *gorm.DB 52 oauthClient *oauth.Client 53 xrpcCli *oauth.XrpcClient 54 jwksResponse *oauth.JwksResponseObject 55} 56 57type TemplateRenderer struct { 58 templates *template.Template 59} 60 61func (t *TemplateRenderer) Render(w io.Writer, name string, data interface{}, c echo.Context) error { 62 if viewContext, isMap := data.(map[string]interface{}); isMap { 63 viewContext["reverse"] = c.Echo().Reverse 64 } 65 66 return t.templates.ExecuteTemplate(w, name, data) 67} 68 69func run(cmd *cli.Context) error { 70 s, err := NewServer() 71 if err != nil { 72 panic(err) 73 } 74 75 s.run() 76 77 return nil 78} 79 80func NewServer() (*TestServer, error) { 81 e := echo.New() 82 83 e.Use(slogecho.New(slog.Default())) 84 e.Use(session.Middleware(sessions.NewCookieStore([]byte(sessionSecret)))) 85 86 renderer := &TemplateRenderer{ 87 templates: template.Must(template.ParseGlob(getFilePath("*.html"))), 88 } 89 e.Renderer = renderer 90 91 fmt.Println("atproto goauth demo webserver") 92 93 b, err := os.ReadFile("./jwks.json") 94 if err != nil { 95 if os.IsNotExist(err) { 96 return nil, fmt.Errorf( 97 "could not find jwks.json. does it exist? hint: run `go run ./cmd/cmd generate-jwks --prefix demo` to create one.", 98 ) 99 } 100 return nil, err 101 } 102 103 k, err := oauth.ParseJWKFromBytes(b) 104 if err != nil { 105 return nil, err 106 } 107 108 pubKey, err := k.PublicKey() 109 if err != nil { 110 return nil, err 111 } 112 113 c, err := oauth.NewClient(oauth.ClientArgs{ 114 ClientJwk: k, 115 ClientId: serverMetadataUrl, 116 RedirectUri: serverCallbackUrl, 117 }) 118 if err != nil { 119 return nil, err 120 } 121 122 httpd := &http.Server{ 123 Addr: serverAddr, 124 Handler: e, 125 } 126 127 db, err := gorm.Open(sqlite.Open("oauth.db"), &gorm.Config{}) 128 if err != nil { 129 return nil, err 130 } 131 132 db.AutoMigrate(&OauthRequest{}, &OauthSession{}) 133 134 xrpcCli := &oauth.XrpcClient{ 135 OnDpopPdsNonceChanged: func(did, newNonce string) { 136 if err := db.Exec("UPDATE oauth_sessions SET dpop_pds_nonce = ? WHERE did = ?", newNonce, did).Error; err != nil { 137 slog.Default().Error("error updating pds nonce", "err", err) 138 } 139 }, 140 } 141 142 return &TestServer{ 143 httpd: httpd, 144 e: e, 145 db: db, 146 oauthClient: c, 147 xrpcCli: xrpcCli, 148 jwksResponse: oauth.CreateJwksResponseObject(pubKey), 149 }, nil 150} 151 152func (s *TestServer) run() error { 153 s.e.GET("/", s.handleHome) 154 s.e.File("/login", getFilePath("login.html")) 155 s.e.POST("/login", s.handleLoginSubmit) 156 s.e.GET("/logout", s.handleLogout) 157 s.e.GET("/profile", s.handleProfile) 158 s.e.GET("/make-post", s.handleMakePost) 159 s.e.GET("/callback", s.handleCallback) 160 s.e.GET("/oauth/client-metadata.json", s.handleClientMetadata) 161 s.e.GET("/oauth/jwks.json", s.handleJwks) 162 163 if err := s.httpd.ListenAndServe(); err != nil { 164 return err 165 } 166 167 return nil 168} 169 170func (s *TestServer) handleHome(e echo.Context) error { 171 sess, err := session.Get("session", e) 172 if err != nil { 173 return err 174 } 175 176 return e.Render(200, "index.html", map[string]any{ 177 "Did": sess.Values["did"], 178 }) 179} 180 181func (s *TestServer) handleClientMetadata(e echo.Context) error { 182 metadata := map[string]any{ 183 "client_id": serverMetadataUrl, 184 "client_name": "Atproto GoAuth Demo Webserver", 185 "client_uri": serverUrlRoot, 186 "logo_uri": fmt.Sprintf("%s/logo.png", serverUrlRoot), 187 "tos_uri": fmt.Sprintf("%s/tos", serverUrlRoot), 188 "policy_url": fmt.Sprintf("%s/policy", serverUrlRoot), 189 "redirect_uris": []string{serverCallbackUrl}, 190 "grant_types": []string{"authorization_code", "refresh_token"}, 191 "response_types": []string{"code"}, 192 "application_type": "web", 193 "dpop_bound_access_tokens": true, 194 "jwks_uri": fmt.Sprintf("%s/oauth/jwks.json", serverUrlRoot), 195 "scope": "atproto transition:generic", 196 "token_endpoint_auth_method": "private_key_jwt", 197 "token_endpoint_auth_signing_alg": "ES256", 198 } 199 200 return e.JSON(200, metadata) 201} 202 203func (s *TestServer) handleJwks(e echo.Context) error { 204 return e.JSON(200, s.jwksResponse) 205} 206 207func getFilePath(file string) string { 208 return fmt.Sprintf("%s/%s", staticFilePath, file) 209}