Keeping Secrets in Linux: JWT Authentication in a Python CLI Application

JSON Web Token is an open standard for creating access tokens based on the JSON format. Typically used to pass authentication data in client-server applications. Wikipedia

When it comes to storing sensitive data in a browser, you just need to use one of the two available options: cookies or localStorage. Here everyone chooses to taste. However, I dedicated this article to Secret Service, a service that runs over D-Bus and is designed to store "secrets" in Linux.

The service has an API that GNOME Keyring uses to store application secrets.

Why Secret Service

The thing is, I was not receiving the token in the browser. I was writing client authentication for a console application similar to the one used in git.

The question of how to store the details arose immediately, since I did not want to force users to log in the next time I launch my application.

At first there was an option to store the token in an encrypted file, but it disappeared right away because I guessed that my encryption and decryption functions would be a bike.

Then I thought about how Linux keeps secrets, and it turned out that similar mechanisms are implemented in other operating systems.

As a result, the password of the Linux user account will serve as the access key to the token.

Secret Service architecture at a glance

The main data structure of Secret Service is a collection of elements with attributes and a secret.

Collection

This is a collection of all kinds of authentication data. The system uses the default collection under the alias "default". All user applications are written to it. Seahorse will show it to us.

As you can see, Google Chrome and VSCode are saved in my storage. My application will also be saved here.

Each such record is called an item.

Element

The part of the collection that stores attributes and a secret.

Attributes

A pair of the form key, value, which contains the name of the application and serves to identify the element.

Secret

, , , .

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 generates help automatically. To do this, he needs the lines that I specified in the parameter of helphis decorators.

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

Options:
  --help  Show this message and exit.

Commands:
  login  Login into your account.

Login command help

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

Check

After starting the application with the command, python app.py loginit will ask for a mail and password. If this data is correct, then the corresponding element will appear in the Secrete Service.

It actually stores the token.

When you restart the application, it will not ask for details, but will download the token from the Secret Service.

Links




All Articles