You've already forked authentication-service
mirror of
https://github.com/matrix-org/matrix-authentication-service.git
synced 2025-08-07 17:03:01 +03:00
Move storage module to its own crate
This commit is contained in:
25
crates/storage/Cargo.toml
Normal file
25
crates/storage/Cargo.toml
Normal file
@@ -0,0 +1,25 @@
|
||||
[package]
|
||||
name = "mas-storage"
|
||||
version = "0.1.0"
|
||||
authors = ["Quentin Gliech <quenting@element.io>"]
|
||||
edition = "2021"
|
||||
license = "Apache-2.0"
|
||||
|
||||
[dependencies]
|
||||
tokio = "1.14.0"
|
||||
sqlx = { version = "0.5.9", features = ["runtime-tokio-rustls", "postgres", "migrate", "chrono", "offline"] }
|
||||
chrono = { version = "0.4.19", features = ["serde"] }
|
||||
serde = { version = "1.0.131", features = ["derive"] }
|
||||
thiserror = "1.0.30"
|
||||
anyhow = "1.0.51"
|
||||
tracing = "0.1.29"
|
||||
warp = "0.3.2"
|
||||
|
||||
# Password hashing
|
||||
argon2 = { version = "0.3.2", features = ["password-hash"] }
|
||||
password-hash = { version = "0.3.2", features = ["std"] }
|
||||
rand = "0.8.4"
|
||||
url = { version = "2.2.2", features = ["serde"] }
|
||||
|
||||
oauth2-types = { path = "../oauth2-types", features = ["sqlx_type"] }
|
||||
mas-data-model = { path = "../data-model" }
|
18
crates/storage/build.rs
Normal file
18
crates/storage/build.rs
Normal file
@@ -0,0 +1,18 @@
|
||||
// Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
fn main() {
|
||||
// trigger recompilation when a new migration is added
|
||||
println!("cargo:rerun-if-changed=migrations");
|
||||
}
|
@@ -0,0 +1,15 @@
|
||||
-- Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
--
|
||||
-- Licensed under the Apache License, Version 2.0 (the "License");
|
||||
-- you may not use this file except in compliance with the License.
|
||||
-- You may obtain a copy of the License at
|
||||
--
|
||||
-- http://www.apache.org/licenses/LICENSE-2.0
|
||||
--
|
||||
-- Unless required by applicable law or agreed to in writing, software
|
||||
-- distributed under the License is distributed on an "AS IS" BASIS,
|
||||
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
-- See the License for the specific language governing permissions and
|
||||
-- limitations under the License.
|
||||
|
||||
DROP FUNCTION IF EXISTS trigger_set_timestamp();
|
@@ -0,0 +1,21 @@
|
||||
-- Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
--
|
||||
-- Licensed under the Apache License, Version 2.0 (the "License");
|
||||
-- you may not use this file except in compliance with the License.
|
||||
-- You may obtain a copy of the License at
|
||||
--
|
||||
-- http://www.apache.org/licenses/LICENSE-2.0
|
||||
--
|
||||
-- Unless required by applicable law or agreed to in writing, software
|
||||
-- distributed under the License is distributed on an "AS IS" BASIS,
|
||||
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
-- See the License for the specific language governing permissions and
|
||||
-- limitations under the License.
|
||||
|
||||
CREATE OR REPLACE FUNCTION trigger_set_timestamp()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = NOW();
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
16
crates/storage/migrations/20210716213724_users.down.sql
Normal file
16
crates/storage/migrations/20210716213724_users.down.sql
Normal file
@@ -0,0 +1,16 @@
|
||||
-- Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
--
|
||||
-- Licensed under the Apache License, Version 2.0 (the "License");
|
||||
-- you may not use this file except in compliance with the License.
|
||||
-- You may obtain a copy of the License at
|
||||
--
|
||||
-- http://www.apache.org/licenses/LICENSE-2.0
|
||||
--
|
||||
-- Unless required by applicable law or agreed to in writing, software
|
||||
-- distributed under the License is distributed on an "AS IS" BASIS,
|
||||
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
-- See the License for the specific language governing permissions and
|
||||
-- limitations under the License.
|
||||
|
||||
DROP TRIGGER set_timestamp ON users;
|
||||
DROP TABLE users;
|
26
crates/storage/migrations/20210716213724_users.up.sql
Normal file
26
crates/storage/migrations/20210716213724_users.up.sql
Normal file
@@ -0,0 +1,26 @@
|
||||
-- Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
--
|
||||
-- Licensed under the Apache License, Version 2.0 (the "License");
|
||||
-- you may not use this file except in compliance with the License.
|
||||
-- You may obtain a copy of the License at
|
||||
--
|
||||
-- http://www.apache.org/licenses/LICENSE-2.0
|
||||
--
|
||||
-- Unless required by applicable law or agreed to in writing, software
|
||||
-- distributed under the License is distributed on an "AS IS" BASIS,
|
||||
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
-- See the License for the specific language governing permissions and
|
||||
-- limitations under the License.
|
||||
|
||||
CREATE TABLE users (
|
||||
"id" BIGSERIAL PRIMARY KEY,
|
||||
"username" TEXT NOT NULL UNIQUE,
|
||||
"hashed_password" TEXT NOT NULL,
|
||||
"created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
|
||||
"updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE TRIGGER set_timestamp
|
||||
BEFORE UPDATE ON users
|
||||
FOR EACH ROW
|
||||
EXECUTE PROCEDURE trigger_set_timestamp();
|
@@ -0,0 +1,17 @@
|
||||
-- Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
--
|
||||
-- Licensed under the Apache License, Version 2.0 (the "License");
|
||||
-- you may not use this file except in compliance with the License.
|
||||
-- You may obtain a copy of the License at
|
||||
--
|
||||
-- http://www.apache.org/licenses/LICENSE-2.0
|
||||
--
|
||||
-- Unless required by applicable law or agreed to in writing, software
|
||||
-- distributed under the License is distributed on an "AS IS" BASIS,
|
||||
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
-- See the License for the specific language governing permissions and
|
||||
-- limitations under the License.
|
||||
|
||||
DROP TRIGGER set_timestamp ON user_sessions;
|
||||
DROP TABLE user_session_authentications;
|
||||
DROP TABLE user_sessions;
|
@@ -0,0 +1,35 @@
|
||||
-- Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
--
|
||||
-- Licensed under the Apache License, Version 2.0 (the "License");
|
||||
-- you may not use this file except in compliance with the License.
|
||||
-- You may obtain a copy of the License at
|
||||
--
|
||||
-- http://www.apache.org/licenses/LICENSE-2.0
|
||||
--
|
||||
-- Unless required by applicable law or agreed to in writing, software
|
||||
-- distributed under the License is distributed on an "AS IS" BASIS,
|
||||
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
-- See the License for the specific language governing permissions and
|
||||
-- limitations under the License.
|
||||
|
||||
-- A logged in session
|
||||
CREATE TABLE user_sessions (
|
||||
"id" BIGSERIAL PRIMARY KEY,
|
||||
"user_id" BIGINT NOT NULL REFERENCES users (id) ON DELETE CASCADE,
|
||||
"active" BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
|
||||
"created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
|
||||
"updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE TRIGGER set_timestamp
|
||||
BEFORE UPDATE ON user_sessions
|
||||
FOR EACH ROW
|
||||
EXECUTE PROCEDURE trigger_set_timestamp();
|
||||
|
||||
-- An authentication within a session
|
||||
CREATE TABLE user_session_authentications (
|
||||
"id" BIGSERIAL PRIMARY KEY,
|
||||
"session_id" BIGINT NOT NULL REFERENCES user_sessions (id) ON DELETE CASCADE,
|
||||
"created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()
|
||||
);
|
@@ -0,0 +1,17 @@
|
||||
-- Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
--
|
||||
-- Licensed under the Apache License, Version 2.0 (the "License");
|
||||
-- you may not use this file except in compliance with the License.
|
||||
-- You may obtain a copy of the License at
|
||||
--
|
||||
-- http://www.apache.org/licenses/LICENSE-2.0
|
||||
--
|
||||
-- Unless required by applicable law or agreed to in writing, software
|
||||
-- distributed under the License is distributed on an "AS IS" BASIS,
|
||||
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
-- See the License for the specific language governing permissions and
|
||||
-- limitations under the License.
|
||||
|
||||
DROP TRIGGER set_timestamp ON oauth2_sessions;
|
||||
DROP TABLE oauth2_codes;
|
||||
DROP TABLE oauth2_sessions;
|
@@ -0,0 +1,45 @@
|
||||
-- Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
--
|
||||
-- Licensed under the Apache License, Version 2.0 (the "License");
|
||||
-- you may not use this file except in compliance with the License.
|
||||
-- You may obtain a copy of the License at
|
||||
--
|
||||
-- http://www.apache.org/licenses/LICENSE-2.0
|
||||
--
|
||||
-- Unless required by applicable law or agreed to in writing, software
|
||||
-- distributed under the License is distributed on an "AS IS" BASIS,
|
||||
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
-- See the License for the specific language governing permissions and
|
||||
-- limitations under the License.
|
||||
|
||||
CREATE TABLE oauth2_sessions (
|
||||
"id" BIGSERIAL PRIMARY KEY,
|
||||
"user_session_id" BIGINT REFERENCES user_sessions (id) ON DELETE CASCADE,
|
||||
"client_id" TEXT NOT NULL,
|
||||
"redirect_uri" TEXT NOT NULL,
|
||||
"scope" TEXT NOT NULL,
|
||||
"state" TEXT,
|
||||
"nonce" TEXT,
|
||||
"max_age" INT,
|
||||
"response_type" TEXT NOT NULL,
|
||||
"response_mode" TEXT NOT NULL,
|
||||
|
||||
"created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
|
||||
"updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE TRIGGER set_timestamp
|
||||
BEFORE UPDATE ON oauth2_sessions
|
||||
FOR EACH ROW
|
||||
EXECUTE PROCEDURE trigger_set_timestamp();
|
||||
|
||||
CREATE TABLE oauth2_codes (
|
||||
"id" BIGSERIAL PRIMARY KEY,
|
||||
"oauth2_session_id" BIGINT NOT NULL REFERENCES oauth2_sessions (id) ON DELETE CASCADE,
|
||||
"code" TEXT UNIQUE NOT NULL,
|
||||
"code_challenge_method" SMALLINT,
|
||||
"code_challenge" TEXT,
|
||||
|
||||
CHECK (("code_challenge" IS NULL AND "code_challenge_method" IS NULL)
|
||||
OR ("code_challenge" IS NOT NULL AND "code_challenge_method" IS NOT NULL))
|
||||
);
|
@@ -0,0 +1,15 @@
|
||||
-- Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
--
|
||||
-- Licensed under the Apache License, Version 2.0 (the "License");
|
||||
-- you may not use this file except in compliance with the License.
|
||||
-- You may obtain a copy of the License at
|
||||
--
|
||||
-- http://www.apache.org/licenses/LICENSE-2.0
|
||||
--
|
||||
-- Unless required by applicable law or agreed to in writing, software
|
||||
-- distributed under the License is distributed on an "AS IS" BASIS,
|
||||
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
-- See the License for the specific language governing permissions and
|
||||
-- limitations under the License.
|
||||
|
||||
DROP TABLE oauth2_access_tokens;
|
@@ -0,0 +1,23 @@
|
||||
-- Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
--
|
||||
-- Licensed under the Apache License, Version 2.0 (the "License");
|
||||
-- you may not use this file except in compliance with the License.
|
||||
-- You may obtain a copy of the License at
|
||||
--
|
||||
-- http://www.apache.org/licenses/LICENSE-2.0
|
||||
--
|
||||
-- Unless required by applicable law or agreed to in writing, software
|
||||
-- distributed under the License is distributed on an "AS IS" BASIS,
|
||||
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
-- See the License for the specific language governing permissions and
|
||||
-- limitations under the License.
|
||||
|
||||
CREATE TABLE oauth2_access_tokens (
|
||||
"id" BIGSERIAL PRIMARY KEY,
|
||||
"oauth2_session_id" BIGINT NOT NULL REFERENCES oauth2_sessions (id) ON DELETE CASCADE,
|
||||
|
||||
"token" TEXT UNIQUE NOT NULL,
|
||||
|
||||
"expires_after" INT NOT NULL,
|
||||
"created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()
|
||||
);
|
@@ -0,0 +1,16 @@
|
||||
-- Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
--
|
||||
-- Licensed under the Apache License, Version 2.0 (the "License");
|
||||
-- you may not use this file except in compliance with the License.
|
||||
-- You may obtain a copy of the License at
|
||||
--
|
||||
-- http://www.apache.org/licenses/LICENSE-2.0
|
||||
--
|
||||
-- Unless required by applicable law or agreed to in writing, software
|
||||
-- distributed under the License is distributed on an "AS IS" BASIS,
|
||||
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
-- See the License for the specific language governing permissions and
|
||||
-- limitations under the License.
|
||||
|
||||
DROP TRIGGER set_timestamp ON oauth2_refresh_tokens;
|
||||
DROP TABLE oauth2_refresh_tokens;
|
@@ -0,0 +1,30 @@
|
||||
-- Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
--
|
||||
-- Licensed under the Apache License, Version 2.0 (the "License");
|
||||
-- you may not use this file except in compliance with the License.
|
||||
-- You may obtain a copy of the License at
|
||||
--
|
||||
-- http://www.apache.org/licenses/LICENSE-2.0
|
||||
--
|
||||
-- Unless required by applicable law or agreed to in writing, software
|
||||
-- distributed under the License is distributed on an "AS IS" BASIS,
|
||||
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
-- See the License for the specific language governing permissions and
|
||||
-- limitations under the License.
|
||||
|
||||
CREATE TABLE oauth2_refresh_tokens (
|
||||
"id" BIGSERIAL PRIMARY KEY,
|
||||
"oauth2_session_id" BIGINT NOT NULL REFERENCES oauth2_sessions (id) ON DELETE CASCADE,
|
||||
"oauth2_access_token_id" BIGINT REFERENCES oauth2_access_tokens (id) ON DELETE SET NULL,
|
||||
|
||||
"token" TEXT UNIQUE NOT NULL,
|
||||
"next_token_id" BIGINT REFERENCES oauth2_refresh_tokens (id),
|
||||
|
||||
"created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
|
||||
"updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE TRIGGER set_timestamp
|
||||
BEFORE UPDATE ON oauth2_refresh_tokens
|
||||
FOR EACH ROW
|
||||
EXECUTE PROCEDURE trigger_set_timestamp();
|
103
crates/storage/migrations/20211021201500_oauth2_sessions.up.sql
Normal file
103
crates/storage/migrations/20211021201500_oauth2_sessions.up.sql
Normal file
@@ -0,0 +1,103 @@
|
||||
-- Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
--
|
||||
-- Licensed under the Apache License, Version 2.0 (the "License");
|
||||
-- you may not use this file except in compliance with the License.
|
||||
-- You may obtain a copy of the License at
|
||||
--
|
||||
-- http://www.apache.org/licenses/LICENSE-2.0
|
||||
--
|
||||
-- Unless required by applicable law or agreed to in writing, software
|
||||
-- distributed under the License is distributed on an "AS IS" BASIS,
|
||||
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
-- See the License for the specific language governing permissions and
|
||||
-- limitations under the License.
|
||||
|
||||
|
||||
-- Replace the old "sessions" table
|
||||
ALTER TABLE oauth2_sessions RENAME TO oauth2_sessions_old;
|
||||
|
||||
-- TODO: how do we handle temporary session upgrades (aka. sudo mode)?
|
||||
CREATE TABLE oauth2_sessions (
|
||||
"id" BIGSERIAL PRIMARY KEY,
|
||||
"user_session_id" BIGINT NOT NULL REFERENCES user_sessions (id) ON DELETE CASCADE,
|
||||
"client_id" TEXT NOT NULL, -- The "authorization party" would be more accurate in that case
|
||||
"scope" TEXT NOT NULL,
|
||||
|
||||
"created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
TRUNCATE oauth2_access_tokens, oauth2_refresh_tokens;
|
||||
ALTER TABLE oauth2_access_tokens
|
||||
DROP CONSTRAINT oauth2_access_tokens_oauth2_session_id_fkey,
|
||||
ADD CONSTRAINT oauth2_access_tokens_oauth2_session_id_fkey
|
||||
FOREIGN KEY (oauth2_session_id) REFERENCES oauth2_sessions (id);
|
||||
ALTER TABLE oauth2_refresh_tokens
|
||||
DROP CONSTRAINT oauth2_refresh_tokens_oauth2_session_id_fkey,
|
||||
ADD CONSTRAINT oauth2_refresh_tokens_oauth2_session_id_fkey
|
||||
FOREIGN KEY (oauth2_session_id) REFERENCES oauth2_sessions (id);
|
||||
DROP TABLE oauth2_codes, oauth2_sessions_old;
|
||||
|
||||
CREATE TABLE oauth2_authorization_grants (
|
||||
"id" BIGSERIAL PRIMARY KEY, -- Saved as encrypted cookie
|
||||
|
||||
-- All this comes from the authorization request
|
||||
"client_id" TEXT NOT NULL, -- This should be verified before insertion
|
||||
"redirect_uri" TEXT NOT NULL, -- This should be verified before insertion
|
||||
"scope" TEXT NOT NULL, -- This should be verified before insertion
|
||||
"state" TEXT,
|
||||
"nonce" TEXT,
|
||||
"max_age" INT CHECK ("max_age" IS NULL OR "max_age" > 0),
|
||||
"acr_values" TEXT, -- This should be verified before insertion
|
||||
"response_mode" TEXT NOT NULL,
|
||||
"code_challenge_method" TEXT,
|
||||
"code_challenge" TEXT,
|
||||
|
||||
-- The "response_type" parameter broken down
|
||||
"response_type_code" BOOLEAN NOT NULL,
|
||||
"response_type_token" BOOLEAN NOT NULL,
|
||||
"response_type_id_token" BOOLEAN NOT NULL,
|
||||
|
||||
-- This one is created eagerly on grant creation if the response_type
|
||||
-- includes "code"
|
||||
-- When looking up codes, it should do "where fulfilled_at is not null" and
|
||||
-- "inner join on oauth2_sessions". When doing that, it should check the
|
||||
-- "exchanged_at" field: if it is not null and was exchanged more than 30s
|
||||
-- ago, the session shold be considered as hijacked and fully invalidated
|
||||
"code" TEXT UNIQUE,
|
||||
|
||||
"created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
|
||||
"fulfilled_at" TIMESTAMP WITH TIME ZONE, -- When we got back to the client
|
||||
"cancelled_at" TIMESTAMP WITH TIME ZONE, -- When that grant was cancelled
|
||||
"exchanged_at" TIMESTAMP WITH TIME ZONE, -- When the code was exchanged by the client
|
||||
|
||||
"oauth2_session_id" BIGINT REFERENCES oauth2_sessions (id) ON DELETE CASCADE,
|
||||
|
||||
-- Check a few invariants to keep a coherent state.
|
||||
-- Even though the service should never violate those, it helps ensuring we're not doing anything wrong
|
||||
|
||||
-- Code exchange can only happen after the grant was fulfilled
|
||||
CONSTRAINT "oauth2_authorization_grants_exchanged_after_fullfill"
|
||||
CHECK (("exchanged_at" IS NULL)
|
||||
OR ("exchanged_at" IS NOT NULL AND
|
||||
"fulfilled_at" IS NOT NULL AND
|
||||
"exchanged_at" >= "fulfilled_at")),
|
||||
|
||||
-- A grant can be either fulfilled or cancelled, but not both
|
||||
CONSTRAINT "oauth2_authorization_grants_fulfilled_xor_cancelled"
|
||||
CHECK ("fulfilled_at" IS NULL OR "cancelled_at" IS NULL),
|
||||
|
||||
-- If it was fulfilled there is an oauth2_session_id attached to it
|
||||
CONSTRAINT "oauth2_authorization_grants_fulfilled_and_session"
|
||||
CHECK (("fulfilled_at" IS NULL AND "oauth2_session_id" IS NULL)
|
||||
OR ("fulfilled_at" IS NOT NULL AND "oauth2_session_id" IS NOT NULL)),
|
||||
|
||||
-- We should have a code if and only if the "code" response_type was asked
|
||||
CONSTRAINT "oauth2_authorization_grants_code"
|
||||
CHECK (("response_type_code" IS TRUE AND "code" IS NOT NULL)
|
||||
OR ("response_type_code" IS FALSE AND "code" IS NULL)),
|
||||
|
||||
-- If we have a challenge, we also have a challenge method and a code
|
||||
CONSTRAINT "oauth2_authorization_grants_code_challenge"
|
||||
CHECK (("code_challenge" IS NULL AND "code_challenge_method" IS NULL)
|
||||
OR ("code_challenge" IS NOT NULL AND "code_challenge_method" IS NOT NULL AND "response_type_code" IS TRUE))
|
||||
);
|
@@ -0,0 +1,26 @@
|
||||
-- Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
--
|
||||
-- Licensed under the Apache License, Version 2.0 (the "License");
|
||||
-- you may not use this file except in compliance with the License.
|
||||
-- You may obtain a copy of the License at
|
||||
--
|
||||
-- http://www.apache.org/licenses/LICENSE-2.0
|
||||
--
|
||||
-- Unless required by applicable law or agreed to in writing, software
|
||||
-- distributed under the License is distributed on an "AS IS" BASIS,
|
||||
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
-- See the License for the specific language governing permissions and
|
||||
-- limitations under the License.
|
||||
|
||||
ALTER TABLE users ADD COLUMN hashed_password TEXT;
|
||||
|
||||
UPDATE users u
|
||||
SET hashed_password = up.hashed_password
|
||||
FROM user_passwords up
|
||||
WHERE up.user_id = u.id;
|
||||
|
||||
ALTER TABLE users
|
||||
ALTER COLUMN hashed_password
|
||||
SET NOT NULL;
|
||||
|
||||
DROP TABLE user_passwords;
|
@@ -0,0 +1,27 @@
|
||||
-- Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
--
|
||||
-- Licensed under the Apache License, Version 2.0 (the "License");
|
||||
-- you may not use this file except in compliance with the License.
|
||||
-- You may obtain a copy of the License at
|
||||
--
|
||||
-- http://www.apache.org/licenses/LICENSE-2.0
|
||||
--
|
||||
-- Unless required by applicable law or agreed to in writing, software
|
||||
-- distributed under the License is distributed on an "AS IS" BASIS,
|
||||
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
-- See the License for the specific language governing permissions and
|
||||
-- limitations under the License.
|
||||
|
||||
CREATE TABLE user_passwords (
|
||||
"id" BIGSERIAL PRIMARY KEY,
|
||||
"user_id" BIGINT NOT NULL REFERENCES users (id) ON DELETE CASCADE,
|
||||
"hashed_password" TEXT NOT NULL,
|
||||
"created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
INSERT INTO
|
||||
user_passwords (user_id, hashed_password, created_at)
|
||||
SELECT id, hashed_password, updated_at
|
||||
FROM users;
|
||||
|
||||
ALTER TABLE users DROP COLUMN hashed_password;
|
956
crates/storage/sqlx-data.json
Normal file
956
crates/storage/sqlx-data.json
Normal file
@@ -0,0 +1,956 @@
|
||||
{
|
||||
"db": "PostgreSQL",
|
||||
"037ba804eabd0b4290d87d1de37054f358eb11397d3a8e4b69a81cdce0a178e0": {
|
||||
"query": "\n SELECT id, username\n FROM users\n WHERE username = $1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "username",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false
|
||||
]
|
||||
}
|
||||
},
|
||||
"0c056fcc1a85d00db88034bcc582376cf220e1933d2932e520c44ed9931f5c9d": {
|
||||
"query": "\n INSERT INTO oauth2_refresh_tokens\n (oauth2_session_id, oauth2_access_token_id, token)\n VALUES\n ($1, $2, $3)\n RETURNING\n id, created_at\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "created_at",
|
||||
"type_info": "Timestamptz"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"Int8",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false
|
||||
]
|
||||
}
|
||||
},
|
||||
"18d98b65c82142c28fb350f596c4439dbb04a55ff5b84586c1cb54601000d00d": {
|
||||
"query": "\n SELECT\n og.id AS grant_id,\n og.created_at AS grant_created_at,\n og.cancelled_at AS grant_cancelled_at,\n og.fulfilled_at AS grant_fulfilled_at,\n og.exchanged_at AS grant_exchanged_at,\n og.scope AS grant_scope,\n og.state AS grant_state,\n og.redirect_uri AS grant_redirect_uri,\n og.response_mode AS grant_response_mode,\n og.nonce AS grant_nonce,\n og.max_age AS grant_max_age,\n og.acr_values AS grant_acr_values,\n og.client_id AS client_id,\n og.code AS grant_code,\n og.response_type_code AS grant_response_type_code,\n og.response_type_token AS grant_response_type_token,\n og.response_type_id_token AS grant_response_type_id_token,\n og.code_challenge AS grant_code_challenge,\n og.code_challenge_method AS grant_code_challenge_method,\n os.id AS \"session_id?\",\n us.id AS \"user_session_id?\",\n us.created_at AS \"user_session_created_at?\",\n u.id AS \"user_id?\",\n u.username AS \"user_username?\",\n usa.id AS \"user_session_last_authentication_id?\",\n usa.created_at AS \"user_session_last_authentication_created_at?\"\n FROM\n oauth2_authorization_grants og\n LEFT JOIN oauth2_sessions os\n ON os.id = og.oauth2_session_id\n LEFT JOIN user_sessions us\n ON us.id = os.user_session_id\n LEFT JOIN users u\n ON u.id = us.user_id\n LEFT JOIN user_session_authentications usa\n ON usa.session_id = us.id\n WHERE\n og.code = $1\n\n ORDER BY usa.created_at DESC\n LIMIT 1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "grant_id",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "grant_created_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "grant_cancelled_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "grant_fulfilled_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "grant_exchanged_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "grant_scope",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "grant_state",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "grant_redirect_uri",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "grant_response_mode",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "grant_nonce",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"name": "grant_max_age",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"name": "grant_acr_values",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 12,
|
||||
"name": "client_id",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 13,
|
||||
"name": "grant_code",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 14,
|
||||
"name": "grant_response_type_code",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 15,
|
||||
"name": "grant_response_type_token",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 16,
|
||||
"name": "grant_response_type_id_token",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 17,
|
||||
"name": "grant_code_challenge",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 18,
|
||||
"name": "grant_code_challenge_method",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 19,
|
||||
"name": "session_id?",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 20,
|
||||
"name": "user_session_id?",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 21,
|
||||
"name": "user_session_created_at?",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 22,
|
||||
"name": "user_id?",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 23,
|
||||
"name": "user_username?",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 24,
|
||||
"name": "user_session_last_authentication_id?",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 25,
|
||||
"name": "user_session_last_authentication_created_at?",
|
||||
"type_info": "Timestamptz"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
}
|
||||
},
|
||||
"2dbccaf2fb557dd36598bf4d00941280535cc523ac3a481903ed825088901bce": {
|
||||
"query": "\n SELECT\n at.id AS \"access_token_id\",\n at.token AS \"access_token\",\n at.expires_after AS \"access_token_expires_after\",\n at.created_at AS \"access_token_created_at\",\n os.id AS \"session_id!\",\n os.client_id AS \"client_id!\",\n os.scope AS \"scope!\",\n us.id AS \"user_session_id!\",\n us.created_at AS \"user_session_created_at!\",\n u.id AS \"user_id!\",\n u.username AS \"user_username!\",\n usa.id AS \"user_session_last_authentication_id?\",\n usa.created_at AS \"user_session_last_authentication_created_at?\"\n\n FROM oauth2_access_tokens at\n INNER JOIN oauth2_sessions os\n ON os.id = at.oauth2_session_id\n INNER JOIN user_sessions us\n ON us.id = os.user_session_id\n INNER JOIN users u\n ON u.id = us.user_id\n LEFT JOIN user_session_authentications usa\n ON usa.session_id = us.id\n\n WHERE at.token = $1\n AND at.created_at + (at.expires_after * INTERVAL '1 second') >= now()\n AND us.active\n\n ORDER BY usa.created_at DESC\n LIMIT 1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "access_token_id",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "access_token",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "access_token_expires_after",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "access_token_created_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "session_id!",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "client_id!",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "scope!",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "user_session_id!",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "user_session_created_at!",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "user_id!",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"name": "user_username!",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"name": "user_session_last_authentication_id?",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 12,
|
||||
"name": "user_session_last_authentication_created_at?",
|
||||
"type_info": "Timestamptz"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
}
|
||||
},
|
||||
"307fd9f71e7a94a0a0d9ce523ee9792e127485d0d12480c43f179dd9b75afbab": {
|
||||
"query": "\n INSERT INTO user_sessions (user_id)\n VALUES ($1)\n RETURNING id, created_at\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "created_at",
|
||||
"type_info": "Timestamptz"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false
|
||||
]
|
||||
}
|
||||
},
|
||||
"3205a180aaa4661a016dada3a015ffd7a1019cd121e284f11e8120a6664e6288": {
|
||||
"query": "\n SELECT\n og.id AS grant_id,\n og.created_at AS grant_created_at,\n og.cancelled_at AS grant_cancelled_at,\n og.fulfilled_at AS grant_fulfilled_at,\n og.exchanged_at AS grant_exchanged_at,\n og.scope AS grant_scope,\n og.state AS grant_state,\n og.redirect_uri AS grant_redirect_uri,\n og.response_mode AS grant_response_mode,\n og.nonce AS grant_nonce,\n og.max_age AS grant_max_age,\n og.acr_values AS grant_acr_values,\n og.client_id AS client_id,\n og.code AS grant_code,\n og.response_type_code AS grant_response_type_code,\n og.response_type_token AS grant_response_type_token,\n og.response_type_id_token AS grant_response_type_id_token,\n og.code_challenge AS grant_code_challenge,\n og.code_challenge_method AS grant_code_challenge_method,\n os.id AS \"session_id?\",\n us.id AS \"user_session_id?\",\n us.created_at AS \"user_session_created_at?\",\n u.id AS \"user_id?\",\n u.username AS \"user_username?\",\n usa.id AS \"user_session_last_authentication_id?\",\n usa.created_at AS \"user_session_last_authentication_created_at?\"\n FROM\n oauth2_authorization_grants og\n LEFT JOIN oauth2_sessions os\n ON os.id = og.oauth2_session_id\n LEFT JOIN user_sessions us\n ON us.id = os.user_session_id\n LEFT JOIN users u\n ON u.id = us.user_id\n LEFT JOIN user_session_authentications usa\n ON usa.session_id = us.id\n WHERE\n og.id = $1\n\n ORDER BY usa.created_at DESC\n LIMIT 1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "grant_id",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "grant_created_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "grant_cancelled_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "grant_fulfilled_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "grant_exchanged_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "grant_scope",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "grant_state",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "grant_redirect_uri",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "grant_response_mode",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "grant_nonce",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"name": "grant_max_age",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"name": "grant_acr_values",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 12,
|
||||
"name": "client_id",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 13,
|
||||
"name": "grant_code",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 14,
|
||||
"name": "grant_response_type_code",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 15,
|
||||
"name": "grant_response_type_token",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 16,
|
||||
"name": "grant_response_type_id_token",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 17,
|
||||
"name": "grant_code_challenge",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 18,
|
||||
"name": "grant_code_challenge_method",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 19,
|
||||
"name": "session_id?",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 20,
|
||||
"name": "user_session_id?",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 21,
|
||||
"name": "user_session_created_at?",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 22,
|
||||
"name": "user_id?",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 23,
|
||||
"name": "user_username?",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 24,
|
||||
"name": "user_session_last_authentication_id?",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 25,
|
||||
"name": "user_session_last_authentication_created_at?",
|
||||
"type_info": "Timestamptz"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
}
|
||||
},
|
||||
"38641231a3bff71252e8bc0ead3a033c9148762ea64d707642551c01a4c89b84": {
|
||||
"query": "\n INSERT INTO oauth2_authorization_grants\n (client_id, redirect_uri, scope, state, nonce, max_age,\n acr_values, response_mode, code_challenge, code_challenge_method,\n response_type_code, response_type_token, response_type_id_token,\n code)\n VALUES\n ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)\n RETURNING id, created_at\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "created_at",
|
||||
"type_info": "Timestamptz"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text",
|
||||
"Text",
|
||||
"Text",
|
||||
"Text",
|
||||
"Int4",
|
||||
"Text",
|
||||
"Text",
|
||||
"Text",
|
||||
"Text",
|
||||
"Bool",
|
||||
"Bool",
|
||||
"Bool",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false
|
||||
]
|
||||
}
|
||||
},
|
||||
"59e8a5de682642883a9b9fc1b522736fa4397f0a0c97074f2c8908e5956c0166": {
|
||||
"query": "\n INSERT INTO oauth2_access_tokens\n (oauth2_session_id, token, expires_after)\n VALUES\n ($1, $2, $3)\n RETURNING\n id, created_at\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "created_at",
|
||||
"type_info": "Timestamptz"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"Text",
|
||||
"Int4"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false
|
||||
]
|
||||
}
|
||||
},
|
||||
"5d1a17b2ad6153217551ae31549ad9d62cc39d2f9a4e62a7ccb60fd91e0ac685": {
|
||||
"query": "\n DELETE FROM oauth2_access_tokens\n WHERE created_at + (expires_after * INTERVAL '1 second') + INTERVAL '15 minutes' < now()\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": []
|
||||
}
|
||||
},
|
||||
"647a2a5bbde39d0ed3931d0287b468bc7dedf6171e1dc6171a5d9f079b9ed0fa": {
|
||||
"query": "\n SELECT up.hashed_password\n FROM user_passwords up\n WHERE up.user_id = $1\n ORDER BY up.created_at DESC\n LIMIT 1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "hashed_password",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
}
|
||||
},
|
||||
"6765e725d31a1490ddee3f28e32dea41abdd9acefb1edd9a7b4e6790ec131173": {
|
||||
"query": "\n SELECT\n rt.id AS refresh_token_id,\n rt.token AS refresh_token,\n rt.created_at AS refresh_token_created_at,\n at.id AS \"access_token_id?\",\n at.token AS \"access_token?\",\n at.expires_after AS \"access_token_expires_after?\",\n at.created_at AS \"access_token_created_at?\",\n os.id AS \"session_id!\",\n os.client_id AS \"client_id!\",\n os.scope AS \"scope!\",\n us.id AS \"user_session_id!\",\n us.created_at AS \"user_session_created_at!\",\n u.id AS \"user_id!\",\n u.username AS \"user_username!\",\n usa.id AS \"user_session_last_authentication_id?\",\n usa.created_at AS \"user_session_last_authentication_created_at?\"\n FROM oauth2_refresh_tokens rt\n LEFT JOIN oauth2_access_tokens at\n ON at.id = rt.oauth2_access_token_id\n INNER JOIN oauth2_sessions os\n ON os.id = rt.oauth2_session_id\n INNER JOIN user_sessions us\n ON us.id = os.user_session_id\n INNER JOIN users u\n ON u.id = us.user_id\n LEFT JOIN user_session_authentications usa\n ON usa.session_id = us.id\n\n WHERE rt.token = $1\n AND rt.next_token_id IS NULL\n AND us.active\n\n ORDER BY usa.created_at DESC\n LIMIT 1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "refresh_token_id",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "refresh_token",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "refresh_token_created_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "access_token_id?",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "access_token?",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "access_token_expires_after?",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "access_token_created_at?",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "session_id!",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "client_id!",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "scope!",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"name": "user_session_id!",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"name": "user_session_created_at!",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 12,
|
||||
"name": "user_id!",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 13,
|
||||
"name": "user_username!",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 14,
|
||||
"name": "user_session_last_authentication_id?",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 15,
|
||||
"name": "user_session_last_authentication_created_at?",
|
||||
"type_info": "Timestamptz"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
}
|
||||
},
|
||||
"703850ba4e001d53776d77a64cbc1ee6feb61485ce41aff1103251f9b3778128": {
|
||||
"query": "\n UPDATE oauth2_authorization_grants AS og\n SET\n oauth2_session_id = os.id,\n fulfilled_at = os.created_at\n FROM oauth2_sessions os\n WHERE\n og.id = $1 AND os.id = $2\n RETURNING fulfilled_at AS \"fulfilled_at!: DateTime<Utc>\"\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "fulfilled_at!: DateTime<Utc>",
|
||||
"type_info": "Timestamptz"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
true
|
||||
]
|
||||
}
|
||||
},
|
||||
"7fa7d001583e98f5e4626751fa2c8743e7b83a240d1a51de50a7dba95c4e8a6b": {
|
||||
"query": "\n SELECT\n s.id,\n u.id as user_id,\n u.username,\n s.created_at,\n a.id as \"last_authentication_id?\",\n a.created_at as \"last_authd_at?\"\n FROM user_sessions s\n INNER JOIN users u \n ON s.user_id = u.id\n LEFT JOIN user_session_authentications a\n ON a.session_id = s.id\n WHERE s.id = $1 AND s.active\n ORDER BY a.created_at DESC\n LIMIT 1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "user_id",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "username",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "created_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "last_authentication_id?",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "last_authd_at?",
|
||||
"type_info": "Timestamptz"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
}
|
||||
},
|
||||
"88ac8783bd5881c42eafd9cf87a16fe6031f3153fd6a8618e689694584aeb2de": {
|
||||
"query": "\n DELETE FROM oauth2_access_tokens\n WHERE id = $1\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
}
|
||||
},
|
||||
"a09dfe1019110f2ec6eba0d35bafa467ab4b7980dd8b556826f03863f8edb0ab": {
|
||||
"query": "UPDATE user_sessions SET active = FALSE WHERE id = $1",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
}
|
||||
},
|
||||
"c29e741474aacc91c0aacc028a9e7452a5327d5ce6d4b791bf20a2636069087e": {
|
||||
"query": "\n INSERT INTO oauth2_sessions\n (user_session_id, client_id, scope)\n SELECT\n $1,\n og.client_id,\n og.scope\n FROM\n oauth2_authorization_grants og\n WHERE\n og.id = $2\n RETURNING id, created_at\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "created_at",
|
||||
"type_info": "Timestamptz"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false
|
||||
]
|
||||
}
|
||||
},
|
||||
"c2c402cfe0adcafa615f14a499caba4c96ca71d9ffb163e1feb05e5d85f3462c": {
|
||||
"query": "\n UPDATE oauth2_refresh_tokens\n SET next_token_id = $2\n WHERE id = $1\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
}
|
||||
},
|
||||
"d604e13bdfb2ff3d354d995f0b68f04091847755db98bafea7c45bd7b5c4ab68": {
|
||||
"query": "\n UPDATE oauth2_authorization_grants\n SET\n exchanged_at = NOW()\n WHERE\n id = $1\n RETURNING exchanged_at AS \"exchanged_at!: DateTime<Utc>\"\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "exchanged_at!: DateTime<Utc>",
|
||||
"type_info": "Timestamptz"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
true
|
||||
]
|
||||
}
|
||||
},
|
||||
"d7200c0def0662fda4af259c7872e06b8208e36f320ca90ea781c13d2bf85a9f": {
|
||||
"query": "\n INSERT INTO user_passwords (user_id, hashed_password)\n VALUES ($1, $2)\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
}
|
||||
},
|
||||
"d9d27eb4a0c11818a636d407438c4bc567a39396e7e236b3e776504417988eab": {
|
||||
"query": "\n INSERT INTO user_session_authentications (session_id)\n VALUES ($1)\n RETURNING id, created_at\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "created_at",
|
||||
"type_info": "Timestamptz"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false
|
||||
]
|
||||
}
|
||||
},
|
||||
"dda03ba41249bff965cb8f129acc15f4e40807adb9b75dee0ac43edd7809de84": {
|
||||
"query": "\n INSERT INTO users (username)\n VALUES ($1)\n RETURNING id\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
}
|
||||
},
|
||||
"e5cd99bdaf9c678fc659431fecc5d76b25bb08b781fd17e50eda82ea3aa8cea8": {
|
||||
"query": "\n SELECT COUNT(*) as \"count!\"\n FROM user_sessions s\n WHERE s.user_id = $1 AND s.active\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "count!",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
54
crates/storage/src/lib.rs
Normal file
54
crates/storage/src/lib.rs
Normal file
@@ -0,0 +1,54 @@
|
||||
// Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! Interactions with the database
|
||||
|
||||
#![allow(clippy::used_underscore_binding)] // This is needed by sqlx macros
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use mas_data_model::{StorageBackend, StorageBackendMarker};
|
||||
use serde::Serialize;
|
||||
use sqlx::migrate::Migrator;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
#[error("database query returned an inconsistent state")]
|
||||
pub struct DatabaseInconsistencyError;
|
||||
|
||||
#[derive(Serialize, Debug, Clone, PartialEq)]
|
||||
pub struct PostgresqlBackend;
|
||||
|
||||
impl StorageBackend for PostgresqlBackend {
|
||||
type AccessTokenData = i64;
|
||||
type AuthenticationData = i64;
|
||||
type AuthorizationGrantData = i64;
|
||||
type BrowserSessionData = i64;
|
||||
type ClientData = ();
|
||||
type RefreshTokenData = i64;
|
||||
type SessionData = i64;
|
||||
type UserData = i64;
|
||||
}
|
||||
|
||||
impl StorageBackendMarker for PostgresqlBackend {}
|
||||
|
||||
struct IdAndCreationTime {
|
||||
id: i64,
|
||||
created_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
pub mod oauth2;
|
||||
pub mod user;
|
||||
|
||||
/// Embedded migrations, allowing them to run on startup
|
||||
pub static MIGRATOR: Migrator = sqlx::migrate!();
|
221
crates/storage/src/oauth2/access_token.rs
Normal file
221
crates/storage/src/oauth2/access_token.rs
Normal file
@@ -0,0 +1,221 @@
|
||||
// Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use anyhow::Context;
|
||||
use chrono::{DateTime, Duration, Utc};
|
||||
use mas_data_model::{AccessToken, Authentication, BrowserSession, Client, Session, User};
|
||||
use sqlx::PgExecutor;
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::{DatabaseInconsistencyError, IdAndCreationTime, PostgresqlBackend};
|
||||
|
||||
pub async fn add_access_token(
|
||||
executor: impl PgExecutor<'_>,
|
||||
session: &Session<PostgresqlBackend>,
|
||||
token: &str,
|
||||
expires_after: Duration,
|
||||
) -> anyhow::Result<AccessToken<PostgresqlBackend>> {
|
||||
// Checked convertion of duration to i32, maxing at i32::MAX
|
||||
let expires_after_seconds = i32::try_from(expires_after.num_seconds()).unwrap_or(i32::MAX);
|
||||
|
||||
let res = sqlx::query_as!(
|
||||
IdAndCreationTime,
|
||||
r#"
|
||||
INSERT INTO oauth2_access_tokens
|
||||
(oauth2_session_id, token, expires_after)
|
||||
VALUES
|
||||
($1, $2, $3)
|
||||
RETURNING
|
||||
id, created_at
|
||||
"#,
|
||||
session.data,
|
||||
token,
|
||||
expires_after_seconds,
|
||||
)
|
||||
.fetch_one(executor)
|
||||
.await
|
||||
.context("could not insert oauth2 access token")?;
|
||||
|
||||
Ok(AccessToken {
|
||||
data: res.id,
|
||||
expires_after,
|
||||
token: token.to_string(),
|
||||
jti: format!("{}", res.id),
|
||||
created_at: res.created_at,
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct OAuth2AccessTokenLookup {
|
||||
access_token_id: i64,
|
||||
access_token: String,
|
||||
access_token_expires_after: i32,
|
||||
access_token_created_at: DateTime<Utc>,
|
||||
session_id: i64,
|
||||
client_id: String,
|
||||
scope: String,
|
||||
user_session_id: i64,
|
||||
user_session_created_at: DateTime<Utc>,
|
||||
user_id: i64,
|
||||
user_username: String,
|
||||
user_session_last_authentication_id: Option<i64>,
|
||||
user_session_last_authentication_created_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
#[error("failed to lookup access token")]
|
||||
pub enum AccessTokenLookupError {
|
||||
Database(#[from] sqlx::Error),
|
||||
Inconsistency(#[from] DatabaseInconsistencyError),
|
||||
}
|
||||
|
||||
impl AccessTokenLookupError {
|
||||
#[must_use]
|
||||
pub fn not_found(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
&AccessTokenLookupError::Database(sqlx::Error::RowNotFound)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn lookup_active_access_token(
|
||||
executor: impl PgExecutor<'_>,
|
||||
token: &str,
|
||||
) -> Result<(AccessToken<PostgresqlBackend>, Session<PostgresqlBackend>), AccessTokenLookupError> {
|
||||
let res = sqlx::query_as!(
|
||||
OAuth2AccessTokenLookup,
|
||||
r#"
|
||||
SELECT
|
||||
at.id AS "access_token_id",
|
||||
at.token AS "access_token",
|
||||
at.expires_after AS "access_token_expires_after",
|
||||
at.created_at AS "access_token_created_at",
|
||||
os.id AS "session_id!",
|
||||
os.client_id AS "client_id!",
|
||||
os.scope AS "scope!",
|
||||
us.id AS "user_session_id!",
|
||||
us.created_at AS "user_session_created_at!",
|
||||
u.id AS "user_id!",
|
||||
u.username AS "user_username!",
|
||||
usa.id AS "user_session_last_authentication_id?",
|
||||
usa.created_at AS "user_session_last_authentication_created_at?"
|
||||
|
||||
FROM oauth2_access_tokens at
|
||||
INNER JOIN oauth2_sessions os
|
||||
ON os.id = at.oauth2_session_id
|
||||
INNER JOIN user_sessions us
|
||||
ON us.id = os.user_session_id
|
||||
INNER JOIN users u
|
||||
ON u.id = us.user_id
|
||||
LEFT JOIN user_session_authentications usa
|
||||
ON usa.session_id = us.id
|
||||
|
||||
WHERE at.token = $1
|
||||
AND at.created_at + (at.expires_after * INTERVAL '1 second') >= now()
|
||||
AND us.active
|
||||
|
||||
ORDER BY usa.created_at DESC
|
||||
LIMIT 1
|
||||
"#,
|
||||
token,
|
||||
)
|
||||
.fetch_one(executor)
|
||||
.await?;
|
||||
|
||||
let access_token = AccessToken {
|
||||
data: res.access_token_id,
|
||||
jti: format!("{}", res.access_token_id),
|
||||
token: res.access_token,
|
||||
created_at: res.access_token_created_at,
|
||||
expires_after: Duration::seconds(res.access_token_expires_after.into()),
|
||||
};
|
||||
|
||||
let client = Client {
|
||||
data: (),
|
||||
client_id: res.client_id,
|
||||
};
|
||||
|
||||
let user = User {
|
||||
data: res.user_id,
|
||||
username: res.user_username,
|
||||
sub: format!("fake-sub-{}", res.user_id),
|
||||
};
|
||||
|
||||
let last_authentication = match (
|
||||
res.user_session_last_authentication_id,
|
||||
res.user_session_last_authentication_created_at,
|
||||
) {
|
||||
(None, None) => None,
|
||||
(Some(id), Some(created_at)) => Some(Authentication {
|
||||
data: id,
|
||||
created_at,
|
||||
}),
|
||||
_ => return Err(DatabaseInconsistencyError.into()),
|
||||
};
|
||||
|
||||
let browser_session = BrowserSession {
|
||||
data: res.user_session_id,
|
||||
created_at: res.user_session_created_at,
|
||||
user,
|
||||
last_authentication,
|
||||
};
|
||||
|
||||
let scope = res.scope.parse().map_err(|_e| DatabaseInconsistencyError)?;
|
||||
|
||||
let session = Session {
|
||||
data: res.session_id,
|
||||
client,
|
||||
browser_session,
|
||||
scope,
|
||||
};
|
||||
|
||||
Ok((access_token, session))
|
||||
}
|
||||
|
||||
pub async fn revoke_access_token(
|
||||
executor: impl PgExecutor<'_>,
|
||||
access_token: &AccessToken<PostgresqlBackend>,
|
||||
) -> anyhow::Result<()> {
|
||||
let res = sqlx::query!(
|
||||
r#"
|
||||
DELETE FROM oauth2_access_tokens
|
||||
WHERE id = $1
|
||||
"#,
|
||||
access_token.data,
|
||||
)
|
||||
.execute(executor)
|
||||
.await
|
||||
.context("could not revoke access tokens")?;
|
||||
|
||||
if res.rows_affected() == 1 {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(anyhow::anyhow!("no row were affected when revoking token"))
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn cleanup_expired(executor: impl PgExecutor<'_>) -> anyhow::Result<u64> {
|
||||
let res = sqlx::query!(
|
||||
r#"
|
||||
DELETE FROM oauth2_access_tokens
|
||||
WHERE created_at + (expires_after * INTERVAL '1 second') + INTERVAL '15 minutes' < now()
|
||||
"#,
|
||||
)
|
||||
.execute(executor)
|
||||
.await
|
||||
.context("could not cleanup expired access tokens")?;
|
||||
|
||||
Ok(res.rows_affected())
|
||||
}
|
510
crates/storage/src/oauth2/authorization_grant.rs
Normal file
510
crates/storage/src/oauth2/authorization_grant.rs
Normal file
@@ -0,0 +1,510 @@
|
||||
// Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#![allow(clippy::unused_async)]
|
||||
|
||||
use std::num::NonZeroU32;
|
||||
|
||||
use anyhow::Context;
|
||||
use chrono::{DateTime, Utc};
|
||||
use mas_data_model::{
|
||||
Authentication, AuthorizationCode, AuthorizationGrant, AuthorizationGrantStage, BrowserSession,
|
||||
Client, Pkce, Session, User,
|
||||
};
|
||||
use oauth2_types::{pkce::CodeChallengeMethod, requests::ResponseMode, scope::Scope};
|
||||
use sqlx::PgExecutor;
|
||||
use url::Url;
|
||||
|
||||
use crate::{DatabaseInconsistencyError, IdAndCreationTime, PostgresqlBackend};
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn new_authorization_grant(
|
||||
executor: impl PgExecutor<'_>,
|
||||
client_id: String,
|
||||
redirect_uri: Url,
|
||||
scope: Scope,
|
||||
code: Option<AuthorizationCode>,
|
||||
state: Option<String>,
|
||||
nonce: Option<String>,
|
||||
max_age: Option<NonZeroU32>,
|
||||
acr_values: Option<String>,
|
||||
response_mode: ResponseMode,
|
||||
response_type_token: bool,
|
||||
response_type_id_token: bool,
|
||||
) -> anyhow::Result<AuthorizationGrant<PostgresqlBackend>> {
|
||||
let code_challenge = code
|
||||
.as_ref()
|
||||
.and_then(|c| c.pkce.as_ref())
|
||||
.map(|p| &p.challenge);
|
||||
let code_challenge_method = code
|
||||
.as_ref()
|
||||
.and_then(|c| c.pkce.as_ref())
|
||||
.map(|p| p.challenge_method.to_string());
|
||||
let code_str = code.as_ref().map(|c| &c.code);
|
||||
let res = sqlx::query_as!(
|
||||
IdAndCreationTime,
|
||||
r#"
|
||||
INSERT INTO oauth2_authorization_grants
|
||||
(client_id, redirect_uri, scope, state, nonce, max_age,
|
||||
acr_values, response_mode, code_challenge, code_challenge_method,
|
||||
response_type_code, response_type_token, response_type_id_token,
|
||||
code)
|
||||
VALUES
|
||||
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)
|
||||
RETURNING id, created_at
|
||||
"#,
|
||||
&client_id,
|
||||
redirect_uri.to_string(),
|
||||
scope.to_string(),
|
||||
state,
|
||||
nonce,
|
||||
// TODO: this conversion is a bit ugly
|
||||
max_age.map(|x| i32::try_from(u32::from(x)).unwrap_or(i32::MAX)),
|
||||
acr_values,
|
||||
response_mode.to_string(),
|
||||
code_challenge,
|
||||
code_challenge_method,
|
||||
code.is_some(),
|
||||
response_type_token,
|
||||
response_type_id_token,
|
||||
code_str,
|
||||
)
|
||||
.fetch_one(executor)
|
||||
.await
|
||||
.context("could not insert oauth2 authorization grant")?;
|
||||
|
||||
let client = Client {
|
||||
data: (),
|
||||
client_id,
|
||||
};
|
||||
|
||||
Ok(AuthorizationGrant {
|
||||
data: res.id,
|
||||
stage: AuthorizationGrantStage::Pending,
|
||||
code,
|
||||
redirect_uri,
|
||||
client,
|
||||
scope,
|
||||
state,
|
||||
nonce,
|
||||
max_age,
|
||||
acr_values,
|
||||
response_mode,
|
||||
created_at: res.created_at,
|
||||
response_type_token,
|
||||
response_type_id_token,
|
||||
})
|
||||
}
|
||||
|
||||
struct GrantLookup {
|
||||
grant_id: i64,
|
||||
grant_created_at: DateTime<Utc>,
|
||||
grant_cancelled_at: Option<DateTime<Utc>>,
|
||||
grant_fulfilled_at: Option<DateTime<Utc>>,
|
||||
grant_exchanged_at: Option<DateTime<Utc>>,
|
||||
grant_scope: String,
|
||||
grant_state: Option<String>,
|
||||
grant_redirect_uri: String,
|
||||
grant_response_mode: String,
|
||||
grant_nonce: Option<String>,
|
||||
grant_max_age: Option<i32>,
|
||||
grant_acr_values: Option<String>,
|
||||
grant_response_type_code: bool,
|
||||
grant_response_type_token: bool,
|
||||
grant_response_type_id_token: bool,
|
||||
grant_code: Option<String>,
|
||||
grant_code_challenge: Option<String>,
|
||||
grant_code_challenge_method: Option<String>,
|
||||
client_id: String,
|
||||
session_id: Option<i64>,
|
||||
user_session_id: Option<i64>,
|
||||
user_session_created_at: Option<DateTime<Utc>>,
|
||||
user_id: Option<i64>,
|
||||
user_username: Option<String>,
|
||||
user_session_last_authentication_id: Option<i64>,
|
||||
user_session_last_authentication_created_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
impl TryInto<AuthorizationGrant<PostgresqlBackend>> for GrantLookup {
|
||||
type Error = DatabaseInconsistencyError;
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
fn try_into(self) -> Result<AuthorizationGrant<PostgresqlBackend>, Self::Error> {
|
||||
let scope: Scope = self
|
||||
.grant_scope
|
||||
.parse()
|
||||
.map_err(|_e| DatabaseInconsistencyError)?;
|
||||
|
||||
let client = Client {
|
||||
data: (),
|
||||
client_id: self.client_id,
|
||||
};
|
||||
|
||||
let last_authentication = match (
|
||||
self.user_session_last_authentication_id,
|
||||
self.user_session_last_authentication_created_at,
|
||||
) {
|
||||
(Some(id), Some(created_at)) => Some(Authentication {
|
||||
data: id,
|
||||
created_at,
|
||||
}),
|
||||
(None, None) => None,
|
||||
_ => return Err(DatabaseInconsistencyError),
|
||||
};
|
||||
|
||||
let session = match (
|
||||
self.session_id,
|
||||
self.user_session_id,
|
||||
self.user_session_created_at,
|
||||
self.user_id,
|
||||
self.user_username,
|
||||
last_authentication,
|
||||
) {
|
||||
(
|
||||
Some(session_id),
|
||||
Some(user_session_id),
|
||||
Some(user_session_created_at),
|
||||
Some(user_id),
|
||||
Some(user_username),
|
||||
last_authentication,
|
||||
) => {
|
||||
let user = User {
|
||||
data: user_id,
|
||||
username: user_username,
|
||||
sub: format!("fake-sub-{}", user_id),
|
||||
};
|
||||
|
||||
let browser_session = BrowserSession {
|
||||
data: user_session_id,
|
||||
user,
|
||||
created_at: user_session_created_at,
|
||||
last_authentication,
|
||||
};
|
||||
|
||||
let client = client.clone();
|
||||
let scope = scope.clone();
|
||||
|
||||
let session = Session {
|
||||
data: session_id,
|
||||
client,
|
||||
browser_session,
|
||||
scope,
|
||||
};
|
||||
|
||||
Some(session)
|
||||
}
|
||||
(None, None, None, None, None, None) => None,
|
||||
_ => return Err(DatabaseInconsistencyError),
|
||||
};
|
||||
|
||||
let stage = match (
|
||||
self.grant_fulfilled_at,
|
||||
self.grant_exchanged_at,
|
||||
self.grant_cancelled_at,
|
||||
session,
|
||||
) {
|
||||
(None, None, None, None) => AuthorizationGrantStage::Pending,
|
||||
(Some(fulfilled_at), None, None, Some(session)) => AuthorizationGrantStage::Fulfilled {
|
||||
session,
|
||||
fulfilled_at,
|
||||
},
|
||||
(Some(fulfilled_at), Some(exchanged_at), None, Some(session)) => {
|
||||
AuthorizationGrantStage::Exchanged {
|
||||
session,
|
||||
fulfilled_at,
|
||||
exchanged_at,
|
||||
}
|
||||
}
|
||||
(None, None, Some(cancelled_at), None) => {
|
||||
AuthorizationGrantStage::Cancelled { cancelled_at }
|
||||
}
|
||||
_ => {
|
||||
return Err(DatabaseInconsistencyError);
|
||||
}
|
||||
};
|
||||
|
||||
let pkce = match (self.grant_code_challenge, self.grant_code_challenge_method) {
|
||||
(Some(challenge), Some(challenge_method)) if challenge_method == "plain" => {
|
||||
Some(Pkce {
|
||||
challenge_method: CodeChallengeMethod::Plain,
|
||||
challenge,
|
||||
})
|
||||
}
|
||||
(Some(challenge), Some(challenge_method)) if challenge_method == "S256" => Some(Pkce {
|
||||
challenge_method: CodeChallengeMethod::S256,
|
||||
challenge,
|
||||
}),
|
||||
(None, None) => None,
|
||||
_ => {
|
||||
return Err(DatabaseInconsistencyError);
|
||||
}
|
||||
};
|
||||
|
||||
let code: Option<AuthorizationCode> =
|
||||
match (self.grant_response_type_code, self.grant_code, pkce) {
|
||||
(false, None, None) => None,
|
||||
(true, Some(code), pkce) => Some(AuthorizationCode { code, pkce }),
|
||||
_ => {
|
||||
return Err(DatabaseInconsistencyError);
|
||||
}
|
||||
};
|
||||
|
||||
let redirect_uri = self
|
||||
.grant_redirect_uri
|
||||
.parse()
|
||||
.map_err(|_e| DatabaseInconsistencyError)?;
|
||||
|
||||
let response_mode = self
|
||||
.grant_response_mode
|
||||
.parse()
|
||||
.map_err(|_e| DatabaseInconsistencyError)?;
|
||||
|
||||
let max_age = self
|
||||
.grant_max_age
|
||||
.map(u32::try_from)
|
||||
.transpose()
|
||||
.map_err(|_e| DatabaseInconsistencyError)?
|
||||
.map(NonZeroU32::try_from)
|
||||
.transpose()
|
||||
.map_err(|_e| DatabaseInconsistencyError)?;
|
||||
|
||||
Ok(AuthorizationGrant {
|
||||
data: self.grant_id,
|
||||
stage,
|
||||
client,
|
||||
code,
|
||||
acr_values: self.grant_acr_values,
|
||||
scope,
|
||||
state: self.grant_state,
|
||||
nonce: self.grant_nonce,
|
||||
max_age, // TODO
|
||||
response_mode,
|
||||
redirect_uri,
|
||||
created_at: self.grant_created_at,
|
||||
response_type_token: self.grant_response_type_token,
|
||||
response_type_id_token: self.grant_response_type_id_token,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_grant_by_id(
|
||||
executor: impl PgExecutor<'_>,
|
||||
id: i64,
|
||||
) -> anyhow::Result<AuthorizationGrant<PostgresqlBackend>> {
|
||||
// TODO: handle "not found" cases
|
||||
let res = sqlx::query_as!(
|
||||
GrantLookup,
|
||||
r#"
|
||||
SELECT
|
||||
og.id AS grant_id,
|
||||
og.created_at AS grant_created_at,
|
||||
og.cancelled_at AS grant_cancelled_at,
|
||||
og.fulfilled_at AS grant_fulfilled_at,
|
||||
og.exchanged_at AS grant_exchanged_at,
|
||||
og.scope AS grant_scope,
|
||||
og.state AS grant_state,
|
||||
og.redirect_uri AS grant_redirect_uri,
|
||||
og.response_mode AS grant_response_mode,
|
||||
og.nonce AS grant_nonce,
|
||||
og.max_age AS grant_max_age,
|
||||
og.acr_values AS grant_acr_values,
|
||||
og.client_id AS client_id,
|
||||
og.code AS grant_code,
|
||||
og.response_type_code AS grant_response_type_code,
|
||||
og.response_type_token AS grant_response_type_token,
|
||||
og.response_type_id_token AS grant_response_type_id_token,
|
||||
og.code_challenge AS grant_code_challenge,
|
||||
og.code_challenge_method AS grant_code_challenge_method,
|
||||
os.id AS "session_id?",
|
||||
us.id AS "user_session_id?",
|
||||
us.created_at AS "user_session_created_at?",
|
||||
u.id AS "user_id?",
|
||||
u.username AS "user_username?",
|
||||
usa.id AS "user_session_last_authentication_id?",
|
||||
usa.created_at AS "user_session_last_authentication_created_at?"
|
||||
FROM
|
||||
oauth2_authorization_grants og
|
||||
LEFT JOIN oauth2_sessions os
|
||||
ON os.id = og.oauth2_session_id
|
||||
LEFT JOIN user_sessions us
|
||||
ON us.id = os.user_session_id
|
||||
LEFT JOIN users u
|
||||
ON u.id = us.user_id
|
||||
LEFT JOIN user_session_authentications usa
|
||||
ON usa.session_id = us.id
|
||||
WHERE
|
||||
og.id = $1
|
||||
|
||||
ORDER BY usa.created_at DESC
|
||||
LIMIT 1
|
||||
"#,
|
||||
id,
|
||||
)
|
||||
.fetch_one(executor)
|
||||
.await
|
||||
.context("failed to get grant by id")?;
|
||||
|
||||
let grant = res.try_into()?;
|
||||
|
||||
Ok(grant)
|
||||
}
|
||||
|
||||
pub async fn lookup_grant_by_code(
|
||||
executor: impl PgExecutor<'_>,
|
||||
code: &str,
|
||||
) -> anyhow::Result<AuthorizationGrant<PostgresqlBackend>> {
|
||||
// TODO: handle "not found" cases
|
||||
let res = sqlx::query_as!(
|
||||
GrantLookup,
|
||||
r#"
|
||||
SELECT
|
||||
og.id AS grant_id,
|
||||
og.created_at AS grant_created_at,
|
||||
og.cancelled_at AS grant_cancelled_at,
|
||||
og.fulfilled_at AS grant_fulfilled_at,
|
||||
og.exchanged_at AS grant_exchanged_at,
|
||||
og.scope AS grant_scope,
|
||||
og.state AS grant_state,
|
||||
og.redirect_uri AS grant_redirect_uri,
|
||||
og.response_mode AS grant_response_mode,
|
||||
og.nonce AS grant_nonce,
|
||||
og.max_age AS grant_max_age,
|
||||
og.acr_values AS grant_acr_values,
|
||||
og.client_id AS client_id,
|
||||
og.code AS grant_code,
|
||||
og.response_type_code AS grant_response_type_code,
|
||||
og.response_type_token AS grant_response_type_token,
|
||||
og.response_type_id_token AS grant_response_type_id_token,
|
||||
og.code_challenge AS grant_code_challenge,
|
||||
og.code_challenge_method AS grant_code_challenge_method,
|
||||
os.id AS "session_id?",
|
||||
us.id AS "user_session_id?",
|
||||
us.created_at AS "user_session_created_at?",
|
||||
u.id AS "user_id?",
|
||||
u.username AS "user_username?",
|
||||
usa.id AS "user_session_last_authentication_id?",
|
||||
usa.created_at AS "user_session_last_authentication_created_at?"
|
||||
FROM
|
||||
oauth2_authorization_grants og
|
||||
LEFT JOIN oauth2_sessions os
|
||||
ON os.id = og.oauth2_session_id
|
||||
LEFT JOIN user_sessions us
|
||||
ON us.id = os.user_session_id
|
||||
LEFT JOIN users u
|
||||
ON u.id = us.user_id
|
||||
LEFT JOIN user_session_authentications usa
|
||||
ON usa.session_id = us.id
|
||||
WHERE
|
||||
og.code = $1
|
||||
|
||||
ORDER BY usa.created_at DESC
|
||||
LIMIT 1
|
||||
"#,
|
||||
code,
|
||||
)
|
||||
.fetch_one(executor)
|
||||
.await
|
||||
.context("failed to lookup grant by code")?;
|
||||
|
||||
let grant = res.try_into()?;
|
||||
|
||||
Ok(grant)
|
||||
}
|
||||
|
||||
pub async fn derive_session(
|
||||
executor: impl PgExecutor<'_>,
|
||||
grant: &AuthorizationGrant<PostgresqlBackend>,
|
||||
browser_session: BrowserSession<PostgresqlBackend>,
|
||||
) -> anyhow::Result<Session<PostgresqlBackend>> {
|
||||
let res = sqlx::query_as!(
|
||||
IdAndCreationTime,
|
||||
r#"
|
||||
INSERT INTO oauth2_sessions
|
||||
(user_session_id, client_id, scope)
|
||||
SELECT
|
||||
$1,
|
||||
og.client_id,
|
||||
og.scope
|
||||
FROM
|
||||
oauth2_authorization_grants og
|
||||
WHERE
|
||||
og.id = $2
|
||||
RETURNING id, created_at
|
||||
"#,
|
||||
browser_session.data,
|
||||
grant.data,
|
||||
)
|
||||
.fetch_one(executor)
|
||||
.await
|
||||
.context("could not insert oauth2 session")?;
|
||||
|
||||
Ok(Session {
|
||||
data: res.id,
|
||||
browser_session,
|
||||
client: grant.client.clone(),
|
||||
scope: grant.scope.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn fulfill_grant(
|
||||
executor: impl PgExecutor<'_>,
|
||||
mut grant: AuthorizationGrant<PostgresqlBackend>,
|
||||
session: Session<PostgresqlBackend>,
|
||||
) -> anyhow::Result<AuthorizationGrant<PostgresqlBackend>> {
|
||||
let fulfilled_at = sqlx::query_scalar!(
|
||||
r#"
|
||||
UPDATE oauth2_authorization_grants AS og
|
||||
SET
|
||||
oauth2_session_id = os.id,
|
||||
fulfilled_at = os.created_at
|
||||
FROM oauth2_sessions os
|
||||
WHERE
|
||||
og.id = $1 AND os.id = $2
|
||||
RETURNING fulfilled_at AS "fulfilled_at!: DateTime<Utc>"
|
||||
"#,
|
||||
grant.data,
|
||||
session.data,
|
||||
)
|
||||
.fetch_one(executor)
|
||||
.await
|
||||
.context("could not makr grant as fulfilled")?;
|
||||
|
||||
grant.stage = grant.stage.fulfill(fulfilled_at, session)?;
|
||||
|
||||
Ok(grant)
|
||||
}
|
||||
|
||||
pub async fn exchange_grant(
|
||||
executor: impl PgExecutor<'_>,
|
||||
mut grant: AuthorizationGrant<PostgresqlBackend>,
|
||||
) -> anyhow::Result<AuthorizationGrant<PostgresqlBackend>> {
|
||||
let exchanged_at = sqlx::query_scalar!(
|
||||
r#"
|
||||
UPDATE oauth2_authorization_grants
|
||||
SET
|
||||
exchanged_at = NOW()
|
||||
WHERE
|
||||
id = $1
|
||||
RETURNING exchanged_at AS "exchanged_at!: DateTime<Utc>"
|
||||
"#,
|
||||
grant.data,
|
||||
)
|
||||
.fetch_one(executor)
|
||||
.await
|
||||
.context("could not mark grant as exchanged")?;
|
||||
|
||||
grant.stage = grant.stage.exchange(exchanged_at)?;
|
||||
|
||||
Ok(grant)
|
||||
}
|
17
crates/storage/src/oauth2/mod.rs
Normal file
17
crates/storage/src/oauth2/mod.rs
Normal file
@@ -0,0 +1,17 @@
|
||||
// Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
pub mod access_token;
|
||||
pub mod authorization_grant;
|
||||
pub mod refresh_token;
|
214
crates/storage/src/oauth2/refresh_token.rs
Normal file
214
crates/storage/src/oauth2/refresh_token.rs
Normal file
@@ -0,0 +1,214 @@
|
||||
// Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use anyhow::Context;
|
||||
use chrono::{DateTime, Duration, Utc};
|
||||
use mas_data_model::{
|
||||
AccessToken, Authentication, BrowserSession, Client, RefreshToken, Session, User,
|
||||
};
|
||||
use sqlx::PgExecutor;
|
||||
|
||||
use crate::{DatabaseInconsistencyError, IdAndCreationTime, PostgresqlBackend};
|
||||
|
||||
pub async fn add_refresh_token(
|
||||
executor: impl PgExecutor<'_>,
|
||||
session: &Session<PostgresqlBackend>,
|
||||
access_token: AccessToken<PostgresqlBackend>,
|
||||
token: &str,
|
||||
) -> anyhow::Result<RefreshToken<PostgresqlBackend>> {
|
||||
let res = sqlx::query_as!(
|
||||
IdAndCreationTime,
|
||||
r#"
|
||||
INSERT INTO oauth2_refresh_tokens
|
||||
(oauth2_session_id, oauth2_access_token_id, token)
|
||||
VALUES
|
||||
($1, $2, $3)
|
||||
RETURNING
|
||||
id, created_at
|
||||
"#,
|
||||
session.data,
|
||||
access_token.data,
|
||||
token,
|
||||
)
|
||||
.fetch_one(executor)
|
||||
.await
|
||||
.context("could not insert oauth2 refresh token")?;
|
||||
|
||||
Ok(RefreshToken {
|
||||
data: res.id,
|
||||
token: token.to_string(),
|
||||
access_token: Some(access_token),
|
||||
created_at: res.created_at,
|
||||
})
|
||||
}
|
||||
|
||||
struct OAuth2RefreshTokenLookup {
|
||||
refresh_token_id: i64,
|
||||
refresh_token: String,
|
||||
refresh_token_created_at: DateTime<Utc>,
|
||||
access_token_id: Option<i64>,
|
||||
access_token: Option<String>,
|
||||
access_token_expires_after: Option<i32>,
|
||||
access_token_created_at: Option<DateTime<Utc>>,
|
||||
session_id: i64,
|
||||
client_id: String,
|
||||
scope: String,
|
||||
user_session_id: i64,
|
||||
user_session_created_at: DateTime<Utc>,
|
||||
user_id: i64,
|
||||
user_username: String,
|
||||
user_session_last_authentication_id: Option<i64>,
|
||||
user_session_last_authentication_created_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
pub async fn lookup_active_refresh_token(
|
||||
executor: impl PgExecutor<'_>,
|
||||
token: &str,
|
||||
) -> anyhow::Result<(RefreshToken<PostgresqlBackend>, Session<PostgresqlBackend>)> {
|
||||
let res = sqlx::query_as!(
|
||||
OAuth2RefreshTokenLookup,
|
||||
r#"
|
||||
SELECT
|
||||
rt.id AS refresh_token_id,
|
||||
rt.token AS refresh_token,
|
||||
rt.created_at AS refresh_token_created_at,
|
||||
at.id AS "access_token_id?",
|
||||
at.token AS "access_token?",
|
||||
at.expires_after AS "access_token_expires_after?",
|
||||
at.created_at AS "access_token_created_at?",
|
||||
os.id AS "session_id!",
|
||||
os.client_id AS "client_id!",
|
||||
os.scope AS "scope!",
|
||||
us.id AS "user_session_id!",
|
||||
us.created_at AS "user_session_created_at!",
|
||||
u.id AS "user_id!",
|
||||
u.username AS "user_username!",
|
||||
usa.id AS "user_session_last_authentication_id?",
|
||||
usa.created_at AS "user_session_last_authentication_created_at?"
|
||||
FROM oauth2_refresh_tokens rt
|
||||
LEFT JOIN oauth2_access_tokens at
|
||||
ON at.id = rt.oauth2_access_token_id
|
||||
INNER JOIN oauth2_sessions os
|
||||
ON os.id = rt.oauth2_session_id
|
||||
INNER JOIN user_sessions us
|
||||
ON us.id = os.user_session_id
|
||||
INNER JOIN users u
|
||||
ON u.id = us.user_id
|
||||
LEFT JOIN user_session_authentications usa
|
||||
ON usa.session_id = us.id
|
||||
|
||||
WHERE rt.token = $1
|
||||
AND rt.next_token_id IS NULL
|
||||
AND us.active
|
||||
|
||||
ORDER BY usa.created_at DESC
|
||||
LIMIT 1
|
||||
"#,
|
||||
token,
|
||||
)
|
||||
.fetch_one(executor)
|
||||
.await
|
||||
.context("failed to fetch oauth2 refresh token")?;
|
||||
|
||||
let access_token = match (
|
||||
res.access_token_id,
|
||||
res.access_token,
|
||||
res.access_token_created_at,
|
||||
res.access_token_expires_after,
|
||||
) {
|
||||
(None, None, None, None) => None,
|
||||
(Some(id), Some(token), Some(created_at), Some(expires_after)) => Some(AccessToken {
|
||||
data: id,
|
||||
jti: format!("{}", id),
|
||||
token,
|
||||
created_at,
|
||||
expires_after: Duration::seconds(expires_after.into()),
|
||||
}),
|
||||
_ => return Err(DatabaseInconsistencyError.into()),
|
||||
};
|
||||
|
||||
let refresh_token = RefreshToken {
|
||||
data: res.refresh_token_id,
|
||||
token: res.refresh_token,
|
||||
created_at: res.refresh_token_created_at,
|
||||
access_token,
|
||||
};
|
||||
|
||||
let client = Client {
|
||||
data: (),
|
||||
client_id: res.client_id,
|
||||
};
|
||||
|
||||
let user = User {
|
||||
data: res.user_id,
|
||||
username: res.user_username,
|
||||
sub: format!("fake-sub-{}", res.user_id),
|
||||
};
|
||||
|
||||
let last_authentication = match (
|
||||
res.user_session_last_authentication_id,
|
||||
res.user_session_last_authentication_created_at,
|
||||
) {
|
||||
(None, None) => None,
|
||||
(Some(id), Some(created_at)) => Some(Authentication {
|
||||
data: id,
|
||||
created_at,
|
||||
}),
|
||||
_ => return Err(DatabaseInconsistencyError.into()),
|
||||
};
|
||||
|
||||
let browser_session = BrowserSession {
|
||||
data: res.user_session_id,
|
||||
created_at: res.user_session_created_at,
|
||||
user,
|
||||
last_authentication,
|
||||
};
|
||||
|
||||
let session = Session {
|
||||
data: res.session_id,
|
||||
client,
|
||||
browser_session,
|
||||
scope: res.scope.parse().context("invalid scope in database")?,
|
||||
};
|
||||
|
||||
Ok((refresh_token, session))
|
||||
}
|
||||
|
||||
pub async fn replace_refresh_token(
|
||||
executor: impl PgExecutor<'_>,
|
||||
refresh_token: &RefreshToken<PostgresqlBackend>,
|
||||
next_refresh_token: &RefreshToken<PostgresqlBackend>,
|
||||
) -> anyhow::Result<()> {
|
||||
let res = sqlx::query!(
|
||||
r#"
|
||||
UPDATE oauth2_refresh_tokens
|
||||
SET next_token_id = $2
|
||||
WHERE id = $1
|
||||
"#,
|
||||
refresh_token.data,
|
||||
next_refresh_token.data
|
||||
)
|
||||
.execute(executor)
|
||||
.await
|
||||
.context("failed to update oauth2 refresh token")?;
|
||||
|
||||
if res.rows_affected() == 1 {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(anyhow::anyhow!(
|
||||
"no row were affected when updating refresh token"
|
||||
))
|
||||
}
|
||||
}
|
414
crates/storage/src/user.rs
Normal file
414
crates/storage/src/user.rs
Normal file
@@ -0,0 +1,414 @@
|
||||
// Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::borrow::BorrowMut;
|
||||
|
||||
use anyhow::Context;
|
||||
use argon2::Argon2;
|
||||
use chrono::{DateTime, Utc};
|
||||
use mas_data_model::{errors::HtmlError, Authentication, BrowserSession, User};
|
||||
use password_hash::{PasswordHash, PasswordHasher, SaltString};
|
||||
use rand::rngs::OsRng;
|
||||
use sqlx::{Acquire, PgExecutor, Postgres, Transaction};
|
||||
use thiserror::Error;
|
||||
use tokio::task;
|
||||
use tracing::{info_span, Instrument};
|
||||
use warp::reject::Reject;
|
||||
|
||||
use super::{DatabaseInconsistencyError, PostgresqlBackend};
|
||||
use crate::IdAndCreationTime;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct UserLookup {
|
||||
pub id: i64,
|
||||
pub username: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum LoginError {
|
||||
#[error("could not find user {username:?}")]
|
||||
NotFound {
|
||||
username: String,
|
||||
#[source]
|
||||
source: sqlx::Error,
|
||||
},
|
||||
|
||||
#[error("authentication failed for {username:?}")]
|
||||
Authentication {
|
||||
username: String,
|
||||
#[source]
|
||||
source: AuthenticationError,
|
||||
},
|
||||
|
||||
#[error("failed to login")]
|
||||
Other(#[from] anyhow::Error),
|
||||
}
|
||||
|
||||
impl HtmlError for LoginError {
|
||||
fn html_display(&self) -> String {
|
||||
match self {
|
||||
LoginError::NotFound { .. } => "Could not find user".to_string(),
|
||||
LoginError::Authentication { .. } => "Failed to authenticate user".to_string(),
|
||||
LoginError::Other(e) => format!("Internal error: <pre>{}</pre>", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(conn, password))]
|
||||
pub async fn login(
|
||||
conn: impl Acquire<'_, Database = Postgres>,
|
||||
username: &str,
|
||||
password: String,
|
||||
) -> Result<BrowserSession<PostgresqlBackend>, LoginError> {
|
||||
let mut txn = conn.begin().await.context("could not start transaction")?;
|
||||
let user = lookup_user_by_username(&mut txn, username)
|
||||
.await
|
||||
.map_err(|source| {
|
||||
if matches!(source, sqlx::Error::RowNotFound) {
|
||||
LoginError::NotFound {
|
||||
username: username.to_string(),
|
||||
source,
|
||||
}
|
||||
} else {
|
||||
LoginError::Other(source.into())
|
||||
}
|
||||
})?;
|
||||
|
||||
let mut session = start_session(&mut txn, user).await?;
|
||||
authenticate_session(&mut txn, &mut session, password)
|
||||
.await
|
||||
.map_err(|source| {
|
||||
if matches!(source, AuthenticationError::Password { .. }) {
|
||||
LoginError::Authentication {
|
||||
username: username.to_string(),
|
||||
source,
|
||||
}
|
||||
} else {
|
||||
LoginError::Other(source.into())
|
||||
}
|
||||
})?;
|
||||
|
||||
txn.commit().await.context("could not commit transaction")?;
|
||||
Ok(session)
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
#[error("could not fetch session")]
|
||||
pub enum ActiveSessionLookupError {
|
||||
Fetch(#[from] sqlx::Error),
|
||||
Conversion(#[from] DatabaseInconsistencyError),
|
||||
}
|
||||
|
||||
impl Reject for ActiveSessionLookupError {}
|
||||
|
||||
impl ActiveSessionLookupError {
|
||||
#[must_use]
|
||||
pub fn not_found(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
ActiveSessionLookupError::Fetch(sqlx::Error::RowNotFound)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
struct SessionLookup {
|
||||
id: i64,
|
||||
user_id: i64,
|
||||
username: String,
|
||||
created_at: DateTime<Utc>,
|
||||
last_authentication_id: Option<i64>,
|
||||
last_authd_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
impl TryInto<BrowserSession<PostgresqlBackend>> for SessionLookup {
|
||||
type Error = DatabaseInconsistencyError;
|
||||
|
||||
fn try_into(self) -> Result<BrowserSession<PostgresqlBackend>, Self::Error> {
|
||||
let user = User {
|
||||
data: self.user_id,
|
||||
username: self.username,
|
||||
sub: format!("fake-sub-{}", self.user_id),
|
||||
};
|
||||
|
||||
let last_authentication = match (self.last_authentication_id, self.last_authd_at) {
|
||||
(Some(id), Some(created_at)) => Some(Authentication {
|
||||
data: id,
|
||||
created_at,
|
||||
}),
|
||||
(None, None) => None,
|
||||
_ => return Err(DatabaseInconsistencyError),
|
||||
};
|
||||
|
||||
Ok(BrowserSession {
|
||||
data: self.id,
|
||||
user,
|
||||
created_at: self.created_at,
|
||||
last_authentication,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn lookup_active_session(
|
||||
executor: impl PgExecutor<'_>,
|
||||
id: i64,
|
||||
) -> Result<BrowserSession<PostgresqlBackend>, ActiveSessionLookupError> {
|
||||
let res = sqlx::query_as!(
|
||||
SessionLookup,
|
||||
r#"
|
||||
SELECT
|
||||
s.id,
|
||||
u.id as user_id,
|
||||
u.username,
|
||||
s.created_at,
|
||||
a.id as "last_authentication_id?",
|
||||
a.created_at as "last_authd_at?"
|
||||
FROM user_sessions s
|
||||
INNER JOIN users u
|
||||
ON s.user_id = u.id
|
||||
LEFT JOIN user_session_authentications a
|
||||
ON a.session_id = s.id
|
||||
WHERE s.id = $1 AND s.active
|
||||
ORDER BY a.created_at DESC
|
||||
LIMIT 1
|
||||
"#,
|
||||
id,
|
||||
)
|
||||
.fetch_one(executor)
|
||||
.await?
|
||||
.try_into()?;
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
pub async fn start_session(
|
||||
executor: impl PgExecutor<'_>,
|
||||
user: User<PostgresqlBackend>,
|
||||
) -> anyhow::Result<BrowserSession<PostgresqlBackend>> {
|
||||
let res = sqlx::query_as!(
|
||||
IdAndCreationTime,
|
||||
r#"
|
||||
INSERT INTO user_sessions (user_id)
|
||||
VALUES ($1)
|
||||
RETURNING id, created_at
|
||||
"#,
|
||||
user.data,
|
||||
)
|
||||
.fetch_one(executor)
|
||||
.await
|
||||
.context("could not create session")?;
|
||||
|
||||
let session = BrowserSession {
|
||||
data: res.id,
|
||||
user,
|
||||
created_at: res.created_at,
|
||||
last_authentication: None,
|
||||
};
|
||||
|
||||
Ok(session)
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all, fields(user.id = user.data))]
|
||||
pub async fn count_active_sessions(
|
||||
executor: impl PgExecutor<'_>,
|
||||
user: &User<PostgresqlBackend>,
|
||||
) -> Result<usize, anyhow::Error> {
|
||||
let res = sqlx::query_scalar!(
|
||||
r#"
|
||||
SELECT COUNT(*) as "count!"
|
||||
FROM user_sessions s
|
||||
WHERE s.user_id = $1 AND s.active
|
||||
"#,
|
||||
user.data,
|
||||
)
|
||||
.fetch_one(executor)
|
||||
.await?
|
||||
.try_into()?;
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum AuthenticationError {
|
||||
#[error("could not verify password")]
|
||||
Password(#[from] password_hash::Error),
|
||||
|
||||
#[error("could not fetch user password hash")]
|
||||
Fetch(sqlx::Error),
|
||||
|
||||
#[error("could not save session auth")]
|
||||
Save(sqlx::Error),
|
||||
|
||||
#[error("runtime error")]
|
||||
Internal(#[from] tokio::task::JoinError),
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all, fields(session.id = session.data, user.id = session.user.data))]
|
||||
pub async fn authenticate_session(
|
||||
txn: &mut Transaction<'_, Postgres>,
|
||||
session: &mut BrowserSession<PostgresqlBackend>,
|
||||
password: String,
|
||||
) -> Result<(), AuthenticationError> {
|
||||
// First, fetch the hashed password from the user associated with that session
|
||||
let hashed_password: String = sqlx::query_scalar!(
|
||||
r#"
|
||||
SELECT up.hashed_password
|
||||
FROM user_passwords up
|
||||
WHERE up.user_id = $1
|
||||
ORDER BY up.created_at DESC
|
||||
LIMIT 1
|
||||
"#,
|
||||
session.user.data,
|
||||
)
|
||||
.fetch_one(txn.borrow_mut())
|
||||
.instrument(tracing::info_span!("Lookup hashed password"))
|
||||
.await
|
||||
.map_err(AuthenticationError::Fetch)?;
|
||||
|
||||
// TODO: pass verifiers list as parameter
|
||||
// Verify the password in a blocking thread to avoid blocking the async executor
|
||||
task::spawn_blocking(move || {
|
||||
let context = Argon2::default();
|
||||
let hasher = PasswordHash::new(&hashed_password).map_err(AuthenticationError::Password)?;
|
||||
hasher
|
||||
.verify_password(&[&context], &password)
|
||||
.map_err(AuthenticationError::Password)
|
||||
})
|
||||
.instrument(tracing::info_span!("Verify hashed password"))
|
||||
.await??;
|
||||
|
||||
// That went well, let's insert the auth info
|
||||
let res = sqlx::query_as!(
|
||||
IdAndCreationTime,
|
||||
r#"
|
||||
INSERT INTO user_session_authentications (session_id)
|
||||
VALUES ($1)
|
||||
RETURNING id, created_at
|
||||
"#,
|
||||
session.data,
|
||||
)
|
||||
.fetch_one(txn.borrow_mut())
|
||||
.instrument(tracing::info_span!("Save authentication"))
|
||||
.await
|
||||
.map_err(AuthenticationError::Save)?;
|
||||
|
||||
session.last_authentication = Some(Authentication {
|
||||
data: res.id,
|
||||
created_at: res.created_at,
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(txn, phf, password))]
|
||||
pub async fn register_user(
|
||||
txn: &mut Transaction<'_, Postgres>,
|
||||
phf: impl PasswordHasher,
|
||||
username: &str,
|
||||
password: &str,
|
||||
) -> anyhow::Result<User<PostgresqlBackend>> {
|
||||
let id: i64 = sqlx::query_scalar!(
|
||||
r#"
|
||||
INSERT INTO users (username)
|
||||
VALUES ($1)
|
||||
RETURNING id
|
||||
"#,
|
||||
username,
|
||||
)
|
||||
.fetch_one(txn.borrow_mut())
|
||||
.instrument(info_span!("Register user"))
|
||||
.await
|
||||
.context("could not insert user")?;
|
||||
|
||||
let user = User {
|
||||
data: id,
|
||||
username: username.to_string(),
|
||||
sub: format!("fake-sub-{}", id),
|
||||
};
|
||||
|
||||
set_password(txn.borrow_mut(), phf, &user, password).await?;
|
||||
|
||||
Ok(user)
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all, fields(user.id = user.data))]
|
||||
pub async fn set_password(
|
||||
executor: impl PgExecutor<'_>,
|
||||
phf: impl PasswordHasher,
|
||||
user: &User<PostgresqlBackend>,
|
||||
password: &str,
|
||||
) -> anyhow::Result<()> {
|
||||
let salt = SaltString::generate(&mut OsRng);
|
||||
let hashed_password = PasswordHash::generate(phf, password, salt.as_str())?;
|
||||
|
||||
sqlx::query_scalar!(
|
||||
r#"
|
||||
INSERT INTO user_passwords (user_id, hashed_password)
|
||||
VALUES ($1, $2)
|
||||
"#,
|
||||
user.data,
|
||||
hashed_password.to_string(),
|
||||
)
|
||||
.execute(executor)
|
||||
.instrument(info_span!("Save user credentials"))
|
||||
.await
|
||||
.context("could not insert user password")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all, fields(session.id = session.data))]
|
||||
pub async fn end_session(
|
||||
executor: impl PgExecutor<'_>,
|
||||
session: &BrowserSession<PostgresqlBackend>,
|
||||
) -> anyhow::Result<()> {
|
||||
let res = sqlx::query!(
|
||||
"UPDATE user_sessions SET active = FALSE WHERE id = $1",
|
||||
session.data,
|
||||
)
|
||||
.execute(executor)
|
||||
.instrument(info_span!("End session"))
|
||||
.await
|
||||
.context("could not end session")?;
|
||||
|
||||
match res.rows_affected() {
|
||||
1 => Ok(()),
|
||||
0 => Err(anyhow::anyhow!("no row affected")),
|
||||
_ => Err(anyhow::anyhow!("too many row affected")),
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(executor))]
|
||||
pub async fn lookup_user_by_username(
|
||||
executor: impl PgExecutor<'_>,
|
||||
username: &str,
|
||||
) -> Result<User<PostgresqlBackend>, sqlx::Error> {
|
||||
let res = sqlx::query_as!(
|
||||
UserLookup,
|
||||
r#"
|
||||
SELECT id, username
|
||||
FROM users
|
||||
WHERE username = $1
|
||||
"#,
|
||||
username,
|
||||
)
|
||||
.fetch_one(executor)
|
||||
.instrument(info_span!("Fetch user"))
|
||||
.await?;
|
||||
|
||||
Ok(User {
|
||||
data: res.id,
|
||||
username: res.username,
|
||||
sub: format!("fake-sub-{}", res.id),
|
||||
})
|
||||
}
|
Reference in New Issue
Block a user