You've already forked authentication-service
mirror of
https://github.com/matrix-org/matrix-authentication-service.git
synced 2025-07-09 10:01:45 +03:00
Simple consent screen and storage
This commit is contained in:
@ -124,6 +124,10 @@ where
|
|||||||
"/oauth2/authorize/step",
|
"/oauth2/authorize/step",
|
||||||
get(self::oauth2::authorization::step_get),
|
get(self::oauth2::authorization::step_get),
|
||||||
)
|
)
|
||||||
|
.route(
|
||||||
|
"/consent",
|
||||||
|
get(self::oauth2::consent::get).post(self::oauth2::consent::post),
|
||||||
|
)
|
||||||
.merge(api_router)
|
.merge(api_router)
|
||||||
.layer(Extension(pool.clone()))
|
.layer(Extension(pool.clone()))
|
||||||
.layer(Extension(templates.clone()))
|
.layer(Extension(templates.clone()))
|
||||||
|
@ -38,6 +38,7 @@ use mas_storage::{
|
|||||||
derive_session, fulfill_grant, get_grant_by_id, new_authorization_grant,
|
derive_session, fulfill_grant, get_grant_by_id, new_authorization_grant,
|
||||||
},
|
},
|
||||||
client::{lookup_client_by_client_id, ClientFetchError},
|
client::{lookup_client_by_client_id, ClientFetchError},
|
||||||
|
consent::fetch_client_consent,
|
||||||
refresh_token::add_refresh_token,
|
refresh_token::add_refresh_token,
|
||||||
},
|
},
|
||||||
PostgresqlBackend,
|
PostgresqlBackend,
|
||||||
@ -62,6 +63,7 @@ use sqlx::{PgConnection, PgPool, Postgres, Transaction};
|
|||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
|
use super::consent::ConsentRequest;
|
||||||
use crate::views::{LoginRequest, PostAuthAction, ReauthRequest, RegisterRequest};
|
use crate::views::{LoginRequest, PostAuthAction, ReauthRequest, RegisterRequest};
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
@ -508,8 +510,16 @@ async fn step(
|
|||||||
return Err(anyhow::anyhow!("authorization grant not pending").into());
|
return Err(anyhow::anyhow!("authorization grant not pending").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
let reply = match browser_session.last_authentication {
|
let current_consent =
|
||||||
Some(Authentication { created_at, .. }) if created_at > grant.max_auth_time() => {
|
fetch_client_consent(&mut txn, &browser_session.user, &grant.client).await?;
|
||||||
|
|
||||||
|
let lacks_consent = grant
|
||||||
|
.scope
|
||||||
|
.difference(¤t_consent)
|
||||||
|
.any(|scope| !scope.starts_with("urn:matrix:device:"));
|
||||||
|
|
||||||
|
let reply = match (lacks_consent, &browser_session.last_authentication) {
|
||||||
|
(false, Some(Authentication { created_at, .. })) if created_at > &grant.max_auth_time() => {
|
||||||
let session = derive_session(&mut txn, &grant, browser_session).await?;
|
let session = derive_session(&mut txn, &grant, browser_session).await?;
|
||||||
|
|
||||||
let grant = fulfill_grant(&mut txn, grant, session.clone()).await?;
|
let grant = fulfill_grant(&mut txn, grant, session.clone()).await?;
|
||||||
@ -562,6 +572,12 @@ async fn step(
|
|||||||
)
|
)
|
||||||
.await?
|
.await?
|
||||||
}
|
}
|
||||||
|
(true, Some(Authentication { created_at, .. })) if created_at > &grant.max_auth_time() => {
|
||||||
|
let next: ConsentRequest = next.into();
|
||||||
|
let next = next.build_uri()?;
|
||||||
|
|
||||||
|
Redirect::to(&next.to_string()).into_response()
|
||||||
|
}
|
||||||
_ => {
|
_ => {
|
||||||
let next: PostAuthAction = next.into();
|
let next: PostAuthAction = next.into();
|
||||||
let next: ReauthRequest = next.into();
|
let next: ReauthRequest = next.into();
|
||||||
|
165
crates/handlers/src/oauth2/consent.rs
Normal file
165
crates/handlers/src/oauth2/consent.rs
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
// Copyright 2022 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 axum::{
|
||||||
|
extract::{Extension, Form, Query},
|
||||||
|
http::uri::{Parts, PathAndQuery},
|
||||||
|
response::{Html, IntoResponse, Redirect, Response},
|
||||||
|
};
|
||||||
|
use hyper::{StatusCode, Uri};
|
||||||
|
use mas_axum_utils::{
|
||||||
|
csrf::{CsrfExt, ProtectedForm},
|
||||||
|
PrivateCookieJar, SessionInfoExt,
|
||||||
|
};
|
||||||
|
use mas_config::Encrypter;
|
||||||
|
use mas_data_model::AuthorizationGrantStage;
|
||||||
|
use mas_storage::oauth2::consent::insert_client_consent;
|
||||||
|
use mas_templates::{ConsentContext, TemplateContext, Templates};
|
||||||
|
use sqlx::PgPool;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
use super::ContinueAuthorizationGrant;
|
||||||
|
use crate::views::{LoginRequest, PostAuthAction};
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum RouteError {
|
||||||
|
#[error(transparent)]
|
||||||
|
Anyhow(#[from] anyhow::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoResponse for RouteError {
|
||||||
|
fn into_response(self) -> axum::response::Response {
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR.into_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct ConsentRequest {
|
||||||
|
grant: ContinueAuthorizationGrant,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ContinueAuthorizationGrant> for ConsentRequest {
|
||||||
|
fn from(grant: ContinueAuthorizationGrant) -> Self {
|
||||||
|
Self { grant }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ConsentRequest {
|
||||||
|
pub fn build_uri(&self) -> anyhow::Result<Uri> {
|
||||||
|
let qs = serde_urlencoded::to_string(&self.grant)?;
|
||||||
|
let path_and_query = PathAndQuery::try_from(format!("/consent?{}", qs))?;
|
||||||
|
let uri = Uri::from_parts({
|
||||||
|
let mut parts = Parts::default();
|
||||||
|
parts.path_and_query = Some(path_and_query);
|
||||||
|
parts
|
||||||
|
})?;
|
||||||
|
Ok(uri)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn get(
|
||||||
|
Extension(templates): Extension<Templates>,
|
||||||
|
Extension(pool): Extension<PgPool>,
|
||||||
|
cookie_jar: PrivateCookieJar<Encrypter>,
|
||||||
|
Query(next): Query<ContinueAuthorizationGrant>,
|
||||||
|
) -> Result<Response, RouteError> {
|
||||||
|
let mut conn = pool
|
||||||
|
.acquire()
|
||||||
|
.await
|
||||||
|
.context("failed to acquire db connection")?;
|
||||||
|
|
||||||
|
let (session_info, cookie_jar) = cookie_jar.session_info();
|
||||||
|
|
||||||
|
let maybe_session = session_info
|
||||||
|
.load_session(&mut conn)
|
||||||
|
.await
|
||||||
|
.context("could not load session")?;
|
||||||
|
|
||||||
|
let grant = next.fetch_authorization_grant(&mut conn).await?;
|
||||||
|
|
||||||
|
if !matches!(grant.stage, AuthorizationGrantStage::Pending) {
|
||||||
|
return Err(anyhow::anyhow!("authorization grant not pending").into());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(session) = maybe_session {
|
||||||
|
let (csrf_token, cookie_jar) = cookie_jar.csrf_token();
|
||||||
|
|
||||||
|
let ctx = ConsentContext::new(grant)
|
||||||
|
.with_session(session)
|
||||||
|
.with_csrf(csrf_token.form_value());
|
||||||
|
|
||||||
|
let content = templates
|
||||||
|
.render_consent(&ctx)
|
||||||
|
.await
|
||||||
|
.context("failed to render template")?;
|
||||||
|
|
||||||
|
Ok((cookie_jar, Html(content)).into_response())
|
||||||
|
} else {
|
||||||
|
let login = LoginRequest::from(PostAuthAction::from(next));
|
||||||
|
let login = login.build_uri()?;
|
||||||
|
Ok((cookie_jar, Redirect::to(&login.to_string())).into_response())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn post(
|
||||||
|
Extension(pool): Extension<PgPool>,
|
||||||
|
cookie_jar: PrivateCookieJar<Encrypter>,
|
||||||
|
Query(next): Query<ContinueAuthorizationGrant>,
|
||||||
|
Form(form): Form<ProtectedForm<()>>,
|
||||||
|
) -> Result<Response, RouteError> {
|
||||||
|
let mut txn = pool
|
||||||
|
.begin()
|
||||||
|
.await
|
||||||
|
.context("failed to begin db transaction")?;
|
||||||
|
|
||||||
|
cookie_jar
|
||||||
|
.verify_form(form)
|
||||||
|
.context("csrf verification failed")?;
|
||||||
|
|
||||||
|
let (session_info, cookie_jar) = cookie_jar.session_info();
|
||||||
|
|
||||||
|
let maybe_session = session_info
|
||||||
|
.load_session(&mut txn)
|
||||||
|
.await
|
||||||
|
.context("could not load session")?;
|
||||||
|
|
||||||
|
let session = if let Some(session) = maybe_session {
|
||||||
|
session
|
||||||
|
} else {
|
||||||
|
let login = LoginRequest::from(PostAuthAction::from(next));
|
||||||
|
let login = login.build_uri()?;
|
||||||
|
return Ok((cookie_jar, Redirect::to(&login.to_string())).into_response());
|
||||||
|
};
|
||||||
|
|
||||||
|
let grant = next.fetch_authorization_grant(&mut txn).await?;
|
||||||
|
// Do not consent for the "urn:matrix:device:*" scope
|
||||||
|
let scope_without_device = grant
|
||||||
|
.scope
|
||||||
|
.iter()
|
||||||
|
.filter(|s| !s.starts_with("urn:matrix:device:"))
|
||||||
|
.cloned()
|
||||||
|
.collect();
|
||||||
|
insert_client_consent(
|
||||||
|
&mut txn,
|
||||||
|
&session.user,
|
||||||
|
&grant.client,
|
||||||
|
&scope_without_device,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
txn.commit().await.context("could not commit txn")?;
|
||||||
|
|
||||||
|
let uri = next.build_uri()?;
|
||||||
|
Ok((cookie_jar, Redirect::to(&uri.to_string())).into_response())
|
||||||
|
}
|
@ -13,6 +13,7 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
pub mod authorization;
|
pub mod authorization;
|
||||||
|
pub mod consent;
|
||||||
pub mod discovery;
|
pub mod discovery;
|
||||||
pub mod introspection;
|
pub mod introspection;
|
||||||
pub mod keys;
|
pub mod keys;
|
||||||
|
@ -84,6 +84,14 @@ impl ToString for ScopeToken {
|
|||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct Scope(HashSet<ScopeToken>);
|
pub struct Scope(HashSet<ScopeToken>);
|
||||||
|
|
||||||
|
impl std::ops::Deref for Scope {
|
||||||
|
type Target = HashSet<ScopeToken>;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl FromStr for Scope {
|
impl FromStr for Scope {
|
||||||
type Err = InvalidScope;
|
type Err = InvalidScope;
|
||||||
|
|
||||||
|
@ -0,0 +1,17 @@
|
|||||||
|
-- Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
--
|
||||||
|
-- Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
-- you may not use this file except in compliance with the License.
|
||||||
|
-- You may obtain a copy of the License at
|
||||||
|
--
|
||||||
|
-- http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
--
|
||||||
|
-- Unless required by applicable law or agreed to in writing, software
|
||||||
|
-- distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
-- See the License for the specific language governing permissions and
|
||||||
|
-- limitations under the License.
|
||||||
|
|
||||||
|
DROP TRIGGER set_timestamp ON oauth2_consents;
|
||||||
|
DROP INDEX oauth2_consents_client_id_user_id_key;
|
||||||
|
DROP TABLE oauth2_consents;
|
@ -0,0 +1,32 @@
|
|||||||
|
-- Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
--
|
||||||
|
-- Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
-- you may not use this file except in compliance with the License.
|
||||||
|
-- You may obtain a copy of the License at
|
||||||
|
--
|
||||||
|
-- http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
--
|
||||||
|
-- Unless required by applicable law or agreed to in writing, software
|
||||||
|
-- distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
-- See the License for the specific language governing permissions and
|
||||||
|
-- limitations under the License.
|
||||||
|
|
||||||
|
CREATE TABLE oauth2_consents (
|
||||||
|
"id" BIGSERIAL PRIMARY KEY,
|
||||||
|
"oauth2_client_id" BIGINT NOT NULL REFERENCES oauth2_clients (id) ON DELETE CASCADE,
|
||||||
|
"user_id" BIGINT NOT NULL REFERENCES users (id) ON DELETE CASCADE,
|
||||||
|
"scope_token" TEXT NOT NULL,
|
||||||
|
"created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
|
||||||
|
"updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
|
||||||
|
|
||||||
|
CONSTRAINT user_client_scope_tuple UNIQUE ("oauth2_client_id", "user_id", "scope_token")
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX oauth2_consents_client_id_user_id_key
|
||||||
|
ON oauth2_consents ("oauth2_client_id", "user_id");
|
||||||
|
|
||||||
|
CREATE TRIGGER set_timestamp
|
||||||
|
BEFORE UPDATE ON oauth2_consents
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE PROCEDURE trigger_set_timestamp();
|
@ -399,6 +399,27 @@
|
|||||||
},
|
},
|
||||||
"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.oauth2_client_id AS oauth2_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 ue.id AS \"user_email_id?\",\n ue.email AS \"user_email?\",\n ue.created_at AS \"user_email_created_at?\",\n ue.confirmed_at AS \"user_email_confirmed_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 LEFT JOIN user_emails ue\n ON ue.id = u.primary_email_id\n\n WHERE og.id = $1\n\n ORDER BY usa.created_at DESC\n LIMIT 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.oauth2_client_id AS oauth2_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 ue.id AS \"user_email_id?\",\n ue.email AS \"user_email?\",\n ue.created_at AS \"user_email_created_at?\",\n ue.confirmed_at AS \"user_email_confirmed_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 LEFT JOIN user_emails ue\n ON ue.id = u.primary_email_id\n\n WHERE og.id = $1\n\n ORDER BY usa.created_at DESC\n LIMIT 1\n "
|
||||||
},
|
},
|
||||||
|
"51158bfcaa1a8d8e051bffe7c5ba0369bf53fb162f7622626054e89e68fc07bd": {
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "scope_token",
|
||||||
|
"ordinal": 0,
|
||||||
|
"type_info": "Text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"nullable": [
|
||||||
|
false
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Left": [
|
||||||
|
"Int8",
|
||||||
|
"Int8"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"query": "\n SELECT scope_token\n FROM oauth2_consents\n WHERE user_id = $1 AND oauth2_client_id = $2\n "
|
||||||
|
},
|
||||||
"581243a7f0c033548cc9644e0c60855ecb8bfefe51779eb135dd7547b886de79": {
|
"581243a7f0c033548cc9644e0c60855ecb8bfefe51779eb135dd7547b886de79": {
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [],
|
"columns": [],
|
||||||
@ -1027,6 +1048,20 @@
|
|||||||
},
|
},
|
||||||
"query": "\n DELETE FROM oauth2_access_tokens\n WHERE id = $1\n "
|
"query": "\n DELETE FROM oauth2_access_tokens\n WHERE id = $1\n "
|
||||||
},
|
},
|
||||||
|
"8ff8e80c3af4f8ab47b30301a573dee26c85bf21f61317278a892d54875ae983": {
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"nullable": [],
|
||||||
|
"parameters": {
|
||||||
|
"Left": [
|
||||||
|
"Int8",
|
||||||
|
"Int8",
|
||||||
|
"TextArray"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"query": "\n INSERT INTO oauth2_consents (user_id, oauth2_client_id, scope_token)\n SELECT $1, $2, scope_token FROM UNNEST($3::text[]) scope_token\n "
|
||||||
|
},
|
||||||
"99270fd3ddcc7421c5b26d0b8e0116356c13166887e7cf6ed6352cc879c80a68": {
|
"99270fd3ddcc7421c5b26d0b8e0116356c13166887e7cf6ed6352cc879c80a68": {
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [
|
"columns": [
|
||||||
|
70
crates/storage/src/oauth2/consent.rs
Normal file
70
crates/storage/src/oauth2/consent.rs
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
// Copyright 2022 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::str::FromStr;
|
||||||
|
|
||||||
|
use mas_data_model::{Client, User};
|
||||||
|
use oauth2_types::scope::{Scope, ScopeToken};
|
||||||
|
use sqlx::PgExecutor;
|
||||||
|
|
||||||
|
use crate::PostgresqlBackend;
|
||||||
|
|
||||||
|
pub async fn fetch_client_consent(
|
||||||
|
executor: impl PgExecutor<'_>,
|
||||||
|
user: &User<PostgresqlBackend>,
|
||||||
|
client: &Client<PostgresqlBackend>,
|
||||||
|
) -> anyhow::Result<Scope> {
|
||||||
|
let scope_tokens: Vec<String> = sqlx::query_scalar!(
|
||||||
|
r#"
|
||||||
|
SELECT scope_token
|
||||||
|
FROM oauth2_consents
|
||||||
|
WHERE user_id = $1 AND oauth2_client_id = $2
|
||||||
|
"#,
|
||||||
|
user.data,
|
||||||
|
client.data,
|
||||||
|
)
|
||||||
|
.fetch_all(executor)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let scope: Result<Scope, _> = scope_tokens
|
||||||
|
.into_iter()
|
||||||
|
.map(|s| ScopeToken::from_str(&s))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Ok(scope?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn insert_client_consent(
|
||||||
|
executor: impl PgExecutor<'_>,
|
||||||
|
user: &User<PostgresqlBackend>,
|
||||||
|
client: &Client<PostgresqlBackend>,
|
||||||
|
scope: &Scope,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
let tokens: Vec<String> = scope.iter().map(ToString::to_string).collect();
|
||||||
|
|
||||||
|
sqlx::query!(
|
||||||
|
r#"
|
||||||
|
INSERT INTO oauth2_consents (user_id, oauth2_client_id, scope_token)
|
||||||
|
SELECT $1, $2, scope_token FROM UNNEST($3::text[]) scope_token
|
||||||
|
ON CONFLICT (user_id, oauth2_client_id, scope_token) DO UPDATE SET updated_at = NOW()
|
||||||
|
"#,
|
||||||
|
user.data,
|
||||||
|
client.data,
|
||||||
|
&tokens,
|
||||||
|
)
|
||||||
|
.execute(executor)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
// Copyright 2021 The Matrix.org Foundation C.I.C.
|
// Copyright 2021, 2022 The Matrix.org Foundation C.I.C.
|
||||||
//
|
//
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
// you may not use this file except in compliance with the License.
|
// you may not use this file except in compliance with the License.
|
||||||
@ -20,6 +20,7 @@ use crate::PostgresqlBackend;
|
|||||||
pub mod access_token;
|
pub mod access_token;
|
||||||
pub mod authorization_grant;
|
pub mod authorization_grant;
|
||||||
pub mod client;
|
pub mod client;
|
||||||
|
pub mod consent;
|
||||||
pub mod refresh_token;
|
pub mod refresh_token;
|
||||||
|
|
||||||
pub async fn end_oauth_session(
|
pub async fn end_oauth_session(
|
||||||
|
@ -367,6 +367,35 @@ impl Default for RegisterContext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Context used by the `consent.html` template
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct ConsentContext {
|
||||||
|
grant: AuthorizationGrant<()>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TemplateContext for ConsentContext {
|
||||||
|
fn sample() -> Vec<Self>
|
||||||
|
where
|
||||||
|
Self: Sized,
|
||||||
|
{
|
||||||
|
// TODO
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ConsentContext {
|
||||||
|
/// Constructs a context for the client consent page
|
||||||
|
#[must_use]
|
||||||
|
pub fn new<T>(grant: T) -> Self
|
||||||
|
where
|
||||||
|
T: Into<AuthorizationGrant<()>>,
|
||||||
|
{
|
||||||
|
Self {
|
||||||
|
grant: grant.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Fields of the reauthentication form
|
/// Fields of the reauthentication form
|
||||||
#[derive(Serialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
|
#[derive(Serialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
|
||||||
#[serde(rename_all = "kebab-case")]
|
#[serde(rename_all = "kebab-case")]
|
||||||
|
@ -43,9 +43,9 @@ mod functions;
|
|||||||
mod macros;
|
mod macros;
|
||||||
|
|
||||||
pub use self::context::{
|
pub use self::context::{
|
||||||
AccountContext, AccountEmailsContext, EmailVerificationContext, EmptyContext, ErrorContext,
|
AccountContext, AccountEmailsContext, ConsentContext, EmailVerificationContext, EmptyContext,
|
||||||
FormPostContext, IndexContext, LoginContext, LoginFormField, PostAuthContext, ReauthContext,
|
ErrorContext, FormPostContext, IndexContext, LoginContext, LoginFormField, PostAuthContext,
|
||||||
ReauthFormField, RegisterContext, RegisterFormField, TemplateContext, WithCsrf,
|
ReauthContext, ReauthFormField, RegisterContext, RegisterFormField, TemplateContext, WithCsrf,
|
||||||
WithOptionalSession, WithSession,
|
WithOptionalSession, WithSession,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -285,6 +285,9 @@ register_templates! {
|
|||||||
/// Render the registration page
|
/// Render the registration page
|
||||||
pub fn render_register(WithCsrf<RegisterContext>) { "pages/register.html" }
|
pub fn render_register(WithCsrf<RegisterContext>) { "pages/register.html" }
|
||||||
|
|
||||||
|
/// Render the registration page
|
||||||
|
pub fn render_consent(WithCsrf<WithSession<ConsentContext>>) { "pages/consent.html" }
|
||||||
|
|
||||||
/// Render the home page
|
/// Render the home page
|
||||||
pub fn render_index(WithCsrf<WithOptionalSession<IndexContext>>) { "pages/index.html" }
|
pub fn render_index(WithCsrf<WithOptionalSession<IndexContext>>) { "pages/index.html" }
|
||||||
|
|
||||||
@ -322,6 +325,7 @@ impl Templates {
|
|||||||
pub async fn check_render(&self) -> anyhow::Result<()> {
|
pub async fn check_render(&self) -> anyhow::Result<()> {
|
||||||
check::render_login(self).await?;
|
check::render_login(self).await?;
|
||||||
check::render_register(self).await?;
|
check::render_register(self).await?;
|
||||||
|
check::render_consent(self).await?;
|
||||||
check::render_index(self).await?;
|
check::render_index(self).await?;
|
||||||
check::render_account_index(self).await?;
|
check::render_account_index(self).await?;
|
||||||
check::render_account_password(self).await?;
|
check::render_account_password(self).await?;
|
||||||
|
31
crates/templates/src/res/pages/consent.html
Normal file
31
crates/templates/src/res/pages/consent.html
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
{#
|
||||||
|
Copyright 2022 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.
|
||||||
|
#}
|
||||||
|
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<section class="flex items-center justify-center flex-1">
|
||||||
|
<form method="POST" class="grid grid-cols-1 gap-6 w-96 m-2">
|
||||||
|
<div class="text-center">
|
||||||
|
<h1 class="text-lg text-center font-medium">Authorize <em>{{ grant.client.client_name | default(value=grand.client.client_id) }}</em></h1>
|
||||||
|
</div>
|
||||||
|
<div>Scope: {{ grant.scope }}</div>
|
||||||
|
<input type="hidden" name="csrf" value="{{ csrf_token }}" />
|
||||||
|
{{ button::button(text="Allow") }}
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
{% endblock content %}
|
||||||
|
|
Reference in New Issue
Block a user