···1+use std::collections::HashMap;
2+3+use crate::{config::OAuthBackendConfig, contextual_error, select_template, storage::identity_profile::{handle_for_did, identity_profile_set_email}};
4+use anyhow::{Context, Result, anyhow};
5use axum::{
6 extract::State,
7 response::{IntoResponse, Redirect},
8};
9use axum_extra::extract::{
010 Form, PrivateCookieJar,
11+ cookie::{Cookie, SameSite},
12};
13use minijinja::context as template_context;
14use serde::{Deserialize, Serialize};
···16use super::{
17 context::WebContext,
18 errors::{LoginError, WebError},
19+ middleware_auth::{AUTH_COOKIE_NAME, WebSession},
20 middleware_i18n::Language,
21};
22···124125 let token_response = token_response.unwrap();
126127+ let identity_profile = handle_for_did(&web_context.pool, &oauth_request.did).await?;
128+129+ let maybe_email = get_email_from_userinfo(&web_context.http_client, hostname, &identity_profile.did, &token_response.access_token).await;
130+ let maybe_email = match maybe_email {
131+ Ok(value) => value,
132+ Err(err) => {
133+ tracing::error!(error = ?err, "error getting AIP userinfo");
134+ None
135+ }
136+ };
137+ if let Some(email) = maybe_email {
138+ // Write the email address to the database if it already isn't in the database.
139+ // Only set if the identity_profile's email field is None (not even an empty string)
140+ if identity_profile.email.is_none() {
141+ if let Err(err) = identity_profile_set_email(&web_context.pool, &oauth_request.did, Some(&email)).await {
142+ tracing::error!(error = ?err, "Failed to set email from OAuth userinfo");
143+ }
144+ }
145+ }
146+147 let cookie_value: String = WebSession::Aip {
148 did: oauth_request.did.clone(),
149 access_token: token_response.access_token.clone(),
···169170 Ok((updated_jar, Redirect::to("/")).into_response())
171}
172+173+#[derive(Clone, Deserialize)]
174+pub struct OpenIDClaims {
175+ pub sub: String,
176+177+ #[serde(skip_serializing_if = "Option::is_none")]
178+ pub email: Option<String>,
179+180+ #[serde(flatten)]
181+ pub additional_claims: HashMap<String, serde_json::Value>,
182+}
183+184+async fn get_email_from_userinfo(
185+ http_client: &reqwest::Client,
186+ aip_server: &str,
187+ did: &str,
188+ aip_access_token: &str,
189+) -> Result<Option<String>> {
190+ let userinfo_endpoint = format!("{}/oauth/userinfo", aip_server);
191+192+ let response: OpenIDClaims = http_client
193+ .get(userinfo_endpoint)
194+ .bearer_auth(aip_access_token)
195+ .send()
196+ .await
197+ .context(anyhow!("HTTP request for userinfo failed"))?
198+ .json()
199+ .await
200+ .context(anyhow!("Parsing HTTP response for userinfo failed"))?;
201+202+ if response.sub != did {
203+ return Err(anyhow!("DID does not match userinfo subject"));
204+ }
205+206+ Ok(response.email)
207+}
+1-1
src/http/handle_oauth_aip_login.rs
···158 state: state.clone(),
159 nonce: nonce.clone(),
160 code_challenge,
161- scope: "atproto:atproto atproto:transition:generic".to_string(),
162 };
163164 // Get AIP server configuration - config validation ensures these are set when oauth_backend is AIP
···158 state: state.clone(),
159 nonce: nonce.clone(),
160 code_challenge,
161+ scope: "openid profile email atproto:atproto atproto:transition:generic atproto:transition:email".to_string(),
162 };
163164 // Get AIP server configuration - config validation ensures these are set when oauth_backend is AIP