Skip to content

Commit

Permalink
Update default auth
Browse files Browse the repository at this point in the history
- Remove dependency on `useAuth` API config option for endpoint
  authentication (#49)
- Rename API config option `useAuth` to `useDefaultAuth` to better
  reflect it's purpose, which is to generate the default auth endpoints
  - `useAuth` will still work, for backwards-compatibility
- Return a 401 error when auth info is not provided in endpoint
  requiring default auth (#91)
  • Loading branch information
kahmali committed Jun 29, 2015
1 parent 6ff8a33 commit 5bf092b
Show file tree
Hide file tree
Showing 5 changed files with 131 additions and 47 deletions.
17 changes: 12 additions & 5 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,33 +10,38 @@

#### Changed
- Replace Iron Router with [JSON Routes]
(https://github.com/stubailo/meteor-rest/tree/master/packages/json-routes)! (see [#38][])
(https://github.com/stubailo/meteor-rest/tree/master/packages/json-routes)! ([#38][])
- Export Restivus as a "class" instead of an object
- Remove the non-standard `deleteAll` collection endpoint, as it had the potential to be quite
destructive (see [#47][])
- Return standard response codes in any autogenerated endpoints (see [#48][])
destructive ([#47][])
- Return standard response codes in any autogenerated endpoints ([#48][])
- `201`: Resource created (in `POST` collection endpoints)
- `403`: Role permission errors
- `405`: API method not found (but route exists)
- Remove dependency on `useAuth` API config option for endpoint authentication ([#49][])
- Rename API config option `useAuth` to `useDefaultAuth` to better reflect it's purpose, which is to
generate the default auth endpoints (`useAuth` will still work, for backwards-compatibility)

#### Fixed
- Iron Router related issues
- Issue [#24][]: Interference with other routers
- Issue [#35][]: App hangs on iOS
- Issue [#43][]: Deployed app shows `iron:router` welcome screen
- Issue [#91][]: Return a `401` error when auth info is not provided in endpoint requiring default
auth (previously returned `500` error)


## [v0.7.0] - 2015-06-18

**_WARNING!_ Potentially breaking changes! Please be aware when upgrading!**

#### Changed
- Update default auth endpoints to match current Accounts token storage (see [#79][])
- Update default auth endpoints to match current Accounts token storage ([#79][])
- **_WARNING!_ All clients consuming a Restivus API _with the default authentication_ will need to
reauthenticate after this update**
- Login token is now stored as `hashedToken` instead of `token`
- Return `401 Unauthorized` for failed authentication
- When logging in with bad credentials, randomly delay the response (see [#81][])
- When logging in with bad credentials, randomly delay the response ([#81][])
- Declare dependency on `accounts-base` package


Expand Down Expand Up @@ -310,6 +315,8 @@
[#43]: https://github.com/kahmali/meteor-restivus/issues/43 "Issue #43"
[#47]: https://github.com/kahmali/meteor-restivus/issues/47 "Issue #47"
[#48]: https://github.com/kahmali/meteor-restivus/issues/48 "Issue #48"
[#49]: https://github.com/kahmali/meteor-restivus/issues/49 "Issue #49"
[#68]: https://github.com/kahmali/meteor-restivus/issues/68 "Issue #68"
[#79]: https://github.com/kahmali/meteor-restivus/issues/79 "Issue #79"
[#81]: https://github.com/kahmali/meteor-restivus/issues/81 "Issue #81"
[#91]: https://github.com/kahmali/meteor-restivus/issues/91 "Issue #91"
38 changes: 22 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ if Meteor.isServer

# Global API configuration
Api = new Restivus
useAuth: true
useDefaultAuth: true
prettyJson: true

# Generates: GET, POST on /api/items and GET, PUT, DELETE on
Expand Down Expand Up @@ -137,7 +137,7 @@ if (Meteor.isServer) {

// Global API configuration
var Api = new Restivus({
useAuth: true,
useDefaultAuth: true,
prettyJson: true
});

Expand Down Expand Up @@ -336,11 +336,11 @@ The following configuration options are available when initializing an API using
- Default: `false`
- If `true`, render formatted JSON in response.

##### `useAuth`
##### `useDefaultAuth`
- _Boolean_
- Default: `false`
- If `true`, `POST /login` and `GET /logout` endpoints are added to the API. You can access
`this.user` and `this.userId` in [authenticated](#authenticating) endpoints.
- If `true`, `POST /login` and `GET /logout` endpoints are added to the API. See [Authenticating]
(#authenticating) for details on using these endpoints.

##### `version`
- _String_
Expand Down Expand Up @@ -378,7 +378,7 @@ configuration is not recommended):
onLoggedIn: -> console.log "#{@user.username} (#{@userId}) logged in"
onLoggedOut: -> console.log "#{@user.username} (#{@userId}) logged out"
prettyJson: true
useAuth: true
useDefaultAuth: true
version: 'v1'
```

Expand All @@ -405,7 +405,7 @@ configuration is not recommended):
console.log(this.user.username + ' (' + this.userId + ') logged out');
},
prettyJson: true,
useAuth: true,
useDefaultAuth: true,
version: 'v1'
});
```
Expand Down Expand Up @@ -979,13 +979,13 @@ Each endpoint has access to:

##### `this.user`
- _Meteor.user_
- The authenticated `Meteor.user`. Only available if `useAuth` and `authRequired` are both `true`.
If not, it will be `undefined`.
- The authenticated `Meteor.user`. Only available if `authRequired` is `true` and a user is
successfully authenticated. If not, it will be `undefined`.

##### `this.userId`
- _String_
- The authenticated user's `Meteor.userId`. Only available if `useAuth` and `authRequired` are both
`true`. If not, it will be `undefined`.
- The authenticated user's `Meteor.userId`. Only available if `authRequired` is `true` and a user is
successfully authenticated. If not, it will be `undefined`.

##### `this.urlParams`
- _Object_
Expand Down Expand Up @@ -1090,14 +1090,16 @@ their own convenience, while providing the latest and greatest API to those read
Currently, there is only a single versioning strategy supported in Restivus: URL path versioning. In
this strategy, the version of the API is appended to the base path of all routes belonging to that
API. This allows us to easily maintain multiple versions of an API, each with their own set of
configuration options. Here's a [good write-up](http://www.troyhunt.com/2014/02/your-api-versioning-is-wrong-which-is.html) on some of the different API versioning strategies.
configuration options. Here's a [good write-up]
(http://www.troyhunt.com/2014/02/your-api-versioning-is-wrong-which-is.html) on some of the
different API versioning strategies.

###### CoffeeScript
```coffeescript
# Configure first version of the API
ApiV1 = new Restivus
version: 'v1'
useAuth: true
useDefaultAuth: true
prettyJson: true

# Maps to api/v1/items and api/v1/items/:id
Expand Down Expand Up @@ -1129,7 +1131,7 @@ ApiV2.addRoute 'custom',
// Configure first version of the API
var ApiV1 = new Restivus({
version: 'v1',
useAuth: true,
useDefaultAuth: true,
prettyJson: true
});

Expand Down Expand Up @@ -1188,10 +1190,14 @@ curl -d "message=Some message details" http://localhost:3000/api/posts/3/comment

**Warning: Make sure you're using HTTPS, otherwise this is insecure!**

If you have `useAuth` set to `true`, you now have a `POST /api/login` endpoint that returns a
_Note: To use the default authentication, you must first [create a user with the `accounts-password`
package](http://docs.meteor.com/#/full/accounts_passwords). You can do this with Restivus if you
[setup a POST collection endpoint for the `Meteor.users` collection](#users-collection-endpoints)._

If you have `useDefaultAuth` set to `true`, you now have a `POST /api/login` endpoint that returns a
`userId` and `authToken`. You must save these, and include them in subsequent requests.

```bash
```bash
curl http://localhost:3000/api/login/ -d "password=testpassword&user=test"
```

Expand Down
10 changes: 6 additions & 4 deletions lib/restivus.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,17 @@ class @Restivus
@_routes = []
@_config =
paths: []
useAuth: false
useDefaultAuth: false
apiPath: 'api/'
version: null
prettyJson: false
auth:
token: 'services.resume.loginTokens.hashedToken'
user: ->
if @request.headers['x-auth-token']
token = Accounts._hashLoginToken @request.headers['x-auth-token']
userId: @request.headers['x-user-id']
token: Accounts._hashLoginToken @request.headers['x-auth-token']
token: token
onLoggedIn: -> {}
onLoggedOut: -> {}
defaultHeaders:
Expand All @@ -37,8 +39,8 @@ class @Restivus
if @_config.version
@_config.apiPath += @_config.version + '/'

# Add default login and logout endpoints if auth is configured
if @_config.useAuth
# Add default login and logout endpoints if auth is configured (legacy config name: useAuth)
if @_config.useDefaultAuth or @_config.useAuth
@_initAuth()

return this
Expand Down
10 changes: 4 additions & 6 deletions lib/route.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -101,9 +101,9 @@ class @Route
Configure the authentication and role requirement on all endpoints (except OPTIONS, which must
be configured directly on the endpoint)
Once it's globally configured in the API, authentication can be required on an entire route or
individual endpoints. If required on an entire route, that serves as the default. If required in
any individual endpoints, that will override the default.
Authentication can be required on an entire route or individual endpoints. If required on an
entire route, that serves as the default. If required in any individual endpoints, that will
override the default.
After the endpoint is configured, all authentication and role requirements of an endpoint can be
accessed at <code>endpoint.authRequired</code> and <code>endpoint.roleRequired</code>,
Expand All @@ -126,9 +126,7 @@ class @Route
endpoint.roleRequired = false

# Configure auth requirement
if not @api._config.useAuth
endpoint.authRequired = false
else if endpoint.authRequired is undefined
if endpoint.authRequired is undefined
if @options?.authRequired or endpoint.roleRequired
endpoint.authRequired = true
else
Expand Down
103 changes: 87 additions & 16 deletions test/authentication_tests.coffee
Original file line number Diff line number Diff line change
@@ -1,19 +1,47 @@
DefaultAuthApi = new Restivus
apiPath: 'default-auth'
useDefaultAuth: true
auth:
token: 'services.resume.loginTokens.hashedToken'
user: ->
userId: @request.headers['x-user-id']
token: Accounts._hashLoginToken @request.headers['x-auth-token']

NoDefaultAuthApi = new Restivus
apiPath: 'no-default-auth'
useDefaultAuth: false

LegacyDefaultAuthApi = new Restivus
apiPath: 'legacy-default-auth'
useAuth: true

LegacyNoDefaultAuthApi = new Restivus
apiPath: 'legacy-no-default-auth'
useAuth: false

describe 'Authentication', ->

it 'can be required even when the default endpoints aren\'t configured', (test, waitFor) ->
NoDefaultAuthApi.addRoute 'require-auth', { authRequired: true },
get: ->
data: 'test'
startTime = new Date()
HTTP.get Meteor.absoluteUrl('no-default-auth/require-auth'), waitFor (error, result) ->
response = result.data
test.isTrue error
test.equal result.statusCode, 401
test.equal response.status, 'error'
durationInMilliseconds = new Date() - startTime
# Check for security delay for failed auth
test.isTrue durationInMilliseconds >= 500

describe 'The default authentication endpoints', ->
token = null
emailLoginToken = null
username = 'test'
email = 'test@ivus.com'
password = 'password'

AuthApi = new Restivus
apiPath: 'auth/v1'
useAuth: true
auth:
token: 'services.resume.loginTokens.hashedToken'
user: ->
userId: @request.headers['x-user-id']
token: Accounts._hashLoginToken @request.headers['x-auth-token']

# Delete the test account if it's still present
Meteor.users.remove username: username

Expand All @@ -23,8 +51,51 @@ describe 'The default authentication endpoints', ->
password: password
}

it 'should only be available when configured', (test, waitFor) ->
HTTP.post Meteor.absoluteUrl('default-auth/login'), {
data:
user: username
password: password
}, waitFor (error, result) ->
response = result.data
test.equal result.statusCode, 200
test.equal response.status, 'success'
test.equal response.data.userId, userId
test.isTrue response.data.authToken

HTTP.post Meteor.absoluteUrl('no-default-auth/login'), {
data:
user: username
password: password
}, waitFor (error, result) ->
response = result.data
test.isUndefined response?.data?.userId
test.isUndefined response?.data?.authToken

HTTP.post Meteor.absoluteUrl('legacy-default-auth/login'), {
data:
user: username
password: password
}, waitFor (error, result) ->
response = result.data
test.equal result.statusCode, 200
test.equal response.status, 'success'
test.equal response.data.userId, userId
test.isTrue response.data.authToken

HTTP.post Meteor.absoluteUrl('legacy-no-default-auth/login'), {
data:
user: username
password: password
}, waitFor (error, result) ->
response = result.data
test.isUndefined response?.data?.userId
test.isUndefined response?.data?.authToken



it 'should allow a user to login', (test, waitFor) ->
HTTP.post Meteor.absoluteUrl('auth/v1/login'), {
HTTP.post Meteor.absoluteUrl('default-auth/login'), {
data:
user: username
password: password
Expand All @@ -40,7 +111,7 @@ describe 'The default authentication endpoints', ->


it 'should allow a user to login again, without affecting the first login', (test, waitFor) ->
HTTP.post Meteor.absoluteUrl('auth/v1/login'), {
HTTP.post Meteor.absoluteUrl('default-auth/login'), {
data:
user: email
password: password
Expand All @@ -59,7 +130,7 @@ describe 'The default authentication endpoints', ->
it 'should not allow a user with wrong password to login and should respond after 500 msec', (test, waitFor) ->
# This test should take 500 msec or more. To speed up testing, these two tests have been combined.
startTime = new Date()
HTTP.post Meteor.absoluteUrl('auth/v1/login'), {
HTTP.post Meteor.absoluteUrl('default-auth/login'), {
data:
user: username
password: "NotAllowed"
Expand All @@ -72,7 +143,7 @@ describe 'The default authentication endpoints', ->


it 'should allow a user to logout', (test, waitFor) ->
HTTP.get Meteor.absoluteUrl('auth/v1/logout'), {
HTTP.get Meteor.absoluteUrl('default-auth/logout'), {
headers:
'X-User-Id': userId
'X-Auth-Token': token
Expand All @@ -82,11 +153,11 @@ describe 'The default authentication endpoints', ->
test.equal response.status, 'success'

it 'should remove the logout token after logging out and should respond after 500 msec', (test, waitFor) ->
AuthApi.addRoute 'prevent-access-after-logout', {authRequired: true},
DefaultAuthApi.addRoute 'prevent-access-after-logout', {authRequired: true},
get: -> true
# This test should take 500 msec or more. To speed up testing, these two tests have been combined.
startTime = new Date()
HTTP.get Meteor.absoluteUrl('auth/v1/prevent-access-after-logout'), {
HTTP.get Meteor.absoluteUrl('default-auth/prevent-access-after-logout'), {
headers:
'X-User-Id': userId
'X-Auth-Token': token
Expand All @@ -99,7 +170,7 @@ describe 'The default authentication endpoints', ->
test.isTrue durationInMilliseconds >= 500

it 'should allow a second logged in user to logout', (test, waitFor) ->
HTTP.get Meteor.absoluteUrl('auth/v1/logout'), {
HTTP.get Meteor.absoluteUrl('default-auth/logout'), {
headers:
'X-User-Id': userId
'X-Auth-Token': emailLoginToken
Expand Down

0 comments on commit 5bf092b

Please sign in to comment.