use std::{collections::HashMap, net::Ipv4Addr};
use kynos::{
error::rejection::AuthRejection,
prelude::*,
security::{
Authenticates, Authenticator,
auth::{Auth, MaybeAuth, Scoped, Scopes},
carrier::{BearerToken, Carries},
schemes::{Basic, Credentials, MutualTls},
},
server::Server,
};
#[derive(Clone, Debug)]
struct Claims {
subject: String,
scopes: Vec<String>,
}
#[derive(SecurityScheme)]
#[security(bearer(format = "JWT"))]
#[security(credential = Claims, description = "A short-lived access token")]
struct AccessToken;
#[derive(SecurityScheme)]
#[security(api_key(in = "header", name = "X-Api-Key"))]
#[security(name = "ServiceKey", description = "Issued per integration")]
struct ServiceKey;
#[derive(SecurityScheme)]
#[security(api_key(in = "cookie", name = "session"))]
struct SessionCookie;
#[derive(SecurityScheme)]
#[security(basic)]
#[security(credential = Credentials, challenge = "Basic realm=\"admin\", charset=\"UTF-8\"")]
struct AdminLogin;
#[derive(SecurityScheme)]
#[security(mutual_tls)]
#[security(credential = Vec<u8>, description = "A certificate issued by the partner CA")]
struct PartnerCertificate;
#[derive(SecurityScheme)]
#[security(oauth2(authorization_code(
authorization_url = "https://auth.example.com/authorize",
token_url = "https://auth.example.com/token",
refresh_url = "https://auth.example.com/token",
scopes("users:read" = "Read a user's profile", "users:write"),
),))]
#[security(name = "DelegatedAccess", scopes("users:read"))]
struct DelegatedAccess;
#[derive(SecurityScheme)]
#[security(openid_connect(url = "https://auth.example.com/.well-known/openid-configuration"))]
struct Federated;
struct Tokens {
issued: HashMap<&'static str, Claims>,
}
const INSUFFICIENT_SCOPE: &str = "https://errors.example.com/insufficient-scope";
impl Tokens {
fn seeded() -> Self {
let mut issued = HashMap::new();
issued.insert(
"tok_reader",
Claims {
subject: "user-1".to_owned(),
scopes: vec!["reports:read".to_owned()],
},
);
issued.insert(
"tok_plain",
Claims {
subject: "user-2".to_owned(),
scopes: Vec::new(),
},
);
Self { issued }
}
}
impl<C: Sync> Authenticator<AccessToken, C> for Tokens {
async fn authenticate(
&self,
presented: BearerToken,
context: &C,
) -> Result<Claims, AuthRejection> {
let _ = context;
self.issued
.get(presented.as_str())
.cloned()
.ok_or_else(AuthRejection::unauthenticated)
}
async fn authorize(
&self,
credential: &Claims,
scopes: &'static [&'static str],
context: &C,
) -> Result<(), AuthRejection> {
let _ = context;
if scopes
.iter()
.all(|demanded| credential.scopes.iter().any(|held| held == demanded))
{
Ok(())
} else {
Err(AuthRejection::forbidden_as(INSUFFICIENT_SCOPE))
}
}
}
struct Rejects;
impl<S: Carries, C: Sync> Authenticator<S, C> for Rejects
where
S::Credential: Sync,
{
async fn authenticate(
&self,
presented: S::Presented,
context: &C,
) -> Result<S::Credential, AuthRejection> {
let _ = (presented, context);
Err(AuthRejection::unauthenticated())
}
async fn authorize(
&self,
credential: &S::Credential,
scopes: &'static [&'static str],
context: &C,
) -> Result<(), AuthRejection> {
let _ = (credential, scopes, context);
Err(AuthRejection::forbidden())
}
}
struct App {
tokens: Tokens,
verifier: Rejects,
}
impl Authenticates<AccessToken> for App {
type Authenticator = Tokens;
fn authenticator(&self) -> &Self::Authenticator {
&self.tokens
}
}
macro_rules! verifies {
($($scheme:ty),+ $(,)?) => {
$(
impl Authenticates<$scheme> for App {
type Authenticator = Rejects;
fn authenticator(&self) -> &Self::Authenticator {
&self.verifier
}
}
)+
};
}
verifies!(
ServiceKey,
SessionCookie,
Basic<Credentials>,
MutualTls,
DelegatedAccess,
Federated,
);
#[kynos::get("/me")]
async fn get_me(auth: Auth<AccessToken>) -> NoContent {
let _ = auth.into_inner().subject;
NoContent
}
#[kynos::get("/feed")]
async fn get_feed(caller: MaybeAuth<AccessToken>) -> NoContent {
if let Some(claims) = caller.into_inner() {
let _ = claims.subject;
}
NoContent
}
#[kynos::get("/usage")]
async fn get_usage(auth: Auth<ServiceKey>) -> NoContent {
let _ = auth.into_inner();
NoContent
}
#[kynos::get("/preferences")]
async fn get_preferences(auth: Auth<SessionCookie>) -> NoContent {
let _ = auth.into_inner();
NoContent
}
#[kynos::post("/admin/session")]
async fn admin_sign_in(auth: Auth<Basic<Credentials>>) -> NoContent {
let _ = auth.into_inner();
NoContent
}
#[kynos::post("/partners/events")]
async fn partner_event(auth: Auth<MutualTls>) -> NoContent {
let _ = auth.into_inner();
NoContent
}
#[kynos::get("/delegated/users")]
async fn delegated_users(auth: Auth<DelegatedAccess>) -> NoContent {
let _ = auth.into_inner();
NoContent
}
#[kynos::get("/federated/me")]
async fn federated_me(auth: Auth<Federated>) -> NoContent {
let _ = auth.into_inner();
NoContent
}
struct ReadReports;
impl Scopes for ReadReports {
const SCOPES: &'static [&'static str] = &["reports:read"];
const FORBIDDEN_TYPE: Option<&'static str> = Some(INSUFFICIENT_SCOPE);
}
#[kynos::get("/reports")]
async fn reports(caller: Scoped<AccessToken, ReadReports>) -> NoContent {
let _ = caller.into_inner();
NoContent
}
#[tokio::main]
async fn main() -> kynos::Result<()> {
let router = Router::<App>::new()
.security_scheme::<AdminLogin>()
.security_scheme::<PartnerCertificate>()
.mount(kynos::routes![
get_me,
get_feed,
get_usage,
get_preferences,
admin_sign_in,
partner_event,
delegated_users,
federated_me,
reports,
]);
let document = router.openapi()?;
println!("{}", document.to_json()?);
let context = App {
tokens: Tokens::seeded(),
verifier: Rejects,
};
Server::new(router.build(context)?)
.bind((Ipv4Addr::UNSPECIFIED, 3000))
.serve()
.await
}