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}