1use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct OidcConfig {
8 pub issuer: String,
10 pub client_id: String,
12 pub client_secret: String,
14 pub redirect_uri: String,
16 pub scope: Vec<String>,
18}
19
20#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct OidcDiscovery {
23 pub issuer: String,
25 pub authorization_endpoint: String,
27 pub token_endpoint: String,
29 pub userinfo_endpoint: String,
31 pub jwks_uri: String,
33}
34
35#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct OidcTokenResponse {
38 pub access_token: String,
40 pub token_type: String,
42 #[serde(skip_serializing_if = "Option::is_none")]
44 pub expires_in: Option<u64>,
45 #[serde(skip_serializing_if = "Option::is_none")]
47 pub refresh_token: Option<String>,
48 #[serde(skip_serializing_if = "Option::is_none")]
50 pub id_token: Option<String>,
51}
52
53#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct OidcUserInfo {
56 pub sub: String,
58 #[serde(skip_serializing_if = "Option::is_none")]
60 pub email: Option<String>,
61 #[serde(skip_serializing_if = "Option::is_none")]
63 pub name: Option<String>,
64 #[serde(skip_serializing_if = "Option::is_none")]
66 pub given_name: Option<String>,
67 #[serde(skip_serializing_if = "Option::is_none")]
69 pub family_name: Option<String>,
70 #[serde(skip_serializing_if = "Option::is_none")]
72 pub picture: Option<String>,
73}
74
75#[derive(Debug, thiserror::Error)]
77pub enum OidcError {
78 #[error("discovery failed: {0}")]
80 DiscoveryFailed(String),
81
82 #[error("token exchange failed: {0}")]
84 TokenExchangeFailed(String),
85
86 #[error("userinfo fetch failed: {0}")]
88 UserInfoFailed(String),
89
90 #[error("HTTP error: {0}")]
92 HttpError(String),
93}
94
95pub async fn fetch_discovery(
97 client: &reqwest::Client,
98 issuer: &str,
99) -> Result<OidcDiscovery, OidcError> {
100 let url = format!(
101 "{}/.well-known/openid-configuration",
102 issuer.trim_end_matches('/')
103 );
104
105 let resp = client
106 .get(&url)
107 .send()
108 .await
109 .map_err(|e| OidcError::DiscoveryFailed(e.to_string()))?;
110
111 let discovery: OidcDiscovery = resp
112 .json()
113 .await
114 .map_err(|e| OidcError::DiscoveryFailed(e.to_string()))?;
115
116 Ok(discovery)
117}
118
119pub async fn exchange_code(
123 client: &reqwest::Client,
124 token_endpoint: &str,
125 config: &OidcConfig,
126 code: &str,
127 code_verifier: Option<&str>,
128) -> Result<OidcTokenResponse, OidcError> {
129 let mut params = vec![
130 ("grant_type", "authorization_code".to_string()),
131 ("code", code.to_string()),
132 ("redirect_uri", config.redirect_uri.clone()),
133 ("client_id", config.client_id.clone()),
134 ("client_secret", config.client_secret.clone()),
135 ];
136
137 if let Some(verifier) = code_verifier {
138 params.push(("code_verifier", verifier.to_string()));
139 }
140
141 let resp = client
142 .post(token_endpoint)
143 .form(¶ms)
144 .send()
145 .await
146 .map_err(|e| OidcError::HttpError(e.to_string()))?;
147
148 let token_resp: OidcTokenResponse = resp
149 .json()
150 .await
151 .map_err(|e| OidcError::TokenExchangeFailed(e.to_string()))?;
152
153 Ok(token_resp)
154}
155
156pub async fn fetch_user_info(
158 client: &reqwest::Client,
159 userinfo_endpoint: &str,
160 access_token: &str,
161) -> Result<OidcUserInfo, OidcError> {
162 let resp = client
163 .get(userinfo_endpoint)
164 .bearer_auth(access_token)
165 .send()
166 .await
167 .map_err(|e| OidcError::HttpError(e.to_string()))?;
168
169 let user_info: OidcUserInfo = resp
170 .json()
171 .await
172 .map_err(|e| OidcError::UserInfoFailed(e.to_string()))?;
173
174 Ok(user_info)
175}
176
177pub async fn refresh_token(
179 client: &reqwest::Client,
180 token_endpoint: &str,
181 client_id: &str,
182 client_secret: &str,
183 refresh_token: &str,
184) -> Result<OidcTokenResponse, OidcError> {
185 let resp = client
186 .post(token_endpoint)
187 .form(&[
188 ("grant_type", "refresh_token"),
189 ("client_id", client_id),
190 ("client_secret", client_secret),
191 ("refresh_token", refresh_token),
192 ])
193 .send()
194 .await
195 .map_err(|e| OidcError::HttpError(e.to_string()))?;
196
197 let token_resp: OidcTokenResponse = resp
198 .json()
199 .await
200 .map_err(|e| OidcError::TokenExchangeFailed(e.to_string()))?;
201
202 Ok(token_resp)
203}
204
205pub fn end_session_url(issuer: &str, client_id: &str, post_logout_redirect: &str) -> String {
207 format!(
208 "{}/logout?client_id={}&post_logout_redirect_uri={}",
209 issuer.trim_end_matches('/'),
210 urlencoding::encode(client_id),
211 urlencoding::encode(post_logout_redirect),
212 )
213}