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

Commit

Permalink
feat(refresh_tokens): add refresh_tokens to /token endpoint
Browse files Browse the repository at this point in the history
See docs/api.md for changes to endpoints.

Closes #209
  • Loading branch information
seanmonstar committed Jun 30, 2015
1 parent e70e564 commit 16e787f
Show file tree
Hide file tree
Showing 20 changed files with 980 additions and 299 deletions.
2 changes: 1 addition & 1 deletion config/test.json
Expand Up @@ -47,7 +47,7 @@
}
],
"logging": {
"level": "warn",
"level": "error",
"fmt": "pretty"
}
}
22 changes: 21 additions & 1 deletion docs/api.md
Expand Up @@ -47,6 +47,7 @@ The currently-defined error responses are:
| 403 | 112 | forbidden |
| 415 | 113 | invalid content type |
| 400 | 114 | invalid scopes |
| 400 | 115 | expired token |
| 500 | 999 | internal server error |

## API Endpoints
Expand Down Expand Up @@ -283,6 +284,7 @@ content-server page.
- `state`: A value that will be returned to the client as-is upon redirection, so that clients can verify the redirect is authentic.
- `redirect_uri`: Optional. If supplied, a string URL of where to redirect afterwards. Must match URL from registration.
- `scope`: Optional. A space-separated list of scopes that the user has authorized. This could be pruned by the user at the confirmation dialog.
- `access_type`: Optional. If provided, should be `online` or `offline`. `offline` will result in a refresh_token being provided, so that the access_token can be refreshed after it expires.
- `action`: Optional. If provided, should be `signup`, `signin`, or `force_auth`. Send to improve the user experience, based on whether they clicked on a Sign In or Sign Up button. `force_auth` requires the user to sign in using the address specified in `email`. If unspecified then Firefox Accounts will try choose intelligently between `signin` and `signup` based on the user's browser state.
- `email`: Optional if `action` is `signup` or `signin`. Required if `action`
is `force_auth`.
Expand Down Expand Up @@ -318,6 +320,7 @@ back to the client. This code will be traded for a token at the
- `response_type`: Optional. If supplied, must be either `code` or `token`. `code` is the default. `token` means the implicit grant is desired, and requires that the client have special permission to do so.
- `redirect_uri`: Optional. If supplied, a string URL of where to redirect afterwards. Must match URL from registration.
- `scope`: Optional. A string-separated list of scopes that the user has authorized. This could be pruned by the user at the confirmation dialog.
- `access_type`: Optional. A value of `offline` will generate a refresh token along with the access token.

**Example:**

Expand Down Expand Up @@ -366,7 +369,18 @@ particular user.

- `client_id`: The id returned from client registration.
- `client_secret`: The secret returned from client registration.
- `code`: A string that was received from the [authorization][] endpoint.
- `ttl`: (optional) Seconds that this access_token should be valid.

The default and maximum value is 2 weeks.
- `grant_type`: Either the string `authorization_code` or `refresh_token`.
- If `authorization_code`:
- `code`: A string that was received from the [authorization][] endpoint.
- If `refresh_token`:
- `refresh_token`: A string that received from the [token][]
endpoint specifically as a refresh token.
- `scope`: (optional) A subset of scopes provided to this
refresh_token originally, to receive an access_token with less
permissions.

**Example:**

Expand All @@ -378,6 +392,8 @@ curl -v \
-d '{
"client_id": "5901bd09376fadaa",
"client_secret": "20c6882ef864d75ad1587c38f9d733c80751d2cbc8614e30202dc3d1d25301ff",
"ttl": 3600,
"grant_type": "authorization_code",
"code": "4ab433e31ef3a7cf7c20590f047987922b5c9ceb1faff56f0f8164df053dd94c"
}'
```
Expand All @@ -388,6 +404,8 @@ A valid request will return a JSON response with these properties:

- `access_token`: A string that can be used for authorized requests to service providers.
- `scope`: A string of space-separated permissions that this token has. May differ from requested scopes, since user can deny permissions.
- `refresh_token`: (Optional) A refresh token to fetch a new access token when this one expires. Only will be present if `grant_type=authorization_code` and the original authorization request included `access_type=offline`.
- `expires_in`: **Seconds** until this access token will no longer be valid.
- `token_type`: A string representing the token type. Currently will always be "bearer".
- `auth_at`: An integer giving the time at which the user authenticated to the Firefox Accounts server when generating this token, as a UTC unix timestamp (i.e. **seconds since epoch**).

Expand All @@ -398,6 +416,8 @@ A valid request will return a JSON response with these properties:
"access_token": "558f9980ad5a9c279beb52123653967342f702e84d3ab34c7f80427a6a37e2c0",
"scope": "profile:email profile:avatar",
"token_type": "bearer",
"expires_in": 3600,
"refresh_token": "58d59cc97c3ca183b3a87a65eec6f93d5be051415b53afbf8491cc4c45dbb0c6",
"auth_at": 1422336613
}
```
Expand Down
8 changes: 7 additions & 1 deletion lib/config.js
Expand Up @@ -80,10 +80,16 @@ const conf = convict({
}
},
expiration: {
// in JavaScript, we live in milliseconds
accessToken: {
doc: 'Access Tokens maximum expiration (can live shorter)',
format: 'duration',
default: 1000 * 60 * 60 * 24 * 2 // 2weeks
},
code: {
doc: 'Clients must trade codes for tokens before they expire',
format: 'duration',
default: 1000 * 60 * 15
default: 1000 * 60 * 15 // 15mins
}
},
git: {
Expand Down
100 changes: 70 additions & 30 deletions lib/db/memory.js
Expand Up @@ -5,11 +5,14 @@
const buf = require('buf').hex;
const unbuf = require('buf').unbuf.hex;

const config = require('../config');
const encrypt = require('../encrypt');
const logger = require('../logging')('db.memory');
const P = require('../promise');
const unique = require('../unique');

const MAX_TTL = config.get('expiration.accessToken');

/*
* MemoryStore structure:
* MemoryStore = {
Expand All @@ -33,7 +36,8 @@ const unique = require('../unique');
* code: <string>
* scope: <string>,
* authAt: <timestamp>,
* createdAt: <timestamp>
* createdAt: <timestamp>,
* offline: <boolean>
* }
* },
* developers: {
Expand All @@ -57,6 +61,16 @@ const unique = require('../unique');
* userId: <user_id>,
* type: <string>,
* scope: <string>,
* createdAt: <timestamp>,
* expiresAt: <timestamp>
* }
* },
* refreshTokens: {
* <token>: {
* token: <string>,
* clientId: <client_id>,
* userId: <user_id>,
* scope: <string>,
* createdAt: <timestamp>
* }
* }
Expand All @@ -71,13 +85,17 @@ function MemoryStore() {
this.tokens = {};
this.developers = {};
this.clientDevelopers = {};
this.refreshTokens = {};
}

MemoryStore.connect = function memoryConnect() {
return P.resolve(new MemoryStore());
};

function clone(obj) {
if (!obj) {
return obj;
}
var clone = {};
for (var k in obj) {
clone[k] = obj[k];
Expand Down Expand Up @@ -179,54 +197,76 @@ MemoryStore.prototype = {
delete this.clients[unbuf(id)];
return P.resolve();
},
generateCode: function generateCode(clientId, userId, email, scope, authAt) {
var code = {};
code.clientId = clientId;
code.userId = userId;
code.email = email;
code.scope = scope;
code.authAt = authAt;
code.createdAt = new Date();
var _code = unique.code();
code.code = encrypt.hash(_code);
this.codes[unbuf(code.code)] = code;
return P.resolve(_code);
generateCode: function generateCode(codeObj) {
codeObj = clone(codeObj);
codeObj.createdAt = new Date();
var code = unique.code();
codeObj.code = encrypt.hash(code);
this.codes[unbuf(codeObj.code)] = codeObj;
return P.resolve(code);
},
getCode: function getCode(code) {
return P.resolve(this.codes[unbuf(encrypt.hash(code))]);
return P.resolve(clone(this.codes[unbuf(encrypt.hash(code))]));
},
removeCode: function removeCode(id) {
delete this.codes[unbuf(id)];
return P.resolve();
},
generateToken: function generateToken(vals) {
var token = {};
token.clientId = vals.clientId;
token.userId = vals.userId;
token.email = vals.email;
token.scope = vals.scope;
token.createdAt = new Date();
token.type = 'bearer';
var _token = unique.token();
var ret = clone(token);
token.token = encrypt.hash(_token);
this.tokens[unbuf(token.token)] = token;
ret.token = _token;
generateAccessToken: function generateAccessToken(vals) {
var token = unique.token();
var now = new Date();
var t = {
clientId: vals.clientId,
userId: vals.userId,
email: vals.email,
scope: vals.scope,
type: 'bearer',
createdAt: now,
// ttl is in seconds
expiresAt: new Date(+now + (vals.ttl * 1000 || MAX_TTL)),
token: encrypt.hash(token)
};
var ret = clone(t);
this.tokens[unbuf(t.token)] = t;
ret.token = token;
return P.resolve(ret);
},
getToken: function getToken(token) {
return P.resolve(this.tokens[unbuf(token)]);
getAccessToken: function getAccessToken(token) {
return P.resolve(clone(this.tokens[unbuf(token)]));
},
removeToken: function removeToken(id) {
removeAccessToken: function removeAccessToken(id) {
delete this.tokens[unbuf(id)];
return P.resolve();
},
generateRefreshToken: function generateRefreshToken(vals) {
var token = unique.token();
var t = {
clientId: vals.clientId,
userId: vals.userId,
email: vals.email,
scope: vals.scope,
createdAt: new Date(),
token: encrypt.hash(token)
};
var ret = clone(t);
this.refreshTokens[unbuf(t.token)] = t;
ret.token = token;
return P.resolve(ret);
},
getRefreshToken: function getRefreshToken(token) {
return P.resolve(clone(this.refreshTokens[unbuf(token)]));
},
removeRefreshToken: function removeRefreshToken(id) {
delete this.refreshTokens[unbuf(id)];
return P.resolve();
},
getEncodingInfo: function getEncodingInfo() {
console.warn('getEncodingInfo has no meaning with memory implementation');
return P.resolve({});
},
removeUser: function removeUser(userId) {
deleteByUserId(this.tokens, userId);
deleteByUserId(this.refreshTokens, userId);
deleteByUserId(this.codes, userId);
return P.resolve();
},
Expand Down

0 comments on commit 16e787f

Please sign in to comment.