mod password;
mod pkce;
mod secret;
pub use password::PasswordVerifier;
pub use pkce::{CodeChallengeMethod, PkceChallenge};
pub use secret::ClientSecretHasher;
use std::sync::Arc;
use std::time::Duration;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::config::SaTokenConfig;
use crate::dao::SaTokenDao;
use crate::error::{SaTokenError, SaTokenResult};
use crate::manager::SaTokenManager;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OAuth2Client {
pub client_id: String,
#[serde(default, alias = "client_secret")]
pub client_secret_hash: String,
#[serde(default, skip_serializing, skip_deserializing)]
pub client_secret: String,
pub redirect_uris: Vec<String>,
pub grant_types: Vec<String>,
pub scope: Vec<String>,
#[serde(default)]
pub public_client: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuthorizationCode {
pub code: String,
pub client_id: String,
pub user_id: String,
pub redirect_uri: String,
pub scope: Vec<String>,
pub created_at: DateTime<Utc>,
pub expires_at: DateTime<Utc>,
pub pkce: Option<PkceChallenge>,
pub state: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AccessToken {
pub access_token: String,
pub token_type: String,
pub expires_in: i64,
pub refresh_token: Option<String>,
pub scope: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OAuth2TokenInfo {
pub access_token: String,
pub client_id: String,
pub user_id: String,
pub scope: Vec<String>,
pub created_at: DateTime<Utc>,
pub expires_at: DateTime<Utc>,
pub refresh_token: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OAuth2RefreshRecord {
pub user_id: String,
pub client_id: String,
pub scope: Vec<String>,
pub access_token: String,
pub created_at: DateTime<Utc>,
}
#[derive(Debug, Default)]
pub struct TokenIssueRequest {
pub grant_type: String,
pub client_id: String,
pub client_secret: String,
pub code: Option<String>,
pub redirect_uri: Option<String>,
pub refresh_token: Option<String>,
pub username: Option<String>,
pub password: Option<String>,
pub scope: Vec<String>,
pub code_verifier: Option<String>,
}
pub struct OAuth2Manager {
dao: Arc<SaTokenDao>,
code_ttl: i64,
token_ttl: i64,
refresh_token_ttl: i64,
require_pkce: bool,
allow_legacy_plain_secret: bool,
password_verifier: Option<Arc<dyn PasswordVerifier>>,
}
impl std::fmt::Debug for OAuth2Manager {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("OAuth2Manager { .. }")
}
}
impl OAuth2Manager {
pub fn new(storage: Arc<dyn sa_token_adapter::storage::SaStorage>) -> Self {
let dao = Arc::new(SaTokenDao::new(storage, Arc::new(SaTokenConfig::default())));
Self::from_dao(dao)
}
pub fn from_dao(dao: Arc<SaTokenDao>) -> Self {
Self {
dao,
code_ttl: 600,
token_ttl: 3600,
refresh_token_ttl: 2592000,
require_pkce: false,
allow_legacy_plain_secret: false,
password_verifier: None,
}
}
pub fn from_manager(manager: &SaTokenManager) -> Self {
Self::from_dao(manager.dao().clone())
}
pub fn with_ttl(mut self, code_ttl: i64, token_ttl: i64, refresh_token_ttl: i64) -> Self {
self.code_ttl = code_ttl;
self.token_ttl = token_ttl;
self.refresh_token_ttl = refresh_token_ttl;
self
}
pub fn with_require_pkce(mut self, require: bool) -> Self {
self.require_pkce = require;
self
}
pub fn with_allow_legacy_plain_secret(mut self, allow: bool) -> Self {
self.allow_legacy_plain_secret = allow;
self
}
pub fn with_password_verifier(mut self, verifier: Arc<dyn PasswordVerifier>) -> Self {
self.password_verifier = Some(verifier);
self
}
pub async fn register_client_with_secret(
&self,
mut client: OAuth2Client,
plain_secret: &str,
) -> SaTokenResult<()> {
if client.public_client {
client.client_secret_hash.clear();
} else {
client.client_secret_hash = ClientSecretHasher::hash_plain_secret(plain_secret)?;
}
client.client_secret.clear();
let key = self.dao.keys().oauth2_client(&client.client_id);
self.dao.set_object(&key, &client, None).await
}
pub async fn register_client(&self, client: &OAuth2Client) -> SaTokenResult<()> {
self.register_client_with_secret(client.clone(), &client.client_secret)
.await
}
pub async fn get_client(&self, client_id: &str) -> SaTokenResult<OAuth2Client> {
let key = self.dao.keys().oauth2_client(client_id);
self.dao
.get_object(&key)
.await?
.ok_or(SaTokenError::OAuth2ClientNotFound)
}
pub async fn verify_client(&self, client_id: &str, client_secret: &str) -> SaTokenResult<bool> {
let client = self.get_client(client_id).await?;
if client.public_client {
return Ok(true);
}
if ClientSecretHasher::is_hashed(&client.client_secret_hash) {
return ClientSecretHasher::verify_plain_secret(
client_secret,
&client.client_secret_hash,
);
}
if self.allow_legacy_plain_secret {
return Ok(crate::http_basic::ct_eq(
client_secret.as_bytes(),
client.client_secret_hash.as_bytes(),
));
}
Ok(false)
}
pub fn generate_authorization_code(
&self,
client_id: String,
user_id: String,
redirect_uri: String,
scope: Vec<String>,
pkce: Option<PkceChallenge>,
state: Option<String>,
) -> AuthorizationCode {
let now = Utc::now();
AuthorizationCode {
code: format!("code_{}", Uuid::new_v4().simple()),
client_id,
user_id,
redirect_uri,
scope,
created_at: now,
expires_at: now + chrono::Duration::seconds(self.code_ttl),
pkce,
state,
}
}
pub async fn store_authorization_code(
&self,
auth_code: &AuthorizationCode,
) -> SaTokenResult<()> {
let key = self.dao.keys().oauth2_code(&auth_code.code);
let ttl = Some(Duration::from_secs(self.code_ttl as u64));
self.dao.set_object(&key, auth_code, ttl).await
}
pub async fn consume_authorization_code(&self, code: &str) -> SaTokenResult<AuthorizationCode> {
let key = self.dao.keys().oauth2_code(code);
let raw = self
.dao
.take_string(&key)
.await?
.ok_or(SaTokenError::OAuth2CodeNotFound)?;
let auth_code: AuthorizationCode = self.dao.decode(&raw)?;
if Utc::now() > auth_code.expires_at {
return Err(SaTokenError::TokenExpired);
}
Ok(auth_code)
}
pub async fn exchange_code_for_token(
&self,
code: &str,
client_id: &str,
client_secret: &str,
redirect_uri: &str,
code_verifier: Option<&str>,
) -> SaTokenResult<AccessToken> {
let client = self.get_client(client_id).await?;
if !client.public_client && !self.verify_client(client_id, client_secret).await? {
return Err(SaTokenError::OAuth2InvalidCredentials);
}
let auth_code = self.consume_authorization_code(code).await?;
if auth_code.client_id != client_id {
return Err(SaTokenError::OAuth2ClientIdMismatch);
}
if auth_code.redirect_uri != redirect_uri {
return Err(SaTokenError::OAuth2RedirectUriMismatch);
}
let need_pkce = client.public_client || self.require_pkce || auth_code.pkce.is_some();
if client.public_client {
let pkce = auth_code
.pkce
.as_ref()
.ok_or(SaTokenError::OAuth2PkceRequiredForPublicClient)?;
if !matches!(pkce.code_challenge_method, CodeChallengeMethod::S256) {
return Err(SaTokenError::OAuth2PkceRequiredForPublicClient);
}
let verifier = code_verifier.ok_or(SaTokenError::OAuth2PkceRequired)?;
pkce.verify(verifier)?;
} else if need_pkce {
let pkce = auth_code
.pkce
.as_ref()
.ok_or(SaTokenError::OAuth2PkceRequired)?;
let verifier = code_verifier.ok_or(SaTokenError::OAuth2PkceRequired)?;
pkce.verify(verifier)?;
}
self.generate_access_token(&auth_code.client_id, &auth_code.user_id, auth_code.scope)
.await
}
pub async fn generate_access_token(
&self,
client_id: &str,
user_id: &str,
scope: Vec<String>,
) -> SaTokenResult<AccessToken> {
let now = Utc::now();
let access_token = format!("at_{}", Uuid::new_v4().simple());
let refresh_token = format!("rt_{}", Uuid::new_v4().simple());
let token_info = OAuth2TokenInfo {
access_token: access_token.clone(),
client_id: client_id.to_string(),
user_id: user_id.to_string(),
scope: scope.clone(),
created_at: now,
expires_at: now + chrono::Duration::seconds(self.token_ttl),
refresh_token: Some(refresh_token.clone()),
};
let at_key = self.dao.keys().oauth2_token(&access_token);
self.dao
.set_object(
&at_key,
&token_info,
Some(Duration::from_secs(self.token_ttl as u64)),
)
.await?;
let record = OAuth2RefreshRecord {
user_id: user_id.to_string(),
client_id: client_id.to_string(),
scope: scope.clone(),
access_token: access_token.clone(),
created_at: now,
};
let rt_key = self.dao.keys().oauth2_refresh(&refresh_token);
self.dao
.set_object(
&rt_key,
&record,
Some(Duration::from_secs(self.refresh_token_ttl as u64)),
)
.await?;
Ok(AccessToken {
access_token,
token_type: "Bearer".to_string(),
expires_in: self.token_ttl,
refresh_token: Some(refresh_token),
scope,
})
}
pub async fn verify_access_token(&self, access_token: &str) -> SaTokenResult<OAuth2TokenInfo> {
let key = self.dao.keys().oauth2_token(access_token);
let info: OAuth2TokenInfo = self
.dao
.get_object(&key)
.await?
.ok_or(SaTokenError::OAuth2AccessTokenNotFound)?;
if Utc::now() > info.expires_at {
let _ = self.dao.delete(&key).await;
return Err(SaTokenError::TokenExpired);
}
Ok(info)
}
pub async fn refresh_access_token(
&self,
refresh_token: &str,
client_id: &str,
client_secret: &str,
) -> SaTokenResult<AccessToken> {
if !self.verify_client(client_id, client_secret).await? {
return Err(SaTokenError::OAuth2InvalidCredentials);
}
let rt_key = self.dao.keys().oauth2_refresh(refresh_token);
let raw = self
.dao
.take_string(&rt_key)
.await?
.ok_or(SaTokenError::OAuth2RefreshTokenNotFound)?;
let record: OAuth2RefreshRecord = self.dao.decode(&raw)?;
if record.client_id != client_id {
let ttl = Some(Duration::from_secs(self.refresh_token_ttl as u64));
let _ = self.dao.set_string(&rt_key, &raw, ttl).await;
return Err(SaTokenError::OAuth2ClientIdMismatch);
}
match self
.generate_access_token(&record.client_id, &record.user_id, record.scope.clone())
.await
{
Ok(new_token) => {
let old_at = self.dao.keys().oauth2_token(&record.access_token);
self.dao.delete(&old_at).await?;
Ok(new_token)
}
Err(e) => {
let ttl = Some(Duration::from_secs(self.refresh_token_ttl as u64));
self.dao.set_string(&rt_key, &raw, ttl).await?;
Err(e)
}
}
}
pub async fn revoke_token(&self, token: &str) -> SaTokenResult<()> {
let access_key = self.dao.keys().oauth2_token(token);
let refresh_key = self.dao.keys().oauth2_refresh(token);
self.dao.delete(&access_key).await?;
self.dao.delete(&refresh_key).await?;
Ok(())
}
pub fn validate_redirect_uri(&self, client: &OAuth2Client, redirect_uri: &str) -> bool {
if redirect_uri.is_empty() || redirect_uri.contains('#') {
return false;
}
client.redirect_uris.iter().any(|uri| uri == redirect_uri)
}
pub fn validate_scope(&self, client: &OAuth2Client, requested_scope: &[String]) -> bool {
requested_scope.iter().all(|s| client.scope.contains(s))
}
pub fn supports_grant_type(client: &OAuth2Client, grant_type: &str) -> bool {
client.grant_types.iter().any(|g| g == grant_type)
}
pub async fn password_grant(
&self,
client_id: &str,
client_secret: &str,
username: &str,
password: &str,
scope: Vec<String>,
) -> SaTokenResult<AccessToken> {
let verifier = self.password_verifier.as_ref().ok_or_else(|| {
SaTokenError::ConfigError("password verifier is not configured".into())
})?;
let client = self.get_client(client_id).await?;
if !Self::supports_grant_type(&client, "password") {
return Err(SaTokenError::OAuth2UnsupportedGrant);
}
if !self.verify_client(client_id, client_secret).await? {
return Err(SaTokenError::OAuth2InvalidCredentials);
}
if !self.validate_scope(&client, &scope) {
return Err(SaTokenError::OAuth2InvalidScope);
}
verifier.verify_password(username, password).await?;
self.generate_access_token(client_id, username, scope).await
}
pub async fn client_credentials_grant(
&self,
client_id: &str,
client_secret: &str,
scope: Vec<String>,
) -> SaTokenResult<AccessToken> {
let client = self.get_client(client_id).await?;
if !Self::supports_grant_type(&client, "client_credentials") {
return Err(SaTokenError::OAuth2UnsupportedGrant);
}
if client.public_client {
return Err(SaTokenError::OAuth2InvalidCredentials);
}
if !self.verify_client(client_id, client_secret).await? {
return Err(SaTokenError::OAuth2InvalidCredentials);
}
if !self.validate_scope(&client, &scope) {
return Err(SaTokenError::OAuth2InvalidScope);
}
let subject = format!("client:{client_id}");
self.generate_access_token(client_id, &subject, scope).await
}
pub async fn issue_token(&self, req: TokenIssueRequest) -> SaTokenResult<AccessToken> {
match req.grant_type.as_str() {
"authorization_code" => {
let code = req.code.ok_or(SaTokenError::OAuth2CodeNotFound)?;
let redirect_uri = req
.redirect_uri
.ok_or(SaTokenError::OAuth2RedirectUriMismatch)?;
self.exchange_code_for_token(
&code,
&req.client_id,
&req.client_secret,
&redirect_uri,
req.code_verifier.as_deref(),
)
.await
}
"refresh_token" => {
let refresh = req
.refresh_token
.ok_or(SaTokenError::OAuth2RefreshTokenNotFound)?;
self.refresh_access_token(&refresh, &req.client_id, &req.client_secret)
.await
}
"password" => {
let username = req.username.ok_or(SaTokenError::OAuth2InvalidCredentials)?;
let password = req.password.ok_or(SaTokenError::OAuth2InvalidCredentials)?;
self.password_grant(
&req.client_id,
&req.client_secret,
&username,
&password,
req.scope,
)
.await
}
"client_credentials" => {
self.client_credentials_grant(&req.client_id, &req.client_secret, req.scope)
.await
}
_ => Err(SaTokenError::OAuth2UnsupportedGrant),
}
}
pub async fn issue_authorization_code(
&self,
client_id: String,
user_id: String,
redirect_uri: String,
scope: Vec<String>,
pkce: Option<PkceChallenge>,
state: Option<String>,
) -> SaTokenResult<AuthorizationCode> {
let client = self.get_client(&client_id).await?;
if !self.validate_redirect_uri(&client, &redirect_uri) {
return Err(SaTokenError::OAuth2RedirectUriMismatch);
}
if !self.validate_scope(&client, &scope) {
return Err(SaTokenError::OAuth2InvalidScope);
}
if (client.public_client || self.require_pkce) && pkce.is_none() {
return Err(if client.public_client {
SaTokenError::OAuth2PkceRequiredForPublicClient
} else {
SaTokenError::OAuth2PkceRequired
});
}
let code =
self.generate_authorization_code(client_id, user_id, redirect_uri, scope, pkce, state);
self.store_authorization_code(&code).await?;
Ok(code)
}
}