SecureToken.pm 6.67 KB
Newer Older
1 2 3 4 5 6 7
##@file
# Secure Token

##@class
# Secure Token
#
# Create a secure token used to resolve user identity by a protected application
8 9 10

# This specific handler is intended to be called directly by Apache

11
package Lemonldap::NG::Handler::Lib::SecureToken;
12 13 14 15 16

use strict;
use Cache::Memcached;
use Apache::Session::Generate::MD5;

17
our $VERSION = '2.0.0';
18 19

# Shared variables
20
our $secureTokenMemcachedConnection;
21 22 23 24 25 26 27 28

BEGIN {
    eval {
        require threads::shared;
        threads::share($secureTokenMemcachedConnection);
    };
}

29 30 31
## @rmethod Apache2::Const run(Apache2::RequestRec r)
# Overload main run method
# @param r Current request
32
# @return Apache2::Const value ($class->OK, $class->FORBIDDEN, $class->REDIRECT or $class->SERVER_ERROR)
33 34 35
sub run {
    my $class = shift;
    my $r     = $_[0];
36
    my $ret   = $class->Lemonldap::NG::Handler::Main::run($r);
37 38

    # Continue only if user is authorized
39
    return $ret unless ( $ret == $class->OK );
40 41

    # Get current URI
42
    my $uri = $r->{env}->{REQUEST_URI};
43 44

    # Catch Secure Token parameters
45
    my $localConfig = $class->localConfig;
46 47 48 49 50 51
    my $secureTokenMemcachedServers =
      $localConfig->{secureTokenMemcachedServers} || ['127.0.0.1:11211'];
    my $secureTokenExpiration = $localConfig->{secureTokenExpiration} || 60;
    my $secureTokenAttribute  = $localConfig->{secureTokenAttribute}  || 'uid';
    my $secureTokenUrls       = $localConfig->{'secureTokenUrls'}     || ['.*'];
    my $secureTokenHeader = $localConfig->{secureTokenHeader} || 'Auth-Token';
52 53
    my $secureTokenAllowOnError = $localConfig->{'secureTokenAllowOnError'}
      // 1;
54 55 56 57 58

    # Force some parameters to be array references
    foreach (qw/secureTokenMemcachedServers secureTokenUrls/) {
        no strict 'refs';
        unless ( ref ${$_} eq "ARRAY" ) {
59
            $class->logger->debug("Transform $_ value into an array reference");
60 61 62 63 64 65
            my @array = split( /\s+/, ${$_} );
            ${$_} = \@array;
        }
    }

    # Display found values in debug mode
66 67 68 69 70 71 72
    $class->logger->debug(
        "secureTokenMemcachedServers: @$secureTokenMemcachedServers");
    $class->logger->debug("secureTokenExpiration: $secureTokenExpiration");
    $class->logger->debug("secureTokenAttribute: $secureTokenAttribute");
    $class->logger->debug("secureTokenUrls: @$secureTokenUrls");
    $class->logger->debug("secureTokenHeader: $secureTokenHeader");
    $class->logger->debug("secureTokenAllowOnError: $secureTokenAllowOnError");
73 74 75 76 77 78

    # Return if we are not on a secure token URL
    my $checkurl = 0;
    foreach (@$secureTokenUrls) {
        if ( $uri =~ m#$_# ) {
            $checkurl = 1;
79 80
            $class->logger->debug(
                "URL $uri detected as an Secure Token URL (rule $_)");
81 82 83
            last;
        }
    }
84
    return $class->OK unless ($checkurl);
85 86 87

    # Test Memcached connection
    unless ( $class->_isAlive() ) {
88 89
        $secureTokenMemcachedConnection =
          $class->_createMemcachedConnection($secureTokenMemcachedServers);
90 91 92
    }

    # Exit if no connection
93 94
    return $class->_returnError( $r, $secureTokenAllowOnError )
      unless $class->_isAlive();
95 96

    # Value to store
97
    my $value = $class->datas->{$secureTokenAttribute};
98 99

    # Set token
100 101
    my $key = $class->_setToken( $value, $secureTokenExpiration );
    return $class->_returnError( $r, $secureTokenAllowOnError ) unless $key;
102 103

    # Header location
104
    $class->set_header_in( $r, $secureTokenHeader => $key );
105 106 107 108 109 110 111 112 113 114 115 116 117

    # Remove token
    eval 'use Apache2::Filter' unless ( $INC{"Apache2/Filter.pm"} );

    $r->add_output_filter(
        sub {
            my $f = shift;
            while ( $f->read( my $buffer, 1024 ) ) {
                $f->print($buffer);
            }
            if ( $f->seen_eos ) {
                $class->_deleteToken($key);
            }
118
            return $class->OK;
119 120 121
        }
    );

122 123
    # Return $class->OK
    return $class->OK;
124 125
}

126
## @method private Cache::Memcached _createMemcachedConnection(ArrayRef secureTokenMemcachedServers)
127
# Create Memcached connexion
128
# @param $secureTokenMemcachedServers Memcached servers
129 130
# @return Cache::Memcached object
sub _createMemcachedConnection {
131
    my ( $class, $secureTokenMemcachedServers ) = @_;
132 133 134 135 136 137 138

    # Open memcached connexion
    my $memd = new Cache::Memcached {
        'servers' => $secureTokenMemcachedServers,
        'debug'   => 0,
    };

139
    $class->logger->debug("Memcached connection created");
140 141 142 143

    return $memd;
}

144
## @method private string _setToken(string value, int secureTokenExpiration)
145 146
# Set token value
# @param value Value
147
# @param secureTokenExpiration expiration
148 149
# @return Token key
sub _setToken {
150
    my ( $class, $value, $secureTokenExpiration ) = @_;
151 152 153 154 155 156 157 158

    my $key = Apache::Session::Generate::MD5::generate();

    my $res =
      $secureTokenMemcachedConnection->set( $key, $value,
        $secureTokenExpiration );

    unless ($res) {
159
        $class->( "Unable to store secure token $key", 'error' );
160 161 162
        return;
    }

163
    $class->logger->info("Set $value in token $key");
164 165 166 167 168 169 170 171 172

    return $key;
}

## @method private boolean _deleteToken(string key)
# Delete token
# @param key Key
# @return result
sub _deleteToken {
173
    my ( $class, $key ) = @_;
174 175 176 177

    my $res = $secureTokenMemcachedConnection->delete($key);

    unless ($res) {
178
        $class->( "Unable to delete secure token $key", 'error' );
179 180
    }
    else {
181
        $class->logger->info("Token $key deleted");
182 183 184 185 186 187 188 189 190 191
    }

    return $res;
}

## @method private boolean _isAlive()
# Run a STATS command to see if Memcached connection is alive
# @param connection Cache::Memcached object
# @return result
sub _isAlive {
192
    my ($class) = @_;
193 194 195 196 197 198 199 200 201

    return 0 unless defined $secureTokenMemcachedConnection;

    my $stats = $secureTokenMemcachedConnection->stats();

    if ( $stats and defined $stats->{'total'} ) {
        my $total_c = $stats->{'total'}->{'connection_structures'};
        my $total_i = $stats->{'total'}->{'total_items'};

202
        $class->(
203 204 205 206 207 208 209
"Memcached connection is alive ($total_c connections / $total_i items)",
            'debug'
        );

        return 1;
    }

210
    $class->( "Memcached connection is not alive", 'error' );
211 212 213 214

    return 0;
}

215
## @method private int _returnError(boolean secureTokenAllowOnError)
216
# Give hand back to Apache
217
# @param secureTokenAllowOnError
218 219
# @return Apache2::Const value
sub _returnError {
220
    my ( $class, $r, $secureTokenAllowOnError ) = @_;
221 222

    if ($secureTokenAllowOnError) {
223 224
        $class->( "Allow request without secure token", 'debug' );
        return $class->OK;
225 226 227
    }

    # Redirect or Forbidden?
228
    if ( $class->tsv->{useRedirectOnError} ) {
229
        $class->logger->debug("Use redirect for error");
230
        return $class->goToError( '/', 500 );
231 232 233
    }

    else {
234
        $class->logger->debug("Return error");
235
        return $class->SERVER_ERROR;
236 237 238 239
    }
}

1;