# Authentication Code Generation

**Status**: Implemented
**Date**: 2026-04-07
**Related**: plexus-core authentication-framework.md, plexus-transport authentication-transport.md

## Overview

The Plexus macro system (`#[hub]`, `#[activation]`) automatically detects authentication requirements in method signatures and generates code to inject `AuthContext` from the connection state. This allows developers to simply declare `auth: &AuthContext` parameters and let the framework handle extraction and validation.

## Detection Logic

### Parameter Parsing (`parse.rs`)

During proc-macro expansion, the parser inspects each method's parameters to detect authentication requirements:

```rust
// In parse.rs:295
pub struct MethodInfo {
    // ...
    /// True if method requires auth context (detected from `auth: &AuthContext` parameter)
    pub requires_auth: bool,
}
```

**Detection algorithm** (`parse.rs:350-356`):
1. Iterate through method parameters
2. For each parameter with identifier `auth`:
   - Check if type is `&AuthContext` → set `requires_auth = true`
   - Check if type is `Option<&AuthContext>` → set `requires_auth = true` (future support)
3. **Exclude** `auth` from regular param list (it's framework-provided, not from RPC args)

**Supported patterns:**
- `auth: &AuthContext` → Required authentication (most common)
- `Option<&AuthContext>` → Optional authentication (detected but not yet fully implemented)

**Type checking** (`parse.rs:430-449`):
```rust
fn is_auth_context_type(ty: &Type) -> bool {
    // Handle &AuthContext
    if let Type::Reference(TypeReference { elem, .. }) = ty {
        if let Type::Path(TypePath { path, .. }) = &**elem {
            if let Some(last_segment) = path.segments.last() {
                if last_segment.ident == "AuthContext" {
                    return true;
                }
            }
        }
    }

    // Handle Option<&AuthContext>
    // ...
}
```

### Why Not Attributes?

We considered `#[hub_method(auth)]` but chose automatic detection because:
1. **Less boilerplate**: No need to repeat `auth` in both attribute and signature
2. **Type safety**: Compiler enforces correct `&AuthContext` type
3. **Consistency**: Follows Rust conventions (parameter types determine behavior, like `self`)
4. **Future-proof**: Can support `Option<&AuthContext>` without syntax changes

## Code Generation

### Activation Dispatch (`codegen/activation.rs`)

When `requires_auth = true`, the macro generates additional code in the dispatch match arm:

**Generated code structure:**
```rust
// Activation trait dispatch method
async fn activate(
    &self,
    method: &str,
    params: Value,
    auth: Option<&AuthContext>,  // Provided by transport layer
) -> Result<PlexusStream, PlexusError> {
    match method {
        "my_method" => {
            // 1. Extract and validate auth (generated by macro)
            let auth_ctx = auth.ok_or_else(|| PlexusError::Unauthenticated(
                "This method requires authentication".to_string()
            ))?;

            // 2. Extract regular params from JSON
            let param1: String = serde_json::from_value(params["param1"].clone())?;

            // 3. Call implementation with auth injected
            let result = self.my_method(auth_ctx, param1).await?;

            // 4. Convert to stream
            Ok(PlexusStream::single(result))
        }
        _ => Err(PlexusError::MethodNotFound { /* ... */ })
    }
}
```

### Auth Injection Code (`activation.rs:461-467`)

```rust
let auth_injection = if m.requires_auth {
    quote! {
        let auth_ctx = auth.ok_or_else(|| #crate_path::plexus::PlexusError::Unauthenticated(
            "This method requires authentication".to_string()
        ))?;
    }
} else {
    quote! {}
};
```

**Error handling:**
- If `auth` is `None` (no validator configured or validation failed), returns `Unauthenticated` error
- Error bubbles up to client as RPC error response
- Connection remains open (client can retry or call non-auth methods)

### Parameter List Construction (`activation.rs:470-474`)

```rust
let method_params = if m.requires_auth {
    quote! { auth_ctx, #(#param_names),* }
} else {
    quote! { #(#param_names),* }
};
```

**Parameter order:**
1. `auth_ctx: &AuthContext` (if `requires_auth`)
2. Regular method params (extracted from JSON)

### Bidirectional Streaming Support

For bidirectional methods (e.g., `#[hub_method(bidirectional)]`), auth is injected after the channel context:

**Parameter order for bidirectional methods:**
1. `ctx: &BidirContext<Req, Resp>` (channel context)
2. `auth_ctx: &AuthContext` (if `requires_auth`)
3. Regular params

**Generated code** (`activation.rs:495-500`):
```rust
let bidir_params = if m.requires_auth {
    quote! { &ctx, auth_ctx, #(#param_names),* }
} else {
    quote! { &ctx, #(#param_names),* }
};
```

## Example Expansion

### Input (User Code)

```rust
#[hub]
impl DocumentService {
    async fn get_document(&self, auth: &AuthContext, doc_id: String) -> Result<Document> {
        // Implementation
    }

    async fn public_metadata(&self, doc_id: String) -> Result<Metadata> {
        // No auth required
    }
}
```

### Output (Generated Code - Simplified)

```rust
// Method info parsed by macro
const GET_DOCUMENT_METHOD: MethodInfo = MethodInfo {
    method_name: "get_document",
    requires_auth: true,  // Detected from `auth: &AuthContext`
    params: vec!["doc_id"],  // `auth` excluded from param list
    // ...
};

const PUBLIC_METADATA_METHOD: MethodInfo = MethodInfo {
    method_name: "public_metadata",
    requires_auth: false,
    params: vec!["doc_id"],
    // ...
};

// Generated activation implementation
impl Activation for DocumentService {
    async fn activate(
        &self,
        method: &str,
        params: Value,
        auth: Option<&AuthContext>,
    ) -> Result<PlexusStream, PlexusError> {
        match method {
            "get_document" => {
                // ---- Auth injection (generated) ----
                let auth_ctx = auth.ok_or_else(|| {
                    PlexusError::Unauthenticated("This method requires authentication".to_string())
                })?;

                // ---- Param extraction ----
                let doc_id: String = serde_json::from_value(params["doc_id"].clone())?;

                // ---- Method call with auth injected ----
                let result = self.get_document(auth_ctx, doc_id).await?;

                Ok(PlexusStream::single(result))
            }

            "public_metadata" => {
                // No auth injection (requires_auth = false)
                let doc_id: String = serde_json::from_value(params["doc_id"].clone())?;
                let result = self.public_metadata(doc_id).await?;
                Ok(PlexusStream::single(result))
            }

            _ => Err(PlexusError::MethodNotFound { /* ... */ })
        }
    }
}
```

### Key Differences

**With auth:**
- Extra validation: `auth.ok_or_else(...)?`
- Auth passed to method: `self.get_document(auth_ctx, doc_id)`
- Parameter order: `auth` comes first, then regular params

**Without auth:**
- No validation (auth can be `None`)
- Direct call: `self.public_metadata(doc_id)`
- Only regular params

## Integration with Transport Layer

The transport layer (`plexus-transport`) provides the `auth: Option<&AuthContext>` parameter to the activation:

1. **WebSocket upgrade**: Extract `Cookie` header, call `SessionValidator::validate()`
2. **Store AuthContext**: If valid, attach to connection state
3. **Per-RPC call**: Pass `Some(&auth_ctx)` to activation if present, `None` otherwise
4. **Dispatch**: Macro-generated code handles `None` → `Unauthenticated` error

See `plexus-transport/docs/architecture/authentication-transport.md` for details.

## Schema Generation

Authentication requirements are **not** reflected in the generated schema:
- Client sees `get_document(doc_id: String)` (no `auth` parameter)
- Auth is implicit in the connection, not an explicit parameter
- TypeScript bindings don't include `auth` in method signatures

**Rationale:**
- Auth is connection-level state, not per-request data
- Clients don't "pass" auth to each method (it's in the connection context)
- Matches WebSocket semantics (auth during handshake, not per-message)

## Future Enhancements

### 1. Optional Authentication (`Option<&AuthContext>`)

**User code:**
```rust
async fn mixed_endpoint(&self, auth: Option<&AuthContext>, query: String) -> Result<Data> {
    if let Some(auth) = auth {
        // Return private + public data
    } else {
        // Return only public data
    }
}
```

**Generated code:**
```rust
// No auth.ok_or_else() check - pass Option directly
let auth_ctx = auth;
let result = self.mixed_endpoint(auth_ctx, query).await?;
```

**Status**: Type detection exists, but code generation needs update.

### 2. Method-Level Auth Attributes

Allow overriding auth behavior via attributes:

```rust
#[hub_method(require_role = "admin")]
async fn admin_action(&self, auth: &AuthContext) -> Result<()> { /* ... */ }

#[hub_method(require_tenant)]
async fn tenant_data(&self, auth: &AuthContext) -> Result<Data> { /* ... */ }
```

**Generated code:**
```rust
let auth_ctx = auth.ok_or_else(/* ... */)?;

// Role check
if !auth_ctx.has_role("admin") {
    return Err(PlexusError::Forbidden("Admin role required".into()));
}

// Tenant check
if auth_ctx.tenant().is_none() {
    return Err(PlexusError::Forbidden("Tenant required".into()));
}

let result = self.admin_action(auth_ctx).await?;
```

**Benefit**: Declarative RBAC without boilerplate in method bodies.

### 3. Auth Hooks

Allow custom validation logic via traits:

```rust
trait AuthHook: Send + Sync {
    async fn before_method(
        &self,
        auth: &AuthContext,
        method: &str,
        params: &Value,
    ) -> Result<(), PlexusError>;
}

#[hub(auth_hook = MyAuthHook)]
impl MyService { /* ... */ }
```

**Generated code:**
```rust
let auth_ctx = auth.ok_or_else(/* ... */)?;
self.auth_hook().before_method(auth_ctx, method, &params).await?;
let result = self.my_method(auth_ctx, params).await?;
```

**Use cases**: Rate limiting, audit logging, tenant-specific authorization.

### 4. Schema Annotations

Optionally document auth requirements in schema:

```rust
#[hub_method(doc_auth = "Requires admin role")]
async fn admin_action(&self, auth: &AuthContext) -> Result<()> { /* ... */ }
```

**Generated schema:**
```json
{
  "method": "admin_action",
  "auth_required": true,
  "auth_description": "Requires admin role",
  "params": {}
}
```

**Benefit**: Better documentation for client developers.

## Testing

### Unit Tests (Macro Parser)

Test `is_auth_context_type()` function:
- Detects `&AuthContext` ✓
- Detects `Option<&AuthContext>` ✓
- Ignores `&String`, `&User`, etc. ✓
- Handles qualified paths: `plexus::AuthContext` ✓

### Integration Tests (Code Generation)

Test generated activation code:
1. **With auth** (requires_auth = true):
   - Valid auth → method succeeds
   - No auth (`None`) → `Unauthenticated` error
   - Invalid auth (validation returns `None`) → `Unauthenticated` error

2. **Without auth** (requires_auth = false):
   - No auth → method succeeds
   - With auth → method succeeds (auth ignored)

3. **Parameter order**:
   - Regular params: `(param1, param2)` → correct JSON mapping
   - With auth: `(auth, param1, param2)` → auth injected, params correct
   - Bidirectional: `(ctx, auth, param1)` → all three in correct order

### E2E Tests (FormVeritasV2)

See `FormVeritasV2/uscis-web/tests/auth-flow.spec.ts`:
- Connect with test cookie → auth methods succeed
- Connect without cookie → auth methods fail with "Unauthenticated"
- Public methods work regardless of auth

## Performance Considerations

### Compile-Time Cost

Auth detection adds minimal overhead:
- Type traversal: O(parameters × type depth)
- For typical methods (2-5 params, depth ≤3): negligible

**No impact** on incremental compilation (detection is local to each method).

### Runtime Cost

Generated auth injection code:
```rust
let auth_ctx = auth.ok_or_else(|| PlexusError::Unauthenticated(/*...*/))?;
```

**Cost**: One `Option` check + conditional error allocation
- **Fast path** (authenticated): ~1-2 CPU cycles (branch prediction)
- **Slow path** (unauthenticated): Error allocation + early return

**No** per-method overhead for non-auth methods (no code generated).

### Code Size

Per authenticated method:
- Auth injection: ~3 lines of generated code
- Parameter list adjustment: Marginal increase

For 100 auth methods: ~300 lines of generated code (~10-20 KB in binary).

## Migration Guide

### Adding Auth to Existing Methods

**Before:**
```rust
async fn get_document(&self, doc_id: String) -> Result<Document> {
    // How do we know who's calling?
}
```

**After:**
```rust
async fn get_document(&self, auth: &AuthContext, doc_id: String) -> Result<Document> {
    let tenant = auth.tenant().ok_or(Error::MissingTenant)?;
    let doc = self.db.get_document(&doc_id, &tenant).await?;
    Ok(doc)
}
```

**Steps:**
1. Add `auth: &AuthContext` as first parameter
2. Use `auth` for tenant isolation, role checks, audit logging
3. Recompile → macro generates auth injection automatically
4. Test with/without auth cookie → verify error handling

**No changes needed:**
- Client code (auth is connection-level)
- Activation trait impl (generated by macro)
- Schema generation (auth is implicit)

### Gradual Rollout

**Phase 1**: Add validator to server (all methods continue working):
```rust
let validator = Arc::new(TestSessionValidator::new());
server.with_session_validator(validator);
```

**Phase 2**: Add `auth` to critical methods (tenant isolation):
```rust
async fn get_documents(&self, auth: &AuthContext) -> Result<Vec<Document>> {
    // Now enforces authentication + tenant filtering
}
```

**Phase 3**: Update clients to send cookies (graceful degradation):
- Old clients (no cookie) → `Unauthenticated` on auth methods, public methods work
- New clients (with cookie) → all methods work

**Phase 4**: Remove non-auth versions of sensitive methods.

## Common Patterns

### Tenant Isolation

```rust
async fn get_data(&self, auth: &AuthContext, id: String) -> Result<Data> {
    let tenant = auth.tenant().ok_or(Error::MissingTenant)?;
    self.db.query("SELECT * FROM data WHERE id = $1 AND tenant = $2", &[&id, &tenant]).await
}
```

### Role-Based Access Control

```rust
async fn admin_action(&self, auth: &AuthContext) -> Result<()> {
    if !auth.has_role("admin") {
        return Err(Error::Forbidden("Admin role required".into()));
    }
    // ... admin logic
}
```

### Audit Logging

```rust
async fn sensitive_action(&self, auth: &AuthContext, data: Data) -> Result<()> {
    tracing::info!(
        user_id = %auth.user_id,
        tenant = ?auth.tenant(),
        "Sensitive action performed"
    );
    // ... business logic
}
```

### Mixed Public/Private Data

```rust
// Current: Separate methods
async fn public_metadata(&self, id: String) -> Result<Metadata> { /* ... */ }
async fn private_data(&self, auth: &AuthContext, id: String) -> Result<FullData> { /* ... */ }

// Future (with Option<&AuthContext>):
async fn get_data(&self, auth: Option<&AuthContext>, id: String) -> Result<Data> {
    let public_data = self.db.get_public(id).await?;
    if let Some(auth) = auth {
        let private_data = self.db.get_private(id, auth.tenant()?).await?;
        Ok(Data::Full { public_data, private_data })
    } else {
        Ok(Data::Public(public_data))
    }
}
```

## Related Documentation

- **Core framework**: `plexus-core/docs/architecture/authentication-framework.md` - AuthContext, SessionValidator traits
- **Transport layer**: `plexus-transport/docs/architecture/authentication-transport.md` - Cookie extraction, WebSocket upgrade
- **FormVeritasV2**: `docs/authentication.md` - Keycloak JWT implementation
- **E2E tests**: `FormVeritasV2/uscis-web/tests/auth-flow.spec.ts` - Test examples
