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