You've already forked authentication-service
mirror of
https://github.com/matrix-org/matrix-authentication-service.git
synced 2025-07-31 09:24:31 +03:00
data-model: Make the user_id optional in the OAuth 2.0 sessions
This commit is contained in:
@ -66,7 +66,7 @@ pub struct Session {
|
|||||||
pub id: Ulid,
|
pub id: Ulid,
|
||||||
pub state: SessionState,
|
pub state: SessionState,
|
||||||
pub created_at: DateTime<Utc>,
|
pub created_at: DateTime<Utc>,
|
||||||
pub user_id: Ulid,
|
pub user_id: Option<Ulid>,
|
||||||
pub user_session_id: Option<Ulid>,
|
pub user_session_id: Option<Ulid>,
|
||||||
pub client_id: Ulid,
|
pub client_id: Ulid,
|
||||||
pub scope: Scope,
|
pub scope: Scope,
|
||||||
|
@ -63,7 +63,7 @@ pub enum Requester {
|
|||||||
BrowserSession(BrowserSession),
|
BrowserSession(BrowserSession),
|
||||||
|
|
||||||
/// The requester is a OAuth2 session, with an access token.
|
/// The requester is a OAuth2 session, with an access token.
|
||||||
OAuth2Session(Session, User),
|
OAuth2Session(Session, Option<User>),
|
||||||
}
|
}
|
||||||
|
|
||||||
trait OwnerId {
|
trait OwnerId {
|
||||||
@ -90,7 +90,7 @@ impl OwnerId for mas_data_model::UserEmail {
|
|||||||
|
|
||||||
impl OwnerId for Session {
|
impl OwnerId for Session {
|
||||||
fn owner_id(&self) -> Option<Ulid> {
|
fn owner_id(&self) -> Option<Ulid> {
|
||||||
Some(self.user_id)
|
self.user_id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -126,7 +126,7 @@ impl Requester {
|
|||||||
fn user(&self) -> Option<&User> {
|
fn user(&self) -> Option<&User> {
|
||||||
match self {
|
match self {
|
||||||
Self::BrowserSession(session) => Some(&session.user),
|
Self::BrowserSession(session) => Some(&session.user),
|
||||||
Self::OAuth2Session(_session, user) => Some(user),
|
Self::OAuth2Session(_session, user) => user.as_ref(),
|
||||||
Self::Anonymous => None,
|
Self::Anonymous => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -22,7 +22,7 @@ use ulid::Ulid;
|
|||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use super::{BrowserSession, NodeType, User};
|
use super::{BrowserSession, NodeType, User};
|
||||||
use crate::state::ContextExt;
|
use crate::{state::ContextExt, UserId};
|
||||||
|
|
||||||
/// The state of an OAuth 2.0 session.
|
/// The state of an OAuth 2.0 session.
|
||||||
#[derive(Enum, Copy, Clone, Eq, PartialEq)]
|
#[derive(Enum, Copy, Clone, Eq, PartialEq)]
|
||||||
@ -108,17 +108,25 @@ impl OAuth2Session {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// User authorized for this session.
|
/// User authorized for this session.
|
||||||
pub async fn user(&self, ctx: &Context<'_>) -> Result<User, async_graphql::Error> {
|
pub async fn user(&self, ctx: &Context<'_>) -> Result<Option<User>, async_graphql::Error> {
|
||||||
let state = ctx.state();
|
let state = ctx.state();
|
||||||
|
let Some(user_id) = self.0.user_id else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
|
||||||
|
if !ctx.requester().is_owner_or_admin(&UserId(user_id)) {
|
||||||
|
return Err(async_graphql::Error::new("Unauthorized"));
|
||||||
|
}
|
||||||
|
|
||||||
let mut repo = state.repository().await?;
|
let mut repo = state.repository().await?;
|
||||||
let user = repo
|
let user = repo
|
||||||
.user()
|
.user()
|
||||||
.lookup(self.0.user_id)
|
.lookup(user_id)
|
||||||
.await?
|
.await?
|
||||||
.context("Could not load user")?;
|
.context("Could not load user")?;
|
||||||
repo.cancel().await?;
|
repo.cancel().await?;
|
||||||
|
|
||||||
Ok(User(user))
|
Ok(Some(User(user)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -96,9 +96,10 @@ impl OAuth2SessionMutations {
|
|||||||
return Ok(EndOAuth2SessionPayload::NotFound);
|
return Ok(EndOAuth2SessionPayload::NotFound);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(user_id) = session.user_id {
|
||||||
let user = repo
|
let user = repo
|
||||||
.user()
|
.user()
|
||||||
.lookup(session.user_id)
|
.lookup(user_id)
|
||||||
.await?
|
.await?
|
||||||
.context("Could not load user")?;
|
.context("Could not load user")?;
|
||||||
|
|
||||||
@ -116,6 +117,7 @@ impl OAuth2SessionMutations {
|
|||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let session = repo.oauth2_session().finish(&clock, session).await?;
|
let session = repo.oauth2_session().finish(&clock, session).await?;
|
||||||
|
|
||||||
|
@ -31,8 +31,8 @@ impl ViewerQuery {
|
|||||||
|
|
||||||
match requester {
|
match requester {
|
||||||
Requester::BrowserSession(session) => Viewer::user(session.user.clone()),
|
Requester::BrowserSession(session) => Viewer::user(session.user.clone()),
|
||||||
Requester::OAuth2Session(_session, user) => Viewer::user(user.clone()),
|
Requester::OAuth2Session(_session, Some(user)) => Viewer::user(user.clone()),
|
||||||
Requester::Anonymous => Viewer::anonymous(),
|
Requester::OAuth2Session(_, None) | Requester::Anonymous => Viewer::anonymous(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -29,6 +29,7 @@ use futures_util::TryStreamExt;
|
|||||||
use headers::{authorization::Bearer, Authorization, ContentType, HeaderValue};
|
use headers::{authorization::Bearer, Authorization, ContentType, HeaderValue};
|
||||||
use hyper::header::CACHE_CONTROL;
|
use hyper::header::CACHE_CONTROL;
|
||||||
use mas_axum_utils::{cookies::CookieJar, FancyError, SessionInfo, SessionInfoExt};
|
use mas_axum_utils::{cookies::CookieJar, FancyError, SessionInfo, SessionInfoExt};
|
||||||
|
use mas_data_model::User;
|
||||||
use mas_graphql::{Requester, Schema};
|
use mas_graphql::{Requester, Schema};
|
||||||
use mas_matrix::HomeserverConnection;
|
use mas_matrix::HomeserverConnection;
|
||||||
use mas_policy::{InstantiateError, Policy, PolicyFactory};
|
use mas_policy::{InstantiateError, Policy, PolicyFactory};
|
||||||
@ -204,13 +205,22 @@ async fn get_requester(
|
|||||||
.await?
|
.await?
|
||||||
.ok_or(RouteError::LoadFailed)?;
|
.ok_or(RouteError::LoadFailed)?;
|
||||||
|
|
||||||
|
// Load the user if there is one
|
||||||
|
let user = if let Some(user_id) = session.user_id {
|
||||||
let user = repo
|
let user = repo
|
||||||
.user()
|
.user()
|
||||||
.lookup(session.user_id)
|
.lookup(user_id)
|
||||||
.await?
|
.await?
|
||||||
.ok_or(RouteError::LoadFailed)?;
|
.ok_or(RouteError::LoadFailed)?;
|
||||||
|
Some(user)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
if !token.is_valid(clock.now()) || !session.is_valid() || !user.is_valid() {
|
// If there is a user for this session, check that it is not locked
|
||||||
|
let user_valid = user.as_ref().map_or(false, User::is_valid);
|
||||||
|
|
||||||
|
if !token.is_valid(clock.now()) || !session.is_valid() || !user_valid {
|
||||||
return Err(RouteError::InvalidToken);
|
return Err(RouteError::InvalidToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -184,24 +184,32 @@ pub(crate) async fn post(
|
|||||||
// XXX: is that the right error to bubble up?
|
// XXX: is that the right error to bubble up?
|
||||||
.ok_or(RouteError::UnknownToken)?;
|
.ok_or(RouteError::UnknownToken)?;
|
||||||
|
|
||||||
|
// The session might not have a user on it (for Client Credentials grants for
|
||||||
|
// example), so we're optionally fetching the user
|
||||||
|
let (sub, username) = if let Some(user_id) = session.user_id {
|
||||||
let user = repo
|
let user = repo
|
||||||
.user()
|
.user()
|
||||||
.lookup(session.user_id)
|
.lookup(user_id)
|
||||||
.await?
|
.await?
|
||||||
|
// Fail if the user is not valid (e.g. locked)
|
||||||
.filter(User::is_valid)
|
.filter(User::is_valid)
|
||||||
// XXX: is that the right error to bubble up?
|
|
||||||
.ok_or(RouteError::UnknownToken)?;
|
.ok_or(RouteError::UnknownToken)?;
|
||||||
|
|
||||||
|
(Some(user.sub), Some(user.username))
|
||||||
|
} else {
|
||||||
|
(None, None)
|
||||||
|
};
|
||||||
|
|
||||||
IntrospectionResponse {
|
IntrospectionResponse {
|
||||||
active: true,
|
active: true,
|
||||||
scope: Some(session.scope),
|
scope: Some(session.scope),
|
||||||
client_id: Some(session.client_id.to_string()),
|
client_id: Some(session.client_id.to_string()),
|
||||||
username: Some(user.username),
|
username,
|
||||||
token_type: Some(OAuthTokenTypeHint::AccessToken),
|
token_type: Some(OAuthTokenTypeHint::AccessToken),
|
||||||
exp: Some(token.expires_at),
|
exp: Some(token.expires_at),
|
||||||
iat: Some(token.created_at),
|
iat: Some(token.created_at),
|
||||||
nbf: Some(token.created_at),
|
nbf: Some(token.created_at),
|
||||||
sub: Some(user.sub),
|
sub,
|
||||||
aud: None,
|
aud: None,
|
||||||
iss: None,
|
iss: None,
|
||||||
jti: Some(token.jti()),
|
jti: Some(token.jti()),
|
||||||
@ -224,24 +232,32 @@ pub(crate) async fn post(
|
|||||||
// XXX: is that the right error to bubble up?
|
// XXX: is that the right error to bubble up?
|
||||||
.ok_or(RouteError::UnknownToken)?;
|
.ok_or(RouteError::UnknownToken)?;
|
||||||
|
|
||||||
|
// The session might not have a user on it (for Client Credentials grants for
|
||||||
|
// example), so we're optionally fetching the user
|
||||||
|
let (sub, username) = if let Some(user_id) = session.user_id {
|
||||||
let user = repo
|
let user = repo
|
||||||
.user()
|
.user()
|
||||||
.lookup(session.user_id)
|
.lookup(user_id)
|
||||||
.await?
|
.await?
|
||||||
|
// Fail if the user is not valid (e.g. locked)
|
||||||
.filter(User::is_valid)
|
.filter(User::is_valid)
|
||||||
// XXX: is that the right error to bubble up?
|
|
||||||
.ok_or(RouteError::UnknownToken)?;
|
.ok_or(RouteError::UnknownToken)?;
|
||||||
|
|
||||||
|
(Some(user.sub), Some(user.username))
|
||||||
|
} else {
|
||||||
|
(None, None)
|
||||||
|
};
|
||||||
|
|
||||||
IntrospectionResponse {
|
IntrospectionResponse {
|
||||||
active: true,
|
active: true,
|
||||||
scope: Some(session.scope),
|
scope: Some(session.scope),
|
||||||
client_id: Some(session.client_id.to_string()),
|
client_id: Some(session.client_id.to_string()),
|
||||||
username: Some(user.username),
|
username,
|
||||||
token_type: Some(OAuthTokenTypeHint::RefreshToken),
|
token_type: Some(OAuthTokenTypeHint::RefreshToken),
|
||||||
exp: None,
|
exp: None,
|
||||||
iat: Some(token.created_at),
|
iat: Some(token.created_at),
|
||||||
nbf: Some(token.created_at),
|
nbf: Some(token.created_at),
|
||||||
sub: Some(user.sub),
|
sub,
|
||||||
aud: None,
|
aud: None,
|
||||||
iss: None,
|
iss: None,
|
||||||
jti: Some(token.jti()),
|
jti: Some(token.jti()),
|
||||||
|
@ -199,10 +199,13 @@ pub(crate) async fn post(
|
|||||||
return Err(RouteError::UnauthorizedClient);
|
return Err(RouteError::UnauthorizedClient);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If the session is associated with a user, make sure we schedule a device
|
||||||
|
// deletion job for all the devices associated with the session.
|
||||||
|
if let Some(user_id) = session.user_id {
|
||||||
// Fetch the user
|
// Fetch the user
|
||||||
let user = repo
|
let user = repo
|
||||||
.user()
|
.user()
|
||||||
.lookup(session.user_id)
|
.lookup(user_id)
|
||||||
.await?
|
.await?
|
||||||
.ok_or(RouteError::UnknownToken)?;
|
.ok_or(RouteError::UnknownToken)?;
|
||||||
|
|
||||||
@ -220,6 +223,7 @@ pub(crate) async fn post(
|
|||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Now that we checked everything, we can end the session.
|
// Now that we checked everything, we can end the session.
|
||||||
repo.oauth2_session().finish(&clock, session).await?;
|
repo.oauth2_session().finish(&clock, session).await?;
|
||||||
|
@ -65,6 +65,9 @@ pub enum RouteError {
|
|||||||
#[from] AuthorizationVerificationError<mas_storage::RepositoryError>,
|
#[from] AuthorizationVerificationError<mas_storage::RepositoryError>,
|
||||||
),
|
),
|
||||||
|
|
||||||
|
#[error("session is not allowed to access the userinfo endpoint")]
|
||||||
|
Unauthorized,
|
||||||
|
|
||||||
#[error("no suitable key found for signing")]
|
#[error("no suitable key found for signing")]
|
||||||
InvalidSigningKey,
|
InvalidSigningKey,
|
||||||
|
|
||||||
@ -86,7 +89,9 @@ impl IntoResponse for RouteError {
|
|||||||
Self::Internal(_) | Self::InvalidSigningKey | Self::NoSuchClient | Self::NoSuchUser => {
|
Self::Internal(_) | Self::InvalidSigningKey | Self::NoSuchClient | Self::NoSuchUser => {
|
||||||
(StatusCode::INTERNAL_SERVER_ERROR, self.to_string()).into_response()
|
(StatusCode::INTERNAL_SERVER_ERROR, self.to_string()).into_response()
|
||||||
}
|
}
|
||||||
Self::AuthorizationVerificationError(_e) => StatusCode::UNAUTHORIZED.into_response(),
|
Self::AuthorizationVerificationError(_) | Self::Unauthorized => {
|
||||||
|
StatusCode::UNAUTHORIZED.into_response()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -102,9 +107,19 @@ pub async fn get(
|
|||||||
) -> Result<Response, RouteError> {
|
) -> Result<Response, RouteError> {
|
||||||
let session = user_authorization.protected(&mut repo, &clock).await?;
|
let session = user_authorization.protected(&mut repo, &clock).await?;
|
||||||
|
|
||||||
|
// This endpoint requires the `openid` scope.
|
||||||
|
if !session.scope.contains("openid") {
|
||||||
|
return Err(RouteError::Unauthorized);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fail if the session is not associated with a user.
|
||||||
|
let Some(user_id) = session.user_id else {
|
||||||
|
return Err(RouteError::Unauthorized);
|
||||||
|
};
|
||||||
|
|
||||||
let user = repo
|
let user = repo
|
||||||
.user()
|
.user()
|
||||||
.lookup(session.user_id)
|
.lookup(user_id)
|
||||||
.await?
|
.await?
|
||||||
.ok_or(RouteError::NoSuchUser)?;
|
.ok_or(RouteError::NoSuchUser)?;
|
||||||
|
|
||||||
|
@ -135,7 +135,6 @@ impl<'c> OAuth2AccessTokenRepository for PgOAuth2AccessTokenRepository<'c> {
|
|||||||
fields(
|
fields(
|
||||||
db.statement,
|
db.statement,
|
||||||
%session.id,
|
%session.id,
|
||||||
user.id = %session.user_id,
|
|
||||||
client.id = %session.client_id,
|
client.id = %session.client_id,
|
||||||
access_token.id,
|
access_token.id,
|
||||||
),
|
),
|
||||||
|
@ -406,7 +406,6 @@ impl<'c> OAuth2AuthorizationGrantRepository for PgOAuth2AuthorizationGrantReposi
|
|||||||
%grant.id,
|
%grant.id,
|
||||||
client.id = %grant.client_id,
|
client.id = %grant.client_id,
|
||||||
%session.id,
|
%session.id,
|
||||||
user.id = %session.user_id,
|
|
||||||
),
|
),
|
||||||
err,
|
err,
|
||||||
)]
|
)]
|
||||||
|
@ -143,7 +143,6 @@ impl<'c> OAuth2RefreshTokenRepository for PgOAuth2RefreshTokenRepository<'c> {
|
|||||||
fields(
|
fields(
|
||||||
db.statement,
|
db.statement,
|
||||||
%session.id,
|
%session.id,
|
||||||
user.id = %session.user_id,
|
|
||||||
client.id = %session.client_id,
|
client.id = %session.client_id,
|
||||||
refresh_token.id,
|
refresh_token.id,
|
||||||
),
|
),
|
||||||
|
@ -86,7 +86,7 @@ impl TryFrom<OAuthSessionLookup> for Session {
|
|||||||
state,
|
state,
|
||||||
created_at: value.created_at,
|
created_at: value.created_at,
|
||||||
client_id: value.oauth2_client_id.into(),
|
client_id: value.oauth2_client_id.into(),
|
||||||
user_id: value.user_id.into(),
|
user_id: Some(value.user_id.into()),
|
||||||
user_session_id: value.user_session_id.map(Ulid::from),
|
user_session_id: value.user_session_id.map(Ulid::from),
|
||||||
scope,
|
scope,
|
||||||
})
|
})
|
||||||
@ -186,7 +186,7 @@ impl<'c> OAuth2SessionRepository for PgOAuth2SessionRepository<'c> {
|
|||||||
id,
|
id,
|
||||||
state: SessionState::Valid,
|
state: SessionState::Valid,
|
||||||
created_at,
|
created_at,
|
||||||
user_id: user_session.user.id,
|
user_id: Some(user_session.user.id),
|
||||||
user_session_id: Some(user_session.id),
|
user_session_id: Some(user_session.id),
|
||||||
client_id: client.id,
|
client_id: client.id,
|
||||||
scope,
|
scope,
|
||||||
@ -200,7 +200,6 @@ impl<'c> OAuth2SessionRepository for PgOAuth2SessionRepository<'c> {
|
|||||||
db.statement,
|
db.statement,
|
||||||
%session.id,
|
%session.id,
|
||||||
%session.scope,
|
%session.scope,
|
||||||
user.id = %session.user_id,
|
|
||||||
client.id = %session.client_id,
|
client.id = %session.client_id,
|
||||||
),
|
),
|
||||||
err,
|
err,
|
||||||
|
@ -695,7 +695,7 @@ type Oauth2Session implements Node & CreationEvent {
|
|||||||
"""
|
"""
|
||||||
User authorized for this session.
|
User authorized for this session.
|
||||||
"""
|
"""
|
||||||
user: User!
|
user: User
|
||||||
}
|
}
|
||||||
|
|
||||||
type Oauth2SessionConnection {
|
type Oauth2SessionConnection {
|
||||||
|
@ -517,7 +517,7 @@ export type Oauth2Session = CreationEvent &
|
|||||||
/** The state of the session. */
|
/** The state of the session. */
|
||||||
state: Oauth2SessionState;
|
state: Oauth2SessionState;
|
||||||
/** User authorized for this session. */
|
/** User authorized for this session. */
|
||||||
user: User;
|
user?: Maybe<User>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Oauth2SessionConnection = {
|
export type Oauth2SessionConnection = {
|
||||||
|
@ -1390,13 +1390,10 @@ export default {
|
|||||||
{
|
{
|
||||||
name: "user",
|
name: "user",
|
||||||
type: {
|
type: {
|
||||||
kind: "NON_NULL",
|
|
||||||
ofType: {
|
|
||||||
kind: "OBJECT",
|
kind: "OBJECT",
|
||||||
name: "User",
|
name: "User",
|
||||||
ofType: null,
|
ofType: null,
|
||||||
},
|
},
|
||||||
},
|
|
||||||
args: [],
|
args: [],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
Reference in New Issue
Block a user