···1+package main
2+3+import (
4+ "context"
5+ "crypto/sha256"
6+ "crypto/subtle"
7+ "errors"
8+ "net/http"
9+ "net/url"
10+ "os"
11+ "strings"
12+ "time"
13+14+ "github.com/puregarlic/space/pages"
15+16+ "github.com/a-h/templ"
17+ "github.com/aidarkhanov/nanoid"
18+ "github.com/golang-jwt/jwt/v5"
19+ "go.hacdias.com/indielib/indieauth"
20+)
21+22+// storeAuthorization stores the authorization request and returns a code for it.
23+// Something such as JWT tokens could be used in a production environment.
24+func (s *server) storeAuthorization(req *indieauth.AuthenticationRequest) string {
25+ code := nanoid.New()
26+27+ s.db.Authorization.Set(code, req, 0)
28+29+ return code
30+}
31+32+type CustomTokenClaims struct {
33+ Scopes []string `json:"scopes"`
34+ jwt.RegisteredClaims
35+}
36+37+type contextKey string
38+39+const (
40+ scopesContextKey contextKey = "scopes"
41+)
42+43+// authorizationGetHandler handles the GET method for the authorization endpoint.
44+func (s *server) authorizationGetHandler(w http.ResponseWriter, r *http.Request) {
45+ // In a production server, this page would usually be protected. In order for
46+ // the user to authorize this request, they must be authenticated. This could
47+ // be done in different ways: username/password, passkeys, etc.
48+49+ // Parse the authorization request.
50+ req, err := s.ias.ParseAuthorization(r)
51+ if err != nil {
52+ serveErrorJSON(w, http.StatusBadRequest, "invalid_request", err.Error())
53+ return
54+ }
55+56+ // Do a best effort attempt at fetching more information about the application
57+ // that we can show to the user. Not all applications provide this sort of
58+ // information.
59+ app, _ := s.ias.DiscoverApplicationMetadata(r.Context(), req.ClientID)
60+61+ // Here, we just display a small HTML document where the user has to press
62+ // to authorize this request. Please note that this template contains a form
63+ // where we dump all the request information. This makes it possible to reuse
64+ // [indieauth.Server.ParseAuthorization] when the user authorizes the request.
65+ templ.Handler(pages.Auth(req, app)).ServeHTTP(w, r)
66+}
67+68+// authorizationPostHandler handles the POST method for the authorization endpoint.
69+func (s *server) authorizationPostHandler(w http.ResponseWriter, r *http.Request) {
70+ s.authorizationCodeExchange(w, r, false)
71+}
72+73+// tokenHandler handles the token endpoint. In our case, we only accept the default
74+// type which is exchanging an authorization code for a token.
75+func (s *server) tokenHandler(w http.ResponseWriter, r *http.Request) {
76+ if r.Method != http.MethodPost {
77+ httpError(w, http.StatusMethodNotAllowed)
78+ return
79+ }
80+81+ if r.Form.Get("grant_type") == "refresh_token" {
82+ // NOTE: this server does not implement refresh tokens.
83+ // https://indieauth.spec.indieweb.org/#refresh-tokens
84+ w.WriteHeader(http.StatusNotImplemented)
85+ return
86+ }
87+88+ s.authorizationCodeExchange(w, r, true)
89+}
90+91+type tokenResponse struct {
92+ Me string `json:"me"`
93+ AccessToken string `json:"access_token,omitempty"`
94+ TokenType string `json:"token_type,omitempty"`
95+ Scope string `json:"scope,omitempty"`
96+ ExpiresIn int64 `json:"expires_in,omitempty"`
97+}
98+99+// authorizationCodeExchange handles the authorization code exchange. It is used by
100+// both the authorization handler to exchange the code for the user's profile URL,
101+// and by the token endpoint, to exchange the code by a token.
102+func (s *server) authorizationCodeExchange(w http.ResponseWriter, r *http.Request, withToken bool) {
103+ if err := r.ParseForm(); err != nil {
104+ serveErrorJSON(w, http.StatusBadRequest, "invalid_request", err.Error())
105+ return
106+ }
107+108+ // t := s.getAuthorization(r.Form.Get("code"))
109+ req, present := s.db.Authorization.GetAndDelete(r.Form.Get("code"))
110+ if !present {
111+ serveErrorJSON(w, http.StatusBadRequest, "invalid_request", "invalid authorization")
112+ return
113+ }
114+ authRequest := req.Value()
115+116+ err := s.ias.ValidateTokenExchange(authRequest, r)
117+ if err != nil {
118+ serveErrorJSON(w, http.StatusBadRequest, "invalid_request", err.Error())
119+ return
120+ }
121+122+ response := &tokenResponse{
123+ Me: s.profileURL,
124+ }
125+126+ scopes := authRequest.Scopes
127+128+ if withToken {
129+ now := time.Now()
130+ expiresAt := now.Add(15 * time.Minute)
131+ claims := CustomTokenClaims{
132+ scopes,
133+ jwt.RegisteredClaims{
134+ ExpiresAt: jwt.NewNumericDate(expiresAt),
135+ IssuedAt: jwt.NewNumericDate(now),
136+ NotBefore: jwt.NewNumericDate(now),
137+ },
138+ }
139+ token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
140+141+ secret := os.Getenv("JWT_SECRET")
142+ jwt, err := token.SignedString([]byte(secret))
143+ if err != nil {
144+ panic(err)
145+ }
146+147+ response.AccessToken = jwt
148+ response.TokenType = "Bearer"
149+ response.ExpiresIn = int64(time.Until(expiresAt).Seconds())
150+ response.Scope = strings.Join(scopes, " ")
151+ }
152+153+ // An actual server may want to include the "profile" in the response if the
154+ // scope "profile" is included.
155+ serveJSON(w, http.StatusOK, response)
156+}
157+158+func (s *server) authorizationAcceptHandler(w http.ResponseWriter, r *http.Request) {
159+ // Parse authorization information. This only works because our authorization page
160+ // includes all the required information. This can be done in other ways: database,
161+ // whether temporary or not, cookies, etc.
162+ req, err := s.ias.ParseAuthorization(r)
163+ if err != nil {
164+ serveErrorJSON(w, http.StatusBadRequest, "invalid_request", err.Error())
165+ return
166+ }
167+168+ // Generate a random code and persist the information associated to that code.
169+ // You could do this in other ways: database, or JWT tokens, or both, for example.
170+ code := s.storeAuthorization(req)
171+172+ // Redirect to client callback.
173+ query := url.Values{}
174+ query.Set("code", code)
175+ query.Set("state", req.State)
176+ http.Redirect(w, r, req.RedirectURI+"?"+query.Encode(), http.StatusFound)
177+}
178+179+// mustAuth is a middleware to ensure that the request is authorized. The way this
180+// works depends on the implementation. It then stores the scopes in the context.
181+func (s *server) mustAuth(next http.Handler) http.Handler {
182+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
183+ tokenStr := r.Header.Get("Authorization")
184+ tokenStr = strings.TrimPrefix(tokenStr, "Bearer")
185+ tokenStr = strings.TrimSpace(tokenStr)
186+187+ if len(tokenStr) <= 0 {
188+ serveErrorJSON(w, http.StatusUnauthorized, "invalid_request", "no credentials")
189+ return
190+ }
191+192+ token, err := jwt.ParseWithClaims(tokenStr, &CustomTokenClaims{}, func(t *jwt.Token) (interface{}, error) {
193+ return []byte(os.Getenv("JWT_SECRET")), nil
194+ })
195+196+ if err != nil {
197+ serveErrorJSON(w, http.StatusUnauthorized, "invalid_request", "invalid token")
198+ return
199+ } else if claims, ok := token.Claims.(*CustomTokenClaims); ok {
200+ ctx := context.WithValue(r.Context(), scopesContextKey, claims.Scopes)
201+ next.ServeHTTP(w, r.WithContext(ctx))
202+ return
203+ } else {
204+ serveErrorJSON(w, http.StatusUnauthorized, "invalid_request", "malformed claims")
205+ return
206+ }
207+ })
208+}
209+210+func (s *server) mustBasicAuth(next http.Handler) http.Handler {
211+ user, ok := os.LookupEnv("ADMIN_USERNAME")
212+ if !ok {
213+ panic(errors.New("ADMIN_USERNAME is not set, cannot start"))
214+ }
215+216+ pass, ok := os.LookupEnv("ADMIN_PASSWORD")
217+ if !ok {
218+ panic(errors.New("ADMIN_PASSWORD is not set, cannot start"))
219+ }
220+221+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
222+ username, password, ok := r.BasicAuth()
223+ if ok {
224+ usernameHash := sha256.Sum256([]byte(username))
225+ passwordHash := sha256.Sum256([]byte(password))
226+ expectedUsernameHash := sha256.Sum256([]byte(user))
227+ expectedPasswordHash := sha256.Sum256([]byte(pass))
228+229+ usernameMatch := (subtle.ConstantTimeCompare(usernameHash[:], expectedUsernameHash[:]) == 1)
230+ passwordMatch := (subtle.ConstantTimeCompare(passwordHash[:], expectedPasswordHash[:]) == 1)
231+232+ if usernameMatch && passwordMatch {
233+ next.ServeHTTP(w, r)
234+ return
235+ }
236+ }
237+238+ w.Header().Set("WWW-Authenticate", `Basic realm="restricted", charset="UTF-8"`)
239+ http.Error(w, "Unauthorized", http.StatusUnauthorized)
240+ })
241+}