Skip to main content

potree_auth/authentication/adapters/authentication_engine/
oidc.rs

1use 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    /// The oidc client that is performing interaction with the IdP.
49    oidc_client: PotreeAuthClient,
50
51    /// The name of the OIDC claim containing and array of
52    /// groups that a user is part of.
53    groups_claim: String,
54}
55
56impl OidcAuthenticationEngine {
57    /// Creates a new [`OidcAuthenticationService`] instance with the specified
58    /// `projects_directory`.
59    ///
60    /// # Arguments
61    ///
62    /// - `idp_url`: The URL to the IdP service.
63    /// - `redirect_url`: The URL to which the IdP should redirect the
64    ///   user-agent after successful authentication.
65    /// - `client_id`: The `id` of the application on the IdP.
66    /// - `client_secret`: The Authorization Code Flow client secret shared
67    ///   between the IdP and the application.
68    /// - `groups_claim`: The name of the OIDC claim containing and array of
69    ///   groups that a user is part of.
70    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        // Sets up an http client to interact with the IdP
83        let http_client = reqwest::ClientBuilder::new()
84            // Following redirects opens the client up to SSRF vulnerabilities.
85            .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        // Request the oidc config from the IdP.
92        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    /// Initiates an OIDC authentication flow with the IdP.
112    ///
113    /// Returns the `auth_url` needed to redirect the user agent to the IdP's
114    /// OIDC `/authorize` route. And the `persisted_data` that is needed to
115    /// complete the authentication flow in the callback handler.
116    #[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    /// Finalizes the OIDC authentication flow with the IdP.
134    ///
135    /// Returns the authenticated [`User`].
136    #[tracing::instrument]
137    async fn callback(
138        &self,
139        callback_params: CallbackRequestParams,
140        persisted_data: OidcSessionPersisted,
141    ) -> Result<User, AuthenticationEngineError> {
142        // Sets up an http client to interact with the IdP
143        let http_client = reqwest::ClientBuilder::new()
144            // Following redirects opens the client up to SSRF vulnerabilities.
145            .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        // Verify that state matches that from the session
152        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        // Request the tokens from the IdP
159        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        // Extract the claims from the id token.
172        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/// The claims from the IdP that are required to extract a [`User`].
244///
245/// The custom claims are required because we don't know the id token claims
246/// that represent their associated groups at compile time (i.e. they are in the
247/// config).
248#[derive(Debug, Clone, Deserialize, Serialize)]
249pub struct PotreeAuthClaims(pub serde_json::Value);
250
251impl AdditionalClaims for PotreeAuthClaims {}