Como integrar o OAuth 2 em seu back-end Django/DRF sem enlouquecer

Publicados: 2022-03-11

Todos nós já estivemos lá. Você está trabalhando no back-end da API e está feliz com o andamento. Você concluiu recentemente o produto mínimo viável (MVP), todos os testes estão passando e você está ansioso para implementar alguns novos recursos.

Aí o chefe te manda um e-mail: “Aliás, precisamos deixar as pessoas logarem via Facebook e Google; eles não deveriam ter que criar uma conta apenas para um pequeno site como o nosso.”

Excelente. Scope creep ataca novamente.

A boa notícia é que o OAuth 2 surgiu como o padrão da indústria para autenticação social e de terceiros (usado por serviços como Facebook, Google, etc.) provedores de autenticação.

É provável que você não esteja familiarizado com o OAuth 2; Eu não estava, quando isso aconteceu comigo.

Integre o OAuth 2 ao seu back-end Django/DRF

Como desenvolvedor Python, seu instinto pode levar a pip, a ferramenta recomendada do Python Package Index (PyPA) para instalar pacotes Python. A má notícia é que o pip conhece cerca de 278 pacotes que lidam com OAuth – 53 dos quais mencionam especificamente o Django. É uma semana de trabalho apenas para pesquisar as opções, não importa começar a escrever código.

Neste tutorial, você aprenderá como integrar o OAuth 2 em seu Django ou Django Rest Framework usando o Python Social Auth. Embora este artigo se concentre no Django REST Framework, você pode aplicar as informações fornecidas aqui para implementar o mesmo em uma variedade de outros frameworks de back-end comuns.

Uma visão geral rápida do fluxo do OAuth 2

OAuth 2 foi projetado desde o início como um protocolo de autenticação da web. Isso não é exatamente o mesmo que se tivesse sido projetado como um protocolo de autenticação de rede; ele assume que ferramentas como renderização de HTML e redirecionamentos de navegador estão disponíveis para você.

Obviamente, isso é um obstáculo para uma API baseada em JSON, mas você pode contornar isso.

Você passará pelo processo como se estivesse escrevendo um site tradicional do lado do servidor.

O fluxo OAuth 2 do lado do servidor

A primeira etapa acontece totalmente fora do fluxo do aplicativo. O proprietário do projeto deve registrar seu aplicativo em cada provedor OAuth 2 para o qual você precisa de logins.

Durante esse registro, eles fornecem ao provedor OAuth 2 um URI de retorno de chamada, no qual seu aplicativo estará disponível para receber solicitações. Em troca, eles recebem uma chave de cliente e um segredo de cliente . Esses tokens são trocados durante o processo de autenticação para validar as solicitações de login.

Os tokens referem-se ao código do seu servidor como o cliente. O host é o provedor OAuth 2. Eles não são destinados aos clientes da sua API.

O fluxo começa quando seu aplicativo gera uma página que inclui um botão, como “Fazer login com o Facebook” ou “Fazer login com o Google+”. Fundamentalmente, estes não são nada além de links simples, cada um dos quais aponta para um URL como o seguinte:

 https://oauth2provider.com/auth? response_type=code& client_id=CLIENT_KEY& redirect_uri=CALLBACK_URI& scope=profile& scope=email

(Observação: quebras de linha inseridas no URI acima para facilitar a leitura.)

Você forneceu a chave do cliente e o URI de redirecionamento, mas nenhum segredo. Em troca, você disse ao servidor que gostaria de um código de autenticação em resposta e acesso aos escopos 'perfil' e 'e-mail'. Esses escopos definem as permissões que você solicita do usuário e limitam a autorização do token de acesso que você recebe.

Após o recebimento, o navegador do usuário é direcionado para uma página dinâmica controlada pelo provedor OAuth 2. O provedor OAuth 2 verifica se o URI de retorno de chamada e a chave do cliente correspondem entre si antes de continuar. Se o fizerem, o fluxo diverge brevemente dependendo dos tokens de sessão do usuário.

Se o usuário não estiver conectado a esse serviço no momento, ele será solicitado a fazê-lo. Depois de fazer login, o usuário verá uma caixa de diálogo solicitando permissão para permitir que seu aplicativo faça login.

Supondo que o usuário aprove, o servidor OAuth 2 os redirecionará de volta para o URI de retorno de chamada que você forneceu, incluindo um código de autorização nos parâmetros de consulta: GET https://api.yourapp.com/oauth2/callback/?code=AUTH_CODE .

O código de autorização é um token de uso único e de expiração rápida; imediatamente após o recebimento, seu servidor deve se virar e fazer outra solicitação ao provedor OAuth 2, incluindo o código de autorização e o segredo do cliente:

 POST https://oauth2provider.com/token/? grant_type=authorization_code& code=AUTH_CODE& redirect_uri=CALLBACK_URI& client_id=CLIENT_KEY& client_secret=CLIENT_SECRET

A finalidade deste código de autorização é autenticar a solicitação POST acima, mas devido à natureza do fluxo, ela deve ser roteada pelo sistema do usuário. Como tal, é inerentemente inseguro.

As restrições sobre o código de autorização (isto é, que ele expire rapidamente e possa ser usado apenas uma vez) existem para mitigar o risco inerente de passar uma credencial de autenticação por meio de um sistema não confiável.

Essa chamada, feita diretamente do seu servidor para o servidor do provedor OAuth 2, é o componente principal do processo de login do lado do servidor OAuth 2. Controlar a chamada significa que você sabe que a chamada é protegida por TLS, ajudando assim a protegê-la contra ataques de escutas telefônicas.

A inclusão do código de autorização garante que o usuário concedeu explicitamente o consentimento. Incluir o segredo do cliente, que nunca é visível para seus usuários, garante que essa solicitação não seja originada de algum vírus ou malware no sistema do usuário, que interceptou o código de autorização.

Se tudo estiver correto, o servidor retornará um token de acesso , com o qual você poderá fazer chamadas para esse provedor enquanto estiver autenticado como usuário.

Depois de receber o token de acesso do servidor, seu servidor redireciona o navegador do usuário mais uma vez para a página de destino dos usuários que acabaram de fazer login. É comum reter o token de acesso no cache de sessão do lado do servidor do usuário, então que o servidor pode fazer chamadas para determinado provedor social sempre que necessário.

O token de acesso nunca deve ser disponibilizado ao usuário!

Há mais detalhes em que poderíamos mergulhar.

Por exemplo, o Google inclui um token de atualização que estende a vida útil do seu token de acesso, enquanto o Facebook fornece um endpoint no qual você pode trocar tokens de acesso de curta duração por algo de vida útil mais longa. Esses detalhes não importam para nós, porém, porque não vamos usar esse fluxo.

Esse fluxo é complicado para uma API REST. Embora você possa fazer com que o cliente front-end gere a página de login inicial e faça com que o back-end forneça um URL de retorno de chamada, você eventualmente terá um problema. Você deseja redirecionar o usuário para a página de destino do front-end depois de receber o token de acesso e não há uma maneira RESTful clara de fazer isso.

Felizmente, há outro fluxo OAuth 2 disponível, que funciona muito melhor nesse caso.

O fluxo OAuth 2 do lado do cliente

Nesse fluxo, o front-end se torna responsável por tratar todo o processo OAuth 2. Geralmente se assemelha ao fluxo do lado do servidor, com uma exceção importante – os front-ends vivem em máquinas que os usuários controlam, portanto, não podem ser confiados ao segredo do cliente. A solução é simplesmente eliminar toda essa etapa do processo.

A primeira etapa, como no fluxo do lado do servidor, é registrar o aplicativo.

Nesse caso, o proprietário do projeto ainda registra o aplicativo, mas como um aplicativo da web. O provedor OAuth 2 ainda fornecerá uma chave de cliente , mas não poderá fornecer nenhum segredo de cliente.

O front-end fornece ao usuário um botão de login social, que direciona para uma página da Web controlada pelo provedor OAuth 2 e solicita permissão para que nosso aplicativo acesse determinados aspectos do perfil do usuário.

O URL parece um pouco diferente desta vez:

 https://oauth2provider.com/auth? response_type=token& client_id=CLIENT_KEY& redirect_uri=CALLBACK_URI& scope=profile& scope=email

Observe que o parâmetro response_type desta vez na URL é token .

E o URI de redirecionamento?

Isso é simplesmente qualquer endereço no front-end que esteja preparado para lidar com o token de acesso adequadamente.

Dependendo da biblioteca OAuth 2 em uso, o front-end pode executar temporariamente um servidor capaz de aceitar solicitações HTTP no dispositivo do usuário; nesse caso, o URL de redirecionamento tem o formato http://localhost:7862/callback/?token=TOKEN .

Como o servidor OAuth 2 retorna um redirecionamento HTTP após a aceitação do usuário, e esse redirecionamento é processado pelo navegador no dispositivo do usuário, esse endereço é interpretado corretamente, dando acesso de front-end ao token.

Alternativamente, o front-end pode implementar diretamente uma página apropriada. De qualquer forma, o front-end é responsável, neste momento, por analisar os parâmetros de consulta e processar o token de acesso.

A partir deste ponto, o front-end pode chamar diretamente a API do provedor OAuth 2 usando o token. Mas os usuários realmente não querem isso; eles querem acesso autenticado à sua API. Tudo o que o back-end precisa fornecer é um endpoint no qual o front-end pode trocar o token de acesso de um provedor social por um token que concede acesso à sua API.

Por que permitir isso, já que fornecer o token de acesso ao front-end é inerentemente menos seguro do que o fluxo do lado do servidor?

O fluxo do lado do cliente permite uma separação mais estrita entre uma API REST de back-end e um front-end voltado para o usuário. Não há nada que impeça você de especificar seu servidor de back-end como o URI de redirecionamento; o efeito final seria algum tipo de fluxo híbrido.

O problema é que o servidor deve gerar uma página apropriada voltada para o usuário e, em seguida, entregar o controle de volta ao front-end de alguma forma.

É comum em projetos modernos separar estritamente as preocupações entre a interface do usuário de front-end e o back-end que lida com toda a lógica de negócios. Eles normalmente se comunicam por meio de uma API JSON bem definida. O fluxo híbrido descrito acima atrapalha essa separação de preocupações, forçando o back-end a servir uma página voltada para o usuário e, em seguida, projetar algum fluxo para de alguma forma entregar o controle de volta ao front-end.

Permitir que o front-end lide com o token de acesso é uma técnica conveniente que mantém a separação de interesses. Isso aumenta um pouco o risco de um cliente comprometido, mas funciona bem, em geral.

Esse fluxo pode parecer complicado para o front-end, e é, se você exigir que a equipe de front-end desenvolva tudo por conta própria. No entanto, o Facebook e o Google fornecem bibliotecas que permitem que o front-end inclua botões de login que lidam com todo o processo com uma configuração mínima.

Aqui está uma receita para troca de tokens no back-end.

No fluxo do cliente, o back-end é bastante isolado do processo OAuth 2. Não se deixe enganar: Este não é um trabalho simples. Você vai querer que ele suporte pelo menos as seguintes funcionalidades.

  • Envie pelo menos uma solicitação para o provedor OAuth 2, apenas para garantir que o token fornecido pelo front-end seja válido, não uma string aleatória arbitrária.
  • Quando o token for válido, retorne um token válido para sua API. Caso contrário, retorne um erro informativo.
  • Se este for um novo usuário, crie um modelo de User para ele e preencha-o adequadamente.
  • Se este for um usuário para o qual já existe um modelo de User , corresponda-o por seu endereço de e-mail, para que ele tenha acesso à conta existente correta em vez de criar uma nova para o login social.
  • Atualize os detalhes do perfil do usuário com base no que ele forneceu nas mídias sociais.

A boa notícia é que implementar toda essa funcionalidade no back-end é muito mais simples do que você imagina.

Aqui está a mágica de como fazer tudo isso funcionar no back-end em apenas duas dúzias de linhas de código. Isso depende da biblioteca Python Social Auth (“PSA” daqui em diante), portanto, você precisará incluir social-auth-core e social-auth-app-django em seu requirements.txt .

Você também precisará configurar a biblioteca conforme documentado aqui. Observe que isso exclui algum tratamento de exceção para maior clareza.

O código completo para este exemplo pode ser encontrado aqui.

 @api_view(http_method_names=['POST']) @permission_classes([AllowAny]) @psa() def exchange_token(request, backend): serializer = SocialSerializer(data=request.data) if serializer.is_valid(raise_exception=True): # This is the key line of code: with the @psa() decorator above, # it engages the PSA machinery to perform whatever social authentication # steps are configured in your SOCIAL_AUTH_PIPELINE. At the end, it either # hands you a populated User model of whatever type you've configured in # your project, or None. user = request.backend.do_auth(serializer.validated_data['access_token']) if user: # if using some other token back-end than DRF's built-in TokenAuthentication, # you'll need to customize this to get an appropriate token object token, _ = Token.objects.get_or_create(user=user) return Response({'token': token.key}) else: return Response( {'errors': {'token': 'Invalid token'}}, status=status.HTTP_400_BAD_REQUEST, )

Há apenas um pouco mais que precisa ir em suas configurações (código completo), e então está tudo pronto:

 AUTHENTICATION_BACKENDS = ( 'social_core.backends.google.GoogleOAuth2', 'social_core.backends.facebook.FacebookOAuth2', 'django.contrib.auth.backends.ModelBackend', ) for key in ['GOOGLE_OAUTH2_KEY', 'GOOGLE_OAUTH2_SECRET', 'FACEBOOK_KEY', 'FACEBOOK_SECRET']: # Use exec instead of eval here because we're not just trying to evaluate a dynamic value here; # we're setting a module attribute whose name varies. exec("SOCIAL_AUTH_{key} = os.environ.get('{key}')".format(key=key)) SOCIAL_AUTH_PIPELINE = ( 'social_core.pipeline.social_auth.social_details', 'social_core.pipeline.social_auth.social_uid', 'social_core.pipeline.social_auth.auth_allowed', 'social_core.pipeline.social_auth.social_user', 'social_core.pipeline.user.get_username', 'social_core.pipeline.social_auth.associate_by_email', 'social_core.pipeline.user.create_user', 'social_core.pipeline.social_auth.associate_user', 'social_core.pipeline.social_auth.load_extra_data', 'social_core.pipeline.user.user_details', )

Adicione um mapeamento a esta função em seu urls.py e está tudo pronto!

Como funciona essa mágica?

O Python Social Auth é uma máquina muito legal e muito complexa. Ele fica perfeitamente feliz em lidar com autenticação e acesso a qualquer uma das várias dezenas de provedores de autenticação social e funciona nos frameworks web Python mais populares, incluindo Django, Flask, Pyramid, CherryPy e WebPy.

Na maior parte, o código acima é uma visão baseada em função do Django REST framework (DRF) muito padrão: ele escuta solicitações POST em qualquer caminho para o qual você o mapeie em seu urls.py e, supondo que você envie uma solicitação no formato que ele espera, ele obtém um objeto User ou None .

Se você obtiver um objeto User , é do tipo de modelo que você configurou em outro lugar em seu projeto, que pode ou não já ter existido. A PSA já cuidou da validação do token, identificando se existia ou não uma correspondência de usuário, criando um usuário se necessário e atualizando os detalhes do usuário do provedor social.

Os detalhes exatos de como um usuário é mapeado do usuário do provedor social para o seu e associado aos usuários existentes são especificados pelo SOCIAL_AUTH_PIPELINE definido acima. Há muito mais para aprender sobre como tudo isso funciona, mas está fora do escopo deste post. Você pode ler mais sobre isso aqui.

A chave da mágica é o decorador @psa() na visualização, que adiciona alguns membros ao objeto de request que é passado para sua visualização. O mais interessante para nós é request.backend (para PSA, um backend é qualquer provedor de autenticação social).

O back-end apropriado foi escolhido para nós e anexado ao objeto de request com base no argumento de backend -end para a visualização, que é preenchida pela própria URL.

Uma vez que você tenha o objeto de backend -end em mãos, é perfeitamente possível autenticá-lo em relação a esse provedor, dado seu código de acesso; esse é o método do_auth . Isso, por sua vez, envolve a totalidade do SOCIAL_AUTH_PIPELINE do seu arquivo de configuração.

O pipeline pode fazer algumas coisas muito poderosas se você o estender, embora ele já faça tudo o que você precisa com nada além de sua funcionalidade interna padrão.

Depois disso, é só voltar ao código DRF normal: se você tiver um objeto User válido, poderá retornar facilmente um token de API apropriado. Se você não recebeu um objeto User válido de volta, é fácil gerar um erro.

Uma desvantagem dessa técnica é que, embora seja relativamente simples retornar erros se eles ocorrerem, é difícil obter muitos insights sobre o que especificamente deu errado. O PSA engole todos os detalhes que o servidor possa ter retornado sobre qual era o problema.

Por outro lado, é da natureza de sistemas de autenticação bem projetados serem bastante opacos sobre as fontes de erro. Se um aplicativo disser a um usuário “Senha inválida” após uma tentativa de login, isso equivale a dizer “Parabéns! Você adivinhou um nome de usuário válido.”

Por que não apenas rolar o seu próprio?

Em uma palavra: extensibilidade. Muito poucos provedores OAuth 2 sociais exigem ou retornam exatamente as mesmas informações em suas chamadas de API exatamente da mesma maneira. Existem todos os tipos de casos especiais e exceções.

Adicionar um novo provedor social depois de configurar um PSA é uma questão de algumas linhas de configuração em seus arquivos de configurações. Você não precisa ajustar nenhum código. O PSA abstrai tudo isso, para que você possa se concentrar em seu próprio aplicativo.

Como diabos eu testo isso?

Boa pergunta! unittest.mock não é adequado para simular chamadas de API enterradas sob uma camada de abstração nas profundezas de uma biblioteca; apenas descobrir o caminho preciso para zombar exigiria um esforço substancial.

Em vez disso, como o PSA é construído sobre a biblioteca Requests, você usa a excelente biblioteca Responses para simular os provedores no nível HTTP.

Uma discussão completa sobre testes está além do escopo deste artigo, mas uma amostra de nossos testes está incluída aqui. Funções específicas a serem observadas são o gerenciador de contexto SocialAuthTests mocked

Deixe a PSA fazer o trabalho pesado.

O processo OAuth2 é detalhado e complicado com muita complexidade inerente. Felizmente, é possível contornar grande parte dessa complexidade trazendo uma biblioteca dedicada a manipulá-la da maneira mais simples possível.

O Python Social Auth faz um ótimo trabalho nisso. Demonstramos uma visualização Django/DRF que utiliza o fluxo OAuth2 implícito do lado do cliente para obter criação e correspondência perfeitas de usuários em apenas 25 linhas de código. Isso não é muito pobre.