Skip to content

Commit b503e1d

Browse files
authored
Merge pull request #177 from StatelessStudio/v2.0.0
[2.0.0] Jun-06-2019
2 parents 6d059fa + b83943a commit b503e1d

33 files changed

Lines changed: 425 additions & 63 deletions

CHANGELOG.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,30 @@
11
# PointyApi Changelog
22

3+
## [2.0.0] Jun-06-2019
4+
5+
### Breaking Changes (See migration.md)
6+
7+
- [Issue #167] Auth token should remain stateless
8+
- [Issue #170] Guards should use forbiddenResponder instead of unauthorizedResponder
9+
- [Issue #174] Don't use auto-increment id
10+
- [Issue #175] Issue refresh tokens
11+
12+
### Additions
13+
14+
- [Issue #93] Add security checks to test-suite
15+
- [Issue #168] Add pointy.ready hook to fire when server is listening
16+
- [Issue #169] CORS function should utilize process.env.CLIENT_URL
17+
- [Issue #173] Package.json should include repository info
18+
19+
### Fixes
20+
21+
- npm update
22+
- Removed typedoc from dependencies
23+
- [Issue #165] loadUser() should use uniform Could not/Couldn't
24+
- [Issue #166] validationResponder() "Not null violation" should specify field name
25+
- [Issue #171] Http expect should accept success codes
26+
- [Issue #172] [README] Misidentifies Authentication/Authorization
27+
328
## [1.2.4] May-29-2019
429

530
### Fixes

README.md

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ PointyAPI is a library for quickly creating robust API servers.
1414

1515
- **ORM** *(TypeORM)* Create models which automatically create and maintain your database
1616
- **Validation** *(Class Validator)* Use Typescript decorators to automatically validate fields
17-
- **Authorization** *(JWT)* JWT and `request.user` make authorization a breeze
18-
- **Authentication** Use `CanRead()` and `CanWrite()` fields to ensure the user can read/write specific fields
17+
- **Authentication** *(JWT)* JWT and `request.user` make authorization a breeze
18+
- **Authorization** Use Guards, `CanRead()`, and `CanWrite()` fields to ensure the user can read/write specific fields
1919

2020
### Models
2121

@@ -28,16 +28,16 @@ class User extends BaseUser
2828
// User ID
2929
@PrimaryGeneratedColumn() // Primary column
3030
@IsInt() // Validation - Value must be integer
31-
@BodyguardKey() // Authorization - User must match this to be considered "self"
32-
@AnyoneCanRead() // Authentication - Anyone is allowed to read this field
31+
@BodyguardKey() // Authentication - User must match this to be considered "self"
32+
@AnyoneCanRead() // Authorization - Anyone is allowed to read this field
3333
public id: number = undefined;
3434

3535
// Username
3636
@Column({ unique: true }) // Database column - Usernames are unique
3737
@IsAlphanumeric() // Validation - must be alphanumeric
3838
@Length(4, 16) // Validation - must be between 4-16 characters
3939
@AnyoneCanRead() // Authentication - Anyone has read privelege to this member
40-
@OnlySelfCanWrite() // Authentication - Only self can write this member
40+
@OnlySelfCanWrite() // Authorization - Only self can write this member
4141
public username: string = undefined;
4242

4343
// Password
@@ -366,15 +366,45 @@ npm i pointyapi
366366

367367
Notice that now we get a `204 No Content` (which means deleted successfully!).
368368

369+
**What about the refreshToken?**
370+
You may notice you got two tokens back: `token` and `refreshToken`.
371+
372+
The `token` is short-lived - it only lasts 15 minutes or so.
373+
The `refreshToken` is long-living - it lasts about a week.
374+
375+
You can use the `refreshToken` to issue a new `accessToken` when it expires.
376+
369377
9. **Production**
370378

371379
To launch in production mode, please make sure the following variables are set (environment variables/.env)
372380

373381
- **SITE_TITLE** - Set the site title
374382
- **CLIENT_URL** - Set your client URL to add the client to the CORS policy
375383
- **JWT_KEY** - Set your token key to make JWT cryptographically secure
376-
- **JWT_TTL** - Set your token time-to-live. Default is 4 hours
384+
- **JWT_TTL** - Set your token time-to-live (seconds). Default is 15 minutes
385+
- **JWT_REFRESH_TTL** - Set your refresh token time-to-live (seconds). Default is 7 days.
386+
387+
#### UUID vs Auto-Incremented IDs
388+
389+
It is a security risk to use auto-incremented IDs, and you should instead use UUID for all ID columns.
377390

391+
To switch to using UUIDs:
392+
393+
- Install the `pgcrypto` extension
394+
- Tell TypeORM to use pgcrypto by placing the line `uuidExtension: 'pgcrypto'` in `orm-cli.js`
395+
- Change all `PrimaryGeneratedColumn()` to `PrimaryGeneratedColumn('uuid')`
396+
- Change all `public id: number` members to `public id: string`
397+
- Make sure you remove `IsInt()` from all ID fields, if it exists
398+
- Ensure your front-end and anywhere you may access the ID is expecting a string
399+
400+
Example:
401+
```typescript
402+
// User ID
403+
@PrimaryGeneratedColumn('uuid') // Primary column
404+
@BodyguardKey() // Authentication - User must match this to be considered "self"
405+
@AnyoneCanRead() // Authorization - Anyone is allowed to read this field
406+
public id: string = undefined;
407+
```
378408

379409
### Continued Reading
380410

migration.md

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,4 +103,18 @@
103103
17. Remove empty searches.
104104
Get queries no longer require a `search` key to access other query keys
105105
18. All mdoel members must be initialized to undefined, including relational arrays
106-
19. Queries may no longer pass special keys with `__` or `___`. You should now put these queries in the `additionalParameters` query object
106+
19. Queries may no longer pass special keys with `__` or `___`. You should now put these queries in the `additionalParameters` query object
107+
108+
## Version 1.x.x -> 2.x.x
109+
110+
1. Auth tokens are now completely stateless. Remove the `token` field from your User entity.
111+
2. Login now issues a refresh token.
112+
1. Make a POST endpoint in your auth router:
113+
`router.post('/refresh', refreshTokenEndpoint);`
114+
2. Update your front-end auth service to save the `refreshToken` and `refreshExpiration` from the `loginEndpoint`
115+
3. Set a timeout to call the `refreshTokenEndpoint` route when the access token expires. `refreshTokenEndpoint` will return an updated user object, including a new access token & expiration time.
116+
3. **(Optional)** PointyAPI now supports `UUID`. Follow the steps in the Readme to enable UUID (strongly recommended for production).
117+
118+
**NOTE** If you are already in production and decide to migrate to UUID, you must make sure to update relations etc
119+
120+
4. **(Optional)** Guards will now issue a `401` only if a token is not present/valid, otherwise will issue a `403`. This may help determine if the user is authenticated/authorized on the front-end.

orm-cli.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,6 @@ module.exports = {
88
user: dbSettings.user,
99
password: dbSettings.password,
1010
database: dbSettings.database,
11-
entities: [ 'lib/src/models/base-user.ts' ]
11+
entities: [ 'lib/src/models/base-user.ts' ],
12+
uuidExtension: 'pgcrypto'
1213
};

package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "pointyapi",
3-
"version": "1.2.4",
3+
"version": "2.0.0",
44
"author": "stateless-studio",
55
"license": "MIT",
66
"scripts": {
@@ -57,5 +57,18 @@
5757
"nyc": "^14.1.1",
5858
"source-map-support": "^0.5.12",
5959
"ts-node": "^7.0.1"
60-
}
60+
},
61+
"description": "*\"Stop writing endpoints\"*",
62+
"directories": {
63+
"lib": "lib",
64+
"test": "test"
65+
},
66+
"repository": {
67+
"type": "git",
68+
"url": "git+https://github.com/statelessstudio/pointyapi.git"
69+
},
70+
"bugs": {
71+
"url": "https://github.com/statelessstudio/pointyapi/issues"
72+
},
73+
"homepage": "https://github.com/statelessstudio/pointyapi#readme"
6174
}

src/database/postgres.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,10 @@ export class PointyPostgres extends BaseDb {
9191
password: pgOptions.password,
9292
database: pgOptions.database,
9393
entities: this.entities,
94-
synchronize: this.shouldSync
94+
synchronize: this.shouldSync,
95+
uuidExtension: pgOptions.uuidExtension
96+
? pgOptions.uuidExtension
97+
: 'pgcrypto'
9598
}).catch((error) => this.errorHandler(error));
9699
}
97100
}

src/endpoints.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,3 +66,4 @@ export { postEndpoint } from './endpoints/post-endpoint';
6666
export { patchEndpoint } from './endpoints/patch-endpoint';
6767
export { loginEndpoint } from './endpoints/login-endpoint';
6868
export { logoutEndpoint } from './endpoints/logout-endpoint';
69+
export { refreshTokenEndpoint } from './endpoints/refresh-token-endpoint';

src/endpoints/login-endpoint.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,10 @@ export async function loginEndpoint(
7878
const expiration = jwtBearer.getExpiration();
7979
const token = jwtBearer.sign(match);
8080

81-
if (token) {
81+
const refreshExpiration = jwtBearer.getExpiration(true);
82+
const refreshToken = jwtBearer.sign(match, true);
83+
84+
if (token && refreshToken) {
8285
// Set request user
8386
request.user = match;
8487

@@ -92,6 +95,8 @@ export async function loginEndpoint(
9295

9396
match['expiration'] = expiration;
9497
match['token'] = token;
98+
match['refreshExpiration'] = refreshExpiration;
99+
match['refreshToken'] = refreshToken;
95100

96101
// Send response
97102
if (!await runHook('afterLogin', request.user, request, response)) {
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { Request, Response } from 'express';
2+
3+
import { jwtBearer } from '../jwt-bearer';
4+
import { runHook } from '../utils/run-hook';
5+
import { readFilter } from '../bodyguard/read-filter';
6+
import { BaseUser } from '../models';
7+
8+
/**
9+
* Refresh the user's token
10+
* @param request Request object to query by
11+
* @param response Response object to call responder with
12+
*/
13+
export async function refreshTokenEndpoint(
14+
request: Request,
15+
response: Response
16+
): Promise<void> {
17+
// Check request body
18+
if ('refreshToken' in request.body) {
19+
// Check refresh token
20+
const token = jwtBearer.dryVerify(request.body.refreshToken);
21+
22+
if (token && 'isRefresh' in token && token.isRefresh) {
23+
// Load user
24+
let match = await request.repository
25+
.findOne({ id: token.id })
26+
.catch((error) => response.error(error));
27+
28+
// Check matching user
29+
if (match && match instanceof BaseUser) {
30+
// Create token
31+
const expiration = jwtBearer.getExpiration();
32+
const token = jwtBearer.sign(match);
33+
34+
// Set request user
35+
request.user = match;
36+
37+
// Create response
38+
match = readFilter(
39+
match,
40+
request.user,
41+
request.payloadType,
42+
request.userType
43+
);
44+
45+
match['expiration'] = expiration;
46+
match['token'] = token;
47+
48+
// Send response
49+
if (
50+
!await runHook(
51+
'tokenRefresh',
52+
request.user,
53+
request,
54+
response
55+
)
56+
) {
57+
return;
58+
}
59+
60+
response.json(match);
61+
}
62+
else {
63+
// No match found
64+
response.unauthorizedResponder('Could not authenticate user');
65+
}
66+
}
67+
else {
68+
// No match found
69+
response.unauthorizedResponder('Could not authenticate user');
70+
}
71+
}
72+
else {
73+
// Respond with 400
74+
response.validationResponder('No refresh token sent');
75+
}
76+
}

0 commit comments

Comments
 (0)