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