@@ -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 != "" { | |||
@@ -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:"-"` | |||
} | |||
@@ -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 { | |||
@@ -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 | |||
} |
@@ -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, | |||
} | |||
@@ -1,163 +1,38 @@ | |||
{{define "head"}} | |||
<title>Sign up — {{.SiteName}}</title> | |||
<style type="text/css"> | |||
h2 { | |||
font-weight: normal; | |||
} | |||
#pricing.content-container div.form-container #payment-form { | |||
display: block !important; | |||
} | |||
#pricing #signup-form table { | |||
max-width: inherit !important; | |||
width: 100%; | |||
} | |||
#pricing #payment-form table { | |||
margin-top: 0 !important; | |||
max-width: inherit !important; | |||
width: 100%; | |||
} | |||
tr.subscription { | |||
border-spacing: 0; | |||
} | |||
#pricing.content-container tr.subscription button { | |||
margin-top: 0 !important; | |||
margin-bottom: 0 !important; | |||
width: 100%; | |||
} | |||
#pricing tr.subscription td { | |||
padding: 0 0.5em; | |||
} | |||
#pricing table.billing > tbody > tr > td:first-child { | |||
vertical-align: middle !important; | |||
} | |||
.billing-section { | |||
display: none; | |||
} | |||
.billing-section.bill-me { | |||
display: table-row; | |||
} | |||
#btn-create { | |||
color: white !important; | |||
} | |||
#total-price { | |||
padding-left: 0.5em; | |||
} | |||
#alias-site.demo { | |||
color: #999; | |||
} | |||
#alias-site { | |||
text-align: left; | |||
margin: 0.5em 0; | |||
} | |||
form dd { | |||
margin: 0; | |||
} | |||
</style> | |||
{{define "head"}}<title>Log in — {{.SiteName}}</title> | |||
<meta name="description" content="Log in to {{.SiteName}}."> | |||
<meta itemprop="description" content="Log in to {{.SiteName}}."> | |||
<style>input{margin-bottom:0.5em;}</style> | |||
{{end}} | |||
{{define "content"}} | |||
<div id="pricing" class="content-container wide-form"> | |||
<div class="row"> | |||
<div style="margin: 0 auto; max-width: 25em;"> | |||
<h1>Sign up</h1> | |||
{{ if .Error }} | |||
<p style="font-style: italic">{{.Error}}</p> | |||
{{ else }} | |||
{{if .Flashes}}<ul class="errors"> | |||
{{range .Flashes}}<li class="urgent">{{.}}</li>{{end}} | |||
</ul>{{end}} | |||
<div class="tight content-container"> | |||
<h1>Log in to {{.SiteName}}</h1> | |||
{{if .Flashes}}<ul class="errors"> | |||
{{range .Flashes}}<li class="urgent">{{.}}</li>{{end}} | |||
</ul>{{end}} | |||
<form action="/oauth/signup" method="post" style="text-align: center;margin-top:1em;" onsubmit="disableSubmit()"> | |||
<input type="hidden" name="access_token" value="{{ .AccessToken }}" /> | |||
<input type="hidden" name="token_username" value="{{ .TokenUsername }}" /> | |||
<input type="hidden" name="token_alias" value="{{ .TokenAlias }}" /> | |||
<input type="hidden" name="token_email" value="{{ .TokenEmail }}" /> | |||
<input type="hidden" name="token_remote_user" value="{{ .TokenRemoteUser }}" /> | |||
<input type="hidden" name="provider" value="{{ .Provider }}" /> | |||
<input type="hidden" name="client_id" value="{{ .ClientID }}" /> | |||
<input type="hidden" name="signature" value="{{ .TokenHash }}" /> | |||
<input type="text" name="username" placeholder="Username" value="{{.Username}}" /><br /> | |||
<input type="text" name="alias" placeholder="Alias"{{ if .Alias }} value="{{.Alias}}"{{ end }} /><br /> | |||
<input type="text" name="email" placeholder="Email"{{ if .Email }} value="{{.Email}}"{{ end }} /><br /> | |||
<input type="password" name="password" placeholder="Password" /><br /> | |||
<input type="submit" id="btn-login" value="Login" /> | |||
</form> | |||
<div id="billing"> | |||
<form action="/auth/signup-oauth" method="POST" id="signup-form" onsubmit="return signup()"> | |||
<input type="hidden" name="provider" value="{{.Provider}}" /> | |||
<input type="hidden" name="access_token" value="{{.AccessToken}}" /> | |||
<dl class="billing"> | |||
<label> | |||
<dt>Username</dt> | |||
<dd> | |||
<input type="text" id="alias" name="alias" style="width: 100%; box-sizing: border-box;" tabindex="1" autofocus /> | |||
{{if .Federation}}<p id="alias-site" class="demo">@<strong>your-username</strong>@{{.FriendlyHost}}</p>{{else}}<p id="alias-site" class="demo">{{.FriendlyHost}}/<strong>your-username</strong></p>{{end}} | |||
</dd> | |||
</label> | |||
<label> | |||
<dt>Email (optional)</dt> | |||
<dd><input type="email" name="email" id="email" style="letter-spacing: 1px; width: 100%; box-sizing: border-box;" placeholder="me@example.com" tabindex="3" /></dd> | |||
</label> | |||
<dt> | |||
<button id="btn-create" type="submit" style="margin-top: 0">Create blog</button> | |||
</dt> | |||
</dl> | |||
</form> | |||
</div> | |||
{{ end }} | |||
</div> | |||
</div> | |||
<script type="text/javascript" src="/js/h.js"></script> | |||
<script type="text/javascript"> | |||
function signup() { | |||
// Validate input | |||
if (!aliasOK) { | |||
var $a = $alias; | |||
$a.el.className = 'error'; | |||
$a.el.focus(); | |||
$a.el.scrollIntoView(); | |||
return false; | |||
} | |||
var $btn = document.getElementById('btn-create'); | |||
function disableSubmit() { | |||
var $btn = document.getElementById("btn-login"); | |||
$btn.value = "Logging in..."; | |||
$btn.disabled = true; | |||
$btn.value = 'Creating...'; | |||
return true; | |||
} | |||
var $alias = H.getEl('alias'); | |||
var $aliasSite = document.getElementById('alias-site'); | |||
var aliasOK = true; | |||
var typingTimer; | |||
var doneTypingInterval = 750; | |||
var doneTyping = function() { | |||
// Check on username | |||
var alias = $alias.el.value; | |||
if (alias != "") { | |||
var params = { | |||
username: alias | |||
}; | |||
var http = new XMLHttpRequest(); | |||
http.open("POST", '/api/alias', true); | |||
// Send the proper header information along with the request | |||
http.setRequestHeader("Content-type", "application/json"); | |||
http.onreadystatechange = function() { | |||
if (http.readyState == 4) { | |||
data = JSON.parse(http.responseText); | |||
if (http.status == 200) { | |||
aliasOK = true; | |||
$alias.removeClass('error'); | |||
$aliasSite.className = $aliasSite.className.replace(/(?:^|\s)demo(?!\S)/g, ''); | |||
$aliasSite.className = $aliasSite.className.replace(/(?:^|\s)error(?!\S)/g, ''); | |||
$aliasSite.innerHTML = '{{ if .Federation }}@<strong>' + data.data + '</strong>@{{.FriendlyHost}}{{ else }}{{.FriendlyHost}}/<strong>' + data.data + '</strong>/{{ end }}'; | |||
} else { | |||
aliasOK = false; | |||
$alias.setClass('error'); | |||
$aliasSite.className = 'error'; | |||
$aliasSite.textContent = data.error_msg; | |||
} | |||
} | |||
} | |||
http.send(JSON.stringify(params)); | |||
} else { | |||
$aliasSite.className += ' demo'; | |||
$aliasSite.innerHTML = '{{ if .Federation }}@<strong>your-username</strong>@{{.FriendlyHost}}{{ else }}{{.FriendlyHost}}/<strong>your-username</strong>/{{ end }}'; | |||
} | |||
}; | |||
$alias.on('keyup input', function() { | |||
clearTimeout(typingTimer); | |||
typingTimer = setTimeout(doneTyping, doneTypingInterval); | |||
}); | |||
</script> | |||
{{end}} |