potree_auth/authentication/adapters/authentication_engine/
oidc.rs1use async_trait::async_trait;
2use openidconnect::AdditionalClaims;
3use openidconnect::Client;
4use openidconnect::ClientId;
5use openidconnect::ClientSecret;
6use openidconnect::CsrfToken;
7use openidconnect::EmptyExtraTokenFields;
8use openidconnect::EndpointMaybeSet;
9use openidconnect::EndpointNotSet;
10use openidconnect::EndpointSet;
11use openidconnect::IdTokenFields;
12use openidconnect::IssuerUrl;
13use openidconnect::Nonce;
14use openidconnect::RedirectUrl;
15use openidconnect::StandardErrorResponse;
16use openidconnect::StandardTokenResponse;
17use openidconnect::TokenResponse;
18use openidconnect::core::CoreAuthDisplay;
19use openidconnect::core::CoreAuthPrompt;
20use openidconnect::core::CoreAuthenticationFlow;
21use openidconnect::core::CoreErrorResponseType;
22use openidconnect::core::CoreGenderClaim;
23use openidconnect::core::CoreJsonWebKey;
24use openidconnect::core::CoreJweContentEncryptionAlgorithm;
25use openidconnect::core::CoreJwsSigningAlgorithm;
26use openidconnect::core::CoreProviderMetadata;
27use openidconnect::core::CoreRevocableToken;
28use openidconnect::core::CoreRevocationErrorResponse;
29use openidconnect::core::CoreTokenIntrospectionResponse;
30use openidconnect::core::CoreTokenType;
31use serde::Deserialize;
32use serde::Serialize;
33use url::Url;
34
35use super::super::super::domain::AuthorizeData;
36use super::super::super::domain::CallbackRequestParams;
37use super::super::super::domain::OidcSessionPersisted;
38use super::super::super::ports::authentication_engine::AuthenticationEngine;
39use super::utils::extract_user_email;
40use super::utils::extract_user_groups;
41use super::utils::extract_user_id;
42use super::utils::extract_user_name;
43use crate::authentication::ports::authentication_engine::AuthenticationEngineError;
44use crate::user::domain::User;
45
46#[derive(Debug, Clone)]
47pub struct OidcAuthenticationEngine {
48 oidc_client: PotreeAuthClient,
50
51 groups_claim: String,
54}
55
56impl OidcAuthenticationEngine {
57 pub async fn new(
71 idp_url: Url,
72 redirect_url: Url,
73 client_id: String,
74 client_secret: String,
75 groups_claim: String,
76 ) -> Result<Self, AuthenticationEngineError> {
77 let idp_url = IssuerUrl::from_url(idp_url);
78 let redirect_url = RedirectUrl::from_url(redirect_url);
79 let client_id = ClientId::new(client_id);
80 let client_secret = ClientSecret::new(client_secret);
81
82 let http_client = reqwest::ClientBuilder::new()
84 .redirect(reqwest::redirect::Policy::none())
86 .build()
87 .map_err(|e| AuthenticationEngineError::Infrastructure {
88 message: format!("unable to build OIDC http client: {e}"),
89 })?;
90
91 let provider_metadata = CoreProviderMetadata::discover_async(idp_url, &http_client)
93 .await
94 .map_err(|e| AuthenticationEngineError::IdpExchange {
95 message: format!("unable to perform OIDC discovery: {e}"),
96 })?;
97
98 let oidc_client = PotreeAuthClient::from_provider_metadata(
99 provider_metadata,
100 client_id,
101 Some(client_secret),
102 )
103 .set_redirect_uri(redirect_url);
104
105 Ok(Self {
106 oidc_client,
107 groups_claim,
108 })
109 }
110
111 #[tracing::instrument]
117 async fn login(&self) -> Result<AuthorizeData, AuthenticationEngineError> {
118 let (auth_url, state, nonce) = self
119 .oidc_client
120 .authorize_url(
121 CoreAuthenticationFlow::AuthorizationCode,
122 CsrfToken::new_random,
123 Nonce::new_random,
124 )
125 .url();
126
127 Ok(AuthorizeData {
128 auth_url,
129 persisted_data: OidcSessionPersisted { state, nonce },
130 })
131 }
132
133 #[tracing::instrument]
137 async fn callback(
138 &self,
139 callback_params: CallbackRequestParams,
140 persisted_data: OidcSessionPersisted,
141 ) -> Result<User, AuthenticationEngineError> {
142 let http_client = reqwest::ClientBuilder::new()
144 .redirect(reqwest::redirect::Policy::none())
146 .build()
147 .map_err(|e| AuthenticationEngineError::Infrastructure {
148 message: format!("unable to build OIDC http client: {e}"),
149 })?;
150
151 if callback_params.state.secret() != persisted_data.state.secret() {
153 return Err(AuthenticationEngineError::Validation {
154 message: "the session and request `state`s do not match".to_owned(),
155 });
156 }
157
158 let token_response = self
160 .oidc_client
161 .exchange_code(callback_params.code)
162 .map_err(|e| AuthenticationEngineError::Infrastructure {
163 message: format!("unable to initialize OIDC code exchange client: {e}"),
164 })?
165 .request_async(&http_client)
166 .await
167 .map_err(|e| AuthenticationEngineError::IdpExchange {
168 message: format!("unable to perform token request: {e}"),
169 })?;
170
171 let id_token_claims = token_response
173 .id_token()
174 .ok_or(AuthenticationEngineError::Validation {
175 message: "IdP did not return id_token".to_owned(),
176 })?
177 .claims(&self.oidc_client.id_token_verifier(), &persisted_data.nonce)
178 .map_err(|err| AuthenticationEngineError::Validation {
179 message: format!("unable to extract claims from id_token: {err}"),
180 })?;
181
182 Ok(User {
183 id: extract_user_id(id_token_claims),
184 name: extract_user_name(id_token_claims)?,
185 email: extract_user_email(id_token_claims)?,
186 groups: extract_user_groups(id_token_claims, &self.groups_claim),
187 })
188 }
189}
190
191#[async_trait]
192impl AuthenticationEngine for OidcAuthenticationEngine {
193 async fn authorize(&self) -> Result<AuthorizeData, AuthenticationEngineError> {
194 Self::login(self).await
195 }
196
197 async fn callback(
198 &self,
199 callback_params: CallbackRequestParams,
200 persisted_data: OidcSessionPersisted,
201 ) -> Result<User, AuthenticationEngineError> {
202 Self::callback(self, callback_params, persisted_data).await
203 }
204}
205
206pub type PotreeAuthTokenFields = IdTokenFields<
207 PotreeAuthClaims,
208 EmptyExtraTokenFields,
209 CoreGenderClaim,
210 CoreJweContentEncryptionAlgorithm,
211 CoreJwsSigningAlgorithm,
212>;
213
214pub type PotreeAuthTokenResponse = StandardTokenResponse<PotreeAuthTokenFields, CoreTokenType>;
215
216type PotreeAuthClient<
217 HasAuthUrl = EndpointSet,
218 HasDeviceAuthUrl = EndpointNotSet,
219 HasIntrospectionUrl = EndpointNotSet,
220 HasRevocationUrl = EndpointNotSet,
221 HasUserInfoUrl = EndpointMaybeSet,
222 HasTokenUrl = EndpointMaybeSet,
223> = Client<
224 PotreeAuthClaims,
225 CoreAuthDisplay,
226 CoreGenderClaim,
227 CoreJweContentEncryptionAlgorithm,
228 CoreJsonWebKey,
229 CoreAuthPrompt,
230 StandardErrorResponse<CoreErrorResponseType>,
231 PotreeAuthTokenResponse,
232 CoreTokenIntrospectionResponse,
233 CoreRevocableToken,
234 CoreRevocationErrorResponse,
235 HasAuthUrl,
236 HasDeviceAuthUrl,
237 HasIntrospectionUrl,
238 HasRevocationUrl,
239 HasTokenUrl,
240 HasUserInfoUrl,
241>;
242
243#[derive(Debug, Clone, Deserialize, Serialize)]
249pub struct PotreeAuthClaims(pub serde_json::Value);
250
251impl AdditionalClaims for PotreeAuthClaims {}