Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions crates/openshell-server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,39 @@ after one minute or a five-second blocked send. Each gateway process accepts at
most 32 watches. Reconnect and relist on stream termination; revisions are opaque
equality tokens, not resumable cursors or an ordering across gateway replicas.

## Gateway application authentication

Gateway-mediated application HTTP requests and WebSocket upgrades authenticate
through the gateway chain or verified TLS client identity before endpoint lookup,
independently of app-listener enablement. User access requires TCP-forwarding
role/scope policy (`sandbox:write` when enforced) and current workspace membership;
operators retain their platform grant. Internal principals are rejected. Only
explicit local-development unauthenticated access bypasses this gate.

`/auth/services` hosts browser login on the control-plane origin. The application
origin receives a single-use bootstrap code bound to a browser login nonce, then
a distinct host-only, Secure/HttpOnly session cookie. Shared metadata contains
only credential-driver handles and hashes; the configured credential driver
stores the access token. Login codes expire after two minutes, sessions after
at most ten minutes or token expiry. Every request revalidates the token and
workspace authorization. Session deletion, credential revocation, and redemption
CAS use the shared database. Expired metadata cleanup and admission are bounded.
The application path consumes standard gateway bearer headers, or authenticates
through a separate `X-OpenShell-Authorization`, browser session, or mTLS identity
while preserving application bearer passthrough and cookies. Gateway headers and
reserved cookies never reach the workload, and workloads cannot set reserved
browser auth cookies. Browser ambient credentials require origin checks for
unsafe methods and WebSocket upgrades. Direct supervisor ingress delegates
application authentication to its ingress provider.

The optional real-Chrome login test uses an isolated profile and local test
credentials. Run it with `OPENSHELL_BROWSER_E2E_CHROME` pointing to Chrome:

```shell
OPENSHELL_BROWSER_E2E_CHROME='/Applications/Google Chrome.app/Contents/MacOS/Google Chrome' \
cargo test -p openshell-server --lib gateway_service_auth_real_browser_cross_site_login -- --ignored
```

## Application URL publication

`supervisor.app_listener.public_url` is an optional HTTPS origin matching the
Expand Down
14 changes: 14 additions & 0 deletions crates/openshell-server/src/auth/http.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ struct ConnectParams {
/// Create the auth router.
pub fn router(state: Arc<ServerState>) -> Router {
Router::new()
.route(
"/auth/services",
get(service_browser_login).post(service_browser_login),
)
.route("/auth/connect", get(auth_connect))
.route("/auth/oidc-config", get(oidc_config_handler))
.route("/.well-known/jwks.json", get(gateway_jwks_handler))
Expand All @@ -67,6 +71,16 @@ pub fn router(state: Arc<ServerState>) -> Router {
.with_state(state)
}

async fn service_browser_login(
State(state): State<Arc<ServerState>>,
req: axum::extract::Request,
) -> axum::response::Response {
match super::service_session::control(&state, req).await {
Ok(response) => response,
Err(error) => super::service_session::error(&error),
}
}

/// Publish OIDC-shaped discovery metadata for extension services.
///
/// This lets an extension be configured with only the trusted gateway URL:
Expand Down
2 changes: 2 additions & 0 deletions crates/openshell-server/src/auth/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ pub mod principal;
pub mod sandbox_jwt;
pub mod sandbox_methods;
pub mod sandbox_session;
pub mod service_access;
pub mod service_session;
pub mod supervisor_token;
pub mod workspace_authz;

Expand Down
8 changes: 4 additions & 4 deletions crates/openshell-server/src/auth/oidc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -948,7 +948,7 @@ impl Authenticator for OidcAuthenticator {
}

#[cfg(test)]
mod tests {
pub mod tests {
use super::*;

fn transport_test_config(issuer: impl Into<String>) -> OidcConfig {
Expand Down Expand Up @@ -1418,7 +1418,7 @@ mod tests {
}

/// Sign `claims` with the test key, tagging the header with `kid`.
fn mint_rs256(claims: &serde_json::Value, kid: &str) -> String {
pub fn mint_rs256(claims: &serde_json::Value, kid: &str) -> String {
crate::install_jsonwebtoken_crypto_provider();

let mut header = jsonwebtoken::Header::new(Algorithm::RS256);
Expand All @@ -1428,7 +1428,7 @@ mod tests {
openshell_crypto::jwt::encode(&header, claims, &key).expect("sign RS256 token")
}

fn claims_for(issuer: &str, audience: &str, exp: i64) -> serde_json::Value {
pub fn claims_for(issuer: &str, audience: &str, exp: i64) -> serde_json::Value {
serde_json::json!({
"sub": "user-42",
"preferred_username": "ada",
Expand All @@ -1442,7 +1442,7 @@ mod tests {

/// Serve an OIDC discovery document and a JWKS carrying the test key, then
/// build a cache against them the same way production does.
async fn cache_with_mock_issuer(server: &wiremock::MockServer) -> JwksCache {
pub async fn cache_with_mock_issuer(server: &wiremock::MockServer) -> JwksCache {
use wiremock::matchers::{method, path};
use wiremock::{Mock, ResponseTemplate};

Expand Down
Loading
Loading