Skip to main content

moq_auth/
error.rs

1/// Renders an error and its `source()` chain into a single message.
2///
3/// Dependency errors are stored as messages so their crates stay out of this crate's public
4/// API. Several of them keep the actionable half in `source()` and nothing but a category in
5/// `Display`, so a plain `to_string()` would drop the only detail worth reporting.
6pub(crate) fn message(err: impl std::error::Error) -> String {
7	use std::fmt::Write;
8
9	let mut out = err.to_string();
10	let mut source = err.source();
11	while let Some(err) = source {
12		let _ = write!(out, ": {err}");
13		source = err.source();
14	}
15	out
16}
17
18/// Errors related to key configuration and cryptographic operations.
19#[derive(Debug, thiserror::Error)]
20#[non_exhaustive]
21pub enum KeyError {
22	#[error("invalid algorithm for key type")]
23	InvalidAlgorithm,
24
25	#[error("invalid algorithm for {0} curve")]
26	InvalidAlgorithmForCurve(&'static str),
27
28	#[error("invalid coordinate length for {0}")]
29	InvalidCoordinateLength(&'static str),
30
31	#[error("invalid curve for {0} key")]
32	InvalidCurve(&'static str),
33
34	#[error("missing private key")]
35	MissingPrivateKey,
36
37	#[error("oct key secret must be at least {0} bytes")]
38	SecretTooShort(usize),
39
40	#[error("OCT key cannot be converted to public key")]
41	NoPublicKey,
42
43	#[error("key does not support verification")]
44	VerifyUnsupported,
45
46	#[error("key does not support signing")]
47	SignUnsupported,
48
49	#[error("cannot find signing key")]
50	NoSigningKey,
51
52	#[error("cannot find key with kid {0}")]
53	KeyNotFound(String),
54
55	#[error("missing kid in JWT header")]
56	MissingKid,
57
58	#[error("missing x() point in EC key")]
59	MissingEcX,
60
61	#[error("missing y() point in EC key")]
62	MissingEcY,
63}
64
65/// Top-level error type for moq-auth.
66#[derive(Debug, thiserror::Error)]
67#[non_exhaustive]
68pub enum Error {
69	#[error(transparent)]
70	Key(#[from] KeyError),
71
72	#[error("no publish or subscribe allowed; token is useless")]
73	UselessToken,
74
75	#[error("path `{0}` does not overlap the token root")]
76	RootMismatch(String),
77
78	#[error("token grants no access to path `{0}`")]
79	NoAccess(String),
80
81	#[error("no publish or subscribe allowed; key scope is useless")]
82	UselessScope,
83
84	#[error("token capabilities exceed the key scope")]
85	ScopeExceeded,
86
87	#[error("invalid algorithm: {0}")]
88	InvalidAlgorithm(String),
89
90	#[error("token has expired")]
91	TokenExpired,
92
93	#[error("token is not valid yet")]
94	TokenNotYetValid,
95
96	#[error(transparent)]
97	Pattern(#[from] moq_pattern::InvalidPattern),
98
99	#[error("grant names nothing; the session is refused")]
100	UselessGrant,
101
102	#[error("grant asks to be revalidated but never expires")]
103	UnboundedRevalidate,
104
105	#[error("session limits need a revalidate cadence, which ages out the slots of a relay that died")]
106	LimitsWithoutRevalidate,
107
108	#[error("the grant bound reaches past the system clock's range")]
109	ExpiresOutOfRange,
110
111	#[error("grant asks to be revalidated at no interval")]
112	ZeroRevalidate,
113
114	#[error("grant has already expired")]
115	GrantExpired,
116
117	#[error("the auth server refused the session")]
118	Refused,
119
120	#[error("auth server unavailable: {0}")]
121	Unavailable(String),
122
123	#[error("auth URL must be https://, unix://, or http:// to a loopback address: {0}")]
124	InsecureUrl(String),
125
126	#[error("invalid auth URL: {0}")]
127	InvalidUrl(String),
128
129	/// A JWK or claims document couldn't be parsed or serialized.
130	#[error("{0}")]
131	Json(String),
132
133	#[error(transparent)]
134	Io(#[from] std::io::Error),
135
136	/// A base64url field (a JWK coordinate, a JWT segment) isn't valid base64.
137	#[error("{0}")]
138	Base64(String),
139
140	#[error(transparent)]
141	Utf8(#[from] std::string::FromUtf8Error),
142
143	/// The JWT itself couldn't be signed, decoded, or verified.
144	#[error("{0}")]
145	Jwt(String),
146
147	/// A key couldn't be parsed, imported, or used by the crypto backend.
148	#[error("{0}")]
149	Crypto(String),
150
151	/// Fetching a remote JWKS failed.
152	#[error("{0}")]
153	Other(String),
154}
155
156// Dependency errors are flattened to their message so their crates stay out of this crate's
157// public API. Every one of them is opaque to a caller anyway: there is nothing to match on,
158// only something to report.
159macro_rules! from_message {
160	($($ty:ty => $variant:ident),* $(,)?) => {
161		$(
162			impl From<$ty> for Error {
163				fn from(err: $ty) -> Self {
164					Self::$variant(message(err))
165				}
166			}
167		)*
168	};
169}
170
171from_message! {
172	serde_json::Error => Json,
173	base64::DecodeError => Base64,
174	jsonwebtoken::errors::Error => Jwt,
175	p256::elliptic_curve::pkcs8::Error => Crypto,
176	p256::elliptic_curve::Error => Crypto,
177	rsa::Error => Crypto,
178	rsa::pkcs1::Error => Crypto,
179	aws_lc_rs::error::Unspecified => Crypto,
180	aws_lc_rs::error::KeyRejected => Crypto,
181}
182
183#[cfg(feature = "client")]
184impl From<reqwest::Error> for Error {
185	fn from(err: reqwest::Error) -> Self {
186		// reqwest prints the full URL in its error, and a dialed URL can carry
187		// credentials in its query or userinfo.
188		Self::Unavailable(message(err.without_url()))
189	}
190}
191
192pub type Result<T> = std::result::Result<T, Error>;
193
194#[cfg(test)]
195mod tests {
196	use super::*;
197
198	/// A dependency that reports only a category in `Display` and keeps the real cause in
199	/// `source()` (reqwest is the one that matters here) must not lose it on conversion.
200	#[test]
201	fn message_flattens_the_source_chain() {
202		#[derive(Debug, thiserror::Error)]
203		#[error("inner")]
204		struct Inner;
205
206		#[derive(Debug, thiserror::Error)]
207		#[error("outer")]
208		struct Outer(#[source] Inner);
209
210		assert_eq!(message(Outer(Inner)), "outer: inner");
211	}
212}