A clean, Markdown-based publishing platform made for writers. Write together, and build a community. https://writefreely.org
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

232 lines
6.9 KiB

  1. /*
  2. * Copyright © 2020-2021 Musing Studio LLC.
  3. *
  4. * This file is part of WriteFreely.
  5. *
  6. * WriteFreely is free software: you can redistribute it and/or modify
  7. * it under the terms of the GNU Affero General Public License, included
  8. * in the LICENSE file in this source code package.
  9. */
  10. package writefreely
  11. import (
  12. "crypto/sha256"
  13. "encoding/hex"
  14. "fmt"
  15. "github.com/writeas/impart"
  16. "github.com/writeas/web-core/auth"
  17. "github.com/writeas/web-core/log"
  18. "github.com/writefreely/writefreely/page"
  19. "html/template"
  20. "net/http"
  21. "strings"
  22. "time"
  23. )
  24. type viewOauthSignupVars struct {
  25. page.StaticPage
  26. To string
  27. Message template.HTML
  28. Flashes []template.HTML
  29. AccessToken string
  30. TokenUsername string
  31. TokenAlias string // TODO: rename this to match the data it represents: the collection title
  32. TokenEmail string
  33. TokenRemoteUser string
  34. Provider string
  35. ClientID string
  36. TokenHash string
  37. InviteCode string
  38. LoginUsername string
  39. Alias string // TODO: rename this to match the data it represents: the collection title
  40. Email string
  41. }
  42. const (
  43. oauthParamAccessToken = "access_token"
  44. oauthParamTokenUsername = "token_username"
  45. oauthParamTokenAlias = "token_alias"
  46. oauthParamTokenEmail = "token_email"
  47. oauthParamTokenRemoteUserID = "token_remote_user"
  48. oauthParamClientID = "client_id"
  49. oauthParamProvider = "provider"
  50. oauthParamHash = "signature"
  51. oauthParamUsername = "username"
  52. oauthParamAlias = "alias"
  53. oauthParamEmail = "email"
  54. oauthParamPassword = "password"
  55. oauthParamInviteCode = "invite_code"
  56. )
  57. type oauthSignupPageParams struct {
  58. AccessToken string
  59. TokenUsername string
  60. TokenAlias string // TODO: rename this to match the data it represents: the collection title
  61. TokenEmail string
  62. TokenRemoteUser string
  63. ClientID string
  64. Provider string
  65. TokenHash string
  66. InviteCode string
  67. }
  68. func (p oauthSignupPageParams) HashTokenParams(key string) string {
  69. hasher := sha256.New()
  70. hasher.Write([]byte(key))
  71. hasher.Write([]byte(p.AccessToken))
  72. hasher.Write([]byte(p.TokenUsername))
  73. hasher.Write([]byte(p.TokenAlias))
  74. hasher.Write([]byte(p.TokenEmail))
  75. hasher.Write([]byte(p.TokenRemoteUser))
  76. hasher.Write([]byte(p.ClientID))
  77. hasher.Write([]byte(p.Provider))
  78. return hex.EncodeToString(hasher.Sum(nil))
  79. }
  80. func (h oauthHandler) viewOauthSignup(app *App, w http.ResponseWriter, r *http.Request) error {
  81. tp := &oauthSignupPageParams{
  82. AccessToken: r.FormValue(oauthParamAccessToken),
  83. TokenUsername: r.FormValue(oauthParamTokenUsername),
  84. TokenAlias: r.FormValue(oauthParamTokenAlias),
  85. TokenEmail: r.FormValue(oauthParamTokenEmail),
  86. TokenRemoteUser: r.FormValue(oauthParamTokenRemoteUserID),
  87. ClientID: r.FormValue(oauthParamClientID),
  88. Provider: r.FormValue(oauthParamProvider),
  89. InviteCode: r.FormValue(oauthParamInviteCode),
  90. }
  91. if tp.HashTokenParams(h.Config.Server.HashSeed) != r.FormValue(oauthParamHash) {
  92. return impart.HTTPError{Status: http.StatusBadRequest, Message: "Request has been tampered with."}
  93. }
  94. tp.TokenHash = tp.HashTokenParams(h.Config.Server.HashSeed)
  95. if err := h.validateOauthSignup(r); err != nil {
  96. return h.showOauthSignupPage(app, w, r, tp, err)
  97. }
  98. var err error
  99. hashedPass := []byte{}
  100. clearPass := r.FormValue(oauthParamPassword)
  101. hasPass := clearPass != ""
  102. if hasPass {
  103. hashedPass, err = auth.HashPass([]byte(clearPass))
  104. if err != nil {
  105. return h.showOauthSignupPage(app, w, r, tp, fmt.Errorf("unable to hash password"))
  106. }
  107. }
  108. newUser := &User{
  109. Username: r.FormValue(oauthParamUsername),
  110. HashedPass: hashedPass,
  111. HasPass: hasPass,
  112. Email: prepareUserEmail(r.FormValue(oauthParamEmail), h.EmailKey),
  113. Created: time.Now().Truncate(time.Second).UTC(),
  114. }
  115. displayName := r.FormValue(oauthParamAlias)
  116. if len(displayName) == 0 {
  117. displayName = r.FormValue(oauthParamUsername)
  118. }
  119. err = h.DB.CreateUser(h.Config, newUser, displayName, "")
  120. if err != nil {
  121. return h.showOauthSignupPage(app, w, r, tp, err)
  122. }
  123. // Log invite if needed
  124. if tp.InviteCode != "" {
  125. err = app.db.CreateInvitedUser(tp.InviteCode, newUser.ID)
  126. if err != nil {
  127. return err
  128. }
  129. }
  130. err = h.DB.RecordRemoteUserID(r.Context(), newUser.ID, r.FormValue(oauthParamTokenRemoteUserID), r.FormValue(oauthParamProvider), r.FormValue(oauthParamClientID), r.FormValue(oauthParamAccessToken))
  131. if err != nil {
  132. return h.showOauthSignupPage(app, w, r, tp, err)
  133. }
  134. if err := loginOrFail(h.Store, w, r, newUser); err != nil {
  135. return h.showOauthSignupPage(app, w, r, tp, err)
  136. }
  137. return nil
  138. }
  139. func (h oauthHandler) validateOauthSignup(r *http.Request) error {
  140. username := r.FormValue(oauthParamUsername)
  141. if len(username) < h.Config.App.MinUsernameLen {
  142. return impart.HTTPError{Status: http.StatusBadRequest, Message: "Username is too short."}
  143. }
  144. if len(username) > 100 {
  145. return impart.HTTPError{Status: http.StatusBadRequest, Message: "Username is too long."}
  146. }
  147. collTitle := r.FormValue(oauthParamAlias)
  148. if len(collTitle) == 0 {
  149. collTitle = username
  150. }
  151. email := r.FormValue(oauthParamEmail)
  152. if len(email) > 0 {
  153. parts := strings.Split(email, "@")
  154. if len(parts) != 2 || (len(parts[0]) < 1 || len(parts[1]) < 1) {
  155. return impart.HTTPError{Status: http.StatusBadRequest, Message: "Invalid email address"}
  156. }
  157. }
  158. return nil
  159. }
  160. func (h oauthHandler) showOauthSignupPage(app *App, w http.ResponseWriter, r *http.Request, tp *oauthSignupPageParams, errMsg error) error {
  161. username := tp.TokenUsername
  162. collTitle := tp.TokenAlias
  163. email := tp.TokenEmail
  164. session, err := app.sessionStore.Get(r, cookieName)
  165. if err != nil {
  166. // Ignore this
  167. log.Error("Unable to get session; ignoring: %v", err)
  168. }
  169. if tmpValue := r.FormValue(oauthParamUsername); len(tmpValue) > 0 {
  170. username = tmpValue
  171. }
  172. if tmpValue := r.FormValue(oauthParamAlias); len(tmpValue) > 0 {
  173. collTitle = tmpValue
  174. }
  175. if tmpValue := r.FormValue(oauthParamEmail); len(tmpValue) > 0 {
  176. email = tmpValue
  177. }
  178. p := &viewOauthSignupVars{
  179. StaticPage: pageForReq(app, r),
  180. To: r.FormValue("to"),
  181. Flashes: []template.HTML{},
  182. AccessToken: tp.AccessToken,
  183. TokenUsername: tp.TokenUsername,
  184. TokenAlias: tp.TokenAlias,
  185. TokenEmail: tp.TokenEmail,
  186. TokenRemoteUser: tp.TokenRemoteUser,
  187. Provider: tp.Provider,
  188. ClientID: tp.ClientID,
  189. TokenHash: tp.TokenHash,
  190. InviteCode: tp.InviteCode,
  191. LoginUsername: username,
  192. Alias: collTitle,
  193. Email: email,
  194. }
  195. // Display any error messages
  196. flashes, _ := getSessionFlashes(app, w, r, session)
  197. for _, flash := range flashes {
  198. p.Flashes = append(p.Flashes, template.HTML(flash))
  199. }
  200. if errMsg != nil {
  201. p.Flashes = append(p.Flashes, template.HTML(errMsg.Error()))
  202. }
  203. err = pages["signup-oauth.tmpl"].ExecuteTemplate(w, "base", p)
  204. if err != nil {
  205. log.Error("Unable to render signup-oauth: %v", err)
  206. return err
  207. }
  208. return nil
  209. }