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