プログラマーとして認証システムに遭遇することは非常に一般的です。今日、ほぼすべての Web システムが顧客データを制御および維持する必要があり、そのほとんどが機密リソースであるため、安全に保つ必要があるからです。私は、セキュリティは、API の多くの非機能要件と同様に、さまざまなシナリオを想像することで測定またはテストできると考えています。たとえば、認証サービスでは、次のように考えることができます。誰かが総当たりでユーザーのパスワードを発見しようとした場合はどうなるか、別のユーザーが別のクライアントのアクセス トークンを使用しようとした場合はどうなるか、誤って 2 人のユーザーが資格情報を作成した場合はどうなるか」同じパスワードなど
これらの状況を想像することで、予防策を予測し、作成することができます。パスワードの基準を作成すると、ブルート フォースによる検出が非常に困難になる場合があります。また、API にレート制限を適用すると、悪意のあるアクションを防ぐことができます。この記事では、最後のシナリオの問題に焦点を当てたいと思います。 2 人のユーザーが同じパスワードを使用して同じシステムに登録することは、システムに対する重大な侵害です。
銀行ではユーザーのパスワードを暗号化しておくことをお勧めします。これにより、データが漏洩から安全に保たれます。以下のコードは、単純な資格情報登録システムが Python でどのように動作するかを示しています。
@dataclass class CreateCredentialUsecase: _credential_repository: CredentialRepositoryInterface _password_salt_repository: PasswordSaltRepositoryInterface async def handle(self, data: CreateCredentialInputDto) -> CreateCredentialOutputDto: try: now = datetime.now() self.__hash = sha256() self.__hash.update(data.password.encode()) self.__credential = Credential( uuid4(), data.email, self.__hash.hexdigest(), now, now ) credential_id = await self._credential_repository.create(self.__credential) return CreateCredentialOutputDto(UUID(credential_id)) except Exception as e: raise e
最初の 4 行は、@dataclass デコレータを使用してコンストラクター メソッド、そのプロパティ、関数シグネチャを省略したクラス定義です。 try/excel ブロック内では、まず現在のタイムスタンプが定義され、ハッシュ オブジェクトをインスタンス化し、提供されたパスワードで更新し、バンクに保存して、最後に資格情報 ID をユーザーに返します。ここで、「大丈夫...パスワードが暗号化されていれば心配する必要はないですよね?」と思うかもしれません。しかし、そうではないので説明します。
パスワードの暗号化は、入力を最終値にマッピングするデータ構造の一種であるハッシュを通じて行われますが、2 つの入力が同じである場合は、同じパスワードが保存されます。これは、ハッシュが決定的であると言っているのと同じです。ユーザーとハッシュを保存するデータベース内の単純なテーブルを示す以下の例に注目してください。
user | password |
---|---|
alice@example.com | 5e884898da28047151d0e56f8dc6292773603d0d |
bob@example.com | 6dcd4ce23d88e2ee9568ba546c007c63e8f6f8d6 |
carol@example.com | a3c5b2c98b4325c6c8c6f6e6dbda6cf17b5d7f9a |
dave@example.com | 1a79a4d60de6718e8e5b326e338ae533 |
eve@example.com | 5e884898da28047151d0e56f8dc6292773603d0d |
frank@example.com | 7c6a180b36896a8a8c6a2c29e7d7b1d3 |
grace@example.com | 3c59dc048e885024e146d1e4d9d0e4b2 |
Neste exemplo, as linhas 1 e 5 compartilham o mesmo hash e, portanto, a mesma senha. Para contornarmos esse problema podemos utilizar o salt.
Vamos colocar um pouco de sal nessa senha...
A ideia é que no momento do cadastro do usuário uma string seja gerada de forma aleatória e seja concatenada a senha do usuário antes das credenciais serem salvas no banco. Em seguida esse salt é salvo em uma tabela separada e deve ser utilizada novamente durante o login do usuário. O código alterado ficaria como o exemplo abaixo:
@dataclass class CreateCredentialUsecase: _credential_repository: CredentialRepositoryInterface _password_salt_repository: PasswordSaltRepositoryInterface async def handle(self, data: CreateCredentialInputDto) -> CreateCredentialOutputDto: try: now = datetime.now() self.__salt = urandom(32) self.__hash = sha256() self.__hash.update(self.__salt + data.password.encode()) self.__credential = Credential( uuid4(), data.email, self.__hash.hexdigest(), now, now ) self.__salt = PasswordSalt( uuid4(), self.__salt.hex(), self.__credential.id, now, now ) credential_id = await self._credential_repository.create(self.__credential) await self._password_salt_repository.create(self.__salt) return CreateCredentialOutputDto(UUID(credential_id)) except Exception as e: raise e
Agora é possível notar o salt gerado na linha 59. Em seguida ele é utilizado para gerar o hash junto com a senha que o usuário cadastrou, na linha 61. Por fim ele é instanciado através da classe PasswordSalt na linha 65 e armazenado no banco na linha 70. Por último, o código abaixo é o caso de uso de autenticação/login utilizando o salt.
@dataclass class AuthUsecase: _credential_repository: CredentialRepositoryInterface _jwt_service: JWTService _refresh_token_repository: RefreshTokenRepositoryInterface async def handle(self, data: AuthInputDto) -> AuthOutputDto: try: ACCESS_TOKEN_HOURS_TO_EXPIRATION = int( getenv("ACCESS_TOKEN_HOURS_TO_EXPIRATION") ) REFRESH_TOKEN_HOURS_TO_EXPIRATION = int( getenv("REFRESH_TOKEN_HOURS_TO_EXPIRATION") ) self.__credential = await self._credential_repository.find_by_email( data.email ) if self.__credential is None: raise InvalidCredentials() self.__hash = sha256() self.__hash.update( bytes.fromhex(self.__credential.salt) + data.password.encode() ) if self.__hash.hexdigest() != self.__credential.hashed_password: raise InvalidCredentials() access_token_expiration_time = datetime.now() + timedelta( hours=( ACCESS_TOKEN_HOURS_TO_EXPIRATION if ACCESS_TOKEN_HOURS_TO_EXPIRATION is not None else 24 ) ) refresh_token_expiration_time = datetime.now() + timedelta( hours=( REFRESH_TOKEN_HOURS_TO_EXPIRATION if REFRESH_TOKEN_HOURS_TO_EXPIRATION is not None else 48 ) ) access_token_payload = { "credential_id": self.__credential.id, "email": self.__credential.email, "exp": access_token_expiration_time, } access_token = self._jwt_service.encode(access_token_payload) refresh_token_payload = { "exp": refresh_token_expiration_time, "context": { "credential": { "id": self.__credential.id, "email": self.__credential.email, }, }, } refresh_token = self._jwt_service.encode(refresh_token_payload) print(self._jwt_service.decode(refresh_token)) now = datetime.now() await self._refresh_token_repository.create( RefreshToken( uuid4(), refresh_token, False, self.__credential.id, refresh_token_expiration_time, now, now, now, ) ) return AuthOutputDto( UUID(self.__credential.id), self.__credential.email, access_token, refresh_token, ) except Exception as e: raise e
O tempo de expiração dos tokens é recuperado através de variáveis de ambiente e a credencial com o salt são recuperados através do email. Entre as linhas 103 e 106 a senha fornecida pelo usuário é concatenada ao salt e o hash dessa string resultante é gerado, assim é possível comparar com a senha armazenada no banco. Por fim acontecem os processos de criação dos access_token e refresh_token, o armazenamento do refresh_token e o retorno dos mesmos ao client. Utilizar essa técnica é bem simples e permite fechar uma falha de segurança no seu sistema, além de dificultar alguns outros possíveis ataques. O código exposto no texto faz parte de um projeto maior meu e está no meu github: https://github.com/geovanymds/auth.
Espero que esse texto tenha sido útil para deixar os processos de autenticação no seu sistem mais seguros. Nos vemos no próximo artigo!
以上がSalt を使用した Python での認証サービスの作成の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。