1use creusot_std::prelude::ensures;
4#[cfg(creusot)]
5use creusot_std::prelude::{DeepModel, Int, logic};
6
7#[krabka_macros::model_type]
9pub enum OAuthExpiryPresence {
10 Missing,
11 Present,
12}
13
14#[krabka_macros::model_type]
16pub enum OAuthSessionCap {
17 Disabled,
18 Enabled,
19}
20
21#[krabka_macros::model_type]
23pub enum OAuthAuthenticationKind {
24 Initial,
25 Reauthentication,
26}
27
28#[krabka_macros::model_type]
30pub enum OAuthPrincipalMatch {
31 Matches,
32 Differs,
33}
34
35#[krabka_macros::model_type]
37pub struct OAuthSessionFacts {
38 pub expiry: OAuthExpiryPresence,
39 pub token_expires_at_ms: i64,
40 pub now_ms: i64,
41 pub cap: OAuthSessionCap,
42 pub cap_ms: i64,
43 pub authentication: OAuthAuthenticationKind,
44 pub principal: OAuthPrincipalMatch,
45}
46
47#[krabka_macros::model_type]
49pub enum OAuthSessionDecision {
50 Reject,
51 Admit {
52 session_lifetime_ms: i64,
53 effective_expires_at_ms: i64,
54 },
55}
56
57#[cfg(creusot)]
59#[cfg_attr(test, mutants::skip)]
60#[logic(open)]
61pub(crate) fn oauth_session_admissible_at(facts: OAuthSessionFacts, now: Int) -> bool {
63 pearlite! {
64 facts.expiry == OAuthExpiryPresence::Present
65 && facts.token_expires_at_ms@ > now
66 && facts.token_expires_at_ms@ - now <= i64::MAX@
67 && (facts.cap == OAuthSessionCap::Disabled || facts.cap_ms@ > 0)
68 && (facts.authentication == OAuthAuthenticationKind::Initial
69 || facts.principal == OAuthPrincipalMatch::Matches)
70 }
71}
72
73#[cfg(creusot)]
79#[cfg_attr(test, mutants::skip)]
80#[logic(open(crate))]
81pub fn oauth_session_admissible(facts: OAuthSessionFacts) -> bool {
82 pearlite! { oauth_session_admissible_at(facts, facts.now_ms@) }
83}
84
85#[cfg(creusot)]
89#[cfg_attr(test, mutants::skip)]
90#[logic(open(crate))]
91pub fn oauth_session_lifetime(facts: OAuthSessionFacts) -> Int {
92 pearlite! {
93 let token_lifetime = facts.token_expires_at_ms@ - facts.now_ms@;
94 if facts.cap == OAuthSessionCap::Enabled && facts.cap_ms@ < token_lifetime {
95 facts.cap_ms@
96 } else {
97 token_lifetime
98 }
99 }
100}
101
102#[ensures(match result {
105 OAuthSessionDecision::Reject => !oauth_session_admissible(facts),
106 OAuthSessionDecision::Admit {
107 session_lifetime_ms,
108 effective_expires_at_ms,
109 } => oauth_session_admissible(facts)
110 && session_lifetime_ms@ == oauth_session_lifetime(facts)
111 && effective_expires_at_ms@ == facts.now_ms@ + session_lifetime_ms@,
112})]
113#[must_use]
114pub fn oauth_session_admission(facts: OAuthSessionFacts) -> OAuthSessionDecision {
115 if matches!(facts.expiry, OAuthExpiryPresence::Missing) {
116 return OAuthSessionDecision::Reject;
117 }
118 if facts.token_expires_at_ms <= facts.now_ms {
119 return OAuthSessionDecision::Reject;
120 }
121 if matches!(
122 (facts.authentication, facts.principal),
123 (
124 OAuthAuthenticationKind::Reauthentication,
125 OAuthPrincipalMatch::Differs
126 )
127 ) {
128 return OAuthSessionDecision::Reject;
129 }
130 if matches!(facts.cap, OAuthSessionCap::Enabled) && facts.cap_ms <= 0 {
131 return OAuthSessionDecision::Reject;
132 }
133 let Some(token_lifetime_ms) = facts.token_expires_at_ms.checked_sub(facts.now_ms) else {
134 return OAuthSessionDecision::Reject;
135 };
136 let session_lifetime_ms = match facts.cap {
137 OAuthSessionCap::Enabled => token_lifetime_ms.min(facts.cap_ms),
138 OAuthSessionCap::Disabled => token_lifetime_ms,
139 };
140 let Some(effective_expires_at_ms) = facts.now_ms.checked_add(session_lifetime_ms) else {
141 return OAuthSessionDecision::Reject;
142 };
143 OAuthSessionDecision::Admit {
144 session_lifetime_ms,
145 effective_expires_at_ms,
146 }
147}
148
149#[cfg(test)]
150mod tests {
151 use super::{
152 OAuthAuthenticationKind, OAuthExpiryPresence, OAuthPrincipalMatch, OAuthSessionCap,
153 OAuthSessionDecision, OAuthSessionFacts, oauth_session_admission,
154 };
155
156 fn facts() -> OAuthSessionFacts {
157 OAuthSessionFacts {
158 expiry: OAuthExpiryPresence::Present,
159 token_expires_at_ms: 1_100,
160 now_ms: 1_000,
161 cap: OAuthSessionCap::Disabled,
162 cap_ms: 0,
163 authentication: OAuthAuthenticationKind::Initial,
164 principal: OAuthPrincipalMatch::Matches,
165 }
166 }
167
168 #[yare::parameterized(
169 default_lifetime = {
170 facts(),
171 OAuthSessionDecision::Admit { session_lifetime_ms: 100, effective_expires_at_ms: 1_100 },
172 },
173 capped_lifetime = {
174 OAuthSessionFacts { cap: OAuthSessionCap::Enabled, cap_ms: 40, ..facts() },
175 OAuthSessionDecision::Admit { session_lifetime_ms: 40, effective_expires_at_ms: 1_040 },
176 },
177 longer_cap = {
178 OAuthSessionFacts { cap: OAuthSessionCap::Enabled, cap_ms: 500, ..facts() },
179 OAuthSessionDecision::Admit { session_lifetime_ms: 100, effective_expires_at_ms: 1_100 },
180 },
181 same_principal_reauthentication = {
182 OAuthSessionFacts { authentication: OAuthAuthenticationKind::Reauthentication, ..facts() },
183 OAuthSessionDecision::Admit { session_lifetime_ms: 100, effective_expires_at_ms: 1_100 },
184 },
185 max_lifetime = {
186 OAuthSessionFacts { token_expires_at_ms: i64::MAX, now_ms: 0, ..facts() },
187 OAuthSessionDecision::Admit { session_lifetime_ms: i64::MAX, effective_expires_at_ms: i64::MAX },
188 },
189 )]
190 fn session_expiry_is_exact_and_capped(
191 facts: OAuthSessionFacts,
192 expected: OAuthSessionDecision,
193 ) {
194 assert2::check!(oauth_session_admission(facts) == expected);
195 }
196
197 #[yare::parameterized(
198 missing_expiry = { OAuthSessionFacts { expiry: OAuthExpiryPresence::Missing, ..facts() } },
199 expired = { OAuthSessionFacts { token_expires_at_ms: 1_000, ..facts() } },
200 overflowing_lifetime = {
201 OAuthSessionFacts { token_expires_at_ms: i64::MAX, now_ms: -1, ..facts() },
202 },
203 nonpositive_cap = {
204 OAuthSessionFacts { cap: OAuthSessionCap::Enabled, cap_ms: 0, ..facts() },
205 },
206 changed_principal = {
207 OAuthSessionFacts {
208 authentication: OAuthAuthenticationKind::Reauthentication,
209 principal: OAuthPrincipalMatch::Differs,
210 ..facts()
211 },
212 },
213 )]
214 fn invalid_lifetime_or_principal_fails_closed(rejected: OAuthSessionFacts) {
215 assert2::check!(oauth_session_admission(rejected) == OAuthSessionDecision::Reject);
216 }
217}
218
219mod completion;
220pub use completion::oauth_validation_admission;