You've already forked authentication-service
mirror of
https://github.com/matrix-org/matrix-authentication-service.git
synced 2025-08-07 17:03:01 +03:00
Implement the device access token request
This commit is contained in:
@@ -280,7 +280,7 @@ pub(crate) async fn complete(
|
|||||||
url_builder,
|
url_builder,
|
||||||
&key_store,
|
&key_store,
|
||||||
client,
|
client,
|
||||||
&grant,
|
Some(&grant),
|
||||||
browser_session,
|
browser_session,
|
||||||
None,
|
None,
|
||||||
Some(&valid_authentication),
|
Some(&valid_authentication),
|
||||||
|
@@ -90,6 +90,7 @@ pub(crate) async fn get(
|
|||||||
GrantType::AuthorizationCode,
|
GrantType::AuthorizationCode,
|
||||||
GrantType::RefreshToken,
|
GrantType::RefreshToken,
|
||||||
GrantType::ClientCredentials,
|
GrantType::ClientCredentials,
|
||||||
|
GrantType::DeviceCode,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
let token_endpoint_auth_methods_supported = client_auth_methods_supported.clone();
|
let token_endpoint_auth_methods_supported = client_auth_methods_supported.clone();
|
||||||
|
@@ -59,7 +59,7 @@ pub(crate) fn generate_id_token(
|
|||||||
url_builder: &UrlBuilder,
|
url_builder: &UrlBuilder,
|
||||||
key_store: &Keystore,
|
key_store: &Keystore,
|
||||||
client: &Client,
|
client: &Client,
|
||||||
grant: &AuthorizationGrant,
|
grant: Option<&AuthorizationGrant>,
|
||||||
browser_session: &BrowserSession,
|
browser_session: &BrowserSession,
|
||||||
access_token: Option<&AccessToken>,
|
access_token: Option<&AccessToken>,
|
||||||
last_authentication: Option<&Authentication>,
|
last_authentication: Option<&Authentication>,
|
||||||
@@ -72,8 +72,8 @@ pub(crate) fn generate_id_token(
|
|||||||
claims::IAT.insert(&mut claims, now)?;
|
claims::IAT.insert(&mut claims, now)?;
|
||||||
claims::EXP.insert(&mut claims, now + Duration::hours(1))?;
|
claims::EXP.insert(&mut claims, now + Duration::hours(1))?;
|
||||||
|
|
||||||
if let Some(ref nonce) = grant.nonce {
|
if let Some(nonce) = grant.and_then(|grant| grant.nonce.as_ref()) {
|
||||||
claims::NONCE.insert(&mut claims, nonce.clone())?;
|
claims::NONCE.insert(&mut claims, nonce)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(last_authentication) = last_authentication {
|
if let Some(last_authentication) = last_authentication {
|
||||||
@@ -92,7 +92,7 @@ pub(crate) fn generate_id_token(
|
|||||||
claims::AT_HASH.insert(&mut claims, hash_token(&alg, &access_token.access_token)?)?;
|
claims::AT_HASH.insert(&mut claims, hash_token(&alg, &access_token.access_token)?)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(ref code) = grant.code {
|
if let Some(code) = grant.and_then(|grant| grant.code.as_ref()) {
|
||||||
claims::C_HASH.insert(&mut claims, hash_token(&alg, &code.code)?)?;
|
claims::C_HASH.insert(&mut claims, hash_token(&alg, &code.code)?)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -21,7 +21,7 @@ use mas_axum_utils::{
|
|||||||
http_client_factory::HttpClientFactory,
|
http_client_factory::HttpClientFactory,
|
||||||
sentry::SentryEventID,
|
sentry::SentryEventID,
|
||||||
};
|
};
|
||||||
use mas_data_model::{AuthorizationGrantStage, Client, Device, TokenType};
|
use mas_data_model::{AuthorizationGrantStage, Client, Device, DeviceCodeGrantState, TokenType};
|
||||||
use mas_keystore::{Encrypter, Keystore};
|
use mas_keystore::{Encrypter, Keystore};
|
||||||
use mas_oidc_client::types::scope::ScopeToken;
|
use mas_oidc_client::types::scope::ScopeToken;
|
||||||
use mas_policy::Policy;
|
use mas_policy::Policy;
|
||||||
@@ -40,7 +40,7 @@ use oauth2_types::{
|
|||||||
pkce::CodeChallengeError,
|
pkce::CodeChallengeError,
|
||||||
requests::{
|
requests::{
|
||||||
AccessTokenRequest, AccessTokenResponse, AuthorizationCodeGrant, ClientCredentialsGrant,
|
AccessTokenRequest, AccessTokenResponse, AuthorizationCodeGrant, ClientCredentialsGrant,
|
||||||
GrantType, RefreshTokenGrant,
|
DeviceCodeGrant, GrantType, RefreshTokenGrant,
|
||||||
},
|
},
|
||||||
scope,
|
scope,
|
||||||
};
|
};
|
||||||
@@ -123,6 +123,18 @@ pub(crate) enum RouteError {
|
|||||||
|
|
||||||
#[error("failed to load oauth session")]
|
#[error("failed to load oauth session")]
|
||||||
NoSuchOAuthSession,
|
NoSuchOAuthSession,
|
||||||
|
|
||||||
|
#[error("device code grant expired")]
|
||||||
|
DeviceCodeExpired,
|
||||||
|
|
||||||
|
#[error("device code grant is still pending")]
|
||||||
|
DeviceCodePending,
|
||||||
|
|
||||||
|
#[error("device code grant was rejected")]
|
||||||
|
DeviceCodeRejected,
|
||||||
|
|
||||||
|
#[error("device code grant was already exchanged")]
|
||||||
|
DeviceCodeExchanged,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IntoResponse for RouteError {
|
impl IntoResponse for RouteError {
|
||||||
@@ -165,7 +177,20 @@ impl IntoResponse for RouteError {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
Self::DeviceCodeRejected => (
|
||||||
|
StatusCode::FORBIDDEN,
|
||||||
|
Json(ClientError::from(ClientErrorCode::AccessDenied)),
|
||||||
|
),
|
||||||
|
Self::DeviceCodeExpired => (
|
||||||
|
StatusCode::FORBIDDEN,
|
||||||
|
Json(ClientError::from(ClientErrorCode::ExpiredToken)),
|
||||||
|
),
|
||||||
|
Self::DeviceCodePending => (
|
||||||
|
StatusCode::FORBIDDEN,
|
||||||
|
Json(ClientError::from(ClientErrorCode::AuthorizationPending)),
|
||||||
|
),
|
||||||
Self::InvalidGrant
|
Self::InvalidGrant
|
||||||
|
| Self::DeviceCodeExchanged
|
||||||
| Self::RefreshTokenNotFound
|
| Self::RefreshTokenNotFound
|
||||||
| Self::RefreshTokenInvalid(_)
|
| Self::RefreshTokenInvalid(_)
|
||||||
| Self::SessionInvalid(_)
|
| Self::SessionInvalid(_)
|
||||||
@@ -265,6 +290,20 @@ pub(crate) async fn post(
|
|||||||
)
|
)
|
||||||
.await?
|
.await?
|
||||||
}
|
}
|
||||||
|
AccessTokenRequest::DeviceCode(grant) => {
|
||||||
|
device_code_grant(
|
||||||
|
&mut rng,
|
||||||
|
&clock,
|
||||||
|
&activity_tracker,
|
||||||
|
&grant,
|
||||||
|
&client,
|
||||||
|
&key_store,
|
||||||
|
&url_builder,
|
||||||
|
&site_config,
|
||||||
|
repo,
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
}
|
||||||
_ => {
|
_ => {
|
||||||
return Err(RouteError::UnsupportedGrantType);
|
return Err(RouteError::UnsupportedGrantType);
|
||||||
}
|
}
|
||||||
@@ -397,7 +436,7 @@ async fn authorization_code_grant(
|
|||||||
url_builder,
|
url_builder,
|
||||||
key_store,
|
key_store,
|
||||||
client,
|
client,
|
||||||
&authz_grant,
|
Some(&authz_grant),
|
||||||
&browser_session,
|
&browser_session,
|
||||||
Some(&access_token),
|
Some(&access_token),
|
||||||
last_authentication.as_ref(),
|
last_authentication.as_ref(),
|
||||||
@@ -575,6 +614,136 @@ async fn client_credentials_grant(
|
|||||||
Ok((params, repo))
|
Ok((params, repo))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn device_code_grant(
|
||||||
|
rng: &mut BoxRng,
|
||||||
|
clock: &impl Clock,
|
||||||
|
activity_tracker: &BoundActivityTracker,
|
||||||
|
grant: &DeviceCodeGrant,
|
||||||
|
client: &Client,
|
||||||
|
key_store: &Keystore,
|
||||||
|
url_builder: &UrlBuilder,
|
||||||
|
site_config: &SiteConfig,
|
||||||
|
mut repo: BoxRepository,
|
||||||
|
) -> Result<(AccessTokenResponse, BoxRepository), RouteError> {
|
||||||
|
// TODO: Check that the client is allowed to use this grant type
|
||||||
|
//if !client.grant_types.contains(&GrantType::DeviceCode) {
|
||||||
|
// return Err(RouteError::UnauthorizedClient);
|
||||||
|
//}
|
||||||
|
|
||||||
|
let grant = repo
|
||||||
|
.oauth2_device_code_grant()
|
||||||
|
.find_by_device_code(&grant.device_code)
|
||||||
|
.await?
|
||||||
|
.ok_or(RouteError::GrantNotFound)?;
|
||||||
|
|
||||||
|
// Check that the client match
|
||||||
|
if client.id != grant.client_id {
|
||||||
|
return Err(RouteError::ClientIDMismatch {
|
||||||
|
expected: grant.client_id,
|
||||||
|
actual: client.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if grant.expires_at < clock.now() {
|
||||||
|
return Err(RouteError::DeviceCodeExpired);
|
||||||
|
}
|
||||||
|
|
||||||
|
let browser_session_id = match &grant.state {
|
||||||
|
DeviceCodeGrantState::Pending => {
|
||||||
|
return Err(RouteError::DeviceCodePending);
|
||||||
|
}
|
||||||
|
DeviceCodeGrantState::Rejected { .. } => {
|
||||||
|
return Err(RouteError::DeviceCodeRejected);
|
||||||
|
}
|
||||||
|
DeviceCodeGrantState::Exchanged { .. } => {
|
||||||
|
return Err(RouteError::DeviceCodeExchanged);
|
||||||
|
}
|
||||||
|
DeviceCodeGrantState::Fulfilled {
|
||||||
|
browser_session_id, ..
|
||||||
|
} => browser_session_id,
|
||||||
|
};
|
||||||
|
|
||||||
|
let browser_session = repo
|
||||||
|
.browser_session()
|
||||||
|
.lookup(*browser_session_id)
|
||||||
|
.await?
|
||||||
|
.ok_or(RouteError::NoSuchBrowserSession)?;
|
||||||
|
|
||||||
|
// Start the session
|
||||||
|
let session = repo
|
||||||
|
.oauth2_session()
|
||||||
|
.add_from_browser_session(rng, clock, client, &browser_session, grant.scope)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let ttl = site_config.access_token_ttl;
|
||||||
|
let access_token_str = TokenType::AccessToken.generate(rng);
|
||||||
|
|
||||||
|
let access_token = repo
|
||||||
|
.oauth2_access_token()
|
||||||
|
.add(rng, clock, &session, access_token_str, Some(ttl))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let mut params =
|
||||||
|
AccessTokenResponse::new(access_token.access_token.clone()).with_expires_in(ttl);
|
||||||
|
|
||||||
|
// If the client uses the refresh token grant type, we also generate a refresh token
|
||||||
|
if client.grant_types.contains(&GrantType::RefreshToken) {
|
||||||
|
let refresh_token_str = TokenType::RefreshToken.generate(rng);
|
||||||
|
|
||||||
|
let refresh_token = repo
|
||||||
|
.oauth2_refresh_token()
|
||||||
|
.add(rng, clock, &session, &access_token, refresh_token_str)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
params = params.with_refresh_token(refresh_token.refresh_token);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the client asked for an ID token, we generate one
|
||||||
|
if session.scope.contains(&scope::OPENID) {
|
||||||
|
let id_token = generate_id_token(
|
||||||
|
rng,
|
||||||
|
clock,
|
||||||
|
url_builder,
|
||||||
|
key_store,
|
||||||
|
client,
|
||||||
|
None,
|
||||||
|
&browser_session,
|
||||||
|
Some(&access_token),
|
||||||
|
None,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
params = params.with_id_token(id_token);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look for device to provision
|
||||||
|
for scope in &*session.scope {
|
||||||
|
if let Some(device) = Device::from_scope_token(scope) {
|
||||||
|
// Note that we're not waiting for the job to finish, we just schedule it. We
|
||||||
|
// might get in a situation where the provisioning job is not finished when the
|
||||||
|
// client does its first request to the Homeserver. This is fine for now, since
|
||||||
|
// Synapse still provision devices on-the-fly if it doesn't find them in the
|
||||||
|
// database.
|
||||||
|
repo.job()
|
||||||
|
.schedule_job(ProvisionDeviceJob::new(&browser_session.user, &device))
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// XXX: there is a potential (but unlikely) race here, where the activity for
|
||||||
|
// the session is recorded before the transaction is committed. We would have to
|
||||||
|
// save the repository here to fix that.
|
||||||
|
activity_tracker
|
||||||
|
.record_oauth2_session(clock, &session)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
if !session.scope.is_empty() {
|
||||||
|
// We only return the scope if it's not empty
|
||||||
|
params = params.with_scope(session.scope);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok((params, repo))
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use hyper::Request;
|
use hyper::Request;
|
||||||
@@ -582,7 +751,7 @@ mod tests {
|
|||||||
use mas_router::SimpleRoute;
|
use mas_router::SimpleRoute;
|
||||||
use oauth2_types::{
|
use oauth2_types::{
|
||||||
registration::ClientRegistrationResponse,
|
registration::ClientRegistrationResponse,
|
||||||
requests::ResponseMode,
|
requests::{DeviceAuthorizationResponse, ResponseMode},
|
||||||
scope::{Scope, OPENID},
|
scope::{Scope, OPENID},
|
||||||
};
|
};
|
||||||
use sqlx::PgPool;
|
use sqlx::PgPool;
|
||||||
@@ -1048,6 +1217,191 @@ mod tests {
|
|||||||
response.assert_status(StatusCode::OK);
|
response.assert_status(StatusCode::OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")]
|
||||||
|
async fn test_device_code_grant(pool: PgPool) {
|
||||||
|
init_tracing();
|
||||||
|
let state = TestState::from_pool(pool).await.unwrap();
|
||||||
|
|
||||||
|
// Provision a client
|
||||||
|
let request =
|
||||||
|
Request::post(mas_router::OAuth2RegistrationEndpoint::PATH).json(serde_json::json!({
|
||||||
|
"client_uri": "https://example.com/",
|
||||||
|
"contacts": ["contact@example.com"],
|
||||||
|
"token_endpoint_auth_method": "none",
|
||||||
|
"grant_types": ["urn:ietf:params:oauth:grant-type:device_code", "refresh_token"],
|
||||||
|
"response_types": [],
|
||||||
|
}));
|
||||||
|
|
||||||
|
let response = state.request(request).await;
|
||||||
|
response.assert_status(StatusCode::CREATED);
|
||||||
|
|
||||||
|
let response: ClientRegistrationResponse = response.json();
|
||||||
|
let client_id = response.client_id;
|
||||||
|
|
||||||
|
// Start a device code grant
|
||||||
|
let request = Request::post(mas_router::OAuth2DeviceAuthorizationEndpoint::PATH).form(
|
||||||
|
serde_json::json!({
|
||||||
|
"client_id": client_id,
|
||||||
|
"scope": "openid",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
let response = state.request(request).await;
|
||||||
|
response.assert_status(StatusCode::OK);
|
||||||
|
|
||||||
|
let device_grant: DeviceAuthorizationResponse = response.json();
|
||||||
|
|
||||||
|
// Poll the token endpoint, it should be pending
|
||||||
|
let request =
|
||||||
|
Request::post(mas_router::OAuth2TokenEndpoint::PATH).form(serde_json::json!({
|
||||||
|
"grant_type": "urn:ietf:params:oauth:grant-type:device_code",
|
||||||
|
"device_code": device_grant.device_code,
|
||||||
|
"client_id": client_id,
|
||||||
|
}));
|
||||||
|
let response = state.request(request).await;
|
||||||
|
response.assert_status(StatusCode::FORBIDDEN);
|
||||||
|
|
||||||
|
let ClientError { error, .. } = response.json();
|
||||||
|
assert_eq!(error, ClientErrorCode::AuthorizationPending);
|
||||||
|
|
||||||
|
// Let's provision a user and create a browser session for them. This part is hard to
|
||||||
|
// test with just HTTP requests, so we'll use the repository directly.
|
||||||
|
let mut repo = state.repository().await.unwrap();
|
||||||
|
|
||||||
|
let user = repo
|
||||||
|
.user()
|
||||||
|
.add(&mut state.rng(), &state.clock, "alice".to_owned())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let browser_session = repo
|
||||||
|
.browser_session()
|
||||||
|
.add(&mut state.rng(), &state.clock, &user, None)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Find the grant
|
||||||
|
let grant = repo
|
||||||
|
.oauth2_device_code_grant()
|
||||||
|
.find_by_user_code(&device_grant.user_code)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// And fulfill it
|
||||||
|
let grant = repo
|
||||||
|
.oauth2_device_code_grant()
|
||||||
|
.fulfill(&state.clock, grant, &browser_session)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
repo.save().await.unwrap();
|
||||||
|
|
||||||
|
// Now call the token endpoint to get an access token.
|
||||||
|
let request =
|
||||||
|
Request::post(mas_router::OAuth2TokenEndpoint::PATH).form(serde_json::json!({
|
||||||
|
"grant_type": "urn:ietf:params:oauth:grant-type:device_code",
|
||||||
|
"device_code": grant.device_code,
|
||||||
|
"client_id": client_id,
|
||||||
|
}));
|
||||||
|
|
||||||
|
let response = state.request(request).await;
|
||||||
|
response.assert_status(StatusCode::OK);
|
||||||
|
|
||||||
|
let response: AccessTokenResponse = response.json();
|
||||||
|
|
||||||
|
// Check that the token is valid
|
||||||
|
assert!(state.is_access_token_valid(&response.access_token).await);
|
||||||
|
// We advertised the refresh token grant type, so we should have a refresh token
|
||||||
|
assert!(response.refresh_token.is_some());
|
||||||
|
// We asked for the openid scope, so we should have an ID token
|
||||||
|
assert!(response.id_token.is_some());
|
||||||
|
|
||||||
|
// Do another grant and make it expire
|
||||||
|
let request = Request::post(mas_router::OAuth2DeviceAuthorizationEndpoint::PATH).form(
|
||||||
|
serde_json::json!({
|
||||||
|
"client_id": client_id,
|
||||||
|
"scope": "openid",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
let response = state.request(request).await;
|
||||||
|
response.assert_status(StatusCode::OK);
|
||||||
|
|
||||||
|
let device_grant: DeviceAuthorizationResponse = response.json();
|
||||||
|
|
||||||
|
// Poll the token endpoint, it should be pending
|
||||||
|
let request =
|
||||||
|
Request::post(mas_router::OAuth2TokenEndpoint::PATH).form(serde_json::json!({
|
||||||
|
"grant_type": "urn:ietf:params:oauth:grant-type:device_code",
|
||||||
|
"device_code": device_grant.device_code,
|
||||||
|
"client_id": client_id,
|
||||||
|
}));
|
||||||
|
let response = state.request(request).await;
|
||||||
|
response.assert_status(StatusCode::FORBIDDEN);
|
||||||
|
|
||||||
|
let ClientError { error, .. } = response.json();
|
||||||
|
assert_eq!(error, ClientErrorCode::AuthorizationPending);
|
||||||
|
|
||||||
|
state.clock.advance(Duration::hours(1));
|
||||||
|
|
||||||
|
// Poll again, it should be expired
|
||||||
|
let request =
|
||||||
|
Request::post(mas_router::OAuth2TokenEndpoint::PATH).form(serde_json::json!({
|
||||||
|
"grant_type": "urn:ietf:params:oauth:grant-type:device_code",
|
||||||
|
"device_code": device_grant.device_code,
|
||||||
|
"client_id": client_id,
|
||||||
|
}));
|
||||||
|
let response = state.request(request).await;
|
||||||
|
response.assert_status(StatusCode::FORBIDDEN);
|
||||||
|
|
||||||
|
let ClientError { error, .. } = response.json();
|
||||||
|
assert_eq!(error, ClientErrorCode::ExpiredToken);
|
||||||
|
|
||||||
|
// Do another grant and reject it
|
||||||
|
let request = Request::post(mas_router::OAuth2DeviceAuthorizationEndpoint::PATH).form(
|
||||||
|
serde_json::json!({
|
||||||
|
"client_id": client_id,
|
||||||
|
"scope": "openid",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
let response = state.request(request).await;
|
||||||
|
response.assert_status(StatusCode::OK);
|
||||||
|
|
||||||
|
let device_grant: DeviceAuthorizationResponse = response.json();
|
||||||
|
|
||||||
|
// Find the grant and reject it
|
||||||
|
let mut repo = state.repository().await.unwrap();
|
||||||
|
|
||||||
|
// Find the grant
|
||||||
|
let grant = repo
|
||||||
|
.oauth2_device_code_grant()
|
||||||
|
.find_by_user_code(&device_grant.user_code)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// And reject it
|
||||||
|
let grant = repo
|
||||||
|
.oauth2_device_code_grant()
|
||||||
|
.reject(&state.clock, grant, &browser_session)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
repo.save().await.unwrap();
|
||||||
|
|
||||||
|
// Poll the token endpoint, it should be rejected
|
||||||
|
let request =
|
||||||
|
Request::post(mas_router::OAuth2TokenEndpoint::PATH).form(serde_json::json!({
|
||||||
|
"grant_type": "urn:ietf:params:oauth:grant-type:device_code",
|
||||||
|
"device_code": grant.device_code,
|
||||||
|
"client_id": client_id,
|
||||||
|
}));
|
||||||
|
let response = state.request(request).await;
|
||||||
|
response.assert_status(StatusCode::FORBIDDEN);
|
||||||
|
|
||||||
|
let ClientError { error, .. } = response.json();
|
||||||
|
assert_eq!(error, ClientErrorCode::AccessDenied);
|
||||||
|
}
|
||||||
|
|
||||||
#[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")]
|
#[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")]
|
||||||
async fn test_unsupported_grant(pool: PgPool) {
|
async fn test_unsupported_grant(pool: PgPool) {
|
||||||
init_tracing();
|
init_tracing();
|
||||||
|
@@ -500,7 +500,7 @@ pub struct ClientCredentialsGrant {
|
|||||||
#[derive(Serialize, Deserialize, Clone, PartialEq, Eq)]
|
#[derive(Serialize, Deserialize, Clone, PartialEq, Eq)]
|
||||||
pub struct DeviceCodeGrant {
|
pub struct DeviceCodeGrant {
|
||||||
/// The device verification code, from the device authorization response.
|
/// The device verification code, from the device authorization response.
|
||||||
pub device_code: Option<Scope>,
|
pub device_code: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Debug for DeviceCodeGrant {
|
impl fmt::Debug for DeviceCodeGrant {
|
||||||
@@ -559,6 +559,7 @@ pub enum GrantType {
|
|||||||
/// [Token Endpoint]: https://www.rfc-editor.org/rfc/rfc6749#section-3.2
|
/// [Token Endpoint]: https://www.rfc-editor.org/rfc/rfc6749#section-3.2
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||||
#[serde(tag = "grant_type", rename_all = "snake_case")]
|
#[serde(tag = "grant_type", rename_all = "snake_case")]
|
||||||
|
#[non_exhaustive]
|
||||||
pub enum AccessTokenRequest {
|
pub enum AccessTokenRequest {
|
||||||
/// A request in the Authorization Code flow.
|
/// A request in the Authorization Code flow.
|
||||||
AuthorizationCode(AuthorizationCodeGrant),
|
AuthorizationCode(AuthorizationCodeGrant),
|
||||||
|
Reference in New Issue
Block a user