package clientapi import ( "errors" "net/http" "strings" "github.com/dariuszjaniak/warp/server/internal/httputil" "github.com/dariuszjaniak/warp/server/internal/userapi" ) // AuthedHandler is a handler that needs an authenticated user. The identity // is passed in rather than hidden in the request context, so a handler // cannot forget to check it and the compiler says which endpoints are // protected. type AuthedHandler func(w http.ResponseWriter, r *http.Request, auth userapi.Authenticated) error // withAuth resolves the access token before calling h. func (c *ClientAPI) withAuth(h AuthedHandler) httputil.Handler { return func(w http.ResponseWriter, r *http.Request) error { auth, err := c.authenticate(r) if err != nil { return err } // An application service has no device to record a visit on. if auth.AppService == "" { c.users.RecordActivity(r.Context(), auth, c.clientIP(r), r.UserAgent()) } return h(w, r, auth) } } // authenticate turns a request's access token into an identity. func (c *ClientAPI) authenticate(r *http.Request) (userapi.Authenticated, error) { token := accessTokenFrom(r) if token == "" { return userapi.Authenticated{}, httputil.MissingToken() } if svc := c.appservices.Registry().ByToken(token); svc != nil { return c.authenticateAppService(r, svc) } auth, err := c.users.Authenticate(r.Context(), token) switch { case err == nil: return auth, nil case errors.Is(err, userapi.ErrNoSuchToken): return userapi.Authenticated{}, httputil.UnknownToken(false) case errors.Is(err, userapi.ErrTokenExpired): // A soft logout keeps the client's encryption state: the token // aged out, the device is still real, and logging in again // restores the session rather than replacing it. return userapi.Authenticated{}, httputil.UnknownToken(true) case errors.Is(err, userapi.ErrAccountDeactivated): return userapi.Authenticated{}, httputil.UserDeactivated() default: return userapi.Authenticated{}, err } } // accessTokenFrom extracts the access token from a request. // // The Authorization header is the only form the current spec defines, but // the ?access_token= query parameter is still what media URLs embedded in // web clients and a long tail of older clients use, so both are accepted. func accessTokenFrom(r *http.Request) string { if header := r.Header.Get("Authorization"); header != "" { if token, ok := bearerToken(header); ok { return token } // A malformed Authorization header is not a reason to fall through // to the query string: that would let a broken header hide a // working token and make the failure hard to see. return "" } return r.URL.Query().Get("access_token") } // bearerToken parses "Bearer ", case-insensitively on the scheme. func bearerToken(header string) (string, bool) { scheme, token, found := strings.Cut(header, " ") if !found || !strings.EqualFold(scheme, "Bearer") { return "", false } token = strings.TrimSpace(token) return token, token != "" } // requireSelf checks that the user ID in a path belongs to the requester. // // Several endpoints are addressed by user ID even though they only ever // operate on the caller's own data. The spec's answer for someone else's is // M_FORBIDDEN; admins are allowed through, which is what makes the admin // panel able to fix up a user's profile. func (c *ClientAPI) requireSelf(auth userapi.Authenticated, pathUserID string) (string, error) { localpart, err := c.users.ParseLocalUserID(pathUserID) switch { case errors.Is(err, userapi.ErrForeignServer): return "", httputil.Forbidden("Cannot change data belonging to another server") case err != nil: return "", httputil.InvalidParam("%s is not a valid user ID", pathUserID) } if localpart != auth.Localpart && !auth.Admin { return "", httputil.Forbidden("Cannot change the data of another user") } return localpart, nil }