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:
+143
-35
@@ -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,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
+3
-4
@@ -6,6 +6,7 @@ pub mod permission;
|
||||
|
||||
use std::net::SocketAddr;
|
||||
|
||||
use crate::pb::core::token_service_server::TokenServiceServer;
|
||||
use crate::pb::im::channel_audit_service_server::ChannelAuditServiceServer;
|
||||
use crate::pb::im::channel_invitation_service_server::ChannelInvitationServiceServer;
|
||||
use crate::pb::im::channel_repo_link_service_server::ChannelRepoLinkServiceServer;
|
||||
@@ -16,7 +17,6 @@ use crate::pb::im::channel_webhook_service_server::ChannelWebhookServiceServer;
|
||||
use crate::pb::im::custom_emoji_service_server::CustomEmojiServiceServer;
|
||||
use crate::pb::im::forum_tag_service_server::ForumTagServiceServer;
|
||||
use crate::pb::im::im_integration_service_server::ImIntegrationServiceServer;
|
||||
use crate::pb::im::internal_auth_service_server::InternalAuthServiceServer;
|
||||
use crate::pb::im::member_service_server::MemberServiceServer;
|
||||
use crate::pb::im::permission_service_server::PermissionServiceServer;
|
||||
use crate::pb::im::stage_service_server::StageServiceServer;
|
||||
@@ -27,18 +27,17 @@ pub async fn start_grpc_server(
|
||||
addr: SocketAddr,
|
||||
service: AppService,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let auth_service = service.internal_auth.clone();
|
||||
let token_svc = auth::TokenGrpcService::new(service.internal_auth.clone());
|
||||
let channel_svc = channel::ChannelGrpcService::new(service.clone());
|
||||
let member_svc = member::MemberGrpcService::new(service.clone());
|
||||
let permission_svc = permission::PermissionGrpcService::new(service.clone());
|
||||
let internal_auth_svc = auth::InternalAuthGrpcService::new(auth_service);
|
||||
|
||||
let cs = channel_settings::ChannelSettingsServices::new(service);
|
||||
|
||||
tracing::info!(%addr, "gRPC server listening");
|
||||
|
||||
tonic::transport::Server::builder()
|
||||
.add_service(InternalAuthServiceServer::new(internal_auth_svc))
|
||||
.add_service(TokenServiceServer::new(token_svc))
|
||||
.add_service(ChannelServiceServer::new(channel_svc))
|
||||
.add_service(MemberServiceServer::new(member_svc))
|
||||
.add_service(PermissionServiceServer::new(permission_svc))
|
||||
|
||||
Reference in New Issue
Block a user