1use crate::code_challenge::CodeChallenge;
4use crate::errors::{OpenIdError, Res};
5use crate::primitives::{OpenIDConfig, OpenIDTokenResponse, OpenIDUserInfo};
6use crate::utils::http_client;
7use jsonwebtoken::{DecodingKey, TokenData, Validation};
8use serde::de::DeserializeOwned;
9use std::collections::{HashMap, HashSet};
10use std::fmt::Display;
11use zeroize::Zeroizing;
12
13#[derive(Debug, Clone)]
15pub struct OpenIDClientOpts {
16 pub client_id: String,
18 pub client_secret: Option<Zeroizing<String>>,
20 pub redirect_uri: String,
22 pub validate_id_token_sig: bool,
24 pub accepted_audiences: Vec<String>,
26 pub validate_aud: bool,
28 pub validate_exp: bool,
30 pub validate_nbf: bool,
32}
33
34impl OpenIDClientOpts {
35 pub fn new(
36 client_id: impl Display,
37 client_secret: Option<&str>,
38 redirect_uri: impl Display,
39 ) -> Self {
40 Self {
41 client_id: client_id.to_string(),
42 client_secret: client_secret.map(|s| Zeroizing::new(s.to_string())),
43 redirect_uri: redirect_uri.to_string(),
44 accepted_audiences: vec![client_id.to_string()],
45 validate_id_token_sig: true,
46 validate_aud: true,
47 validate_exp: true,
48 validate_nbf: false,
49 }
50 }
51}
52
53enum TokenEndpointAuth<'a> {
54 AuthorizationCode(&'a str),
55 RefreshToken(&'a str),
56}
57
58#[derive(Clone)]
60pub struct OpenIDClient {
61 pub config: OpenIDConfig,
62 pub jwks: Option<jsonwebtoken::jwk::JwkSet>,
63 pub opts: OpenIDClientOpts,
64}
65
66impl OpenIDClient {
67 pub async fn new(
69 config: OpenIDConfig,
70 jwks: Option<jsonwebtoken::jwk::JwkSet>,
71 opts: &OpenIDClientOpts,
72 ) -> Self {
73 Self {
74 config,
75 jwks,
76 opts: opts.clone(),
77 }
78 }
79
80 #[tracing::instrument(skip(opts))]
83 pub async fn new_from_url(url: &str, opts: &OpenIDClientOpts) -> Res<Self> {
84 let config = http_client::get_json_request(url).await?;
85
86 let mut client = Self {
87 config,
88 jwks: None,
89 opts: opts.clone(),
90 };
91
92 if opts.validate_id_token_sig {
93 client.jwks = Some(http_client::get_json_request(&client.config.jwks_uri).await?)
94 }
95
96 Ok(client)
97 }
98
99 #[tracing::instrument(skip(self, state))]
101 pub fn gen_authorization_url(
102 &self,
103 state: &str,
104 code_challenge: Option<CodeChallenge>,
105 ) -> String {
106 let client_id = urlencoding::encode(self.opts.client_id.as_str());
107 let state = urlencoding::encode(state);
108 let redirect_uri = urlencoding::encode(self.opts.redirect_uri.as_str());
109
110 let mut url = format!(
111 "{}?response_type=code&scope=openid%20profile%20email&client_id={client_id}&state={state}&redirect_uri={redirect_uri}",
112 self.config.authorization_endpoint
113 );
114
115 if let Some(chlg) = code_challenge {
116 let code_challenge = urlencoding::encode(&chlg.code_challenge);
117 let code_challenge_method = urlencoding::encode(&chlg.code_challenge_method);
118
119 url.push_str(&format!(
120 "&code_challenge={code_challenge}&code_challenge_method={code_challenge_method}"
121 ))
122 }
123
124 url
125 }
126
127 #[tracing::instrument(skip(self, code, code_verifier))]
129 pub async fn request_token_from_code(
130 &self,
131 code: &str,
132 code_verifier: Option<&str>,
133 ) -> Res<(OpenIDTokenResponse, String)> {
134 self.request_token(&TokenEndpointAuth::AuthorizationCode(code), code_verifier)
135 .await
136 }
137
138 #[tracing::instrument(skip(self, refresh_token))]
140 pub async fn request_token_from_refresh_token(
141 &self,
142 refresh_token: &str,
143 ) -> Res<(OpenIDTokenResponse, String)> {
144 self.request_token(&TokenEndpointAuth::RefreshToken(refresh_token), None)
145 .await
146 }
147
148 async fn request_token(
153 &self,
154 auth: &TokenEndpointAuth<'_>,
155 code_verifier: Option<&str>,
156 ) -> Res<(OpenIDTokenResponse, String)> {
157 let mut params = HashMap::new();
158 match auth {
159 TokenEndpointAuth::AuthorizationCode(code) => {
160 params.insert("grant_type", "authorization_code");
161 params.insert("code", code);
162 }
163 TokenEndpointAuth::RefreshToken(token) => {
164 params.insert("grant_type", "refresh_token");
165 params.insert("refresh_token", token);
166 }
167 }
168 if let Some(verifier) = code_verifier {
169 params.insert("code_verifier", verifier);
170 }
171 params.insert("redirect_uri", self.opts.redirect_uri.as_str());
172
173 let response = http_client::send_request(
174 reqwest::Client::new()
175 .post(&self.config.token_endpoint)
176 .basic_auth(&self.opts.client_id, self.opts.client_secret.as_deref())
177 .form(¶ms),
178 )
179 .await?
180 .text()
181 .await?;
182
183 let token: OpenIDTokenResponse = serde_json::from_str(&response)?;
184
185 if self.opts.validate_id_token_sig
187 && self.jwks.is_some()
188 && let Some(id_token) = &token.id_token
189 {
190 self.verify_jwt::<serde_json::Value>(id_token)
191 .map_err(|e| OpenIdError::ValidateIdToken(Box::new(e)))?;
192 }
193
194 Ok((token, response))
195 }
196
197 pub fn verify_jwt<T: DeserializeOwned>(&self, jwt: &str) -> Res<TokenData<T>> {
199 let header = jsonwebtoken::decode_header(jwt).map_err(OpenIdError::DecodeJWTHeader)?;
200
201 let Some(kid) = header.kid else {
202 return Err(OpenIdError::KidRequiredForSignatureVerification);
203 };
204
205 let Some(jwks) = &self.jwks else {
206 return Err(OpenIdError::MissingJWKs);
207 };
208
209 let Some(jwk) = jwks.find(&kid) else {
210 return Err(OpenIdError::UnknownKid(kid));
211 };
212
213 let decoding_key = DecodingKey::from_jwk(jwk).map_err(OpenIdError::DecodeJWK)?;
214
215 let mut validation = Validation::new(header.alg);
216 validation.validate_aud = true;
217 validation.aud = Some(HashSet::from_iter(
218 self.opts.accepted_audiences.iter().cloned(),
219 ));
220 validation.validate_aud = self.opts.validate_aud;
221 validation.validate_exp = self.opts.validate_exp;
222 validation.validate_nbf = self.opts.validate_nbf;
223
224 jsonwebtoken::decode(jwt, &decoding_key, &validation).map_err(OpenIdError::ValidateJWT)
225 }
226
227 #[tracing::instrument(skip(self, token))]
234 pub async fn request_user_info(
235 &self,
236 token: &OpenIDTokenResponse,
237 ) -> Res<(OpenIDUserInfo, String)> {
238 let response = http_client::send_request(
239 reqwest::Client::new()
240 .get(self.config.userinfo_endpoint.as_ref().expect(
241 "This client only support information retrieval through userinfo endpoint!",
242 ))
243 .header("Authorization", format!("Bearer {}", token.access_token)),
244 )
245 .await?
246 .text()
247 .await?;
248
249 Ok((serde_json::from_str(&response)?, response))
250 }
251}