Source code for mod_auth.mod_auth

"""
This module implements the session cookie format from mod_auth_tkt_ and mod_auth_pubtkt_.
In this documentation show you how to use and integrate mod_auth library into your project.

Contributors:

Before start I want say a BIG TANKS to plone.session team for tkauth.py module. It help us to start with this library:

 plone-session: https://github.com/plone/plone.session/blob/master/plone/session/tktauth.py

And to Andrey Plotnikov for a easy implementation fo mod_auth_pubtkt

 auth_pubtkt: https://github.com/AndreyPlotnikov/auth_pubtkt

mod_auth_tkt style cookie authentication
========================================

Mod_auth library implements the session cookie format from mod_auth_tkt_, the class used is Ticket.
Now ``createTicket`` and ``validateTicket`` functions use the MD5_ based
double hashing scheme in the original mod_auth_tkt.

.. _mod_auth_tkt: http://www.openfusion.com.au/labs/mod_auth_tkt/
.. _mod_auth_pubtkt: https://neon1.net/mod_auth_pubtkt/index.html
.. _MD5: http://en.wikipedia.org/wiki/MD5
.. _HMAC: http://en.wikipedia.org/wiki/HMAC
.. _SHA-256: http://en.wikipedia.org/wiki/SHA-256


Configuration
-------------
In mod_auth_tkt the protocol depends on a secret string shared between servers.
From time to time this string should be changed, so store it in a configuration file.

  >>> SECRET = 'b8fb7b6df0d64dd98b8ccd00577434d7'

The tickets are only valid for a limited time. Here we will use 24 hours

  >>> DEFAULT_TIMEOUT = 24*60*60


Cookie creation
---------------
The minimal set of attributes to create a ticket are composed only from a userid:

    >>> userid = 'testUser'

First stemp is to init Ticket object:

    >>> from mod_auth import Ticket
    >>> mod_auth_Ticket = Ticket(SECRET)

So, set the validuntil that the user will log out.

    >>> validuntil = int(time.time())+ (24*60*60)


We will create a mod_auth_tkt compatible ticket. In the simplest case no extra
data is supplied.

    >>> ticket = mod_auth_Ticket.createTkt(userid,validuntil=validuntil)
    >>>'b054eeab313d4b75e10f4fd4ddb36ecf50115dcctestUser!'

The cookie itself should be base64 encoded. We will use the built-in Cookie
module here, your web framework may supply it's own mechanism.

  >>> import Cookie, binascii
  >>> cookie = Cookie.SimpleCookie()
  >>> cookie['auth_tkt'] = binascii.b2a_base64(ticket).strip()
  >>> print cookie
  Set-Cookie: auth_tkt=YjA1NGVlYWIzMTNkNGI3NWUxMGY0ZmQ0ZGRiMzZlY2Y1MDExNWRjY3Rlc3RVc2VyIQ==


Cookie validation
-----------------

First the ticket has to be read from the cookie and unencoded:

  >>> ticket = binascii.a2b_base64(cookie['auth_tkt'].value)
  >>> ticket
  'b054eeab313d4b75e10f4fd4ddb36ecf50115dcctestUser!'

The server that invoke validateTkt and open a session cookie
need of the SECRET to validate the digest into ticket.

Init the Ticket object:

    >>> from mod_auth import Ticket
    >>> mod_auth_Ticket = Ticket(SECRET)

next step is to validate:

    >>> mod_auth_Ticket.validateTkt(ticket)
    >>> (u'testUser', (), u'', 1343315404)

If the ticket is valid and not expired , validateTkt return all information
about logged user else raise an Exception (see function documentation for detail)


Tokens and user data
--------------------

The format allows for optional user data and tokens. For detail you can see
the test.py into mod_auh module, where there are some use test of this class.
Here an example:

    >>> Secret = str(uuid.uuid4().hex)
    >>> # Init SignedTicket object
    >>> simpleTicket = Ticket(Secret)

    >>> #USER DATA

    >>> userid = 'TestUser'
    >>> tokens = ('role1', 'role2')
    >>> userdata = ('testuser@mail.com','Italy','Bologna')
    >>> cip = '127.0.0.1'
    >>> # ticket is valdi until 24 from now
    >>> validuntil = int(time.time())+ (24*60*60)

    >>> #END USERDATA

    >>> ticket = simpleTicket.createTkt(userid,tokens,userdata,cip,validuntil)


Mod_auth_pubtkt style cookie authentication
===========================================

mod_auth_pubtkt_ is a module that authenticates a user based on a cookie
with a ticket that has been issued by a central login server and digitally signed
using either RSA or DSA. This means that only the trusted login server has the private key
required to generate tickets, while web servers only need the corresponding public key to verify them.

In mod_auth module is implemented  by SignedTicket class.

Configuration
-------------
BE CAREFUL!For your safety, please, if you use this module in your project,
generate new keys (DSA or RSA) , to do that see the section below:

From your unix shell.
DSA:

     openssl dsaparam -out dsaparam.pem 2048

     openssl gendsa -out privDSAkey.pem dsaparam.pem

     openssl dsa -in privDSAkey.pem -out pubDSAkey.pem -pubout

     The dsaparam.pem file is not needed anymore after key generation and can safely be deleted.

RSA:

     openssl genDSArsa -out privkey.pem 2048

     openssl rsa -in privDSAkey.pem -out pubkey.pem -pubout


Cookie creation
---------------
Like into Ticket class , the minimal set of attributes
to create a ticket are composed only by a userid:

    >>> userid = 'testUser'

First stemp is to init SignedTicket object with your keys:

    >>> from mod_auth import SignedTicket
    >>> mod_auth_pubTicket = Ticket(path_pub_key,path_priv_key)

you can use RSA or DSA keys in pem or der format.

So, set the validuntil that the user will log out.

    >>> validuntil = int(time.time())+ (24*60*60)

We will create a mod_auth_pubtkt compatible ticket. In the simplest case no extra
data is supplied.

    >>> ticket = mod_auth_pubTicket.createTkt(userid,validuntil=validuntil)
    >>>'uid=testUser;validuntil=1343379094;cip=0.0.0.0;sig=MC0CFQCJexq0701MPIcUYHoacJCKCbor1gIUI+oPZElmsNY8/rmk069+ef/u47o='

The cookie itself should be base64 encoded. We will use the built-in Cookie
module here, your web framework may supply it's own mechanism.

  >>> import Cookie, binascii
  >>> cookie = Cookie.SimpleCookie()
  >>> cookie['auth_tkt'] = binascii.b2a_base64(ticket).strip()
  >>> print cookie
  Set-Cookie: auth_tkt=dWlkPXRlc3RVc2VyO3ZhbGlkdW50aWw9MTM0MzM3OTA5NDtjaXA9MC4wLjAuMDtzaWc9TUMwQ0ZEK1RibmpjMi91OEdjZVBGMm1MK24xTXk5bjRBaFVBalBFYTRDZ1RORHhMV2dlWjZTVjhjSGN3S3pRPQ==

Cookie validation
-----------------

First the ticket has to be read from the cookie and unencoded:

  >>> ticket = binascii.a2b_base64(cookie['auth_tkt'].value)
  >>> ticket
  'uid=testUser;validuntil=1343379094;cip=0.0.0.0;sig=MC0CFQCJexq0701MPIcUYHoacJCKCbor1gIUI+oPZElmsNY8/rmk069+ef/u47o='

The server that invoke validateTkt and open a session cookie
need at least public Key.
Init the Ticket object:

    >>> from mod_auth import SignedTicket
    >>> mod_auth_pubTicket = SignedTicket(path_pub_key)
    ## if you init with public key , SignedTicket can only validate and not create

next step is to validate:

    >>> mod_auth_pubTicket.validateTkt(ticket)
    >>> (u'testUser', [], [], 1343380332)

If the ticket is valid with valid sign and not expired , validateTkt return all information
about logged user else raise an Exception (see function documentation for detail)

Tokens and user data
--------------------

The format allows for optional user data and tokens. For detail you can see
the test.py into mod_auh module, where there are some use test of this class.
Here an example:

    >>> # Init SignedTicket object
    >>> signTicket = SignedTicket('./DSApubkey.pem','./DSAprivkey.pem')

    >>> #USER DATA
    >>> userid = 'TestUser'
    >>> tokens = ('role1', 'role2')
    >>> userdata = ('testuser@mail.com','Italy','Bologna')
    >>> cip = '127.0.0.1'
    >>> # ticket is valdi until 24h from now
    >>> validuntil = int(time.time())+ (24*60*60)
    >>> #END USERDATA

    >>> ticket = signTicket.createTkt(userid,tokens,userdata,cip,validuntil)

"""

__author__ = 'Alfredo Saglimbeni'
__mail__ = 'repirro(at)gmail.com, as.aglimbeni(at)scsitaly.com'

from socket import inet_aton
from struct import pack
import hashlib
import time
import base64
from exception import *

###IMPORT FOR MOD_AUTHPUBTKT###
from M2Crypto import RSA, DSA

## DEFAULT CONFIGURATION
DEFAULT_TIMEOUT= 12*60*60
########################

###########################
#### MOD_AUTH_PUBTKT ######
###########################

[docs]class SignedTicket(object): """ TEST """ def __init__(self,pub_key_Path, priv_key_Path=None ): ##LOAD priv_key try: try: priv_key = RSA.load_key(priv_key_Path) except Exception, e: priv_key = DSA.load_key(priv_key_Path) if priv_key_Path is not None and isinstance(priv_key, RSA.RSA): pub_key = RSA.load_pub_key(pub_key_Path) else: pub_key = DSA.load_pub_key(pub_key_Path) except Exception, e: raise ValueError('Unknown key type: %s' % self.pub_key) self.priv_key = priv_key self.pub_key = pub_key def __verify_sig(self, data, sig): """Verify ticket signature. Returns False if ticket is tampered with and True if ticket is good. Arguments: ``pubkey``: Public key object. It must be M2Crypto.RSA.RSA_pub or M2Crypto.DSA.DSA_pub instance ``data``: Ticket string without signature part. ``sig``: Ticket's sig field value. """ sig = base64.b64decode(sig) dgst = hashlib.sha1(data).digest() if isinstance(self.pub_key, RSA.RSA_pub): try: self.pub_key.verify(dgst, sig, 'sha1') except RSA.RSAError: return False return True elif isinstance(self.pub_key, DSA.DSA_pub): return not not self.pub_key.verify_asn1(dgst, sig) else: raise ValueError('Unknown key type: %s' % self.pub_key) def __calculate_sig(self,data): """Calculates and returns ticket's signature. Arguments: ``privkey``: Private key object. It must be M2Crypto.RSA.RSA or M2Crypto.DSA.DSA instance. ``data``: Ticket string without signature part. """ dgst = hashlib.sha1(data).digest() if isinstance(self.priv_key, RSA.RSA): sig = self.priv_key.sign(dgst, 'sha1') sig = base64.b64encode(sig) elif isinstance(self.priv_key, DSA.DSA): sig = self.priv_key.sign_asn1(dgst) sig = base64.b64encode(sig) else: raise ValueError('Unknonw key type: %s' % self.priv_key) return sig def __create_ticket(self, uid, validuntil, ip=None, tokens=(),udata=(), graceperiod=None, extra_fields = () , encoding = "utf8"): """Returns signed mod_auth_pubtkt ticket. Mandatory arguments: ``privkey``: Private key object. It must be M2Crypto.RSA.RSA or M2Crypto.DSA.DSA instance. ``uid``: The user ID. String value 32 chars max. ``validuntil``: A unix timestamp that describe when this ticket will expire. Integer value. Optional arguments: ``ip``: The IP address of the client that the ticket has been issued for. ``tokens``: List of authorization tokens. ``udata``: Misc user data. ``graceperiod``: A unix timestamp after which GET requests will be redirected to refresh URL. ``extra_fields``: List of (field_name, field_value) pairs which contains addtional, non-standard fields. """ uid = uid.encode(encoding) v = 'uid=%s;validuntil=%d' % (uid, validuntil) if ip: v += ';cip=%s' % ip if tokens: v += ';tokens=%s' % ','.join(tokens).encode(encoding) if graceperiod: ##TODO not used in 1.0 version v += ';graceperiod=%d' % graceperiod if udata: v += ';udata=%s' % ','.join(udata).encode(encoding) for k,fv in extra_fields: ##TODO not userd in 1.0 version v += ';%s=%s' % (k,fv) v += ';sig=%s' % self.__calculate_sig(v) return v def __parse_ticket(self, ticket, encoding = 'utf8'): """Parse and verify auth_pubtkt ticket. Returns dict with ticket's fields. ``BadTicket`` and ``BadSignature`` exceptions can be raised in case of invalid ticket format or signature verification failure. Arguments: ``ticket``: Ticket string value. ``pubkey``: Public key object. It must be M2Crypto.RSA.RSA_pub or M2Crypto.DSA.DSA_pub instance ``verify_sig``: Function which perform signature verification. By default verify_sig function from this module is used. This argument is needed for testing purposes only. """ i = ticket.rfind(';') sig = ticket[i+1:] if sig[:4] != 'sig=': raise BadTicket(ticket) sig = sig[4:] data = ticket[:i] if not self.__verify_sig( data, sig): raise BadSignature(ticket) data = data.decode(encoding) try: fields = dict(f.split('=', 1) for f in data.split(';')) except ValueError: raise BadTicket(ticket) if 'uid' not in fields: raise BadTicket(ticket, 'uid field required') if 'validuntil' not in fields: raise BadTicket(ticket, 'validuntil field required') try: fields['validuntil'] = int(fields['validuntil']) except ValueError: raise BadTicket(ticket, 'Bad value for validuntil field') if 'tokens' in fields: tokens = fields['tokens'].split(',') if tokens == ['']: tokens = [] fields['tokens'] = tokens else: fields['tokens'] = () if 'udata' in fields: udata = fields['udata'].split(',') if udata == ['']: udata = [] fields['udata'] = udata else: fields['udata'] = () if 'graceperiod' in fields: try: fields['graceperiod'] = int(fields['graceperiod']) except ValueError: raise BadTicket(ticket, 'Bad value for graceperiod field') return fields
[docs] def validateTkt(self,ticket, now=None, encoding='utf8'): try: parsed_ticket = self.__parse_ticket(ticket,encoding) ( validuntil , userid, cip, token_list, user_data) = parsed_ticket['validuntil'], parsed_ticket['uid'], parsed_ticket['cip'] ,parsed_ticket['tokens'] ,parsed_ticket['udata'] if now is None: now = time.time() if int(validuntil) > now: return userid,token_list,user_data,validuntil else: raise TicketExpired(ticket) except Exception, e: raise TicketParseError(ticket,'Validate error')
[docs] def createTkt(self,userid, tokens=(), user_data=(), cip='0.0.0.0', validuntil=None, encoding='utf8' ): if self.priv_key is None: raise Exception('Private key is not Loaded: you can only validate') if validuntil is None: validuntil = int(time.time()) + DEFAULT_TIMEOUT userid = userid.encode(encoding) #TODO graceperiod and extra_field is not used in 1.0 version ticket=self.__create_ticket(userid,validuntil,cip,tokens,user_data,encoding=encoding) return ticket ########################### ###//END:MOD_AUTH_PUBTKT### ########################### ####################### #### MOD_AUT_TKT ###### #######################
[docs]class Ticket(object): def __init__(self, secret): self.secret=secret def __mod_auth_tkt_digest(self, data1, data2): digest0 = hashlib.md5(data1 + self.secret + data2).hexdigest() digest = hashlib.md5(digest0 + self.secret).hexdigest() return digest def __splitTicket(self,ticket, encoding='utf8'): digest = ticket[:32] val = ticket[32:40] if not val: raise ValueError timestamp = int(val, 16) # convert from hexadecimal+ parts = ticket[40:].decode(encoding).split("!") if len(parts) == 2: userid, user_data = parts tokens = () if len(user_data)>0: user_data = tuple(user_data.split(',')) elif len(parts) == 3: userid, token_list, user_data = parts tokens = tuple(token_list.split(',')) user_data = tuple(user_data.split(',')) else: raise ValueError return digest, userid, tokens, user_data, timestamp
[docs] def createTkt( self, userid, tokens=(), user_data=(), cip='0.0.0.0', validuntil=None, encoding='utf8'): """ Create Ticket from given data. Arguments: userid (string) : The user unique identifier.\n tokens (tupla) : Permission for Given user.\n user_data (tupla) : User's infromations.\n cip : sender ip.\n validuntil : ticket validate until (unix timestamp) .\n encoding : encoding\n Return: Ticket (string) : Resulting ticket.\n """ if validuntil is None: validuntil = int(time.time()) + DEFAULT_TIMEOUT userid = userid.encode(encoding) ##OLD VERSION WITHOUT DSA SIGN token_list = ','.join(tokens).encode(encoding) user_list = ','.join(user_data).encode(encoding) # ip address is part of the format, set it to 0.0.0.0 to be ignored. # inet_aton packs the ip address into a 4 bytes in network byte order. # pack is used to convert timestamp from an unsigned integer to 4 bytes # in network byte order. data1 = inet_aton(cip) + pack("!I", validuntil) data2 = '\0'.join((userid, token_list, user_list)) digest = self.__mod_auth_tkt_digest(data1, data2) # digest + timestamp as an eight character hexadecimal + userid + ! ticket = "%s%08x%s!" % (digest, validuntil, userid) if tokens: ticket += token_list + '!' if user_data: ticket += user_list return ticket
[docs] def validateTkt(self, ticket, cip='0.0.0.0', now=None, encoding='utf8'): """ To validate, a new ticket is created from the data extracted from cookie and the shared secret. The two digests are compared and timestamp checked. Successful validation returns (digest, userid, tokens, user_data, timestamp). On failure, return None. Arguments: secret (string) : secret.\n ticket: given ticket.\n ip : sender ip.\n now: now timestamp.\n encoding : encoding.\n Return: Ticket (string) : Resulting ticket. """ try: (digest, userid, tokens, user_data, validuntil) = data = self.__splitTicket(ticket) new_ticket = self.createTkt(userid, tokens, user_data, cip, validuntil, encoding) if new_ticket[:32] == digest: if now is None: now = time.time() if validuntil > now: return data[1:] except Exception, e: raise BadTicket(ticket,'ticket is not valid.') raise BadTicket(ticket,'ticket is not valid.') ####################### ## //END:MOD_AUT_TKT ## ####################### ####################### ##### EASY USE ######## #######################
def createDefaultTicket(secret, userid, tokens=(), user_data=()): ##import uuid ##uuid.uuid4().hex # init Ticket with secret key ticket = Ticket(secret) #generate ticket with user information return ticket.createTkt(userid,tokens,user_data) def validateDefaultTicket(secret, ticket): # init Ticket with secret key ticket = Ticket(secret) return ticket.validateTkt(ticket)