-
Notifications
You must be signed in to change notification settings - Fork 118
Authentication Documentation #328
Changes from 6 commits
b636667
764b085
19be5c2
8edd4d7
2ea3224
d075453
fe836be
e81fce5
01f356f
fa7b0a8
ba30854
393baa8
f7c8266
e92ee7b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
# Authentication Chain | ||
|
||
The Authentication chain is a made up of special type hooks called "Verification Hooks" that allow you to run transport agnostic code to authorize a request. What this means is that no matter **how** things connect to your API, your authentication chain stays the same. | ||
|
||
Since sockets are inherently stateful and HTTP(S) requests are not, this caused a lot of redundancy in the authentication plugin and was not very scalable. Now, you can simply add special verification hooks to your authentication chain which allows you to customize authentication however you need to. All you need to do is set `params.authenticated` to true or false throughout this chain and ensure that you are checking for this on your services, like so: | ||
|
||
```js | ||
const auth = require('feathers-authentication'); | ||
// An example user service | ||
app.service('users').hooks({ | ||
before: { | ||
all: [ | ||
auth.hooks.authenticate(), | ||
auth.hooks.isAuthenticated() | ||
], | ||
create: [ | ||
auth.hooks.hashPassword() | ||
] | ||
} | ||
}); | ||
``` | ||
|
||
The `.authenticate()` function triggers your verification chain, and `.isAuthenticated()` function checks for `hook.params.authenticated` and if it is falsy, returns a `NotAuthenticated` error. | ||
|
||
## Example | ||
|
||
Refer to [the main spec](./spec.md#usage) for full usage. | ||
|
||
## Examples of Hooks | ||
|
||
```js | ||
function verifyClientId(params) { | ||
const app = this; | ||
|
||
// If verification has already failed in a previous | ||
// middleware just skip to save resources. | ||
if (params.authenticated === false) { | ||
return Promise.resolve(params); | ||
} | ||
|
||
// look up clientId | ||
return app.service('clients').get(params.payload.clientId).then(client => { | ||
params.authenticated = true; | ||
return Promise.resolve(params); | ||
}) | ||
.catch(error => { | ||
params.authenticated = false; | ||
return Promise.resolve(params); | ||
}); | ||
} | ||
``` | ||
|
||
```js | ||
function checkAuthorizationExpiration(params) { | ||
const app = this; | ||
|
||
if (params.authorization.expiry > Date.now()) { | ||
params.authenticated = false; | ||
} | ||
|
||
return Promise.resolve(params); | ||
} | ||
``` |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,154 @@ | ||
# Authentication Examples | ||
|
||
>TODO (EK): This is kind of a dumping ground right now and needs to be cleaned up. Likely, each example will be moved to their own file. | ||
|
||
### Example 1: IoT Device Authentication | ||
|
||
1. Register device with server. Send device serial or MAC address. Get back ClientID & ClientSecret. ClientID could be serial or MAC address. | ||
2. Client stores both securely. Especially the ClientSecret. | ||
3. Client authenticates with server by sending ClientID and ClientSecret. | ||
4. Server verifies these and generates an Authorization for the Client, any default tokens (ie. a JWT access token and refresh token), and any default permissions. It encodes the ClientID and AuthorizationID in the JWT payload. | ||
5. Sends the token back to the server. | ||
6. Client stores refresh token securely | ||
7. Client sends access token in `Authorization` header | ||
|
||
### Example 2: Username + Password Authentication | ||
|
||
1. Client authenticates with server by sending ClientID (username) & ClientSecret (password) to server. | ||
2. Server verifies these and generates an Authorization for the Client, any default tokens (ie. a JWT access token and refresh token), and any default permissions. It encodes the ClientID and AuthorizationID in the JWT payload. | ||
3. Sends the token back to the server. | ||
4. Client stores refresh token securely | ||
5. Client sends access token in `Authorization` header | ||
|
||
Depending on your transport these auth flows are modified as in the examples below. I think keeping an `Authorizations` table or collection resolves the blacklist/whitelist scenario. | ||
|
||
### Customizing JWT Payload | ||
|
||
```js | ||
// Server Side | ||
app.service('authentication').hooks({ | ||
before: { | ||
create: [ | ||
hasAcceptedTerms(), | ||
localAuth({ | ||
clientIdField: 'email', | ||
clientSecretField: 'password', | ||
entity: 'user', | ||
service: 'users' | ||
}), | ||
populate({ | ||
idField: '_id', // optional but should come from service.id | ||
service: 'users', | ||
entity: 'user', | ||
on: 'params' | ||
}), | ||
setJWTPayload() | ||
] | ||
} | ||
}); | ||
``` | ||
|
||
### Authenticating Username and Password over Ajax | ||
|
||
This is the most basic. | ||
|
||
1. Client `POST`s username + password to `/authentication` over Ajax in the request body. | ||
2. Server looks up entity by username | ||
3. Server verifies password is correct using bcrypt hashing algorithm | ||
4. Server creates a JWT access token with the entity's database ID encoded in the JWT payload | ||
5. Server sends the signed JWT access token back to the client | ||
|
||
### Authenticating Username and Password over Sockets | ||
|
||
1. Client establishes socket connection | ||
2. Client sends an `authenticate` message with the username + password in the message body | ||
3. Server looks up entity by username | ||
4. Server verifies password is correct using bcrypt hashing algorithm | ||
5. Server creates a JWT access token with the entity's database ID encoded in the JWT payload | ||
6. Server attaches the token to the socket | ||
7. Server attaches the entity to the socket and registers event listeners to keep the entity up to date. | ||
8. Server emits a `login` event with the token (can only be listened for server side) | ||
9. Server sends the signed JWT access token back to the client | ||
|
||
### Authenticating with Access Token over Ajax | ||
|
||
1. Client `POST`s JWT access token to `/authentication` over Ajax in request body. | ||
2. Server verifies token isn't expired, hasn't been tampered with and is a valid JWT format | ||
3. Server decodes the token and passes the payload down the authentication chain | ||
4. Server verifies the token is not in the blacklist or is in the whitelist, depending on how you want to set up things (this is not implemented so far). | ||
5. Server looks up entity in the database by entity ID from the payload | ||
6. Server emits a `login` event with the token (can only be listened for server side) | ||
7. Server returns the same access token to the client. **This is not currently what happens and is what should happen.** Instead, the server creates a new JWT access token with the entity's database ID encoded in the JWT payload and sends the new signed JWT access token back to the client. **This is a security risk because an attacker could, with one compromised valid JWT, generate an infinite number of valid JWTs!** | ||
|
||
### Authenticating with Access Token over Sockets | ||
|
||
1. Client establishes socket connection | ||
2. Client sends an `authenticate` message with the access token in the message body | ||
3. Server verifies token isn't expired, hasn't been tampered with and is a valid JWT format | ||
4. Server decodes the token and passes the payload down the authentication chain | ||
5. Server verifies the token is not in the blacklist or is in the whitelist, depending on how you want to set up things (this is not implemented so far). | ||
6. Server looks up entity in the database by entity ID from the payload | ||
Server attaches the token to the socket | ||
7. Server attaches the entity to the socket and registers event listeners to keep the entity up to date. | ||
8. Server emits a `login` event with the token (can only be listened for server side) | ||
9. Server returns the same access token to the client. **This is not currently what happens and is what should happen.** Instead, the server creates a new JWT access token with the entity's database ID encoded in the JWT payload and sends the new signed JWT access token back to the client. **This is a security risk because an attacker could, with one compromised valid JWT, generate an infinite number of valid JWTs!** | ||
|
||
### Authenticating with Refresh Token over Ajax | ||
|
||
TODO | ||
|
||
### Authenticating with Refresh Token over Sockets | ||
|
||
TODO | ||
|
||
### Authorizing Access Token over HTTP Authorization Header | ||
|
||
1. Client sends JWT access token to any endpoint in an `Authorization` header. | ||
2. Server parses the token from the header if it exists and attaches it to the request object. | ||
3. Server verifies token isn't expired, hasn't been tampered with and is a valid JWT format | ||
4. Server decodes the token and passes the payload down the authentication chain | ||
5. Server verifies the token is not in the blacklist or is in the whitelist, depending on how you want to set up things (this is not implemented so far). | ||
6. Server looks up entity in the database by entity ID from the payload | ||
7. Server checks entity permissions | ||
8. Server processes the request | ||
|
||
### Authorizing Access Token over Sockets | ||
|
||
1. Client has already authenticated so the access token is attached to the socket object on server side. | ||
2. Client sends any message to the server | ||
3. Server verifies token isn't expired, hasn't been tampered with and is a valid JWT format | ||
4. Server decodes the token and passes the payload down the authentication chain | ||
5. Server verifies the token is not in the blacklist or is in the whitelist, depending on how you want to set up things (this is not implemented so far). | ||
6. Server looks up entity in the database by entity ID from the payload | ||
7. Server checks entity permissions | ||
8. Server processes the request | ||
|
||
### Old School Server Side Template | ||
|
||
This utilizes an `httpOnly` cookie with the same expiration as the access token. | ||
|
||
### Server Side Rendering/Universal app flow | ||
|
||
This utilizes a cookie. | ||
|
||
TODO | ||
|
||
### Authenticating and Redirecting to another domain | ||
|
||
Utilizes putting token in the querystring or a cookie scoped to the subdomain. | ||
|
||
### API Key Authentication | ||
|
||
TODO: Might not be needed or would simply be a ClientID | ||
|
||
### Regular OAuth | ||
|
||
TODO | ||
|
||
### Custom Passport Authentication | ||
|
||
TODO | ||
|
||
### Access Token Based OAuth | ||
|
||
In most cases this is for IoT devices or mobile devices. The user has already granted your application access to their profile through another mechanism that the regular OAuth HTTP redirects (ie. via a mobile app or CLI). The client sends their profile, OAuth `access_token` and possibly `refresh_token` to your server. The server verifies these with the OAuth authentication provider (ie. Facebook). |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
# Common Authentication Data Flows | ||
|
||
## Authorization Creation (ie. Login) | ||
1. Client authenticates without JWT. (ie. email/password, oauth, etc) | ||
2. Invalidate any existing authorizations and refresh tokens for that user/client combo. | ||
3. Create a new authorization record in the database with | ||
- id (unique) | ||
- entityId (unique) | ||
- clientId (unique) | ||
- permissions (maybe if they get default permissions) | ||
|
||
_A authorization can have any number of verifying ids/values (ie. we could also add an organizationId, MFA pin, etc. into the mix)_ | ||
|
||
4. Create an access token (JWT) with the verification values/ids: | ||
- authorizationId | ||
- entityId (verification values) | ||
- clientId | ||
|
||
**NOTE:** This could now actually have a long TTL | ||
5. Create a refresh token with a very long TTL | ||
- refreshId | ||
- entityId (verification values) | ||
- clientId | ||
6. Send back the access token (JWT) and refresh token (JWT) to the client | ||
|
||
## Access Token Verification | ||
The access token (JWT) is passed via an `Authorization` header **or** is attached to the connected socket. These are requests made to anything but the `/authentication` service. | ||
|
||
1. Client sends valid JWT access token or it's attached to the connected socket | ||
2. Server verifies JWT (in our special auth middleware) to ensure it is not: | ||
- expired (then it removes or flags authorization as revoked/expired) | ||
- malformed | ||
- tampered with | ||
3. Server decodes the token payload and passes it down the auth middleware chain | ||
4. Server looks up authorization record by the `authorizationId` and passes it down the auth middleware chain (populateAuthorization) | ||
5. Your own custom auth middleware validates the authorization | ||
- if valid then proceed to the service before hooks or any other auth middleware. | ||
- if invalid set `data.authenticated = false` and move on to next middleware | ||
6. Service `before` hook, `isAuthenticated` checks to see if the request is authenticated. | ||
- If it is proceed to next hook or service method | ||
- If it isn't it rejects with a `NotAuthenticated` error | ||
7. Service `before` hook, `isPermitted` checks to see if the request is authenticated. | ||
- If it is proceed to next hook or service method | ||
- If it isn't it rejects with a `Forbidden` error | ||
8. Service method is called and returns through after hooks | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Steps 6 and 7 above seem too much alike? afaik, 401 Not authenticated is meant for when the client has not yet presented any credentials (so pre-login), whereas 403 Forbidden is meant for clients that did show credentials (so post-login) but are simply not allowed access. So I would expect the one hook to check whether the client is authenticated, whereas the other should check whether he is authorized to perform the action. |
||
9. Response is sent back to the client | ||
|
||
## Logout (Not Expired Token) | ||
|
||
1. Client `DELETE` to `/authentication/:authorization_id` service with a valid JWT access token. | ||
2. Server verifies the token | ||
3. Looks up the authorization | ||
4. Calls `remove` on the `authorization` service | ||
5. Calls `app.emit('logout)` | ||
6. Responds with success | ||
7. Client can do whatever it wants (typically redirect to somewhere) | ||
|
||
## Logout (Expired Token) | ||
|
||
1. Client `DELETE` to `/authentication/:authorization_id` service with a expired JWT access token. | ||
2. Server recognizes the token is expired (removes or flags authorization as revoked/expired) | ||
3. Returns a `TokenExpired` error to the client | ||
4. Client can do whatever it wants (typically redirect to somewhere) | ||
|
||
## Authorization Renewal/Extension without Refresh Token | ||
An expired access token (JWT) is passed via an `Authorization` header **or** is attached to the connected socket. These are requests made to anything but the `/authentication` service. | ||
|
||
1. Client sends expired JWT access token or it's attached to the connected socket | ||
2. Server verifies JWT and determines it is expired | ||
- returns a `NotAuthenticated` error with a "token expired" message to the client (current) | ||
- **Proposed Change:** Return a special error type of `TokenExpired` and flags authorization as expired/revoked or delete it altogether. | ||
3. Client handles the error however they want (typically a redirect to login again) | ||
- The client could also send a refresh token, or ClientId and ClientSecret to automatically get a new JWT access token depending on what is applicable to your app. | ||
|
||
## Authorization Renewal/Extension via Refresh Token | ||
An expired access token (JWT) is passed via an `Authorization` header **or** is attached to the connected socket. These are requests made to anything but the `/authentication` service. | ||
|
||
1. Client sends expired JWT access token or it's attached to the connected socket | ||
2. Server verifies JWT and determines it is expired | ||
- returns a `NotAuthenticated` error with a "token expired" message to the client (current) | ||
- **Proposed Change:** Return a special error type of `TokenExpired` and flags authorization as expired/revoked | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Actually I would think 401 Not Authenticated would be the correct response (at least HTTP status code wise). The refresh token is effectively a form of credentials (so login, but automated) |
||
3. Client sends a valid refresh token to `/authentication` service | ||
4. Server verifies the refresh token to ensure it has not | ||
- expired | ||
- malformed | ||
- has not been tampered with | ||
5. Server decodes the payload | ||
6. Server looks up refreshId on some service (TBD) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should be the aauthorizations collection |
||
- If valid goes through access token creation flow which deletes the `refreshId` from the database. | ||
- Else returns a `NotAuthenticated` error |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not stateless :(
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Download I think the idea here is that we will allow you to have the ability to retain statelessness however we also need to support identity provider that require state. There will be a callback here that you can implement, or to make this decision.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Download maybe @ekryski or @daffl can clarify for me if I'm mistaken, but the auth middleware chain will be called like a normal hook. This will allow you to pass params that will customize which middleware will run. You have complete control over the middleware. Here's a contrived example:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@marshallswain is right you will be able to pass options and do whatever you want in your authentication chain by registering whatever verification hooks you would like.
As a side note, @marshallswain they aren't called middleware anymore. They really are just special hooks used to do verification in the authentication chain. Which can be stateless (ie. just using the JWT verify function) or can be stateful (ie. you can look up whatever you want in order to do verification).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's great to hear. Thanks for clarifying!