Skip to content
Merged
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
42 changes: 32 additions & 10 deletions AUTH_README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## Overview

This API implements a simplified token-based authentication system. The existing root endpoint (`POST /`) remains tokenless for backward compatibility, while a new secured endpoint (`POST /identify`) requires authentication.
This API implements a simple token-based authentication system.

## Authentication System

Expand All @@ -13,9 +13,6 @@ This API implements a simplified token-based authentication system. The existing

### Endpoints

#### Legacy Endpoint (No Authentication)
- `POST /` - Species identification (legacy, no token required)

#### Secured Endpoints
- `POST /identify` - Species identification (requires API token or admin token)
- All other endpoints require admin token
Expand All @@ -35,7 +32,8 @@ curl -X POST https://your-api-domain.com/admin/tokens \
-d '{
"name": "New Client Name",
"application": "my-app",
"description": "Token for my application"
"description": "Token for my application",
"model": "Norwegian"
}'

# Enable a token (using token prefix)
Expand Down Expand Up @@ -82,7 +80,7 @@ API tokens are managed in `auth/tokens.json`. Each token must be associated with
{
"your-api-token-1": {
"name": "Client Name 1",
"application": "artsorakelet",
"application": "artsorakel",
"enabled": true,
"created": "2024-01-01T00:00:00Z",
"description": "Description of what this token is for"
Expand All @@ -92,17 +90,21 @@ API tokens are managed in `auth/tokens.json`. Each token must be associated with
"application": "research-tool",
"enabled": false,
"created": "2024-01-01T00:00:00Z",
"description": "Another client token (disabled)"
"description": "Another client token (disabled)",
"model": "Swedish"
}
}
```

**Required Fields:**
- `name`: Human-readable name for the token
- `application`: Application identifier used for logging and identification

**Optional Fields:**
- `enabled`: Boolean indicating if the token is active (defaults to true)
- `created`: Timestamp when the token was created
- `description`: Optional description of the token's purpose
- `description`: Description of the token's purpose
- `model`: Restrict token to a specific model (`Norwegian`, `Swedish`, or `European`). If not set, model is selected based on location/IP.

## Token Management

Expand All @@ -116,7 +118,7 @@ curl -X POST https://your-api-domain.com/admin/tokens \
-H "Content-Type: application/json" \
-d '{
"name": "Mobile App Client",
"application": "artsorakelet-mobile",
"application": "artsorakel-mobile",
"description": "Token for the mobile application"
}'
```
Expand All @@ -127,13 +129,33 @@ curl -X POST https://your-api-domain.com/admin/tokens \
"message": "Token created successfully",
"token": "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1v2w3x4y5z6",
"name": "Mobile App Client",
"application": "artsorakelet-mobile",
"application": "artsorakel-mobile",
"enabled": true,
"created": "2024-01-15T10:30:00Z",
"warning": "Store this token securely. It will not be shown again in full."
}
```

### Model-Restricted Tokens

Tokens can be restricted to use a specific identification model regardless of location. This is useful for applications that should always use a particular regional model.

```bash
curl -X POST https://your-api-domain.com/admin/tokens \
-H "Authorization: Bearer your-admin-token" \
-H "Content-Type: application/json" \
-d '{
"name": "Swedish Partner App",
"application": "swedish-partner",
"description": "Token restricted to Swedish model",
"model": "Swedish"
}'
```

**Valid model values:** `Norwegian`, `Swedish`, `European`

When a model-restricted token is used, the response `modelInfo.locationSource` will be `"token"` instead of `"coordinates"` or `"ip"`.

### Managing Token Status

**Enable a token:**
Expand Down
2 changes: 2 additions & 0 deletions config/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const pictureFile = `${cachedir}/taxonPictures.json`;
const uploadsdir = "./uploads";
const listVersionsFile = `${cachedir}/listversions.json`;
const TOKENS_FILE = "./auth/tokens.json";
const VALID_MODELS = ['Norwegian', 'Swedish', 'European'];

const headfile = ".git/HEAD";

Expand Down Expand Up @@ -66,6 +67,7 @@ module.exports = {
uploadsdir,
listVersionsFile,
TOKENS_FILE,
VALID_MODELS,
branch,
server_url,
warningsConfig,
Expand Down
3 changes: 1 addition & 2 deletions jobs/cron.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,8 @@ const setupCronJobs = () => {
if (time_between >= survival_length) {
fs.unlink(`${uploadsdir}/${file}`, (err) => {
if (err) {
console.log("could not delete file");
console.log("could not delete file: " + filename);
}
console.log("The file has been deleted!");
});
}
});
Expand Down
3 changes: 2 additions & 1 deletion middleware/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,8 @@ const authenticateApiToken = (req, res, next) => {
type: "api",
token: token,
name: validTokens[token].name,
application: validTokens[token].application
application: validTokens[token].application,
model: validTokens[token].model
};
return next();
}
Expand Down
45 changes: 34 additions & 11 deletions routes/admin.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,25 @@ const {
} = require("../middleware/auth");
const { writeErrorLog } = require("../services/logging");
const { taxadir } = require("../services/taxon");
const { logdir } = require("../config/constants");
const { logdir, VALID_MODELS } = require("../config/constants");

module.exports = (app, upload) => {
app.get("/admin/tokens", authLimiter, authenticateAdminToken, (req, res) => {
try {
const validTokens = getValidTokens();
const tokenList = Object.keys(validTokens).map((token) => ({
token: token.substring(0, 8) + "...",
name: validTokens[token].name,
application: validTokens[token].application,
enabled: validTokens[token].enabled,
created: validTokens[token].created,
}));
const tokenList = Object.keys(validTokens).map((token) => {
const tokenInfo = {
token: token.substring(0, 8) + "...",
name: validTokens[token].name,
application: validTokens[token].application,
enabled: validTokens[token].enabled,
created: validTokens[token].created,
};
if (validTokens[token].model) {
tokenInfo.model = validTokens[token].model;
}
return tokenInfo;
});
res.status(200).json({
count: tokenList.length,
tokens: tokenList,
Expand All @@ -49,7 +55,7 @@ module.exports = (app, upload) => {

app.post("/admin/tokens", authLimiter, authenticateAdminToken, (req, res) => {
try {
const { name, application, description } = req.body;
const { name, application, description, model } = req.body;

if (!name || !application) {
return res.status(400).json({
Expand All @@ -58,6 +64,13 @@ module.exports = (app, upload) => {
});
}

if (model && !VALID_MODELS.includes(model)) {
return res.status(400).json({
error: "Bad request",
message: `Invalid model. Valid options: ${VALID_MODELS.join(', ')}`,
});
}

const newToken = generateSecureToken();
const validTokens = getValidTokens();

Expand All @@ -69,6 +82,10 @@ module.exports = (app, upload) => {
description: description ? description.trim() : `Token for ${name}`,
};

if (model) {
tokenData.model = model;
}

validTokens[newToken] = tokenData;

if (!saveTokens()) {
Expand All @@ -77,15 +94,21 @@ module.exports = (app, upload) => {
});
}

res.status(201).json({
const response = {
message: "Token created successfully",
token: newToken,
name: tokenData.name,
application: tokenData.application,
enabled: tokenData.enabled,
created: tokenData.created,
warning: "Store this token securely. It will not be shown again in full.",
});
};

if (model) {
response.model = model;
}

res.status(201).json(response);

writeErrorLog(
`Token created successfully`,
Expand Down
2 changes: 1 addition & 1 deletion routes/identify.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ module.exports = (app, upload) => {
json.predictions[0].taxon = {
vernacularName: "*** Utdatert versjon ***",
name:
"Vennligst oppdater Artsorakelet via app store, eller Ctrl-Shift-R på pc",
"Vennligst oppdater Artsorakel via app store, eller Ctrl-Shift-R på pc",
};

res.status(200).json(json);
Expand Down
19 changes: 18 additions & 1 deletion services/identification.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,25 @@ const getId = async (req) => {
let username = process.env.NATURALIS_USERNAME_NORWAY
let password = process.env.NATURALIS_PASSWORD_NORWAY

const tokenModelRestriction = req.auth?.model;

const selectModelCredentials = (model) => {
if (model === 'Swedish') {
username = process.env.NATURALIS_USERNAME_SWEDEN;
password = process.env.NATURALIS_PASSWORD_SWEDEN;
return process.env.NATURALIS_TOKEN_SWEDEN;
} else if (model === 'Norwegian') {
return process.env.NATURALIS_TOKEN_NORWAY;
} else {
return process.env.NATURALIS_TOKEN_EUROPE;
}
};

if (receivedParams.includes('model') && req.body.model && req.body.model.toLowerCase() === "global") {
if (tokenModelRestriction) {
token = selectModelCredentials(tokenModelRestriction);
modelUsed = tokenModelRestriction;
locationSource = 'token';
} else if (receivedParams.includes('model') && req.body.model && req.body.model.toLowerCase() === "global") {
token = process.env.NATURALIS_TOKEN_EUROPE;
modelUsed = 'European';
} else if (country === 'SE') {
Expand Down
Loading