Commit eb225204 authored by David Venhoek's avatar David Venhoek Committed by Sietse Ringers
Browse files

Added email verification generation to the registration process.

parent c7f07e06
package server
import "net/smtp"
func SendHTMLMail(addr string, a smtp.Auth, from, to, subject string, msg []byte) error {
headers := []byte("To: " + to + "\r\n" +
"From: " + from + "\r\n" +
"Subject: " + subject + "\r\n" +
"Content-Type: text/html; charset=UTF-8\r\n" +
"Content-Transfer-Encoding: binary\r\n" +
"\r\n")
return smtp.SendMail(addr, a, from, []string{to}, append(headers, msg...))
}
DROP SCHEMA irma CASCADE;
\ No newline at end of file
......@@ -2,7 +2,9 @@ package keyshareserver
import (
"encoding/binary"
"html/template"
"io/ioutil"
"net/smtp"
"os"
"strings"
......@@ -68,6 +70,16 @@ type Configuration struct {
KeyshareCredential string
KeyshareAttribute string
// Configuration for email sending during registration (email address use will be disabled if not present)
EmailServer string
EmailAuth smtp.Auth
EmailFrom string
RegistrationEmailFiles map[string]string
RegistrationEmailTemplates map[string]*template.Template
RegistrationEmailSubject map[string]string
VerificationURL map[string]string
DefaultLanguage string
// Logging verbosity level: 0 is normal, 1 includes DEBUG level, 2 includes TRACE level
Verbose int `json:"verbose" mapstructure:"verbose"`
// Don't log anything at all
......@@ -128,6 +140,31 @@ func processConfiguration(conf *Configuration) (*keysharecore.KeyshareCore, erro
// Force production status to match
conf.ServerConfiguration.Production = conf.Production
// Setup email templates
if conf.EmailServer != "" && conf.RegistrationEmailTemplates == nil {
conf.RegistrationEmailTemplates = map[string]*template.Template{}
for lang, templateFile := range conf.RegistrationEmailFiles {
var err error
conf.RegistrationEmailTemplates[lang], err = template.ParseFiles(templateFile)
if err != nil {
return nil, server.LogError(err)
}
}
}
// Verify email configuration
if conf.EmailServer != "" {
if _, ok := conf.RegistrationEmailTemplates[conf.DefaultLanguage]; !ok {
return nil, server.LogError(errors.Errorf("Missing registration email template for default language"))
}
if _, ok := conf.RegistrationEmailSubject[conf.DefaultLanguage]; !ok {
return nil, server.LogError(errors.Errorf("Missing registration email subject for default language"))
}
if _, ok := conf.VerificationURL[conf.DefaultLanguage]; !ok {
return nil, server.LogError(errors.Errorf("Missing verification base url for default lanaguage"))
}
}
// Load configuration (because server setup needs this to be in place)
if conf.ServerConfiguration.IrmaConfiguration == nil {
var (
......
......@@ -30,7 +30,7 @@ const (
)
type KeyshareDB interface {
NewUser(user KeyshareUserData) error
NewUser(user KeyshareUserData) (KeyshareUser, error)
User(username string) (KeyshareUser, error)
UpdateUser(user KeyshareUser) error
......@@ -40,6 +40,8 @@ type KeyshareDB interface {
SetSeen(user KeyshareUser) error
AddLog(user KeyshareUser, eventType LogEntryType, param interface{}) error
AddEmailVerification(user KeyshareUser, emailAddress, token string) error
}
type KeyshareUser interface {
......@@ -81,7 +83,7 @@ func (db *keyshareMemoryDB) User(username string) (KeyshareUser, error) {
return &keyshareMemoryUser{KeyshareUserData{Username: username, Coredata: data}}, nil
}
func (db *keyshareMemoryDB) NewUser(user KeyshareUserData) error {
func (db *keyshareMemoryDB) NewUser(user KeyshareUserData) (KeyshareUser, error) {
// Ensure access to database is single-threaded
db.lock.Lock()
defer db.lock.Unlock()
......@@ -89,10 +91,10 @@ func (db *keyshareMemoryDB) NewUser(user KeyshareUserData) error {
// Check and insert user
_, exists := db.users[user.Username]
if exists {
return ErrUserAlreadyExists
return nil, ErrUserAlreadyExists
}
db.users[user.Username] = user.Coredata
return nil
return &keyshareMemoryUser{KeyshareUserData: user}, nil
}
func (db *keyshareMemoryDB) UpdateUser(user KeyshareUser) error {
......@@ -132,13 +134,17 @@ func (db *keyshareMemoryDB) AddLog(user KeyshareUser, eventType LogEntryType, pa
return nil
}
func (db *keyshareMemoryDB) AddEmailVerification(user KeyshareUser, emailAddress, token string) error {
return nil
}
type keysharePostgresDatabase struct {
db *sql.DB
}
type keysharePostgresUser struct {
KeyshareUserData
id int
id int64
}
func (m *keysharePostgresUser) Data() *KeyshareUserData {
......@@ -158,19 +164,24 @@ func NewPostgresDatabase(connstring string) (KeyshareDB, error) {
}, nil
}
func (db *keysharePostgresDatabase) NewUser(user KeyshareUserData) error {
res, err := db.db.Exec("INSERT INTO irma.users (username, coredata, pinCounter, pinBlockDate) VALUES ($1, $2, 0, 0);", user.Username, user.Coredata[:])
func (db *keysharePostgresDatabase) NewUser(user KeyshareUserData) (KeyshareUser, error) {
res, err := db.db.Query("INSERT INTO irma.users (username, coredata, lastSeen, pinCounter, pinBlockDate) VALUES ($1, $2, $3, 0, 0) RETURNING id",
user.Username,
user.Coredata[:],
time.Now().Unix())
if err != nil {
return err
return nil, err
}
c, err := res.RowsAffected()
if err != nil {
return err
defer res.Close()
if !res.Next() {
return nil, ErrUserAlreadyExists
}
if c == 0 {
return ErrUserAlreadyExists
var id int64
err = res.Scan(&id)
if err != nil {
return nil, err
}
return nil
return &keysharePostgresUser{KeyshareUserData: user, id: id}, nil
}
func (db *keysharePostgresDatabase) User(username string) (KeyshareUser, error) {
......@@ -335,3 +346,16 @@ func (db *keysharePostgresDatabase) AddLog(user KeyshareUser, eventType LogEntry
userdata.id)
return err
}
func (db *keysharePostgresDatabase) AddEmailVerification(user KeyshareUser, emailAddress, token string) error {
userdata, ok := user.(*keysharePostgresUser)
if !ok {
return ErrInvalidData
}
_, err := db.db.Exec("INSERT INTO irma.email_verification_tokens (token, email, user_id) VALUES ($1, $2, $3)",
token,
emailAddress,
userdata.id)
return err
}
CREATE SCHEMA irma;
CREATE TABLE IF NOT EXISTS irma.users
(
id serial PRIMARY KEY,
username varchar(128),
coredata bytea,
lastSeen bigint,
pinCounter int,
pinBlockDate bigint
username text NOT NULL,
coredata bytea NOT NULL,
lastSeen bigint NOT NULL,
pinCounter int NOT NULL,
pinBlockDate bigint NOT NULL
);
CREATE UNIQUE INDEX username_index ON irma.users (username);
GRANT ALL PRIVILEGES ON TABLE irma.users TO irma;
CREATE TABLE IF NOT EXISTS irma.log_entry_records
(
id serial PRIMARY KEY,
time bigint,
event varchar(256),
time bigint NOT NULL,
event text NOT NULL,
param text,
user_id int
user_id int NOT NULL
);
CREATE INDEX log_entry_records_user_id_index ON irma.log_entry_records (user_id, time);
CREATE TABLE IF NOT EXISTS irma.email_verification_tokens
(
id serial PRIMARY KEY,
token text NOT NULL,
email text NOT NULL,
user_id int NOT NULL
);
CREATE UNIQUE INDEX email_verification_token_index ON irma.email_verification_tokens (token);
CREATE TABLE IF NOT EXISTS irma.email_addresses
(
id serial PRIMARY KEY,
user_id int NOT NULL,
emailAddress text NOT NULL
);
CREATE INDEX log_entry_records_user_id_index ON irma.log_entry_records (user_id);
GRANT ALL PRIVILEGES ON TABLE irma.log_entry_records TO irma;
\ No newline at end of file
CREATE INDEX emailAddress_index ON irma.email_addresses (emailAddress);
CREATE INDEX emailAddress_userid_index ON irma.email_addresses (user_id);
package keyshareserver
import (
"bytes"
"crypto/rand"
"encoding/base32"
"encoding/base64"
"encoding/json"
"fmt"
......@@ -448,13 +450,66 @@ func (s *Server) handleRegister(w http.ResponseWriter, r *http.Request) {
server.WriteError(w, server.ErrorInvalidRequest, err.Error())
return
}
err = s.db.NewUser(KeyshareUserData{Username: username, Coredata: coredata})
user, err := s.db.NewUser(KeyshareUserData{Username: username, Coredata: coredata})
if err != nil {
s.conf.Logger.WithField("error", err).Error("Could not store new user in database")
server.WriteError(w, server.ErrorInternal, err.Error())
return
}
// Send email if user specified email address
if msg.Email != nil && s.conf.EmailServer != "" {
// Fetch template and configuration data for users language, falling back if needed
template, ok := s.conf.RegistrationEmailTemplates[msg.Language]
if !ok {
template = s.conf.RegistrationEmailTemplates[s.conf.DefaultLanguage]
}
verificationBaseURL, ok := s.conf.VerificationURL[msg.Language]
if !ok {
verificationBaseURL = s.conf.VerificationURL[s.conf.DefaultLanguage]
}
subject, ok := s.conf.RegistrationEmailSubject[msg.Language]
if !ok {
subject = s.conf.RegistrationEmailSubject[s.conf.DefaultLanguage]
}
// Generate token
tokenData := make([]byte, 35)
_, err = rand.Read(tokenData)
if err != nil {
s.conf.Logger.WithField("error", err).Error("Could not generate email verification token")
server.WriteError(w, server.ErrorInternal, err.Error())
return
}
token := base32.StdEncoding.EncodeToString(tokenData)
// Add it to the database
err = s.db.AddEmailVerification(user, *msg.Email, token)
if err != nil {
s.conf.Logger.WithField("error", err).Error("Could not add email verification record to user")
server.WriteError(w, server.ErrorInternal, err.Error())
return
}
// Build message
var emsg bytes.Buffer
err = template.Execute(&emsg, map[string]string{"VerificationURL": verificationBaseURL + token})
if err != nil {
s.conf.Logger.WithField("error", err).Error("Could not generate email verifcation mail")
server.WriteError(w, server.ErrorInternal, err.Error())
return
}
// And send it
err = server.SendHTMLMail(
s.conf.EmailServer,
s.conf.EmailAuth,
s.conf.EmailFrom,
*msg.Email,
subject,
emsg.Bytes())
}
// Setup and return issuance session for keyshare credential.
request := irma.NewIssuanceRequest([]*irma.CredentialRequest{
{
......
......@@ -17,6 +17,18 @@ func main() {
StoragePrimaryKeyFile: "storagekey",
KeyshareCredential: "test.test.mijnirma",
KeyshareAttribute: "email",
RegistrationEmailSubject: map[string]string{
"en": "Test",
},
RegistrationEmailFiles: map[string]string{
"en": "registration.html",
},
DefaultLanguage: "en",
VerificationURL: map[string]string{
"en": "http://example.com/verify/",
},
EmailServer: "localhost:1025",
EmailFrom: "test@example.com",
})
if err != nil {
......
<p>Welcome to irma</p>
<p><a href="{{.VerificationURL}}"> Click here to verify your account </a> or paste the following in your browser: {{.VerificationURL}}</p>
\ No newline at end of file
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment