use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use std::task::{Context, Poll};
use axum::extract::FromRequestParts;
use axum::response::{IntoResponse, Response};
use http::StatusCode;
use http::request::Parts;
const DEFAULT_BCRYPT_COST: u32 = 12;
pub async fn hash_password(password: &str) -> crate::AutumnResult<String> {
let password = password.to_string();
tokio::task::spawn_blocking(move || {
bcrypt::hash(password, DEFAULT_BCRYPT_COST)
.map_err(|e| crate::AutumnError::from(std::io::Error::other(e.to_string())))
})
.await
.map_err(|e| crate::AutumnError::from(std::io::Error::other(e.to_string())))?
}
pub async fn verify_password(password: &str, hash: &str) -> crate::AutumnResult<bool> {
let password = password.to_string();
let is_valid_format = hash.len() == 60 && hash.starts_with('$');
let hash_to_verify = if is_valid_format {
hash.to_string()
} else {
"$2b$12$KIXe8K4j1sH6/xH.x9d71uJ5Jk8t6O4m6Q110g4H8y1r6J6O6O6O6".to_string()
};
let result = tokio::task::spawn_blocking(move || bcrypt::verify(&password, &hash_to_verify))
.await
.map_err(|e| crate::AutumnError::from(std::io::Error::other(e.to_string())))?;
if !is_valid_format {
return Ok(false);
}
result.map_err(|e| crate::AutumnError::from(std::io::Error::other(e.to_string())))
}
#[doc(hidden)]
pub async fn __check_secured(
session: &crate::session::Session,
roles: &[&str],
) -> crate::AutumnResult<()> {
if session.get("user_id").await.is_none() {
return Err(crate::AutumnError::unauthorized_msg(
"authentication required",
));
}
if !roles.is_empty() {
let user_role = session.get("role").await.unwrap_or_default();
if !roles.iter().any(|&r| r == user_role) {
return Err(crate::AutumnError::forbidden_msg(
"insufficient permissions",
));
}
}
Ok(())
}
pub struct Auth<T>(pub T);
impl<T, S> FromRequestParts<S> for Auth<T>
where
T: Clone + Send + Sync + 'static,
S: Send + Sync,
{
type Rejection = AuthRejection;
fn from_request_parts(
parts: &mut Parts,
_state: &S,
) -> impl Future<Output = Result<Self, Self::Rejection>> + Send {
let user = parts.extensions.get::<T>().cloned();
async move { user.map_or_else(|| Err(AuthRejection), |user| Ok(Self(user))) }
}
}
#[derive(Debug)]
pub struct AuthRejection;
impl IntoResponse for AuthRejection {
fn into_response(self) -> Response {
(
StatusCode::UNAUTHORIZED,
axum::Json(serde_json::json!({
"error": {
"status": 401,
"message": "authentication required"
}
})),
)
.into_response()
}
}
impl std::fmt::Display for AuthRejection {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("authentication required")
}
}
#[derive(Clone)]
pub struct RequireAuth {
session_key: Arc<str>,
}
impl RequireAuth {
pub fn new(session_key: impl Into<String>) -> Self {
Self {
session_key: Arc::from(session_key.into()),
}
}
}
impl<S> tower::Layer<S> for RequireAuth {
type Service = RequireAuthService<S>;
fn layer(&self, inner: S) -> Self::Service {
RequireAuthService {
inner,
session_key: Arc::clone(&self.session_key),
}
}
}
#[derive(Clone)]
pub struct RequireAuthService<S> {
inner: S,
session_key: Arc<str>,
}
impl<S, ResBody> tower::Service<axum::extract::Request> for RequireAuthService<S>
where
S: tower::Service<axum::extract::Request, Response = Response<ResBody>>
+ Clone
+ Send
+ 'static,
S::Future: Send + 'static,
S::Error: Send + 'static,
ResBody: From<String> + Default + Send + 'static,
{
type Response = Response<ResBody>;
type Error = S::Error;
type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.inner.poll_ready(cx)
}
fn call(&mut self, req: axum::extract::Request) -> Self::Future {
let session_key = Arc::clone(&self.session_key);
let mut inner = self.inner.clone();
std::mem::swap(&mut self.inner, &mut inner);
Box::pin(async move {
let session = req.extensions().get::<crate::session::Session>().cloned();
let is_authenticated = if let Some(ref session) = session {
session.contains_key(&session_key).await
} else {
false
};
if is_authenticated {
inner.call(req).await
} else {
let body = serde_json::json!({
"error": {
"status": 401,
"message": "authentication required"
}
});
let response = Response::builder()
.status(StatusCode::UNAUTHORIZED)
.header(http::header::CONTENT_TYPE, "application/json")
.body(ResBody::from(
serde_json::to_string(&body).unwrap_or_default(),
))
.unwrap_or_default();
Ok(response)
}
})
}
}
#[derive(Debug, Clone, serde::Deserialize)]
pub struct AuthConfig {
#[serde(default = "default_bcrypt_cost")]
pub bcrypt_cost: u32,
#[serde(default = "default_session_key")]
pub session_key: String,
}
const fn default_bcrypt_cost() -> u32 {
DEFAULT_BCRYPT_COST
}
fn default_session_key() -> String {
"user_id".to_owned()
}
impl Default for AuthConfig {
fn default() -> Self {
Self {
bcrypt_cost: default_bcrypt_cost(),
session_key: default_session_key(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn hash_and_verify_password() {
let hash = hash_password("test_password").await.unwrap();
assert!(hash.starts_with("$2b$"));
assert!(verify_password("test_password", &hash).await.unwrap());
assert!(!verify_password("wrong_password", &hash).await.unwrap());
}
#[tokio::test]
async fn verify_invalid_hash_returns_false() {
let result = verify_password("test", "not-a-valid-hash").await;
assert!(result.is_ok());
assert!(!result.unwrap());
}
#[tokio::test]
async fn verify_password_rejects_invalid_hash_format_safely() {
let result = verify_password("test", "short").await;
assert!(result.is_ok());
assert!(!result.unwrap());
let bad_prefix = "a".repeat(60);
let result = verify_password("test", &bad_prefix).await;
assert!(result.is_ok());
assert!(!result.unwrap());
let bad_length = "$2b$12$short";
let result = verify_password("test", bad_length).await;
assert!(result.is_ok());
assert!(!result.unwrap());
}
#[test]
fn auth_config_defaults() {
let config = AuthConfig::default();
assert_eq!(config.bcrypt_cost, 12);
assert_eq!(config.session_key, "user_id");
}
#[test]
fn auth_rejection_is_401() {
let rejection = AuthRejection;
let response = rejection.into_response();
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
}
#[test]
fn auth_rejection_display() {
assert_eq!(AuthRejection.to_string(), "authentication required");
}
#[tokio::test]
async fn auth_extractor_returns_401_when_no_user() {
use crate::state::AppState;
use axum::Router;
use axum::body::Body;
use axum::routing::get;
use tower::ServiceExt;
#[derive(Clone)]
struct TestUser {
name: String,
}
async fn handler(Auth(user): Auth<TestUser>) -> String {
user.name
}
let state = AppState {
extensions: std::sync::Arc::new(
std::sync::Mutex::new(std::collections::HashMap::new()),
),
#[cfg(feature = "db")]
pool: None,
profile: None,
started_at: std::time::Instant::now(),
health_detailed: false,
probes: crate::probe::ProbeState::ready_for_test(),
metrics: crate::middleware::MetricsCollector::new(),
log_levels: crate::actuator::LogLevels::new("info"),
task_registry: crate::actuator::TaskRegistry::new(),
config_props: crate::actuator::ConfigProperties::default(),
#[cfg(feature = "ws")]
channels: crate::channels::Channels::new(32),
#[cfg(feature = "ws")]
shutdown: tokio_util::sync::CancellationToken::new(),
};
let app = Router::new().route("/", get(handler)).with_state(state);
let response = app
.oneshot(
http::Request::builder()
.uri("/")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
}
#[tokio::test]
async fn auth_extractor_returns_user_when_present() {
use crate::state::AppState;
use axum::Router;
use axum::body::Body;
use axum::routing::get;
use tower::ServiceExt;
#[derive(Clone)]
struct TestUser {
name: String,
}
async fn handler(Auth(user): Auth<TestUser>) -> String {
user.name
}
let state = AppState {
extensions: std::sync::Arc::new(
std::sync::Mutex::new(std::collections::HashMap::new()),
),
#[cfg(feature = "db")]
pool: None,
profile: None,
started_at: std::time::Instant::now(),
health_detailed: false,
probes: crate::probe::ProbeState::ready_for_test(),
metrics: crate::middleware::MetricsCollector::new(),
log_levels: crate::actuator::LogLevels::new("info"),
task_registry: crate::actuator::TaskRegistry::new(),
config_props: crate::actuator::ConfigProperties::default(),
#[cfg(feature = "ws")]
channels: crate::channels::Channels::new(32),
#[cfg(feature = "ws")]
shutdown: tokio_util::sync::CancellationToken::new(),
};
let app = Router::new()
.route("/", get(handler))
.layer(axum::middleware::from_fn(
|mut req: axum::extract::Request, next: axum::middleware::Next| async move {
req.extensions_mut().insert(TestUser {
name: "alice".into(),
});
next.run(req).await
},
))
.with_state(state);
let response = app
.oneshot(
http::Request::builder()
.uri("/")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
let body = axum::body::to_bytes(response.into_body(), usize::MAX)
.await
.unwrap();
assert_eq!(std::str::from_utf8(&body).unwrap(), "alice");
}
#[tokio::test]
async fn require_auth_rejects_unauthenticated() {
use axum::Router;
use axum::body::Body;
use axum::routing::get;
use tower::ServiceExt;
use crate::session::{MemoryStore, SessionConfig, SessionLayer};
use crate::state::AppState;
let state = AppState {
extensions: std::sync::Arc::new(
std::sync::Mutex::new(std::collections::HashMap::new()),
),
#[cfg(feature = "db")]
pool: None,
profile: None,
started_at: std::time::Instant::now(),
health_detailed: false,
probes: crate::probe::ProbeState::ready_for_test(),
metrics: crate::middleware::MetricsCollector::new(),
log_levels: crate::actuator::LogLevels::new("info"),
task_registry: crate::actuator::TaskRegistry::new(),
config_props: crate::actuator::ConfigProperties::default(),
#[cfg(feature = "ws")]
channels: crate::channels::Channels::new(32),
#[cfg(feature = "ws")]
shutdown: tokio_util::sync::CancellationToken::new(),
};
let app = Router::new()
.route("/protected", get(|| async { "secret" }))
.layer(RequireAuth::new("user_id"))
.layer(SessionLayer::new(
MemoryStore::new(),
SessionConfig::default(),
))
.with_state(state);
let response = app
.oneshot(
http::Request::builder()
.uri("/protected")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
}
#[tokio::test]
async fn check_secured_rejects_unauthenticated() {
let session =
crate::session::Session::new_for_test(String::new(), std::collections::HashMap::new());
let result = __check_secured(&session, &[]).await;
assert!(result.is_err());
let err = result.unwrap_err();
assert_eq!(err.status(), StatusCode::UNAUTHORIZED);
assert_eq!(err.to_string(), "authentication required");
}
#[tokio::test]
async fn check_secured_allows_authenticated() {
let data = std::collections::HashMap::from([("user_id".into(), "42".into())]);
let session = crate::session::Session::new_for_test("sess".into(), data);
let result = __check_secured(&session, &[]).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn check_secured_rejects_wrong_role() {
let data = std::collections::HashMap::from([
("user_id".into(), "42".into()),
("role".into(), "viewer".into()),
]);
let session = crate::session::Session::new_for_test("sess".into(), data);
let result = __check_secured(&session, &["admin"]).await;
assert!(result.is_err());
let err = result.unwrap_err();
assert_eq!(err.status(), StatusCode::FORBIDDEN);
assert_eq!(err.to_string(), "insufficient permissions");
}
#[tokio::test]
async fn check_secured_allows_matching_role() {
let data = std::collections::HashMap::from([
("user_id".into(), "42".into()),
("role".into(), "admin".into()),
]);
let session = crate::session::Session::new_for_test("sess".into(), data);
let result = __check_secured(&session, &["admin"]).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn check_secured_allows_any_of_multiple_roles() {
let data = std::collections::HashMap::from([
("user_id".into(), "42".into()),
("role".into(), "editor".into()),
]);
let session = crate::session::Session::new_for_test("sess".into(), data);
let result = __check_secured(&session, &["admin", "editor"]).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn secured_macro_rejects_unauthenticated() {
use axum::Router;
use axum::body::Body;
use axum::routing::get;
use tower::ServiceExt;
use crate::session::{MemoryStore, SessionConfig, SessionLayer};
use crate::state::AppState;
#[autumn_macros::secured]
async fn protected_handler() -> crate::AutumnResult<&'static str> {
Ok("secret")
}
let state = AppState {
extensions: std::sync::Arc::new(
std::sync::Mutex::new(std::collections::HashMap::new()),
),
#[cfg(feature = "db")]
pool: None,
profile: None,
started_at: std::time::Instant::now(),
health_detailed: false,
probes: crate::probe::ProbeState::ready_for_test(),
metrics: crate::middleware::MetricsCollector::new(),
log_levels: crate::actuator::LogLevels::new("info"),
task_registry: crate::actuator::TaskRegistry::new(),
config_props: crate::actuator::ConfigProperties::default(),
#[cfg(feature = "ws")]
channels: crate::channels::Channels::new(32),
#[cfg(feature = "ws")]
shutdown: tokio_util::sync::CancellationToken::new(),
};
let app = Router::new()
.route("/", get(protected_handler))
.layer(SessionLayer::new(
MemoryStore::new(),
SessionConfig::default(),
))
.with_state(state);
let response = app
.oneshot(
http::Request::builder()
.uri("/")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
}
#[tokio::test]
async fn secured_macro_allows_authenticated() {
use axum::Router;
use axum::body::Body;
use axum::routing::get;
use http::header::COOKIE;
use tower::ServiceExt;
use crate::session::{MemoryStore, SessionConfig, SessionLayer, SessionStore};
use crate::state::AppState;
#[autumn_macros::secured]
async fn protected_handler() -> crate::AutumnResult<&'static str> {
Ok("secret")
}
let store = MemoryStore::new();
store
.save(
"sess1",
std::collections::HashMap::from([("user_id".into(), "42".into())]),
)
.await
.unwrap();
let state = AppState {
extensions: std::sync::Arc::new(
std::sync::Mutex::new(std::collections::HashMap::new()),
),
#[cfg(feature = "db")]
pool: None,
profile: None,
started_at: std::time::Instant::now(),
health_detailed: false,
probes: crate::probe::ProbeState::ready_for_test(),
metrics: crate::middleware::MetricsCollector::new(),
log_levels: crate::actuator::LogLevels::new("info"),
task_registry: crate::actuator::TaskRegistry::new(),
config_props: crate::actuator::ConfigProperties::default(),
#[cfg(feature = "ws")]
channels: crate::channels::Channels::new(32),
#[cfg(feature = "ws")]
shutdown: tokio_util::sync::CancellationToken::new(),
};
let app = Router::new()
.route("/", get(protected_handler))
.layer(SessionLayer::new(store, SessionConfig::default()))
.with_state(state);
let response = app
.oneshot(
http::Request::builder()
.uri("/")
.header(COOKIE, "autumn.sid=sess1")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
let body = axum::body::to_bytes(response.into_body(), usize::MAX)
.await
.unwrap();
assert_eq!(std::str::from_utf8(&body).unwrap(), "secret");
}
#[tokio::test]
async fn secured_macro_with_role_rejects_wrong_role() {
use axum::Router;
use axum::body::Body;
use axum::routing::get;
use http::header::COOKIE;
use tower::ServiceExt;
use crate::session::{MemoryStore, SessionConfig, SessionLayer, SessionStore};
use crate::state::AppState;
#[autumn_macros::secured("admin")]
async fn admin_only() -> crate::AutumnResult<&'static str> {
Ok("admin area")
}
let store = MemoryStore::new();
store
.save(
"sess1",
std::collections::HashMap::from([
("user_id".into(), "42".into()),
("role".into(), "viewer".into()),
]),
)
.await
.unwrap();
let state = AppState {
extensions: std::sync::Arc::new(
std::sync::Mutex::new(std::collections::HashMap::new()),
),
#[cfg(feature = "db")]
pool: None,
profile: None,
started_at: std::time::Instant::now(),
health_detailed: false,
probes: crate::probe::ProbeState::ready_for_test(),
metrics: crate::middleware::MetricsCollector::new(),
log_levels: crate::actuator::LogLevels::new("info"),
task_registry: crate::actuator::TaskRegistry::new(),
config_props: crate::actuator::ConfigProperties::default(),
#[cfg(feature = "ws")]
channels: crate::channels::Channels::new(32),
#[cfg(feature = "ws")]
shutdown: tokio_util::sync::CancellationToken::new(),
};
let app = Router::new()
.route("/", get(admin_only))
.layer(SessionLayer::new(store, SessionConfig::default()))
.with_state(state);
let response = app
.oneshot(
http::Request::builder()
.uri("/")
.header(COOKIE, "autumn.sid=sess1")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::FORBIDDEN);
}
#[tokio::test]
async fn secured_macro_with_multiple_roles_allows_match() {
use axum::Router;
use axum::body::Body;
use axum::routing::get;
use http::header::COOKIE;
use tower::ServiceExt;
use crate::session::{MemoryStore, SessionConfig, SessionLayer, SessionStore};
use crate::state::AppState;
#[autumn_macros::secured("admin", "editor")]
async fn content_handler() -> crate::AutumnResult<&'static str> {
Ok("content")
}
let store = MemoryStore::new();
store
.save(
"sess1",
std::collections::HashMap::from([
("user_id".into(), "42".into()),
("role".into(), "editor".into()),
]),
)
.await
.unwrap();
let state = AppState {
extensions: std::sync::Arc::new(
std::sync::Mutex::new(std::collections::HashMap::new()),
),
#[cfg(feature = "db")]
pool: None,
profile: None,
started_at: std::time::Instant::now(),
health_detailed: false,
probes: crate::probe::ProbeState::ready_for_test(),
metrics: crate::middleware::MetricsCollector::new(),
log_levels: crate::actuator::LogLevels::new("info"),
task_registry: crate::actuator::TaskRegistry::new(),
config_props: crate::actuator::ConfigProperties::default(),
#[cfg(feature = "ws")]
channels: crate::channels::Channels::new(32),
#[cfg(feature = "ws")]
shutdown: tokio_util::sync::CancellationToken::new(),
};
let app = Router::new()
.route("/", get(content_handler))
.layer(SessionLayer::new(store, SessionConfig::default()))
.with_state(state);
let response = app
.oneshot(
http::Request::builder()
.uri("/")
.header(COOKIE, "autumn.sid=sess1")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
let body = axum::body::to_bytes(response.into_body(), usize::MAX)
.await
.unwrap();
assert_eq!(std::str::from_utf8(&body).unwrap(), "content");
}
#[tokio::test]
async fn require_auth_allows_authenticated() {
use axum::Router;
use axum::body::Body;
use axum::routing::get;
use http::header::COOKIE;
use tower::ServiceExt;
use crate::session::{MemoryStore, SessionConfig, SessionLayer, SessionStore};
use crate::state::AppState;
let store = MemoryStore::new();
let mut session_data = std::collections::HashMap::new();
session_data.insert("user_id".into(), "42".into());
store.save("valid-session", session_data).await.unwrap();
let state = AppState {
extensions: std::sync::Arc::new(
std::sync::Mutex::new(std::collections::HashMap::new()),
),
#[cfg(feature = "db")]
pool: None,
profile: None,
started_at: std::time::Instant::now(),
health_detailed: false,
probes: crate::probe::ProbeState::ready_for_test(),
metrics: crate::middleware::MetricsCollector::new(),
log_levels: crate::actuator::LogLevels::new("info"),
task_registry: crate::actuator::TaskRegistry::new(),
config_props: crate::actuator::ConfigProperties::default(),
#[cfg(feature = "ws")]
channels: crate::channels::Channels::new(32),
#[cfg(feature = "ws")]
shutdown: tokio_util::sync::CancellationToken::new(),
};
let app = Router::new()
.route("/protected", get(|| async { "secret" }))
.layer(RequireAuth::new("user_id"))
.layer(SessionLayer::new(store, SessionConfig::default()))
.with_state(state);
let response = app
.oneshot(
http::Request::builder()
.uri("/protected")
.header(COOKIE, "autumn.sid=valid-session")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
let body = axum::body::to_bytes(response.into_body(), usize::MAX)
.await
.unwrap();
assert_eq!(std::str::from_utf8(&body).unwrap(), "secret");
}
#[tokio::test]
async fn require_auth_poll_ready_propagates() {
use std::task::{Context, Poll};
use tower::{Layer, Service};
#[derive(Clone)]
struct MockService {
ready: bool,
poll_count: std::sync::Arc<std::sync::atomic::AtomicUsize>,
}
impl Service<axum::extract::Request> for MockService {
type Response = axum::response::Response;
type Error = std::convert::Infallible;
type Future = std::future::Ready<Result<Self::Response, Self::Error>>;
fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.poll_count
.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
if self.ready {
Poll::Ready(Ok(()))
} else {
Poll::Pending
}
}
fn call(&mut self, _req: axum::extract::Request) -> Self::Future {
std::future::ready(Ok(axum::response::Response::new(axum::body::Body::empty())))
}
}
let layer = RequireAuth::new("user_id");
let poll_count = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
let mock_service = MockService {
ready: false,
poll_count: poll_count.clone(),
};
let mut service = layer.layer(mock_service);
let waker = futures::task::noop_waker();
let mut cx = Context::from_waker(&waker);
let poll = service.poll_ready(&mut cx);
assert!(poll.is_pending());
assert_eq!(poll_count.load(std::sync::atomic::Ordering::SeqCst), 1);
let mock_service_ready = MockService {
ready: true,
poll_count: poll_count.clone(),
};
let mut service_ready = layer.layer(mock_service_ready);
let poll_ready = service_ready.poll_ready(&mut cx);
assert!(poll_ready.is_ready());
assert_eq!(poll_count.load(std::sync::atomic::Ordering::SeqCst), 2);
}
#[tokio::test]
async fn auth_rejection_into_response() {
let rejection = AuthRejection;
let response = rejection.into_response();
assert_eq!(response.status(), axum::http::StatusCode::UNAUTHORIZED);
let body = axum::body::to_bytes(response.into_body(), usize::MAX)
.await
.unwrap();
let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
assert_eq!(json["error"]["status"], 401);
assert_eq!(json["error"]["message"], "authentication required");
}
#[test]
fn test_auth_config_defaults() {
let config = AuthConfig::default();
assert_eq!(config.bcrypt_cost, DEFAULT_BCRYPT_COST);
assert_eq!(config.session_key, "user_id");
}
}