Skip to main content

openauth_plugins/api_key/
mod.rs

1//! API key plugin.
2
3mod cleanup;
4mod errors;
5mod hashing;
6mod models;
7mod options;
8mod organization;
9mod permissions;
10mod rate_limit;
11mod routes;
12mod schema;
13mod storage;
14
15use std::sync::Arc;
16
17use http::{header, StatusCode};
18use openauth_core::context::request_state;
19use openauth_core::db::Session;
20use openauth_core::error::OpenAuthError;
21use openauth_core::plugin::{AuthPlugin, PluginBeforeHookAction};
22use openauth_core::user::DbUserStore;
23use serde::Serialize;
24use time::{Duration, OffsetDateTime};
25
26pub use errors::*;
27pub use hashing::default_key_hasher;
28pub use models::{ApiKeyCreateRecord, ApiKeyPublicRecord, ApiKeyRecord};
29pub use options::{
30    ApiKeyConfiguration, ApiKeyExpirationOptions, ApiKeyGenerator, ApiKeyGeneratorInput,
31    ApiKeyGetter, ApiKeyOptions, ApiKeyOptionsError, ApiKeyPermissions, ApiKeyRateLimitOptions,
32    ApiKeyReference, ApiKeyStorageMode, ApiKeyValidator, StartingCharactersConfig,
33};
34pub use routes::{
35    CreateApiKeyRequest, DeleteApiKeyRequest, GetApiKeyQuery, ListApiKeysQuery,
36    UpdateApiKeyRequest, UpdateField, VerifyApiKeyRequest, VerifyApiKeyResponse,
37};
38pub use schema::ApiKeySchemaOptions;
39
40pub const UPSTREAM_PLUGIN_ID: &str = "api-key";
41pub const API_KEY_MODEL: &str = "api_key";
42pub const API_KEY_TABLE: &str = "api_keys";
43
44pub fn api_key() -> AuthPlugin {
45    api_key_with_options(ApiKeyOptions::default())
46}
47
48pub fn api_key_with_options(options: ApiKeyOptions) -> AuthPlugin {
49    build_plugin(options::ResolvedConfigurations::single(
50        options.configuration,
51    ))
52}
53
54pub fn api_key_with_configurations(
55    configurations: Vec<ApiKeyConfiguration>,
56) -> Result<AuthPlugin, ApiKeyOptionsError> {
57    Ok(build_plugin(options::ResolvedConfigurations::multiple(
58        configurations,
59    )?))
60}
61
62fn build_plugin(configurations: options::ResolvedConfigurations) -> AuthPlugin {
63    let configurations = Arc::new(configurations);
64    let mut plugin = AuthPlugin::new(UPSTREAM_PLUGIN_ID)
65        .with_version(crate::VERSION)
66        .with_schema(schema::schema_contribution(&ApiKeySchemaOptions::default()))
67        .with_endpoint(routes::create_endpoint(Arc::clone(&configurations)))
68        .with_endpoint(routes::verify_endpoint(Arc::clone(&configurations)))
69        .with_endpoint(routes::get_endpoint(Arc::clone(&configurations)))
70        .with_endpoint(routes::update_endpoint(Arc::clone(&configurations)))
71        .with_endpoint(routes::delete_endpoint(Arc::clone(&configurations)))
72        .with_endpoint(routes::list_endpoint(Arc::clone(&configurations)))
73        .with_endpoint(routes::delete_expired_endpoint(Arc::clone(&configurations)))
74        .with_async_before_hook("*", move |context, request| {
75            let configurations = Arc::clone(&configurations);
76            Box::pin(async move { session_hook(context, request, configurations).await })
77        });
78    for error_code in errors::plugin_error_codes() {
79        plugin = plugin.with_error_code(error_code);
80    }
81    plugin
82}
83
84async fn session_hook(
85    context: &openauth_core::context::AuthContext,
86    request: openauth_core::plugin::PluginRequest,
87    configurations: Arc<options::ResolvedConfigurations>,
88) -> Result<PluginBeforeHookAction, OpenAuthError> {
89    let Some((raw_key, options)) = find_session_key(context, &request, &configurations).await?
90    else {
91        return Ok(PluginBeforeHookAction::Continue(request));
92    };
93    if raw_key.len() < options.default_key_length {
94        return errors::error_response(StatusCode::FORBIDDEN, errors::INVALID_API_KEY)
95            .map(PluginBeforeHookAction::Respond);
96    }
97    if let Some(validator) = &options.custom_api_key_validator {
98        if !validator(context, &raw_key).await? {
99            return Ok(PluginBeforeHookAction::Continue(request));
100        }
101    }
102    let hashed = if options.disable_key_hashing {
103        raw_key.clone()
104    } else {
105        hashing::default_key_hasher(&raw_key)
106    };
107    let api_key = match routes::validate_api_key(context, &options, &hashed, None).await {
108        Ok(api_key) => api_key,
109        Err(_) => return Ok(PluginBeforeHookAction::Continue(request)),
110    };
111    if options.reference != ApiKeyReference::User {
112        return Ok(PluginBeforeHookAction::Continue(request));
113    }
114    let Some(adapter) = context.adapter() else {
115        return Ok(PluginBeforeHookAction::Continue(request));
116    };
117    let Some(user) = DbUserStore::new(adapter.as_ref())
118        .find_user_by_id(&api_key.reference_id)
119        .await?
120    else {
121        return Ok(PluginBeforeHookAction::Continue(request));
122    };
123    let now = OffsetDateTime::now_utc();
124    let expires_at = api_key.expires_at.unwrap_or_else(|| {
125        now + Duration::seconds(i64::try_from(context.session_config.expires_in).unwrap_or(0))
126    });
127    let session = Session {
128        id: api_key.id.clone(),
129        user_id: api_key.reference_id.clone(),
130        expires_at,
131        token: raw_key,
132        ip_address: None,
133        user_agent: request
134            .headers()
135            .get(header::USER_AGENT)
136            .and_then(|value| value.to_str().ok())
137            .map(str::to_owned),
138        created_at: now,
139        updated_at: now,
140    };
141    if request_state::has_request_state() {
142        request_state::set_current_session(session.clone(), user.clone())?;
143    }
144    if request.uri().path().ends_with("/get-session") {
145        return session_response(session, user).map(PluginBeforeHookAction::Respond);
146    }
147    Ok(PluginBeforeHookAction::Continue(request))
148}
149
150async fn find_session_key(
151    context: &openauth_core::context::AuthContext,
152    request: &openauth_core::plugin::PluginRequest,
153    configurations: &options::ResolvedConfigurations,
154) -> Result<Option<(String, ApiKeyConfiguration)>, OpenAuthError> {
155    for configuration in configurations
156        .all()
157        .iter()
158        .filter(|configuration| configuration.enable_session_for_api_keys)
159    {
160        if let Some(getter) = &configuration.custom_api_key_getter {
161            if let Some(key) = getter(context, request).await? {
162                return Ok(Some((key, configuration.clone())));
163            }
164            continue;
165        }
166        for header_name in &configuration.api_key_headers {
167            if let Some(value) = request
168                .headers()
169                .get(header_name)
170                .and_then(|value| value.to_str().ok())
171            {
172                return Ok(Some((value.to_owned(), configuration.clone())));
173            }
174        }
175    }
176    Ok(None)
177}
178
179#[derive(Serialize)]
180struct SessionResponse {
181    session: Session,
182    user: openauth_core::db::User,
183}
184
185fn session_response(
186    session: Session,
187    user: openauth_core::db::User,
188) -> Result<openauth_core::plugin::PluginResponse, OpenAuthError> {
189    let body = serde_json::to_vec(&SessionResponse { session, user })
190        .map_err(|error| OpenAuthError::Api(error.to_string()))?;
191    http::Response::builder()
192        .status(StatusCode::OK)
193        .header(header::CONTENT_TYPE, "application/json")
194        .body(body)
195        .map_err(|error| OpenAuthError::Api(error.to_string()))
196}