mirror of
https://codeberg.org/Toasterson/ips.git
synced 2026-04-10 13:20:42 +00:00
Manifest text is now carried through the solver's ResolvedPkg and written directly to disk during install, eliminating the redundant re-fetch from the repository that could silently fail. save_manifest() is now mandatory (fatal on error) since the .p5m file on disk is the authoritative record for pkg verify and pkg fix. Add ADRs for libips API layer (GUI sharing), OpenID Connect auth, and SQLite catalog as query engine (including normalized installed_actions table). Add phase plans for code hygiene, client completion, catalog expansion, and OIDC authentication. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
3.4 KiB
3.4 KiB
Phase 4: OpenID Connect Authentication
Date: 2026-02-25 Status: Active Depends on: Phase 1 (architecture) Implements: ADR-002
Goal
Secure the REST API with OIDC JWT validation. Allow CLI and GUI clients to authenticate via standard OIDC flows.
Step 1: Server-side JWT validation (pkg6depotd)
1.1: Add dependencies
# pkg6depotd/Cargo.toml
jsonwebtoken = "9"
reqwest = { version = "0.12", features = ["json"] } # for JWKS fetch
serde_json = "1"
1.2: Configuration
// depot.kdl
auth {
enabled true
oidc-issuer "https://keycloak.example.com/realms/ips"
required-scopes "ips:read" "ips:write"
// Optional: per-publisher access via JWT claims
publisher-claim "ips_publishers"
}
1.3: JWKS fetcher
Background task that fetches and caches JWKS from the OIDC provider:
pub struct JwksCache {
keys: RwLock<jwk::JwkSet>,
issuer: String,
jwks_uri: String,
}
impl JwksCache {
pub async fn new(issuer: &str) -> Result<Self> { /* fetch .well-known/openid-configuration */ }
pub async fn refresh(&self) -> Result<()> { /* re-fetch JWKS */ }
pub fn validate_token(&self, token: &str) -> Result<Claims> { /* decode + verify */ }
}
1.4: Auth middleware
Axum middleware that validates Bearer tokens on protected routes:
pub async fn require_auth(
State(jwks): State<Arc<JwksCache>>,
req: Request,
next: Next,
) -> Result<Response, DepotError> {
let token = extract_bearer_token(&req)?;
let claims = jwks.validate_token(&token)?;
// Check required scopes
// Inject claims into request extensions
next.run(req).await
}
Apply to: POST routes (publish, index rebuild). Leave GET routes (catalog, manifest, file, search) unauthenticated by default. Add optional auth.require-read true config to protect everything.
Step 2: Client-side OIDC (libips RestBackend)
2.1: Add dependencies
# libips/Cargo.toml
openidconnect = "4"
2.2: CredentialProvider trait
pub trait CredentialProvider: Send + Sync {
fn get_token(&self) -> Result<String>;
fn refresh_if_needed(&self) -> Result<()>;
}
2.3: Device Code Flow (CLI)
pub struct DeviceCodeProvider {
issuer: String,
client_id: String,
token_path: PathBuf, // cached token on disk
}
Flow:
- Call device authorization endpoint
- Print "Open https://... and enter code: ABCD-EFGH"
- Poll token endpoint until user completes
- Cache token + refresh token to
{image}/.pkg/auth/{publisher}.json - On subsequent calls, use refresh token if access token expired
2.4: Wire into RestBackend
impl RestBackend {
pub fn with_credentials(mut self, provider: Arc<dyn CredentialProvider>) -> Self {
self.credential_provider = Some(provider);
self
}
}
All HTTP requests check for credential provider and add Authorization: Bearer {token} header.
Step 3: Token storage
Tokens stored in image metadata:
{image_root}/.pkg/auth/
{publisher}.json -- { "access_token": "...", "refresh_token": "...", "expires_at": "..." }
File permissions: 0600 (owner read/write only).
Verification
- Start Keycloak/Dex in Docker for testing
- Verify unauthenticated GET requests still work
- Verify protected POST requires valid Bearer token
- Verify expired tokens are rejected
- Verify CLI device code flow obtains and caches token
- Verify token refresh works transparently