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-oauth-golang-tester",
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 oauth golang tester server")
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 Oauth Golang Tester",
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}