diff --git a/account.go b/account.go index 6fb8053..2dcfd27 100644 --- a/account.go +++ b/account.go @@ -306,12 +306,16 @@ func viewLogin(app *App, w http.ResponseWriter, r *http.Request) error { Message template.HTML Flashes []template.HTML LoginUsername string + OauthSlack bool + OauthWriteAs bool }{ pageForReq(app, r), r.FormValue("to"), template.HTML(""), []template.HTML{}, getTempInfo(app, "login-user", r, w), + app.Config().SlackOauth.ClientID != "", + app.Config().WriteAsOauth.ClientID != "", } if earlyError != "" { diff --git a/config/config.go b/config/config.go index 996c1df..6aee69b 100644 --- a/config/config.go +++ b/config/config.go @@ -42,6 +42,8 @@ type ( PagesParentDir string `ini:"pages_parent_dir"` KeysParentDir string `ini:"keys_parent_dir"` + HashSeed string `ini:"hash_seed"` + Dev bool `ini:"-"` } diff --git a/oauth.go b/oauth.go index 4758e0f..67c4ac8 100644 --- a/oauth.go +++ b/oauth.go @@ -7,8 +7,6 @@ import ( "github.com/gorilla/mux" "github.com/gorilla/sessions" "github.com/writeas/impart" - "github.com/writeas/nerds/store" - "github.com/writeas/web-core/auth" "github.com/writeas/web-core/log" "github.com/writeas/writefreely/config" "io" @@ -137,6 +135,7 @@ func configureOauthRoutes(parentHandler *Handler, r *mux.Router, app *App, oauth } r.HandleFunc("/oauth/"+oauthClient.GetProvider(), parentHandler.OAuth(handler.viewOauthInit)).Methods("GET") r.HandleFunc("/oauth/callback", parentHandler.OAuth(handler.viewOauthCallback)).Methods("GET") + r.HandleFunc("/oauth/signup", parentHandler.OAuth(handler.viewOauthSignup)).Methods("POST") } func (h oauthHandler) viewOauthCallback(app *App, w http.ResponseWriter, r *http.Request) error { @@ -171,52 +170,31 @@ func (h oauthHandler) viewOauthCallback(app *App, w http.ResponseWriter, r *http return impart.HTTPError{http.StatusInternalServerError, err.Error()} } - if localUserID == -1 { - // We don't have, nor do we want, the password from the origin, so we - //create a random string. If the user needs to set a password, they - //can do so through the settings page or through the password reset - //flow. - randPass := store.Generate62RandomString(14) - hashedPass, err := auth.HashPass([]byte(randPass)) - if err != nil { - return impart.HTTPError{http.StatusInternalServerError, "unable to create password hash"} - } - newUser := &User{ - Username: tokenInfo.Username, - HashedPass: hashedPass, - HasPass: true, - Email: prepareUserEmail(tokenInfo.Email, h.EmailKey), - Created: time.Now().Truncate(time.Second).UTC(), - } - displayName := tokenInfo.DisplayName - if len(displayName) == 0 { - displayName = tokenInfo.Username - } - - err = h.DB.CreateUser(h.Config, newUser, displayName) - if err != nil { - return impart.HTTPError{http.StatusInternalServerError, err.Error()} - } - - err = h.DB.RecordRemoteUserID(ctx, newUser.ID, tokenInfo.UserID, provider, clientID, tokenResponse.AccessToken) + if localUserID != -1 { + user, err := h.DB.GetUserByID(localUserID) if err != nil { + log.Error("Unable to GetUserByID %d: %s", localUserID, err) return impart.HTTPError{http.StatusInternalServerError, err.Error()} } - - if err := loginOrFail(h.Store, w, r, newUser); err != nil { + if err = loginOrFail(h.Store, w, r, user); err != nil { + log.Error("Unable to loginOrFail %d: %s", localUserID, err) return impart.HTTPError{http.StatusInternalServerError, err.Error()} } return nil } - user, err := h.DB.GetUserByID(localUserID) - if err != nil { - return impart.HTTPError{http.StatusInternalServerError, err.Error()} + tp := &oauthSignupPageParams{ + AccessToken: tokenResponse.AccessToken, + TokenUsername: tokenInfo.Username, + TokenAlias: tokenInfo.DisplayName, + TokenEmail: tokenInfo.Email, + TokenRemoteUser: tokenInfo.UserID, + Provider: provider, + ClientID: clientID, } - if err = loginOrFail(h.Store, w, r, user); err != nil { - return impart.HTTPError{http.StatusInternalServerError, err.Error()} - } - return nil + tp.TokenHash = tp.HashTokenParams(h.Config.Server.HashSeed) + + return h.showOauthSignupPage(app, w, r, tp, nil) } func limitedJsonUnmarshal(body io.ReadCloser, n int, thing interface{}) error { diff --git a/oauth_signup.go b/oauth_signup.go new file mode 100644 index 0000000..53ad5c4 --- /dev/null +++ b/oauth_signup.go @@ -0,0 +1,209 @@ +package writefreely + +import ( + "crypto/sha256" + "encoding/hex" + "fmt" + "github.com/writeas/impart" + "github.com/writeas/web-core/auth" + "github.com/writeas/web-core/log" + "github.com/writeas/writefreely/page" + "html/template" + "net/http" + "strings" + "time" +) + +type viewOauthSignupVars struct { + page.StaticPage + To string + Message template.HTML + Flashes []template.HTML + + AccessToken string + TokenUsername string + TokenAlias string + TokenEmail string + TokenRemoteUser string + Provider string + ClientID string + TokenHash string + + Username string + Alias string + Email string +} + +const ( + oauthParamAccessToken = "access_token" + oauthParamTokenUsername = "token_username" + oauthParamTokenAlias = "token_alias" + oauthParamTokenEmail = "token_email" + oauthParamTokenRemoteUserID = "token_remote_user" + oauthParamClientID = "client_id" + oauthParamProvider = "provider" + oauthParamHash = "signature" + oauthParamUsername = "username" + oauthParamAlias = "alias" + oauthParamEmail = "email" + oauthParamPassword = "password" +) + +type oauthSignupPageParams struct { + AccessToken string + TokenUsername string + TokenAlias string + TokenEmail string + TokenRemoteUser string + ClientID string + Provider string + TokenHash string +} + +func (p oauthSignupPageParams) HashTokenParams(key string) string { + hasher := sha256.New() + hasher.Write([]byte(key)) + hasher.Write([]byte(p.AccessToken)) + hasher.Write([]byte(p.TokenUsername)) + hasher.Write([]byte(p.TokenAlias)) + hasher.Write([]byte(p.TokenEmail)) + hasher.Write([]byte(p.TokenRemoteUser)) + hasher.Write([]byte(p.ClientID)) + hasher.Write([]byte(p.Provider)) + return hex.EncodeToString(hasher.Sum(nil)) +} + +func (h oauthHandler) viewOauthSignup(app *App, w http.ResponseWriter, r *http.Request) error { + tp := &oauthSignupPageParams{ + AccessToken: r.FormValue(oauthParamAccessToken), + TokenUsername: r.FormValue(oauthParamTokenUsername), + TokenAlias: r.FormValue(oauthParamTokenAlias), + TokenEmail: r.FormValue(oauthParamTokenEmail), + TokenRemoteUser: r.FormValue(oauthParamTokenRemoteUserID), + ClientID: r.FormValue(oauthParamClientID), + Provider: r.FormValue(oauthParamProvider), + } + if tp.HashTokenParams(h.Config.Server.HashSeed) != r.FormValue(oauthParamHash) { + return impart.HTTPError{Status: http.StatusBadRequest, Message: "Request has been tampered with."} + } + tp.TokenHash = tp.HashTokenParams(h.Config.Server.HashSeed) + if err := h.validateOauthSignup(r); err != nil { + return h.showOauthSignupPage(app, w, r, tp, err) + } + + hashedPass, err := auth.HashPass([]byte(r.FormValue(oauthParamPassword))) + if err != nil { + return h.showOauthSignupPage(app, w, r, tp, fmt.Errorf("unable to hash password")) + } + newUser := &User{ + Username: r.FormValue(oauthParamUsername), + HashedPass: hashedPass, + HasPass: true, + Email: prepareUserEmail(r.FormValue(oauthParamEmail), h.EmailKey), + Created: time.Now().Truncate(time.Second).UTC(), + } + displayName := r.FormValue(oauthParamAlias) + if len(displayName) == 0 { + displayName = r.FormValue(oauthParamUsername) + } + + err = h.DB.CreateUser(h.Config, newUser, displayName) + if err != nil { + return h.showOauthSignupPage(app, w, r, tp, err) + } + + err = h.DB.RecordRemoteUserID(r.Context(), newUser.ID, r.FormValue(oauthParamTokenRemoteUserID), r.FormValue(oauthParamProvider), r.FormValue(oauthParamClientID), r.FormValue(oauthParamAccessToken)) + if err != nil { + return h.showOauthSignupPage(app, w, r, tp, err) + } + + if err := loginOrFail(h.Store, w, r, newUser); err != nil { + return h.showOauthSignupPage(app, w, r, tp, err) + } + return nil +} + +func (h oauthHandler) validateOauthSignup(r *http.Request) error { + username := r.FormValue(oauthParamUsername) + if len(username) < 5 { + return impart.HTTPError{Status: http.StatusBadRequest, Message: "Username is too short."} + } + if len(username) > 20 { + return impart.HTTPError{Status: http.StatusBadRequest, Message: "Username is too long."} + } + alias := r.FormValue(oauthParamAlias) + if len(alias) < 5 { + return impart.HTTPError{Status: http.StatusBadRequest, Message: "Alias is too short."} + } + if len(alias) > 20 { + return impart.HTTPError{Status: http.StatusBadRequest, Message: "Alias is too long."} + } + password := r.FormValue("password") + if len(password) < 5 { + return impart.HTTPError{Status: http.StatusBadRequest, Message: "Password is too short."} + } + email := r.FormValue(oauthParamEmail) + if len(email) > 0 { + parts := strings.Split(email, "@") + if len(parts) != 2 || (len(parts[0]) < 1 || len(parts[1]) < 1) { + return impart.HTTPError{Status: http.StatusBadRequest, Message: "Invalid email address"} + } + } + return nil +} + +func (h oauthHandler) showOauthSignupPage(app *App, w http.ResponseWriter, r *http.Request, tp *oauthSignupPageParams, errMsg error) error { + username := tp.TokenUsername + alias := tp.TokenAlias + email := tp.TokenEmail + + session, err := app.sessionStore.Get(r, cookieName) + if err != nil { + // Ignore this + log.Error("Unable to get session; ignoring: %v", err) + } + + if tmpValue := r.FormValue(oauthParamUsername); len(tmpValue) > 0 { + username = tmpValue + } + if tmpValue := r.FormValue(oauthParamAlias); len(tmpValue) > 0 { + alias = tmpValue + } + if tmpValue := r.FormValue(oauthParamEmail); len(tmpValue) > 0 { + email = tmpValue + } + + p := &viewOauthSignupVars{ + StaticPage: pageForReq(app, r), + To: r.FormValue("to"), + Flashes: []template.HTML{}, + + AccessToken: tp.AccessToken, + TokenUsername: tp.TokenUsername, + TokenAlias: tp.TokenAlias, + TokenEmail: tp.TokenEmail, + TokenRemoteUser: tp.TokenRemoteUser, + Provider: tp.Provider, + ClientID: tp.ClientID, + TokenHash: tp.TokenHash, + + Username: username, + Alias: alias, + Email: email, + } + + // Display any error messages + flashes, _ := getSessionFlashes(app, w, r, session) + for _, flash := range flashes { + p.Flashes = append(p.Flashes, template.HTML(flash)) + } + if errMsg != nil { + p.Flashes = append(p.Flashes, template.HTML(errMsg.Error())) + } + err = pages["signup-oauth.tmpl"].ExecuteTemplate(w, "base", p) + if err != nil { + log.Error("Unable to render signup-oauth: %v", err) + return err + } + return nil +} diff --git a/oauth_slack.go b/oauth_slack.go index 066aa18..5a6f4ed 100644 --- a/oauth_slack.go +++ b/oauth_slack.go @@ -3,6 +3,8 @@ package writefreely import ( "context" "errors" + "fmt" + "github.com/writeas/nerds/store" "github.com/writeas/slug" "net/http" "net/url" @@ -151,7 +153,7 @@ func (c slackOauthClient) inspectOauthAccessToken(ctx context.Context, accessTok func (resp slackUserIdentityResponse) InspectResponse() *InspectResponse { return &InspectResponse{ UserID: resp.User.ID, - Username: slug.Make(resp.User.Name), + Username: fmt.Sprintf("%s-%s", slug.Make(resp.User.Name), store.Generate62RandomString(5)), DisplayName: resp.User.Name, Email: resp.User.Email, } diff --git a/pages/signup-oauth.tmpl b/pages/signup-oauth.tmpl index 1ca1101..35810bb 100644 --- a/pages/signup-oauth.tmpl +++ b/pages/signup-oauth.tmpl @@ -1,163 +1,38 @@ -{{define "head"}} -
{{.Error}}
- {{ else }} - {{if .Flashes}}