Skip to content

Commit e305b89

Browse files
committed
Implement oidc code exchange and token storage
1 parent 938f51c commit e305b89

File tree

1 file changed

+72
-48
lines changed

1 file changed

+72
-48
lines changed

src/webserver/oidc.rs

Lines changed: 72 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,17 @@ use crate::{app_config::AppConfig, AppState};
44
use actix_web::{
55
dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform},
66
middleware::Condition,
7-
web, Error, HttpResponse,
7+
web::{self, Query},
8+
Error, HttpResponse,
89
};
910
use anyhow::{anyhow, Context};
1011
use awc::Client;
1112
use openidconnect::{
1213
core::{CoreAuthDisplay, CoreAuthenticationFlow},
1314
AsyncHttpClient, CsrfToken, EmptyAdditionalClaims, EndpointMaybeSet, EndpointNotSet,
14-
EndpointSet, IssuerUrl, Nonce, RedirectUrl, Scope,
15+
EndpointSet, IssuerUrl, Nonce, OAuth2TokenResponse, RedirectUrl, Scope, TokenResponse,
1516
};
17+
use serde::Deserialize;
1618

1719
use super::http_client::make_http_client;
1820

@@ -97,12 +99,11 @@ impl OidcMiddleware {
9799
}
98100

99101
async fn discover_provider_metadata(
100-
app_config: &AppConfig,
102+
http_client: &AwcHttpClient,
101103
issuer_url: IssuerUrl,
102104
) -> anyhow::Result<openidconnect::core::CoreProviderMetadata> {
103-
let http_client = AwcHttpClient::new(app_config)?;
104105
let provider_metadata =
105-
openidconnect::core::CoreProviderMetadata::discover_async(issuer_url, &http_client).await?;
106+
openidconnect::core::CoreProviderMetadata::discover_async(issuer_url, http_client).await?;
106107
Ok(provider_metadata)
107108
}
108109

@@ -141,7 +142,8 @@ pub struct OidcService<S> {
141142
service: S,
142143
app_state: web::Data<AppState>,
143144
config: Arc<OidcConfig>,
144-
client: Arc<OidcClient>,
145+
oidc_client: Arc<OidcClient>,
146+
http_client: Arc<AwcHttpClient>,
145147
}
146148

147149
impl<S> OidcService<S> {
@@ -151,19 +153,21 @@ impl<S> OidcService<S> {
151153
config: Arc<OidcConfig>,
152154
) -> anyhow::Result<Self> {
153155
let issuer_url = config.issuer_url.clone();
154-
let provider_metadata = discover_provider_metadata(&app_state.config, issuer_url).await?;
156+
let http_client = AwcHttpClient::new(&app_state.config)?;
157+
let provider_metadata = discover_provider_metadata(&http_client, issuer_url).await?;
155158
let client: OidcClient = make_oidc_client(&config, provider_metadata)?;
156159
Ok(Self {
157160
service,
158161
app_state: web::Data::clone(app_state),
159162
config,
160-
client: Arc::new(client),
163+
oidc_client: Arc::new(client),
164+
http_client: Arc::new(http_client),
161165
})
162166
}
163167

164168
fn build_auth_url(&self, request: &ServiceRequest) -> String {
165169
let (auth_url, csrf_token, nonce) = self
166-
.client
170+
.oidc_client
167171
.authorize_url(
168172
CoreAuthenticationFlow::AuthorizationCode,
169173
CsrfToken::new_random,
@@ -192,54 +196,20 @@ impl<S> OidcService<S> {
192196
&self,
193197
request: ServiceRequest,
194198
) -> LocalBoxFuture<Result<ServiceResponse<BoxBody>, Error>> {
195-
let client = Arc::clone(&self.client);
199+
let oidc_client = Arc::clone(&self.oidc_client);
200+
let http_client = Arc::clone(&self.http_client);
196201

197202
Box::pin(async move {
198203
let query_string = request.query_string();
199-
let result = Self::process_oidc_callback(&client, &query_string).await;
200-
match result {
204+
match process_oidc_callback(&oidc_client, &http_client, query_string).await {
201205
Ok(response) => Ok(request.into_response(response)),
202206
Err(e) => {
203-
log::error!("Failed to process OIDC callback: {}", e);
204-
Ok(request
205-
.into_response(HttpResponse::BadRequest().body("Authentication failed")))
207+
log::error!("Failed to process OIDC callback with params {query_string}: {e}");
208+
Ok(request.into_response(HttpResponse::BadRequest().body(e.to_string())))
206209
}
207210
}
208211
})
209212
}
210-
211-
async fn process_oidc_callback(
212-
client: &Arc<OidcClient>,
213-
query_params: &str,
214-
) -> anyhow::Result<HttpResponse> {
215-
let token_response = Self::exchange_code_for_token(client, query_params).await?;
216-
let mut response = build_redirect_response(format!("/"));
217-
Self::set_auth_cookie(&mut response, &token_response);
218-
Ok(response)
219-
}
220-
221-
async fn exchange_code_for_token(
222-
client: &Arc<OidcClient>,
223-
query_string: &str,
224-
) -> anyhow::Result<openidconnect::core::CoreTokenResponse> {
225-
todo!("Extract 'code' and 'state' from query_string");
226-
todo!("Verify the state matches the expected CSRF token");
227-
todo!("Use client.exchange_code() to get the token response");
228-
}
229-
230-
fn set_auth_cookie(
231-
response: &mut HttpResponse,
232-
token_response: &openidconnect::core::CoreTokenResponse,
233-
) {
234-
// Extract token information (access token, id token, etc.)
235-
todo!("Extract access_token and id_token from token_response");
236-
237-
// Create a secure cookie with the token information
238-
todo!("Create a cookie with token information");
239-
240-
// Add the cookie to the response
241-
todo!("response.cookie() to add the cookie to the response");
242-
}
243213
}
244214

245215
type LocalBoxFuture<T> = Pin<Box<dyn Future<Output = T> + 'static>>;
@@ -275,6 +245,54 @@ where
275245
}
276246
}
277247

248+
async fn process_oidc_callback(
249+
oidc_client: &Arc<OidcClient>,
250+
http_client: &Arc<AwcHttpClient>,
251+
query_string: &str,
252+
) -> anyhow::Result<HttpResponse> {
253+
let params = Query::<OidcCallbackParams>::from_query(query_string)?.into_inner();
254+
let token_response = exchange_code_for_token(oidc_client, http_client, params).await?;
255+
let mut response = build_redirect_response(format!("/"));
256+
set_auth_cookie(&mut response, &token_response)?;
257+
Ok(response)
258+
}
259+
260+
async fn exchange_code_for_token(
261+
oidc_client: &OidcClient,
262+
http_client: &AwcHttpClient,
263+
oidc_callback_params: OidcCallbackParams,
264+
) -> anyhow::Result<openidconnect::core::CoreTokenResponse> {
265+
// TODO: Verify the state matches the expected CSRF token
266+
let token_response = oidc_client
267+
.exchange_code(openidconnect::AuthorizationCode::new(
268+
oidc_callback_params.code,
269+
))?
270+
.request_async(http_client)
271+
.await?;
272+
Ok(token_response)
273+
}
274+
275+
fn set_auth_cookie(
276+
response: &mut HttpResponse,
277+
token_response: &openidconnect::core::CoreTokenResponse,
278+
) -> anyhow::Result<()> {
279+
let access_token = token_response.access_token();
280+
log::debug!("Received access token: {}", access_token.secret());
281+
let id_token = token_response
282+
.id_token()
283+
.context("No ID token found in the token response. You may have specified an oauth2 provider that does not support OIDC.")?;
284+
285+
let cookie = actix_web::cookie::Cookie::build(SQLPAGE_AUTH_COOKIE_NAME, id_token.to_string())
286+
.secure(true)
287+
.http_only(true)
288+
.same_site(actix_web::cookie::SameSite::Lax)
289+
.path("/")
290+
.finish();
291+
292+
response.add_cookie(&cookie).unwrap();
293+
Ok(())
294+
}
295+
278296
fn build_redirect_response(target_url: String) -> HttpResponse {
279297
HttpResponse::TemporaryRedirect()
280298
.append_header(("Location", target_url))
@@ -404,3 +422,9 @@ fn make_oidc_client(
404422

405423
Ok(client)
406424
}
425+
426+
#[derive(Debug, Deserialize)]
427+
struct OidcCallbackParams {
428+
code: String,
429+
state: String,
430+
}

0 commit comments

Comments
 (0)