基于FreeRadius ldap实现双因素认证
需求:
通过radius 连接ldap,并且实现双因素认证(每个用户每次在密码后,需要输入6位随机数字,作为动态密码认证)。
设计:
找到radius代码,验证用户ldap帐号的功能
提取出用户名和密码
提取密码的最后6位数字为随机PIN,新的密码去掉6位数字,保存
将用户名和PIN发送到远程进行校验
根据用户名和新密码,查询ldap,判断是否成功
实现:
1.验证过程分析 版本freeradius-server-4.0.x
1) \src\modules\rlm_ldap\rlm_ldap.c
line:662 mod_authenticate 实现ldap认证,传入的REQUEST中,保存了用户输入的用户名和密码
2) \src\include\radiusd.h
line:234 struct rad\_request 定义额REQUEST, username,password 都是VALUE_PAIR类型
3) \src\include\pair.h
line:74 typedef struct value_pair VALUE_PAIR, 其中数据都是fr_value_box_t data; 类型,并且定义宏
#define vp_strvalue data.vb_strvalue
4) \src\include\value.h
line:26 typedef struct value_box fr_value_box_t; 其中最关键的是datum 联合体,并且定义了宏
#define vb_strvalue datum.strvalue
5) \src\modules\rlm_ldap\rlm_ldap.c
line:740 读取用户名和密码的方法
request->username->vp_strvalue
request->password->vp_strvalue
但由于freeradius3.0 以上对环境要求比较苛刻,因此如果系统是版本内核,建议安装低版本freeradius,这里选择2.2.5
2.验证过程分析 版本freeradius-server-2.2.5
1) freeradius模块化实现,对接ldap的验证代码在 \src\modules\rlm_ldap\rlm_ldap.c
导出功能中,主要包括认证和授权函数:
/* globally exported name */
module_t rlm_ldap = {
RLM_MODULE_INIT,
"LDAP",
RLM_TYPE_THREAD_SAFE, /* type: reserved */
ldap_instantiate, /* instantiation */
ldap_detach, /* detach */
{
ldap_authenticate, /* authentication */
ldap_authorize, /* authorization */
NULL, /* preaccounting */
NULL, /* accounting */
NULL, /* checksimul */
NULL, /* pre-proxy */
NULL, /* post-proxy */
#ifdef NOVELL
ldap_postauth /* post-auth */
#else
NULL
#endif
},
};
通过对ldap_authenticate(Check the user's password against ldap database)和ldap_authorize( Check if user is authorized for remote access)的注释查看,ldap_authenticate 函数是做密码校验的
2) ldap_authenticate 函数的原型,传入主要参数是 REQUEST,里面包含主要寻找的用户名和密码
3) REQUEST是结构体,定义在 src\include\radiusd.h , 具体内容 typedef struct auth_req REQUEST; 其中用户名和密码 定义如下:
struct auth_req {
....
VALUE_PAIR *config_items;
VALUE_PAIR *username; //用户输入的用户名
VALUE_PAIR *password; //用户输入的密码
}
4) 用户名和密码类型为 VALUE_PAIR ,也是结构体,具体定义在 \src\include\libradius.h, 如下所示,最主要的是 VALUE_PAIR_DATA 类型,具体定义也在 \src\include\libradius.h中。
typedef struct value_pair {
...
struct value_pair *next;
uint32_t lvalue;
VALUE_PAIR_DATA data;
} VALUE_PAIR;
typedef union value_pair_data {
char strvalue[MAX_STRING_LEN]; //保存密码或者用户名的变量
...
} VALUE_PAIR_DATA;
同时,该文件中定义了 #define vp_strvalue data.strvalue
因此,从一个request变量中,获得用户名或者密码字符串的写法为
request->username->vp_strvalue
request->password->vp_strvalue
对应获得的值为 char 数组。
5) 基于以上分析,可知,直接在\src\modules\rlm_ldap\rlm_ldap.c文件的ldap_authenticate 函数中添加代码实现,如下功能即可:
1) 获得用户输入的用户名和密码,request->username->vp_strvalue 和request->password->vp_strvalue
2) 校验密码长度,小于6(因为pin码为6位)直接返回认证失败
3) 将用户名和密码 传入自己实现的函数 google_authenticate
/*
传入参数:
req_username:请求用户名
req_password:请求密码
输出结果:
0 : 输入错误,或者网络超时等原因失败
1 : 双因素认证成功
2 :双因素认证失败
具体实现:
1. 校验输入的用户名和密码是否合法,长度,pin码是否为6位数字
2. 根据用户名和pin码作为参数,请求google authenticator接口, 尽可能排除网络问题,网络异常时,会连续请求三次
3. 判断返回结果,如果包含\"errcode\": 0, 则表示校验成功,否则失败。
可能问题:
1. 发送请求 超时时间 2秒
2. 验证双因素返回结果,由于c解析json相对麻烦,因此这里暂时使用字符串判断验证是否成功
*/
static int google_authenticate(char *req_username, char* req_password)
具体源代码:
////////////////////////////new code
#include <sys/socket.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <netdb.h>
#include <fcntl.h>
#include <ctype.h>
#include <string.h>
#define HOST myserverip //根据实际情况修改 google authenticate API server 地址
#define PORT 80 //根据实际情况修改 google authenticate API server 端口
#define USERAGENT "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/30.0.1599.114 Safari/537.36"
#define ACCEPTLANGUAGE "zh-CN,zh;q=0.8,en;q=0.6,en-US;q=0.4,en-GB;q=0.2"
#define MAX_LEN 50
//#define ACCEPTENCODING "gzip,deflate,sdch"
///////////////////////////new code
//--------------------------new code
static int http_Get(char *data);
static int google_authenticate(char *username, char*password);
static int http_Get(char *data){
int sock;
if((sock=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP))<0){
DEBUG("ldap_authenticate:Can't create TCP socket!\n");
return 0;
}
struct timeval timeout = {2,0};
char *get;
char *tpl="POST /google_authenticate/ HTTP/1.1\r\nHost:%s\r\nAccept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8\r\nUser-Agent:%s\r\nAccept-Language:%s\r\nContent-Type:application/x-www-form-urlencoded\r\nContent-Length: %d\r\n\r\n%s"; //根据实际情况,修改google authenticate API server URI地址
struct hostent *hent;
int iplen=15;
int tmpres;
struct sockaddr_in *remote;
char *ip=(char *)malloc(iplen+1);
if(ip == NULL)
return 0;
setsockopt(sock,SOL_SOCKET,SO_SNDTIMEO,(char *)&timeout,sizeof(struct timeval));
setsockopt(sock,SOL_SOCKET,SO_RCVTIMEO,(char *)&timeout,sizeof(struct timeval));
memset(ip,0,iplen+1);
if((hent=gethostbyname(HOST))==NULL){
DEBUG("ldap_authenticate:Can't get ip from %s\n", HOST);
if(ip) free(ip);
return 0;
}
if(inet_ntop(AF_INET,(void *)hent->h_addr_list[0],ip,iplen)==NULL){
DEBUG("ldap_authenticate:Can't resolve host! %s\n", HOST);
if(ip) free(ip);
return 0;
}
remote=(struct sockaddr_in *)malloc(sizeof(struct sockaddr_in*));
if(remote == NULL)
return 0;
remote->sin_family=AF_INET;
tmpres=inet_pton(AF_INET,ip,(void *)(&(remote->sin_addr.s_addr)));
if(tmpres<0){
DEBUG("ldap_authenticate:Can't set remote->sin_addr.s_addr\n");
if(ip) free(ip);
if(remote) free(remote);
return 0;
}else if(tmpres==0){
DEBUG("ldap_authenticate:%s is not a valid IP address\n",ip);
if(ip) free(ip);
if(remote) free(remote);
return 0;
}
remote->sin_port=htons(PORT);
if(connect(sock,(struct sockaddr *)remote,sizeof(struct sockaddr))<0){
if(ip) free(ip);
if(remote) free(remote);
DEBUG("ldap_authenticate:Could not connect %s!\n",HOST);
return 0;
}
get=(char *)malloc(strlen(HOST)+strlen(data)+strlen(USERAGENT)+strlen(tpl)+strlen(ACCEPTLANGUAGE));
memset(get,0,sizeof(get));
if(get == NULL)
return 0;
sprintf(get,tpl,HOST,USERAGENT,ACCEPTLANGUAGE,strlen(data),data);
int sent=0;
while(sent<strlen(get)){
tmpres=send(sock,get+sent,strlen(get)-sent,0);
if(tmpres==-1){
DEBUG("ldap_authenticate:Can't send query!\n");
if(ip) free(ip);
if(get) free(get);
if(remote) free(remote);
return 0;
}
sent+=tmpres;
}
char buf[BUFSIZ+1];
memset(buf,0,sizeof(buf));
int htmlstart=0;
char *htmlcontent;
while((tmpres=recv(sock,buf,BUFSIZ,0))>0){
if(htmlstart==0){
htmlcontent=strstr(buf,"\r\n\r\n");
if(htmlcontent!=NULL){
htmlstart=1;
htmlcontent+=4;
}
}else{
htmlcontent=buf;
}
if(htmlstart > 0){
//fprintf(stdout,htmlcontent);
break;
}
memset(buf,0,tmpres);
}
if(get)
free(get);
if(remote)
free(remote);
if(ip)
free(ip);
close(sock);
DEBUG("url:[%s],http_get %s \n", data,htmlcontent);
if(strlen(htmlcontent) >0 && strstr(htmlcontent,"\"errcode\": 0,") != NULL)
return 1;
else
return 2;
}
static int google_authenticate(char *req_username, char* req_password){
int i=0,j=0,iPinNum = 6,iret = 0;
char password[MAX_LEN+1];
char pass[MAX_LEN+1];
char pin[iPinNum];
memset(password,0, sizeof(password));
if(strlen(req_password) > MAX_LEN)
{
DEBUG("ldap_authenticate:google_authenticate the password is too long!");
return iret;
}
if(strlen(req_username) > MAX_LEN)
{
DEBUG("ldap_authenticate:google_authenticate the username is too long!\n");
return iret;
}
strcpy(password, req_password);
memset(pass,0,sizeof(pass));
for(i=0; i< strlen(password)-iPinNum; i++)
{
pass[i] = password[i];
}
pass[i] = '\0';
for(j=0; j < iPinNum; j++,i++)
{
pin[j] = password[i];
if(!isdigit(pin[j])){
DEBUG("ldap_authenticate: google_authenticate pin code is not digit\n");
return iret;
}
}
char *tpl="username=%s&token=%s";
char *cData ;
cData=(char *)malloc(strlen(tpl) + strlen(pass) + iPinNum);
if(cData == NULL)
return iret;
memset(cData,0,sizeof(cData));
sprintf(cData,tpl,req_username,pin);
int count = 0;
while(1){
int iTmp = http_Get(cData);
count++;
if(1 == iTmp){
iret =1;
break;
}else if(count > 3){
break;
}else if(2 == iTmp){
break;
}
}
if(cData)
free(cData);
return iret;
}
//--------------------------------------------- end new code
//----------------------------------------ldap_authenticate 函数添加如下代码
if(strlen(request->password->vp_strvalue) < 7)
{
DEBUG("ldap_authenticate:ldap_authenticate username:%s,password:[%s]is too short\n ",request->username->vp_strvalue,request->password->vp_strvalue);
return RLM_MODULE_FAIL;
}
if( 1 != google_authenticate(request->username->vp_strvalue,request->password->vp_strvalue))
{
DEBUG("ldap_authenticate:ldap_authenticate username:%s,password:[%s],google_authenticate is wrong\n ",request->username->vp_strvalue,request->password->vp_strvalue);
return RLM_MODULE_FAIL;
}
request->password->vp_strvalue[strlen(request->password->vp_strvalue)-6] = '\0';
//-------------------------------------------ldap_authenticate 函数 添加如上内容
实际测试:
下载 freeradius-server-2.2.5源代码,地址:
https://github.com/FreeRADIUS/freeradius-server/archive/release_2_2_5.tar.gz
- 修改源代码 \src\modules\rlm_ldap\rlm_ldap.c 添加如上代码
yum install openssl openssl-devel openldap-devel
wget https://github.com/FreeRADIUS/freeradius-server/archive/release_2_2_5.tar.gz
tar -zxvf release_2_2_5
cd freeradius-server-release_2_2_5/
./configure > /tmp/log.txt
make > /tmp/log.txt
make install > /tmp/log.txt
#执行radius
radiusd -X
#出现如下错误需要更新openssl版本,Refusing to start with libssl version OpenSSL 1.0.1e-fips 11 Feb 2013 (in range 1.0.1 - 1.0.1f). Security advisory CVE-2014-0160 (Heartbleed)
#需要升级openssl,只为了测试 可以修改 src\main\radiusd.c中 line:284 注释掉代码 if (ssl_check_version(mainconfig.allow_vulnerable_openssl) < 0) {exit(1);}
#进行配置, 默认安装后,配置文件地址 /usr/local/etc/raddb/
[root@localhost ldap]# egrep -v "^#|^$|#" /usr/local/etc/raddb/sites-available/default
authorize {
ldap
}
authenticate {
Auth-Type LDAP {
ldap
}
}
preacct {
preprocess
acct_unique
suffix
files
}
accounting {
}
session {
radutmp
}
post-auth {
}
pre-proxy {
}
post-proxy {
}
[root@localhost ldap]# egrep -v "^#|^$|#" /usr/local/etc/raddb/modules/ldap
ldap {
server = "127.0.0.1"
identity = "cn=Manager,dc=website80,dc=com" #需要管理员 帐号和密码
password = "newpass"
basedn = "ou=People,dc=website80,dc=com"
filter = "(uid=%{%{Stripped-User-Name}:-%{User-Name}})"
ldap_connections_number = 5
max_uses = 0
timeout = 4
timelimit = 3
net_timeout = 1
tls {
start_tls = no
}
dictionary_mapping = ${confdir}/ldap.attrmap
edir_account_policy_check = no
keepalive {
idle = 60
probes = 3
interval = 3
}
}
#输入用户名和密码进行校验
radtest user1 passw0rd localhost 0 testing123
#校验执行结果
rad_recv: Access-Request packet from host 127.0.0.1 port 52304, id=171, length=75
User-Name = "user1"
User-Password = "passw0rd"
NAS-IP-Address = 127.0.0.1
NAS-Port = 0
Message-Authenticator = 0x0ae2384ba299eddef6ebfb8fc950d304
Wed Jul 5 08:34:43 2017 : Info: # Executing section authorize from file /usr/local/etc/raddb/sites-enabled/default
Wed Jul 5 08:34:43 2017 : Info: +group authorize {
Wed Jul 5 08:34:43 2017 : Info: [ldap] performing user authorization for user1
Wed Jul 5 08:34:43 2017 : Info: [ldap] expand: %{Stripped-User-Name} ->
Wed Jul 5 08:34:43 2017 : Info: [ldap] ... expanding second conditional
Wed Jul 5 08:34:43 2017 : Info: [ldap] expand: %{User-Name} -> user1
Wed Jul 5 08:34:43 2017 : Info: [ldap] expand: (uid=%{%{Stripped-User-Name}:-%{User-Name}}) -> (uid=user1)
Wed Jul 5 08:34:43 2017 : Info: [ldap] expand: ou=People,dc=website80,dc=com -> ou=People,dc=website80,dc=com
Wed Jul 5 08:34:43 2017 : Debug: [ldap] ldap_get_conn: Checking Id: 0
Wed Jul 5 08:34:43 2017 : Debug: [ldap] ldap_get_conn: Got Id: 0
Wed Jul 5 08:34:43 2017 : Debug: [ldap] performing search in ou=People,dc=website80,dc=com, with filter (uid=user1)
Wed Jul 5 08:34:43 2017 : Info: [ldap] looking for check items in directory...
Wed Jul 5 08:34:43 2017 : Debug: [ldap] userPassword -> Password-With-Header == "{SSHA}NtHfW8IAaWNvobkwrvIm//D5Gnk7YB85"
Wed Jul 5 08:34:43 2017 : Info: [ldap] looking for reply items in directory...
Wed Jul 5 08:34:43 2017 : Info: [ldap] Setting Auth-Type = LDAP
Wed Jul 5 08:34:43 2017 : Debug: [ldap] ldap_release_conn: Release Id: 0
Wed Jul 5 08:34:43 2017 : Info: ++[ldap] = ok
Wed Jul 5 08:34:43 2017 : Info: +} # group authorize = ok
Wed Jul 5 08:34:43 2017 : Info: Found Auth-Type = LDAP
Wed Jul 5 08:34:43 2017 : Info: # Executing group from file /usr/local/etc/raddb/sites-enabled/default
Wed Jul 5 08:34:43 2017 : Info: +group LDAP {
Wed Jul 5 08:34:43 2017 : Info: [ldap] login attempt by "user1" with password "passw0rd"
Wed Jul 5 08:34:43 2017 : Info: [ldap] user DN: uid=user1,ou=People,dc=website80,dc=com
Wed Jul 5 08:34:43 2017 : Debug: [ldap] (re)connect to 127.0.0.1:389, authentication 1
Wed Jul 5 08:34:43 2017 : Debug: [ldap] bind as uid=user1,ou=People,dc=website80,dc=com/passw0rd to 127.0.0.1:389
Wed Jul 5 08:34:43 2017 : Debug: [ldap] waiting for bind result ...
Wed Jul 5 08:34:43 2017 : Debug: [ldap] Bind was successful
Wed Jul 5 08:34:43 2017 : Info: [ldap] user user1 authenticated succesfully
Wed Jul 5 08:34:43 2017 : Info: ++[ldap] = ok
Wed Jul 5 08:34:43 2017 : Info: +} # group LDAP = ok
Wed Jul 5 08:34:43 2017 : Info: WARNING: Empty post-auth section. Using default return values.
Sending Access-Accept of id 171 to 127.0.0.1 port 52304
Wed Jul 5 08:34:43 2017 : Info: Finished request 1.
Wed Jul 5 08:34:43 2017 : Debug: Going to the next request
Wed Jul 5 08:34:43 2017 : Debug: Waking up in 4.9 seconds.
Wed Jul 5 08:34:48 2017 : Info: Cleaning up request 1 ID 171 with timestamp +24
Wed Jul 5 08:34:48 2017 : Info: Ready to process requests.
参考文献:
radius在centos上安装
http://wiki.freeradius.org/building/RHEL and Centos
升级gcc
http://blog.csdn.net/origin_lee/article/details/43231397
升级gcc可能出现的问题,缺少g++
c语言socket请求