···11+package main
22+33+import (
44+ "context"
55+ "crypto/sha256"
66+ "crypto/subtle"
77+ "errors"
88+ "net/http"
99+ "net/url"
1010+ "os"
1111+ "strings"
1212+ "time"
1313+1414+ "github.com/puregarlic/space/pages"
1515+1616+ "github.com/a-h/templ"
1717+ "github.com/aidarkhanov/nanoid"
1818+ "github.com/golang-jwt/jwt/v5"
1919+ "go.hacdias.com/indielib/indieauth"
2020+)
2121+2222+// storeAuthorization stores the authorization request and returns a code for it.
2323+// Something such as JWT tokens could be used in a production environment.
2424+func (s *server) storeAuthorization(req *indieauth.AuthenticationRequest) string {
2525+ code := nanoid.New()
2626+2727+ s.db.Authorization.Set(code, req, 0)
2828+2929+ return code
3030+}
3131+3232+type CustomTokenClaims struct {
3333+ Scopes []string `json:"scopes"`
3434+ jwt.RegisteredClaims
3535+}
3636+3737+type contextKey string
3838+3939+const (
4040+ scopesContextKey contextKey = "scopes"
4141+)
4242+4343+// authorizationGetHandler handles the GET method for the authorization endpoint.
4444+func (s *server) authorizationGetHandler(w http.ResponseWriter, r *http.Request) {
4545+ // In a production server, this page would usually be protected. In order for
4646+ // the user to authorize this request, they must be authenticated. This could
4747+ // be done in different ways: username/password, passkeys, etc.
4848+4949+ // Parse the authorization request.
5050+ req, err := s.ias.ParseAuthorization(r)
5151+ if err != nil {
5252+ serveErrorJSON(w, http.StatusBadRequest, "invalid_request", err.Error())
5353+ return
5454+ }
5555+5656+ // Do a best effort attempt at fetching more information about the application
5757+ // that we can show to the user. Not all applications provide this sort of
5858+ // information.
5959+ app, _ := s.ias.DiscoverApplicationMetadata(r.Context(), req.ClientID)
6060+6161+ // Here, we just display a small HTML document where the user has to press
6262+ // to authorize this request. Please note that this template contains a form
6363+ // where we dump all the request information. This makes it possible to reuse
6464+ // [indieauth.Server.ParseAuthorization] when the user authorizes the request.
6565+ templ.Handler(pages.Auth(req, app)).ServeHTTP(w, r)
6666+}
6767+6868+// authorizationPostHandler handles the POST method for the authorization endpoint.
6969+func (s *server) authorizationPostHandler(w http.ResponseWriter, r *http.Request) {
7070+ s.authorizationCodeExchange(w, r, false)
7171+}
7272+7373+// tokenHandler handles the token endpoint. In our case, we only accept the default
7474+// type which is exchanging an authorization code for a token.
7575+func (s *server) tokenHandler(w http.ResponseWriter, r *http.Request) {
7676+ if r.Method != http.MethodPost {
7777+ httpError(w, http.StatusMethodNotAllowed)
7878+ return
7979+ }
8080+8181+ if r.Form.Get("grant_type") == "refresh_token" {
8282+ // NOTE: this server does not implement refresh tokens.
8383+ // https://indieauth.spec.indieweb.org/#refresh-tokens
8484+ w.WriteHeader(http.StatusNotImplemented)
8585+ return
8686+ }
8787+8888+ s.authorizationCodeExchange(w, r, true)
8989+}
9090+9191+type tokenResponse struct {
9292+ Me string `json:"me"`
9393+ AccessToken string `json:"access_token,omitempty"`
9494+ TokenType string `json:"token_type,omitempty"`
9595+ Scope string `json:"scope,omitempty"`
9696+ ExpiresIn int64 `json:"expires_in,omitempty"`
9797+}
9898+9999+// authorizationCodeExchange handles the authorization code exchange. It is used by
100100+// both the authorization handler to exchange the code for the user's profile URL,
101101+// and by the token endpoint, to exchange the code by a token.
102102+func (s *server) authorizationCodeExchange(w http.ResponseWriter, r *http.Request, withToken bool) {
103103+ if err := r.ParseForm(); err != nil {
104104+ serveErrorJSON(w, http.StatusBadRequest, "invalid_request", err.Error())
105105+ return
106106+ }
107107+108108+ // t := s.getAuthorization(r.Form.Get("code"))
109109+ req, present := s.db.Authorization.GetAndDelete(r.Form.Get("code"))
110110+ if !present {
111111+ serveErrorJSON(w, http.StatusBadRequest, "invalid_request", "invalid authorization")
112112+ return
113113+ }
114114+ authRequest := req.Value()
115115+116116+ err := s.ias.ValidateTokenExchange(authRequest, r)
117117+ if err != nil {
118118+ serveErrorJSON(w, http.StatusBadRequest, "invalid_request", err.Error())
119119+ return
120120+ }
121121+122122+ response := &tokenResponse{
123123+ Me: s.profileURL,
124124+ }
125125+126126+ scopes := authRequest.Scopes
127127+128128+ if withToken {
129129+ now := time.Now()
130130+ expiresAt := now.Add(15 * time.Minute)
131131+ claims := CustomTokenClaims{
132132+ scopes,
133133+ jwt.RegisteredClaims{
134134+ ExpiresAt: jwt.NewNumericDate(expiresAt),
135135+ IssuedAt: jwt.NewNumericDate(now),
136136+ NotBefore: jwt.NewNumericDate(now),
137137+ },
138138+ }
139139+ token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
140140+141141+ secret := os.Getenv("JWT_SECRET")
142142+ jwt, err := token.SignedString([]byte(secret))
143143+ if err != nil {
144144+ panic(err)
145145+ }
146146+147147+ response.AccessToken = jwt
148148+ response.TokenType = "Bearer"
149149+ response.ExpiresIn = int64(time.Until(expiresAt).Seconds())
150150+ response.Scope = strings.Join(scopes, " ")
151151+ }
152152+153153+ // An actual server may want to include the "profile" in the response if the
154154+ // scope "profile" is included.
155155+ serveJSON(w, http.StatusOK, response)
156156+}
157157+158158+func (s *server) authorizationAcceptHandler(w http.ResponseWriter, r *http.Request) {
159159+ // Parse authorization information. This only works because our authorization page
160160+ // includes all the required information. This can be done in other ways: database,
161161+ // whether temporary or not, cookies, etc.
162162+ req, err := s.ias.ParseAuthorization(r)
163163+ if err != nil {
164164+ serveErrorJSON(w, http.StatusBadRequest, "invalid_request", err.Error())
165165+ return
166166+ }
167167+168168+ // Generate a random code and persist the information associated to that code.
169169+ // You could do this in other ways: database, or JWT tokens, or both, for example.
170170+ code := s.storeAuthorization(req)
171171+172172+ // Redirect to client callback.
173173+ query := url.Values{}
174174+ query.Set("code", code)
175175+ query.Set("state", req.State)
176176+ http.Redirect(w, r, req.RedirectURI+"?"+query.Encode(), http.StatusFound)
177177+}
178178+179179+// mustAuth is a middleware to ensure that the request is authorized. The way this
180180+// works depends on the implementation. It then stores the scopes in the context.
181181+func (s *server) mustAuth(next http.Handler) http.Handler {
182182+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
183183+ tokenStr := r.Header.Get("Authorization")
184184+ tokenStr = strings.TrimPrefix(tokenStr, "Bearer")
185185+ tokenStr = strings.TrimSpace(tokenStr)
186186+187187+ if len(tokenStr) <= 0 {
188188+ serveErrorJSON(w, http.StatusUnauthorized, "invalid_request", "no credentials")
189189+ return
190190+ }
191191+192192+ token, err := jwt.ParseWithClaims(tokenStr, &CustomTokenClaims{}, func(t *jwt.Token) (interface{}, error) {
193193+ return []byte(os.Getenv("JWT_SECRET")), nil
194194+ })
195195+196196+ if err != nil {
197197+ serveErrorJSON(w, http.StatusUnauthorized, "invalid_request", "invalid token")
198198+ return
199199+ } else if claims, ok := token.Claims.(*CustomTokenClaims); ok {
200200+ ctx := context.WithValue(r.Context(), scopesContextKey, claims.Scopes)
201201+ next.ServeHTTP(w, r.WithContext(ctx))
202202+ return
203203+ } else {
204204+ serveErrorJSON(w, http.StatusUnauthorized, "invalid_request", "malformed claims")
205205+ return
206206+ }
207207+ })
208208+}
209209+210210+func (s *server) mustBasicAuth(next http.Handler) http.Handler {
211211+ user, ok := os.LookupEnv("ADMIN_USERNAME")
212212+ if !ok {
213213+ panic(errors.New("ADMIN_USERNAME is not set, cannot start"))
214214+ }
215215+216216+ pass, ok := os.LookupEnv("ADMIN_PASSWORD")
217217+ if !ok {
218218+ panic(errors.New("ADMIN_PASSWORD is not set, cannot start"))
219219+ }
220220+221221+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
222222+ username, password, ok := r.BasicAuth()
223223+ if ok {
224224+ usernameHash := sha256.Sum256([]byte(username))
225225+ passwordHash := sha256.Sum256([]byte(password))
226226+ expectedUsernameHash := sha256.Sum256([]byte(user))
227227+ expectedPasswordHash := sha256.Sum256([]byte(pass))
228228+229229+ usernameMatch := (subtle.ConstantTimeCompare(usernameHash[:], expectedUsernameHash[:]) == 1)
230230+ passwordMatch := (subtle.ConstantTimeCompare(passwordHash[:], expectedPasswordHash[:]) == 1)
231231+232232+ if usernameMatch && passwordMatch {
233233+ next.ServeHTTP(w, r)
234234+ return
235235+ }
236236+ }
237237+238238+ w.Header().Set("WWW-Authenticate", `Basic realm="restricted", charset="UTF-8"`)
239239+ http.Error(w, "Unauthorized", http.StatusUnauthorized)
240240+ })
241241+}