Skip to content

Commit 4a3d7cd

Browse files
committed
add API authentication via Biscuit tokens
1 parent f56c9dd commit 4a3d7cd

File tree

13 files changed

+1155
-51
lines changed

13 files changed

+1155
-51
lines changed

Cargo.lock

Lines changed: 405 additions & 36 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ axum-extra = "0.9.4"
1515
# axum-macros = "0.4.2" # uncomment to use debug_handler
1616
baid58 = "0.4.4"
1717
base64 = "0.22.1"
18+
biscuit-auth = "6.0.0"
1819
bitcoin = "0.32"
1920
bitcoin-bech32 = "0.13"
2021
chacha20poly1305 = { version = "0.10.1", features = ["stream"] }

README.md

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,7 @@ The node currently exposes the following APIs:
197197
- `/postassetmedia` (POST)
198198
- `/refreshtransfers` (POST)
199199
- `/restore` (POST)
200+
- `/revoketoken` (POST)
200201
- `/rgbinvoice` (POST)
201202
- `/sendasset` (POST)
202203
- `/sendbtc` (POST)
@@ -223,6 +224,95 @@ given above, you can even call the APIs directly from the Swagger UI.
223224

224225
To stop the daemon, exit with the `/shutdown` API (or press `Ctrl+C`).
225226

227+
### Authentication
228+
229+
RLN provides API authentication via [Biscuit tokens].
230+
231+
#### One-time setup
232+
233+
First, generate a root keypair. This keypair is your issuer key: the private
234+
half signs new tokens, and the public half is shared with your node so it can
235+
verify them.
236+
237+
```sh
238+
# install the biscuit CLI (or download a prebuilt binary from the Biscuit releases page
239+
cargo install biscuit-cli
240+
241+
# generate a root keypair (prints both keys)
242+
biscuit keypair
243+
244+
# alternatively, you can export just the private key
245+
biscuit keypair --only-private-key > private-key-file
246+
# and later derive the public key from it
247+
biscuit keypair --from-private-key-file private-key-file --only-public-key
248+
```
249+
250+
Save the private key in a secure way (e.g. in a secret manager).
251+
252+
When starting the node, pass the public key with:
253+
```sh
254+
--root-public-key <public_key>
255+
```
256+
To **disable** authentication provide the explicit `--disable-authentication`
257+
arg and do not provide any key.
258+
259+
#### Minting tokens
260+
261+
You can now create Biscuit tokens that will allow calling the authenticated
262+
APIs.
263+
264+
Tokens must carry a **role**, these are the available roles:
265+
266+
- **admin** token (full access):
267+
```sh
268+
echo 'role("admin");' \
269+
| biscuit generate --private-key-file private-key-file -
270+
```
271+
272+
- **read-only** token (allows access only to endpoints that do not make any
273+
write operations):
274+
```sh
275+
echo 'role("read-only");' \
276+
| biscuit generate --private-key-file private-key-file -
277+
```
278+
- **custom** token (allows access only to the specified API paths), for
279+
example:
280+
```sh
281+
echo 'role("custom");
282+
right("api", "/nodeinfo");
283+
right("api", "/networkinfo");' \
284+
| biscuit generate --private-key-file private-key-file -
285+
```
286+
287+
Tokens can also carry an **expiry** date. Add a `check` clause to enforce
288+
expiration, for example:
289+
```sh
290+
echo 'role("admin");
291+
check if time($t), $t <= 2025-08-30T00:00:00Z;' \
292+
| biscuit generate --private-key-file private-key-file -
293+
```
294+
295+
#### Using tokens
296+
297+
All authenticated requests must include the Biscuit token in the
298+
`Authorization` header:
299+
300+
```sh
301+
curl -H "Authorization: Bearer <token>" [...] http://<node-address>/networkinfo
302+
```
303+
304+
In the Swagger UI you can add the token by clicking the Authorize button (lock
305+
icon) at the top right, pasting the token and clicking Authorize.
306+
307+
#### Revoking tokens
308+
309+
A token can be revoked before its expiration.
310+
When you revoke a token, the node will reject any future request carrying that
311+
token.
312+
The node exposes a `/revoketoken` endpoint for this purpose.
313+
Internally, the node extracts the token’s revocation identifiers and adds them
314+
to its revocation list. Every request checks this list before authenticating.
315+
226316
## Test
227317

228318
Tests for a few scenarios using the regtest network are included. The same
@@ -244,6 +334,7 @@ Here is a list of projects using RLN, in alphabetical order:
244334
- [Thunderstack]
245335

246336

337+
[Biscuit tokens]: https://www.biscuitsec.org/
247338
[RGB proxy server]: https://github.com/RGB-Tools/rgb-proxy-server
248339
[ldk-sample]: https://github.com/lightningdevkit/ldk-sample
249340
[OpenAPI specification]: /openapi.yaml

openapi.yaml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -784,6 +784,24 @@ paths:
784784
application/json:
785785
schema:
786786
$ref: '#/components/schemas/EmptyResponse'
787+
/revoketoken:
788+
post:
789+
tags:
790+
- Other
791+
summary: Revoke a token
792+
description: Revoke an authentication token
793+
requestBody:
794+
content:
795+
application/json:
796+
schema:
797+
$ref: '#/components/schemas/RevokeTokenRequest'
798+
responses:
799+
'200':
800+
description: Successful operation
801+
content:
802+
application/json:
803+
schema:
804+
$ref: '#/components/schemas/EmptyResponse'
787805
/rgbinvoice:
788806
post:
789807
tags:
@@ -1994,6 +2012,12 @@ components:
19942012
password:
19952013
type: string
19962014
example: nodepassword
2015+
RevokeTokenRequest:
2016+
type: object
2017+
properties:
2018+
token:
2019+
type: string
2020+
example: EnYKDBgDIggKBggGEgIYDRIkCAASICqCgqtFMIJ1eLCM3raDzqg9UqV-6nJWzGjjJG0S5IIUGkBpF-itmppHcdcSrSCiKklz9VZT4UmIND_0RFc32Imq3bLR_Y7GYaSpJo5lJfU1cA2BG_hy7P1UN4g5jKTKS88GIiIKIAUKXrrx0Ca-rMZa537VOFw2X8q_KVQ6OC4Z0ztro0sQ
19972021
RgbAllocation:
19982022
type: object
19992023
properties:
@@ -2387,3 +2411,10 @@ components:
23872411
colorable:
23882412
type: boolean
23892413
example: true
2414+
securitySchemes:
2415+
bearerAuth:
2416+
type: http
2417+
scheme: bearer
2418+
bearerFormat: Biscuit
2419+
security:
2420+
- bearerAuth: []

src/args.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use clap::{value_parser, Parser};
22
use rgb_lib::BitcoinNetwork;
33
use std::path::PathBuf;
44

5+
use crate::auth::check_auth_args;
56
use crate::error::AppError;
67
use crate::utils::check_port_is_available;
78

@@ -26,6 +27,14 @@ struct Args {
2627
/// Max allowed media size for upload (in MB)
2728
#[arg(long, default_value_t = 5)]
2829
max_media_upload_size_mb: u16,
30+
31+
/// Root public key for biscuit token authentication (hex-encoded)
32+
#[arg(long)]
33+
root_public_key: Option<String>,
34+
35+
/// Disable authentication
36+
#[arg(long, default_value_t = false)]
37+
disable_authentication: bool,
2938
}
3039

3140
pub(crate) struct UserArgs {
@@ -34,6 +43,7 @@ pub(crate) struct UserArgs {
3443
pub(crate) ldk_peer_listening_port: u16,
3544
pub(crate) network: BitcoinNetwork,
3645
pub(crate) max_media_upload_size_mb: u16,
46+
pub(crate) root_public_key: Option<biscuit_auth::PublicKey>,
3747
}
3848

3949
pub(crate) fn parse_startup_args() -> Result<UserArgs, AppError> {
@@ -46,11 +56,14 @@ pub(crate) fn parse_startup_args() -> Result<UserArgs, AppError> {
4656
let ldk_peer_listening_port = args.ldk_peer_listening_port;
4757
check_port_is_available(ldk_peer_listening_port)?;
4858

59+
let root_public_key = check_auth_args(args.disable_authentication, args.root_public_key)?;
60+
4961
Ok(UserArgs {
5062
storage_dir_path: args.storage_directory_path,
5163
daemon_listening_port,
5264
ldk_peer_listening_port,
5365
network,
5466
max_media_upload_size_mb: args.max_media_upload_size_mb,
67+
root_public_key,
5568
})
5669
}

0 commit comments

Comments
 (0)