You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

13 KiB

Simples OAuth2 com senha e Bearer

Agora vamos construir a partir do capítulo anterior e adicionar as partes que faltam para ter um fluxo de segurança completo.

Pegue o username (nome de usuário) e password (senha)

É utilizado o utils de segurança da FastAPI para obter o username e a password.

OAuth2 especifica que ao usar o "password flow" (fluxo de senha), que estamos usando, o cliente/usuário deve enviar os campos username e password como dados do formulário.

E a especificação diz que os campos devem ser nomeados assim. Portanto, user-name ou email não funcionariam.

Mas não se preocupe, você pode mostrá-lo como quiser aos usuários finais no frontend.

E seus modelos de banco de dados podem usar qualquer outro nome que você desejar.

Mas para a operação de rota de login, precisamos usar esses nomes para serem compatíveis com a especificação (e poder, por exemplo, usar o sistema integrado de documentação da API).

A especificação também afirma que o username e a password devem ser enviados como dados de formulário (portanto, não há JSON aqui).

scope

A especificação também diz que o cliente pode enviar outro campo de formulário "scope" (Escopo).

O nome do campo do formulário é scope (no singular), mas na verdade é uma longa string com "escopos" separados por espaços.

Cada “scope” é apenas uma string (sem espaços).

Normalmente são usados para declarar permissões de segurança específicas, por exemplo:

  • users:read ou users:write são exemplos comuns.
  • instagram_basic é usado pelo Facebook e Instagram.
  • https://www.googleapis.com/auth/drive é usado pelo Google.

/// info | Informação

No OAuth2, um "scope" é apenas uma string que declara uma permissão específica necessária.

Não importa se tem outros caracteres como : ou se é uma URL.

Esses detalhes são específicos da implementação.

Para OAuth2 são apenas strings.

///

Código para conseguir o username e a password

Agora vamos usar os utilitários fornecidos pelo FastAPI para lidar com isso.

OAuth2PasswordRequestForm

Primeiro, importe OAuth2PasswordRequestForm e use-o como uma dependência com Depends na operação de rota para /token:

//// tab | Python 3.10+

{!> ../../docs_src/security/tutorial003_an_py310.py!}

////

//// tab | Python 3.9+

{!> ../../docs_src/security/tutorial003_an_py39.py!}

////

//// tab | Python 3.8+

{!> ../../docs_src/security/tutorial003_an.py!}

////

//// tab | Python 3.10+ non-Annotated

/// tip | Dica

Prefira usar a versão Annotated, se possível.

///

{!> ../../docs_src/security/tutorial003_py310.py!}

////

//// tab | Python 3.8+ non-Annotated

/// tip | Dica

Prefira usar a versão Annotated, se possível.

///

{!> ../../docs_src/security/tutorial003.py!}

////

OAuth2PasswordRequestForm é uma dependência de classe que declara um corpo de formulário com:

  • O username.
  • A password.
  • Um campo scope opcional como uma string grande, composta de strings separadas por espaços.
  • Um grant_type (tipo de concessão) opcional.

/// tip | Dica

A especificação OAuth2 na verdade requer um campo grant_type com um valor fixo de password, mas OAuth2PasswordRequestForm não o impõe.

Se você precisar aplicá-lo, use OAuth2PasswordRequestFormStrict em vez de OAuth2PasswordRequestForm.

///

  • Um client_id opcional (não precisamos dele em nosso exemplo).
  • Um client_secret opcional (não precisamos dele em nosso exemplo).

/// info | Informação

O OAuth2PasswordRequestForm não é uma classe especial para FastAPI como é OAuth2PasswordBearer.

OAuth2PasswordBearer faz com que FastAPI saiba que é um esquema de segurança. Portanto, é adicionado dessa forma ao OpenAPI.

Mas OAuth2PasswordRequestForm é apenas uma dependência de classe que você mesmo poderia ter escrito ou poderia ter declarado os parâmetros do Form (formulário) diretamente.

Mas como é um caso de uso comum, ele é fornecido diretamente pelo FastAPI, apenas para facilitar.

///

Use os dados do formulário

/// tip | Dica

A instância da classe de dependência OAuth2PasswordRequestForm não terá um atributo scope com a string longa separada por espaços, em vez disso, terá um atributo scopes com a lista real de strings para cada escopo enviado.

Não estamos usando scopes neste exemplo, mas a funcionalidade está disponível se você precisar.

///

Agora, obtenha os dados do usuário do banco de dados (falso), usando o username do campo do formulário.

Se não existir tal usuário, retornaremos um erro dizendo "Incorrect username or password" (Nome de usuário ou senha incorretos).

Para o erro, usamos a exceção HTTPException:

//// tab | Python 3.10+

{!> ../../docs_src/security/tutorial003_an_py310.py!}

////

//// tab | Python 3.9+

{!> ../../docs_src/security/tutorial003_an_py39.py!}

////

//// tab | Python 3.8+

{!> ../../docs_src/security/tutorial003_an.py!}

////

//// tab | Python 3.10+ non-Annotated

/// tip | Dica

Prefira usar a versão Annotated, se possível.

///

{!> ../../docs_src/security/tutorial003_py310.py!}

////

//// tab | Python 3.8+ non-Annotated

/// tip | Dica

Prefira usar a versão Annotated, se possível.

///

{!> ../../docs_src/security/tutorial003.py!}

////

Confira a password (senha)

Neste ponto temos os dados do usuário do nosso banco de dados, mas não verificamos a senha.

Vamos colocar esses dados primeiro no modelo UserInDB do Pydantic.

Você nunca deve salvar senhas em texto simples, portanto, usaremos o sistema de hashing de senhas (falsas).

Se as senhas não corresponderem, retornaremos o mesmo erro.

Hashing de senha

"Hashing" significa: converter algum conteúdo (uma senha neste caso) em uma sequência de bytes (apenas uma string) que parece algo sem sentido.

Sempre que você passa exatamente o mesmo conteúdo (exatamente a mesma senha), você obtém exatamente a mesma sequência aleatória de caracteres.

Mas você não pode converter a sequência aleatória de caracteres de volta para a senha.

Porque usar hashing de senha

Se o seu banco de dados for roubado, o ladrão não terá as senhas em texto simples dos seus usuários, apenas os hashes.

Assim, o ladrão não poderá tentar usar essas mesmas senhas em outro sistema (como muitos usuários usam a mesma senha em todos os lugares, isso seria perigoso).

//// tab | Python 3.10+

{!> ../../docs_src/security/tutorial003_an_py310.py!}

////

//// tab | Python 3.9+

{!> ../../docs_src/security/tutorial003_an_py39.py!}

////

//// tab | Python 3.8+

{!> ../../docs_src/security/tutorial003_an.py!}

////

//// tab | Python 3.10+ non-Annotated

/// tip | Dica

Prefira usar a versão Annotated, se possível.

///

{!> ../../docs_src/security/tutorial003_py310.py!}

////

//// tab | Python 3.8+ non-Annotated

/// tip | Dica

Prefira usar a versão Annotated, se possível.

///

{!> ../../docs_src/security/tutorial003.py!}

////

Sobre **user_dict

UserInDB(**user_dict) significa:

Passe as keys (chaves) e values (valores) de user_dict diretamente como argumentos de valor-chave, equivalente a:

UserInDB(
    username = user_dict["username"],
    email = user_dict["email"],
    full_name = user_dict["full_name"],
    disabled = user_dict["disabled"],
    hashed_password = user_dict["hashed_password"],
)

/// info | Informação

Para uma explicação mais completa de **user_dict, verifique a documentação para Extra Models{.internal-link target=_blank}.

///

Retorne o token

A resposta do endpoint token deve ser um objeto JSON.

Deve ter um token_type. No nosso caso, como estamos usando tokens "Bearer", o tipo de token deve ser "bearer".

E deve ter um access_token, com uma string contendo nosso token de acesso.

Para este exemplo simples, seremos completamente inseguros e retornaremos o mesmo username do token.

/// tip | Dica

No próximo capítulo, você verá uma implementação realmente segura, com hash de senha e tokens JWT.

Mas, por enquanto, vamos nos concentrar nos detalhes específicos de que precisamos.

///

//// tab | Python 3.10+

{!> ../../docs_src/security/tutorial003_an_py310.py!}

////

//// tab | Python 3.9+

{!> ../../docs_src/security/tutorial003_an_py39.py!}

////

//// tab | Python 3.8+

{!> ../../docs_src/security/tutorial003_an.py!}

////

//// tab | Python 3.10+ non-Annotated

/// tip | Dica

Prefira usar a versão Annotated, se possível.

///

{!> ../../docs_src/security/tutorial003_py310.py!}

////

//// tab | Python 3.8+ non-Annotated

/// tip | Dica

Prefira usar a versão Annotated, se possível.

///

{!> ../../docs_src/security/tutorial003.py!}

////

/// tip | Dica

Pela especificação, você deve retornar um JSON com um access_token e um token_type, o mesmo que neste exemplo.

Isso é algo que você mesmo deve fazer em seu código e certifique-se de usar essas chaves JSON.

É quase a única coisa que você deve se lembrar de fazer corretamente, para estar em conformidade com as especificações.

De resto, FastAPI cuida disso para você.

///

Atualize as dependências

Agora vamos atualizar nossas dependências.

Queremos obter o user_user somente se este usuário estiver ativo.

Portanto, criamos uma dependência adicional get_current_active_user que por sua vez usa get_current_user como dependência.

Ambas as dependências retornarão apenas um erro HTTP se o usuário não existir ou se estiver inativo.

Portanto, em nosso endpoint, só obteremos um usuário se o usuário existir, tiver sido autenticado corretamente e estiver ativo:

//// tab | Python 3.10+

{!> ../../docs_src/security/tutorial003_an_py310.py!}

////

//// tab | Python 3.9+

{!> ../../docs_src/security/tutorial003_an_py39.py!}

////

//// tab | Python 3.8+

{!> ../../docs_src/security/tutorial003_an.py!}

////

//// tab | Python 3.10+ non-Annotated

/// tip | Dica

Prefira usar a versão Annotated, se possível.

///

{!> ../../docs_src/security/tutorial003_py310.py!}

////

//// tab | Python 3.8+ non-Annotated

/// tip | Dica

Prefira usar a versão Annotated, se possível.

///

{!> ../../docs_src/security/tutorial003.py!}

////

/// info | Informação

O cabeçalho adicional WWW-Authenticate com valor Bearer que estamos retornando aqui também faz parte da especificação.

Qualquer código de status HTTP (erro) 401 "UNAUTHORIZED" também deve retornar um cabeçalho WWW-Authenticate.

No caso de tokens ao portador (nosso caso), o valor desse cabeçalho deve ser Bearer.

Na verdade, você pode pular esse cabeçalho extra e ainda funcionaria.

Mas é fornecido aqui para estar em conformidade com as especificações.

Além disso, pode haver ferramentas que esperam e usam isso (agora ou no futuro) e que podem ser úteis para você ou seus usuários, agora ou no futuro.

Esse é o benefício dos padrões...

///

Veja em ação

Abra o docs interativo: http://127.0.0.1:8000/docs.

Autenticação

Clique no botão "Authorize".

Use as credenciais:

User: johndoe

Password: secret

Após autenticar no sistema, você verá assim:

Obtenha seus próprios dados de usuário

Agora use a operação GET com o caminho /users/me.

Você obterá os dados do seu usuário, como:

{
  "username": "johndoe",
  "email": "johndoe@example.com",
  "full_name": "John Doe",
  "disabled": false,
  "hashed_password": "fakehashedsecret"
}

Se você clicar no ícone de cadeado, sair e tentar a mesma operação novamente, receberá um erro HTTP 401 de:

{
  "detail": "Not authenticated"
}

Usuário inativo

Agora tente com um usuário inativo, autentique-se com:

User: alice

Password: secret2

E tente usar a operação GET com o caminho /users/me.

Você receberá um erro "Usuário inativo", como:

{
  "detail": "Inactive user"
}

Recaptulando

Agora você tem as ferramentas para implementar um sistema de segurança completo baseado em username e password para sua API.

Usando essas ferramentas, você pode tornar o sistema de segurança compatível com qualquer banco de dados e com qualquer usuário ou modelo de dados.

O único detalhe que falta é que ainda não é realmente "seguro".

No próximo capítulo você verá como usar uma biblioteca de hashing de senha segura e tokens JWT.