this repo has no description
1package oauth
2
3import (
4 "encoding/json"
5 "fmt"
6 "log"
7 "net/http"
8 "net/url"
9 "strings"
10
11 "github.com/go-chi/chi/v5"
12 "github.com/gorilla/sessions"
13 "github.com/haileyok/atproto-oauth-golang/helpers"
14 "github.com/lestrrat-go/jwx/v2/jwk"
15 "tangled.sh/tangled.sh/core/appview"
16 "tangled.sh/tangled.sh/core/appview/db"
17 "tangled.sh/tangled.sh/core/appview/oauth"
18 "tangled.sh/tangled.sh/core/appview/oauth/client"
19 "tangled.sh/tangled.sh/core/appview/pages"
20)
21
22const (
23 oauthScope = "atproto transition:generic"
24)
25
26type OAuthHandler struct {
27 Config *appview.Config
28 Pages *pages.Pages
29 Resolver *appview.Resolver
30 Db *db.DB
31 Store *sessions.CookieStore
32 OAuth *oauth.OAuth
33}
34
35func (o *OAuthHandler) Router() http.Handler {
36 r := chi.NewRouter()
37
38 r.Get("/login", o.login)
39 r.Post("/login", o.login)
40
41 r.Get("/oauth/client-metadata.json", o.clientMetadata)
42 r.Get("/oauth/jwks.json", o.jwks)
43 r.Get("/oauth/callback", o.callback)
44 return r
45}
46
47func (o *OAuthHandler) clientMetadata(w http.ResponseWriter, r *http.Request) {
48 metadata := map[string]any{
49 "client_id": o.Config.OAuth.ServerMetadataUrl,
50 "client_name": "Tangled",
51 "subject_type": "public",
52 "client_uri": o.Config.Core.AppviewHost,
53 "redirect_uris": []string{fmt.Sprintf("%s/oauth/callback", o.Config.Core.AppviewHost)},
54 "grant_types": []string{"authorization_code", "refresh_token"},
55 "response_types": []string{"code"},
56 "application_type": "web",
57 "dpop_bound_access_tokens": true,
58 "jwks_uri": fmt.Sprintf("%s/oauth/jwks.json", o.Config.Core.AppviewHost),
59 "scope": "atproto transition:generic",
60 "token_endpoint_auth_method": "private_key_jwt",
61 "token_endpoint_auth_signing_alg": "ES256",
62 }
63
64 fmt.Println("clientMetadata", metadata)
65
66 w.Header().Set("Content-Type", "application/json")
67 w.WriteHeader(http.StatusOK)
68 json.NewEncoder(w).Encode(metadata)
69}
70
71func (o *OAuthHandler) jwks(w http.ResponseWriter, r *http.Request) {
72 jwks := o.Config.OAuth.Jwks
73 pubKey, err := pubKeyFromJwk(jwks)
74 if err != nil {
75 log.Printf("error parsing public key: %v", err)
76 http.Error(w, err.Error(), http.StatusInternalServerError)
77 return
78 }
79
80 response := helpers.CreateJwksResponseObject(pubKey)
81
82 w.Header().Set("Content-Type", "application/json")
83 w.WriteHeader(http.StatusOK)
84 json.NewEncoder(w).Encode(response)
85}
86
87func (o *OAuthHandler) login(w http.ResponseWriter, r *http.Request) {
88 switch r.Method {
89 case http.MethodGet:
90 o.Pages.Login(w, pages.LoginParams{})
91 case http.MethodPost:
92 handle := strings.TrimPrefix(r.FormValue("handle"), "@")
93
94 resolved, err := o.Resolver.ResolveIdent(r.Context(), handle)
95 if err != nil {
96 log.Println("failed to resolve handle:", err)
97 o.Pages.Notice(w, "login-msg", fmt.Sprintf("\"%s\" is an invalid handle.", handle))
98 return
99 }
100 oauthClient, err := client.NewClient(
101 o.Config.OAuth.ServerMetadataUrl,
102 o.Config.OAuth.Jwks,
103 fmt.Sprintf("%s/oauth/callback", o.Config.Core.AppviewHost))
104
105 if err != nil {
106 log.Println("failed to create oauth client:", err)
107 o.Pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.")
108 return
109 }
110
111 authServer, err := oauthClient.ResolvePdsAuthServer(r.Context(), resolved.PDSEndpoint())
112 if err != nil {
113 log.Println("failed to resolve auth server:", err)
114 o.Pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.")
115 return
116 }
117
118 authMeta, err := oauthClient.FetchAuthServerMetadata(r.Context(), authServer)
119 if err != nil {
120 log.Println("failed to fetch auth server metadata:", err)
121 o.Pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.")
122 return
123 }
124
125 dpopKey, err := helpers.GenerateKey(nil)
126 if err != nil {
127 log.Println("failed to generate dpop key:", err)
128 o.Pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.")
129 return
130 }
131
132 dpopKeyJson, err := json.Marshal(dpopKey)
133 if err != nil {
134 log.Println("failed to marshal dpop key:", err)
135 o.Pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.")
136 return
137 }
138
139 parResp, err := oauthClient.SendParAuthRequest(r.Context(), authServer, authMeta, handle, oauthScope, dpopKey)
140 if err != nil {
141 log.Println("failed to send par auth request:", err)
142 o.Pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.")
143 return
144 }
145
146 err = db.SaveOAuthRequest(o.Db, db.OAuthRequest{
147 Did: resolved.DID.String(),
148 PdsUrl: resolved.PDSEndpoint(),
149 Handle: handle,
150 AuthserverIss: authMeta.Issuer,
151 PkceVerifier: parResp.PkceVerifier,
152 DpopAuthserverNonce: parResp.DpopAuthserverNonce,
153 DpopPrivateJwk: string(dpopKeyJson),
154 State: parResp.State,
155 })
156 if err != nil {
157 log.Println("failed to save oauth request:", err)
158 o.Pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.")
159 return
160 }
161
162 u, _ := url.Parse(authMeta.AuthorizationEndpoint)
163 u.RawQuery = fmt.Sprintf("client_id=%s&request_uri=%s", url.QueryEscape(o.Config.OAuth.ServerMetadataUrl), parResp.RequestUri)
164 o.Pages.HxRedirect(w, u.String())
165 }
166}
167
168func (o *OAuthHandler) callback(w http.ResponseWriter, r *http.Request) {
169 state := r.FormValue("state")
170
171 oauthRequest, err := db.GetOAuthRequestByState(o.Db, state)
172 if err != nil {
173 log.Println("failed to get oauth request:", err)
174 o.Pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.")
175 return
176 }
177
178 defer func() {
179 err := db.DeleteOAuthRequestByState(o.Db, state)
180 if err != nil {
181 log.Println("failed to delete oauth request for state:", state, err)
182 }
183 }()
184
185 code := r.FormValue("code")
186 if code == "" {
187 log.Println("missing code for state: ", state)
188 o.Pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.")
189 return
190 }
191
192 iss := r.FormValue("iss")
193 if iss == "" {
194 log.Println("missing iss for state: ", state)
195 o.Pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.")
196 return
197 }
198
199 oauthClient, err := client.NewClient(
200 o.Config.OAuth.ServerMetadataUrl,
201 o.Config.OAuth.Jwks,
202 fmt.Sprintf("%s/oauth/callback", o.Config.Core.AppviewHost))
203
204 if err != nil {
205 log.Println("failed to create oauth client:", err)
206 o.Pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.")
207 return
208 }
209
210 jwk, err := helpers.ParseJWKFromBytes([]byte(oauthRequest.DpopPrivateJwk))
211 if err != nil {
212 log.Println("failed to parse jwk:", err)
213 o.Pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.")
214 return
215 }
216
217 tokenResp, err := oauthClient.InitialTokenRequest(
218 r.Context(),
219 code,
220 oauthRequest.AuthserverIss,
221 oauthRequest.PkceVerifier,
222 oauthRequest.DpopAuthserverNonce,
223 jwk,
224 )
225 if err != nil {
226 log.Println("failed to get token:", err)
227 o.Pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.")
228 return
229 }
230
231 if tokenResp.Scope != oauthScope {
232 log.Println("scope doesn't match:", tokenResp.Scope)
233 o.Pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.")
234 return
235 }
236
237 err = o.OAuth.SaveSession(w, r, oauthRequest, tokenResp)
238 if err != nil {
239 log.Println("failed to save session:", err)
240 o.Pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.")
241 return
242 }
243
244 log.Println("session saved successfully")
245
246 http.Redirect(w, r, "/", http.StatusFound)
247}
248
249func pubKeyFromJwk(jwks string) (jwk.Key, error) {
250 k, err := helpers.ParseJWKFromBytes([]byte(jwks))
251 if err != nil {
252 return nil, err
253 }
254 pubKey, err := k.PublicKey()
255 if err != nil {
256 return nil, err
257 }
258 return pubKey, nil
259}