1
0
mirror of https://github.com/matrix-org/matrix-authentication-service.git synced 2025-11-20 12:02:22 +03:00

handlers: add tests for introspection endpoint

This commit is contained in:
Quentin Gliech
2023-02-23 17:18:26 +01:00
parent 97635375cc
commit 67753c0e26
2 changed files with 354 additions and 0 deletions

View File

@@ -330,3 +330,346 @@ pub(crate) async fn post(
Ok(Json(reply))
}
#[cfg(test)]
mod tests {
use chrono::Duration;
use hyper::{Request, StatusCode};
use mas_data_model::{AccessToken, RefreshToken};
use mas_iana::oauth::OAuthTokenTypeHint;
use mas_router::{OAuth2Introspection, OAuth2RegistrationEndpoint, SimpleRoute};
use oauth2_types::{
registration::ClientRegistrationResponse,
requests::IntrospectionResponse,
scope::{Scope, OPENID},
};
use serde_json::json;
use sqlx::PgPool;
use zeroize::Zeroizing;
use crate::{
oauth2::generate_token_pair,
test_utils::{init_tracing, RequestBuilderExt, ResponseExt, TestState},
};
#[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")]
async fn test_introspect_oauth_tokens(pool: PgPool) {
init_tracing();
let state = TestState::from_pool(pool).await.unwrap();
// Provision a client which will be used to do introspection requests
let request = Request::post(OAuth2RegistrationEndpoint::PATH).json(json!({
"contacts": ["hello@introspecting.com"],
"client_uri": "https://introspecting.com/",
// XXX: even though we don't use the authorization_code flow, we need to specify at
// least one redirect_uri
"redirect_uris": ["https://introspecting.com/"],
"response_types": [],
"grant_types": [],
"token_endpoint_auth_method": "client_secret_basic",
}));
let response = state.request(request).await;
response.assert_status(StatusCode::CREATED);
let client: ClientRegistrationResponse = response.json();
let introspecting_client_id = client.client_id;
let introspecting_client_secret = client.client_secret.unwrap();
// Provision a client which will be used to generate tokens
let request = Request::post(OAuth2RegistrationEndpoint::PATH).json(json!({
"contacts": ["hello@client.com"],
"client_uri": "https://client.com/",
"redirect_uris": ["https://client.com/"],
"response_types": ["code"],
"grant_types": ["authorization_code", "refresh_token"],
"token_endpoint_auth_method": "none",
}));
let response = state.request(request).await;
response.assert_status(StatusCode::CREATED);
let ClientRegistrationResponse { client_id, .. } = response.json();
let mut repo = state.repository().await.unwrap();
// Provision a user and an oauth session
let user = repo
.user()
.add(&mut state.rng(), &state.clock, "alice".to_owned())
.await
.unwrap();
let client = repo
.oauth2_client()
.find_by_client_id(&client_id)
.await
.unwrap()
.unwrap();
let browser_session = repo
.browser_session()
.add(&mut state.rng(), &state.clock, &user)
.await
.unwrap();
let session = repo
.oauth2_session()
.add(
&mut state.rng(),
&state.clock,
&client,
&browser_session,
Scope::from_iter([OPENID]),
)
.await
.unwrap();
let (AccessToken { access_token, .. }, RefreshToken { refresh_token, .. }) =
generate_token_pair(
&mut state.rng(),
&state.clock,
&mut repo,
&session,
Duration::minutes(5),
)
.await
.unwrap();
repo.save().await.unwrap();
// Now that we have a token, we can introspect it
let request = Request::post(OAuth2Introspection::PATH)
.basic_auth(&introspecting_client_id, &introspecting_client_secret)
.form(json!({ "token": access_token }));
let response = state.request(request).await;
response.assert_status(StatusCode::OK);
let response: IntrospectionResponse = response.json();
assert!(response.active);
assert_eq!(response.username, Some("alice".to_owned()));
assert_eq!(response.client_id, Some(client_id.clone()));
assert_eq!(response.token_type, Some(OAuthTokenTypeHint::AccessToken));
assert_eq!(response.scope, Some(Scope::from_iter([OPENID])));
// Do the same request, but with a token_type_hint
let request = Request::post(OAuth2Introspection::PATH)
.basic_auth(&introspecting_client_id, &introspecting_client_secret)
.form(json!({"token": access_token, "token_type_hint": "access_token"}));
let response = state.request(request).await;
response.assert_status(StatusCode::OK);
let response: IntrospectionResponse = response.json();
assert!(response.active);
// Do the same request, but with the wrong token_type_hint
let request = Request::post(OAuth2Introspection::PATH)
.basic_auth(&introspecting_client_id, &introspecting_client_secret)
.form(json!({"token": access_token, "token_type_hint": "refresh_token"}));
let response = state.request(request).await;
response.assert_status(StatusCode::OK);
let response: IntrospectionResponse = response.json();
assert!(!response.active); // It shouldn't be active
// Do the same, but with a refresh token
let request = Request::post(OAuth2Introspection::PATH)
.basic_auth(&introspecting_client_id, &introspecting_client_secret)
.form(json!({ "token": refresh_token }));
let response = state.request(request).await;
response.assert_status(StatusCode::OK);
let response: IntrospectionResponse = response.json();
assert!(response.active);
assert_eq!(response.username, Some("alice".to_owned()));
assert_eq!(response.client_id, Some(client_id.clone()));
assert_eq!(response.token_type, Some(OAuthTokenTypeHint::RefreshToken));
assert_eq!(response.scope, Some(Scope::from_iter([OPENID])));
// Do the same request, but with a token_type_hint
let request = Request::post(OAuth2Introspection::PATH)
.basic_auth(&introspecting_client_id, &introspecting_client_secret)
.form(json!({"token": refresh_token, "token_type_hint": "refresh_token"}));
let response = state.request(request).await;
response.assert_status(StatusCode::OK);
let response: IntrospectionResponse = response.json();
assert!(response.active);
// Do the same request, but with the wrong token_type_hint
let request = Request::post(OAuth2Introspection::PATH)
.basic_auth(&introspecting_client_id, &introspecting_client_secret)
.form(json!({"token": refresh_token, "token_type_hint": "access_token"}));
let response = state.request(request).await;
response.assert_status(StatusCode::OK);
let response: IntrospectionResponse = response.json();
assert!(!response.active); // It shouldn't be active
// Advance the clock to invalidate the access token
state.clock.advance(Duration::hours(1));
let request = Request::post(OAuth2Introspection::PATH)
.basic_auth(&introspecting_client_id, &introspecting_client_secret)
.form(json!({ "token": access_token }));
let response = state.request(request).await;
response.assert_status(StatusCode::OK);
let response: IntrospectionResponse = response.json();
assert!(!response.active); // It shouldn't be active anymore
// But the refresh token should still be valid
let request = Request::post(OAuth2Introspection::PATH)
.basic_auth(&introspecting_client_id, &introspecting_client_secret)
.form(json!({ "token": refresh_token }));
let response = state.request(request).await;
response.assert_status(StatusCode::OK);
let response: IntrospectionResponse = response.json();
assert!(response.active);
}
#[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")]
async fn test_introspect_compat_tokens(pool: PgPool) {
init_tracing();
let state = TestState::from_pool(pool).await.unwrap();
// Provision a client which will be used to do introspection requests
let request = Request::post(OAuth2RegistrationEndpoint::PATH).json(json!({
"contacts": ["hello@introspecting.com"],
"client_uri": "https://introspecting.com/",
// XXX: even though we don't use the authorization_code flow, we need to specify at
// least one redirect_uri
"redirect_uris": ["https://introspecting.com/"],
"response_types": [],
"grant_types": [],
"token_endpoint_auth_method": "client_secret_basic",
}));
let response = state.request(request).await;
response.assert_status(StatusCode::CREATED);
let client: ClientRegistrationResponse = response.json();
let introspecting_client_id = client.client_id;
let introspecting_client_secret = client.client_secret.unwrap();
// Provision a user with a password, so that we can use the password flow
let mut repo = state.repository().await.unwrap();
let user = repo
.user()
.add(&mut state.rng(), &state.clock, "alice".to_owned())
.await
.unwrap();
let (version, hashed_password) = state
.password_manager
.hash(&mut state.rng(), Zeroizing::new(b"password".to_vec()))
.await
.unwrap();
repo.user_password()
.add(
&mut state.rng(),
&state.clock,
&user,
version,
hashed_password,
None,
)
.await
.unwrap();
repo.save().await.unwrap();
// Now do a password flow to get an access token and a refresh token
let request = Request::post("/_matrix/client/v3/login").json(json!({
"type": "m.login.password",
"refresh_token": true,
"identifier": {
"type": "m.id.user",
"user": "alice",
},
"password": "password",
}));
let response = state.request(request).await;
response.assert_status(StatusCode::OK);
let response: serde_json::Value = response.json();
let access_token = response["access_token"].as_str().unwrap();
let refresh_token = response["refresh_token"].as_str().unwrap();
let device_id = response["device_id"].as_str().unwrap();
let expected_scope: Scope =
format!("urn:matrix:org.matrix.msc2967.client:api:* urn:matrix:org.matrix.msc2967.client:device:{device_id}")
.parse()
.unwrap();
// Now that we have a token, we can introspect it
let request = Request::post(OAuth2Introspection::PATH)
.basic_auth(&introspecting_client_id, &introspecting_client_secret)
.form(json!({ "token": access_token }));
let response = state.request(request).await;
response.assert_status(StatusCode::OK);
let response: IntrospectionResponse = response.json();
assert!(response.active);
assert_eq!(response.username, Some("alice".to_owned()));
assert_eq!(response.client_id, Some("legacy".to_owned()));
assert_eq!(response.token_type, Some(OAuthTokenTypeHint::AccessToken));
assert_eq!(response.scope, Some(expected_scope.clone()));
// Do the same request, but with a token_type_hint
let request = Request::post(OAuth2Introspection::PATH)
.basic_auth(&introspecting_client_id, &introspecting_client_secret)
.form(json!({"token": access_token, "token_type_hint": "access_token"}));
let response = state.request(request).await;
response.assert_status(StatusCode::OK);
let response: IntrospectionResponse = response.json();
assert!(response.active);
// Do the same request, but with the wrong token_type_hint
let request = Request::post(OAuth2Introspection::PATH)
.basic_auth(&introspecting_client_id, &introspecting_client_secret)
.form(json!({"token": access_token, "token_type_hint": "refresh_token"}));
let response = state.request(request).await;
response.assert_status(StatusCode::OK);
let response: IntrospectionResponse = response.json();
assert!(!response.active); // It shouldn't be active
// Do the same, but with a refresh token
let request = Request::post(OAuth2Introspection::PATH)
.basic_auth(&introspecting_client_id, &introspecting_client_secret)
.form(json!({ "token": refresh_token }));
let response = state.request(request).await;
response.assert_status(StatusCode::OK);
let response: IntrospectionResponse = response.json();
assert!(response.active);
assert_eq!(response.username, Some("alice".to_owned()));
assert_eq!(response.client_id, Some("legacy".to_owned()));
assert_eq!(response.token_type, Some(OAuthTokenTypeHint::RefreshToken));
assert_eq!(response.scope, Some(expected_scope.clone()));
// Do the same request, but with a token_type_hint
let request = Request::post(OAuth2Introspection::PATH)
.basic_auth(&introspecting_client_id, &introspecting_client_secret)
.form(json!({"token": refresh_token, "token_type_hint": "refresh_token"}));
let response = state.request(request).await;
response.assert_status(StatusCode::OK);
let response: IntrospectionResponse = response.json();
assert!(response.active);
// Do the same request, but with the wrong token_type_hint
let request = Request::post(OAuth2Introspection::PATH)
.basic_auth(&introspecting_client_id, &introspecting_client_secret)
.form(json!({"token": refresh_token, "token_type_hint": "access_token"}));
let response = state.request(request).await;
response.assert_status(StatusCode::OK);
let response: IntrospectionResponse = response.json();
assert!(!response.active); // It shouldn't be active
// Advance the clock to invalidate the access token
state.clock.advance(Duration::hours(1));
let request = Request::post(OAuth2Introspection::PATH)
.basic_auth(&introspecting_client_id, &introspecting_client_secret)
.form(json!({ "token": access_token }));
let response = state.request(request).await;
response.assert_status(StatusCode::OK);
let response: IntrospectionResponse = response.json();
assert!(!response.active); // It shouldn't be active anymore
// But the refresh token should still be valid
let request = Request::post(OAuth2Introspection::PATH)
.basic_auth(&introspecting_client_id, &introspecting_client_secret)
.form(json!({ "token": refresh_token }));
let response = state.request(request).await;
response.assert_status(StatusCode::OK);
let response: IntrospectionResponse = response.json();
assert!(response.active);
}
}