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