auth works, getting email not yet
This commit is contained in:
parent
663af59488
commit
5eabc89280
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -2287,6 +2287,7 @@ dependencies = [
|
||||||
"futures",
|
"futures",
|
||||||
"indexmap 2.2.6",
|
"indexmap 2.2.6",
|
||||||
"listenfd",
|
"listenfd",
|
||||||
|
"log",
|
||||||
"openidconnect",
|
"openidconnect",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
|
|
@ -15,6 +15,7 @@ eyre = "0.6.12"
|
||||||
futures = "0.3.30"
|
futures = "0.3.30"
|
||||||
indexmap = { version = "2.2.6", features = ["serde"] }
|
indexmap = { version = "2.2.6", features = ["serde"] }
|
||||||
listenfd = "1.0.1"
|
listenfd = "1.0.1"
|
||||||
|
log = "0.4.21"
|
||||||
openidconnect = "3.5.0"
|
openidconnect = "3.5.0"
|
||||||
serde = { version = "1.0.197", features = ["derive"] }
|
serde = { version = "1.0.197", features = ["derive"] }
|
||||||
serde_json = "1.0.115"
|
serde_json = "1.0.115"
|
||||||
|
|
|
@ -3,6 +3,7 @@ pretty_name = "Google"
|
||||||
issuer_url = "https://accounts.google.com"
|
issuer_url = "https://accounts.google.com"
|
||||||
client_id = "<TODO>"
|
client_id = "<TODO>"
|
||||||
secret = "<TODO>"
|
secret = "<TODO>"
|
||||||
|
redirect_url = "https://my-site/subscription/callback/google"
|
||||||
scopes = ["email"]
|
scopes = ["email"]
|
||||||
|
|
||||||
[oidc.debian-salsa]
|
[oidc.debian-salsa]
|
||||||
|
@ -10,4 +11,5 @@ pretty_name = "Debian Salsa"
|
||||||
issuer_url = "https://salsa.debian.org"
|
issuer_url = "https://salsa.debian.org"
|
||||||
client_id = "<TODO>"
|
client_id = "<TODO>"
|
||||||
secret = "<TODO>"
|
secret = "<TODO>"
|
||||||
|
redirect_url = "https://my-site/subscription/callback/debian-salsa"
|
||||||
scopes = ["email"]
|
scopes = ["email"]
|
||||||
|
|
74
src/app.rs
74
src/app.rs
|
@ -5,16 +5,18 @@ use actix_web::{
|
||||||
get,
|
get,
|
||||||
http::{
|
http::{
|
||||||
header::{ContentType, LOCATION},
|
header::{ContentType, LOCATION},
|
||||||
|
uri::PathAndQuery,
|
||||||
StatusCode, Uri,
|
StatusCode, Uri,
|
||||||
},
|
},
|
||||||
web::{self, ServiceConfig},
|
web::{self, ServiceConfig},
|
||||||
HttpResponse, HttpResponseBuilder, Responder,
|
HttpResponse, HttpResponseBuilder, Responder,
|
||||||
};
|
};
|
||||||
use openidconnect::{
|
use openidconnect::{
|
||||||
core::CoreAuthenticationFlow, http::HeaderValue, CsrfToken, EndUserEmail, Nonce,
|
core::CoreAuthenticationFlow, http::HeaderValue, reqwest::async_http_client, url::Url,
|
||||||
|
AuthorizationCode, CsrfToken, EndUserEmail, Nonce, RedirectUrl, TokenResponse,
|
||||||
};
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::{fmt::Write, marker::PhantomData};
|
use std::{borrow::Cow, fmt::Write, marker::PhantomData};
|
||||||
use tinytemplate::TinyTemplate;
|
use tinytemplate::TinyTemplate;
|
||||||
|
|
||||||
pub trait DynTemplate {
|
pub trait DynTemplate {
|
||||||
|
@ -125,12 +127,76 @@ impl OIDCProvider {}
|
||||||
const NONCE: &'static str = "nonce";
|
const NONCE: &'static str = "nonce";
|
||||||
const CSRF_TOKEN: &'static str = "csrf_token";
|
const CSRF_TOKEN: &'static str = "csrf_token";
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct AuthQuery {
|
||||||
|
code: AuthorizationCode,
|
||||||
|
state: CsrfToken,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/subscription/callback/{provider}")]
|
||||||
|
pub async fn callback(
|
||||||
|
config: web::Data<Config>,
|
||||||
|
session: Session,
|
||||||
|
path: web::Path<(String,)>,
|
||||||
|
query: web::Query<AuthQuery>,
|
||||||
|
) -> impl Responder {
|
||||||
|
let mut resp = HttpResponse::SeeOther().body("");
|
||||||
|
resp.headers_mut()
|
||||||
|
.insert(LOCATION, "/subscription".parse().unwrap());
|
||||||
|
if SessionState::get(&session).is_some() {
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
let Ok(Some(csrf_token)) = session.get::<CsrfToken>(CSRF_TOKEN) else {
|
||||||
|
return resp;
|
||||||
|
};
|
||||||
|
let Ok(Some(nonce)) = session.get::<Nonce>(NONCE) else {
|
||||||
|
return resp;
|
||||||
|
};
|
||||||
|
session.remove(CSRF_TOKEN);
|
||||||
|
session.remove(NONCE);
|
||||||
|
if csrf_token.secret() != query.state.secret() {
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
let Some(provider) = config.oidc.get(&path.0) else {
|
||||||
|
return resp;
|
||||||
|
};
|
||||||
|
let token = match provider
|
||||||
|
.state()
|
||||||
|
.client
|
||||||
|
.exchange_code(query.into_inner().code)
|
||||||
|
.request_async(async_http_client)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(token) => token,
|
||||||
|
Err(e) => {
|
||||||
|
log::warn!("Error getting token: {e}");
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
dbg!(&token);
|
||||||
|
let claims = match token
|
||||||
|
.id_token()
|
||||||
|
.unwrap()
|
||||||
|
.claims(&provider.state().client.id_token_verifier(), &nonce)
|
||||||
|
{
|
||||||
|
Ok(claims) => claims,
|
||||||
|
Err(e) => {
|
||||||
|
log::warn!("Error verifying token: {e}");
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let Some(email) = claims.email().cloned() else {
|
||||||
|
return resp;
|
||||||
|
};
|
||||||
|
SessionState { email }.set(&session);
|
||||||
|
resp
|
||||||
|
}
|
||||||
|
|
||||||
#[get("/subscription/login/{provider}")]
|
#[get("/subscription/login/{provider}")]
|
||||||
pub async fn login(
|
pub async fn login(
|
||||||
config: web::Data<Config>,
|
config: web::Data<Config>,
|
||||||
session: Session,
|
session: Session,
|
||||||
path: web::Path<(String,)>,
|
path: web::Path<(String,)>,
|
||||||
uri: Uri,
|
|
||||||
) -> impl Responder {
|
) -> impl Responder {
|
||||||
let mut resp = HttpResponse::SeeOther().body("");
|
let mut resp = HttpResponse::SeeOther().body("");
|
||||||
resp.headers_mut()
|
resp.headers_mut()
|
||||||
|
@ -179,7 +245,7 @@ pub async fn subscription(config: web::Data<Config>, session: Session) -> impl R
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn all_services(cfg: &mut ServiceConfig) {
|
pub fn all_services(cfg: &mut ServiceConfig) {
|
||||||
cfg.service(subscription).service(login);
|
cfg.service(subscription).service(login).service(callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|
|
@ -5,7 +5,7 @@ use futures::future::try_join_all;
|
||||||
use indexmap::IndexMap;
|
use indexmap::IndexMap;
|
||||||
use openidconnect::{
|
use openidconnect::{
|
||||||
core::{CoreClient, CoreProviderMetadata},
|
core::{CoreClient, CoreProviderMetadata},
|
||||||
ClientId, ClientSecret, IssuerUrl, Scope,
|
ClientId, ClientSecret, IssuerUrl, RedirectUrl, Scope,
|
||||||
};
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
@ -55,6 +55,7 @@ pub struct OIDCProvider {
|
||||||
pub issuer_url: IssuerUrl,
|
pub issuer_url: IssuerUrl,
|
||||||
pub client_id: ClientId,
|
pub client_id: ClientId,
|
||||||
pub secret: ClientSecret,
|
pub secret: ClientSecret,
|
||||||
|
pub redirect_url: RedirectUrl,
|
||||||
pub scopes: Vec<Scope>,
|
pub scopes: Vec<Scope>,
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
state: Option<OIDCProviderState>,
|
state: Option<OIDCProviderState>,
|
||||||
|
@ -76,6 +77,7 @@ impl OIDCProvider {
|
||||||
Some(self.secret.clone()),
|
Some(self.secret.clone()),
|
||||||
);
|
);
|
||||||
let client = client.disable_openid_scope();
|
let client = client.disable_openid_scope();
|
||||||
|
let client = client.set_redirect_uri(self.redirect_url.clone());
|
||||||
self.state = Some(OIDCProviderState {
|
self.state = Some(OIDCProviderState {
|
||||||
client,
|
client,
|
||||||
provider_metadata,
|
provider_metadata,
|
||||||
|
|
Loading…
Reference in a new issue