alibi_plugins/oauth_provider/
authentication.rs1use super::OAuthProviderPlugin;
3use alibi_core::endpoint::{
4 EndpointCall, EndpointError, EndpointResponse, with_endpoint_call_context,
5};
6use alibi_core::store::OAuthClient;
7use alibi_core::utils::json::{JsValue, parse_value};
8use alibi_core::{AuthContext, AuthError, AuthSchema};
9use serde_json::{Value, json};
10
11#[derive(Debug, Clone)]
13pub struct OAuthClientAuthenticationOptions {
14 pub require_credentials: bool,
15 pub scopes: Option<Vec<String>>,
16}
17impl Default for OAuthClientAuthenticationOptions {
18 fn default() -> Self {
19 Self {
20 require_credentials: true,
21 scopes: None,
22 }
23 }
24}
25#[derive(Debug, Clone)]
27pub struct OAuthAuthenticatedClient {
28 pub client_id: String,
29 pub client: OAuthClient,
30 pub method: Option<String>,
31 pub confirmation: Option<Value>,
32}
33impl OAuthProviderPlugin {
34 pub async fn authenticate_client<S: AuthSchema>(
38 &self,
39 ctx: &AuthContext<S>,
40 call: &EndpointCall,
41 grant_type: Option<&str>,
42 options: &OAuthClientAuthenticationOptions,
43 ) -> Result<OAuthAuthenticatedClient, EndpointError> {
44 with_endpoint_call_context(
45 call.clone(),
46 self.authenticate_client_inner(ctx, call, grant_type, options),
47 )
48 .await
49 }
50 async fn authenticate_client_inner<S: AuthSchema>(
51 &self,
52 ctx: &AuthContext<S>,
53 call: &EndpointCall,
54 grant_type: Option<&str>,
55 options: &OAuthClientAuthenticationOptions,
56 ) -> Result<OAuthAuthenticatedClient, EndpointError> {
57 let mut body = serde_json::Map::new();
59 if let Some(input) = call.body() {
60 for field in [
61 "client_id",
62 "client_secret",
63 "client_assertion",
64 "client_assertion_type",
65 ] {
66 if let Some(value) = input.get(field) {
67 _ = body.insert(
68 field.into(),
69 match value {
70 JsValue::String(value) => Value::String(value.clone()),
71 _ => Value::Null,
72 },
73 );
74 }
75 }
76 }
77 let audience = format!(
78 "{}{}{}",
79 ctx.config.base_url.trim_end_matches('/'),
80 ctx.config.base_path,
81 call.path().unwrap_or("/oauth2/token")
82 );
83 let credentials = match super::credentials::extract_for_audience(
84 &Value::Object(body),
85 call,
86 ctx,
87 self,
88 &audience,
89 )
90 .await?
91 {
92 Ok(credentials) => credentials,
93 Err(response) => return Err(response_error(&response)?),
94 };
95 if credentials.client_id.is_empty() {
96 return Err(failure("invalid_request", "Missing required client_id"));
97 }
98 if options.require_credentials && credentials.secret.is_empty() && !credentials.pre_verified
99 {
100 return Err(failure(
101 "invalid_client",
102 "Missing required client credentials",
103 ));
104 }
105 let client = match credentials.authenticate(ctx, self).await? {
106 Ok(client) => client,
107 Err(response) => return Err(response_error(&response)?),
108 };
109 if let Some(requested) = &options.scopes {
110 let allowed = client.metadata.get("scope").and_then(|scope| {
111 scope
112 .as_str()
113 .map(|scope| scope.split(' ').collect::<Vec<_>>())
114 .or_else(|| {
115 scope
116 .as_array()
117 .map(|scopes| scopes.iter().filter_map(Value::as_str).collect())
118 })
119 });
120 if let Some(allowed) = allowed {
121 for scope in requested {
122 if !allowed.contains(&scope.as_str()) {
123 return Err(failure(
124 "invalid_scope",
125 &format!("client does not allow scope {scope}"),
126 ));
127 }
128 }
129 }
130 }
131 if let Some(grant) = grant_type.filter(|grant| !grant.is_empty())
132 && !client_allows_grant(&client, grant)
133 {
134 return Err(failure(
135 "unauthorized_client",
136 &format!("client is not authorized to use grant type {grant}"),
137 ));
138 }
139 Ok(OAuthAuthenticatedClient {
140 client_id: credentials.client_id,
141 client,
142 method: Some(credentials.method.into()),
143 confirmation: None,
144 })
145 }
146}
147pub(super) fn client_allows_grant(client: &OAuthClient, grant: &str) -> bool {
148 let allowed = client
149 .metadata
150 .get("grant_types")
151 .and_then(Value::as_array)
152 .filter(|grants| !grants.is_empty());
153 allowed.map_or(
154 matches!(grant, "authorization_code" | "refresh_token"),
155 |grants| {
156 grants.contains(&json!(grant))
157 || (grant == "refresh_token" && grants.contains(&json!("authorization_code")))
158 },
159 )
160}
161fn failure(code: &str, description: &str) -> EndpointError {
162 EndpointError {
163 error: AuthError::Api {
164 status: 400,
165 code: None,
166 message: String::new(),
167 },
168 headers: None,
169 body: Some(Box::new(JsValue::from(
170 json!({"error":code,"error_description":description}),
171 ))),
172 body_absent: false,
173 }
174}
175pub(super) fn response_error(response: &EndpointResponse) -> Result<EndpointError, EndpointError> {
176 let response = response.to_response()?;
177 let body_absent = response.body.is_empty();
178 let body = if body_absent {
179 None
180 } else {
181 Some(Box::new(
182 parse_value(String::from_utf8_lossy(&response.body).as_ref())
183 .map_err(AuthError::from)?,
184 ))
185 };
186 Ok(EndpointError {
187 error: AuthError::Api {
188 status: response.status,
189 code: None,
190 message: String::new(),
191 },
192 headers: Some(response.headers),
193 body,
194 body_absent,
195 })
196}