Skip to content
Open
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
21 changes: 21 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
module.exports = {
root: true,
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint'],
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
],
parserOptions: {
ecmaVersion: 2020,
sourceType: 'module',
},
env: {
node: true,
es6: true,
},
rules: {
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
},
};
89 changes: 89 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
name: CI

on:
push:
branches:
- master
pull_request:
branches:
- master

permissions:
contents: read

jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '24'

- name: Install dependencies
run: npm install

- name: Run lint
run: npm run lint

test:
needs: [lint]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '24'

- name: Install dependencies
run: npm install

- name: Run tests
run: npm test

coverage:
needs: [lint]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '24'

- name: Install dependencies
run: npm install

- name: Run coverage
run: npm run coverage

semantic-release:
needs: [lint, test, coverage]
runs-on: ubuntu-latest
if: github.event_name == 'push' && github.ref == 'refs/heads/master'
permissions:
contents: write
steps:
- uses: actions/checkout@v4

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '24'

- name: Install dependencies
run: npm install

- name: Build
run: npm run build

- name: Run semantic-release
run: npm run release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,9 @@ out
.nuxt
dist

# Build output
lib

# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
Expand Down
203 changes: 202 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,202 @@
# qwik-authz
# Qwik-Authz

[![CI](https://github.com/node-casbin/qwik-authz/actions/workflows/ci.yml/badge.svg)](https://github.com/node-casbin/qwik-authz/actions/workflows/ci.yml)
[![NPM version](https://img.shields.io/npm/v/qwik-authz.svg?style=flat-square)](https://npmjs.com/package/qwik-authz)
[![NPM download](https://img.shields.io/npm/dm/qwik-authz.svg?style=flat-square)](https://npmjs.com/package/qwik-authz)
[![Discord](https://img.shields.io/discord/1022748306096537660?logo=discord&label=discord&color=5865F2)](https://discord.gg/S5UjpzGZjN)

Qwik-Authz is an authorization middleware for [Qwik](https://qwik.builder.io/), based on [Node-Casbin](https://github.com/casbin/node-casbin).

## Installation

```bash
npm install qwik-authz casbin
```

## Quick Start

### Basic Usage with HTTP Basic Authentication

```typescript
// src/routes/layout.tsx or plugin.ts
import { newEnforcer } from 'casbin';
import { authz } from 'qwik-authz';
import type { RequestHandler } from '@builder.io/qwik-city';

// Initialize Casbin enforcer
const enforcer = await newEnforcer('path/to/model.conf', 'path/to/policy.csv');

// Apply authz middleware
export const onRequest: RequestHandler = authz({ newEnforcer: enforcer });
```

By default, qwik-authz uses HTTP Basic Authentication in the format:
```
Authorization: Basic {Base64Encoded(username:password)}
```

### Usage with Custom Authentication (JWT, OAuth, etc.)

For other authentication methods, set the username in `event.sharedMap` before applying the authz middleware:

```typescript
import { newEnforcer } from 'casbin';
import { authz } from 'qwik-authz';
import type { RequestHandler } from '@builder.io/qwik-city';

const enforcer = await newEnforcer('path/to/model.conf', 'path/to/policy.csv');

// Custom authentication middleware
export const onRequest: RequestHandler = async (event) => {
// Extract username from your auth method (JWT, session, etc.)
const token = event.request.headers.get('Authorization')?.replace('Bearer ', '');

if (token) {
const username = await verifyToken(token); // Your token verification logic
event.sharedMap.set('username', username);
}
};

// Apply authz middleware
export const onGet: RequestHandler = authz({ newEnforcer: enforcer });
```

### Usage with Custom Authorizer

Implement the `Authorizer` interface to add custom authorization logic:

```typescript
import { Enforcer, newEnforcer } from 'casbin';
import { authz, Authorizer } from 'qwik-authz';
import type { RequestHandler, RequestEventCommon } from '@builder.io/qwik-city';

const enforcer = await newEnforcer('path/to/model.conf', 'path/to/policy.csv');

class CustomAuthorizer implements Authorizer {
private event: RequestEventCommon;
private enforcer: Enforcer;

constructor(event: RequestEventCommon, enforcer: Enforcer) {
this.event = event;
this.enforcer = enforcer;
}

async checkPermission(): Promise<boolean> {
// Allow public access to certain paths
if (this.event.url.pathname.startsWith('/public/')) {
return true;
}

// Use Casbin for other paths
const username = this.event.sharedMap.get('username') as string || 'anonymous';
return this.enforcer.enforce(
username,
this.event.url.pathname,
this.event.request.method
);
}
}

export const onRequest: RequestHandler = authz({
newEnforcer: enforcer,
authorizer: CustomAuthorizer,
});
```

## How Authorization Works

The authorization is determined based on `{subject, object, action}`:

- **subject**: The logged-in username
- **object**: The URL path (e.g., `/dataset1/resource`)
- **action**: The HTTP method (GET, POST, PUT, DELETE, etc.)

### Example Model File (`model.conf`)

```ini
[request_definition]
r = sub, obj, act

[policy_definition]
p = sub, obj, act

[role_definition]
g = _, _

[policy_effect]
e = some(where (p.eft == allow))

[matchers]
m = g(r.sub, p.sub) && keyMatch(r.obj, p.obj) && (r.act == p.act || p.act == "*")
```

### Example Policy File (`policy.csv`)

```csv
p, alice, /dataset1/*, GET
p, alice, /dataset1/resource1, POST
p, bob, /dataset2/*, *
p, admin, /*, *

g, alice, admin
```

In this example:
- `alice` can `GET` any resource under `/dataset1/` and `POST` to `/dataset1/resource1`
- `bob` can perform any action on resources under `/dataset2/`
- `admin` role has access to all resources
- `alice` has the `admin` role (via role inheritance)

## API Reference

### `authz(options: AuthzOptions)`

Creates an authorization middleware for Qwik.

#### Parameters

- `options.newEnforcer` - A Casbin `Enforcer` instance or Promise that resolves to one
- `options.authorizer` - (Optional) Custom `Authorizer` instance or constructor

#### Returns

A Qwik `RequestHandler` middleware function

### `Authorizer` Interface

```typescript
interface Authorizer {
checkPermission(): Promise<boolean>;
}
```

Implement this interface to create custom authorization logic.

## Examples

See the [examples](./examples) directory for more usage examples:

- [basic.ts](./examples/basic.ts) - Basic usage with HTTP Basic Authentication
- [custom-auth.ts](./examples/custom-auth.ts) - Custom authentication methods
- [custom-authorizer.ts](./examples/custom-authorizer.ts) - Custom authorization logic

## Documentation

For more information about Casbin and policy configuration:

- [Casbin Documentation](https://casbin.org)
- [Node-Casbin](https://github.com/casbin/node-casbin)
- [Qwik Documentation](https://qwik.builder.io/)

## License

This project is licensed under the [Apache 2.0 License](LICENSE).

## Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

## Support

- [GitHub Issues](https://github.com/node-casbin/qwik-authz/issues)
- [Discord Community](https://discord.gg/S5UjpzGZjN)
37 changes: 37 additions & 0 deletions examples/basic.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Copyright 2026 The Casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import { newEnforcer } from 'casbin';
import { authz } from 'qwik-authz';
import type { RequestHandler } from '@builder.io/qwik-city';

/**
* Example: Basic usage with HTTP Basic Authentication
*/

// Initialize the enforcer with model and policy files
const enforcer = await newEnforcer('examples/model.conf', 'examples/policy.csv');

// Create the authz middleware
export const onRequest: RequestHandler = authz({ newEnforcer: enforcer });

/**
* Usage in your Qwik route:
*
* This middleware will:
* 1. Extract username from HTTP Basic Authentication header
* 2. Get the request path and method
* 3. Check if the user is authorized using Casbin
* 4. Return 403 if not authorized, or continue if authorized
*/
Loading