Access tokens for Specter's REST API: Part 1 | Summer of Bitcoin '22

Access tokens for Specter's REST API: Part 1 | Summer of Bitcoin '22

This blog covers the progress of my Summer of Bitcoin project - Access tokens for the Specter's REST API

ยท

4 min read

Abstract

Specter Desktop is a desktop GUI for Bitcoin Core optimized to work with hardware wallets.

specter.png

Specter already has a REST API system, but the authorization is currently done by HTTPBasicAuth and to improve the security, HTTPTokenAuth is really necessary. Access tokens would be a significant improvement.

My Project

367ei5.jpg

I have to use access tokens for the token-based authorization, the access token I opted for was JSON Web Token (JWT) because:

  • I have already used it

  • I read their official documentation and got a fan of JWT and its advantages over Simple WebToken (SWT) and Security Assertion Markup Language Tokens (SAML). source

JWT authentication flow

Expected Outcomes:

  • User will be able to create a JWT token when the request is sent to a particular endpoint of the api

  • Once the token is generated the user can only see it once and the token needs to be stored somewhere in order to access sessions later on.

  • Authorization will be based on HTTPTokenAuth

Project Progress

Headstart ๐Ÿš€

At first, when I went through Specter's codebase it was difficult for me to understand and I was not able to figure out where to make the changes, but thanks to my mentor (k9ert) who helped me sort things out when I was stuck. He suggested making a small FLASK API in a similar structured way as Specter's API was made. This really helped me understand Flask-RESTful and Flask_HTTPAuth.

PoC Implementation ๐Ÿ“–

Step 1

The very first step was to install PyJWT:

pip install PyJWT

Next, we need to create a 'jwt_token' variable in the UserMixinof src/cryptoadvance/specter/user.py, then we pass it in the user_dict with the help of a property.

class User(UserMixin):

     def __init__(
        ...
        jwt_token, 
        ...
    ):
        ...
        self.jwt_token = jwt_token
        ...
    # TODO: User obj instantiation belongs in UserManager
    @classmethod
    def from_json(cls, user_dict, specter):
        try:
            user_args = {
                ...
                // Setting default value of jwt_token to None so that it can be stored in the user's data and then later on updated
                "jwt_token": user_dict.get("jwt_token", None),
                ...
            }
            if not user_dict["is_admin"]:
                user_args["config"] = user_dict["config"]
                return cls(**user_args)
            else:
                user_args["is_admin"] = True
                return cls(**user_args)

        except Exception as e:
            handle_exception(e)
            raise SpecterError(f"Unable to parse user JSON.:{e}")
    @property
    def json(self):
        user_dict = {
            ...
            "jwt_token": self.jwt_token,
            ...
        }
        if not self.is_admin:
            user_dict["config"] = self.config
        return user_dict

We also need to add helper functions in order to fetch and delete the token:

    def save_jwt_token(self, jwt_token):
        self.jwt_token = jwt_token
        self.save_info()

    def delete_jwt_token(
        self,
    ):
        self.jwt_token = None
        self.save_info()

Step 2

This step includes the creation of token based endpoints in the API. For this I created a new file namely jwt.py in the rest directory.

Directory tree

import jwt
from flask import current_app as app
from cryptoadvance.specter.api.rest.base import (
    BaseResource,
    rest_resource,
    AdminResource,
)
import uuid
import datetime
import logging
from ...user import *
from .base import *

from .. import auth

logger = logging.getLogger(__name__)


def generate_jwt(user):
    payload = {
        "user": user.username,
        "exp": datetime.datetime.utcnow() + datetime.timedelta(days=1),
    }
    return jwt.encode(payload, app.config["SECRET_KEY"], algorithm="HS256")


@rest_resource
class TokenResource(AdminResource):
    endpoints = ["/v1alpha/token/"]

    def get(self):
        user = auth.current_user()
        user_details = app.specter.user_manager.get_user(user)
        jwt_token = user_details.jwt_token
        return_dict = {
            "username": user_details.username,
            "id": user_details.id,
            "jwt_token": jwt_token,
        }
        if jwt_token is None:
            return_dict["jwt_token"] = generate_jwt(user_details)
            jwt_token = return_dict["jwt_token"]
            user_details.save_jwt_token(jwt_token)
            return {
                "message": "Token generated",
                "username": user_details.username,
                "jwt_token": jwt_token,
            }
        return {"message": "Token already exists", "jwt_token": jwt_token}

    def delete(self):
        user = auth.current_user()
        user_details = app.specter.user_manager.get_user(user)
        jwt_token = user_details.jwt_token
        if jwt_token is None:
            return {"message": "Token does not exist"}
        user_details.delete_jwt_token()
        return {"message": "Token deleted"}

This basically includes the function generate_jwt which takes user as an argument and generates the token with the payload as user.username and the expiry date of the token (exp).

Then we have two endpoints - GET and DELETE which receive data from the User model using user_manager by passing the authenticated user.

GET request functionality

DELETE request functionality

Last Step

The last step is to register the endpoints in the API, this can be done by adding:

from .jwt import TokenResource

in src/cryptoadvance/specter/api/rest/api.py

PR related to this project github.com/cryptoadvance/specter-desktop/pu..


Demo of the implemented PoC:

Future milestones

The plans for the rest of my journey are:

  • Adding a verfiy_token function that verifies if the given token is correct or not.

  • Replacing HTTPBasicAuth with HTTPTokenAuth

  • Add one-time view functionality for the users.

Conclusion

Thank you for reading, hope you enjoyed it! I'll continue to update my progress via the series of blogs ;)

Follow me on Twitter | LinkedIn for more web development-related tips and posts.

That's all for today! You have read the article till the end.

Did you find this article valuable?

Support Ankur Patil by becoming a sponsor. Any amount is appreciated!

ย