Skip to content

Commit

Permalink
feat(driver): add instagram driver
Browse files Browse the repository at this point in the history
* feat(social-auth):create social login via instagram

* feat(social-auth):Add instagram auth tests

* fix(social-auth):Fix standard and linting issues

* fix data bugs

* removing client id and client secret from config file

* fix(adonis-ally):change twitter OauthException parameter from github to twitter in twitter driver file

* feat(social-auth):wrote twitter tests

* worked on the changes requested
  • Loading branch information
iamraphson authored and thetutlage committed Jan 3, 2017
1 parent 59d4a2e commit 3d5ca8f
Show file tree
Hide file tree
Showing 8 changed files with 332 additions and 3 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Expand Up @@ -2,3 +2,6 @@ coverage
node_modules
.DS_Store
npm-debug.log
.env
.idea
.idea/
15 changes: 15 additions & 0 deletions examples/config.js
Expand Up @@ -60,6 +60,21 @@ module.exports = {
clientId: Env.get('GITHUB_CLIENT_ID'),
clientSecret: Env.get('GITHUB_CLIENT_SECRET'),
redirectUri: `${Env.get('APP_URL')}/authenticated/github`
},

/*
|--------------------------------------------------------------------------
| Instagram Configuration
|--------------------------------------------------------------------------
|
| You can access your application credentials from the instagram developers
| console. https://www.instagram.com/developer/
|
*/
instagram: {
clientId: Env.get('INSTAGRAM_CLIENT_ID'),
clientSecret: Env.get('INSTAGRAM_CLIENT_SECRET'),
redirectUri: `${Env.get('APP_URL')}/authenticated/instagram`
}
}
}
34 changes: 34 additions & 0 deletions examples/instagram.js
@@ -0,0 +1,34 @@
'use strict'

const Ioc = require('adonis-fold').Ioc
const config = require('./setup/config')
const http = require('./setup/http')
const AllyManager = require('../src/AllyManager')
Ioc.bind('Adonis/Src/Config', () => {
return config
})

http.get('/instagram', function * (request, response) {
const ally = new AllyManager(request, response)
const instagram = ally.driver('instagram')
response.writeHead(200, {'content-type': 'text/html'})
const url = yield instagram.getRedirectUrl()
response.write(`<a href="${url}">Login With Instagram</a>`)
response.end()
})

http.get('/instagram/authenticated', function * (request, response) {
const ally = new AllyManager(request, response)
const instagram = ally.driver('instagram')
try {
const user = yield instagram.getUser()
response.writeHead(200, {'content-type': 'application/json'})
response.write(JSON.stringify({ original: user.getOriginal(), profile: user.toJSON() }))
} catch (e) {
response.writeHead(500, {'content-type': 'application/json'})
response.write(JSON.stringify({ error: e }))
}
response.end()
})

http.start().listen(8000)
2 changes: 1 addition & 1 deletion package.json
Expand Up @@ -28,7 +28,7 @@
"cz-conventional-changelog": "^1.2.0",
"istanbul": "^0.4.5",
"mocha": "^3.0.2",
"standard": "^8.0.0"
"standard": "^8.6.0"
},
"standard": {
"global": [
Expand Down
193 changes: 193 additions & 0 deletions src/Drivers/Instagram.js
@@ -0,0 +1,193 @@
'use strict'

/*
* adonis-ally
*
* (c) Ayeni Olusegun <nsegun5@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

const CE = require('../Exceptions')
const OAuth2Scheme = require('../Schemes/OAuth2')
const AllyUser = require('../AllyUser')
const got = require('got')
const utils = require('../../lib/utils')
const _ = utils.mixLodash(require('lodash'))

class Instagram extends OAuth2Scheme {

constructor (Config) {
const config = Config.get('services.ally.instagram')

if (!_.hasAll(config, ['clientId', 'clientSecret', 'redirectUri'])) {
throw CE.OAuthException.missingConfig('instagram')
}

super(config.clientId, config.clientSecret, config.headers)

/**
* Oauth specific values to be used when creating the redirect
* url or fetching user profile.
*/
this._scope = this._getInitialScopes(config.scope)
this._redirectUri = config.redirectUri
this._redirectUriOptions = _.merge({response_type: 'code'}, config.options)
}

/**
* Injections to be made by the IoC container
*
* @return {Array}
*/
static get inject () {
return ['Adonis/Src/Config']
}

/**
* Scope seperator for seperating multiple
* scopes.
*
* @return {String}
*/
get scopeSeperator () {
return ' '
}

/**
* Base url to be used for constructing
* facebook oauth urls.
*
* @return {String}
*/
get baseUrl () {
return 'https://api.instagram.com/'
}

/**
* Relative url to be used for redirecting
* user.
*
* @return {String} [description]
*/
get authorizeUrl () {
return 'oauth/authorize'
}

/**
* Relative url to be used for exchanging
* access token.
*
* @return {String}
*/
get accessTokenUrl () {
return 'oauth/access_token'
}

/**
* Returns initial scopes to be used right from the
* config file. Otherwise it will fallback to the
* commonly used scopes
*
* @param {Array} scopes
*
* @return {Array}
*
* @private
*/
_getInitialScopes (scopes) {
return _.size(scopes) ? scopes : ['basic']
}

/**
* Returns the user profile as an object using the
* access token
*
* @param {String} accessToken
*
* @return {Object}
*
* @private
*/
* _getUserProfile (accessToken) {
const profileUrl = `${this.baseUrl}v1/users/self?access_token=${accessToken}`
const response = yield got(profileUrl, {
headers: {
'Accept': 'application/json'
},
json: true
})
return response.body
}

/**
* Returns the redirect url for a given provider.
*
* @param {Array} scope
*
* @return {String}
*/
* getRedirectUrl (scope) {
scope = _.size(scope) ? scope : this._scope
return this.getUrl(this._redirectUri, scope, this._redirectUriOptions)
}

/**
* Parses the redirect errors returned by facebook
* and returns the error message.
*
* @param {Object} queryParams
*
* @return {String}
*/
parseRedirectError (queryParams) {
return queryParams.error_description || queryParams.error || 'Oauth failed during redirect'
}

/**
* Returns the user profile with it's access token, refresh token
* and token expiry
*
* @param {Object} queryParams
*
* @return {Object}
*/
* getUser (queryParams) {
const code = queryParams.code

/**
* Throw an exception when query string does not have
* code.
*/
if (!code) {
const errorMessage = this.parseRedirectError(queryParams)
throw CE.OAuthException.tokenExchangeException(errorMessage, null, errorMessage)
}

const accessTokenResponse = yield this.getAccessToken(code, this._redirectUri, {
grant_type: 'authorization_code'
})
const userProfile = yield this._getUserProfile(accessTokenResponse.accessToken)
const user = new AllyUser()
user
.setOriginal(userProfile)
.setFields(
userProfile.data.id,
userProfile.data.full_name,
null,
userProfile.data.username,
userProfile.data.profile_picture
)
.setToken(
accessTokenResponse.accessToken,
accessTokenResponse.refreshToken,
null,
null
)

return user
}
}

module.exports = Instagram
2 changes: 1 addition & 1 deletion src/Drivers/Twitter.js
Expand Up @@ -21,7 +21,7 @@ class Twitter extends OAuthScheme {
const config = Config.get('services.ally.twitter')

if (!_.hasAll(config, ['clientId', 'clientSecret', 'redirectUri'])) {
throw CE.OAuthException.missingConfig('github')
throw CE.OAuthException.missingConfig('twitter')
}

super(config.clientId, config.clientSecret, config.redirectUri)
Expand Down
3 changes: 2 additions & 1 deletion src/Drivers/index.js
Expand Up @@ -14,5 +14,6 @@ module.exports = {
github: require('./Github'),
google: require('./Google'),
linkedin: require('./LinkedIn'),
twitter: require('./Twitter')
twitter: require('./Twitter'),
instagram: require('./Instagram')
}
83 changes: 83 additions & 0 deletions test/unit/drivers.spec.js
Expand Up @@ -17,6 +17,8 @@ const Google = drivers.google
const Facebook = drivers.facebook
const Github = drivers.github
const LinkedIn = drivers.linkedin
const Instagram = drivers.instagram
const Twitter = drivers.twitter
const assert = chai.assert
require('co-mocha')

Expand Down Expand Up @@ -256,4 +258,85 @@ describe('Oauth Drivers', function () {
assert.equal(redirectToUrl, providerUrl)
})
})

context('Instagram', function () {
it('should throw an exception when config has not been defined', function () {
const instagram = () => new Instagram({get: function () { return null }})
assert.throw(instagram, 'OAuthException: E_MISSING_OAUTH_CONFIG: Make sure to define instagram configuration inside config/services.js file')
})

it('should throw an exception when clientid is missing', function () {
const instagram = () => new Instagram({get: function () { return {clientSecret: '1', redirectUri: '2'} }})
assert.throw(instagram, 'OAuthException: E_MISSING_OAUTH_CONFIG: Make sure to define instagram configuration inside config/services.js file')
})

it('should throw an exception when clientSecret is missing', function () {
const instagram = () => new Instagram({get: function () { return {clientId: '1', redirectUri: '2'} }})
assert.throw(instagram, 'OAuthException: E_MISSING_OAUTH_CONFIG: Make sure to define instagram configuration inside config/services.js file')
})

it('should throw an exception when redirectUri is missing', function () {
const instagram = () => new Instagram({get: function () { return {clientId: '1', clientSecret: '2'} }})
assert.throw(instagram, 'OAuthException: E_MISSING_OAUTH_CONFIG: Make sure to define instagram configuration inside config/services.js file')
})

it('should generate the redirect_uri with correct signature', function * () {
const instagram = new Instagram(config)
const redirectUrl = qs.escape(config.get().redirectUri)
const scope = qs.escape(['basic'].join(' '))
const providerUrl = `https://api.instagram.com/oauth/authorize?redirect_uri=${redirectUrl}&scope=${scope}&response_type=code&client_id=${config.get().clientId}`
const redirectToUrl = yield instagram.getRedirectUrl()
assert.equal(redirectToUrl, providerUrl)
})

it('should make use of the scopes defined in the config file', function * () {
const customConfig = {
get: function () {
return {
clientId: 12,
clientSecret: 123,
redirectUri: 'http://localhost',
scope: ['basic']
}
}
}
const instagram = new Instagram(customConfig)
const redirectUrl = qs.escape(customConfig.get().redirectUri)
const scope = qs.escape(['basic'].join(' '))
const providerUrl = `https://api.instagram.com/oauth/authorize?redirect_uri=${redirectUrl}&scope=${scope}&response_type=code&client_id=${customConfig.get().clientId}`
const redirectToUrl = yield instagram.getRedirectUrl()
assert.equal(redirectToUrl, providerUrl)
})

it('should make use of the scopes passed to the generate method', function * () {
const instagram = new Instagram(config)
const redirectUrl = qs.escape(config.get().redirectUri)
const scope = qs.escape(['basic'].join(' '))
const providerUrl = `https://api.instagram.com/oauth/authorize?redirect_uri=${redirectUrl}&scope=${scope}&response_type=code&client_id=${config.get().clientId}`
const redirectToUrl = yield instagram.getRedirectUrl(['basic'])
assert.equal(redirectToUrl, providerUrl)
})
})

context('Twitter', function () {
it('should throw an exception when config has not been defined', function () {
const twitter = () => new Twitter({get: function () { return null }})
assert.throw(twitter, 'OAuthException: E_MISSING_OAUTH_CONFIG: Make sure to define twitter configuration inside config/services.js file')
})

it('should throw an exception when clientid is missing', function () {
const twitter = () => new Twitter({get: function () { return {clientSecret: '1', redirectUri: '2'} }})
assert.throw(twitter, 'OAuthException: E_MISSING_OAUTH_CONFIG: Make sure to define twitter configuration inside config/services.js file')
})

it('should throw an exception when clientSecret is missing', function () {
const twitter = () => new Twitter({get: function () { return {clientId: '1', redirectUri: '2'} }})
assert.throw(twitter, 'OAuthException: E_MISSING_OAUTH_CONFIG: Make sure to define twitter configuration inside config/services.js file')
})

it('should throw an exception when redirectUri is missing', function () {
const twitter = () => new Twitter({get: function () { return {clientId: '1', clientSecret: '2'} }})
assert.throw(twitter, 'OAuthException: E_MISSING_OAUTH_CONFIG: Make sure to define twitter configuration inside config/services.js file')
})
})
})

0 comments on commit 3d5ca8f

Please sign in to comment.