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 ...@@ -2,7 +2,9 @@ package keyshareserver
import ( import (
"encoding/binary" "encoding/binary"
"html/template"
"io/ioutil" "io/ioutil"
"net/smtp"
"os" "os"
"strings" "strings"
...@@ -68,6 +70,16 @@ type Configuration struct { ...@@ -68,6 +70,16 @@ type Configuration struct {
KeyshareCredential string KeyshareCredential string
KeyshareAttribute 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 // Logging verbosity level: 0 is normal, 1 includes DEBUG level, 2 includes TRACE level
Verbose int `json:"verbose" mapstructure:"verbose"` Verbose int `json:"verbose" mapstructure:"verbose"`
// Don't log anything at all // Don't log anything at all
...@@ -128,6 +140,31 @@ func processConfiguration(conf *Configuration) (*keysharecore.KeyshareCore, erro ...@@ -128,6 +140,31 @@ func processConfiguration(conf *Configuration) (*keysharecore.KeyshareCore, erro
// Force production status to match // Force production status to match
conf.ServerConfiguration.Production = conf.Production 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) // Load configuration (because server setup needs this to be in place)
if conf.ServerConfiguration.IrmaConfiguration == nil { if conf.ServerConfiguration.IrmaConfiguration == nil {
var ( var (
......
...@@ -30,7 +30,7 @@ const ( ...@@ -30,7 +30,7 @@ const (
) )
type KeyshareDB interface { type KeyshareDB interface {
NewUser(user KeyshareUserData) error NewUser(user KeyshareUserData) (KeyshareUser, error)
User(username string) (KeyshareUser, error) User(username string) (KeyshareUser, error)
UpdateUser(user KeyshareUser) error UpdateUser(user KeyshareUser) error
...@@ -40,6 +40,8 @@ type KeyshareDB interface { ...@@ -40,6 +40,8 @@ type KeyshareDB interface {
SetSeen(user KeyshareUser) error SetSeen(user KeyshareUser) error
AddLog(user KeyshareUser, eventType LogEntryType, param interface{}) error AddLog(user KeyshareUser, eventType LogEntryType, param interface{}) error
AddEmailVerification(user KeyshareUser, emailAddress, token string) error
} }
type KeyshareUser interface { type KeyshareUser interface {
...@@ -81,7 +83,7 @@ func (db *keyshareMemoryDB) User(username string) (KeyshareUser, error) { ...@@ -81,7 +83,7 @@ func (db *keyshareMemoryDB) User(username string) (KeyshareUser, error) {
return &keyshareMemoryUser{KeyshareUserData{Username: username, Coredata: data}}, nil 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 // Ensure access to database is single-threaded
db.lock.Lock() db.lock.Lock()
defer db.lock.Unlock() defer db.lock.Unlock()
...@@ -89,10 +91,10 @@ func (db *keyshareMemoryDB) NewUser(user KeyshareUserData) error { ...@@ -89,10 +91,10 @@ func (db *keyshareMemoryDB) NewUser(user KeyshareUserData) error {
// Check and insert user // Check and insert user
_, exists := db.users[user.Username] _, exists := db.users[user.Username]
if exists { if exists {
return ErrUserAlreadyExists return nil, ErrUserAlreadyExists
} }
db.users[user.Username] = user.Coredata db.users[user.Username] = user.Coredata
return nil return &keyshareMemoryUser{KeyshareUserData: user}, nil
} }
func (db *keyshareMemoryDB) UpdateUser(user KeyshareUser) error { func (db *keyshareMemoryDB) UpdateUser(user KeyshareUser) error {
...@@ -132,13 +134,17 @@ func (db *keyshareMemoryDB) AddLog(user KeyshareUser, eventType LogEntryType, pa ...@@ -132,13 +134,17 @@ func (db *keyshareMemoryDB) AddLog(user KeyshareUser, eventType LogEntryType, pa
return nil return nil
} }
func (db *keyshareMemoryDB) AddEmailVerification(user KeyshareUser, emailAddress, token string) error {
return nil
}
type keysharePostgresDatabase struct { type keysharePostgresDatabase struct {
db *sql.DB db *sql.DB
} }
type keysharePostgresUser struct { type keysharePostgresUser struct {
KeyshareUserData KeyshareUserData
id int id int64
} }
func (m *keysharePostgresUser) Data() *KeyshareUserData { func (m *keysharePostgresUser) Data() *KeyshareUserData {
...@@ -158,19 +164,24 @@ func NewPostgresDatabase(connstring string) (KeyshareDB, error) { ...@@ -158,19 +164,24 @@ func NewPostgresDatabase(connstring string) (KeyshareDB, error) {
}, nil }, nil
} }
func (db *keysharePostgresDatabase) NewUser(user KeyshareUserData) error { func (db *keysharePostgresDatabase) NewUser(user KeyshareUserData) (KeyshareUser, error) {
res, err := db.db.Exec("INSERT INTO irma.users (username, coredata, pinCounter, pinBlockDate) VALUES ($1, $2, 0, 0);", user.Username, user.Coredata[:]) 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 { if err != nil {
return err return nil, err
} }
c, err := res.RowsAffected() defer res.Close()
if err != nil { if !res.Next() {
return err return nil, ErrUserAlreadyExists
} }
if c == 0 { var id int64
return ErrUserAlreadyExists 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) { func (db *keysharePostgresDatabase) User(username string) (KeyshareUser, error) {
...@@ -335,3 +346,16 @@ func (db *keysharePostgresDatabase) AddLog(user KeyshareUser, eventType LogEntry ...@@ -335,3 +346,16 @@ func (db *keysharePostgresDatabase) AddLog(user KeyshareUser, eventType LogEntry
userdata.id) userdata.id)
return err 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 CREATE TABLE IF NOT EXISTS irma.users
( (
id serial PRIMARY KEY, id serial PRIMARY KEY,
username varchar(128), username text NOT NULL,
coredata bytea, coredata bytea NOT NULL,
lastSeen bigint, lastSeen bigint NOT NULL,
pinCounter int, pinCounter int NOT NULL,
pinBlockDate bigint pinBlockDate bigint NOT NULL
); );
CREATE UNIQUE INDEX username_index ON irma.users (username); 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 CREATE TABLE IF NOT EXISTS irma.log_entry_records
( (
id serial PRIMARY KEY, id serial PRIMARY KEY,
time bigint, time bigint NOT NULL,
event varchar(256), event text NOT NULL,
param text, 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); CREATE INDEX emailAddress_index ON irma.email_addresses (emailAddress);
GRANT ALL PRIVILEGES ON TABLE irma.log_entry_records TO irma; CREATE INDEX emailAddress_userid_index ON irma.email_addresses (user_id);
\ No newline at end of file
package keyshareserver package keyshareserver
import ( import (
"bytes"
"crypto/rand" "crypto/rand"
"encoding/base32"
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
"fmt" "fmt"
...@@ -448,13 +450,66 @@ func (s *Server) handleRegister(w http.ResponseWriter, r *http.Request) { ...@@ -448,13 +450,66 @@ func (s *Server) handleRegister(w http.ResponseWriter, r *http.Request) {
server.WriteError(w, server.ErrorInvalidRequest, err.Error()) server.WriteError(w, server.ErrorInvalidRequest, err.Error())
return return
} }
err = s.db.NewUser(KeyshareUserData{Username: username, Coredata: coredata}) user, err := s.db.NewUser(KeyshareUserData{Username: username, Coredata: coredata})
if err != nil { if err != nil {
s.conf.Logger.WithField("error", err).Error("Could not store new user in database") s.conf.Logger.WithField("error", err).Error("Could not store new user in database")
server.WriteError(w, server.ErrorInternal, err.Error()) server.WriteError(w, server.ErrorInternal, err.Error())
return 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. // Setup and return issuance session for keyshare credential.
request := irma.NewIssuanceRequest([]*irma.CredentialRequest{ request := irma.NewIssuanceRequest([]*irma.CredentialRequest{
{ {
......
...@@ -17,6 +17,18 @@ func main() { ...@@ -17,6 +17,18 @@ func main() {
StoragePrimaryKeyFile: "storagekey", StoragePrimaryKeyFile: "storagekey",
KeyshareCredential: "test.test.mijnirma", KeyshareCredential: "test.test.mijnirma",
KeyshareAttribute: "email", 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 { 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