feat(auth): replace internal auth with JWT token service

- Replace InternalAuthService with TokenService using JWT tokens
- Add support for token issuance, refresh, verification and revocation
- Implement automatic signing key rotation with Redis storage
- Add database migration checks for indexes and foreign key constraints
- Update gRPC endpoints to use token-based authentication
- Remove deprecated API key based authentication system
- Add JSON Web Token support with HMAC-SHA256 signing
- Implement refresh token handling with automatic rotation
- Add token revocation by JTI and user ID
- Update build configuration to include core proto files
- Migrate database schema to handle token-based authentication
- Add comprehensive token validation and verification logic
This commit is contained in:
zhenyi
2026-06-11 15:08:13 +08:00
parent a0bea36041
commit dbbfb747a4
16 changed files with 833 additions and 186 deletions
+143 -35
View File
@@ -1,53 +1,161 @@
use tonic::{Request, Response, Status};
use crate::pb::im::internal_auth_service_server::InternalAuthService as InternalAuthServiceTrait;
use crate::pb::im::{AuthenticateRequest, AuthenticateResponse};
use crate::service::internal_auth::InternalAuthService;
use crate::pb::core::token_service_server::TokenService as TokenServiceTrait;
use crate::pb::core::{
GetSigningKeysRequest, GetSigningKeysResponse, IssueTokenRequest, IssueTokenResponse,
RefreshTokenRequest, RefreshTokenResponse, RevokeTokenRequest, RevokeTokenResponse,
SigningKey, TokenClaims as PbTokenClaims, VerifyTokenRequest, VerifyTokenResponse,
revoke_token_request::Target,
};
use crate::service::internal_auth::TokenService;
pub struct InternalAuthGrpcService {
service: InternalAuthService,
pub struct TokenGrpcService {
service: TokenService,
}
impl InternalAuthGrpcService {
pub fn new(service: InternalAuthService) -> Self {
impl TokenGrpcService {
pub fn new(service: TokenService) -> Self {
Self { service }
}
}
#[tonic::async_trait]
impl InternalAuthServiceTrait for InternalAuthGrpcService {
async fn authenticate(
impl TokenServiceTrait for TokenGrpcService {
async fn issue_token(
&self,
request: Request<AuthenticateRequest>,
) -> Result<Response<AuthenticateResponse>, Status> {
request: Request<IssueTokenRequest>,
) -> Result<Response<IssueTokenResponse>, Status> {
let req = request.into_inner();
if req.api_key.is_empty() {
return Ok(Response::new(AuthenticateResponse {
authenticated: false,
service_name: String::new(),
service_id: String::new(),
scopes: vec![],
expires_at: 0,
}));
if req.user_id.is_empty() {
return Err(Status::invalid_argument("user_id is required"));
}
match self.service.verify_api_key(&req.api_key).await {
Ok(Some(identity)) => Ok(Response::new(AuthenticateResponse {
authenticated: true,
service_name: identity.service_name,
service_id: identity.service_id,
scopes: identity.scopes,
expires_at: identity.expires_at,
})),
Ok(None) => Ok(Response::new(AuthenticateResponse {
authenticated: false,
service_name: String::new(),
service_id: String::new(),
scopes: vec![],
expires_at: 0,
})),
Err(e) => Err(Status::internal(format!("auth verification failed: {e}"))),
let ttl = if req.ttl_secs > 0 { req.ttl_secs } else { 3600 };
let tokens = self
.service
.issue_token(
&req.user_id,
ttl,
req.scopes,
req.extra,
)
.await
.map_err(|e| Status::internal(e.to_string()))?;
Ok(Response::new(IssueTokenResponse {
access_token: tokens.access_token,
refresh_token: tokens.refresh_token,
expires_at: tokens.expires_at,
key_id: tokens.key_id,
}))
}
async fn refresh_token(
&self,
request: Request<RefreshTokenRequest>,
) -> Result<Response<RefreshTokenResponse>, Status> {
let req = request.into_inner();
let tokens = self
.service
.refresh_token(&req.refresh_token, 3600)
.await
.map_err(|e| Status::unauthenticated(e.to_string()))?;
Ok(Response::new(RefreshTokenResponse {
access_token: tokens.access_token,
refresh_token: tokens.refresh_token,
expires_at: tokens.expires_at,
key_id: tokens.key_id,
}))
}
async fn revoke_token(
&self,
request: Request<RevokeTokenRequest>,
) -> Result<Response<RevokeTokenResponse>, Status> {
let req = request.into_inner();
match req.target {
Some(Target::Jti(jti)) => {
self.service
.revoke_by_jti(&jti, 86400)
.await
.map_err(|e| Status::internal(e.to_string()))?;
Ok(Response::new(RevokeTokenResponse { revoked_count: 1 }))
}
Some(Target::UserId(user_id)) => {
let count = self
.service
.revoke_user_tokens(&user_id)
.await
.map_err(|e| Status::internal(e.to_string()))?;
Ok(Response::new(RevokeTokenResponse {
revoked_count: count as i32,
}))
}
None => Err(Status::invalid_argument("target is required")),
}
}
async fn verify_token(
&self,
request: Request<VerifyTokenRequest>,
) -> Result<Response<VerifyTokenResponse>, Status> {
let req = request.into_inner();
match self
.service
.verify_token(&req.token)
.await
.map_err(|e| Status::internal(e.to_string()))?
{
Ok(claims) => Ok(Response::new(VerifyTokenResponse {
valid: true,
claims: Some(PbTokenClaims {
sub: claims.sub,
iss: claims.iss,
iat: claims.iat,
exp: claims.exp,
jti: claims.jti,
scope: claims.scope,
extra: claims.extra,
}),
reason: String::new(),
})),
Err(reason) => Ok(Response::new(VerifyTokenResponse {
valid: false,
claims: None,
reason,
})),
}
}
async fn get_signing_keys(
&self,
_request: Request<GetSigningKeysRequest>,
) -> Result<Response<GetSigningKeysResponse>, Status> {
let (keys, next_rotation_at) = self
.service
.get_signing_keys()
.await
.map_err(|e| Status::internal(e.to_string()))?;
Ok(Response::new(GetSigningKeysResponse {
keys: keys
.into_iter()
.map(|k| SigningKey {
kid: k.kid,
algorithm: k.algorithm,
key_material: k.key_material,
issued_at: k.issued_at,
expires_at: k.expires_at,
active: k.active,
})
.collect(),
next_rotation_at,
}))
}
}