Skip to content

Commit

Permalink
Merge branch 'develop'
Browse files Browse the repository at this point in the history
  • Loading branch information
william26 committed Jun 10, 2014
2 parents 8cf5d6a + 7cfd26c commit 9c5cb2c
Show file tree
Hide file tree
Showing 8 changed files with 463 additions and 159 deletions.
145 changes: 119 additions & 26 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ Features
- Server-side OAuth authentication flow
- Requests to API from the backend, including the unified "me" method
- Unified user information requests for available providers from the backend
- Access token renewal with the refresh_token when available

With this SDK, your OAuth flow is also more secure as the oauth token never
leaves your backend.
Expand Down Expand Up @@ -107,15 +108,42 @@ Let's say your endpoint will be on /oauth/state_token :

```JavaScript
app.get('/oauth/state_token', function (req, res) {
var token = OAuth.generateStateToken(req);
var token = OAuth.generateStateToken(req.session);

res.send(200, {
token:token
});
});
```

**Creating an authentication endpoint**
**Authentication**

The SDK gives you an `auth` method that allows you to retrieve a `request_object`. That `request_object` allows you to make API calls, and contains the access token.

The `auth` method takes :

1. a provider name
2. the session array
3. (optional) an options object

It returns a promise to let you handle the callback and error management.

```
OAuth.auth('provider', req.session, {
option_field: option_value,
//...
});
```

The options object can contain the following fields :

- code: an OAuth code, that will be used to get credentials from the provider and build a request_object
- credentials: a credentials object, that will be used to rebuild a refreshed request_object
- force_refresh: forces the credentials refresh if a refresh token is available

If nothing is given in the options object, the auth method tries to build a request_object from the session.

*Authenticating the user for the first time*

When you launch the authentication flow from the front-end (that is to say when you show a popup to the user so that he can allow your app to use his/her data), you'll be given a code (see next section, "Integrating the front-end SDK" to learn how to get the code).

Expand All @@ -125,26 +153,29 @@ To do that, you have to create an authentication endpoint on your backend. This

```JavaScript
app.post('/api/signin', function (req, res) {
OAuth.auth(JSON.parse(req.body).code, req)
.then(function (result) {
//result contains the access_token if OAuth 2.0
//or the couple oauth_token,oauth_token_secret if OAuth 1.0
var code = JSON.parse(req.body).code;

// Here the auth method takes the field 'code'
// in its options object. It will thus use that code
// to retrieve credentials from the provider.
OAuth.auth('facebook', req.session, {
code: code
})
.then(function (request_object) {
// request_object contains the access_token if OAuth 2.0
// or the couple oauth_token,oauth_token_secret if OAuth 1.0

//result also contains methods get|post|patch|put|delete|me
result.get('/me')
.then(function (info) {
var user = {
email: info.email,
firstname: info.first_name,
lastname: info.last_name
};
//login your user here.
res.send(200, 'Successfully logged in');
})
.fail(function (e) {
//handle errors here
res.send(500, 'An error occured');
});
// request_object also contains methods get|post|patch|put|delete|me
return request_object.get('/me');
})
.then(function (info) {
var user = {
email: info.email,
firstname: info.first_name,
lastname: info.last_name
};
//login your user here.
res.send(200, 'Successfully logged in');
})
.fail(function (e) {
//handle errors here
Expand All @@ -153,19 +184,21 @@ app.post('/api/signin', function (req, res) {
});
```

**Use the authentication info in other endpoints**
*Authenticating a user from the session*

Once a user is authenticaded on a service, the auth result object is stored
Once a user is authenticaded on a service, the credentials are stored
in the session. You can access it very easily from any other endpoint to use it. Let's say for example that you want to post something on your user's wall on Facebook :

```JavaScript
app.post('/api/wall_message', function (req, res){
var data = JSON.parse(req.body);
//data contains field "message", containing the message to post

OAuth.create(req, 'facebook')
.post('/me/feed', {
message: data.message
OAuth.auth('facebook', req.session)
.then(function (request_object) {
return request_object.post('/me/feed', {
message: data.message
});
})
.then(function (r) {
//r contains Facebook's response, normaly an id
Expand All @@ -180,6 +213,66 @@ app.post('/api/wall_message', function (req, res){
});
```

*Authenticating a user from saved credentials*

* Saving credentials
If you want to save the credentials to use them when the user is offline, (e.g. in a cron loading information), you can save the credentials in the data storage of your choice. All you need to do is to retrieve the credentials object from the request_object :

```javascript
OAuth.auth('provider', req.session, {
code: code
})
.then(function (request_object) {
var credentials = request_object->getCredentials();

// Here you can save the credentials object wherever you want

});
```

* Using saved credentials

You can then rebuild a request_object from the credentials you saved earlier :
```javascript
// Here you retrieved the credentials object from your data storage

OAuth.auth('provider', req.session, {
credentials: credentials
})
.then(function (request_object) {
// Here request_object has been rebuilt from the credentials object
// If the credentials are expired and contain a refresh token,
// the auth method automatically refresh them.
});

```

*Refreshing saved credentials*

Tokens are automatically refreshed when you use the `auth` method with the session or with saved credentials. The SDK checks that the access token is expired whenever it's called.

If it is, and if a refresh token is available in the credentials (you may have to configure the OAuth.io app in a specific way for some providers), it automatically calls the OAuth.io refresh token endpoint.

If you want to force a refresh from the `auth` method, you can pass the `force_refresh` field in the option object, like this :

```javascript
OAuth.auth('provider', req.session, {
force_refresh: true
});
```

You can also refresh a credentials object manually. To do that, call the OAuth.refreshCredentials on the request_object or on a credentials object :

```javascript
OAuth.refreshCredentials(request_object, req.session)
.then(function (request_object) {
// Here request_object has been refreshed
})
.fail(function (e) {
// Handle an error
});
```

**3. Integrating Front-end SDK**

This SDK is available on our website : [Get it on oauth.io][1].
Expand Down
111 changes: 91 additions & 20 deletions coffee/lib/authentication.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,88 @@ request = require 'request'
Q = require 'q'

module.exports = (cache, requestio) ->
return {
authenticate: (code, req) ->
a = {
refresh_tokens: (credentials, session, force) ->
defer = Q.defer()
credentials.refreshed = false
now = new Date()
if credentials.refresh_token and ((credentials.expires and now.getTime() > credentials.expires) or force)
request.post {
url: cache.oauthd_url + '/auth/refresh_token/' + credentials.provider,
form: {
token: credentials.refresh_token,
key: cache.public_key,
secret: cache.secret_key
}
}, (e, r, body) ->
if (e)
defer.reject e
return defer.promise
else
if typeof body is "string"
try
body = JSON.parse body
catch e
defer.reject e
if typeof body == "object" and body.access_token and body.expires_in
credentials.expires = new Date().getTime() + body.expires_in * 1000
for k of body
credentials[k] = body[k]
if (session?)
session.oauth = session.oauth || {}
session.oauth[credentials.provider] = credentials
credentials.refreshed = true
credentials.last_refresh = new Date().getTime()
defer.resolve credentials
else
defer.resolve credentials
else
defer.resolve credentials
return defer.promise
auth: (provider, session, opts) ->
defer = Q.defer()

if opts?.code
return a.authenticate(opts.code, session)

if opts?.credentials
a.refresh_tokens(opts.credentials, session, opts?.force_refresh)
.then (credentials) ->
defer.resolve(a.construct_request_object(credentials))
return defer.promise
if (not opts?.credentials) and (not opts?.code)
if session.oauth[provider]
a.refresh_tokens(session.oauth[provider], session, opts?.force_refresh)
.then (credentials) ->
defer.resolve(a.construct_request_object(credentials))
else
defer.reject new Error('Cannot authenticate from session for provider \'' + provider + '\'')
return defer.promise

defer.reject new Error('Could not authenticate, parameters are missing or wrong')
return defer.promise
construct_request_object: (credentials) ->
request_object = {}
for k of credentials
request_object[k] = credentials[k]
request_object.get = (url, options) ->
return requestio.make_request(request_object, 'GET', url, options)
request_object.post = (url, options) ->
return requestio.make_request(request_object, 'POST',url, options)
request_object.patch = (url, options) ->
return requestio.make_request(request_object, 'PATCH', url, options)
request_object.put = (url, options) ->
return requestio.make_request(request_object, 'PUT', url, options)
request_object.del = (url, options) ->
return requestio.make_request(request_object, 'DELETE', url, options)
request_object.me = (options) ->
return requestio.make_me_request(request_object, options)
request_object.getCredentials = () ->
return credentials
request_object.wasRefreshed = () ->
return credentials.refreshed
return request_object
authenticate: (code, session) ->
defer = Q.defer()
request.post {
url: cache.oauthd_url + '/auth/access_token',
Expand All @@ -26,25 +106,16 @@ module.exports = (cache, requestio) ->
if (not response.state?)
defer.reject new Error 'State is missing from response'
return

if (not req?.session?.csrf_tokens? or response.state not in req.session.csrf_tokens)
if (not session?.csrf_tokens? or response.state not in session.csrf_tokens)
defer.reject new Error 'State is not matching'

response.get = (url, options) ->
return requestio.make_request(response, 'GET', url, options)
response.post = (url, options) ->
return requestio.make_request(response, 'POST',url, options)
response.patch = (url, options) ->
return requestio.make_request(response, 'PATCH', url, options)
response.put = (url, options) ->
return requestio.make_request(response, 'PUT', url, options)
response.del = (url, options) ->
return requestio.make_request(response, 'DELETE', url, options)
response.me = (options) ->
return requestio.make_me_request(response, options)
if (req?.session?)
req.session.oauth = req.session.oauth || {}
req.session.oauth[response.provider] = response
if response.expires_in
response.expires = new Date().getTime() + response.expires_in * 1000
response = a.construct_request_object response
if (session?)
session.oauth = session.oauth || {}
session.oauth[response.provider] = response
defer.resolve response
return defer.promise

}
return a
8 changes: 4 additions & 4 deletions coffee/lib/csrf_generator.coffee
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
module.exports = (guid) ->
return (req) ->
return (session) ->
csrf_token = guid()
req.session.csrf_tokens = req.session.csrf_tokens or []
req.session.csrf_tokens.push csrf_token
req.session.csrf_tokens.shift() if req.session.csrf_tokens.length > 4
session.csrf_tokens = session.csrf_tokens or []
session.csrf_tokens.push csrf_token
session.csrf_tokens.shift() if session.csrf_tokens.length > 4
return csrf_token
Loading

0 comments on commit 9c5cb2c

Please sign in to comment.