server.go 4.38 KB
package auth

import (
	"net"
	"strings"

	"github.com/gliderlabs/ssh"
	gossh "golang.org/x/crypto/ssh"

	"github.com/jumpserver/koko/pkg/common"
	"github.com/jumpserver/koko/pkg/config"
	"github.com/jumpserver/koko/pkg/logger"
	"github.com/jumpserver/koko/pkg/model"
	"github.com/jumpserver/koko/pkg/service"
)

var mfaInstruction = "Please enter 6 digits."
var mfaQuestion = "[MFA auth]: "

var confirmInstruction = "Please wait for your admin to confirm."
var confirmQuestion = "Do you want to continue [Y/n]? : "

const (
	actionAccepted        = "Accepted"
	actionFailed          = "Failed"
	actionPartialAccepted = "Partial accepted"
)

func checkAuth(ctx ssh.Context, password, publicKey string) (res ssh.AuthResult) {
	username := ctx.User()
	authMethod := "publickey"
	action := actionAccepted
	res = ssh.AuthFailed
	if password != "" {
		authMethod = "password"
	}
	remoteAddr, _, _ := net.SplitHostPort(ctx.RemoteAddr().String())
	userClient, ok := ctx.Value(model.ContextKeyClient).(*service.SessionClient)
	if !ok {
		sessionClient := service.NewSessionClient(service.Username(username),
			service.RemoteAddr(remoteAddr), service.LoginType("T"))
		userClient = &sessionClient
		ctx.SetValue(model.ContextKeyClient, userClient)
	}
	userClient.SetOption(service.Password(password), service.PublicKey(publicKey))
	user, authStatus := userClient.Authenticate(ctx)
	switch authStatus {
	case service.AuthMFARequired:
		action = actionPartialAccepted
		res = ssh.AuthPartiallySuccessful
	case service.AuthSuccess:
		res = ssh.AuthSuccessful
		ctx.SetValue(model.ContextKeyUser, &user)
	case service.AuthConfirmRequired:
		required := true
		ctx.SetValue(model.ContextKeyConfirmRequired, &required)
		action = actionPartialAccepted
		res = ssh.AuthPartiallySuccessful
	default:
		action = actionFailed
	}
	logger.Infof("%s %s for %s from %s", action, authMethod, username, remoteAddr)
	return

}

func CheckUserPassword(ctx ssh.Context, password string) ssh.AuthResult {
	if !config.GetConf().PasswordAuth {
		return ssh.AuthFailed
	}
	return checkAuth(ctx, password, "")
}

func CheckUserPublicKey(ctx ssh.Context, key ssh.PublicKey) ssh.AuthResult {
	if !config.GetConf().PublicKeyAuth {
		return ssh.AuthFailed
	}
	b := key.Marshal()
	publicKey := common.Base64Encode(string(b))
	return checkAuth(ctx, "", publicKey)
}

func CheckMFA(ctx ssh.Context, challenger gossh.KeyboardInteractiveChallenge) (res ssh.AuthResult) {
	if value, ok := ctx.Value(model.ContextKeyConfirmFailed).(*bool); ok && *value {
		return ssh.AuthFailed
	}

	username := ctx.User()
	remoteAddr, _, _ := net.SplitHostPort(ctx.RemoteAddr().String())
	res = ssh.AuthFailed

	var confirmAction bool
	instruction := mfaInstruction
	question := mfaQuestion

	client, ok := ctx.Value(model.ContextKeyClient).(*service.SessionClient)
	if !ok {
		logger.Errorf("User %s Mfa Auth failed: not found session client.", username, )
		return
	}
	value, ok := ctx.Value(model.ContextKeyConfirmRequired).(*bool)
	if ok && *value {
		confirmAction = true
		instruction = confirmInstruction
		question = confirmQuestion
	}
	answers, err := challenger(username, instruction, []string{question}, []bool{true})
	if err != nil || len(answers) != 1 {
		if confirmAction {
			client.CancelConfirm()
		}
		logger.Errorf("User %s happened err: %s", username, err)
		return
	}
	if confirmAction {
		switch strings.TrimSpace(strings.ToLower(answers[0])) {
		case "yes", "y", "":
			user, authStatus := client.CheckConfirm(ctx)
			switch authStatus {
			case service.AuthSuccess:
				res = ssh.AuthSuccessful
				ctx.SetValue(model.ContextKeyUser, &user)
				return
			}
		case "no", "n":
			client.CancelConfirm()
		default:
			return
		}
		failed := true
		ctx.SetValue(model.ContextKeyConfirmFailed, &failed)
		return
	}
	mfaCode := answers[0]
	user, authStatus := client.CheckUserOTP(ctx, mfaCode)
	switch authStatus {
	case service.AuthSuccess:
		res = ssh.AuthSuccessful
		ctx.SetValue(model.ContextKeyUser, &user)
		logger.Infof("%s MFA for %s from %s", actionAccepted, username, remoteAddr)
	case service.AuthConfirmRequired:
		res = ssh.AuthPartiallySuccessful
		required := true
		ctx.SetValue(model.ContextKeyConfirmRequired, &required)
		logger.Infof("%s MFA for %s from %s", actionPartialAccepted, username, remoteAddr)
	default:
		logger.Errorf("%s MFA for %s from %s", actionFailed, username, remoteAddr)
	}
	return
}

func MFAAuthMethods(ctx ssh.Context) (methods []string) {
	return []string{"keyboard-interactive"}
}