Skip to content
This repository has been archived by the owner on Apr 3, 2019. It is now read-only.

Commit

Permalink
Merge pull request #152 from mozilla/register
Browse files Browse the repository at this point in the history
Implements Client Management Endpoints
  • Loading branch information
seanmonstar committed Oct 20, 2014
2 parents 4461508 + 1a80294 commit 98f41f7
Show file tree
Hide file tree
Showing 21 changed files with 818 additions and 79 deletions.
166 changes: 165 additions & 1 deletion docs/api.md
Expand Up @@ -43,6 +43,8 @@ The currently-defined error responses are:
| 400 | 108 | invalid token |
| 400 | 109 | invalid request parameter |
| 400 | 110 | invalid response_type |
| 401 | 111 | unauthorized |
| 403 | 112 | forbidden |
| 500 | 999 | internal server error |

## API Endpoints
Expand All @@ -52,7 +54,12 @@ The currently-defined error responses are:
- [POST /v1/authorization][authorization]
- [POST /v1/token][token]
- [POST /v1/destroy][delete]
- [GET /v1/client/:id][client]
- Clients
- [GET /v1/client/:id][client]
- [GET /v1/clients][clients]
- [POST /v1/client][register]
- [POST /v1/client/:id][client-update]
- [DELETE /v1/client/:id][client-delete]
- [POST /v1/verify][verify]

### GET /v1/client/:id
Expand Down Expand Up @@ -88,6 +95,159 @@ A valid 200 response will be a JSON blob with the following properties:
}
```

### GET /v1/clients

Get a list of all registered clients.

**Required scope:** `oauth`

#### Request

**Example:**


```sh
curl -v \
-H "Authorization: Bearer 558f9980ad5a9c279beb52123653967342f702e84d3ab34c7f80427a6a37e2c0" \
"https://oauth.accounts.firefox.com/v1/clients"
```

#### Response

A valid 200 response will be a JSON object with a property of `clients`,
which contains an array of client objects.

**Example:**

```json
{
"clients": [
{
"id": "5901bd09376fadaa",
"name": "Example",
"redirect_uri": "https://ex.am.ple/path",
"image_uri": "https://ex.am.ple/logo.png",
"can_grant": false,
"whitelisted": false
}
]
}
```

### POST /v1/client

Register a new client (FxA relier).

**Required scope:** `oauth`

#### Request Parameters

- `name`: The name of the client.
- `redirect_uri`: The URI to redirect to after logging in.
- `image_uri`: A URI to an image to show to a user when logging in.
- `whitelisted`: A whitelisted client is whitelisted.
- `can_grant`: A client needs permission to get implicit grants.

**Example:**

```sh
curl -v \
-X POST \
-H "Content-Type: application/json" \
-H "Authorization: Bearer 558f9980ad5a9c279beb52123653967342f702e84d3ab34c7f80427a6a37e2c0" \
"https://oauth.accounts.firefox.com/v1/client" \
-d '{
"name": "Example",
"redirect_uri": "https://ex.am.ple/path",
"image_uri": "https://ex.am.ple/logo.png",
"whitelisted": false,
"can_grant": false
}'
```

#### Response

A valid 201 response will be a JSON blob with the following properties:

- `client_id`: The generated id for this client.
- `client_secret`: The generated secret for this client. *NOTE: This is
the only time you can get the secret, because we only keep a hashed
version.*
- `name`: A string name of the client.
- `image_uri`: A url to a logo or image that represents the client.
- `redirect_uri`: The url registered to redirect to after successful oauth.
- `can_grant`: If the client can get implicit grants.
- `whitelisted`: If the client is whitelisted.

**Example:**

```json
{
"client_id": "5901bd09376fadaa",
"client_secret": "4ab433e31ef3a7cf7c20590f047987922b5c9ceb1faff56f0f8164df053dd94c",
"name": "Example",
"redirect_uri": "https://ex.am.ple/path",
"image_uri": "https://ex.am.ple/logo.png",
"can_grant": false,
"whitelisted": false
}
```

### POST /v1/client/:id

Update the details of a client. Any parameter not included in the
request will stay unchanged.

**Required scope:** `oauth`

#### Request Parameters

- `name`: The name of the client.
- `redirect_uri`: The URI to redirect to after logging in.
- `image_uri`: A URI to an image to show to a user when logging in.
- `whitelisted`: A whitelisted client is whitelisted.
- `can_grant`: A client needs permission to get implicit grants.

**Example:**

```sh
curl -v \
-X POST \
-H "Content-Type: application/json" \
-H "Authorization: Bearer 558f9980ad5a9c279beb52123653967342f702e84d3ab34c7f80427a6a37e2c0" \
"https://oauth.accounts.firefox.com/v1/client/5901bd09376fadaa" \
-d '{
"name": "Example2",
"redirect_uri": "https://ex.am.ple/path/2",
"image_uri": "https://ex.am.ple/logo2.png",
}'
```

#### Response

A valid reponse will have a 200 status code and empty body.

### DELETE /v1/client/:id

Delete a client. It will be no more. Zilch. Nada. Nuked from orbit.

**Required scope:** `oauth`

#### Request Parameters

**Example:**

```sh
curl -v \
-X DELETE \
-H "Authorization: Bearer 558f9980ad5a9c279beb52123653967342f702e84d3ab34c7f80427a6a37e2c0" \
"https://oauth.accounts.firefox.com/v1/client/5901bd09376fadaa"
```

#### Response

A valid reponse will have a 204 response code and an empty body.

### GET /v1/authorization

This endpoint starts the OAuth flow. A client redirects the user agent
Expand Down Expand Up @@ -272,6 +432,10 @@ A valid request will return JSON with these properties:
```

[client]: #get-v1clientid
[register]: #post-v1clientregister
[clients]: #get-v1clients
[client-update]: #post-v1clientid
[client-delete]: #delete-v1clientid
[redirect]: #get-v1authorization
[authorization]: #post-v1authorization
[token]: #post-v1token
Expand Down
59 changes: 59 additions & 0 deletions lib/auth.js
@@ -0,0 +1,59 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

const AppError = require('./error');
const logger = require('./logging').getLogger('fxa.server.auth');
const token = require('./token');
const validators = require('./validators');

const WHITELIST = require('./config').get('admin.whitelist').map(function(re) {
logger.verbose('compiling whitelist', re);
return new RegExp(re);
});

exports.AUTH_STRATEGY = 'dogfood';
exports.AUTH_SCHEME = 'bearer';

exports.SCOPE_CLIENT_MANAGEMENT = 'oauth';

exports.strategy = function() {
return {
authenticate: function dogfoodStrategy(req, reply) {
var auth = req.headers.authorization;
logger.debug('checking auth', auth);
if (!auth || auth.indexOf('Bearer ') !== 0) {
return reply(AppError.unauthorized('Bearer token not provided'));
}
var tok = auth.split(' ')[1];

if (!validators.HEX_STRING.test(tok)) {
return reply(AppError.unauthorized('Illegal Bearer token'));
}

token.verify(tok).done(function(details) {
if (details.scope.indexOf(exports.SCOPE_CLIENT_MANAGEMENT) !== -1) {
logger.debug('checking whitelist');
var blocked = !WHITELIST.some(function(re) {
return re.test(details._email);
});
if (blocked) {
logger.warn('auth.whitelist.blocked', {
email: details._email,
token: tok
});
return reply(AppError.forbidden());
}
}

logger.info('auth.success', details);
reply(null, {
credentials: details
});
}, function(err) {
logger.debug('auth.error', err);
reply(AppError.unauthorized('Bearer token invalid'));
});
}
};
};
6 changes: 6 additions & 0 deletions lib/config.js
Expand Up @@ -8,6 +8,12 @@ const path = require('path');
const convict = require('convict');

const conf = convict({
admin: {
whitelist: {
doc: 'An array of regexes. Passing any one will get through.',
default: ['@mozilla\\.com$']
}
},
api: {
version: {
doc: 'Number part of versioned endpoints - ex: /v1/token',
Expand Down
4 changes: 2 additions & 2 deletions lib/db/index.js
Expand Up @@ -102,7 +102,7 @@ function withDriver() {
const proxyReturn = logger.isEnabledFor(logger.VERBOSE) ?
function verboseReturn(promise, method) {
return promise.then(function(ret) {
logger.verbose('proxied %s < %j', method, ret);
logger.verbose('proxied %s < %:2j', method, ret);
return ret;
});
} : function identity(x) {
Expand All @@ -113,7 +113,7 @@ function proxy(method) {
return function proxied() {
var args = arguments;
return withDriver().then(function(driver) {
logger.verbose('proxying %s > %j', method, args);
logger.verbose('proxying %s > %:2j', method, args);
return proxyReturn(driver[method].apply(driver, args), method);
}).catch(function(err) {
logger.error('%s: %s', method, err);
Expand Down
17 changes: 16 additions & 1 deletion lib/db/memory.js
Expand Up @@ -80,6 +80,8 @@ MemoryStore.prototype = {
var hex = unbuf(client.id);
logger.debug('registerClient', client.name, hex);
client.createdAt = new Date();
client.canGrant = !!client.canGrant;
client.whitelisted = !!client.whitelisted;
this.clients[hex] = client;
client.secret = client.hashedSecret;
return P.resolve(client);
Expand All @@ -98,7 +100,7 @@ MemoryStore.prototype = {
// nothing
} else if (key === 'hashedSecret') {
old.secret = buf(client[key]);
} else {
} else if (client[key] !== undefined) {
old[key] = client[key];
}
}
Expand All @@ -107,6 +109,19 @@ MemoryStore.prototype = {
getClient: function getClient(id) {
return P.resolve(this.clients[unbuf(id)]);
},
getClients: function getClients() {
return P.resolve(Object.keys(this.clients).map(function(id) {
var client = this.clients[id];
return {
id: client.id,
name: client.name,
imageUri: client.imageUri,
redirectUri: client.redirectUri,
canGrant: client.canGrant,
whitelisted: client.whitelisted
};
}, this));
},
removeClient: function removeClient(id) {
delete this.clients[unbuf(id)];
return P.resolve();
Expand Down
24 changes: 21 additions & 3 deletions lib/db/mysql.js
Expand Up @@ -18,6 +18,12 @@ function MysqlStore(options) {
} else {
options.charset = 'UTF8_UNICODE_CI';
}
options.typeCast = function(field, next) {
if (field.type === 'TINY' && field.length === 1) {
return field.string() === '1';
}
return next();
};
this._pool = mysql.createPool(options);
}

Expand Down Expand Up @@ -89,8 +95,13 @@ const QUERY_CLIENT_REGISTER =
'(id, name, imageUri, secret, redirectUri, whitelisted, canGrant) ' +
'VALUES (?, ?, ?, ?, ?, ?, ?);';
const QUERY_CLIENT_GET = 'SELECT * FROM clients WHERE id=?';
const QUERY_CLIENT_UPDATE = 'UPDATE clients SET name=?, imageUri=?, secret=?,' +
'redirectUri=?, whitelisted=?, canGrant=? WHERE id=?';
const QUERY_CLIENT_LIST = 'SELECT id, name, redirectUri, imageUri, canGrant, ' +
'whitelisted FROM clients';
const QUERY_CLIENT_UPDATE = 'UPDATE clients SET ' +
'name=COALESCE(?, name), imageUri=COALESCE(?, imageUri), ' +
'secret=COALESCE(?, secret), redirectUri=COALESCE(?, redirectUri), ' +
'whitelisted=COALESCE(?, whitelisted), canGrant=COALESCE(?, canGrant) ' +
'WHERE id=?';
const QUERY_CLIENT_DELETE = 'DELETE FROM clients WHERE id=?';
const QUERY_CODE_INSERT =
'INSERT INTO codes (clientId, userId, email, scope, code) VALUES ' +
Expand Down Expand Up @@ -160,11 +171,15 @@ MysqlStore.prototype = {
if (!client.id) {
return P.reject(new Error('Update client needs an id'));
}
var secret = client.hashedSecret || client.secret || null;
if (secret) {
secret = buf(secret);
}
return this._write(QUERY_CLIENT_UPDATE, [
// VALUES
client.name,
client.imageUri,
buf(client.hashedSecret),
secret,
client.redirectUri,
client.whitelisted,
client.canGrant,
Expand All @@ -177,6 +192,9 @@ MysqlStore.prototype = {
getClient: function getClient(id) {
return this._readOne(QUERY_CLIENT_GET, [buf(id)]);
},
getClients: function getClients() {
return this._read(QUERY_CLIENT_LIST);
},
removeClient: function removeClient(id) {
return this._write(QUERY_CLIENT_DELETE, [buf(id)]);
},
Expand Down

0 comments on commit 98f41f7

Please sign in to comment.