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