Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Push Token Functionality #28

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
205 changes: 159 additions & 46 deletions privacyidea_radius.pm
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
# Add the possibility to read config from
# /etc/privacyidea/rlm_perl.ini
# 2015-06-10 Cornelius Kölbel <[email protected]>
# Add using of Stripped-User-Name and Realm from the
# Add using of Stripped-User-Name and Realm from the
# RAD_REQUEST
# 2015-04-10 Cornelius Kölbel <[email protected]>
# fix typo in log
Expand All @@ -29,22 +29,22 @@
# 2014-06-25 Cornelius Kölbel
# changed the used modules from Config::Files to Config::IniFile
# to make it easily run on CentOS with EPEL, without CPAN
#
#
# Copyright (C) 2010 - 2014 LSE Leading Security Experts GmbH
#
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
#
#
# Copyright 2002 The FreeRADIUS server project
# Copyright 2002 Boian Jordanov <[email protected]>
Expand All @@ -64,23 +64,23 @@

=head1 NAME

freeradius_perl - Perl module for use with FreeRADIUS rlm_perl, to authenticate against
freeradius_perl - Perl module for use with FreeRADIUS rlm_perl, to authenticate against
LinOTP http://www.linotp.org
privacyIDEA http://www.privacyidea.org

=head1 SYNOPSIS

use with freeradius:
use with freeradius:

Configure rlm_perl to work with privacyIDEA:
in /etc/freeradius/users
in /etc/freeradius/users
set:
DEFAULT Auth-type := perl

in /etc/freeradius/modules/perl
point
perl {
module =
module =
to this file

in /etc/freeradius/sites-enabled/<yoursite>
Expand All @@ -93,33 +93,33 @@ freeradius_perl - Perl module for use with FreeRADIUS rlm_perl, to authenticate

This module enables freeradius to authenticate using privacyIDEA or LinOTP.

TODO:
TODO:
* checking of server certificate


=head2 Methods

* authenticate


=head1 CONFIGURATION

The authentication request with its URL and default LinOTP/privacyIDEA Realm
The authentication request with its URL and default LinOTP/privacyIDEA Realm
could be defined in a dedicated configuration file, which is expected to be:

/opt/privacyIDEA/rlm_perl.ini

This configuration file could contain default definition for URL and REALM like
[Default]
URL = http://192.168.56.1:5001/validate/check
REALM =
REALM =

But as well could contain "Access-Type" specific configurations, e.g. for the
But as well could contain "Access-Type" specific configurations, e.g. for the
Access-Type 'scope1', this would look like:

[Default]
URL = https://localhost/validate/check
REALM =
REALM =
CLIENTATTRIBUTE = Calling-Station-Id

[scope1]
Expand All @@ -135,7 +135,7 @@ Cornelius Koelbel ([email protected])

Copyright 2013, 2014

This library is free software; you can redistribute it
This library is free software; you can redistribute it
under the GPLv2.

=head1 SEE ALSO
Expand All @@ -151,6 +151,8 @@ use Data::Dump;
use Try::Tiny;
use JSON;
use Time::HiRes qw( gettimeofday tv_interval );
use Date::Parse;
use Date::Format;


# use ...
Expand All @@ -177,12 +179,12 @@ use constant RLM_MODULE_NOOP => 7; # /* module succeeded without doing anyt
use constant RLM_MODULE_UPDATED => 8; # /* OK (pairs modified) */
use constant RLM_MODULE_NUMCODES => 9; # /* How many return codes there are */

our $ret_hash = {
our $ret_hash = {
0 => "RLM_MODULE_REJECT",
1 => "RLM_MODULE_FAIL",
2 => "RLM_MODULE_OK",
3 => "RLM_MODULE_HANDLED",
4 => "RLM_MODULE_INVALID",
4 => "RLM_MODULE_INVALID",
5 => "RLM_MODULE_USERLOCK",
6 => "RLM_MODULE_NOTFOUND",
7 => "RLM_MODULE_NOOP",
Expand Down Expand Up @@ -211,6 +213,10 @@ our $cfg_file;

$Config->{FSTAT} = "not found!";
$Config->{URL} = 'https://127.0.0.1/validate/check';
$Config->{AUTHURL} = 'https://127.0.0.1/auth';
$Config->{POLLURL} = 'https://127.0.0.1/token/challenges';
$Config->{PIUSER} = '';
$Config->{PIPASS} = '';
$Config->{REALM} = '';
$Config->{CLIENTATTRIBUTE} = '';
$Config->{RESCONF} = "";
Expand All @@ -226,14 +232,18 @@ foreach my $file (@CONFIG_FILES) {
$CONFIG_FILE = $file;
$Config->{FSTAT} = "found!";
$Config->{URL} = $cfg_file->val("Default", "URL");
$Config->{AUTHURL} = $cfg_file->val("Default", "AUTHURL");
$Config->{POLLURL} = $cfg_file->val("Default", "POLLURL");
$Config->{PIUSER} = $cfg_file->val("Default", "PIUSER");
$Config->{PIPASS} = $cfg_file->val("Default", "PIPASS");
$Config->{REALM} = $cfg_file->val("Default", "REALM");
$Config->{RESCONF} = $cfg_file->val("Default", "RESCONF");
$Config->{Debug} = $cfg_file->val("Default", "DEBUG");
$Config->{SPLIT_NULL_BYTE} = $cfg_file->val("Default", "SPLIT_NULL_BYTE");
$Config->{SPLIT_NULL_BYTE} = $cfg_file->val("Default", "SPLIT_NULL_BYTE");
$Config->{SSL_CHECK} = $cfg_file->val("Default", "SSL_CHECK");
$Config->{TIMEOUT} = $cfg_file->val("Default", "TIMEOUT", 10);
$Config->{CLIENTATTRIBUTE} = $cfg_file->val("Default", "CLIENTATTRIBUTE");
}
$Config->{CLIENTATTRIBUTE} = $cfg_file->val("Default", "CLIENTATTRIBUTE");
}
}

sub mapResponse {
Expand Down Expand Up @@ -279,8 +289,8 @@ sub mapResponse {
my @values = ();
if (ref($attributevalue) eq "") {
&radiusd::radlog(Info, "+++++++ User attribute is a string: $attributevalue");
push(@values, $attributevalue);
}
push(@values, $attributevalue);
}
if (ref($attributevalue) eq "ARRAY") {
&radiusd::radlog(Info, "+++++++ User attribute is a list: $attributevalue");
@values = @$attributevalue;
Expand All @@ -298,28 +308,32 @@ sub mapResponse {
}
}
}

foreach my $key ($cfg_file->Parameters("Mapping")) {
my $radiusAttribute = $cfg_file->val("Mapping", $key);
&radiusd::radlog( Info, "+++ Map: $key -> $radiusAttribute");
$radReply{$radiusAttribute} = $decoded->{detail}{$key};
}

return %radReply;
}

# Function to handle authenticate
sub authenticate {

## show where the config comes from -
## show where the config comes from -
# in the module init we can't print this out, so it starts here
&radiusd::radlog( Info, "Config File $CONFIG_FILE ".$Config->{FSTAT} );

# we inherrit the defaults
my $URL = $Config->{URL};
my $AUTHURL = $Config->{AUTHURL};
my $POLLURL = $Config->{POLLURL};
my $PIUSER = $Config->{PIUSER};
my $PIPASS = $Config->{PIPASS};
my $REALM = $Config->{REALM};
my $RESCONF = $Config->{RESCONF};

my $debug = false;
if ( $Config->{Debug} =~ /true/i ) {
$debug = true;
Expand All @@ -339,20 +353,31 @@ sub authenticate {
my $auth_type = $RAD_CONFIG{"Auth-Type"};

try {
&radiusd::radlog( Info, "Looking for config for auth-type $auth_type");
if ( ( $cfg_file->val( $auth_type, "URL") )) {
&radiusd::radlog( Info, "Looking for config for auth-type $auth_type");
if ( ( $cfg_file->val( $auth_type, "URL") )) {
$URL = $cfg_file->val( $auth_type, "URL" );
}
if ( ( $cfg_file->val( $auth_type, "REALM") )) {
if ( ( $cfg_file->val( $auth_type, "AUTHURL") )) {
$AUTHURL = $cfg_file->val( $auth_type, "AUTHURL" );
}
if ( ( $cfg_file->val( $auth_type, "POLLURL") )) {
$POLLURL = $cfg_file->val( $auth_type, "POLLURL" );
}
if ( ( $cfg_file->val( $auth_type, "PIUSER") )) {
$PIUSER = $cfg_file->val( $auth_type, "PIUSER" );
}
if ( ( $cfg_file->val( $auth_type, "PIPASS") )) {
$PIPASS = $cfg_file->val( $auth_type, "PIPASS" );
}
if ( ( $cfg_file->val( $auth_type, "REALM") )) {
$REALM = $cfg_file->val( $auth_type, "REALM" );
}
}
if ( ( $cfg_file->val( $auth_type, "RESCONF") )) {
$RESCONF = $cfg_file->val( $auth_type, "RESCONF" );
}
}
catch {
} catch {
&radiusd::radlog( Info, "Warning: $@" );
};
};

if ( $debug == true ) {
&log_request_attributes;
Expand Down Expand Up @@ -401,6 +426,8 @@ sub authenticate {

&radiusd::radlog( Info, "Auth-Type: $auth_type" );
&radiusd::radlog( Info, "url: $URL" );
&radiusd::radlog( Info, "pollurl: $POLLURL" );
&radiusd::radlog( Info, "authurl: $AUTHURL" );
&radiusd::radlog( Info, "user sent to privacyidea: $params{'user'}" );
&radiusd::radlog( Info, "realm sent to privacyidea: $params{'realm'}" );
&radiusd::radlog( Info, "resolver sent to privacyidea: $params{'resConf'}" );
Expand All @@ -414,7 +441,7 @@ sub authenticate {
&radiusd::radlog( Info, "urlparam $_ \n" ) for ( keys %params );
}

my $ua = LWP::UserAgent->new();
my $ua = LWP::UserAgent->new();
$ua->env_proxy;
$ua->timeout($timeout);
&radiusd::radlog( Info, "Request timeout: $timeout " );
Expand Down Expand Up @@ -453,7 +480,7 @@ sub authenticate {
my $message = $decoded->{detail}{message};
if ( $decoded->{result}{value} ) {
&radiusd::radlog( Info, "privacyIDEA access granted" );
$RAD_REPLY{'Reply-Message'} = "privacyIDEA access granted";
$RAD_REPLY{'Reply-Message'} = "privacyIDEA access granted";
# Add the response hash to the Radius Reply
%RAD_REPLY = ( %RAD_REPLY, mapResponse($decoded));
$g_return = RLM_MODULE_OK;
Expand All @@ -462,17 +489,103 @@ sub authenticate {
&radiusd::radlog( Info, "privacyIDEA Result status is true!" );
$RAD_REPLY{'Reply-Message'} = $decoded->{detail}{message};
if ( $decoded->{detail}{transaction_id} ) {
$RAD_REPLY{'State'} = $decoded->{detail}{transaction_id};
## we are in challenge response mode:
## 1. split the response in fail, state and challenge
## OTP
## 1. split the response into fail, state and challenge
## 2. show the client the challenge and the state
## 3. get the response and
## 4. submit the response and the state to linotp and
## 4. submit the response and the state to privacyIDEA and
## 5. reply ok or reject
$RAD_REPLY{'State'} = $decoded->{detail}{transaction_id};
$RAD_CHECK{'Response-Packet-Type'} = "Access-Challenge";
# Add the response hash to the Radius Reply
%RAD_REPLY = ( %RAD_REPLY, mapResponse($decoded));
$g_return = RLM_MODULE_HANDLED;
##
## PUSH
## 1. split the response into fail, state and challenge
## 2. poll endpoint for challenge
## 3. reply ok or reject if expired
my $token_type = $decoded->{detail}{type};
if ($token_type eq "push") {
# perform periodic polling for response
my $token_serial = $decoded->{detail}{serial};
$RAD_REPLY{'Reply-Message'} = "privacyIDEA token challenge poll failed";
$g_return = RLM_MODULE_FAIL;

# retrieve an authorization token from privacyIDEA
%params = ();
$params{"username"} = $PIUSER;
$params{"password"} = $PIPASS;
$starttime = [gettimeofday];
$response = $ua->post( $AUTHURL, \%params );
$content = $response->decoded_content();
$elapsedtime = tv_interval($starttime);
&radiusd::radlog( Info, "elapsed time for privacyidea call: $elapsedtime" );
if ( $debug == true ) {
&radiusd::radlog( Debug, "Auth Content $content" );
}
$decoded = $coder->decode($content);
if ($response->is_success && $decoded->{result}{value}) {
my $auth_token = $decoded->{result}{value}{token};
$ua->default_header('Authorization' => $auth_token);

# periodic poll for challenges
my $continue_poll = true;
my $challenge_found = false;
do {
sleep(5);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are blocking the RADIUS request in the freeradius server.
So the VPN is still waiting for a RADIUS response from the RADIUS server and the UDP request will soon time out.

How would this be better than having privacyIDEA itself using push_wait?

$response = $ua->get("$POLLURL/$token_serial");
my $content = $response->decoded_content();
if ($debug == true) {
&radiusd::radlog(Debug, "TokenChallenges Content $content");
}
$decoded = $coder->decode($content);
if ($response->is_success && $decoded->{result}{status}) {
# check for challenge matching our transaction id
for my $challenge( @{$decoded->{result}{value}{challenges}} ) {
if ($debug == true) {
&radiusd::radlog(Debug, $challenge);
}

if ($challenge->{"transaction_id"} eq $RAD_REPLY{'State'}) {
$challenge_found = true;
&radiusd::radlog( Info, "challenge object found: ".$challenge->{"challenge"} );
if ($challenge->{"otp_valid"} == true) {
# user approved request
&radiusd::radlog( Info, "privacyIDEA access granted" );
$RAD_REPLY{'Reply-Message'} = "privacyIDEA access granted";
# Add the response hash to the Radius Reply
%RAD_REPLY = ( %RAD_REPLY, mapResponse($decoded));
$g_return = RLM_MODULE_OK;
$continue_poll = false;
last;
} else {
# check for expiration of request
my $expiry = str2time($challenge->{"expiration"});
if ($expiry < time()) {
&radiusd::radlog( Info, "privacyIDEA access denied" );
$RAD_REPLY{'Reply-Message'} = "privacyIDEA push token timed out!";
$g_return = RLM_MODULE_REJECT;
$continue_poll = false;
last;
}
}
}
}
} else {
# request failed
if ($debug == true) {
&radiusd::radlog(Debug, "privacyIDEA token challenge failed");
}
last;
}

} while ($continue_poll && $challenge_found)

}
} else {
$RAD_CHECK{'Response-Packet-Type'} = "Access-Challenge";
# Add the response hash to the Radius Reply
%RAD_REPLY = (%RAD_REPLY, mapResponse($decoded));
$g_return = RLM_MODULE_HANDLED;
}
} else {
&radiusd::radlog( Info, "privacyIDEA access denied" );
#$RAD_REPLY{'Reply-Message'} = "privacyIDEA access denied";
Expand Down
Loading