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}