openvpn-mgt/vpnsession.go

323 lines
7.4 KiB
Go

package main
import (
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"log"
"os"
"regexp"
"strconv"
"strings"
"time"
hibp "github.com/mattevans/pwned-passwords"
)
type vpnSession struct {
Time time.Time `json:"time"`
Login string `json:"username"`
Operation string `json:"operation"`
Status string `json:"status"`
Profile string `json:"profile"`
TwoFA bool `json:"2fa_auth"`
IP string `json:"client_ip"`
PrivIP string `json:"private_ip"`
AsNumber string `json:"as_number"`
AsName string `json:"as_name"`
NewAS bool `json:"as_new"`
PwnedPasswd bool `json:"pwned_passwd"`
Hostname string `json:"hostname"`
TooMuchPwn bool `json:"too_much_pwn"`
Mail string `json:"-"`
cID int `json:"-"`
kID int `json:"-"`
port int `json:"-"`
dev string `json:"-"`
password string `json:"-"`
otpCode string `json:"-"`
localIP string `json:"-"`
vpnserver string `json:"-"`
pwnMail string `json:"-"`
newAsMail string `json:"-"`
MailFrom string `json:"-"`
CcPwnPassword string `json:"-"`
}
func NewVPNSession(operation string) *vpnSession {
v := vpnSession{
Time: time.Now().Round(time.Second),
Status: "system failure",
Operation: operation,
}
v.Hostname, _ = os.Hostname()
return &v
}
func (c *vpnSession) String() string {
if res, err := json.MarshalIndent(c, " ", " "); err == nil {
return string(res)
}
return ""
}
func (c *vpnSession) b64Login() string {
return base64.StdEncoding.EncodeToString([]byte(c.Login))
}
func (c *vpnSession) ParseSessionId(line string) error {
var err error
client_id := strings.Split(strings.Replace(line, ">CLIENT:CONNECT,", "", 1), ",")
if c.cID, err = strconv.Atoi(client_id[0]); err != nil {
return err
}
if c.kID, err = strconv.Atoi(client_id[1]); err != nil {
return err
}
return nil
}
func (c *vpnSession) CheckPwn(password string) error {
client := hibp.NewClient()
pwned, err := client.Pwned.Compromised(password)
if err != nil {
return err
}
c.PwnedPasswd = pwned
return nil
}
func (c *vpnSession) ParseEnv(infos *[]string) error {
var err error
r := regexp.MustCompile("[^a-zA-Z0-9./_@-]")
for _, line := range *infos {
p := strings.Split(strings.Replace(line, ">CLIENT:ENV,", "", 1), "=")
switch p[0] {
case "trusted_port":
if c.port, err = strconv.Atoi(r.ReplaceAllString(p[1], "")); err != nil {
return err
}
case "untrusted_port":
if c.port, err = strconv.Atoi(r.ReplaceAllString(p[1], "")); err != nil {
return err
}
case "trusted_ip":
c.IP = r.ReplaceAllString(p[1], "")
case "untrusted_ip":
c.IP = r.ReplaceAllString(p[1], "")
case "ifconfig_pool_remote_ip":
c.PrivIP = r.ReplaceAllString(p[1], "")
case "ifconfig_local":
c.localIP = r.ReplaceAllString(p[1], "")
case "password":
switch {
case strings.HasPrefix(p[1], "CRV1"):
split := strings.Split(p[1], ":")
if len(split) != 5 {
break
}
c.password = split[2]
c.otpCode = split[4]
if c.otpCode == "" {
c.otpCode = "***"
}
go c.CheckPwn(c.password)
case strings.HasPrefix(p[1], "SCRV1"):
split := strings.Split(p[1], ":")
if len(split) != 3 {
break
}
data, err := base64.StdEncoding.DecodeString(split[1])
if err != nil {
break
}
c.password = string(data)
data, err = base64.StdEncoding.DecodeString(split[2])
if err != nil {
c.password = p[1]
break
}
c.otpCode = string(data)
if c.otpCode == "" {
c.otpCode = "***"
}
default:
c.password = p[1]
c.otpCode = ""
go c.CheckPwn(c.password)
}
case "username":
c.Login = r.ReplaceAllString(p[1], "")
case "dev":
c.dev = r.ReplaceAllString(p[1], "")
}
}
return nil
}
func (c *vpnSession) Auth(s *OpenVpnMgt) {
var cmd []string
var ip string
var errIP error
err, ok := c.auth(s)
// if auth is ok, time to get an IP address
if ok == 0 {
ip, errIP = s.getIP(c)
if errIP != nil {
ok = -10
err = errIP
}
}
switch {
case ok == 0:
cmd = []string{
fmt.Sprintf("client-auth %d %d", c.cID, c.kID),
fmt.Sprintf("ifconfig-push %s %s", ip, c.localIP),
}
for _, r := range s.ldap[c.Profile].routes {
cmd = append(cmd, fmt.Sprintf("push \"route %s vpn_gateway\"", r))
}
cmd = append(cmd, "END")
case ok < 0:
cmd = []string{fmt.Sprintf("client-deny %d %d \"%s\" \"%s\"",
c.cID, c.kID, err, err)}
case ok == 1:
cmd = []string{fmt.Sprintf(
"client-deny %d %d \"Need OTP\" \"CRV1:R,E:%s:%s:OTP Code \"",
c.cID, c.kID, c.password, c.b64Login())}
}
if err, _ := s.sendCommand(cmd, c.vpnserver); err != nil {
c.LogPrintln(err)
}
return
}
// main authentication function.
// returns 0 if auth is valid
// returns 1 if an TOTP code is necessary
// returns a negative if auth is not valid
func (c *vpnSession) auth(s *OpenVpnMgt) (error, int) {
// an empty password is not good
if c.password == "" {
c.Status = "Empty Password"
return errors.New("Empty Password"), -1
}
// check if the password is a valid token (see TOTP request)
tokenPasswordOk, tokenPassword := s.TokenPassword(c)
// password is a token. We remove it from the session object to
// avoid checking it against the ldap
if tokenPasswordOk {
c.password = ""
}
// if the otp is not empty, we check it against the valid codes as soon as
// possible
otpvalidated := false
if c.otpCode != "" {
codes, err := s.GenerateOTP(c.Login)
if err != nil {
return err, -2
}
for _, possible := range codes {
if possible == c.otpCode {
otpvalidated = true
}
}
}
c.Profile = ""
login := []string{c.Login}
pass := c.password
for {
n := c.Profile
for k, ldap := range s.ldap {
if ldap.upgradeFrom != c.Profile {
continue
}
err, userOk, passOk, secondary := ldap.Auth(login, pass)
// if there is an error, try the other configurations
if err != nil {
c.LogPrintln(err)
continue
}
// we did find a valid User
if userOk {
// the login for the new auth level is given by the current one
login = secondary
if c.Mail == "" {
c.Mail = secondary[0]
}
if passOk && c.Profile != "" {
// it's at least the second auth level, and we have a valid
// password on 2 different auth system. It's a dupplicate
// password, let's log it
log.Printf("User %s has a dupplicate password\n", c.Login)
}
// we have either a positive auth ok a previous valid one
if passOk || c.Profile != "" || tokenPasswordOk {
c.Profile = k
}
}
}
// no profile update this turn, no need to continue
if n == c.Profile {
break
}
}
// no profile validated, we stop here
if c.Profile == "" {
c.Status = "fail (password)"
return errors.New("Authentication Failed"), -3
}
// check the MFA requested by the secured profile
c.TwoFA = true
switch s.ldap[c.Profile].mfaType {
case "internal":
if otpvalidated {
return nil, 0
}
// log that the failure is due to the OTP
if c.otpCode == "" {
c.Status = "Need OTP Code"
} else {
c.Status = "fail (OTP) : "
}
c.password = tokenPassword
return errors.New("Need OTP Code"), 1
case "okta":
//TODO implement okta MFA
c.Status = "fail (Okta)"
return nil, -4
default:
c.TwoFA = false
}
// no MFA requested, the login is valid
return nil, 0
}