Add email validation after registration

This commit is contained in:
nemunaire 2020-05-23 18:47:09 +02:00
parent c56bd75abb
commit fc64e8d40d
8 changed files with 444 additions and 4 deletions

View File

@ -174,7 +174,13 @@ func checkAuth(opts *config.Options, _ httprouter.Params, body io.Reader) Respon
}
} else if !user.CheckAuth(lf.Password) {
return APIErrorResponse{
err: errors.New(`Invalid username or password`),
err: errors.New(`Invalid username or password.`),
status: http.StatusUnauthorized,
}
} else if user.EmailValidated == nil {
return APIErrorResponse{
err: errors.New(`Please validate your e-mail address before your first login.`),
href: "/email-validation",
status: http.StatusUnauthorized,
}
} else {

View File

@ -34,17 +34,28 @@ package api
import (
"encoding/json"
"errors"
"fmt"
"io"
"log"
"net/http"
"net/mail"
"strconv"
"strings"
"unicode"
"github.com/julienschmidt/httprouter"
"git.happydns.org/happydns/config"
"git.happydns.org/happydns/model"
"git.happydns.org/happydns/storage"
"git.happydns.org/happydns/utils"
)
func init() {
router.POST("/api/users", apiHandler(registerUser))
router.PATCH("/api/users", apiHandler(resendValidationLink))
router.GET("/api/users/:uid", apiAuthHandler(sameUserHandler(getUser)))
router.POST("/api/users/:uid/email", apiHandler(userHandler(validateUserAddress)))
}
type UploadedUser struct {
@ -52,21 +63,76 @@ type UploadedUser struct {
Password string
}
func sendValidationLink(opts *config.Options, user *happydns.User) error {
var toName string
if n := strings.Index(user.Email, "+"); n > 0 {
toName = user.Email[0:n]
} else {
toName = user.Email[0:strings.Index(user.Email, "@")]
}
if len(toName) > 1 {
toNameCopy := strings.Replace(toName, ".", " ", -1)
toName = ""
lastRuneIsSpace := true
for _, runeValue := range toNameCopy {
if lastRuneIsSpace {
lastRuneIsSpace = false
toName += string(unicode.ToTitle(runeValue))
} else {
toName += string(runeValue)
}
if unicode.IsSpace(runeValue) || unicode.IsPunct(runeValue) || unicode.IsSymbol(runeValue) {
lastRuneIsSpace = true
}
}
}
log.Println("test to", user.Email, toName)
return utils.SendMail(
&mail.Address{Name: toName, Address: user.Email},
"Your new account on happyDNS",
`Welcome to happyDNS!
--------------------
Hi `+toName+`,
We are pleased that you created an account on our great domain name
management platform!
In order to validate your account, please follow this link now:
[Validate my account](`+opts.GetRegistrationURL(user)+`)`,
)
}
func registerUser(opts *config.Options, p httprouter.Params, body io.Reader) Response {
var uu UploadedUser
err := json.NewDecoder(body).Decode(&uu)
if err != nil {
return APIErrorResponse{
err: err,
err: fmt.Errorf("Something is wrong in received data: %w", err),
}
}
if len(uu.Email) <= 3 {
if len(uu.Email) <= 3 || strings.Index(uu.Email, "@") == -1 {
return APIErrorResponse{
err: errors.New("The given email is invalid."),
}
}
if len(uu.Password) <= 7 {
return APIErrorResponse{
err: errors.New("The given email is invalid."),
}
}
if storage.MainStore.UserExists(uu.Email) {
return APIErrorResponse{
err: errors.New("An account already exists with the given address. Try login now."),
}
}
if user, err := happydns.NewUser(uu.Email, uu.Password); err != nil {
return APIErrorResponse{
err: err,
@ -75,9 +141,121 @@ func registerUser(opts *config.Options, p httprouter.Params, body io.Reader) Res
return APIErrorResponse{
err: err,
}
} else if sendValidationLink(opts, user); err != nil {
return APIErrorResponse{
err: err,
}
} else {
return APIResponse{
response: user,
}
}
}
func resendValidationLink(opts *config.Options, p httprouter.Params, body io.Reader) Response {
var uu UploadedUser
err := json.NewDecoder(body).Decode(&uu)
if err != nil {
return APIErrorResponse{
err: fmt.Errorf("Something is wrong in received data: %w", err),
}
}
if user, err := storage.MainStore.GetUserByEmail(uu.Email); err != nil {
log.Println(err)
return APIErrorResponse{
err: errors.New("If this address exists in our database, you'll receive a new validation link."),
status: http.StatusOK,
}
} else if user.EmailValidated != nil {
return APIErrorResponse{
err: errors.New("If this address exists in our database, you'll receive a new validation link."),
status: http.StatusOK,
}
} else if err = sendValidationLink(opts, user); err != nil {
return APIErrorResponse{
err: err,
}
} else {
return APIErrorResponse{
err: errors.New("If this address exists in our database, you'll receive a new validation link."),
status: http.StatusOK,
}
}
}
func sameUserHandler(f func(*config.Options, *happydns.User, io.Reader) Response) func(*config.Options, *happydns.User, httprouter.Params, io.Reader) Response {
return func(opts *config.Options, u *happydns.User, ps httprouter.Params, body io.Reader) Response {
if uid, err := strconv.ParseInt(ps.ByName("uid"), 16, 64); err != nil {
return APIErrorResponse{
status: http.StatusNotFound,
err: fmt.Errorf("Invalid user identifier given: %w", err),
}
} else if user, err := storage.MainStore.GetUser(uid); err != nil {
return APIErrorResponse{
status: http.StatusNotFound,
err: errors.New("User not found"),
}
} else if user.Id != u.Id {
return APIErrorResponse{
status: http.StatusNotFound,
err: errors.New("User not found"),
}
} else {
return f(opts, user, body)
}
}
}
func getUser(opts *config.Options, user *happydns.User, _ io.Reader) Response {
return APIResponse{
response: user,
}
}
func userHandler(f func(*config.Options, *happydns.User, io.Reader) Response) func(*config.Options, httprouter.Params, io.Reader) Response {
return func(opts *config.Options, ps httprouter.Params, body io.Reader) Response {
if uid, err := strconv.ParseInt(ps.ByName("uid"), 16, 64); err != nil {
return APIErrorResponse{
status: http.StatusNotFound,
err: fmt.Errorf("Invalid user identifier given: %w", err),
}
} else if user, err := storage.MainStore.GetUser(uid); err != nil {
return APIErrorResponse{
status: http.StatusNotFound,
err: errors.New("User not found"),
}
} else {
return f(opts, user, body)
}
}
}
type UploadedAddressValidation struct {
Key string
}
func validateUserAddress(opts *config.Options, user *happydns.User, body io.Reader) Response {
var uav UploadedAddressValidation
err := json.NewDecoder(body).Decode(&uav)
if err != nil {
return APIErrorResponse{
err: fmt.Errorf("Something is wrong in received data: %w", err),
}
}
if err := user.ValidateEmail(uav.Key); err != nil {
return APIErrorResponse{
err: err,
}
} else if err := storage.MainStore.UpdateUser(user); err != nil {
return APIErrorResponse{
status: http.StatusNotFound,
err: errors.New("User not found"),
}
} else {
return APIResponse{
response: true,
}
}
}

42
config/user.go Normal file
View File

@ -0,0 +1,42 @@
// Copyright or © or Copr. happyDNS (2020)
//
// contact@happydns.org
//
// This software is a computer program whose purpose is to provide a modern
// interface to interact with DNS systems.
//
// This software is governed by the CeCILL license under French law and abiding
// by the rules of distribution of free software. You can use, modify and/or
// redistribute the software under the terms of the CeCILL license as
// circulated by CEA, CNRS and INRIA at the following URL
// "http://www.cecill.info".
//
// As a counterpart to the access to the source code and rights to copy, modify
// and redistribute granted by the license, users are provided only with a
// limited warranty and the software's author, the holder of the economic
// rights, and the successive licensors have only limited liability.
//
// In this respect, the user's attention is drawn to the risks associated with
// loading, using, modifying and/or developing or reproducing the software by
// the user in light of its specific status of free software, that may mean
// that it is complicated to manipulate, and that also therefore means that it
// is reserved for developers and experienced professionals having in-depth
// computer knowledge. Users are therefore encouraged to load and test the
// software's suitability as regards their requirements in conditions enabling
// the security of their systems and/or data to be ensured and, more generally,
// to use and operate it in the same conditions as regards security.
//
// The fact that you are presently reading this means that you have had
// knowledge of the CeCILL license and that you accept its terms.
package config
import (
"fmt"
"git.happydns.org/happydns/model"
)
func (o *Options) GetRegistrationURL(u *happydns.User) string {
return fmt.Sprintf("%s%s/email-validation?u=%x&k=%s", o.ExternalURL, o.BaseURL, u.Id, u.GenRegistrationHash(false))
}

View File

@ -70,6 +70,13 @@ const routes = [
return import(/* webpackChunkName: "signup" */ '../views/signup.vue')
}
},
{
path: '/email-validation',
name: 'email-validation',
component: function () {
return import(/* webpackChunkName: "signup" */ '../views/email-validation.vue')
}
},
{
path: '/domains',
name: 'domains',

View File

@ -0,0 +1,160 @@
<!--
Copyright or © or Copr. happyDNS (2020)
contact@happydns.org
This software is a computer program whose purpose is to provide a modern
interface to interact with DNS systems.
This software is governed by the CeCILL license under French law and abiding
by the rules of distribution of free software. You can use, modify and/or
redistribute the software under the terms of the CeCILL license as
circulated by CEA, CNRS and INRIA at the following URL
"http://www.cecill.info".
As a counterpart to the access to the source code and rights to copy, modify
and redistribute granted by the license, users are provided only with a
limited warranty and the software's author, the holder of the economic
rights, and the successive licensors have only limited liability.
In this respect, the user's attention is drawn to the risks associated with
loading, using, modifying and/or developing or reproducing the software by
the user in light of its specific status of free software, that may mean
that it is complicated to manipulate, and that also therefore means that it
is reserved for developers and experienced professionals having in-depth
computer knowledge. Users are therefore encouraged to load and test the
software's suitability as regards their requirements in conditions enabling
the security of their systems and/or data to be ensured and, more generally,
to use and operate it in the same conditions as regards security.
The fact that you are presently reading this means that you have had
knowledge of the CeCILL license and that you accept its terms.
-->
<template>
<b-container style="margin-top: 10vh; margin-bottom: 10vh;">
<b-alert v-if="error !== null" variant="danger" :show="error.length > 0">
{{ error }}
</b-alert>
<div v-if="isLoading" class="text-center">
<b-spinner variant="primary" label="Spinning" class="mr-3" /> Please wait
</div>
<b-form v-else ref="form" class="mt-2" @submit.stop.prevent="goResend">
<p>
In order to validate your e-mail address, please check your inbox, and follow the link contained in the message.
</p>
<p>
If you need a new confirmation e-mail, just enter your address in the form below.
</p>
<b-form-row>
<label for="email-input" class="col-md-4 col-form-label text-truncate text-md-right font-weight-bold">Email address</label>
<b-col md="6">
<b-form-input
id="email-input"
ref="signupemail"
v-model="email"
:state="emailState"
required
autofocus
type="email"
placeholder="jPostel@isi.edu"
autocomplete="username"
/>
</b-col>
</b-form-row>
<b-form-row class="mt-3">
<b-button class="offset-sm-4 col-sm-4" type="submit" variant="primary">
Re-send the confirmation e-mail
</b-button>
</b-form-row>
</b-form>
</b-container>
</template>
<script>
import axios from 'axios'
export default {
data: function () {
return {
email: '',
emailState: null,
error: null
}
},
computed: {
isLoading () {
return this.error === null
}
},
mounted () {
if (this.$route.query.u) {
axios
.post('/api/users/' + encodeURIComponent(this.$route.query.u) + '/email', {
key: this.$route.query.k
})
.then(
(response) => {
this.error = ''
this.$root.$bvToast.toast(
'Ready to login!', {
title: 'Your new e-mail address is now validated!',
autoHideDelay: 5000,
variant: 'success',
toaster: 'b-toaster-content-right'
}
)
this.$router.push('/login')
},
(error) => {
this.error = error.response.data.errmsg
}
)
} else {
this.error = ''
}
},
methods: {
goResend () {
const valid = this.$refs.form.checkValidity()
this.emailState = valid
if (valid) {
axios
.patch('/api/users', {
email: this.email
})
.then(
(response) => {
this.$root.$bvToast.toast(
'Please check your inbox in order to valiate your e-mail address.', {
title: 'Confirmation e-mail sent!',
autoHideDelay: 5000,
variant: 'success',
toaster: 'b-toaster-content-right'
}
)
this.$router.push('/email-validation')
},
(error) => {
this.$bvToast.toast(
error.response.data.errmsg, {
title: 'Registration problem',
autoHideDelay: 5000,
variant: 'danger',
toaster: 'b-toaster-content-right'
}
)
}
)
}
}
}
}
</script>

View File

@ -32,6 +32,10 @@
package happydns
import (
"crypto/hmac"
"crypto/sha512"
"encoding/base64"
"fmt"
"time"
"golang.org/x/crypto/bcrypt"
@ -42,6 +46,7 @@ type User struct {
Email string
Password []byte
RegistrationTime *time.Time
EmailValidated *time.Time
}
type Users []*User
@ -69,3 +74,30 @@ func (u *User) DefinePassword(password string) (err error) {
func (u *User) CheckAuth(password string) bool {
return bcrypt.CompareHashAndPassword(u.Password, []byte(password)) == nil
}
const RegistrationHashValidity = 24 * time.Hour
func (u *User) GenRegistrationHash(previous bool) string {
date := time.Now()
if previous {
date = date.Add(RegistrationHashValidity * -1)
}
date = date.Truncate(RegistrationHashValidity)
return base64.StdEncoding.EncodeToString(
hmac.New(
sha512.New,
[]byte(u.RegistrationTime.Format(time.RFC3339Nano)),
).Sum(date.AppendFormat([]byte{}, time.RFC3339)),
)
}
func (u *User) ValidateEmail(key string) error {
if key == u.GenRegistrationHash(false) || key == u.GenRegistrationHash(true) {
now := time.Now()
u.EmailValidated = &now
return nil
}
return fmt.Errorf("The validation address link you follow is invalid or has expired (it is valid during %d hours)", RegistrationHashValidity/time.Hour)
}

View File

@ -137,6 +137,20 @@ func init() {
fwd_request(w, r, opts.DevProxy)
}
})
api.Router().GET("/email-validation", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
opts := r.Context().Value("opts").(*config.Options)
if opts.DevProxy == "" {
if data, err := Asset("htdocs/dist/index.html"); err != nil {
fmt.Fprintf(w, "{\"errmsg\":%q}", err)
} else {
w.Write(data)
}
} else {
r.URL.Path = "/"
fwd_request(w, r, opts.DevProxy)
}
})
api.Router().GET("/join", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
opts := r.Context().Value("opts").(*config.Options)

View File

@ -164,6 +164,7 @@ const mailHTMLTpl = `
</html>`
const mailTXTTpl = `{{ .Content }}
--
--
Fred - customer support @ happyDNS
Legal notice: https://www.happydns.org/en/legal-notice/`