[Controle de sessão em Golang como Gorilla sessions]

Controle de sessão é um conceito bastante antigo, basicamente se usa cookies para guardar alguns dados da sessão do usuário, por segurança geralmente só preservamos no cookie um UUID o mais aleatório possível, que o servidor usa para consultar outros dados, como por exemplo saber qual usuário esta logado.

O UUID precisa ser o mais aleatório possível porque se alguém conseguir prever qual será o próximo UUID é possível roubar a sessão de um usuário legitimo.

Supondo que o UUID seja seguro o suficiente é sempre mais seguro manter os dados da sessão do lado do servidor, com isso ataques como session poisoning ficam consideravelmente mais difíceis.

Entretanto, manter os dados da sessão do lado do servidor cria um problema de escalabilidade, é necessário no mínimo consultar um micro-serviço responsável por manter as sessões que por sua vez provavelmente vai consultar um banco de dados e no fim vamos ter mais um gargalo e muito mais complexidade ainda mais se quisermos usar algum sistema de balanceamento de carga.

AES-256

Uma alternativa é manter os dados da sessão no próprio cookie e criptografa-los usando um bom algoritmo, no nosso caso vamos usar AES-256.

Existem dois pontos importantes para essa estratégia ter sucesso, o primeiro é usar uma boa chave, precisa ser uma chave de 32 caracteres o mais aleatória possível, no nosso caso vamos usar o pacote gorilla/securecookie para gerar a chave que por sua vez usa crypto/rand. O segundo ponto é espirar a chave periodicamente para evitar ataques de força bruta.

Nem preciso dizer que gerar a chave manualmente esta fora de cogitação.

Quais dados armazenar

Como vamos guardar os dados da sessão no próprio cookie temos que ficar atentos ao tamanho que podemos usar. Precisamos ficar abaixo do limite de 4096 bytes. Se formos por exemplo guardar o Access token do OAuth2, já vamos gastar 2048 bytes, e se armazenarmos também o Refresh token são mais 512 bytes. Alem disso haverá espaço ocupado pela criptografia e também por converter os dados binários em base64.

Então seja minimalista, guarde apenas o essencial.

Se precisar de mais espaço é uma boa ideia dar uma olhada em outras formas de storage para a sessão, como por exemplo, salvar em arquivos em disco usando NewFilesystemStore no lugar de NewCookieStore.

Limpando sessões

Existem várias situações que podemos querer limpar o cookie com os dados da sessão, por exemplo, no caso do usuário querer desligar da página. Outra situação é quando acontece um erro de algum tipo com a criptografia eu prefiro apagar o cookie e começar do inicio.

Esta é uma pequena função utilitária para apagar cookies:

func clearSession(w http.ResponseWriter, session string) {
	cookie := &http.Cookie{
		Name:   session,
		Value:  "",
		Path:   "/",
		MaxAge: -1,
	}
	http.SetCookie(w, cookie)
}

Preparando o storage

A primeira coisa a fazer é instanciar uma forma de guardar a sessão, no nosso exemplo criamos uma variável store que vai criar um cookie e criptografado.

var store = sessions.NewCookieStore(securecookie.GenerateRandomKey(32))

Da forma como esta no exemplo sempre que o serviço for carregado ele vai criar uma nova chave, isso pode ser uma estratégia válida ainda mais se você não quer preservar essa chave em lugar nenhum e quer que ela mude sempre que você lançar uma nova versão, claro, com a pequena inconveniência dos seus usuários perderem a sessão sempre que você atualizar o sistema.

Uma outra alternativa seria carregar essa chave de algum sistema seguro ou colocando em uma variável de ambiente.

Controlando a sessão

Para criar ou carregar uma sessão usamos o código abaixo em um handler html.

session, err := store.Get(r, sessionName)
if err != nil {
	clearSession(w, sessionName)
	http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
	//http.Error(w, err.Error(), http.StatusInternalServerError)
	return
}

Na primeira linha do código acima instanciamos uma variável session que vai conter uma nova sessão ou pegar a sessão existente.

Em seguida temos o tradicional tratamento de erro, caso algo de errado, como, por exemplo, se não conseguirmos descriptografar o cookie da sessão existente, primeiro limpamos o cookie atual, afinal ele não serve para nada, então podemos redirecionar o usuário para a página inicial ou para a página de login e dai ganhar um novo cookie de sessão. Ou então podemos simplesmente retornar um erro.

Salvando dados

A sessão fica armazenada em um mapa interface de interfaces, essa sem duvida não é minha solução favorita mas sem duvida é a mais flexível. Só precisamos tomar cuidado na hora de recuperar os dados.

Para salvar um valor qualquer na sessão fazemos o seguinte:

session.Values["foo"] = "bar"
session.Values[42] = "The answer to life, the universe and everything"

err = session.Save(r, w)
if err != nil {
	http.Error(w, err.Error(), http.StatusInternalServerError)
	return
}

Como podemos ver no exemplo acima, tanto a chave como o valor podem ser qualquer coisa, letras números, etc. mas é uma boa ideia tomar cuidado com isso e sempre é importante validar se estamos lendo o tipo correto para evitar que o sistema caia por causa de um panic.

Depois basta usar a função Save para persistir os dados.

Carregando a sessão

Para ler os dados de uma sessão, fazemos como no primeiro exemplo e usamos a função Get para instanciar a variável da sessão e então podemos ler os dados diretamente do mapa Values.

a, ok := session.Values["key_name"]
if !ok {
	http.Error(w, "value not set", http.StatusInternalServerError)
	return
}

Aqui é como ler qualquer mapa em Go, basta passar a chave para o mapa e pegar os dois retornos possíveis, o primeiro é o valor armazenado no mapa se houver algum, e o segundo é um booleano, se esse segundo retorno for true então o valor foi encontrado, caso contrario podemos retornar um erro indicando que o valor não existe.

Tomando cuidado com interfaces em Go

Os valores desse mapa sempre são uma interface, então precisamos fazer cast para o tipo correto, o problema é que se você tentar fazer cast para o tipo errado, seu sistema vai fechar com um panic.

Este é um pequeno exemplo de como validar o tipo do dado antes de fazer cast.

switch a.(type) {
case string:
	w.Write([]byte(a.(string)))
default:
	http.Error(w, "value is not type string", http.StatusInternalServerError)
}

Código fonte

O código fonte completo esta no repositório do Grupo de Estudos de Go no GitHub junto com muitos outros exemplos.

Também vale a pena dar uma passada no repositório do package sessions do Gorilla que tem uma lista muito útil de opções de storage compatíveis com a interface do package.

Cesar Gimenes