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
GraphQL API
This commit is contained in:
@ -33,6 +33,8 @@ pub struct UpstreamOAuthProvider {
|
|||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
||||||
pub struct UpstreamOAuthLink {
|
pub struct UpstreamOAuthLink {
|
||||||
pub id: Ulid,
|
pub id: Ulid,
|
||||||
|
pub provider_id: Ulid,
|
||||||
|
pub user_id: Option<Ulid>,
|
||||||
pub subject: String,
|
pub subject: String,
|
||||||
pub created_at: DateTime<Utc>,
|
pub created_at: DateTime<Utc>,
|
||||||
}
|
}
|
||||||
@ -40,6 +42,8 @@ pub struct UpstreamOAuthLink {
|
|||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
||||||
pub struct UpstreamOAuthAuthorizationSession {
|
pub struct UpstreamOAuthAuthorizationSession {
|
||||||
pub id: Ulid,
|
pub id: Ulid,
|
||||||
|
pub provider_id: Ulid,
|
||||||
|
pub link_id: Option<Ulid>,
|
||||||
pub state: String,
|
pub state: String,
|
||||||
pub code_challenge_verifier: Option<String>,
|
pub code_challenge_verifier: Option<String>,
|
||||||
pub nonce: String,
|
pub nonce: String,
|
||||||
|
@ -22,12 +22,18 @@
|
|||||||
#![warn(clippy::pedantic)]
|
#![warn(clippy::pedantic)]
|
||||||
#![allow(clippy::module_name_repetitions, clippy::missing_errors_doc)]
|
#![allow(clippy::module_name_repetitions, clippy::missing_errors_doc)]
|
||||||
|
|
||||||
use async_graphql::{Context, Description, EmptyMutation, EmptySubscription, ID};
|
use async_graphql::{
|
||||||
|
connection::{query, Connection, Edge, OpaqueCursor},
|
||||||
|
Context, Description, EmptyMutation, EmptySubscription, ID,
|
||||||
|
};
|
||||||
use mas_axum_utils::SessionInfo;
|
use mas_axum_utils::SessionInfo;
|
||||||
use mas_storage::LookupResultExt;
|
use mas_storage::LookupResultExt;
|
||||||
use sqlx::PgPool;
|
use sqlx::PgPool;
|
||||||
|
|
||||||
use self::model::{BrowserSession, Node, NodeType, OAuth2Client, User, UserEmail};
|
use self::model::{
|
||||||
|
BrowserSession, Cursor, Node, NodeCursor, NodeType, OAuth2Client, UpstreamOAuth2Link,
|
||||||
|
UpstreamOAuth2Provider, User, UserEmail,
|
||||||
|
};
|
||||||
|
|
||||||
mod model;
|
mod model;
|
||||||
|
|
||||||
@ -167,6 +173,100 @@ impl RootQuery {
|
|||||||
Ok(user_email.map(UserEmail))
|
Ok(user_email.map(UserEmail))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Fetch an upstream OAuth 2.0 link by its ID.
|
||||||
|
async fn upstream_oauth2_link(
|
||||||
|
&self,
|
||||||
|
ctx: &Context<'_>,
|
||||||
|
id: ID,
|
||||||
|
) -> Result<Option<UpstreamOAuth2Link>, async_graphql::Error> {
|
||||||
|
let id = NodeType::UpstreamOAuth2Link.extract_ulid(&id)?;
|
||||||
|
let database = ctx.data::<PgPool>()?;
|
||||||
|
let session_info = ctx.data::<SessionInfo>()?;
|
||||||
|
let mut conn = database.acquire().await?;
|
||||||
|
let session = session_info.load_session(&mut conn).await?;
|
||||||
|
|
||||||
|
let Some(session) = session else { return Ok(None) };
|
||||||
|
let current_user = session.user;
|
||||||
|
|
||||||
|
let link = mas_storage::upstream_oauth2::lookup_link(&mut conn, id)
|
||||||
|
.await
|
||||||
|
.to_option()?;
|
||||||
|
|
||||||
|
// Ensure that the link belongs to the current user
|
||||||
|
let link = link.filter(|link| link.user_id == Some(current_user.data));
|
||||||
|
|
||||||
|
Ok(link.map(UpstreamOAuth2Link::new))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fetch an upstream OAuth 2.0 provider by its ID.
|
||||||
|
async fn upstream_oauth2_provider(
|
||||||
|
&self,
|
||||||
|
ctx: &Context<'_>,
|
||||||
|
id: ID,
|
||||||
|
) -> Result<Option<UpstreamOAuth2Provider>, async_graphql::Error> {
|
||||||
|
let id = NodeType::UpstreamOAuth2Provider.extract_ulid(&id)?;
|
||||||
|
let database = ctx.data::<PgPool>()?;
|
||||||
|
let mut conn = database.acquire().await?;
|
||||||
|
|
||||||
|
let provider = mas_storage::upstream_oauth2::lookup_provider(&mut conn, id)
|
||||||
|
.await
|
||||||
|
.to_option()?;
|
||||||
|
|
||||||
|
Ok(provider.map(UpstreamOAuth2Provider::new))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a list of upstream OAuth 2.0 providers.
|
||||||
|
async fn upstream_oauth2_providers(
|
||||||
|
&self,
|
||||||
|
ctx: &Context<'_>,
|
||||||
|
|
||||||
|
#[graphql(desc = "Returns the elements in the list that come after the cursor.")]
|
||||||
|
after: Option<String>,
|
||||||
|
#[graphql(desc = "Returns the elements in the list that come before the cursor.")]
|
||||||
|
before: Option<String>,
|
||||||
|
#[graphql(desc = "Returns the first *n* elements from the list.")] first: Option<i32>,
|
||||||
|
#[graphql(desc = "Returns the last *n* elements from the list.")] last: Option<i32>,
|
||||||
|
) -> Result<Connection<Cursor, UpstreamOAuth2Provider>, async_graphql::Error> {
|
||||||
|
let database = ctx.data::<PgPool>()?;
|
||||||
|
|
||||||
|
query(
|
||||||
|
after,
|
||||||
|
before,
|
||||||
|
first,
|
||||||
|
last,
|
||||||
|
|after, before, first, last| async move {
|
||||||
|
let mut conn = database.acquire().await?;
|
||||||
|
let after_id = after
|
||||||
|
.map(|x: OpaqueCursor<NodeCursor>| {
|
||||||
|
x.extract_for_type(NodeType::UpstreamOAuth2Provider)
|
||||||
|
})
|
||||||
|
.transpose()?;
|
||||||
|
let before_id = before
|
||||||
|
.map(|x: OpaqueCursor<NodeCursor>| {
|
||||||
|
x.extract_for_type(NodeType::UpstreamOAuth2Provider)
|
||||||
|
})
|
||||||
|
.transpose()?;
|
||||||
|
|
||||||
|
let (has_previous_page, has_next_page, edges) =
|
||||||
|
mas_storage::upstream_oauth2::get_paginated_providers(
|
||||||
|
&mut conn, before_id, after_id, first, last,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let mut connection = Connection::new(has_previous_page, has_next_page);
|
||||||
|
connection.edges.extend(edges.into_iter().map(|p| {
|
||||||
|
Edge::new(
|
||||||
|
OpaqueCursor(NodeCursor(NodeType::UpstreamOAuth2Provider, p.id)),
|
||||||
|
UpstreamOAuth2Provider::new(p),
|
||||||
|
)
|
||||||
|
}));
|
||||||
|
|
||||||
|
Ok::<_, async_graphql::Error>(connection)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
/// Fetches an object given its ID.
|
/// Fetches an object given its ID.
|
||||||
async fn node(&self, ctx: &Context<'_>, id: ID) -> Result<Option<Node>, async_graphql::Error> {
|
async fn node(&self, ctx: &Context<'_>, id: ID) -> Result<Option<Node>, async_graphql::Error> {
|
||||||
let (node_type, _id) = NodeType::from_id(&id)?;
|
let (node_type, _id) = NodeType::from_id(&id)?;
|
||||||
@ -178,6 +278,16 @@ impl RootQuery {
|
|||||||
| NodeType::CompatSsoLogin
|
| NodeType::CompatSsoLogin
|
||||||
| NodeType::OAuth2Session => None,
|
| NodeType::OAuth2Session => None,
|
||||||
|
|
||||||
|
NodeType::UpstreamOAuth2Provider => self
|
||||||
|
.upstream_oauth2_provider(ctx, id)
|
||||||
|
.await?
|
||||||
|
.map(|c| Node::UpstreamOAuth2Provider(Box::new(c))),
|
||||||
|
|
||||||
|
NodeType::UpstreamOAuth2Link => self
|
||||||
|
.upstream_oauth2_link(ctx, id)
|
||||||
|
.await?
|
||||||
|
.map(|c| Node::UpstreamOAuth2Link(Box::new(c))),
|
||||||
|
|
||||||
NodeType::OAuth2Client => self
|
NodeType::OAuth2Client => self
|
||||||
.oauth2_client(ctx, id)
|
.oauth2_client(ctx, id)
|
||||||
.await?
|
.await?
|
||||||
|
@ -20,6 +20,7 @@ mod compat_sessions;
|
|||||||
mod cursor;
|
mod cursor;
|
||||||
mod node;
|
mod node;
|
||||||
mod oauth;
|
mod oauth;
|
||||||
|
mod upstream_oauth;
|
||||||
mod users;
|
mod users;
|
||||||
|
|
||||||
pub use self::{
|
pub use self::{
|
||||||
@ -28,6 +29,7 @@ pub use self::{
|
|||||||
cursor::{Cursor, NodeCursor},
|
cursor::{Cursor, NodeCursor},
|
||||||
node::{Node, NodeType},
|
node::{Node, NodeType},
|
||||||
oauth::{OAuth2Client, OAuth2Consent, OAuth2Session},
|
oauth::{OAuth2Client, OAuth2Consent, OAuth2Session},
|
||||||
|
upstream_oauth::{UpstreamOAuth2Link, UpstreamOAuth2Provider},
|
||||||
users::{User, UserEmail},
|
users::{User, UserEmail},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -42,4 +44,6 @@ pub enum CreationEvent {
|
|||||||
CompatSession(Box<CompatSession>),
|
CompatSession(Box<CompatSession>),
|
||||||
BrowserSession(Box<BrowserSession>),
|
BrowserSession(Box<BrowserSession>),
|
||||||
UserEmail(Box<UserEmail>),
|
UserEmail(Box<UserEmail>),
|
||||||
|
UpstreamOAuth2Provider(Box<UpstreamOAuth2Provider>),
|
||||||
|
UpstreamOAuth2Link(Box<UpstreamOAuth2Link>),
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,7 @@ use ulid::Ulid;
|
|||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
Authentication, BrowserSession, CompatSession, CompatSsoLogin, OAuth2Client, OAuth2Session,
|
Authentication, BrowserSession, CompatSession, CompatSsoLogin, OAuth2Client, OAuth2Session,
|
||||||
User, UserEmail,
|
UpstreamOAuth2Link, UpstreamOAuth2Provider, User, UserEmail,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
@ -30,6 +30,8 @@ pub enum NodeType {
|
|||||||
CompatSsoLogin,
|
CompatSsoLogin,
|
||||||
OAuth2Client,
|
OAuth2Client,
|
||||||
OAuth2Session,
|
OAuth2Session,
|
||||||
|
UpstreamOAuth2Provider,
|
||||||
|
UpstreamOAuth2Link,
|
||||||
User,
|
User,
|
||||||
UserEmail,
|
UserEmail,
|
||||||
}
|
}
|
||||||
@ -52,6 +54,8 @@ impl NodeType {
|
|||||||
NodeType::CompatSsoLogin => "compat_sso_login",
|
NodeType::CompatSsoLogin => "compat_sso_login",
|
||||||
NodeType::OAuth2Client => "oauth2_client",
|
NodeType::OAuth2Client => "oauth2_client",
|
||||||
NodeType::OAuth2Session => "oauth2_session",
|
NodeType::OAuth2Session => "oauth2_session",
|
||||||
|
NodeType::UpstreamOAuth2Provider => "upstream_oauth2_provider",
|
||||||
|
NodeType::UpstreamOAuth2Link => "upstream_oauth2_link",
|
||||||
NodeType::User => "user",
|
NodeType::User => "user",
|
||||||
NodeType::UserEmail => "user_email",
|
NodeType::UserEmail => "user_email",
|
||||||
}
|
}
|
||||||
@ -65,6 +69,8 @@ impl NodeType {
|
|||||||
"compat_sso_login" => Some(NodeType::CompatSsoLogin),
|
"compat_sso_login" => Some(NodeType::CompatSsoLogin),
|
||||||
"oauth2_client" => Some(NodeType::OAuth2Client),
|
"oauth2_client" => Some(NodeType::OAuth2Client),
|
||||||
"oauth2_session" => Some(NodeType::OAuth2Session),
|
"oauth2_session" => Some(NodeType::OAuth2Session),
|
||||||
|
"upstream_oauth2_provider" => Some(NodeType::UpstreamOAuth2Provider),
|
||||||
|
"upstream_oauth2_link" => Some(NodeType::UpstreamOAuth2Link),
|
||||||
"user" => Some(NodeType::User),
|
"user" => Some(NodeType::User),
|
||||||
"user_email" => Some(NodeType::UserEmail),
|
"user_email" => Some(NodeType::UserEmail),
|
||||||
_ => None,
|
_ => None,
|
||||||
@ -116,6 +122,8 @@ pub enum Node {
|
|||||||
CompatSsoLogin(Box<CompatSsoLogin>),
|
CompatSsoLogin(Box<CompatSsoLogin>),
|
||||||
OAuth2Client(Box<OAuth2Client>),
|
OAuth2Client(Box<OAuth2Client>),
|
||||||
OAuth2Session(Box<OAuth2Session>),
|
OAuth2Session(Box<OAuth2Session>),
|
||||||
|
UpstreamOAuth2Provider(Box<UpstreamOAuth2Provider>),
|
||||||
|
UpstreamOAuth2Link(Box<UpstreamOAuth2Link>),
|
||||||
User(Box<User>),
|
User(Box<User>),
|
||||||
UserEmail(Box<UserEmail>),
|
UserEmail(Box<UserEmail>),
|
||||||
}
|
}
|
||||||
|
121
crates/graphql/src/model/upstream_oauth.rs
Normal file
121
crates/graphql/src/model/upstream_oauth.rs
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
// 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 async_graphql::{Context, Object, ID};
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use mas_storage::PostgresqlBackend;
|
||||||
|
use sqlx::PgPool;
|
||||||
|
|
||||||
|
use super::{NodeType, User};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct UpstreamOAuth2Provider {
|
||||||
|
provider: mas_data_model::UpstreamOAuthProvider,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UpstreamOAuth2Provider {
|
||||||
|
#[must_use]
|
||||||
|
pub const fn new(provider: mas_data_model::UpstreamOAuthProvider) -> Self {
|
||||||
|
Self { provider }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Object]
|
||||||
|
impl UpstreamOAuth2Provider {
|
||||||
|
/// ID of the object.
|
||||||
|
pub async fn id(&self) -> ID {
|
||||||
|
NodeType::UpstreamOAuth2Provider.id(self.provider.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// When the object was created.
|
||||||
|
pub async fn created_at(&self) -> DateTime<Utc> {
|
||||||
|
self.provider.created_at
|
||||||
|
}
|
||||||
|
|
||||||
|
/// OpenID Connect issuer URL.
|
||||||
|
pub async fn issuer(&self) -> &str {
|
||||||
|
&self.provider.issuer
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Client ID used for this provider.
|
||||||
|
pub async fn client_id(&self) -> &str {
|
||||||
|
&self.provider.client_id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UpstreamOAuth2Link {
|
||||||
|
#[must_use]
|
||||||
|
pub const fn new(link: mas_data_model::UpstreamOAuthLink) -> Self {
|
||||||
|
Self {
|
||||||
|
link,
|
||||||
|
provider: None,
|
||||||
|
user: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct UpstreamOAuth2Link {
|
||||||
|
link: mas_data_model::UpstreamOAuthLink,
|
||||||
|
provider: Option<mas_data_model::UpstreamOAuthProvider>,
|
||||||
|
user: Option<mas_data_model::User<PostgresqlBackend>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Object]
|
||||||
|
impl UpstreamOAuth2Link {
|
||||||
|
/// ID of the object.
|
||||||
|
pub async fn id(&self) -> ID {
|
||||||
|
NodeType::UpstreamOAuth2Link.id(self.link.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// When the object was created.
|
||||||
|
pub async fn created_at(&self) -> DateTime<Utc> {
|
||||||
|
self.link.created_at
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The provider for which this link is.
|
||||||
|
pub async fn provider(
|
||||||
|
&self,
|
||||||
|
ctx: &Context<'_>,
|
||||||
|
) -> Result<UpstreamOAuth2Provider, async_graphql::Error> {
|
||||||
|
let provider = if let Some(provider) = &self.provider {
|
||||||
|
// Cached
|
||||||
|
provider.clone()
|
||||||
|
} else {
|
||||||
|
// Fetch on-the-fly
|
||||||
|
let database = ctx.data::<PgPool>()?;
|
||||||
|
let mut conn = database.acquire().await?;
|
||||||
|
mas_storage::upstream_oauth2::lookup_provider(&mut conn, self.link.provider_id).await?
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(UpstreamOAuth2Provider::new(provider))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The user to which this link is associated.
|
||||||
|
pub async fn user(&self, ctx: &Context<'_>) -> Result<Option<User>, async_graphql::Error> {
|
||||||
|
let user = if let Some(user) = &self.user {
|
||||||
|
// Cached
|
||||||
|
user.clone()
|
||||||
|
} else if let Some(user_id) = &self.link.user_id {
|
||||||
|
// Fetch on-the-fly
|
||||||
|
let database = ctx.data::<PgPool>()?;
|
||||||
|
let mut conn = database.acquire().await?;
|
||||||
|
mas_storage::user::lookup_user(&mut conn, *user_id).await?
|
||||||
|
} else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Some(User(user)))
|
||||||
|
}
|
||||||
|
}
|
@ -250,7 +250,7 @@ pub(crate) async fn get(
|
|||||||
.await
|
.await
|
||||||
.to_option()?;
|
.to_option()?;
|
||||||
|
|
||||||
let link = if let Some((link, _maybe_user_id)) = maybe_link {
|
let link = if let Some(link) = maybe_link {
|
||||||
link
|
link
|
||||||
} else {
|
} else {
|
||||||
add_link(&mut txn, &mut rng, &clock, &provider, subject).await?
|
add_link(&mut txn, &mut rng, &clock, &provider, subject).await?
|
||||||
|
@ -114,7 +114,7 @@ pub(crate) async fn get(
|
|||||||
let mut txn = pool.begin().await?;
|
let mut txn = pool.begin().await?;
|
||||||
let (clock, mut rng) = crate::rng_and_clock()?;
|
let (clock, mut rng) = crate::rng_and_clock()?;
|
||||||
|
|
||||||
let (link, _provider_id, maybe_user_id) = lookup_link(&mut txn, link_id)
|
let link = lookup_link(&mut txn, link_id)
|
||||||
.await
|
.await
|
||||||
.to_option()?
|
.to_option()?
|
||||||
.ok_or(RouteError::LinkNotFound)?;
|
.ok_or(RouteError::LinkNotFound)?;
|
||||||
@ -141,7 +141,7 @@ pub(crate) async fn get(
|
|||||||
let (csrf_token, mut cookie_jar) = cookie_jar.csrf_token(clock.now(), &mut rng);
|
let (csrf_token, mut cookie_jar) = cookie_jar.csrf_token(clock.now(), &mut rng);
|
||||||
let maybe_user_session = user_session_info.load_session(&mut txn).await?;
|
let maybe_user_session = user_session_info.load_session(&mut txn).await?;
|
||||||
|
|
||||||
let render = match (maybe_user_session, maybe_user_id) {
|
let render = match (maybe_user_session, link.user_id) {
|
||||||
(Some(mut session), Some(user_id)) if session.user.data == user_id => {
|
(Some(mut session), Some(user_id)) if session.user.data == user_id => {
|
||||||
// Session already linked, and link matches the currently logged
|
// Session already linked, and link matches the currently logged
|
||||||
// user. Mark the session as consumed and renew the authentication.
|
// user. Mark the session as consumed and renew the authentication.
|
||||||
@ -215,7 +215,7 @@ pub(crate) async fn post(
|
|||||||
let (clock, mut rng) = crate::rng_and_clock()?;
|
let (clock, mut rng) = crate::rng_and_clock()?;
|
||||||
let form = cookie_jar.verify_form(clock.now(), form)?;
|
let form = cookie_jar.verify_form(clock.now(), form)?;
|
||||||
|
|
||||||
let (link, _provider_id, maybe_user_id) = lookup_link(&mut txn, link_id)
|
let link = lookup_link(&mut txn, link_id)
|
||||||
.await
|
.await
|
||||||
.to_option()?
|
.to_option()?
|
||||||
.ok_or(RouteError::LinkNotFound)?;
|
.ok_or(RouteError::LinkNotFound)?;
|
||||||
@ -241,7 +241,7 @@ pub(crate) async fn post(
|
|||||||
let (user_session_info, cookie_jar) = cookie_jar.session_info();
|
let (user_session_info, cookie_jar) = cookie_jar.session_info();
|
||||||
let maybe_user_session = user_session_info.load_session(&mut txn).await?;
|
let maybe_user_session = user_session_info.load_session(&mut txn).await?;
|
||||||
|
|
||||||
let mut session = match (maybe_user_session, maybe_user_id, form) {
|
let mut session = match (maybe_user_session, link.user_id, form) {
|
||||||
(Some(session), None, FormData::Link) => {
|
(Some(session), None, FormData::Link) => {
|
||||||
associate_link_to_user(&mut txn, &link, &session.user).await?;
|
associate_link_to_user(&mut txn, &link, &session.user).await?;
|
||||||
session
|
session
|
||||||
|
@ -57,11 +57,11 @@ impl OptionalPostAuthAction {
|
|||||||
Some(PostAuthAction::ChangePassword) => Ok(Some(PostAuthContext::ChangePassword)),
|
Some(PostAuthAction::ChangePassword) => Ok(Some(PostAuthContext::ChangePassword)),
|
||||||
|
|
||||||
Some(PostAuthAction::LinkUpstream { id }) => {
|
Some(PostAuthAction::LinkUpstream { id }) => {
|
||||||
let (link, provider_id, _user_id) =
|
let link = mas_storage::upstream_oauth2::lookup_link(&mut *conn, *id).await?;
|
||||||
mas_storage::upstream_oauth2::lookup_link(&mut *conn, *id).await?;
|
|
||||||
|
|
||||||
let provider =
|
let provider =
|
||||||
mas_storage::upstream_oauth2::lookup_provider(&mut *conn, provider_id).await?;
|
mas_storage::upstream_oauth2::lookup_provider(&mut *conn, link.provider_id)
|
||||||
|
.await?;
|
||||||
|
|
||||||
let provider = Box::new(provider);
|
let provider = Box::new(provider);
|
||||||
let link = Box::new(link);
|
let link = Box::new(link);
|
||||||
|
@ -634,6 +634,81 @@
|
|||||||
},
|
},
|
||||||
"query": "\n INSERT INTO users (user_id, username, created_at)\n VALUES ($1, $2, $3)\n "
|
"query": "\n INSERT INTO users (user_id, username, created_at)\n VALUES ($1, $2, $3)\n "
|
||||||
},
|
},
|
||||||
|
"2ca7b990c11e84db62fb7887a2bc3410ec1eee2f6a0ec124db36575111970ca9": {
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "upstream_oauth_authorization_session_id",
|
||||||
|
"ordinal": 0,
|
||||||
|
"type_info": "Uuid"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "upstream_oauth_provider_id",
|
||||||
|
"ordinal": 1,
|
||||||
|
"type_info": "Uuid"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "upstream_oauth_link_id",
|
||||||
|
"ordinal": 2,
|
||||||
|
"type_info": "Uuid"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "state",
|
||||||
|
"ordinal": 3,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "code_challenge_verifier",
|
||||||
|
"ordinal": 4,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "nonce",
|
||||||
|
"ordinal": 5,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "id_token",
|
||||||
|
"ordinal": 6,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "created_at",
|
||||||
|
"ordinal": 7,
|
||||||
|
"type_info": "Timestamptz"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "completed_at",
|
||||||
|
"ordinal": 8,
|
||||||
|
"type_info": "Timestamptz"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "consumed_at",
|
||||||
|
"ordinal": 9,
|
||||||
|
"type_info": "Timestamptz"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"nullable": [
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Left": [
|
||||||
|
"Uuid",
|
||||||
|
"Uuid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"query": "\n SELECT\n upstream_oauth_authorization_session_id,\n upstream_oauth_provider_id,\n upstream_oauth_link_id,\n state,\n code_challenge_verifier,\n nonce,\n id_token,\n created_at,\n completed_at,\n consumed_at\n FROM upstream_oauth_authorization_sessions\n WHERE upstream_oauth_authorization_session_id = $1\n AND upstream_oauth_link_id = $2\n "
|
||||||
|
},
|
||||||
"2e756fe7be50128c0acc5f79df3a084230e9ca13cd45bd0858f97e59da20006e": {
|
"2e756fe7be50128c0acc5f79df3a084230e9ca13cd45bd0858f97e59da20006e": {
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [],
|
"columns": [],
|
||||||
@ -1284,116 +1359,6 @@
|
|||||||
},
|
},
|
||||||
"query": "\n SELECT\n ue.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 user_emails ue\n\n WHERE ue.user_id = $1\n\n ORDER BY ue.email ASC\n "
|
"query": "\n SELECT\n ue.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 user_emails ue\n\n WHERE ue.user_id = $1\n\n ORDER BY ue.email ASC\n "
|
||||||
},
|
},
|
||||||
"605e9370d233169760dafd0ac5dea4d161b4ad1903c79ad35499732533a1b641": {
|
|
||||||
"describe": {
|
|
||||||
"columns": [
|
|
||||||
{
|
|
||||||
"name": "upstream_oauth_authorization_session_id",
|
|
||||||
"ordinal": 0,
|
|
||||||
"type_info": "Uuid"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "upstream_oauth_provider_id",
|
|
||||||
"ordinal": 1,
|
|
||||||
"type_info": "Uuid"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "state",
|
|
||||||
"ordinal": 2,
|
|
||||||
"type_info": "Text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "code_challenge_verifier",
|
|
||||||
"ordinal": 3,
|
|
||||||
"type_info": "Text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "nonce",
|
|
||||||
"ordinal": 4,
|
|
||||||
"type_info": "Text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "id_token",
|
|
||||||
"ordinal": 5,
|
|
||||||
"type_info": "Text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "created_at",
|
|
||||||
"ordinal": 6,
|
|
||||||
"type_info": "Timestamptz"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "completed_at",
|
|
||||||
"ordinal": 7,
|
|
||||||
"type_info": "Timestamptz"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "consumed_at",
|
|
||||||
"ordinal": 8,
|
|
||||||
"type_info": "Timestamptz"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "provider_issuer",
|
|
||||||
"ordinal": 9,
|
|
||||||
"type_info": "Text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "provider_scope",
|
|
||||||
"ordinal": 10,
|
|
||||||
"type_info": "Text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "provider_client_id",
|
|
||||||
"ordinal": 11,
|
|
||||||
"type_info": "Text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "provider_encrypted_client_secret",
|
|
||||||
"ordinal": 12,
|
|
||||||
"type_info": "Text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "provider_token_endpoint_auth_method",
|
|
||||||
"ordinal": 13,
|
|
||||||
"type_info": "Text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "provider_token_endpoint_signing_alg",
|
|
||||||
"ordinal": 14,
|
|
||||||
"type_info": "Text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "provider_created_at",
|
|
||||||
"ordinal": 15,
|
|
||||||
"type_info": "Timestamptz"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"nullable": [
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
true,
|
|
||||||
false,
|
|
||||||
true,
|
|
||||||
false,
|
|
||||||
true,
|
|
||||||
true,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
true,
|
|
||||||
false,
|
|
||||||
true,
|
|
||||||
false
|
|
||||||
],
|
|
||||||
"parameters": {
|
|
||||||
"Left": [
|
|
||||||
"Uuid"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"query": "\n SELECT\n ua.upstream_oauth_authorization_session_id,\n ua.upstream_oauth_provider_id,\n ua.state,\n ua.code_challenge_verifier,\n ua.nonce,\n ua.id_token,\n ua.created_at,\n ua.completed_at,\n ua.consumed_at,\n up.issuer AS \"provider_issuer\",\n up.scope AS \"provider_scope\",\n up.client_id AS \"provider_client_id\",\n up.encrypted_client_secret AS \"provider_encrypted_client_secret\",\n up.token_endpoint_auth_method AS \"provider_token_endpoint_auth_method\",\n up.token_endpoint_signing_alg AS \"provider_token_endpoint_signing_alg\",\n up.created_at AS \"provider_created_at\"\n FROM upstream_oauth_authorization_sessions ua\n INNER JOIN upstream_oauth_providers up\n USING (upstream_oauth_provider_id)\n WHERE upstream_oauth_authorization_session_id = $1\n "
|
|
||||||
},
|
|
||||||
"60d039442cfa57e187602c0ff5e386e32fb774b5ad2d2f2c616040819b76873e": {
|
"60d039442cfa57e187602c0ff5e386e32fb774b5ad2d2f2c616040819b76873e": {
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [],
|
"columns": [],
|
||||||
@ -1457,6 +1422,122 @@
|
|||||||
},
|
},
|
||||||
"query": "\n UPDATE user_sessions\n SET finished_at = $1\n WHERE user_session_id = $2\n "
|
"query": "\n UPDATE user_sessions\n SET finished_at = $1\n WHERE user_session_id = $2\n "
|
||||||
},
|
},
|
||||||
|
"65c7600f1af07cb6ea49d89ae6fbca5374a57c5a866c8aadd7b75ed1d2d1d0cd": {
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "upstream_oauth_authorization_session_id",
|
||||||
|
"ordinal": 0,
|
||||||
|
"type_info": "Uuid"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "upstream_oauth_provider_id",
|
||||||
|
"ordinal": 1,
|
||||||
|
"type_info": "Uuid"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "upstream_oauth_link_id",
|
||||||
|
"ordinal": 2,
|
||||||
|
"type_info": "Uuid"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "state",
|
||||||
|
"ordinal": 3,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "code_challenge_verifier",
|
||||||
|
"ordinal": 4,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "nonce",
|
||||||
|
"ordinal": 5,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "id_token",
|
||||||
|
"ordinal": 6,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "created_at",
|
||||||
|
"ordinal": 7,
|
||||||
|
"type_info": "Timestamptz"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "completed_at",
|
||||||
|
"ordinal": 8,
|
||||||
|
"type_info": "Timestamptz"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "consumed_at",
|
||||||
|
"ordinal": 9,
|
||||||
|
"type_info": "Timestamptz"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "provider_issuer",
|
||||||
|
"ordinal": 10,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "provider_scope",
|
||||||
|
"ordinal": 11,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "provider_client_id",
|
||||||
|
"ordinal": 12,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "provider_encrypted_client_secret",
|
||||||
|
"ordinal": 13,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "provider_token_endpoint_auth_method",
|
||||||
|
"ordinal": 14,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "provider_token_endpoint_signing_alg",
|
||||||
|
"ordinal": 15,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "provider_created_at",
|
||||||
|
"ordinal": 16,
|
||||||
|
"type_info": "Timestamptz"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"nullable": [
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
false
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Left": [
|
||||||
|
"Uuid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"query": "\n SELECT\n ua.upstream_oauth_authorization_session_id,\n ua.upstream_oauth_provider_id,\n ua.upstream_oauth_link_id,\n ua.state,\n ua.code_challenge_verifier,\n ua.nonce,\n ua.id_token,\n ua.created_at,\n ua.completed_at,\n ua.consumed_at,\n up.issuer AS \"provider_issuer\",\n up.scope AS \"provider_scope\",\n up.client_id AS \"provider_client_id\",\n up.encrypted_client_secret AS \"provider_encrypted_client_secret\",\n up.token_endpoint_auth_method AS \"provider_token_endpoint_auth_method\",\n up.token_endpoint_signing_alg AS \"provider_token_endpoint_signing_alg\",\n up.created_at AS \"provider_created_at\"\n FROM upstream_oauth_authorization_sessions ua\n INNER JOIN upstream_oauth_providers up\n USING (upstream_oauth_provider_id)\n WHERE upstream_oauth_authorization_session_id = $1\n "
|
||||||
|
},
|
||||||
"6bf0da5ba3dd07b499193a2e0ddeea6e712f9df8f7f28874ff56a952a9f10e54": {
|
"6bf0da5ba3dd07b499193a2e0ddeea6e712f9df8f7f28874ff56a952a9f10e54": {
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [],
|
"columns": [],
|
||||||
@ -1994,69 +2075,6 @@
|
|||||||
},
|
},
|
||||||
"query": "\n UPDATE users\n SET primary_user_email_id = user_emails.user_email_id\n FROM user_emails\n WHERE user_emails.user_email_id = $1\n AND users.user_id = user_emails.user_id\n "
|
"query": "\n UPDATE users\n SET primary_user_email_id = user_emails.user_email_id\n FROM user_emails\n WHERE user_emails.user_email_id = $1\n AND users.user_id = user_emails.user_id\n "
|
||||||
},
|
},
|
||||||
"83ae2f24b4e5029a2e28b5404b8f3ae635ad9ec19f4e92d8e1823156fd836b4c": {
|
|
||||||
"describe": {
|
|
||||||
"columns": [
|
|
||||||
{
|
|
||||||
"name": "upstream_oauth_authorization_session_id",
|
|
||||||
"ordinal": 0,
|
|
||||||
"type_info": "Uuid"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "state",
|
|
||||||
"ordinal": 1,
|
|
||||||
"type_info": "Text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "code_challenge_verifier",
|
|
||||||
"ordinal": 2,
|
|
||||||
"type_info": "Text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "nonce",
|
|
||||||
"ordinal": 3,
|
|
||||||
"type_info": "Text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "id_token",
|
|
||||||
"ordinal": 4,
|
|
||||||
"type_info": "Text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "created_at",
|
|
||||||
"ordinal": 5,
|
|
||||||
"type_info": "Timestamptz"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "completed_at",
|
|
||||||
"ordinal": 6,
|
|
||||||
"type_info": "Timestamptz"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "consumed_at",
|
|
||||||
"ordinal": 7,
|
|
||||||
"type_info": "Timestamptz"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"nullable": [
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
true,
|
|
||||||
false,
|
|
||||||
true,
|
|
||||||
false,
|
|
||||||
true,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
"parameters": {
|
|
||||||
"Left": [
|
|
||||||
"Uuid",
|
|
||||||
"Uuid"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"query": "\n SELECT\n upstream_oauth_authorization_session_id,\n state,\n code_challenge_verifier,\n nonce,\n id_token,\n created_at,\n completed_at,\n consumed_at\n FROM upstream_oauth_authorization_sessions\n WHERE upstream_oauth_authorization_session_id = $1\n AND upstream_oauth_link_id = $2\n "
|
|
||||||
},
|
|
||||||
"874e677f82c221c5bb621c12f293bcef4e70c68c87ec003fcd475bcb994b5a4c": {
|
"874e677f82c221c5bb621c12f293bcef4e70c68c87ec003fcd475bcb994b5a4c": {
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [],
|
"columns": [],
|
||||||
|
@ -37,7 +37,7 @@ struct LinkLookup {
|
|||||||
pub async fn lookup_link(
|
pub async fn lookup_link(
|
||||||
executor: impl PgExecutor<'_>,
|
executor: impl PgExecutor<'_>,
|
||||||
id: Ulid,
|
id: Ulid,
|
||||||
) -> Result<(UpstreamOAuthLink, Ulid, Option<Ulid>), GenericLookupError> {
|
) -> Result<UpstreamOAuthLink, GenericLookupError> {
|
||||||
let res = sqlx::query_as!(
|
let res = sqlx::query_as!(
|
||||||
LinkLookup,
|
LinkLookup,
|
||||||
r#"
|
r#"
|
||||||
@ -56,15 +56,13 @@ pub async fn lookup_link(
|
|||||||
.await
|
.await
|
||||||
.map_err(GenericLookupError::what("Upstream OAuth 2.0 link"))?;
|
.map_err(GenericLookupError::what("Upstream OAuth 2.0 link"))?;
|
||||||
|
|
||||||
Ok((
|
Ok(UpstreamOAuthLink {
|
||||||
UpstreamOAuthLink {
|
|
||||||
id: Ulid::from(res.upstream_oauth_link_id),
|
id: Ulid::from(res.upstream_oauth_link_id),
|
||||||
|
provider_id: Ulid::from(res.upstream_oauth_provider_id),
|
||||||
|
user_id: res.user_id.map(Ulid::from),
|
||||||
subject: res.subject,
|
subject: res.subject,
|
||||||
created_at: res.created_at,
|
created_at: res.created_at,
|
||||||
},
|
})
|
||||||
Ulid::from(res.upstream_oauth_provider_id),
|
|
||||||
res.user_id.map(Ulid::from),
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(
|
#[tracing::instrument(
|
||||||
@ -81,7 +79,7 @@ pub async fn lookup_link_by_subject(
|
|||||||
executor: impl PgExecutor<'_>,
|
executor: impl PgExecutor<'_>,
|
||||||
upstream_oauth_provider: &UpstreamOAuthProvider,
|
upstream_oauth_provider: &UpstreamOAuthProvider,
|
||||||
subject: &str,
|
subject: &str,
|
||||||
) -> Result<(UpstreamOAuthLink, Option<Ulid>), GenericLookupError> {
|
) -> Result<UpstreamOAuthLink, GenericLookupError> {
|
||||||
let res = sqlx::query_as!(
|
let res = sqlx::query_as!(
|
||||||
LinkLookup,
|
LinkLookup,
|
||||||
r#"
|
r#"
|
||||||
@ -102,14 +100,13 @@ pub async fn lookup_link_by_subject(
|
|||||||
.await
|
.await
|
||||||
.map_err(GenericLookupError::what("Upstream OAuth 2.0 link"))?;
|
.map_err(GenericLookupError::what("Upstream OAuth 2.0 link"))?;
|
||||||
|
|
||||||
Ok((
|
Ok(UpstreamOAuthLink {
|
||||||
UpstreamOAuthLink {
|
|
||||||
id: Ulid::from(res.upstream_oauth_link_id),
|
id: Ulid::from(res.upstream_oauth_link_id),
|
||||||
|
provider_id: Ulid::from(res.upstream_oauth_provider_id),
|
||||||
|
user_id: res.user_id.map(Ulid::from),
|
||||||
subject: res.subject,
|
subject: res.subject,
|
||||||
created_at: res.created_at,
|
created_at: res.created_at,
|
||||||
},
|
})
|
||||||
res.user_id.map(Ulid::from),
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(
|
#[tracing::instrument(
|
||||||
@ -154,6 +151,8 @@ pub async fn add_link(
|
|||||||
|
|
||||||
Ok(UpstreamOAuthLink {
|
Ok(UpstreamOAuthLink {
|
||||||
id,
|
id,
|
||||||
|
provider_id: upstream_oauth_provider.id,
|
||||||
|
user_id: None,
|
||||||
subject,
|
subject,
|
||||||
created_at,
|
created_at,
|
||||||
})
|
})
|
||||||
|
@ -18,7 +18,7 @@ mod session;
|
|||||||
|
|
||||||
pub use self::{
|
pub use self::{
|
||||||
link::{add_link, associate_link_to_user, lookup_link, lookup_link_by_subject},
|
link::{add_link, associate_link_to_user, lookup_link, lookup_link_by_subject},
|
||||||
provider::{add_provider, lookup_provider, ProviderLookupError},
|
provider::{add_provider, get_paginated_providers, lookup_provider, ProviderLookupError},
|
||||||
session::{
|
session::{
|
||||||
add_session, complete_session, consume_session, lookup_session, lookup_session_on_link,
|
add_session, complete_session, consume_session, lookup_session, lookup_session_on_link,
|
||||||
SessionLookupError,
|
SessionLookupError,
|
||||||
|
@ -17,12 +17,16 @@ use mas_data_model::UpstreamOAuthProvider;
|
|||||||
use mas_iana::{jose::JsonWebSignatureAlg, oauth::OAuthClientAuthenticationMethod};
|
use mas_iana::{jose::JsonWebSignatureAlg, oauth::OAuthClientAuthenticationMethod};
|
||||||
use oauth2_types::scope::Scope;
|
use oauth2_types::scope::Scope;
|
||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
use sqlx::PgExecutor;
|
use sqlx::{PgExecutor, QueryBuilder};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
use tracing::{info_span, Instrument};
|
||||||
use ulid::Ulid;
|
use ulid::Ulid;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::{Clock, DatabaseInconsistencyError, LookupError};
|
use crate::{
|
||||||
|
pagination::{process_page, QueryBuilderExt},
|
||||||
|
Clock, DatabaseInconsistencyError, LookupError,
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
#[error("Failed to lookup upstream OAuth 2.0 provider")]
|
#[error("Failed to lookup upstream OAuth 2.0 provider")]
|
||||||
@ -37,6 +41,7 @@ impl LookupError for ProviderLookupError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(sqlx::FromRow)]
|
||||||
struct ProviderLookup {
|
struct ProviderLookup {
|
||||||
upstream_oauth_provider_id: Uuid,
|
upstream_oauth_provider_id: Uuid,
|
||||||
issuer: String,
|
issuer: String,
|
||||||
@ -48,6 +53,37 @@ struct ProviderLookup {
|
|||||||
created_at: DateTime<Utc>,
|
created_at: DateTime<Utc>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl TryFrom<ProviderLookup> for UpstreamOAuthProvider {
|
||||||
|
type Error = DatabaseInconsistencyError;
|
||||||
|
fn try_from(value: ProviderLookup) -> Result<Self, Self::Error> {
|
||||||
|
let id = value.upstream_oauth_provider_id.into();
|
||||||
|
let scope = value
|
||||||
|
.scope
|
||||||
|
.parse()
|
||||||
|
.map_err(|_| DatabaseInconsistencyError)?;
|
||||||
|
let token_endpoint_auth_method = value
|
||||||
|
.token_endpoint_auth_method
|
||||||
|
.parse()
|
||||||
|
.map_err(|_| DatabaseInconsistencyError)?;
|
||||||
|
let token_endpoint_signing_alg = value
|
||||||
|
.token_endpoint_signing_alg
|
||||||
|
.map(|x| x.parse())
|
||||||
|
.transpose()
|
||||||
|
.map_err(|_| DatabaseInconsistencyError)?;
|
||||||
|
|
||||||
|
Ok(UpstreamOAuthProvider {
|
||||||
|
id,
|
||||||
|
issuer: value.issuer,
|
||||||
|
scope,
|
||||||
|
client_id: value.client_id,
|
||||||
|
encrypted_client_secret: value.encrypted_client_secret,
|
||||||
|
token_endpoint_auth_method,
|
||||||
|
token_endpoint_signing_alg,
|
||||||
|
created_at: value.created_at,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[tracing::instrument(
|
#[tracing::instrument(
|
||||||
skip_all,
|
skip_all,
|
||||||
fields(upstream_oauth_provider.id = %id),
|
fields(upstream_oauth_provider.id = %id),
|
||||||
@ -77,23 +113,7 @@ pub async fn lookup_provider(
|
|||||||
.fetch_one(executor)
|
.fetch_one(executor)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(UpstreamOAuthProvider {
|
Ok(res.try_into()?)
|
||||||
id: res.upstream_oauth_provider_id.into(),
|
|
||||||
issuer: res.issuer,
|
|
||||||
scope: res.scope.parse().map_err(|_| DatabaseInconsistencyError)?,
|
|
||||||
client_id: res.client_id,
|
|
||||||
encrypted_client_secret: res.encrypted_client_secret,
|
|
||||||
token_endpoint_auth_method: res
|
|
||||||
.token_endpoint_auth_method
|
|
||||||
.parse()
|
|
||||||
.map_err(|_| DatabaseInconsistencyError)?,
|
|
||||||
token_endpoint_signing_alg: res
|
|
||||||
.token_endpoint_signing_alg
|
|
||||||
.map(|x| x.parse())
|
|
||||||
.transpose()
|
|
||||||
.map_err(|_| DatabaseInconsistencyError)?,
|
|
||||||
created_at: res.created_at,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(
|
#[tracing::instrument(
|
||||||
@ -157,3 +177,45 @@ pub async fn add_provider(
|
|||||||
created_at,
|
created_at,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(skip_all, err(Display))]
|
||||||
|
pub async fn get_paginated_providers(
|
||||||
|
executor: impl PgExecutor<'_>,
|
||||||
|
before: Option<Ulid>,
|
||||||
|
after: Option<Ulid>,
|
||||||
|
first: Option<usize>,
|
||||||
|
last: Option<usize>,
|
||||||
|
) -> Result<(bool, bool, Vec<UpstreamOAuthProvider>), anyhow::Error> {
|
||||||
|
let mut query = QueryBuilder::new(
|
||||||
|
r#"
|
||||||
|
SELECT
|
||||||
|
upstream_oauth_provider_id,
|
||||||
|
issuer,
|
||||||
|
scope,
|
||||||
|
client_id,
|
||||||
|
encrypted_client_secret,
|
||||||
|
token_endpoint_signing_alg,
|
||||||
|
token_endpoint_auth_method,
|
||||||
|
created_at
|
||||||
|
FROM upstream_oauth_providers
|
||||||
|
WHERE 1 = 1
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
|
||||||
|
query.generate_pagination("upstream_oauth_provider_id", before, after, first, last)?;
|
||||||
|
|
||||||
|
let span = info_span!(
|
||||||
|
"Fetch paginated upstream OAuth 2.0 providers",
|
||||||
|
db.statement = query.sql()
|
||||||
|
);
|
||||||
|
let page: Vec<ProviderLookup> = query
|
||||||
|
.build_query_as()
|
||||||
|
.fetch_all(executor)
|
||||||
|
.instrument(span)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let (has_previous_page, has_next_page, page) = process_page(page, first, last)?;
|
||||||
|
|
||||||
|
let page: Result<Vec<_>, _> = page.into_iter().map(TryInto::try_into).collect();
|
||||||
|
Ok((has_previous_page, has_next_page, page?))
|
||||||
|
}
|
||||||
|
@ -38,6 +38,7 @@ impl LookupError for SessionLookupError {
|
|||||||
struct SessionAndProviderLookup {
|
struct SessionAndProviderLookup {
|
||||||
upstream_oauth_authorization_session_id: Uuid,
|
upstream_oauth_authorization_session_id: Uuid,
|
||||||
upstream_oauth_provider_id: Uuid,
|
upstream_oauth_provider_id: Uuid,
|
||||||
|
upstream_oauth_link_id: Option<Uuid>,
|
||||||
state: String,
|
state: String,
|
||||||
code_challenge_verifier: Option<String>,
|
code_challenge_verifier: Option<String>,
|
||||||
nonce: String,
|
nonce: String,
|
||||||
@ -70,6 +71,7 @@ pub async fn lookup_session(
|
|||||||
SELECT
|
SELECT
|
||||||
ua.upstream_oauth_authorization_session_id,
|
ua.upstream_oauth_authorization_session_id,
|
||||||
ua.upstream_oauth_provider_id,
|
ua.upstream_oauth_provider_id,
|
||||||
|
ua.upstream_oauth_link_id,
|
||||||
ua.state,
|
ua.state,
|
||||||
ua.code_challenge_verifier,
|
ua.code_challenge_verifier,
|
||||||
ua.nonce,
|
ua.nonce,
|
||||||
@ -120,6 +122,8 @@ pub async fn lookup_session(
|
|||||||
|
|
||||||
let session = UpstreamOAuthAuthorizationSession {
|
let session = UpstreamOAuthAuthorizationSession {
|
||||||
id: res.upstream_oauth_authorization_session_id.into(),
|
id: res.upstream_oauth_authorization_session_id.into(),
|
||||||
|
provider_id: provider.id,
|
||||||
|
link_id: res.upstream_oauth_link_id.map(Ulid::from),
|
||||||
state: res.state,
|
state: res.state,
|
||||||
code_challenge_verifier: res.code_challenge_verifier,
|
code_challenge_verifier: res.code_challenge_verifier,
|
||||||
nonce: res.nonce,
|
nonce: res.nonce,
|
||||||
@ -185,6 +189,8 @@ pub async fn add_session(
|
|||||||
|
|
||||||
Ok(UpstreamOAuthAuthorizationSession {
|
Ok(UpstreamOAuthAuthorizationSession {
|
||||||
id,
|
id,
|
||||||
|
provider_id: upstream_oauth_provider.id,
|
||||||
|
link_id: None,
|
||||||
state,
|
state,
|
||||||
code_challenge_verifier,
|
code_challenge_verifier,
|
||||||
nonce,
|
nonce,
|
||||||
@ -267,6 +273,8 @@ pub async fn consume_session(
|
|||||||
|
|
||||||
struct SessionLookup {
|
struct SessionLookup {
|
||||||
upstream_oauth_authorization_session_id: Uuid,
|
upstream_oauth_authorization_session_id: Uuid,
|
||||||
|
upstream_oauth_provider_id: Uuid,
|
||||||
|
upstream_oauth_link_id: Option<Uuid>,
|
||||||
state: String,
|
state: String,
|
||||||
code_challenge_verifier: Option<String>,
|
code_challenge_verifier: Option<String>,
|
||||||
nonce: String,
|
nonce: String,
|
||||||
@ -295,6 +303,8 @@ pub async fn lookup_session_on_link(
|
|||||||
r#"
|
r#"
|
||||||
SELECT
|
SELECT
|
||||||
upstream_oauth_authorization_session_id,
|
upstream_oauth_authorization_session_id,
|
||||||
|
upstream_oauth_provider_id,
|
||||||
|
upstream_oauth_link_id,
|
||||||
state,
|
state,
|
||||||
code_challenge_verifier,
|
code_challenge_verifier,
|
||||||
nonce,
|
nonce,
|
||||||
@ -317,6 +327,8 @@ pub async fn lookup_session_on_link(
|
|||||||
|
|
||||||
Ok(UpstreamOAuthAuthorizationSession {
|
Ok(UpstreamOAuthAuthorizationSession {
|
||||||
id: res.upstream_oauth_authorization_session_id.into(),
|
id: res.upstream_oauth_authorization_session_id.into(),
|
||||||
|
provider_id: res.upstream_oauth_provider_id.into(),
|
||||||
|
link_id: res.upstream_oauth_link_id.map(Ulid::from),
|
||||||
state: res.state,
|
state: res.state,
|
||||||
code_challenge_verifier: res.code_challenge_verifier,
|
code_challenge_verifier: res.code_challenge_verifier,
|
||||||
nonce: res.nonce,
|
nonce: res.nonce,
|
||||||
|
@ -310,11 +310,95 @@ type RootQuery {
|
|||||||
"""
|
"""
|
||||||
userEmail(id: ID!): UserEmail
|
userEmail(id: ID!): UserEmail
|
||||||
"""
|
"""
|
||||||
|
Fetch an upstream OAuth 2.0 link by its ID.
|
||||||
|
"""
|
||||||
|
upstreamOauth2Link(id: ID!): UpstreamOAuth2Link
|
||||||
|
"""
|
||||||
|
Fetch an upstream OAuth 2.0 provider by its ID.
|
||||||
|
"""
|
||||||
|
upstreamOauth2Provider(id: ID!): UpstreamOAuth2Provider
|
||||||
|
"""
|
||||||
|
Get a list of upstream OAuth 2.0 providers.
|
||||||
|
"""
|
||||||
|
upstreamOauth2Providers(
|
||||||
|
after: String
|
||||||
|
before: String
|
||||||
|
first: Int
|
||||||
|
last: Int
|
||||||
|
): UpstreamOAuth2ProviderConnection!
|
||||||
|
"""
|
||||||
Fetches an object given its ID.
|
Fetches an object given its ID.
|
||||||
"""
|
"""
|
||||||
node(id: ID!): Node
|
node(id: ID!): Node
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type UpstreamOAuth2Link implements Node {
|
||||||
|
"""
|
||||||
|
ID of the object.
|
||||||
|
"""
|
||||||
|
id: ID!
|
||||||
|
"""
|
||||||
|
When the object was created.
|
||||||
|
"""
|
||||||
|
createdAt: DateTime!
|
||||||
|
"""
|
||||||
|
The provider for which this link is.
|
||||||
|
"""
|
||||||
|
provider: UpstreamOAuth2Provider!
|
||||||
|
"""
|
||||||
|
The user to which this link is associated.
|
||||||
|
"""
|
||||||
|
user: User
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpstreamOAuth2Provider implements Node {
|
||||||
|
"""
|
||||||
|
ID of the object.
|
||||||
|
"""
|
||||||
|
id: ID!
|
||||||
|
"""
|
||||||
|
When the object was created.
|
||||||
|
"""
|
||||||
|
createdAt: DateTime!
|
||||||
|
"""
|
||||||
|
OpenID Connect issuer URL.
|
||||||
|
"""
|
||||||
|
issuer: String!
|
||||||
|
"""
|
||||||
|
Client ID used for this provider.
|
||||||
|
"""
|
||||||
|
clientId: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpstreamOAuth2ProviderConnection {
|
||||||
|
"""
|
||||||
|
Information to aid in pagination.
|
||||||
|
"""
|
||||||
|
pageInfo: PageInfo!
|
||||||
|
"""
|
||||||
|
A list of edges.
|
||||||
|
"""
|
||||||
|
edges: [UpstreamOAuth2ProviderEdge!]!
|
||||||
|
"""
|
||||||
|
A list of nodes.
|
||||||
|
"""
|
||||||
|
nodes: [UpstreamOAuth2Provider!]!
|
||||||
|
}
|
||||||
|
|
||||||
|
"""
|
||||||
|
An edge in a connection.
|
||||||
|
"""
|
||||||
|
type UpstreamOAuth2ProviderEdge {
|
||||||
|
"""
|
||||||
|
A cursor for use in pagination
|
||||||
|
"""
|
||||||
|
cursor: String!
|
||||||
|
"""
|
||||||
|
The item at the end of the edge
|
||||||
|
"""
|
||||||
|
node: UpstreamOAuth2Provider!
|
||||||
|
}
|
||||||
|
|
||||||
"""
|
"""
|
||||||
URL is a String implementing the [URL Standard](http://url.spec.whatwg.org/)
|
URL is a String implementing the [URL Standard](http://url.spec.whatwg.org/)
|
||||||
"""
|
"""
|
||||||
|
Reference in New Issue
Block a user