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
6 changes: 6 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
node_modules
npm-debug.log
.git
test
*.png
*.paw
45 changes: 45 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
name: CI

on:
pull_request:
push:
branches: [master]

permissions:
contents: read

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci
- run: npm test

image:
needs: test
if: github.event_name == 'push'
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/build-push-action@v6
with:
context: .
platforms: linux/amd64
push: true
tags: |
ghcr.io/imkit/imkit-auth-server:${{ github.sha }}
ghcr.io/imkit/imkit-auth-server:master
13 changes: 5 additions & 8 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,18 +1,15 @@
FROM node:8-alpine
FROM node:22-alpine

# Create app directory
WORKDIR /usr/src/app
ENV NODE_ENV=production

# Install app dependencies
#COPY package.json .
# For npm@5 or later, copy package-lock.json as well
COPY package.json package-lock.json ./

RUN npm install
RUN npm ci --omit=dev && npm cache clean --force

COPY routes ./routes
COPY app.js app.js
COPY bin/www bin/www

USER node
EXPOSE 3110
CMD [ "npm", "start" ]
CMD ["node", "./bin/www"]
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,25 @@
## Type B
![Architecture 2](Auth%202.2.png)

## Running this service

```bash
docker run -e JWT_SECRET=<random secret, 32+ bytes> -p 3110:3110 ghcr.io/imkit/imkit-auth-server:master
```

- `JWT_SECRET` is required; the service exits at startup without it. Use a long random value and keep it in a secret store.
- `POST /sign` issues an HS256 token for any posted claims (adds a one-year `exp`). **Anyone who can reach `/sign` can mint a token for any user, so never expose this service outside a private network.**
- `POST /verify` accepts only HS256 tokens signed with `JWT_SECRET` and returns their claims, or 401.
- Tokens and claims are never logged; the request log records method, path, status and timing only.
- Tokens are compatible with the 1.x release (Node 8, jsonwebtoken 8): a token issued by either version verifies in the other when both use the same `JWT_SECRET`.

## Development

```bash
npm ci
npm test
```

## The IMKit Auth Compatible API MUST satisfy the API signature:
### Verify
Method: POST
Expand Down
92 changes: 45 additions & 47 deletions app.js
Original file line number Diff line number Diff line change
@@ -1,50 +1,48 @@
var express = require('express');
var path = require('path');
var favicon = require('serve-favicon');
var logger = require('morgan');
var bodyParser = require('body-parser');
var cors = require('cors')

var app = express();

// CORS
app.use(cors({
origin: '*',
methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
preflightContinue: false,
credentials: false,
allowedHeaders: "Origin, X-Requested-With, Content-Type, Accept, CLIENT_KEY, Authorization, API_KEY",
}));

// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'jade');

// uncomment after placing your favicon in /public
//app.use(favicon(path.join(__dirname, 'public', 'favicon.ico')));
app.use(logger('dev'));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));

app.use('/', require('./routes/auth'));

// catch 404 and forward to error handler
app.use(function(req, res, next) {
var err = new Error('Not Found');
err.status = 404;
next(err);
});

// error handlers
app.use(function(err, req, res, next) {
if (!err.status) {
err.status = 500;
const express = require('express');
const logger = require('morgan');
const cors = require('cors');
const { createAuthRouter } = require('./routes/auth');

function createApp({ secret, logRequests = true } = {}) {
if (!secret) {
throw new Error('JWT_SECRET is required');
}
res.status(err.status);
res.json({
message: err.message,
error: err

const app = express();

app.use(cors({
origin: '*',
methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
preflightContinue: false,
credentials: false,
allowedHeaders: 'Origin, X-Requested-With, Content-Type, Accept, CLIENT_KEY, Authorization, API_KEY',
}));

// 'dev' logs method, path, status and timing only; request bodies carry tokens.
if (logRequests) {
app.use(logger('dev'));
}
app.use(express.json());
app.use(express.urlencoded({ extended: false }));

app.use('/', createAuthRouter(secret));

app.use((req, res, next) => {
const err = new Error('Not Found');
err.status = 404;
next(err);
});

// eslint-disable-next-line no-unused-vars
app.use((err, req, res, next) => {
const status = err.status || err.statusCode || 500;
res.status(status).json({
message: err.message,
error: { name: err.name, message: err.message },
});
});
});

module.exports = app;
return app;
}

module.exports = { createApp };
100 changes: 17 additions & 83 deletions bin/www
Original file line number Diff line number Diff line change
@@ -1,90 +1,24 @@
#!/usr/bin/env node
const http = require('http');
const { createApp } = require('../app');

/**
* Module dependencies.
*/
const port = Number.parseInt(process.env.PORT || '3110', 10);

var app = require('../app');
var debug = require('debug')('imkit-auth:server');
var http = require('http');

/**
* Get port from environment and store in Express.
*/

var port = normalizePort(process.env.PORT || '3110');
app.set('port', port);

/**
* Create HTTP server.
*/

var server = http.createServer(app);

/**
* Listen on provided port, on all network interfaces.
*/

server.listen(port);
server.on('error', onError);
server.on('listening', onListening);

/**
* Normalize a port into a number, string, or false.
*/

function normalizePort(val) {
var port = parseInt(val, 10);

if (isNaN(port)) {
// named pipe
return val;
}

if (port >= 0) {
// port number
return port;
}

return false;
}

/**
* Event listener for HTTP server "error" event.
*/

function onError(error) {
if (error.syscall !== 'listen') {
throw error;
}

var bind = typeof port === 'string'
? 'Pipe ' + port
: 'Port ' + port;

// handle specific listen errors with friendly messages
switch (error.code) {
case 'EACCES':
console.error(bind + ' requires elevated privileges');
process.exit(1);
break;
case 'EADDRINUSE':
console.error(bind + ' is already in use');
process.exit(1);
break;
default:
throw error;
}
let app;
try {
app = createApp({ secret: process.env.JWT_SECRET });
} catch (err) {
console.error(`imkit-auth: ${err.message}`);
process.exit(1);
}

/**
* Event listener for HTTP server "listening" event.
*/
const server = http.createServer(app);
server.listen(port, () => {
console.log(`imkit-auth listening on ${server.address().port}`);
});

function onListening() {
var addr = server.address();
var bind = typeof addr === 'string'
? 'pipe ' + addr
: 'port ' + addr.port;
debug('Listening on ' + bind);
function shutdown() {
server.close(() => process.exit(0));
}
process.on('SIGTERM', shutdown);
process.on('SIGINT', shutdown);
Loading
Loading