1use std::collections::BTreeMap;
8
9use chrono::{DateTime, Utc};
10use mas_iana::oauth::PkceCodeChallengeMethod;
11use oauth2_types::{
12 pkce::{CodeChallengeError, CodeChallengeMethodExt},
13 requests::ResponseMode,
14 scope::{OPENID, PROFILE, Scope},
15};
16use rand::{
17 RngCore,
18 distributions::{Alphanumeric, DistString},
19};
20use serde::Serialize;
21use ulid::Ulid;
22use url::Url;
23
24use super::session::Session;
25use crate::{BrowserSession, InvalidTransitionError, UlidExt as _};
26
27#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
28pub struct Pkce {
29 pub challenge_method: PkceCodeChallengeMethod,
30 pub challenge: String,
31}
32
33impl Pkce {
34 #[must_use]
36 pub fn new(challenge_method: PkceCodeChallengeMethod, challenge: String) -> Self {
37 Pkce {
38 challenge_method,
39 challenge,
40 }
41 }
42
43 pub fn verify(&self, verifier: &str) -> Result<(), CodeChallengeError> {
49 self.challenge_method.verify(&self.challenge, verifier)
50 }
51}
52
53#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
54pub struct AuthorizationCode {
55 pub code: String,
56 pub pkce: Option<Pkce>,
57}
58
59#[derive(Debug, Clone, PartialEq, Eq, Serialize, Default)]
60#[serde(tag = "stage", rename_all = "lowercase")]
61pub enum AuthorizationGrantStage {
62 #[default]
63 Pending,
64 Fulfilled {
65 browser_session_id: Ulid,
66 fulfilled_at: DateTime<Utc>,
67 },
68 Exchanged {
69 session_id: Ulid,
70 fulfilled_at: DateTime<Utc>,
71 exchanged_at: DateTime<Utc>,
72 },
73 Cancelled {
74 cancelled_at: DateTime<Utc>,
75 },
76}
77
78impl AuthorizationGrantStage {
79 #[must_use]
80 pub fn new() -> Self {
81 Self::Pending
82 }
83
84 fn fulfill(
85 self,
86 fulfilled_at: DateTime<Utc>,
87 browser_session: &BrowserSession,
88 ) -> Result<Self, InvalidTransitionError> {
89 match self {
90 Self::Pending => Ok(Self::Fulfilled {
91 fulfilled_at,
92 browser_session_id: browser_session.id,
93 }),
94 _ => Err(InvalidTransitionError),
95 }
96 }
97
98 fn exchange(
99 self,
100 exchanged_at: DateTime<Utc>,
101 session: &Session,
102 ) -> Result<Self, InvalidTransitionError> {
103 match self {
104 Self::Fulfilled {
105 fulfilled_at,
106 browser_session_id: _,
107 } => Ok(Self::Exchanged {
108 fulfilled_at,
109 exchanged_at,
110 session_id: session.id,
111 }),
112 _ => Err(InvalidTransitionError),
113 }
114 }
115
116 fn cancel(self, cancelled_at: DateTime<Utc>) -> Result<Self, InvalidTransitionError> {
117 match self {
118 Self::Pending => Ok(Self::Cancelled { cancelled_at }),
119 _ => Err(InvalidTransitionError),
120 }
121 }
122
123 #[must_use]
127 pub fn is_pending(&self) -> bool {
128 matches!(self, Self::Pending)
129 }
130
131 #[must_use]
135 pub fn is_fulfilled(&self) -> bool {
136 matches!(self, Self::Fulfilled { .. })
137 }
138
139 #[must_use]
143 pub fn is_exchanged(&self) -> bool {
144 matches!(self, Self::Exchanged { .. })
145 }
146}
147
148#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
149pub struct AuthorizationGrant {
150 pub id: Ulid,
151 #[serde(flatten)]
152 pub stage: AuthorizationGrantStage,
153 pub code: Option<AuthorizationCode>,
154 pub client_id: Ulid,
155 pub redirect_uri: Url,
156 pub scope: Scope,
157 pub state: Option<String>,
158 pub nonce: Option<String>,
159 pub response_mode: ResponseMode,
160 pub response_type_id_token: bool,
161 pub created_at: DateTime<Utc>,
162 pub login_hint: Option<String>,
163 pub locale: Option<String>,
164 pub raw_parameters: BTreeMap<String, String>,
167}
168
169impl std::ops::Deref for AuthorizationGrant {
170 type Target = AuthorizationGrantStage;
171
172 fn deref(&self) -> &Self::Target {
173 &self.stage
174 }
175}
176
177impl AuthorizationGrant {
178 pub fn exchange(
186 mut self,
187 exchanged_at: DateTime<Utc>,
188 session: &Session,
189 ) -> Result<Self, InvalidTransitionError> {
190 self.stage = self.stage.exchange(exchanged_at, session)?;
191 Ok(self)
192 }
193
194 pub fn fulfill(
202 mut self,
203 fulfilled_at: DateTime<Utc>,
204 browser_session: &BrowserSession,
205 ) -> Result<Self, InvalidTransitionError> {
206 self.stage = self.stage.fulfill(fulfilled_at, browser_session)?;
207 Ok(self)
208 }
209
210 pub fn cancel(mut self, canceld_at: DateTime<Utc>) -> Result<Self, InvalidTransitionError> {
222 self.stage = self.stage.cancel(canceld_at)?;
223 Ok(self)
224 }
225
226 #[doc(hidden)]
227 pub fn sample(now: DateTime<Utc>, rng: &mut impl RngCore) -> Self {
228 Self {
229 id: Ulid::from_datetime_with_rng(now, rng),
230 stage: AuthorizationGrantStage::Pending,
231 code: Some(AuthorizationCode {
232 code: Alphanumeric.sample_string(rng, 10),
233 pkce: None,
234 }),
235 client_id: Ulid::from_datetime_with_rng(now, rng),
236 redirect_uri: Url::parse("http://localhost:8080").unwrap(),
237 scope: Scope::from_iter([OPENID, PROFILE]),
238 state: Some(Alphanumeric.sample_string(rng, 10)),
239 nonce: Some(Alphanumeric.sample_string(rng, 10)),
240 response_mode: ResponseMode::Query,
241 response_type_id_token: false,
242 created_at: now,
243 login_hint: Some(String::from("mxid:@example-user:example.com")),
244 locale: Some(String::from("fr")),
245 raw_parameters: BTreeMap::new(),
246 }
247 }
248}