SecureToken.pm 6.76 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 36 37 38
sub run {
    my $class = shift;
    my $r     = $_[0];
    my $ret   = $class->SUPER::run();

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

    # Get current URI
    my $uri = Lemonldap::NG::Handler::API->uri_with_args($r);
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 60
            $class->lmLog( "Transform $_ value into an array reference",
                'debug' );
61 62 63 64 65 66
            my @array = split( /\s+/, ${$_} );
            ${$_} = \@array;
        }
    }

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

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

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

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

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

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

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

    # 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);
            }
120
            return $class->OK;
121 122 123
        }
    );

124 125
    # Return $class->OK
    return $class->OK;
126 127
}

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

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

141
    $class->lmLog( "Memcached connection created", 'debug' );
142 143 144 145

    return $memd;
}

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

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

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

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

165
    $class->lmLog( "Set $value in token $key", 'info' );
166 167 168 169 170 171 172 173 174

    return $key;
}

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

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

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

    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 {
194
    my ($class) = @_;
195 196 197 198 199 200 201 202 203

    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'};

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

        return 1;
    }

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

    return 0;
}

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

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

    # Redirect or Forbidden?
230
    if ( $class->tsv->{useRedirectOnError} ) {
231
        $class->lmLog( "Use redirect for error", 'debug' );
232
        return $class->goToPortal( '/', 'lmError=500' );
233 234 235
    }

    else {
236 237
        $class->lmLog( "Return error", 'debug' );
        return $class->SERVER_ERROR;
238 239 240 241
    }
}

1;