feat: unify recommend anonymous runtime guest auth
- Route recommended runtime launches through shared runtime guest token handling - Extend recommend-page anonymous play beyond jump-hop - Add regression coverage for runtime guest launch clients - Update docs to reflect the full anonymous-play matrix
This commit is contained in:
@@ -9,9 +9,13 @@ use axum::{
|
||||
response::Response,
|
||||
};
|
||||
use platform_auth::{
|
||||
AccessTokenClaims, AuthProvider, BindingStatus, read_refresh_session_token, verify_access_token,
|
||||
AccessTokenClaims, AuthProvider, BindingStatus, RuntimeGuestTokenClaims,
|
||||
RuntimeGuestTokenClaimsInput, RUNTIME_GUEST_SCOPE_PUBLIC_PLAY, read_refresh_session_token,
|
||||
sign_runtime_guest_token, verify_access_token, verify_runtime_guest_token,
|
||||
};
|
||||
use serde_json::{Value, json};
|
||||
use shared_contracts::auth::RuntimeGuestTokenResponse;
|
||||
use shared_kernel::{format_rfc3339, new_uuid_simple_string};
|
||||
use time::OffsetDateTime;
|
||||
use tracing::warn;
|
||||
|
||||
@@ -34,6 +38,18 @@ pub struct RefreshSessionToken {
|
||||
token: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum RuntimePrincipal {
|
||||
User(AuthenticatedAccessToken),
|
||||
Guest(RuntimeGuestTokenClaims),
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum RuntimePrincipalKind {
|
||||
User,
|
||||
Guest,
|
||||
}
|
||||
|
||||
impl AuthenticatedAccessToken {
|
||||
pub fn new(claims: AccessTokenClaims) -> Self {
|
||||
Self { claims }
|
||||
@@ -54,6 +70,66 @@ impl RefreshSessionToken {
|
||||
}
|
||||
}
|
||||
|
||||
impl RuntimePrincipal {
|
||||
pub fn subject(&self) -> &str {
|
||||
match self {
|
||||
Self::User(authenticated) => authenticated.claims().user_id(),
|
||||
Self::Guest(claims) => claims.subject(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn kind(&self) -> RuntimePrincipalKind {
|
||||
match self {
|
||||
Self::User(_) => RuntimePrincipalKind::User,
|
||||
Self::Guest(_) => RuntimePrincipalKind::Guest,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RuntimePrincipalKind {
|
||||
pub fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::User => "user",
|
||||
Self::Guest => "guest",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn issue_runtime_guest_token(
|
||||
State(state): State<AppState>,
|
||||
Extension(request_context): Extension<RequestContext>,
|
||||
) -> Result<Json<Value>, AppError> {
|
||||
let issued_at = OffsetDateTime::now_utc();
|
||||
let claims = RuntimeGuestTokenClaims::from_input(
|
||||
RuntimeGuestTokenClaimsInput {
|
||||
subject: format!("guest-runtime-{}", new_uuid_simple_string()),
|
||||
scope: RUNTIME_GUEST_SCOPE_PUBLIC_PLAY.to_string(),
|
||||
},
|
||||
state.auth_jwt_config(),
|
||||
issued_at,
|
||||
)
|
||||
.map_err(|error| {
|
||||
AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR).with_message(error.to_string())
|
||||
})?;
|
||||
let token = sign_runtime_guest_token(&claims, state.auth_jwt_config()).map_err(|error| {
|
||||
AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR).with_message(error.to_string())
|
||||
})?;
|
||||
let expires_at = OffsetDateTime::from_unix_timestamp(claims.expires_at_unix() as i64)
|
||||
.ok()
|
||||
.and_then(|value| format_rfc3339(value).ok())
|
||||
.unwrap_or_else(|| "1970-01-01T00:00:00Z".to_string());
|
||||
|
||||
Ok(json_success_body(
|
||||
Some(&request_context),
|
||||
RuntimeGuestTokenResponse {
|
||||
token,
|
||||
expires_at,
|
||||
subject: claims.subject().to_string(),
|
||||
scope: claims.scope().to_string(),
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn require_bearer_auth(
|
||||
State(state): State<AppState>,
|
||||
mut request: Request,
|
||||
@@ -70,29 +146,70 @@ pub async fn require_bearer_auth(
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
pub async fn attach_optional_bearer_auth(
|
||||
pub async fn require_runtime_principal_auth(
|
||||
State(state): State<AppState>,
|
||||
mut request: Request,
|
||||
next: Next,
|
||||
) -> Result<Response, AppError> {
|
||||
if let Some(authenticated) = authenticate_request(&state, &request)? {
|
||||
request.extensions_mut().insert(authenticated.clone());
|
||||
let mut response = next.run(request).await;
|
||||
response.extensions_mut().insert(authenticated);
|
||||
return Ok(response);
|
||||
let Some(principal) = authenticate_runtime_principal(&state, &request)? else {
|
||||
return Err(AppError::from_status(StatusCode::UNAUTHORIZED));
|
||||
};
|
||||
request.extensions_mut().insert(principal.clone());
|
||||
|
||||
let mut response = next.run(request).await;
|
||||
response.extensions_mut().insert(principal);
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
fn authenticate_runtime_principal(
|
||||
state: &AppState,
|
||||
request: &Request,
|
||||
) -> Result<Option<RuntimePrincipal>, AppError> {
|
||||
if !request.headers().contains_key(AUTHORIZATION) {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
Ok(next.run(request).await)
|
||||
match authenticate_request(state, request) {
|
||||
Ok(Some(authenticated)) => Ok(Some(RuntimePrincipal::User(authenticated))),
|
||||
Ok(None) => Ok(None),
|
||||
Err(_) => {
|
||||
let bearer_token = extract_bearer_token(request.headers())?;
|
||||
let request_id = request
|
||||
.extensions()
|
||||
.get::<RequestContext>()
|
||||
.map(|context| context.request_id().to_string())
|
||||
.unwrap_or_else(|| "unknown".to_string());
|
||||
let claims = verify_runtime_guest_token(&bearer_token, state.auth_jwt_config())
|
||||
.map_err(|error| {
|
||||
warn!(
|
||||
%request_id,
|
||||
error = %error,
|
||||
"runtime guest JWT 校验失败"
|
||||
);
|
||||
AppError::from_status(StatusCode::UNAUTHORIZED)
|
||||
})?;
|
||||
if claims.scope() != RUNTIME_GUEST_SCOPE_PUBLIC_PLAY {
|
||||
warn!(
|
||||
%request_id,
|
||||
scope = %claims.scope(),
|
||||
"runtime guest JWT scope 非法"
|
||||
);
|
||||
return Err(AppError::from_status(StatusCode::UNAUTHORIZED));
|
||||
}
|
||||
Ok(Some(RuntimePrincipal::Guest(claims)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn authenticate_request(
|
||||
state: &AppState,
|
||||
request: &Request,
|
||||
) -> Result<Option<AuthenticatedAccessToken>, AppError> {
|
||||
if allows_internal_forwarded_auth(request.uri().path())
|
||||
&& let Some(claims) = try_build_internal_forwarded_claims(&state, request.headers())
|
||||
{
|
||||
return Ok(Some(AuthenticatedAccessToken::new(claims)));
|
||||
if allows_internal_forwarded_auth(request.uri().path()) {
|
||||
if let Some(claims) = try_build_internal_forwarded_claims(&state, request.headers()) {
|
||||
return Ok(Some(AuthenticatedAccessToken::new(claims)));
|
||||
}
|
||||
}
|
||||
|
||||
if !request.headers().contains_key(AUTHORIZATION) {
|
||||
|
||||
Reference in New Issue
Block a user