use crate::error::{Error, Result};
use crate::multitenancy::{Permission, TenantId};
use std::collections::HashMap;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
#[derive(Debug, Clone)]
pub struct OAuthConfig {
pub provider_url: String,
pub client_id: String,
pub client_secret: String,
pub redirect_uri: Option<String>,
pub scopes: Vec<String>,
pub token_endpoint: String,
pub authorization_endpoint: String,
pub introspection_endpoint: Option<String>,
pub revocation_endpoint: Option<String>,
pub token_expiry: Duration,
}
impl OAuthConfig {
pub fn new(
provider_url: impl Into<String>,
client_id: impl Into<String>,
client_secret: impl Into<String>,
) -> Self {
let provider = provider_url.into();
OAuthConfig {
provider_url: provider.clone(),
client_id: client_id.into(),
client_secret: client_secret.into(),
redirect_uri: None,
scopes: vec!["openid".to_string(), "profile".to_string()],
token_endpoint: format!("{}/oauth/token", provider),
authorization_endpoint: format!("{}/oauth/authorize", provider),
introspection_endpoint: Some(format!("{}/oauth/introspect", provider)),
revocation_endpoint: Some(format!("{}/oauth/revoke", provider)),
token_expiry: Duration::from_secs(3600),
}
}
pub fn with_redirect_uri(mut self, uri: impl Into<String>) -> Self {
self.redirect_uri = Some(uri.into());
self
}
pub fn with_scopes(mut self, scopes: Vec<String>) -> Self {
self.scopes = scopes;
self
}
pub fn with_scope(mut self, scope: impl Into<String>) -> Self {
self.scopes.push(scope.into());
self
}
pub fn with_token_endpoint(mut self, endpoint: impl Into<String>) -> Self {
self.token_endpoint = endpoint.into();
self
}
pub fn with_authorization_endpoint(mut self, endpoint: impl Into<String>) -> Self {
self.authorization_endpoint = endpoint.into();
self
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum OAuthGrantType {
AuthorizationCode,
ClientCredentials,
RefreshToken,
Password,
}
impl OAuthGrantType {
pub fn as_str(&self) -> &str {
match self {
OAuthGrantType::AuthorizationCode => "authorization_code",
OAuthGrantType::ClientCredentials => "client_credentials",
OAuthGrantType::RefreshToken => "refresh_token",
OAuthGrantType::Password => "password",
}
}
}
#[derive(Debug, Clone)]
pub struct AuthorizationRequest {
pub client_id: String,
pub redirect_uri: String,
pub response_type: String,
pub scopes: Vec<String>,
pub state: String,
pub code_challenge: Option<String>,
pub code_challenge_method: Option<String>,
pub extra_params: HashMap<String, String>,
}
impl AuthorizationRequest {
pub fn new(client_id: impl Into<String>, redirect_uri: impl Into<String>) -> Self {
AuthorizationRequest {
client_id: client_id.into(),
redirect_uri: redirect_uri.into(),
response_type: "code".to_string(),
scopes: vec!["openid".to_string()],
state: generate_state(),
code_challenge: None,
code_challenge_method: None,
extra_params: HashMap::new(),
}
}
pub fn with_scopes(mut self, scopes: Vec<String>) -> Self {
self.scopes = scopes;
self
}
pub fn with_pkce(mut self) -> (Self, String) {
let (verifier, challenge) = generate_pkce_pair();
self.code_challenge = Some(challenge);
self.code_challenge_method = Some("S256".to_string());
(self, verifier)
}
pub fn build_url(&self, base_url: &str) -> String {
let mut params = vec![
("client_id", self.client_id.as_str()),
("redirect_uri", self.redirect_uri.as_str()),
("response_type", self.response_type.as_str()),
("state", self.state.as_str()),
];
let scopes = self.scopes.join(" ");
params.push(("scope", &scopes));
if let Some(ref challenge) = self.code_challenge {
params.push(("code_challenge", challenge));
}
if let Some(ref method) = self.code_challenge_method {
params.push(("code_challenge_method", method));
}
let query: Vec<String> = params
.iter()
.map(|(k, v)| format!("{}={}", k, url_encode(v)))
.collect();
format!("{}?{}", base_url, query.join("&"))
}
}
#[derive(Debug, Clone)]
pub struct TokenRequest {
pub grant_type: OAuthGrantType,
pub client_id: String,
pub client_secret: String,
pub code: Option<String>,
pub redirect_uri: Option<String>,
pub code_verifier: Option<String>,
pub refresh_token: Option<String>,
pub username: Option<String>,
pub password: Option<String>,
pub scopes: Option<Vec<String>>,
}
impl TokenRequest {
pub fn client_credentials(
client_id: impl Into<String>,
client_secret: impl Into<String>,
) -> Self {
TokenRequest {
grant_type: OAuthGrantType::ClientCredentials,
client_id: client_id.into(),
client_secret: client_secret.into(),
code: None,
redirect_uri: None,
code_verifier: None,
refresh_token: None,
username: None,
password: None,
scopes: None,
}
}
pub fn authorization_code(
client_id: impl Into<String>,
client_secret: impl Into<String>,
code: impl Into<String>,
redirect_uri: impl Into<String>,
) -> Self {
TokenRequest {
grant_type: OAuthGrantType::AuthorizationCode,
client_id: client_id.into(),
client_secret: client_secret.into(),
code: Some(code.into()),
redirect_uri: Some(redirect_uri.into()),
code_verifier: None,
refresh_token: None,
username: None,
password: None,
scopes: None,
}
}
pub fn refresh_token(
client_id: impl Into<String>,
client_secret: impl Into<String>,
refresh_token: impl Into<String>,
) -> Self {
TokenRequest {
grant_type: OAuthGrantType::RefreshToken,
client_id: client_id.into(),
client_secret: client_secret.into(),
code: None,
redirect_uri: None,
code_verifier: None,
refresh_token: Some(refresh_token.into()),
username: None,
password: None,
scopes: None,
}
}
pub fn with_code_verifier(mut self, verifier: impl Into<String>) -> Self {
self.code_verifier = Some(verifier.into());
self
}
pub fn with_scopes(mut self, scopes: Vec<String>) -> Self {
self.scopes = Some(scopes);
self
}
pub fn build_body(&self) -> HashMap<String, String> {
let mut body = HashMap::new();
body.insert(
"grant_type".to_string(),
self.grant_type.as_str().to_string(),
);
body.insert("client_id".to_string(), self.client_id.clone());
body.insert("client_secret".to_string(), self.client_secret.clone());
if let Some(ref code) = self.code {
body.insert("code".to_string(), code.clone());
}
if let Some(ref redirect_uri) = self.redirect_uri {
body.insert("redirect_uri".to_string(), redirect_uri.clone());
}
if let Some(ref verifier) = self.code_verifier {
body.insert("code_verifier".to_string(), verifier.clone());
}
if let Some(ref refresh_token) = self.refresh_token {
body.insert("refresh_token".to_string(), refresh_token.clone());
}
if let Some(ref scopes) = self.scopes {
body.insert("scope".to_string(), scopes.join(" "));
}
body
}
}
#[derive(Debug, Clone)]
pub struct TokenResponse {
pub access_token: String,
pub token_type: String,
pub expires_in: u64,
pub refresh_token: Option<String>,
pub scope: Option<String>,
pub id_token: Option<String>,
}
impl TokenResponse {
pub fn new(access_token: impl Into<String>, expires_in: u64) -> Self {
TokenResponse {
access_token: access_token.into(),
token_type: "Bearer".to_string(),
expires_in,
refresh_token: None,
scope: None,
id_token: None,
}
}
pub fn with_refresh_token(mut self, token: impl Into<String>) -> Self {
self.refresh_token = Some(token.into());
self
}
pub fn with_scope(mut self, scope: impl Into<String>) -> Self {
self.scope = Some(scope.into());
self
}
pub fn with_id_token(mut self, token: impl Into<String>) -> Self {
self.id_token = Some(token.into());
self
}
pub fn is_expired(&self, issued_at: SystemTime) -> bool {
issued_at + Duration::from_secs(self.expires_in) < SystemTime::now()
}
pub fn from_json(json: &str) -> Result<Self> {
let value: serde_json::Value = serde_json::from_str(json)
.map_err(|e| Error::InvalidInput(format!("Invalid JSON: {}", e)))?;
let access_token = value
.get("access_token")
.and_then(|v| v.as_str())
.ok_or_else(|| Error::InvalidInput("Missing access_token".to_string()))?
.to_string();
let token_type = value
.get("token_type")
.and_then(|v| v.as_str())
.unwrap_or("Bearer")
.to_string();
let expires_in = value
.get("expires_in")
.and_then(|v| v.as_u64())
.unwrap_or(3600);
let refresh_token = value
.get("refresh_token")
.and_then(|v| v.as_str())
.map(String::from);
let scope = value
.get("scope")
.and_then(|v| v.as_str())
.map(String::from);
let id_token = value
.get("id_token")
.and_then(|v| v.as_str())
.map(String::from);
Ok(TokenResponse {
access_token,
token_type,
expires_in,
refresh_token,
scope,
id_token,
})
}
}
#[derive(Debug, Clone)]
pub struct IntrospectionResponse {
pub active: bool,
pub scope: Option<String>,
pub client_id: Option<String>,
pub username: Option<String>,
pub token_type: Option<String>,
pub exp: Option<u64>,
pub iat: Option<u64>,
pub nbf: Option<u64>,
pub sub: Option<String>,
pub aud: Option<String>,
pub iss: Option<String>,
pub jti: Option<String>,
}
impl IntrospectionResponse {
pub fn from_json(json: &str) -> Result<Self> {
let value: serde_json::Value = serde_json::from_str(json)
.map_err(|e| Error::InvalidInput(format!("Invalid JSON: {}", e)))?;
let active = value
.get("active")
.and_then(|v| v.as_bool())
.unwrap_or(false);
Ok(IntrospectionResponse {
active,
scope: value
.get("scope")
.and_then(|v| v.as_str())
.map(String::from),
client_id: value
.get("client_id")
.and_then(|v| v.as_str())
.map(String::from),
username: value
.get("username")
.and_then(|v| v.as_str())
.map(String::from),
token_type: value
.get("token_type")
.and_then(|v| v.as_str())
.map(String::from),
exp: value.get("exp").and_then(|v| v.as_u64()),
iat: value.get("iat").and_then(|v| v.as_u64()),
nbf: value.get("nbf").and_then(|v| v.as_u64()),
sub: value.get("sub").and_then(|v| v.as_str()).map(String::from),
aud: value.get("aud").and_then(|v| v.as_str()).map(String::from),
iss: value.get("iss").and_then(|v| v.as_str()).map(String::from),
jti: value.get("jti").and_then(|v| v.as_str()).map(String::from),
})
}
}
#[derive(Debug)]
pub struct OAuthClient {
config: OAuthConfig,
clients: HashMap<String, OAuthClientInfo>,
auth_codes: HashMap<String, AuthCode>,
tokens: HashMap<String, OAuthToken>,
}
#[derive(Debug, Clone)]
pub struct OAuthClientInfo {
pub client_id: String,
pub client_secret_hash: String,
pub redirect_uris: Vec<String>,
pub grant_types: Vec<OAuthGrantType>,
pub scopes: Vec<String>,
pub tenant_id: TenantId,
}
#[derive(Debug, Clone)]
struct AuthCode {
code: String,
client_id: String,
redirect_uri: String,
scopes: Vec<String>,
user_id: String,
code_challenge: Option<String>,
code_challenge_method: Option<String>,
expires_at: SystemTime,
}
#[derive(Debug, Clone)]
struct OAuthToken {
access_token: String,
refresh_token: Option<String>,
client_id: String,
user_id: Option<String>,
scopes: Vec<String>,
expires_at: SystemTime,
revoked: bool,
}
impl OAuthClient {
pub fn new(config: OAuthConfig) -> Self {
OAuthClient {
config,
clients: HashMap::new(),
auth_codes: HashMap::new(),
tokens: HashMap::new(),
}
}
pub fn register_client(&mut self, info: OAuthClientInfo) {
self.clients.insert(info.client_id.clone(), info);
}
pub fn validate_client(
&self,
client_id: &str,
client_secret: &str,
) -> Result<&OAuthClientInfo> {
let client = self
.clients
.get(client_id)
.ok_or_else(|| Error::InvalidInput("Invalid client_id".to_string()))?;
let secret_hash = hash_client_secret(client_secret);
if client.client_secret_hash != secret_hash {
return Err(Error::InvalidInput("Invalid client_secret".to_string()));
}
Ok(client)
}
pub fn create_authorization_code(
&mut self,
client_id: &str,
redirect_uri: &str,
scopes: Vec<String>,
user_id: &str,
code_challenge: Option<String>,
code_challenge_method: Option<String>,
) -> Result<String> {
let client = self
.clients
.get(client_id)
.ok_or_else(|| Error::InvalidInput("Invalid client_id".to_string()))?;
if !client.redirect_uris.contains(&redirect_uri.to_string()) {
return Err(Error::InvalidInput("Invalid redirect_uri".to_string()));
}
for scope in &scopes {
if !client.scopes.contains(scope) {
return Err(Error::InvalidInput(format!("Invalid scope: {}", scope)));
}
}
let code = generate_authorization_code();
let auth_code = AuthCode {
code: code.clone(),
client_id: client_id.to_string(),
redirect_uri: redirect_uri.to_string(),
scopes,
user_id: user_id.to_string(),
code_challenge,
code_challenge_method,
expires_at: SystemTime::now() + Duration::from_secs(600), };
self.auth_codes.insert(code.clone(), auth_code);
Ok(code)
}
pub fn exchange_code(
&mut self,
client_id: &str,
client_secret: &str,
code: &str,
redirect_uri: &str,
code_verifier: Option<&str>,
) -> Result<TokenResponse> {
self.validate_client(client_id, client_secret)?;
let auth_code = self
.auth_codes
.remove(code)
.ok_or_else(|| Error::InvalidInput("Invalid authorization code".to_string()))?;
if auth_code.expires_at < SystemTime::now() {
return Err(Error::InvalidOperation(
"Authorization code expired".to_string(),
));
}
if auth_code.client_id != client_id {
return Err(Error::InvalidInput("Client ID mismatch".to_string()));
}
if auth_code.redirect_uri != redirect_uri {
return Err(Error::InvalidInput("Redirect URI mismatch".to_string()));
}
if let Some(challenge) = &auth_code.code_challenge {
let verifier = code_verifier
.ok_or_else(|| Error::InvalidInput("Missing code_verifier".to_string()))?;
let expected_challenge = compute_code_challenge(verifier);
if &expected_challenge != challenge {
return Err(Error::InvalidInput("Invalid code_verifier".to_string()));
}
}
let access_token = generate_access_token();
let refresh_token = generate_refresh_token();
let token = OAuthToken {
access_token: access_token.clone(),
refresh_token: Some(refresh_token.clone()),
client_id: client_id.to_string(),
user_id: Some(auth_code.user_id),
scopes: auth_code.scopes.clone(),
expires_at: SystemTime::now() + self.config.token_expiry,
revoked: false,
};
self.tokens.insert(access_token.clone(), token);
Ok(
TokenResponse::new(access_token, self.config.token_expiry.as_secs())
.with_refresh_token(refresh_token)
.with_scope(auth_code.scopes.join(" ")),
)
}
pub fn client_credentials_grant(
&mut self,
client_id: &str,
client_secret: &str,
scopes: Option<Vec<String>>,
) -> Result<TokenResponse> {
let client = self.validate_client(client_id, client_secret)?;
if !client
.grant_types
.contains(&OAuthGrantType::ClientCredentials)
{
return Err(Error::InvalidOperation(
"Client credentials grant not allowed".to_string(),
));
}
let requested_scopes = scopes.unwrap_or_else(|| client.scopes.clone());
for scope in &requested_scopes {
if !client.scopes.contains(scope) {
return Err(Error::InvalidInput(format!("Invalid scope: {}", scope)));
}
}
let access_token = generate_access_token();
let token = OAuthToken {
access_token: access_token.clone(),
refresh_token: None, client_id: client_id.to_string(),
user_id: None,
scopes: requested_scopes.clone(),
expires_at: SystemTime::now() + self.config.token_expiry,
revoked: false,
};
self.tokens.insert(access_token.clone(), token);
Ok(
TokenResponse::new(access_token, self.config.token_expiry.as_secs())
.with_scope(requested_scopes.join(" ")),
)
}
pub fn introspect_token(&self, token: &str) -> IntrospectionResponse {
match self.tokens.get(token) {
Some(oauth_token)
if !oauth_token.revoked && oauth_token.expires_at > SystemTime::now() =>
{
IntrospectionResponse {
active: true,
scope: Some(oauth_token.scopes.join(" ")),
client_id: Some(oauth_token.client_id.clone()),
username: oauth_token.user_id.clone(),
token_type: Some("Bearer".to_string()),
exp: Some(
oauth_token
.expires_at
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs(),
),
iat: None,
nbf: None,
sub: oauth_token.user_id.clone(),
aud: None,
iss: None,
jti: None,
}
}
_ => IntrospectionResponse {
active: false,
scope: None,
client_id: None,
username: None,
token_type: None,
exp: None,
iat: None,
nbf: None,
sub: None,
aud: None,
iss: None,
jti: None,
},
}
}
pub fn revoke_token(&mut self, token: &str) -> bool {
if let Some(oauth_token) = self.tokens.get_mut(token) {
oauth_token.revoked = true;
true
} else {
false
}
}
}
fn generate_state() -> String {
use rand::Rng;
let mut bytes = [0u8; 16];
rand::rng().fill_bytes(&mut bytes);
bytes.iter().map(|b| format!("{:02x}", b)).collect()
}
fn generate_pkce_pair() -> (String, String) {
use rand::Rng;
let mut bytes = [0u8; 32];
rand::rng().fill_bytes(&mut bytes);
let verifier = bytes
.iter()
.map(|b| format!("{:02x}", b))
.collect::<String>();
let challenge = compute_code_challenge(&verifier);
(verifier, challenge)
}
fn compute_code_challenge(verifier: &str) -> String {
use sha2::{Digest, Sha256};
let mut hasher = Sha256::new();
hasher.update(verifier.as_bytes());
let hash = hasher.finalize();
const ALPHABET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
let mut result = String::with_capacity(43);
for chunk in hash.chunks(3) {
let b0 = chunk[0] as usize;
let b1 = chunk.get(1).copied().unwrap_or(0) as usize;
let b2 = chunk.get(2).copied().unwrap_or(0) as usize;
result.push(ALPHABET[b0 >> 2] as char);
result.push(ALPHABET[((b0 & 0x03) << 4) | (b1 >> 4)] as char);
if chunk.len() > 1 {
result.push(ALPHABET[((b1 & 0x0f) << 2) | (b2 >> 6)] as char);
}
if chunk.len() > 2 {
result.push(ALPHABET[b2 & 0x3f] as char);
}
}
result
}
fn generate_authorization_code() -> String {
use rand::Rng;
let mut bytes = [0u8; 32];
rand::rng().fill_bytes(&mut bytes);
bytes.iter().map(|b| format!("{:02x}", b)).collect()
}
fn generate_access_token() -> String {
use rand::Rng;
let mut bytes = [0u8; 32];
rand::rng().fill_bytes(&mut bytes);
format!(
"at_{}",
bytes
.iter()
.map(|b| format!("{:02x}", b))
.collect::<String>()
)
}
fn generate_refresh_token() -> String {
use rand::Rng;
let mut bytes = [0u8; 32];
rand::rng().fill_bytes(&mut bytes);
format!(
"rt_{}",
bytes
.iter()
.map(|b| format!("{:02x}", b))
.collect::<String>()
)
}
fn hash_client_secret(secret: &str) -> String {
use sha2::{Digest, Sha256};
let mut hasher = Sha256::new();
hasher.update(secret.as_bytes());
let result = hasher.finalize();
result.iter().map(|b| format!("{:02x}", b)).collect()
}
fn url_encode(s: &str) -> String {
s.chars()
.map(|c| match c {
'A'..='Z' | 'a'..='z' | '0'..='9' | '-' | '_' | '.' | '~' => c.to_string(),
_ => format!("%{:02X}", c as u8),
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_oauth_config() {
let config = OAuthConfig::new("https://auth.example.com", "client_id", "client_secret")
.with_redirect_uri("https://app.example.com/callback")
.with_scope("email");
assert_eq!(config.provider_url, "https://auth.example.com");
assert!(config.scopes.contains(&"email".to_string()));
}
#[test]
fn test_authorization_request() {
let request = AuthorizationRequest::new("client_id", "https://app.example.com/callback")
.with_scopes(vec!["openid".to_string(), "profile".to_string()]);
let url = request.build_url("https://auth.example.com/authorize");
assert!(url.contains("client_id=client_id"));
assert!(url.contains("redirect_uri="));
assert!(url.contains("response_type=code"));
}
#[test]
fn test_pkce() {
let (verifier, challenge) = generate_pkce_pair();
assert!(!verifier.is_empty());
assert!(!challenge.is_empty());
let computed_challenge = compute_code_challenge(&verifier);
assert_eq!(challenge, computed_challenge);
}
#[test]
fn test_token_request() {
let request = TokenRequest::client_credentials("client_id", "client_secret")
.with_scopes(vec!["read".to_string()]);
let body = request.build_body();
assert_eq!(
body.get("grant_type"),
Some(&"client_credentials".to_string())
);
assert_eq!(body.get("client_id"), Some(&"client_id".to_string()));
}
#[test]
fn test_token_response_parsing() {
let json = r#"{"access_token":"abc123","token_type":"Bearer","expires_in":3600,"refresh_token":"xyz789"}"#;
let response = TokenResponse::from_json(json).expect("operation should succeed");
assert_eq!(response.access_token, "abc123");
assert_eq!(response.token_type, "Bearer");
assert_eq!(response.expires_in, 3600);
assert_eq!(response.refresh_token, Some("xyz789".to_string()));
}
#[test]
fn test_oauth_client_credentials_flow() {
let config = OAuthConfig::new("https://auth.example.com", "test", "test");
let mut client = OAuthClient::new(config);
let client_info = OAuthClientInfo {
client_id: "test_client".to_string(),
client_secret_hash: hash_client_secret("test_secret"),
redirect_uris: vec!["https://app.example.com/callback".to_string()],
grant_types: vec![
OAuthGrantType::ClientCredentials,
OAuthGrantType::AuthorizationCode,
],
scopes: vec!["read".to_string(), "write".to_string()],
tenant_id: "tenant_a".to_string(),
};
client.register_client(client_info);
let response = client
.client_credentials_grant("test_client", "test_secret", Some(vec!["read".to_string()]))
.expect("operation should succeed");
assert!(!response.access_token.is_empty());
assert!(response.access_token.starts_with("at_"));
let introspection = client.introspect_token(&response.access_token);
assert!(introspection.active);
assert_eq!(introspection.client_id, Some("test_client".to_string()));
}
#[test]
fn test_oauth_authorization_code_flow() {
let config = OAuthConfig::new("https://auth.example.com", "test", "test");
let mut client = OAuthClient::new(config);
let client_info = OAuthClientInfo {
client_id: "test_client".to_string(),
client_secret_hash: hash_client_secret("test_secret"),
redirect_uris: vec!["https://app.example.com/callback".to_string()],
grant_types: vec![OAuthGrantType::AuthorizationCode],
scopes: vec!["read".to_string(), "write".to_string()],
tenant_id: "tenant_a".to_string(),
};
client.register_client(client_info);
let code = client
.create_authorization_code(
"test_client",
"https://app.example.com/callback",
vec!["read".to_string()],
"user123",
None,
None,
)
.expect("operation should succeed");
let response = client
.exchange_code(
"test_client",
"test_secret",
&code,
"https://app.example.com/callback",
None,
)
.expect("operation should succeed");
assert!(!response.access_token.is_empty());
assert!(response.refresh_token.is_some());
}
#[test]
fn test_token_revocation() {
let config = OAuthConfig::new("https://auth.example.com", "test", "test");
let mut client = OAuthClient::new(config);
let client_info = OAuthClientInfo {
client_id: "test_client".to_string(),
client_secret_hash: hash_client_secret("test_secret"),
redirect_uris: vec![],
grant_types: vec![OAuthGrantType::ClientCredentials],
scopes: vec!["read".to_string()],
tenant_id: "tenant_a".to_string(),
};
client.register_client(client_info);
let response = client
.client_credentials_grant("test_client", "test_secret", None)
.expect("operation should succeed");
assert!(client.introspect_token(&response.access_token).active);
assert!(client.revoke_token(&response.access_token));
assert!(!client.introspect_token(&response.access_token).active);
}
}