From d8fb394585b3611a98c3f2af4f6f3ba9b6152bd7 Mon Sep 17 00:00:00 2001
From: ale <ale@incal.net>
Date: Mon, 2 Apr 2018 10:53:31 +0100
Subject: [PATCH] Add methods to enable/disable OTP

---
 actions.go       | 63 +++++++++++++++++++++++++++++++++++++++++++++++-
 actions_test.go  |  8 ++++++
 backend/model.go | 16 ++++++++++++
 server/server.go | 33 +++++++++++++++++++++++++
 4 files changed, 119 insertions(+), 1 deletion(-)

diff --git a/actions.go b/actions.go
index 823e42f1..852fa017 100644
--- a/actions.go
+++ b/actions.go
@@ -7,8 +7,9 @@ import (
 	"errors"
 
 	"git.autistici.org/ai3/go-common/pwhash"
-	sso "git.autistici.org/id/go-sso"
+	"git.autistici.org/id/go-sso"
 	"git.autistici.org/id/keystore/userenckey"
+	"github.com/pquerna/otp/totp"
 )
 
 // Backend user database interface.
@@ -36,6 +37,8 @@ type Backend interface {
 	SetUserEncryptionPublicKey(context.Context, *User, []byte) error
 	SetApplicationSpecificPassword(context.Context, *User, *AppSpecificPasswordInfo, string) error
 	DeleteApplicationSpecificPassword(context.Context, *User, string) error
+	SetUserTOTPSecret(context.Context, *User, string) error
+	DeleteUserTOTPSecret(context.Context, *User) error
 }
 
 // AccountService implements the business logic and high-level
@@ -313,6 +316,11 @@ func (s *AccountService) CreateApplicationSpecificPassword(ctx context.Context,
 		return nil, newRequestError(err)
 	}
 
+	// No application-specific passwords unless 2FA is enabled.
+	if !user.Has2FA {
+		return nil, newRequestError(errors.New("2FA is not enabled for this user"))
+	}
+
 	// Create a new application-specific password and set it in
 	// the database. We don't need to update the User object as
 	// we're not reusing it.
@@ -454,6 +462,51 @@ func (s *AccountService) MoveResource(ctx context.Context, req *MoveResourceRequ
 	return &resp, nil
 }
 
+type EnableOTPRequest struct {
+	RequestBase
+}
+
+type EnableOTPResponse struct {
+	TOTPSecret string `json:"totp_secret"`
+}
+
+func (s *AccountService) EnableOTP(ctx context.Context, req *EnableOTPRequest) (*EnableOTPResponse, error) {
+	user, err := s.authorizeUser(ctx, req.Username, req.SSO)
+	if err != nil {
+		return nil, err
+	}
+
+	// Replace or initialize the TOTP secret.
+	totpSecret, err := generateTOTPSecret()
+	if err != nil {
+		return nil, err
+	}
+	if err := s.backend.SetUserTOTPSecret(ctx, user, totpSecret); err != nil {
+		return nil, newBackendError(err)
+	}
+
+	return &EnableOTPResponse{
+		TOTPSecret: totpSecret,
+	}, nil
+}
+
+type DisableOTPRequest struct {
+	RequestBase
+}
+
+func (s *AccountService) DisableOTP(ctx context.Context, req *DisableOTPRequest) error {
+	user, err := s.authorizeUser(ctx, req.Username, req.SSO)
+	if err != nil {
+		return err
+	}
+
+	// Delete the TOTP secret (if present).
+	if err := s.backend.DeleteUserTOTPSecret(ctx, user); err != nil {
+		return newBackendError(err)
+	}
+	return nil
+}
+
 const appSpecificPasswordLen = 64
 
 func randomBase64(n int) string {
@@ -474,3 +527,11 @@ const appSpecificPasswordIDLen = 4
 func randomAppSpecificPasswordID() string {
 	return randomBase64(appSpecificPasswordIDLen)
 }
+
+func generateTOTPSecret() (string, error) {
+	key, err := totp.Generate(totp.GenerateOpts{})
+	if err != nil {
+		return "", err
+	}
+	return key.Secret(), nil
+}
diff --git a/actions_test.go b/actions_test.go
index fe100bf6..e875b365 100644
--- a/actions_test.go
+++ b/actions_test.go
@@ -58,6 +58,14 @@ func (b *fakeBackend) DeleteApplicationSpecificPassword(_ context.Context, user
 	return nil
 }
 
+func (b *fakeBackend) SetUserTOTPSecret(_ context.Context, user *User, secret string) error {
+	return nil
+}
+
+func (b *fakeBackend) DeleteUserTOTPSecret(_ context.Context, user *User) error {
+	return nil
+}
+
 const testAdminGroupName = "admins"
 
 type fakeValidator struct {
diff --git a/backend/model.go b/backend/model.go
index 6befa5cd..8537fd91 100644
--- a/backend/model.go
+++ b/backend/model.go
@@ -521,6 +521,22 @@ func (b *LDAPBackend) DeleteApplicationSpecificPassword(ctx context.Context, use
 	return b.conn.Modify(ctx, mod)
 }
 
+func (b *LDAPBackend) SetUserTOTPSecret(ctx context.Context, user *accountserver.User, secret string) error {
+	mod := ldap.NewModifyRequest(getUserDN(user))
+	if b.readAttributeValue(ctx, getUserDN(user), "totpSecret") == "" {
+		mod.Add("totpSecret", []string{secret})
+	} else {
+		mod.Replace("totpSecret", []string{secret})
+	}
+	return b.conn.Modify(ctx, mod)
+}
+
+func (b *LDAPBackend) DeleteUserTOTPSecret(ctx context.Context, user *accountserver.User) error {
+	mod := ldap.NewModifyRequest(getUserDN(user))
+	mod.Delete("totpSecret", nil)
+	return b.conn.Modify(ctx, mod)
+}
+
 func (b *LDAPBackend) SetResourcePassword(ctx context.Context, _ string, r *accountserver.Resource, encryptedPassword string) error {
 	mod := ldap.NewModifyRequest(getResourceDN(r))
 	mod.Replace("userPassword", []string{encryptedPassword})
diff --git a/server/server.go b/server/server.go
index c1f46a5b..de4df645 100644
--- a/server/server.go
+++ b/server/server.go
@@ -155,10 +155,43 @@ func (s *AccountServer) handleMoveResource(w http.ResponseWriter, r *http.Reques
 	serverutil.EncodeJSONResponse(w, resp)
 }
 
+func (s *AccountServer) handleEnableOTP(w http.ResponseWriter, r *http.Request) {
+	var req as.EnableOTPRequest
+	if !serverutil.DecodeJSONRequest(w, r, &req) {
+		return
+	}
+
+	resp, err := s.service.EnableOTP(r.Context(), &req)
+	if err != nil {
+		log.Printf("EnableOTP(%s): error: %v", req.Username, err)
+		http.Error(w, err.Error(), errToStatus(err))
+		return
+	}
+
+	serverutil.EncodeJSONResponse(w, resp)
+}
+
+func (s *AccountServer) handleDisableOTP(w http.ResponseWriter, r *http.Request) {
+	var req as.DisableOTPRequest
+	if !serverutil.DecodeJSONRequest(w, r, &req) {
+		return
+	}
+
+	if err := s.service.DisableOTP(r.Context(), &req); err != nil {
+		log.Printf("DisableOTP(%s): error: %v", req.Username, err)
+		http.Error(w, err.Error(), errToStatus(err))
+		return
+	}
+
+	serverutil.EncodeJSONResponse(w, emptyResponse)
+}
+
 func (s *AccountServer) Handler() http.Handler {
 	h := http.NewServeMux()
 	h.HandleFunc("/api/user/get", s.handleGetUser)
 	h.HandleFunc("/api/user/change_password", s.handleChangeUserPassword)
+	h.HandleFunc("/api/user/enable_otp", s.handleEnableOTP)
+	h.HandleFunc("/api/user/disable_otp", s.handleDisableOTP)
 	h.HandleFunc("/api/app_specific_password/create", s.handleCreateApplicationSpecificPassword)
 	h.HandleFunc("/api/app_specific_password/delete", s.handleDeleteApplicationSpecificPassword)
 	h.HandleFunc("/api/resource/enable", s.handleEnableResource)
-- 
GitLab