Skip to content
This repository was archived by the owner on Sep 10, 2024. It is now read-only.

Commit 83ca90e

Browse files
committed
Add a GraphQL mutation to create arbitrary OAuth2 sessions.
1 parent b8012bb commit 83ca90e

File tree

8 files changed

+369
-71
lines changed

8 files changed

+369
-71
lines changed

crates/graphql/src/lib.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,13 @@ impl Requester {
131131
}
132132
}
133133

134+
fn oauth2_session(&self) -> Option<&Session> {
135+
match self {
136+
Self::OAuth2Session(session, _) => Some(session),
137+
Self::BrowserSession(_) | Self::Anonymous => None,
138+
}
139+
}
140+
134141
/// Returns true if the requester can access the resource.
135142
fn is_owner_or_admin(&self, resource: &impl OwnerId) -> bool {
136143
// If the requester is an admin, they can do anything.

crates/graphql/src/mutations/oauth2_session.rs

Lines changed: 136 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,19 @@
1313
// limitations under the License.
1414

1515
use anyhow::Context as _;
16-
use async_graphql::{Context, Enum, InputObject, Object, ID};
17-
use mas_data_model::Device;
16+
use async_graphql::{Context, Description, Enum, InputObject, Object, ID};
17+
use chrono::Duration;
18+
use mas_data_model::{Device, TokenType};
1819
use mas_storage::{
19-
job::{DeleteDeviceJob, JobRepositoryExt},
20-
oauth2::OAuth2SessionRepository,
20+
job::{DeleteDeviceJob, JobRepositoryExt, ProvisionDeviceJob},
21+
oauth2::{
22+
OAuth2AccessTokenRepository, OAuth2ClientRepository, OAuth2RefreshTokenRepository,
23+
OAuth2SessionRepository,
24+
},
25+
user::UserRepository,
2126
RepositoryAccess,
2227
};
28+
use oauth2_types::scope::Scope;
2329

2430
use crate::{
2531
model::{NodeType, OAuth2Session},
@@ -31,6 +37,45 @@ pub struct OAuth2SessionMutations {
3137
_private: (),
3238
}
3339

40+
/// The input of the `createOauth2Session` mutation.
41+
#[derive(InputObject)]
42+
pub struct CreateOAuth2SessionInput {
43+
/// The scope of the session
44+
scope: String,
45+
46+
/// The ID of the user for which to create the session
47+
user_id: ID,
48+
49+
/// Whether the session should issue a never-expiring access token
50+
permanent: Option<bool>,
51+
}
52+
53+
/// The payload of the `createOauth2Session` mutation.
54+
#[derive(Description)]
55+
pub struct CreateOAuth2SessionPayload {
56+
access_token: String,
57+
refresh_token: Option<String>,
58+
session: mas_data_model::Session,
59+
}
60+
61+
#[Object(use_type_description)]
62+
impl CreateOAuth2SessionPayload {
63+
/// Access token for this session
64+
pub async fn access_token(&self) -> &str {
65+
&self.access_token
66+
}
67+
68+
/// Refresh token for this session, if it is not a permanent session
69+
pub async fn refresh_token(&self) -> Option<&str> {
70+
self.refresh_token.as_deref()
71+
}
72+
73+
/// The OAuth 2.0 session which was just created
74+
pub async fn oauth2_session(&self) -> OAuth2Session {
75+
OAuth2Session(self.session.clone())
76+
}
77+
}
78+
3479
/// The input of the `endOauth2Session` mutation.
3580
#[derive(InputObject)]
3681
pub struct EndOAuth2SessionInput {
@@ -75,6 +120,93 @@ impl EndOAuth2SessionPayload {
75120

76121
#[Object]
77122
impl OAuth2SessionMutations {
123+
/// Create a new arbitrary OAuth 2.0 Session.
124+
///
125+
/// Only available for administrators.
126+
async fn create_oauth2_session(
127+
&self,
128+
ctx: &Context<'_>,
129+
input: CreateOAuth2SessionInput,
130+
) -> Result<CreateOAuth2SessionPayload, async_graphql::Error> {
131+
let state = ctx.state();
132+
let user_id = NodeType::User.extract_ulid(&input.user_id)?;
133+
let scope: Scope = input.scope.parse().context("Invalid scope")?;
134+
let permanent = input.permanent.unwrap_or(false);
135+
let requester = ctx.requester();
136+
137+
if !requester.is_admin() {
138+
return Err(async_graphql::Error::new("Unauthorized"));
139+
}
140+
141+
let session = requester
142+
.oauth2_session()
143+
.context("Requester should be a OAuth 2.0 client")?;
144+
145+
let mut repo = state.repository().await?;
146+
let clock = state.clock();
147+
let mut rng = state.rng();
148+
149+
let client = repo
150+
.oauth2_client()
151+
.lookup(session.client_id)
152+
.await?
153+
.context("Client not found")?;
154+
155+
let user = repo
156+
.user()
157+
.lookup(user_id)
158+
.await?
159+
.context("User not found")?;
160+
161+
// Generate a new access token
162+
let access_token = TokenType::AccessToken.generate(&mut rng);
163+
164+
// Create the OAuth 2.0 Session
165+
let session = repo
166+
.oauth2_session()
167+
.add(&mut rng, &clock, &client, Some(&user), None, scope)
168+
.await?;
169+
170+
// Look for devices to provision
171+
for scope in &*session.scope {
172+
if let Some(device) = Device::from_scope_token(scope) {
173+
repo.job()
174+
.schedule_job(ProvisionDeviceJob::new(&user, &device))
175+
.await?;
176+
}
177+
}
178+
179+
let ttl = if permanent {
180+
// XXX: that's lazy
181+
Duration::days(365 * 50)
182+
} else {
183+
Duration::minutes(5)
184+
};
185+
let access_token = repo
186+
.oauth2_access_token()
187+
.add(&mut rng, &clock, &session, access_token, ttl)
188+
.await?;
189+
190+
let refresh_token = if !permanent {
191+
let refresh_token = TokenType::RefreshToken.generate(&mut rng);
192+
193+
let refresh_token = repo
194+
.oauth2_refresh_token()
195+
.add(&mut rng, &clock, &session, &access_token, refresh_token)
196+
.await?;
197+
198+
Some(refresh_token)
199+
} else {
200+
None
201+
};
202+
203+
Ok(CreateOAuth2SessionPayload {
204+
session,
205+
access_token: access_token.access_token,
206+
refresh_token: refresh_token.map(|t| t.refresh_token),
207+
})
208+
}
209+
78210
async fn end_oauth2_session(
79211
&self,
80212
ctx: &Context<'_>,

crates/handlers/src/graphql/tests.rs

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -510,6 +510,7 @@ async fn test_oauth2_client_credentials(pool: PgPool) {
510510
mutation {
511511
addUser(input: {username: "alice"}) {
512512
user {
513+
id
513514
username
514515
}
515516
}
@@ -519,15 +520,42 @@ async fn test_oauth2_client_credentials(pool: PgPool) {
519520
let response = state.request(request).await;
520521
response.assert_status(StatusCode::OK);
521522
let response: GraphQLResponse = response.json();
522-
assert!(response.errors.is_empty());
523+
assert!(response.errors.is_empty(), "{:?}", response.errors);
524+
let user_id = &response.data["addUser"]["user"]["id"];
525+
523526
assert_eq!(
524527
response.data,
525528
serde_json::json!({
526529
"addUser": {
527530
"user": {
531+
"id": user_id,
528532
"username": "alice"
529533
}
530534
}
531535
})
532536
);
537+
538+
// We should now be able to create an arbitrary access token for the user
539+
let request = Request::post("/graphql")
540+
.bearer(&access_token)
541+
.json(serde_json::json!({
542+
"query": r#"
543+
mutation CreateSession($userId: String!, $scope: String!) {
544+
createOauth2Session(input: {userId: $userId, permanent: true, scope: $scope}) {
545+
accessToken
546+
refreshToken
547+
}
548+
}
549+
"#,
550+
"variables": {
551+
"userId": user_id,
552+
"scope": "urn:matrix:org.matrix.msc2967.client:device:AABBCCDDEE urn:matrix:org.matrix.msc2967.client:api:* urn:synapse:admin:*"
553+
},
554+
}));
555+
let response = state.request(request).await;
556+
response.assert_status(StatusCode::OK);
557+
let response: GraphQLResponse = response.json();
558+
assert!(response.errors.is_empty(), "{:?}", response.errors);
559+
assert!(response.data["createOauth2Session"]["refreshToken"].is_null());
560+
assert!(response.data["createOauth2Session"]["accessToken"].is_string());
533561
}

crates/storage-pg/src/oauth2/session.rs

Lines changed: 9 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
use async_trait::async_trait;
1616
use chrono::{DateTime, Utc};
17-
use mas_data_model::{BrowserSession, Client, Session, SessionState};
17+
use mas_data_model::{BrowserSession, Client, Session, SessionState, User};
1818
use mas_storage::{
1919
oauth2::{OAuth2SessionFilter, OAuth2SessionRepository},
2020
Clock, Page, Pagination,
@@ -133,24 +133,23 @@ impl<'c> OAuth2SessionRepository for PgOAuth2SessionRepository<'c> {
133133
}
134134

135135
#[tracing::instrument(
136-
name = "db.oauth2_session.add_from_browser_session",
136+
name = "db.oauth2_session.add",
137137
skip_all,
138138
fields(
139139
db.statement,
140-
%user_session.id,
141-
user.id = %user_session.user.id,
142140
%client.id,
143141
session.id,
144142
session.scope = %scope,
145143
),
146144
err,
147145
)]
148-
async fn add_from_browser_session(
146+
async fn add(
149147
&mut self,
150148
rng: &mut (dyn RngCore + Send),
151149
clock: &dyn Clock,
152150
client: &Client,
153-
user_session: &BrowserSession,
151+
user: Option<&User>,
152+
user_session: Option<&BrowserSession>,
154153
scope: Scope,
155154
) -> Result<Session, Self::Error> {
156155
let created_at = clock.now();
@@ -172,8 +171,8 @@ impl<'c> OAuth2SessionRepository for PgOAuth2SessionRepository<'c> {
172171
VALUES ($1, $2, $3, $4, $5, $6)
173172
"#,
174173
Uuid::from(id),
175-
Uuid::from(user_session.user.id),
176-
Uuid::from(user_session.id),
174+
user.map(|u| Uuid::from(u.id)),
175+
user_session.map(|s| Uuid::from(s.id)),
177176
Uuid::from(client.id),
178177
&scope_list,
179178
created_at,
@@ -186,62 +185,8 @@ impl<'c> OAuth2SessionRepository for PgOAuth2SessionRepository<'c> {
186185
id,
187186
state: SessionState::Valid,
188187
created_at,
189-
user_id: Some(user_session.user.id),
190-
user_session_id: Some(user_session.id),
191-
client_id: client.id,
192-
scope,
193-
})
194-
}
195-
196-
#[tracing::instrument(
197-
name = "db.oauth2_session.add_from_client_credentials",
198-
skip_all,
199-
fields(
200-
db.statement,
201-
%client.id,
202-
session.id,
203-
session.scope = %scope,
204-
),
205-
err,
206-
)]
207-
async fn add_from_client_credentials(
208-
&mut self,
209-
rng: &mut (dyn RngCore + Send),
210-
clock: &dyn Clock,
211-
client: &Client,
212-
scope: Scope,
213-
) -> Result<Session, Self::Error> {
214-
let created_at = clock.now();
215-
let id = Ulid::from_datetime_with_source(created_at.into(), rng);
216-
tracing::Span::current().record("session.id", tracing::field::display(id));
217-
218-
let scope_list: Vec<String> = scope.iter().map(|s| s.as_str().to_owned()).collect();
219-
220-
sqlx::query!(
221-
r#"
222-
INSERT INTO oauth2_sessions
223-
( oauth2_session_id
224-
, oauth2_client_id
225-
, scope_list
226-
, created_at
227-
)
228-
VALUES ($1, $2, $3, $4)
229-
"#,
230-
Uuid::from(id),
231-
Uuid::from(client.id),
232-
&scope_list,
233-
created_at,
234-
)
235-
.traced()
236-
.execute(&mut *self.conn)
237-
.await?;
238-
239-
Ok(Session {
240-
id,
241-
state: SessionState::Valid,
242-
created_at,
243-
user_id: None,
244-
user_session_id: None,
188+
user_id: user.map(|u| u.id),
189+
user_session_id: user_session.map(|s| s.id),
245190
client_id: client.id,
246191
scope,
247192
})

0 commit comments

Comments
 (0)