Skip to content

Commit

Permalink
Feature: Implement JWT authentication in REST API (#1785)
Browse files Browse the repository at this point in the history
* Added jwt auth

* Added get and delete request, also added jwt functions

* Added proper naming conventions and status codes

* Added hashmap for storing key-value pair of jwt tokens

* Updated nomenclature and added descriptions to the Resources

* Implemented nested hashmap of jwt tokens

Signed-off-by: ankur12-1610 <[email protected]>

* Added jwt token verification which can be used inplace of basic password verification

* Implemented token expiration check

* Added time parser for converting different units to seconds

* Changed parsing method

* Adding seperate token resource instead of secure resource

* Fixed test-rest.py and added token auth to AdminResource

Signed-off-by: ankur12-1610 <[email protected]>

* Added new tests for jwt endpoints

Signed-off-by: ankur12-1610 <[email protected]>

* Added documentation for jwt_endpoints

* Added curl and python usage

* reduce differences to master for requirements.txt

* refined API Documentation

* typo

* typos

* fix test

Signed-off-by: ankur12-1610 <[email protected]>
Co-authored-by: Kim Neunert <[email protected]>
  • Loading branch information
ankur12-1610 and k9ert authored Oct 20, 2022
1 parent 6fbe4b1 commit 793a8c5
Show file tree
Hide file tree
Showing 15 changed files with 698 additions and 43 deletions.
83 changes: 77 additions & 6 deletions docs/api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,91 @@ Specter provides a Rest-API which is, by default, in production deactivated. In
export SPECTER_API_ACTIVE=True
```

The Authentication is also necessary if you don't activate any Authentication mechanism.
The Authentication is also necessary even if you don't activate any Authentication mechanism.
In order to make reasonable assumptions about how stable a specific endpoint is, we're versioning them via the URL. Currently, all endpoints are preset with `v1alpha` which pretty much don't give you any guarantee.
## Basic Usage

Curl:
The Specter API is using JWT tokens for Authentication. In order to use the API, you need to obtain such a token. Currently, obtaining a token is not possible via the UI but only via a special endpoint, which accepts BasicAuth (as the only endpoint).

## Curl:

Create the token like this:
```bash
curl -u admin:password --location --request POST 'http://127.0.0.1:25441/api/v1alpha/token' \
--header 'Content-Type: application/json' \
-d '{
"jwt_token_description": "A free description here to know for what the token is used",
"jwt_token_life": "30 days"
}'
```
As a result, you get a json like this:
```json
{
"message": "Token generated",
"jwt_token_id": "4969e9fb-2097-41e7-af53-5e2082a3e4d3",
"jwt_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiand0X3Rva2VuX2lkIjoiNDk2OWU5ZmItMjA5Ny00MWU3LWFmNTMtNWUyMDgyYTNlNGQzIiwiand0X3Rva2VuX2Rlc2NyaXB0aW9uIjoiQSBmcmVlIGRlc2NyaXB0aW9uIGhlcmUgdG8ga25vdyBmb3Igd2hhdCB0aGUgdG9rZW4gaXMgdXNlZCIsImV4cCI6MTY5NjU4NDQ0MiwiaWF0IjoxNjY1MDQ4NDQyfQ.S2NIQknkNqoe-u0xA-W8ZxxkDM-I5B8eDCUwLrG-98E",
"jwt_token_description": "A free description here to know for what the token is used",
"jwt_token_life": 31536000
}
```

The token will only be shown once. However, apart from the token itself, you can still get the details of a specific token like this:

```bash
curl -s -u admin:secret --location --request GET 'http://127.0.0.1:25441/api/v1alpha/token/4969e9fb-2097-41e7-af53-5e2082a3e4d3' | jq .
```

```json
{
"message": "Token exists",
"jwt_token_description": "A free description here to know for what the token is used",
"jwt_token_life": 2592000,
"jwt_token_life_remaining": 2591960.19173622,
"expiry_status": "Valid"
}
```

The `jwt_token_life` value and the other one are expressed in seconds.

In order to use that token, you can e.g. call the specter-endpoint like this:
```bash
curl -u admin:secret -X GET http://127.0.0.1:25441/api/v1alpha/specter | jq .
curl -s --location --request GET 'http://127.0.0.1:25441/api/v1alpha/specter' \
--header 'Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiand0X3Rva2VuX2lkIjoiNDk2OWU5ZmItMjA5Ny00MWU3LWFmNTMtNWUyMDgyYTNlNGQzIiwiand0X3Rva2VuX2Rlc2NyaXB0aW9uIjoiQSBmcmVlIGRlc2NyaXB0aW9uIGhlcmUgdG8ga25vdyBmb3Igd2hhdCB0aGUgdG9rZW4gaXMgdXNlZCIsImV4cCI6MTY5NjU4NDQ0MiwiaWF0IjoxNjY1MDQ4NDQyfQ.S2NIQknkNqoe-u0xA-W8ZxxkDM-I5B8eDCUwLrG-98E' | jq .
```
The result would be something like this:

```json
{
"data_folder": "/home/someuser/.specter",
"config": {
"auth": {
"method": "usernamepassword",
"password_min_chars": 6,
"rate_limit": "10",
"registration_link_timeout": "1"
},
[...]
"wallets_names": [],
"last_update": "10/06/2022, 11:35:21",
"alias_name": {},
"name_alias": {},
"wallets_alias": []
}
```
## Python

Here is an example of using the API with python. We don't assume that you use BasicAuth via python. Instead of an example of a real token, we use `<token>` and `<token_id>`.

```python
import requests
response = requests.get('http://127.0.0.1:25441/api/v1alpha/token/<token_id>', auth=('admin', 'secret'))
json.loads(response.text)
```

Python:

```python
### Pass the token to get authorized
import requests
response = requests.get('http://127.0.0.1:25441/api/v1alpha/specter', auth=('admin', 'secret'))
response = requests.get('http://127.0.0.1:25441/api/v1alpha/specter', headers={'Authorization': 'Bearer <token>'})
json.loads(response.text)
```

Expand All @@ -31,6 +101,7 @@ json.loads(response.text)
* [Specter Full Tx List](./ep_specter_fulltxlist.md): Gives a full tx_list of all transactions.
* [Wallet](./ep_wallets_wallet.md): Details about a specific Wallet
* [Wallet PSBT](./ep_wallets_psbt.md): Listing and creating PSBTs
* [JWT Tokens](./ep_jwt_tokens.md): Listing, creating and managing JWT Tokens ]



135 changes: 135 additions & 0 deletions docs/api/ep_jwt_tokens.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
# Token Endpoint

Creates a new token for the user and Gives all the tokens created by the user.

**URL** : `/v1alpha/token`

## GET

**Method** : `GET`

**Auth required** : Yes

**Permissions required** : None

### Success Response

**Code** : `200 OK`

**Content examples**

### Get Result

```json
{
"message": "Tokens exists",
"jwt_tokens": {
"94f10f9b-2139-4f31-ab57-52ac175b9acc": {
"jwt_token_description": "Token beta",
"jwt_token_life": 5400,
"jwt_token_remaining_life": 5395.147431612015
},
"2bc0160d-edf4-4ab6-9801-52d185f65b59": {
"jwt_token_description": "Token alpha",
"jwt_token_life": 360,
"jwt_token_remaining_life": 232.19542360305786
}
}
}
```

## POST

**Method** : `POST`

**Auth required** : YES

**Permissions required** : None

```
curl -u admin:password --location --request POST 'http://127.0.0.1:25441/api/v1alpha/token' \
--header 'Content-Type: application/json' \
-d '{
"jwt_token_description": "Token specter",
"jwt_token_life": "6 hours"
}'
```

As a result, you get all the created tokens.

### Success Response

**Code** : `201 Created`

**Content examples**

### Post Result

```json
{
"message": "Token generated",
"jwt_token_id": "b56929f3-54f1-4dc2-9984-9bba615e26e6",
"jwt_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiand0X3Rva2VuX2lkIjoiYjU2OTI5ZjMtNTRmMS00ZGMyLTk5ODQtOWJiYTYxNWUyNmU2Iiwiand0X3Rva2VuX2Rlc2NyaXB0aW9uIjoiVG9rZW4gc3BlY3RlciIsImV4cCI6MTY2Mjc0MTczNSwiaWF0IjoxNjYyNzIwMTM1fQ.gBE7S4lJfpPQctt2Dk_581-6v1YOzn4UPHYO18LZpF8",
"jwt_token_description": "Token specter",
"jwt_token_life": 21600
}
```

Gives the token details of which the id is passed in the URL and deletes the same token.

**URL** : `/v1alpha/token/<jwt_token_id>`

## GET

**Method** : `GET`

**Auth required** : Yes

**Permissions required** : None

```
curl -u admin:secret --location --request GET 'http://127.0.0.1:25441/api/v1alpha/token/<jwt_token_id>' | jq .
```

### Success Response

**Code** : `200 OK`

**Content examples**

### Get Result

```json
{
"message": "Tokens exists",
"jwt_token_description": "Token alpha",
"jwt_token_life": 360,
"jwt_token_life_remaining": 232.19542360305786,
"expiry_status": "Valid"
}
```
## DELETE

**Method** : `DELETE`

**Auth required** : Yes

**Permissions required** : None

```
curl -u admin:secret --location --request DELETE 'http://127.0.0.1:25441/api/v1alpha/token/<jwt_token_id>' | jq .
```

### Success Response

**Code** : `200 OK`

**Content examples**

### Delete Result

```json
{
"message": "Token deleted"
}
```
2 changes: 1 addition & 1 deletion docs/release-notes.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Release Notes

## v1.13.1 Oktober 17, 2022
## v1.13.1 October 17, 2022
- Bugfix: Hover effect in balance display #1904 (Manolis Mandrapilias)
- Bugfix: Remove black empty bar in tx-table after search #1912 (relativisticelectron)
- Bugfix: upgrade hwi to 2.1.1 to fix #1840 #1909 (k9ert)
Expand Down
2 changes: 2 additions & 0 deletions requirements.in
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ Flask-APScheduler==1.12.3
backports.zoneinfo==0.2.1 ; python_version < '3.10'
gunicorn==20.1.0
protobuf==3.20.1
PyJWT==2.4.0
pytimeparse==1.1.8
# Extensions
cryptoadvance-liquidissuer==0.2.4
specterext-exfund==0.1.7
Expand Down
8 changes: 8 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,10 @@ pycparser==2.21 \
--hash=sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9 \
--hash=sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206
# via cffi
pyjwt==2.4.0 \
--hash=sha256:72d1d253f32dbd4f5c88eaf1fdc62f3a19f676ccbadb9dbc5d07e951b2b26daf \
--hash=sha256:d42908208c699b3b973cbeb01a969ba6a96c821eefb1c5bfe4c390c01d67abba
# via -r requirements.in
pyopenssl==20.0.1 \
--hash=sha256:4c231c759543ba02560fcd2480c48dcec4dae34c9da7d3747c508227e0624b51 \
--hash=sha256:818ae18e06922c066f777a33f1fca45786d85edfe71cd043de6379337a7f274b
Expand All @@ -431,6 +435,10 @@ python-dotenv==0.13.0 \
--hash=sha256:25c0ff1a3e12f4bde8d592cc254ab075cfe734fc5dd989036716fd17ee7e5ec7 \
--hash=sha256:3b9909bc96b0edc6b01586e1eed05e71174ef4e04c71da5786370cebea53ad74
# via -r requirements.in
pytimeparse==1.1.8 \
--hash=sha256:04b7be6cc8bd9f5647a6325444926c3ac34ee6bc7e69da4367ba282f076036bd \
--hash=sha256:e86136477be924d7e670646a98561957e8ca7308d44841e21f5ddea757556a0a
# via -r requirements.in
pytz-deprecation-shim==0.1.0.post0 \
--hash=sha256:8314c9692a636c8eb3bda879b9f119e350e93223ae83e70e80c31675a0fdc1a6 \
--hash=sha256:af097bae1b616dde5c5744441e2ddc69e74dfdcb0c263129610d85b87445a59d
Expand Down
4 changes: 3 additions & 1 deletion src/cryptoadvance/specter/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import os
from flask import Flask, Blueprint, session
from flask_restful import Api
from flask_httpauth import HTTPBasicAuth
from flask_httpauth import HTTPBasicAuth, HTTPTokenAuth
from flask import current_app as app

api_bp = Blueprint("api_bp", __name__, template_folder="templates", url_prefix="/api")
Expand All @@ -12,5 +12,7 @@

auth = HTTPBasicAuth()

token_auth = HTTPTokenAuth()

from . import views
from .rest import api
5 changes: 3 additions & 2 deletions src/cryptoadvance/specter/api/rest/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@

from ...util.fee_estimation import get_fees

from .. import auth
from .. import token_auth
from .resource_jwt import JWTResource, JWTResourceById
from .resource_healthz import ResourceLiveness, ResourceReadyness
from .resource_psbt import ResourcePsbt
from .resource_specter import ResourceSpecter
Expand All @@ -30,7 +31,7 @@ class ResourceWallet(SecureResource):
endpoints = ["/v1alpha/wallets/<wallet_alias>/"]

def get(self, wallet_alias):
user = auth.current_user()
user = token_auth.current_user()
try:
wallet: Wallet = app.specter.user_manager.get_user(
user
Expand Down
10 changes: 8 additions & 2 deletions src/cryptoadvance/specter/api/rest/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

from cryptoadvance.specter.api import api_rest
from cryptoadvance.specter.api.security import require_admin
from cryptoadvance.specter.api import auth
from cryptoadvance.specter.api import auth, token_auth
from cryptoadvance.specter.specter_error import SpecterError

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -72,13 +72,19 @@ def delete(self, *args, **kwargs):
class SecureResource(BaseResource):
"""A REST-resource which makes sure that the user is Authenticated"""

method_decorators = [error_handling, token_auth.login_required]


class BasicAuthResource(BaseResource):
"""A REST-resource which makes sure that the user is Authenticated"""

method_decorators = [error_handling, auth.login_required]


class AdminResource(BaseResource):
"""A REST-resource which makes sure that the user is an admin"""

method_decorators = [error_handling, require_admin, auth.login_required]
method_decorators = [error_handling, require_admin, token_auth.login_required]


def rest_resource(resource_cls):
Expand Down
Loading

0 comments on commit 793a8c5

Please sign in to comment.