Skip to content

Commit

Permalink
Add authorization update API call; updated docs
Browse files Browse the repository at this point in the history
  • Loading branch information
danielgtaylor committed Jul 15, 2013
1 parent c066f4d commit eede73a
Show file tree
Hide file tree
Showing 4 changed files with 277 additions and 11 deletions.
93 changes: 92 additions & 1 deletion apidoc.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,8 @@ Authorizations API
------------------
The authorizations API is a bit different from other calls in Tapline, in that it requires HTTP basic auth along with third party client information for every request instead of using OAuth bearer tokens. This means that the authorizations API requires that you temporarily collect a user's name and password until you can get an OAuth2 bearer token.

The authorizations API is heavily inspired by the [Github API](http://developer.github.com/v3/oauth/).

### Create an Authorization Token
Create a new authorization for a third party client and a specific user. The response will give you an OAuth bearer token to use for authorized API requests on behalf of that user with the given scopes, if any. The request __must__ use HTTP basic auth (the `Authorization` header below) using the user's `name` and `password`, as well as including the `clientId` and `clientSecret` of the registered third party client.

Expand Down Expand Up @@ -232,7 +234,96 @@ X-Response-Time: 100ms
"created": "2013-07-11T17:28:52.787Z",
"id": "51deeb54763ce70000000001",
"scopes": [
"user",
"profile",
"recipe"
],
"token": "b608d4b097c838067aba07eb9206faab1bf4b446",
"userId": "51de54131084ffeef8000001"
}
```

#### Errors

| Code | Description |
| ---- | -------------------------------------- |
| 400 | Invalid request arguments |
| 401 | Invalid user/password or client/secret |
| 500 | Internal server error |

### Listing Authorization Tokens
List a third-party client's authorization tokens for a specific user. The responses will give you a OAuth bearer tokens to use for authorized API requests on behalf of that user with the given scopes, if any. The request __must__ use HTTP basic auth (the `Authorization` header below) using the user's `name` and `password`, as well as including the `clientId` and `clientSecret` of the registered third party client.

#### Request
```http
GET /v1/authorizations.json?clientId=abc123&clientSecret=some-secret HTTP/1.1
Content-Type: application/json
Authorization: Basic ZGFuaWVsOmFiYzEyMw==
```

#### Response
```http
HTTP/1.1 200 OK
Content-Type: application/json
X-Request-ID: 64a359b2
X-Response-Time: 100ms
[
{
"clientId": "abc123",
"created": "2013-07-11T17:28:52.787Z",
"id": "51deeb54763ce70000000001",
"scopes": [
"profile",
"recipe"
],
"token": "b608d4b097c838067aba07eb9206faab1bf4b446",
"userId": "51de54131084ffeef8000001"
}
]
```

#### Errors

| Code | Description |
| ---- | -------------------------------------- |
| 400 | Invalid request arguments |
| 401 | Invalid user/password or client/secret |
| 500 | Internal server error |

### Update an Authorization Token
Update an existing authorization by its unique id. The request __must__ use HTTP basic auth (the `Authorization` header below) using the user's `name` and `password`, as well as including the `clientId` and `clientSecret` of the registered third party client.

Scopes can be set to a new list via `scopes`, appended to via `addScopes` or removed from via `removeScopes`. One of these three is required. Passing more than one will result in an error.

#### Request
```http
PUT /v1/authorizations/51deeb54763ce70000000001.json HTTP/1.1
Content-Type: application/json
Authorization: Basic ZGFuaWVsOmFiYzEyMw==
{
"clientId": "abc123",
"clientSecret": "some-secret",
"addScopes": [
"profile:delete"
]
}
```

#### Response
```http
HTTP/1.1 200 OK
Content-Type: application/json
X-Request-ID: 64a359b2
X-Response-Time: 100ms
{
"clientId": "abc123",
"created": "2013-07-11T17:28:52.787Z",
"id": "51deeb54763ce70000000001",
"scopes": [
"profile",
"profile:delete",
"recipe"
],
"token": "b608d4b097c838067aba07eb9206faab1bf4b446",
Expand Down
64 changes: 56 additions & 8 deletions src/controllers/authorizations.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,28 @@ createSchema = jsonGate.createSchema
type: 'string'
default: []

updateSchema = jsonGate.createSchema
type: 'object'
properties:
clientId:
type: 'string'
required: true
clientSecret:
type: 'string'
required: true
scopes:
type: 'array'
items:
type: 'string'
addScopes:
type: 'array'
items:
type: 'string'
removeScopes:
type: 'array'
items:
type: 'string'

# Get the client, making sure it exists and the secrets match up
getClient = (key, secret, done) ->
Client.findOne {key}, (err, client) ->
Expand All @@ -39,6 +61,23 @@ getClient = (key, secret, done) ->

done(null, client)

authController.create = (req, res) ->
createSchema.validate req.body, (err, data) ->
if err then return res.send(400, err.toString())

getClient data.clientId, data.clientSecret, (err, client) ->
if err then return res.send(client, err)

auth = new Authorization
userId: req.user.id
clientId: data.clientId
scopes: data.scopes

auth.save (err, auth) ->
if err then return res.send(500, err.toString())

res.json 201, auth

authController.list = (req, res) ->
listSchema.validate req.query, (err, data) ->
if err then return res.send(400, err.toString())
Expand All @@ -55,19 +94,28 @@ authController.list = (req, res) ->

res.json auths

authController.create = (req, res) ->
createSchema.validate req.body, (err, data) ->
authController.update = (req, res) ->
updateSchema.validate req.body, (err, data) ->
if err then return res.send(400, err.toString())

count = [data.scopes, data.addScopes, data.removeScopes].filter((x) -> x).length

if count is 0
return res.send(400, 'Must supply at least one of scopes, addScopes or removeScopes')
else if count > 1
return res.send(400, 'Only one of scopes, addScopes or removeScopes can be given')

getClient data.clientId, data.clientSecret, (err, client) ->
if err then return res.send(client, err)

auth = new Authorization
userId: req.user.id
clientId: data.clientId
scopes: data.scopes
update = {}

auth.save (err, auth) ->
if data.scopes then update.$set = {scopes: data.scopes}
if data.addScopes then update.$addToSet = {scopes: {$each: data.addScopes}}
if data.removeScopes then update.$pullAll = {scopes: data.removeScopes}

# Find and update an entry
Authorization.findByIdAndUpdate req.params.id, update, (err, auth) ->
if err then return res.send(500, err.toString())

res.json 201, auth
res.json auth
3 changes: 2 additions & 1 deletion src/server.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,9 @@ app.post '/v1/calculate/recipe.json', recipeCalculateController.calculate
app.post '/v1/users.json', userController.create

# Basic authenticated routes
app.get '/v1/authorizations.json', authBasic, authController.list
app.post '/v1/authorizations.json', authBasic, authController.create
app.get '/v1/authorizations.json', authBasic, authController.list
app.put '/v1/authorizations/:id.json', authBasic, authController.update

# OAuth2 Authenticated routes
app.get '/v1/users.json', authBearer(), userController.list
Expand Down
128 changes: 127 additions & 1 deletion test/authorizations.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ util = require '../lib/util'

{app} = require '../lib/server'

Authorization = require '../lib/models/authorization'

authInfo = {}
newAuthId = null

describe '/v1/authorizations.json', ->
before (done) ->
Expand All @@ -22,7 +25,9 @@ describe '/v1/authorizations.json', ->
done()

after (done) ->
db.close done
Authorization.remove {_id: newAuthId}, (err) ->
if err then return done(err)
db.close done

describe 'Create a new authorization', ->
it 'Should return JSON on success', (done) ->
Expand All @@ -36,6 +41,9 @@ describe '/v1/authorizations.json', ->
if err then return done(err)

assert.ok res.body
assert.ok res.body.token

newAuthId = res.body.id

done()

Expand All @@ -59,6 +67,20 @@ describe '/v1/authorizations.json', ->
.send(clientId: authInfo.client.key, clientSecret: 'invalid')
.expect 401, done

it 'Should require client id', (done) ->
request(app)
.post('/v1/authorizations.json')
.auth(authInfo.user.name, 'abc123')
.send(clientSecret: authInfo.client.secret)
.expect 400, done

it 'Should require client secret', (done) ->
request(app)
.post('/v1/authorizations.json')
.auth(authInfo.user.name, 'abc123')
.send(clientId: authInfo.client.key)
.expect 400, done

describe 'List authorizations', ->
it 'Should return JSON on success', (done) ->
request(app)
Expand Down Expand Up @@ -94,3 +116,107 @@ describe '/v1/authorizations.json', ->
.query(clientId: authInfo.client.key, clientSecret: 'invalid')
.expect 401, done

it 'Should require client id', (done) ->
request(app)
.get('/v1/authorizations.json')
.auth(authInfo.user.name, 'abc123')
.query(clientSecret: authInfo.client.secret)
.expect 400, done

it 'Should require client secret', (done) ->
request(app)
.get('/v1/authorizations.json')
.auth(authInfo.user.name, 'abc123')
.query(clientId: authInfo.client.key)
.expect 400, done

describe 'Update authorizations', ->
it 'Should return JSON on success', (done) ->
request(app)
.put("/v1/authorizations/#{newAuthId}.json")
.auth(authInfo.user.name, 'abc123')
.send(clientId: authInfo.client.key, clientSecret: authInfo.client.secret, scopes: ['test1', 'test2'])
.expect('Content-Type', /json/)
.expect(200)
.end (err, res) ->
if err then return done(err)

assert.ok res.body

done()

it 'Should return error on missing basic auth', (done) ->
request(app)
.put("/v1/authorizations/#{newAuthId}.json")
.send(clientId: authInfo.client.key, clientSecret: authInfo.client.secret, scopes: [])
.expect 401, done

it 'Should return error on invalid client id', (done) ->
request(app)
.put("/v1/authorizations/#{newAuthId}.json")
.auth(authInfo.user.name, 'abc123')
.send(clientId: 'invalid', clientSecret: authInfo.client.secret, scopes: [])
.expect 401, done

it 'Should return error on invalid client secret', (done) ->
request(app)
.put("/v1/authorizations/#{newAuthId}.json")
.auth(authInfo.user.name, 'abc123')
.send(clientId: authInfo.client.key, clientSecret: 'invalid', scopes: [])
.expect 401, done

it 'Should require client id', (done) ->
request(app)
.put("/v1/authorizations/#{newAuthId}.json")
.auth(authInfo.user.name, 'abc123')
.send(clientSecret: authInfo.client.secret, scopes: [])
.expect 400, done

it 'Should require client secret', (done) ->
request(app)
.put("/v1/authorizations/#{newAuthId}.json")
.auth(authInfo.user.name, 'abc123')
.send(clientId: authInfo.client.key, scopes: [])
.expect 400, done

it 'Should require scopes', (done) ->
request(app)
.put("/v1/authorizations/#{newAuthId}.json")
.auth(authInfo.user.name, 'abc123')
.send(clientId: authInfo.client.key, clientSecret: authInfo.client.secret)
.expect 400, done

it 'Should set scopes', (done) ->
request(app)
.put("/v1/authorizations/#{newAuthId}.json")
.auth(authInfo.user.name, 'abc123')
.send(clientId: authInfo.client.key, clientSecret: authInfo.client.secret, scopes: ['test1'])
.expect 200, done

it 'Should add scopes', (done) ->
request(app)
.put("/v1/authorizations/#{newAuthId}.json")
.auth(authInfo.user.name, 'abc123')
.send(clientId: authInfo.client.key, clientSecret: authInfo.client.secret, addScopes: ['test2', 'test3'])
.expect(200)
.end (err, res) ->
items = ['test1', 'test2', 'test3']

for x in [0...items.length]
assert.equal items[x], res.body.scopes[x]

done()

it 'Should remove scopes', (done) ->
request(app)
.put("/v1/authorizations/#{newAuthId}.json")
.auth(authInfo.user.name, 'abc123')
.send(clientId: authInfo.client.key, clientSecret: authInfo.client.secret, removeScopes: ['test2', 'test3'])
.expect(200)
.end (err, res) ->
items = ['test1']

for x in [0...items.length]
assert.equal items[x], res.body.scopes[x]

done()

0 comments on commit eede73a

Please sign in to comment.