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