You've already forked authentication-service
mirror of
https://github.com/matrix-org/matrix-authentication-service.git
synced 2025-07-29 22:01:14 +03:00
Rewrite the authorization grant logic
This commit is contained in:
@ -46,7 +46,7 @@ impl UrlBuilder {
|
||||
/// OAuth 2.0 authorization endpoint
|
||||
#[must_use]
|
||||
pub fn oauth_authorization_endpoint(&self) -> Url {
|
||||
self.base.join("oauth2/authorize").expect("build URL")
|
||||
self.base.join("authorize").expect("build URL")
|
||||
}
|
||||
|
||||
/// OAuth 2.0 token endpoint
|
||||
|
@ -119,6 +119,14 @@ impl<T: StorageBackend> AuthorizationGrantStage<T> {
|
||||
_ => Err(InvalidTransitionError),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if the authorization grant stage is [`Pending`].
|
||||
///
|
||||
/// [`Pending`]: AuthorizationGrantStage::Pending
|
||||
#[must_use]
|
||||
pub fn is_pending(&self) -> bool {
|
||||
matches!(self, Self::Pending)
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: StorageBackendMarker> From<AuthorizationGrantStage<S>> for AuthorizationGrantStage<()> {
|
||||
@ -166,6 +174,7 @@ pub struct AuthorizationGrant<T: StorageBackend> {
|
||||
pub response_type_token: bool,
|
||||
pub response_type_id_token: bool,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub requires_consent: bool,
|
||||
}
|
||||
|
||||
impl<S: StorageBackendMarker> From<AuthorizationGrant<S>> for AuthorizationGrant<()> {
|
||||
@ -185,6 +194,7 @@ impl<S: StorageBackendMarker> From<AuthorizationGrant<S>> for AuthorizationGrant
|
||||
response_type_token: g.response_type_token,
|
||||
response_type_id_token: g.response_type_id_token,
|
||||
created_at: g.created_at,
|
||||
requires_consent: g.requires_consent,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -89,6 +89,16 @@ impl<S: StorageBackendMarker> From<BrowserSession<S>> for BrowserSession<()> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: StorageBackend> BrowserSession<S> {
|
||||
pub fn was_authenticated_after(&self, after: DateTime<Utc>) -> bool {
|
||||
if let Some(auth) = &self.last_authentication {
|
||||
auth.created_at > after
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: StorageBackend> BrowserSession<T>
|
||||
where
|
||||
T::BrowserSessionData: Default,
|
||||
|
@ -119,13 +119,13 @@ where
|
||||
"/account/emails",
|
||||
get(self::views::account::emails::get).post(self::views::account::emails::post),
|
||||
)
|
||||
.route("/oauth2/authorize", get(self::oauth2::authorization::get))
|
||||
.route("/authorize", get(self::oauth2::authorization::get))
|
||||
.route(
|
||||
"/oauth2/authorize/step",
|
||||
get(self::oauth2::authorization::step_get),
|
||||
"/authorize/:grant_id",
|
||||
get(self::oauth2::authorization::complete::get),
|
||||
)
|
||||
.route(
|
||||
"/consent",
|
||||
"/consent/:grant_id",
|
||||
get(self::oauth2::consent::get).post(self::oauth2::consent::post),
|
||||
)
|
||||
.merge(api_router)
|
||||
|
@ -1,517 +0,0 @@
|
||||
// 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.
|
||||
// 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::{anyhow, Context};
|
||||
use axum::{
|
||||
extract::{Extension, Form, Query},
|
||||
response::{IntoResponse, Redirect, Response},
|
||||
};
|
||||
use axum_extra::extract::PrivateCookieJar;
|
||||
use chrono::Duration;
|
||||
use hyper::{
|
||||
http::uri::{Parts, PathAndQuery, Uri},
|
||||
StatusCode,
|
||||
};
|
||||
use mas_axum_utils::SessionInfoExt;
|
||||
use mas_config::Encrypter;
|
||||
use mas_data_model::{
|
||||
Authentication, AuthorizationCode, AuthorizationGrant, AuthorizationGrantStage, BrowserSession,
|
||||
Pkce, StorageBackend, TokenType,
|
||||
};
|
||||
use mas_iana::oauth::OAuthAuthorizationEndpointResponseType;
|
||||
use mas_storage::{
|
||||
oauth2::{
|
||||
access_token::add_access_token,
|
||||
authorization_grant::{
|
||||
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,
|
||||
};
|
||||
use mas_templates::Templates;
|
||||
use oauth2_types::{
|
||||
errors::{
|
||||
INVALID_REQUEST, LOGIN_REQUIRED, REGISTRATION_NOT_SUPPORTED, REQUEST_NOT_SUPPORTED,
|
||||
REQUEST_URI_NOT_SUPPORTED, UNAUTHORIZED_CLIENT,
|
||||
},
|
||||
pkce,
|
||||
prelude::*,
|
||||
requests::{
|
||||
AccessTokenResponse, AuthorizationRequest, AuthorizationResponse, GrantType, Prompt,
|
||||
ResponseMode,
|
||||
},
|
||||
scope::ScopeToken,
|
||||
};
|
||||
use rand::{distributions::Alphanumeric, thread_rng, Rng};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::{PgConnection, PgPool, Postgres, Transaction};
|
||||
use thiserror::Error;
|
||||
|
||||
use self::callback::CallbackDestination;
|
||||
use super::consent::ConsentRequest;
|
||||
use crate::views::{LoginRequest, PostAuthAction, ReauthRequest, RegisterRequest};
|
||||
|
||||
mod callback;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum RouteError {
|
||||
#[error(transparent)]
|
||||
Internal(Box<dyn std::error::Error + Send + Sync + 'static>),
|
||||
|
||||
#[error(transparent)]
|
||||
Anyhow(anyhow::Error),
|
||||
|
||||
#[error("could not find client")]
|
||||
ClientNotFound,
|
||||
|
||||
#[error("invalid redirect uri")]
|
||||
InvalidRedirectUri(#[from] self::callback::InvalidRedirectUriError),
|
||||
|
||||
#[error("invalid redirect uri")]
|
||||
UnknownRedirectUri(#[from] mas_data_model::InvalidRedirectUriError),
|
||||
}
|
||||
|
||||
impl IntoResponse for RouteError {
|
||||
fn into_response(self) -> axum::response::Response {
|
||||
// TODO: better error pages
|
||||
match self {
|
||||
RouteError::Internal(e) => {
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response()
|
||||
}
|
||||
RouteError::Anyhow(e) => {
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response()
|
||||
}
|
||||
RouteError::ClientNotFound => {
|
||||
(StatusCode::BAD_REQUEST, "could not find client").into_response()
|
||||
}
|
||||
RouteError::InvalidRedirectUri(e) => (
|
||||
StatusCode::BAD_REQUEST,
|
||||
format!("Invalid redirect URI ({})", e),
|
||||
)
|
||||
.into_response(),
|
||||
RouteError::UnknownRedirectUri(e) => (
|
||||
StatusCode::BAD_REQUEST,
|
||||
format!("Invalid redirect URI ({})", e),
|
||||
)
|
||||
.into_response(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<sqlx::Error> for RouteError {
|
||||
fn from(e: sqlx::Error) -> Self {
|
||||
Self::Internal(Box::new(e))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<self::callback::CallbackDestinationError> for RouteError {
|
||||
fn from(e: self::callback::CallbackDestinationError) -> Self {
|
||||
Self::Internal(Box::new(e))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ClientFetchError> for RouteError {
|
||||
fn from(e: ClientFetchError) -> Self {
|
||||
if e.not_found() {
|
||||
Self::ClientNotFound
|
||||
} else {
|
||||
Self::Internal(Box::new(e))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<anyhow::Error> for RouteError {
|
||||
fn from(e: anyhow::Error) -> Self {
|
||||
Self::Anyhow(e)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub(crate) struct Params {
|
||||
#[serde(flatten)]
|
||||
auth: AuthorizationRequest,
|
||||
|
||||
#[serde(flatten)]
|
||||
pkce: Option<pkce::AuthorizationRequest>,
|
||||
}
|
||||
|
||||
/// Given a list of response types and an optional user-defined response mode,
|
||||
/// figure out what response mode must be used, and emit an error if the
|
||||
/// suggested response mode isn't allowed for the given response types.
|
||||
fn resolve_response_mode(
|
||||
response_type: OAuthAuthorizationEndpointResponseType,
|
||||
suggested_response_mode: Option<ResponseMode>,
|
||||
) -> anyhow::Result<ResponseMode> {
|
||||
use ResponseMode as M;
|
||||
|
||||
// If the response type includes either "token" or "id_token", the default
|
||||
// response mode is "fragment" and the response mode "query" must not be
|
||||
// used
|
||||
if response_type.has_token() || response_type.has_id_token() {
|
||||
match suggested_response_mode {
|
||||
None => Ok(M::Fragment),
|
||||
Some(M::Query) => Err(anyhow!("invalid response mode")),
|
||||
Some(mode) => Ok(mode),
|
||||
}
|
||||
} else {
|
||||
// In other cases, all response modes are allowed, defaulting to "query"
|
||||
Ok(suggested_response_mode.unwrap_or(M::Query))
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
#[tracing::instrument(skip_all, err)]
|
||||
pub(crate) async fn get(
|
||||
Extension(templates): Extension<Templates>,
|
||||
Extension(pool): Extension<PgPool>,
|
||||
cookie_jar: PrivateCookieJar<Encrypter>,
|
||||
Form(params): Form<Params>,
|
||||
) -> Result<Response, RouteError> {
|
||||
let mut txn = pool.begin().await?;
|
||||
|
||||
// First, fetch the current session if there is one
|
||||
let (session_info, cookie_jar) = cookie_jar.session_info();
|
||||
|
||||
let maybe_session = session_info
|
||||
.load_session(&mut txn)
|
||||
.await
|
||||
.context("failed to load browser session")?;
|
||||
|
||||
// Then, find out what client it is
|
||||
let client = lookup_client_by_client_id(&mut txn, ¶ms.auth.client_id).await?;
|
||||
|
||||
let redirect_uri = client
|
||||
.resolve_redirect_uri(¶ms.auth.redirect_uri)?
|
||||
.clone();
|
||||
let response_type = params.auth.response_type;
|
||||
let response_mode = resolve_response_mode(response_type, params.auth.response_mode)?;
|
||||
|
||||
let callback_destination = CallbackDestination::try_new(
|
||||
response_mode,
|
||||
redirect_uri.clone(),
|
||||
params.auth.state.clone(),
|
||||
)?;
|
||||
|
||||
// One day, we will have try blocks
|
||||
let res: Result<Response, RouteError> = (async move {
|
||||
// Check if the request/request_uri/registration params are used. If so, reply
|
||||
// with the right error since we don't support them.
|
||||
if params.auth.request.is_some() {
|
||||
return Ok(callback_destination
|
||||
.go(&templates, REQUEST_NOT_SUPPORTED)
|
||||
.await?);
|
||||
}
|
||||
|
||||
if params.auth.request_uri.is_some() {
|
||||
return Ok(callback_destination
|
||||
.go(&templates, REQUEST_URI_NOT_SUPPORTED)
|
||||
.await?);
|
||||
}
|
||||
|
||||
if params.auth.registration.is_some() {
|
||||
return Ok(callback_destination
|
||||
.go(&templates, REGISTRATION_NOT_SUPPORTED)
|
||||
.await?);
|
||||
}
|
||||
|
||||
// Check if it is allowed to use this grant type
|
||||
if !client.grant_types.contains(&GrantType::AuthorizationCode) {
|
||||
return Ok(callback_destination
|
||||
.go(&templates, UNAUTHORIZED_CLIENT)
|
||||
.await?);
|
||||
}
|
||||
|
||||
let code: Option<AuthorizationCode> = if response_type.has_code() {
|
||||
// 32 random alphanumeric characters, about 190bit of entropy
|
||||
let code: String = thread_rng()
|
||||
.sample_iter(&Alphanumeric)
|
||||
.take(32)
|
||||
.map(char::from)
|
||||
.collect();
|
||||
|
||||
let pkce = params.pkce.map(|p| Pkce {
|
||||
challenge: p.code_challenge,
|
||||
challenge_method: p.code_challenge_method,
|
||||
});
|
||||
|
||||
Some(AuthorizationCode { code, pkce })
|
||||
} 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(callback_destination.go(&templates, INVALID_REQUEST).await?);
|
||||
}
|
||||
|
||||
None
|
||||
};
|
||||
|
||||
// Generate the device ID
|
||||
// TODO: this should probably be done somewhere else?
|
||||
let device_id: String = thread_rng()
|
||||
.sample_iter(&Alphanumeric)
|
||||
.take(10)
|
||||
.map(char::from)
|
||||
.collect();
|
||||
let device_scope: ScopeToken = format!("urn:matrix:device:{}", device_id)
|
||||
.parse()
|
||||
.context("could not parse generated device scope")?;
|
||||
|
||||
let scope = {
|
||||
let mut s = params.auth.scope.clone();
|
||||
s.insert(device_scope);
|
||||
s
|
||||
};
|
||||
|
||||
let grant = new_authorization_grant(
|
||||
&mut txn,
|
||||
client,
|
||||
redirect_uri.clone(),
|
||||
scope,
|
||||
code,
|
||||
params.auth.state.clone(),
|
||||
params.auth.nonce,
|
||||
params.auth.max_age,
|
||||
None,
|
||||
response_mode,
|
||||
response_type.has_token(),
|
||||
response_type.has_id_token(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let next = ContinueAuthorizationGrant::from_authorization_grant(&grant);
|
||||
|
||||
match (maybe_session, params.auth.prompt) {
|
||||
(None, Some(Prompt::None)) => {
|
||||
// If there is no session and prompt=none was asked, go back to the client
|
||||
txn.commit().await?;
|
||||
Ok(callback_destination.go(&templates, LOGIN_REQUIRED).await?)
|
||||
}
|
||||
(Some(_), Some(Prompt::Consent)) => {
|
||||
// We're already logged in but consent was asked
|
||||
txn.commit().await?;
|
||||
|
||||
let next: ConsentRequest = next.into();
|
||||
let next = next.build_uri()?;
|
||||
|
||||
Ok(Redirect::to(&next.to_string()).into_response())
|
||||
}
|
||||
(Some(_), Some(Prompt::Login | Prompt::SelectAccount)) => {
|
||||
// We're already logged in but login|select_account was asked, reauth
|
||||
// TODO: better pages here
|
||||
txn.commit().await?;
|
||||
|
||||
let next: PostAuthAction = next.into();
|
||||
let next: ReauthRequest = next.into();
|
||||
let next = next.build_uri()?;
|
||||
|
||||
Ok(Redirect::to(&next.to_string()).into_response())
|
||||
}
|
||||
(Some(user_session), _) => {
|
||||
// Other cases where we already have a session
|
||||
step(next, user_session, txn, &templates).await
|
||||
}
|
||||
(None, Some(Prompt::Create)) => {
|
||||
// Client asked for a registration, show the registration prompt
|
||||
txn.commit().await?;
|
||||
|
||||
let next: PostAuthAction = next.into();
|
||||
let next: RegisterRequest = next.into();
|
||||
let next = next.build_uri()?;
|
||||
|
||||
Ok(Redirect::to(&next.to_string()).into_response())
|
||||
}
|
||||
(None, _) => {
|
||||
// Other cases where we don't have a session, ask for a login
|
||||
txn.commit().await?;
|
||||
|
||||
let next: PostAuthAction = next.into();
|
||||
let next: LoginRequest = next.into();
|
||||
let next = next.build_uri()?;
|
||||
|
||||
Ok(Redirect::to(&next.to_string()).into_response())
|
||||
}
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
let response = match res {
|
||||
Ok(r) => r,
|
||||
Err(err) => {
|
||||
tracing::error!(%err);
|
||||
StatusCode::INTERNAL_SERVER_ERROR.into_response()
|
||||
}
|
||||
};
|
||||
|
||||
Ok((cookie_jar, response).into_response())
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub(crate) struct ContinueAuthorizationGrant {
|
||||
data: String,
|
||||
}
|
||||
|
||||
impl ContinueAuthorizationGrant {
|
||||
pub fn from_authorization_grant<S: StorageBackend>(grant: &AuthorizationGrant<S>) -> Self
|
||||
where
|
||||
S::AuthorizationGrantData: std::fmt::Display,
|
||||
{
|
||||
Self {
|
||||
data: grant.data.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build_uri(&self) -> anyhow::Result<Uri> {
|
||||
let qs = serde_urlencoded::to_string(self)?;
|
||||
let path_and_query = PathAndQuery::try_from(format!("/oauth2/authorize/step?{}", qs))?;
|
||||
let uri = Uri::from_parts({
|
||||
let mut parts = Parts::default();
|
||||
parts.path_and_query = Some(path_and_query);
|
||||
parts
|
||||
})?;
|
||||
Ok(uri)
|
||||
}
|
||||
|
||||
pub async fn fetch_authorization_grant(
|
||||
&self,
|
||||
conn: &mut PgConnection,
|
||||
) -> anyhow::Result<AuthorizationGrant<PostgresqlBackend>> {
|
||||
let data = self.data.parse()?;
|
||||
get_grant_by_id(conn, data).await
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn step_get(
|
||||
Extension(templates): Extension<Templates>,
|
||||
Extension(pool): Extension<PgPool>,
|
||||
Query(next): Query<ContinueAuthorizationGrant>,
|
||||
cookie_jar: PrivateCookieJar<Encrypter>,
|
||||
) -> Result<Response, RouteError> {
|
||||
let mut txn = pool.begin().await?;
|
||||
|
||||
let (session_info, cookie_jar) = cookie_jar.session_info();
|
||||
|
||||
let maybe_session = session_info
|
||||
.load_session(&mut txn)
|
||||
.await
|
||||
// TODO
|
||||
.context("could not load db session")?;
|
||||
|
||||
let session = if let Some(session) = maybe_session {
|
||||
session
|
||||
} else {
|
||||
// If there is no session, redirect to the login screen, redirecting here after
|
||||
// logout
|
||||
let next: PostAuthAction = next.into();
|
||||
let login: LoginRequest = next.into();
|
||||
let login = login.build_uri()?;
|
||||
return Ok((cookie_jar, Redirect::to(&login.to_string())).into_response());
|
||||
};
|
||||
|
||||
step(next, session, txn, &templates).await
|
||||
}
|
||||
|
||||
async fn step(
|
||||
next: ContinueAuthorizationGrant,
|
||||
browser_session: BrowserSession<PostgresqlBackend>,
|
||||
mut txn: Transaction<'_, Postgres>,
|
||||
templates: &Templates,
|
||||
) -> Result<Response, RouteError> {
|
||||
// TODO: we should check if the grant here was started by the browser doing that
|
||||
// request using a signed cookie
|
||||
let grant = next.fetch_authorization_grant(&mut txn).await?;
|
||||
|
||||
let callback_destination = CallbackDestination::try_from(&grant)?;
|
||||
|
||||
if !matches!(grant.stage, AuthorizationGrantStage::Pending) {
|
||||
return Err(anyhow!("authorization grant not pending").into());
|
||||
}
|
||||
|
||||
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?;
|
||||
|
||||
// Yep! Let's complete the auth now
|
||||
let mut params = AuthorizationResponse::default();
|
||||
|
||||
// Did they request an auth code?
|
||||
if let Some(code) = grant.code {
|
||||
params.code = Some(code.code);
|
||||
}
|
||||
|
||||
// Did they request an access token?
|
||||
if grant.response_type_token {
|
||||
let ttl = Duration::minutes(5);
|
||||
let (access_token_str, refresh_token_str) = {
|
||||
let mut rng = thread_rng();
|
||||
(
|
||||
TokenType::AccessToken.generate(&mut rng),
|
||||
TokenType::RefreshToken.generate(&mut rng),
|
||||
)
|
||||
};
|
||||
|
||||
let access_token =
|
||||
add_access_token(&mut txn, &session, &access_token_str, ttl).await?;
|
||||
|
||||
let _refresh_token =
|
||||
add_refresh_token(&mut txn, &session, access_token, &refresh_token_str).await?;
|
||||
|
||||
params.response = Some(
|
||||
AccessTokenResponse::new(access_token_str)
|
||||
.with_expires_in(ttl)
|
||||
.with_refresh_token(refresh_token_str),
|
||||
);
|
||||
}
|
||||
|
||||
// Did they request an ID token?
|
||||
if grant.response_type_id_token {
|
||||
return Err(RouteError::Anyhow(anyhow!(
|
||||
"id tokens are not implemented yet"
|
||||
)));
|
||||
}
|
||||
|
||||
let params = serde_json::to_value(¶ms).unwrap();
|
||||
|
||||
callback_destination.go(templates, params).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();
|
||||
let next = next.build_uri()?;
|
||||
|
||||
Redirect::to(&next.to_string()).into_response()
|
||||
}
|
||||
};
|
||||
|
||||
txn.commit().await?;
|
||||
Ok(reply)
|
||||
}
|
@ -24,6 +24,7 @@ use serde::Serialize;
|
||||
use thiserror::Error;
|
||||
use url::Url;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
enum CallbackDestinationMode {
|
||||
Query {
|
||||
existing_params: HashMap<String, String>,
|
||||
@ -32,6 +33,7 @@ enum CallbackDestinationMode {
|
||||
FormPost,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CallbackDestination {
|
||||
mode: CallbackDestinationMode,
|
||||
safe_redirect_uri: Url,
|
||||
|
257
crates/handlers/src/oauth2/authorization/complete.rs
Normal file
257
crates/handlers/src/oauth2/authorization/complete.rs
Normal file
@ -0,0 +1,257 @@
|
||||
// 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::anyhow;
|
||||
use axum::{
|
||||
extract::Path,
|
||||
response::{IntoResponse, Response},
|
||||
Extension,
|
||||
};
|
||||
use axum_extra::extract::PrivateCookieJar;
|
||||
use chrono::Duration;
|
||||
use hyper::StatusCode;
|
||||
use mas_axum_utils::SessionInfoExt;
|
||||
use mas_config::Encrypter;
|
||||
use mas_data_model::{AuthorizationGrant, BrowserSession, TokenType};
|
||||
use mas_storage::{
|
||||
oauth2::{
|
||||
access_token::add_access_token,
|
||||
authorization_grant::{derive_session, fulfill_grant, get_grant_by_id},
|
||||
consent::fetch_client_consent,
|
||||
refresh_token::add_refresh_token,
|
||||
},
|
||||
user::ActiveSessionLookupError,
|
||||
PostgresqlBackend,
|
||||
};
|
||||
use mas_templates::Templates;
|
||||
use oauth2_types::requests::{AccessTokenResponse, AuthorizationResponse};
|
||||
use rand::thread_rng;
|
||||
use sqlx::{PgPool, Postgres, Transaction};
|
||||
use thiserror::Error;
|
||||
|
||||
use super::callback::{CallbackDestination, CallbackDestinationError, InvalidRedirectUriError};
|
||||
use crate::{
|
||||
oauth2::consent::ConsentRequest,
|
||||
views::{LoginRequest, PostAuthAction, ReauthRequest},
|
||||
};
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum RouteError {
|
||||
#[error(transparent)]
|
||||
Internal(Box<dyn std::error::Error + Send + Sync + 'static>),
|
||||
|
||||
#[error(transparent)]
|
||||
Anyhow(anyhow::Error),
|
||||
|
||||
#[error("authorization grant is not in a pending state")]
|
||||
NotPending,
|
||||
}
|
||||
|
||||
impl IntoResponse for RouteError {
|
||||
fn into_response(self) -> axum::response::Response {
|
||||
// TODO: better error pages
|
||||
match self {
|
||||
RouteError::NotPending => (
|
||||
StatusCode::BAD_REQUEST,
|
||||
"authorization grant not in a pending state",
|
||||
)
|
||||
.into_response(),
|
||||
RouteError::Anyhow(e) => {
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response()
|
||||
}
|
||||
RouteError::Internal(e) => {
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<anyhow::Error> for RouteError {
|
||||
fn from(e: anyhow::Error) -> Self {
|
||||
Self::Anyhow(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<sqlx::Error> for RouteError {
|
||||
fn from(e: sqlx::Error) -> Self {
|
||||
Self::Internal(Box::new(e))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ActiveSessionLookupError> for RouteError {
|
||||
fn from(e: ActiveSessionLookupError) -> Self {
|
||||
Self::Internal(Box::new(e))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<InvalidRedirectUriError> for RouteError {
|
||||
fn from(e: InvalidRedirectUriError) -> Self {
|
||||
Self::Internal(Box::new(e))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CallbackDestinationError> for RouteError {
|
||||
fn from(e: CallbackDestinationError) -> Self {
|
||||
Self::Internal(Box::new(e))
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn get(
|
||||
Extension(templates): Extension<Templates>,
|
||||
Extension(pool): Extension<PgPool>,
|
||||
cookie_jar: PrivateCookieJar<Encrypter>,
|
||||
Path(grant_id): Path<i64>,
|
||||
) -> Result<Response, RouteError> {
|
||||
let mut txn = pool.begin().await?;
|
||||
|
||||
let (session_info, cookie_jar) = cookie_jar.session_info();
|
||||
|
||||
let maybe_session = session_info.load_session(&mut txn).await?;
|
||||
|
||||
let grant = get_grant_by_id(&mut txn, grant_id).await?;
|
||||
|
||||
let callback_destination = CallbackDestination::try_from(&grant)?;
|
||||
let continue_grant = PostAuthAction::continue_grant(&grant);
|
||||
let consent_request = ConsentRequest::for_grant(&grant);
|
||||
|
||||
let session = if let Some(session) = maybe_session {
|
||||
session
|
||||
} else {
|
||||
// If there is no session, redirect to the login screen, redirecting here after
|
||||
// logout
|
||||
return Ok((cookie_jar, LoginRequest::from(continue_grant).go()).into_response());
|
||||
};
|
||||
|
||||
match complete(grant, session, txn).await {
|
||||
Ok(params) => {
|
||||
let res = callback_destination.go(&templates, params).await?;
|
||||
Ok((cookie_jar, res).into_response())
|
||||
}
|
||||
Err(GrantCompletionError::RequiresReauth) => {
|
||||
Ok((cookie_jar, ReauthRequest::from(continue_grant).go()).into_response())
|
||||
}
|
||||
Err(GrantCompletionError::RequiresConsent) => {
|
||||
Ok((cookie_jar, consent_request.go()).into_response())
|
||||
}
|
||||
Err(GrantCompletionError::NotPending) => Err(RouteError::NotPending),
|
||||
Err(GrantCompletionError::Internal(e)) => Err(RouteError::Internal(e)),
|
||||
Err(GrantCompletionError::Anyhow(e)) => Err(RouteError::Anyhow(e)),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum GrantCompletionError {
|
||||
#[error(transparent)]
|
||||
Internal(Box<dyn std::error::Error + Send + Sync + 'static>),
|
||||
|
||||
#[error(transparent)]
|
||||
Anyhow(#[from] anyhow::Error),
|
||||
|
||||
#[error("authorization grant is not in a pending state")]
|
||||
NotPending,
|
||||
|
||||
#[error("user needs to reauthenticate")]
|
||||
RequiresReauth,
|
||||
|
||||
#[error("client lacks consent")]
|
||||
RequiresConsent,
|
||||
}
|
||||
|
||||
impl From<sqlx::Error> for GrantCompletionError {
|
||||
fn from(e: sqlx::Error) -> Self {
|
||||
Self::Internal(Box::new(e))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<InvalidRedirectUriError> for GrantCompletionError {
|
||||
fn from(e: InvalidRedirectUriError) -> Self {
|
||||
Self::Internal(Box::new(e))
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn complete(
|
||||
grant: AuthorizationGrant<PostgresqlBackend>,
|
||||
browser_session: BrowserSession<PostgresqlBackend>,
|
||||
mut txn: Transaction<'_, Postgres>,
|
||||
) -> Result<AuthorizationResponse<Option<AccessTokenResponse>>, GrantCompletionError> {
|
||||
// Verify that the grant is in a pending stage
|
||||
if !grant.stage.is_pending() {
|
||||
return Err(GrantCompletionError::NotPending);
|
||||
}
|
||||
|
||||
// Check if the authentication is fresh enough
|
||||
if !browser_session.was_authenticated_after(grant.max_auth_time()) {
|
||||
txn.commit().await?;
|
||||
return Err(GrantCompletionError::RequiresReauth);
|
||||
}
|
||||
|
||||
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:"));
|
||||
|
||||
// Check if the client lacks consent *or* if consent was explicitely asked
|
||||
if lacks_consent || grant.requires_consent {
|
||||
txn.commit().await?;
|
||||
return Err(GrantCompletionError::RequiresConsent);
|
||||
}
|
||||
|
||||
// All good, let's start the session
|
||||
let session = derive_session(&mut txn, &grant, browser_session).await?;
|
||||
|
||||
let grant = fulfill_grant(&mut txn, grant, session.clone()).await?;
|
||||
|
||||
// Yep! Let's complete the auth now
|
||||
let mut params = AuthorizationResponse::default();
|
||||
|
||||
// Did they request an auth code?
|
||||
if let Some(code) = grant.code {
|
||||
params.code = Some(code.code);
|
||||
}
|
||||
|
||||
// Did they request an access token?
|
||||
// TODO: maybe we don't want to support the implicit flows
|
||||
if grant.response_type_token {
|
||||
let ttl = Duration::minutes(5);
|
||||
let (access_token_str, refresh_token_str) = {
|
||||
let mut rng = thread_rng();
|
||||
(
|
||||
TokenType::AccessToken.generate(&mut rng),
|
||||
TokenType::RefreshToken.generate(&mut rng),
|
||||
)
|
||||
};
|
||||
|
||||
let access_token = add_access_token(&mut txn, &session, &access_token_str, ttl).await?;
|
||||
|
||||
let _refresh_token =
|
||||
add_refresh_token(&mut txn, &session, access_token, &refresh_token_str).await?;
|
||||
|
||||
params.response = Some(
|
||||
AccessTokenResponse::new(access_token_str)
|
||||
.with_expires_in(ttl)
|
||||
.with_refresh_token(refresh_token_str),
|
||||
);
|
||||
}
|
||||
|
||||
// Did they request an ID token?
|
||||
if grant.response_type_id_token {
|
||||
return Err(anyhow!("id tokens are not implemented yet").into());
|
||||
}
|
||||
|
||||
txn.commit().await?;
|
||||
Ok(params)
|
||||
}
|
381
crates/handlers/src/oauth2/authorization/mod.rs
Normal file
381
crates/handlers/src/oauth2/authorization/mod.rs
Normal file
@ -0,0 +1,381 @@
|
||||
// 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.
|
||||
// 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::{anyhow, Context};
|
||||
use axum::{
|
||||
extract::{Extension, Form},
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use axum_extra::extract::PrivateCookieJar;
|
||||
use hyper::StatusCode;
|
||||
use mas_axum_utils::SessionInfoExt;
|
||||
use mas_config::Encrypter;
|
||||
use mas_data_model::{AuthorizationCode, Pkce};
|
||||
use mas_iana::oauth::OAuthAuthorizationEndpointResponseType;
|
||||
use mas_storage::oauth2::{
|
||||
authorization_grant::new_authorization_grant,
|
||||
client::{lookup_client_by_client_id, ClientFetchError},
|
||||
};
|
||||
use mas_templates::Templates;
|
||||
use oauth2_types::{
|
||||
errors::{
|
||||
CONSENT_REQUIRED, INTERACTION_REQUIRED, INVALID_REQUEST, LOGIN_REQUIRED,
|
||||
REGISTRATION_NOT_SUPPORTED, REQUEST_NOT_SUPPORTED, REQUEST_URI_NOT_SUPPORTED, SERVER_ERROR,
|
||||
UNAUTHORIZED_CLIENT,
|
||||
},
|
||||
pkce,
|
||||
prelude::*,
|
||||
requests::{AuthorizationRequest, GrantType, Prompt, ResponseMode},
|
||||
scope::ScopeToken,
|
||||
};
|
||||
use rand::{distributions::Alphanumeric, thread_rng, Rng};
|
||||
use serde::Deserialize;
|
||||
use sqlx::PgPool;
|
||||
use thiserror::Error;
|
||||
|
||||
use self::{callback::CallbackDestination, complete::GrantCompletionError};
|
||||
use super::consent::ConsentRequest;
|
||||
use crate::views::{LoginRequest, PostAuthAction, ReauthRequest, RegisterRequest};
|
||||
|
||||
mod callback;
|
||||
pub mod complete;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum RouteError {
|
||||
#[error(transparent)]
|
||||
Internal(Box<dyn std::error::Error + Send + Sync + 'static>),
|
||||
|
||||
#[error(transparent)]
|
||||
Anyhow(anyhow::Error),
|
||||
|
||||
#[error("could not find client")]
|
||||
ClientNotFound,
|
||||
|
||||
#[error("invalid redirect uri")]
|
||||
InvalidRedirectUri(#[from] self::callback::InvalidRedirectUriError),
|
||||
|
||||
#[error("invalid redirect uri")]
|
||||
UnknownRedirectUri(#[from] mas_data_model::InvalidRedirectUriError),
|
||||
}
|
||||
|
||||
impl IntoResponse for RouteError {
|
||||
fn into_response(self) -> axum::response::Response {
|
||||
// TODO: better error pages
|
||||
match self {
|
||||
RouteError::Internal(e) => {
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response()
|
||||
}
|
||||
RouteError::Anyhow(e) => {
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response()
|
||||
}
|
||||
RouteError::ClientNotFound => {
|
||||
(StatusCode::BAD_REQUEST, "could not find client").into_response()
|
||||
}
|
||||
RouteError::InvalidRedirectUri(e) => (
|
||||
StatusCode::BAD_REQUEST,
|
||||
format!("Invalid redirect URI ({})", e),
|
||||
)
|
||||
.into_response(),
|
||||
RouteError::UnknownRedirectUri(e) => (
|
||||
StatusCode::BAD_REQUEST,
|
||||
format!("Invalid redirect URI ({})", e),
|
||||
)
|
||||
.into_response(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<sqlx::Error> for RouteError {
|
||||
fn from(e: sqlx::Error) -> Self {
|
||||
Self::Internal(Box::new(e))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<self::callback::CallbackDestinationError> for RouteError {
|
||||
fn from(e: self::callback::CallbackDestinationError) -> Self {
|
||||
Self::Internal(Box::new(e))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ClientFetchError> for RouteError {
|
||||
fn from(e: ClientFetchError) -> Self {
|
||||
if e.not_found() {
|
||||
Self::ClientNotFound
|
||||
} else {
|
||||
Self::Internal(Box::new(e))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<anyhow::Error> for RouteError {
|
||||
fn from(e: anyhow::Error) -> Self {
|
||||
Self::Anyhow(e)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub(crate) struct Params {
|
||||
#[serde(flatten)]
|
||||
auth: AuthorizationRequest,
|
||||
|
||||
#[serde(flatten)]
|
||||
pkce: Option<pkce::AuthorizationRequest>,
|
||||
}
|
||||
|
||||
/// Given a list of response types and an optional user-defined response mode,
|
||||
/// figure out what response mode must be used, and emit an error if the
|
||||
/// suggested response mode isn't allowed for the given response types.
|
||||
fn resolve_response_mode(
|
||||
response_type: OAuthAuthorizationEndpointResponseType,
|
||||
suggested_response_mode: Option<ResponseMode>,
|
||||
) -> anyhow::Result<ResponseMode> {
|
||||
use ResponseMode as M;
|
||||
|
||||
// If the response type includes either "token" or "id_token", the default
|
||||
// response mode is "fragment" and the response mode "query" must not be
|
||||
// used
|
||||
if response_type.has_token() || response_type.has_id_token() {
|
||||
match suggested_response_mode {
|
||||
None => Ok(M::Fragment),
|
||||
Some(M::Query) => Err(anyhow!("invalid response mode")),
|
||||
Some(mode) => Ok(mode),
|
||||
}
|
||||
} else {
|
||||
// In other cases, all response modes are allowed, defaulting to "query"
|
||||
Ok(suggested_response_mode.unwrap_or(M::Query))
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
pub(crate) async fn get(
|
||||
Extension(templates): Extension<Templates>,
|
||||
Extension(pool): Extension<PgPool>,
|
||||
cookie_jar: PrivateCookieJar<Encrypter>,
|
||||
Form(params): Form<Params>,
|
||||
) -> Result<Response, RouteError> {
|
||||
let mut txn = pool.begin().await?;
|
||||
|
||||
// First, figure out what client it is
|
||||
let client = lookup_client_by_client_id(&mut txn, ¶ms.auth.client_id).await?;
|
||||
|
||||
// And resolve the redirect_uri and response_mode
|
||||
let redirect_uri = client
|
||||
.resolve_redirect_uri(¶ms.auth.redirect_uri)?
|
||||
.clone();
|
||||
let response_type = params.auth.response_type;
|
||||
let response_mode = resolve_response_mode(response_type, params.auth.response_mode)?;
|
||||
|
||||
// Now we have a proper callback destination to go to on error
|
||||
let callback_destination = CallbackDestination::try_new(
|
||||
response_mode,
|
||||
redirect_uri.clone(),
|
||||
params.auth.state.clone(),
|
||||
)?;
|
||||
|
||||
// Get the session info from the cookie
|
||||
let (session_info, cookie_jar) = cookie_jar.session_info();
|
||||
|
||||
// One day, we will have try blocks
|
||||
let res: Result<Response, RouteError> = ({
|
||||
let templates = templates.clone();
|
||||
let callback_destination = callback_destination.clone();
|
||||
async move {
|
||||
let maybe_session = session_info
|
||||
.load_session(&mut txn)
|
||||
.await
|
||||
.context("failed to load browser session")?;
|
||||
|
||||
// Check if the request/request_uri/registration params are used. If so, reply
|
||||
// with the right error since we don't support them.
|
||||
if params.auth.request.is_some() {
|
||||
return Ok(callback_destination
|
||||
.go(&templates, REQUEST_NOT_SUPPORTED)
|
||||
.await?);
|
||||
}
|
||||
|
||||
if params.auth.request_uri.is_some() {
|
||||
return Ok(callback_destination
|
||||
.go(&templates, REQUEST_URI_NOT_SUPPORTED)
|
||||
.await?);
|
||||
}
|
||||
|
||||
if params.auth.registration.is_some() {
|
||||
return Ok(callback_destination
|
||||
.go(&templates, REGISTRATION_NOT_SUPPORTED)
|
||||
.await?);
|
||||
}
|
||||
|
||||
// Check if it is allowed to use this grant type
|
||||
if !client.grant_types.contains(&GrantType::AuthorizationCode) {
|
||||
return Ok(callback_destination
|
||||
.go(&templates, UNAUTHORIZED_CLIENT)
|
||||
.await?);
|
||||
}
|
||||
|
||||
// Fail early if prompt=none and there is no active session
|
||||
if params.auth.prompt == Some(Prompt::None) && maybe_session.is_none() {
|
||||
return Ok(callback_destination.go(&templates, LOGIN_REQUIRED).await?);
|
||||
}
|
||||
|
||||
let code: Option<AuthorizationCode> = if response_type.has_code() {
|
||||
// 32 random alphanumeric characters, about 190bit of entropy
|
||||
let code: String = thread_rng()
|
||||
.sample_iter(&Alphanumeric)
|
||||
.take(32)
|
||||
.map(char::from)
|
||||
.collect();
|
||||
|
||||
let pkce = params.pkce.map(|p| Pkce {
|
||||
challenge: p.code_challenge,
|
||||
challenge_method: p.code_challenge_method,
|
||||
});
|
||||
|
||||
Some(AuthorizationCode { code, pkce })
|
||||
} 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(callback_destination.go(&templates, INVALID_REQUEST).await?);
|
||||
}
|
||||
|
||||
None
|
||||
};
|
||||
|
||||
// Generate the device ID
|
||||
// TODO: this should probably be done somewhere else?
|
||||
let device_id: String = thread_rng()
|
||||
.sample_iter(&Alphanumeric)
|
||||
.take(10)
|
||||
.map(char::from)
|
||||
.collect();
|
||||
let device_scope: ScopeToken = format!("urn:matrix:device:{}", device_id)
|
||||
.parse()
|
||||
.context("could not parse generated device scope")?;
|
||||
|
||||
let scope = {
|
||||
let mut s = params.auth.scope.clone();
|
||||
s.insert(device_scope);
|
||||
s
|
||||
};
|
||||
|
||||
let requires_consent = params.auth.prompt == Some(Prompt::Consent);
|
||||
|
||||
let grant = new_authorization_grant(
|
||||
&mut txn,
|
||||
client,
|
||||
redirect_uri.clone(),
|
||||
scope,
|
||||
code,
|
||||
params.auth.state.clone(),
|
||||
params.auth.nonce,
|
||||
params.auth.max_age,
|
||||
None,
|
||||
response_mode,
|
||||
response_type.has_token(),
|
||||
response_type.has_id_token(),
|
||||
requires_consent,
|
||||
)
|
||||
.await?;
|
||||
let continue_grant = PostAuthAction::continue_grant(&grant);
|
||||
let consent_request = ConsentRequest::for_grant(&grant);
|
||||
|
||||
let res = match (maybe_session, params.auth.prompt) {
|
||||
// Cases where there is no active session, redirect to the relevant page
|
||||
(None, Some(Prompt::None)) => {
|
||||
// This case should already be handled earlier
|
||||
unreachable!();
|
||||
}
|
||||
(None, Some(Prompt::Create)) => {
|
||||
// Client asked for a registration, show the registration prompt
|
||||
txn.commit().await?;
|
||||
|
||||
RegisterRequest::from(continue_grant).go().into_response()
|
||||
}
|
||||
(None, _) => {
|
||||
// Other cases where we don't have a session, ask for a login
|
||||
txn.commit().await?;
|
||||
|
||||
LoginRequest::from(continue_grant).go().into_response()
|
||||
}
|
||||
|
||||
// Special case when we already have a sesion but prompt=login|select_account
|
||||
(Some(_), Some(Prompt::Login | Prompt::SelectAccount)) => {
|
||||
// TODO: better pages here
|
||||
txn.commit().await?;
|
||||
|
||||
ReauthRequest::from(continue_grant).go().into_response()
|
||||
}
|
||||
|
||||
// Else, we immediately try to complete the authorization grant
|
||||
(Some(user_session), Some(Prompt::None)) => {
|
||||
// With prompt=none, we should get back to the client immediately
|
||||
match self::complete::complete(grant, user_session, txn).await {
|
||||
Ok(params) => callback_destination.go(&templates, params).await?,
|
||||
Err(GrantCompletionError::RequiresConsent) => {
|
||||
callback_destination
|
||||
.go(&templates, CONSENT_REQUIRED)
|
||||
.await?
|
||||
}
|
||||
Err(GrantCompletionError::RequiresReauth) => {
|
||||
callback_destination
|
||||
.go(&templates, INTERACTION_REQUIRED)
|
||||
.await?
|
||||
}
|
||||
Err(GrantCompletionError::Anyhow(a)) => return Err(RouteError::Anyhow(a)),
|
||||
Err(GrantCompletionError::Internal(e)) => {
|
||||
return Err(RouteError::Internal(e))
|
||||
}
|
||||
Err(GrantCompletionError::NotPending) => {
|
||||
// This should never happen
|
||||
return Err(anyhow!("authorization grant is not pending").into());
|
||||
}
|
||||
}
|
||||
}
|
||||
(Some(user_session), _) => {
|
||||
// Else, we show the relevant reauth/consent page if necessary
|
||||
match self::complete::complete(grant, user_session, txn).await {
|
||||
Ok(params) => callback_destination.go(&templates, params).await?,
|
||||
Err(GrantCompletionError::RequiresConsent) => {
|
||||
consent_request.go().into_response()
|
||||
}
|
||||
Err(GrantCompletionError::RequiresReauth) => {
|
||||
ReauthRequest::from(continue_grant).go().into_response()
|
||||
}
|
||||
Err(GrantCompletionError::Anyhow(a)) => return Err(RouteError::Anyhow(a)),
|
||||
Err(GrantCompletionError::Internal(e)) => {
|
||||
return Err(RouteError::Internal(e))
|
||||
}
|
||||
Err(GrantCompletionError::NotPending) => {
|
||||
// This should never happen
|
||||
return Err(anyhow!("authorization grant is not pending").into());
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
let response = match res {
|
||||
Ok(r) => r,
|
||||
Err(err) => {
|
||||
tracing::error!(%err);
|
||||
callback_destination.go(&templates, SERVER_ERROR).await?
|
||||
}
|
||||
};
|
||||
|
||||
Ok((cookie_jar, response).into_response())
|
||||
}
|
@ -14,24 +14,28 @@
|
||||
|
||||
use anyhow::Context;
|
||||
use axum::{
|
||||
extract::{Extension, Form, Query},
|
||||
http::uri::{Parts, PathAndQuery},
|
||||
extract::{Extension, Form, Path},
|
||||
response::{Html, IntoResponse, Redirect, Response},
|
||||
};
|
||||
use axum_extra::extract::PrivateCookieJar;
|
||||
use hyper::{StatusCode, Uri};
|
||||
use hyper::StatusCode;
|
||||
use mas_axum_utils::{
|
||||
csrf::{CsrfExt, ProtectedForm},
|
||||
SessionInfoExt,
|
||||
};
|
||||
use mas_config::Encrypter;
|
||||
use mas_data_model::AuthorizationGrantStage;
|
||||
use mas_storage::oauth2::consent::insert_client_consent;
|
||||
use mas_data_model::{AuthorizationGrant, AuthorizationGrantStage};
|
||||
use mas_storage::{
|
||||
oauth2::{
|
||||
authorization_grant::{get_grant_by_id, give_consent_to_grant},
|
||||
consent::insert_client_consent,
|
||||
},
|
||||
PostgresqlBackend,
|
||||
};
|
||||
use mas_templates::{ConsentContext, TemplateContext, Templates};
|
||||
use sqlx::PgPool;
|
||||
use thiserror::Error;
|
||||
|
||||
use super::ContinueAuthorizationGrant;
|
||||
use crate::views::{LoginRequest, PostAuthAction};
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
@ -47,25 +51,19 @@ impl IntoResponse for RouteError {
|
||||
}
|
||||
|
||||
pub(crate) struct ConsentRequest {
|
||||
grant: ContinueAuthorizationGrant,
|
||||
}
|
||||
|
||||
impl From<ContinueAuthorizationGrant> for ConsentRequest {
|
||||
fn from(grant: ContinueAuthorizationGrant) -> Self {
|
||||
Self { grant }
|
||||
}
|
||||
grant_id: i64,
|
||||
}
|
||||
|
||||
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 fn for_grant(grant: &AuthorizationGrant<PostgresqlBackend>) -> Self {
|
||||
Self {
|
||||
grant_id: grant.data,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn go(&self) -> Redirect {
|
||||
let uri = format!("/consent/{}", self.grant_id);
|
||||
Redirect::to(&uri)
|
||||
}
|
||||
}
|
||||
|
||||
@ -73,7 +71,7 @@ pub(crate) async fn get(
|
||||
Extension(templates): Extension<Templates>,
|
||||
Extension(pool): Extension<PgPool>,
|
||||
cookie_jar: PrivateCookieJar<Encrypter>,
|
||||
Query(next): Query<ContinueAuthorizationGrant>,
|
||||
Path(grant_id): Path<i64>,
|
||||
) -> Result<Response, RouteError> {
|
||||
let mut conn = pool
|
||||
.acquire()
|
||||
@ -87,7 +85,7 @@ pub(crate) async fn get(
|
||||
.await
|
||||
.context("could not load session")?;
|
||||
|
||||
let grant = next.fetch_authorization_grant(&mut conn).await?;
|
||||
let grant = get_grant_by_id(&mut conn, grant_id).await?;
|
||||
|
||||
if !matches!(grant.stage, AuthorizationGrantStage::Pending) {
|
||||
return Err(anyhow::anyhow!("authorization grant not pending").into());
|
||||
@ -107,16 +105,15 @@ pub(crate) async fn get(
|
||||
|
||||
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())
|
||||
let login = LoginRequest::from(PostAuthAction::continue_grant(&grant));
|
||||
Ok((cookie_jar, login.go()).into_response())
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn post(
|
||||
Extension(pool): Extension<PgPool>,
|
||||
cookie_jar: PrivateCookieJar<Encrypter>,
|
||||
Query(next): Query<ContinueAuthorizationGrant>,
|
||||
Path(grant_id): Path<i64>,
|
||||
Form(form): Form<ProtectedForm<()>>,
|
||||
) -> Result<Response, RouteError> {
|
||||
let mut txn = pool
|
||||
@ -135,15 +132,16 @@ pub(crate) async fn post(
|
||||
.await
|
||||
.context("could not load session")?;
|
||||
|
||||
let grant = get_grant_by_id(&mut txn, grant_id).await?;
|
||||
let next = PostAuthAction::continue_grant(&grant);
|
||||
|
||||
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 login = LoginRequest::from(next);
|
||||
return Ok((cookie_jar, login.go()).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
|
||||
@ -159,8 +157,11 @@ pub(crate) async fn post(
|
||||
)
|
||||
.await?;
|
||||
|
||||
let _grant = give_consent_to_grant(&mut txn, grant)
|
||||
.await
|
||||
.context("failed to give consent to grant")?;
|
||||
|
||||
txn.commit().await.context("could not commit txn")?;
|
||||
|
||||
let uri = next.build_uri()?;
|
||||
Ok((cookie_jar, Redirect::to(&uri.to_string())).into_response())
|
||||
Ok((cookie_jar, next.redirect()).into_response())
|
||||
}
|
||||
|
@ -21,5 +21,3 @@ pub mod registration;
|
||||
pub mod token;
|
||||
pub mod userinfo;
|
||||
pub mod webfinger;
|
||||
|
||||
pub(crate) use authorization::ContinueAuthorizationGrant;
|
||||
|
@ -14,7 +14,7 @@
|
||||
|
||||
use axum::{
|
||||
extract::{Extension, Form},
|
||||
response::{Html, IntoResponse, Redirect, Response},
|
||||
response::{Html, IntoResponse, Response},
|
||||
};
|
||||
use axum_extra::extract::PrivateCookieJar;
|
||||
use lettre::{message::Mailbox, Address};
|
||||
@ -70,8 +70,7 @@ pub(crate) async fn get(
|
||||
render(templates, session, cookie_jar, &mut conn).await
|
||||
} else {
|
||||
let login = LoginRequest::default();
|
||||
let login = login.build_uri().map_err(fancy_error(templates.clone()))?;
|
||||
Ok((cookie_jar, Redirect::to(&login.to_string())).into_response())
|
||||
Ok((cookie_jar, login.go()).into_response())
|
||||
}
|
||||
}
|
||||
|
||||
@ -151,8 +150,7 @@ pub(crate) async fn post(
|
||||
session
|
||||
} else {
|
||||
let login = LoginRequest::default();
|
||||
let login = login.build_uri().map_err(fancy_error(templates.clone()))?;
|
||||
return Ok((cookie_jar, Redirect::to(&login.to_string())).into_response());
|
||||
return Ok((cookie_jar, login.go()).into_response());
|
||||
};
|
||||
|
||||
let form = cookie_jar
|
||||
|
@ -17,7 +17,7 @@ pub mod password;
|
||||
|
||||
use axum::{
|
||||
extract::Extension,
|
||||
response::{Html, IntoResponse, Redirect, Response},
|
||||
response::{Html, IntoResponse, Response},
|
||||
};
|
||||
use axum_extra::extract::PrivateCookieJar;
|
||||
use mas_axum_utils::{csrf::CsrfExt, fancy_error, FancyError, SessionInfoExt};
|
||||
@ -50,8 +50,7 @@ pub(crate) async fn get(
|
||||
session
|
||||
} else {
|
||||
let login = LoginRequest::default();
|
||||
let login = login.build_uri().map_err(fancy_error(templates.clone()))?;
|
||||
return Ok((cookie_jar, Redirect::to(&login.to_string())).into_response());
|
||||
return Ok((cookie_jar, login.go()).into_response());
|
||||
};
|
||||
|
||||
let active_sessions = count_active_sessions(&mut conn, &session.user)
|
||||
|
@ -15,7 +15,7 @@
|
||||
use argon2::Argon2;
|
||||
use axum::{
|
||||
extract::{Extension, Form},
|
||||
response::{Html, IntoResponse, Redirect, Response},
|
||||
response::{Html, IntoResponse, Response},
|
||||
};
|
||||
use axum_extra::extract::PrivateCookieJar;
|
||||
use mas_axum_utils::{
|
||||
@ -62,8 +62,7 @@ pub(crate) async fn get(
|
||||
render(templates, session, cookie_jar).await
|
||||
} else {
|
||||
let login = LoginRequest::default();
|
||||
let login = login.build_uri().map_err(fancy_error(templates.clone()))?;
|
||||
Ok((cookie_jar, Redirect::to(&login.to_string())).into_response())
|
||||
Ok((cookie_jar, login.go()).into_response())
|
||||
}
|
||||
}
|
||||
|
||||
@ -109,8 +108,7 @@ pub(crate) async fn post(
|
||||
session
|
||||
} else {
|
||||
let login = LoginRequest::default();
|
||||
let login = login.build_uri().map_err(fancy_error(templates.clone()))?;
|
||||
return Ok((cookie_jar, Redirect::to(&login.to_string())).into_response());
|
||||
return Ok((cookie_jar, login.go()).into_response());
|
||||
};
|
||||
|
||||
authenticate_session(&mut txn, &mut session, form.current_password)
|
||||
|
@ -12,12 +12,13 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::borrow::Cow;
|
||||
|
||||
use axum::{
|
||||
extract::{Extension, Form, Query},
|
||||
response::{Html, IntoResponse, Redirect, Response},
|
||||
};
|
||||
use axum_extra::extract::PrivateCookieJar;
|
||||
use hyper::http::uri::{Parts, PathAndQuery, Uri};
|
||||
use mas_axum_utils::{
|
||||
csrf::{CsrfExt, ProtectedForm},
|
||||
fancy_error, FancyError, SessionInfoExt,
|
||||
@ -31,7 +32,7 @@ use sqlx::PgPool;
|
||||
|
||||
use super::{shared::PostAuthAction, RegisterRequest};
|
||||
|
||||
#[derive(Deserialize, Default)]
|
||||
#[derive(Deserialize, Default, Debug)]
|
||||
pub(crate) struct LoginRequest {
|
||||
#[serde(flatten)]
|
||||
post_auth_action: Option<PostAuthAction>,
|
||||
@ -50,29 +51,25 @@ impl From<Option<PostAuthAction>> for LoginRequest {
|
||||
}
|
||||
|
||||
impl LoginRequest {
|
||||
pub fn build_uri(&self) -> anyhow::Result<Uri> {
|
||||
let path_and_query = if let Some(next) = &self.post_auth_action {
|
||||
let qs = serde_urlencoded::to_string(next)?;
|
||||
PathAndQuery::try_from(format!("/login?{}", qs))?
|
||||
pub fn as_link(&self) -> Cow<'static, str> {
|
||||
if let Some(next) = &self.post_auth_action {
|
||||
let qs = serde_urlencoded::to_string(next).unwrap();
|
||||
Cow::Owned(format!("/login?{}", qs))
|
||||
} else {
|
||||
PathAndQuery::from_static("/login")
|
||||
};
|
||||
let uri = Uri::from_parts({
|
||||
let mut parts = Parts::default();
|
||||
parts.path_and_query = Some(path_and_query);
|
||||
parts
|
||||
})?;
|
||||
Ok(uri)
|
||||
Cow::Borrowed("/login")
|
||||
}
|
||||
}
|
||||
|
||||
fn redirect(self) -> Result<impl IntoResponse, anyhow::Error> {
|
||||
let uri = if let Some(action) = self.post_auth_action {
|
||||
action.build_uri()?
|
||||
} else {
|
||||
Uri::from_static("/")
|
||||
};
|
||||
pub fn go(&self) -> Redirect {
|
||||
Redirect::to(&self.as_link())
|
||||
}
|
||||
|
||||
Ok(Redirect::to(&uri.to_string()))
|
||||
fn redirect(self) -> Redirect {
|
||||
if let Some(action) = self.post_auth_action {
|
||||
action.redirect()
|
||||
} else {
|
||||
Redirect::to("/")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -82,6 +79,7 @@ pub(crate) struct LoginForm {
|
||||
password: String,
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(templates, pool, cookie_jar))]
|
||||
pub(crate) async fn get(
|
||||
Extension(templates): Extension<Templates>,
|
||||
Extension(pool): Extension<PgPool>,
|
||||
@ -102,18 +100,13 @@ pub(crate) async fn get(
|
||||
.map_err(fancy_error(templates.clone()))?;
|
||||
|
||||
if maybe_session.is_some() {
|
||||
let response = query
|
||||
.redirect()
|
||||
.map_err(fancy_error(templates.clone()))?
|
||||
.into_response();
|
||||
let response = query.redirect().into_response();
|
||||
Ok(response)
|
||||
} else {
|
||||
let ctx = LoginContext::default();
|
||||
let ctx = match query.post_auth_action {
|
||||
Some(next) => {
|
||||
let register_link = RegisterRequest::from(next.clone())
|
||||
.build_uri()
|
||||
.map_err(fancy_error(templates.clone()))?;
|
||||
let register_link = RegisterRequest::from(next.clone()).as_link();
|
||||
let next = next
|
||||
.load_context(&mut conn)
|
||||
.await
|
||||
@ -157,7 +150,7 @@ pub(crate) async fn post(
|
||||
match login(&mut conn, &form.username, form.password).await {
|
||||
Ok(session_info) => {
|
||||
let cookie_jar = cookie_jar.set_session(&session_info);
|
||||
let reply = query.redirect().map_err(fancy_error(templates.clone()))?;
|
||||
let reply = query.redirect();
|
||||
Ok((cookie_jar, reply).into_response())
|
||||
}
|
||||
Err(e) => {
|
||||
|
@ -12,15 +12,13 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::borrow::Cow;
|
||||
|
||||
use axum::{
|
||||
extract::{Extension, Form, Query},
|
||||
response::{Html, IntoResponse, Redirect, Response},
|
||||
};
|
||||
use axum_extra::extract::PrivateCookieJar;
|
||||
use hyper::{
|
||||
http::uri::{Parts, PathAndQuery},
|
||||
Uri,
|
||||
};
|
||||
use mas_axum_utils::{
|
||||
csrf::{CsrfExt, ProtectedForm},
|
||||
fancy_error, FancyError, SessionInfoExt,
|
||||
@ -48,26 +46,24 @@ impl From<PostAuthAction> for ReauthRequest {
|
||||
}
|
||||
|
||||
impl ReauthRequest {
|
||||
pub fn build_uri(&self) -> anyhow::Result<Uri> {
|
||||
let path_and_query = if let Some(next) = &self.post_auth_action {
|
||||
let qs = serde_urlencoded::to_string(next)?;
|
||||
PathAndQuery::try_from(format!("/reauth?{}", qs))?
|
||||
pub fn as_link(&self) -> Cow<'static, str> {
|
||||
if let Some(next) = &self.post_auth_action {
|
||||
let qs = serde_urlencoded::to_string(next).unwrap();
|
||||
Cow::Owned(format!("/reauth?{}", qs))
|
||||
} else {
|
||||
PathAndQuery::from_static("/reauth")
|
||||
};
|
||||
let uri = Uri::from_parts({
|
||||
let mut parts = Parts::default();
|
||||
parts.path_and_query = Some(path_and_query);
|
||||
parts
|
||||
})?;
|
||||
Ok(uri)
|
||||
Cow::Borrowed("/reauth")
|
||||
}
|
||||
}
|
||||
|
||||
fn redirect(self) -> Result<impl IntoResponse, anyhow::Error> {
|
||||
pub fn go(&self) -> Redirect {
|
||||
Redirect::to(&self.as_link())
|
||||
}
|
||||
|
||||
fn redirect(self) -> Redirect {
|
||||
if let Some(action) = self.post_auth_action {
|
||||
Ok(Redirect::to(&action.build_uri()?.to_string()))
|
||||
action.redirect()
|
||||
} else {
|
||||
Ok(Redirect::to("/"))
|
||||
Redirect::to("/")
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -102,8 +98,7 @@ pub(crate) async fn get(
|
||||
// If there is no session, redirect to the login screen, keeping the
|
||||
// PostAuthAction
|
||||
let login: LoginRequest = query.post_auth_action.into();
|
||||
let login = login.build_uri().map_err(fancy_error(templates.clone()))?;
|
||||
return Ok((cookie_jar, Redirect::to(&login.to_string())).into_response());
|
||||
return Ok((cookie_jar, login.go()).into_response());
|
||||
};
|
||||
|
||||
let ctx = ReauthContext::default();
|
||||
@ -153,8 +148,7 @@ pub(crate) async fn post(
|
||||
// If there is no session, redirect to the login screen, keeping the
|
||||
// PostAuthAction
|
||||
let login: LoginRequest = query.post_auth_action.into();
|
||||
let login = login.build_uri().map_err(fancy_error(templates.clone()))?;
|
||||
return Ok((cookie_jar, Redirect::to(&login.to_string())).into_response());
|
||||
return Ok((cookie_jar, login.go()).into_response());
|
||||
};
|
||||
|
||||
// TODO: recover from errors here
|
||||
@ -164,6 +158,6 @@ pub(crate) async fn post(
|
||||
let cookie_jar = cookie_jar.set_session(&session);
|
||||
txn.commit().await.map_err(fancy_error(templates.clone()))?;
|
||||
|
||||
let redirection = query.redirect().map_err(fancy_error(templates.clone()))?;
|
||||
let redirection = query.redirect();
|
||||
Ok((cookie_jar, redirection).into_response())
|
||||
}
|
||||
|
@ -14,13 +14,14 @@
|
||||
|
||||
#![allow(clippy::trait_duplication_in_bounds)]
|
||||
|
||||
use std::borrow::Cow;
|
||||
|
||||
use argon2::Argon2;
|
||||
use axum::{
|
||||
extract::{Extension, Form, Query},
|
||||
response::{Html, IntoResponse, Redirect, Response},
|
||||
};
|
||||
use axum_extra::extract::PrivateCookieJar;
|
||||
use hyper::http::uri::{Parts, PathAndQuery, Uri};
|
||||
use mas_axum_utils::{
|
||||
csrf::{CsrfExt, ProtectedForm},
|
||||
fancy_error, FancyError, SessionInfoExt,
|
||||
@ -48,27 +49,24 @@ impl From<PostAuthAction> for RegisterRequest {
|
||||
}
|
||||
|
||||
impl RegisterRequest {
|
||||
#[allow(dead_code)]
|
||||
pub fn build_uri(&self) -> anyhow::Result<Uri> {
|
||||
let path_and_query = if let Some(next) = &self.post_auth_action {
|
||||
let qs = serde_urlencoded::to_string(next)?;
|
||||
PathAndQuery::try_from(format!("/register?{}", qs))?
|
||||
pub fn as_link(&self) -> Cow<'static, str> {
|
||||
if let Some(next) = &self.post_auth_action {
|
||||
let qs = serde_urlencoded::to_string(next).unwrap();
|
||||
Cow::Owned(format!("/register?{}", qs))
|
||||
} else {
|
||||
PathAndQuery::from_static("/register")
|
||||
};
|
||||
let uri = Uri::from_parts({
|
||||
let mut parts = Parts::default();
|
||||
parts.path_and_query = Some(path_and_query);
|
||||
parts
|
||||
})?;
|
||||
Ok(uri)
|
||||
Cow::Borrowed("/register")
|
||||
}
|
||||
}
|
||||
|
||||
fn redirect(self) -> Result<impl IntoResponse, anyhow::Error> {
|
||||
pub fn go(&self) -> Redirect {
|
||||
Redirect::to(&self.as_link())
|
||||
}
|
||||
|
||||
fn redirect(self) -> Redirect {
|
||||
if let Some(action) = self.post_auth_action {
|
||||
Ok(Redirect::to(&action.build_uri()?.to_string()))
|
||||
action.redirect()
|
||||
} else {
|
||||
Ok(Redirect::to("/"))
|
||||
Redirect::to("/")
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -100,10 +98,7 @@ pub(crate) async fn get(
|
||||
.map_err(fancy_error(templates.clone()))?;
|
||||
|
||||
if maybe_session.is_some() {
|
||||
let response = query
|
||||
.redirect()
|
||||
.map_err(fancy_error(templates.clone()))?
|
||||
.into_response();
|
||||
let response = query.redirect().into_response();
|
||||
Ok(response)
|
||||
} else {
|
||||
let ctx = RegisterContext::default();
|
||||
@ -117,9 +112,7 @@ pub(crate) async fn get(
|
||||
}
|
||||
None => ctx,
|
||||
};
|
||||
let login_link = LoginRequest::from(query.post_auth_action)
|
||||
.build_uri()
|
||||
.map_err(fancy_error(templates.clone()))?;
|
||||
let login_link = LoginRequest::from(query.post_auth_action).as_link();
|
||||
let ctx = ctx.with_login_link(login_link.to_string());
|
||||
let ctx = ctx.with_csrf(csrf_token.form_value());
|
||||
|
||||
@ -162,6 +155,6 @@ pub(crate) async fn post(
|
||||
txn.commit().await.map_err(fancy_error(templates.clone()))?;
|
||||
|
||||
let cookie_jar = cookie_jar.set_session(&session);
|
||||
let reply = query.redirect().map_err(fancy_error(templates.clone()))?;
|
||||
let reply = query.redirect();
|
||||
Ok((cookie_jar, reply).into_response())
|
||||
}
|
||||
|
@ -12,23 +12,33 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use hyper::Uri;
|
||||
use axum::response::Redirect;
|
||||
use mas_data_model::AuthorizationGrant;
|
||||
use mas_storage::{oauth2::authorization_grant::get_grant_by_id, PostgresqlBackend};
|
||||
use mas_templates::PostAuthContext;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::PgConnection;
|
||||
|
||||
use super::super::oauth2::ContinueAuthorizationGrant;
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone)]
|
||||
#[derive(Deserialize, Serialize, Clone, Debug)]
|
||||
#[serde(rename_all = "snake_case", tag = "next")]
|
||||
pub(crate) enum PostAuthAction {
|
||||
ContinueAuthorizationGrant(ContinueAuthorizationGrant),
|
||||
ContinueAuthorizationGrant {
|
||||
#[serde(deserialize_with = "serde_with::rust::display_fromstr::deserialize")]
|
||||
data: i64,
|
||||
},
|
||||
}
|
||||
|
||||
impl PostAuthAction {
|
||||
pub fn build_uri(&self) -> anyhow::Result<Uri> {
|
||||
pub fn continue_grant(grant: &AuthorizationGrant<PostgresqlBackend>) -> Self {
|
||||
Self::ContinueAuthorizationGrant { data: grant.data }
|
||||
}
|
||||
|
||||
pub fn redirect(&self) -> Redirect {
|
||||
match self {
|
||||
PostAuthAction::ContinueAuthorizationGrant(c) => c.build_uri(),
|
||||
PostAuthAction::ContinueAuthorizationGrant { data } => {
|
||||
let url = format!("/authorize/{}", data);
|
||||
Redirect::to(&url)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -37,8 +47,8 @@ impl PostAuthAction {
|
||||
conn: &mut PgConnection,
|
||||
) -> anyhow::Result<PostAuthContext> {
|
||||
match self {
|
||||
Self::ContinueAuthorizationGrant(c) => {
|
||||
let grant = c.fetch_authorization_grant(conn).await?;
|
||||
Self::ContinueAuthorizationGrant { data } => {
|
||||
let grant = get_grant_by_id(conn, *data).await?;
|
||||
let grant = grant.into();
|
||||
Ok(PostAuthContext::ContinueAuthorizationGrant { grant })
|
||||
}
|
||||
@ -46,8 +56,18 @@ impl PostAuthAction {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ContinueAuthorizationGrant> for PostAuthAction {
|
||||
fn from(g: ContinueAuthorizationGrant) -> Self {
|
||||
Self::ContinueAuthorizationGrant(g)
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parse_post_auth_action() {
|
||||
let action: PostAuthAction =
|
||||
serde_urlencoded::from_str("next=continue_authorization_grant&data=123").unwrap();
|
||||
|
||||
assert!(matches!(
|
||||
action,
|
||||
PostAuthAction::ContinueAuthorizationGrant { data: 123 }
|
||||
));
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,16 @@
|
||||
-- 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.
|
||||
|
||||
ALTER TABLE oauth2_authorization_grants
|
||||
DROP COLUMN requires_consent;
|
@ -0,0 +1,16 @@
|
||||
-- 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.
|
||||
|
||||
ALTER TABLE oauth2_authorization_grants
|
||||
ADD COLUMN requires_consent BOOLEAN NOT NULL DEFAULT 'f';
|
@ -1,5 +1,217 @@
|
||||
{
|
||||
"db": "PostgreSQL",
|
||||
"08896e50738af687ac53dc5ac5ae0b19bcac7503230ba90e11de799978d7a026": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "grant_id",
|
||||
"ordinal": 0,
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"name": "grant_created_at",
|
||||
"ordinal": 1,
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"name": "grant_cancelled_at",
|
||||
"ordinal": 2,
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"name": "grant_fulfilled_at",
|
||||
"ordinal": 3,
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"name": "grant_exchanged_at",
|
||||
"ordinal": 4,
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"name": "grant_scope",
|
||||
"ordinal": 5,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "grant_state",
|
||||
"ordinal": 6,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "grant_redirect_uri",
|
||||
"ordinal": 7,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "grant_response_mode",
|
||||
"ordinal": 8,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "grant_nonce",
|
||||
"ordinal": 9,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "grant_max_age",
|
||||
"ordinal": 10,
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"name": "grant_acr_values",
|
||||
"ordinal": 11,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "oauth2_client_id",
|
||||
"ordinal": 12,
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"name": "grant_code",
|
||||
"ordinal": 13,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "grant_response_type_code",
|
||||
"ordinal": 14,
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"name": "grant_response_type_token",
|
||||
"ordinal": 15,
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"name": "grant_response_type_id_token",
|
||||
"ordinal": 16,
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"name": "grant_code_challenge",
|
||||
"ordinal": 17,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "grant_code_challenge_method",
|
||||
"ordinal": 18,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "grant_requires_consent",
|
||||
"ordinal": 19,
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"name": "session_id?",
|
||||
"ordinal": 20,
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"name": "user_session_id?",
|
||||
"ordinal": 21,
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"name": "user_session_created_at?",
|
||||
"ordinal": 22,
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"name": "user_id?",
|
||||
"ordinal": 23,
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"name": "user_username?",
|
||||
"ordinal": 24,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "user_session_last_authentication_id?",
|
||||
"ordinal": 25,
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"name": "user_session_last_authentication_created_at?",
|
||||
"ordinal": 26,
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"name": "user_email_id?",
|
||||
"ordinal": 27,
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"name": "user_email?",
|
||||
"ordinal": 28,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "user_email_created_at?",
|
||||
"ordinal": 29,
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"name": "user_email_confirmed_at?",
|
||||
"ordinal": 30,
|
||||
"type_info": "Timestamptz"
|
||||
}
|
||||
],
|
||||
"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,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
}
|
||||
},
|
||||
"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 og.requires_consent AS grant_requires_consent,\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 "
|
||||
},
|
||||
"096060f2be446fd77ee29308c673f9ba9210fb110444f4fccfeb976424ef4376": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "\n UPDATE oauth2_authorization_grants AS og\n SET\n requires_consent = 'f'\n WHERE\n og.id = $1\n "
|
||||
},
|
||||
"0c056fcc1a85d00db88034bcc582376cf220e1933d2932e520c44ed9931f5c9d": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
@ -28,6 +240,46 @@
|
||||
},
|
||||
"query": "\n INSERT INTO oauth2_refresh_tokens\n (oauth2_session_id, oauth2_access_token_id, token)\n VALUES\n ($1, $2, $3)\n RETURNING\n id, created_at\n "
|
||||
},
|
||||
"0ce16ae459b815e4fbef78784fafea08b30443741b6817dd1d722f4960dc19f8": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "id",
|
||||
"ordinal": 0,
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"name": "created_at",
|
||||
"ordinal": 1,
|
||||
"type_info": "Timestamptz"
|
||||
}
|
||||
],
|
||||
"nullable": [
|
||||
false,
|
||||
false
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"Text",
|
||||
"Text",
|
||||
"Text",
|
||||
"Text",
|
||||
"Int4",
|
||||
"Text",
|
||||
"Text",
|
||||
"Text",
|
||||
"Text",
|
||||
"Bool",
|
||||
"Bool",
|
||||
"Bool",
|
||||
"Text",
|
||||
"Bool"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "\n INSERT INTO oauth2_authorization_grants\n (oauth2_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, requires_consent)\n VALUES\n ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)\n RETURNING id, created_at\n "
|
||||
},
|
||||
"11f29a7b467bef1cf483d91eede7849707e01847542e4fc3c1be702560bf36bf": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
@ -205,200 +457,6 @@
|
||||
},
|
||||
"query": "\n UPDATE users\n SET primary_email_id = user_emails.id \n FROM user_emails\n WHERE user_emails.id = $1\n AND users.id = user_emails.user_id\n "
|
||||
},
|
||||
"4f0e5c9a6d345a1f1e154d61cd7bb4d67f5d20499b411a44e6d8c39b5ef75ca6": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "grant_id",
|
||||
"ordinal": 0,
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"name": "grant_created_at",
|
||||
"ordinal": 1,
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"name": "grant_cancelled_at",
|
||||
"ordinal": 2,
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"name": "grant_fulfilled_at",
|
||||
"ordinal": 3,
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"name": "grant_exchanged_at",
|
||||
"ordinal": 4,
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"name": "grant_scope",
|
||||
"ordinal": 5,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "grant_state",
|
||||
"ordinal": 6,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "grant_redirect_uri",
|
||||
"ordinal": 7,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "grant_response_mode",
|
||||
"ordinal": 8,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "grant_nonce",
|
||||
"ordinal": 9,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "grant_max_age",
|
||||
"ordinal": 10,
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"name": "grant_acr_values",
|
||||
"ordinal": 11,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "oauth2_client_id",
|
||||
"ordinal": 12,
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"name": "grant_code",
|
||||
"ordinal": 13,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "grant_response_type_code",
|
||||
"ordinal": 14,
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"name": "grant_response_type_token",
|
||||
"ordinal": 15,
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"name": "grant_response_type_id_token",
|
||||
"ordinal": 16,
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"name": "grant_code_challenge",
|
||||
"ordinal": 17,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "grant_code_challenge_method",
|
||||
"ordinal": 18,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "session_id?",
|
||||
"ordinal": 19,
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"name": "user_session_id?",
|
||||
"ordinal": 20,
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"name": "user_session_created_at?",
|
||||
"ordinal": 21,
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"name": "user_id?",
|
||||
"ordinal": 22,
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"name": "user_username?",
|
||||
"ordinal": 23,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "user_session_last_authentication_id?",
|
||||
"ordinal": 24,
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"name": "user_session_last_authentication_created_at?",
|
||||
"ordinal": 25,
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"name": "user_email_id?",
|
||||
"ordinal": 26,
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"name": "user_email?",
|
||||
"ordinal": 27,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "user_email_created_at?",
|
||||
"ordinal": 28,
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"name": "user_email_confirmed_at?",
|
||||
"ordinal": 29,
|
||||
"type_info": "Timestamptz"
|
||||
}
|
||||
],
|
||||
"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,
|
||||
false,
|
||||
false,
|
||||
true
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
}
|
||||
},
|
||||
"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": [
|
||||
@ -1048,7 +1106,7 @@
|
||||
},
|
||||
"query": "\n DELETE FROM oauth2_access_tokens\n WHERE id = $1\n "
|
||||
},
|
||||
"99270fd3ddcc7421c5b26d0b8e0116356c13166887e7cf6ed6352cc879c80a68": {
|
||||
"9882e49f34dff80c1442565f035a1b47ed4dbae1a405f58cf2db198885bb9f47": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@ -1147,58 +1205,63 @@
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "session_id?",
|
||||
"name": "grant_requires_consent",
|
||||
"ordinal": 19,
|
||||
"type_info": "Int8"
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"name": "user_session_id?",
|
||||
"name": "session_id?",
|
||||
"ordinal": 20,
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"name": "user_session_created_at?",
|
||||
"name": "user_session_id?",
|
||||
"ordinal": 21,
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"name": "user_session_created_at?",
|
||||
"ordinal": 22,
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"name": "user_id?",
|
||||
"ordinal": 22,
|
||||
"ordinal": 23,
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"name": "user_username?",
|
||||
"ordinal": 23,
|
||||
"ordinal": 24,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "user_session_last_authentication_id?",
|
||||
"ordinal": 24,
|
||||
"ordinal": 25,
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"name": "user_session_last_authentication_created_at?",
|
||||
"ordinal": 25,
|
||||
"ordinal": 26,
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"name": "user_email_id?",
|
||||
"ordinal": 26,
|
||||
"ordinal": 27,
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"name": "user_email?",
|
||||
"ordinal": 27,
|
||||
"ordinal": 28,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "user_email_created_at?",
|
||||
"ordinal": 28,
|
||||
"ordinal": 29,
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"name": "user_email_confirmed_at?",
|
||||
"ordinal": 29,
|
||||
"ordinal": 30,
|
||||
"type_info": "Timestamptz"
|
||||
}
|
||||
],
|
||||
@ -1232,6 +1295,7 @@
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true
|
||||
],
|
||||
"parameters": {
|
||||
@ -1240,7 +1304,7 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"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.code = $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 og.requires_consent AS grant_requires_consent,\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.code = $1\n\n ORDER BY usa.created_at DESC\n LIMIT 1\n "
|
||||
},
|
||||
"99a1504e3cf80fb4eaad40e8593ac722ba1da7ee29ae674fa9ffe37dffa8b361": {
|
||||
"describe": {
|
||||
@ -1280,45 +1344,6 @@
|
||||
},
|
||||
"query": "\n INSERT INTO oauth2_client_redirect_uris (oauth2_client_id, redirect_uri)\n SELECT $1, uri FROM UNNEST($2::text[]) uri\n "
|
||||
},
|
||||
"aadf15f5f4396c9f571419784ef776827ec44e2b3b1b11c2934276c66f96f7d9": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "id",
|
||||
"ordinal": 0,
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"name": "created_at",
|
||||
"ordinal": 1,
|
||||
"type_info": "Timestamptz"
|
||||
}
|
||||
],
|
||||
"nullable": [
|
||||
false,
|
||||
false
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"Text",
|
||||
"Text",
|
||||
"Text",
|
||||
"Text",
|
||||
"Int4",
|
||||
"Text",
|
||||
"Text",
|
||||
"Text",
|
||||
"Text",
|
||||
"Bool",
|
||||
"Bool",
|
||||
"Bool",
|
||||
"Text"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "\n INSERT INTO oauth2_authorization_grants\n (oauth2_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 "
|
||||
},
|
||||
"aea289a04e151da235825305a5085bc6aa100fce139dbf10a2c1bed4867fc52a": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
|
@ -44,6 +44,7 @@ pub async fn new_authorization_grant(
|
||||
response_mode: ResponseMode,
|
||||
response_type_token: bool,
|
||||
response_type_id_token: bool,
|
||||
requires_consent: bool,
|
||||
) -> anyhow::Result<AuthorizationGrant<PostgresqlBackend>> {
|
||||
let code_challenge = code
|
||||
.as_ref()
|
||||
@ -61,9 +62,9 @@ pub async fn new_authorization_grant(
|
||||
(oauth2_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)
|
||||
code, requires_consent)
|
||||
VALUES
|
||||
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)
|
||||
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)
|
||||
RETURNING id, created_at
|
||||
"#,
|
||||
&client.data,
|
||||
@ -81,6 +82,7 @@ pub async fn new_authorization_grant(
|
||||
response_type_token,
|
||||
response_type_id_token,
|
||||
code_str,
|
||||
requires_consent,
|
||||
)
|
||||
.fetch_one(executor)
|
||||
.await
|
||||
@ -101,9 +103,11 @@ pub async fn new_authorization_grant(
|
||||
created_at: res.created_at,
|
||||
response_type_token,
|
||||
response_type_id_token,
|
||||
requires_consent,
|
||||
})
|
||||
}
|
||||
|
||||
#[allow(clippy::struct_excessive_bools)]
|
||||
struct GrantLookup {
|
||||
grant_id: i64,
|
||||
grant_created_at: DateTime<Utc>,
|
||||
@ -123,6 +127,7 @@ struct GrantLookup {
|
||||
grant_code: Option<String>,
|
||||
grant_code_challenge: Option<String>,
|
||||
grant_code_challenge_method: Option<String>,
|
||||
grant_requires_consent: bool,
|
||||
oauth2_client_id: i64,
|
||||
session_id: Option<i64>,
|
||||
user_session_id: Option<i64>,
|
||||
@ -315,6 +320,7 @@ impl GrantLookup {
|
||||
created_at: self.grant_created_at,
|
||||
response_type_token: self.grant_response_type_token,
|
||||
response_type_id_token: self.grant_response_type_id_token,
|
||||
requires_consent: self.grant_requires_consent,
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -347,6 +353,7 @@ pub async fn get_grant_by_id(
|
||||
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,
|
||||
og.requires_consent AS grant_requires_consent,
|
||||
os.id AS "session_id?",
|
||||
us.id AS "user_session_id?",
|
||||
us.created_at AS "user_session_created_at?",
|
||||
@ -415,6 +422,7 @@ pub async fn lookup_grant_by_code(
|
||||
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,
|
||||
og.requires_consent AS grant_requires_consent,
|
||||
os.id AS "session_id?",
|
||||
us.id AS "user_session_id?",
|
||||
us.created_at AS "user_session_created_at?",
|
||||
@ -511,13 +519,35 @@ pub async fn fulfill_grant(
|
||||
)
|
||||
.fetch_one(executor)
|
||||
.await
|
||||
.context("could not makr grant as fulfilled")?;
|
||||
.context("could not mark grant as fulfilled")?;
|
||||
|
||||
grant.stage = grant.stage.fulfill(fulfilled_at, session)?;
|
||||
|
||||
Ok(grant)
|
||||
}
|
||||
|
||||
pub async fn give_consent_to_grant(
|
||||
executor: impl PgExecutor<'_>,
|
||||
mut grant: AuthorizationGrant<PostgresqlBackend>,
|
||||
) -> Result<AuthorizationGrant<PostgresqlBackend>, sqlx::Error> {
|
||||
sqlx::query!(
|
||||
r#"
|
||||
UPDATE oauth2_authorization_grants AS og
|
||||
SET
|
||||
requires_consent = 'f'
|
||||
WHERE
|
||||
og.id = $1
|
||||
"#,
|
||||
grant.data,
|
||||
)
|
||||
.execute(executor)
|
||||
.await?;
|
||||
|
||||
grant.requires_consent = false;
|
||||
|
||||
Ok(grant)
|
||||
}
|
||||
|
||||
pub async fn exchange_grant(
|
||||
executor: impl PgExecutor<'_>,
|
||||
mut grant: AuthorizationGrant<PostgresqlBackend>,
|
||||
|
@ -205,10 +205,6 @@ impl Templates {
|
||||
bail!("Builtin templates are not included in dev binaries")
|
||||
}
|
||||
|
||||
tokio::fs::create_dir_all(&path)
|
||||
.await
|
||||
.context("could not create destination folder")?;
|
||||
|
||||
let templates = TEMPLATES.into_iter().chain(EXTRA_TEMPLATES);
|
||||
|
||||
let mut options = OpenOptions::new();
|
||||
@ -224,6 +220,12 @@ impl Templates {
|
||||
if let Some(source) = source {
|
||||
let path = path.join(name);
|
||||
|
||||
if let Some(parent) = path.parent() {
|
||||
tokio::fs::create_dir_all(&parent)
|
||||
.await
|
||||
.context("could not create destination")?;
|
||||
}
|
||||
|
||||
let mut file = match options.open(&path).await {
|
||||
Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {
|
||||
// Not overwriting a template is a soft error
|
||||
|
Reference in New Issue
Block a user