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

Completed email login/logout flow.

parent 04bfcb72
package myirmaserver
import (
"html/template"
"net/smtp"
"strings"
......@@ -61,10 +62,14 @@ type Configuration struct {
KeyshareAttributes []irma.AttributeTypeIdentifier
// Configuration for email sending during login (email address use will be disabled if not present)
EmailServer string
EmailAuth smtp.Auth
EmailFrom string
DefaultLanguage string
EmailServer string
EmailAuth smtp.Auth
EmailFrom string
DefaultLanguage string
LoginEmailFiles map[string]string
LoginEmailTemplates map[string]*template.Template
LoginEmailSubject map[string]string
LoginEmailBaseURL map[string]string
// Logging verbosity level: 0 is normal, 1 includes DEBUG level, 2 includes TRACE level
Verbose int `json:"verbose" mapstructure:"verbose"`
......@@ -117,11 +122,29 @@ func processConfiguration(conf *Configuration) error {
}
}
// TODO: Setup email templates
// Setup email templates
if conf.EmailServer != "" && conf.LoginEmailTemplates == nil {
conf.LoginEmailTemplates = map[string]*template.Template{}
for lang, templateFile := range conf.LoginEmailFiles {
var err error
conf.LoginEmailTemplates[lang], err = template.ParseFiles(templateFile)
if err != nil {
return server.LogError(err)
}
}
}
// Verify email configuration
if conf.EmailServer != "" {
// TODO
if _, ok := conf.LoginEmailTemplates[conf.DefaultLanguage]; !ok {
return server.LogError(errors.Errorf("Missing login email template for default language"))
}
if _, ok := conf.LoginEmailSubject[conf.DefaultLanguage]; !ok {
return server.LogError(errors.Errorf("Missing login email subject for default language"))
}
if _, ok := conf.LoginEmailBaseURL[conf.DefaultLanguage]; !ok {
return server.LogError(errors.Errorf("Missing login email base url for default language"))
}
}
// Setup database
......
......@@ -3,30 +3,44 @@ package myirmaserver
import (
"errors"
"sync"
"time"
)
var (
ErrUserAlreadyExists = errors.New("Cannot create user, username already taken")
ErrUserNotFound = errors.New("Could not find specified user")
ErrInvalidRecord = errors.New("Invalid record in database")
ErrUserNotFound = errors.New("Could not find specified user")
)
type MyirmaDB interface {
GetUserID(username string) (int64, error)
AddEmailLoginToken(email, token string) error
LoginTokenGetCandidates(token string) ([]LoginCandidate, error)
LoginTokenGetEmail(token string) (string, error)
TryUserLoginToken(token, username string) (bool, error)
}
type LoginCandidate struct {
Username string `json:"username"`
LastActive int64 `json:"last_active"`
}
type MemoryUserData struct {
ID int64
ID int64
Email string
LastActive time.Time
}
type MyirmaMemoryDB struct {
lock sync.Mutex
UserData map[string]MemoryUserData
LoginEmailTokens map[string]string
}
func NewMyirmaMemoryDB() MyirmaDB {
return &MyirmaMemoryDB{
UserData: map[string]MemoryUserData{},
UserData: map[string]MemoryUserData{},
LoginEmailTokens: map[string]string{},
}
}
......@@ -39,3 +53,73 @@ func (db *MyirmaMemoryDB) GetUserID(username string) (int64, error) {
}
return data.ID, nil
}
func (db *MyirmaMemoryDB) AddEmailLoginToken(email, token string) error {
db.lock.Lock()
defer db.lock.Unlock()
found := false
for _, v := range db.UserData {
if v.Email == email {
found = true
break
}
}
if !found {
return ErrUserNotFound
}
db.LoginEmailTokens[token] = email
return nil
}
func (db *MyirmaMemoryDB) LoginTokenGetCandidates(token string) ([]LoginCandidate, error) {
db.lock.Lock()
defer db.lock.Unlock()
email, ok := db.LoginEmailTokens[token]
if !ok {
return nil, ErrUserNotFound
}
result := []LoginCandidate{}
for k, v := range db.UserData {
if v.Email == email {
result = append(result, LoginCandidate{Username: k, LastActive: v.LastActive.Unix()})
}
}
return result, nil
}
func (db *MyirmaMemoryDB) LoginTokenGetEmail(token string) (string, error) {
db.lock.Lock()
defer db.lock.Unlock()
v, ok := db.LoginEmailTokens[token]
if !ok {
return "", ErrUserNotFound
}
return v, nil
}
func (db *MyirmaMemoryDB) TryUserLoginToken(token, username string) (bool, error) {
db.lock.Lock()
defer db.lock.Unlock()
email, ok := db.LoginEmailTokens[token]
if !ok {
return false, nil
}
user, ok := db.UserData[username]
if !ok {
return false, ErrUserNotFound
}
if user.Email == email {
delete(db.LoginEmailTokens, token)
return true, nil
} else {
return false, nil
}
}
package myirmaserver
import (
"bytes"
"encoding/json"
"io/ioutil"
"net/http"
"time"
......@@ -56,6 +59,10 @@ func (s *Server) Handler() http.Handler {
router := chi.NewRouter()
router.Post("/checksession", s.handleCheckSession)
router.Post("/login/irma", s.handleIrmaLogin)
router.Post("/login/email", s.handleEmailLogin)
router.Post("/login/token/candidates", s.handleGetCandidates)
router.Post("/login/token", s.handleTokenLogin)
router.Post("/logout", s.handleLogout)
router.Mount("/irma/", s.sessionserver.HandlerFunc())
if s.conf.StaticPath != "" {
......@@ -90,6 +97,161 @@ func (s *Server) handleCheckSession(w http.ResponseWriter, r *http.Request) {
}
}
type EmailLoginRequest struct {
Email string `json:"email"`
Language string `json:"language"`
}
func (s *Server) handleEmailLogin(w http.ResponseWriter, r *http.Request) {
if s.conf.EmailServer == "" {
server.WriteError(w, server.ErrorInternal, "not enabled in configuration")
return
}
requestData, err := ioutil.ReadAll(r.Body)
if err != nil {
s.conf.Logger.WithField("error", err).Info("Malformed request: could not read request body")
server.WriteError(w, server.ErrorInvalidRequest, err.Error())
return
}
var request EmailLoginRequest
err = json.Unmarshal(requestData, &request)
if err != nil {
s.conf.Logger.WithField("error", err).Info("Malformed request: could not parse request body")
server.WriteError(w, server.ErrorInvalidRequest, err.Error())
return
}
token := server.NewSessionToken()
err = s.db.AddEmailLoginToken(request.Email, token)
if err == ErrUserNotFound {
server.WriteError(w, server.ErrorUserNotRegistered, "")
return
} else if err != nil {
s.conf.Logger.WithField("error", err).Error("Error adding login token to database")
server.WriteError(w, server.ErrorInternal, err.Error())
return
}
template, ok := s.conf.LoginEmailTemplates[request.Language]
if !ok {
template = s.conf.LoginEmailTemplates[s.conf.DefaultLanguage]
}
subject, ok := s.conf.LoginEmailSubject[request.Language]
if !ok {
subject = s.conf.LoginEmailSubject[s.conf.DefaultLanguage]
}
baseURL, ok := s.conf.LoginEmailBaseURL[request.Language]
if !ok {
baseURL = s.conf.LoginEmailBaseURL[s.conf.DefaultLanguage]
}
var emsg bytes.Buffer
err = template.Execute(&emsg, map[string]string{"TokenURL": baseURL + token})
if err != nil {
s.conf.Logger.WithField("error", err).Error("Could not generate login mail from template")
server.WriteError(w, server.ErrorInternal, err.Error())
return
}
err = server.SendHTMLMail(
s.conf.EmailServer,
s.conf.EmailAuth,
s.conf.EmailFrom,
request.Email,
subject,
emsg.Bytes())
if err != nil {
s.conf.Logger.WithField("error", err).Error("Could not send login mail")
server.WriteError(w, server.ErrorInternal, err.Error())
return
}
w.WriteHeader(http.StatusNoContent) // No need for content.
}
func (s *Server) handleGetCandidates(w http.ResponseWriter, r *http.Request) {
requestData, err := ioutil.ReadAll(r.Body)
if err != nil {
s.conf.Logger.WithField("error", err).Info("Malformed request: could not read body")
server.WriteError(w, server.ErrorInvalidRequest, "could not read request body")
return
}
token := string(requestData)
candidates, err := s.db.LoginTokenGetCandidates(token)
if err == ErrUserNotFound {
server.WriteError(w, server.ErrorInvalidRequest, "token invalid")
return
} else if err != nil {
s.conf.Logger.WithField("error", err).Error("Could not retrieve candidates for token")
server.WriteError(w, server.ErrorInternal, err.Error())
return
}
server.WriteJson(w, candidates)
}
type TokenLoginRequest struct {
Token string `json:"token"`
Username string `json:"username"`
}
func (s *Server) handleTokenLogin(w http.ResponseWriter, r *http.Request) {
requestData, err := ioutil.ReadAll(r.Body)
if err != nil {
s.conf.Logger.WithField("error", err).Info("Malformed request: could not read body")
server.WriteError(w, server.ErrorInvalidRequest, "could not read request body")
return
}
var request TokenLoginRequest
err = json.Unmarshal(requestData, &request)
if err != nil {
s.conf.Logger.WithField("error", err).Info("Malformed request: could not parse request body")
server.WriteError(w, server.ErrorInvalidRequest, err.Error())
return
}
ok, err := s.db.TryUserLoginToken(request.Token, request.Username)
if err == ErrUserNotFound {
server.WriteError(w, server.ErrorInvalidRequest, "Invalid login request")
return
}
if err != nil {
s.conf.Logger.WithField("error", err).Error("Could not login user using token")
server.WriteError(w, server.ErrorInvalidRequest, err.Error())
return
}
if !ok {
server.WriteError(w, server.ErrorInvalidRequest, "Invalid login request")
return
}
session := s.store.create()
session.userID = new(int64)
*session.userID, err = s.db.GetUserID(request.Username) // username is trusted, since it was validated by s.db.TryUserLoginToken
if err != nil {
s.conf.Logger.WithField("error", err).Error("Could not fetch userid for username validated in earlier step")
server.WriteError(w, server.ErrorInternal, err.Error())
return
}
token := session.token
http.SetCookie(w, &http.Cookie{
Name: "session",
Value: token,
MaxAge: s.conf.SessionLifetime,
Secure: s.conf.Production,
Path: "/",
HttpOnly: true,
})
w.WriteHeader(http.StatusNoContent)
}
func (s *Server) handleIrmaLogin(w http.ResponseWriter, r *http.Request) {
session := s.store.create()
sessiontoken := session.token
......@@ -112,6 +274,7 @@ func (s *Server) handleIrmaLogin(w http.ResponseWriter, r *http.Request) {
session.pendingErrorMessage = ""
return
} else if err != nil {
s.conf.Logger.WithField("error", err).Error("Error during processing of login irma session result")
session.pendingError = &server.ErrorInternal
session.pendingErrorMessage = err.Error()
return
......@@ -122,6 +285,7 @@ func (s *Server) handleIrmaLogin(w http.ResponseWriter, r *http.Request) {
})
if err != nil {
s.conf.Logger.WithField("error", err).Error("Error during startup of irma session for login")
server.WriteError(w, server.ErrorInternal, err.Error())
return
}
......@@ -137,6 +301,17 @@ func (s *Server) handleIrmaLogin(w http.ResponseWriter, r *http.Request) {
server.WriteJson(w, qr)
}
func (s *Server) handleLogout(w http.ResponseWriter, r *http.Request) {
http.SetCookie(w, &http.Cookie{
Name: "session",
Value: "",
Secure: s.conf.Production,
Path: "/",
HttpOnly: true,
})
w.WriteHeader(http.StatusNoContent)
}
func (s *Server) StaticFilesHandler() http.Handler {
return http.StripPrefix(s.conf.StaticPrefix, http.FileServer(http.Dir(s.conf.StaticPath)))
}
......@@ -10,9 +10,11 @@ func main() {
db := &myirmaserver.MyirmaMemoryDB{
UserData: map[string]myirmaserver.MemoryUserData{
"rgBpfxdwfE": myirmaserver.MemoryUserData{
ID: 1,
ID: 1,
Email: "test@test.com",
},
},
LoginEmailTokens: map[string]string{},
}
s, err := myirmaserver.New(&myirmaserver.Configuration{
URL: "http://127.0.0.1:8080",
......@@ -20,6 +22,12 @@ func main() {
StaticPrefix: "/test/",
DB: db,
KeyshareAttributeNames: []string{"pbdf.sidn-pbdf.irma.pseudonym"},
EmailServer: "localhost:1025",
EmailFrom: "test@example.com",
DefaultLanguage: "en",
LoginEmailFiles: map[string]string{"en": "testtemplate.html"},
LoginEmailSubject: map[string]string{"en": "Login MyIRMA"},
LoginEmailBaseURL: map[string]string{"en": "http://127.0.0.1:8080/test/#token="},
})
if err != nil {
panic(err)
......
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