← Files FirebaseARCHIVED FILE

reference/security.md

8.73 KB · Sep 30, 2026 · 23:02 UTC

↓ Download file

# Security Reference

## Contents

- [@auth Directive](#auth-directive)
- [Access Levels](#access-levels)
- [CEL Expressions](#cel-expressions)
- [@check and @redact](#check-and-redact)
- [Authorization Patterns](#authorization-patterns)
- [Anti-Patterns](#anti-patterns)

______________________________________________________________________

## @auth Directive

Every deployable query/mutation must have `@auth`. Without it, operations
default to `NO_ACCESS`.

```graphql
query PublicData @auth(level: PUBLIC) { ... }
query UserData @auth(level: USER) { ... }
query AdminOnly @auth(expr: "auth.token.admin == true") { ... }
```

| Argument         | Description                                        |
| ---------------- | -------------------------------------------------- |
| `level`          | Preset access level                                |
| `expr`           | CEL expression (alternative to level)              |
| `insecureReason` | Suppress deploy warning for PUBLIC/unfiltered USER |

______________________________________________________________________

## Access Levels

| Level                 | Who Can Access                               | CEL Equivalent                                                           |
| --------------------- | -------------------------------------------- | ------------------------------------------------------------------------ |
| `PUBLIC`              | Anyone, authenticated or not                 | `true`                                                                   |
| `USER_ANON`           | Any authenticated user (including anonymous) | `auth.uid != nil`                                                        |
| `USER`                | Authenticated users (excludes anonymous)     | `auth.uid != nil && auth.token.firebase.sign_in_provider != 'anonymous'` |
| `USER_EMAIL_VERIFIED` | Users with verified email                    | `auth.uid != nil && auth.token.email_verified`                           |
| `NO_ACCESS`           | Admin SDK only                               | `false`                                                                  |

> **Important:** Levels like `USER` are starting points. Always add filters or
> expressions to verify the user can access specific data.

______________________________________________________________________

## CEL Expressions

### Available Bindings

| Binding                 | Description                                |
| ----------------------- | ------------------------------------------ |
| `auth.uid`              | Current user's Firebase UID                |
| `auth.token`            | Auth token claims (see below)              |
| `vars`                  | Operation variables (e.g., `vars.movieId`) |
| `request.time`          | Server timestamp                           |
| `request.operationName` | "query" or "mutation"                      |

### auth.token Fields

| Field                       | Description                                 |
| --------------------------- | ------------------------------------------- |
| `email`                     | User's email address                        |
| `email_verified`            | Boolean: email verified                     |
| `phone_number`              | User's phone                                |
| `name`                      | Display name                                |
| `sub`                       | Firebase UID (same as auth.uid)             |
| `firebase.sign_in_provider` | `password`, `google.com`, `anonymous`, etc. |
| `<custom_claim>`            | Custom claims set via Admin SDK             |

### Expression Examples

```graphql
# Check custom claim
@auth(expr: "auth.token.role == 'admin'")

# Check verified email domain
@auth(expr: "auth.token.email_verified && auth.token.email.endsWith('@company.com')")

# Check multiple conditions
@auth(expr: "auth.uid != nil && (auth.token.role == 'editor' || auth.token.role == 'admin')")

# Check variable
@auth(expr: "has(vars.status) && vars.status in ['draft', 'published']")
```

### Using eq_expr in Filters

Compare database fields with auth values:

```graphql
query MyPosts @auth(level: USER) {
  posts(where: { authorUid: { eq_expr: "auth.uid" }}) {
    id title
  }
}

mutation UpdateMyPost($id: UUID!, $title: String!) @auth(level: USER) {
  post_update(
    first: { where: {
      id: { eq: $id },
      authorUid: { eq_expr: "auth.uid" }
    }},
    data: { title: $title }
  )
}
```

______________________________________________________________________

## @check and @redact

Use `@check` to validate data and `@redact` to hide results from client:

### @check

Validates a field value; aborts if check fails.

```graphql
@check(expr: "this != null", message: "Not found")
@check(expr: "this == 'editor'", message: "Must be editor")
@check(expr: "this.exists(p, p.role == 'admin')", message: "No admin found")
```

| Argument   | Description                                  |
| ---------- | -------------------------------------------- |
| `expr`     | CEL expression; `this` = current field value |
| `message`  | Error message if check fails                 |
| `optional` | If `true`, pass when field not present       |

### @redact

Hides field from response (still evaluated for @check):

```graphql
query @redact { ... }  # Query result hidden but @check still runs
```

### Authorization Data Lookup

Check database permissions before allowing mutation:

```graphql
mutation UpdateMovie($id: UUID!, $title: String!) 
  @auth(level: USER) 
  @transaction {
  # Step 1: Check user has permission
  query @redact {
    moviePermission(
      key: { movieId: $id, userId_expr: "auth.uid" }
    ) @check(expr: "this != null", message: "No access to movie") {
      role @check(expr: "this == 'editor'", message: "Must be editor")
    }
  }
  # Step 2: Update if authorized
  movie_update(id: $id, data: { title: $title })
}
```

### Validate Key Exists

```graphql
mutation MustDeleteMovie($id: UUID!) @auth(level: USER) @transaction {
  movie_delete(id: $id) 
    @check(expr: "this != null", message: "Movie not found")
}
```

______________________________________________________________________

## Authorization Patterns

### User-Owned Resources

```graphql
# Create with owner
mutation CreatePost($content: String!) @auth(level: USER) {
  post_insert(data: {
    authorUid_expr: "auth.uid",
    content: $content
  })
}

# Read own data only
query MyPosts @auth(level: USER) {
  posts(where: { authorUid: { eq_expr: "auth.uid" }}) {
    id content
  }
}

# Update own data only
mutation UpdatePost($id: UUID!, $content: String!) @auth(level: USER) {
  post_update(
    first: { where: { id: { eq: $id }, authorUid: { eq_expr: "auth.uid" }}},
    data: { content: $content }
  )
}

# Delete own data only
mutation DeletePost($id: UUID!) @auth(level: USER) {
  post_delete(
    first: { where: { id: { eq: $id }, authorUid: { eq_expr: "auth.uid" }}}
  )
}
```

### Role-Based Access

```graphql
# Admin-only query
query AllUsers @auth(expr: "auth.token.admin == true") {
  users { id email name }
}

# Role from database
mutation AdminAction($id: UUID!) @auth(level: USER) @transaction {
  query @redact {
    user(key: { uid_expr: "auth.uid" }) {
      role @check(expr: "this == 'admin'", message: "Admin required")
    }
  }
  # ... admin action
}
```

### Public Data with Filters

```graphql
query PublicPosts @auth(level: PUBLIC) {
  posts(where: {
    visibility: { eq: "public" },
    publishedAt: { lt_expr: "request.time" }
  }) {
    id title content
  }
}
```

### Tiered Access (Pro Content)

```graphql
query ProContent @auth(expr: "auth.token.plan == 'pro'") {
  posts(where: { visibility: { in: ["public", "pro"] }}) {
    id title content
  }
}
```

______________________________________________________________________

## Anti-Patterns

### ❌ Don't Pass User ID as Variable

```graphql
# BAD - any user can pass any userId
query GetUserPosts($userId: String!) @auth(level: USER) {
  posts(where: { authorUid: { eq: $userId }}) { ... }
}

# GOOD - use auth.uid
query GetMyPosts @auth(level: USER) {
  posts(where: { authorUid: { eq_expr: "auth.uid" }}) { ... }
}
```

### ❌ Don't Use USER Without Filters

```graphql
# BAD - any authenticated user sees all documents
query AllDocs @auth(level: USER) {
  documents { id title content }
}

# GOOD - filter to user's documents
query MyDocs @auth(level: USER) {
  documents(where: { ownerId: { eq_expr: "auth.uid" }}) { ... }
}
```

### ❌ Don't Trust Unverified Email

```graphql
# BAD - email not verified
@auth(expr: "auth.token.email.endsWith('@company.com')")

# GOOD - verify email first
@auth(expr: "auth.token.email_verified && auth.token.email.endsWith('@company.com')")
```

### ❌ Don't Use PUBLIC/USER for Prototyping

During development, set operations to `NO_ACCESS` until you implement proper
authorization. Use emulator and VS Code extension for testing.

SHA-256: 96944466954df5d0147bdafeaa4e9cf77cac32bccd67e8f0955fc4d0696fac3e