Commit d8fb3945 authored by ale's avatar ale

Add methods to enable/disable OTP

parent ea481352
......@@ -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
}
......@@ -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 {
......
......@@ -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})
......
......@@ -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)
......
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