1
0
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:
Quentin Gliech
2022-05-06 17:12:16 +02:00
parent fbd774a9fd
commit 436c0dcb19
22 changed files with 1141 additions and 915 deletions

View File

@ -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

View File

@ -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,
}
}
}

View File

@ -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,

View File

@ -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)

View File

@ -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, &params.auth.client_id).await?;
let redirect_uri = client
.resolve_redirect_uri(&params.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(&current_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(&params).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)
}

View File

@ -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,

View 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(&current_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)
}

View 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, &params.auth.client_id).await?;
// And resolve the redirect_uri and response_mode
let redirect_uri = client
.resolve_redirect_uri(&params.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())
}

View File

@ -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())
}

View File

@ -21,5 +21,3 @@ pub mod registration;
pub mod token;
pub mod userinfo;
pub mod webfinger;
pub(crate) use authorization::ContinueAuthorizationGrant;

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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) => {

View File

@ -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())
}

View File

@ -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())
}

View File

@ -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 }
));
}
}

View File

@ -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;

View File

@ -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';

View File

@ -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": [

View File

@ -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>,

View File

@ -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