Skip to content

Commit

Permalink
Implemented passwordless login using email
Browse files Browse the repository at this point in the history
Signed-off-by: William <[email protected]>
  • Loading branch information
kwesidev committed Dec 5, 2023
1 parent 0491c4a commit 9c5714b
Show file tree
Hide file tree
Showing 5 changed files with 174 additions and 8 deletions.
2 changes: 2 additions & 0 deletions internal/apiserver/api_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ func (ap *APIServer) Run() {
func (ap *APIServer) registerGlobalFunctions() {
authController := controllers.NewAuthController(ap.db)
http.HandleFunc("/api/auth/login", middlewares.Method("POST", authController.Login))
http.HandleFunc("/api/auth/passwordLessLogin", middlewares.Method("POST", authController.PasswordLessLogin))
http.HandleFunc("/api/auth/completePasswordLessLogin", middlewares.Method("POST", authController.CompletePasswordLessLogin))
http.HandleFunc("/api/auth/tokenRefresh", middlewares.Method("POST", authController.RefreshToken))
http.HandleFunc("/api/auth/register", middlewares.Method("POST", authController.Register))
http.HandleFunc("/api/auth/passwordResetRequest", middlewares.Method("POST", authController.PasswordResetRequest))
Expand Down
62 changes: 60 additions & 2 deletions internal/controllers/auth_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,42 @@ func (authCtrl *AuthController) Login(w http.ResponseWriter, r *http.Request) {
utilities.JSONError(w, err.Error(), http.StatusBadRequest)
return
}
//Validates the requests
err = authCtrl.validate.Struct(authRequest)
if err != nil {
log.Println(err)
utilities.JSONError(w, err.Error(), http.StatusBadRequest)
return
}
authResult, err := authCtrl.authService.LoginByUsernamePassword(authRequest.Username, authRequest.Password, "", "")
var authResult *models.AuthenticationResponse
authResult, err = authCtrl.authService.LoginByUsernamePassword(authRequest.Username, authRequest.Password, "", "")

if err != nil {
if errors.Is(err, services.ErrorInvalidUsername) || errors.Is(err, services.ErrorInvalidPassword) || errors.Is(err, services.ErrorAccountNotActive) {
utilities.JSONError(w, err.Error(), http.StatusUnauthorized)
} else {
utilities.JSONError(w, services.ErrorServer.Error(), http.StatusInternalServerError)
}
return
}
utilities.JSONResponse(w, authResult)
}

// Login Handler To Authenticate user without passsword
func (authCtrl *AuthController) PasswordLessLogin(w http.ResponseWriter, r *http.Request) {
passwordLessAuthRequest := models.PasswordLessAuthRequest{}
err := utilities.GetJsonInput(&passwordLessAuthRequest, r)
if err != nil {
utilities.JSONError(w, err.Error(), http.StatusBadRequest)
return
}
err = authCtrl.validate.Struct(passwordLessAuthRequest)
if err != nil {
log.Println(err)
utilities.JSONError(w, err.Error(), http.StatusBadRequest)
return
}
var passwordLessAuthResponse *models.PasswordLessAuthResponse
passwordLessAuthResponse, err = authCtrl.authService.PasswordLessLogin(passwordLessAuthRequest.Username, passwordLessAuthRequest.SendMethod, "", "")
if err != nil {
if errors.Is(err, services.ErrorInvalidUsername) || errors.Is(err, services.ErrorInvalidPassword) || errors.Is(err, services.ErrorAccountNotActive) {
utilities.JSONError(w, err.Error(), http.StatusUnauthorized)
Expand All @@ -56,6 +84,36 @@ func (authCtrl *AuthController) Login(w http.ResponseWriter, r *http.Request) {
}
return
}
utilities.JSONResponse(w, passwordLessAuthResponse)
}

// Completes passwordless login
func (authCtrl *AuthController) CompletePasswordLessLogin(w http.ResponseWriter, r *http.Request) {
completePasswordLessLogin := models.CompletePasswordLessRequest{}
err := utilities.GetJsonInput(&completePasswordLessLogin, r)
if err != nil {
utilities.JSONError(w, err.Error(), http.StatusBadRequest)
return
}
// Validates requests
err = authCtrl.validate.Struct(completePasswordLessLogin)
if err != nil {
log.Println(err)
utilities.JSONError(w, err.Error(), http.StatusBadRequest)
return
}
var (
authResult *models.AuthenticationResponse
)
authResult, err = authCtrl.authService.CompletePasswordLessLogin(completePasswordLessLogin.Code, completePasswordLessLogin.RequestId)
if err != nil {
if errors.Is(err, services.ErrorInvalidCode) {
utilities.JSONError(w, err.Error(), http.StatusUnauthorized)
} else {
utilities.JSONError(w, services.ErrorServer.Error(), http.StatusInternalServerError)
}
return
}
utilities.JSONResponse(w, authResult)
}

Expand Down
13 changes: 13 additions & 0 deletions internal/models/auth_model.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,19 @@ type AuthenticationRequest struct {
Username string `json:"username" validate:"required"`
Password string `json:"password" validate:"required"`
}

type PasswordLessAuthRequest struct {
Username string `json:"username"`
SendMethod string `json:"sendMethod"`
}
type PasswordLessAuthResponse struct {
RequestId string `json:"requestId" validate:"required"`
SendMethod string `json:"sendMethod" validate:"required"`
}
type CompletePasswordLessRequest struct {
RequestId string `json:"requestId" validate:"required"`
Code string `json:"code" validate:"required"`
}
type TokenRefreshRequest struct {
RefreshToken string `json:"refreshToken"`
}
Expand Down
80 changes: 75 additions & 5 deletions internal/services/auth_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ func NewAuthService(db *sql.DB) *AuthService {

}

// Login function to authenticate user
// Login function to authenticate user by username and password
func (authSrv *AuthService) LoginByUsernamePassword(username, password, ipAddress, userAgent string) (*models.AuthenticationResponse, error) {
var (
userId int
Expand All @@ -51,28 +51,98 @@ func (authSrv *AuthService) LoginByUsernamePassword(username, password, ipAddres
if err = bcrypt.CompareHashAndPassword([]byte(passwordHash), []byte(password)); err != nil {
return nil, ErrorInvalidPassword
}
return authSrv.generateAuthResponse(*userDetails, ipAddress, userAgent)
}
func (authSrv *AuthService) generateAuthResponse(userDetails models.User, ipAddress, userAgent string) (*models.AuthenticationResponse, error) {
// Check if two authentication is required
if userDetails.TwoFactorEnabled {
if userDetails.TwoFactorMethod != "TOTP" {
return authSrv.twoFactorRequest(*userDetails, ipAddress, userAgent)
return authSrv.twoFactorRequest(userDetails, ipAddress, userAgent)
}
// Otherwise its TOTP then
authResult := &models.AuthenticationResponse{}
// Generate a short token which expires after 5minutes
shortToken, _ := utilities.GenerateJwtToken(userId, userDetails.Roles, (time.Second * 300))
shortToken, _ := utilities.GenerateJwtToken(userDetails.ID, userDetails.Roles, (time.Second * 300))
authResult.TwoFactorEnabled = true
authResult.Token = shortToken
authResult.TwoFactorMethod = userDetails.TwoFactorMethod
return authResult, nil
}
// Get user roles
roles, err := authSrv.userService.GetRoles(userId)
roles, err := authSrv.userService.GetRoles(userDetails.ID)
userDetails.Roles = roles
if err != nil {
log.Println(err)
return nil, err
}
return authSrv.generateTokenDetails(*userDetails, ipAddress, userAgent)
return authSrv.generateTokenDetails(userDetails, ipAddress, userAgent)
}

// Func loginByUsername this will send an otp to the user which then be verified
func (authSrv *AuthService) PasswordLessLogin(username, sendMethod, ipAddress, userAgent string) (*models.PasswordLessAuthResponse, error) {
userDetails := authSrv.userService.GetByUsername(username)
if userDetails == nil {
return nil, ErrorInvalidUsername
}
if userDetails.Active == false {
return nil, ErrorAccountNotActive
}
tx, err := authSrv.db.Begin()
defer tx.Rollback()
if err != nil {
log.Println(err)
return nil, err
}
// Generates request ID
requestId := utilities.GenerateOpaqueToken(45)
// Generate 6 random code
randomCodes := utilities.GenerateRandomDigits(6)
if _, err = tx.Exec("INSERT INTO otp_requests(user_id, request_id, code, send_method, expiry_time, ip_address, user_agent, created_at) values($1, $2, $3, $4, $5, $6, $7, NOW())", userDetails.ID, requestId, randomCodes, "EMAIL", time.Now().Add(1*time.Minute), ipAddress, userAgent); err != nil {
log.Println(err)
return nil, err
}
if err = authSrv.emailService.SendEmailLoginRequest(randomCodes, *userDetails); err != nil {
log.Println("Email Error", err)
return nil, ErrSendingMail
}
if err = tx.Commit(); err != nil {
return nil, err
}
passwordLessAuthResponse := models.PasswordLessAuthResponse{}
passwordLessAuthResponse.RequestId = requestId
passwordLessAuthResponse.SendMethod = "EMAIL"
return &passwordLessAuthResponse, nil
}

// Func completePasswordLessLogin
func (authSrv *AuthService) CompletePasswordLessLogin(code, requestId string) (*models.AuthenticationResponse, error) {
var (
userId int
userAgent, ipAddress string
)
println(code, requestId)
tx, err := authSrv.db.Begin()
defer tx.Rollback()
if err != nil {
log.Println(err)
return nil, err
}
row := tx.QueryRow("SELECT user_id, ip_address,user_agent FROM otp_requests WHERE code = $1 AND request_id = $2 AND expiry_time >= NOW()", code, requestId)
row.Scan(&userId, &ipAddress, &userAgent)
if userId == 0 {
log.Println("Invalid Code or Request Id Invalid")
return nil, ErrorInvalidCode
}
userDetails := authSrv.userService.Get(userId)
// Deletes the otp requests
if _, err = tx.Exec("DELETE FROM otp_requests WHERE request_id = $1", requestId); err != nil {
log.Println(err)
return nil, err
}
if err = tx.Commit(); err != nil {
return nil, err
}
return authSrv.generateAuthResponse(*userDetails, ipAddress, userAgent)
}

// Refresh Token generates a new refresh token that will be used to get a new access token and a refresh token
Expand Down
25 changes: 24 additions & 1 deletion internal/services/email_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@ func (emSrv *EmailService) SendTwoFactorRequest(randomCodes string, userDetails
// Get email template from directory and assign random code to it
emailTemplateFile, err := template.ParseFiles("static/email_templates/TwoFactorLogin.html")
if err != nil {
log.Println("Template reading :", err)
return err
}
tmpl := template.Must(emailTemplateFile, err)
Expand All @@ -75,6 +74,30 @@ func (emSrv *EmailService) SendTwoFactorRequest(randomCodes string, userDetails
return nil
}

// SendTwoFactorRequest sends two factor mail
func (emSrv *EmailService) SendEmailLoginRequest(randomCodes string, userDetails models.User) error {
var twoFactorRequestTemplateBuffer bytes.Buffer
// Get email template from directory and assign random code to it
emailTemplateFile, err := template.ParseFiles("static/email_templates/EmailLogin.html")
if err != nil {
return err
}
tmpl := template.Must(emailTemplateFile, err)
emailTemplateData := struct {
FullName string
RandomCode string
}{}
emailTemplateData.RandomCode = randomCodes
emailTemplateData.FullName = userDetails.FirstName + " " + userDetails.LastName
tmpl.Execute(&twoFactorRequestTemplateBuffer, emailTemplateData)
recipient := []string{userDetails.EmailAddress}
if err = emSrv.sendEmail(recipient, "Email login", twoFactorRequestTemplateBuffer.String()); err != nil {
log.Println("Sending Email Login Request Error", err)
return err
}
return nil
}

// SendPasswordRequest
// Sends a password request mail to the receiver
func (emSrv *EmailService) SendPasswordResetRequest(randomCodes string, userDetails models.User) error {
Expand Down

0 comments on commit 9c5714b

Please sign in to comment.