Source code for clortho.auth

#!/usr/bin/env python 
# -*- coding: utf-8 -*- 
#
#
# Copyright 2012 ShopWiki
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#


import bcrypt
import random
import string
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import Column
from sqlalchemy.types import Integer, Unicode, String, Boolean


[docs]class ActivationError(Exception): pass
Base = declarative_base()
[docs]class UserBase(Base): ''' The :class:`UserBase` class subclasses sqlalchemy's default declarative base to provide a schema for basic user authentication. :class:`UserBase` provides fields and methods for setting and checking a password, generating and checking an activation code, and disabling the user entirely. The :attr:`email` field is unique. This class **cannot be used on its own**. In order to use this class in your application, subclass it and define the :attr:`__tablename__` attribute. ''' __abstract__ = True id = Column(Integer, primary_key=True) ''' The primary key and id which will always be used to reference a user. ''' email = Column(Unicode(255), unique=True) ''' The user's email. The uniqueness is enforced at the database level. ''' password_hash = Column(String(80)) ''' The hash of the user's password, with salt, as generated by bcrypt. ''' password_is_set = Column(Boolean()) ''' :attr:`password_hash` might be empty, so we explicitly record whether or not the user has a password set. ''' disabled = Column(Boolean()) ''' Prevent check_password from returning ``True``, regardless of the password supplied. ''' activated = Column(Boolean()) ''' :attr:`activated` is ``True`` if the user has completed the e-mail verification process using :attr:`activation_code_hash`. ''' activation_code_hash = Column(String(80)) ''' The :attr:`activation_code_hash` is set to a hash of the activation code that is generated by :meth:`generate_activation_code` and subsequently e-mailed to the user. This code is immediately forgotten by the application; only the bcrypt hashed version of it is stored in :attr:`activation_code_hash`. The activation code can be checked using :meth:`.activate`. '''
[docs] def set_password(self, password_plaintext): ''' :param password_plaintext: The plaintext of the password to set. Given a plaintext password, generate a salted hash using bcrypt. Set :attr:`password_is_set` to ``True`` and :attr:`password_hash` to the generated hash. The user can now be authenticated with the plain text password and :meth:`check_password`. ''' hashed = bcrypt.hashpw(password_plaintext, bcrypt.gensalt()) self.password_is_set = True self.password_hash = hashed
[docs] def check_password(self, password_plaintext): ''' :param password_plaintext: The plaintext of the password to check. If :attr:`password_is_set` is ``False`` or :attr:`disabled` is ``True``, return ``False``. Otherwise, take a plaintext password string and hash it using bcrypt along with the salt for this user. If the resulting hash matches the hash in :attr:`password_hash`, return ``True``. The session can now be considered authenticated for this user. ''' return (self.password_is_set and not self.disabled and bcrypt.hashpw(password_plaintext, self.password_hash) == self.password_hash)
[docs] def activate(self, activation_code_plaintext): ''' :param activation_code_plaintext: The activation code to check. If its hash matches :attr:`activation_code_hash`, the user will be activated. Take the plaintext activation code that was sent to the user, generally via e-mail. Hash the plaintext and compare it against :attr:`activation_code_hash`. If they are the same, set :attr:`activated` to ``True``. Finally, call :meth:`generate_activation_code` in order to prevent reactivation of the account using the same code. Since we don't save the new reactivation code, it is useless. If the account becomes inactive in the future for any reason, :meth:`generate_activation_code` must be called again in order to reactivate. If activation fails, an exception is raised and no change is made to the model. ''' hashed = bcrypt.hashpw(activation_code_plaintext, self.activation_code_hash) if hashed == self.activation_code_hash: self.activated = True self.generate_activation_code() else: raise ActivationError('Activation code failed')
[docs] def generate_activation_code(self): ''' Generate a 20 character random code from letters and digits. This is the activation code that will be sent to the user (presumably via e-mail) and allow them to activate their account. The actual code is not stored locally; like a password, it is hashed and stored in :attr:`activation_code_hash`. The plaintext activation code is returned to the caller. In order to activate the user with the plaintext activation code, call :meth:`activate`. ''' alphabet = string.ascii_letters + string.digits sr = random.SystemRandom() activation_code = ''.join(sr.choice(alphabet) for x in range(20)) self.activation_code_hash = bcrypt.hashpw(activation_code, bcrypt.gensalt()) return activation_code
[docs] def __init__(self, email): ''' :param email: **Required**. The user's email address. Validity as an email address is not enforced; any string will be allowed. Create a new :class:`UserBase` object. By default, :attr:`password_is_set`, :attr:`disabled`, and :attr:`activated` are all initialized to ``False``. ''' self.email = email self.password_is_set = False self.disabled = False self.activated = False
def __repr__(self): return "<User('{email}')>".format(**dict(email=self.email))