pw_reset.go 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310
  1. package user
  2. import (
  3. "fmt"
  4. "net/http"
  5. "net/url"
  6. "time"
  7. "github.com/porter-dev/porter/api/server/handlers"
  8. "github.com/porter-dev/porter/api/server/shared"
  9. "github.com/porter-dev/porter/api/server/shared/apierrors"
  10. "github.com/porter-dev/porter/api/server/shared/config"
  11. "github.com/porter-dev/porter/api/types"
  12. "github.com/porter-dev/porter/internal/models"
  13. "github.com/porter-dev/porter/internal/notifier"
  14. "github.com/porter-dev/porter/internal/random"
  15. "github.com/porter-dev/porter/internal/repository"
  16. "golang.org/x/crypto/bcrypt"
  17. "gorm.io/gorm"
  18. )
  19. type UserPasswordInitiateResetHandler struct {
  20. handlers.PorterHandlerReader
  21. }
  22. func NewUserPasswordInitiateResetHandler(
  23. config *config.Config,
  24. decoderValidator shared.RequestDecoderValidator,
  25. writer shared.ResultWriter,
  26. ) *UserPasswordInitiateResetHandler {
  27. return &UserPasswordInitiateResetHandler{
  28. PorterHandlerReader: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
  29. }
  30. }
  31. func (c *UserPasswordInitiateResetHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  32. request := &types.InitiateResetUserPasswordRequest{}
  33. ok := c.DecodeAndValidate(w, r, request)
  34. if !ok {
  35. return
  36. }
  37. // check that the email exists; return 200 status code even if it doesn't
  38. user, err := c.Repo().User().ReadUserByEmail(request.Email)
  39. if err == gorm.ErrRecordNotFound {
  40. w.WriteHeader(http.StatusOK)
  41. return
  42. } else if err != nil {
  43. c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
  44. return
  45. }
  46. // if the user is a Github user, send them a Github email
  47. if user.GithubUserID != 0 {
  48. err := c.Config().UserNotifier.SendGithubRelinkEmail(
  49. &notifier.SendGithubRelinkEmailOpts{
  50. Email: user.Email,
  51. URL: fmt.Sprintf("%s/api/oauth/login/github", c.Config().ServerConf.ServerURL),
  52. },
  53. )
  54. if err != nil {
  55. c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
  56. return
  57. }
  58. w.WriteHeader(http.StatusOK)
  59. return
  60. }
  61. pwReset, rawToken, err := CreatePWResetTokenForEmail(
  62. c.Repo().PWResetToken(),
  63. c.HandleAPIError,
  64. w,
  65. r,
  66. request,
  67. )
  68. if err != nil {
  69. return
  70. }
  71. queryVals := url.Values{
  72. "token": []string{rawToken},
  73. "email": []string{request.Email},
  74. "token_id": []string{fmt.Sprintf("%d", pwReset.ID)},
  75. }
  76. err = c.Config().UserNotifier.SendPasswordResetEmail(
  77. &notifier.SendPasswordResetEmailOpts{
  78. Email: user.Email,
  79. URL: fmt.Sprintf("%s/password/reset/finalize?%s", c.Config().ServerConf.ServerURL, queryVals.Encode()),
  80. },
  81. )
  82. if err != nil {
  83. c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
  84. return
  85. }
  86. w.WriteHeader(http.StatusOK)
  87. return
  88. }
  89. type UserPasswordVerifyResetHandler struct {
  90. handlers.PorterHandlerReader
  91. }
  92. func NewUserPasswordVerifyResetHandler(
  93. config *config.Config,
  94. decoderValidator shared.RequestDecoderValidator,
  95. writer shared.ResultWriter,
  96. ) *UserPasswordVerifyResetHandler {
  97. return &UserPasswordVerifyResetHandler{
  98. PorterHandlerReader: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
  99. }
  100. }
  101. func (c *UserPasswordVerifyResetHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  102. request := &types.VerifyResetUserPasswordRequest{}
  103. ok := c.DecodeAndValidate(w, r, request)
  104. if !ok {
  105. return
  106. }
  107. VerifyToken(
  108. c.Repo().PWResetToken(),
  109. c.HandleAPIError,
  110. w,
  111. r,
  112. &request.VerifyTokenFinalizeRequest,
  113. request.Email,
  114. )
  115. }
  116. type UserPasswordFinalizeResetHandler struct {
  117. handlers.PorterHandlerReader
  118. }
  119. func NewUserPasswordFinalizeResetHandler(
  120. config *config.Config,
  121. decoderValidator shared.RequestDecoderValidator,
  122. writer shared.ResultWriter,
  123. ) *UserPasswordFinalizeResetHandler {
  124. return &UserPasswordFinalizeResetHandler{
  125. PorterHandlerReader: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
  126. }
  127. }
  128. func (c *UserPasswordFinalizeResetHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  129. request := &types.FinalizeResetUserPasswordRequest{}
  130. ok := c.DecodeAndValidate(w, r, request)
  131. if !ok {
  132. return
  133. }
  134. token, err := VerifyToken(
  135. c.Repo().PWResetToken(),
  136. c.HandleAPIError,
  137. w,
  138. r,
  139. &request.VerifyTokenFinalizeRequest,
  140. request.Email,
  141. )
  142. if err != nil {
  143. return
  144. }
  145. // check that the email exists
  146. user, err := c.Repo().User().ReadUserByEmail(request.Email)
  147. if err != nil {
  148. if err == gorm.ErrRecordNotFound {
  149. err = fmt.Errorf("finalize password reset failed: email does not exist")
  150. c.HandleAPIError(w, r, apierrors.NewErrForbidden(err))
  151. } else {
  152. c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
  153. }
  154. return
  155. }
  156. hashedPW, err := bcrypt.GenerateFromPassword([]byte(request.NewPassword), 8)
  157. if err != nil {
  158. c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
  159. return
  160. }
  161. user.Password = string(hashedPW)
  162. user, err = c.Repo().User().UpdateUser(user)
  163. if err != nil {
  164. c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
  165. return
  166. }
  167. // invalidate the token
  168. token.IsValid = false
  169. _, err = c.Repo().PWResetToken().UpdatePWResetToken(token)
  170. if err != nil {
  171. c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
  172. return
  173. }
  174. w.WriteHeader(http.StatusOK)
  175. return
  176. }
  177. func VerifyToken(
  178. pwResetRepo repository.PWResetTokenRepository,
  179. handleErr func(w http.ResponseWriter, r *http.Request, err apierrors.RequestError),
  180. w http.ResponseWriter,
  181. r *http.Request,
  182. request *types.VerifyTokenFinalizeRequest,
  183. email string,
  184. ) (*models.PWResetToken, error) {
  185. token, err := pwResetRepo.ReadPWResetToken(request.TokenID)
  186. if err != nil {
  187. if err == gorm.ErrRecordNotFound {
  188. err = fmt.Errorf("verify token failed: token does not exist")
  189. handleErr(w, r, apierrors.NewErrForbidden(err))
  190. return nil, err
  191. } else {
  192. handleErr(w, r, apierrors.NewErrInternal(err))
  193. }
  194. return nil, err
  195. }
  196. // make sure the token is still valid and has not expired
  197. if !token.IsValid || token.IsExpired() {
  198. err = fmt.Errorf("verify token failed: expired %t, valid %t", token.IsExpired(), token.IsValid)
  199. handleErr(w, r, apierrors.NewErrForbidden(err))
  200. return nil, err
  201. }
  202. // check that the email matches
  203. if token.Email != email {
  204. err = fmt.Errorf("verify token failed: token email does not match request email")
  205. handleErr(w, r, apierrors.NewErrForbidden(err))
  206. return nil, err
  207. }
  208. // make sure the token is correct
  209. if err := bcrypt.CompareHashAndPassword([]byte(token.Token), []byte(request.Token)); err != nil {
  210. err = fmt.Errorf("verify token failed: %s", err)
  211. handleErr(w, r, apierrors.NewErrForbidden(err))
  212. return nil, err
  213. }
  214. return token, nil
  215. }
  216. func CreatePWResetTokenForEmail(
  217. pwResetRepo repository.PWResetTokenRepository,
  218. handleErr func(w http.ResponseWriter, r *http.Request, err apierrors.RequestError),
  219. w http.ResponseWriter,
  220. r *http.Request,
  221. request *types.InitiateResetUserPasswordRequest,
  222. ) (*models.PWResetToken, string, error) {
  223. // convert the form to a project model
  224. expiry := time.Now().Add(30 * time.Minute)
  225. rawToken, err := random.StringWithCharset(32, "")
  226. if err != nil {
  227. handleErr(w, r, apierrors.NewErrInternal(err))
  228. return nil, "", err
  229. }
  230. hashedToken, err := bcrypt.GenerateFromPassword([]byte(rawToken), 8)
  231. if err != nil {
  232. handleErr(w, r, apierrors.NewErrInternal(err))
  233. return nil, "", err
  234. }
  235. pwReset := &models.PWResetToken{
  236. Email: request.Email,
  237. IsValid: true,
  238. Expiry: &expiry,
  239. Token: string(hashedToken),
  240. }
  241. // handle write to the database
  242. pwReset, err = pwResetRepo.CreatePWResetToken(pwReset)
  243. if err != nil {
  244. handleErr(w, r, apierrors.NewErrInternal(err))
  245. return nil, "", err
  246. }
  247. return pwReset, rawToken, nil
  248. }