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
370 changes: 333 additions & 37 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,60 +1,356 @@
Use CKAN as an auth service in your application.
# 🔐 Using CKAN as an Authentication Service

It adds a new `user_login` action to the CKAN API so that you can call it for authentication of a user from a third party application:
This extension allows you to use CKAN as an authentication provider for third-party applications. It adds a new endpoint for user authentication, a user registration API with email verification, and a secure password reset workflow using JWT tokens.

* Method: POST
* Endpoint: `http://ckan:5000/api/3/action/user_login`
* Body: `{"id": <username>, "password": <password>}`
## ✨ Features

Example of using it in the NodeJS app:
- User registration API with email verification (accounts start in `pending` state until verified)
- Resend verification email with Redis-backed rate limiting
- Authenticate users via the `user_login` API
- Secure password reset workflow using JWT tokens
- Optional frontend token generation for seamless integration

## 🔧 Installation

1. **Activate your CKAN virtual environment**:

```bash
. /usr/lib/ckan/default/bin/activate
```

2. **Install the extension**:

```bash
pip install --no-cache-dir -e git+https://github.com/datopian/ckanext-auth.git#egg=ckanext-auth
```

3. **Enable the plugin** by adding it to your CKAN config file (`/etc/ckan/default/production.ini`):

```ini
ckan.plugins = ... auth
```

4. **Restart CKAN** (if using Apache on Ubuntu):

```bash
sudo service apache2 reload
```

## 🔑 User Login API

Use the `user_login` action to authenticate users from third-party applications.

- **Method**: `POST`
- **Endpoint**: `http://ckan:5000/api/3/action/user_login`
- **Request Body**:

```json
{
"id": "<username>",
"password": "<password>"
}
```

## ⚙️ Configuration Options

### `ckan.ini` Settings:

```ini
# URL used in password reset email links
ckanext.auth.frontend_url = http://example.com/

# Generate a frontend token on login
ckanext.auth.include_frontend_login_token = True
# NOTE: This is optional. If set to True, a frontend token will be generated and included in the login response, you can use this token to authenticate users in your frontend application.
```

### ✅ User login Flow

- **Endpoint**:
`POST /api/3/action/user_login`
- **Request Body**:

```json
{
"id": "<username>",
"password": "<password>"
}
```

- **Responses**:

- **Success**:

```json
{
"success": true,
"result": {
"id": "<user_id>",
"name": "<username>",
"email": "<user_email>",
"frontend_token": "<frontend_token>"
}
}
```
Below is an example of how to implement this in a Node.js application using Express.

```javascript
const loginViaCKAN = async function(body) {
// Call `user_login` action here
}
const loginViaCKAN = async function (body) {
const response = await fetch("http://ckan:5000/api/3/action/user_login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
const result = await response.json();
return result.success ? result.result : null;
};

app.post("/login", async (req, res) => {
const loggedUser = await loginViaCKAN(req.body)
if (loggedUser) {
// Add logged user to session
req.session.ckan_user = loggedUser
res.redirect('/dashboard')
} else {
req.flash('error_messages', 'Invalid username or password.')
res.redirect('/login')
}
})
const loggedUser = await loginViaCKAN(req.body);
if (loggedUser) {
req.session.ckan_user = loggedUser;
res.redirect("/dashboard");
} else {
req.flash("error_messages", "Invalid username or password.");
res.redirect("/login");
}
});
```

## Requirements
## 📝 User Registration & Email Verification

### Configuration

Add to your `ckan.ini`:

```ini
# Base URL of your frontend (used to build the verification link in emails)
ckanext.auth.frontend_url = https://example.com

# Token expiry in hours (default: 24)
ckanext.auth.email_verification_expiry_hours = 24
```

> **Redis required**: the resend endpoint uses CKAN's built-in Redis connection (same one used for background jobs). Ensure `ckan.redis.url` is configured.

---

### 1. Register a User

Creates a new user account in `pending` state and sends a verification email.

- **Endpoint**: `POST /api/3/action/user_register`
- **Request Body**:

```json
{
"name": "jdoe",
"email": "jdoe@example.com",
"password": "secretpassword"
}
```

- **Responses**:

- **Success** — account created, verification email sent:

```json
{
"success": true,
"result": {
"id": "<user_id>",
"name": "jdoe",
"email": "jdoe@example.com",
"state": "pending"
}
}
```

- **Error** — missing fields:

```json
{
"error": {
"name": ["Missing value"],
"email": ["Missing value"],
"password": ["Missing value"]
}
}
```

---

### 2. Verify Email

The user clicks the link in the verification email which contains a short-lived JWT token.

- **Endpoint**: `POST /api/3/action/user_verify_email`
- **Request Body**:

```json
{
"token": "<jwt_token_from_email>"
}
```

- **Responses**:

- **Success**:

```json
{
"success": true,
"message": "Email verified successfully. You can now log in."
}
```

- **Already verified**:

```json
{
"success": true,
"message": "Email already verified"
}
```

- **Errors**:

```json
{ "error": { "token": ["Verification link has expired"] } }
```

```json
{ "error": { "token": ["Invalid verification token"] } }
```

---

### 3. Resend Verification Email

Resends the verification email for accounts still in `pending` state.

**Rate limiting (Redis-backed)**:
- Per email: one resend allowed every **60 seconds**
- Per IP: maximum **5 requests per 5 minutes**

- **Endpoint**: `POST /api/3/action/user_resend_verification`
- **Request Body**:

```json
{
"email": "jdoe@example.com"
}
```

- **Response** (always the same regardless of whether the email exists, to avoid user enumeration):

```json
{
"success": true,
"message": "If this email is registered and pending verification, a new email link has been sent"
}
```

- **Rate limit errors**:

```json
{ "error": { "email": ["Please wait before requesting another verification email"] } }
```

```json
{ "error": { "email": ["Too many requests. Please try again later."] } }
```

---

## 🔄 Password Reset Flow

### 1. **Request Reset Email**

- **Endpoint**:
`POST /api/3/action/user_password_reset_request`

- **Request Body**:

```json
{
"email": "user@example.com"
}
```

- **Responses**:

- **Success**:

```json
{
"success": true,
"message": "Password reset email sent"
}
```

- **Errors**:

```json
{
"error": {
"email": ["Email is required"]
}
}
```

This has been tested on CKAN v2.10.
```json
{
"error": {
"message": "User with this email does not exist"
}
}
```

## Installation
---

To install ckanext-auth:
### 2. **Confirm Password Reset**

1. Activate your CKAN virtual environment, for example::
- **Endpoint**:
`POST /api/3/action/user_password_reset_confirm`

. /usr/lib/ckan/default/bin/activate
- **Request Body**:

2. Install the ckanext-auth Python package into your virtual environment::
```json
{
"token": "jwt_token_here",
"new_password": "new_secure_password"
}
```

pip install --no-cache-dir -e git+https://github.com/datopian/ckanext-auth.git#egg=ckanext-auth
- **Responses**:

3. Add ``auth`` to the ``ckan.plugins`` setting in your CKAN
config file (by default the config file is located at
``/etc/ckan/default/production.ini``).
- **Success**:

4. Restart CKAN. For example if you've deployed CKAN with Apache on Ubuntu::
```json
{
"success": true,
"message": "Password has been reset successfully"
}
```

sudo service apache2 reload
- **Errors**:

## Configuration
```json
{
"error": {
"token": ["Token is required"],
"new_password": ["New password is required"]
}
}
```

If you're using a separate frontend library to handle user logins, you might need to pass a user API token to the frontend for CRUD operations. The following configuration option generates a new user API token each time a user logs in. The token will be included as `frontend_token` in the `user` object response of the `user_login` action:
```json
{
"error": {
"token": ["Reset token has expired"]
}
}
```

ckanext.auth.include_frontend_login_token = True

> **Note**:
> This token is generated when the user logs in, but **it is not revoked when they logout**. It's only revoked the next time they login, just before the new token is created.
Loading