The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.

NAME

Cookie - Cookie Object with Encryption or Signature

SYNOPSIS

    use Cookie;
    my $c = Cookie->new(
        name => 'my-cookie',
        domain => 'example.com',
        value => 'sid1234567',
        path => '/',
        expires => '+10D',
        # or alternatively
        maxage => 864000
        # to make it exclusively accessible by regular http request and not javascript
        http_only => 1,
        same_site => 'Lax',
        # should it be used under ssl only?
        secure => 1,
    );
    # make the cookie expired
    # Sets the expiration datetime to Thu, 01 Jan 1970 09:00:00 GMT
    $c->elapse;
    # Get cookie as an hash reference
    my $hash = $c->as_hash;
    print $c->as_string, "\n";
    # or
    print "$c\n";
    # If expires is set, we can use its underlying DateTime object
    my $now = DateTime->now;
    if( $c->expires && $c->expires > $now )
    {
        # ok, we're good
    }
    # Unset expiration, effectively transforming it into a session cookie
    $c->expires( undef );
    print "Is session cookie? ", $c->is_session ? 'yes' : 'no', "\n";
    $c->match_host( 'www.example.com' );
    # Set max-age (in seconds) that takes precedence over expiration
    $c->max_age( 86400 );
    # Make it expired to tell the http client to remove it:
    $c->max_age(0) # or $c->max_age(-1)
    # Unset max-age
    $c->max_age( undef );
    print "Is it same? ", $c->same_as( $other ) ? 'yes' : 'no', "\n";
    # Conveniently set port, path and domain in one go, but not the secure flag
    $c->uri( 'https://www.example.com:8080/some/where' );

    # Create encrypted cookie
    # You can generate a key or type one as long as it meets the size requirement
    use Bytes::Random::Secure ();
    my $c = Cookie->new(
        name => 'my-cookie',
        domain => 'example.com',
        value => 'sid1234567',
        path => '/',
        expires => '+10D',
        # or alternatively
        maxage => 864000
        # to make it exclusively accessible by regular http request and not ajax
        http_only => 1,
        same_site => 'Lax',
        # should it be used under ssl only?
        secure => 1,
        # Encryption parameters
        key       => Bytes::Random::Secure::random_bytes(32),
        algo      => 'AES',
        encrypt   => 1,
    );
    print( "My encrypted cookie: $c\n" );

    # Sign cookie only
    my $c = Cookie->new(
        name => 'my-cookie',
        domain => 'example.com',
        value => 'sid1234567',
        path => '/',
        expires => '+10D',
        # or alternatively
        maxage => 864000
        # to make it exclusively accessible by regular http request and not ajax
        http_only => 1,
        same_site => 'Lax',
        # should it be used under ssl only?
        secure => 1,
        # Encryption parameters
        # No size constraint for signature, but obviously the longer the better
        key       => Bytes::Random::Secure::random_bytes(32),
        sign      => 1,
    );
    print( "My signed cookie: $c\n" );

VERSION

    v0.1.11

DESCRIPTION

This is a powerful and versatile package to create and represent a cookie compliant with the latest standard as set by rfc6265. This can be used as a standalone module, or can be managed as part of the cookie jar Cookie::Jar

The object is overloaded and will call "as_string" upon stringification and can also be used in comparison with other cookie object:

    if( $cookie1 eq $cookie2 )
    {
        # do something
    }

This module does not die upon error, but instead returns undef and sets an error, so you should always check the return value of a method.

See also the Cookie::Jar package to manage server and client side handling of cookies:

    use Cookie::Jar;
    # Possibly passing the cookie repository the Apache2::RequestRec object
    my $jar = Cookie::Jar->new( $r );
    my $c = $jar->make(
        name => 'my_cookie',
        value => 'some value',
        domain => 'example.org',
        path => '/',
        secure => 1,
        http_only => 1,
    ) || die( $jar->error );
    # Set it in the server response C<Set-Cookie> header:
    $jar->set( $c ) || die( $jar->error );

METHODS

new

Provided with an hash or hash reference of parameters, and this initiates a new cookie object and return it. Each of the following parameters has a corresponding method.

debug

Optional. If set with a positive integer, this will activate verbose debugging message

name string
value string
comment string
commentURL URI string or object
discard boolean
domain string
expires datetime str | DateTime object | integer
http_only boolean
implicit boolean
max_age integer
path string
port integer
same_site string
secure boolean
version integer

Other extra parameters not directly related to the cookie standard:

accessed_on datetime
algo string
created_on datetime
encrypt boolean
key string
sign boolean

accessed_on

Set or get the datetime of the cookie object last accessed.

According to rfc6265, section 5.3.12.3, when deciding which cookies to remove, for those who have equal removal priority:

"If two cookies have the same removal priority, the user agent MUST evict the cookie with the earliest last-access date first."

algo

This set or get the the algorithm used to encrypt the cookie value.

It can be any of AES, Anubis, Blowfish, CAST5, Camellia, DES, DES_EDE, KASUMI, Khazad, MULTI2, Noekeon, RC2, RC5, RC6, SAFERP, SAFER_K128, SAFER_K64, SAFER_SK128, SAFER_SK64, SEED, Skipjack, Twofish, XTEA, IDEA, Serpent or simply any <NAME> for which there exists Crypt::Cipher::<NAME>

See also Stackoverflow on the choice of encryption algorithm

By default, the algorithm is set to AES

If the algorithm set is unsupported, this method returns an error

It returns the current value as a scalar object

apply

Provided with an hash ore hash reference of cookie parameter, and this will apply them to each of their equivalent method.

    $c->apply(
        expires => 'now',
        secure => 1,
        http_only => 1,
    );

In the example above, this will call methods "expires", "secure" and "http_only" passing them the relevant values.

It returns the current object.

as_hash

Returns an hash reference of the cookie value.

The hash reference returned will contain the following keys: name value comment commentURL domain expires http_only implicit max_age path port same_site secure version

as_string

Returns a string representation of the object.

    my $cookie_string = $cookie->as_string;
    # or
    my $cookie_string = "$cookie";
    my-cookie="sid1234567"; Domain=example.com; Path=/; Expires=Mon, 09 Jan 2020 12:17:30 GMT; Secure; HttpOnly

If encryption is enabled with "encrypt", the cookie value will be encrypted using the key provided with "key" and the Initialisation Vector. If the latter was not provided, it will be generated automatically. The resulting encrypted value is then encoded in base64 and escaped. For example:

    my $cookie_value = "toc_ok=1";
    my $key = Bytes::Random::Secure::random_bytes(32);
    # result:
    # session=PyJTlRJniAYVJJF6%2FswuPw%3D%3D; Path=/; SameSite=Lax; Secure; HttpOnly

If cookie signature is enabled for integrity protection with "sign", an sha256 hmac will be generated using the key provided with "key" and the resulting hash appended to the cookie value separated by a dot. For example:

    my $cookie_value = "toc_ok=1";
    my $key = "hard to guess key";
    # I2M4/rh/TiNV5RZDSBJkhLblBvrN5k9448G6w/gp/jg=
    my $signature = Crypt::Mac::HMAC::hmac_b64( $key, $cookie_value );
    # result: toc_ok=1.I2M4/rh/TiNV5RZDSBJkhLblBvrN5k9448G6w/gp/jg=
    # ultimately the cookie value sent will be:
    # toc_ok%3D1.I2M4%2Frh%2FTiNV5RZDSBJkhLblBvrN5k9448G6w%2Fgp%2Fjg%3D

The returned value is cached so the next time, it simply return the cached version and not re-process it. You can reset it by calling "reset".

comment

    $cookie->comment( 'Some comment' );
    my $comment = $cookie->comment;

Sets or gets the optional comment for this cookie. This was used in version 2 of cookies but has since been deprecated.

Returns a Module::Generic::Scalar object.

commentURL

    $cookie->commentURL( 'https://example.com/some/where.html' );
    my $comment = $cookie->commentURL;

Sets or gets the optional comment URL for this cookie. This was used in version 2 of cookies but has since been deprecated.

Returns an URI object.

created_on

Set or get the datetime of the cookie object created. This value is primarily used by Cookie::Jar, as per the rfc6265, when setting the http request header Cookie to differentiate two cookies that share the same domain and path. The cookie that has their creation datetime earlier are set first:

"Among cookies that have equal-length path fields, cookies with earlier creation-times are listed before cookies with later creation-times." (rfc6265, section 5.4.2)

decrypt

This returns the cookie decrypted value. If it used on a non-encrypted cookie, this would return undef and set an error

It takes an optional hash or hash reference of parameters:

algo string

The algorithm to use for encryption. Defaults to the value set with "algo". See this method for more information on acceptable values.

iv string

The Initialisation Vector used for encryption and decryption. Default to the value set with "initialisation_vector"

key string

The encryption key. Defaults to the value set with "key"

discard

Boolean. Set or get this value to true to flag this cookie to be discarded, whatever that means to you the user. This is not a standard protocol property.

This method is used in "save_as_lwp" in Cookie::Jar and "save_as_netscape" in Cookie::Jar with the option skip_discard

It returns the current value as a Module::Generic::Boolean object.

domain

    $cookie->domain( 'example.com' );
    my $dom = $cookie->domain;

Sets or gets the domain for this cookie.

Returns the current value as a Module::Generic::Scalar object.

Note that you can also call it using the alias method host

elapse

Set the expires value for this cookie to 0, which, in turn, will set it to Thu, 01 Jan 1970 09:00:00 GMT

When sent to the http client, this will have the effect of removing the cookie.

See rfc6265 for more information.

encrypt

Set or get the boolean value. If true, the this will tell "as_string" to encrypt the cookie value.

To use this feature, an encryption key must be set and the module Crypt::Cipher must be installed.

You can read more about the differences between sign and encryption at Stackoverflow

expires

Sets or gets the expiration date and time for this cookie.

The value provided can be one of:

A date compliant with rfc7231

For example: 01 Nov 2021 08:42:17 GMT

unix timestamp.

For example: 1631099228

variable time.

For example: 30s (30 seconds), 5m (5 minutes), 12h (12 hours), 30D (30 days), 2M (2 months), 1Y (1 year)

However, this is not sprintf, so you cannot combine them, thus you cannot do this: 5m1D

now

Special keyword

In last resort, the value provided will be parsed using "_parse_timestamp" in Module::Generic. If parsing fails, it will return undef and set an error.

Ultimately, a DateTime will be derived from those values, or undef will be returned and an error will be set.

The DateTime object will be set with a formatter to allow a stringification that is compliant with rfc6265.

And you can use "max_age" alternatively.

See also https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Date

Note that a cookie without an expiration datetime is referred as a session cookie, so setting the cookie expiration change a cookie from being a session cookie to being a more permanent cookie.

As documented, if expiration is "unspecified, the cookie becomes a session cookie. A session finishes when the client shuts down, after which the session cookie is removed."

fields

Returns an array object of cookie fields available. This is essentially used by "apply"

host

Alias for "domain"

host_only

This is an alias for "implicit". It has been added to comply with the language of rfc6265, section 5.3.6

If the domain attribute was not provided by the server for this cookie, then: "set the cookie's host-only-flag to true." and "set the cookie's domain to the canonicalized request-host"

Returns the current value as a Module::Generic::Boolean object (that is stringifyable).

http_only

Sets or gets the boolean for httpOnly

Returns a Module::Generic::Boolean object.

httponly

Alias for "http_only"

implicit

This boolean is set to true if the domain was not initially set and has been derived from the current host.

Returns a Module::Generic::Boolean object.

initialisation_vector

Set or get the Initialisation Vector used for cookie encryption. If you do not provide one, it will be automatically generated. If you want to provide your own, make sure the size meets the encryption algorithm size requirement.

To find the right size for the Initialisation Vector, for example for algorithm AES, you could do:

    perl -MCrypt::Cipher::AES -lE 'say Crypt::Cipher::AES->blocksize'

which would yield 16

is_expired

Returns true if this cookie has an expiration datetime set and it has expired, i.e. the expiration datetime is in the past. Otherwise, it returns false.

Return value is in the form of a Module::Generic::Boolean object that stringifies to 1 or 0;

is_persistent

Boolean. This returns true if the cookie sent from the server is not a session cookie, i.e. it has an "expires" value set.

See rfc62655, section 5.3.3

is_session

Returns true if this is a session cookie, i.e. it has no expiration datetime nor any "max_age" set, otherwise, it returns false.

Return value is in the form of a Module::Generic::Boolean object that stringifies to 1 or 0;

is_tainted

Sets or gets the boolean value. This is a legacy method of old cookie module, but not used anymore.

Returns a Module::Generic::Boolean object.

is_valid

This takes an optional hash or hash reference of parameters.

It returns true if the cookie was signed and the signature is valid, or false otherwise.

If an error occurred, this method returns undef and sets an error instead, so check the return value.

    my $rv = $c->is_valid;
    die( $c->error ) if( !defined( $rv ) );
    print( "Cookie is valid? ", $rv ? 'yes' : 'no', "\n" );

Return value is in the form of a Module::Generic::Boolean object that stringifies to 1 or 0;

Possible parameters are:

key string

The encryption key to use to sign and verify the cookie signature. Defaults to the value set with "key"

iv

This is an alias for "initialisation_vector"

key

Set or get the encryption key used to encrypt the cookie value. This is used when "encrypt" or "sign" are set to true.

When used for cookie encryption, make sure the key size is big enough to satisfy the encryption algorithm requirement, which you can check with, say for AES:

    perl -MCrypt::Cipher::AES -lE 'say Crypt::Cipher::AES->keysize'

In this case, it will yield 32. Replace above AES, byt whatever algorithm you have chosen.

    perl -MCrypt::Cipher::Blowfish -lE 'say Crypt::Cipher::Blowfish->keysize'

would yield 56 for Blowfish

You can use "random_bytes" in Bytes::Random::Secure to generate a random key:

    # will generate a 32 bytes-long key
    my $key = Bytes::Random::Secure::random_bytes(32);

match_host

Provided with an host name and this returns true if this cookie domain either is a perfect match or if the "implicit" flag is on and the cookie domain is a subset of the host provided.

Otherwise this returns false.

max_age

Sets or gets the integer value for Max-Age

This value should be an integer representing the number of seconds until this cookie expires.

As per the rfc6265, Max-Age takes precedence over Expires when set, so if you set this, any value set with "expires" will be discarded.

Returns a Module::Generic::Number object.

maxage

Alias for "max_age"

name

Sets or gets the cookie name.

As per the Mozilla documentation, a cookie name cannot contain any of the following charadcters:

    \(\)\<\>\@\,\;\:\\\"\/\[\]\?\=\{\}

Returns a Module::Generic::Scalar object.

path

Sets or gets the path.

Returns a Module::Generic::Scalar object.

port

Sets or gets the port number.

Returns a Module::Generic::Number object.

reset

Set the reset flag to true, which will force "as_string" to recompute the string value of the cookie.

same_as

Provided with another object and this returns true if it has the same property values, false otherwise.

This is used in overloaded object comparison, such as:

    print( "Same cookie\n" ) if( $c1 eq $c2 );
    # or
    print( "Same cookie\n" ) if( $c1 == $c2 );

same_site

Sets or gets the boolean value for Same-Site.

The proper values should be Relaxed, Strict or None, but this module does not enforce the value you set. Setting a proper value is your responsibility.

See Mozilla documentation for more information.

If set to None, secure should be set to true.

See rfc 6265 for more information.

Returns a Module::Generic::Scalar object.

samesite

Alias for "same_site".

secure

Sets or gets the boolean value for Secure.

Returns a Module::Generic::Boolean object.

sign

Set or get the boolean value. If true, then the cookie value will be signed. The way this works, is that "hmac_b64" in Crypt::Mac::HMAC will create a SHA256 encrypted digest using the encryption key you provided with "key" and attach the signature to the cookie value separated by a dot. For example:

    my $cookie_value = "toc_ok=1";
    my $key = "hard to guess key";
    my $signature = Crypt::Mac::HMAC::hmac_b64( $key, $cookie_value );
    # signature is I2M4/rh/TiNV5RZDSBJkhLblBvrN5k9448G6w/gp/jg=
    # cookie resulting value before uri encoding:
    # toc_ok%3D1.I2M4/rh/TiNV5RZDSBJkhLblBvrN5k9448G6w/gp/jg=

So, you need to have the module Crypt::Mac installed to be able to use this feature.

Signature are used to ensure data integrity protection for content that are not secret.

For more secret content, use "encrypt".

You can read more about the difference between sign and encryption at Stackoverflow

uri

If a value is provided, it will be transformed into a URI object, and its port, path and host components will be used to set the values for "port", "path" and "domain" respectively.

Otherwise, with no value provided, this will form an URI object based on the cookie secure flag, domain, port, and path

    $c->uri( 'https://www.example.com:8080/some/where?q=find+me' );
    # sets host to www.example.com, port to 8080 and path to /some/where
    my $uri = $c->uri;
    # get an uri based on cookie properties value, such as:
    # https://www.example.com:8080/some/where

value

Sets or gets the value for this cookie.

Returns a Module::Generic::Scalar object.

version

Sets or gets the cookie version. This was used in version 2 of the cookie standard, but has since been deprecated by rfc6265.

Returns a Module::Generic::Number object.

_header_datetime

Given a DateTime object, or by default will instantiate a new one, and this will set its formatter to DateTime::Format::Strptime with the appropriate format to ensure the stringification produces a rfc6265 compliant datetime string.

TO_JSON

This method is used so that if the cookie object is part of some data encoded into json, this will convert the cookie data properly to be used by JSON

SIGNED COOKIES

As shown in the "SYNOPSIS" you can sign cookies effortlessly. This package has taken all the hassle of doing it for you.

To use this feature you need to have installed Crypt::Mode::CBC which is part of CryptX

The methods available to use for cookie integrity protection are: "key", "sign" to enable cookie signature, "is_valid" to check if the signature is valid.

Cookie signature is performed by CryptX, which is an XS module, and thus very fast.

ENCRYPTED COOKIES

As shown in the "SYNOPSIS" you can encrypt cookies effortlessly. This package has taken all the hassle of doing it for you.

To use this feature you need to have installed Crypt::Mode::CBC which is part of CryptX

The methods available to use for cookie encryption are: "algo" to set the desired algorithm, "key", "encrypt" to enable encryption, "decrypt" to decrypt the cookie value, and optionally "initialisation_vector".

Cookie encryption is performed by CryptX, which is an XS module, and thus very fast.

INSTALLATION

As usual, to install this module, you can do:

    perl Makefile.PL
    make
    make test
    sudo make install

If you have Apache/modperl2 installed, this will also prepare the Makefile and run test under modperl.

The Makefile.PL tries hard to find your Apache configuration, but you can give it a hand by specifying some command line parameters. See Apache::TestMM for available parameters or you can type on the command line:

    perl -MApache::TestConfig -le 'Apache::TestConfig::usage()'

For example:

    perl Makefile.PL -apxs /usr/bin/apxs -port 1234
    # which will also set the path to httpd_conf, otherwise
    perl Makefile.PL -httpd_conf /etc/apache2/apache2.conf

    # then
    make
    make test
    sudo make install

See also modperl testing documentation

But, if for some reason, you do not want to perform the mod_perl tests, you can use NO_MOD_PERL=1 when calling perl Makefile.PL, such as:

    NO_MOD_PERL=1 perl Makefile.PL
    make
    make test
    sudo make install

AUTHOR

Jacques Deguest <jack@deguest.jp>

SEE ALSO

Cookie::Jar, Apache2::Cookies, APR::Request::Cookie

rfc6265

Latest tentative version of the cookie standard

COPYRIGHT & LICENSE

Copyright (c) 2019-2021 DEGUEST Pte. Ltd.

You can use, copy, modify and redistribute this package and associated files under the same terms as Perl itself.