Geheimnisse unter Linux bewahren: JWT-Authentifizierung in einer Python-CLI-Anwendung

JSON Web Token ist ein offener Standard zum Erstellen von Zugriffstoken basierend auf dem JSON-Format. Wird normalerweise zum Übergeben von Authentifizierungsdaten in Client-Server-Anwendungen verwendet. Wikipedia

Wenn Sie vertrauliche Daten in einem Browser speichern möchten, müssen Sie nur eine der beiden verfügbaren Optionen verwenden: Cookies oder localStorage. Hier schmeckt jeder. Ich habe diesen Artikel jedoch dem Secret Service gewidmet, einem Dienst, der über D-Bus ausgeführt wird und zum Speichern von "Geheimnissen" unter Linux entwickelt wurde.

Der Dienst verfügt über eine API, mit der GNOME Keyring Anwendungsgeheimnisse speichert.

Warum Geheimdienst?

Die Sache ist, ich habe das Token nicht im Browser erhalten. Ich habe eine Clientauthentifizierung für eine Konsolenanwendung geschrieben, die der in git verwendeten ähnelt.

Die Frage nach der Methode zum Speichern der Details stellte sich sofort, da ich Benutzer nicht zwingen wollte, sich beim nächsten Start meiner Anwendung anzumelden.

Anfangs gab es eine Option, um das Token in einer verschlüsselten Datei zu speichern, aber es verschwand sofort, weil ich vermutete, dass meine Verschlüsselungs- und Entschlüsselungsfunktionen ein Fahrrad sein würden.

, Linux, , .

Linux.

Secret Service

Secret Service — .

. - «default». . Seahorse .

, Google Chrome VSCode. .

.

, .

, , .

, , , .

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 generiert automatisch Hilfe. Dazu benötigt er die Zeilen, die ich im Parameter helpseiner Dekorateure angegeben habe.

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

Options:
  --help  Show this message and exit.

Commands:
  login  Login into your account.

Hilfe zum Anmeldebefehl

$ 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.

Prüfen

Nachdem die Anwendung mit dem Befehl gestartet wurde, werden python app.py loginSie nach einer E-Mail und einem Kennwort gefragt. Wenn diese Daten korrekt sind, wird das entsprechende Element im Secrete Service angezeigt.

Es speichert tatsächlich den Token.

Wenn Sie die Anwendung neu starten, werden keine Details abgefragt, sondern das Token vom Secret Service heruntergeladen.

Links




All Articles