Implemented /oauth/signup. T712

pull/232/head
Nick Gerakines 5 years ago
parent a4e373065c
commit 6429d495a2
  1. 4
      account.go
  2. 2
      config/config.go
  3. 56
      oauth.go
  4. 209
      oauth_signup.go
  5. 4
      oauth_slack.go
  6. 173
      pages/signup-oauth.tmpl

@ -306,12 +306,16 @@ func viewLogin(app *App, w http.ResponseWriter, r *http.Request) error {
Message template.HTML Message template.HTML
Flashes []template.HTML Flashes []template.HTML
LoginUsername string LoginUsername string
OauthSlack bool
OauthWriteAs bool
}{ }{
pageForReq(app, r), pageForReq(app, r),
r.FormValue("to"), r.FormValue("to"),
template.HTML(""), template.HTML(""),
[]template.HTML{}, []template.HTML{},
getTempInfo(app, "login-user", r, w), getTempInfo(app, "login-user", r, w),
app.Config().SlackOauth.ClientID != "",
app.Config().WriteAsOauth.ClientID != "",
} }
if earlyError != "" { if earlyError != "" {

@ -42,6 +42,8 @@ type (
PagesParentDir string `ini:"pages_parent_dir"` PagesParentDir string `ini:"pages_parent_dir"`
KeysParentDir string `ini:"keys_parent_dir"` KeysParentDir string `ini:"keys_parent_dir"`
HashSeed string `ini:"hash_seed"`
Dev bool `ini:"-"` Dev bool `ini:"-"`
} }

@ -7,8 +7,6 @@ import (
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/gorilla/sessions" "github.com/gorilla/sessions"
"github.com/writeas/impart" "github.com/writeas/impart"
"github.com/writeas/nerds/store"
"github.com/writeas/web-core/auth"
"github.com/writeas/web-core/log" "github.com/writeas/web-core/log"
"github.com/writeas/writefreely/config" "github.com/writeas/writefreely/config"
"io" "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/"+oauthClient.GetProvider(), parentHandler.OAuth(handler.viewOauthInit)).Methods("GET")
r.HandleFunc("/oauth/callback", parentHandler.OAuth(handler.viewOauthCallback)).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 { 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()} return impart.HTTPError{http.StatusInternalServerError, err.Error()}
} }
if localUserID == -1 { if localUserID != -1 {
// We don't have, nor do we want, the password from the origin, so we user, err := h.DB.GetUserByID(localUserID)
//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 err != nil { if err != nil {
log.Error("Unable to GetUserByID %d: %s", localUserID, err)
return impart.HTTPError{http.StatusInternalServerError, err.Error()} return impart.HTTPError{http.StatusInternalServerError, err.Error()}
} }
if err = loginOrFail(h.Store, w, r, user); err != nil {
if err := loginOrFail(h.Store, w, r, newUser); err != nil { log.Error("Unable to loginOrFail %d: %s", localUserID, err)
return impart.HTTPError{http.StatusInternalServerError, err.Error()} return impart.HTTPError{http.StatusInternalServerError, err.Error()}
} }
return nil return nil
} }
user, err := h.DB.GetUserByID(localUserID) tp := &oauthSignupPageParams{
if err != nil { AccessToken: tokenResponse.AccessToken,
return impart.HTTPError{http.StatusInternalServerError, err.Error()} 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 { tp.TokenHash = tp.HashTokenParams(h.Config.Server.HashSeed)
return impart.HTTPError{http.StatusInternalServerError, err.Error()}
} return h.showOauthSignupPage(app, w, r, tp, nil)
return nil
} }
func limitedJsonUnmarshal(body io.ReadCloser, n int, thing interface{}) error { 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 ( import (
"context" "context"
"errors" "errors"
"fmt"
"github.com/writeas/nerds/store"
"github.com/writeas/slug" "github.com/writeas/slug"
"net/http" "net/http"
"net/url" "net/url"
@ -151,7 +153,7 @@ func (c slackOauthClient) inspectOauthAccessToken(ctx context.Context, accessTok
func (resp slackUserIdentityResponse) InspectResponse() *InspectResponse { func (resp slackUserIdentityResponse) InspectResponse() *InspectResponse {
return &InspectResponse{ return &InspectResponse{
UserID: resp.User.ID, 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, DisplayName: resp.User.Name,
Email: resp.User.Email, Email: resp.User.Email,
} }

@ -1,163 +1,38 @@
{{define "head"}} {{define "head"}}<title>Log in &mdash; {{.SiteName}}</title>
<title>Sign up &mdash; {{.SiteName}}</title> <meta name="description" content="Log in to {{.SiteName}}.">
<meta itemprop="description" content="Log in to {{.SiteName}}.">
<style type="text/css"> <style>input{margin-bottom:0.5em;}</style>
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>
{{end}} {{end}}
{{define "content"}} {{define "content"}}
<div id="pricing" class="content-container wide-form"> <div class="tight content-container">
<h1>Log in to {{.SiteName}}</h1>
<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"> {{if .Flashes}}<ul class="errors">
{{range .Flashes}}<li class="urgent">{{.}}</li>{{end}} {{range .Flashes}}<li class="urgent">{{.}}</li>{{end}}
</ul>{{end}} </ul>{{end}}
<div id="billing"> <form action="/oauth/signup" method="post" style="text-align: center;margin-top:1em;" onsubmit="disableSubmit()">
<form action="/auth/signup-oauth" method="POST" id="signup-form" onsubmit="return signup()"> <input type="hidden" name="access_token" value="{{ .AccessToken }}" />
<input type="hidden" name="provider" value="{{.Provider}}" /> <input type="hidden" name="token_username" value="{{ .TokenUsername }}" />
<input type="hidden" name="access_token" value="{{.AccessToken}}" /> <input type="hidden" name="token_alias" value="{{ .TokenAlias }}" />
<dl class="billing"> <input type="hidden" name="token_email" value="{{ .TokenEmail }}" />
<label> <input type="hidden" name="token_remote_user" value="{{ .TokenRemoteUser }}" />
<dt>Username</dt> <input type="hidden" name="provider" value="{{ .Provider }}" />
<dd> <input type="hidden" name="client_id" value="{{ .ClientID }}" />
<input type="text" id="alias" name="alias" style="width: 100%; box-sizing: border-box;" tabindex="1" autofocus /> <input type="hidden" name="signature" value="{{ .TokenHash }}" />
{{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> <input type="text" name="username" placeholder="Username" value="{{.Username}}" /><br />
</label> <input type="text" name="alias" placeholder="Alias"{{ if .Alias }} value="{{.Alias}}"{{ end }} /><br />
<label> <input type="text" name="email" placeholder="Email"{{ if .Email }} value="{{.Email}}"{{ end }} /><br />
<dt>Email (optional)</dt> <input type="password" name="password" placeholder="Password" /><br />
<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> <input type="submit" id="btn-login" value="Login" />
</label>
<dt>
<button id="btn-create" type="submit" style="margin-top: 0">Create blog</button>
</dt>
</dl>
</form> </form>
</div>
{{ end }}
</div>
</div>
<script type="text/javascript" src="/js/h.js"></script>
<script type="text/javascript"> <script type="text/javascript">
function signup() { function disableSubmit() {
// Validate input var $btn = document.getElementById("btn-login");
if (!aliasOK) { $btn.value = "Logging in...";
var $a = $alias;
$a.el.className = 'error';
$a.el.focus();
$a.el.scrollIntoView();
return false;
}
var $btn = document.getElementById('btn-create');
$btn.disabled = true; $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> </script>
{{end}} {{end}}

Loading…
Cancel
Save