Implement the device consent logic
This commit is contained in:
@@ -412,6 +412,10 @@ where
|
||||
mas_router::DeviceCodeLink::route(),
|
||||
get(self::oauth2::device::link::get).post(self::oauth2::device::link::post),
|
||||
)
|
||||
.route(
|
||||
mas_router::DeviceCodeConsent::route(),
|
||||
get(self::oauth2::device::consent::get).post(self::oauth2::device::consent::post),
|
||||
)
|
||||
.layer(AndThenLayer::new(
|
||||
move |response: axum::response::Response| async move {
|
||||
if response.status().is_server_error() {
|
||||
|
||||
@@ -155,8 +155,7 @@ pub(crate) async fn post(
|
||||
TypedHeader(CacheControl::new().with_no_store()),
|
||||
TypedHeader(Pragma::no_cache()),
|
||||
Json(response),
|
||||
)
|
||||
.into_response())
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
184
crates/handlers/src/oauth2/device/consent.rs
Normal file
184
crates/handlers/src/oauth2/device/consent.rs
Normal file
@@ -0,0 +1,184 @@
|
||||
// Copyright 2023 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::{Path, State},
|
||||
response::{IntoResponse, Response},
|
||||
Form,
|
||||
};
|
||||
use axum_extra::response::Html;
|
||||
use mas_axum_utils::{
|
||||
cookies::CookieJar,
|
||||
csrf::{CsrfExt, ProtectedForm},
|
||||
FancyError, SessionInfoExt,
|
||||
};
|
||||
use mas_router::UrlBuilder;
|
||||
use mas_storage::{BoxClock, BoxRepository, BoxRng};
|
||||
use mas_templates::{DeviceConsentContext, TemplateContext, Templates};
|
||||
use serde::Deserialize;
|
||||
use tracing::warn;
|
||||
use ulid::Ulid;
|
||||
|
||||
use crate::{BoundActivityTracker, PreferredLanguage};
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
enum Action {
|
||||
Consent,
|
||||
Reject,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub(crate) struct ConsentForm {
|
||||
action: Action,
|
||||
}
|
||||
|
||||
pub(crate) async fn get(
|
||||
mut rng: BoxRng,
|
||||
clock: BoxClock,
|
||||
PreferredLanguage(locale): PreferredLanguage,
|
||||
State(templates): State<Templates>,
|
||||
State(url_builder): State<UrlBuilder>,
|
||||
mut repo: BoxRepository,
|
||||
activity_tracker: BoundActivityTracker,
|
||||
cookie_jar: CookieJar,
|
||||
Path(grant_id): Path<Ulid>,
|
||||
) -> Result<Response, FancyError> {
|
||||
let (session_info, cookie_jar) = cookie_jar.session_info();
|
||||
let (csrf_token, cookie_jar) = cookie_jar.csrf_token(&clock, &mut rng);
|
||||
|
||||
let maybe_session = session_info.load_session(&mut repo).await?;
|
||||
|
||||
let Some(session) = maybe_session else {
|
||||
let login = mas_router::Login::and_continue_device_code_grant(grant_id);
|
||||
return Ok((cookie_jar, url_builder.redirect(&login)).into_response());
|
||||
};
|
||||
|
||||
activity_tracker
|
||||
.record_browser_session(&clock, &session)
|
||||
.await;
|
||||
|
||||
// TODO: better error handling
|
||||
let grant = repo
|
||||
.oauth2_device_code_grant()
|
||||
.lookup(grant_id)
|
||||
.await?
|
||||
.context("Device grant not found")?;
|
||||
|
||||
if grant.expires_at < clock.now() {
|
||||
return Err(FancyError::from(anyhow::anyhow!("Grant is expired")));
|
||||
}
|
||||
|
||||
let client = repo
|
||||
.oauth2_client()
|
||||
.lookup(grant.client_id)
|
||||
.await?
|
||||
.context("Client not found")?;
|
||||
|
||||
let ctx = DeviceConsentContext::new(grant, client)
|
||||
.with_session(session)
|
||||
.with_csrf(csrf_token.form_value())
|
||||
.with_language(locale);
|
||||
|
||||
let rendered = templates
|
||||
.render_device_consent(&ctx)
|
||||
.context("Failed to render template")?;
|
||||
|
||||
Ok((cookie_jar, Html(rendered)).into_response())
|
||||
}
|
||||
|
||||
pub(crate) async fn post(
|
||||
mut rng: BoxRng,
|
||||
clock: BoxClock,
|
||||
PreferredLanguage(locale): PreferredLanguage,
|
||||
State(templates): State<Templates>,
|
||||
State(url_builder): State<UrlBuilder>,
|
||||
mut repo: BoxRepository,
|
||||
activity_tracker: BoundActivityTracker,
|
||||
cookie_jar: CookieJar,
|
||||
Path(grant_id): Path<Ulid>,
|
||||
Form(form): Form<ProtectedForm<ConsentForm>>,
|
||||
) -> Result<Response, FancyError> {
|
||||
let (session_info, cookie_jar) = cookie_jar.session_info();
|
||||
let form = cookie_jar.verify_form(&clock, form)?;
|
||||
let (csrf_token, cookie_jar) = cookie_jar.csrf_token(&clock, &mut rng);
|
||||
|
||||
let maybe_session = session_info.load_session(&mut repo).await?;
|
||||
|
||||
let Some(session) = maybe_session else {
|
||||
let login = mas_router::Login::and_continue_device_code_grant(grant_id);
|
||||
return Ok((cookie_jar, url_builder.redirect(&login)).into_response());
|
||||
};
|
||||
|
||||
activity_tracker
|
||||
.record_browser_session(&clock, &session)
|
||||
.await;
|
||||
|
||||
// TODO: better error handling
|
||||
let grant = repo
|
||||
.oauth2_device_code_grant()
|
||||
.lookup(grant_id)
|
||||
.await?
|
||||
.context("Device grant not found")?;
|
||||
|
||||
if grant.expires_at < clock.now() {
|
||||
return Err(FancyError::from(anyhow::anyhow!("Grant is expired")));
|
||||
}
|
||||
|
||||
let client = repo
|
||||
.oauth2_client()
|
||||
.lookup(grant.client_id)
|
||||
.await?
|
||||
.context("Client not found")?;
|
||||
|
||||
// TODO: run through the policy
|
||||
let grant = if grant.is_pending() {
|
||||
match form.action {
|
||||
Action::Consent => {
|
||||
repo.oauth2_device_code_grant()
|
||||
.fulfill(&clock, grant, &session)
|
||||
.await?
|
||||
}
|
||||
Action::Reject => {
|
||||
repo.oauth2_device_code_grant()
|
||||
.reject(&clock, grant, &session)
|
||||
.await?
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// XXX: In case we're not pending, let's just return the grant as-is
|
||||
// since it might just be a form resubmission, and feedback is nice enough
|
||||
warn!(
|
||||
oauth2_device_code.id = %grant.id,
|
||||
browser_session.id = %session.id,
|
||||
user.id = %session.user.id,
|
||||
"Grant is not pending",
|
||||
);
|
||||
grant
|
||||
};
|
||||
|
||||
repo.save().await?;
|
||||
|
||||
let ctx = DeviceConsentContext::new(grant, client)
|
||||
.with_session(session)
|
||||
.with_csrf(csrf_token.form_value())
|
||||
.with_language(locale);
|
||||
|
||||
let rendered = templates
|
||||
.render_device_consent(&ctx)
|
||||
.context("Failed to render template")?;
|
||||
|
||||
Ok((cookie_jar, Html(rendered)).into_response())
|
||||
}
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
use axum::{
|
||||
extract::{Query, State},
|
||||
response::IntoResponse,
|
||||
response::{IntoResponse, Response},
|
||||
Form,
|
||||
};
|
||||
use axum_extra::response::Html;
|
||||
@@ -23,7 +23,8 @@ use mas_axum_utils::{
|
||||
csrf::{CsrfExt, ProtectedForm},
|
||||
FancyError,
|
||||
};
|
||||
use mas_storage::{BoxClock, BoxRng};
|
||||
use mas_router::UrlBuilder;
|
||||
use mas_storage::{BoxClock, BoxRepository, BoxRng};
|
||||
use mas_templates::{
|
||||
DeviceLinkContext, DeviceLinkFormField, FieldError, FormState, TemplateContext, Templates,
|
||||
};
|
||||
@@ -76,27 +77,42 @@ pub(crate) async fn get(
|
||||
pub(crate) async fn post(
|
||||
mut rng: BoxRng,
|
||||
clock: BoxClock,
|
||||
mut repo: BoxRepository,
|
||||
PreferredLanguage(locale): PreferredLanguage,
|
||||
State(templates): State<Templates>,
|
||||
State(url_builder): State<UrlBuilder>,
|
||||
cookie_jar: CookieJar,
|
||||
Form(form): Form<ProtectedForm<Params>>,
|
||||
) -> Result<impl IntoResponse, FancyError> {
|
||||
) -> Result<Response, FancyError> {
|
||||
let form = cookie_jar.verify_form(&clock, form)?;
|
||||
let (csrf_token, cookie_jar) = cookie_jar.csrf_token(&clock, &mut rng);
|
||||
|
||||
let form_state = FormState::from_form(&form)
|
||||
.with_error_on_field(DeviceLinkFormField::Code, FieldError::Required);
|
||||
let code = form.code.to_uppercase();
|
||||
let grant = repo
|
||||
.oauth2_device_code_grant()
|
||||
.find_by_user_code(&code)
|
||||
.await?
|
||||
// XXX: We should have different error messages for already exchanged and expired
|
||||
.filter(|grant| grant.is_pending())
|
||||
.filter(|grant| grant.expires_at > clock.now());
|
||||
|
||||
// TODO: find the device code grant in the database
|
||||
// and redirect to /oauth2/device/link/:id
|
||||
// That then will trigger a login if we don't have a session
|
||||
let Some(grant) = grant else {
|
||||
let form_state = FormState::from_form(&form)
|
||||
.with_error_on_field(DeviceLinkFormField::Code, FieldError::Invalid);
|
||||
|
||||
let ctx = DeviceLinkContext::new()
|
||||
.with_form_state(form_state)
|
||||
.with_csrf(csrf_token.form_value())
|
||||
.with_language(locale);
|
||||
let ctx = DeviceLinkContext::new()
|
||||
.with_form_state(form_state)
|
||||
.with_csrf(csrf_token.form_value())
|
||||
.with_language(locale);
|
||||
|
||||
let content = templates.render_device_link(&ctx)?;
|
||||
let content = templates.render_device_link(&ctx)?;
|
||||
|
||||
Ok((cookie_jar, Html(content)))
|
||||
return Ok((cookie_jar, Html(content)).into_response());
|
||||
};
|
||||
|
||||
// Redirect to the consent page
|
||||
// This will in turn redirect to the login page if the user is not logged in
|
||||
let destination = url_builder.redirect(&mas_router::DeviceCodeConsent::new(grant.id));
|
||||
|
||||
Ok((cookie_jar, destination).into_response())
|
||||
}
|
||||
|
||||
@@ -13,4 +13,5 @@
|
||||
// limitations under the License.
|
||||
|
||||
pub mod authorize;
|
||||
pub mod consent;
|
||||
pub mod link;
|
||||
|
||||
@@ -63,6 +63,16 @@ impl OptionalPostAuthAction {
|
||||
PostAuthContextInner::ContinueAuthorizationGrant { grant }
|
||||
}
|
||||
|
||||
PostAuthAction::ContinueDeviceCodeGrant { id } => {
|
||||
let grant = repo
|
||||
.oauth2_device_code_grant()
|
||||
.lookup(id)
|
||||
.await?
|
||||
.context("Failed to load device code grant")?;
|
||||
let grant = Box::new(grant);
|
||||
PostAuthContextInner::ContinueDeviceCodeGrant { grant }
|
||||
}
|
||||
|
||||
PostAuthAction::ContinueCompatSsoLogin { id } => {
|
||||
let login = repo
|
||||
.compat_sso_login()
|
||||
|
||||
@@ -24,6 +24,9 @@ pub enum PostAuthAction {
|
||||
ContinueAuthorizationGrant {
|
||||
id: Ulid,
|
||||
},
|
||||
ContinueDeviceCodeGrant {
|
||||
id: Ulid,
|
||||
},
|
||||
ContinueCompatSsoLogin {
|
||||
id: Ulid,
|
||||
},
|
||||
@@ -43,6 +46,11 @@ impl PostAuthAction {
|
||||
PostAuthAction::ContinueAuthorizationGrant { id }
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn continue_device_code_grant(id: Ulid) -> Self {
|
||||
PostAuthAction::ContinueDeviceCodeGrant { id }
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn continue_compat_sso_login(id: Ulid) -> Self {
|
||||
PostAuthAction::ContinueCompatSsoLogin { id }
|
||||
@@ -63,6 +71,9 @@ impl PostAuthAction {
|
||||
Self::ContinueAuthorizationGrant { id } => {
|
||||
url_builder.redirect(&ContinueAuthorizationGrant(*id))
|
||||
}
|
||||
Self::ContinueDeviceCodeGrant { id } => {
|
||||
url_builder.redirect(&DeviceCodeConsent::new(*id))
|
||||
}
|
||||
Self::ContinueCompatSsoLogin { id } => {
|
||||
url_builder.redirect(&CompatLoginSsoComplete::new(*id, None))
|
||||
}
|
||||
@@ -203,6 +214,13 @@ impl Login {
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn and_continue_device_code_grant(id: Ulid) -> Self {
|
||||
Self {
|
||||
post_auth_action: Some(PostAuthAction::continue_device_code_grant(id)),
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn and_continue_compat_sso_login(id: Ulid) -> Self {
|
||||
Self {
|
||||
@@ -266,6 +284,13 @@ impl Reauth {
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn and_continue_device_code_grant(data: Ulid) -> Self {
|
||||
Self {
|
||||
post_auth_action: Some(PostAuthAction::continue_device_code_grant(data)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a reference to the reauth's post auth action.
|
||||
#[must_use]
|
||||
pub fn post_auth_action(&self) -> Option<&PostAuthAction> {
|
||||
@@ -713,6 +738,30 @@ impl Route for DeviceCodeLink {
|
||||
}
|
||||
}
|
||||
|
||||
/// `GET|POST /link/:device_code_id`
|
||||
#[derive(Default, Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct DeviceCodeConsent {
|
||||
id: Ulid,
|
||||
}
|
||||
|
||||
impl Route for DeviceCodeConsent {
|
||||
type Query = ();
|
||||
fn route() -> &'static str {
|
||||
"/link/:device_code_id"
|
||||
}
|
||||
|
||||
fn path(&self) -> std::borrow::Cow<'static, str> {
|
||||
format!("/link/{}", self.id).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl DeviceCodeConsent {
|
||||
#[must_use]
|
||||
pub fn new(id: Ulid) -> Self {
|
||||
Self { id }
|
||||
}
|
||||
}
|
||||
|
||||
/// `POST /oauth2/device`
|
||||
#[derive(Default, Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct OAuth2DeviceAuthorizationEndpoint;
|
||||
|
||||
@@ -32,7 +32,12 @@ pub trait Route {
|
||||
let path = self.path();
|
||||
if let Some(query) = self.query() {
|
||||
let query = serde_urlencoded::to_string(query).unwrap();
|
||||
format!("{path}?{query}").into()
|
||||
|
||||
if query.is_empty() {
|
||||
path
|
||||
} else {
|
||||
format!("{path}?{query}").into()
|
||||
}
|
||||
} else {
|
||||
path
|
||||
}
|
||||
|
||||
@@ -18,15 +18,20 @@ mod branding;
|
||||
|
||||
use std::fmt::Formatter;
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use chrono::{DateTime, Duration, Utc};
|
||||
use http::{Method, Uri, Version};
|
||||
use mas_data_model::{
|
||||
AuthorizationGrant, BrowserSession, Client, CompatSsoLogin, CompatSsoLoginState,
|
||||
UpstreamOAuthLink, UpstreamOAuthProvider, User, UserEmail, UserEmailVerification,
|
||||
DeviceCodeGrant, UpstreamOAuthLink, UpstreamOAuthProvider, User, UserEmail,
|
||||
UserEmailVerification,
|
||||
};
|
||||
use mas_i18n::DataLocale;
|
||||
use mas_router::{Account, GraphQL, PostAuthAction, UrlBuilder};
|
||||
use rand::Rng;
|
||||
use oauth2_types::scope::OPENID;
|
||||
use rand::{
|
||||
distributions::{Alphanumeric, DistString},
|
||||
Rng,
|
||||
};
|
||||
use serde::{ser::SerializeStruct, Deserialize, Serialize};
|
||||
use ulid::Ulid;
|
||||
use url::Url;
|
||||
@@ -346,6 +351,12 @@ pub enum PostAuthContextInner {
|
||||
grant: Box<AuthorizationGrant>,
|
||||
},
|
||||
|
||||
/// Continue a device code grant
|
||||
ContinueDeviceCodeGrant {
|
||||
/// The device code grant that will be continued after authentication
|
||||
grant: Box<DeviceCodeGrant>,
|
||||
},
|
||||
|
||||
/// Continue legacy login
|
||||
/// TODO: add the login context in there
|
||||
ContinueCompatSsoLogin {
|
||||
@@ -1075,6 +1086,45 @@ impl TemplateContext for DeviceLinkContext {
|
||||
}
|
||||
}
|
||||
|
||||
/// Context used by the `device_consent.html` template
|
||||
#[derive(Serialize, Debug)]
|
||||
pub struct DeviceConsentContext {
|
||||
grant: DeviceCodeGrant,
|
||||
client: Client,
|
||||
}
|
||||
|
||||
impl DeviceConsentContext {
|
||||
/// Constructs a new context with an existing linked user
|
||||
#[must_use]
|
||||
pub fn new(grant: DeviceCodeGrant, client: Client) -> Self {
|
||||
Self { grant, client }
|
||||
}
|
||||
}
|
||||
|
||||
impl TemplateContext for DeviceConsentContext {
|
||||
fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng) -> Vec<Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
Client::samples(now, rng)
|
||||
.into_iter()
|
||||
.map(|client| {
|
||||
let grant = DeviceCodeGrant {
|
||||
id: Ulid::from_datetime_with_source(now.into(), rng),
|
||||
state: mas_data_model::DeviceCodeGrantState::Pending,
|
||||
client_id: client.id,
|
||||
scope: [OPENID].into_iter().collect(),
|
||||
user_code: Alphanumeric.sample_string(rng, 6).to_uppercase(),
|
||||
device_code: Alphanumeric.sample_string(rng, 32),
|
||||
created_at: now - Duration::minutes(5),
|
||||
expires_at: now + Duration::minutes(25),
|
||||
};
|
||||
Self { grant, client }
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
/// Context used by the `form_post.html` template
|
||||
#[derive(Serialize)]
|
||||
pub struct FormPostContext<T> {
|
||||
|
||||
@@ -42,13 +42,14 @@ mod macros;
|
||||
|
||||
pub use self::{
|
||||
context::{
|
||||
AppContext, CompatSsoContext, ConsentContext, DeviceLinkContext, DeviceLinkFormField,
|
||||
EmailAddContext, EmailVerificationContext, EmailVerificationPageContext, EmptyContext,
|
||||
ErrorContext, FormPostContext, IndexContext, LoginContext, LoginFormField, NotFoundContext,
|
||||
PolicyViolationContext, PostAuthContext, PostAuthContextInner, ReauthContext,
|
||||
ReauthFormField, RegisterContext, RegisterFormField, SiteBranding, TemplateContext,
|
||||
UpstreamExistingLinkContext, UpstreamRegister, UpstreamRegisterFormField,
|
||||
UpstreamSuggestLink, WithCsrf, WithLanguage, WithOptionalSession, WithSession,
|
||||
AppContext, CompatSsoContext, ConsentContext, DeviceConsentContext, DeviceLinkContext,
|
||||
DeviceLinkFormField, EmailAddContext, EmailVerificationContext,
|
||||
EmailVerificationPageContext, EmptyContext, ErrorContext, FormPostContext, IndexContext,
|
||||
LoginContext, LoginFormField, NotFoundContext, PolicyViolationContext, PostAuthContext,
|
||||
PostAuthContextInner, ReauthContext, ReauthFormField, RegisterContext, RegisterFormField,
|
||||
SiteBranding, TemplateContext, UpstreamExistingLinkContext, UpstreamRegister,
|
||||
UpstreamRegisterFormField, UpstreamSuggestLink, WithCsrf, WithLanguage,
|
||||
WithOptionalSession, WithSession,
|
||||
},
|
||||
forms::{FieldError, FormError, FormField, FormState, ToFormState},
|
||||
};
|
||||
@@ -368,6 +369,9 @@ register_templates! {
|
||||
|
||||
/// Render the device code link page
|
||||
pub fn render_device_link(WithLanguage<WithCsrf<DeviceLinkContext>>) { "pages/device_link.html" }
|
||||
|
||||
/// Render the device code consent page
|
||||
pub fn render_device_consent(WithLanguage<WithCsrf<WithSession<DeviceConsentContext>>>) { "pages/device_consent.html" }
|
||||
}
|
||||
|
||||
impl Templates {
|
||||
|
||||
@@ -176,6 +176,15 @@
|
||||
}
|
||||
}
|
||||
|
||||
&.success {
|
||||
background-color: var(--cpd-color-bg-success-subtle);
|
||||
|
||||
& svg {
|
||||
color: var(--cpd-color-icon-success-primary);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
& svg {
|
||||
height: var(--cpd-space-10x);
|
||||
width: var(--cpd-space-10x);
|
||||
|
||||
124
templates/pages/device_consent.html
Normal file
124
templates/pages/device_consent.html
Normal file
@@ -0,0 +1,124 @@
|
||||
{#
|
||||
Copyright 2023 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 %}
|
||||
{% set client_name = client.client_name or client.client_id %}
|
||||
|
||||
{% if grant.state == "pending" %}
|
||||
<header class="page-heading">
|
||||
{% if client.logo_uri %}
|
||||
<img class="consent-client-icon image" referrerpolicy="no-referrer" src="{{ client.logo_uri }}" />
|
||||
{% else %}
|
||||
<div class="consent-client-icon generic">
|
||||
{{ icon.web_browser() }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="header">
|
||||
<h1 class="title">Allow access to your account?</h1>
|
||||
<p class="text">
|
||||
{% if client.client_uri %}
|
||||
<a target="_blank" href="{{ client.client_uri }}">{{ client_name }}</a>
|
||||
{% else %}
|
||||
{{ client_name }}
|
||||
{% endif %}
|
||||
wants to access your account. This will allow <span class="whitespace-nowrap">{{ client_name }}</span> to:</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="consent-scope-list">
|
||||
{{ scope.list(scopes=grant.scope) }}
|
||||
</section>
|
||||
|
||||
<section class="text-center cpd-text-secondary cpd-text-body-md-regular">
|
||||
<span class="font-semibold cpd-text-primary">Make sure that you trust <span class="whitespace-nowrap">{{ client_name }}</span>.</span>
|
||||
You may be sharing sensitive information with this site or app.
|
||||
{% if client.policy_uri or client.tos_uri %}
|
||||
Find out how {{ client_name }} will handle your data by reviewing its
|
||||
{% if client.policy_uri %}
|
||||
<a target="_blank" href="{{ client.policy_uri }}" class="cpd-link" data-kind="primary">privacy policy</a>{% if not client.tos_uri %}.{% endif %}
|
||||
{% endif %}
|
||||
{% if client.policy_uri and client.tos_uri%}
|
||||
and
|
||||
{% endif %}
|
||||
{% if client.tos_uri %}
|
||||
<a target="_blank" href="{{ client.tos_uri }}" class="cpd-link" data-kind="primary">terms of service</a>.
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</section>
|
||||
|
||||
<section class="cpd-text-primary cpd-text-body-md-regular">
|
||||
This request was made on another device, which should display the following code: {{ grant.user_code }}.
|
||||
</section>
|
||||
|
||||
<section class="flex flex-col gap-6">
|
||||
<form method="POST" class="cpd-form-root">
|
||||
<input type="hidden" name="csrf" value="{{ csrf_token }}" />
|
||||
<button type="submit" name="action" value="consent" class="cpd-button" data-kind="primary" data-size="lg">
|
||||
{{ _("action.continue") }}
|
||||
</button>
|
||||
<button type="submit" name="action" value="reject" class="cpd-button" data-kind="destructive" data-size="lg">
|
||||
{{ _("action.cancel") }}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="flex gap-1 justify-center items-center">
|
||||
<p class="cpd-text-secondary cpd-text-body-md-regular">
|
||||
{{ _("mas.not_you", username=current_session.user.username) }}
|
||||
</p>
|
||||
|
||||
{{ logout.button(text=_("action.sign_out"), csrf_token=csrf_token, post_logout_action=action, as_link=true) }}
|
||||
</div>
|
||||
</section>
|
||||
{% elif grant.state == "rejected" %}
|
||||
<header class="page-heading">
|
||||
<div class="icon invalid">
|
||||
{{ icon.block() }}
|
||||
</div>
|
||||
|
||||
<div class="header">
|
||||
<h1 class="title">Access denied</h1>
|
||||
<p class="text">
|
||||
You denied access to
|
||||
{% if client.client_uri %}
|
||||
<a target="_blank" href="{{ client.client_uri }}">{{ client_name }}</a>
|
||||
{%- else %}
|
||||
{{ client_name -}}
|
||||
{% endif -%}. You can now close this window.
|
||||
</div>
|
||||
</header>
|
||||
{% else %}
|
||||
<header class="page-heading">
|
||||
<div class="icon success">
|
||||
{{ icon.check() }}
|
||||
</div>
|
||||
|
||||
<div class="header">
|
||||
<h1 class="title">Access granted</h1>
|
||||
<p class="text">
|
||||
You granted access to
|
||||
{% if client.client_uri %}
|
||||
<a target="_blank" href="{{ client.client_uri }}">{{ client_name }}</a>
|
||||
{%- else %}
|
||||
{{ client_name -}}
|
||||
{% endif -%}. You can now close this window.
|
||||
</div>
|
||||
</header>
|
||||
{% endif %}
|
||||
{% endblock content %}
|
||||
|
||||
Reference in New Issue
Block a user