SecureToken.pm 6.79 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;

Christophe Maudoux's avatar
Christophe Maudoux committed
17
our $VERSION = '2.0.7';
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
sub run {
    my $class = shift;
Christophe Maudoux's avatar
Christophe Maudoux committed
35
    my $r     = shift;
36
    my ( $ret, $session ) = $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
Xavier Guimard's avatar
Xavier Guimard committed
97
    my $value = $class->data->{$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

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

109
    if ( $INC{"Apache2/Filter.pm"} ) {
110
        $r->{env}->{'psgi.r'}->add_output_filter(
111 112 113 114 115 116 117 118 119
            sub {
                my $f = shift;
                while ( $f->read( my $buffer, 1024 ) ) {
                    $f->print($buffer);
                }
                if ( $f->seen_eos ) {
                    $class->_deleteToken($key);
                }
                return $class->OK;
120
            }
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->logger->debug("Memcached connection created");
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
    my $key = Apache::Session::Generate::MD5::generate();
    my $res =
      $secureTokenMemcachedConnection->set( $key, $value,
        $secureTokenExpiration );

    unless ($res) {
Clément OUDOT's avatar
Clément OUDOT committed
159
        $class->logger->error("Unable to store secure token $key");
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
    my $res = $secureTokenMemcachedConnection->delete($key);

    unless ($res) {
Clément OUDOT's avatar
Clément OUDOT committed
177
        $class->logger->error("Unable to delete secure token $key");
178 179
    }
    else {
180
        $class->logger->info("Token $key deleted");
181 182 183 184 185 186 187 188 189 190
    }

    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 {
191
    my ($class) = @_;
192 193 194 195 196 197
    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'};
Christophe Maudoux's avatar
Christophe Maudoux committed
198
        $class->logger->debug(
Clément OUDOT's avatar
Clément OUDOT committed
199
"Memcached connection is alive ($total_c connections / $total_i items)"
200 201 202 203 204
        );

        return 1;
    }

Clément OUDOT's avatar
Clément OUDOT committed
205
    $class->logger->error("Memcached connection is not alive");
206 207 208 209

    return 0;
}

210
## @method private int _returnError(boolean secureTokenAllowOnError)
211
# Give hand back to Apache
212
# @param secureTokenAllowOnError
213 214
# @return Apache2::Const value
sub _returnError {
215
    my ( $class, $r, $secureTokenAllowOnError ) = @_;
216 217

    if ($secureTokenAllowOnError) {
Clément OUDOT's avatar
Clément OUDOT committed
218
        $class->logger->debug("Allow request without secure token");
219
        return $class->OK;
220 221 222
    }

    # Redirect or Forbidden?
223
    if ( $class->tsv->{useRedirectOnError} ) {
224
        $class->logger->debug("Use redirect for error");
225
        return $class->goToError( '/', 500 );
226 227 228
    }

    else {
229
        $class->logger->debug("Return error");
230
        return $class->SERVER_ERROR;
231 232 233 234
    }
}

1;