package main import ( "context" "fmt" "html/template" "io" "log/slog" "net/http" "os" "github.com/gorilla/sessions" oauth "github.com/haileyok/atproto-oauth-golang" oauth_helpers "github.com/haileyok/atproto-oauth-golang/helpers" _ "github.com/joho/godotenv/autoload" "github.com/labstack/echo-contrib/session" "github.com/labstack/echo/v4" slogecho "github.com/samber/slog-echo" "github.com/urfave/cli/v2" "gorm.io/driver/sqlite" "gorm.io/gorm" ) var ( ctx = context.Background() serverMetadataPath = "/oauth/client-metadata.json" serverCallbackPath = "/callback" scope = "atproto transition:generic" ) func main() { app := &cli.App{ Name: "atproto-goauth-demo-webserver", Action: run, Flags: []cli.Flag{ &cli.StringFlag{ Name: "addr", Value: ":8080", EnvVars: []string{"OAUTH_TEST_SERVER_ADDR"}, }, &cli.StringFlag{ Name: "url-root", Required: true, EnvVars: []string{"OAUTH_TEST_SERVER_URL_ROOT"}, }, &cli.StringFlag{ Name: "static-file-path", Value: "./cmd/web_server_demo/html", EnvVars: []string{"OAUTH_TEST_SERVER_STATIC_PATH"}, }, &cli.StringFlag{ Name: "session-secret", Value: "session-secret", EnvVars: []string{"OAUTH_TEST_SERVER_SESSION_SECRET"}, }, }, } app.Run(os.Args) } type TestServer struct { httpd *http.Server e *echo.Echo db *gorm.DB oauthClient *oauth.Client xrpcCli *oauth.XrpcClient jwksResponse *oauth_helpers.JwksResponseObject args ServerArgs } type TemplateRenderer struct { templates *template.Template } func (t *TemplateRenderer) Render(w io.Writer, name string, data any, c echo.Context) error { if viewContext, isMap := data.(map[string]any); isMap { viewContext["reverse"] = c.Echo().Reverse } return t.templates.ExecuteTemplate(w, name, data) } func run(cmd *cli.Context) error { s, err := NewServer(ServerArgs{ Addr: cmd.String("addr"), UrlRoot: cmd.String("url-root"), StaticFilePath: cmd.String("static-file-path"), SessionSecret: cmd.String("session-secret"), }) if err != nil { panic(err) } s.run() return nil } type ServerArgs struct { Addr string UrlRoot string StaticFilePath string SessionSecret string } func NewServer(args ServerArgs) (*TestServer, error) { e := echo.New() e.Use(slogecho.New(slog.Default())) e.Use(session.Middleware(sessions.NewCookieStore([]byte(args.SessionSecret)))) fmt.Println("atproto goauth demo webserver") b, err := os.ReadFile("./jwks.json") if err != nil { if os.IsNotExist(err) { return nil, fmt.Errorf( "could not find jwks.json. does it exist? hint: run `go run ./cmd/cmd generate-jwks --prefix demo` to create one.", ) } return nil, err } k, err := oauth_helpers.ParseJWKFromBytes(b) if err != nil { return nil, err } pubKey, err := k.PublicKey() if err != nil { return nil, err } c, err := oauth.NewClient(oauth.ClientArgs{ ClientJwk: k, ClientId: args.UrlRoot + serverMetadataPath, RedirectUri: args.UrlRoot + serverCallbackPath, }) if err != nil { return nil, err } httpd := &http.Server{ Addr: args.Addr, Handler: e, } db, err := gorm.Open(sqlite.Open("oauth.db"), &gorm.Config{}) if err != nil { return nil, err } db.AutoMigrate(&OauthRequest{}, &OauthSession{}) xrpcCli := &oauth.XrpcClient{ OnDpopPdsNonceChanged: func(did, newNonce string) { if err := db.Exec("UPDATE oauth_sessions SET dpop_pds_nonce = ? WHERE did = ?", newNonce, did).Error; err != nil { slog.Default().Error("error updating pds nonce", "err", err) } }, } s := &TestServer{ httpd: httpd, e: e, db: db, oauthClient: c, xrpcCli: xrpcCli, jwksResponse: oauth_helpers.CreateJwksResponseObject(pubKey), args: args, } renderer := &TemplateRenderer{ templates: template.Must(template.ParseGlob(s.getFilePath("*.html"))), } e.Renderer = renderer return s, nil } func (s *TestServer) run() error { s.e.GET("/", s.handleHome) s.e.File("/login", s.getFilePath("login.html")) s.e.POST("/login", s.handleLoginSubmit) s.e.GET("/logout", s.handleLogout) s.e.GET("/profile", s.handleProfile) s.e.GET("/make-post", s.handleMakePost) s.e.GET("/callback", s.handleCallback) s.e.GET("/oauth/client-metadata.json", s.handleClientMetadata) s.e.GET("/oauth/jwks.json", s.handleJwks) slog.Default().Info("starting http server", "addr", s.args.Addr) if err := s.httpd.ListenAndServe(); err != nil { return err } return nil } func (s *TestServer) handleHome(e echo.Context) error { sess, err := session.Get("session", e) if err != nil { return err } return e.Render(200, "index.html", map[string]any{ "Did": sess.Values["did"], }) } func (s *TestServer) handleClientMetadata(e echo.Context) error { metadata := map[string]any{ "client_id": s.args.UrlRoot + serverMetadataPath, "client_name": "Atproto GoAuth Demo Webserver", "client_uri": s.args.UrlRoot, "logo_uri": fmt.Sprintf("%s/logo.png", s.args.UrlRoot), "tos_uri": fmt.Sprintf("%s/tos", s.args.UrlRoot), "policy_url": fmt.Sprintf("%s/policy", s.args.UrlRoot), "redirect_uris": []string{s.args.UrlRoot + serverCallbackPath}, "grant_types": []string{"authorization_code", "refresh_token"}, "response_types": []string{"code"}, "application_type": "web", "dpop_bound_access_tokens": true, "jwks_uri": fmt.Sprintf("%s/oauth/jwks.json", s.args.UrlRoot), "scope": "atproto transition:generic", "token_endpoint_auth_method": "private_key_jwt", "token_endpoint_auth_signing_alg": "ES256", } return e.JSON(200, metadata) } func (s *TestServer) handleJwks(e echo.Context) error { return e.JSON(200, s.jwksResponse) } func (s *TestServer) getFilePath(file string) string { return fmt.Sprintf("%s/%s", s.args.StaticFilePath, file) }