openvpn-2fa-otp-freeradius-ldap
210 строк · 7.3 Кб
1#! /usr/bin/env python3
2
3import radiusd
4import yaml
5from typing import Tuple, TypedDict, List, Literal
6import pyotp
7from strongpass_ldap import LDAPConnection
8from strongpass_db import DBConnection
9from config import Config, CONFIG_FILE
10from base64 import b64decode
11from util import log_msg
12
13cfg: Config
14ldap_conn: LDAPConnection
15db_conn: DBConnection
16
17
18def get_user_pass(req: Tuple[Tuple[str, str]]) -> Tuple[str|None, str|None]:
19user_name = None
20pwd = None
21for item in req:
22if not item:
23continue
24for key, value in item:
25if key == "User-Name":
26user_name = value
27if key == "User-Password":
28pwd = value
29return (user_name, pwd)
30
31
32# return 0 for success or -1 for failure
33def instantiate(p):
34global cfg
35global ldap_conn
36global db_conn
37print("*** instantiate strongpass_otp ***")
38log_msg(radiusd.L_INFO, "*** instantiate strongpass_otp ***")
39print(p)
40try:
41with open(CONFIG_FILE, encoding='utf8') as file:
42cfg = yaml.load(file, Loader=yaml.FullLoader)
43ldap_conn = LDAPConnection(cfg["ldap"])
44db_conn = DBConnection(cfg["db"])
45return 0
46except Exception as e:
47log_msg(radiusd.L_ERR, e)
48return -1
49
50
51def decode_ovpn_pass_otp(s: str)-> Tuple[bool, str | None, str| None]:
52parts: List[str] = s.split(':')
53if len(parts) != 3:
54log_msg(radiusd.L_ERR,
55"wrong password format. Password format must be SCRV1:x:y. Where x,y base64 encoded values")
56return (False, None, None)
57if parts[0] != "SCRV1":
58log_msg(radiusd.L_ERR,
59"wrong password format. Header 'SCRV1' is absent")
60return (False, None, None)
61passwd = b64decode(parts[1]).decode("utf-8")
62otp = b64decode(parts[2]).decode("utf-8")
63return (True, passwd, otp)
64
65
66def decode_pass_and_otp(s: str)-> Tuple[bool, str | None, str | None]:
67if len(s)<7:
68log_msg(radiusd.L_ERR,
69"Строка 'пароль плюс OTP код' слишком короткая. Минимальная длина должна быть 7 символов. 1 символ для пароля и 6 символов для OTP кода")
70return (False, None, None)
71
72passwd = s[:len(s)-6]
73otp=s[len(s)-6:]
74return (True, passwd, otp)
75
76
77def decode_pwd(s: str) -> Tuple[bool, str | None, str| None]:
78global cfg
79match cfg['otp_option']:
80case 1:
81return decode_ovpn_pass_otp(s)
82case 2:
83return decode_pass_and_otp(s)
84case 3:
85return (True, "", s)
86case _:
87log_msg(radiusd.L_ERR,
88"Параметр otp_option в конфигурационном файле содержит недопустимое значение")
89return (False, None, None)
90
91
92def auth_via_ldap_and_otp(user:str, pwd: str, otp: str) -> Literal[0]|Literal[2]:
93try:
94otp_secret = db_conn.get_otp_secret(user)
95if otp_secret is None:
96log_msg(radiusd.L_ERR,
97"OTP secret for user {} not found in database.".format(user))
98return radiusd.RLM_MODULE_REJECT
99totp = pyotp.TOTP(otp_secret)
100if not totp.verify(otp):
101log_msg(radiusd.L_ERR,
102"OTP код для пользователя {} не прошел проверку. Отказ в аутентификации".format(user))
103return radiusd.RLM_MODULE_REJECT
104isAuthenticated = ldap_conn.authenticate(user, pwd)
105if isAuthenticated is False:
106log_msg(radiusd.L_ERR,
107"user {} has not been authenticated. Wrong password or user not found".format(user))
108return radiusd.RLM_MODULE_REJECT
109return radiusd.RLM_MODULE_OK
110except Exception as ex:
111log_msg(radiusd.L_ERR,
112"Unhandled exception when trying to authenticate user {}. Exception message: {}".format(user, ex))
113return radiusd.RLM_MODULE_REJECT
114
115def auth_via_otp_only(user: str, otp: str) -> Literal[0]| Literal[2]:
116try:
117otp_secret = db_conn.get_otp_secret(user)
118if otp_secret is None:
119log_msg(radiusd.L_ERR,
120"OTP secret for user {} not found in database.".format(user))
121return radiusd.RLM_MODULE_REJECT
122totp = pyotp.TOTP(otp_secret)
123if not totp.verify(otp):
124log_msg(radiusd.L_ERR,
125"OTP код для пользователя {} не прошел проверку. Отказ в аутентификации".format(user))
126return radiusd.RLM_MODULE_REJECT
127return radiusd.RLM_MODULE_OK
128except Exception as ex:
129log_msg(radiusd.L_ERR,
130"Unhandled exception when trying to authenticate user {}. Exception message: {}".format(user, ex))
131return radiusd.RLM_MODULE_REJECT
132
133def auth_test_user(user: str, pwd: str)->Literal[0] | Literal[2]:
134if cfg['otp_option'] != 1 and cfg['otp_option'] != 2:
135log_msg(radiusd.L_ERR,
136"Тестовый пользователь при этом значении otp_option не может быть аутентифицирован")
137return radiusd.RLM_MODULE_REJECT
138isAuthenticated = ldap_conn.authenticate(user, pwd)
139if isAuthenticated is False:
140log_msg(radiusd.L_ERR,
141"user {} has not been authenticated. Wrong password or user not found".format(user))
142return radiusd.RLM_MODULE_REJECT
143return radiusd.RLM_MODULE_OK
144
145
146def authenticate(p):
147global cfg
148try:
149user, p = get_user_pass(p)
150if user is None or p is None:
151return radiusd.RLM_MODULE_REJECT
152if user == cfg['auth_test_user']:
153return auth_test_user(user, p)
154result, passwd, otp = decode_pwd(p)
155if result is False:
156return radiusd.RLM_MODULE_REJECT
157match cfg['otp_option']:
158case 1:
159return auth_via_ldap_and_otp(user, passwd, otp)
160case 2:
161return auth_via_ldap_and_otp(user, passwd, otp)
162case 3:
163return auth_via_otp_only(user, otp)
164case _:
165log_msg(radiusd.L_ERR,
166"Параметр otp_option в конфигурационном файле содержит недопустимое значение")
167return radiusd.RLM_MODULE_REJECT
168except Exception as ex:
169log_msg(radiusd.L_ERR,
170"Unhandled exception when trying to authenticate user {}. Exception message: {}".format(user, ex))
171return radiusd.RLM_MODULE_REJECT
172
173
174def authorize(p):
175user, pwd = get_user_pass(p)
176if user is None or pwd is None:
177return radiusd.RLM_MODULE_REJECT
178return (radiusd.RLM_MODULE_OK, (), (('Auth-Type', 'PAP'),))
179
180
181def preacct(p):
182return radiusd.RLM_MODULE_OK
183
184
185def accounting(p):
186return radiusd.RLM_MODULE_OK
187
188
189def pre_proxy(p):
190return radiusd.RLM_MODULE_OK
191
192
193def post_proxy(p):
194return radiusd.RLM_MODULE_OK
195
196
197def post_auth(p):
198return radiusd.RLM_MODULE_OK
199
200
201def recv_coa(p):
202return radiusd.RLM_MODULE_OK
203
204
205def send_coa(p):
206return radiusd.RLM_MODULE_OK
207
208
209def detach():
210return radiusd.RLM_MODULE_OK
211