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