Skip to main content

lexe_common/api/
auth.rs

1// bearer auth v1
2
3use std::{
4    fmt,
5    time::{Duration, SystemTime},
6};
7
8use base64::Engine;
9use lexe_crypto::ed25519::{self, Signed};
10use lexe_std::array;
11#[cfg(any(test, feature = "test-utils"))]
12use proptest_derive::Arbitrary;
13use serde::{Deserialize, Serialize};
14use thiserror::Error;
15
16use super::user::{NodePkProof, UserPk};
17use crate::byte_str::ByteStr;
18#[cfg(any(test, feature = "test-utils"))]
19use crate::test_utils::arbitrary;
20
21#[derive(Debug, Error)]
22pub enum Error {
23    #[error("Error verifying signed bearer auth request: {0}")]
24    UserVerifyError(#[source] ed25519::Error),
25
26    #[error("Decoded bearer auth token appears malformed")]
27    MalformedToken,
28
29    #[error("Issued timestamp is too far from current auth server clock")]
30    ClockDrift,
31
32    #[error("Auth token or auth request is expired")]
33    Expired,
34
35    #[error("Auth token is not valid yet")]
36    NotYetValid,
37
38    #[error("Timestamp is not a valid unix timestamp")]
39    InvalidTimestamp,
40
41    #[error("Requested token lifetime is too long")]
42    InvalidLifetime,
43
44    #[error("User not signed up yet")]
45    NoUser,
46
47    #[error("Bearer auth token is not valid base64")]
48    Base64Decode,
49
50    #[error("Bearer auth token was not provided")]
51    Missing,
52
53    // TODO(phlip9): this is an authorization error, not an authentication
54    // error. Add a new type?
55    #[error(
56        "Auth token's granted scope ({granted:?}) is not sufficient for \
57         requested scope ({requested:?})"
58    )]
59    InsufficientScope {
60        granted: LexeScope,
61        requested: LexeScope,
62    },
63}
64
65/// The inner, signed part of the request a new user makes when they first sign
66/// up. We use this to prove the user owns both their claimed [`UserPk`] and
67/// [`NodePk`].
68///
69/// One caveat: we can't verify the presented, valid, signed [`UserPk`] and
70/// [`NodePk`] are actually derived from the same [`RootSeed`]. In the case that
71/// these are different, the account will be created, but the user node will
72/// fail to ever run or provision.
73///
74/// [`UserPk`]: crate::api::user::UserPk
75/// [`NodePk`]: crate::api::user::NodePk
76/// [`RootSeed`]: crate::root_seed::RootSeed
77#[cfg_attr(any(test, feature = "test-utils"), derive(Arbitrary))]
78#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
79pub enum UserSignupRequestWire {
80    V2(UserSignupRequestWireV2),
81}
82
83#[cfg_attr(any(test, feature = "test-utils"), derive(Arbitrary))]
84#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
85pub struct UserSignupRequestWireV2 {
86    pub v1: UserSignupRequestWireV1,
87
88    /// The partner that signed up this user, if any.
89    // Added in `node-v0.7.12+`
90    pub partner: Option<UserPk>,
91}
92
93#[cfg_attr(any(test, feature = "test-utils"), derive(Arbitrary))]
94#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
95pub struct UserSignupRequestWireV1 {
96    /// The lightning node pubkey in a Proof-of-Key-Possession
97    pub node_pk_proof: NodePkProof,
98    /// Signup codes are no longer required. This field is kept for BCS
99    /// backwards compatibility with old clients sending `Option<String>`.
100    /// New clients serialize `None` (1 byte). Old clients may send
101    /// `Some(code)` which is consumed and ignored.
102    #[cfg_attr(
103        any(test, feature = "test-utils"),
104        proptest(strategy = "arbitrary::any_option_string()")
105    )]
106    _signup_code: Option<String>,
107}
108
109impl UserSignupRequestWireV1 {
110    pub fn new(node_pk_proof: NodePkProof) -> Self {
111        Self {
112            node_pk_proof,
113            _signup_code: None,
114        }
115    }
116}
117
118/// A client's request for a new [`BearerAuthToken`].
119///
120/// This is the convenient in-memory representation.
121#[derive(Clone, Debug)]
122pub struct BearerAuthRequest {
123    /// The timestamp of this auth request, in seconds since UTC Unix time,
124    /// interpreted relative to the server clock. Used to prevent replaying old
125    /// auth requests after expiration.
126    ///
127    /// The server will reject timestamps w/ > 30 minute clock skew from the
128    /// server clock.
129    pub request_timestamp_secs: u64,
130
131    /// How long the new auth token should be valid for, in seconds. Must be at
132    /// most 1 hour. The new token expiration is generated relative to the
133    /// server clock.
134    pub lifetime_secs: u32,
135
136    /// The [`LexeScope`] requested for the new bearer auth token.
137    // TODO(phlip9): implement proper scope attenuation from identity's allowed
138    // scopes
139    pub scope: LexeScope,
140}
141
142/// A client's request for a new [`BearerAuthToken`].
143///
144/// This is the over-the-wire BCS-serializable representation structured for
145/// backwards compatibility.
146#[cfg_attr(any(test, feature = "test-utils"), derive(Arbitrary))]
147#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
148pub enum BearerAuthRequestWire {
149    V1(BearerAuthRequestWireV1),
150    // Added in node-v0.7.9+
151    V2(BearerAuthRequestWireV2),
152}
153
154/// A user client's request for auth token with certain restrictions.
155#[cfg_attr(any(test, feature = "test-utils"), derive(Arbitrary))]
156#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
157pub struct BearerAuthRequestWireV1 {
158    request_timestamp_secs: u64,
159    lifetime_secs: u32,
160}
161
162/// A user client's request for auth token with certain restrictions.
163#[cfg_attr(any(test, feature = "test-utils"), derive(Arbitrary))]
164#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
165pub struct BearerAuthRequestWireV2 {
166    // v2 includes all fields from v1
167    v1: BearerAuthRequestWireV1,
168    // node-v0.7.9 added this field as `Option` along with the V2 wire format.
169    //
170    // We must keep this `Option` so that we can continue deserializing those
171    // older `None` requests (and assume `LexeScope::All` in such a case);
172    // current clients always send `Some`.
173    scope: Option<LexeScope>,
174}
175
176/// An infra-level scope which gates a bearer auth token's access to Lexe-run
177/// services and resources.
178///
179/// This is distinct from the *application-level* authorization that determines
180/// which node endpoints a given client credential may call; that lives with the
181/// revocable client machinery, not here. A `LexeScope` answers "what may this
182/// token do against *Lexe's* infrastructure", e.g. the backend or the gateway
183/// CONNECT proxy.
184///
185/// Scopes form a lattice: a token granted a broader scope is accepted wherever
186/// a narrower scope is required (see [`LexeScope::has_permission_for`]).
187#[cfg_attr(any(test, feature = "test-utils"), derive(Arbitrary))]
188#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
189pub enum LexeScope {
190    /// Full access to all Lexe-run services. Short-lived (max 1 hour).
191    All,
192
193    /// Authorizes the gateway to act as the user's proxy, in either of two
194    /// ways:
195    /// - a CONNECT tunnel to the user's own node, or
196    /// - a request the gateway makes to the backend (`AppBackendApi`) on the
197    ///   user's behalf, using its own backend credentials.
198    ///
199    /// This grants neither direct backend access nor forwarding of this token
200    /// to the backend; that's [`LexeScope::All`]. May be long-lived (max 20
201    /// years), so it can back a long-lived client credential.
202    // TODO(phlip9): should be a fine-grained scope
203    GatewayProxy,
204    //
205    // // TODO(phlip9): fine-grained scopes?
206    // Restricted { .. },
207    // ReadOnly,
208}
209
210#[derive(Clone, Debug, Serialize, Deserialize)]
211pub struct BearerAuthResponse {
212    pub bearer_auth_token: BearerAuthToken,
213}
214
215/// An opaque bearer auth token for authenticating user clients against lexe
216/// infra as a particular [`UserPk`].
217///
218/// Most user clients should just treat this as an opaque Bearer token with a
219/// very short (~15 min) expiration.
220#[derive(Clone, Serialize, Deserialize)]
221#[cfg_attr(any(test, feature = "test-utils"), derive(Eq, PartialEq))]
222pub struct BearerAuthToken(pub ByteStr);
223
224impl fmt::Debug for BearerAuthToken {
225    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
226        f.write_str("BearerAuthToken(..)")
227    }
228}
229
230/// A [`BearerAuthToken`] and its expected expiration time, or `None` if the
231/// token never expires.
232#[derive(Clone)]
233pub struct TokenWithExpiration {
234    pub token: BearerAuthToken,
235    pub expiration: Option<SystemTime>,
236}
237
238// --- impl UserSignupRequestWire --- //
239
240impl UserSignupRequestWire {
241    pub fn node_pk_proof(&self) -> &NodePkProof {
242        match self {
243            UserSignupRequestWire::V2(v2) => &v2.v1.node_pk_proof,
244        }
245    }
246
247    pub fn partner(&self) -> Option<&UserPk> {
248        match self {
249            UserSignupRequestWire::V2(v2) => v2.partner.as_ref(),
250        }
251    }
252}
253
254impl ed25519::Signable for UserSignupRequestWire {
255    // Name gets cut off to stay within 32 B
256    const DOMAIN_SEPARATOR: [u8; 32] =
257        array::pad(*b"LEXE-REALM::UserSignupRequestWir");
258}
259
260// -- impl UserSignupRequestWireV1 -- //
261
262impl UserSignupRequestWireV1 {
263    pub fn deserialize_verify(
264        serialized: &[u8],
265    ) -> Result<Signed<Self>, Error> {
266        // for user sign up, the signed signup request is just used to prove
267        // ownership of a user_pk.
268        ed25519::verify::signed_struct_by_any_signer(serialized)
269            .map_err(Error::UserVerifyError)
270    }
271}
272
273impl ed25519::Signable for UserSignupRequestWireV1 {
274    // Name is different for backwards compat after rename
275    const DOMAIN_SEPARATOR: [u8; 32] =
276        array::pad(*b"LEXE-REALM::UserSignupRequest");
277}
278
279// --- impl UserSignupRequestWireV2 --- //
280
281impl From<UserSignupRequestWireV1> for UserSignupRequestWireV2 {
282    fn from(v1: UserSignupRequestWireV1) -> Self {
283        Self { v1, partner: None }
284    }
285}
286
287// -- impl BearerAuthRequest -- //
288
289impl BearerAuthRequest {
290    pub fn new(
291        now: SystemTime,
292        token_lifetime_secs: u32,
293        scope: LexeScope,
294    ) -> Self {
295        Self {
296            request_timestamp_secs: now
297                .duration_since(SystemTime::UNIX_EPOCH)
298                .expect("Something is very wrong with our clock")
299                .as_secs(),
300            lifetime_secs: token_lifetime_secs,
301            scope,
302        }
303    }
304
305    /// Get the `request_timestamp` as a [`SystemTime`]. Returns `Err` if the
306    /// `issued_timestamp` is too large to be represented as a unix timestamp
307    /// (> 2^63 on linux).
308    pub fn request_timestamp(&self) -> Result<SystemTime, Error> {
309        let t_secs = self.request_timestamp_secs;
310        let t_dur_secs = Duration::from_secs(t_secs);
311        SystemTime::UNIX_EPOCH
312            .checked_add(t_dur_secs)
313            .ok_or(Error::InvalidTimestamp)
314    }
315}
316
317impl From<BearerAuthRequestWire> for BearerAuthRequest {
318    fn from(wire: BearerAuthRequestWire) -> Self {
319        match wire {
320            // V1 predates scopes; fall back to the broadest scope for these
321            // (very old) clients.
322            BearerAuthRequestWire::V1(v1) => Self {
323                request_timestamp_secs: v1.request_timestamp_secs,
324                lifetime_secs: v1.lifetime_secs,
325                scope: LexeScope::All,
326            },
327            // A V2 client that omits the scope likewise defaults to `All`.
328            BearerAuthRequestWire::V2(v2) => Self {
329                request_timestamp_secs: v2.v1.request_timestamp_secs,
330                lifetime_secs: v2.v1.lifetime_secs,
331                scope: v2.scope.unwrap_or(LexeScope::All),
332            },
333        }
334    }
335}
336
337impl From<BearerAuthRequest> for BearerAuthRequestWire {
338    fn from(req: BearerAuthRequest) -> Self {
339        Self::V2(BearerAuthRequestWireV2 {
340            v1: BearerAuthRequestWireV1 {
341                request_timestamp_secs: req.request_timestamp_secs,
342                lifetime_secs: req.lifetime_secs,
343            },
344            scope: Some(req.scope),
345        })
346    }
347}
348
349// -- impl BearerAuthRequestWire -- //
350
351impl BearerAuthRequestWire {
352    pub fn deserialize_verify(
353        serialized: &[u8],
354    ) -> Result<Signed<Self>, Error> {
355        // likewise, user/node auth is (currently) just used to prove ownership
356        // of a user_pk.
357        ed25519::verify::signed_struct_by_any_signer(serialized)
358            .map_err(Error::UserVerifyError)
359    }
360}
361
362impl ed25519::Signable for BearerAuthRequestWire {
363    // Uses "LEXE-REALM::BearerAuthRequest" for backwards compatibility
364    const DOMAIN_SEPARATOR: [u8; 32] =
365        array::pad(*b"LEXE-REALM::BearerAuthRequest");
366}
367
368// -- impl BearerAuthToken -- //
369
370impl BearerAuthToken {
371    /// base64 serialize a bearer auth token from the internal raw bytes.
372    pub fn encode_from_raw_bytes(signed_token_bytes: &[u8]) -> Self {
373        let b64_token = base64::engine::general_purpose::URL_SAFE_NO_PAD
374            .encode(signed_token_bytes);
375        Self(ByteStr::from(b64_token))
376    }
377
378    /// base64 decode the bearer auth token into the internal raw bytes.
379    pub fn decode_into_raw_bytes(&self) -> Result<Vec<u8>, Error> {
380        Self::decode_slice_into_raw_bytes(self.0.as_bytes())
381    }
382
383    /// base64 decode a given string bearer auth token into internal raw bytes.
384    pub fn decode_slice_into_raw_bytes(bytes: &[u8]) -> Result<Vec<u8>, Error> {
385        base64::engine::general_purpose::URL_SAFE_NO_PAD
386            .decode(bytes)
387            .map_err(|_| Error::Base64Decode)
388    }
389}
390
391impl fmt::Display for BearerAuthToken {
392    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
393        f.write_str(self.0.as_str())
394    }
395}
396
397#[cfg(any(test, feature = "test-utils"))]
398mod arbitrary_impl {
399    use proptest::{
400        arbitrary::{Arbitrary, any},
401        strategy::{BoxedStrategy, Strategy},
402    };
403
404    use super::*;
405
406    impl Arbitrary for BearerAuthToken {
407        type Parameters = ();
408        type Strategy = BoxedStrategy<Self>;
409
410        fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
411            // Generate a random byte array and encode it
412            // This simulates a valid bearer token format
413            any::<Vec<u8>>()
414                .prop_map(|bytes| {
415                    BearerAuthToken::encode_from_raw_bytes(&bytes)
416                })
417                .boxed()
418        }
419    }
420}
421
422// --- impl LexeScope --- //
423
424impl LexeScope {
425    /// Returns `true` if this granted scope covers the `requested` scope.
426    pub fn has_permission_for(&self, requested: &Self) -> bool {
427        match (self, requested) {
428            (LexeScope::All, _) => true,
429            (LexeScope::GatewayProxy, LexeScope::All) => false,
430            (LexeScope::GatewayProxy, LexeScope::GatewayProxy) => true,
431        }
432    }
433}
434
435#[cfg(test)]
436mod test {
437    use base64::Engine;
438    use lexe_hex::hex;
439
440    use super::*;
441    use crate::test_utils::roundtrip::{
442        bcs_roundtrip_ok, bcs_roundtrip_proptest, signed_roundtrip_proptest,
443    };
444
445    #[test]
446    fn test_user_signup_request_wire_canonical() {
447        bcs_roundtrip_proptest::<UserSignupRequestWire>();
448    }
449
450    #[test]
451    fn test_user_signed_request_wire_sign_verify() {
452        signed_roundtrip_proptest::<UserSignupRequestWire>();
453    }
454
455    #[test]
456    fn test_bearer_auth_request_wire_canonical() {
457        bcs_roundtrip_proptest::<BearerAuthRequestWire>();
458    }
459
460    #[test]
461    fn test_bearer_auth_request_wire_sign_verify() {
462        signed_roundtrip_proptest::<BearerAuthRequestWire>();
463    }
464
465    #[test]
466    fn test_bearer_auth_request_wire_snapshot() {
467        let input = "00d20296490000000058020000";
468        let req = BearerAuthRequestWire::V1(BearerAuthRequestWireV1 {
469            request_timestamp_secs: 1234567890,
470            lifetime_secs: 10 * 60,
471        });
472        bcs_roundtrip_ok(&hex::decode(input).unwrap(), &req);
473
474        let input = "01d2029649000000005802000000";
475        let req = BearerAuthRequestWire::V2(BearerAuthRequestWireV2 {
476            v1: BearerAuthRequestWireV1 {
477                request_timestamp_secs: 1234567890,
478                lifetime_secs: 10 * 60,
479            },
480            scope: None,
481        });
482        bcs_roundtrip_ok(&hex::decode(input).unwrap(), &req);
483
484        let input = "01d202964900000000580200000101";
485        let req = BearerAuthRequestWire::V2(BearerAuthRequestWireV2 {
486            v1: BearerAuthRequestWireV1 {
487                request_timestamp_secs: 1234567890,
488                lifetime_secs: 10 * 60,
489            },
490            scope: Some(LexeScope::GatewayProxy),
491        });
492        bcs_roundtrip_ok(&hex::decode(input).unwrap(), &req);
493    }
494
495    #[test]
496    fn test_auth_scope_canonical() {
497        bcs_roundtrip_proptest::<LexeScope>();
498    }
499
500    #[test]
501    fn test_auth_scope_snapshot() {
502        let input = b"\x00";
503        let scope = LexeScope::All;
504        bcs_roundtrip_ok(input, &scope);
505
506        let input = b"\x01";
507        let scope = LexeScope::GatewayProxy;
508        bcs_roundtrip_ok(input, &scope);
509    }
510
511    /// Snapshot test for UserSignupRequestWireV1 BCS serialization.
512    /// These snapshots were generated with the old `signup_code:
513    /// Option<String>` field. Since BCS ignores field names, the current
514    /// `_signup_code` field produces identical serialization.
515    #[test]
516    fn test_user_signup_request_wire_v1_snapshot() {
517        let b64 = base64::engine::general_purpose::STANDARD;
518
519        // Old client with signup_code = Some("ABCD-1234")
520        let input_with_code = "AqqWkI6A9EExJ9suasa1a4Vte7dSztOpSsGNVUHClpLb\
521            RjBEAiANgXon77EhDl3dq6ZASg9u/xjS3OET2um+OA6+/58UmQIgEYmJGcNNWfMy\
522            npScmW9joOortpvHul9bHyojSj3Im70BCUFCQ0QtMTIzNA==";
523        let bytes_with_code = b64.decode(input_with_code).unwrap();
524        let req: UserSignupRequestWireV1 =
525            bcs::from_bytes(&bytes_with_code).unwrap();
526        assert!(req.node_pk_proof.verify().is_ok());
527
528        // Old client with signup_code = None
529        let input_none = "AqqWkI6A9EExJ9suasa1a4Vte7dSztOpSsGNVUHClpLbRjBE\
530            AiANgXon77EhDl3dq6ZASg9u/xjS3OET2um+OA6+/58UmQIgEYmJGcNNWfMynpSc\
531            mW9joOortpvHul9bHyojSj3Im70A";
532        let bytes_none = b64.decode(input_none).unwrap();
533        let req: UserSignupRequestWireV1 =
534            bcs::from_bytes(&bytes_none).unwrap();
535        assert!(req.node_pk_proof.verify().is_ok());
536    }
537}