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",
|
||||
get(self::oauth2::authorization::step_get),
|
||||
)
|
||||
.route(
|
||||
"/consent",
|
||||
get(self::oauth2::consent::get).post(self::oauth2::consent::post),
|
||||
)
|
||||
.merge(api_router)
|
||||
.layer(Extension(pool.clone()))
|
||||
.layer(Extension(templates.clone()))
|
||||
|
@ -38,6 +38,7 @@ use mas_storage::{
|
||||
derive_session, fulfill_grant, get_grant_by_id, new_authorization_grant,
|
||||
},
|
||||
client::{lookup_client_by_client_id, ClientFetchError},
|
||||
consent::fetch_client_consent,
|
||||
refresh_token::add_refresh_token,
|
||||
},
|
||||
PostgresqlBackend,
|
||||
@ -62,6 +63,7 @@ use sqlx::{PgConnection, PgPool, Postgres, Transaction};
|
||||
use thiserror::Error;
|
||||
use url::Url;
|
||||
|
||||
use super::consent::ConsentRequest;
|
||||
use crate::views::{LoginRequest, PostAuthAction, ReauthRequest, RegisterRequest};
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
@ -508,8 +510,16 @@ async fn step(
|
||||
return Err(anyhow::anyhow!("authorization grant not pending").into());
|
||||
}
|
||||
|
||||
let reply = match browser_session.last_authentication {
|
||||
Some(Authentication { created_at, .. }) if created_at > grant.max_auth_time() => {
|
||||
let current_consent =
|
||||
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 grant = fulfill_grant(&mut txn, grant, session.clone()).await?;
|
||||
@ -562,6 +572,12 @@ async fn step(
|
||||
)
|
||||
.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: 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.
|
||||
|
||||
pub mod authorization;
|
||||
pub mod consent;
|
||||
pub mod discovery;
|
||||
pub mod introspection;
|
||||
pub mod keys;
|
||||
|
@ -84,6 +84,14 @@ impl ToString for ScopeToken {
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
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 {
|
||||
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 "
|
||||
},
|
||||
"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": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
@ -1027,6 +1048,20 @@
|
||||
},
|
||||
"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": {
|
||||
"describe": {
|
||||
"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");
|
||||
// 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 authorization_grant;
|
||||
pub mod client;
|
||||
pub mod consent;
|
||||
pub mod refresh_token;
|
||||
|
||||
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
|
||||
#[derive(Serialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
|
@ -43,9 +43,9 @@ mod functions;
|
||||
mod macros;
|
||||
|
||||
pub use self::context::{
|
||||
AccountContext, AccountEmailsContext, EmailVerificationContext, EmptyContext, ErrorContext,
|
||||
FormPostContext, IndexContext, LoginContext, LoginFormField, PostAuthContext, ReauthContext,
|
||||
ReauthFormField, RegisterContext, RegisterFormField, TemplateContext, WithCsrf,
|
||||
AccountContext, AccountEmailsContext, ConsentContext, EmailVerificationContext, EmptyContext,
|
||||
ErrorContext, FormPostContext, IndexContext, LoginContext, LoginFormField, PostAuthContext,
|
||||
ReauthContext, ReauthFormField, RegisterContext, RegisterFormField, TemplateContext, WithCsrf,
|
||||
WithOptionalSession, WithSession,
|
||||
};
|
||||
|
||||
@ -285,6 +285,9 @@ register_templates! {
|
||||
/// Render the registration page
|
||||
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
|
||||
pub fn render_index(WithCsrf<WithOptionalSession<IndexContext>>) { "pages/index.html" }
|
||||
|
||||
@ -322,6 +325,7 @@ impl Templates {
|
||||
pub async fn check_render(&self) -> anyhow::Result<()> {
|
||||
check::render_login(self).await?;
|
||||
check::render_register(self).await?;
|
||||
check::render_consent(self).await?;
|
||||
check::render_index(self).await?;
|
||||
check::render_account_index(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