diff --git a/privacyidea_radius.pm b/privacyidea_radius.pm index 39479c6..b6f8a12 100644 --- a/privacyidea_radius.pm +++ b/privacyidea_radius.pm @@ -20,7 +20,7 @@ # Add the possibility to read config from # /etc/privacyidea/rlm_perl.ini # 2015-06-10 Cornelius Kölbel -# 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 # fix typo in log @@ -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 . -# +# # # Copyright 2002 The FreeRADIUS server project # Copyright 2002 Boian Jordanov @@ -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/ @@ -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] @@ -135,7 +135,7 @@ Cornelius Koelbel (conrelius@privacyidea.org) 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 @@ -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 ... @@ -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", @@ -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} = ""; @@ -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 { @@ -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; @@ -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; @@ -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; @@ -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'}" ); @@ -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 " ); @@ -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; @@ -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); + $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"; diff --git a/rlm_perl.ini b/rlm_perl.ini index 3962355..46b4044 100644 --- a/rlm_perl.ini +++ b/rlm_perl.ini @@ -1,5 +1,9 @@ [Default] URL = https://localhost/validate/check +POLLURL = https://localhost/token/challenges +AUTHURL = https://localhost/auth +PIUSER = username +PIPASS = password #REALM = someRealm #RESCONF = someResolver SSL_CHECK = false