Skip to content

Commit 793a8c5

Browse files
ankur12-1610Kim Neunert
andauthored
Feature: Implement JWT authentication in REST API (#1785)
* 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]>
1 parent 6fbe4b1 commit 793a8c5

File tree

15 files changed

+698
-43
lines changed

15 files changed

+698
-43
lines changed

docs/api/README.md

Lines changed: 77 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,91 @@ Specter provides a Rest-API which is, by default, in production deactivated. In
55
export SPECTER_API_ACTIVE=True
66
```
77

8-
The Authentication is also necessary if you don't activate any Authentication mechanism.
8+
The Authentication is also necessary even if you don't activate any Authentication mechanism.
99
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.
10-
## Basic Usage
1110

12-
Curl:
11+
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).
1312

13+
## Curl:
14+
15+
Create the token like this:
16+
```bash
17+
curl -u admin:password --location --request POST 'http://127.0.0.1:25441/api/v1alpha/token' \
18+
--header 'Content-Type: application/json' \
19+
-d '{
20+
"jwt_token_description": "A free description here to know for what the token is used",
21+
"jwt_token_life": "30 days"
22+
}'
23+
```
24+
As a result, you get a json like this:
25+
```json
26+
{
27+
"message": "Token generated",
28+
"jwt_token_id": "4969e9fb-2097-41e7-af53-5e2082a3e4d3",
29+
"jwt_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiand0X3Rva2VuX2lkIjoiNDk2OWU5ZmItMjA5Ny00MWU3LWFmNTMtNWUyMDgyYTNlNGQzIiwiand0X3Rva2VuX2Rlc2NyaXB0aW9uIjoiQSBmcmVlIGRlc2NyaXB0aW9uIGhlcmUgdG8ga25vdyBmb3Igd2hhdCB0aGUgdG9rZW4gaXMgdXNlZCIsImV4cCI6MTY5NjU4NDQ0MiwiaWF0IjoxNjY1MDQ4NDQyfQ.S2NIQknkNqoe-u0xA-W8ZxxkDM-I5B8eDCUwLrG-98E",
30+
"jwt_token_description": "A free description here to know for what the token is used",
31+
"jwt_token_life": 31536000
32+
}
33+
```
34+
35+
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:
36+
37+
```bash
38+
curl -s -u admin:secret --location --request GET 'http://127.0.0.1:25441/api/v1alpha/token/4969e9fb-2097-41e7-af53-5e2082a3e4d3' | jq .
39+
```
40+
41+
```json
42+
{
43+
"message": "Token exists",
44+
"jwt_token_description": "A free description here to know for what the token is used",
45+
"jwt_token_life": 2592000,
46+
"jwt_token_life_remaining": 2591960.19173622,
47+
"expiry_status": "Valid"
48+
}
49+
```
50+
51+
The `jwt_token_life` value and the other one are expressed in seconds.
52+
53+
In order to use that token, you can e.g. call the specter-endpoint like this:
1454
```bash
15-
curl -u admin:secret -X GET http://127.0.0.1:25441/api/v1alpha/specter | jq .
55+
curl -s --location --request GET 'http://127.0.0.1:25441/api/v1alpha/specter' \
56+
--header 'Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiand0X3Rva2VuX2lkIjoiNDk2OWU5ZmItMjA5Ny00MWU3LWFmNTMtNWUyMDgyYTNlNGQzIiwiand0X3Rva2VuX2Rlc2NyaXB0aW9uIjoiQSBmcmVlIGRlc2NyaXB0aW9uIGhlcmUgdG8ga25vdyBmb3Igd2hhdCB0aGUgdG9rZW4gaXMgdXNlZCIsImV4cCI6MTY5NjU4NDQ0MiwiaWF0IjoxNjY1MDQ4NDQyfQ.S2NIQknkNqoe-u0xA-W8ZxxkDM-I5B8eDCUwLrG-98E' | jq .
57+
```
58+
The result would be something like this:
59+
60+
```json
61+
{
62+
"data_folder": "/home/someuser/.specter",
63+
"config": {
64+
"auth": {
65+
"method": "usernamepassword",
66+
"password_min_chars": 6,
67+
"rate_limit": "10",
68+
"registration_link_timeout": "1"
69+
},
70+
[...]
71+
"wallets_names": [],
72+
"last_update": "10/06/2022, 11:35:21",
73+
"alias_name": {},
74+
"name_alias": {},
75+
"wallets_alias": []
76+
}
77+
```
78+
## Python
79+
80+
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>`.
81+
82+
```python
83+
import requests
84+
response = requests.get('http://127.0.0.1:25441/api/v1alpha/token/<token_id>', auth=('admin', 'secret'))
85+
json.loads(response.text)
1686
```
1787

18-
Python:
1988

2089
```python
90+
### Pass the token to get authorized
2191
import requests
22-
response = requests.get('http://127.0.0.1:25441/api/v1alpha/specter', auth=('admin', 'secret'))
92+
response = requests.get('http://127.0.0.1:25441/api/v1alpha/specter', headers={'Authorization': 'Bearer <token>'})
2393
json.loads(response.text)
2494
```
2595

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

35106

36107

docs/api/ep_jwt_tokens.md

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
# Token Endpoint
2+
3+
Creates a new token for the user and Gives all the tokens created by the user.
4+
5+
**URL** : `/v1alpha/token`
6+
7+
## GET
8+
9+
**Method** : `GET`
10+
11+
**Auth required** : Yes
12+
13+
**Permissions required** : None
14+
15+
### Success Response
16+
17+
**Code** : `200 OK`
18+
19+
**Content examples**
20+
21+
### Get Result
22+
23+
```json
24+
{
25+
"message": "Tokens exists",
26+
"jwt_tokens": {
27+
"94f10f9b-2139-4f31-ab57-52ac175b9acc": {
28+
"jwt_token_description": "Token beta",
29+
"jwt_token_life": 5400,
30+
"jwt_token_remaining_life": 5395.147431612015
31+
},
32+
"2bc0160d-edf4-4ab6-9801-52d185f65b59": {
33+
"jwt_token_description": "Token alpha",
34+
"jwt_token_life": 360,
35+
"jwt_token_remaining_life": 232.19542360305786
36+
}
37+
}
38+
}
39+
```
40+
41+
## POST
42+
43+
**Method** : `POST`
44+
45+
**Auth required** : YES
46+
47+
**Permissions required** : None
48+
49+
```
50+
curl -u admin:password --location --request POST 'http://127.0.0.1:25441/api/v1alpha/token' \
51+
--header 'Content-Type: application/json' \
52+
-d '{
53+
"jwt_token_description": "Token specter",
54+
"jwt_token_life": "6 hours"
55+
}'
56+
```
57+
58+
As a result, you get all the created tokens.
59+
60+
### Success Response
61+
62+
**Code** : `201 Created`
63+
64+
**Content examples**
65+
66+
### Post Result
67+
68+
```json
69+
{
70+
"message": "Token generated",
71+
"jwt_token_id": "b56929f3-54f1-4dc2-9984-9bba615e26e6",
72+
"jwt_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiand0X3Rva2VuX2lkIjoiYjU2OTI5ZjMtNTRmMS00ZGMyLTk5ODQtOWJiYTYxNWUyNmU2Iiwiand0X3Rva2VuX2Rlc2NyaXB0aW9uIjoiVG9rZW4gc3BlY3RlciIsImV4cCI6MTY2Mjc0MTczNSwiaWF0IjoxNjYyNzIwMTM1fQ.gBE7S4lJfpPQctt2Dk_581-6v1YOzn4UPHYO18LZpF8",
73+
"jwt_token_description": "Token specter",
74+
"jwt_token_life": 21600
75+
}
76+
```
77+
78+
Gives the token details of which the id is passed in the URL and deletes the same token.
79+
80+
**URL** : `/v1alpha/token/<jwt_token_id>`
81+
82+
## GET
83+
84+
**Method** : `GET`
85+
86+
**Auth required** : Yes
87+
88+
**Permissions required** : None
89+
90+
```
91+
curl -u admin:secret --location --request GET 'http://127.0.0.1:25441/api/v1alpha/token/<jwt_token_id>' | jq .
92+
```
93+
94+
### Success Response
95+
96+
**Code** : `200 OK`
97+
98+
**Content examples**
99+
100+
### Get Result
101+
102+
```json
103+
{
104+
"message": "Tokens exists",
105+
"jwt_token_description": "Token alpha",
106+
"jwt_token_life": 360,
107+
"jwt_token_life_remaining": 232.19542360305786,
108+
"expiry_status": "Valid"
109+
}
110+
```
111+
## DELETE
112+
113+
**Method** : `DELETE`
114+
115+
**Auth required** : Yes
116+
117+
**Permissions required** : None
118+
119+
```
120+
curl -u admin:secret --location --request DELETE 'http://127.0.0.1:25441/api/v1alpha/token/<jwt_token_id>' | jq .
121+
```
122+
123+
### Success Response
124+
125+
**Code** : `200 OK`
126+
127+
**Content examples**
128+
129+
### Delete Result
130+
131+
```json
132+
{
133+
"message": "Token deleted"
134+
}
135+
```

docs/release-notes.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Release Notes
22

3-
## v1.13.1 Oktober 17, 2022
3+
## v1.13.1 October 17, 2022
44
- Bugfix: Hover effect in balance display #1904 (Manolis Mandrapilias)
55
- Bugfix: Remove black empty bar in tx-table after search #1912 (relativisticelectron)
66
- Bugfix: upgrade hwi to 2.1.1 to fix #1840 #1909 (k9ert)

requirements.in

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ Flask-APScheduler==1.12.3
2727
backports.zoneinfo==0.2.1 ; python_version < '3.10'
2828
gunicorn==20.1.0
2929
protobuf==3.20.1
30+
PyJWT==2.4.0
31+
pytimeparse==1.1.8
3032
# Extensions
3133
cryptoadvance-liquidissuer==0.2.4
3234
specterext-exfund==0.1.7

requirements.txt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -410,6 +410,10 @@ pycparser==2.21 \
410410
--hash=sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9 \
411411
--hash=sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206
412412
# via cffi
413+
pyjwt==2.4.0 \
414+
--hash=sha256:72d1d253f32dbd4f5c88eaf1fdc62f3a19f676ccbadb9dbc5d07e951b2b26daf \
415+
--hash=sha256:d42908208c699b3b973cbeb01a969ba6a96c821eefb1c5bfe4c390c01d67abba
416+
# via -r requirements.in
413417
pyopenssl==20.0.1 \
414418
--hash=sha256:4c231c759543ba02560fcd2480c48dcec4dae34c9da7d3747c508227e0624b51 \
415419
--hash=sha256:818ae18e06922c066f777a33f1fca45786d85edfe71cd043de6379337a7f274b
@@ -431,6 +435,10 @@ python-dotenv==0.13.0 \
431435
--hash=sha256:25c0ff1a3e12f4bde8d592cc254ab075cfe734fc5dd989036716fd17ee7e5ec7 \
432436
--hash=sha256:3b9909bc96b0edc6b01586e1eed05e71174ef4e04c71da5786370cebea53ad74
433437
# via -r requirements.in
438+
pytimeparse==1.1.8 \
439+
--hash=sha256:04b7be6cc8bd9f5647a6325444926c3ac34ee6bc7e69da4367ba282f076036bd \
440+
--hash=sha256:e86136477be924d7e670646a98561957e8ca7308d44841e21f5ddea757556a0a
441+
# via -r requirements.in
434442
pytz-deprecation-shim==0.1.0.post0 \
435443
--hash=sha256:8314c9692a636c8eb3bda879b9f119e350e93223ae83e70e80c31675a0fdc1a6 \
436444
--hash=sha256:af097bae1b616dde5c5744441e2ddc69e74dfdcb0c263129610d85b87445a59d

src/cryptoadvance/specter/api/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import os
44
from flask import Flask, Blueprint, session
55
from flask_restful import Api
6-
from flask_httpauth import HTTPBasicAuth
6+
from flask_httpauth import HTTPBasicAuth, HTTPTokenAuth
77
from flask import current_app as app
88

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

1313
auth = HTTPBasicAuth()
1414

15+
token_auth = HTTPTokenAuth()
16+
1517
from . import views
1618
from .rest import api

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@
1515

1616
from ...util.fee_estimation import get_fees
1717

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

3233
def get(self, wallet_alias):
33-
user = auth.current_user()
34+
user = token_auth.current_user()
3435
try:
3536
wallet: Wallet = app.specter.user_manager.get_user(
3637
user

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

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
from cryptoadvance.specter.api import api_rest
99
from cryptoadvance.specter.api.security import require_admin
10-
from cryptoadvance.specter.api import auth
10+
from cryptoadvance.specter.api import auth, token_auth
1111
from cryptoadvance.specter.specter_error import SpecterError
1212

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

75+
method_decorators = [error_handling, token_auth.login_required]
76+
77+
78+
class BasicAuthResource(BaseResource):
79+
"""A REST-resource which makes sure that the user is Authenticated"""
80+
7581
method_decorators = [error_handling, auth.login_required]
7682

7783

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

81-
method_decorators = [error_handling, require_admin, auth.login_required]
87+
method_decorators = [error_handling, require_admin, token_auth.login_required]
8288

8389

8490
def rest_resource(resource_cls):

0 commit comments

Comments
 (0)