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
dcoutadeur dcoutadeur's avatar
 
dcoutadeur dcoutadeur committed
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;

Xavier Guimard's avatar
Xavier Guimard committed
17
our $VERSION = '2.1.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
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
    our $secureTokenMemcachedServers =
47 48 49
      $localConfig->{secureTokenMemcachedServers} || ['127.0.0.1:11211'];
    my $secureTokenExpiration = $localConfig->{secureTokenExpiration} || 60;
    my $secureTokenAttribute  = $localConfig->{secureTokenAttribute}  || 'uid';
50
    our $secureTokenUrls = $localConfig->{'secureTokenUrls'} || ['.*'];
51
    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"} and defined $r->{env}->{'psgi.r'} ) {
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
    return $class->OK;
125 126
}

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

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

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

    return $memd;
}

145
## @method private string _setToken(string value, int secureTokenExpiration)
146 147
# Set token value
# @param value Value
148
# @param secureTokenExpiration expiration
149 150
# @return Token key
sub _setToken {
151
    my ( $class, $value, $secureTokenExpiration ) = @_;
152 153 154 155 156 157
    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
158
        $class->logger->error("Unable to store secure token $key");
159 160 161
        return;
    }

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

    return $key;
}

## @method private boolean _deleteToken(string key)
# Delete token
# @param key Key
# @return result
sub _deleteToken {
172
    my ( $class, $key ) = @_;
173 174 175
    my $res = $secureTokenMemcachedConnection->delete($key);

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

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

        return 1;
    }

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

    return 0;
}

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

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

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

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

1;