Mantendo segredos no Linux: autenticação JWT em um aplicativo Python CLI

JSON Web Token é um padrão aberto para a criação de tokens de acesso com base no formato JSON. Normalmente usado para transmitir dados de autenticação em aplicativos cliente-servidor. Wikipedia

Quando se trata de armazenar dados confidenciais em um navegador, você só precisa usar uma das duas opções disponíveis: cookies ou localStorage. Aqui, todo mundo escolhe provar. No entanto, dediquei este artigo ao Secret Service, um serviço executado em D-Bus e projetado para armazenar "segredos" no Linux.

O serviço tem uma API que o GNOME Keyring usa para armazenar segredos de aplicativos.

Por que serviço secreto

Acontece que não estava recebendo o token no navegador. Eu estava escrevendo a autenticação do cliente para um aplicativo de console semelhante ao usado no git.

A questão sobre o método de armazenamento dos detalhes surgiu imediatamente, já que eu não queria forçar os usuários a fazer login na próxima vez que meu aplicativo fosse iniciado.

No início, havia uma opção de armazenar o token em um arquivo criptografado, mas ele desapareceu imediatamente porque imaginei que minhas funções de criptografia e descriptografia seriam uma bicicleta.

Então pensei em como o Linux guarda segredos e descobri que mecanismos semelhantes são implementados em outros sistemas operacionais.

Como resultado, a senha da conta de usuário do Linux servirá como a chave de acesso ao token.

Arquitetura do serviço secreto em resumo

A principal estrutura de dados do Serviço Secreto é uma coleção de elementos com atributos e um segredo.

Coleção

Esta é uma coleção de todos os tipos de dados de autenticação. O sistema usa a coleção padrão sob o alias "default". Todos os aplicativos do usuário são gravados nele. Seahorse vai nos mostrar isso.

Como você pode ver, o Google Chrome e o VSCode são salvos em meu armazenamento. Meu aplicativo também será salvo aqui.

Cada um desses registros é chamado de item.

Elemento

A parte da coleção que armazena atributos e um segredo.

Atributos

Um par da chave do formulário, valor, que contém o nome do aplicativo e serve para identificar o elemento.

Segredo

, , , .

Secret Service

, flow chart.

  • « ?» — .

  • « » — .

  • « » — .

  • « API» — .

  • « » — .

  • « » — .

Python

Click Framework CLI .

import click

, , . . , .

@click.group()
def cli():
    pass

.

:

$ app login
Email:
Password:

:

$ app login
Logged in!

login

@cli.command(help="Login into your account.")
@click.option(
    '--email',
    prompt=True,
    help='Registered email address.')
@click.option(
    '--password',
    prompt=True,
    hide_input=True,
    help='Password provided at registration.'
    )
def login(email, password):
        pass

if __name__ == '__main__':
    cli()

login, , email password.

@cli.command , @click.option .

, hide_input .

prompt , lick Framework , .

True False , :

  • True Click Framework . . , WEB API Secret Service, Secret Service API;

  • False Click Framework . , , Secret Service.

, prompt_desicion. Secret Service. , Secret Service API .

. , , Click Framework.

, , Click Framework, .

app Click Framework. auth, Auth .

.
├── auth.py
└── app.py

prompt_desicion auth Auth auth.

@cli.command(help="Login into your account.")
@click.option(
    '--email',
    prompt=auth.prompt_desicion,
    help='Registered email address.')
@click.option(
    '--password',
    prompt=auth.prompt_desicion,
    hide_input=True,
    help='Password provided at registration.'
    )
def login(email, password):
        pass

if __name__ == '__main__':
    cli()

Python SecreteStorage, Secret Service API.

, Secret Service.

Secret Service API WEB API, prompt_desicion .

  • requests — HTTP WEB API.

  • secretstorage — Secret Service API.

  • json — .

import requests
import secretstorage
import json

Secrete Storage

class Auth:
    def __init__(self, email=None, password=None):
        # ,     
        # Secret Service
        self._attributes = {'application': 'MyApp'}
        #   Dbus
        self._connection = secretstorage.dbus_init()
        #   -
        self._collection = secretstorage.collection.get_default_collection(
            self._connection
            )
        #       
        self._items = self._collection.search_items(self._attributes)
        #   
        self._stored_secret = self.get_stored_secret()

.

Secret Service self._attributes.

«_»

, . , . , . , , . , .

, . () SecretStorage () . , , self._items .

get_stored_secret, .

class Auth:
    def get_stored_secret(self):
        for item in self._items:
            if item:
                return json.loads(item.get_secret())

Item secretstorage, get_secret, .

. .

.

True False — ,

, , : « — False».

class Auth:        
    def __init__(self, email=None, password=None):
        # ,     
        self.prompt_desicion = False

; , .

. , .

class Auth:        
    def __init__(self, email=None, password=None):
        # ,     
        #   
        if self._stored_secret:
            #      token
            self.token = self._stored_secret['token']
        #       
        elif email and password:
            #    WEB API
            self.token = self.get_token(email, password)
            #    
            self._valid_secret = {'token': self.token}
            #    Secret Service
            self.set_stored_secret()
        else:
            #     Secret Storage,     
            # 
            self.prompt_desicion = True

- .

  1. Secret Storage API.

  2. , .

  3. Secret Storage.

  1. Secret Storage API.

  2. , Secret Storage.

  3. , .

  4. , WEB API.

  5. , Secret Storage.

Secret Storage WEB API.

WEB API

class Auth:        
    def get_token(self, email: str, password: str) -> str:
        try:
            response = requests.post(
                API_URL,
                data= {
                    'email': email,
                    'passwd': password
                    })
            data = response.json()
        except requests.exceptions.ConnectionError:
            raise requests.exceptions.ConnectionError()
        if response.status_code != 200:
            raise requests.exceptions.HTTPError(data['msg'])
        return data['data']['token']

API_URL API. , . , POST «email» «passwd».

API , API .

API «msg» . try .

«data».

Secret Storage

class Auth:        
    def set_stored_secret(self):
        self._collection.create_item(
            'MyApp',
            self._attributes,
            bytes((json.dumps(self._valid_secret)), 'utf-8')
            )

create_item , .

lick Framework

auth.

from auth import Auth

Secret Storage.

auth = Auth()

.

@cli.command(help="Login into VPN Manager account.")
@click.option(
    '--email',
    prompt=auth.prompt_desicion,
    help='Registered email address.')
@click.option(
    '--password',
    prompt=auth.prompt_desicion,
    hide_input=True,
    help='Password provided at registration.'
    )

login.

def login(email, password):
    global auth
    try:
      	#         
        if auth.prompt_desicion:
          	#        Secret Storage
            auth = Auth(email, password)
    except Exception:
        return click.echo('No API connection')
		#     ,   .
    click.echo(auth.token)

Click Framework gera ajuda automaticamente. Para fazer isso, ele precisa das linhas que especifiquei no parâmetro de helpseus decoradores.

$ python app.py 
Usage: app.py [OPTIONS] COMMAND [ARGS]...

Options:
  --help  Show this message and exit.

Commands:
  login  Login into your account.

Ajuda do comando de login

$ python app.py login --help
Usage: app.py login [OPTIONS]

  Login into your account

Options:
  --email TEXT     Registered email address
  --password TEXT  Password provided at registration
  --help           Show this message and exit.

Verifica

Após iniciar o aplicativo com o comando, python app.py loginele solicitará um e-mail e uma senha. Se esses dados estiverem corretos, o elemento correspondente aparecerá no Serviço Secreto.

Na verdade, ele armazena o token.

Ao reiniciá-lo, o aplicativo não pedirá detalhes, mas fará o download do token do serviço secreto.

Links




All Articles