otp_std/auth/
core.rs

1//! Core functionality for authentication.
2
3use bon::Builder;
4use miette::Diagnostic;
5
6#[cfg(feature = "serde")]
7use serde::{Deserialize, Serialize};
8
9use thiserror::Error;
10
11use crate::{
12    auth::{
13        self,
14        label::{self, Label},
15        query::Query,
16        scheme,
17        url::{self, Url},
18    },
19    macros::errors,
20    otp::{
21        self,
22        core::Otp,
23        type_of::{self, Type},
24    },
25};
26
27/// The scheme of OTP URLs.
28pub const SCHEME: &str = "otpauth";
29
30/// Base OTP URL is always valid.
31pub const BASE_URL_ALWAYS_VALID: &str = "OTP base URL is always valid";
32
33/// Represents OTP authentication.
34#[derive(Debug, Clone, PartialEq, Eq, Hash, Builder)]
35#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
36pub struct Auth<'a> {
37    /// The OTP configuration.
38    #[builder(into)]
39    pub otp: Otp<'a>,
40    /// The authentication label.
41    pub label: Label<'a>,
42}
43
44/// Represents sources of errors that can occur when parsing OTP URLs.
45#[derive(Debug, Error, Diagnostic)]
46#[error(transparent)]
47#[diagnostic(transparent)]
48pub enum ErrorSource {
49    /// URL could not be parsed.
50    Url(#[from] url::Error),
51    /// Unexpected scheme found.
52    Scheme(#[from] scheme::Error),
53    /// OTP type extraction failed.
54    TypeOf(#[from] type_of::Error),
55    /// Label could not be extracted.
56    Label(#[from] label::Error),
57    /// OTP extraction failed.
58    Otp(#[from] otp::core::Error),
59}
60
61/// Represents errors that can occur when parsing OTP URLs.
62#[derive(Debug, Error, Diagnostic)]
63#[error("failed to extract auth from `{string}`")]
64#[diagnostic(code(otp_std::auth::core), help("see the report for more information"))]
65pub struct Error {
66    /// The source of this error.
67    #[source]
68    #[diagnostic_source]
69    pub source: ErrorSource,
70    /// The string that could not be parsed.
71    pub string: String,
72}
73
74impl Error {
75    /// Constructs [`Self`].
76    pub const fn new(source: ErrorSource, string: String) -> Self {
77        Self { source, string }
78    }
79
80    /// Constructs [`Self`] from [`url::Error`].
81    pub fn parse(error: url::Error, string: String) -> Self {
82        Self::new(error.into(), string)
83    }
84
85    /// Constructs [`Self`] from [`scheme::Error`].
86    pub fn scheme(error: scheme::Error, string: String) -> Self {
87        Self::new(error.into(), string)
88    }
89
90    /// Constructs [`Self`] from [`type_of::Error`].
91    pub fn type_of(error: type_of::Error, string: String) -> Self {
92        Self::new(error.into(), string)
93    }
94
95    /// Constructs [`Self`] from [`label::Error`].
96    pub fn label(error: label::Error, string: String) -> Self {
97        Self::new(error.into(), string)
98    }
99
100    /// Constructs [`Self`] from [`otp::core::Error`].
101    pub fn otp(error: otp::core::Error, string: String) -> Self {
102        Self::new(error.into(), string)
103    }
104}
105
106impl Auth<'_> {
107    /// Returns the OTP configuration reference.
108    pub const fn otp(&self) -> &Otp<'_> {
109        &self.otp
110    }
111
112    /// Returns the label reference.
113    pub const fn label(&self) -> &Label<'_> {
114        &self.label
115    }
116}
117
118/// Represents `(otp, label)` parts of the authentication.
119pub type Parts<'p> = (Otp<'p>, Label<'p>);
120
121/// Represents owned [`Parts`].
122pub type OwnedParts = Parts<'static>;
123
124impl<'a> Auth<'a> {
125    /// Constructs [`Self`] from parts.
126    pub fn from_parts(parts: Parts<'a>) -> Self {
127        let (otp, label) = parts;
128
129        Self::builder().otp(otp).label(label).build()
130    }
131
132    /// Consumes [`Self`], returning the contained parts.
133    pub fn into_parts(self) -> Parts<'a> {
134        (self.otp, self.label)
135    }
136}
137
138impl<'p> From<Parts<'p>> for Auth<'p> {
139    fn from(parts: Parts<'p>) -> Self {
140        Self::from_parts(parts)
141    }
142}
143
144impl<'a> From<Auth<'a>> for Parts<'a> {
145    fn from(auth: Auth<'a>) -> Self {
146        auth.into_parts()
147    }
148}
149
150errors! {
151    Type = Error,
152    Hack = $,
153    parse_error => parse(error, string => to_owned),
154    scheme_error => scheme(error, string => to_owned),
155    type_of_error => type_of(error, string => to_owned),
156    label_error => label(error, string => to_owned),
157    otp_error => otp(error, string => to_owned),
158}
159
160impl Auth<'_> {
161    /// Constructs the OTP URL base.
162    ///
163    /// # Panics
164    ///
165    /// The base URL is always valid, so this method should never panic.
166    pub fn base_url(&self) -> Url {
167        url::base(self.otp().type_of(), self.label())
168    }
169
170    /// Builds the OTP URL, applying query parameters to the base URL created.
171    pub fn build_url(&self) -> Url {
172        let mut url = self.base_url();
173
174        self.query_for(&mut url);
175
176        url
177    }
178
179    /// Applies the OTP configuration and the issuer to the given URL.
180    pub fn query_for(&self, url: &mut Url) {
181        self.otp().query_for(url);
182        self.label().query_for(url);
183    }
184
185    /// Parses the OTP URL from the given string.
186    ///
187    /// # Errors
188    ///
189    /// Returns [`struct@Error`] if anything goes wrong.
190    pub fn parse_url<S: AsRef<str>>(string: S) -> Result<Self, Error> {
191        fn parse_url_inner(string: &str) -> Result<OwnedParts, Error> {
192            let url = auth::url::parse(string).map_err(|error| parse_error!(error, string))?;
193
194            auth::scheme::check_url(&url).map_err(|error| scheme_error!(error, string))?;
195
196            let type_of =
197                Type::extract_from(&url).map_err(|error| type_of_error!(error, string))?;
198
199            let mut query: Query<'_> = url.query_pairs().collect();
200
201            let label = Label::extract_from(&mut query, &url)
202                .map_err(|error| label_error!(error, string))?;
203
204            let otp = Otp::extract_from(&mut query, type_of)
205                .map_err(|error| otp_error!(error, string))?;
206
207            Ok((otp, label))
208        }
209
210        parse_url_inner(string.as_ref()).map(Self::from_parts)
211    }
212}
213
214/// Represents owned [`Auth`].
215pub type Owned = Auth<'static>;
216
217impl Auth<'_> {
218    /// Converts [`Self`] into [`Owned`].
219    pub fn into_owned(self) -> Owned {
220        Owned::builder()
221            .otp(self.otp.into_owned())
222            .label(self.label.into_owned())
223            .build()
224    }
225}