You've already forked authentication-service
mirror of
https://github.com/matrix-org/matrix-authentication-service.git
synced 2025-07-31 09:24:31 +03:00
Refactor authorization grant
The authorization grant is now properly separated from the OAuth2 session, which helps avoiding a lot of potential database inconsistencies
This commit is contained in:
103
crates/core/migrations/20211021201500_oauth2_sessions.up.sql
Normal file
103
crates/core/migrations/20211021201500_oauth2_sessions.up.sql
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
-- Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
--
|
||||||
|
-- Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
-- you may not use this file except in compliance with the License.
|
||||||
|
-- You may obtain a copy of the License at
|
||||||
|
--
|
||||||
|
-- http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
--
|
||||||
|
-- Unless required by applicable law or agreed to in writing, software
|
||||||
|
-- distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
-- See the License for the specific language governing permissions and
|
||||||
|
-- limitations under the License.
|
||||||
|
|
||||||
|
|
||||||
|
-- Replace the old "sessions" table
|
||||||
|
ALTER TABLE oauth2_sessions RENAME TO oauth2_sessions_old;
|
||||||
|
|
||||||
|
-- TODO: how do we handle temporary session upgrades (aka. sudo mode)?
|
||||||
|
CREATE TABLE oauth2_sessions (
|
||||||
|
"id" BIGSERIAL PRIMARY KEY,
|
||||||
|
"user_session_id" BIGINT NOT NULL REFERENCES user_sessions (id) ON DELETE CASCADE,
|
||||||
|
"client_id" TEXT NOT NULL, -- The "authorization party" would be more accurate in that case
|
||||||
|
"scope" TEXT NOT NULL,
|
||||||
|
|
||||||
|
"created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
TRUNCATE oauth2_access_tokens, oauth2_refresh_tokens;
|
||||||
|
ALTER TABLE oauth2_access_tokens
|
||||||
|
DROP CONSTRAINT oauth2_access_tokens_oauth2_session_id_fkey,
|
||||||
|
ADD CONSTRAINT oauth2_access_tokens_oauth2_session_id_fkey
|
||||||
|
FOREIGN KEY (oauth2_session_id) REFERENCES oauth2_sessions (id);
|
||||||
|
ALTER TABLE oauth2_refresh_tokens
|
||||||
|
DROP CONSTRAINT oauth2_refresh_tokens_oauth2_session_id_fkey,
|
||||||
|
ADD CONSTRAINT oauth2_refresh_tokens_oauth2_session_id_fkey
|
||||||
|
FOREIGN KEY (oauth2_session_id) REFERENCES oauth2_sessions (id);
|
||||||
|
DROP TABLE oauth2_codes, oauth2_sessions_old;
|
||||||
|
|
||||||
|
CREATE TABLE oauth2_authorization_grants (
|
||||||
|
"id" BIGSERIAL PRIMARY KEY, -- Saved as encrypted cookie
|
||||||
|
|
||||||
|
-- All this comes from the authorization request
|
||||||
|
"client_id" TEXT NOT NULL, -- This should be verified before insertion
|
||||||
|
"redirect_uri" TEXT NOT NULL, -- This should be verified before insertion
|
||||||
|
"scope" TEXT NOT NULL, -- This should be verified before insertion
|
||||||
|
"state" TEXT,
|
||||||
|
"nonce" TEXT,
|
||||||
|
"max_age" INT CHECK ("max_age" IS NULL OR "max_age" > 0),
|
||||||
|
"acr_values" TEXT, -- This should be verified before insertion
|
||||||
|
"response_mode" TEXT NOT NULL,
|
||||||
|
"code_challenge_method" TEXT,
|
||||||
|
"code_challenge" TEXT,
|
||||||
|
|
||||||
|
-- The "response_type" parameter broken down
|
||||||
|
"response_type_code" BOOLEAN NOT NULL,
|
||||||
|
"response_type_token" BOOLEAN NOT NULL,
|
||||||
|
"response_type_id_token" BOOLEAN NOT NULL,
|
||||||
|
|
||||||
|
-- This one is created eagerly on grant creation if the response_type
|
||||||
|
-- includes "code"
|
||||||
|
-- When looking up codes, it should do "where fulfilled_at is not null" and
|
||||||
|
-- "inner join on oauth2_sessions". When doing that, it should check the
|
||||||
|
-- "exchanged_at" field: if it is not null and was exchanged more than 30s
|
||||||
|
-- ago, the session shold be considered as hijacked and fully invalidated
|
||||||
|
"code" TEXT UNIQUE,
|
||||||
|
|
||||||
|
"created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
|
||||||
|
"fulfilled_at" TIMESTAMP WITH TIME ZONE, -- When we got back to the client
|
||||||
|
"cancelled_at" TIMESTAMP WITH TIME ZONE, -- When that grant was cancelled
|
||||||
|
"exchanged_at" TIMESTAMP WITH TIME ZONE, -- When the code was exchanged by the client
|
||||||
|
|
||||||
|
"oauth2_session_id" BIGINT REFERENCES oauth2_sessions (id) ON DELETE CASCADE,
|
||||||
|
|
||||||
|
-- Check a few invariants to keep a coherent state.
|
||||||
|
-- Even though the service should never violate those, it helps ensuring we're not doing anything wrong
|
||||||
|
|
||||||
|
-- Code exchange can only happen after the grant was fulfilled
|
||||||
|
CONSTRAINT "oauth2_authorization_grants_exchanged_after_fullfill"
|
||||||
|
CHECK (("exchanged_at" IS NULL)
|
||||||
|
OR ("exchanged_at" IS NOT NULL AND
|
||||||
|
"fulfilled_at" IS NOT NULL AND
|
||||||
|
"exchanged_at" >= "fulfilled_at")),
|
||||||
|
|
||||||
|
-- A grant can be either fulfilled or cancelled, but not both
|
||||||
|
CONSTRAINT "oauth2_authorization_grants_fulfilled_xor_cancelled"
|
||||||
|
CHECK ("fulfilled_at" IS NULL OR "cancelled_at" IS NULL),
|
||||||
|
|
||||||
|
-- If it was fulfilled there is an oauth2_session_id attached to it
|
||||||
|
CONSTRAINT "oauth2_authorization_grants_fulfilled_and_session"
|
||||||
|
CHECK (("fulfilled_at" IS NULL AND "oauth2_session_id" IS NULL)
|
||||||
|
OR ("fulfilled_at" IS NOT NULL AND "oauth2_session_id" IS NOT NULL)),
|
||||||
|
|
||||||
|
-- We should have a code if and only if the "code" response_type was asked
|
||||||
|
CONSTRAINT "oauth2_authorization_grants_code"
|
||||||
|
CHECK (("response_type_code" IS TRUE AND "code" IS NOT NULL)
|
||||||
|
OR ("response_type_code" IS FALSE AND "code" IS NULL)),
|
||||||
|
|
||||||
|
-- If we have a challenge, we also have a challenge method and a code
|
||||||
|
CONSTRAINT "oauth2_authorization_grants_code_challenge"
|
||||||
|
CHECK (("code_challenge" IS NULL AND "code_challenge_method" IS NULL)
|
||||||
|
OR ("code_challenge" IS NOT NULL AND "code_challenge_method" IS NOT NULL AND "response_type_code" IS TRUE))
|
||||||
|
);
|
@ -54,14 +54,139 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"17729fd0354a84e04bfcd525db6575ed2ba75dd730bea3f2be964f4b347dd484": {
|
"0cc63e00143cf94f63695be24acdcdffd8e8a3da50ea1ddf973a39bc34f861d4": {
|
||||||
"query": "\n SELECT code\n FROM oauth2_codes\n WHERE oauth2_session_id = $1\n ",
|
"query": "\n SELECT\n og.id AS grant_id,\n og.created_at AS grant_created_at,\n og.cancelled_at AS grant_cancelled_at,\n og.fulfilled_at AS grant_fulfilled_at,\n og.exchanged_at AS grant_exchanged_at,\n og.scope AS grant_scope,\n og.state AS grant_state,\n og.redirect_uri AS grant_redirect_uri,\n og.response_mode AS grant_response_mode,\n og.nonce AS grant_nonce,\n og.max_age AS grant_max_age,\n og.acr_values AS grant_acr_values,\n og.client_id AS client_id,\n og.code AS grant_code,\n og.response_type_code AS grant_response_type_code,\n og.response_type_token AS grant_response_type_token,\n og.response_type_id_token AS grant_response_type_id_token,\n og.code_challenge AS grant_code_challenge,\n og.code_challenge_method AS grant_code_challenge_method,\n os.id AS \"session_id?\",\n us.id AS \"user_session_id?\",\n us.created_at AS \"user_session_created_at?\",\n u.id AS \"user_id?\",\n u.username AS \"user_username?\",\n usa.id AS \"user_session_last_authentication_id?\",\n usa.created_at AS \"user_session_last_authentication_created_at?\"\n FROM\n oauth2_authorization_grants og\n LEFT JOIN oauth2_sessions os\n ON os.id = og.oauth2_session_id\n LEFT JOIN user_sessions us\n ON us.id = os.user_session_id\n LEFT JOIN users u\n ON u.id = us.user_id\n LEFT JOIN user_session_authentications usa\n ON usa.session_id = us.id\n WHERE\n og.id = $1\n ",
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [
|
"columns": [
|
||||||
{
|
{
|
||||||
"ordinal": 0,
|
"ordinal": 0,
|
||||||
"name": "code",
|
"name": "grant_id",
|
||||||
|
"type_info": "Int8"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 1,
|
||||||
|
"name": "grant_created_at",
|
||||||
|
"type_info": "Timestamptz"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 2,
|
||||||
|
"name": "grant_cancelled_at",
|
||||||
|
"type_info": "Timestamptz"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 3,
|
||||||
|
"name": "grant_fulfilled_at",
|
||||||
|
"type_info": "Timestamptz"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 4,
|
||||||
|
"name": "grant_exchanged_at",
|
||||||
|
"type_info": "Timestamptz"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 5,
|
||||||
|
"name": "grant_scope",
|
||||||
"type_info": "Text"
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 6,
|
||||||
|
"name": "grant_state",
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 7,
|
||||||
|
"name": "grant_redirect_uri",
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 8,
|
||||||
|
"name": "grant_response_mode",
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 9,
|
||||||
|
"name": "grant_nonce",
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 10,
|
||||||
|
"name": "grant_max_age",
|
||||||
|
"type_info": "Int4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 11,
|
||||||
|
"name": "grant_acr_values",
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 12,
|
||||||
|
"name": "client_id",
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 13,
|
||||||
|
"name": "grant_code",
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 14,
|
||||||
|
"name": "grant_response_type_code",
|
||||||
|
"type_info": "Bool"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 15,
|
||||||
|
"name": "grant_response_type_token",
|
||||||
|
"type_info": "Bool"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 16,
|
||||||
|
"name": "grant_response_type_id_token",
|
||||||
|
"type_info": "Bool"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 17,
|
||||||
|
"name": "grant_code_challenge",
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 18,
|
||||||
|
"name": "grant_code_challenge_method",
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 19,
|
||||||
|
"name": "session_id?",
|
||||||
|
"type_info": "Int8"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 20,
|
||||||
|
"name": "user_session_id?",
|
||||||
|
"type_info": "Int8"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 21,
|
||||||
|
"name": "user_session_created_at?",
|
||||||
|
"type_info": "Timestamptz"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 22,
|
||||||
|
"name": "user_id?",
|
||||||
|
"type_info": "Int8"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 23,
|
||||||
|
"name": "user_username?",
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 24,
|
||||||
|
"name": "user_session_last_authentication_id?",
|
||||||
|
"type_info": "Int8"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 25,
|
||||||
|
"name": "user_session_last_authentication_created_at?",
|
||||||
|
"type_info": "Timestamptz"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"parameters": {
|
"parameters": {
|
||||||
@ -70,42 +195,67 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"nullable": [
|
"nullable": [
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
false
|
false
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"282548c5ad51bd95b7d9ad290714bab5860f1e1291021e7d786dc926d12b5dd9": {
|
"2dbccaf2fb557dd36598bf4d00941280535cc523ac3a481903ed825088901bce": {
|
||||||
"query": "\n SELECT\n oc.id,\n oc.code_challenge,\n oc.code_challenge_method,\n os.id AS \"oauth2_session_id!\",\n os.client_id AS \"client_id!\",\n os.redirect_uri,\n os.scope AS \"scope!\",\n os.nonce,\n us.id AS \"user_session_id?\",\n us.created_at AS \"user_session_created_at?\",\n u.id AS \"user_id?\",\n u.username AS \"user_username?\",\n usa.id AS \"user_session_last_authentication_id?\",\n usa.created_at AS \"user_session_last_authentication_created_at?\"\n FROM oauth2_codes oc\n INNER JOIN oauth2_sessions os\n ON os.id = oc.oauth2_session_id\n LEFT JOIN user_sessions us\n ON us.id = os.user_session_id\n LEFT JOIN user_session_authentications usa\n ON usa.session_id = us.id\n LEFT JOIN users u\n ON u.id = us.user_id\n WHERE oc.code = $1\n ORDER BY usa.created_at DESC\n LIMIT 1\n ",
|
"query": "\n SELECT\n at.id AS \"access_token_id\",\n at.token AS \"access_token\",\n at.expires_after AS \"access_token_expires_after\",\n at.created_at AS \"access_token_created_at\",\n os.id AS \"session_id!\",\n os.client_id AS \"client_id!\",\n os.scope AS \"scope!\",\n us.id AS \"user_session_id!\",\n us.created_at AS \"user_session_created_at!\",\n u.id AS \"user_id!\",\n u.username AS \"user_username!\",\n usa.id AS \"user_session_last_authentication_id?\",\n usa.created_at AS \"user_session_last_authentication_created_at?\"\n\n FROM oauth2_access_tokens at\n INNER JOIN oauth2_sessions os\n ON os.id = at.oauth2_session_id\n INNER JOIN user_sessions us\n ON us.id = os.user_session_id\n INNER JOIN users u\n ON u.id = us.user_id\n LEFT JOIN user_session_authentications usa\n ON usa.session_id = us.id\n\n WHERE at.token = $1\n AND at.created_at + (at.expires_after * INTERVAL '1 second') >= now()\n AND us.active\n\n ORDER BY usa.created_at DESC\n LIMIT 1\n ",
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [
|
"columns": [
|
||||||
{
|
{
|
||||||
"ordinal": 0,
|
"ordinal": 0,
|
||||||
"name": "id",
|
"name": "access_token_id",
|
||||||
"type_info": "Int8"
|
"type_info": "Int8"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ordinal": 1,
|
"ordinal": 1,
|
||||||
"name": "code_challenge",
|
"name": "access_token",
|
||||||
"type_info": "Text"
|
"type_info": "Text"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ordinal": 2,
|
"ordinal": 2,
|
||||||
"name": "code_challenge_method",
|
"name": "access_token_expires_after",
|
||||||
"type_info": "Int2"
|
"type_info": "Int4"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ordinal": 3,
|
"ordinal": 3,
|
||||||
"name": "oauth2_session_id!",
|
"name": "access_token_created_at",
|
||||||
"type_info": "Int8"
|
"type_info": "Timestamptz"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ordinal": 4,
|
"ordinal": 4,
|
||||||
"name": "client_id!",
|
"name": "session_id!",
|
||||||
"type_info": "Text"
|
"type_info": "Int8"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ordinal": 5,
|
"ordinal": 5,
|
||||||
"name": "redirect_uri",
|
"name": "client_id!",
|
||||||
"type_info": "Text"
|
"type_info": "Text"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -115,36 +265,31 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ordinal": 7,
|
"ordinal": 7,
|
||||||
"name": "nonce",
|
"name": "user_session_id!",
|
||||||
"type_info": "Text"
|
"type_info": "Int8"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ordinal": 8,
|
"ordinal": 8,
|
||||||
"name": "user_session_id?",
|
"name": "user_session_created_at!",
|
||||||
"type_info": "Int8"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ordinal": 9,
|
|
||||||
"name": "user_session_created_at?",
|
|
||||||
"type_info": "Timestamptz"
|
"type_info": "Timestamptz"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ordinal": 10,
|
"ordinal": 9,
|
||||||
"name": "user_id?",
|
"name": "user_id!",
|
||||||
"type_info": "Int8"
|
"type_info": "Int8"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ordinal": 11,
|
"ordinal": 10,
|
||||||
"name": "user_username?",
|
"name": "user_username!",
|
||||||
"type_info": "Text"
|
"type_info": "Text"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ordinal": 12,
|
"ordinal": 11,
|
||||||
"name": "user_session_last_authentication_id?",
|
"name": "user_session_last_authentication_id?",
|
||||||
"type_info": "Int8"
|
"type_info": "Int8"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ordinal": 13,
|
"ordinal": 12,
|
||||||
"name": "user_session_last_authentication_created_at?",
|
"name": "user_session_last_authentication_created_at?",
|
||||||
"type_info": "Timestamptz"
|
"type_info": "Timestamptz"
|
||||||
}
|
}
|
||||||
@ -156,13 +301,12 @@
|
|||||||
},
|
},
|
||||||
"nullable": [
|
"nullable": [
|
||||||
false,
|
false,
|
||||||
true,
|
|
||||||
true,
|
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
true,
|
false,
|
||||||
|
false,
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
@ -198,25 +342,41 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"47a7a8d2ef7db8bb1d41230626ded4e4661d488891fbda9b872c0749a9ba58f4": {
|
"38641231a3bff71252e8bc0ead3a033c9148762ea64d707642551c01a4c89b84": {
|
||||||
"query": "\n INSERT INTO oauth2_codes\n (oauth2_session_id, code, code_challenge_method, code_challenge)\n VALUES\n ($1, $2, $3, $4)\n RETURNING\n id\n ",
|
"query": "\n INSERT INTO oauth2_authorization_grants\n (client_id, redirect_uri, scope, state, nonce, max_age,\n acr_values, response_mode, code_challenge, code_challenge_method,\n response_type_code, response_type_token, response_type_id_token,\n code)\n VALUES\n ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)\n RETURNING id, created_at\n ",
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [
|
"columns": [
|
||||||
{
|
{
|
||||||
"ordinal": 0,
|
"ordinal": 0,
|
||||||
"name": "id",
|
"name": "id",
|
||||||
"type_info": "Int8"
|
"type_info": "Int8"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 1,
|
||||||
|
"name": "created_at",
|
||||||
|
"type_info": "Timestamptz"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"Left": [
|
"Left": [
|
||||||
"Int8",
|
|
||||||
"Text",
|
"Text",
|
||||||
"Int2",
|
"Text",
|
||||||
|
"Text",
|
||||||
|
"Text",
|
||||||
|
"Text",
|
||||||
|
"Int4",
|
||||||
|
"Text",
|
||||||
|
"Text",
|
||||||
|
"Text",
|
||||||
|
"Text",
|
||||||
|
"Bool",
|
||||||
|
"Bool",
|
||||||
|
"Bool",
|
||||||
"Text"
|
"Text"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"nullable": [
|
"nullable": [
|
||||||
|
false,
|
||||||
false
|
false
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@ -249,8 +409,18 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"5d032f4bdb28534da7cf8e9806442a12708d632b7be28f8b952bd3cb63a8b1af": {
|
"5d1a17b2ad6153217551ae31549ad9d62cc39d2f9a4e62a7ccb60fd91e0ac685": {
|
||||||
"query": "\n SELECT\n rt.id AS refresh_token_id,\n rt.token AS refresh_token,\n rt.created_at AS refresh_token_created_at,\n at.id AS \"access_token_id?\",\n at.token AS \"access_token?\",\n at.expires_after AS \"access_token_expires_after?\",\n at.created_at AS \"access_token_created_at?\",\n os.id AS \"session_id!\",\n os.client_id AS \"client_id!\",\n os.scope AS \"scope!\",\n os.redirect_uri AS \"redirect_uri!\",\n os.nonce AS \"nonce\",\n us.id AS \"user_session_id!\",\n us.created_at AS \"user_session_created_at!\",\n u.id AS \"user_id!\",\n u.username AS \"user_username!\",\n usa.id AS \"user_session_last_authentication_id?\",\n usa.created_at AS \"user_session_last_authentication_created_at?\"\n FROM oauth2_refresh_tokens rt\n LEFT JOIN oauth2_access_tokens at\n ON at.id = rt.oauth2_access_token_id\n INNER JOIN oauth2_sessions os\n ON os.id = rt.oauth2_session_id\n INNER JOIN user_sessions us\n ON us.id = os.user_session_id\n INNER JOIN users u\n ON u.id = us.user_id\n LEFT JOIN user_session_authentications usa\n ON usa.session_id = us.id\n\n WHERE rt.token = $1\n AND rt.next_token_id IS NULL\n AND us.active\n\n ORDER BY usa.created_at DESC\n LIMIT 1\n ",
|
"query": "\n DELETE FROM oauth2_access_tokens\n WHERE created_at + (expires_after * INTERVAL '1 second') + INTERVAL '15 minutes' < now()\n ",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Left": []
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"6765e725d31a1490ddee3f28e32dea41abdd9acefb1edd9a7b4e6790ec131173": {
|
||||||
|
"query": "\n SELECT\n rt.id AS refresh_token_id,\n rt.token AS refresh_token,\n rt.created_at AS refresh_token_created_at,\n at.id AS \"access_token_id?\",\n at.token AS \"access_token?\",\n at.expires_after AS \"access_token_expires_after?\",\n at.created_at AS \"access_token_created_at?\",\n os.id AS \"session_id!\",\n os.client_id AS \"client_id!\",\n os.scope AS \"scope!\",\n us.id AS \"user_session_id!\",\n us.created_at AS \"user_session_created_at!\",\n u.id AS \"user_id!\",\n u.username AS \"user_username!\",\n usa.id AS \"user_session_last_authentication_id?\",\n usa.created_at AS \"user_session_last_authentication_created_at?\"\n FROM oauth2_refresh_tokens rt\n LEFT JOIN oauth2_access_tokens at\n ON at.id = rt.oauth2_access_token_id\n INNER JOIN oauth2_sessions os\n ON os.id = rt.oauth2_session_id\n INNER JOIN user_sessions us\n ON us.id = os.user_session_id\n INNER JOIN users u\n ON u.id = us.user_id\n LEFT JOIN user_session_authentications usa\n ON usa.session_id = us.id\n\n WHERE rt.token = $1\n AND rt.next_token_id IS NULL\n AND us.active\n\n ORDER BY usa.created_at DESC\n LIMIT 1\n ",
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [
|
"columns": [
|
||||||
{
|
{
|
||||||
@ -305,41 +475,31 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ordinal": 10,
|
"ordinal": 10,
|
||||||
"name": "redirect_uri!",
|
|
||||||
"type_info": "Text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ordinal": 11,
|
|
||||||
"name": "nonce",
|
|
||||||
"type_info": "Text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ordinal": 12,
|
|
||||||
"name": "user_session_id!",
|
"name": "user_session_id!",
|
||||||
"type_info": "Int8"
|
"type_info": "Int8"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ordinal": 13,
|
"ordinal": 11,
|
||||||
"name": "user_session_created_at!",
|
"name": "user_session_created_at!",
|
||||||
"type_info": "Timestamptz"
|
"type_info": "Timestamptz"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ordinal": 14,
|
"ordinal": 12,
|
||||||
"name": "user_id!",
|
"name": "user_id!",
|
||||||
"type_info": "Int8"
|
"type_info": "Int8"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ordinal": 15,
|
"ordinal": 13,
|
||||||
"name": "user_username!",
|
"name": "user_username!",
|
||||||
"type_info": "Text"
|
"type_info": "Text"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ordinal": 16,
|
"ordinal": 14,
|
||||||
"name": "user_session_last_authentication_id?",
|
"name": "user_session_last_authentication_id?",
|
||||||
"type_info": "Int8"
|
"type_info": "Int8"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ordinal": 17,
|
"ordinal": 15,
|
||||||
"name": "user_session_last_authentication_created_at?",
|
"name": "user_session_last_authentication_created_at?",
|
||||||
"type_info": "Timestamptz"
|
"type_info": "Timestamptz"
|
||||||
}
|
}
|
||||||
@ -361,8 +521,6 @@
|
|||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
true,
|
|
||||||
false,
|
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
@ -371,117 +529,24 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"5d1a17b2ad6153217551ae31549ad9d62cc39d2f9a4e62a7ccb60fd91e0ac685": {
|
"703850ba4e001d53776d77a64cbc1ee6feb61485ce41aff1103251f9b3778128": {
|
||||||
"query": "\n DELETE FROM oauth2_access_tokens\n WHERE created_at + (expires_after * INTERVAL '1 second') + INTERVAL '15 minutes' < now()\n ",
|
"query": "\n UPDATE oauth2_authorization_grants AS og\n SET\n oauth2_session_id = os.id,\n fulfilled_at = os.created_at\n FROM oauth2_sessions os\n WHERE\n og.id = $1 AND os.id = $2\n RETURNING fulfilled_at AS \"fulfilled_at!: DateTime<Utc>\"\n ",
|
||||||
"describe": {
|
|
||||||
"columns": [],
|
|
||||||
"parameters": {
|
|
||||||
"Left": []
|
|
||||||
},
|
|
||||||
"nullable": []
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"686a796a7de689b73a9377083718c95ac5ac51ce396dcf32e614402051d93e16": {
|
|
||||||
"query": "\n SELECT\n at.id AS \"access_token_id\",\n at.token AS \"access_token\",\n at.expires_after AS \"access_token_expires_after\",\n at.created_at AS \"access_token_created_at\",\n os.id AS \"session_id!\",\n os.client_id AS \"client_id!\",\n os.scope AS \"scope!\",\n os.redirect_uri AS \"redirect_uri!\",\n os.nonce AS \"nonce\",\n us.id AS \"user_session_id!\",\n us.created_at AS \"user_session_created_at!\",\n u.id AS \"user_id!\",\n u.username AS \"user_username!\",\n usa.id AS \"user_session_last_authentication_id?\",\n usa.created_at AS \"user_session_last_authentication_created_at?\"\n\n FROM oauth2_access_tokens at\n INNER JOIN oauth2_sessions os\n ON os.id = at.oauth2_session_id\n INNER JOIN user_sessions us\n ON us.id = os.user_session_id\n INNER JOIN users u\n ON u.id = us.user_id\n LEFT JOIN user_session_authentications usa\n ON usa.session_id = us.id\n\n WHERE at.token = $1\n AND at.created_at + (at.expires_after * INTERVAL '1 second') >= now()\n AND us.active\n\n ORDER BY usa.created_at DESC\n LIMIT 1\n ",
|
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [
|
"columns": [
|
||||||
{
|
{
|
||||||
"ordinal": 0,
|
"ordinal": 0,
|
||||||
"name": "access_token_id",
|
"name": "fulfilled_at!: DateTime<Utc>",
|
||||||
"type_info": "Int8"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ordinal": 1,
|
|
||||||
"name": "access_token",
|
|
||||||
"type_info": "Text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ordinal": 2,
|
|
||||||
"name": "access_token_expires_after",
|
|
||||||
"type_info": "Int4"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ordinal": 3,
|
|
||||||
"name": "access_token_created_at",
|
|
||||||
"type_info": "Timestamptz"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ordinal": 4,
|
|
||||||
"name": "session_id!",
|
|
||||||
"type_info": "Int8"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ordinal": 5,
|
|
||||||
"name": "client_id!",
|
|
||||||
"type_info": "Text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ordinal": 6,
|
|
||||||
"name": "scope!",
|
|
||||||
"type_info": "Text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ordinal": 7,
|
|
||||||
"name": "redirect_uri!",
|
|
||||||
"type_info": "Text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ordinal": 8,
|
|
||||||
"name": "nonce",
|
|
||||||
"type_info": "Text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ordinal": 9,
|
|
||||||
"name": "user_session_id!",
|
|
||||||
"type_info": "Int8"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ordinal": 10,
|
|
||||||
"name": "user_session_created_at!",
|
|
||||||
"type_info": "Timestamptz"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ordinal": 11,
|
|
||||||
"name": "user_id!",
|
|
||||||
"type_info": "Int8"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ordinal": 12,
|
|
||||||
"name": "user_username!",
|
|
||||||
"type_info": "Text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ordinal": 13,
|
|
||||||
"name": "user_session_last_authentication_id?",
|
|
||||||
"type_info": "Int8"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ordinal": 14,
|
|
||||||
"name": "user_session_last_authentication_created_at?",
|
|
||||||
"type_info": "Timestamptz"
|
"type_info": "Timestamptz"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"Left": [
|
"Left": [
|
||||||
"Text"
|
"Int8",
|
||||||
|
"Int8"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"nullable": [
|
"nullable": [
|
||||||
false,
|
true
|
||||||
false,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
true,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
false
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -547,6 +612,176 @@
|
|||||||
"nullable": []
|
"nullable": []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"8dde452a37c8faad20df68eb2b665202e0fb6b4ce805138e5f19d4e7eb0ce802": {
|
||||||
|
"query": "\n SELECT\n og.id AS grant_id,\n og.created_at AS grant_created_at,\n og.cancelled_at AS grant_cancelled_at,\n og.fulfilled_at AS grant_fulfilled_at,\n og.exchanged_at AS grant_exchanged_at,\n og.scope AS grant_scope,\n og.state AS grant_state,\n og.redirect_uri AS grant_redirect_uri,\n og.response_mode AS grant_response_mode,\n og.nonce AS grant_nonce,\n og.max_age AS grant_max_age,\n og.acr_values AS grant_acr_values,\n og.client_id AS client_id,\n og.code AS grant_code,\n og.response_type_code AS grant_response_type_code,\n og.response_type_token AS grant_response_type_token,\n og.response_type_id_token AS grant_response_type_id_token,\n og.code_challenge AS grant_code_challenge,\n og.code_challenge_method AS grant_code_challenge_method,\n os.id AS \"session_id?\",\n us.id AS \"user_session_id?\",\n us.created_at AS \"user_session_created_at?\",\n u.id AS \"user_id?\",\n u.username AS \"user_username?\",\n usa.id AS \"user_session_last_authentication_id?\",\n usa.created_at AS \"user_session_last_authentication_created_at?\"\n FROM\n oauth2_authorization_grants og\n LEFT JOIN oauth2_sessions os\n ON os.id = og.oauth2_session_id\n LEFT JOIN user_sessions us\n ON us.id = os.user_session_id\n LEFT JOIN users u\n ON u.id = us.user_id\n LEFT JOIN user_session_authentications usa\n ON usa.session_id = us.id\n WHERE\n og.code = $1\n ",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"ordinal": 0,
|
||||||
|
"name": "grant_id",
|
||||||
|
"type_info": "Int8"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 1,
|
||||||
|
"name": "grant_created_at",
|
||||||
|
"type_info": "Timestamptz"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 2,
|
||||||
|
"name": "grant_cancelled_at",
|
||||||
|
"type_info": "Timestamptz"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 3,
|
||||||
|
"name": "grant_fulfilled_at",
|
||||||
|
"type_info": "Timestamptz"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 4,
|
||||||
|
"name": "grant_exchanged_at",
|
||||||
|
"type_info": "Timestamptz"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 5,
|
||||||
|
"name": "grant_scope",
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 6,
|
||||||
|
"name": "grant_state",
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 7,
|
||||||
|
"name": "grant_redirect_uri",
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 8,
|
||||||
|
"name": "grant_response_mode",
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 9,
|
||||||
|
"name": "grant_nonce",
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 10,
|
||||||
|
"name": "grant_max_age",
|
||||||
|
"type_info": "Int4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 11,
|
||||||
|
"name": "grant_acr_values",
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 12,
|
||||||
|
"name": "client_id",
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 13,
|
||||||
|
"name": "grant_code",
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 14,
|
||||||
|
"name": "grant_response_type_code",
|
||||||
|
"type_info": "Bool"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 15,
|
||||||
|
"name": "grant_response_type_token",
|
||||||
|
"type_info": "Bool"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 16,
|
||||||
|
"name": "grant_response_type_id_token",
|
||||||
|
"type_info": "Bool"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 17,
|
||||||
|
"name": "grant_code_challenge",
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 18,
|
||||||
|
"name": "grant_code_challenge_method",
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 19,
|
||||||
|
"name": "session_id?",
|
||||||
|
"type_info": "Int8"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 20,
|
||||||
|
"name": "user_session_id?",
|
||||||
|
"type_info": "Int8"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 21,
|
||||||
|
"name": "user_session_created_at?",
|
||||||
|
"type_info": "Timestamptz"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 22,
|
||||||
|
"name": "user_id?",
|
||||||
|
"type_info": "Int8"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 23,
|
||||||
|
"name": "user_username?",
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 24,
|
||||||
|
"name": "user_session_last_authentication_id?",
|
||||||
|
"type_info": "Int8"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 25,
|
||||||
|
"name": "user_session_last_authentication_created_at?",
|
||||||
|
"type_info": "Timestamptz"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Left": [
|
||||||
|
"Text"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
"a09dfe1019110f2ec6eba0d35bafa467ab4b7980dd8b556826f03863f8edb0ab": {
|
"a09dfe1019110f2ec6eba0d35bafa467ab4b7980dd8b556826f03863f8edb0ab": {
|
||||||
"query": "UPDATE user_sessions SET active = FALSE WHERE id = $1",
|
"query": "UPDATE user_sessions SET active = FALSE WHERE id = $1",
|
||||||
"describe": {
|
"describe": {
|
||||||
@ -579,17 +814,31 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"a6eb935107d060dd01bf9824ceff87b9ff5492b58cefef002a49f444d3a3daa1": {
|
"c29e741474aacc91c0aacc028a9e7452a5327d5ce6d4b791bf20a2636069087e": {
|
||||||
"query": "UPDATE oauth2_sessions SET user_session_id = $1 WHERE id = $2",
|
"query": "\n INSERT INTO oauth2_sessions\n (user_session_id, client_id, scope)\n SELECT\n $1,\n og.client_id,\n og.scope\n FROM\n oauth2_authorization_grants og\n WHERE\n og.id = $2\n RETURNING id, created_at\n ",
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [],
|
"columns": [
|
||||||
|
{
|
||||||
|
"ordinal": 0,
|
||||||
|
"name": "id",
|
||||||
|
"type_info": "Int8"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 1,
|
||||||
|
"name": "created_at",
|
||||||
|
"type_info": "Timestamptz"
|
||||||
|
}
|
||||||
|
],
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"Left": [
|
"Left": [
|
||||||
"Int8",
|
"Int8",
|
||||||
"Int8"
|
"Int8"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"nullable": []
|
"nullable": [
|
||||||
|
false,
|
||||||
|
false
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"c2c402cfe0adcafa615f14a499caba4c96ca71d9ffb163e1feb05e5d85f3462c": {
|
"c2c402cfe0adcafa615f14a499caba4c96ca71d9ffb163e1feb05e5d85f3462c": {
|
||||||
@ -605,97 +854,23 @@
|
|||||||
"nullable": []
|
"nullable": []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"cacec823f5d4ed886854fbd62b5f5bb2def792582df58c8a047c769d34d9b190": {
|
"d604e13bdfb2ff3d354d995f0b68f04091847755db98bafea7c45bd7b5c4ab68": {
|
||||||
"query": "\n INSERT INTO oauth2_sessions\n (user_session_id, client_id, redirect_uri, scope, state, nonce, max_age,\n response_type, response_mode)\n VALUES\n ($1, $2, $3, $4, $5, $6, $7, $8, $9)\n RETURNING\n id, user_session_id, client_id, redirect_uri, scope, state, nonce, max_age,\n response_type, response_mode, created_at, updated_at\n ",
|
"query": "\n UPDATE oauth2_authorization_grants\n SET\n exchanged_at = NOW()\n WHERE\n id = $1\n RETURNING exchanged_at AS \"exchanged_at!: DateTime<Utc>\"\n ",
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [
|
"columns": [
|
||||||
{
|
{
|
||||||
"ordinal": 0,
|
"ordinal": 0,
|
||||||
"name": "id",
|
"name": "exchanged_at!: DateTime<Utc>",
|
||||||
"type_info": "Int8"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ordinal": 1,
|
|
||||||
"name": "user_session_id",
|
|
||||||
"type_info": "Int8"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ordinal": 2,
|
|
||||||
"name": "client_id",
|
|
||||||
"type_info": "Text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ordinal": 3,
|
|
||||||
"name": "redirect_uri",
|
|
||||||
"type_info": "Text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ordinal": 4,
|
|
||||||
"name": "scope",
|
|
||||||
"type_info": "Text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ordinal": 5,
|
|
||||||
"name": "state",
|
|
||||||
"type_info": "Text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ordinal": 6,
|
|
||||||
"name": "nonce",
|
|
||||||
"type_info": "Text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ordinal": 7,
|
|
||||||
"name": "max_age",
|
|
||||||
"type_info": "Int4"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ordinal": 8,
|
|
||||||
"name": "response_type",
|
|
||||||
"type_info": "Text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ordinal": 9,
|
|
||||||
"name": "response_mode",
|
|
||||||
"type_info": "Text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ordinal": 10,
|
|
||||||
"name": "created_at",
|
|
||||||
"type_info": "Timestamptz"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ordinal": 11,
|
|
||||||
"name": "updated_at",
|
|
||||||
"type_info": "Timestamptz"
|
"type_info": "Timestamptz"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"Left": [
|
"Left": [
|
||||||
"Int8",
|
"Int8"
|
||||||
"Text",
|
|
||||||
"Text",
|
|
||||||
"Text",
|
|
||||||
"Text",
|
|
||||||
"Text",
|
|
||||||
"Int4",
|
|
||||||
"Text",
|
|
||||||
"Text"
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"nullable": [
|
"nullable": [
|
||||||
false,
|
true
|
||||||
true,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
true,
|
|
||||||
true,
|
|
||||||
true,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
false
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -725,18 +900,6 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"eaddc1e33715ad31b4195fda72dbe870f179dd8da53a88d0543b72a278ed1d3d": {
|
|
||||||
"query": "\n DELETE FROM oauth2_codes\n WHERE id = $1\n ",
|
|
||||||
"describe": {
|
|
||||||
"columns": [],
|
|
||||||
"parameters": {
|
|
||||||
"Left": [
|
|
||||||
"Int8"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"nullable": []
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"f9a09ff53b6f221649f4f050e3d5ade114f852ddf50a78610a6c0ef0689af681": {
|
"f9a09ff53b6f221649f4f050e3d5ade114f852ddf50a78610a6c0ef0689af681": {
|
||||||
"query": "\n INSERT INTO users (username, hashed_password)\n VALUES ($1, $2)\n RETURNING id\n ",
|
"query": "\n INSERT INTO users (username, hashed_password)\n VALUES ($1, $2)\n RETURNING id\n ",
|
||||||
"describe": {
|
"describe": {
|
||||||
@ -757,91 +920,5 @@
|
|||||||
false
|
false
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"ff515ebb80ba4af1948472f5c7120a03e25b1ebe42151b8a2036bfbb042f17f6": {
|
|
||||||
"query": "\n SELECT\n id, user_session_id, client_id, redirect_uri, scope, state, nonce,\n max_age, response_type, response_mode, created_at, updated_at\n FROM oauth2_sessions\n WHERE id = $1\n ",
|
|
||||||
"describe": {
|
|
||||||
"columns": [
|
|
||||||
{
|
|
||||||
"ordinal": 0,
|
|
||||||
"name": "id",
|
|
||||||
"type_info": "Int8"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ordinal": 1,
|
|
||||||
"name": "user_session_id",
|
|
||||||
"type_info": "Int8"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ordinal": 2,
|
|
||||||
"name": "client_id",
|
|
||||||
"type_info": "Text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ordinal": 3,
|
|
||||||
"name": "redirect_uri",
|
|
||||||
"type_info": "Text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ordinal": 4,
|
|
||||||
"name": "scope",
|
|
||||||
"type_info": "Text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ordinal": 5,
|
|
||||||
"name": "state",
|
|
||||||
"type_info": "Text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ordinal": 6,
|
|
||||||
"name": "nonce",
|
|
||||||
"type_info": "Text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ordinal": 7,
|
|
||||||
"name": "max_age",
|
|
||||||
"type_info": "Int4"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ordinal": 8,
|
|
||||||
"name": "response_type",
|
|
||||||
"type_info": "Text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ordinal": 9,
|
|
||||||
"name": "response_mode",
|
|
||||||
"type_info": "Text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ordinal": 10,
|
|
||||||
"name": "created_at",
|
|
||||||
"type_info": "Timestamptz"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ordinal": 11,
|
|
||||||
"name": "updated_at",
|
|
||||||
"type_info": "Timestamptz"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"parameters": {
|
|
||||||
"Left": [
|
|
||||||
"Int8"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"nullable": [
|
|
||||||
false,
|
|
||||||
true,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
true,
|
|
||||||
true,
|
|
||||||
true,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
false
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -23,11 +23,12 @@ use hyper::{
|
|||||||
http::uri::{Parts, PathAndQuery, Uri},
|
http::uri::{Parts, PathAndQuery, Uri},
|
||||||
StatusCode,
|
StatusCode,
|
||||||
};
|
};
|
||||||
use itertools::Itertools;
|
use mas_data_model::{
|
||||||
use mas_data_model::BrowserSession;
|
Authentication, AuthorizationCode, AuthorizationGrantStage, BrowserSession, Pkce,
|
||||||
|
};
|
||||||
use mas_templates::{FormPostContext, Templates};
|
use mas_templates::{FormPostContext, Templates};
|
||||||
use oauth2_types::{
|
use oauth2_types::{
|
||||||
errors::{ErrorResponse, InvalidRequest, OAuth2Error},
|
errors::{ErrorResponse, InvalidGrant, InvalidRequest, OAuth2Error},
|
||||||
pkce,
|
pkce,
|
||||||
requests::{
|
requests::{
|
||||||
AccessTokenResponse, AuthorizationRequest, AuthorizationResponse, ResponseMode,
|
AccessTokenResponse, AuthorizationRequest, AuthorizationResponse, ResponseMode,
|
||||||
@ -58,8 +59,10 @@ use crate::{
|
|||||||
storage::{
|
storage::{
|
||||||
oauth2::{
|
oauth2::{
|
||||||
access_token::add_access_token,
|
access_token::add_access_token,
|
||||||
|
authorization_grant::{
|
||||||
|
derive_session, fulfill_grant, get_grant_by_id, new_authorization_grant,
|
||||||
|
},
|
||||||
refresh_token::add_refresh_token,
|
refresh_token::add_refresh_token,
|
||||||
session::{get_session_by_id, start_session},
|
|
||||||
},
|
},
|
||||||
PostgresqlBackend,
|
PostgresqlBackend,
|
||||||
},
|
},
|
||||||
@ -308,13 +311,6 @@ async fn get(
|
|||||||
.ok_or_else(|| anyhow::anyhow!("could not find client"))
|
.ok_or_else(|| anyhow::anyhow!("could not find client"))
|
||||||
.wrap_error()?;
|
.wrap_error()?;
|
||||||
|
|
||||||
let maybe_session_id = maybe_session.as_ref().map(|s| s.data);
|
|
||||||
|
|
||||||
let scope: String = {
|
|
||||||
let it = params.auth.scope.iter().map(ToString::to_string);
|
|
||||||
Itertools::intersperse(it, " ".to_string()).collect()
|
|
||||||
};
|
|
||||||
|
|
||||||
let redirect_uri = client
|
let redirect_uri = client
|
||||||
.resolve_redirect_uri(¶ms.auth.redirect_uri)
|
.resolve_redirect_uri(¶ms.auth.redirect_uri)
|
||||||
.wrap_error()?;
|
.wrap_error()?;
|
||||||
@ -322,23 +318,7 @@ async fn get(
|
|||||||
let response_mode =
|
let response_mode =
|
||||||
resolve_response_mode(response_type, params.auth.response_mode).wrap_error()?;
|
resolve_response_mode(response_type, params.auth.response_mode).wrap_error()?;
|
||||||
|
|
||||||
let oauth2_session = start_session(
|
let code: Option<AuthorizationCode> = if response_type.contains(&ResponseType::Code) {
|
||||||
&mut txn,
|
|
||||||
maybe_session_id,
|
|
||||||
&client.client_id,
|
|
||||||
redirect_uri,
|
|
||||||
&scope,
|
|
||||||
params.auth.state.as_deref(),
|
|
||||||
params.auth.nonce.as_deref(),
|
|
||||||
params.auth.max_age,
|
|
||||||
response_type,
|
|
||||||
response_mode,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.wrap_error()?;
|
|
||||||
|
|
||||||
// Generate the code at this stage, since we have the PKCE params ready
|
|
||||||
if response_type.contains(&ResponseType::Code) {
|
|
||||||
// 32 random alphanumeric characters, about 190bit of entropy
|
// 32 random alphanumeric characters, about 190bit of entropy
|
||||||
let code: String = thread_rng()
|
let code: String = thread_rng()
|
||||||
.sample_iter(&Alphanumeric)
|
.sample_iter(&Alphanumeric)
|
||||||
@ -346,22 +326,47 @@ async fn get(
|
|||||||
.map(char::from)
|
.map(char::from)
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
oauth2_session
|
let pkce = params.pkce.map(|p| Pkce {
|
||||||
.add_code(&mut txn, &code, ¶ms.pkce)
|
challenge: p.code_challenge,
|
||||||
.await
|
challenge_method: p.code_challenge_method,
|
||||||
.wrap_error()?;
|
});
|
||||||
}
|
|
||||||
|
|
||||||
// Do we already have a user session for this oauth2 session?
|
Some(AuthorizationCode { code, pkce })
|
||||||
let user_session = oauth2_session.fetch_session(&mut txn).await.wrap_error()?;
|
} else {
|
||||||
|
// If the request had PKCE params but no code asked, it should get back with an
|
||||||
|
// error
|
||||||
|
if params.pkce.is_some() {
|
||||||
|
return Ok(ReplyOrBackToClient::Error(Box::new(InvalidGrant)));
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(user_session) = user_session {
|
None
|
||||||
step(oauth2_session.id, user_session, txn).await
|
};
|
||||||
|
|
||||||
|
let grant = new_authorization_grant(
|
||||||
|
&mut txn,
|
||||||
|
client.client_id.clone(),
|
||||||
|
redirect_uri.clone(),
|
||||||
|
params.auth.scope,
|
||||||
|
code,
|
||||||
|
params.auth.state,
|
||||||
|
params.auth.nonce,
|
||||||
|
// TODO: support max_age and acr_values
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
response_mode,
|
||||||
|
response_type.contains(&ResponseType::Token),
|
||||||
|
response_type.contains(&ResponseType::IdToken),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.wrap_error()?;
|
||||||
|
|
||||||
|
if let Some(user_session) = maybe_session {
|
||||||
|
step(grant.data, user_session, txn).await
|
||||||
} else {
|
} else {
|
||||||
// If not, redirect the user to the login page
|
// If not, redirect the user to the login page
|
||||||
txn.commit().await.wrap_error()?;
|
txn.commit().await.wrap_error()?;
|
||||||
|
|
||||||
let next = StepRequest::new(oauth2_session.id)
|
let next = StepRequest::new(grant.data)
|
||||||
.build_uri()
|
.build_uri()
|
||||||
.wrap_error()?
|
.wrap_error()?
|
||||||
.to_string();
|
.to_string();
|
||||||
@ -393,85 +398,84 @@ impl StepRequest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn reauth() -> ReplyOrBackToClient {
|
||||||
|
// Ask for a reauth
|
||||||
|
// TODO: have the OAuth2 session ID in there
|
||||||
|
ReplyOrBackToClient::Reply(Box::new(see_other(Uri::from_static("/reauth"))))
|
||||||
|
}
|
||||||
|
|
||||||
async fn step(
|
async fn step(
|
||||||
oauth2_session_id: i64,
|
grant_id: i64,
|
||||||
browser_session: BrowserSession<PostgresqlBackend>,
|
browser_session: BrowserSession<PostgresqlBackend>,
|
||||||
mut txn: Transaction<'_, Postgres>,
|
mut txn: Transaction<'_, Postgres>,
|
||||||
) -> Result<ReplyOrBackToClient, Rejection> {
|
) -> Result<ReplyOrBackToClient, Rejection> {
|
||||||
let mut oauth2_session = get_session_by_id(&mut txn, oauth2_session_id)
|
// TODO: we should check if the grant here was started by the browser doing that
|
||||||
.await
|
// request using a signed cookie
|
||||||
.wrap_error()?;
|
let grant = get_grant_by_id(&mut txn, grant_id).await.wrap_error()?;
|
||||||
|
|
||||||
let user_session = oauth2_session
|
if !matches!(grant.stage, AuthorizationGrantStage::Pending) {
|
||||||
.match_or_set_session(&mut txn, browser_session)
|
return Err(anyhow::anyhow!("authorization grant not pending")).wrap_error();
|
||||||
.await
|
}
|
||||||
.wrap_error()?;
|
|
||||||
|
|
||||||
let response_mode = oauth2_session.response_mode().wrap_error()?;
|
let reply = match browser_session.last_authentication {
|
||||||
let response_type = oauth2_session.response_type().wrap_error()?;
|
Some(Authentication { created_at, .. }) if created_at < grant.max_auth_time() => {
|
||||||
let redirect_uri = oauth2_session.redirect_uri().wrap_error()?;
|
let session = derive_session(&mut txn, &grant, browser_session)
|
||||||
|
.await
|
||||||
|
.wrap_error()?;
|
||||||
|
|
||||||
// Check if the active session is valid
|
let grant = fulfill_grant(&mut txn, grant, session.clone())
|
||||||
// TODO: this is ugly & should check if the session is active
|
.await
|
||||||
let reply = if user_session.last_authentication.map(|x| x.created_at)
|
.wrap_error()?;
|
||||||
>= oauth2_session.max_auth_time()
|
|
||||||
{
|
|
||||||
// Yep! Let's complete the auth now
|
|
||||||
let mut params = AuthorizationResponse::default();
|
|
||||||
|
|
||||||
// Did they request an auth code?
|
// Yep! Let's complete the auth now
|
||||||
if response_type.contains(&ResponseType::Code) {
|
let mut params = AuthorizationResponse::default();
|
||||||
params.code = Some(oauth2_session.fetch_code(&mut txn).await.wrap_error()?);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Did they request an access token?
|
// Did they request an auth code?
|
||||||
if response_type.contains(&ResponseType::Token) {
|
if let Some(code) = grant.code {
|
||||||
let ttl = Duration::minutes(5);
|
params.code = Some(code.code);
|
||||||
let (access_token_str, refresh_token_str) = {
|
}
|
||||||
let mut rng = thread_rng();
|
|
||||||
(
|
|
||||||
AccessToken.generate(&mut rng),
|
|
||||||
RefreshToken.generate(&mut rng),
|
|
||||||
)
|
|
||||||
};
|
|
||||||
|
|
||||||
let access_token =
|
// Did they request an access token?
|
||||||
add_access_token(&mut txn, oauth2_session_id, &access_token_str, ttl)
|
if grant.response_type_token {
|
||||||
|
let ttl = Duration::minutes(5);
|
||||||
|
let (access_token_str, refresh_token_str) = {
|
||||||
|
let mut rng = thread_rng();
|
||||||
|
(
|
||||||
|
AccessToken.generate(&mut rng),
|
||||||
|
RefreshToken.generate(&mut rng),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
let access_token = add_access_token(&mut txn, &session, &access_token_str, ttl)
|
||||||
.await
|
.await
|
||||||
.wrap_error()?;
|
.wrap_error()?;
|
||||||
|
|
||||||
let _refresh_token = add_refresh_token(
|
let _refresh_token =
|
||||||
&mut txn,
|
add_refresh_token(&mut txn, &session, access_token, &refresh_token_str)
|
||||||
oauth2_session_id,
|
.await
|
||||||
access_token,
|
.wrap_error()?;
|
||||||
&refresh_token_str,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.wrap_error()?;
|
|
||||||
|
|
||||||
params.response = Some(
|
params.response = Some(
|
||||||
AccessTokenResponse::new(access_token_str)
|
AccessTokenResponse::new(access_token_str)
|
||||||
.with_expires_in(ttl)
|
.with_expires_in(ttl)
|
||||||
.with_refresh_token(refresh_token_str),
|
.with_refresh_token(refresh_token_str),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Did they request an ID token?
|
// Did they request an ID token?
|
||||||
if response_type.contains(&ResponseType::IdToken) {
|
if grant.response_type_id_token {
|
||||||
todo!("id tokens are not implemented yet");
|
todo!("id tokens are not implemented yet");
|
||||||
}
|
}
|
||||||
|
|
||||||
let params = serde_json::to_value(¶ms).unwrap();
|
let params = serde_json::to_value(¶ms).unwrap();
|
||||||
ReplyOrBackToClient::BackToClient {
|
ReplyOrBackToClient::BackToClient {
|
||||||
redirect_uri,
|
redirect_uri: grant.redirect_uri,
|
||||||
response_mode,
|
response_mode: grant.response_mode,
|
||||||
state: oauth2_session.state.clone(),
|
state: grant.state,
|
||||||
params,
|
params,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
_ => reauth(),
|
||||||
// Ask for a reauth
|
|
||||||
// TODO: have the OAuth2 session ID in there
|
|
||||||
ReplyOrBackToClient::Reply(Box::new(see_other(Uri::from_static("/reauth"))))
|
|
||||||
};
|
};
|
||||||
|
|
||||||
txn.commit().await.wrap_error()?;
|
txn.commit().await.wrap_error()?;
|
||||||
|
@ -94,12 +94,12 @@ async fn introspect(
|
|||||||
active: true,
|
active: true,
|
||||||
scope: Some(session.scope),
|
scope: Some(session.scope),
|
||||||
client_id: Some(session.client.client_id),
|
client_id: Some(session.client.client_id),
|
||||||
username: session.browser_session.clone().map(|s| s.user.username),
|
username: Some(session.browser_session.user.username),
|
||||||
token_type: Some(TokenTypeHint::AccessToken),
|
token_type: Some(TokenTypeHint::AccessToken),
|
||||||
exp: Some(exp),
|
exp: Some(exp),
|
||||||
iat: Some(token.created_at),
|
iat: Some(token.created_at),
|
||||||
nbf: Some(token.created_at),
|
nbf: Some(token.created_at),
|
||||||
sub: session.browser_session.map(|s| s.user.sub),
|
sub: Some(session.browser_session.user.sub),
|
||||||
aud: None,
|
aud: None,
|
||||||
iss: None,
|
iss: None,
|
||||||
jti: None,
|
jti: None,
|
||||||
@ -114,12 +114,12 @@ async fn introspect(
|
|||||||
active: true,
|
active: true,
|
||||||
scope: Some(session.scope),
|
scope: Some(session.scope),
|
||||||
client_id: Some(session.client.client_id),
|
client_id: Some(session.client.client_id),
|
||||||
username: session.browser_session.clone().map(|s| s.user.username),
|
username: Some(session.browser_session.user.username),
|
||||||
token_type: Some(TokenTypeHint::RefreshToken),
|
token_type: Some(TokenTypeHint::RefreshToken),
|
||||||
exp: None,
|
exp: None,
|
||||||
iat: Some(token.created_at),
|
iat: Some(token.created_at),
|
||||||
nbf: Some(token.created_at),
|
nbf: Some(token.created_at),
|
||||||
sub: session.browser_session.map(|s| s.user.sub),
|
sub: Some(session.browser_session.user.sub),
|
||||||
aud: None,
|
aud: None,
|
||||||
iss: None,
|
iss: None,
|
||||||
jti: None,
|
jti: None,
|
||||||
|
@ -18,6 +18,7 @@ use data_encoding::BASE64URL_NOPAD;
|
|||||||
use headers::{CacheControl, Pragma};
|
use headers::{CacheControl, Pragma};
|
||||||
use hyper::{Method, StatusCode};
|
use hyper::{Method, StatusCode};
|
||||||
use jwt_compact::{Claims, Header, TimeOptions};
|
use jwt_compact::{Claims, Header, TimeOptions};
|
||||||
|
use mas_data_model::AuthorizationGrantStage;
|
||||||
use oauth2_types::{
|
use oauth2_types::{
|
||||||
errors::{InvalidGrant, InvalidRequest, OAuth2Error, OAuth2ErrorCode, UnauthorizedClient},
|
errors::{InvalidGrant, InvalidRequest, OAuth2Error, OAuth2ErrorCode, UnauthorizedClient},
|
||||||
requests::{
|
requests::{
|
||||||
@ -30,6 +31,7 @@ use serde::Serialize;
|
|||||||
use serde_with::skip_serializing_none;
|
use serde_with::skip_serializing_none;
|
||||||
use sha2::{Digest, Sha256};
|
use sha2::{Digest, Sha256};
|
||||||
use sqlx::{pool::PoolConnection, Acquire, PgPool, Postgres};
|
use sqlx::{pool::PoolConnection, Acquire, PgPool, Postgres};
|
||||||
|
use tracing::debug;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
use warp::{
|
use warp::{
|
||||||
reject::Reject,
|
reject::Reject,
|
||||||
@ -47,10 +49,15 @@ use crate::{
|
|||||||
with_keys,
|
with_keys,
|
||||||
},
|
},
|
||||||
reply::with_typed_header,
|
reply::with_typed_header,
|
||||||
storage::oauth2::{
|
storage::{
|
||||||
access_token::{add_access_token, revoke_access_token},
|
oauth2::{
|
||||||
authorization_code::{consume_code, lookup_code},
|
access_token::{add_access_token, revoke_access_token},
|
||||||
refresh_token::{add_refresh_token, lookup_active_refresh_token, replace_refresh_token},
|
authorization_grant::{exchange_grant, lookup_grant_by_code},
|
||||||
|
refresh_token::{
|
||||||
|
add_refresh_token, lookup_active_refresh_token, replace_refresh_token,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
DatabaseInconsistencyError,
|
||||||
},
|
},
|
||||||
tokens::{AccessToken, RefreshToken},
|
tokens::{AccessToken, RefreshToken},
|
||||||
};
|
};
|
||||||
@ -156,15 +163,50 @@ async fn authorization_code_grant(
|
|||||||
issuer: Url,
|
issuer: Url,
|
||||||
conn: &mut PoolConnection<Postgres>,
|
conn: &mut PoolConnection<Postgres>,
|
||||||
) -> Result<AccessTokenResponse, Rejection> {
|
) -> Result<AccessTokenResponse, Rejection> {
|
||||||
|
// TODO: there is a bunch of unnecessary cloning here
|
||||||
let mut txn = conn.begin().await.wrap_error()?;
|
let mut txn = conn.begin().await.wrap_error()?;
|
||||||
|
|
||||||
// TODO: we should invalidate the existing session if a code is used twice after
|
// TODO: handle "not found" cases
|
||||||
// some period of time. See the `oidcc-codereuse-30seconds` test from the
|
let authz_grant = lookup_grant_by_code(&mut txn, &grant.code)
|
||||||
// conformance suite
|
.await
|
||||||
let (code, session) = match lookup_code(&mut txn, &grant.code).await {
|
.wrap_error()?;
|
||||||
Err(e) if e.not_found() => return error(InvalidGrant),
|
|
||||||
x => x,
|
let session = match authz_grant.stage {
|
||||||
}?;
|
AuthorizationGrantStage::Cancelled { cancelled_at } => {
|
||||||
|
debug!(%cancelled_at, "Authorization grant was cancelled");
|
||||||
|
return error(InvalidGrant);
|
||||||
|
}
|
||||||
|
AuthorizationGrantStage::Exchanged {
|
||||||
|
exchanged_at,
|
||||||
|
fulfilled_at,
|
||||||
|
session: _,
|
||||||
|
} => {
|
||||||
|
// TODO: we should invalidate the existing session if a code is used twice after
|
||||||
|
// some period of time. See the `oidcc-codereuse-30seconds` test from the
|
||||||
|
// conformance suite
|
||||||
|
debug!(%exchanged_at, %fulfilled_at, "Authorization code was already exchanged");
|
||||||
|
return error(InvalidGrant);
|
||||||
|
}
|
||||||
|
AuthorizationGrantStage::Pending => {
|
||||||
|
debug!("Authorization grant has not been fulfilled yet");
|
||||||
|
return error(InvalidGrant);
|
||||||
|
}
|
||||||
|
AuthorizationGrantStage::Fulfilled {
|
||||||
|
ref session,
|
||||||
|
fulfilled_at: _,
|
||||||
|
} => {
|
||||||
|
// TODO: we should check that the session was not fullfilled too long ago
|
||||||
|
// (30s to 1min?). The main problem is getting a timestamp from the database
|
||||||
|
session
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// This should never happen, since we looked up in the database using the code
|
||||||
|
let code = authz_grant
|
||||||
|
.code
|
||||||
|
.as_ref()
|
||||||
|
.ok_or(DatabaseInconsistencyError)
|
||||||
|
.wrap_error()?;
|
||||||
|
|
||||||
if client.client_id != session.client.client_id {
|
if client.client_id != session.client.client_id {
|
||||||
return error(UnauthorizedClient);
|
return error(UnauthorizedClient);
|
||||||
@ -182,13 +224,7 @@ async fn authorization_code_grant(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO: this should probably not happen?
|
let browser_session = &session.browser_session;
|
||||||
let browser_session = session
|
|
||||||
.browser_session
|
|
||||||
.ok_or_else(|| {
|
|
||||||
anyhow::anyhow!("this oauth2 session has no database session attached to it")
|
|
||||||
})
|
|
||||||
.wrap_error()?;
|
|
||||||
|
|
||||||
let ttl = Duration::minutes(5);
|
let ttl = Duration::minutes(5);
|
||||||
let (access_token_str, refresh_token_str) = {
|
let (access_token_str, refresh_token_str) = {
|
||||||
@ -199,23 +235,22 @@ async fn authorization_code_grant(
|
|||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
let access_token = add_access_token(&mut txn, session.data, &access_token_str, ttl)
|
let access_token = add_access_token(&mut txn, session, &access_token_str, ttl)
|
||||||
.await
|
.await
|
||||||
.wrap_error()?;
|
.wrap_error()?;
|
||||||
|
|
||||||
let _refresh_token =
|
let _refresh_token = add_refresh_token(&mut txn, session, access_token, &refresh_token_str)
|
||||||
add_refresh_token(&mut txn, session.data, access_token, &refresh_token_str)
|
.await
|
||||||
.await
|
.wrap_error()?;
|
||||||
.wrap_error()?;
|
|
||||||
|
|
||||||
let id_token = if session.scope.contains(&OPENID) {
|
let id_token = if session.scope.contains(&OPENID) {
|
||||||
let header = Header::default();
|
let header = Header::default();
|
||||||
let options = TimeOptions::default();
|
let options = TimeOptions::default();
|
||||||
let claims = Claims::new(CustomClaims {
|
let claims = Claims::new(CustomClaims {
|
||||||
issuer,
|
issuer,
|
||||||
subject: browser_session.user.sub,
|
subject: browser_session.user.sub.clone(),
|
||||||
audiences: vec![client.client_id.clone()],
|
audiences: vec![client.client_id.clone()],
|
||||||
nonce: session.nonce,
|
nonce: authz_grant.nonce.clone(),
|
||||||
at_hash: hash(Sha256::new(), &access_token_str).wrap_error()?,
|
at_hash: hash(Sha256::new(), &access_token_str).wrap_error()?,
|
||||||
c_hash: hash(Sha256::new(), &grant.code).wrap_error()?,
|
c_hash: hash(Sha256::new(), &grant.code).wrap_error()?,
|
||||||
})
|
})
|
||||||
@ -234,13 +269,13 @@ async fn authorization_code_grant(
|
|||||||
let mut params = AccessTokenResponse::new(access_token_str)
|
let mut params = AccessTokenResponse::new(access_token_str)
|
||||||
.with_expires_in(ttl)
|
.with_expires_in(ttl)
|
||||||
.with_refresh_token(refresh_token_str)
|
.with_refresh_token(refresh_token_str)
|
||||||
.with_scope(session.scope);
|
.with_scope(session.scope.clone());
|
||||||
|
|
||||||
if let Some(id_token) = id_token {
|
if let Some(id_token) = id_token {
|
||||||
params = params.with_id_token(id_token);
|
params = params.with_id_token(id_token);
|
||||||
}
|
}
|
||||||
|
|
||||||
consume_code(&mut txn, code).await.wrap_error()?;
|
exchange_grant(&mut txn, authz_grant).await.wrap_error()?;
|
||||||
|
|
||||||
txn.commit().await.wrap_error()?;
|
txn.commit().await.wrap_error()?;
|
||||||
|
|
||||||
@ -271,12 +306,12 @@ async fn refresh_token_grant(
|
|||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
let new_access_token = add_access_token(&mut txn, session.data, &access_token_str, ttl)
|
let new_access_token = add_access_token(&mut txn, &session, &access_token_str, ttl)
|
||||||
.await
|
.await
|
||||||
.wrap_error()?;
|
.wrap_error()?;
|
||||||
|
|
||||||
let new_refresh_token =
|
let new_refresh_token =
|
||||||
add_refresh_token(&mut txn, session.data, new_access_token, &refresh_token_str)
|
add_refresh_token(&mut txn, &session, new_access_token, &refresh_token_str)
|
||||||
.await
|
.await
|
||||||
.wrap_error()?;
|
.wrap_error()?;
|
||||||
|
|
||||||
@ -285,7 +320,7 @@ async fn refresh_token_grant(
|
|||||||
.wrap_error()?;
|
.wrap_error()?;
|
||||||
|
|
||||||
if let Some(access_token) = refresh_token.access_token {
|
if let Some(access_token) = refresh_token.access_token {
|
||||||
revoke_access_token(&mut txn, access_token.data)
|
revoke_access_token(&mut txn, &access_token)
|
||||||
.await
|
.await
|
||||||
.wrap_error()?;
|
.wrap_error()?;
|
||||||
}
|
}
|
||||||
|
@ -52,8 +52,7 @@ async fn userinfo(
|
|||||||
_token: AccessToken<PostgresqlBackend>,
|
_token: AccessToken<PostgresqlBackend>,
|
||||||
session: Session<PostgresqlBackend>,
|
session: Session<PostgresqlBackend>,
|
||||||
) -> Result<impl Reply, Rejection> {
|
) -> Result<impl Reply, Rejection> {
|
||||||
// TODO: we really should not have an Option here
|
let user = session.browser_session.user;
|
||||||
let user = session.browser_session.unwrap().user;
|
|
||||||
Ok(warp::reply::json(&UserInfo {
|
Ok(warp::reply::json(&UserInfo {
|
||||||
sub: user.sub,
|
sub: user.sub,
|
||||||
username: user.username,
|
username: user.username,
|
||||||
|
@ -32,7 +32,7 @@ pub struct PostgresqlBackend;
|
|||||||
impl StorageBackend for PostgresqlBackend {
|
impl StorageBackend for PostgresqlBackend {
|
||||||
type AccessTokenData = i64;
|
type AccessTokenData = i64;
|
||||||
type AuthenticationData = i64;
|
type AuthenticationData = i64;
|
||||||
type AuthorizationCodeData = i64;
|
type AuthorizationGrantData = i64;
|
||||||
type BrowserSessionData = i64;
|
type BrowserSessionData = i64;
|
||||||
type ClientData = ();
|
type ClientData = ();
|
||||||
type RefreshTokenData = i64;
|
type RefreshTokenData = i64;
|
||||||
|
@ -24,7 +24,7 @@ use crate::storage::{DatabaseInconsistencyError, IdAndCreationTime, PostgresqlBa
|
|||||||
|
|
||||||
pub async fn add_access_token(
|
pub async fn add_access_token(
|
||||||
executor: impl PgExecutor<'_>,
|
executor: impl PgExecutor<'_>,
|
||||||
oauth2_session_id: i64,
|
session: &Session<PostgresqlBackend>,
|
||||||
token: &str,
|
token: &str,
|
||||||
expires_after: Duration,
|
expires_after: Duration,
|
||||||
) -> anyhow::Result<AccessToken<PostgresqlBackend>> {
|
) -> anyhow::Result<AccessToken<PostgresqlBackend>> {
|
||||||
@ -41,7 +41,7 @@ pub async fn add_access_token(
|
|||||||
RETURNING
|
RETURNING
|
||||||
id, created_at
|
id, created_at
|
||||||
"#,
|
"#,
|
||||||
oauth2_session_id,
|
session.data,
|
||||||
token,
|
token,
|
||||||
expires_after_seconds,
|
expires_after_seconds,
|
||||||
)
|
)
|
||||||
@ -67,8 +67,6 @@ pub struct OAuth2AccessTokenLookup {
|
|||||||
session_id: i64,
|
session_id: i64,
|
||||||
client_id: String,
|
client_id: String,
|
||||||
scope: String,
|
scope: String,
|
||||||
redirect_uri: String,
|
|
||||||
nonce: Option<String>,
|
|
||||||
user_session_id: i64,
|
user_session_id: i64,
|
||||||
user_session_created_at: DateTime<Utc>,
|
user_session_created_at: DateTime<Utc>,
|
||||||
user_id: i64,
|
user_id: i64,
|
||||||
@ -109,8 +107,6 @@ pub async fn lookup_active_access_token(
|
|||||||
os.id AS "session_id!",
|
os.id AS "session_id!",
|
||||||
os.client_id AS "client_id!",
|
os.client_id AS "client_id!",
|
||||||
os.scope AS "scope!",
|
os.scope AS "scope!",
|
||||||
os.redirect_uri AS "redirect_uri!",
|
|
||||||
os.nonce AS "nonce",
|
|
||||||
us.id AS "user_session_id!",
|
us.id AS "user_session_id!",
|
||||||
us.created_at AS "user_session_created_at!",
|
us.created_at AS "user_session_created_at!",
|
||||||
u.id AS "user_id!",
|
u.id AS "user_id!",
|
||||||
@ -171,39 +167,35 @@ pub async fn lookup_active_access_token(
|
|||||||
_ => return Err(DatabaseInconsistencyError.into()),
|
_ => return Err(DatabaseInconsistencyError.into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let browser_session = Some(BrowserSession {
|
let browser_session = BrowserSession {
|
||||||
data: res.user_session_id,
|
data: res.user_session_id,
|
||||||
created_at: res.user_session_created_at,
|
created_at: res.user_session_created_at,
|
||||||
user,
|
user,
|
||||||
last_authentication,
|
last_authentication,
|
||||||
});
|
};
|
||||||
|
|
||||||
let scope = res.scope.parse().map_err(|_e| DatabaseInconsistencyError)?;
|
let scope = res.scope.parse().map_err(|_e| DatabaseInconsistencyError)?;
|
||||||
|
|
||||||
let redirect_uri = res
|
|
||||||
.redirect_uri
|
|
||||||
.parse()
|
|
||||||
.map_err(|_e| DatabaseInconsistencyError)?;
|
|
||||||
|
|
||||||
let session = Session {
|
let session = Session {
|
||||||
data: res.session_id,
|
data: res.session_id,
|
||||||
client,
|
client,
|
||||||
browser_session,
|
browser_session,
|
||||||
scope,
|
scope,
|
||||||
redirect_uri,
|
|
||||||
nonce: res.nonce,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok((access_token, session))
|
Ok((access_token, session))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn revoke_access_token(executor: impl PgExecutor<'_>, id: i64) -> anyhow::Result<()> {
|
pub async fn revoke_access_token(
|
||||||
|
executor: impl PgExecutor<'_>,
|
||||||
|
access_token: &AccessToken<PostgresqlBackend>,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
let res = sqlx::query!(
|
let res = sqlx::query!(
|
||||||
r#"
|
r#"
|
||||||
DELETE FROM oauth2_access_tokens
|
DELETE FROM oauth2_access_tokens
|
||||||
WHERE id = $1
|
WHERE id = $1
|
||||||
"#,
|
"#,
|
||||||
id,
|
access_token.data,
|
||||||
)
|
)
|
||||||
.execute(executor)
|
.execute(executor)
|
||||||
.await
|
.await
|
||||||
|
@ -1,263 +0,0 @@
|
|||||||
// Copyright 2021 The Matrix.org Foundation C.I.C.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
use anyhow::Context;
|
|
||||||
use chrono::{DateTime, Utc};
|
|
||||||
use mas_data_model::{
|
|
||||||
Authentication, AuthorizationCode, BrowserSession, Client, Pkce, Session, User,
|
|
||||||
};
|
|
||||||
use oauth2_types::pkce;
|
|
||||||
use sqlx::PgExecutor;
|
|
||||||
use thiserror::Error;
|
|
||||||
use warp::reject::Reject;
|
|
||||||
|
|
||||||
use crate::storage::{DatabaseInconsistencyError, PostgresqlBackend};
|
|
||||||
|
|
||||||
pub async fn add_code(
|
|
||||||
executor: impl PgExecutor<'_>,
|
|
||||||
oauth2_session_id: i64,
|
|
||||||
code: &str,
|
|
||||||
pkce: &Option<pkce::AuthorizationRequest>,
|
|
||||||
) -> anyhow::Result<AuthorizationCode<PostgresqlBackend>> {
|
|
||||||
let code_challenge_method = pkce.as_ref().map(|c| c.code_challenge_method as i16);
|
|
||||||
let code_challenge = pkce.as_ref().map(|c| &c.code_challenge);
|
|
||||||
let id = sqlx::query_scalar!(
|
|
||||||
r#"
|
|
||||||
INSERT INTO oauth2_codes
|
|
||||||
(oauth2_session_id, code, code_challenge_method, code_challenge)
|
|
||||||
VALUES
|
|
||||||
($1, $2, $3, $4)
|
|
||||||
RETURNING
|
|
||||||
id
|
|
||||||
"#,
|
|
||||||
oauth2_session_id,
|
|
||||||
code,
|
|
||||||
code_challenge_method,
|
|
||||||
code_challenge,
|
|
||||||
)
|
|
||||||
.fetch_one(executor)
|
|
||||||
.await
|
|
||||||
.context("could not insert oauth2 authorization code")?;
|
|
||||||
|
|
||||||
let pkce = pkce
|
|
||||||
.as_ref()
|
|
||||||
.map(|c| Pkce::new(c.code_challenge_method, c.code_challenge.clone()));
|
|
||||||
|
|
||||||
Ok(AuthorizationCode {
|
|
||||||
data: id,
|
|
||||||
code: code.to_string(),
|
|
||||||
pkce,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
struct OAuth2CodeLookup {
|
|
||||||
id: i64,
|
|
||||||
oauth2_session_id: i64,
|
|
||||||
client_id: String,
|
|
||||||
redirect_uri: String,
|
|
||||||
scope: String,
|
|
||||||
nonce: Option<String>,
|
|
||||||
code_challenge: Option<String>,
|
|
||||||
code_challenge_method: Option<i16>,
|
|
||||||
user_session_id: Option<i64>,
|
|
||||||
user_session_created_at: Option<DateTime<Utc>>,
|
|
||||||
user_id: Option<i64>,
|
|
||||||
user_username: Option<String>,
|
|
||||||
user_session_last_authentication_id: Option<i64>,
|
|
||||||
user_session_last_authentication_created_at: Option<DateTime<Utc>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn browser_session_from_database(
|
|
||||||
user_session_id: Option<i64>,
|
|
||||||
user_session_created_at: Option<DateTime<Utc>>,
|
|
||||||
user_id: Option<i64>,
|
|
||||||
user_username: Option<String>,
|
|
||||||
user_session_last_authentication_id: Option<i64>,
|
|
||||||
user_session_last_authentication_created_at: Option<DateTime<Utc>>,
|
|
||||||
) -> Result<Option<BrowserSession<PostgresqlBackend>>, DatabaseInconsistencyError> {
|
|
||||||
match (
|
|
||||||
user_session_id,
|
|
||||||
user_session_created_at,
|
|
||||||
user_id,
|
|
||||||
user_username,
|
|
||||||
) {
|
|
||||||
(None, None, None, None) => Ok(None),
|
|
||||||
(Some(session_id), Some(session_created_at), Some(user_id), Some(user_username)) => {
|
|
||||||
let user = User {
|
|
||||||
data: user_id,
|
|
||||||
username: user_username,
|
|
||||||
sub: format!("fake-sub-{}", user_id),
|
|
||||||
};
|
|
||||||
|
|
||||||
let last_authentication = match (
|
|
||||||
user_session_last_authentication_id,
|
|
||||||
user_session_last_authentication_created_at,
|
|
||||||
) {
|
|
||||||
(None, None) => None,
|
|
||||||
(Some(id), Some(created_at)) => Some(Authentication {
|
|
||||||
data: id,
|
|
||||||
created_at,
|
|
||||||
}),
|
|
||||||
_ => return Err(DatabaseInconsistencyError),
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(Some(BrowserSession {
|
|
||||||
data: session_id,
|
|
||||||
created_at: session_created_at,
|
|
||||||
user,
|
|
||||||
last_authentication,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
_ => Err(DatabaseInconsistencyError),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
|
||||||
#[error("failed to lookup oauth2 code")]
|
|
||||||
pub enum CodeLookupError {
|
|
||||||
Database(#[from] sqlx::Error),
|
|
||||||
Inconsistency(#[from] DatabaseInconsistencyError),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Reject for CodeLookupError {}
|
|
||||||
|
|
||||||
impl CodeLookupError {
|
|
||||||
#[must_use]
|
|
||||||
pub fn not_found(&self) -> bool {
|
|
||||||
matches!(self, &CodeLookupError::Database(sqlx::Error::RowNotFound))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(clippy::too_many_lines)]
|
|
||||||
pub async fn lookup_code(
|
|
||||||
executor: impl PgExecutor<'_>,
|
|
||||||
code: &str,
|
|
||||||
) -> Result<
|
|
||||||
(
|
|
||||||
AuthorizationCode<PostgresqlBackend>,
|
|
||||||
Session<PostgresqlBackend>,
|
|
||||||
),
|
|
||||||
CodeLookupError,
|
|
||||||
> {
|
|
||||||
let res = sqlx::query_as!(
|
|
||||||
OAuth2CodeLookup,
|
|
||||||
r#"
|
|
||||||
SELECT
|
|
||||||
oc.id,
|
|
||||||
oc.code_challenge,
|
|
||||||
oc.code_challenge_method,
|
|
||||||
os.id AS "oauth2_session_id!",
|
|
||||||
os.client_id AS "client_id!",
|
|
||||||
os.redirect_uri,
|
|
||||||
os.scope AS "scope!",
|
|
||||||
os.nonce,
|
|
||||||
us.id AS "user_session_id?",
|
|
||||||
us.created_at AS "user_session_created_at?",
|
|
||||||
u.id AS "user_id?",
|
|
||||||
u.username AS "user_username?",
|
|
||||||
usa.id AS "user_session_last_authentication_id?",
|
|
||||||
usa.created_at AS "user_session_last_authentication_created_at?"
|
|
||||||
FROM oauth2_codes oc
|
|
||||||
INNER JOIN oauth2_sessions os
|
|
||||||
ON os.id = oc.oauth2_session_id
|
|
||||||
LEFT JOIN user_sessions us
|
|
||||||
ON us.id = os.user_session_id
|
|
||||||
LEFT JOIN user_session_authentications usa
|
|
||||||
ON usa.session_id = us.id
|
|
||||||
LEFT JOIN users u
|
|
||||||
ON u.id = us.user_id
|
|
||||||
WHERE oc.code = $1
|
|
||||||
ORDER BY usa.created_at DESC
|
|
||||||
LIMIT 1
|
|
||||||
"#,
|
|
||||||
code,
|
|
||||||
)
|
|
||||||
.fetch_one(executor)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let pkce = match (res.code_challenge_method, res.code_challenge) {
|
|
||||||
(None, None) => None,
|
|
||||||
(Some(0 /* Plain */), Some(challenge)) => {
|
|
||||||
Some(Pkce::new(pkce::CodeChallengeMethod::Plain, challenge))
|
|
||||||
}
|
|
||||||
(Some(1 /* S256 */), Some(challenge)) => {
|
|
||||||
Some(Pkce::new(pkce::CodeChallengeMethod::S256, challenge))
|
|
||||||
}
|
|
||||||
_ => return Err(DatabaseInconsistencyError.into()),
|
|
||||||
};
|
|
||||||
|
|
||||||
let code = AuthorizationCode {
|
|
||||||
data: res.id,
|
|
||||||
code: code.to_string(),
|
|
||||||
pkce,
|
|
||||||
};
|
|
||||||
|
|
||||||
let client = Client {
|
|
||||||
data: (),
|
|
||||||
client_id: res.client_id,
|
|
||||||
};
|
|
||||||
|
|
||||||
let browser_session = browser_session_from_database(
|
|
||||||
res.user_session_id,
|
|
||||||
res.user_session_created_at,
|
|
||||||
res.user_id,
|
|
||||||
res.user_username,
|
|
||||||
res.user_session_last_authentication_id,
|
|
||||||
res.user_session_last_authentication_created_at,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
let scope = res.scope.parse().map_err(|_e| DatabaseInconsistencyError)?;
|
|
||||||
|
|
||||||
let redirect_uri = res
|
|
||||||
.redirect_uri
|
|
||||||
.parse()
|
|
||||||
.map_err(|_e| DatabaseInconsistencyError)?;
|
|
||||||
|
|
||||||
let session = Session {
|
|
||||||
data: res.oauth2_session_id,
|
|
||||||
client,
|
|
||||||
browser_session,
|
|
||||||
scope,
|
|
||||||
redirect_uri,
|
|
||||||
nonce: res.nonce,
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok((code, session))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn consume_code(
|
|
||||||
executor: impl PgExecutor<'_>,
|
|
||||||
code: AuthorizationCode<PostgresqlBackend>,
|
|
||||||
) -> anyhow::Result<()> {
|
|
||||||
// TODO: mark the code as invalid instead to allow invalidating the whole
|
|
||||||
// session on code reuse
|
|
||||||
let res = sqlx::query!(
|
|
||||||
r#"
|
|
||||||
DELETE FROM oauth2_codes
|
|
||||||
WHERE id = $1
|
|
||||||
"#,
|
|
||||||
code.data,
|
|
||||||
)
|
|
||||||
.execute(executor)
|
|
||||||
.await
|
|
||||||
.context("could not consume authorization code")?;
|
|
||||||
|
|
||||||
if res.rows_affected() == 1 {
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
Err(anyhow::anyhow!(
|
|
||||||
"no row were affected when consuming authorization code"
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
499
crates/core/src/storage/oauth2/authorization_grant.rs
Normal file
499
crates/core/src/storage/oauth2/authorization_grant.rs
Normal file
@ -0,0 +1,499 @@
|
|||||||
|
// Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
#![allow(clippy::unused_async)]
|
||||||
|
|
||||||
|
use std::{
|
||||||
|
convert::{TryFrom, TryInto},
|
||||||
|
num::NonZeroU32,
|
||||||
|
};
|
||||||
|
|
||||||
|
use anyhow::Context;
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use mas_data_model::{
|
||||||
|
Authentication, AuthorizationCode, AuthorizationGrant, AuthorizationGrantStage, BrowserSession,
|
||||||
|
Client, Pkce, Session, User,
|
||||||
|
};
|
||||||
|
use oauth2_types::{pkce::CodeChallengeMethod, requests::ResponseMode, scope::Scope};
|
||||||
|
use sqlx::PgExecutor;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
use crate::storage::{DatabaseInconsistencyError, IdAndCreationTime, PostgresqlBackend};
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
pub async fn new_authorization_grant(
|
||||||
|
executor: impl PgExecutor<'_>,
|
||||||
|
client_id: String,
|
||||||
|
redirect_uri: Url,
|
||||||
|
scope: Scope,
|
||||||
|
code: Option<AuthorizationCode>,
|
||||||
|
state: Option<String>,
|
||||||
|
nonce: Option<String>,
|
||||||
|
max_age: Option<NonZeroU32>,
|
||||||
|
acr_values: Option<String>,
|
||||||
|
response_mode: ResponseMode,
|
||||||
|
response_type_token: bool,
|
||||||
|
response_type_id_token: bool,
|
||||||
|
) -> anyhow::Result<AuthorizationGrant<PostgresqlBackend>> {
|
||||||
|
let code_challenge = code
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|c| c.pkce.as_ref())
|
||||||
|
.map(|p| &p.challenge);
|
||||||
|
let code_challenge_method = code
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|c| c.pkce.as_ref())
|
||||||
|
.map(|p| p.challenge_method.to_string());
|
||||||
|
let code_str = code.as_ref().map(|c| &c.code);
|
||||||
|
let res = sqlx::query_as!(
|
||||||
|
IdAndCreationTime,
|
||||||
|
r#"
|
||||||
|
INSERT INTO oauth2_authorization_grants
|
||||||
|
(client_id, redirect_uri, scope, state, nonce, max_age,
|
||||||
|
acr_values, response_mode, code_challenge, code_challenge_method,
|
||||||
|
response_type_code, response_type_token, response_type_id_token,
|
||||||
|
code)
|
||||||
|
VALUES
|
||||||
|
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)
|
||||||
|
RETURNING id, created_at
|
||||||
|
"#,
|
||||||
|
&client_id,
|
||||||
|
redirect_uri.to_string(),
|
||||||
|
scope.to_string(),
|
||||||
|
state,
|
||||||
|
nonce,
|
||||||
|
// TODO: this conversion is a bit ugly
|
||||||
|
max_age.map(|x| i32::try_from(u32::from(x)).unwrap_or(i32::MAX)),
|
||||||
|
acr_values,
|
||||||
|
response_mode.to_string(),
|
||||||
|
code_challenge,
|
||||||
|
code_challenge_method,
|
||||||
|
code.is_some(),
|
||||||
|
response_type_token,
|
||||||
|
response_type_id_token,
|
||||||
|
code_str,
|
||||||
|
)
|
||||||
|
.fetch_one(executor)
|
||||||
|
.await
|
||||||
|
.context("could not insert oauth2 authorization grant")?;
|
||||||
|
|
||||||
|
let client = Client {
|
||||||
|
data: (),
|
||||||
|
client_id,
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(AuthorizationGrant {
|
||||||
|
data: res.id,
|
||||||
|
stage: AuthorizationGrantStage::Pending,
|
||||||
|
code,
|
||||||
|
redirect_uri,
|
||||||
|
client,
|
||||||
|
scope,
|
||||||
|
state,
|
||||||
|
nonce,
|
||||||
|
max_age,
|
||||||
|
acr_values,
|
||||||
|
response_mode,
|
||||||
|
created_at: res.created_at,
|
||||||
|
response_type_token,
|
||||||
|
response_type_id_token,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
struct GrantLookup {
|
||||||
|
grant_id: i64,
|
||||||
|
grant_created_at: DateTime<Utc>,
|
||||||
|
grant_cancelled_at: Option<DateTime<Utc>>,
|
||||||
|
grant_fulfilled_at: Option<DateTime<Utc>>,
|
||||||
|
grant_exchanged_at: Option<DateTime<Utc>>,
|
||||||
|
grant_scope: String,
|
||||||
|
grant_state: Option<String>,
|
||||||
|
grant_redirect_uri: String,
|
||||||
|
grant_response_mode: String,
|
||||||
|
grant_nonce: Option<String>,
|
||||||
|
#[allow(dead_code)]
|
||||||
|
grant_max_age: Option<i32>,
|
||||||
|
grant_acr_values: Option<String>,
|
||||||
|
grant_response_type_code: bool,
|
||||||
|
grant_response_type_token: bool,
|
||||||
|
grant_response_type_id_token: bool,
|
||||||
|
grant_code: Option<String>,
|
||||||
|
grant_code_challenge: Option<String>,
|
||||||
|
grant_code_challenge_method: Option<String>,
|
||||||
|
client_id: String,
|
||||||
|
session_id: Option<i64>,
|
||||||
|
user_session_id: Option<i64>,
|
||||||
|
user_session_created_at: Option<DateTime<Utc>>,
|
||||||
|
user_id: Option<i64>,
|
||||||
|
user_username: Option<String>,
|
||||||
|
user_session_last_authentication_id: Option<i64>,
|
||||||
|
user_session_last_authentication_created_at: Option<DateTime<Utc>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryInto<AuthorizationGrant<PostgresqlBackend>> for GrantLookup {
|
||||||
|
type Error = DatabaseInconsistencyError;
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_lines)]
|
||||||
|
fn try_into(self) -> Result<AuthorizationGrant<PostgresqlBackend>, Self::Error> {
|
||||||
|
let scope: Scope = self
|
||||||
|
.grant_scope
|
||||||
|
.parse()
|
||||||
|
.map_err(|_e| DatabaseInconsistencyError)?;
|
||||||
|
|
||||||
|
let client = Client {
|
||||||
|
data: (),
|
||||||
|
client_id: self.client_id,
|
||||||
|
};
|
||||||
|
|
||||||
|
let last_authentication = match (
|
||||||
|
self.user_session_last_authentication_id,
|
||||||
|
self.user_session_last_authentication_created_at,
|
||||||
|
) {
|
||||||
|
(Some(id), Some(created_at)) => Some(Authentication {
|
||||||
|
data: id,
|
||||||
|
created_at,
|
||||||
|
}),
|
||||||
|
(None, None) => None,
|
||||||
|
_ => return Err(DatabaseInconsistencyError),
|
||||||
|
};
|
||||||
|
|
||||||
|
let session = match (
|
||||||
|
self.session_id,
|
||||||
|
self.user_session_id,
|
||||||
|
self.user_session_created_at,
|
||||||
|
self.user_id,
|
||||||
|
self.user_username,
|
||||||
|
last_authentication,
|
||||||
|
) {
|
||||||
|
(
|
||||||
|
Some(session_id),
|
||||||
|
Some(user_session_id),
|
||||||
|
Some(user_session_created_at),
|
||||||
|
Some(user_id),
|
||||||
|
Some(user_username),
|
||||||
|
last_authentication,
|
||||||
|
) => {
|
||||||
|
let user = User {
|
||||||
|
data: user_id,
|
||||||
|
username: user_username,
|
||||||
|
sub: format!("fake-sub-{}", user_id),
|
||||||
|
};
|
||||||
|
|
||||||
|
let browser_session = BrowserSession {
|
||||||
|
data: user_session_id,
|
||||||
|
user,
|
||||||
|
created_at: user_session_created_at,
|
||||||
|
last_authentication,
|
||||||
|
};
|
||||||
|
|
||||||
|
let client = client.clone();
|
||||||
|
let scope = scope.clone();
|
||||||
|
|
||||||
|
let session = Session {
|
||||||
|
data: session_id,
|
||||||
|
client,
|
||||||
|
browser_session,
|
||||||
|
scope,
|
||||||
|
};
|
||||||
|
|
||||||
|
Some(session)
|
||||||
|
}
|
||||||
|
(None, None, None, None, None, None) => None,
|
||||||
|
_ => return Err(DatabaseInconsistencyError),
|
||||||
|
};
|
||||||
|
|
||||||
|
let stage = match (
|
||||||
|
self.grant_fulfilled_at,
|
||||||
|
self.grant_exchanged_at,
|
||||||
|
self.grant_cancelled_at,
|
||||||
|
session,
|
||||||
|
) {
|
||||||
|
(None, None, None, None) => AuthorizationGrantStage::Pending,
|
||||||
|
(Some(fulfilled_at), None, None, Some(session)) => AuthorizationGrantStage::Fulfilled {
|
||||||
|
session,
|
||||||
|
fulfilled_at,
|
||||||
|
},
|
||||||
|
(Some(fulfilled_at), Some(exchanged_at), None, Some(session)) => {
|
||||||
|
AuthorizationGrantStage::Exchanged {
|
||||||
|
session,
|
||||||
|
fulfilled_at,
|
||||||
|
exchanged_at,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(None, None, Some(cancelled_at), None) => {
|
||||||
|
AuthorizationGrantStage::Cancelled { cancelled_at }
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
return Err(DatabaseInconsistencyError);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let pkce = match (self.grant_code_challenge, self.grant_code_challenge_method) {
|
||||||
|
(Some(challenge), Some(challenge_method)) if challenge_method == "plain" => {
|
||||||
|
Some(Pkce {
|
||||||
|
challenge_method: CodeChallengeMethod::Plain,
|
||||||
|
challenge,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
(Some(challenge), Some(challenge_method)) if challenge_method == "S256" => Some(Pkce {
|
||||||
|
challenge_method: CodeChallengeMethod::S256,
|
||||||
|
challenge,
|
||||||
|
}),
|
||||||
|
(None, None) => None,
|
||||||
|
_ => {
|
||||||
|
return Err(DatabaseInconsistencyError);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let code: Option<AuthorizationCode> =
|
||||||
|
match (self.grant_response_type_code, self.grant_code, pkce) {
|
||||||
|
(false, None, None) => None,
|
||||||
|
(true, Some(code), pkce) => Some(AuthorizationCode { code, pkce }),
|
||||||
|
_ => {
|
||||||
|
return Err(DatabaseInconsistencyError);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let redirect_uri = self
|
||||||
|
.grant_redirect_uri
|
||||||
|
.parse()
|
||||||
|
.map_err(|_e| DatabaseInconsistencyError)?;
|
||||||
|
|
||||||
|
let response_mode = self
|
||||||
|
.grant_response_mode
|
||||||
|
.parse()
|
||||||
|
.map_err(|_e| DatabaseInconsistencyError)?;
|
||||||
|
|
||||||
|
Ok(AuthorizationGrant {
|
||||||
|
data: self.grant_id,
|
||||||
|
stage,
|
||||||
|
client,
|
||||||
|
code,
|
||||||
|
acr_values: self.grant_acr_values,
|
||||||
|
scope,
|
||||||
|
state: self.grant_state,
|
||||||
|
nonce: self.grant_nonce,
|
||||||
|
max_age: None, // TODO
|
||||||
|
response_mode,
|
||||||
|
redirect_uri,
|
||||||
|
created_at: self.grant_created_at,
|
||||||
|
response_type_token: self.grant_response_type_token,
|
||||||
|
response_type_id_token: self.grant_response_type_id_token,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_grant_by_id(
|
||||||
|
executor: impl PgExecutor<'_>,
|
||||||
|
id: i64,
|
||||||
|
) -> anyhow::Result<AuthorizationGrant<PostgresqlBackend>> {
|
||||||
|
// TODO: handle "not found" cases
|
||||||
|
let res = sqlx::query_as!(
|
||||||
|
GrantLookup,
|
||||||
|
r#"
|
||||||
|
SELECT
|
||||||
|
og.id AS grant_id,
|
||||||
|
og.created_at AS grant_created_at,
|
||||||
|
og.cancelled_at AS grant_cancelled_at,
|
||||||
|
og.fulfilled_at AS grant_fulfilled_at,
|
||||||
|
og.exchanged_at AS grant_exchanged_at,
|
||||||
|
og.scope AS grant_scope,
|
||||||
|
og.state AS grant_state,
|
||||||
|
og.redirect_uri AS grant_redirect_uri,
|
||||||
|
og.response_mode AS grant_response_mode,
|
||||||
|
og.nonce AS grant_nonce,
|
||||||
|
og.max_age AS grant_max_age,
|
||||||
|
og.acr_values AS grant_acr_values,
|
||||||
|
og.client_id AS client_id,
|
||||||
|
og.code AS grant_code,
|
||||||
|
og.response_type_code AS grant_response_type_code,
|
||||||
|
og.response_type_token AS grant_response_type_token,
|
||||||
|
og.response_type_id_token AS grant_response_type_id_token,
|
||||||
|
og.code_challenge AS grant_code_challenge,
|
||||||
|
og.code_challenge_method AS grant_code_challenge_method,
|
||||||
|
os.id AS "session_id?",
|
||||||
|
us.id AS "user_session_id?",
|
||||||
|
us.created_at AS "user_session_created_at?",
|
||||||
|
u.id AS "user_id?",
|
||||||
|
u.username AS "user_username?",
|
||||||
|
usa.id AS "user_session_last_authentication_id?",
|
||||||
|
usa.created_at AS "user_session_last_authentication_created_at?"
|
||||||
|
FROM
|
||||||
|
oauth2_authorization_grants og
|
||||||
|
LEFT JOIN oauth2_sessions os
|
||||||
|
ON os.id = og.oauth2_session_id
|
||||||
|
LEFT JOIN user_sessions us
|
||||||
|
ON us.id = os.user_session_id
|
||||||
|
LEFT JOIN users u
|
||||||
|
ON u.id = us.user_id
|
||||||
|
LEFT JOIN user_session_authentications usa
|
||||||
|
ON usa.session_id = us.id
|
||||||
|
WHERE
|
||||||
|
og.id = $1
|
||||||
|
"#,
|
||||||
|
id,
|
||||||
|
)
|
||||||
|
.fetch_one(executor)
|
||||||
|
.await
|
||||||
|
.context("failed to get grant by id")?;
|
||||||
|
|
||||||
|
let grant = res.try_into()?;
|
||||||
|
|
||||||
|
Ok(grant)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn lookup_grant_by_code(
|
||||||
|
executor: impl PgExecutor<'_>,
|
||||||
|
code: &str,
|
||||||
|
) -> anyhow::Result<AuthorizationGrant<PostgresqlBackend>> {
|
||||||
|
// TODO: handle "not found" cases
|
||||||
|
let res = sqlx::query_as!(
|
||||||
|
GrantLookup,
|
||||||
|
r#"
|
||||||
|
SELECT
|
||||||
|
og.id AS grant_id,
|
||||||
|
og.created_at AS grant_created_at,
|
||||||
|
og.cancelled_at AS grant_cancelled_at,
|
||||||
|
og.fulfilled_at AS grant_fulfilled_at,
|
||||||
|
og.exchanged_at AS grant_exchanged_at,
|
||||||
|
og.scope AS grant_scope,
|
||||||
|
og.state AS grant_state,
|
||||||
|
og.redirect_uri AS grant_redirect_uri,
|
||||||
|
og.response_mode AS grant_response_mode,
|
||||||
|
og.nonce AS grant_nonce,
|
||||||
|
og.max_age AS grant_max_age,
|
||||||
|
og.acr_values AS grant_acr_values,
|
||||||
|
og.client_id AS client_id,
|
||||||
|
og.code AS grant_code,
|
||||||
|
og.response_type_code AS grant_response_type_code,
|
||||||
|
og.response_type_token AS grant_response_type_token,
|
||||||
|
og.response_type_id_token AS grant_response_type_id_token,
|
||||||
|
og.code_challenge AS grant_code_challenge,
|
||||||
|
og.code_challenge_method AS grant_code_challenge_method,
|
||||||
|
os.id AS "session_id?",
|
||||||
|
us.id AS "user_session_id?",
|
||||||
|
us.created_at AS "user_session_created_at?",
|
||||||
|
u.id AS "user_id?",
|
||||||
|
u.username AS "user_username?",
|
||||||
|
usa.id AS "user_session_last_authentication_id?",
|
||||||
|
usa.created_at AS "user_session_last_authentication_created_at?"
|
||||||
|
FROM
|
||||||
|
oauth2_authorization_grants og
|
||||||
|
LEFT JOIN oauth2_sessions os
|
||||||
|
ON os.id = og.oauth2_session_id
|
||||||
|
LEFT JOIN user_sessions us
|
||||||
|
ON us.id = os.user_session_id
|
||||||
|
LEFT JOIN users u
|
||||||
|
ON u.id = us.user_id
|
||||||
|
LEFT JOIN user_session_authentications usa
|
||||||
|
ON usa.session_id = us.id
|
||||||
|
WHERE
|
||||||
|
og.code = $1
|
||||||
|
"#,
|
||||||
|
code,
|
||||||
|
)
|
||||||
|
.fetch_one(executor)
|
||||||
|
.await
|
||||||
|
.context("failed to lookup grant by code")?;
|
||||||
|
|
||||||
|
let grant = res.try_into()?;
|
||||||
|
|
||||||
|
Ok(grant)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn derive_session(
|
||||||
|
executor: impl PgExecutor<'_>,
|
||||||
|
grant: &AuthorizationGrant<PostgresqlBackend>,
|
||||||
|
browser_session: BrowserSession<PostgresqlBackend>,
|
||||||
|
) -> anyhow::Result<Session<PostgresqlBackend>> {
|
||||||
|
let res = sqlx::query_as!(
|
||||||
|
IdAndCreationTime,
|
||||||
|
r#"
|
||||||
|
INSERT INTO oauth2_sessions
|
||||||
|
(user_session_id, client_id, scope)
|
||||||
|
SELECT
|
||||||
|
$1,
|
||||||
|
og.client_id,
|
||||||
|
og.scope
|
||||||
|
FROM
|
||||||
|
oauth2_authorization_grants og
|
||||||
|
WHERE
|
||||||
|
og.id = $2
|
||||||
|
RETURNING id, created_at
|
||||||
|
"#,
|
||||||
|
browser_session.data,
|
||||||
|
grant.data,
|
||||||
|
)
|
||||||
|
.fetch_one(executor)
|
||||||
|
.await
|
||||||
|
.context("could not insert oauth2 session")?;
|
||||||
|
|
||||||
|
Ok(Session {
|
||||||
|
data: res.id,
|
||||||
|
browser_session,
|
||||||
|
client: grant.client.clone(),
|
||||||
|
scope: grant.scope.clone(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn fulfill_grant(
|
||||||
|
executor: impl PgExecutor<'_>,
|
||||||
|
mut grant: AuthorizationGrant<PostgresqlBackend>,
|
||||||
|
session: Session<PostgresqlBackend>,
|
||||||
|
) -> anyhow::Result<AuthorizationGrant<PostgresqlBackend>> {
|
||||||
|
let fulfilled_at = sqlx::query_scalar!(
|
||||||
|
r#"
|
||||||
|
UPDATE oauth2_authorization_grants AS og
|
||||||
|
SET
|
||||||
|
oauth2_session_id = os.id,
|
||||||
|
fulfilled_at = os.created_at
|
||||||
|
FROM oauth2_sessions os
|
||||||
|
WHERE
|
||||||
|
og.id = $1 AND os.id = $2
|
||||||
|
RETURNING fulfilled_at AS "fulfilled_at!: DateTime<Utc>"
|
||||||
|
"#,
|
||||||
|
grant.data,
|
||||||
|
session.data,
|
||||||
|
)
|
||||||
|
.fetch_one(executor)
|
||||||
|
.await
|
||||||
|
.context("could not makr grant as fulfilled")?;
|
||||||
|
|
||||||
|
grant.stage = grant.stage.fulfill(fulfilled_at, session)?;
|
||||||
|
|
||||||
|
Ok(grant)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn exchange_grant(
|
||||||
|
executor: impl PgExecutor<'_>,
|
||||||
|
mut grant: AuthorizationGrant<PostgresqlBackend>,
|
||||||
|
) -> anyhow::Result<AuthorizationGrant<PostgresqlBackend>> {
|
||||||
|
let exchanged_at = sqlx::query_scalar!(
|
||||||
|
r#"
|
||||||
|
UPDATE oauth2_authorization_grants
|
||||||
|
SET
|
||||||
|
exchanged_at = NOW()
|
||||||
|
WHERE
|
||||||
|
id = $1
|
||||||
|
RETURNING exchanged_at AS "exchanged_at!: DateTime<Utc>"
|
||||||
|
"#,
|
||||||
|
grant.data,
|
||||||
|
)
|
||||||
|
.fetch_one(executor)
|
||||||
|
.await
|
||||||
|
.context("could not mark grant as exchanged")?;
|
||||||
|
|
||||||
|
grant.stage = grant.stage.exchange(exchanged_at)?;
|
||||||
|
|
||||||
|
Ok(grant)
|
||||||
|
}
|
@ -13,6 +13,5 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
pub mod access_token;
|
pub mod access_token;
|
||||||
pub mod authorization_code;
|
pub mod authorization_grant;
|
||||||
pub mod refresh_token;
|
pub mod refresh_token;
|
||||||
pub mod session;
|
|
||||||
|
@ -23,7 +23,7 @@ use crate::storage::{DatabaseInconsistencyError, IdAndCreationTime, PostgresqlBa
|
|||||||
|
|
||||||
pub async fn add_refresh_token(
|
pub async fn add_refresh_token(
|
||||||
executor: impl PgExecutor<'_>,
|
executor: impl PgExecutor<'_>,
|
||||||
oauth2_session_id: i64,
|
session: &Session<PostgresqlBackend>,
|
||||||
access_token: AccessToken<PostgresqlBackend>,
|
access_token: AccessToken<PostgresqlBackend>,
|
||||||
token: &str,
|
token: &str,
|
||||||
) -> anyhow::Result<RefreshToken<PostgresqlBackend>> {
|
) -> anyhow::Result<RefreshToken<PostgresqlBackend>> {
|
||||||
@ -37,7 +37,7 @@ pub async fn add_refresh_token(
|
|||||||
RETURNING
|
RETURNING
|
||||||
id, created_at
|
id, created_at
|
||||||
"#,
|
"#,
|
||||||
oauth2_session_id,
|
session.data,
|
||||||
access_token.data,
|
access_token.data,
|
||||||
token,
|
token,
|
||||||
)
|
)
|
||||||
@ -64,8 +64,6 @@ struct OAuth2RefreshTokenLookup {
|
|||||||
session_id: i64,
|
session_id: i64,
|
||||||
client_id: String,
|
client_id: String,
|
||||||
scope: String,
|
scope: String,
|
||||||
redirect_uri: String,
|
|
||||||
nonce: Option<String>,
|
|
||||||
user_session_id: i64,
|
user_session_id: i64,
|
||||||
user_session_created_at: DateTime<Utc>,
|
user_session_created_at: DateTime<Utc>,
|
||||||
user_id: i64,
|
user_id: i64,
|
||||||
@ -93,8 +91,6 @@ pub async fn lookup_active_refresh_token(
|
|||||||
os.id AS "session_id!",
|
os.id AS "session_id!",
|
||||||
os.client_id AS "client_id!",
|
os.client_id AS "client_id!",
|
||||||
os.scope AS "scope!",
|
os.scope AS "scope!",
|
||||||
os.redirect_uri AS "redirect_uri!",
|
|
||||||
os.nonce AS "nonce",
|
|
||||||
us.id AS "user_session_id!",
|
us.id AS "user_session_id!",
|
||||||
us.created_at AS "user_session_created_at!",
|
us.created_at AS "user_session_created_at!",
|
||||||
u.id AS "user_id!",
|
u.id AS "user_id!",
|
||||||
@ -173,23 +169,18 @@ pub async fn lookup_active_refresh_token(
|
|||||||
_ => return Err(DatabaseInconsistencyError.into()),
|
_ => return Err(DatabaseInconsistencyError.into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let browser_session = Some(BrowserSession {
|
let browser_session = BrowserSession {
|
||||||
data: res.user_session_id,
|
data: res.user_session_id,
|
||||||
created_at: res.user_session_created_at,
|
created_at: res.user_session_created_at,
|
||||||
user,
|
user,
|
||||||
last_authentication,
|
last_authentication,
|
||||||
});
|
};
|
||||||
|
|
||||||
let session = Session {
|
let session = Session {
|
||||||
data: res.session_id,
|
data: res.session_id,
|
||||||
client,
|
client,
|
||||||
browser_session,
|
browser_session,
|
||||||
scope: res.scope.parse().context("invalid scope in database")?,
|
scope: res.scope.parse().context("invalid scope in database")?,
|
||||||
redirect_uri: res
|
|
||||||
.redirect_uri
|
|
||||||
.parse()
|
|
||||||
.context("invalid redirect_uri in database")?,
|
|
||||||
nonce: res.nonce,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok((refresh_token, session))
|
Ok((refresh_token, session))
|
||||||
|
@ -1,212 +0,0 @@
|
|||||||
// Copyright 2021 The Matrix.org Foundation C.I.C.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
use std::{collections::HashSet, convert::TryFrom, str::FromStr, string::ToString};
|
|
||||||
|
|
||||||
use anyhow::Context;
|
|
||||||
use chrono::{DateTime, Duration, Utc};
|
|
||||||
use itertools::Itertools;
|
|
||||||
use mas_data_model::{AuthorizationCode, BrowserSession};
|
|
||||||
use oauth2_types::{
|
|
||||||
pkce,
|
|
||||||
requests::{ResponseMode, ResponseType},
|
|
||||||
};
|
|
||||||
use serde::Serialize;
|
|
||||||
use sqlx::PgExecutor;
|
|
||||||
use url::Url;
|
|
||||||
|
|
||||||
use super::authorization_code::add_code;
|
|
||||||
use crate::storage::{lookup_active_session, PostgresqlBackend};
|
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
pub struct OAuth2Session {
|
|
||||||
pub id: i64,
|
|
||||||
user_session_id: Option<i64>,
|
|
||||||
pub client_id: String,
|
|
||||||
redirect_uri: String,
|
|
||||||
scope: String,
|
|
||||||
pub state: Option<String>,
|
|
||||||
nonce: Option<String>,
|
|
||||||
max_age: Option<i32>,
|
|
||||||
response_type: String,
|
|
||||||
response_mode: String,
|
|
||||||
|
|
||||||
created_at: DateTime<Utc>,
|
|
||||||
updated_at: DateTime<Utc>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl OAuth2Session {
|
|
||||||
pub async fn add_code<'e>(
|
|
||||||
&self,
|
|
||||||
executor: impl PgExecutor<'e>,
|
|
||||||
code: &str,
|
|
||||||
code_challenge: &Option<pkce::AuthorizationRequest>,
|
|
||||||
) -> anyhow::Result<AuthorizationCode<PostgresqlBackend>> {
|
|
||||||
add_code(executor, self.id, code, code_challenge).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn fetch_session(
|
|
||||||
&self,
|
|
||||||
executor: impl PgExecutor<'_>,
|
|
||||||
) -> anyhow::Result<Option<BrowserSession<PostgresqlBackend>>> {
|
|
||||||
match self.user_session_id {
|
|
||||||
Some(id) => {
|
|
||||||
// TODO: and if the session is inactive?
|
|
||||||
let info = lookup_active_session(executor, id).await?;
|
|
||||||
Ok(Some(info))
|
|
||||||
}
|
|
||||||
None => Ok(None),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn fetch_code(&self, executor: impl PgExecutor<'_>) -> anyhow::Result<String> {
|
|
||||||
get_code_for_session(executor, self.id).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn match_or_set_session(
|
|
||||||
&mut self,
|
|
||||||
executor: impl PgExecutor<'_>,
|
|
||||||
session: BrowserSession<PostgresqlBackend>,
|
|
||||||
) -> anyhow::Result<BrowserSession<PostgresqlBackend>> {
|
|
||||||
match self.user_session_id {
|
|
||||||
Some(id) if id == session.data => Ok(session),
|
|
||||||
Some(id) => Err(anyhow::anyhow!(
|
|
||||||
"session mismatch, expected {}, got {}",
|
|
||||||
id,
|
|
||||||
session.data
|
|
||||||
)),
|
|
||||||
None => {
|
|
||||||
sqlx::query!(
|
|
||||||
"UPDATE oauth2_sessions SET user_session_id = $1 WHERE id = $2",
|
|
||||||
session.data,
|
|
||||||
self.id,
|
|
||||||
)
|
|
||||||
.execute(executor)
|
|
||||||
.await
|
|
||||||
.context("could not update oauth2 session")?;
|
|
||||||
Ok(session)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[must_use]
|
|
||||||
pub fn max_auth_time(&self) -> Option<DateTime<Utc>> {
|
|
||||||
self.max_age
|
|
||||||
.map(|d| Duration::seconds(i64::from(d)))
|
|
||||||
.map(|d| self.created_at - d)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn response_type(&self) -> anyhow::Result<HashSet<ResponseType>> {
|
|
||||||
self.response_type
|
|
||||||
.split(' ')
|
|
||||||
.map(|s| {
|
|
||||||
ResponseType::from_str(s).with_context(|| format!("invalid response type {}", s))
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn response_mode(&self) -> anyhow::Result<ResponseMode> {
|
|
||||||
self.response_mode.parse().context("invalid response mode")
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn redirect_uri(&self) -> anyhow::Result<Url> {
|
|
||||||
self.redirect_uri.parse().context("invalid redirect uri")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
pub async fn start_session(
|
|
||||||
executor: impl PgExecutor<'_>,
|
|
||||||
optional_session_id: Option<i64>,
|
|
||||||
client_id: &str,
|
|
||||||
redirect_uri: &Url,
|
|
||||||
scope: &str,
|
|
||||||
state: Option<&str>,
|
|
||||||
nonce: Option<&str>,
|
|
||||||
max_age: Option<Duration>,
|
|
||||||
response_type: &HashSet<ResponseType>,
|
|
||||||
response_mode: ResponseMode,
|
|
||||||
) -> anyhow::Result<OAuth2Session> {
|
|
||||||
// Checked convertion of duration to i32, maxing at i32::MAX
|
|
||||||
let max_age = max_age.map(|d| i32::try_from(d.num_seconds()).unwrap_or(i32::MAX));
|
|
||||||
let response_mode = response_mode.to_string();
|
|
||||||
let redirect_uri = redirect_uri.to_string();
|
|
||||||
let response_type: String = {
|
|
||||||
let it = response_type.iter().map(ToString::to_string);
|
|
||||||
Itertools::intersperse(it, " ".to_string()).collect()
|
|
||||||
};
|
|
||||||
|
|
||||||
sqlx::query_as!(
|
|
||||||
OAuth2Session,
|
|
||||||
r#"
|
|
||||||
INSERT INTO oauth2_sessions
|
|
||||||
(user_session_id, client_id, redirect_uri, scope, state, nonce, max_age,
|
|
||||||
response_type, response_mode)
|
|
||||||
VALUES
|
|
||||||
($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
|
||||||
RETURNING
|
|
||||||
id, user_session_id, client_id, redirect_uri, scope, state, nonce, max_age,
|
|
||||||
response_type, response_mode, created_at, updated_at
|
|
||||||
"#,
|
|
||||||
optional_session_id,
|
|
||||||
client_id,
|
|
||||||
redirect_uri,
|
|
||||||
scope,
|
|
||||||
state,
|
|
||||||
nonce,
|
|
||||||
max_age,
|
|
||||||
response_type,
|
|
||||||
response_mode,
|
|
||||||
)
|
|
||||||
.fetch_one(executor)
|
|
||||||
.await
|
|
||||||
.context("could not insert oauth2 session")
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_session_by_id(
|
|
||||||
executor: impl PgExecutor<'_>,
|
|
||||||
oauth2_session_id: i64,
|
|
||||||
) -> anyhow::Result<OAuth2Session> {
|
|
||||||
sqlx::query_as!(
|
|
||||||
OAuth2Session,
|
|
||||||
r#"
|
|
||||||
SELECT
|
|
||||||
id, user_session_id, client_id, redirect_uri, scope, state, nonce,
|
|
||||||
max_age, response_type, response_mode, created_at, updated_at
|
|
||||||
FROM oauth2_sessions
|
|
||||||
WHERE id = $1
|
|
||||||
"#,
|
|
||||||
oauth2_session_id
|
|
||||||
)
|
|
||||||
.fetch_one(executor)
|
|
||||||
.await
|
|
||||||
.context("could not fetch oauth2 session")
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_code_for_session(
|
|
||||||
executor: impl PgExecutor<'_>,
|
|
||||||
oauth2_session_id: i64,
|
|
||||||
) -> anyhow::Result<String> {
|
|
||||||
sqlx::query_scalar!(
|
|
||||||
r#"
|
|
||||||
SELECT code
|
|
||||||
FROM oauth2_codes
|
|
||||||
WHERE oauth2_session_id = $1
|
|
||||||
"#,
|
|
||||||
oauth2_session_id
|
|
||||||
)
|
|
||||||
.fetch_one(executor)
|
|
||||||
.await
|
|
||||||
.context("could not fetch oauth2 code")
|
|
||||||
}
|
|
@ -12,9 +12,12 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
|
use std::num::NonZeroU32;
|
||||||
|
|
||||||
use chrono::{DateTime, Duration, Utc};
|
use chrono::{DateTime, Duration, Utc};
|
||||||
use oauth2_types::{pkce::CodeChallengeMethod, scope::Scope};
|
use oauth2_types::{pkce::CodeChallengeMethod, requests::ResponseMode, scope::Scope};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
use thiserror::Error;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
pub mod errors;
|
pub mod errors;
|
||||||
@ -27,7 +30,7 @@ pub trait StorageBackend {
|
|||||||
type BrowserSessionData: Clone + std::fmt::Debug + PartialEq;
|
type BrowserSessionData: Clone + std::fmt::Debug + PartialEq;
|
||||||
type ClientData: Clone + std::fmt::Debug + PartialEq;
|
type ClientData: Clone + std::fmt::Debug + PartialEq;
|
||||||
type SessionData: Clone + std::fmt::Debug + PartialEq;
|
type SessionData: Clone + std::fmt::Debug + PartialEq;
|
||||||
type AuthorizationCodeData: Clone + std::fmt::Debug + PartialEq;
|
type AuthorizationGrantData: Clone + std::fmt::Debug + PartialEq;
|
||||||
type AccessTokenData: Clone + std::fmt::Debug + PartialEq;
|
type AccessTokenData: Clone + std::fmt::Debug + PartialEq;
|
||||||
type RefreshTokenData: Clone + std::fmt::Debug + PartialEq;
|
type RefreshTokenData: Clone + std::fmt::Debug + PartialEq;
|
||||||
}
|
}
|
||||||
@ -35,7 +38,7 @@ pub trait StorageBackend {
|
|||||||
impl StorageBackend for () {
|
impl StorageBackend for () {
|
||||||
type AccessTokenData = ();
|
type AccessTokenData = ();
|
||||||
type AuthenticationData = ();
|
type AuthenticationData = ();
|
||||||
type AuthorizationCodeData = ();
|
type AuthorizationGrantData = ();
|
||||||
type BrowserSessionData = ();
|
type BrowserSessionData = ();
|
||||||
type ClientData = ();
|
type ClientData = ();
|
||||||
type RefreshTokenData = ();
|
type RefreshTokenData = ();
|
||||||
@ -153,60 +156,18 @@ impl<S: StorageBackendMarker> From<Client<S>> for Client<()> {
|
|||||||
pub struct Session<T: StorageBackend> {
|
pub struct Session<T: StorageBackend> {
|
||||||
#[serde(skip_serializing)]
|
#[serde(skip_serializing)]
|
||||||
pub data: T::SessionData,
|
pub data: T::SessionData,
|
||||||
pub browser_session: Option<BrowserSession<T>>,
|
pub browser_session: BrowserSession<T>,
|
||||||
pub client: Client<T>,
|
pub client: Client<T>,
|
||||||
pub scope: Scope,
|
pub scope: Scope,
|
||||||
pub redirect_uri: Url,
|
|
||||||
pub nonce: Option<String>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<S: StorageBackendMarker> From<Session<S>> for Session<()> {
|
impl<S: StorageBackendMarker> From<Session<S>> for Session<()> {
|
||||||
fn from(s: Session<S>) -> Self {
|
fn from(s: Session<S>) -> Self {
|
||||||
Session {
|
Session {
|
||||||
data: (),
|
data: (),
|
||||||
browser_session: s.browser_session.map(Into::into),
|
browser_session: s.browser_session.into(),
|
||||||
client: s.client.into(),
|
client: s.client.into(),
|
||||||
scope: s.scope,
|
scope: s.scope,
|
||||||
redirect_uri: s.redirect_uri,
|
|
||||||
nonce: s.nonce,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
|
||||||
pub struct Pkce {
|
|
||||||
challenge_method: CodeChallengeMethod,
|
|
||||||
challenge: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Pkce {
|
|
||||||
pub fn new(challenge_method: CodeChallengeMethod, challenge: String) -> Self {
|
|
||||||
Pkce {
|
|
||||||
challenge_method,
|
|
||||||
challenge,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn verify(&self, verifier: &str) -> bool {
|
|
||||||
self.challenge_method.verify(&self.challenge, verifier)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
|
||||||
#[serde(bound = "T: StorageBackend")]
|
|
||||||
pub struct AuthorizationCode<T: StorageBackend> {
|
|
||||||
#[serde(skip_serializing)]
|
|
||||||
pub data: T::AuthorizationCodeData,
|
|
||||||
pub code: String,
|
|
||||||
pub pkce: Option<Pkce>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<S: StorageBackendMarker> From<AuthorizationCode<S>> for AuthorizationCode<()> {
|
|
||||||
fn from(c: AuthorizationCode<S>) -> Self {
|
|
||||||
AuthorizationCode {
|
|
||||||
data: (),
|
|
||||||
code: c.code,
|
|
||||||
pkce: c.pkce,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -256,3 +217,125 @@ impl<S: StorageBackendMarker> From<RefreshToken<S>> for RefreshToken<()> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
||||||
|
pub struct Pkce {
|
||||||
|
pub challenge_method: CodeChallengeMethod,
|
||||||
|
pub challenge: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Pkce {
|
||||||
|
pub fn new(challenge_method: CodeChallengeMethod, challenge: String) -> Self {
|
||||||
|
Pkce {
|
||||||
|
challenge_method,
|
||||||
|
challenge,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn verify(&self, verifier: &str) -> bool {
|
||||||
|
self.challenge_method.verify(&self.challenge, verifier)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
||||||
|
pub struct AuthorizationCode {
|
||||||
|
pub code: String,
|
||||||
|
pub pkce: Option<Pkce>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
#[error("invalid state transition")]
|
||||||
|
pub struct InvalidTransitionError;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Serialize)]
|
||||||
|
#[serde(bound = "T: StorageBackend")]
|
||||||
|
pub enum AuthorizationGrantStage<T: StorageBackend> {
|
||||||
|
Pending,
|
||||||
|
Fulfilled {
|
||||||
|
session: Session<T>,
|
||||||
|
fulfilled_at: DateTime<Utc>,
|
||||||
|
},
|
||||||
|
Exchanged {
|
||||||
|
session: Session<T>,
|
||||||
|
fulfilled_at: DateTime<Utc>,
|
||||||
|
exchanged_at: DateTime<Utc>,
|
||||||
|
},
|
||||||
|
Cancelled {
|
||||||
|
cancelled_at: DateTime<Utc>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: StorageBackend> Default for AuthorizationGrantStage<T> {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::Pending
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: StorageBackend> AuthorizationGrantStage<T> {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self::Pending
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fulfill(
|
||||||
|
self,
|
||||||
|
fulfilled_at: DateTime<Utc>,
|
||||||
|
session: Session<T>,
|
||||||
|
) -> Result<Self, InvalidTransitionError> {
|
||||||
|
match self {
|
||||||
|
Self::Pending => Ok(Self::Fulfilled {
|
||||||
|
fulfilled_at,
|
||||||
|
session,
|
||||||
|
}),
|
||||||
|
_ => Err(InvalidTransitionError),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn exchange(self, exchanged_at: DateTime<Utc>) -> Result<Self, InvalidTransitionError> {
|
||||||
|
match self {
|
||||||
|
Self::Fulfilled {
|
||||||
|
fulfilled_at,
|
||||||
|
session,
|
||||||
|
} => Ok(Self::Exchanged {
|
||||||
|
fulfilled_at,
|
||||||
|
exchanged_at,
|
||||||
|
session,
|
||||||
|
}),
|
||||||
|
_ => Err(InvalidTransitionError),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cancel(self, cancelled_at: DateTime<Utc>) -> Result<Self, InvalidTransitionError> {
|
||||||
|
match self {
|
||||||
|
Self::Pending => Ok(Self::Cancelled { cancelled_at }),
|
||||||
|
_ => Err(InvalidTransitionError),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Serialize)]
|
||||||
|
#[serde(bound = "T: StorageBackend")]
|
||||||
|
pub struct AuthorizationGrant<T: StorageBackend> {
|
||||||
|
#[serde(skip_serializing)]
|
||||||
|
pub data: T::AuthorizationGrantData,
|
||||||
|
#[serde(flatten)]
|
||||||
|
pub stage: AuthorizationGrantStage<T>,
|
||||||
|
pub code: Option<AuthorizationCode>,
|
||||||
|
pub client: Client<T>,
|
||||||
|
pub redirect_uri: Url,
|
||||||
|
pub scope: Scope,
|
||||||
|
pub state: Option<String>,
|
||||||
|
pub nonce: Option<String>,
|
||||||
|
pub max_age: Option<NonZeroU32>,
|
||||||
|
pub acr_values: Option<String>,
|
||||||
|
pub response_mode: ResponseMode,
|
||||||
|
pub response_type_token: bool,
|
||||||
|
pub response_type_id_token: bool,
|
||||||
|
pub created_at: DateTime<Utc>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: StorageBackend> AuthorizationGrant<T> {
|
||||||
|
pub fn max_auth_time(&self) -> DateTime<Utc> {
|
||||||
|
let max_age: Option<i64> = self.max_age.map(|x| x.get().into());
|
||||||
|
self.created_at + Duration::seconds(max_age.unwrap_or(3600 * 24 * 365))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -148,8 +148,7 @@ pub struct AuthorizationRequest {
|
|||||||
|
|
||||||
pub redirect_uri: Option<Url>,
|
pub redirect_uri: Option<Url>,
|
||||||
|
|
||||||
#[serde_as(as = "StringWithSeparator::<SpaceSeparator, String>")]
|
pub scope: Scope,
|
||||||
pub scope: HashSet<String>,
|
|
||||||
|
|
||||||
pub state: Option<String>,
|
pub state: Option<String>,
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user