mirror of
https://tangled.org/tranquil.farm/tranquil-pds
synced 2026-08-16 14:26:06 +00:00
Initial oauth impl
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
[store]
|
||||
dir = "target/nextest"
|
||||
|
||||
[profile.default]
|
||||
retries = 0
|
||||
fail-fast = true
|
||||
test-threads = "num-cpus"
|
||||
|
||||
[profile.ci]
|
||||
retries = 2
|
||||
fail-fast = false
|
||||
test-threads = "num-cpus"
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO session_tokens (did, access_jti, refresh_jti, access_expires_at, refresh_expires_at) VALUES ($1, $2, $3, $4, $5)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text",
|
||||
"Text",
|
||||
"Timestamptz",
|
||||
"Timestamptz"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "0198d73145b29c2b66c2bc437ff6578faa08d56a26b9aa98a311bd39547146b3"
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO oauth_account_device (did, device_id, created_at, updated_at)\n VALUES ($1, $2, NOW(), NOW())\n ON CONFLICT (did, device_id) DO UPDATE SET updated_at = NOW()\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "03d4d87f64aa35c3e5d02ef6222dd35b56cb4e20ba631a66774968ed59418262"
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n DELETE FROM oauth_token\n WHERE id IN (\n SELECT id FROM oauth_token\n WHERE did = $1\n ORDER BY updated_at ASC\n OFFSET $2\n )\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "0afc7d45fdda0cb437988727a44c15d961ad6154cfb58a02ca05784a6c5b3e52"
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n UPDATE oauth_device\n SET last_seen_at = NOW()\n WHERE id = $1\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "0b413e61a11231b3d5ccb2ab4f0aa95a6701204873bc835f87d00f7cb5b87c78"
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT COUNT(*) as \"count!\" FROM oauth_token WHERE did = $1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "count!",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "0d4087a12feff131ddddb34eb3c702370555d99806455219e1d2ee59ced221eb"
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT handle, email FROM users WHERE did = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "handle",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "email",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "1658a90aede20695b0e6e87d2536fad5a538dbfc442625ef306272d2530ddc3a"
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT data FROM oauth_authorized_client\n WHERE did = $1 AND client_id = $2\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "data",
|
||||
"type_info": "Jsonb"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "1fca3948872f8abc5050865c18ab7b56d4ab98f0f1253afb57e2e4a9f5c04587"
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n DELETE FROM oauth_device WHERE id = $1\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "235620af9a007538bdbd6b7751a9ee287f06b7cd39b8e66f79bb4afe52bd0766"
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n UPDATE oauth_authorization_request\n SET did = $2, device_id = $3, code = $4\n WHERE id = $1\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text",
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "26039af44364b143af3a9f09b50ab05fe4352811f9d74bb7dae72cc920162533"
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO oauth_used_refresh_token (refresh_token, token_id)\n VALUES ($1, $2)\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Int4"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "2ea85a7507f974267cd300075ce6e60b3cfa5f705aed80879b30b5f3f120a8cc"
|
||||
}
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM sessions WHERE did = $1",
|
||||
"query": "DELETE FROM session_tokens WHERE did = $1",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
@@ -10,5 +10,5 @@
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "9c42b607a971b3a102d247def6c6fd322013f3885e9d0232d6e846220f893c49"
|
||||
"hash": "31fef6c193390b791edd988e40963706d4cc731cea6e19538794eb1588aa8b09"
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT st.id, st.did, k.key_bytes, k.encryption_version\n FROM session_tokens st\n JOIN users u ON st.did = u.did\n JOIN user_keys k ON u.id = k.user_id\n WHERE st.refresh_jti = $1 AND st.refresh_expires_at > NOW()",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "did",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "key_bytes",
|
||||
"type_info": "Bytea"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "encryption_version",
|
||||
"type_info": "Int4"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "3889903e58405370152b9ded229d843c0114e71454ea7da2b212519e98d09817"
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM session_tokens WHERE did = (SELECT did FROM users WHERE id = $1)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "3b1176253dc7b94d3fc58c077310d8058f90edf1fa27200b52b464b9c37335dd"
|
||||
}
|
||||
-34
@@ -1,34 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT s.did, k.key_bytes, u.handle\n FROM sessions s\n JOIN users u ON s.did = u.did\n JOIN user_keys k ON u.id = k.user_id\n WHERE s.access_jwt = $1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "did",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "key_bytes",
|
||||
"type_info": "Bytea"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "handle",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "47c914ca6080c5cedf0c3f6ca7cd4cd49e8fb691b34d19511b7a1ab8b3606cdf"
|
||||
}
|
||||
-28
@@ -1,28 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT s.did, k.key_bytes\n FROM sessions s\n JOIN users u ON s.did = u.did\n JOIN user_keys k ON u.id = k.user_id\n WHERE s.access_jwt = $1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "did",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "key_bytes",
|
||||
"type_info": "Bytea"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "48ae289ec37b367a6ec3d74895acaf8c3dc93e65d243434b6947ead95ca8c416"
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE session_tokens SET access_jti = $1, refresh_jti = $2, access_expires_at = $3, refresh_expires_at = $4, updated_at = NOW() WHERE id = $5",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text",
|
||||
"Timestamptz",
|
||||
"Timestamptz",
|
||||
"Int4"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "4dcee809896ead3de8ca0433856ed424211d79df201d08bbea0e4c576931a234"
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO oauth_authorization_request\n (id, did, device_id, client_id, client_auth, parameters, expires_at, code)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8)\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text",
|
||||
"Text",
|
||||
"Text",
|
||||
"Jsonb",
|
||||
"Jsonb",
|
||||
"Timestamptz",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "52b59474e567add52f112ccfaeb300ebf790cf4ecc1c243ad9563fa136c33550"
|
||||
}
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT did, token_id, created_at, updated_at, expires_at, client_id, client_auth,\n device_id, parameters, details, code, current_refresh_token, scope\n FROM oauth_token\n WHERE did = $1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "did",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "token_id",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "created_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "updated_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "expires_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "client_id",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "client_auth",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "device_id",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "parameters",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "details",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"name": "code",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"name": "current_refresh_token",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 12,
|
||||
"name": "scope",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "53d124a7cbdf5e121a3469f82225fa9ec69fb74c3fbf335be6ca76ecf9c16765"
|
||||
}
|
||||
-46
@@ -1,46 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT s.did, u.id as user_id, u.email, u.handle, k.key_bytes\n FROM sessions s\n JOIN users u ON s.did = u.did\n JOIN user_keys k ON u.id = k.user_id\n WHERE s.access_jwt = $1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "did",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "user_id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "email",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "handle",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "key_bytes",
|
||||
"type_info": "Bytea"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "55c4e13e5ff23aaa71c3ab417891a5f56542571ba3f15c6d9dae153405bc4275"
|
||||
}
|
||||
+9
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT u.id, u.did, u.handle, u.password_hash, k.key_bytes FROM users u JOIN user_keys k ON u.id = k.user_id WHERE u.handle = $1 OR u.email = $1",
|
||||
"query": "SELECT u.id, u.did, u.handle, u.password_hash, k.key_bytes, k.encryption_version FROM users u JOIN user_keys k ON u.id = k.user_id WHERE u.handle = $1 OR u.email = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -27,6 +27,11 @@
|
||||
"ordinal": 4,
|
||||
"name": "key_bytes",
|
||||
"type_info": "Bytea"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "encryption_version",
|
||||
"type_info": "Int4"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -39,8 +44,9 @@
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
false,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "2305db96343fcb721adc4a6a608b64678f707928d3f9395070f5e21a5ca9b601"
|
||||
"hash": "583ab12e7634fa1ac888dbe319f8cd77405ae6246656c8698a7618a5a29a4ccb"
|
||||
}
|
||||
-40
@@ -1,40 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT s.did, k.key_bytes, u.id as user_id, u.handle\n FROM sessions s\n JOIN users u ON s.did = u.did\n JOIN user_keys k ON u.id = k.user_id\n WHERE s.access_jwt = $1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "did",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "key_bytes",
|
||||
"type_info": "Bytea"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "user_id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "handle",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "6a233f0ca94195935bf32ee749c8429c2292bb3907f129e06aff033a31681175"
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO oauth_token\n (did, token_id, created_at, updated_at, expires_at, client_id, client_auth,\n device_id, parameters, details, code, current_refresh_token, scope)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)\n RETURNING id\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Int4"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text",
|
||||
"Timestamptz",
|
||||
"Timestamptz",
|
||||
"Timestamptz",
|
||||
"Text",
|
||||
"Jsonb",
|
||||
"Text",
|
||||
"Jsonb",
|
||||
"Jsonb",
|
||||
"Text",
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "6b30d0a7dc0759c336334c2d34d3302b883795730c5dfa97925319dc998a43f0"
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT k.key_bytes, k.encryption_version, u.deactivated_at, u.takedown_ref\n FROM users u\n JOIN user_keys k ON u.id = k.user_id\n WHERE u.did = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "key_bytes",
|
||||
"type_info": "Bytea"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "encryption_version",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "deactivated_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "takedown_ref",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "6b67b2b6759f01be11d5997a3ad68d381f59a02235a6940877f62193af8d9761"
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO user_keys (user_id, key_bytes, encryption_version, encrypted_at) VALUES ($1, $2, $3, NOW())",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid",
|
||||
"Bytea",
|
||||
"Int4"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "73335e777fe754f55f384343f483747e84dc307b76738379ae018895b5182eb7"
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO used_refresh_tokens (refresh_jti, session_id) VALUES ($1, $2)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Int4"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "7b76e2fcd809a1536465306c79da7985354175e0f025b29c6004dffa310feebd"
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO oauth_dpop_jti (jti)\n VALUES ($1)\n ON CONFLICT (jti) DO NOTHING\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "7b9fbadc505176c4afdb8a55ffeefa9f6a38924a3577f0b3ff77f7373aba4974"
|
||||
}
|
||||
+7
-7
@@ -1,17 +1,17 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT s.did, k.key_bytes FROM sessions s JOIN users u ON s.did = u.did JOIN user_keys k ON u.id = k.user_id WHERE s.access_jwt = $1",
|
||||
"query": "SELECT k.key_bytes, k.encryption_version FROM user_keys k JOIN users u ON k.user_id = u.id WHERE u.did = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "did",
|
||||
"type_info": "Text"
|
||||
"name": "key_bytes",
|
||||
"type_info": "Bytea"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "key_bytes",
|
||||
"type_info": "Bytea"
|
||||
"name": "encryption_version",
|
||||
"type_info": "Int4"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -21,8 +21,8 @@
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "f91a07e40484ade5b4c72addf62e4ad82feab312645c0b7a4ea69c0e55e17b14"
|
||||
"hash": "7bb1388dec372fe749462cd9b604e5802b770aeb110462208988141d31c86c92"
|
||||
}
|
||||
-58
@@ -1,58 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT s.did, k.key_bytes, u.id as user_id, u.email as current_email,\n u.email_confirmation_code, u.email_confirmation_code_expires_at,\n u.email_pending_verification\n FROM sessions s\n JOIN users u ON s.did = u.did\n JOIN user_keys k ON u.id = k.user_id\n WHERE s.access_jwt = $1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "did",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "key_bytes",
|
||||
"type_info": "Bytea"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "user_id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "current_email",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "email_confirmation_code",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "email_confirmation_code_expires_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "email_pending_verification",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "7d8993cdd6f859d38d1e017bbb2bd02278d75baec57b7d2c97ba590b52f8e2d9"
|
||||
}
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM sessions WHERE access_jwt = $1",
|
||||
"query": "DELETE FROM session_tokens WHERE access_jti = $1",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
@@ -10,5 +10,5 @@
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "52437f0d7f91d29d7438263a1f658a838601038d911be8781b91ebeec8a54b89"
|
||||
"hash": "847ce3c34985d0957526c87e0a20c6b4e5daae08a338f7635def682ac0689cf6"
|
||||
}
|
||||
+13
-13
@@ -1,27 +1,27 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT u.handle, u.did, u.email, k.key_bytes\n FROM sessions s\n JOIN users u ON s.did = u.did\n JOIN user_keys k ON u.id = k.user_id\n WHERE s.access_jwt = $1\n ",
|
||||
"query": "\n SELECT did, password_hash, deactivated_at, takedown_ref\n FROM users\n WHERE handle = $1 OR email = $1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "handle",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "did",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "email",
|
||||
"ordinal": 1,
|
||||
"name": "password_hash",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "deactivated_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "key_bytes",
|
||||
"type_info": "Bytea"
|
||||
"name": "takedown_ref",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -32,9 +32,9 @@
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "09d75b756a6bd981cf2a9e922eccc38677bee474813c66465904aec3c0da1c3e"
|
||||
"hash": "91ab872f41891370baf9d405e8812b8d4cfb0b7555430eb45f16fe550fac4b43"
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT current_refresh_token FROM oauth_token WHERE id = $1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "current_refresh_token",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int4"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "93eafc96f8007ae089dfb14b14601e9edb0d7341ebff2a99ccafcb9516fd2043"
|
||||
}
|
||||
-16
@@ -1,16 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE sessions SET access_jwt = $1, refresh_jwt = $2 WHERE refresh_jwt = $3",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "a5b7ceaa177ef136a0e2421eaca3f3edf283e9305bd4675d72a1b7a02c3dfc83"
|
||||
}
|
||||
+11
-17
@@ -1,35 +1,30 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT s.did, k.key_bytes, u.id as user_id, u.email_confirmation_code, u.email_confirmation_code_expires_at, u.email_pending_verification\n FROM sessions s\n JOIN users u ON s.did = u.did\n JOIN user_keys k ON u.id = k.user_id\n WHERE s.access_jwt = $1\n ",
|
||||
"query": "SELECT id, email, email_confirmation_code, email_confirmation_code_expires_at, email_pending_verification FROM users WHERE did = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "did",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "key_bytes",
|
||||
"type_info": "Bytea"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "user_id",
|
||||
"name": "id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"ordinal": 1,
|
||||
"name": "email",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "email_confirmation_code",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"ordinal": 3,
|
||||
"name": "email_confirmation_code_expires_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"ordinal": 4,
|
||||
"name": "email_pending_verification",
|
||||
"type_info": "Text"
|
||||
}
|
||||
@@ -40,7 +35,6 @@
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
@@ -48,5 +42,5 @@
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "3377750b73c3831cbd6c96b971ea8b6d4da38f1bc740afce3136d86c27b8ce8d"
|
||||
"hash": "a7e1e6092df6481e64bf0c2237737b846628ed20ffa70b81fa2e416d5776185a"
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT key_bytes, encryption_version FROM user_keys WHERE user_id = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "key_bytes",
|
||||
"type_info": "Bytea"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "encryption_version",
|
||||
"type_info": "Int4"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "b51ed30a0421d19beba933234679b39dc7cc9b02d18bbce1958ac9b0ee6f6268"
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT id, email_confirmation_code, email_confirmation_code_expires_at, email_pending_verification FROM users WHERE did = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "email_confirmation_code",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "email_confirmation_code_expires_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "email_pending_verification",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "b551a83dbe436c1d0e4ce674f23668a9d5ef7ac5b76a332a8f8b5dc2220e9ea5"
|
||||
}
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT did, token_id, created_at, updated_at, expires_at, client_id, client_auth,\n device_id, parameters, details, code, current_refresh_token, scope\n FROM oauth_token\n WHERE token_id = $1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "did",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "token_id",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "created_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "updated_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "expires_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "client_id",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "client_auth",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "device_id",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "parameters",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "details",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"name": "code",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"name": "current_refresh_token",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 12,
|
||||
"name": "scope",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "b5d3a6a68443fbf3e6027f462ffaf5ac7e0d44344ce181e5a81932e7610265c8"
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n DELETE FROM oauth_authorization_request\n WHERE expires_at < NOW()\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "b6a1284c921cdb40254965adbaf7c2c61c4dba6938287d85f247fec94fed5230"
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n UPDATE oauth_token\n SET token_id = $2, current_refresh_token = $3, expires_at = $4, updated_at = NOW()\n WHERE id = $1\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int4",
|
||||
"Text",
|
||||
"Text",
|
||||
"Timestamptz"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "b9b57cad3948c2883a05c22ba918232d066fe8cb6f67410a4b4ef99d80386284"
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n DELETE FROM oauth_token WHERE token_id = $1\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "bab0b553f6ff88955ab84eac3fc958ab2f95944ab7f414d0b7256776c766c2a5"
|
||||
}
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT id, did, token_id, created_at, updated_at, expires_at, client_id, client_auth,\n device_id, parameters, details, code, current_refresh_token, scope\n FROM oauth_token\n WHERE current_refresh_token = $1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "did",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "token_id",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "created_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "updated_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "expires_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "client_id",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "client_auth",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "device_id",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "parameters",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"name": "details",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"name": "code",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 12,
|
||||
"name": "current_refresh_token",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 13,
|
||||
"name": "scope",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "bc816a96fa2e186cd0ff279f98543bebd9a815677d86fa8852f51fe76f95ce95"
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT handle FROM users u JOIN user_keys k ON u.id = k.user_id WHERE u.did = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "handle",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "bcc1fb4f23f1486f0ff49c96ce2e6c5d24bd8963a82d52763d3b535d4af192f3"
|
||||
}
|
||||
-15
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO user_keys (user_id, key_bytes) VALUES ($1, $2)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid",
|
||||
"Bytea"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "c4c9842e69c5fd4f4a2ebc176078af2a5f98beb3ea4d3c6af5b1b8fed2ec50e3"
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n DELETE FROM oauth_token WHERE id = $1\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int4"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "c72a8fb702f63cd07e25cf3bd41c3f4673b08623fd9746ee960e59bae07681d5"
|
||||
}
|
||||
-28
@@ -1,28 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT s.did, k.key_bytes FROM sessions s JOIN users u ON s.did = u.did JOIN user_keys k ON u.id = k.user_id WHERE s.refresh_jwt = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "did",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "key_bytes",
|
||||
"type_info": "Bytea"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "c949b23cf6d795c58e4c35907628bbb85714e9c49a569653b17acab60e1674ac"
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT session_id, user_agent, ip_address, last_seen_at\n FROM oauth_device\n WHERE id = $1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "session_id",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "user_agent",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "ip_address",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "last_seen_at",
|
||||
"type_info": "Timestamptz"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "c9dacba9ac1c6baec49e4b98117f803fff9b4cc722def305ba90218b0087798e"
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO oauth_device (id, session_id, user_agent, ip_address, last_seen_at)\n VALUES ($1, $2, $3, $4, $5)\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text",
|
||||
"Text",
|
||||
"Text",
|
||||
"Timestamptz"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "cb02d222787a1dea81f99ef25627c3439f7c754fce0c0460a293411e278ebd6b"
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT id, handle FROM users WHERE did = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "handle",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "cd047d9291c29265659dfc4f94d254467ace166865ea60d27ee39737119872c1"
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO oauth_authorized_client (did, client_id, created_at, updated_at, data)\n VALUES ($1, $2, NOW(), NOW(), $3)\n ON CONFLICT (did, client_id) DO UPDATE SET updated_at = NOW(), data = $3\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text",
|
||||
"Jsonb"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "cd88fece35ccc213ad5bdb7ad063c1e6e5b1e6d308c1f7800cdef9408c776789"
|
||||
}
|
||||
-22
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT key_bytes FROM user_keys WHERE user_id = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "key_bytes",
|
||||
"type_info": "Bytea"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "ce27e2da1f15cad97d2e31fda964e1d7017154fa559a8d9851728fb23af871cd"
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM session_tokens WHERE id = $1",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int4"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "cf874abcb72017e775fe699a0b77ae9341355f30e4af84968ffeb9135dba745f"
|
||||
}
|
||||
-34
@@ -1,34 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT s.did, k.key_bytes, u.id as user_id\n FROM sessions s\n JOIN users u ON s.did = u.did\n JOIN user_keys k ON u.id = k.user_id\n WHERE s.access_jwt = $1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "did",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "key_bytes",
|
||||
"type_info": "Bytea"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "user_id",
|
||||
"type_info": "Uuid"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "d31423ddcb625250d7c15581e8c9242ec6290b41507eb710744ad900d482222d"
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT token_id FROM oauth_used_refresh_token WHERE refresh_token = $1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "token_id",
|
||||
"type_info": "Int4"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "d402596403270a4cc6a2ce2050ba171155241a575bafacf859d65cd2c78f7367"
|
||||
}
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT did, device_id, client_id, client_auth, parameters, expires_at, code\n FROM oauth_authorization_request\n WHERE id = $1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "did",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "device_id",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "client_id",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "client_auth",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "parameters",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "expires_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "code",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "d5ec5d1952918c1d6ca035446cc5ffb805f271d621116b3ab314a1c57e3ba5c3"
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT 1 as one FROM session_tokens WHERE did = $1 AND access_jti = $2 AND access_expires_at > NOW()",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "one",
|
||||
"type_info": "Int4"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "d69f93ad69fe627d6939dced19b752efc49f6a807a0ae21ebf682433a0d63dd7"
|
||||
}
|
||||
-16
@@ -1,16 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO sessions (access_jwt, refresh_jwt, did) VALUES ($1, $2, $3)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "db9950690548510474a2bf755b4c4c103b284e82e3cf23d17fc99cd2fc728c64"
|
||||
}
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n DELETE FROM oauth_authorization_request\n WHERE code = $1\n RETURNING did, device_id, client_id, client_auth, parameters, expires_at, code\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "did",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "device_id",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "client_id",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "client_auth",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "parameters",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "expires_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "code",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "df7b49e30dd3388a7f0e6e8b531f0bf15f52cf6e943f7fe74382ac8090a3caf4"
|
||||
}
|
||||
-22
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT k.key_bytes FROM user_keys k JOIN users u ON k.user_id = u.id WHERE u.did = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "key_bytes",
|
||||
"type_info": "Bytea"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "ef55a06bcea9b1a0d744df4fe353260ae4d6d93bbf5ea73133db65e38f6241ee"
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT t.did, t.expires_at, u.deactivated_at, u.takedown_ref\n FROM oauth_token t\n JOIN users u ON t.did = u.did\n WHERE t.token_id = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "did",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "expires_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "deactivated_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "takedown_ref",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "efe82a97fd456c85dc7f51ece87f85950cca79fe0fac4ef6caa44fecf0911b07"
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n DELETE FROM oauth_dpop_jti\n WHERE created_at < NOW() - INTERVAL '1 second' * $1\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Float8"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "f06350c8f7baa88205a6872c974286364170e74cd3a936b80f762ae6e83f1f8e"
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n DELETE FROM oauth_authorization_request WHERE id = $1\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "f0faffe74f48c68bf98e6d3ec93ba3a410b41a7acc117f768033ca9a017f45ce"
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT session_id FROM used_refresh_tokens WHERE refresh_jti = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "session_id",
|
||||
"type_info": "Int4"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "fcd868a192d27fd4eccae92a884e881b8d6f09bf7ae08a9b431a44acbf2f91f3"
|
||||
}
|
||||
-14
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM sessions WHERE did = (SELECT did FROM users WHERE id = $1)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "fe9d108977af562e9e0439e755749253e52d92031e27a71d18b21265b20a4535"
|
||||
}
|
||||
Generated
+91
@@ -27,6 +27,41 @@ version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
||||
|
||||
[[package]]
|
||||
name = "aead"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0"
|
||||
dependencies = [
|
||||
"crypto-common",
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aes"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cipher",
|
||||
"cpufeatures",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aes-gcm"
|
||||
version = "0.10.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1"
|
||||
dependencies = [
|
||||
"aead",
|
||||
"aes",
|
||||
"cipher",
|
||||
"ctr",
|
||||
"ghash",
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "1.1.4"
|
||||
@@ -865,6 +900,7 @@ dependencies = [
|
||||
name = "bspds"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"aes-gcm",
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"aws-config",
|
||||
@@ -877,7 +913,10 @@ dependencies = [
|
||||
"cid",
|
||||
"ctor",
|
||||
"dotenvy",
|
||||
"ed25519-dalek",
|
||||
"futures",
|
||||
"hkdf",
|
||||
"hmac",
|
||||
"iroh-car",
|
||||
"jacquard",
|
||||
"jacquard-axum",
|
||||
@@ -886,6 +925,8 @@ dependencies = [
|
||||
"k256",
|
||||
"multibase",
|
||||
"multihash",
|
||||
"p256 0.13.2",
|
||||
"p384",
|
||||
"rand 0.8.5",
|
||||
"reqwest",
|
||||
"serde",
|
||||
@@ -894,6 +935,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"sqlx",
|
||||
"subtle",
|
||||
"testcontainers",
|
||||
"testcontainers-modules",
|
||||
"thiserror 2.0.17",
|
||||
@@ -901,6 +943,7 @@ dependencies = [
|
||||
"tokio-tungstenite",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"urlencoding",
|
||||
"uuid",
|
||||
"wiremock",
|
||||
]
|
||||
@@ -1303,6 +1346,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
"rand_core 0.6.4",
|
||||
"typenum",
|
||||
]
|
||||
|
||||
@@ -1322,6 +1366,15 @@ version = "0.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "52560adf09603e58c9a7ee1fe1dcb95a16927b17c127f0ac02d6e768a0e25bc1"
|
||||
|
||||
[[package]]
|
||||
name = "ctr"
|
||||
version = "0.9.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835"
|
||||
dependencies = [
|
||||
"cipher",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "curve25519-dalek"
|
||||
version = "4.1.3"
|
||||
@@ -2071,6 +2124,16 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ghash"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1"
|
||||
dependencies = [
|
||||
"opaque-debug",
|
||||
"polyval",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "glob"
|
||||
version = "0.3.3"
|
||||
@@ -3610,6 +3673,12 @@ version = "1.21.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||
|
||||
[[package]]
|
||||
name = "opaque-debug"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
|
||||
|
||||
[[package]]
|
||||
name = "openssl"
|
||||
version = "0.10.75"
|
||||
@@ -3905,6 +3974,18 @@ version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
|
||||
|
||||
[[package]]
|
||||
name = "polyval"
|
||||
version = "0.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"opaque-debug",
|
||||
"universal-hash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "portable-atomic"
|
||||
version = "1.11.1"
|
||||
@@ -5855,6 +5936,16 @@ version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
||||
|
||||
[[package]]
|
||||
name = "universal-hash"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea"
|
||||
dependencies = [
|
||||
"crypto-common",
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unsigned-varint"
|
||||
version = "0.7.2"
|
||||
|
||||
+10
@@ -16,6 +16,9 @@ chrono = { version = "0.4.42", features = ["serde"] }
|
||||
cid = "0.11.1"
|
||||
dotenvy = "0.15.7"
|
||||
futures = "0.3.30"
|
||||
hkdf = "0.12"
|
||||
hmac = "0.12"
|
||||
aes-gcm = "0.10"
|
||||
jacquard = { version = "0.9.3", default-features = false, features = ["api", "api_bluesky", "api_full", "derive", "dns"] }
|
||||
jacquard-axum = "0.9.2"
|
||||
jacquard-repo = "0.9.2"
|
||||
@@ -30,12 +33,17 @@ serde_bytes = "0.11.14"
|
||||
serde_ipld_dagcbor = "0.6.4"
|
||||
serde_json = "1.0.145"
|
||||
sha2 = "0.10.9"
|
||||
subtle = "2.5"
|
||||
p256 = { version = "0.13", features = ["ecdsa"] }
|
||||
p384 = { version = "0.13", features = ["ecdsa"] }
|
||||
ed25519-dalek = { version = "2.1", features = ["pkcs8"] }
|
||||
sqlx = { version = "0.8.6", features = ["runtime-tokio-rustls", "postgres", "uuid", "chrono", "json"] }
|
||||
thiserror = "2.0.17"
|
||||
tokio = { version = "1.48.0", features = ["macros", "rt-multi-thread", "time", "signal", "process"] }
|
||||
tracing = "0.1.43"
|
||||
tracing-subscriber = "0.3.22"
|
||||
tokio-tungstenite = { version = "0.28.0", features = ["native-tls"] }
|
||||
urlencoding = "2.1"
|
||||
uuid = { version = "1.19.0", features = ["v4", "fast-rng"] }
|
||||
|
||||
[dev-dependencies]
|
||||
@@ -44,3 +52,5 @@ iroh-car = "0.5.1"
|
||||
testcontainers = "0.26.0"
|
||||
testcontainers-modules = { version = "0.14.0", features = ["postgres"] }
|
||||
wiremock = "0.6.5"
|
||||
|
||||
# urlencoding is also in dependencies, but tests use it directly
|
||||
|
||||
@@ -110,23 +110,53 @@ Lewis' corrected big boy todofile
|
||||
## Temp Namespace (`com.atproto.temp`)
|
||||
- [ ] Implement `com.atproto.temp.checkSignupQueue` (signup queue status for gated signups).
|
||||
|
||||
## OAuth 2.0 Support
|
||||
The reference PDS implements full OAuth 2.0 provider functionality for native app authentication.
|
||||
- [ ] OAuth Provider Core
|
||||
- [ ] Implement `/.well-known/oauth-protected-resource` metadata endpoint.
|
||||
- [ ] Implement `/.well-known/oauth-authorization-server` metadata endpoint.
|
||||
- [ ] Implement `/oauth/authorize` authorization endpoint.
|
||||
- [ ] Implement `/oauth/par` Pushed Authorization Request endpoint.
|
||||
- [ ] Implement `/oauth/token` token endpoint.
|
||||
- [ ] Implement `/oauth/jwks` JSON Web Key Set endpoint.
|
||||
- [ ] OAuth Database Tables
|
||||
- [ ] Device table for tracking authorized devices.
|
||||
- [ ] Authorization request table.
|
||||
- [ ] Authorized client table.
|
||||
- [ ] Token table for OAuth tokens.
|
||||
- [ ] Used refresh token table.
|
||||
- [ ] DPoP (Demonstrating Proof-of-Possession) support.
|
||||
- [ ] Client metadata fetching and validation.
|
||||
## OAuth 2.1 Support
|
||||
Full OAuth 2.1 provider for ATProto native app authentication.
|
||||
- [x] OAuth Provider Core
|
||||
- [x] Implement `/.well-known/oauth-protected-resource` metadata endpoint.
|
||||
- [x] Implement `/.well-known/oauth-authorization-server` metadata endpoint.
|
||||
- [x] Implement `/oauth/authorize` authorization endpoint (headless JSON mode).
|
||||
- [x] Implement `/oauth/par` Pushed Authorization Request endpoint.
|
||||
- [x] Implement `/oauth/token` token endpoint (authorization_code + refresh_token grants).
|
||||
- [x] Implement `/oauth/jwks` JSON Web Key Set endpoint.
|
||||
- [x] Implement `/oauth/revoke` token revocation endpoint.
|
||||
- [x] Implement `/oauth/introspect` token introspection endpoint.
|
||||
- [x] OAuth Database Tables
|
||||
- [x] Device table for tracking authorized devices.
|
||||
- [x] Authorization request table.
|
||||
- [x] Authorized client table.
|
||||
- [x] Token table for OAuth tokens.
|
||||
- [x] Used refresh token table (replay protection).
|
||||
- [x] DPoP JTI tracking table.
|
||||
- [x] DPoP (Demonstrating Proof-of-Possession) support.
|
||||
- [x] Client metadata fetching and validation.
|
||||
- [x] PKCE (S256) enforcement.
|
||||
- [x] OAuth token verification extractor for protected resources.
|
||||
- [ ] Authorization UI templates (currently headless-only, returns JSON for programmatic flows).
|
||||
- [ ] Implement `private_key_jwt` signature verification (currently rejects with clear error).
|
||||
|
||||
## OAuth Security Notes
|
||||
|
||||
I've tried to ensure that this codebase is not vulnerable to the following:
|
||||
|
||||
- Constant-time comparison for signature verification (prevents timing attacks)
|
||||
- HMAC-SHA256 for access token signing with configurable secret
|
||||
- Production secrets require 32+ character minimum
|
||||
- DPoP JTI replay protection via database
|
||||
- DPoP nonce validation with HMAC-based timestamps (5 min validity)
|
||||
- Refresh token rotation with reuse detection (revokes token family on reuse)
|
||||
- PKCE S256 enforced (plain not allowed)
|
||||
- Authorization code single-use enforcement
|
||||
- URL encoding for redirect parameters (prevents injection)
|
||||
- All database queries use parameterized statements (no SQL injection)
|
||||
- Deactivated/taken-down accounts blocked from OAuth authorization
|
||||
- Client ID validation on token exchange (defense-in-depth against cross-client attacks)
|
||||
|
||||
### Auth Notes
|
||||
- Algorithm choice: Using ES256K (secp256k1 ECDSA) with per-user keys. Ref PDS uses HS256 (HMAC) with single server key. Our approach provides better key isolation but differs from reference implementation.
|
||||
- [ ] Support the ref PDS HS256 system too.
|
||||
- Token storage: Now storing only token JTIs in session_tokens table (defense in depth against DB breaches). Refresh token family tracking enables detection of token reuse attacks.
|
||||
- Key encryption: User signing keys encrypted at rest using AES-256-GCM with keys derived via HKDF from MASTER_KEY environment variable. Migration-safe: supports both encrypted (version 1) and plaintext (version 0) keys.
|
||||
|
||||
## PDS-Level App Endpoints
|
||||
These endpoints need to be implemented at the PDS level (not just proxied to appview).
|
||||
|
||||
@@ -27,32 +27,27 @@ fmt-check:
|
||||
|
||||
lint: fmt-check clippy
|
||||
|
||||
test:
|
||||
cargo test
|
||||
# Run tests (auto-starts and auto-cleans containers)
|
||||
test *args:
|
||||
./scripts/run-tests.sh {{args}}
|
||||
|
||||
test-verbose:
|
||||
cargo test -- --nocapture
|
||||
# Run a specific test file
|
||||
test-file file:
|
||||
./scripts/run-tests.sh --test {{file}}
|
||||
|
||||
test-repo:
|
||||
cargo test --test repo
|
||||
# Run tests with testcontainers (slower, no shared infra)
|
||||
test-standalone:
|
||||
BSPDS_ALLOW_INSECURE_SECRETS=1 cargo test
|
||||
|
||||
test-lifecycle:
|
||||
cargo test --test lifecycle
|
||||
# Manually manage test infrastructure (for debugging)
|
||||
test-infra-start:
|
||||
./scripts/test-infra.sh start
|
||||
|
||||
test-proxy:
|
||||
cargo test --test proxy
|
||||
test-infra-stop:
|
||||
./scripts/test-infra.sh stop
|
||||
|
||||
test-sync:
|
||||
cargo test --test sync
|
||||
|
||||
test-server:
|
||||
cargo test --test server
|
||||
|
||||
test-identity:
|
||||
cargo test --test identity
|
||||
|
||||
test-auth:
|
||||
cargo test --test auth
|
||||
test-infra-status:
|
||||
./scripts/test-infra.sh status
|
||||
|
||||
clean:
|
||||
cargo clean
|
||||
|
||||
@@ -18,15 +18,12 @@ CREATE TABLE IF NOT EXISTS users (
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
|
||||
-- status & moderation
|
||||
deactivated_at TIMESTAMPTZ,
|
||||
invites_disabled BOOLEAN DEFAULT FALSE,
|
||||
takedown_ref TEXT,
|
||||
|
||||
-- notifs
|
||||
preferred_notification_channel notification_channel NOT NULL DEFAULT 'email',
|
||||
|
||||
-- auth & verification
|
||||
password_reset_code TEXT,
|
||||
password_reset_code_expires_at TIMESTAMPTZ,
|
||||
|
||||
@@ -54,11 +51,12 @@ CREATE TABLE IF NOT EXISTS invite_code_uses (
|
||||
UNIQUE(code, used_by_user)
|
||||
);
|
||||
|
||||
-- TODO: encrypt at rest!
|
||||
CREATE TABLE IF NOT EXISTS user_keys (
|
||||
user_id UUID PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
|
||||
key_bytes BYTEA NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
encrypted_at TIMESTAMPTZ,
|
||||
encryption_version INTEGER DEFAULT 0
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS repos (
|
||||
@@ -68,14 +66,12 @@ CREATE TABLE IF NOT EXISTS repos (
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- content addressable storage
|
||||
CREATE TABLE IF NOT EXISTS blocks (
|
||||
cid BYTEA PRIMARY KEY,
|
||||
data BYTEA NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- denormalized index for fast queries
|
||||
CREATE TABLE IF NOT EXISTS records (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
repo_id UUID NOT NULL REFERENCES repos(user_id) ON DELETE CASCADE,
|
||||
@@ -97,13 +93,6 @@ CREATE TABLE IF NOT EXISTS blobs (
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS sessions (
|
||||
access_jwt TEXT PRIMARY KEY,
|
||||
refresh_jwt TEXT NOT NULL UNIQUE,
|
||||
did TEXT NOT NULL REFERENCES users(did) ON DELETE CASCADE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS app_passwords (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
@@ -114,7 +103,6 @@ CREATE TABLE IF NOT EXISTS app_passwords (
|
||||
UNIQUE(user_id, name)
|
||||
);
|
||||
|
||||
-- naughty list
|
||||
CREATE TABLE reports (
|
||||
id BIGINT PRIMARY KEY,
|
||||
reason_type TEXT NOT NULL,
|
||||
@@ -155,3 +143,125 @@ CREATE INDEX idx_notification_queue_status_scheduled
|
||||
WHERE status = 'pending';
|
||||
|
||||
CREATE INDEX idx_notification_queue_user_id ON notification_queue(user_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS reserved_signing_keys (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
did TEXT,
|
||||
public_key_did_key TEXT NOT NULL,
|
||||
private_key_bytes BYTEA NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
expires_at TIMESTAMPTZ NOT NULL DEFAULT NOW() + INTERVAL '24 hours',
|
||||
used_at TIMESTAMPTZ
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_reserved_signing_keys_did ON reserved_signing_keys(did) WHERE did IS NOT NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_reserved_signing_keys_expires ON reserved_signing_keys(expires_at) WHERE used_at IS NULL;
|
||||
|
||||
CREATE TABLE repo_seq (
|
||||
seq BIGSERIAL PRIMARY KEY,
|
||||
did TEXT NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
event_type TEXT NOT NULL,
|
||||
commit_cid TEXT,
|
||||
prev_cid TEXT,
|
||||
ops JSONB,
|
||||
blobs TEXT[],
|
||||
blocks_cids TEXT[]
|
||||
);
|
||||
|
||||
CREATE INDEX idx_repo_seq_seq ON repo_seq(seq);
|
||||
CREATE INDEX idx_repo_seq_did ON repo_seq(did);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS session_tokens (
|
||||
id SERIAL PRIMARY KEY,
|
||||
did TEXT NOT NULL REFERENCES users(did) ON DELETE CASCADE,
|
||||
access_jti TEXT NOT NULL UNIQUE,
|
||||
refresh_jti TEXT NOT NULL UNIQUE,
|
||||
access_expires_at TIMESTAMPTZ NOT NULL,
|
||||
refresh_expires_at TIMESTAMPTZ NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_session_tokens_did ON session_tokens(did);
|
||||
CREATE INDEX idx_session_tokens_access_jti ON session_tokens(access_jti);
|
||||
CREATE INDEX idx_session_tokens_refresh_jti ON session_tokens(refresh_jti);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS used_refresh_tokens (
|
||||
refresh_jti TEXT PRIMARY KEY,
|
||||
session_id INTEGER NOT NULL REFERENCES session_tokens(id) ON DELETE CASCADE,
|
||||
used_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_used_refresh_tokens_session_id ON used_refresh_tokens(session_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS oauth_device (
|
||||
id TEXT PRIMARY KEY,
|
||||
session_id TEXT NOT NULL UNIQUE,
|
||||
user_agent TEXT,
|
||||
ip_address TEXT NOT NULL,
|
||||
last_seen_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS oauth_authorization_request (
|
||||
id TEXT PRIMARY KEY,
|
||||
did TEXT REFERENCES users(did) ON DELETE CASCADE,
|
||||
device_id TEXT REFERENCES oauth_device(id) ON DELETE SET NULL,
|
||||
client_id TEXT NOT NULL,
|
||||
client_auth JSONB,
|
||||
parameters JSONB NOT NULL,
|
||||
expires_at TIMESTAMPTZ NOT NULL,
|
||||
code TEXT UNIQUE
|
||||
);
|
||||
|
||||
CREATE INDEX idx_oauth_auth_request_expires ON oauth_authorization_request(expires_at);
|
||||
CREATE INDEX idx_oauth_auth_request_code ON oauth_authorization_request(code) WHERE code IS NOT NULL;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS oauth_token (
|
||||
id SERIAL PRIMARY KEY,
|
||||
did TEXT NOT NULL REFERENCES users(did) ON DELETE CASCADE,
|
||||
token_id TEXT NOT NULL UNIQUE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
expires_at TIMESTAMPTZ NOT NULL,
|
||||
client_id TEXT NOT NULL,
|
||||
client_auth JSONB NOT NULL,
|
||||
device_id TEXT REFERENCES oauth_device(id) ON DELETE SET NULL,
|
||||
parameters JSONB NOT NULL,
|
||||
details JSONB,
|
||||
code TEXT UNIQUE,
|
||||
current_refresh_token TEXT UNIQUE,
|
||||
scope TEXT
|
||||
);
|
||||
|
||||
CREATE INDEX idx_oauth_token_did ON oauth_token(did);
|
||||
CREATE INDEX idx_oauth_token_code ON oauth_token(code) WHERE code IS NOT NULL;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS oauth_account_device (
|
||||
did TEXT NOT NULL REFERENCES users(did) ON DELETE CASCADE,
|
||||
device_id TEXT NOT NULL REFERENCES oauth_device(id) ON DELETE CASCADE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
PRIMARY KEY (did, device_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS oauth_authorized_client (
|
||||
did TEXT NOT NULL REFERENCES users(did) ON DELETE CASCADE,
|
||||
client_id TEXT NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
data JSONB NOT NULL,
|
||||
PRIMARY KEY (did, client_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS oauth_used_refresh_token (
|
||||
refresh_token TEXT PRIMARY KEY,
|
||||
token_id INTEGER NOT NULL REFERENCES oauth_token(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE oauth_dpop_jti (
|
||||
jti TEXT PRIMARY KEY,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_oauth_dpop_jti_created_at ON oauth_dpop_jti(created_at);
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
CREATE TABLE IF NOT EXISTS reserved_signing_keys (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
did TEXT,
|
||||
public_key_did_key TEXT NOT NULL,
|
||||
private_key_bytes BYTEA NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
expires_at TIMESTAMPTZ NOT NULL DEFAULT NOW() + INTERVAL '24 hours',
|
||||
used_at TIMESTAMPTZ
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_reserved_signing_keys_did ON reserved_signing_keys(did) WHERE did IS NOT NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_reserved_signing_keys_expires ON reserved_signing_keys(expires_at) WHERE used_at IS NULL;
|
||||
@@ -1,13 +0,0 @@
|
||||
CREATE TABLE repo_seq (
|
||||
seq BIGSERIAL PRIMARY KEY,
|
||||
did TEXT NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
event_type TEXT NOT NULL,
|
||||
commit_cid TEXT,
|
||||
prev_cid TEXT,
|
||||
ops JSONB,
|
||||
blobs TEXT[]
|
||||
);
|
||||
|
||||
CREATE INDEX idx_repo_seq_seq ON repo_seq(seq);
|
||||
CREATE INDEX idx_repo_seq_did ON repo_seq(did);
|
||||
@@ -1,2 +0,0 @@
|
||||
ALTER TABLE repo_seq ADD COLUMN blocks_cids TEXT[];
|
||||
|
||||
Executable
+29
@@ -0,0 +1,29 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
INFRA_SCRIPT="$SCRIPT_DIR/test-infra.sh"
|
||||
|
||||
cleanup() {
|
||||
echo ""
|
||||
echo "Cleaning up test infrastructure..."
|
||||
"$INFRA_SCRIPT" stop
|
||||
}
|
||||
|
||||
trap cleanup EXIT
|
||||
|
||||
"$INFRA_SCRIPT" start
|
||||
|
||||
source "${TMPDIR:-/tmp}/bspds_test_infra.env"
|
||||
|
||||
echo ""
|
||||
echo "Running database migrations..."
|
||||
sqlx database create 2>/dev/null || true
|
||||
sqlx migrate run --source "$PROJECT_DIR/migrations"
|
||||
|
||||
echo ""
|
||||
echo "Running tests..."
|
||||
echo ""
|
||||
|
||||
cargo nextest run "$@"
|
||||
Executable
+166
@@ -0,0 +1,166 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
INFRA_FILE="${TMPDIR:-/tmp}/bspds_test_infra.env"
|
||||
CONTAINER_PREFIX="bspds-test"
|
||||
|
||||
command_exists() {
|
||||
command -v "$1" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
if command_exists podman; then
|
||||
CONTAINER_CMD="podman"
|
||||
if [[ -z "${DOCKER_HOST:-}" ]]; then
|
||||
RUNTIME_DIR="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}"
|
||||
PODMAN_SOCK="$RUNTIME_DIR/podman/podman.sock"
|
||||
if [[ -S "$PODMAN_SOCK" ]]; then
|
||||
export DOCKER_HOST="unix://$PODMAN_SOCK"
|
||||
fi
|
||||
fi
|
||||
elif command_exists docker; then
|
||||
CONTAINER_CMD="docker"
|
||||
else
|
||||
echo "Error: Neither podman nor docker found" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
start_infra() {
|
||||
echo "Starting test infrastructure..."
|
||||
|
||||
if [[ -f "$INFRA_FILE" ]]; then
|
||||
source "$INFRA_FILE"
|
||||
if $CONTAINER_CMD ps --format '{{.Names}}' 2>/dev/null | grep -q "^${CONTAINER_PREFIX}-postgres$"; then
|
||||
echo "Infrastructure already running (found $INFRA_FILE)"
|
||||
cat "$INFRA_FILE"
|
||||
return 0
|
||||
fi
|
||||
echo "Stale infra file found, cleaning up..."
|
||||
rm -f "$INFRA_FILE"
|
||||
fi
|
||||
|
||||
$CONTAINER_CMD rm -f "${CONTAINER_PREFIX}-postgres" "${CONTAINER_PREFIX}-minio" 2>/dev/null || true
|
||||
|
||||
echo "Starting PostgreSQL..."
|
||||
$CONTAINER_CMD run -d \
|
||||
--name "${CONTAINER_PREFIX}-postgres" \
|
||||
-e POSTGRES_PASSWORD=postgres \
|
||||
-e POSTGRES_USER=postgres \
|
||||
-e POSTGRES_DB=postgres \
|
||||
-P \
|
||||
--label bspds_test=true \
|
||||
postgres:18-alpine >/dev/null
|
||||
|
||||
echo "Starting MinIO..."
|
||||
$CONTAINER_CMD run -d \
|
||||
--name "${CONTAINER_PREFIX}-minio" \
|
||||
-e MINIO_ROOT_USER=minioadmin \
|
||||
-e MINIO_ROOT_PASSWORD=minioadmin \
|
||||
-P \
|
||||
--label bspds_test=true \
|
||||
minio/minio:latest server /data >/dev/null
|
||||
|
||||
echo "Waiting for services to be ready..."
|
||||
sleep 2
|
||||
|
||||
PG_PORT=$($CONTAINER_CMD port "${CONTAINER_PREFIX}-postgres" 5432 | head -1 | cut -d: -f2)
|
||||
MINIO_PORT=$($CONTAINER_CMD port "${CONTAINER_PREFIX}-minio" 9000 | head -1 | cut -d: -f2)
|
||||
|
||||
for i in {1..30}; do
|
||||
if $CONTAINER_CMD exec "${CONTAINER_PREFIX}-postgres" pg_isready -U postgres >/dev/null 2>&1; then
|
||||
break
|
||||
fi
|
||||
echo "Waiting for PostgreSQL... ($i/30)"
|
||||
sleep 1
|
||||
done
|
||||
|
||||
for i in {1..30}; do
|
||||
if curl -s "http://127.0.0.1:${MINIO_PORT}/minio/health/live" >/dev/null 2>&1; then
|
||||
break
|
||||
fi
|
||||
echo "Waiting for MinIO... ($i/30)"
|
||||
sleep 1
|
||||
done
|
||||
|
||||
echo "Creating MinIO bucket..."
|
||||
$CONTAINER_CMD run --rm --network host \
|
||||
-e MC_HOST_minio="http://minioadmin:minioadmin@127.0.0.1:${MINIO_PORT}" \
|
||||
minio/mc:latest mb minio/test-bucket --ignore-existing >/dev/null 2>&1 || true
|
||||
|
||||
cat > "$INFRA_FILE" << EOF
|
||||
export DATABASE_URL="postgres://postgres:postgres@127.0.0.1:${PG_PORT}/postgres"
|
||||
export TEST_DB_PORT="${PG_PORT}"
|
||||
export S3_ENDPOINT="http://127.0.0.1:${MINIO_PORT}"
|
||||
export S3_BUCKET="test-bucket"
|
||||
export AWS_ACCESS_KEY_ID="minioadmin"
|
||||
export AWS_SECRET_ACCESS_KEY="minioadmin"
|
||||
export AWS_REGION="us-east-1"
|
||||
export BSPDS_TEST_INFRA_READY="1"
|
||||
export BSPDS_ALLOW_INSECURE_SECRETS="1"
|
||||
EOF
|
||||
|
||||
echo ""
|
||||
echo "Infrastructure ready!"
|
||||
echo "Config written to: $INFRA_FILE"
|
||||
echo ""
|
||||
cat "$INFRA_FILE"
|
||||
}
|
||||
|
||||
stop_infra() {
|
||||
echo "Stopping test infrastructure..."
|
||||
$CONTAINER_CMD rm -f "${CONTAINER_PREFIX}-postgres" "${CONTAINER_PREFIX}-minio" 2>/dev/null || true
|
||||
rm -f "$INFRA_FILE"
|
||||
echo "Infrastructure stopped."
|
||||
}
|
||||
|
||||
status_infra() {
|
||||
echo "Test Infrastructure Status:"
|
||||
echo "============================"
|
||||
|
||||
if [[ -f "$INFRA_FILE" ]]; then
|
||||
echo "Config file: $INFRA_FILE"
|
||||
source "$INFRA_FILE"
|
||||
echo "Database URL: $DATABASE_URL"
|
||||
echo "S3 Endpoint: $S3_ENDPOINT"
|
||||
else
|
||||
echo "Config file: NOT FOUND"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Containers:"
|
||||
$CONTAINER_CMD ps -a --filter "label=bspds_test=true" --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" 2>/dev/null || echo " (none)"
|
||||
}
|
||||
|
||||
case "${1:-}" in
|
||||
start)
|
||||
start_infra
|
||||
;;
|
||||
stop)
|
||||
stop_infra
|
||||
;;
|
||||
restart)
|
||||
stop_infra
|
||||
start_infra
|
||||
;;
|
||||
status)
|
||||
status_infra
|
||||
;;
|
||||
env)
|
||||
if [[ -f "$INFRA_FILE" ]]; then
|
||||
cat "$INFRA_FILE"
|
||||
else
|
||||
echo "Infrastructure not running. Run: $0 start" >&2
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
echo "Usage: $0 {start|stop|restart|status|env}"
|
||||
echo ""
|
||||
echo "Commands:"
|
||||
echo " start - Start test infrastructure (Postgres, MinIO)"
|
||||
echo " stop - Stop and remove test containers"
|
||||
echo " restart - Stop then start infrastructure"
|
||||
echo " status - Show infrastructure status"
|
||||
echo " env - Output environment variables for sourcing"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
@@ -214,7 +214,7 @@ pub async fn delete_account(
|
||||
}
|
||||
};
|
||||
|
||||
let _ = sqlx::query!("DELETE FROM sessions WHERE did = $1", did)
|
||||
let _ = sqlx::query!("DELETE FROM session_tokens WHERE did = $1", did)
|
||||
.execute(&state.db)
|
||||
.await;
|
||||
|
||||
|
||||
+16
-33
@@ -44,31 +44,22 @@ pub async fn get_timeline(
|
||||
State(state): State<AppState>,
|
||||
headers: axum::http::HeaderMap,
|
||||
) -> Response {
|
||||
let auth_header = headers.get("Authorization");
|
||||
if auth_header.is_none() {
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": "AuthenticationRequired"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
let token = auth_header
|
||||
.unwrap()
|
||||
.to_str()
|
||||
.unwrap_or("")
|
||||
.replace("Bearer ", "");
|
||||
|
||||
let session = sqlx::query!(
|
||||
"SELECT s.did, k.key_bytes FROM sessions s JOIN users u ON s.did = u.did JOIN user_keys k ON u.id = k.user_id WHERE s.access_jwt = $1",
|
||||
token
|
||||
)
|
||||
.fetch_optional(&state.db)
|
||||
.await
|
||||
.unwrap_or(None);
|
||||
|
||||
let (did, key_bytes) = match session {
|
||||
Some(row) => (row.did, row.key_bytes),
|
||||
let token = match crate::auth::extract_bearer_token_from_header(
|
||||
headers.get("Authorization").and_then(|h| h.to_str().ok())
|
||||
) {
|
||||
Some(t) => t,
|
||||
None => {
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": "AuthenticationRequired"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let auth_user = match crate::auth::validate_bearer_token(&state.db, &token).await {
|
||||
Ok(user) => user,
|
||||
Err(_) => {
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": "AuthenticationFailed"})),
|
||||
@@ -77,15 +68,7 @@ pub async fn get_timeline(
|
||||
}
|
||||
};
|
||||
|
||||
if crate::auth::verify_token(&token, &key_bytes).is_err() {
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
|
||||
let user_query = sqlx::query!("SELECT id FROM users WHERE did = $1", did)
|
||||
let user_query = sqlx::query!("SELECT id FROM users WHERE did = $1", auth_user.did)
|
||||
.fetch_optional(&state.db)
|
||||
.await;
|
||||
|
||||
|
||||
+31
-11
@@ -228,10 +228,23 @@ pub async fn create_account(
|
||||
(secret_key.to_bytes().to_vec(), None)
|
||||
};
|
||||
|
||||
let encrypted_key_bytes = match crate::config::encrypt_key(&secret_key_bytes) {
|
||||
Ok(enc) => enc,
|
||||
Err(e) => {
|
||||
error!("Error encrypting user key: {:?}", e);
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({"error": "InternalError"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let key_insert = sqlx::query!(
|
||||
"INSERT INTO user_keys (user_id, key_bytes) VALUES ($1, $2)",
|
||||
"INSERT INTO user_keys (user_id, key_bytes, encryption_version, encrypted_at) VALUES ($1, $2, $3, NOW())",
|
||||
user_id,
|
||||
&secret_key_bytes[..]
|
||||
&encrypted_key_bytes[..],
|
||||
crate::config::ENCRYPTION_VERSION
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await;
|
||||
@@ -345,7 +358,7 @@ pub async fn create_account(
|
||||
}
|
||||
}
|
||||
|
||||
let access_jwt = crate::auth::create_access_token(&did, &secret_key_bytes[..]).map_err(|e| {
|
||||
let access_meta = crate::auth::create_access_token_with_metadata(&did, &secret_key_bytes[..]).map_err(|e| {
|
||||
error!("Error creating access token: {:?}", e);
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
@@ -353,12 +366,12 @@ pub async fn create_account(
|
||||
)
|
||||
.into_response()
|
||||
});
|
||||
let access_jwt = match access_jwt {
|
||||
Ok(t) => t,
|
||||
let access_meta = match access_meta {
|
||||
Ok(m) => m,
|
||||
Err(r) => return r,
|
||||
};
|
||||
|
||||
let refresh_jwt = crate::auth::create_refresh_token(&did, &secret_key_bytes[..]).map_err(|e| {
|
||||
let refresh_meta = crate::auth::create_refresh_token_with_metadata(&did, &secret_key_bytes[..]).map_err(|e| {
|
||||
error!("Error creating refresh token: {:?}", e);
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
@@ -366,13 +379,20 @@ pub async fn create_account(
|
||||
)
|
||||
.into_response()
|
||||
});
|
||||
let refresh_jwt = match refresh_jwt {
|
||||
Ok(t) => t,
|
||||
let refresh_meta = match refresh_meta {
|
||||
Ok(m) => m,
|
||||
Err(r) => return r,
|
||||
};
|
||||
|
||||
let session_insert =
|
||||
sqlx::query!("INSERT INTO sessions (access_jwt, refresh_jwt, did) VALUES ($1, $2, $3)", access_jwt, refresh_jwt, did)
|
||||
sqlx::query!(
|
||||
"INSERT INTO session_tokens (did, access_jti, refresh_jti, access_expires_at, refresh_expires_at) VALUES ($1, $2, $3, $4, $5)",
|
||||
did,
|
||||
access_meta.jti,
|
||||
refresh_meta.jti,
|
||||
access_meta.expires_at,
|
||||
refresh_meta.expires_at
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await;
|
||||
|
||||
@@ -410,8 +430,8 @@ pub async fn create_account(
|
||||
(
|
||||
StatusCode::OK,
|
||||
Json(CreateAccountOutput {
|
||||
access_jwt,
|
||||
refresh_jwt,
|
||||
access_jwt: access_meta.token,
|
||||
refresh_jwt: refresh_meta.token,
|
||||
handle: input.handle,
|
||||
did,
|
||||
}),
|
||||
|
||||
+74
-83
@@ -121,12 +121,23 @@ pub async fn user_did_doc(State(state): State<AppState>, Path(handle): Path<Stri
|
||||
.into_response();
|
||||
}
|
||||
|
||||
let key_row = sqlx::query!("SELECT key_bytes FROM user_keys WHERE user_id = $1", user_id)
|
||||
let key_row = sqlx::query!("SELECT key_bytes, encryption_version FROM user_keys WHERE user_id = $1", user_id)
|
||||
.fetch_optional(&state.db)
|
||||
.await;
|
||||
|
||||
let key_bytes: Vec<u8> = match key_row {
|
||||
Ok(Some(row)) => row.key_bytes,
|
||||
Ok(Some(row)) => {
|
||||
match crate::config::decrypt_key(&row.key_bytes, row.encryption_version) {
|
||||
Ok(k) => k,
|
||||
Err(_) => {
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({"error": "InternalError"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
@@ -270,45 +281,37 @@ pub async fn get_recommended_did_credentials(
|
||||
State(state): State<AppState>,
|
||||
headers: axum::http::HeaderMap,
|
||||
) -> Response {
|
||||
let auth_header = headers.get("Authorization");
|
||||
if auth_header.is_none() {
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": "AuthenticationRequired"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
|
||||
let token = auth_header
|
||||
.unwrap()
|
||||
.to_str()
|
||||
.unwrap_or("")
|
||||
.replace("Bearer ", "");
|
||||
|
||||
let session = sqlx::query!(
|
||||
r#"
|
||||
SELECT s.did, k.key_bytes, u.handle
|
||||
FROM sessions s
|
||||
JOIN users u ON s.did = u.did
|
||||
JOIN user_keys k ON u.id = k.user_id
|
||||
WHERE s.access_jwt = $1
|
||||
"#,
|
||||
token
|
||||
)
|
||||
.fetch_optional(&state.db)
|
||||
.await;
|
||||
|
||||
let (_did, key_bytes, handle) = match session {
|
||||
Ok(Some(row)) => (row.did, row.key_bytes, row.handle),
|
||||
Ok(None) => {
|
||||
let token = match crate::auth::extract_bearer_token_from_header(
|
||||
headers.get("Authorization").and_then(|h| h.to_str().ok())
|
||||
) {
|
||||
Some(t) => t,
|
||||
None => {
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": "AuthenticationFailed"})),
|
||||
Json(json!({"error": "AuthenticationRequired"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let auth_result = crate::auth::validate_bearer_token(&state.db, &token).await;
|
||||
let did = match auth_result {
|
||||
Ok(ref user) => user.did.clone(),
|
||||
Err(e) => {
|
||||
error!("DB error in get_recommended_did_credentials: {:?}", e);
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": e})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let user = match sqlx::query!("SELECT handle FROM users u JOIN user_keys k ON u.id = k.user_id WHERE u.did = $1", did)
|
||||
.fetch_optional(&state.db)
|
||||
.await
|
||||
{
|
||||
Ok(Some(row)) => row,
|
||||
_ => {
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({"error": "InternalError"})),
|
||||
@@ -316,14 +319,18 @@ pub async fn get_recommended_did_credentials(
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
let handle = user.handle;
|
||||
|
||||
if let Err(_) = crate::auth::verify_token(&token, &key_bytes) {
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
let key_bytes = match auth_result.ok().and_then(|u| u.key_bytes) {
|
||||
Some(kb) => kb,
|
||||
None => {
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": "AuthenticationFailed", "message": "OAuth tokens cannot get DID credentials"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
|
||||
let pds_endpoint = format!("https://{}", hostname);
|
||||
@@ -376,45 +383,37 @@ pub async fn update_handle(
|
||||
headers: axum::http::HeaderMap,
|
||||
Json(input): Json<UpdateHandleInput>,
|
||||
) -> Response {
|
||||
let auth_header = headers.get("Authorization");
|
||||
if auth_header.is_none() {
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": "AuthenticationRequired"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
|
||||
let token = auth_header
|
||||
.unwrap()
|
||||
.to_str()
|
||||
.unwrap_or("")
|
||||
.replace("Bearer ", "");
|
||||
|
||||
let session = sqlx::query!(
|
||||
r#"
|
||||
SELECT s.did, k.key_bytes, u.id as user_id
|
||||
FROM sessions s
|
||||
JOIN users u ON s.did = u.did
|
||||
JOIN user_keys k ON u.id = k.user_id
|
||||
WHERE s.access_jwt = $1
|
||||
"#,
|
||||
token
|
||||
)
|
||||
.fetch_optional(&state.db)
|
||||
.await;
|
||||
|
||||
let (_did, key_bytes, user_id) = match session {
|
||||
Ok(Some(row)) => (row.did, row.key_bytes, row.user_id),
|
||||
Ok(None) => {
|
||||
let token = match crate::auth::extract_bearer_token_from_header(
|
||||
headers.get("Authorization").and_then(|h| h.to_str().ok())
|
||||
) {
|
||||
Some(t) => t,
|
||||
None => {
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": "AuthenticationFailed"})),
|
||||
Json(json!({"error": "AuthenticationRequired"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let auth_result = crate::auth::validate_bearer_token(&state.db, &token).await;
|
||||
let did = match auth_result {
|
||||
Ok(user) => user.did,
|
||||
Err(e) => {
|
||||
error!("DB error in update_handle: {:?}", e);
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": e})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let user_id = match sqlx::query_scalar!("SELECT id FROM users WHERE did = $1", did)
|
||||
.fetch_optional(&state.db)
|
||||
.await
|
||||
{
|
||||
Ok(Some(id)) => id,
|
||||
_ => {
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({"error": "InternalError"})),
|
||||
@@ -423,14 +422,6 @@ pub async fn update_handle(
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(_) = crate::auth::verify_token(&token, &key_bytes) {
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
|
||||
let new_handle = input.handle.trim();
|
||||
if new_handle.is_empty() {
|
||||
return (
|
||||
|
||||
+17
-47
@@ -33,60 +33,30 @@ pub async fn create_report(
|
||||
headers: axum::http::HeaderMap,
|
||||
Json(input): Json<CreateReportInput>,
|
||||
) -> Response {
|
||||
let auth_header = headers.get("Authorization");
|
||||
if auth_header.is_none() {
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": "AuthenticationRequired"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
|
||||
let token = auth_header
|
||||
.unwrap()
|
||||
.to_str()
|
||||
.unwrap_or("")
|
||||
.replace("Bearer ", "");
|
||||
|
||||
let session = sqlx::query!(
|
||||
r#"
|
||||
SELECT s.did, k.key_bytes
|
||||
FROM sessions s
|
||||
JOIN users u ON s.did = u.did
|
||||
JOIN user_keys k ON u.id = k.user_id
|
||||
WHERE s.access_jwt = $1
|
||||
"#,
|
||||
token
|
||||
)
|
||||
.fetch_optional(&state.db)
|
||||
.await;
|
||||
|
||||
let (did, key_bytes) = match session {
|
||||
Ok(Some(row)) => (row.did, row.key_bytes),
|
||||
Ok(None) => {
|
||||
let token = match crate::auth::extract_bearer_token_from_header(
|
||||
headers.get("Authorization").and_then(|h| h.to_str().ok())
|
||||
) {
|
||||
Some(t) => t,
|
||||
None => {
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": "AuthenticationFailed"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
Err(e) => {
|
||||
error!("DB error in create_report: {:?}", e);
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({"error": "InternalError"})),
|
||||
Json(json!({"error": "AuthenticationRequired"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(_) = crate::auth::verify_token(&token, &key_bytes) {
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
let auth_result = crate::auth::validate_bearer_token(&state.db, &token).await;
|
||||
let did = match auth_result {
|
||||
Ok(user) => user.did,
|
||||
Err(e) => {
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": e})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let valid_reason_types = [
|
||||
"com.atproto.moderation.defs#reasonSpam",
|
||||
|
||||
+10
-9
@@ -43,17 +43,18 @@ pub async fn proxy_handler(
|
||||
let mut auth_header_val = headers.get("Authorization").map(|h| h.clone());
|
||||
|
||||
if let Some(aud) = &proxy_header {
|
||||
if let Some(auth_val) = &auth_header_val {
|
||||
if let Ok(token) = auth_val.to_str() {
|
||||
let token = token.replace("Bearer ", "");
|
||||
if let Ok(did) = crate::auth::get_did_from_token(&token) {
|
||||
let key_row = sqlx::query!("SELECT k.key_bytes FROM user_keys k JOIN users u ON k.user_id = u.id WHERE u.did = $1", did)
|
||||
.fetch_optional(&state.db)
|
||||
.await;
|
||||
if let Some(token) = crate::auth::extract_bearer_token_from_header(
|
||||
headers.get("Authorization").and_then(|h| h.to_str().ok())
|
||||
) {
|
||||
if let Ok(did) = crate::auth::get_did_from_token(&token) {
|
||||
let key_row = sqlx::query!("SELECT k.key_bytes, k.encryption_version FROM user_keys k JOIN users u ON k.user_id = u.id WHERE u.did = $1", did)
|
||||
.fetch_optional(&state.db)
|
||||
.await;
|
||||
|
||||
if let Ok(Some(row)) = key_row {
|
||||
if let Ok(Some(row)) = key_row {
|
||||
if let Ok(decrypted_key) = crate::config::decrypt_key(&row.key_bytes, row.encryption_version) {
|
||||
if let Ok(new_token) =
|
||||
crate::auth::create_service_token(&did, aud, &method, &row.key_bytes)
|
||||
crate::auth::create_service_token(&did, aud, &method, &decrypted_key)
|
||||
{
|
||||
if let Ok(val) =
|
||||
axum::http::HeaderValue::from_str(&format!("Bearer {}", new_token))
|
||||
|
||||
+32
-64
@@ -20,31 +20,22 @@ pub async fn upload_blob(
|
||||
headers: axum::http::HeaderMap,
|
||||
body: Bytes,
|
||||
) -> Response {
|
||||
let auth_header = headers.get("Authorization");
|
||||
if auth_header.is_none() {
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": "AuthenticationRequired"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
let token = auth_header
|
||||
.unwrap()
|
||||
.to_str()
|
||||
.unwrap_or("")
|
||||
.replace("Bearer ", "");
|
||||
|
||||
let session = sqlx::query!(
|
||||
"SELECT s.did, k.key_bytes FROM sessions s JOIN users u ON s.did = u.did JOIN user_keys k ON u.id = k.user_id WHERE s.access_jwt = $1",
|
||||
token
|
||||
)
|
||||
.fetch_optional(&state.db)
|
||||
.await
|
||||
.unwrap_or(None);
|
||||
|
||||
let (did, key_bytes) = match session {
|
||||
Some(row) => (row.did, row.key_bytes),
|
||||
let token = match crate::auth::extract_bearer_token_from_header(
|
||||
headers.get("Authorization").and_then(|h| h.to_str().ok())
|
||||
) {
|
||||
Some(t) => t,
|
||||
None => {
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": "AuthenticationRequired"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let auth_user = match crate::auth::validate_bearer_token(&state.db, &token).await {
|
||||
Ok(user) => user,
|
||||
Err(_) => {
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": "AuthenticationFailed"})),
|
||||
@@ -52,14 +43,7 @@ pub async fn upload_blob(
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(_) = crate::auth::verify_token(&token, &key_bytes) {
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
let did = auth_user.did;
|
||||
|
||||
let mime_type = headers
|
||||
.get("content-type")
|
||||
@@ -182,32 +166,22 @@ pub async fn list_missing_blobs(
|
||||
headers: axum::http::HeaderMap,
|
||||
Query(params): Query<ListMissingBlobsParams>,
|
||||
) -> Response {
|
||||
let auth_header = headers.get("Authorization");
|
||||
if auth_header.is_none() {
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": "AuthenticationRequired"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
|
||||
let token = auth_header
|
||||
.unwrap()
|
||||
.to_str()
|
||||
.unwrap_or("")
|
||||
.replace("Bearer ", "");
|
||||
|
||||
let session = sqlx::query!(
|
||||
"SELECT s.did, k.key_bytes FROM sessions s JOIN users u ON s.did = u.did JOIN user_keys k ON u.id = k.user_id WHERE s.access_jwt = $1",
|
||||
token
|
||||
)
|
||||
.fetch_optional(&state.db)
|
||||
.await
|
||||
.unwrap_or(None);
|
||||
|
||||
let (did, key_bytes) = match session {
|
||||
Some(row) => (row.did, row.key_bytes),
|
||||
let token = match crate::auth::extract_bearer_token_from_header(
|
||||
headers.get("Authorization").and_then(|h| h.to_str().ok())
|
||||
) {
|
||||
Some(t) => t,
|
||||
None => {
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": "AuthenticationRequired"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let auth_user = match crate::auth::validate_bearer_token(&state.db, &token).await {
|
||||
Ok(user) => user,
|
||||
Err(_) => {
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": "AuthenticationFailed"})),
|
||||
@@ -216,13 +190,7 @@ pub async fn list_missing_blobs(
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(_) = crate::auth::verify_token(&token, &key_bytes) {
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
let did = auth_user.did;
|
||||
|
||||
let user_query = sqlx::query!("SELECT id FROM users WHERE did = $1", did)
|
||||
.fetch_optional(&state.db)
|
||||
|
||||
@@ -73,31 +73,22 @@ pub async fn apply_writes(
|
||||
headers: axum::http::HeaderMap,
|
||||
Json(input): Json<ApplyWritesInput>,
|
||||
) -> Response {
|
||||
let auth_header = headers.get("Authorization");
|
||||
if auth_header.is_none() {
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": "AuthenticationRequired"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
let token = auth_header
|
||||
.unwrap()
|
||||
.to_str()
|
||||
.unwrap_or("")
|
||||
.replace("Bearer ", "");
|
||||
|
||||
let session = sqlx::query!(
|
||||
"SELECT s.did, k.key_bytes FROM sessions s JOIN users u ON s.did = u.did JOIN user_keys k ON u.id = k.user_id WHERE s.access_jwt = $1",
|
||||
token
|
||||
)
|
||||
.fetch_optional(&state.db)
|
||||
.await
|
||||
.unwrap_or(None);
|
||||
|
||||
let (did, key_bytes) = match session {
|
||||
Some(row) => (row.did, row.key_bytes),
|
||||
let token = match crate::auth::extract_bearer_token_from_header(
|
||||
headers.get("Authorization").and_then(|h| h.to_str().ok())
|
||||
) {
|
||||
Some(t) => t,
|
||||
None => {
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": "AuthenticationRequired"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let auth_user = match crate::auth::validate_bearer_token(&state.db, &token).await {
|
||||
Ok(user) => user,
|
||||
Err(_) => {
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": "AuthenticationFailed"})),
|
||||
@@ -106,13 +97,7 @@ pub async fn apply_writes(
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(_) = crate::auth::verify_token(&token, &key_bytes) {
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
let did = auth_user.did;
|
||||
|
||||
if input.repo != did {
|
||||
return (
|
||||
|
||||
@@ -23,45 +23,27 @@ pub async fn prepare_repo_write(
|
||||
headers: &HeaderMap,
|
||||
repo_did: &str,
|
||||
) -> Result<(String, Uuid, Cid), Response> {
|
||||
let auth_header = headers.get("Authorization").ok_or_else(|| {
|
||||
let token = crate::auth::extract_bearer_token_from_header(
|
||||
headers.get("Authorization").and_then(|h| h.to_str().ok())
|
||||
).ok_or_else(|| {
|
||||
(
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": "AuthenticationRequired"})),
|
||||
)
|
||||
.into_response()
|
||||
})?;
|
||||
let token = auth_header
|
||||
.to_str()
|
||||
.unwrap_or("")
|
||||
.replace("Bearer ", "");
|
||||
|
||||
let session = sqlx::query!(
|
||||
"SELECT s.did, k.key_bytes FROM sessions s JOIN users u ON s.did = u.did JOIN user_keys k ON u.id = k.user_id WHERE s.access_jwt = $1",
|
||||
token
|
||||
)
|
||||
.fetch_optional(&state.db)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("DB error fetching session: {}", e);
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response()
|
||||
})?
|
||||
.ok_or_else(|| {
|
||||
(
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": "AuthenticationFailed"})),
|
||||
)
|
||||
.into_response()
|
||||
})?;
|
||||
let auth_user = crate::auth::validate_bearer_token(&state.db, &token)
|
||||
.await
|
||||
.map_err(|_| {
|
||||
(
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": "AuthenticationFailed"})),
|
||||
)
|
||||
.into_response()
|
||||
})?;
|
||||
|
||||
crate::auth::verify_token(&token, &session.key_bytes).map_err(|_| {
|
||||
(
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"})),
|
||||
)
|
||||
.into_response()
|
||||
})?;
|
||||
|
||||
if repo_did != session.did {
|
||||
if repo_did != auth_user.did {
|
||||
return Err((
|
||||
StatusCode::FORBIDDEN,
|
||||
Json(json!({"error": "InvalidRepo", "message": "Repo does not match authenticated user"})),
|
||||
@@ -69,7 +51,7 @@ pub async fn prepare_repo_write(
|
||||
.into_response());
|
||||
}
|
||||
|
||||
let user_id = sqlx::query_scalar!("SELECT id FROM users WHERE did = $1", session.did)
|
||||
let user_id = sqlx::query_scalar!("SELECT id FROM users WHERE did = $1", auth_user.did)
|
||||
.fetch_optional(&state.db)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
@@ -108,7 +90,7 @@ pub async fn prepare_repo_write(
|
||||
.into_response()
|
||||
})?;
|
||||
|
||||
Ok((session.did, user_id, current_root_cid))
|
||||
Ok((auth_user.did, user_id, current_root_cid))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
|
||||
@@ -30,45 +30,37 @@ pub async fn check_account_status(
|
||||
State(state): State<AppState>,
|
||||
headers: axum::http::HeaderMap,
|
||||
) -> Response {
|
||||
let auth_header = headers.get("Authorization");
|
||||
if auth_header.is_none() {
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": "AuthenticationRequired"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
|
||||
let token = auth_header
|
||||
.unwrap()
|
||||
.to_str()
|
||||
.unwrap_or("")
|
||||
.replace("Bearer ", "");
|
||||
|
||||
let session = sqlx::query!(
|
||||
r#"
|
||||
SELECT s.did, k.key_bytes, u.id as user_id
|
||||
FROM sessions s
|
||||
JOIN users u ON s.did = u.did
|
||||
JOIN user_keys k ON u.id = k.user_id
|
||||
WHERE s.access_jwt = $1
|
||||
"#,
|
||||
token
|
||||
)
|
||||
.fetch_optional(&state.db)
|
||||
.await;
|
||||
|
||||
let (did, key_bytes, user_id) = match session {
|
||||
Ok(Some(row)) => (row.did, row.key_bytes, row.user_id),
|
||||
Ok(None) => {
|
||||
let token = match crate::auth::extract_bearer_token_from_header(
|
||||
headers.get("Authorization").and_then(|h| h.to_str().ok())
|
||||
) {
|
||||
Some(t) => t,
|
||||
None => {
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": "AuthenticationFailed"})),
|
||||
Json(json!({"error": "AuthenticationRequired"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let auth_result = crate::auth::validate_bearer_token_allow_deactivated(&state.db, &token).await;
|
||||
let did = match auth_result {
|
||||
Ok(user) => user.did,
|
||||
Err(e) => {
|
||||
error!("DB error in check_account_status: {:?}", e);
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": e})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let user_id = match sqlx::query_scalar!("SELECT id FROM users WHERE did = $1", did)
|
||||
.fetch_optional(&state.db)
|
||||
.await
|
||||
{
|
||||
Ok(Some(id)) => id,
|
||||
_ => {
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({"error": "InternalError"})),
|
||||
@@ -77,14 +69,6 @@ pub async fn check_account_status(
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(_) = crate::auth::verify_token(&token, &key_bytes) {
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
|
||||
let user_status = sqlx::query!("SELECT deactivated_at FROM users WHERE did = $1", did)
|
||||
.fetch_optional(&state.db)
|
||||
.await;
|
||||
@@ -139,60 +123,30 @@ pub async fn activate_account(
|
||||
State(state): State<AppState>,
|
||||
headers: axum::http::HeaderMap,
|
||||
) -> Response {
|
||||
let auth_header = headers.get("Authorization");
|
||||
if auth_header.is_none() {
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": "AuthenticationRequired"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
|
||||
let token = auth_header
|
||||
.unwrap()
|
||||
.to_str()
|
||||
.unwrap_or("")
|
||||
.replace("Bearer ", "");
|
||||
|
||||
let session = sqlx::query!(
|
||||
r#"
|
||||
SELECT s.did, k.key_bytes
|
||||
FROM sessions s
|
||||
JOIN users u ON s.did = u.did
|
||||
JOIN user_keys k ON u.id = k.user_id
|
||||
WHERE s.access_jwt = $1
|
||||
"#,
|
||||
token
|
||||
)
|
||||
.fetch_optional(&state.db)
|
||||
.await;
|
||||
|
||||
let (did, key_bytes) = match session {
|
||||
Ok(Some(row)) => (row.did, row.key_bytes),
|
||||
Ok(None) => {
|
||||
let token = match crate::auth::extract_bearer_token_from_header(
|
||||
headers.get("Authorization").and_then(|h| h.to_str().ok())
|
||||
) {
|
||||
Some(t) => t,
|
||||
None => {
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": "AuthenticationFailed"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
Err(e) => {
|
||||
error!("DB error in activate_account: {:?}", e);
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({"error": "InternalError"})),
|
||||
Json(json!({"error": "AuthenticationRequired"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(_) = crate::auth::verify_token(&token, &key_bytes) {
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
let auth_result = crate::auth::validate_bearer_token_allow_deactivated(&state.db, &token).await;
|
||||
let did = match auth_result {
|
||||
Ok(user) => user.did,
|
||||
Err(e) => {
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": e})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let result = sqlx::query!("UPDATE users SET deactivated_at = NULL WHERE did = $1", did)
|
||||
.execute(&state.db)
|
||||
@@ -222,60 +176,30 @@ pub async fn deactivate_account(
|
||||
headers: axum::http::HeaderMap,
|
||||
Json(_input): Json<DeactivateAccountInput>,
|
||||
) -> Response {
|
||||
let auth_header = headers.get("Authorization");
|
||||
if auth_header.is_none() {
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": "AuthenticationRequired"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
|
||||
let token = auth_header
|
||||
.unwrap()
|
||||
.to_str()
|
||||
.unwrap_or("")
|
||||
.replace("Bearer ", "");
|
||||
|
||||
let session = sqlx::query!(
|
||||
r#"
|
||||
SELECT s.did, k.key_bytes
|
||||
FROM sessions s
|
||||
JOIN users u ON s.did = u.did
|
||||
JOIN user_keys k ON u.id = k.user_id
|
||||
WHERE s.access_jwt = $1
|
||||
"#,
|
||||
token
|
||||
)
|
||||
.fetch_optional(&state.db)
|
||||
.await;
|
||||
|
||||
let (did, key_bytes) = match session {
|
||||
Ok(Some(row)) => (row.did, row.key_bytes),
|
||||
Ok(None) => {
|
||||
let token = match crate::auth::extract_bearer_token_from_header(
|
||||
headers.get("Authorization").and_then(|h| h.to_str().ok())
|
||||
) {
|
||||
Some(t) => t,
|
||||
None => {
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": "AuthenticationFailed"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
Err(e) => {
|
||||
error!("DB error in deactivate_account: {:?}", e);
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({"error": "InternalError"})),
|
||||
Json(json!({"error": "AuthenticationRequired"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(_) = crate::auth::verify_token(&token, &key_bytes) {
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
let auth_result = crate::auth::validate_bearer_token(&state.db, &token).await;
|
||||
let did = match auth_result {
|
||||
Ok(user) => user.did,
|
||||
Err(e) => {
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": e})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let result = sqlx::query!("UPDATE users SET deactivated_at = NOW() WHERE did = $1", did)
|
||||
.execute(&state.db)
|
||||
@@ -298,45 +222,37 @@ pub async fn request_account_delete(
|
||||
State(state): State<AppState>,
|
||||
headers: axum::http::HeaderMap,
|
||||
) -> Response {
|
||||
let auth_header = headers.get("Authorization");
|
||||
if auth_header.is_none() {
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": "AuthenticationRequired"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
|
||||
let token = auth_header
|
||||
.unwrap()
|
||||
.to_str()
|
||||
.unwrap_or("")
|
||||
.replace("Bearer ", "");
|
||||
|
||||
let session = sqlx::query!(
|
||||
r#"
|
||||
SELECT s.did, u.id as user_id, u.email, u.handle, k.key_bytes
|
||||
FROM sessions s
|
||||
JOIN users u ON s.did = u.did
|
||||
JOIN user_keys k ON u.id = k.user_id
|
||||
WHERE s.access_jwt = $1
|
||||
"#,
|
||||
token
|
||||
)
|
||||
.fetch_optional(&state.db)
|
||||
.await;
|
||||
|
||||
let (did, user_id, email, handle, key_bytes) = match session {
|
||||
Ok(Some(row)) => (row.did, row.user_id, row.email, row.handle, row.key_bytes),
|
||||
Ok(None) => {
|
||||
let token = match crate::auth::extract_bearer_token_from_header(
|
||||
headers.get("Authorization").and_then(|h| h.to_str().ok())
|
||||
) {
|
||||
Some(t) => t,
|
||||
None => {
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": "AuthenticationFailed"})),
|
||||
Json(json!({"error": "AuthenticationRequired"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let auth_result = crate::auth::validate_bearer_token_allow_deactivated(&state.db, &token).await;
|
||||
let did = match auth_result {
|
||||
Ok(user) => user.did,
|
||||
Err(e) => {
|
||||
error!("DB error in request_account_delete: {:?}", e);
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": e})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let user = match sqlx::query!("SELECT id, email, handle FROM users WHERE did = $1", did)
|
||||
.fetch_optional(&state.db)
|
||||
.await
|
||||
{
|
||||
Ok(Some(row)) => row,
|
||||
_ => {
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({"error": "InternalError"})),
|
||||
@@ -344,14 +260,9 @@ pub async fn request_account_delete(
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(_) = crate::auth::verify_token(&token, &key_bytes) {
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
let user_id = user.id;
|
||||
let email = user.email;
|
||||
let handle = user.handle;
|
||||
|
||||
let confirmation_token = Uuid::new_v4().to_string();
|
||||
let expires_at = Utc::now() + Duration::minutes(15);
|
||||
@@ -541,7 +452,7 @@ pub async fn delete_account(
|
||||
};
|
||||
|
||||
let deletion_result: Result<(), sqlx::Error> = async {
|
||||
sqlx::query!("DELETE FROM sessions WHERE did = $1", did)
|
||||
sqlx::query!("DELETE FROM session_tokens WHERE did = $1", did)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
|
||||
+75
-123
@@ -26,45 +26,37 @@ pub async fn list_app_passwords(
|
||||
State(state): State<AppState>,
|
||||
headers: axum::http::HeaderMap,
|
||||
) -> Response {
|
||||
let auth_header = headers.get("Authorization");
|
||||
if auth_header.is_none() {
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": "AuthenticationRequired"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
|
||||
let token = auth_header
|
||||
.unwrap()
|
||||
.to_str()
|
||||
.unwrap_or("")
|
||||
.replace("Bearer ", "");
|
||||
|
||||
let session = sqlx::query!(
|
||||
r#"
|
||||
SELECT s.did, k.key_bytes, u.id as user_id
|
||||
FROM sessions s
|
||||
JOIN users u ON s.did = u.did
|
||||
JOIN user_keys k ON u.id = k.user_id
|
||||
WHERE s.access_jwt = $1
|
||||
"#,
|
||||
token
|
||||
)
|
||||
.fetch_optional(&state.db)
|
||||
.await;
|
||||
|
||||
let (_did, key_bytes, user_id) = match session {
|
||||
Ok(Some(row)) => (row.did, row.key_bytes, row.user_id),
|
||||
Ok(None) => {
|
||||
let token = match crate::auth::extract_bearer_token_from_header(
|
||||
headers.get("Authorization").and_then(|h| h.to_str().ok())
|
||||
) {
|
||||
Some(t) => t,
|
||||
None => {
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": "AuthenticationFailed"})),
|
||||
Json(json!({"error": "AuthenticationRequired"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let auth_result = crate::auth::validate_bearer_token(&state.db, &token).await;
|
||||
let did = match auth_result {
|
||||
Ok(user) => user.did,
|
||||
Err(e) => {
|
||||
error!("DB error in list_app_passwords: {:?}", e);
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": e})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let user_id = match sqlx::query_scalar!("SELECT id FROM users WHERE did = $1", did)
|
||||
.fetch_optional(&state.db)
|
||||
.await
|
||||
{
|
||||
Ok(Some(id)) => id,
|
||||
_ => {
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({"error": "InternalError"})),
|
||||
@@ -73,14 +65,6 @@ pub async fn list_app_passwords(
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(_) = crate::auth::verify_token(&token, &key_bytes) {
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
|
||||
let result = sqlx::query!("SELECT name, created_at, privileged FROM app_passwords WHERE user_id = $1 ORDER BY created_at DESC", user_id)
|
||||
.fetch_all(&state.db)
|
||||
.await;
|
||||
@@ -131,45 +115,37 @@ pub async fn create_app_password(
|
||||
headers: axum::http::HeaderMap,
|
||||
Json(input): Json<CreateAppPasswordInput>,
|
||||
) -> Response {
|
||||
let auth_header = headers.get("Authorization");
|
||||
if auth_header.is_none() {
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": "AuthenticationRequired"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
|
||||
let token = auth_header
|
||||
.unwrap()
|
||||
.to_str()
|
||||
.unwrap_or("")
|
||||
.replace("Bearer ", "");
|
||||
|
||||
let session = sqlx::query!(
|
||||
r#"
|
||||
SELECT s.did, k.key_bytes, u.id as user_id
|
||||
FROM sessions s
|
||||
JOIN users u ON s.did = u.did
|
||||
JOIN user_keys k ON u.id = k.user_id
|
||||
WHERE s.access_jwt = $1
|
||||
"#,
|
||||
token
|
||||
)
|
||||
.fetch_optional(&state.db)
|
||||
.await;
|
||||
|
||||
let (_did, key_bytes, user_id) = match session {
|
||||
Ok(Some(row)) => (row.did, row.key_bytes, row.user_id),
|
||||
Ok(None) => {
|
||||
let token = match crate::auth::extract_bearer_token_from_header(
|
||||
headers.get("Authorization").and_then(|h| h.to_str().ok())
|
||||
) {
|
||||
Some(t) => t,
|
||||
None => {
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": "AuthenticationFailed"})),
|
||||
Json(json!({"error": "AuthenticationRequired"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let auth_result = crate::auth::validate_bearer_token(&state.db, &token).await;
|
||||
let did = match auth_result {
|
||||
Ok(user) => user.did,
|
||||
Err(e) => {
|
||||
error!("DB error in create_app_password: {:?}", e);
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": e})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let user_id = match sqlx::query_scalar!("SELECT id FROM users WHERE did = $1", did)
|
||||
.fetch_optional(&state.db)
|
||||
.await
|
||||
{
|
||||
Ok(Some(id)) => id,
|
||||
_ => {
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({"error": "InternalError"})),
|
||||
@@ -178,14 +154,6 @@ pub async fn create_app_password(
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(_) = crate::auth::verify_token(&token, &key_bytes) {
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
|
||||
let name = input.name.trim();
|
||||
if name.is_empty() {
|
||||
return (
|
||||
@@ -275,45 +243,37 @@ pub async fn revoke_app_password(
|
||||
headers: axum::http::HeaderMap,
|
||||
Json(input): Json<RevokeAppPasswordInput>,
|
||||
) -> Response {
|
||||
let auth_header = headers.get("Authorization");
|
||||
if auth_header.is_none() {
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": "AuthenticationRequired"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
|
||||
let token = auth_header
|
||||
.unwrap()
|
||||
.to_str()
|
||||
.unwrap_or("")
|
||||
.replace("Bearer ", "");
|
||||
|
||||
let session = sqlx::query!(
|
||||
r#"
|
||||
SELECT s.did, k.key_bytes, u.id as user_id
|
||||
FROM sessions s
|
||||
JOIN users u ON s.did = u.did
|
||||
JOIN user_keys k ON u.id = k.user_id
|
||||
WHERE s.access_jwt = $1
|
||||
"#,
|
||||
token
|
||||
)
|
||||
.fetch_optional(&state.db)
|
||||
.await;
|
||||
|
||||
let (_did, key_bytes, user_id) = match session {
|
||||
Ok(Some(row)) => (row.did, row.key_bytes, row.user_id),
|
||||
Ok(None) => {
|
||||
let token = match crate::auth::extract_bearer_token_from_header(
|
||||
headers.get("Authorization").and_then(|h| h.to_str().ok())
|
||||
) {
|
||||
Some(t) => t,
|
||||
None => {
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": "AuthenticationFailed"})),
|
||||
Json(json!({"error": "AuthenticationRequired"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let auth_result = crate::auth::validate_bearer_token(&state.db, &token).await;
|
||||
let did = match auth_result {
|
||||
Ok(user) => user.did,
|
||||
Err(e) => {
|
||||
error!("DB error in revoke_app_password: {:?}", e);
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": e})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let user_id = match sqlx::query_scalar!("SELECT id FROM users WHERE did = $1", did)
|
||||
.fetch_optional(&state.db)
|
||||
.await
|
||||
{
|
||||
Ok(Some(id)) => id,
|
||||
_ => {
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({"error": "InternalError"})),
|
||||
@@ -322,14 +282,6 @@ pub async fn revoke_app_password(
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(_) = crate::auth::verify_token(&token, &key_bytes) {
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
|
||||
let name = input.name.trim();
|
||||
if name.is_empty() {
|
||||
return (
|
||||
|
||||
+92
-148
@@ -30,45 +30,37 @@ pub async fn request_email_update(
|
||||
headers: axum::http::HeaderMap,
|
||||
Json(input): Json<RequestEmailUpdateInput>,
|
||||
) -> Response {
|
||||
let auth_header = headers.get("Authorization");
|
||||
if auth_header.is_none() {
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": "AuthenticationRequired"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
|
||||
let token = auth_header
|
||||
.unwrap()
|
||||
.to_str()
|
||||
.unwrap_or("")
|
||||
.replace("Bearer ", "");
|
||||
|
||||
let session = sqlx::query!(
|
||||
r#"
|
||||
SELECT s.did, k.key_bytes, u.id as user_id, u.handle
|
||||
FROM sessions s
|
||||
JOIN users u ON s.did = u.did
|
||||
JOIN user_keys k ON u.id = k.user_id
|
||||
WHERE s.access_jwt = $1
|
||||
"#,
|
||||
token
|
||||
)
|
||||
.fetch_optional(&state.db)
|
||||
.await;
|
||||
|
||||
let (_did, key_bytes, user_id, handle) = match session {
|
||||
Ok(Some(row)) => (row.did, row.key_bytes, row.user_id, row.handle),
|
||||
Ok(None) => {
|
||||
let token = match crate::auth::extract_bearer_token_from_header(
|
||||
headers.get("Authorization").and_then(|h| h.to_str().ok())
|
||||
) {
|
||||
Some(t) => t,
|
||||
None => {
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": "AuthenticationFailed"})),
|
||||
Json(json!({"error": "AuthenticationRequired"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let auth_result = crate::auth::validate_bearer_token(&state.db, &token).await;
|
||||
let did = match auth_result {
|
||||
Ok(user) => user.did,
|
||||
Err(e) => {
|
||||
error!("DB error in request_email_update: {:?}", e);
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": e})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let user = match sqlx::query!("SELECT id, handle FROM users WHERE did = $1", did)
|
||||
.fetch_optional(&state.db)
|
||||
.await
|
||||
{
|
||||
Ok(Some(row)) => row,
|
||||
_ => {
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({"error": "InternalError"})),
|
||||
@@ -76,14 +68,8 @@ pub async fn request_email_update(
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(_) = crate::auth::verify_token(&token, &key_bytes) {
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
let user_id = user.id;
|
||||
let handle = user.handle;
|
||||
|
||||
let email = input.email.trim().to_lowercase();
|
||||
if email.is_empty() {
|
||||
@@ -159,52 +145,40 @@ pub async fn confirm_email(
|
||||
headers: axum::http::HeaderMap,
|
||||
Json(input): Json<ConfirmEmailInput>,
|
||||
) -> Response {
|
||||
let auth_header = headers.get("Authorization");
|
||||
if auth_header.is_none() {
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": "AuthenticationRequired"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
|
||||
let token = auth_header
|
||||
.unwrap()
|
||||
.to_str()
|
||||
.unwrap_or("")
|
||||
.replace("Bearer ", "");
|
||||
|
||||
let session = sqlx::query!(
|
||||
r#"
|
||||
SELECT s.did, k.key_bytes, u.id as user_id, u.email_confirmation_code, u.email_confirmation_code_expires_at, u.email_pending_verification
|
||||
FROM sessions s
|
||||
JOIN users u ON s.did = u.did
|
||||
JOIN user_keys k ON u.id = k.user_id
|
||||
WHERE s.access_jwt = $1
|
||||
"#,
|
||||
token
|
||||
)
|
||||
.fetch_optional(&state.db)
|
||||
.await;
|
||||
|
||||
let (_did, key_bytes, user_id, stored_code, expires_at, email_pending_verification) = match session {
|
||||
Ok(Some(row)) => (
|
||||
row.did,
|
||||
row.key_bytes,
|
||||
row.user_id,
|
||||
row.email_confirmation_code,
|
||||
row.email_confirmation_code_expires_at,
|
||||
row.email_pending_verification,
|
||||
),
|
||||
Ok(None) => {
|
||||
let token = match crate::auth::extract_bearer_token_from_header(
|
||||
headers.get("Authorization").and_then(|h| h.to_str().ok())
|
||||
) {
|
||||
Some(t) => t,
|
||||
None => {
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": "AuthenticationFailed"})),
|
||||
Json(json!({"error": "AuthenticationRequired"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let auth_result = crate::auth::validate_bearer_token(&state.db, &token).await;
|
||||
let did = match auth_result {
|
||||
Ok(user) => user.did,
|
||||
Err(e) => {
|
||||
error!("DB error in confirm_email: {:?}", e);
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": e})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let user = match sqlx::query!(
|
||||
"SELECT id, email_confirmation_code, email_confirmation_code_expires_at, email_pending_verification FROM users WHERE did = $1",
|
||||
did
|
||||
)
|
||||
.fetch_optional(&state.db)
|
||||
.await
|
||||
{
|
||||
Ok(Some(row)) => row,
|
||||
_ => {
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({"error": "InternalError"})),
|
||||
@@ -212,14 +186,10 @@ pub async fn confirm_email(
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(_) = crate::auth::verify_token(&token, &key_bytes) {
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
let user_id = user.id;
|
||||
let stored_code = user.email_confirmation_code;
|
||||
let expires_at = user.email_confirmation_code_expires_at;
|
||||
let email_pending_verification = user.email_pending_verification;
|
||||
|
||||
let email = input.email.trim().to_lowercase();
|
||||
let confirmation_code = input.token.trim();
|
||||
@@ -301,63 +271,40 @@ pub async fn update_email(
|
||||
headers: axum::http::HeaderMap,
|
||||
Json(input): Json<UpdateEmailInput>,
|
||||
) -> Response {
|
||||
let auth_header = headers.get("Authorization");
|
||||
if auth_header.is_none() {
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": "AuthenticationRequired"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
|
||||
let token = auth_header
|
||||
.unwrap()
|
||||
.to_str()
|
||||
.unwrap_or("")
|
||||
.replace("Bearer ", "");
|
||||
|
||||
let session = sqlx::query!(
|
||||
r#"
|
||||
SELECT s.did, k.key_bytes, u.id as user_id, u.email as current_email,
|
||||
u.email_confirmation_code, u.email_confirmation_code_expires_at,
|
||||
u.email_pending_verification
|
||||
FROM sessions s
|
||||
JOIN users u ON s.did = u.did
|
||||
JOIN user_keys k ON u.id = k.user_id
|
||||
WHERE s.access_jwt = $1
|
||||
"#,
|
||||
token
|
||||
)
|
||||
.fetch_optional(&state.db)
|
||||
.await;
|
||||
|
||||
let (
|
||||
_did,
|
||||
key_bytes,
|
||||
user_id,
|
||||
current_email,
|
||||
stored_code,
|
||||
expires_at,
|
||||
email_pending_verification,
|
||||
) = match session {
|
||||
Ok(Some(row)) => (
|
||||
row.did,
|
||||
row.key_bytes,
|
||||
row.user_id,
|
||||
row.current_email,
|
||||
row.email_confirmation_code,
|
||||
row.email_confirmation_code_expires_at,
|
||||
row.email_pending_verification,
|
||||
),
|
||||
Ok(None) => {
|
||||
let token = match crate::auth::extract_bearer_token_from_header(
|
||||
headers.get("Authorization").and_then(|h| h.to_str().ok())
|
||||
) {
|
||||
Some(t) => t,
|
||||
None => {
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": "AuthenticationFailed"})),
|
||||
Json(json!({"error": "AuthenticationRequired"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let auth_result = crate::auth::validate_bearer_token(&state.db, &token).await;
|
||||
let did = match auth_result {
|
||||
Ok(user) => user.did,
|
||||
Err(e) => {
|
||||
error!("DB error in update_email: {:?}", e);
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": e})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let user = match sqlx::query!(
|
||||
"SELECT id, email, email_confirmation_code, email_confirmation_code_expires_at, email_pending_verification FROM users WHERE did = $1",
|
||||
did
|
||||
)
|
||||
.fetch_optional(&state.db)
|
||||
.await
|
||||
{
|
||||
Ok(Some(row)) => row,
|
||||
_ => {
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({"error": "InternalError"})),
|
||||
@@ -365,14 +312,11 @@ pub async fn update_email(
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(_) = crate::auth::verify_token(&token, &key_bytes) {
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
let user_id = user.id;
|
||||
let current_email = user.email;
|
||||
let stored_code = user.email_confirmation_code;
|
||||
let expires_at = user.email_confirmation_code_expires_at;
|
||||
let email_pending_verification = user.email_pending_verification;
|
||||
|
||||
let new_email = input.email.trim().to_lowercase();
|
||||
if new_email.is_empty() {
|
||||
|
||||
+75
-123
@@ -27,14 +27,18 @@ pub async fn create_invite_code(
|
||||
headers: axum::http::HeaderMap,
|
||||
Json(input): Json<CreateInviteCodeInput>,
|
||||
) -> Response {
|
||||
let auth_header = headers.get("Authorization");
|
||||
if auth_header.is_none() {
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": "AuthenticationRequired"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
let token = match crate::auth::extract_bearer_token_from_header(
|
||||
headers.get("Authorization").and_then(|h| h.to_str().ok())
|
||||
) {
|
||||
Some(t) => t,
|
||||
None => {
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": "AuthenticationRequired"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
if input.use_count < 1 {
|
||||
return (
|
||||
@@ -44,36 +48,24 @@ pub async fn create_invite_code(
|
||||
.into_response();
|
||||
}
|
||||
|
||||
let token = auth_header
|
||||
.unwrap()
|
||||
.to_str()
|
||||
.unwrap_or("")
|
||||
.replace("Bearer ", "");
|
||||
|
||||
let session = sqlx::query!(
|
||||
r#"
|
||||
SELECT s.did, k.key_bytes, u.id as user_id
|
||||
FROM sessions s
|
||||
JOIN users u ON s.did = u.did
|
||||
JOIN user_keys k ON u.id = k.user_id
|
||||
WHERE s.access_jwt = $1
|
||||
"#,
|
||||
token
|
||||
)
|
||||
.fetch_optional(&state.db)
|
||||
.await;
|
||||
|
||||
let (did, key_bytes, user_id) = match session {
|
||||
Ok(Some(row)) => (row.did, row.key_bytes, row.user_id),
|
||||
Ok(None) => {
|
||||
let auth_result = crate::auth::validate_bearer_token(&state.db, &token).await;
|
||||
let did = match auth_result {
|
||||
Ok(user) => user.did,
|
||||
Err(e) => {
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": "AuthenticationFailed"})),
|
||||
Json(json!({"error": e})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
Err(e) => {
|
||||
error!("DB error in create_invite_code: {:?}", e);
|
||||
};
|
||||
|
||||
let user_id = match sqlx::query_scalar!("SELECT id FROM users WHERE did = $1", did)
|
||||
.fetch_optional(&state.db)
|
||||
.await
|
||||
{
|
||||
Ok(Some(id)) => id,
|
||||
_ => {
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({"error": "InternalError"})),
|
||||
@@ -82,14 +74,6 @@ pub async fn create_invite_code(
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(_) = crate::auth::verify_token(&token, &key_bytes) {
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
|
||||
let creator_user_id = if let Some(for_account) = &input.for_account {
|
||||
let target = sqlx::query!("SELECT id FROM users WHERE did = $1", for_account)
|
||||
.fetch_optional(&state.db)
|
||||
@@ -184,14 +168,18 @@ pub async fn create_invite_codes(
|
||||
headers: axum::http::HeaderMap,
|
||||
Json(input): Json<CreateInviteCodesInput>,
|
||||
) -> Response {
|
||||
let auth_header = headers.get("Authorization");
|
||||
if auth_header.is_none() {
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": "AuthenticationRequired"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
let token = match crate::auth::extract_bearer_token_from_header(
|
||||
headers.get("Authorization").and_then(|h| h.to_str().ok())
|
||||
) {
|
||||
Some(t) => t,
|
||||
None => {
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": "AuthenticationRequired"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
if input.use_count < 1 {
|
||||
return (
|
||||
@@ -201,36 +189,24 @@ pub async fn create_invite_codes(
|
||||
.into_response();
|
||||
}
|
||||
|
||||
let token = auth_header
|
||||
.unwrap()
|
||||
.to_str()
|
||||
.unwrap_or("")
|
||||
.replace("Bearer ", "");
|
||||
|
||||
let session = sqlx::query!(
|
||||
r#"
|
||||
SELECT s.did, k.key_bytes, u.id as user_id
|
||||
FROM sessions s
|
||||
JOIN users u ON s.did = u.did
|
||||
JOIN user_keys k ON u.id = k.user_id
|
||||
WHERE s.access_jwt = $1
|
||||
"#,
|
||||
token
|
||||
)
|
||||
.fetch_optional(&state.db)
|
||||
.await;
|
||||
|
||||
let (_did, key_bytes, user_id) = match session {
|
||||
Ok(Some(row)) => (row.did, row.key_bytes, row.user_id),
|
||||
Ok(None) => {
|
||||
let auth_result = crate::auth::validate_bearer_token(&state.db, &token).await;
|
||||
let did = match auth_result {
|
||||
Ok(user) => user.did,
|
||||
Err(e) => {
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": "AuthenticationFailed"})),
|
||||
Json(json!({"error": e})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
Err(e) => {
|
||||
error!("DB error in create_invite_codes: {:?}", e);
|
||||
};
|
||||
|
||||
let user_id = match sqlx::query_scalar!("SELECT id FROM users WHERE did = $1", did)
|
||||
.fetch_optional(&state.db)
|
||||
.await
|
||||
{
|
||||
Ok(Some(id)) => id,
|
||||
_ => {
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({"error": "InternalError"})),
|
||||
@@ -239,14 +215,6 @@ pub async fn create_invite_codes(
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(_) = crate::auth::verify_token(&token, &key_bytes) {
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
|
||||
let code_count = input.code_count.unwrap_or(1).max(1);
|
||||
let for_accounts = input.for_accounts.unwrap_or_default();
|
||||
|
||||
@@ -374,45 +342,37 @@ pub async fn get_account_invite_codes(
|
||||
headers: axum::http::HeaderMap,
|
||||
axum::extract::Query(params): axum::extract::Query<GetAccountInviteCodesParams>,
|
||||
) -> Response {
|
||||
let auth_header = headers.get("Authorization");
|
||||
if auth_header.is_none() {
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": "AuthenticationRequired"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
|
||||
let token = auth_header
|
||||
.unwrap()
|
||||
.to_str()
|
||||
.unwrap_or("")
|
||||
.replace("Bearer ", "");
|
||||
|
||||
let session = sqlx::query!(
|
||||
r#"
|
||||
SELECT s.did, k.key_bytes, u.id as user_id
|
||||
FROM sessions s
|
||||
JOIN users u ON s.did = u.did
|
||||
JOIN user_keys k ON u.id = k.user_id
|
||||
WHERE s.access_jwt = $1
|
||||
"#,
|
||||
token
|
||||
)
|
||||
.fetch_optional(&state.db)
|
||||
.await;
|
||||
|
||||
let (did, key_bytes, user_id) = match session {
|
||||
Ok(Some(row)) => (row.did, row.key_bytes, row.user_id),
|
||||
Ok(None) => {
|
||||
let token = match crate::auth::extract_bearer_token_from_header(
|
||||
headers.get("Authorization").and_then(|h| h.to_str().ok())
|
||||
) {
|
||||
Some(t) => t,
|
||||
None => {
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": "AuthenticationFailed"})),
|
||||
Json(json!({"error": "AuthenticationRequired"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let auth_result = crate::auth::validate_bearer_token(&state.db, &token).await;
|
||||
let did = match auth_result {
|
||||
Ok(user) => user.did,
|
||||
Err(e) => {
|
||||
error!("DB error in get_account_invite_codes: {:?}", e);
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": e})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let user_id = match sqlx::query_scalar!("SELECT id FROM users WHERE did = $1", did)
|
||||
.fetch_optional(&state.db)
|
||||
.await
|
||||
{
|
||||
Ok(Some(id)) => id,
|
||||
_ => {
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({"error": "InternalError"})),
|
||||
@@ -421,14 +381,6 @@ pub async fn get_account_invite_codes(
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(_) = crate::auth::verify_token(&token, &key_bytes) {
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
|
||||
let include_used = params.include_used.unwrap_or(true);
|
||||
|
||||
let codes_result = sqlx::query!(
|
||||
|
||||
@@ -211,7 +211,7 @@ pub async fn reset_password(
|
||||
.into_response();
|
||||
}
|
||||
|
||||
let _ = sqlx::query!("DELETE FROM sessions WHERE did = (SELECT did FROM users WHERE id = $1)", user_id)
|
||||
let _ = sqlx::query!("DELETE FROM session_tokens WHERE did = (SELECT did FROM users WHERE id = $1)", user_id)
|
||||
.execute(&state.db)
|
||||
.await;
|
||||
|
||||
|
||||
+318
-165
@@ -27,60 +27,42 @@ pub async fn get_service_auth(
|
||||
headers: axum::http::HeaderMap,
|
||||
Query(params): Query<GetServiceAuthParams>,
|
||||
) -> Response {
|
||||
let auth_header = headers.get("Authorization");
|
||||
if auth_header.is_none() {
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": "AuthenticationRequired"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
|
||||
let token = auth_header
|
||||
.unwrap()
|
||||
.to_str()
|
||||
.unwrap_or("")
|
||||
.replace("Bearer ", "");
|
||||
|
||||
let session = sqlx::query!(
|
||||
r#"
|
||||
SELECT s.did, k.key_bytes
|
||||
FROM sessions s
|
||||
JOIN users u ON s.did = u.did
|
||||
JOIN user_keys k ON u.id = k.user_id
|
||||
WHERE s.access_jwt = $1
|
||||
"#,
|
||||
token
|
||||
)
|
||||
.fetch_optional(&state.db)
|
||||
.await;
|
||||
|
||||
let (did, key_bytes) = match session {
|
||||
Ok(Some(row)) => (row.did, row.key_bytes),
|
||||
Ok(None) => {
|
||||
let token = match crate::auth::extract_bearer_token_from_header(
|
||||
headers.get("Authorization").and_then(|h| h.to_str().ok())
|
||||
) {
|
||||
Some(t) => t,
|
||||
None => {
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": "AuthenticationFailed"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
Err(e) => {
|
||||
error!("DB error in get_service_auth: {:?}", e);
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({"error": "InternalError"})),
|
||||
Json(json!({"error": "AuthenticationRequired"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(_) = crate::auth::verify_token(&token, &key_bytes) {
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
let auth_result = crate::auth::validate_bearer_token(&state.db, &token).await;
|
||||
let (did, key_bytes) = match auth_result {
|
||||
Ok(user) => {
|
||||
let kb = match user.key_bytes {
|
||||
Some(kb) => kb,
|
||||
None => {
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": "AuthenticationFailed", "message": "OAuth tokens cannot create service auth"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
(user.did, kb)
|
||||
}
|
||||
Err(e) => {
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": e})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let lxm = params.lxm.as_deref().unwrap_or("*");
|
||||
|
||||
@@ -122,7 +104,7 @@ pub async fn create_session(
|
||||
info!("create_session: identifier='{}'", input.identifier);
|
||||
|
||||
let user_row = sqlx::query!(
|
||||
"SELECT u.id, u.did, u.handle, u.password_hash, k.key_bytes FROM users u JOIN user_keys k ON u.id = k.user_id WHERE u.handle = $1 OR u.email = $1",
|
||||
"SELECT u.id, u.did, u.handle, u.password_hash, k.key_bytes, k.encryption_version FROM users u JOIN user_keys k ON u.id = k.user_id WHERE u.handle = $1 OR u.email = $1",
|
||||
input.identifier
|
||||
)
|
||||
.fetch_optional(&state.db)
|
||||
@@ -134,7 +116,17 @@ pub async fn create_session(
|
||||
let stored_hash = &row.password_hash;
|
||||
let did = &row.did;
|
||||
let handle = &row.handle;
|
||||
let key_bytes = &row.key_bytes;
|
||||
let key_bytes = match crate::config::decrypt_key(&row.key_bytes, row.encryption_version) {
|
||||
Ok(k) => k,
|
||||
Err(e) => {
|
||||
error!("Failed to decrypt user key: {:?}", e);
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({"error": "InternalError"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let password_valid = if verify(&input.password, stored_hash).unwrap_or(false) {
|
||||
true
|
||||
@@ -150,8 +142,8 @@ pub async fn create_session(
|
||||
};
|
||||
|
||||
if password_valid {
|
||||
let access_jwt = match crate::auth::create_access_token(&did, &key_bytes) {
|
||||
Ok(t) => t,
|
||||
let access_meta = match crate::auth::create_access_token_with_metadata(did, &key_bytes) {
|
||||
Ok(m) => m,
|
||||
Err(e) => {
|
||||
error!("Failed to create access token: {:?}", e);
|
||||
return (
|
||||
@@ -162,8 +154,8 @@ pub async fn create_session(
|
||||
}
|
||||
};
|
||||
|
||||
let refresh_jwt = match crate::auth::create_refresh_token(&did, &key_bytes) {
|
||||
Ok(t) => t,
|
||||
let refresh_meta = match crate::auth::create_refresh_token_with_metadata(did, &key_bytes) {
|
||||
Ok(m) => m,
|
||||
Err(e) => {
|
||||
error!("Failed to create refresh token: {:?}", e);
|
||||
return (
|
||||
@@ -175,10 +167,12 @@ pub async fn create_session(
|
||||
};
|
||||
|
||||
let session_insert = sqlx::query!(
|
||||
"INSERT INTO sessions (access_jwt, refresh_jwt, did) VALUES ($1, $2, $3)",
|
||||
access_jwt,
|
||||
refresh_jwt,
|
||||
did
|
||||
"INSERT INTO session_tokens (did, access_jti, refresh_jti, access_expires_at, refresh_expires_at) VALUES ($1, $2, $3, $4, $5)",
|
||||
did,
|
||||
access_meta.jti,
|
||||
refresh_meta.jti,
|
||||
access_meta.expires_at,
|
||||
refresh_meta.expires_at
|
||||
)
|
||||
.execute(&state.db)
|
||||
.await;
|
||||
@@ -188,8 +182,8 @@ pub async fn create_session(
|
||||
return (
|
||||
StatusCode::OK,
|
||||
Json(CreateSessionOutput {
|
||||
access_jwt,
|
||||
refresh_jwt,
|
||||
access_jwt: access_meta.token,
|
||||
refresh_jwt: refresh_meta.token,
|
||||
handle: handle.clone(),
|
||||
did: did.clone(),
|
||||
}),
|
||||
@@ -236,45 +230,45 @@ pub async fn get_session(
|
||||
State(state): State<AppState>,
|
||||
headers: axum::http::HeaderMap,
|
||||
) -> Response {
|
||||
let auth_header = headers.get("Authorization");
|
||||
if auth_header.is_none() {
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": "AuthenticationRequired"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
let token = match crate::auth::extract_bearer_token_from_header(
|
||||
headers.get("Authorization").and_then(|h| h.to_str().ok())
|
||||
) {
|
||||
Some(t) => t,
|
||||
None => {
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": "AuthenticationRequired", "message": "Invalid Authorization header format"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let token = auth_header
|
||||
.unwrap()
|
||||
.to_str()
|
||||
.unwrap_or("")
|
||||
.replace("Bearer ", "");
|
||||
let auth_result = crate::auth::validate_bearer_token(&state.db, &token).await;
|
||||
let did = match auth_result {
|
||||
Ok(user) => user.did,
|
||||
Err(e) => {
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": e})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let result = sqlx::query!(
|
||||
r#"
|
||||
SELECT u.handle, u.did, u.email, k.key_bytes
|
||||
FROM sessions s
|
||||
JOIN users u ON s.did = u.did
|
||||
JOIN user_keys k ON u.id = k.user_id
|
||||
WHERE s.access_jwt = $1
|
||||
"#,
|
||||
token
|
||||
let user = sqlx::query!(
|
||||
"SELECT handle, email FROM users WHERE did = $1",
|
||||
did
|
||||
)
|
||||
.fetch_optional(&state.db)
|
||||
.await;
|
||||
|
||||
match result {
|
||||
match user {
|
||||
Ok(Some(row)) => {
|
||||
if let Err(_) = crate::auth::verify_token(&token, &row.key_bytes) {
|
||||
return (StatusCode::UNAUTHORIZED, Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"}))).into_response();
|
||||
}
|
||||
|
||||
return (
|
||||
StatusCode::OK,
|
||||
Json(json!({
|
||||
"handle": row.handle,
|
||||
"did": row.did,
|
||||
"did": did,
|
||||
"email": row.email,
|
||||
"didDoc": {}
|
||||
})),
|
||||
@@ -303,22 +297,71 @@ pub async fn delete_session(
|
||||
State(state): State<AppState>,
|
||||
headers: axum::http::HeaderMap,
|
||||
) -> Response {
|
||||
let auth_header = headers.get("Authorization");
|
||||
if auth_header.is_none() {
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": "AuthenticationRequired"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
let token = match crate::auth::extract_bearer_token_from_header(
|
||||
headers.get("Authorization").and_then(|h| h.to_str().ok())
|
||||
) {
|
||||
Some(t) => t,
|
||||
None => {
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": "AuthenticationRequired"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let token = auth_header
|
||||
.unwrap()
|
||||
.to_str()
|
||||
.unwrap_or("")
|
||||
.replace("Bearer ", "");
|
||||
let jti = match crate::auth::get_did_from_token(&token) {
|
||||
Ok(_) => {
|
||||
let parts: Vec<&str> = token.split('.').collect();
|
||||
if parts.len() != 3 {
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": "AuthenticationFailed"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
|
||||
let claims_json = match URL_SAFE_NO_PAD.decode(parts[1]) {
|
||||
Ok(bytes) => bytes,
|
||||
Err(_) => {
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": "AuthenticationFailed"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
let claims: serde_json::Value = match serde_json::from_slice(&claims_json) {
|
||||
Ok(c) => c,
|
||||
Err(_) => {
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": "AuthenticationFailed"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
match claims.get("jti").and_then(|j| j.as_str()) {
|
||||
Some(jti) => jti.to_string(),
|
||||
None => {
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": "AuthenticationFailed"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": "AuthenticationFailed"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let result = sqlx::query!("DELETE FROM sessions WHERE access_jwt = $1", token)
|
||||
let result = sqlx::query!("DELETE FROM session_tokens WHERE access_jti = $1", jti)
|
||||
.execute(&state.db)
|
||||
.await;
|
||||
|
||||
@@ -344,39 +387,114 @@ pub async fn refresh_session(
|
||||
State(state): State<AppState>,
|
||||
headers: axum::http::HeaderMap,
|
||||
) -> Response {
|
||||
let auth_header = headers.get("Authorization");
|
||||
if auth_header.is_none() {
|
||||
use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
|
||||
|
||||
let refresh_token = match crate::auth::extract_bearer_token_from_header(
|
||||
headers.get("Authorization").and_then(|h| h.to_str().ok())
|
||||
) {
|
||||
Some(t) => t,
|
||||
None => {
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": "AuthenticationRequired"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let refresh_jti = {
|
||||
let parts: Vec<&str> = refresh_token.split('.').collect();
|
||||
if parts.len() != 3 {
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": "AuthenticationFailed", "message": "Invalid token format"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
let claims_bytes = match URL_SAFE_NO_PAD.decode(parts[1]) {
|
||||
Ok(b) => b,
|
||||
Err(_) => {
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": "AuthenticationFailed"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
let claims: serde_json::Value = match serde_json::from_slice(&claims_bytes) {
|
||||
Ok(c) => c,
|
||||
Err(_) => {
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": "AuthenticationFailed"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
match claims.get("jti").and_then(|j| j.as_str()) {
|
||||
Some(jti) => jti.to_string(),
|
||||
None => {
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": "AuthenticationFailed"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let reuse_check = sqlx::query_scalar!(
|
||||
"SELECT session_id FROM used_refresh_tokens WHERE refresh_jti = $1",
|
||||
refresh_jti
|
||||
)
|
||||
.fetch_optional(&state.db)
|
||||
.await;
|
||||
|
||||
if let Ok(Some(session_id)) = reuse_check {
|
||||
warn!("Refresh token reuse detected! Revoking token family for session_id: {}", session_id);
|
||||
let _ = sqlx::query!("DELETE FROM session_tokens WHERE id = $1", session_id)
|
||||
.execute(&state.db)
|
||||
.await;
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": "AuthenticationRequired"})),
|
||||
Json(json!({"error": "ExpiredToken", "message": "Refresh token has been revoked due to suspected compromise"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
|
||||
let refresh_token = auth_header
|
||||
.unwrap()
|
||||
.to_str()
|
||||
.unwrap_or("")
|
||||
.replace("Bearer ", "");
|
||||
|
||||
let session = sqlx::query!(
|
||||
"SELECT s.did, k.key_bytes FROM sessions s JOIN users u ON s.did = u.did JOIN user_keys k ON u.id = k.user_id WHERE s.refresh_jwt = $1",
|
||||
refresh_token
|
||||
)
|
||||
.fetch_optional(&state.db)
|
||||
.await;
|
||||
r#"SELECT st.id, st.did, k.key_bytes, k.encryption_version
|
||||
FROM session_tokens st
|
||||
JOIN users u ON st.did = u.did
|
||||
JOIN user_keys k ON u.id = k.user_id
|
||||
WHERE st.refresh_jti = $1 AND st.refresh_expires_at > NOW()"#,
|
||||
refresh_jti
|
||||
)
|
||||
.fetch_optional(&state.db)
|
||||
.await;
|
||||
|
||||
match session {
|
||||
Ok(Some(session_row)) => {
|
||||
let session_id = session_row.id;
|
||||
let did = &session_row.did;
|
||||
let key_bytes = &session_row.key_bytes;
|
||||
let key_bytes = match crate::config::decrypt_key(&session_row.key_bytes, session_row.encryption_version) {
|
||||
Ok(k) => k,
|
||||
Err(e) => {
|
||||
error!("Failed to decrypt user key: {:?}", e);
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({"error": "InternalError"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(_) = crate::auth::verify_token(&refresh_token, &key_bytes) {
|
||||
return (StatusCode::UNAUTHORIZED, Json(json!({"error": "AuthenticationFailed", "message": "Invalid refresh token signature"}))).into_response();
|
||||
if let Err(_) = crate::auth::verify_refresh_token(&refresh_token, &key_bytes) {
|
||||
return (StatusCode::UNAUTHORIZED, Json(json!({"error": "AuthenticationFailed", "message": "Invalid refresh token"}))).into_response();
|
||||
}
|
||||
|
||||
let new_access_jwt = match crate::auth::create_access_token(&did, &key_bytes) {
|
||||
Ok(t) => t,
|
||||
let new_access_meta = match crate::auth::create_access_token_with_metadata(did, &key_bytes) {
|
||||
Ok(m) => m,
|
||||
Err(e) => {
|
||||
error!("Failed to create access token: {:?}", e);
|
||||
return (
|
||||
@@ -386,8 +504,8 @@ pub async fn refresh_session(
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
let new_refresh_jwt = match crate::auth::create_refresh_token(&did, &key_bytes) {
|
||||
Ok(t) => t,
|
||||
let new_refresh_meta = match crate::auth::create_refresh_token_with_metadata(did, &key_bytes) {
|
||||
Ok(m) => m,
|
||||
Err(e) => {
|
||||
error!("Failed to create refresh token: {:?}", e);
|
||||
return (
|
||||
@@ -398,54 +516,89 @@ pub async fn refresh_session(
|
||||
}
|
||||
};
|
||||
|
||||
let update = sqlx::query!(
|
||||
"UPDATE sessions SET access_jwt = $1, refresh_jwt = $2 WHERE refresh_jwt = $3",
|
||||
new_access_jwt,
|
||||
new_refresh_jwt,
|
||||
refresh_token
|
||||
let mut tx = match state.db.begin().await {
|
||||
Ok(tx) => tx,
|
||||
Err(e) => {
|
||||
error!("Failed to begin transaction: {:?}", e);
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({"error": "InternalError"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(e) = sqlx::query!(
|
||||
"INSERT INTO used_refresh_tokens (refresh_jti, session_id) VALUES ($1, $2)",
|
||||
refresh_jti,
|
||||
session_id
|
||||
)
|
||||
.execute(&state.db)
|
||||
.await;
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
{
|
||||
error!("Failed to record used refresh token: {:?}", e);
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({"error": "InternalError"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
|
||||
match update {
|
||||
Ok(_) => {
|
||||
let user = sqlx::query!("SELECT handle FROM users WHERE did = $1", did)
|
||||
.fetch_optional(&state.db)
|
||||
.await;
|
||||
if let Err(e) = sqlx::query!(
|
||||
"UPDATE session_tokens SET access_jti = $1, refresh_jti = $2, access_expires_at = $3, refresh_expires_at = $4, updated_at = NOW() WHERE id = $5",
|
||||
new_access_meta.jti,
|
||||
new_refresh_meta.jti,
|
||||
new_access_meta.expires_at,
|
||||
new_refresh_meta.expires_at,
|
||||
session_id
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
{
|
||||
error!("Database error updating session: {:?}", e);
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({"error": "InternalError"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
|
||||
match user {
|
||||
Ok(Some(u)) => {
|
||||
return (
|
||||
StatusCode::OK,
|
||||
Json(json!({
|
||||
"accessJwt": new_access_jwt,
|
||||
"refreshJwt": new_refresh_jwt,
|
||||
"handle": u.handle,
|
||||
"did": did
|
||||
})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
Ok(None) => {
|
||||
error!("User not found for existing session: {}", did);
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({"error": "InternalError"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Database error fetching user: {:?}", e);
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({"error": "InternalError"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
}
|
||||
if let Err(e) = tx.commit().await {
|
||||
error!("Failed to commit transaction: {:?}", e);
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({"error": "InternalError"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
|
||||
let user = sqlx::query!("SELECT handle FROM users WHERE did = $1", did)
|
||||
.fetch_optional(&state.db)
|
||||
.await;
|
||||
|
||||
match user {
|
||||
Ok(Some(u)) => {
|
||||
return (
|
||||
StatusCode::OK,
|
||||
Json(json!({
|
||||
"accessJwt": new_access_meta.token,
|
||||
"refreshJwt": new_refresh_meta.token,
|
||||
"handle": u.handle,
|
||||
"did": did
|
||||
})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
Ok(None) => {
|
||||
error!("User not found for existing session: {}", did);
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({"error": "InternalError"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Database error updating session: {:?}", e);
|
||||
error!("Database error fetching user: {:?}", e);
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({"error": "InternalError"})),
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
use axum::{
|
||||
extract::FromRequestParts,
|
||||
http::{StatusCode, request::Parts, header::AUTHORIZATION},
|
||||
response::{IntoResponse, Response},
|
||||
Json,
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
use crate::state::AppState;
|
||||
use super::{AuthenticatedUser, validate_bearer_token};
|
||||
|
||||
pub struct BearerAuth(pub AuthenticatedUser);
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum AuthError {
|
||||
MissingToken,
|
||||
InvalidFormat,
|
||||
AuthenticationFailed,
|
||||
AccountDeactivated,
|
||||
AccountTakedown,
|
||||
}
|
||||
|
||||
impl IntoResponse for AuthError {
|
||||
fn into_response(self) -> Response {
|
||||
let (status, error, message) = match self {
|
||||
AuthError::MissingToken => (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
"AuthenticationRequired",
|
||||
"Authorization header is required",
|
||||
),
|
||||
AuthError::InvalidFormat => (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
"InvalidToken",
|
||||
"Invalid authorization header format",
|
||||
),
|
||||
AuthError::AuthenticationFailed => (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
"AuthenticationFailed",
|
||||
"Invalid or expired token",
|
||||
),
|
||||
AuthError::AccountDeactivated => (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
"AccountDeactivated",
|
||||
"Account is deactivated",
|
||||
),
|
||||
AuthError::AccountTakedown => (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
"AccountTakedown",
|
||||
"Account has been taken down",
|
||||
),
|
||||
};
|
||||
|
||||
(status, Json(json!({ "error": error, "message": message }))).into_response()
|
||||
}
|
||||
}
|
||||
|
||||
fn extract_bearer_token(auth_header: &str) -> Result<&str, AuthError> {
|
||||
let auth_header = auth_header.trim();
|
||||
|
||||
if auth_header.len() < 8 {
|
||||
return Err(AuthError::InvalidFormat);
|
||||
}
|
||||
|
||||
let prefix = &auth_header[..7];
|
||||
if !prefix.eq_ignore_ascii_case("bearer ") {
|
||||
return Err(AuthError::InvalidFormat);
|
||||
}
|
||||
|
||||
let token = auth_header[7..].trim();
|
||||
if token.is_empty() {
|
||||
return Err(AuthError::InvalidFormat);
|
||||
}
|
||||
|
||||
Ok(token)
|
||||
}
|
||||
|
||||
pub fn extract_bearer_token_from_header(auth_header: Option<&str>) -> Option<String> {
|
||||
let header = auth_header?;
|
||||
let header = header.trim();
|
||||
|
||||
if header.len() < 7 {
|
||||
return None;
|
||||
}
|
||||
|
||||
if !header[..7].eq_ignore_ascii_case("bearer ") {
|
||||
return None;
|
||||
}
|
||||
|
||||
let token = header[7..].trim();
|
||||
if token.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(token.to_string())
|
||||
}
|
||||
|
||||
impl FromRequestParts<AppState> for BearerAuth {
|
||||
type Rejection = AuthError;
|
||||
|
||||
async fn from_request_parts(
|
||||
parts: &mut Parts,
|
||||
state: &AppState,
|
||||
) -> Result<Self, Self::Rejection> {
|
||||
let auth_header = parts
|
||||
.headers
|
||||
.get(AUTHORIZATION)
|
||||
.ok_or(AuthError::MissingToken)?
|
||||
.to_str()
|
||||
.map_err(|_| AuthError::InvalidFormat)?;
|
||||
|
||||
let token = extract_bearer_token(auth_header)?;
|
||||
|
||||
match validate_bearer_token(&state.db, token).await {
|
||||
Ok(user) => Ok(BearerAuth(user)),
|
||||
Err("AccountDeactivated") => Err(AuthError::AccountDeactivated),
|
||||
Err("AccountTakedown") => Err(AuthError::AccountTakedown),
|
||||
Err(_) => Err(AuthError::AuthenticationFailed),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_extract_bearer_token() {
|
||||
assert_eq!(extract_bearer_token("Bearer abc123").unwrap(), "abc123");
|
||||
assert_eq!(extract_bearer_token("bearer abc123").unwrap(), "abc123");
|
||||
assert_eq!(extract_bearer_token("BEARER abc123").unwrap(), "abc123");
|
||||
assert_eq!(extract_bearer_token("Bearer abc123").unwrap(), "abc123");
|
||||
assert_eq!(extract_bearer_token(" Bearer abc123 ").unwrap(), "abc123");
|
||||
|
||||
assert!(extract_bearer_token("Basic abc123").is_err());
|
||||
assert!(extract_bearer_token("Bearer").is_err());
|
||||
assert!(extract_bearer_token("Bearer ").is_err());
|
||||
assert!(extract_bearer_token("abc123").is_err());
|
||||
assert!(extract_bearer_token("").is_err());
|
||||
}
|
||||
}
|
||||
+119
-2
@@ -1,10 +1,127 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::PgPool;
|
||||
|
||||
pub mod extractor;
|
||||
pub mod token;
|
||||
pub mod verify;
|
||||
|
||||
pub use token::{create_access_token, create_refresh_token, create_service_token};
|
||||
pub use verify::{get_did_from_token, verify_token};
|
||||
pub use extractor::{BearerAuth, AuthError, extract_bearer_token_from_header};
|
||||
pub use token::{
|
||||
create_access_token, create_refresh_token, create_service_token,
|
||||
create_access_token_with_metadata, create_refresh_token_with_metadata,
|
||||
TokenWithMetadata,
|
||||
TOKEN_TYPE_ACCESS, TOKEN_TYPE_REFRESH, TOKEN_TYPE_SERVICE,
|
||||
SCOPE_ACCESS, SCOPE_REFRESH, SCOPE_APP_PASS, SCOPE_APP_PASS_PRIVILEGED,
|
||||
};
|
||||
pub use verify::{get_did_from_token, get_jti_from_token, verify_token, verify_access_token, verify_refresh_token};
|
||||
|
||||
pub struct AuthenticatedUser {
|
||||
pub did: String,
|
||||
pub key_bytes: Option<Vec<u8>>,
|
||||
pub is_oauth: bool,
|
||||
}
|
||||
|
||||
pub async fn validate_bearer_token(
|
||||
db: &PgPool,
|
||||
token: &str,
|
||||
) -> Result<AuthenticatedUser, &'static str> {
|
||||
validate_bearer_token_with_options(db, token, false).await
|
||||
}
|
||||
|
||||
pub async fn validate_bearer_token_allow_deactivated(
|
||||
db: &PgPool,
|
||||
token: &str,
|
||||
) -> Result<AuthenticatedUser, &'static str> {
|
||||
validate_bearer_token_with_options(db, token, true).await
|
||||
}
|
||||
|
||||
async fn validate_bearer_token_with_options(
|
||||
db: &PgPool,
|
||||
token: &str,
|
||||
allow_deactivated: bool,
|
||||
) -> Result<AuthenticatedUser, &'static str> {
|
||||
let did_from_token = get_did_from_token(token).ok();
|
||||
|
||||
if let Some(ref did) = did_from_token {
|
||||
if let Some(user) = sqlx::query!(
|
||||
"SELECT k.key_bytes, k.encryption_version, u.deactivated_at, u.takedown_ref
|
||||
FROM users u
|
||||
JOIN user_keys k ON u.id = k.user_id
|
||||
WHERE u.did = $1",
|
||||
did
|
||||
)
|
||||
.fetch_optional(db)
|
||||
.await
|
||||
.ok()
|
||||
.flatten()
|
||||
{
|
||||
if !allow_deactivated && user.deactivated_at.is_some() {
|
||||
return Err("AccountDeactivated");
|
||||
}
|
||||
if user.takedown_ref.is_some() {
|
||||
return Err("AccountTakedown");
|
||||
}
|
||||
|
||||
let decrypted_key = match crate::config::decrypt_key(&user.key_bytes, user.encryption_version) {
|
||||
Ok(k) => k,
|
||||
Err(_) => return Err("KeyDecryptionFailed"),
|
||||
};
|
||||
|
||||
if let Ok(token_data) = verify_access_token(token, &decrypted_key) {
|
||||
let session_exists = sqlx::query_scalar!(
|
||||
"SELECT 1 as one FROM session_tokens WHERE did = $1 AND access_jti = $2 AND access_expires_at > NOW()",
|
||||
did,
|
||||
token_data.claims.jti
|
||||
)
|
||||
.fetch_optional(db)
|
||||
.await
|
||||
.ok()
|
||||
.flatten();
|
||||
|
||||
if session_exists.is_some() {
|
||||
return Ok(AuthenticatedUser {
|
||||
did: did.clone(),
|
||||
key_bytes: Some(decrypted_key),
|
||||
is_oauth: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(oauth_info) = crate::oauth::verify::extract_oauth_token_info(token) {
|
||||
if let Some(oauth_token) = sqlx::query!(
|
||||
r#"SELECT t.did, t.expires_at, u.deactivated_at, u.takedown_ref
|
||||
FROM oauth_token t
|
||||
JOIN users u ON t.did = u.did
|
||||
WHERE t.token_id = $1"#,
|
||||
oauth_info.token_id
|
||||
)
|
||||
.fetch_optional(db)
|
||||
.await
|
||||
.ok()
|
||||
.flatten()
|
||||
{
|
||||
if !allow_deactivated && oauth_token.deactivated_at.is_some() {
|
||||
return Err("AccountDeactivated");
|
||||
}
|
||||
if oauth_token.takedown_ref.is_some() {
|
||||
return Err("AccountTakedown");
|
||||
}
|
||||
|
||||
let now = chrono::Utc::now();
|
||||
if oauth_token.expires_at > now {
|
||||
return Ok(AuthenticatedUser {
|
||||
did: oauth_token.did,
|
||||
key_bytes: None,
|
||||
is_oauth: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err("AuthenticationFailed")
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Claims {
|
||||
|
||||
+45
-11
@@ -2,16 +2,39 @@ use super::{Claims, Header};
|
||||
use anyhow::Result;
|
||||
use base64::Engine as _;
|
||||
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
|
||||
use chrono::{Duration, Utc};
|
||||
use chrono::{DateTime, Duration, Utc};
|
||||
use k256::ecdsa::{Signature, SigningKey, signature::Signer};
|
||||
use uuid;
|
||||
|
||||
pub const TOKEN_TYPE_ACCESS: &str = "at+jwt";
|
||||
pub const TOKEN_TYPE_REFRESH: &str = "refresh+jwt";
|
||||
pub const TOKEN_TYPE_SERVICE: &str = "jwt";
|
||||
|
||||
pub const SCOPE_ACCESS: &str = "com.atproto.access";
|
||||
pub const SCOPE_REFRESH: &str = "com.atproto.refresh";
|
||||
pub const SCOPE_APP_PASS: &str = "com.atproto.appPass";
|
||||
pub const SCOPE_APP_PASS_PRIVILEGED: &str = "com.atproto.appPassPrivileged";
|
||||
|
||||
pub struct TokenWithMetadata {
|
||||
pub token: String,
|
||||
pub jti: String,
|
||||
pub expires_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
pub fn create_access_token(did: &str, key_bytes: &[u8]) -> Result<String> {
|
||||
create_signed_token(did, "access", key_bytes, Duration::minutes(15))
|
||||
Ok(create_access_token_with_metadata(did, key_bytes)?.token)
|
||||
}
|
||||
|
||||
pub fn create_refresh_token(did: &str, key_bytes: &[u8]) -> Result<String> {
|
||||
create_signed_token(did, "refresh", key_bytes, Duration::days(7))
|
||||
Ok(create_refresh_token_with_metadata(did, key_bytes)?.token)
|
||||
}
|
||||
|
||||
pub fn create_access_token_with_metadata(did: &str, key_bytes: &[u8]) -> Result<TokenWithMetadata> {
|
||||
create_signed_token_with_metadata(did, SCOPE_ACCESS, TOKEN_TYPE_ACCESS, key_bytes, Duration::minutes(120))
|
||||
}
|
||||
|
||||
pub fn create_refresh_token_with_metadata(did: &str, key_bytes: &[u8]) -> Result<TokenWithMetadata> {
|
||||
create_signed_token_with_metadata(did, SCOPE_REFRESH, TOKEN_TYPE_REFRESH, key_bytes, Duration::days(90))
|
||||
}
|
||||
|
||||
pub fn create_service_token(did: &str, aud: &str, lxm: &str, key_bytes: &[u8]) -> Result<String> {
|
||||
@@ -36,18 +59,20 @@ pub fn create_service_token(did: &str, aud: &str, lxm: &str, key_bytes: &[u8]) -
|
||||
sign_claims(claims, &signing_key)
|
||||
}
|
||||
|
||||
fn create_signed_token(
|
||||
fn create_signed_token_with_metadata(
|
||||
did: &str,
|
||||
scope: &str,
|
||||
typ: &str,
|
||||
key_bytes: &[u8],
|
||||
duration: Duration,
|
||||
) -> Result<String> {
|
||||
) -> Result<TokenWithMetadata> {
|
||||
let signing_key = SigningKey::from_slice(key_bytes)?;
|
||||
|
||||
let expiration = Utc::now()
|
||||
let expires_at = Utc::now()
|
||||
.checked_add_signed(duration)
|
||||
.expect("valid timestamp")
|
||||
.timestamp();
|
||||
.expect("valid timestamp");
|
||||
let expiration = expires_at.timestamp();
|
||||
let jti = uuid::Uuid::new_v4().to_string();
|
||||
|
||||
let claims = Claims {
|
||||
iss: did.to_owned(),
|
||||
@@ -60,16 +85,25 @@ fn create_signed_token(
|
||||
iat: Utc::now().timestamp() as usize,
|
||||
scope: Some(scope.to_string()),
|
||||
lxm: None,
|
||||
jti: uuid::Uuid::new_v4().to_string(),
|
||||
jti: jti.clone(),
|
||||
};
|
||||
|
||||
sign_claims(claims, &signing_key)
|
||||
let token = sign_claims_with_type(claims, &signing_key, typ)?;
|
||||
Ok(TokenWithMetadata {
|
||||
token,
|
||||
jti,
|
||||
expires_at,
|
||||
})
|
||||
}
|
||||
|
||||
fn sign_claims(claims: Claims, key: &SigningKey) -> Result<String> {
|
||||
sign_claims_with_type(claims, key, TOKEN_TYPE_SERVICE)
|
||||
}
|
||||
|
||||
fn sign_claims_with_type(claims: Claims, key: &SigningKey, typ: &str) -> Result<String> {
|
||||
let header = Header {
|
||||
alg: "ES256K".to_string(),
|
||||
typ: "JWT".to_string(),
|
||||
typ: typ.to_string(),
|
||||
};
|
||||
|
||||
let header_json = serde_json::to_string(&header)?;
|
||||
|
||||
+67
-1
@@ -1,4 +1,5 @@
|
||||
use super::{Claims, TokenData, UnsafeClaims};
|
||||
use super::{Claims, Header, TokenData, UnsafeClaims};
|
||||
use super::token::{TOKEN_TYPE_ACCESS, TOKEN_TYPE_REFRESH, SCOPE_ACCESS, SCOPE_REFRESH, SCOPE_APP_PASS, SCOPE_APP_PASS_PRIVILEGED};
|
||||
use anyhow::{Context, Result, anyhow};
|
||||
use base64::Engine as _;
|
||||
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
|
||||
@@ -21,7 +22,53 @@ pub fn get_did_from_token(token: &str) -> Result<String, String> {
|
||||
Ok(claims.sub.unwrap_or(claims.iss))
|
||||
}
|
||||
|
||||
pub fn get_jti_from_token(token: &str) -> Result<String, String> {
|
||||
let parts: Vec<&str> = token.split('.').collect();
|
||||
if parts.len() != 3 {
|
||||
return Err("Invalid token format".to_string());
|
||||
}
|
||||
|
||||
let payload_bytes = URL_SAFE_NO_PAD
|
||||
.decode(parts[1])
|
||||
.map_err(|e| format!("Base64 decode failed: {}", e))?;
|
||||
|
||||
let claims: serde_json::Value =
|
||||
serde_json::from_slice(&payload_bytes).map_err(|e| format!("JSON decode failed: {}", e))?;
|
||||
|
||||
claims.get("jti")
|
||||
.and_then(|j| j.as_str())
|
||||
.map(|s| s.to_string())
|
||||
.ok_or_else(|| "No jti claim in token".to_string())
|
||||
}
|
||||
|
||||
pub fn verify_token(token: &str, key_bytes: &[u8]) -> Result<TokenData<Claims>> {
|
||||
verify_token_internal(token, key_bytes, None, None)
|
||||
}
|
||||
|
||||
pub fn verify_access_token(token: &str, key_bytes: &[u8]) -> Result<TokenData<Claims>> {
|
||||
verify_token_internal(
|
||||
token,
|
||||
key_bytes,
|
||||
Some(TOKEN_TYPE_ACCESS),
|
||||
Some(&[SCOPE_ACCESS, SCOPE_APP_PASS, SCOPE_APP_PASS_PRIVILEGED]),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn verify_refresh_token(token: &str, key_bytes: &[u8]) -> Result<TokenData<Claims>> {
|
||||
verify_token_internal(
|
||||
token,
|
||||
key_bytes,
|
||||
Some(TOKEN_TYPE_REFRESH),
|
||||
Some(&[SCOPE_REFRESH]),
|
||||
)
|
||||
}
|
||||
|
||||
fn verify_token_internal(
|
||||
token: &str,
|
||||
key_bytes: &[u8],
|
||||
expected_typ: Option<&str>,
|
||||
allowed_scopes: Option<&[&str]>,
|
||||
) -> Result<TokenData<Claims>> {
|
||||
let parts: Vec<&str> = token.split('.').collect();
|
||||
if parts.len() != 3 {
|
||||
return Err(anyhow!("Invalid token format"));
|
||||
@@ -31,6 +78,18 @@ pub fn verify_token(token: &str, key_bytes: &[u8]) -> Result<TokenData<Claims>>
|
||||
let claims_b64 = parts[1];
|
||||
let signature_b64 = parts[2];
|
||||
|
||||
let header_bytes = URL_SAFE_NO_PAD
|
||||
.decode(header_b64)
|
||||
.context("Base64 decode of header failed")?;
|
||||
let header: Header =
|
||||
serde_json::from_slice(&header_bytes).context("JSON decode of header failed")?;
|
||||
|
||||
if let Some(expected) = expected_typ {
|
||||
if header.typ != expected {
|
||||
return Err(anyhow!("Invalid token type: expected {}, got {}", expected, header.typ));
|
||||
}
|
||||
}
|
||||
|
||||
let signature_bytes = URL_SAFE_NO_PAD
|
||||
.decode(signature_b64)
|
||||
.context("Base64 decode of signature failed")?;
|
||||
@@ -56,5 +115,12 @@ pub fn verify_token(token: &str, key_bytes: &[u8]) -> Result<TokenData<Claims>>
|
||||
return Err(anyhow!("Token expired"));
|
||||
}
|
||||
|
||||
if let Some(scopes) = allowed_scopes {
|
||||
let token_scope = claims.scope.as_deref().unwrap_or("");
|
||||
if !scopes.contains(&token_scope) {
|
||||
return Err(anyhow!("Invalid token scope: {}", token_scope));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(TokenData { claims })
|
||||
}
|
||||
|
||||
+170
@@ -0,0 +1,170 @@
|
||||
use aes_gcm::{
|
||||
Aes256Gcm, KeyInit, Nonce,
|
||||
aead::Aead,
|
||||
};
|
||||
use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
|
||||
use hkdf::Hkdf;
|
||||
use p256::ecdsa::SigningKey;
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::sync::OnceLock;
|
||||
|
||||
static CONFIG: OnceLock<AuthConfig> = OnceLock::new();
|
||||
|
||||
pub const ENCRYPTION_VERSION: i32 = 1;
|
||||
|
||||
pub struct AuthConfig {
|
||||
jwt_secret: String,
|
||||
dpop_secret: String,
|
||||
#[allow(dead_code)]
|
||||
signing_key: SigningKey,
|
||||
pub signing_key_id: String,
|
||||
pub signing_key_x: String,
|
||||
pub signing_key_y: String,
|
||||
key_encryption_key: [u8; 32],
|
||||
}
|
||||
|
||||
impl AuthConfig {
|
||||
pub fn init() -> &'static Self {
|
||||
CONFIG.get_or_init(|| {
|
||||
let jwt_secret = std::env::var("JWT_SECRET").unwrap_or_else(|_| {
|
||||
if cfg!(test) || std::env::var("BSPDS_ALLOW_INSECURE_SECRETS").is_ok() {
|
||||
"test-jwt-secret-not-for-production".to_string()
|
||||
} else {
|
||||
panic!(
|
||||
"JWT_SECRET environment variable must be set in production. \
|
||||
Set BSPDS_ALLOW_INSECURE_SECRETS=1 for development/testing."
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
let dpop_secret = std::env::var("DPOP_SECRET").unwrap_or_else(|_| {
|
||||
if cfg!(test) || std::env::var("BSPDS_ALLOW_INSECURE_SECRETS").is_ok() {
|
||||
"test-dpop-secret-not-for-production".to_string()
|
||||
} else {
|
||||
panic!(
|
||||
"DPOP_SECRET environment variable must be set in production. \
|
||||
Set BSPDS_ALLOW_INSECURE_SECRETS=1 for development/testing."
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
if jwt_secret.len() < 32 && std::env::var("BSPDS_ALLOW_INSECURE_SECRETS").is_err() {
|
||||
panic!("JWT_SECRET must be at least 32 characters");
|
||||
}
|
||||
if dpop_secret.len() < 32 && std::env::var("BSPDS_ALLOW_INSECURE_SECRETS").is_err() {
|
||||
panic!("DPOP_SECRET must be at least 32 characters");
|
||||
}
|
||||
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(b"oauth-signing-key-derivation:");
|
||||
hasher.update(jwt_secret.as_bytes());
|
||||
let seed = hasher.finalize();
|
||||
|
||||
let signing_key = SigningKey::from_slice(&seed)
|
||||
.expect("Failed to create signing key from seed");
|
||||
|
||||
let verifying_key = signing_key.verifying_key();
|
||||
let point = verifying_key.to_encoded_point(false);
|
||||
|
||||
let signing_key_x = URL_SAFE_NO_PAD.encode(point.x().unwrap());
|
||||
let signing_key_y = URL_SAFE_NO_PAD.encode(point.y().unwrap());
|
||||
|
||||
let mut kid_hasher = Sha256::new();
|
||||
kid_hasher.update(signing_key_x.as_bytes());
|
||||
kid_hasher.update(signing_key_y.as_bytes());
|
||||
let kid_hash = kid_hasher.finalize();
|
||||
let signing_key_id = URL_SAFE_NO_PAD.encode(&kid_hash[..8]);
|
||||
|
||||
let master_key = std::env::var("MASTER_KEY").unwrap_or_else(|_| {
|
||||
if cfg!(test) || std::env::var("BSPDS_ALLOW_INSECURE_SECRETS").is_ok() {
|
||||
"test-master-key-not-for-production".to_string()
|
||||
} else {
|
||||
panic!(
|
||||
"MASTER_KEY environment variable must be set in production. \
|
||||
Set BSPDS_ALLOW_INSECURE_SECRETS=1 for development/testing."
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
if master_key.len() < 32 && std::env::var("BSPDS_ALLOW_INSECURE_SECRETS").is_err() {
|
||||
panic!("MASTER_KEY must be at least 32 characters");
|
||||
}
|
||||
|
||||
let hk = Hkdf::<Sha256>::new(None, master_key.as_bytes());
|
||||
let mut key_encryption_key = [0u8; 32];
|
||||
hk.expand(b"bspds-user-key-encryption", &mut key_encryption_key)
|
||||
.expect("HKDF expansion failed");
|
||||
|
||||
AuthConfig {
|
||||
jwt_secret,
|
||||
dpop_secret,
|
||||
signing_key,
|
||||
signing_key_id,
|
||||
signing_key_x,
|
||||
signing_key_y,
|
||||
key_encryption_key,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get() -> &'static Self {
|
||||
CONFIG.get().expect("AuthConfig not initialized - call AuthConfig::init() first")
|
||||
}
|
||||
|
||||
pub fn jwt_secret(&self) -> &str {
|
||||
&self.jwt_secret
|
||||
}
|
||||
|
||||
pub fn dpop_secret(&self) -> &str {
|
||||
&self.dpop_secret
|
||||
}
|
||||
|
||||
pub fn encrypt_user_key(&self, plaintext: &[u8]) -> Result<Vec<u8>, String> {
|
||||
use rand::RngCore;
|
||||
|
||||
let cipher = Aes256Gcm::new_from_slice(&self.key_encryption_key)
|
||||
.map_err(|e| format!("Failed to create cipher: {}", e))?;
|
||||
|
||||
let mut nonce_bytes = [0u8; 12];
|
||||
rand::thread_rng().fill_bytes(&mut nonce_bytes);
|
||||
let nonce = Nonce::from_slice(&nonce_bytes);
|
||||
|
||||
let ciphertext = cipher
|
||||
.encrypt(nonce, plaintext)
|
||||
.map_err(|e| format!("Encryption failed: {}", e))?;
|
||||
|
||||
let mut result = Vec::with_capacity(12 + ciphertext.len());
|
||||
result.extend_from_slice(&nonce_bytes);
|
||||
result.extend_from_slice(&ciphertext);
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub fn decrypt_user_key(&self, encrypted: &[u8]) -> Result<Vec<u8>, String> {
|
||||
if encrypted.len() < 12 {
|
||||
return Err("Encrypted data too short".to_string());
|
||||
}
|
||||
|
||||
let cipher = Aes256Gcm::new_from_slice(&self.key_encryption_key)
|
||||
.map_err(|e| format!("Failed to create cipher: {}", e))?;
|
||||
|
||||
let nonce = Nonce::from_slice(&encrypted[..12]);
|
||||
let ciphertext = &encrypted[12..];
|
||||
|
||||
cipher
|
||||
.decrypt(nonce, ciphertext)
|
||||
.map_err(|e| format!("Decryption failed: {}", e))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn encrypt_key(plaintext: &[u8]) -> Result<Vec<u8>, String> {
|
||||
AuthConfig::get().encrypt_user_key(plaintext)
|
||||
}
|
||||
|
||||
pub fn decrypt_key(encrypted: &[u8], version: Option<i32>) -> Result<Vec<u8>, String> {
|
||||
match version.unwrap_or(0) {
|
||||
0 => Ok(encrypted.to_vec()),
|
||||
1 => AuthConfig::get().decrypt_user_key(encrypted),
|
||||
v => Err(format!("Unknown encryption version: {}", v)),
|
||||
}
|
||||
}
|
||||
+21
@@ -1,6 +1,8 @@
|
||||
pub mod api;
|
||||
pub mod auth;
|
||||
pub mod config;
|
||||
pub mod notifications;
|
||||
pub mod oauth;
|
||||
pub mod repo;
|
||||
pub mod state;
|
||||
pub mod storage;
|
||||
@@ -267,6 +269,25 @@ pub fn app(state: AppState) -> Router {
|
||||
)
|
||||
.route("/.well-known/did.json", get(api::identity::well_known_did))
|
||||
.route("/u/{handle}/did.json", get(api::identity::user_did_doc))
|
||||
// OAuth 2.1 endpoints
|
||||
.route(
|
||||
"/.well-known/oauth-protected-resource",
|
||||
get(oauth::endpoints::oauth_protected_resource),
|
||||
)
|
||||
.route(
|
||||
"/.well-known/oauth-authorization-server",
|
||||
get(oauth::endpoints::oauth_authorization_server),
|
||||
)
|
||||
.route("/oauth/jwks", get(oauth::endpoints::oauth_jwks))
|
||||
.route(
|
||||
"/oauth/par",
|
||||
post(oauth::endpoints::pushed_authorization_request),
|
||||
)
|
||||
.route("/oauth/authorize", get(oauth::endpoints::authorize_get))
|
||||
.route("/oauth/authorize", post(oauth::endpoints::authorize_post))
|
||||
.route("/oauth/token", post(oauth::endpoints::token_endpoint))
|
||||
.route("/oauth/revoke", post(oauth::endpoints::revoke_token))
|
||||
.route("/oauth/introspect", post(oauth::endpoints::introspect_token))
|
||||
.route("/xrpc/{*method}", any(api::proxy::proxy_handler))
|
||||
.with_state(state)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,365 @@
|
||||
use reqwest::Client;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use super::OAuthError;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ClientMetadata {
|
||||
pub client_id: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub client_name: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub client_uri: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub logo_uri: Option<String>,
|
||||
pub redirect_uris: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub grant_types: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub response_types: Vec<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub scope: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub token_endpoint_auth_method: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub dpop_bound_access_tokens: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub jwks: Option<serde_json::Value>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub jwks_uri: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub application_type: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for ClientMetadata {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
client_id: String::new(),
|
||||
client_name: None,
|
||||
client_uri: None,
|
||||
logo_uri: None,
|
||||
redirect_uris: Vec::new(),
|
||||
grant_types: vec!["authorization_code".to_string()],
|
||||
response_types: vec!["code".to_string()],
|
||||
scope: None,
|
||||
token_endpoint_auth_method: Some("none".to_string()),
|
||||
dpop_bound_access_tokens: None,
|
||||
jwks: None,
|
||||
jwks_uri: None,
|
||||
application_type: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ClientMetadataCache {
|
||||
cache: Arc<RwLock<HashMap<String, CachedMetadata>>>,
|
||||
http_client: Client,
|
||||
cache_ttl_secs: u64,
|
||||
}
|
||||
|
||||
struct CachedMetadata {
|
||||
metadata: ClientMetadata,
|
||||
cached_at: std::time::Instant,
|
||||
}
|
||||
|
||||
impl ClientMetadataCache {
|
||||
pub fn new(cache_ttl_secs: u64) -> Self {
|
||||
Self {
|
||||
cache: Arc::new(RwLock::new(HashMap::new())),
|
||||
http_client: Client::new(),
|
||||
cache_ttl_secs,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get(&self, client_id: &str) -> Result<ClientMetadata, OAuthError> {
|
||||
{
|
||||
let cache = self.cache.read().await;
|
||||
if let Some(cached) = cache.get(client_id) {
|
||||
if cached.cached_at.elapsed().as_secs() < self.cache_ttl_secs {
|
||||
return Ok(cached.metadata.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let metadata = self.fetch_metadata(client_id).await?;
|
||||
|
||||
{
|
||||
let mut cache = self.cache.write().await;
|
||||
cache.insert(
|
||||
client_id.to_string(),
|
||||
CachedMetadata {
|
||||
metadata: metadata.clone(),
|
||||
cached_at: std::time::Instant::now(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Ok(metadata)
|
||||
}
|
||||
|
||||
async fn fetch_metadata(&self, client_id: &str) -> Result<ClientMetadata, OAuthError> {
|
||||
if !client_id.starts_with("http://") && !client_id.starts_with("https://") {
|
||||
return Err(OAuthError::InvalidClient(
|
||||
"client_id must be a URL".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
if client_id.starts_with("http://")
|
||||
&& !client_id.contains("localhost")
|
||||
&& !client_id.contains("127.0.0.1")
|
||||
{
|
||||
return Err(OAuthError::InvalidClient(
|
||||
"Non-localhost client_id must use https".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let response = self
|
||||
.http_client
|
||||
.get(client_id)
|
||||
.header("Accept", "application/json")
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| OAuthError::InvalidClient(format!("Failed to fetch client metadata: {}", e)))?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
return Err(OAuthError::InvalidClient(format!(
|
||||
"Failed to fetch client metadata: HTTP {}",
|
||||
response.status()
|
||||
)));
|
||||
}
|
||||
|
||||
let mut metadata: ClientMetadata = response
|
||||
.json()
|
||||
.await
|
||||
.map_err(|e| OAuthError::InvalidClient(format!("Invalid client metadata JSON: {}", e)))?;
|
||||
|
||||
if metadata.client_id.is_empty() {
|
||||
metadata.client_id = client_id.to_string();
|
||||
} else if metadata.client_id != client_id {
|
||||
return Err(OAuthError::InvalidClient(
|
||||
"client_id in metadata does not match request".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
self.validate_metadata(&metadata)?;
|
||||
|
||||
Ok(metadata)
|
||||
}
|
||||
|
||||
fn validate_metadata(&self, metadata: &ClientMetadata) -> Result<(), OAuthError> {
|
||||
if metadata.redirect_uris.is_empty() {
|
||||
return Err(OAuthError::InvalidClient(
|
||||
"redirect_uris is required".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
for uri in &metadata.redirect_uris {
|
||||
self.validate_redirect_uri_format(uri)?;
|
||||
}
|
||||
|
||||
if !metadata.grant_types.is_empty()
|
||||
&& !metadata.grant_types.contains(&"authorization_code".to_string())
|
||||
{
|
||||
return Err(OAuthError::InvalidClient(
|
||||
"authorization_code grant type is required".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
if !metadata.response_types.is_empty()
|
||||
&& !metadata.response_types.contains(&"code".to_string())
|
||||
{
|
||||
return Err(OAuthError::InvalidClient(
|
||||
"code response type is required".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn validate_redirect_uri(
|
||||
&self,
|
||||
metadata: &ClientMetadata,
|
||||
redirect_uri: &str,
|
||||
) -> Result<(), OAuthError> {
|
||||
if !metadata.redirect_uris.contains(&redirect_uri.to_string()) {
|
||||
return Err(OAuthError::InvalidRequest(
|
||||
"redirect_uri not registered for client".to_string(),
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_redirect_uri_format(&self, uri: &str) -> Result<(), OAuthError> {
|
||||
if uri.contains('#') {
|
||||
return Err(OAuthError::InvalidClient(
|
||||
"redirect_uri must not contain a fragment".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let parsed = reqwest::Url::parse(uri).map_err(|_| {
|
||||
OAuthError::InvalidClient(format!("Invalid redirect_uri: {}", uri))
|
||||
})?;
|
||||
|
||||
let scheme = parsed.scheme();
|
||||
|
||||
if scheme == "http" {
|
||||
let host = parsed.host_str().unwrap_or("");
|
||||
if host != "localhost" && host != "127.0.0.1" && host != "[::1]" {
|
||||
return Err(OAuthError::InvalidClient(
|
||||
"http redirect_uri only allowed for localhost".to_string(),
|
||||
));
|
||||
}
|
||||
} else if scheme == "https" {
|
||||
} else if scheme.chars().all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '+' || c == '.' || c == '-') {
|
||||
if !scheme.chars().next().map(|c| c.is_ascii_lowercase()).unwrap_or(false) {
|
||||
return Err(OAuthError::InvalidClient(format!(
|
||||
"Invalid redirect_uri scheme: {}",
|
||||
scheme
|
||||
)));
|
||||
}
|
||||
} else {
|
||||
return Err(OAuthError::InvalidClient(format!(
|
||||
"Invalid redirect_uri scheme: {}",
|
||||
scheme
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl ClientMetadata {
|
||||
pub fn requires_dpop(&self) -> bool {
|
||||
self.dpop_bound_access_tokens.unwrap_or(false)
|
||||
}
|
||||
|
||||
pub fn auth_method(&self) -> &str {
|
||||
self.token_endpoint_auth_method
|
||||
.as_deref()
|
||||
.unwrap_or("none")
|
||||
}
|
||||
}
|
||||
|
||||
pub fn verify_client_auth(
|
||||
metadata: &ClientMetadata,
|
||||
client_auth: &super::ClientAuth,
|
||||
) -> Result<(), OAuthError> {
|
||||
let expected_method = metadata.auth_method();
|
||||
|
||||
match (expected_method, client_auth) {
|
||||
("none", super::ClientAuth::None) => Ok(()),
|
||||
|
||||
("none", _) => Err(OAuthError::InvalidClient(
|
||||
"Client is configured for no authentication, but credentials were provided".to_string(),
|
||||
)),
|
||||
|
||||
("private_key_jwt", super::ClientAuth::PrivateKeyJwt { client_assertion }) => {
|
||||
verify_private_key_jwt(metadata, client_assertion)
|
||||
}
|
||||
|
||||
("private_key_jwt", _) => Err(OAuthError::InvalidClient(
|
||||
"Client requires private_key_jwt authentication".to_string(),
|
||||
)),
|
||||
|
||||
("client_secret_post", super::ClientAuth::SecretPost { .. }) => {
|
||||
Err(OAuthError::InvalidClient(
|
||||
"client_secret_post is not supported for ATProto OAuth".to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
("client_secret_basic", super::ClientAuth::SecretBasic { .. }) => {
|
||||
Err(OAuthError::InvalidClient(
|
||||
"client_secret_basic is not supported for ATProto OAuth".to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
(method, _) => Err(OAuthError::InvalidClient(format!(
|
||||
"Unsupported or mismatched authentication method: {}",
|
||||
method
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
fn verify_private_key_jwt(
|
||||
metadata: &ClientMetadata,
|
||||
client_assertion: &str,
|
||||
) -> Result<(), OAuthError> {
|
||||
use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
|
||||
|
||||
let parts: Vec<&str> = client_assertion.split('.').collect();
|
||||
if parts.len() != 3 {
|
||||
return Err(OAuthError::InvalidClient("Invalid client_assertion format".to_string()));
|
||||
}
|
||||
|
||||
let header_bytes = URL_SAFE_NO_PAD
|
||||
.decode(parts[0])
|
||||
.map_err(|_| OAuthError::InvalidClient("Invalid assertion header encoding".to_string()))?;
|
||||
let header: serde_json::Value = serde_json::from_slice(&header_bytes)
|
||||
.map_err(|_| OAuthError::InvalidClient("Invalid assertion header JSON".to_string()))?;
|
||||
|
||||
let alg = header.get("alg").and_then(|a| a.as_str()).ok_or_else(|| {
|
||||
OAuthError::InvalidClient("Missing alg in client_assertion".to_string())
|
||||
})?;
|
||||
|
||||
if !matches!(alg, "ES256" | "ES384" | "RS256" | "RS384" | "RS512" | "EdDSA") {
|
||||
return Err(OAuthError::InvalidClient(format!(
|
||||
"Unsupported client_assertion algorithm: {}",
|
||||
alg
|
||||
)));
|
||||
}
|
||||
|
||||
let payload_bytes = URL_SAFE_NO_PAD
|
||||
.decode(parts[1])
|
||||
.map_err(|_| OAuthError::InvalidClient("Invalid assertion payload encoding".to_string()))?;
|
||||
let payload: serde_json::Value = serde_json::from_slice(&payload_bytes)
|
||||
.map_err(|_| OAuthError::InvalidClient("Invalid assertion payload JSON".to_string()))?;
|
||||
|
||||
let iss = payload.get("iss").and_then(|i| i.as_str()).ok_or_else(|| {
|
||||
OAuthError::InvalidClient("Missing iss in client_assertion".to_string())
|
||||
})?;
|
||||
if iss != metadata.client_id {
|
||||
return Err(OAuthError::InvalidClient(
|
||||
"client_assertion iss does not match client_id".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let sub = payload.get("sub").and_then(|s| s.as_str()).ok_or_else(|| {
|
||||
OAuthError::InvalidClient("Missing sub in client_assertion".to_string())
|
||||
})?;
|
||||
if sub != metadata.client_id {
|
||||
return Err(OAuthError::InvalidClient(
|
||||
"client_assertion sub does not match client_id".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let exp = payload.get("exp").and_then(|e| e.as_i64()).ok_or_else(|| {
|
||||
OAuthError::InvalidClient("Missing exp in client_assertion".to_string())
|
||||
})?;
|
||||
let now = chrono::Utc::now().timestamp();
|
||||
if exp < now {
|
||||
return Err(OAuthError::InvalidClient("client_assertion has expired".to_string()));
|
||||
}
|
||||
|
||||
let iat = payload.get("iat").and_then(|i| i.as_i64());
|
||||
if let Some(iat) = iat {
|
||||
if iat > now + 60 {
|
||||
return Err(OAuthError::InvalidClient(
|
||||
"client_assertion iat is in the future".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if metadata.jwks.is_none() && metadata.jwks_uri.is_none() {
|
||||
return Err(OAuthError::InvalidClient(
|
||||
"Client using private_key_jwt must have jwks or jwks_uri".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
Err(OAuthError::InvalidClient(
|
||||
"private_key_jwt signature verification not yet implemented - use 'none' auth method".to_string(),
|
||||
))
|
||||
}
|
||||
+641
@@ -0,0 +1,641 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{de::DeserializeOwned, Serialize};
|
||||
use sqlx::PgPool;
|
||||
|
||||
use super::{
|
||||
AuthorizationRequestParameters, ClientAuth, DeviceData, OAuthError, RequestData, TokenData,
|
||||
AuthorizedClientData,
|
||||
};
|
||||
|
||||
fn to_json<T: Serialize>(value: &T) -> Result<serde_json::Value, OAuthError> {
|
||||
serde_json::to_value(value).map_err(|e| {
|
||||
tracing::error!("JSON serialization error: {}", e);
|
||||
OAuthError::ServerError("Internal serialization error".to_string())
|
||||
})
|
||||
}
|
||||
|
||||
fn from_json<T: DeserializeOwned>(value: serde_json::Value) -> Result<T, OAuthError> {
|
||||
serde_json::from_value(value).map_err(|e| {
|
||||
tracing::error!("JSON deserialization error: {}", e);
|
||||
OAuthError::ServerError("Internal data corruption".to_string())
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn create_device(
|
||||
pool: &PgPool,
|
||||
device_id: &str,
|
||||
data: &DeviceData,
|
||||
) -> Result<(), OAuthError> {
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO oauth_device (id, session_id, user_agent, ip_address, last_seen_at)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
"#,
|
||||
device_id,
|
||||
data.session_id,
|
||||
data.user_agent,
|
||||
data.ip_address,
|
||||
data.last_seen_at,
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_device(pool: &PgPool, device_id: &str) -> Result<Option<DeviceData>, OAuthError> {
|
||||
let row = sqlx::query!(
|
||||
r#"
|
||||
SELECT session_id, user_agent, ip_address, last_seen_at
|
||||
FROM oauth_device
|
||||
WHERE id = $1
|
||||
"#,
|
||||
device_id
|
||||
)
|
||||
.fetch_optional(pool)
|
||||
.await?;
|
||||
|
||||
Ok(row.map(|r| DeviceData {
|
||||
session_id: r.session_id,
|
||||
user_agent: r.user_agent,
|
||||
ip_address: r.ip_address,
|
||||
last_seen_at: r.last_seen_at,
|
||||
}))
|
||||
}
|
||||
|
||||
pub async fn update_device_last_seen(
|
||||
pool: &PgPool,
|
||||
device_id: &str,
|
||||
) -> Result<(), OAuthError> {
|
||||
sqlx::query!(
|
||||
r#"
|
||||
UPDATE oauth_device
|
||||
SET last_seen_at = NOW()
|
||||
WHERE id = $1
|
||||
"#,
|
||||
device_id
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn delete_device(pool: &PgPool, device_id: &str) -> Result<(), OAuthError> {
|
||||
sqlx::query!(
|
||||
r#"
|
||||
DELETE FROM oauth_device WHERE id = $1
|
||||
"#,
|
||||
device_id
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn create_authorization_request(
|
||||
pool: &PgPool,
|
||||
request_id: &str,
|
||||
data: &RequestData,
|
||||
) -> Result<(), OAuthError> {
|
||||
let client_auth_json = match &data.client_auth {
|
||||
Some(ca) => Some(to_json(ca)?),
|
||||
None => None,
|
||||
};
|
||||
let parameters_json = to_json(&data.parameters)?;
|
||||
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO oauth_authorization_request
|
||||
(id, did, device_id, client_id, client_auth, parameters, expires_at, code)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||
"#,
|
||||
request_id,
|
||||
data.did,
|
||||
data.device_id,
|
||||
data.client_id,
|
||||
client_auth_json,
|
||||
parameters_json,
|
||||
data.expires_at,
|
||||
data.code,
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_authorization_request(
|
||||
pool: &PgPool,
|
||||
request_id: &str,
|
||||
) -> Result<Option<RequestData>, OAuthError> {
|
||||
let row = sqlx::query!(
|
||||
r#"
|
||||
SELECT did, device_id, client_id, client_auth, parameters, expires_at, code
|
||||
FROM oauth_authorization_request
|
||||
WHERE id = $1
|
||||
"#,
|
||||
request_id
|
||||
)
|
||||
.fetch_optional(pool)
|
||||
.await?;
|
||||
|
||||
match row {
|
||||
Some(r) => {
|
||||
let client_auth: Option<ClientAuth> = match r.client_auth {
|
||||
Some(v) => Some(from_json(v)?),
|
||||
None => None,
|
||||
};
|
||||
let parameters: AuthorizationRequestParameters = from_json(r.parameters)?;
|
||||
|
||||
Ok(Some(RequestData {
|
||||
client_id: r.client_id,
|
||||
client_auth,
|
||||
parameters,
|
||||
expires_at: r.expires_at,
|
||||
did: r.did,
|
||||
device_id: r.device_id,
|
||||
code: r.code,
|
||||
}))
|
||||
}
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn update_authorization_request(
|
||||
pool: &PgPool,
|
||||
request_id: &str,
|
||||
did: &str,
|
||||
device_id: Option<&str>,
|
||||
code: &str,
|
||||
) -> Result<(), OAuthError> {
|
||||
sqlx::query!(
|
||||
r#"
|
||||
UPDATE oauth_authorization_request
|
||||
SET did = $2, device_id = $3, code = $4
|
||||
WHERE id = $1
|
||||
"#,
|
||||
request_id,
|
||||
did,
|
||||
device_id,
|
||||
code
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn consume_authorization_request_by_code(
|
||||
pool: &PgPool,
|
||||
code: &str,
|
||||
) -> Result<Option<RequestData>, OAuthError> {
|
||||
let row = sqlx::query!(
|
||||
r#"
|
||||
DELETE FROM oauth_authorization_request
|
||||
WHERE code = $1
|
||||
RETURNING did, device_id, client_id, client_auth, parameters, expires_at, code
|
||||
"#,
|
||||
code
|
||||
)
|
||||
.fetch_optional(pool)
|
||||
.await?;
|
||||
|
||||
match row {
|
||||
Some(r) => {
|
||||
let client_auth: Option<ClientAuth> = match r.client_auth {
|
||||
Some(v) => Some(from_json(v)?),
|
||||
None => None,
|
||||
};
|
||||
let parameters: AuthorizationRequestParameters = from_json(r.parameters)?;
|
||||
|
||||
Ok(Some(RequestData {
|
||||
client_id: r.client_id,
|
||||
client_auth,
|
||||
parameters,
|
||||
expires_at: r.expires_at,
|
||||
did: r.did,
|
||||
device_id: r.device_id,
|
||||
code: r.code,
|
||||
}))
|
||||
}
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn delete_authorization_request(
|
||||
pool: &PgPool,
|
||||
request_id: &str,
|
||||
) -> Result<(), OAuthError> {
|
||||
sqlx::query!(
|
||||
r#"
|
||||
DELETE FROM oauth_authorization_request WHERE id = $1
|
||||
"#,
|
||||
request_id
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn delete_expired_authorization_requests(pool: &PgPool) -> Result<u64, OAuthError> {
|
||||
let result = sqlx::query!(
|
||||
r#"
|
||||
DELETE FROM oauth_authorization_request
|
||||
WHERE expires_at < NOW()
|
||||
"#
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
Ok(result.rows_affected())
|
||||
}
|
||||
|
||||
pub async fn create_token(
|
||||
pool: &PgPool,
|
||||
data: &TokenData,
|
||||
) -> Result<i32, OAuthError> {
|
||||
let client_auth_json = to_json(&data.client_auth)?;
|
||||
let parameters_json = to_json(&data.parameters)?;
|
||||
|
||||
let row = sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO oauth_token
|
||||
(did, token_id, created_at, updated_at, expires_at, client_id, client_auth,
|
||||
device_id, parameters, details, code, current_refresh_token, scope)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
|
||||
RETURNING id
|
||||
"#,
|
||||
data.did,
|
||||
data.token_id,
|
||||
data.created_at,
|
||||
data.updated_at,
|
||||
data.expires_at,
|
||||
data.client_id,
|
||||
client_auth_json,
|
||||
data.device_id,
|
||||
parameters_json,
|
||||
data.details,
|
||||
data.code,
|
||||
data.current_refresh_token,
|
||||
data.scope,
|
||||
)
|
||||
.fetch_one(pool)
|
||||
.await?;
|
||||
|
||||
Ok(row.id)
|
||||
}
|
||||
|
||||
pub async fn get_token_by_id(
|
||||
pool: &PgPool,
|
||||
token_id: &str,
|
||||
) -> Result<Option<TokenData>, OAuthError> {
|
||||
let row = sqlx::query!(
|
||||
r#"
|
||||
SELECT did, token_id, created_at, updated_at, expires_at, client_id, client_auth,
|
||||
device_id, parameters, details, code, current_refresh_token, scope
|
||||
FROM oauth_token
|
||||
WHERE token_id = $1
|
||||
"#,
|
||||
token_id
|
||||
)
|
||||
.fetch_optional(pool)
|
||||
.await?;
|
||||
|
||||
match row {
|
||||
Some(r) => Ok(Some(TokenData {
|
||||
did: r.did,
|
||||
token_id: r.token_id,
|
||||
created_at: r.created_at,
|
||||
updated_at: r.updated_at,
|
||||
expires_at: r.expires_at,
|
||||
client_id: r.client_id,
|
||||
client_auth: from_json(r.client_auth)?,
|
||||
device_id: r.device_id,
|
||||
parameters: from_json(r.parameters)?,
|
||||
details: r.details,
|
||||
code: r.code,
|
||||
current_refresh_token: r.current_refresh_token,
|
||||
scope: r.scope,
|
||||
})),
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_token_by_refresh_token(
|
||||
pool: &PgPool,
|
||||
refresh_token: &str,
|
||||
) -> Result<Option<(i32, TokenData)>, OAuthError> {
|
||||
let row = sqlx::query!(
|
||||
r#"
|
||||
SELECT id, did, token_id, created_at, updated_at, expires_at, client_id, client_auth,
|
||||
device_id, parameters, details, code, current_refresh_token, scope
|
||||
FROM oauth_token
|
||||
WHERE current_refresh_token = $1
|
||||
"#,
|
||||
refresh_token
|
||||
)
|
||||
.fetch_optional(pool)
|
||||
.await?;
|
||||
|
||||
match row {
|
||||
Some(r) => Ok(Some((
|
||||
r.id,
|
||||
TokenData {
|
||||
did: r.did,
|
||||
token_id: r.token_id,
|
||||
created_at: r.created_at,
|
||||
updated_at: r.updated_at,
|
||||
expires_at: r.expires_at,
|
||||
client_id: r.client_id,
|
||||
client_auth: from_json(r.client_auth)?,
|
||||
device_id: r.device_id,
|
||||
parameters: from_json(r.parameters)?,
|
||||
details: r.details,
|
||||
code: r.code,
|
||||
current_refresh_token: r.current_refresh_token,
|
||||
scope: r.scope,
|
||||
},
|
||||
))),
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn rotate_token(
|
||||
pool: &PgPool,
|
||||
old_db_id: i32,
|
||||
new_token_id: &str,
|
||||
new_refresh_token: &str,
|
||||
new_expires_at: DateTime<Utc>,
|
||||
) -> Result<(), OAuthError> {
|
||||
let mut tx = pool.begin().await?;
|
||||
|
||||
let old_refresh = sqlx::query_scalar!(
|
||||
r#"
|
||||
SELECT current_refresh_token FROM oauth_token WHERE id = $1
|
||||
"#,
|
||||
old_db_id
|
||||
)
|
||||
.fetch_one(&mut *tx)
|
||||
.await?;
|
||||
|
||||
if let Some(old_rt) = old_refresh {
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO oauth_used_refresh_token (refresh_token, token_id)
|
||||
VALUES ($1, $2)
|
||||
"#,
|
||||
old_rt,
|
||||
old_db_id
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
}
|
||||
|
||||
sqlx::query!(
|
||||
r#"
|
||||
UPDATE oauth_token
|
||||
SET token_id = $2, current_refresh_token = $3, expires_at = $4, updated_at = NOW()
|
||||
WHERE id = $1
|
||||
"#,
|
||||
old_db_id,
|
||||
new_token_id,
|
||||
new_refresh_token,
|
||||
new_expires_at
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
tx.commit().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn check_refresh_token_used(
|
||||
pool: &PgPool,
|
||||
refresh_token: &str,
|
||||
) -> Result<Option<i32>, OAuthError> {
|
||||
let row = sqlx::query_scalar!(
|
||||
r#"
|
||||
SELECT token_id FROM oauth_used_refresh_token WHERE refresh_token = $1
|
||||
"#,
|
||||
refresh_token
|
||||
)
|
||||
.fetch_optional(pool)
|
||||
.await?;
|
||||
|
||||
Ok(row)
|
||||
}
|
||||
|
||||
pub async fn delete_token(pool: &PgPool, token_id: &str) -> Result<(), OAuthError> {
|
||||
sqlx::query!(
|
||||
r#"
|
||||
DELETE FROM oauth_token WHERE token_id = $1
|
||||
"#,
|
||||
token_id
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn delete_token_family(pool: &PgPool, db_id: i32) -> Result<(), OAuthError> {
|
||||
sqlx::query!(
|
||||
r#"
|
||||
DELETE FROM oauth_token WHERE id = $1
|
||||
"#,
|
||||
db_id
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn upsert_account_device(
|
||||
pool: &PgPool,
|
||||
did: &str,
|
||||
device_id: &str,
|
||||
) -> Result<(), OAuthError> {
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO oauth_account_device (did, device_id, created_at, updated_at)
|
||||
VALUES ($1, $2, NOW(), NOW())
|
||||
ON CONFLICT (did, device_id) DO UPDATE SET updated_at = NOW()
|
||||
"#,
|
||||
did,
|
||||
device_id
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn upsert_authorized_client(
|
||||
pool: &PgPool,
|
||||
did: &str,
|
||||
client_id: &str,
|
||||
data: &AuthorizedClientData,
|
||||
) -> Result<(), OAuthError> {
|
||||
let data_json = to_json(data)?;
|
||||
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO oauth_authorized_client (did, client_id, created_at, updated_at, data)
|
||||
VALUES ($1, $2, NOW(), NOW(), $3)
|
||||
ON CONFLICT (did, client_id) DO UPDATE SET updated_at = NOW(), data = $3
|
||||
"#,
|
||||
did,
|
||||
client_id,
|
||||
data_json
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_authorized_client(
|
||||
pool: &PgPool,
|
||||
did: &str,
|
||||
client_id: &str,
|
||||
) -> Result<Option<AuthorizedClientData>, OAuthError> {
|
||||
let row = sqlx::query_scalar!(
|
||||
r#"
|
||||
SELECT data FROM oauth_authorized_client
|
||||
WHERE did = $1 AND client_id = $2
|
||||
"#,
|
||||
did,
|
||||
client_id
|
||||
)
|
||||
.fetch_optional(pool)
|
||||
.await?;
|
||||
|
||||
match row {
|
||||
Some(v) => Ok(Some(from_json(v)?)),
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn list_tokens_for_user(
|
||||
pool: &PgPool,
|
||||
did: &str,
|
||||
) -> Result<Vec<TokenData>, OAuthError> {
|
||||
let rows = sqlx::query!(
|
||||
r#"
|
||||
SELECT did, token_id, created_at, updated_at, expires_at, client_id, client_auth,
|
||||
device_id, parameters, details, code, current_refresh_token, scope
|
||||
FROM oauth_token
|
||||
WHERE did = $1
|
||||
"#,
|
||||
did
|
||||
)
|
||||
.fetch_all(pool)
|
||||
.await?;
|
||||
|
||||
let mut tokens = Vec::with_capacity(rows.len());
|
||||
for r in rows {
|
||||
tokens.push(TokenData {
|
||||
did: r.did,
|
||||
token_id: r.token_id,
|
||||
created_at: r.created_at,
|
||||
updated_at: r.updated_at,
|
||||
expires_at: r.expires_at,
|
||||
client_id: r.client_id,
|
||||
client_auth: from_json(r.client_auth)?,
|
||||
device_id: r.device_id,
|
||||
parameters: from_json(r.parameters)?,
|
||||
details: r.details,
|
||||
code: r.code,
|
||||
current_refresh_token: r.current_refresh_token,
|
||||
scope: r.scope,
|
||||
});
|
||||
}
|
||||
Ok(tokens)
|
||||
}
|
||||
|
||||
pub async fn check_and_record_dpop_jti(
|
||||
pool: &PgPool,
|
||||
jti: &str,
|
||||
) -> Result<bool, OAuthError> {
|
||||
let result = sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO oauth_dpop_jti (jti)
|
||||
VALUES ($1)
|
||||
ON CONFLICT (jti) DO NOTHING
|
||||
"#,
|
||||
jti
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
Ok(result.rows_affected() > 0)
|
||||
}
|
||||
|
||||
pub async fn cleanup_expired_dpop_jtis(
|
||||
pool: &PgPool,
|
||||
max_age_secs: i64,
|
||||
) -> Result<u64, OAuthError> {
|
||||
let result = sqlx::query!(
|
||||
r#"
|
||||
DELETE FROM oauth_dpop_jti
|
||||
WHERE created_at < NOW() - INTERVAL '1 second' * $1
|
||||
"#,
|
||||
max_age_secs as f64
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
Ok(result.rows_affected())
|
||||
}
|
||||
|
||||
pub async fn count_tokens_for_user(pool: &PgPool, did: &str) -> Result<i64, OAuthError> {
|
||||
let count = sqlx::query_scalar!(
|
||||
r#"
|
||||
SELECT COUNT(*) as "count!" FROM oauth_token WHERE did = $1
|
||||
"#,
|
||||
did
|
||||
)
|
||||
.fetch_one(pool)
|
||||
.await?;
|
||||
|
||||
Ok(count)
|
||||
}
|
||||
|
||||
pub async fn delete_oldest_tokens_for_user(
|
||||
pool: &PgPool,
|
||||
did: &str,
|
||||
keep_count: i64,
|
||||
) -> Result<u64, OAuthError> {
|
||||
let result = sqlx::query!(
|
||||
r#"
|
||||
DELETE FROM oauth_token
|
||||
WHERE id IN (
|
||||
SELECT id FROM oauth_token
|
||||
WHERE did = $1
|
||||
ORDER BY updated_at ASC
|
||||
OFFSET $2
|
||||
)
|
||||
"#,
|
||||
did,
|
||||
keep_count
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
Ok(result.rows_affected())
|
||||
}
|
||||
|
||||
const MAX_TOKENS_PER_USER: i64 = 100;
|
||||
|
||||
pub async fn enforce_token_limit_for_user(pool: &PgPool, did: &str) -> Result<(), OAuthError> {
|
||||
let count = count_tokens_for_user(pool, did).await?;
|
||||
if count > MAX_TOKENS_PER_USER {
|
||||
let to_keep = MAX_TOKENS_PER_USER - 1;
|
||||
delete_oldest_tokens_for_user(pool, did, to_keep).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,421 @@
|
||||
use base64::Engine;
|
||||
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
|
||||
use chrono::Utc;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
use super::OAuthError;
|
||||
|
||||
const DPOP_NONCE_VALIDITY_SECS: i64 = 300;
|
||||
const DPOP_MAX_AGE_SECS: i64 = 300;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DPoPVerifyResult {
|
||||
pub jkt: String,
|
||||
pub jti: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DPoPProofHeader {
|
||||
pub typ: String,
|
||||
pub alg: String,
|
||||
pub jwk: DPoPJwk,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DPoPJwk {
|
||||
pub kty: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub crv: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub x: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub y: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DPoPProofPayload {
|
||||
pub jti: String,
|
||||
pub htm: String,
|
||||
pub htu: String,
|
||||
pub iat: i64,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub ath: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub nonce: Option<String>,
|
||||
}
|
||||
|
||||
pub struct DPoPVerifier {
|
||||
secret: Vec<u8>,
|
||||
}
|
||||
|
||||
impl DPoPVerifier {
|
||||
pub fn new(secret: &[u8]) -> Self {
|
||||
Self {
|
||||
secret: secret.to_vec(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn generate_nonce(&self) -> String {
|
||||
let timestamp = Utc::now().timestamp();
|
||||
let timestamp_bytes = timestamp.to_be_bytes();
|
||||
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(&self.secret);
|
||||
hasher.update(×tamp_bytes);
|
||||
let hash = hasher.finalize();
|
||||
|
||||
let mut nonce_data = Vec::with_capacity(8 + 16);
|
||||
nonce_data.extend_from_slice(×tamp_bytes);
|
||||
nonce_data.extend_from_slice(&hash[..16]);
|
||||
|
||||
URL_SAFE_NO_PAD.encode(&nonce_data)
|
||||
}
|
||||
|
||||
pub fn validate_nonce(&self, nonce: &str) -> Result<(), OAuthError> {
|
||||
let nonce_bytes = URL_SAFE_NO_PAD
|
||||
.decode(nonce)
|
||||
.map_err(|_| OAuthError::InvalidDpopProof("Invalid nonce encoding".to_string()))?;
|
||||
|
||||
if nonce_bytes.len() < 24 {
|
||||
return Err(OAuthError::InvalidDpopProof("Invalid nonce length".to_string()));
|
||||
}
|
||||
|
||||
let timestamp_bytes: [u8; 8] = nonce_bytes[..8]
|
||||
.try_into()
|
||||
.map_err(|_| OAuthError::InvalidDpopProof("Invalid nonce".to_string()))?;
|
||||
let timestamp = i64::from_be_bytes(timestamp_bytes);
|
||||
|
||||
let now = Utc::now().timestamp();
|
||||
if now - timestamp > DPOP_NONCE_VALIDITY_SECS {
|
||||
return Err(OAuthError::UseDpopNonce(self.generate_nonce()));
|
||||
}
|
||||
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(&self.secret);
|
||||
hasher.update(×tamp_bytes);
|
||||
let expected_hash = hasher.finalize();
|
||||
|
||||
if nonce_bytes[8..24] != expected_hash[..16] {
|
||||
return Err(OAuthError::InvalidDpopProof("Invalid nonce signature".to_string()));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn verify_proof(
|
||||
&self,
|
||||
dpop_header: &str,
|
||||
http_method: &str,
|
||||
http_uri: &str,
|
||||
access_token_hash: Option<&str>,
|
||||
) -> Result<DPoPVerifyResult, OAuthError> {
|
||||
let parts: Vec<&str> = dpop_header.split('.').collect();
|
||||
if parts.len() != 3 {
|
||||
return Err(OAuthError::InvalidDpopProof("Invalid DPoP proof format".to_string()));
|
||||
}
|
||||
|
||||
let header_json = URL_SAFE_NO_PAD
|
||||
.decode(parts[0])
|
||||
.map_err(|_| OAuthError::InvalidDpopProof("Invalid header encoding".to_string()))?;
|
||||
let payload_json = URL_SAFE_NO_PAD
|
||||
.decode(parts[1])
|
||||
.map_err(|_| OAuthError::InvalidDpopProof("Invalid payload encoding".to_string()))?;
|
||||
|
||||
let header: DPoPProofHeader = serde_json::from_slice(&header_json)
|
||||
.map_err(|_| OAuthError::InvalidDpopProof("Invalid header JSON".to_string()))?;
|
||||
let payload: DPoPProofPayload = serde_json::from_slice(&payload_json)
|
||||
.map_err(|_| OAuthError::InvalidDpopProof("Invalid payload JSON".to_string()))?;
|
||||
|
||||
if header.typ != "dpop+jwt" {
|
||||
return Err(OAuthError::InvalidDpopProof("Invalid typ claim".to_string()));
|
||||
}
|
||||
|
||||
if !matches!(header.alg.as_str(), "ES256" | "ES384" | "ES512" | "EdDSA") {
|
||||
return Err(OAuthError::InvalidDpopProof("Unsupported algorithm".to_string()));
|
||||
}
|
||||
|
||||
if payload.htm.to_uppercase() != http_method.to_uppercase() {
|
||||
return Err(OAuthError::InvalidDpopProof("HTTP method mismatch".to_string()));
|
||||
}
|
||||
|
||||
let proof_uri = payload.htu.split('?').next().unwrap_or(&payload.htu);
|
||||
let request_uri = http_uri.split('?').next().unwrap_or(http_uri);
|
||||
if proof_uri != request_uri {
|
||||
return Err(OAuthError::InvalidDpopProof("HTTP URI mismatch".to_string()));
|
||||
}
|
||||
|
||||
let now = Utc::now().timestamp();
|
||||
if (now - payload.iat).abs() > DPOP_MAX_AGE_SECS {
|
||||
return Err(OAuthError::InvalidDpopProof("Proof too old or from the future".to_string()));
|
||||
}
|
||||
|
||||
if let Some(nonce) = &payload.nonce {
|
||||
self.validate_nonce(nonce)?;
|
||||
}
|
||||
|
||||
if let Some(expected_ath) = access_token_hash {
|
||||
match &payload.ath {
|
||||
Some(ath) if ath == expected_ath => {}
|
||||
Some(_) => {
|
||||
return Err(OAuthError::InvalidDpopProof(
|
||||
"Access token hash mismatch".to_string(),
|
||||
));
|
||||
}
|
||||
None => {
|
||||
return Err(OAuthError::InvalidDpopProof(
|
||||
"Missing access token hash".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let signature_bytes = URL_SAFE_NO_PAD
|
||||
.decode(parts[2])
|
||||
.map_err(|_| OAuthError::InvalidDpopProof("Invalid signature encoding".to_string()))?;
|
||||
|
||||
let signing_input = format!("{}.{}", parts[0], parts[1]);
|
||||
verify_dpop_signature(&header.alg, &header.jwk, signing_input.as_bytes(), &signature_bytes)?;
|
||||
|
||||
let jkt = compute_jwk_thumbprint(&header.jwk)?;
|
||||
|
||||
Ok(DPoPVerifyResult {
|
||||
jkt,
|
||||
jti: payload.jti.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn verify_dpop_signature(
|
||||
alg: &str,
|
||||
jwk: &DPoPJwk,
|
||||
message: &[u8],
|
||||
signature: &[u8],
|
||||
) -> Result<(), OAuthError> {
|
||||
match alg {
|
||||
"ES256" => verify_es256(jwk, message, signature),
|
||||
"ES384" => verify_es384(jwk, message, signature),
|
||||
"EdDSA" => verify_eddsa(jwk, message, signature),
|
||||
_ => Err(OAuthError::InvalidDpopProof(format!(
|
||||
"Unsupported algorithm: {}",
|
||||
alg
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
fn verify_es256(jwk: &DPoPJwk, message: &[u8], signature: &[u8]) -> Result<(), OAuthError> {
|
||||
use p256::ecdsa::signature::Verifier;
|
||||
use p256::ecdsa::{Signature, VerifyingKey};
|
||||
use p256::elliptic_curve::sec1::FromEncodedPoint;
|
||||
use p256::{AffinePoint, EncodedPoint};
|
||||
|
||||
let crv = jwk.crv.as_ref().ok_or_else(|| {
|
||||
OAuthError::InvalidDpopProof("Missing crv for ES256".to_string())
|
||||
})?;
|
||||
if crv != "P-256" {
|
||||
return Err(OAuthError::InvalidDpopProof(format!(
|
||||
"Invalid curve for ES256: {}",
|
||||
crv
|
||||
)));
|
||||
}
|
||||
|
||||
let x_bytes = URL_SAFE_NO_PAD
|
||||
.decode(jwk.x.as_ref().ok_or_else(|| {
|
||||
OAuthError::InvalidDpopProof("Missing x coordinate".to_string())
|
||||
})?)
|
||||
.map_err(|_| OAuthError::InvalidDpopProof("Invalid x encoding".to_string()))?;
|
||||
|
||||
let y_bytes = URL_SAFE_NO_PAD
|
||||
.decode(jwk.y.as_ref().ok_or_else(|| {
|
||||
OAuthError::InvalidDpopProof("Missing y coordinate".to_string())
|
||||
})?)
|
||||
.map_err(|_| OAuthError::InvalidDpopProof("Invalid y encoding".to_string()))?;
|
||||
|
||||
let point = EncodedPoint::from_affine_coordinates(
|
||||
x_bytes.as_slice().into(),
|
||||
y_bytes.as_slice().into(),
|
||||
false,
|
||||
);
|
||||
|
||||
let affine = AffinePoint::from_encoded_point(&point);
|
||||
if affine.is_none().into() {
|
||||
return Err(OAuthError::InvalidDpopProof("Invalid EC point".to_string()));
|
||||
}
|
||||
|
||||
let verifying_key = VerifyingKey::from_affine(affine.unwrap())
|
||||
.map_err(|_| OAuthError::InvalidDpopProof("Invalid verifying key".to_string()))?;
|
||||
|
||||
let sig = Signature::from_slice(signature)
|
||||
.map_err(|_| OAuthError::InvalidDpopProof("Invalid signature format".to_string()))?;
|
||||
|
||||
verifying_key
|
||||
.verify(message, &sig)
|
||||
.map_err(|_| OAuthError::InvalidDpopProof("Signature verification failed".to_string()))
|
||||
}
|
||||
|
||||
fn verify_es384(jwk: &DPoPJwk, message: &[u8], signature: &[u8]) -> Result<(), OAuthError> {
|
||||
use p384::ecdsa::signature::Verifier;
|
||||
use p384::ecdsa::{Signature, VerifyingKey};
|
||||
use p384::elliptic_curve::sec1::FromEncodedPoint;
|
||||
use p384::{AffinePoint, EncodedPoint};
|
||||
|
||||
let crv = jwk.crv.as_ref().ok_or_else(|| {
|
||||
OAuthError::InvalidDpopProof("Missing crv for ES384".to_string())
|
||||
})?;
|
||||
if crv != "P-384" {
|
||||
return Err(OAuthError::InvalidDpopProof(format!(
|
||||
"Invalid curve for ES384: {}",
|
||||
crv
|
||||
)));
|
||||
}
|
||||
|
||||
let x_bytes = URL_SAFE_NO_PAD
|
||||
.decode(jwk.x.as_ref().ok_or_else(|| {
|
||||
OAuthError::InvalidDpopProof("Missing x coordinate".to_string())
|
||||
})?)
|
||||
.map_err(|_| OAuthError::InvalidDpopProof("Invalid x encoding".to_string()))?;
|
||||
|
||||
let y_bytes = URL_SAFE_NO_PAD
|
||||
.decode(jwk.y.as_ref().ok_or_else(|| {
|
||||
OAuthError::InvalidDpopProof("Missing y coordinate".to_string())
|
||||
})?)
|
||||
.map_err(|_| OAuthError::InvalidDpopProof("Invalid y encoding".to_string()))?;
|
||||
|
||||
let point = EncodedPoint::from_affine_coordinates(
|
||||
x_bytes.as_slice().into(),
|
||||
y_bytes.as_slice().into(),
|
||||
false,
|
||||
);
|
||||
|
||||
let affine = AffinePoint::from_encoded_point(&point);
|
||||
if affine.is_none().into() {
|
||||
return Err(OAuthError::InvalidDpopProof("Invalid EC point".to_string()));
|
||||
}
|
||||
|
||||
let verifying_key = VerifyingKey::from_affine(affine.unwrap())
|
||||
.map_err(|_| OAuthError::InvalidDpopProof("Invalid verifying key".to_string()))?;
|
||||
|
||||
let sig = Signature::from_slice(signature)
|
||||
.map_err(|_| OAuthError::InvalidDpopProof("Invalid signature format".to_string()))?;
|
||||
|
||||
verifying_key
|
||||
.verify(message, &sig)
|
||||
.map_err(|_| OAuthError::InvalidDpopProof("Signature verification failed".to_string()))
|
||||
}
|
||||
|
||||
fn verify_eddsa(jwk: &DPoPJwk, message: &[u8], signature: &[u8]) -> Result<(), OAuthError> {
|
||||
use ed25519_dalek::{Signature, VerifyingKey};
|
||||
|
||||
let crv = jwk.crv.as_ref().ok_or_else(|| {
|
||||
OAuthError::InvalidDpopProof("Missing crv for EdDSA".to_string())
|
||||
})?;
|
||||
if crv != "Ed25519" {
|
||||
return Err(OAuthError::InvalidDpopProof(format!(
|
||||
"Invalid curve for EdDSA: {}",
|
||||
crv
|
||||
)));
|
||||
}
|
||||
|
||||
let x_bytes = URL_SAFE_NO_PAD
|
||||
.decode(jwk.x.as_ref().ok_or_else(|| {
|
||||
OAuthError::InvalidDpopProof("Missing x coordinate".to_string())
|
||||
})?)
|
||||
.map_err(|_| OAuthError::InvalidDpopProof("Invalid x encoding".to_string()))?;
|
||||
|
||||
let key_bytes: [u8; 32] = x_bytes.try_into().map_err(|_| {
|
||||
OAuthError::InvalidDpopProof("Invalid Ed25519 key length".to_string())
|
||||
})?;
|
||||
|
||||
let verifying_key = VerifyingKey::from_bytes(&key_bytes)
|
||||
.map_err(|_| OAuthError::InvalidDpopProof("Invalid Ed25519 key".to_string()))?;
|
||||
|
||||
let sig_bytes: [u8; 64] = signature.try_into().map_err(|_| {
|
||||
OAuthError::InvalidDpopProof("Invalid Ed25519 signature length".to_string())
|
||||
})?;
|
||||
let sig = Signature::from_bytes(&sig_bytes);
|
||||
|
||||
verifying_key
|
||||
.verify_strict(message, &sig)
|
||||
.map_err(|_| OAuthError::InvalidDpopProof("Signature verification failed".to_string()))
|
||||
}
|
||||
|
||||
pub fn compute_jwk_thumbprint(jwk: &DPoPJwk) -> Result<String, OAuthError> {
|
||||
let canonical = match jwk.kty.as_str() {
|
||||
"EC" => {
|
||||
let crv = jwk
|
||||
.crv
|
||||
.as_ref()
|
||||
.ok_or_else(|| OAuthError::InvalidDpopProof("Missing crv".to_string()))?;
|
||||
let x = jwk
|
||||
.x
|
||||
.as_ref()
|
||||
.ok_or_else(|| OAuthError::InvalidDpopProof("Missing x".to_string()))?;
|
||||
let y = jwk
|
||||
.y
|
||||
.as_ref()
|
||||
.ok_or_else(|| OAuthError::InvalidDpopProof("Missing y".to_string()))?;
|
||||
|
||||
format!(
|
||||
r#"{{"crv":"{}","kty":"EC","x":"{}","y":"{}"}}"#,
|
||||
crv, x, y
|
||||
)
|
||||
}
|
||||
"OKP" => {
|
||||
let crv = jwk
|
||||
.crv
|
||||
.as_ref()
|
||||
.ok_or_else(|| OAuthError::InvalidDpopProof("Missing crv".to_string()))?;
|
||||
let x = jwk
|
||||
.x
|
||||
.as_ref()
|
||||
.ok_or_else(|| OAuthError::InvalidDpopProof("Missing x".to_string()))?;
|
||||
|
||||
format!(r#"{{"crv":"{}","kty":"OKP","x":"{}"}}"#, crv, x)
|
||||
}
|
||||
_ => {
|
||||
return Err(OAuthError::InvalidDpopProof(
|
||||
"Unsupported key type".to_string(),
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(canonical.as_bytes());
|
||||
let hash = hasher.finalize();
|
||||
|
||||
Ok(URL_SAFE_NO_PAD.encode(&hash))
|
||||
}
|
||||
|
||||
pub fn compute_access_token_hash(access_token: &str) -> String {
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(access_token.as_bytes());
|
||||
let hash = hasher.finalize();
|
||||
URL_SAFE_NO_PAD.encode(&hash)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_nonce_generation_and_validation() {
|
||||
let secret = b"test-secret-key-32-bytes-long!!!";
|
||||
let verifier = DPoPVerifier::new(secret);
|
||||
|
||||
let nonce = verifier.generate_nonce();
|
||||
assert!(verifier.validate_nonce(&nonce).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_jwk_thumbprint_ec() {
|
||||
let jwk = DPoPJwk {
|
||||
kty: "EC".to_string(),
|
||||
crv: Some("P-256".to_string()),
|
||||
x: Some("test_x".to_string()),
|
||||
y: Some("test_y".to_string()),
|
||||
};
|
||||
|
||||
let thumbprint = compute_jwk_thumbprint(&jwk).unwrap();
|
||||
assert!(!thumbprint.is_empty());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
use axum::{
|
||||
Form, Json,
|
||||
extract::{Query, State},
|
||||
http::HeaderMap,
|
||||
response::{IntoResponse, Redirect, Response},
|
||||
};
|
||||
use chrono::Utc;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use urlencoding::encode as url_encode;
|
||||
|
||||
use crate::state::AppState;
|
||||
use crate::oauth::{Code, DeviceData, DeviceId, OAuthError, SessionId, db};
|
||||
|
||||
fn extract_client_ip(headers: &HeaderMap) -> String {
|
||||
if let Some(forwarded) = headers.get("x-forwarded-for") {
|
||||
if let Ok(value) = forwarded.to_str() {
|
||||
if let Some(first_ip) = value.split(',').next() {
|
||||
return first_ip.trim().to_string();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(real_ip) = headers.get("x-real-ip") {
|
||||
if let Ok(value) = real_ip.to_str() {
|
||||
return value.trim().to_string();
|
||||
}
|
||||
}
|
||||
|
||||
"0.0.0.0".to_string()
|
||||
}
|
||||
|
||||
fn extract_user_agent(headers: &HeaderMap) -> Option<String> {
|
||||
headers
|
||||
.get("user-agent")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.map(|s| s.to_string())
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct AuthorizeQuery {
|
||||
pub request_uri: Option<String>,
|
||||
pub client_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct AuthorizeResponse {
|
||||
pub client_id: String,
|
||||
pub client_name: Option<String>,
|
||||
pub scope: Option<String>,
|
||||
pub redirect_uri: String,
|
||||
pub state: Option<String>,
|
||||
pub login_hint: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct AuthorizeSubmit {
|
||||
pub request_uri: String,
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
#[serde(default)]
|
||||
pub remember_device: bool,
|
||||
}
|
||||
|
||||
pub async fn authorize_get(
|
||||
State(state): State<AppState>,
|
||||
Query(query): Query<AuthorizeQuery>,
|
||||
) -> Result<Json<AuthorizeResponse>, OAuthError> {
|
||||
let request_uri = query.request_uri.ok_or_else(|| {
|
||||
OAuthError::InvalidRequest("request_uri is required".to_string())
|
||||
})?;
|
||||
|
||||
let request_data = db::get_authorization_request(&state.db, &request_uri)
|
||||
.await?
|
||||
.ok_or_else(|| OAuthError::InvalidRequest("Invalid or expired request_uri".to_string()))?;
|
||||
|
||||
if request_data.expires_at < Utc::now() {
|
||||
db::delete_authorization_request(&state.db, &request_uri).await?;
|
||||
return Err(OAuthError::InvalidRequest("request_uri has expired".to_string()));
|
||||
}
|
||||
|
||||
Ok(Json(AuthorizeResponse {
|
||||
client_id: request_data.parameters.client_id.clone(),
|
||||
client_name: None,
|
||||
scope: request_data.parameters.scope.clone(),
|
||||
redirect_uri: request_data.parameters.redirect_uri.clone(),
|
||||
state: request_data.parameters.state.clone(),
|
||||
login_hint: request_data.parameters.login_hint.clone(),
|
||||
}))
|
||||
}
|
||||
|
||||
pub async fn authorize_post(
|
||||
State(state): State<AppState>,
|
||||
headers: HeaderMap,
|
||||
Form(form): Form<AuthorizeSubmit>,
|
||||
) -> Result<Response, OAuthError> {
|
||||
let request_data = db::get_authorization_request(&state.db, &form.request_uri)
|
||||
.await?
|
||||
.ok_or_else(|| OAuthError::InvalidRequest("Invalid or expired request_uri".to_string()))?;
|
||||
|
||||
if request_data.expires_at < Utc::now() {
|
||||
db::delete_authorization_request(&state.db, &form.request_uri).await?;
|
||||
return Err(OAuthError::InvalidRequest("request_uri has expired".to_string()));
|
||||
}
|
||||
|
||||
let user = sqlx::query!(
|
||||
r#"
|
||||
SELECT did, password_hash, deactivated_at, takedown_ref
|
||||
FROM users
|
||||
WHERE handle = $1 OR email = $1
|
||||
"#,
|
||||
form.username
|
||||
)
|
||||
.fetch_optional(&state.db)
|
||||
.await
|
||||
.map_err(|e| OAuthError::ServerError(e.to_string()))?
|
||||
.ok_or_else(|| OAuthError::AccessDenied("Invalid credentials".to_string()))?;
|
||||
|
||||
if user.deactivated_at.is_some() {
|
||||
return Err(OAuthError::AccessDenied("Account is deactivated".to_string()));
|
||||
}
|
||||
|
||||
if user.takedown_ref.is_some() {
|
||||
return Err(OAuthError::AccessDenied("Account is taken down".to_string()));
|
||||
}
|
||||
|
||||
let password_valid = bcrypt::verify(&form.password, &user.password_hash)
|
||||
.map_err(|_| OAuthError::ServerError("Password verification failed".to_string()))?;
|
||||
|
||||
if !password_valid {
|
||||
return Err(OAuthError::AccessDenied("Invalid credentials".to_string()));
|
||||
}
|
||||
|
||||
let code = Code::generate();
|
||||
let mut device_id: Option<String> = None;
|
||||
|
||||
if form.remember_device {
|
||||
let new_device_id = DeviceId::generate();
|
||||
let device_data = DeviceData {
|
||||
session_id: SessionId::generate().0,
|
||||
user_agent: extract_user_agent(&headers),
|
||||
ip_address: extract_client_ip(&headers),
|
||||
last_seen_at: Utc::now(),
|
||||
};
|
||||
|
||||
db::create_device(&state.db, &new_device_id.0, &device_data).await?;
|
||||
db::upsert_account_device(&state.db, &user.did, &new_device_id.0).await?;
|
||||
device_id = Some(new_device_id.0);
|
||||
}
|
||||
|
||||
db::update_authorization_request(
|
||||
&state.db,
|
||||
&form.request_uri,
|
||||
&user.did,
|
||||
device_id.as_deref(),
|
||||
&code.0,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let redirect_uri = &request_data.parameters.redirect_uri;
|
||||
let mut redirect_url = redirect_uri.to_string();
|
||||
|
||||
let separator = if redirect_url.contains('?') { '&' } else { '?' };
|
||||
redirect_url.push(separator);
|
||||
redirect_url.push_str(&format!("code={}", url_encode(&code.0)));
|
||||
|
||||
if let Some(state) = &request_data.parameters.state {
|
||||
redirect_url.push_str(&format!("&state={}", url_encode(state)));
|
||||
}
|
||||
|
||||
let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
|
||||
redirect_url.push_str(&format!("&iss={}", url_encode(&format!("https://{}", pds_hostname))));
|
||||
|
||||
Ok(Redirect::temporary(&redirect_url).into_response())
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct AuthorizeDenyResponse {
|
||||
pub error: String,
|
||||
pub error_description: String,
|
||||
}
|
||||
|
||||
pub async fn authorize_deny(
|
||||
State(state): State<AppState>,
|
||||
Form(form): Form<AuthorizeDenyForm>,
|
||||
) -> Result<Response, OAuthError> {
|
||||
let request_data = db::get_authorization_request(&state.db, &form.request_uri)
|
||||
.await?
|
||||
.ok_or_else(|| OAuthError::InvalidRequest("Invalid request_uri".to_string()))?;
|
||||
|
||||
db::delete_authorization_request(&state.db, &form.request_uri).await?;
|
||||
|
||||
let redirect_uri = &request_data.parameters.redirect_uri;
|
||||
let mut redirect_url = redirect_uri.to_string();
|
||||
|
||||
let separator = if redirect_url.contains('?') { '&' } else { '?' };
|
||||
redirect_url.push(separator);
|
||||
redirect_url.push_str("error=access_denied");
|
||||
redirect_url.push_str("&error_description=User%20denied%20the%20request");
|
||||
|
||||
if let Some(state) = &request_data.parameters.state {
|
||||
redirect_url.push_str(&format!("&state={}", url_encode(state)));
|
||||
}
|
||||
|
||||
Ok(Redirect::temporary(&redirect_url).into_response())
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct AuthorizeDenyForm {
|
||||
pub request_uri: String,
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
use axum::{Json, extract::State};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::state::AppState;
|
||||
use crate::oauth::jwks::{JwkSet, create_jwk_set};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct ProtectedResourceMetadata {
|
||||
pub resource: String,
|
||||
pub authorization_servers: Vec<String>,
|
||||
pub bearer_methods_supported: Vec<String>,
|
||||
pub scopes_supported: Vec<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub resource_documentation: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct AuthorizationServerMetadata {
|
||||
pub issuer: String,
|
||||
pub authorization_endpoint: String,
|
||||
pub token_endpoint: String,
|
||||
pub jwks_uri: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub registration_endpoint: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub scopes_supported: Option<Vec<String>>,
|
||||
pub response_types_supported: Vec<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub response_modes_supported: Option<Vec<String>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub grant_types_supported: Option<Vec<String>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub token_endpoint_auth_methods_supported: Option<Vec<String>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub code_challenge_methods_supported: Option<Vec<String>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub pushed_authorization_request_endpoint: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub require_pushed_authorization_requests: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub dpop_signing_alg_values_supported: Option<Vec<String>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub authorization_response_iss_parameter_supported: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub revocation_endpoint: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub introspection_endpoint: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn oauth_protected_resource(
|
||||
State(_state): State<AppState>,
|
||||
) -> Json<ProtectedResourceMetadata> {
|
||||
let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
|
||||
let public_url = format!("https://{}", pds_hostname);
|
||||
|
||||
Json(ProtectedResourceMetadata {
|
||||
resource: public_url.clone(),
|
||||
authorization_servers: vec![public_url],
|
||||
bearer_methods_supported: vec!["header".to_string()],
|
||||
scopes_supported: vec![],
|
||||
resource_documentation: Some("https://atproto.com".to_string()),
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn oauth_authorization_server(
|
||||
State(_state): State<AppState>,
|
||||
) -> Json<AuthorizationServerMetadata> {
|
||||
let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
|
||||
let issuer = format!("https://{}", pds_hostname);
|
||||
|
||||
Json(AuthorizationServerMetadata {
|
||||
issuer: issuer.clone(),
|
||||
authorization_endpoint: format!("{}/oauth/authorize", issuer),
|
||||
token_endpoint: format!("{}/oauth/token", issuer),
|
||||
jwks_uri: format!("{}/oauth/jwks", issuer),
|
||||
registration_endpoint: None,
|
||||
scopes_supported: Some(vec![
|
||||
"atproto".to_string(),
|
||||
"transition:generic".to_string(),
|
||||
"transition:chat.bsky".to_string(),
|
||||
]),
|
||||
response_types_supported: vec!["code".to_string()],
|
||||
response_modes_supported: Some(vec!["query".to_string(), "fragment".to_string()]),
|
||||
grant_types_supported: Some(vec![
|
||||
"authorization_code".to_string(),
|
||||
"refresh_token".to_string(),
|
||||
]),
|
||||
token_endpoint_auth_methods_supported: Some(vec![
|
||||
"none".to_string(),
|
||||
"private_key_jwt".to_string(),
|
||||
]),
|
||||
code_challenge_methods_supported: Some(vec!["S256".to_string()]),
|
||||
pushed_authorization_request_endpoint: Some(format!("{}/oauth/par", issuer)),
|
||||
require_pushed_authorization_requests: Some(true),
|
||||
dpop_signing_alg_values_supported: Some(vec![
|
||||
"ES256".to_string(),
|
||||
"ES384".to_string(),
|
||||
"ES512".to_string(),
|
||||
"EdDSA".to_string(),
|
||||
]),
|
||||
authorization_response_iss_parameter_supported: Some(true),
|
||||
revocation_endpoint: Some(format!("{}/oauth/revoke", issuer)),
|
||||
introspection_endpoint: Some(format!("{}/oauth/introspect", issuer)),
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn oauth_jwks(State(_state): State<AppState>) -> Json<JwkSet> {
|
||||
use crate::config::AuthConfig;
|
||||
use crate::oauth::jwks::Jwk;
|
||||
|
||||
let config = AuthConfig::get();
|
||||
|
||||
let server_key = Jwk {
|
||||
kty: "EC".to_string(),
|
||||
key_use: Some("sig".to_string()),
|
||||
kid: Some(config.signing_key_id.clone()),
|
||||
alg: Some("ES256".to_string()),
|
||||
crv: Some("P-256".to_string()),
|
||||
x: Some(config.signing_key_x.clone()),
|
||||
y: Some(config.signing_key_y.clone()),
|
||||
};
|
||||
|
||||
Json(create_jwk_set(vec![server_key]))
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
pub mod metadata;
|
||||
pub mod par;
|
||||
pub mod authorize;
|
||||
pub mod token;
|
||||
|
||||
pub use metadata::*;
|
||||
pub use par::*;
|
||||
pub use authorize::*;
|
||||
pub use token::*;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user