use super::{
OAuthAuthenticatedClient, OAuthClientAuthenticationOptions, OAuthProviderConfig,
OAuthProviderPlugin, OAuthTokenIssueParams,
};
use alibi_core::endpoint::{
EndpointCall, EndpointContextPatch, EndpointDefinition, EndpointError, EndpointOptions,
EndpointResponse, with_endpoint_call_context,
};
use alibi_core::session::SessionRequest;
use alibi_core::store::OAuthClient;
use alibi_core::utils::json::JsValue;
use alibi_core::{AuthContext, AuthError, AuthResult, AuthSchema, CallbackContext, HttpMethod};
use async_trait::async_trait;
use serde_json::Value;
use std::sync::Arc;
#[derive(Clone, Debug, Default)]
pub struct OAuthProviderExtension {
pub grants: indexmap::IndexMap<String, Arc<dyn OAuthExtensionGrantHandler>>,
pub metadata: Option<Arc<dyn OAuthMetadataContributor>>,
pub claims: super::OAuthExtensionClaims,
pub client_authentication:
indexmap::IndexMap<String, super::OAuthExtensionClientAuthentication>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OAuthMetadataType {
OAuthAuthorizationServer,
OpenIdConfiguration,
}
impl OAuthMetadataType {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::OAuthAuthorizationServer => "oauth-authorization-server",
Self::OpenIdConfiguration => "openid-configuration",
}
}
}
pub struct OAuthMetadataExtensionInput<'a> {
pub context: &'a CallbackContext,
pub options: &'a OAuthProviderConfig,
pub metadata_type: OAuthMetadataType,
pub document: &'a mut Value,
}
pub trait OAuthMetadataContributor: Send + Sync + std::fmt::Debug {
fn contribute(
&self,
input: &mut OAuthMetadataExtensionInput<'_>,
) -> AuthResult<serde_json::Map<String, Value>>;
}
pub struct OAuthExtensionGrantInput<'a> {
pub context: CallbackContext,
pub call: &'a EndpointCall,
pub options: &'a OAuthProviderConfig,
pub grant_type: &'a str,
pub provider: &'a dyn OAuthProviderApi,
}
#[async_trait]
pub trait OAuthExtensionGrantHandler: Send + Sync + std::fmt::Debug {
async fn handle(&self, input: OAuthExtensionGrantInput<'_>) -> Result<Value, EndpointError>;
}
#[async_trait]
pub trait OAuthProviderApi: Send + Sync {
async fn get_client(&self, client_id: &str) -> AuthResult<Option<OAuthClient>>;
async fn authenticate_client(
&self,
options: &OAuthClientAuthenticationOptions,
) -> Result<OAuthAuthenticatedClient, EndpointError>;
async fn hash_token(&self, token: &str, kind: &str) -> AuthResult<String>;
async fn issue_tokens(&self, params: &OAuthTokenIssueParams) -> Result<Value, EndpointError>;
async fn validate_access_token(
&self,
token: &str,
client_id: Option<&str>,
) -> Result<Value, EndpointError>;
async fn require_active_access_token(
&self,
token: &str,
client_id: Option<&str>,
) -> Result<Value, EndpointError>;
}
struct ProviderApi<'a, S: AuthSchema> {
provider: &'a OAuthProviderPlugin,
context: &'a AuthContext<S>,
call: &'a EndpointCall,
grant_type: Option<&'a str>,
}
#[async_trait]
impl<S: AuthSchema> OAuthProviderApi for ProviderApi<'_, S> {
async fn get_client(&self, id: &str) -> AuthResult<Option<OAuthClient>> {
self.provider.get_client(self.context, id).await
}
async fn authenticate_client(
&self,
options: &OAuthClientAuthenticationOptions,
) -> Result<OAuthAuthenticatedClient, EndpointError> {
self.provider
.authenticate_client(self.context, self.call, self.grant_type, options)
.await
}
async fn hash_token(&self, token: &str, kind: &str) -> AuthResult<String> {
self.provider.hash_token(token, kind).await
}
async fn issue_tokens(&self, params: &OAuthTokenIssueParams) -> Result<Value, EndpointError> {
self.provider
.issue_tokens(self.context, self.call, self.grant_type, params)
.await
}
async fn validate_access_token(
&self,
token: &str,
id: Option<&str>,
) -> Result<Value, EndpointError> {
self.provider
.validate_access_token(self.context, token, id)
.await
}
async fn require_active_access_token(
&self,
token: &str,
id: Option<&str>,
) -> Result<Value, EndpointError> {
self.provider
.require_active_access_token(self.context, token, id)
.await
}
}
impl OAuthProviderPlugin {
pub(super) fn apply_metadata_extensions<S: AuthSchema>(
&self,
context: &AuthContext<S>,
mut document: Value,
) -> AuthResult<Value> {
let mut next = document
.as_object()
.cloned()
.ok_or_else(|| AuthError::internal("OAuth metadata must be an object"))?;
let callback = CallbackContext::new(context, None);
let metadata_type = if self.config.scopes.iter().any(|scope| scope == "openid") {
OAuthMetadataType::OpenIdConfiguration
} else {
OAuthMetadataType::OAuthAuthorizationServer
};
for extension in &self.config.extensions {
if let Some(contributor) = &extension.metadata {
let mut input = OAuthMetadataExtensionInput {
context: &callback,
options: &self.config,
metadata_type,
document: &mut document,
};
for (key, value) in contributor.contribute(&mut input)? {
if !next.contains_key(&key)
&& !matches!(
key.as_str(),
"__proto__"
| "constructor"
| "toString"
| "toLocaleString"
| "valueOf"
| "hasOwnProperty"
| "isPrototypeOf"
| "propertyIsEnumerable"
| "__defineGetter__"
| "__defineSetter__"
| "__lookupGetter__"
| "__lookupSetter__"
)
{
_ = next.insert(key, value);
}
}
}
}
Ok(Value::Object(next))
}
pub fn get_oauth_provider_api<'a, S: AuthSchema>(
&'a self,
context: &'a AuthContext<S>,
call: &'a EndpointCall,
grant_type: Option<&'a str>,
) -> impl OAuthProviderApi + 'a {
ProviderApi {
provider: self,
context,
call,
grant_type,
}
}
pub(super) fn supported_grant_types(&self) -> Vec<String> {
super::unique(
self.config
.grant_types
.iter()
.cloned()
.chain(
self.config
.extensions
.iter()
.flat_map(|extension| extension.grants.keys().cloned()),
)
.collect(),
)
}
pub(super) fn validate_extensions(&self) -> AuthResult<()> {
let mut grants = Vec::new();
let mut auth_methods = Vec::new();
let mut assertion_types = Vec::new();
for extension in &self.config.extensions {
for grant in extension.grants.keys() {
if alibi_core::utils::javascript::trim(grant).is_empty() {
return Err(AuthError::config(
"OAuth Provider extension grant type cannot be empty",
));
}
if url::Url::parse(grant).is_err() {
return Err(AuthError::config(format!(
"OAuth Provider extension grant type must be an absolute URI: {grant}"
)));
}
grants.push(grant);
}
for (method, strategy) in &extension.client_authentication {
if alibi_core::utils::javascript::trim(method).is_empty() {
return Err(AuthError::config(
"OAuth Provider extension token_endpoint_auth_method cannot be empty",
));
}
if [
"none",
"client_secret_basic",
"client_secret_post",
"private_key_jwt",
]
.contains(&method.as_str())
{
return Err(AuthError::config(format!(
"OAuth Provider extension token_endpoint_auth_method is reserved: {method}"
)));
}
auth_methods.push(method);
let types = strategy
.assertion_types
.as_deref()
.unwrap_or_else(|| std::slice::from_ref(method));
if types.is_empty() {
return Err(AuthError::config(format!(
"OAuth Provider extension client_assertion_type list cannot be empty for {method}"
)));
}
for assertion_type in types {
if alibi_core::utils::javascript::trim(assertion_type).is_empty() {
return Err(AuthError::config(
"OAuth Provider extension client_assertion_type cannot be empty",
));
}
if url::Url::parse(assertion_type).is_err() {
return Err(AuthError::config(format!(
"OAuth Provider extension client_assertion_type must be an absolute URI: {assertion_type}"
)));
}
if assertion_type == "urn:ietf:params:oauth:client-assertion-type:jwt-bearer" {
return Err(AuthError::config(format!(
"OAuth Provider extension client_assertion_type is reserved: {assertion_type}"
)));
}
assertion_types.push(assertion_type);
}
}
}
for (label, values) in [
("grant type", grants),
("token_endpoint_auth_method", auth_methods),
("client_assertion_type", assertion_types),
] {
let mut seen = std::collections::HashSet::new();
for value in values {
if !seen.insert(value) {
return Err(AuthError::config(format!(
"OAuth Provider extensions register {label} \"{value}\" more than once. Extension contributions must be disjoint."
)));
}
}
}
Ok(())
}
pub(super) async fn extension_grant<S: AuthSchema>(
&self,
grant: &str,
input: Option<&JsValue>,
request: &impl SessionRequest,
context: &AuthContext<S>,
) -> AuthResult<Option<EndpointResponse>> {
let Some(handler) = self
.config
.extensions
.iter()
.find_map(|extension| extension.grants.get(grant))
.cloned()
else {
return Ok(None);
};
let mut call = request.endpoint_call().cloned().unwrap_or_else(|| {
EndpointCall::new(
&EndpointDefinition {
name: "oauth2Token",
operation_id: "oauth2Token",
path: Some("/oauth2/token".into()),
method: HttpMethod::Post,
},
input.cloned(),
None,
EndpointOptions {
method: Some(request.session_method().clone()),
headers: Some(request.session_headers().clone()),
request: request.physical_request().cloned(),
},
)
});
EndpointContextPatch {
body: Some(JsValue::Object(
[("grant_type".into(), JsValue::String(grant.into()))]
.into_iter()
.collect(),
)),
..Default::default()
}
.apply(&mut call);
let api = self.get_oauth_provider_api(context, &call, Some(grant));
let result = with_endpoint_call_context(call.clone(), async {
handler
.handle(OAuthExtensionGrantInput {
context: CallbackContext::new(context, request.physical_request()),
call: &call,
options: &self.config,
grant_type: grant,
provider: &api,
})
.await
})
.await;
let response = match result {
Ok(value) => EndpointResponse::json(&value)?,
Err(error) => {
let mut response = EndpointResponse::error(error.error);
if let Some(body) = error.body {
response = response.with_error_body(*body);
}
if error.body_absent {
response = response.without_error_body();
}
if let Some(headers) = error.headers {
response.merge_headers(headers);
}
response
}
};
Ok(Some(
response
.with_header("Cache-Control", "no-store")
.with_header("Pragma", "no-cache"),
))
}
}