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
Abstract
Specter Desktop is a desktop GUI for Bitcoin Core optimized to work with hardware wallets.
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
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 UserMixin
of 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
withHTTPTokenAuth
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.