A autenticação do usuário é um assunto complicado e podemos tornar as coisas ainda mais difíceis se não quisermos usar cookies ou JavaScript. A autenticação é uma necessidade para quase todos os serviços da web que vemos, e existem muitas ferramentas e protocolos para ajudar os desenvolvedores no assunto. Usando o cabeçalho www-authenticate, consegui lidar com sessões de usuário sem nenhum código no lado do cliente, apenas algumas alterações no meu backend. A documentação para o padrão está disponível em [1].

O cabeçalho http www-authenticate é uma ferramenta poderosa e faz parte do padrão básico de autenticação http, mas só fiquei ciente disso recentemente. Neste post, vou contar como usei para lidar com sessões de usuários em meu projeto tasker.

Antes de começarmos, deixe-me dar uma breve visão geral da solução.

Prós

Contras

Implementação

Basicamente, tudo o que temos que fazer é adicionar o cabeçalho http: WWW-Authenticate em uma resposta com status 401. isso fará com que os navegadores mostrem um diálogo para o usuário solicitar as credenciais. Fazendo isso, o navegador irá automaticamente enviar o cabeçalho Authorization: ... em solicitações.

Isso pode ser feito quando o usuário visita uma página de login ou tenta acessar conteúdo privado. No meu caso, adicionei isso a uma página de login. O código é assim:

func login(w http.ResponseWriter, r *http.Request) {
	user, pass, ok := r.BasicAuth()
	if !ok {
		// o usuário não está logado (não enviou credenciais)
		w.Header().Set("WWW-Authenticate", "Basic")
		w.WriteHeader(http.StatusUnauthorized)
		return
	}
	// verificar credenciais
	...

	// se estiver ok, redireciona o usuário para o conteúdo dele
	http.Redirect(w, r, "/user.html", http.StatusFound)
}

Estou usando o esquema de autorização básica, mas existem muitos outros disponíveis, como digest usando MD5, SHA256, e assim por diante. O RFC 7616 [2] tem todas as informações necessárias.

Entrando

O fluxo de login que eu projetei é o seguinte:

  1. o usuário solicita a página /login sem o cabeçalho de autorização
  2. o servidor responde com 401 e inclui o cabeçalho WWW-Authenticate
  3. o usuário preenche o nome de usuário e a senha e solicita o mesmo caminho, mas agora o navegador inclui o cabeçalho de autorização
  4. o servidor verifica as credenciais e, se OK, envia um status de redirecionamento, por exemplo, 301, com a localização da nova página. Se não estiver OK, o servidor envia uma página de erro, para que o usuário possa tentar novamente após uma atualização

Quando os navegadores recebem esse cabeçalho na resposta, eles abrem um diálogo para o usuário, alguns aspectos podem ser definidos, por exemplo, se o realm for definido no cabeçalho, ele será exibido para o usuário, mas tenha cuidado, esta opção serve a um propósito, verifique [1] para mais informações. O diálogo se parece com isso no chromium:

Diálogo de login do Chromium

Clicar em "entrar" faz com que o navegador repita a solicitação, mas com o cabeçalho Authorization: ..., como eu o configurei para o esquema Básico, o navegador enviará as credenciais codificadas em base64.

A coisa legal sobre isso é que o navegador continuará enviando as credenciais para solicitações subsequentes do mesmo domínio até receber uma resposta de status 401.

Código do servidor para usuários autenticados

Agora que os usuários podem fazer login, toda vez que uma página privada é solicitada, devemos verificar as credenciais. Neste projeto, usei o esquema Básico e o verifico usando as funções http próprias do Go:

usuário, senha, ok := r.BasicAuth()
if !ok {
	w.Header().Set("WWW-Authenticate", "Basic")
	w.WriteHeader(http.StatusUnauthorized)
	
	// enviar uma página de erro
	...
	return
}

// verifique usuário e senha
...

// sirva seu conteúdo

Dessa forma, se uma solicitação chegar não autenticada por algum motivo, o servidor solicitará novamente as credenciais. Outra opção aqui seria redirecionar o usuário para a página de login.

Encerrando a sessão

Encerrar a sessão é feito simplesmente retornando um 401 sem o cabeçalho www-authenticate:

func logout(w http.ResponseWriter, r *http.Request) {
	w.WriteHeader(http.StatusUnauthorized)
	// servir uma página HTML de logout
	...
}

Considerações finais

Este é o método que estou usando agora e considero bastante bom: ele usa apenas recursos padrão que existem há anos, nada de novo; não há JavaScript do lado do cliente ou cookies, o que torna fácil de manter e satisfaz até mesmo os usuários mais exigentes.