assay_auth/oidc_provider/
types.rs1use serde::{Deserialize, Serialize};
9
10#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
13#[serde(rename_all = "snake_case")]
14pub enum TokenAuthMethod {
15 ClientSecretBasic,
17 ClientSecretPost,
19 None,
21 PrivateKeyJwt,
24}
25
26impl TokenAuthMethod {
27 pub fn as_str(self) -> &'static str {
28 match self {
29 Self::ClientSecretBasic => "client_secret_basic",
30 Self::ClientSecretPost => "client_secret_post",
31 Self::None => "none",
32 Self::PrivateKeyJwt => "private_key_jwt",
33 }
34 }
35
36 pub fn parse(s: &str) -> Option<Self> {
37 match s {
38 "client_secret_basic" => Some(Self::ClientSecretBasic),
39 "client_secret_post" => Some(Self::ClientSecretPost),
40 "none" => Some(Self::None),
41 "private_key_jwt" => Some(Self::PrivateKeyJwt),
42 _ => None,
43 }
44 }
45}
46
47#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
49pub struct OidcClient {
50 pub client_id: String,
51 pub client_secret_hash: Option<String>,
55 pub redirect_uris: Vec<String>,
56 pub name: String,
57 pub logo_url: Option<String>,
58 pub token_endpoint_auth_method: TokenAuthMethod,
59 pub default_scopes: Vec<String>,
60 pub require_consent: bool,
61 pub grant_types: Vec<String>,
62 pub response_types: Vec<String>,
63 pub pkce_required: bool,
64 pub backchannel_logout_uri: Option<String>,
65 pub created_at: f64,
66}
67
68impl OidcClient {
69 pub fn new(client_id: impl Into<String>, name: impl Into<String>, created_at: f64) -> Self {
73 Self {
74 client_id: client_id.into(),
75 client_secret_hash: None,
76 redirect_uris: Vec::new(),
77 name: name.into(),
78 logo_url: None,
79 token_endpoint_auth_method: TokenAuthMethod::ClientSecretBasic,
80 default_scopes: vec!["openid".to_string()],
81 require_consent: true,
82 grant_types: vec![
83 "authorization_code".to_string(),
84 "refresh_token".to_string(),
85 ],
86 response_types: vec!["code".to_string()],
87 pkce_required: true,
88 backchannel_logout_uri: None,
89 created_at,
90 }
91 }
92
93 pub fn redirect_matches(&self, redirect_uri: &str) -> bool {
97 self.redirect_uris.iter().any(|u| u == redirect_uri)
98 }
99
100 pub fn allows_grant(&self, grant: &str) -> bool {
102 self.grant_types.iter().any(|g| g == grant)
103 }
104}
105
106#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
111pub struct UpstreamProvider {
112 pub slug: String,
113 pub issuer: String,
114 pub client_id: String,
115 pub client_secret: String,
116 pub display_name: String,
117 pub icon_url: Option<String>,
118 pub enabled: bool,
119}
120
121#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
124pub struct AuthorizationCode {
125 pub code: String,
126 pub client_id: String,
127 pub user_id: String,
128 pub redirect_uri: String,
129 pub scopes: Vec<String>,
130 pub code_challenge: String,
131 pub code_challenge_method: String,
132 pub nonce: Option<String>,
133 pub state: Option<String>,
134 pub issued_at: f64,
135 pub expires_at: f64,
136 pub consumed: bool,
137}
138
139#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
143pub struct RefreshToken {
144 pub token_hash: String,
145 pub client_id: String,
146 pub user_id: String,
147 pub scopes: Vec<String>,
148 pub issued_at: f64,
149 pub expires_at: f64,
150 pub revoked: bool,
151}
152
153#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
157pub struct OidcSession {
158 pub sid: String,
159 pub user_id: String,
160 pub client_id: String,
161 pub assay_session_id: Option<String>,
162 pub issued_at: f64,
163 pub backchannel_logout_uri: Option<String>,
164}
165
166#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
171pub struct ConsentGrant {
172 pub user_id: String,
173 pub client_id: String,
174 pub scopes: Vec<String>,
175 pub granted_at: f64,
176}
177
178#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
182pub struct UpstreamLoginState {
183 pub state: String,
184 pub provider_slug: String,
185 pub nonce: String,
186 pub pkce_verifier: String,
187 pub return_to: Option<String>,
188 pub created_at: f64,
189 pub expires_at: f64,
190}
191
192#[cfg(test)]
193mod tests {
194 use super::*;
195
196 #[test]
197 fn token_auth_method_round_trip() {
198 for m in [
199 TokenAuthMethod::ClientSecretBasic,
200 TokenAuthMethod::ClientSecretPost,
201 TokenAuthMethod::None,
202 TokenAuthMethod::PrivateKeyJwt,
203 ] {
204 assert_eq!(TokenAuthMethod::parse(m.as_str()), Some(m));
205 }
206 assert_eq!(TokenAuthMethod::parse("garbage"), None);
207 }
208
209 #[test]
210 fn oidc_client_new_defaults_to_confidential_pkce() {
211 let c = OidcClient::new("c1", "App", 1.0);
212 assert_eq!(c.client_id, "c1");
213 assert!(c.pkce_required);
214 assert!(c.require_consent);
215 assert_eq!(c.token_endpoint_auth_method, TokenAuthMethod::ClientSecretBasic);
216 assert!(c.allows_grant("authorization_code"));
217 assert!(c.allows_grant("refresh_token"));
218 assert!(!c.allows_grant("client_credentials"));
219 }
220
221 #[test]
222 fn redirect_matches_is_exact() {
223 let mut c = OidcClient::new("c1", "App", 0.0);
224 c.redirect_uris = vec!["https://app.example.com/cb".to_string()];
225 assert!(c.redirect_matches("https://app.example.com/cb"));
226 assert!(!c.redirect_matches("https://app.example.com/cb/"));
228 assert!(!c.redirect_matches("https://app.example.com/cb/extra"));
230 }
231}