Commit d1f6b9cb authored by ale's avatar ale
Browse files

Send an email when logging in from a new device

parent 138500f0
From: {{.Sender}}
To: {{.Recipient}}
Subject: Login from a new device
Content-Type: text/plain
Hello,
there has been a login to your account {{.Username}} on {{.PublicURL}}
from a new device:
OS: {{.Device.OS}} {% if .Device.Mobile %}(mobile){% endif %}
Browser: {{.Device.Browser}}
If you do not recognize this device, check your account's security
settings at
{{.PublicURL}}/account/security
Otherwise you can safely ignore this email.
......@@ -207,6 +207,15 @@ func (u *User) SendActivationEmail(mailer Mailer, publicURL string, actionMgr *A
return mailer.SendMail(u.Email, "activation.txt", map[string]interface{}{
"ActionURL": actionURL,
"PublicURL": publicURL,
"Username": u.Name,
})
}
func (u *User) SendNewDeviceLoginEmail(mailer Mailer, publicURL string, dev *DeviceInfo) error {
return mailer.SendMail(u.Email, "new_device_login.txt", map[string]interface{}{
"PublicURL": publicURL,
"Username": u.Name,
"Device": dev,
})
}
......@@ -27,7 +27,6 @@ const adminScope = "idpadmin"
type Service struct {
*web.Service
hc *hydra.Client
mailer idp.Mailer
actions *idp.ActionManager
publicURL string
}
......@@ -35,8 +34,7 @@ type Service struct {
// NewService returns a new Service.
func NewService(db idp.Database, tpl *template.Template, store sessions.Store, hc *hydra.Client, mailer idp.Mailer, actionMgr *idp.ActionManager, publicURL string, insecureCookies bool) *Service {
return &Service{
Service: web.NewService(db, tpl, store, insecureCookies),
mailer: mailer,
Service: web.NewService(db, tpl, store, mailer, insecureCookies),
publicURL: publicURL,
actions: actionMgr,
hc: hc,
......@@ -115,7 +113,7 @@ func (s *Service) handleCreateUser(w http.ResponseWriter, r *http.Request) {
if err := req.Validate(); err != nil {
log.WithFields(log.Fields{
"request": req,
"error": err,
"error": err,
}).Error("bad CreateUser request")
http.Error(w, err.Error(), http.StatusBadRequest)
return
......@@ -151,7 +149,7 @@ func (s *Service) handleCreateUser(w http.ResponseWriter, r *http.Request) {
// Database transaction has been committed, let's send
// the activation email. This has a lower chance of
// failing.
if err := user.SendActivationEmail(s.mailer, s.publicURL, s.actions); err != nil {
if err := user.SendActivationEmail(s.Mailer, s.publicURL, s.actions); err != nil {
log.WithFields(log.Fields{
"user": user.Name,
"error": err,
......
......@@ -49,11 +49,12 @@ func NewApp(hc *hydra.Client, config *Config) (*App, error) {
mailer := newTemplateMailer(config.EmailTemplate, config.SenderAddr, config.SenderName, config.Mailer)
store := sessions.NewCookieStore(config.CookieAuthKey, config.CookieEncKey)
actionMgr := idp.NewActionManager(config.ActionAuthKey, config.ActionEncKey)
publicURL := strings.TrimRight(config.PublicURL, "/")
loginSrv := login.NewService(config.Database, config.Template, store, config.InsecureCookies)
loginSrv := login.NewService(config.Database, config.Template, store, mailer, publicURL, config.InsecureCookies)
acct := mgmt.NewService(config.Database, config.Template, store, mailer, actionMgr, config.InsecureCookies)
admin := admin.NewService(config.Database, config.Template, store, hc, mailer, actionMgr, strings.TrimRight(config.PublicURL, "/"), config.InsecureCookies)
idpSrv, err := consent.NewService(config.Database, config.Template, store, hc, config.InsecureCookies)
admin := admin.NewService(config.Database, config.Template, store, hc, mailer, actionMgr, publicURL, config.InsecureCookies)
idpSrv, err := consent.NewService(config.Database, config.Template, store, hc, mailer, config.InsecureCookies)
if err != nil {
return nil, err
}
......
......@@ -28,9 +28,9 @@ type Service struct {
}
// NewService returns a new Service.
func NewService(db idp.Database, tpl *template.Template, store sessions.Store, hc *hydra.Client, insecureCookies bool) (*Service, error) {
func NewService(db idp.Database, tpl *template.Template, store sessions.Store, hc *hydra.Client, mailer idp.Mailer, insecureCookies bool) (*Service, error) {
s := &Service{
Service: web.NewService(db, tpl, store, insecureCookies),
Service: web.NewService(db, tpl, store, mailer, insecureCookies),
provider: NewProvider(hc, &ProviderConfig{
KeyCacheExpiration: keyCacheExpirationTime,
ClientCacheExpiration: clientCacheExpirationTime,
......
......@@ -46,20 +46,23 @@ func newDeviceInfoFromRequest(r *http.Request) *idp.DeviceInfo {
}
// GetDeviceInfoFromRequest will retrieve or create a DeviceInfo
// object for the given request. It will always return a valid object,
// unless there are database errors.
func GetDeviceInfoFromRequest(r *http.Request, store sessions.Store) *idp.DeviceInfo {
// object for the given request. It will always return a valid
// object. The boolean return value can be used to figure out if the
// DeviceInfo object is new or not.
func GetDeviceInfoFromRequest(r *http.Request, store sessions.Store) (*idp.DeviceInfo, bool) {
session, _ := store.Get(r, deviceSessionName)
session.Options = &sessions.Options{
Path: "/",
MaxAge: tenYears,
}
isNew := false
devInfo, ok := session.Values["info"].(*idp.DeviceInfo)
if !ok || devInfo == nil {
devInfo = newDeviceInfoFromRequest(r)
session.Values["info"] = devInfo
isNew = true
}
return devInfo
return devInfo, isNew
}
......@@ -46,18 +46,20 @@ func init() {
type Service struct {
*web.Service
loginSessionOptions *sessions.Options
publicURL string
}
// NewService returns a new Service.
func NewService(db idp.Database, tpl *template.Template, store sessions.Store, insecureCookies bool) *Service {
func NewService(db idp.Database, tpl *template.Template, store sessions.Store, mailer idp.Mailer, publicURL string, insecureCookies bool) *Service {
return &Service{
Service: web.NewService(db, tpl, store, insecureCookies),
Service: web.NewService(db, tpl, store, mailer, insecureCookies),
loginSessionOptions: &sessions.Options{
Path: "/auth/",
MaxAge: loginSessionTTL,
HttpOnly: true,
Secure: !insecureCookies,
},
publicURL: publicURL,
}
}
......@@ -191,18 +193,40 @@ func (s *Service) handleOTP(w http.ResponseWriter, r *http.Request, ls *loginSes
}
func (s *Service) successfulLogin(w http.ResponseWriter, r *http.Request, username string) error {
// Check out the device information.
dev, isNew := web.GetDeviceInfoFromRequest(r, s.Store)
// Update LastLogin and log the successful login.
var user *idp.User
if err := s.DB.Do(r.Context(), func(txn idp.Txn) error {
user, err := txn.GetUser(username)
var err error
user, err = txn.GetUser(username)
if err != nil {
return err
}
s.AddUserLogEntry(r, txn, user, idp.UserLogTypeLogin, "successful login")
s.AddUserLogEntryWithDevice(r, txn, user, dev, idp.UserLogTypeLogin, "successful login")
user.LastLoginAt = time.Now().UTC()
if err := txn.UpdateUser(user); err != nil {
return err
}
return txn.Commit()
}); err != nil {
return err
}
// If it is the first time we have seen this device (checked
// on the client), send the user a warning email.
if isNew {
if err := user.SendNewDeviceLoginEmail(s.Mailer, s.publicURL, dev); err != nil {
// Errors here do not prevent the login from succeeding.
log.WithFields(log.Fields{
"user": username,
"error": err,
}).Error("could not send new login notification")
}
}
// Set up the authentication session.
as, _ := s.GetAuthSession(r)
if as == nil {
return errors.New("couldn't create auth session")
......
......@@ -16,15 +16,13 @@ import (
type Service struct {
*web.Service
actions *idp.ActionManager
mailer idp.Mailer
}
// NewService creates a new account management web service.
func NewService(db idp.Database, tpl *template.Template, store sessions.Store, mailer idp.Mailer, actionMgr *idp.ActionManager, insecureCookies bool) *Service {
return &Service{
Service: web.NewService(db, tpl, store, insecureCookies),
Service: web.NewService(db, tpl, store, mailer, insecureCookies),
actions: actionMgr,
mailer: mailer,
}
}
......
......@@ -57,7 +57,7 @@ func (s *Service) handlePasswordResetRequest(w http.ResponseWriter, r *http.Requ
log.Printf("error signing action URL: %v", err)
goto silentFail
}
if err := s.mailer.SendMail(user.Email, "password_reset.txt", map[string]interface{}{
if err := s.Mailer.SendMail(user.Email, "password_reset.txt", map[string]interface{}{
"ActionURL": actionURL,
"Username": form.Username,
}); err != nil {
......
......@@ -25,12 +25,13 @@ type Service struct {
tpl *template.Template
authSessionOptions *sessions.Options
DB idp.Database
Store sessions.Store
DB idp.Database
Store sessions.Store
Mailer idp.Mailer
}
// NewService returns a new Service object.
func NewService(db idp.Database, tpl *template.Template, store sessions.Store, insecureCookies bool) *Service {
func NewService(db idp.Database, tpl *template.Template, store sessions.Store, mailer idp.Mailer, insecureCookies bool) *Service {
return &Service{
tpl: tpl,
authSessionOptions: &sessions.Options{
......@@ -38,8 +39,9 @@ func NewService(db idp.Database, tpl *template.Template, store sessions.Store, i
HttpOnly: true,
Secure: !insecureCookies,
},
DB: db,
Store: store,
DB: db,
Store: store,
Mailer: mailer,
}
}
......@@ -198,8 +200,13 @@ func (s *Service) RenderJSON(w http.ResponseWriter, r *http.Request, obj interfa
// AddUserLogEntry creates a new UserLogEntry and stores it in the database.
func (s *Service) AddUserLogEntry(r *http.Request, txn idp.Txn, user *idp.User, logType idp.UserLogType, message string) {
dev := GetDeviceInfoFromRequest(r, s.Store)
dev, _ := GetDeviceInfoFromRequest(r, s.Store)
s.AddUserLogEntryWithDevice(r, txn, user, dev, logType, message)
}
// AddUserLogEntryWithDevice creates a new UserLogEntry with the
// specified DeviceInfo.
func (s *Service) AddUserLogEntryWithDevice(r *http.Request, txn idp.Txn, user *idp.User, dev *idp.DeviceInfo, logType idp.UserLogType, message string) {
log.WithFields(log.Fields{
"user": user.Name,
"log_type": logType,
......
Markdown is supported
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