Codemountain, Paulo Suzart's Blog

Basic Authentication no GAE com Tornado

with 2 comments

Olá! Pra quem já usou o Tornado, vai perceber que o framework permite autenticação via cookie criptografado. O exemplo original da documentação usa uma autenticação por formulário, o que é ótimo para visitantes que desejem utilizar as funcionalidades da nossa aplicação via página web, mas o que fazer para proteger uma API rest construída no GAE com o Tornado?

Uma possibilidade simples é o uso da autenticação no nível do transporte, o HTTP Basic Authentication, que não é lá estas coisas, mas tem seu lugar. Outro ponto importante é que na minha API, eu gostaria de usar um esquema de Decorator em Python (explicar o funcionamento dos decorators está fora do escopo deste post, mas o link anterior oferece bastante info) para facilitar a minha vida. Isto é, eu gostaria de usar algo semelhante a interceptors ou filters em Java. O uso deveria ficar mais ou menos assim:

MyRequestHandler(tornado.web.RequestHandler):
	@authenticated(authenticator)
	def post(self, username):
		#alguma protegida aqui

def authenticator(user,pass):
	#Nossa logica de auth aqui, seja acessar o DataStore
        #ou qualquer coisa para verificar a autenticidade da informação

@authenticated é uma função de decoração que receberá nosso autenticador como parâmetro, que por sua vez será utilizado dentro do código que irá iterceptar a invocação da função post em MyRequestHandler.

Agora a parte mais interessante, o nosso Decorator propriamente dito. Este cara precisa fazer o seguinte: verificar se o usuário tem um cookie criptografado, se não tiver, verificar os cabeçalhos http referentes a Basic Authentication, que por sua vez fornecerão usuário e senha para passarmos para a função authenticator. Naturalmente, se existe um cookie criptografado e válido, pulamos a validação do cabeçalho HTTP e permitimos a invocação do da função post do MyRequestHandler. Abaixo o código inteiro do Decorator e depois os comentários:

def authenticated(auth):
	"""Decorate a function to request user authentication before processing.
		Just uses Basic HTTP Authentication.
	"""
	def decore(f):

		def _request_auth(handler):
			"""Responds 401 to the client, that may prompts the user for auth data."""
			handler.set_header('WWW-Authenticate', 'Basic realm=tmr')
			handler.set_status(401)
			handler.finish()
			return False

		@functools.wraps(f)
		def new_f(*args):
			"""The actual wrapper of the function. Check if auth data is present, if so, invokes
				the auth (authenticated argument) for credential check, if ok, the wrapper function is invoked
				with an aditional argument, the username."""
			handler = args[0]

			auth_header = handler.request.headers.get('Authorization')

			if (not handler.get_secure_cookie("user") and (auth_header is None or not auth_header.startswith('Basic '))):
				return _request_auth(handler)
			elif handler.get_secure_cookie("user"):
				logging.info("####Login by cookie")
				user = handler.get_secure_cookie("user")
			else:
				logging.info("###First login by http")
				auth_decoded = base64.decodestring(auth_header[6:])
				user, password = auth_decoded.split(':', 2)
				if not auth(user, password):
					_request_auth(handler)
				handler.set_secure_cookie("user", user)
			f(username=user, *args)
			return

		return new_f
	return decore

O código poderia ter sido escrito de alguma outra fora, sei lá, organizar e encadear os ifs de outra maneira, enfim. Acho que aqui fica suficiente pra representar o que falei logo acima, que é a lógica de execução da coisa.

Veja que authenticated na verdade é uma função definida como outra qualquer. Ela recebe uma função que recebe dois argumentos (usuário e senha), que por sua vez retornará True ou False, indicando sucesso na autenticação. authenticated de fato retorna outra função, a função decore. decore é uma função qure retorna uma função decorada (a new_f) com @functools.wraps, que nada mais é permitir que new_f fique com a mesma “cara” que a nossa função post do MyRequestHandler. Estranho? Veja detalhes aqui.

Pois bem, nossa decore recebe f como parâmetro, que na verdade é a função post do Request Handler e retorna new_f, que por sua vez recebe os mesmos parâmetros que post na forma de *args. *args é o jeito python de uma função receber um número de argumentos variável, dado que quando decoramos uma função, não sabemos exatamente quantos parâmetros aquela função receberá. Mais detalhes sobre *args e **kwargs aqui.

Quem mantém a lógica de verificação da presença das credenciais (seja por cookie, seja por http) é new_f, que delega para _request_auth os casos onde a credencial não está presente e para auth os casos onde existem as credenciais http. _request_auth só envia um status code 401 para o client, para que este envie as informações do usuário e senha.

A execução de post fica no final de new_f onde ela invoca f (username=user, *args). Aqui podemos ver uma pseudo injeção de dependência, dado que username poderia ser um objeto do tipo user retornado da base ou qualquer outra coisa. Por que injeção? Por que o Tornado não conhece este parâmetro, quer ficaria nulo caso a função post não tivesse sido anotada com @authenticated.

O código aqui está em funcionamento em uma aplicação que estou fazendo. Mas não postei aqui a aplicação completa. Ao usar este decorator, meu log mostra (considerando usuário jordan e uma senha qualquer):

INFO     2010-11-06 13:30:27,448 utils.py:37] ###First login by http
INFO     2010-11-06 13:30:27,448 api.py:34] ########jordan######
INFO     2010-11-06 13:30:27,461 dev_appserver.py:3283] "GET /api/track/twitter/mentions HTTP/1.1" 200 -
INFO     2010-11-06 13:30:31,903 utils.py:34] ####Login by cookie
INFO     2010-11-06 13:30:31,904 api.py:34] ########jordan######

Meu autenticador passado para @authenticated, por enquanto é um lambda que retorna True para qualquer usuário e senha.

@authenticated(lambda x,y: True)
def post(self, username): #...

Acho que é só. A intenção aqui é de orientação e mostrar alguns dos desafios que podemos enfrentar usando o Google App Engine (GAE) e o Tornado.
Não mostro aqui os detalhes gerais de se usar o Tornado no GAE, mas é possível encontrar muito material bom na internet. Espero que tenham gostado e não esqueçam, follow me on twitter: @paulosuzart

Post UPDATE: por algum motivo sobrenatural eu escrevi *Basich* e não Basic. Por isto a URL contem um erro que ficará para a posteridade. Mereço um #Fail!

Written by paulosuzart

novembro 6, 2010 às 2:12 pm

Publicado em python

Tagged with ,

2 Respostas

Subscribe to comments with RSS.

  1. Outro dia estava procurando algum plugin do django para fazer isso no gae mas é tão simples que dá pra fazer na mão.

    Bom post. Bem didático.

    Raphael Miranda

    novembro 6, 2010 at 2:31 pm

    • Obrigado! É até simples sim. Possível que dê pra ajustar a mesma solução pro django. Obrigado pela visita. 😉

      paulosuzart

      novembro 6, 2010 at 2:44 pm


Deixe um comentário