Skip to content

WIP: Implement sessions database table #3307

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 18 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
617a8b2
diesel: Enable `network-address` feature
Turbo87 Feb 15, 2021
657a982
database: Add `sessions` table
Turbo87 Feb 14, 2021
1357225
Add `derive_builder` dependency
Turbo87 Feb 15, 2021
3cc43e9
models: Add `Session` struct
Turbo87 Feb 15, 2021
3980224
tests/authentication: Simplify tests
Turbo87 Feb 16, 2021
7d66257
tests/authentication: Add happy path tests
Turbo87 Feb 16, 2021
4887398
controllers::util: Simplify `ApiToken::find_by_api_token()` call
Turbo87 Feb 16, 2021
ad00a93
controllers::util: Simplify `maybe_authorization` variable assignment
Turbo87 Feb 15, 2021
86e7d77
controllers::util: Move `User::find()` calls into the condition branches
Turbo87 Feb 16, 2021
f4614d4
controllers::util: Move `return` statements into the condition branches
Turbo87 Feb 16, 2021
e46a3d8
controllers::util: Reduce nesting
Turbo87 Feb 16, 2021
372d08b
controllers::util: Allow authentication via `cargo_auth` cookie
Turbo87 Feb 16, 2021
21fb296
controllers::util: Implement `auth_cookie()` utility function
Turbo87 Feb 16, 2021
24b1f5a
tests: Implement `with_session()` function and `MockSessionUser` struct
Turbo87 Feb 16, 2021
14bba97
tests: Implement basic session authentication tests
Turbo87 Feb 16, 2021
36ed665
controllers::user::session: Adjust `logout()` for `cargo_auth` cookie
Turbo87 Feb 16, 2021
3036ff7
models::session: Implement `Session::revoke_by_token()` function
Turbo87 Feb 16, 2021
c2adb0b
controllers::user::session: Adjust `authorize()` for `cargo_auth` cookie
Turbo87 Feb 16, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 86 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,10 @@ conduit-static = "0.9.0-alpha.3"

cookie = { version = "0.14", features = ["secure"] }
ctrlc = { version = "3.0", features = ["termination"] }
derive_builder = "0.9.0"
derive_deref = "1.1.1"
dialoguer = "0.7.1"
diesel = { version = "1.4.0", features = ["postgres", "serde_json", "chrono", "r2d2"] }
diesel = { version = "1.4.0", features = ["postgres", "serde_json", "chrono", "r2d2", "network-address"] }
diesel_full_text_search = "1.0.0"
dotenv = "0.15"
flate2 = "1.0"
Expand All @@ -64,6 +65,7 @@ htmlescape = "0.3.1"
http = "0.2"
hyper = { version = "0.14", features = ["client", "http1"] }
indexmap = "1.0.2"
ipnetwork = "0.16.0"
jemallocator = { version = "0.3", features = ['unprefixed_malloc_on_supported_platforms', 'profiling'] }
lettre = { version = "0.10.0-alpha.1", default-features = false, features = ["file-transport", "smtp-transport", "native-tls", "hostname", "builder"] }
license-exprs = "^1.4"
Expand Down
1 change: 1 addition & 0 deletions migrations/2021-02-14-201950_session-table/down.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
DROP TABLE sessions;
24 changes: 24 additions & 0 deletions migrations/2021-02-14-201950_session-table/up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
CREATE TABLE sessions
(
id SERIAL
CONSTRAINT sessions_pk
PRIMARY KEY,
user_id INTEGER NOT NULL
CONSTRAINT sessions_users_id_fk
REFERENCES users
ON UPDATE CASCADE ON DELETE CASCADE,
Comment on lines +6 to +9
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You'll probably want to delete stale sessions eventually. In a big database, deleting old rows can be relatively expensive. For this, you might want to use partitioning (https://www.postgresql.org/docs/10/ddl-partitioning.html), which lets you drop old, stale partitions cheaply while still treating the partitions as a single table. However, to use partitioning you need to have no foreign keys on the partitioned table. Suggestion: Make user_id not a foreign key. We don't expect that user_ids change, so we don't need ON UPDATE CASCADE. And if the users entry is deleted, we can simply treat the session as invalid. So we don't need ON DELETE CASCADE.

hashed_token bytea NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
last_used_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
revoked BOOLEAN DEFAULT FALSE NOT NULL,
last_ip_address inet NOT NULL,
last_user_agent VARCHAR NOT NULL
);

COMMENT ON TABLE sessions IS 'This table contains the tokens for all of the cookie-based sessions';

CREATE INDEX sessions_user_id_index
ON sessions (user_id);

CREATE UNIQUE INDEX sessions_token_uindex
ON sessions (hashed_token);
2 changes: 1 addition & 1 deletion src/controllers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ mod prelude {
}

pub mod helpers;
mod util;
pub mod util;

pub mod category;
pub mod crate_owner_invitation;
Expand Down
57 changes: 52 additions & 5 deletions src/controllers/user/session.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
use crate::controllers::frontend_prelude::*;

use conduit_cookie::RequestSession;
use conduit_cookie::{RequestCookies, RequestSession};
use oauth2::reqwest::http_client;
use oauth2::{AuthorizationCode, Scope, TokenResponse};

use crate::controllers::util::{auth_cookie, AUTH_COOKIE_NAME};
use crate::github::GithubUser;
use crate::models::{NewUser, User};
use crate::models::{NewUser, Session, User};
use crate::schema::users;
use crate::util::errors::ReadOnlyMode;
use crate::Env;

/// Handles the `GET /api/private/session/begin` route.
///
Expand Down Expand Up @@ -104,9 +106,32 @@ pub fn authorize(req: &mut dyn RequestExt) -> EndpointResult {
let ghuser = req.app().github.current_user(token)?;
let user = save_user_to_database(&ghuser, &token.secret(), &*req.db_conn()?)?;

// Log in by setting a cookie and the middleware authentication
req.session_mut()
.insert("user_id".to_string(), user.id.to_string());
// Create a new `Session`
let token = Session::generate_token();

let ip_addr = req.remote_addr().ip();

let user_agent = req
.headers()
.get(header::USER_AGENT)
.and_then(|value| value.to_str().ok())
.map(|value| value.to_string())
.unwrap_or_default();

Session::new()
.user_id(user.id)
.token(&token)
.last_ip_address(ip_addr)
.last_user_agent(user_agent)
.build()
.map_err(|_err| server_error("Error obtaining token"))?
.insert(&*req.db_conn()?)?;

// Log in by setting an auth cookie
let app = req.app();
let secure = app.config.env == Env::Production;

req.cookies_mut().add(auth_cookie(token, secure));

super::me::me(req)
}
Expand Down Expand Up @@ -143,6 +168,28 @@ fn save_user_to_database(
/// Handles the `DELETE /api/private/session` route.
pub fn logout(req: &mut dyn RequestExt) -> EndpointResult {
req.session_mut().remove(&"user_id".to_string());

// read the current session token, if it exists
let session_token = req
.cookies()
.get(AUTH_COOKIE_NAME)
.map(|cookie| cookie.value().to_string());

if let Some(token) = session_token {
let app = req.app();
let secure = app.config.env == Env::Production;

// remove the `cargo_auth` cookie
req.cookies_mut().remove(auth_cookie("", secure));

// try to revoke the session in the database, but explicitly
// ignore failures
let _result: Result<_, Box<dyn AppError>> = req
.db_conn()
.map_err(Into::into)
.and_then(|conn| Session::revoke_by_token(&conn, &token).map_err(Into::into));
Comment on lines +185 to +190
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps log or increment a stat in this case? Or have a set of expected errors (like timeout) and pass through unexpected errors (like permission denied or malformed SQL).

}

Ok(req.json(&true))
}

Expand Down
Loading