From b985292b1807327c33457c5228bd59f5ac34f019 Mon Sep 17 00:00:00 2001 From: Nick Gerakines Date: Thu, 2 Jan 2020 15:33:39 -0500 Subject: [PATCH 1/9] First take at template updates. T712 --- pages/login.tmpl | 10 +++ pages/signup-oauth.tmpl | 163 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 173 insertions(+) create mode 100644 pages/signup-oauth.tmpl diff --git a/pages/login.tmpl b/pages/login.tmpl index 1c8e862..6c1b3ed 100644 --- a/pages/login.tmpl +++ b/pages/login.tmpl @@ -19,6 +19,16 @@ {{if and (not .SingleUser) .OpenRegistration}}

{{if .Message}}{{.Message}}{{else}}No account yet? Sign up to start a blog.{{end}}

{{end}} + {{ if .OauthSlack }} +

+ Sign-in with Slack. +

+ {{ end }} + {{ if .OauthWriteAs }} +

+ Sign-in with Write.As. +

+ {{ end }} + + +{{end}} From 6429d495a2e9f2c3cef69cdb421603be45c7c340 Mon Sep 17 00:00:00 2001 From: Nick Gerakines Date: Fri, 3 Jan 2020 13:50:21 -0500 Subject: [PATCH 2/9] Implemented /oauth/signup. T712 --- account.go | 4 + config/config.go | 2 + oauth.go | 56 ++++------- oauth_signup.go | 209 ++++++++++++++++++++++++++++++++++++++++ oauth_slack.go | 4 +- pages/signup-oauth.tmpl | 185 ++++++----------------------------- 6 files changed, 265 insertions(+), 195 deletions(-) create mode 100644 oauth_signup.go 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"}} -Sign up — {{.SiteName}} - - +{{define "head"}}Log in — {{.SiteName}} + + + {{end}} {{define "content"}} -
- -
-
-

Sign up

- - {{ if .Error }} -

{{.Error}}

- {{ else }} - {{if .Flashes}}
    - {{range .Flashes}}
  • {{.}}
  • {{end}} -
{{end}} +
+

Log in to {{.SiteName}}

+ + {{if .Flashes}}
    + {{range .Flashes}}
  • {{.}}
  • {{end}} +
{{end}} + +
+ + + + + + + + + +
+
+
+
+ +
-
-
- - -
- - -
- -
-
-
-
- {{ end }} -
-
- - - {{end}} From 5249456ec6791f19ce117535f2d0117c4fb5ada4 Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Sun, 5 Jan 2020 11:00:11 -0500 Subject: [PATCH 3/9] Add .btn.cta link styles --- less/core.less | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/less/core.less b/less/core.less index 8844c84..3669d76 100644 --- a/less/core.less +++ b/less/core.less @@ -684,18 +684,19 @@ select.inputform, textarea.inputform { border: 1px solid #999; } -input, button, select.inputform, textarea.inputform { +input, button, select.inputform, textarea.inputform, a.btn { padding: 0.5em; font-family: @serifFont; font-size: 100%; .rounded(.25em); - &[type=submit], &.submit { + &[type=submit], &.submit, &.cta { border: 1px solid @primary; background: @primary; color: white; .transition(0.2s); &:hover { background-color: lighten(@primary, 3%); + text-decoration: none; } &:disabled { cursor: default; From 77e012680853d023678e76fa784c061d852b531c Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Sun, 5 Jan 2020 11:00:58 -0500 Subject: [PATCH 4/9] Move and restyle OAuth login links - Move them above local login form - Restyle as side-by-side buttons Ref T712 --- pages/login.tmpl | 59 ++++++++++++++++++++++----- static/img/sign_in_with_slack.png | Bin 0 -> 2604 bytes static/img/sign_in_with_slack@2x.png | Bin 0 -> 5120 bytes 3 files changed, 48 insertions(+), 11 deletions(-) create mode 100644 static/img/sign_in_with_slack.png create mode 100644 static/img/sign_in_with_slack@2x.png diff --git a/pages/login.tmpl b/pages/login.tmpl index 6c1b3ed..345b171 100644 --- a/pages/login.tmpl +++ b/pages/login.tmpl @@ -1,7 +1,38 @@ {{define "head"}}Log in — {{.SiteName}} - + {{end}} {{define "content"}}
@@ -11,6 +42,22 @@ {{range .Flashes}}
  • {{.}}
  • {{end}} {{end}} + {{ if or .OauthSlack .OauthWriteAs }} +
    + {{ if .OauthSlack }} + Sign in with Slack + {{ end }} + {{ if .OauthWriteAs }} + Sign in with Write.as + {{ end }} +
    + +
    +

    or

    +
    +
    + {{ end }} +


    @@ -19,16 +66,6 @@
    {{if and (not .SingleUser) .OpenRegistration}}

    {{if .Message}}{{.Message}}{{else}}No account yet? Sign up to start a blog.{{end}}

    {{end}} - {{ if .OauthSlack }} -

    - Sign-in with Slack. -

    - {{ end }} - {{ if .OauthWriteAs }} -

    - Sign-in with Write.As. -

    - {{ end }}