| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267 |
- //! Scope narrowing: narrower is kept, wider is rejected.
- use super::{
- TestError, TestResult, dpop_proof, input, is_invalid_scope, mint_grant, now_unix, service,
- test_redis,
- };
- use crate::protocol::{
- scope::ScopeSet,
- token::{
- ClientKind,
- grants::refresh_token::{RefreshInput, refresh},
- },
- };
- /// Scope narrowing succeeds and the narrowed scope sticks.
- #[tokio::test]
- async fn grant_refresh_scope_narrow_ok() -> TestResult {
- let (_container, mut conn, store, _url) = test_redis().await?;
- let service = service()?;
- let client = super::unique("narrow-client");
- let proof = dpop_proof(&super::unique("narrow-jkt"));
- let minted = mint_grant(
- &service,
- &mut conn,
- &store,
- &client,
- ClientKind::Public,
- "read write",
- proof.clone(),
- )
- .await?;
- let enabled = ScopeSet::parse("read write")?;
- let narrowed = refresh(
- &mut conn,
- &store,
- &service,
- &input(
- &client,
- ClientKind::Public,
- &minted.refresh_token,
- Some("read"),
- &proof,
- &enabled,
- ),
- )
- .await?;
- if narrowed.scope != "read" {
- return Err(TestError::Unexpected(format!(
- "scope not narrowed: {}",
- narrowed.scope
- )));
- }
- Ok(())
- }
- /// Scope widening is rejected with `invalid_scope`.
- #[tokio::test]
- async fn grant_refresh_scope_widen_rejected() -> TestResult {
- let (_container, mut conn, store, _url) = test_redis().await?;
- let service = service()?;
- let client = super::unique("widen-client");
- let proof = dpop_proof(&super::unique("widen-jkt"));
- let minted = mint_grant(
- &service,
- &mut conn,
- &store,
- &client,
- ClientKind::Public,
- "read",
- proof.clone(),
- )
- .await?;
- let enabled = ScopeSet::parse("admin read")?;
- let widened = refresh(
- &mut conn,
- &store,
- &service,
- &input(
- &client,
- ClientKind::Public,
- &minted.refresh_token,
- Some("admin read"),
- &proof,
- &enabled,
- ),
- )
- .await;
- let error = match widened {
- Ok(_) => return Err(TestError::Unexpected("widened scope rotated".to_owned())),
- Err(error) => error,
- };
- if !is_invalid_scope(&error) {
- return Err(TestError::Unexpected(format!(
- "widen code: {}",
- error.code()
- )));
- }
- Ok(())
- }
- /// Narrowing on refresh keeps the new refresh token at the presented scope
- /// (draft-ietf-oauth-v2-1-16 §4.3.3) while the access token narrows
- /// (§4.3.1): the response reports the narrowed access scope, the stored
- /// refresh record keeps the full scope, and the next rotation can still use
- /// the full scope.
- #[tokio::test]
- async fn grant_refresh_narrow_keeps_refresh_scope() -> TestResult {
- let (_container, mut conn, store, _url) = test_redis().await?;
- let service = service()?;
- let client = super::unique("rt-kept-client");
- let proof = dpop_proof(&super::unique("rt-kept-jkt"));
- let minted = mint_grant(
- &service,
- &mut conn,
- &store,
- &client,
- ClientKind::Public,
- "read write",
- proof.clone(),
- )
- .await?;
- let enabled = ScopeSet::parse("read write")?;
- let narrowed = refresh(
- &mut conn,
- &store,
- &service,
- &input(
- &client,
- ClientKind::Public,
- &minted.refresh_token,
- Some("read"),
- &proof,
- &enabled,
- ),
- )
- .await?;
- if narrowed.scope != "read" {
- return Err(TestError::Unexpected(format!(
- "response scope not narrowed: {}",
- narrowed.scope
- )));
- }
- let live_refresh = service
- .find_refresh(&mut conn, &store, &narrowed.refresh_token)
- .await?;
- let refresh_scope = match live_refresh {
- Some(found) => found.record.scope,
- None => {
- return Err(TestError::Unexpected(
- "rotated refresh token not found".to_owned(),
- ));
- }
- };
- if refresh_scope != "read write" {
- return Err(TestError::Unexpected(format!(
- "refresh scope narrowed: {refresh_scope}"
- )));
- }
- let live_access = service
- .find_access(&mut conn, &store, &narrowed.access_token)
- .await?;
- let access_scope = match live_access {
- Some(found) => found.record.scope,
- None => {
- return Err(TestError::Unexpected(
- "rotated access token not found".to_owned(),
- ));
- }
- };
- if access_scope != "read" {
- return Err(TestError::Unexpected(format!(
- "access scope not narrowed: {access_scope}"
- )));
- }
- // The retained refresh scope still authorizes the full grant on the next
- // rotation with an omitted scope.
- let restored = refresh(
- &mut conn,
- &store,
- &service,
- &input(
- &client,
- ClientKind::Public,
- &narrowed.refresh_token,
- None,
- &proof,
- &enabled,
- ),
- )
- .await?;
- if restored.scope != "read write" {
- return Err(TestError::Unexpected(format!(
- "retained scope lost: {}",
- restored.scope
- )));
- }
- Ok(())
- }
- /// A revoked entitlement still drops from the retained refresh scope, while
- /// an explicit narrower request narrows only the access token.
- #[tokio::test]
- async fn grant_refresh_narrow_drops_revoked_from_refresh_scope() -> TestResult {
- let (_container, mut conn, store, _url) = test_redis().await?;
- let service = service()?;
- let client = super::unique("rt-revoked-client");
- let proof = dpop_proof(&super::unique("rt-revoked-jkt"));
- let minted = mint_grant(
- &service,
- &mut conn,
- &store,
- &client,
- ClientKind::Public,
- "read write",
- proof.clone(),
- )
- .await?;
- let enabled = ScopeSet::parse("read write")?;
- let registered = ScopeSet::parse("read write")?;
- // `write` was revoked from the user after issuance.
- let entitled = ScopeSet::parse("read")?;
- let narrowed = refresh(
- &mut conn,
- &store,
- &service,
- &RefreshInput {
- client_id: &client,
- client_kind: ClientKind::Public,
- refresh_token: &minted.refresh_token,
- requested_scope: Some("read"),
- proof: &proof,
- enabled: &enabled,
- client_registered: ®istered,
- user_entitled: &entitled,
- now_unix: now_unix(),
- },
- )
- .await?;
- if narrowed.scope != "read" {
- return Err(TestError::Unexpected(format!(
- "response scope not narrowed: {}",
- narrowed.scope
- )));
- }
- let live_refresh = service
- .find_refresh(&mut conn, &store, &narrowed.refresh_token)
- .await?;
- let refresh_scope = match live_refresh {
- Some(found) => found.record.scope,
- None => {
- return Err(TestError::Unexpected(
- "rotated refresh token not found".to_owned(),
- ));
- }
- };
- if refresh_scope != "read" {
- return Err(TestError::Unexpected(format!(
- "revoked scope survived on refresh token: {refresh_scope}"
- )));
- }
- Ok(())
- }
|