1
0
mirror of https://github.com/matrix-org/matrix-authentication-service.git synced 2026-01-12 22:51:25 +03:00

Implement proper access token generation

This commit is contained in:
Quentin Gliech
2021-08-13 14:15:20 +02:00
parent da13e24789
commit 0596b65f12
11 changed files with 370 additions and 55 deletions

105
Cargo.lock generated
View File

@@ -40,9 +40,9 @@ dependencies = [
[[package]]
name = "alloc-no-stdlib"
version = "2.0.1"
version = "2.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5192ec435945d87bc2f70992b4d818154b5feede43c09fb7592146374eac90a6"
checksum = "35ef4730490ad1c4eae5c4325b2a95f521d023e5c885853ff7aca0a6a1631db3"
[[package]]
name = "alloc-stdlib"
@@ -182,9 +182,9 @@ dependencies = [
[[package]]
name = "bitflags"
version = "1.2.1"
version = "1.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693"
checksum = "2da1976d75adbe5fbc88130ecd119529cf1cc6a93ae1546d8696ee66f0d21af1"
[[package]]
name = "bitvec"
@@ -241,9 +241,9 @@ dependencies = [
[[package]]
name = "brotli"
version = "3.3.0"
version = "3.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f29919120f08613aadcd4383764e00526fc9f18b6c0895814faeed0dd78613e"
checksum = "71cb90ade945043d3d53597b2fc359bb063db8ade2bcffe7997351d0756e9d50"
dependencies = [
"alloc-no-stdlib",
"alloc-stdlib",
@@ -252,9 +252,9 @@ dependencies = [
[[package]]
name = "brotli-decompressor"
version = "2.3.1"
version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1052e1c3b8d4d80eb84a8b94f0a1498797b5fb96314c001156a1c761940ef4ec"
checksum = "59ad2d4653bf5ca36ae797b1f4bb4dbddb60ce49ca4aed8a2ce4829f60425b80"
dependencies = [
"alloc-no-stdlib",
"alloc-stdlib",
@@ -451,6 +451,21 @@ dependencies = [
"build_const",
]
[[package]]
name = "crc"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10c2722795460108a7872e1cd933a85d6ec38abc4baecad51028f702da28889f"
dependencies = [
"crc-catalog",
]
[[package]]
name = "crc-catalog"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccaeedb56da03b09f598226e25e80088cb4cd25f316e6e4df7d695f0feeb1403"
[[package]]
name = "crc32fast"
version = "1.2.1"
@@ -985,9 +1000,9 @@ dependencies = [
[[package]]
name = "http-body"
version = "0.4.2"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60daa14be0e0786db0f03a9e57cb404c9d756eed2b6c62b9ea98ec5743ec75a9"
checksum = "399c583b2979440c60be0821a6199eca73bc3c8dcd9d070d75ac726e2c6186e5"
dependencies = [
"bytes",
"http",
@@ -1140,9 +1155,9 @@ checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736"
[[package]]
name = "js-sys"
version = "0.3.51"
version = "0.3.52"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83bdfbace3a0e81a4253f73b49e960b053e396a11012cbd49b9b74d6a2b67062"
checksum = "ce791b7ca6638aae45be056e068fc756d871eb3b3b10b8efa62d1c9cec616752"
dependencies = [
"wasm-bindgen",
]
@@ -1177,9 +1192,9 @@ dependencies = [
[[package]]
name = "libc"
version = "0.2.98"
version = "0.2.99"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "320cfe77175da3a483efed4bc0adc1968ca050b098ce4f2f1c13a56626128790"
checksum = "a7f823d141fe0a24df1e23b4af4e3c7ba9e5966ec514ea068c93024aa7deb765"
[[package]]
name = "linked-hash-map"
@@ -1222,9 +1237,9 @@ dependencies = [
[[package]]
name = "matches"
version = "0.1.8"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08"
checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f"
[[package]]
name = "matrix-authentication-service"
@@ -1238,6 +1253,7 @@ dependencies = [
"chrono",
"clap",
"cookie",
"crc 2.0.0",
"data-encoding",
"figment",
"headers",
@@ -1803,9 +1819,9 @@ dependencies = [
[[package]]
name = "redox_syscall"
version = "0.2.9"
version = "0.2.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ab49abadf3f9e1c4bc499e8845e152ad87d2ad2d30371841171169e9d75feee"
checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff"
dependencies = [
"bitflags",
]
@@ -1984,18 +2000,18 @@ checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3"
[[package]]
name = "serde"
version = "1.0.126"
version = "1.0.127"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec7505abeacaec74ae4778d9d9328fe5a5d04253220a85c4ee022239fc996d03"
checksum = "f03b9878abf6d14e6779d3f24f07b2cfa90352cfec4acc5aab8f1ac7f146fae8"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.126"
version = "1.0.127"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "963a7dbc9895aeac7ac90e74f34a5d5261828f79df35cbed41e10189d3804d43"
checksum = "a024926d3432516606328597e0f224a51355a493b49fdd67e9209187cbe55ecc"
dependencies = [
"proc-macro2",
"quote",
@@ -2120,9 +2136,9 @@ dependencies = [
[[package]]
name = "sharded-slab"
version = "0.1.1"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79c719719ee05df97490f80a45acfc99e5a30ce98a1e4fb67aee422745ae14e3"
checksum = "740223c51853f3145fe7c90360d2d4232f2b62e3449489c207eccde818979982"
dependencies = [
"lazy_static",
]
@@ -2138,9 +2154,9 @@ dependencies = [
[[package]]
name = "slab"
version = "0.4.3"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f173ac3d1a7e3b28003f40de0b5ce7fe2710f9b9dc3fc38664cebee46b3b6527"
checksum = "c307a32c1c5c437f38c7fd45d753050587732ba8628319fbdf12a7e289ccc590"
[[package]]
name = "slug"
@@ -2209,7 +2225,7 @@ dependencies = [
"byteorder",
"bytes",
"chrono",
"crc",
"crc 1.8.1",
"crossbeam-channel 0.5.1",
"crossbeam-queue",
"crossbeam-utils 0.8.5",
@@ -2556,9 +2572,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
[[package]]
name = "tokio"
version = "1.9.0"
version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b7b349f11a7047e6d1276853e612d152f5e8a352c61917887cc2169e2366b4c"
checksum = "01cf844b23c6131f624accf65ce0e4e9956a8bb329400ea5bcc26ae3a5c20b0b"
dependencies = [
"autocfg",
"bytes",
@@ -2884,12 +2900,9 @@ dependencies = [
[[package]]
name = "unicode-bidi"
version = "0.3.5"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eeb8be209bb1c96b7c177c7420d26e04eccacb0eeae6b980e35fcb74678107e0"
dependencies = [
"matches",
]
checksum = "246f4c42e67e7a4e3c6106ff716a5d067d4132a642840b242e357e468a2a0085"
[[package]]
name = "unicode-normalization"
@@ -3047,9 +3060,9 @@ checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f"
[[package]]
name = "wasm-bindgen"
version = "0.2.74"
version = "0.2.75"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d54ee1d4ed486f78874278e63e4069fc1ab9f6a18ca492076ffb90c5eb2997fd"
checksum = "b608ecc8f4198fe8680e2ed18eccab5f0cd4caaf3d83516fa5fb2e927fda2586"
dependencies = [
"cfg-if 1.0.0",
"wasm-bindgen-macro",
@@ -3057,9 +3070,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.74"
version = "0.2.75"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b33f6a0694ccfea53d94db8b2ed1c3a8a4c86dd936b13b9f0a15ec4a451b900"
checksum = "580aa3a91a63d23aac5b6b267e2d13cb4f363e31dce6c352fca4752ae12e479f"
dependencies = [
"bumpalo",
"lazy_static",
@@ -3072,9 +3085,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.74"
version = "0.2.75"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "088169ca61430fe1e58b8096c24975251700e7b1f6fd91cc9d59b04fb9b18bd4"
checksum = "171ebf0ed9e1458810dfcb31f2e766ad6b3a89dbda42d8901f2b268277e5f09c"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
@@ -3082,9 +3095,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.74"
version = "0.2.75"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be2241542ff3d9f241f5e2cb6dd09b37efe786df8851c54957683a49f0987a97"
checksum = "6c2657dd393f03aa2a659c25c6ae18a13a4048cebd220e147933ea837efc589f"
dependencies = [
"proc-macro2",
"quote",
@@ -3095,15 +3108,15 @@ dependencies = [
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.74"
version = "0.2.75"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7cff876b8f18eed75a66cf49b65e7f967cb354a7aa16003fb55dbfd25b44b4f"
checksum = "2e0c4a743a309662d45f4ede961d7afa4ba4131a59a639f29b0069c3798bbcc2"
[[package]]
name = "web-sys"
version = "0.3.51"
version = "0.3.52"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e828417b379f3df7111d3a2a9e5753706cae29c41f7c4029ee9fd77f3e09e582"
checksum = "01c70a82d842c9979078c772d4a1344685045f1a5628f677c2b2eab4dd7d2696"
dependencies = [
"js-sys",
"wasm-bindgen",

View File

@@ -7,7 +7,7 @@ license = "Apache-2.0"
[dependencies]
# Async runtime
tokio = { version = "1.9.0", features = ["full"] }
tokio = { version = "1.10.0", features = ["full"] }
async-trait = "0.1.51"
# Logging and tracing
@@ -31,7 +31,7 @@ tera = "1.12.1"
sqlx = { version = "0.5.5", features = ["runtime-tokio-rustls", "postgres", "migrate", "chrono", "offline"] }
# Various structure (de)serialization
serde = { version = "1.0.126", features = ["derive"] }
serde = { version = "1.0.127", features = ["derive"] }
serde_yaml = "0.8.17"
serde_with = { version = "1.9.4", features = ["hex", "chrono"] }
@@ -59,3 +59,4 @@ chacha20poly1305 = { version = "0.8.1", features = ["std"] }
oauth2-types = { path = "../oauth2-types", features = ["sqlx_type"] }
serde_json = "1.0.66"
serde_urlencoded = "0.7.0"
crc = "2.0.0"

View File

@@ -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;

View File

@@ -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()
);

View File

@@ -378,6 +378,52 @@
"nullable": []
}
},
"b766b2b41d8770b5bef9928bb3b96abbaf8466b473e12b21f145c015b7cf2f05": {
"query": "\n INSERT INTO oauth2_access_tokens\n (oauth2_session_id, token, expires_after)\n VALUES\n ($1, $2, $3)\n RETURNING\n id, oauth2_session_id, token, expires_after, created_at\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Int8"
},
{
"ordinal": 1,
"name": "oauth2_session_id",
"type_info": "Int8"
},
{
"ordinal": 2,
"name": "token",
"type_info": "Text"
},
{
"ordinal": 3,
"name": "expires_after",
"type_info": "Int4"
},
{
"ordinal": 4,
"name": "created_at",
"type_info": "Timestamptz"
}
],
"parameters": {
"Left": [
"Int8",
"Text",
"Int4"
]
},
"nullable": [
false,
false,
false,
false,
false
]
}
},
"f9a09ff53b6f221649f4f050e3d5ade114f852ddf50a78610a6c0ef0689af681": {
"query": "\n INSERT INTO users (username, hashed_password)\n VALUES ($1, $2)\n RETURNING id\n ",
"describe": {

View File

@@ -17,6 +17,7 @@ use std::{
convert::TryFrom,
};
use chrono::Duration;
use data_encoding::BASE64URL_NOPAD;
use hyper::{
header::LOCATION,
@@ -31,6 +32,7 @@ use oauth2_types::{
ResponseType,
},
};
use rand::thread_rng;
use serde::{Deserialize, Serialize};
use sqlx::{PgPool, Postgres, Transaction};
use url::Url;
@@ -50,10 +52,11 @@ use crate::{
},
handlers::views::LoginRequest,
storage::{
oauth2::{get_session_by_id, start_session},
oauth2::{add_access_token, get_session_by_id, start_session},
SessionInfo,
},
templates::{FormPostContext, Templates},
tokens,
};
fn back_to_client<T>(
@@ -312,10 +315,19 @@ async fn step(
// Did they request an access token?
if response_type.contains(&ResponseType::Token) {
// TODO: generate and store an access token
params.access_token = Some(AccessTokenResponse::new(
"some_static_token_that_should_be_generated".into(),
));
let ttl = Duration::minutes(5);
let (access_token, refresh_token) = {
let mut rng = thread_rng();
(
tokens::generate(&mut rng, tokens::TokenType::AccessToken),
tokens::generate(&mut rng, tokens::TokenType::RefreshToken)
)
};
add_access_token(&mut txn, oauth2_session_id, &access_token, ttl).await.wrap_error()?;
params.access_token = Some(AccessTokenResponse::new(access_token).with_expires_in(ttl));
// TODO: save the refresh token
params.refresh_token = Some(refresh_token);
}
// Did they request an ID token?

View File

@@ -30,6 +30,7 @@ mod filters;
mod handlers;
mod storage;
mod templates;
mod tokens;
use self::cli::RootCommand;

View File

@@ -247,3 +247,40 @@ pub async fn add_code(
.await
.context("could not insert oauth2 authorization code")
}
#[derive(FromRow, Serialize)]
pub struct OAuth2AccessToken {
id: i64,
oauth2_session_id: i64,
token: String,
expires_after: i32,
created_at: DateTime<Utc>,
}
pub async fn add_access_token(
executor: impl Executor<'_, Database = Postgres>,
oauth2_session_id: i64,
token: &str,
expires_after: Duration,
) -> anyhow::Result<OAuth2AccessToken> {
// Checked convertion of duration to i32, maxing at i32::MAX
let expires_after = i32::try_from(expires_after.num_seconds()).unwrap_or(i32::MAX);
sqlx::query_as!(
OAuth2AccessToken,
r#"
INSERT INTO oauth2_access_tokens
(oauth2_session_id, token, expires_after)
VALUES
($1, $2, $3)
RETURNING
id, oauth2_session_id, token, expires_after, created_at
"#,
oauth2_session_id,
token,
expires_after,
)
.fetch_one(executor)
.await
.context("could not insert oauth2 access token")
}

View File

@@ -0,0 +1,166 @@
// 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::convert::TryInto;
use crc::{Crc, CRC_32_ISO_HDLC};
use rand::{distributions::Alphanumeric, Rng};
use thiserror::Error;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TokenType {
AccessToken,
RefreshToken,
}
impl TokenType {
fn prefix(self) -> &'static str {
match self {
TokenType::AccessToken => "mat",
TokenType::RefreshToken => "mar",
}
}
fn match_prefix(prefix: &str) -> Option<Self> {
match prefix {
"mat" => Some(TokenType::AccessToken),
"mar" => Some(TokenType::RefreshToken),
_ => None,
}
}
}
const NUM: [u8; 62] = *b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
fn base62_encode(mut num: u32) -> String {
let mut res = String::with_capacity(6);
while num > 0 {
res.push(NUM[(num % 62) as usize] as char);
num /= 62;
}
format!("{:0>6}", res)
}
const CRC: Crc<u32> = Crc::<u32>::new(&CRC_32_ISO_HDLC);
pub fn generate(rng: impl Rng, token_type: TokenType) -> String {
let random_part: String = rng
.sample_iter(&Alphanumeric)
.take(30)
.map(char::from)
.collect();
let base = format!("{}_{}", token_type.prefix(), random_part);
let crc = CRC.checksum(base.as_bytes());
let crc = base62_encode(crc);
format!("{}_{}", base, crc)
}
#[derive(Debug, Error)]
pub enum TokenFormatError {
#[error("invalid token format")]
InvalidFormat,
#[error("unknown token prefix {prefix:?}")]
UnknownPrefix { prefix: String },
#[error("invalid crc {got:?}, expected {expected:?}")]
InvalidCrc { expected: String, got: String },
}
#[allow(dead_code)]
pub fn check(token: &str) -> Result<TokenType, TokenFormatError> {
let split: Vec<&str> = token.split('_').collect();
let [prefix, random_part, crc]: [&str; 3] = split
.try_into()
.map_err(|_| TokenFormatError::InvalidFormat)?;
if prefix.len() != 3 || random_part.len() != 30 || crc.len() != 6 {
return Err(TokenFormatError::InvalidFormat);
}
let token_type =
TokenType::match_prefix(prefix).ok_or_else(|| TokenFormatError::UnknownPrefix {
prefix: prefix.to_string(),
})?;
let base = format!("{}_{}", token_type.prefix(), random_part);
let expected_crc = CRC.checksum(base.as_bytes());
let expected_crc = base62_encode(expected_crc);
if crc != expected_crc {
return Err(TokenFormatError::InvalidCrc {
expected: expected_crc,
got: crc.to_string(),
});
}
Ok(token_type)
}
#[cfg(test)]
mod tests {
use std::collections::HashSet;
use rand::thread_rng;
use super::*;
#[test]
fn test_prefix_match() {
use TokenType::{AccessToken, RefreshToken};
assert_eq!(TokenType::match_prefix("mat"), Some(AccessToken));
assert_eq!(TokenType::match_prefix("mar"), Some(RefreshToken));
assert_eq!(TokenType::match_prefix("matt"), None);
assert_eq!(TokenType::match_prefix("marr"), None);
assert_eq!(TokenType::match_prefix("ma"), None);
assert_eq!(
TokenType::match_prefix(TokenType::AccessToken.prefix()),
Some(TokenType::AccessToken)
);
assert_eq!(
TokenType::match_prefix(TokenType::RefreshToken.prefix()),
Some(TokenType::RefreshToken)
);
}
#[test]
fn test_generate_and_check() {
const COUNT: usize = 500; // Generate 500 of each token type
let mut rng = thread_rng();
// Generate many access tokens
let tokens: HashSet<String> = (0..COUNT)
.map(|_| generate(&mut rng, TokenType::AccessToken))
.collect();
// Check that they are all different
assert_eq!(tokens.len(), COUNT, "All tokens are unique");
// Check that they are all valid and detected as access tokens
for token in tokens {
assert_eq!(check(&token).unwrap(), TokenType::AccessToken);
}
// Same, but for refresh tokens
let tokens: HashSet<String> = (0..COUNT)
.map(|_| generate(&mut rng, TokenType::RefreshToken))
.collect();
assert_eq!(tokens.len(), COUNT, "All tokens are unique");
for token in tokens {
assert_eq!(check(&token).unwrap(), TokenType::RefreshToken);
}
}
}

View File

@@ -7,8 +7,8 @@ license = "Apache-2.0"
[dependencies]
http = "0.2.4"
serde = "1.0.126"
serde_json = "1.0.64"
serde = "1.0.127"
serde_json = "1.0.66"
language-tags = { version = "0.3.2", features = ["serde"] }
url = { version = "2.2.2", features = ["serde"] }
parse-display = "0.5.1"

View File

@@ -156,6 +156,7 @@ pub struct AuthorizationResponse {
pub state: Option<String>,
#[serde(flatten)]
pub access_token: Option<AccessTokenResponse>,
pub refresh_token: Option<String>,
}
#[derive(