Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for authentication with Okta #299

Merged
merged 2 commits into from Mar 24, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
26 changes: 25 additions & 1 deletion Providers.md
Expand Up @@ -180,7 +180,7 @@ credentials.profile = {

[Provider Documentation](https://dev.fitbit.com/docs/oauth2/)

- `scope`: Defaults to `['activity', 'profile']`
- `scope`: Defaults to `['activity', 'profile']`
- `config`: not applicable
- `auth`: https://www.fitbit.com/oauth2/authorize
- `token`: https://api.fitbit.com/oauth2/token
Expand Down Expand Up @@ -676,6 +676,7 @@ credentials.profile = {
raw: profile
};
```

### Office 365

[Provider Documentation](https://msdn.microsoft.com/en-us/library/azure/dn645545.aspx)
Expand All @@ -697,6 +698,29 @@ credentials.profile = {
};
```

### Okta

[Provider Documentation](http://developer.okta.com/use_cases/authentication/)

- `scope`: Defaults to `['openid', 'email', 'offline_access']`
- `config`:
- `uri`: Point to your Okta enterprise uri. Intentionally no default as Okta is organization specific..
- `auth`: https://your-organization.okta.com/oauth2/v1/authorize
- `token`: https://your-organization.okta.com/oauth2/v1/token

The default profile response will look like this:

```javascript
credentials.profile = {
id: profile.sub,
username: profile.email,
displayName: profile.nickname,
firstName: profile.given_name,
lastName: profile.family_name,
email: profile.email,
raw: profile
};
```

### WordPress

Expand Down
2 changes: 1 addition & 1 deletion README.md
Expand Up @@ -6,7 +6,7 @@ Lead Maintainer: [Lois Desplat](https://github.com/ldesplat)

[![Build Status](https://secure.travis-ci.org/hapijs/bell.png)](http://travis-ci.org/hapijs/bell)

**bell** ships with built-in support for authentication using `Facebook`, `GitHub`, `Google`, `Google Plus`, `Instagram`, `LinkedIn`, `Slack`, `Twitter`, `Yahoo`, `Foursquare`, `VK`, `ArcGIS Online`, `Windows Live`, `Nest`, `Phabricator`, `BitBucket`, `Dropbox`, `Reddit`, `Tumblr`, `Twitch`, `Salesforce`, `Pinterest` and `Discord`. It also supports any compliant `OAuth 1.0a` and `OAuth 2.0` based login services with a simple configuration object.
**bell** ships with built-in support for authentication using `Facebook`, `GitHub`, `Google`, `Google Plus`, `Instagram`, `LinkedIn`, `Slack`, `Twitter`, `Yahoo`, `Foursquare`, `VK`, `ArcGIS Online`, `Windows Live`, `Nest`, `Phabricator`, `BitBucket`, `Dropbox`, `Reddit`, `Tumblr`, `Twitch`, `Salesforce`, `Pinterest`, `Discord`, and `Okta`. It also supports any compliant `OAuth 1.0a` and `OAuth 2.0` based login services with a simple configuration object.

## Documentation

Expand Down
68 changes: 68 additions & 0 deletions examples/okta.js
@@ -0,0 +1,68 @@
'use strict';

// Load modules

const Hapi = require('hapi');
const Hoek = require('hoek');
const Boom = require('boom');

const server = new Hapi.Server();
server.connection({ port: 8000 });

server.register([require('hapi-auth-cookie'), require('../')], (err) => {

Hoek.assert(!err, err);
server.auth.strategy('session', 'cookie', {
password: 'secret_cookie_encryption_password', //Use something more secure in production
redirectTo: '/auth/okta', //If there is no session, redirect here
isSecure: false //Should be set to true (which is the default) in production
});

server.auth.strategy('okta', 'bell', {
provider: 'okta',
password: 'cookie_encryption_password_secure',
isSecure: false,
location: 'http://127.0.0.1:8000',
clientId: 'IIA1yMR7IK4XGhfyfCno',
clientSecret: 'PEh_HemJovaR-Zjs-unX8-cC9IhQgzF5M1RUrUgW'
});

server.route({
method: 'GET',
path: '/auth/okta',
config: {
auth: 'okta',
handler: function (request, reply) {

if (!request.auth.isAuthenticated) {
return reply(Boom.unauthorized('Authentication failed: ' + request.auth.error.message));
}

//Just store the third party credentials in the session as an example. You could do something
//more useful here - like loading or setting up an account (social signup).
request.auth.session.set(request.auth.credentials);

return reply.redirect('/');
}
}
});

server.route({
method: 'GET',
path: '/',
config: {
auth: 'session',
handler: function (request, reply) {

//Return a message using the information from the session
return reply('Hello, ' + request.auth.credentials.profile.email + '!');
}
}
});

server.start((err) => {

Hoek.assert(!err, err);
console.log('Server started at:', server.info.uri);
});
});
27 changes: 14 additions & 13 deletions lib/providers/index.js
@@ -1,37 +1,38 @@
'use strict';

exports = module.exports = {
arcgisonline: require('./arcgisonline'),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes my brain smile 👌

auth0: require('./auth0'),
azuread: require('./azuread'),
bitbucket: require('./bitbucket'),
discord: require('./discord'),
dropbox: require('./dropbox'),
facebook: require('./facebook'),
fitbit: require('./fitbit'),
foursquare: require('./foursquare'),
github: require('./github'),
gitlab: require('./gitlab'),
googleplus: require('./googleplus'),
google: require('./google'),
googleplus: require('./googleplus'),
instagram: require('./instagram'),
linkedin: require('./linkedin'),
live: require('./live'),
medium: require('./medium'),
meetup: require('./meetup'),
nest: require('./nest'),
slack: require('./slack'),
spotify: require('./spotify'),
twitter: require('./twitter'),
vk: require('./vk'),
yahoo: require('./yahoo'),
arcgisonline: require('./arcgisonline'),
office365: require('./office365'),
okta: require('./okta'),
phabricator: require('./phabricator'),
pingfed: require('./pingfed'),
pinterest: require('./pinterest'),
reddit: require('./reddit'),
meetup: require('./meetup'),
salesforce: require('./salesforce'),
slack: require('./slack'),
spotify: require('./spotify'),
tumblr: require('./tumblr'),
twitch: require('./twitch'),
salesforce: require('./salesforce'),
twitter: require('./twitter'),
vk: require('./vk'),
wordpress: require('./wordpress'),
office365: require('./office365'),
pinterest: require('./pinterest'),
pingfed: require('./pingfed'),
discord: require('./discord')
yahoo: require('./yahoo')
};
45 changes: 45 additions & 0 deletions lib/providers/okta.js
@@ -0,0 +1,45 @@
'use strict';

// Load modules
const Joi = require('joi');
const Hoek = require('hoek');

// Declare internals

const internals = {
schema: Joi.object({
uri: Joi.string().uri().required()
}).required()
};

exports = module.exports = function (options) {

const results = Joi.validate(options, internals.schema);
Hoek.assert(!results.error, results.error);
const settings = results.value;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should you not assert there is an uri since you do not provide a default?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is following a very similar pattern as https://github.com/hapijs/bell/blob/master/lib/providers/auth0.js#L12.

It was my understanding that line 17 using Joi.validate would ensure folks pass in the expected schema.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh I totally missed the internals.schema having uri in it 🙄


return {
protocol: 'oauth2',
useParamsAuth: true,
auth: settings.uri + '/oauth2/v1/authorize',
token: settings.uri + '/oauth2/v1/token',
scope: ['openid', 'email', 'offline_access'],
profile: function (credentials, params, get, callback) {

get(settings.uri + '/oauth2/v1/userinfo', null, (profile) => {

credentials.profile = {
id: profile.sub,
username: profile.email,
displayName: profile.nickname,
firstName: profile.given_name,
lastName: profile.family_name,
email: profile.email,
raw: profile
};

return callback();
});
}
};
};
1 change: 1 addition & 0 deletions package.json
Expand Up @@ -28,6 +28,7 @@
"nest",
"phabricator",
"office365",
"okta",
"reddit",
"tumblr",
"twitter",
Expand Down
119 changes: 119 additions & 0 deletions test/providers/okta.js
@@ -0,0 +1,119 @@
'use strict';

// Load modules

const Bell = require('../../');
const Code = require('code');
const Hapi = require('hapi');
const Hoek = require('hoek');
const Lab = require('lab');
const Mock = require('../mock');

// Test shortcuts

const lab = exports.lab = Lab.script();
const describe = lab.describe;
const it = lab.it;
const expect = Code.expect;

describe('Okta', () => {

it('fails with no uri', { parallel: false }, (done) => {

const mock = new Mock.V2();
mock.start((provider) => {

const server = new Hapi.Server();
server.connection({ host: 'localhost', port: 80 });
server.register(Bell, (err) => {

expect(err).to.not.exist();

expect(Bell.providers.okta).to.throw(Error);

mock.stop(done);
});
});
});

it('authenticates with mock and custom uri', { parallel: false }, (done) => {

const mock = new Mock.V2();
mock.start((provider) => {

const server = new Hapi.Server();
server.connection({ host: 'localhost', port: 80 });
server.register(Bell, (err) => {

expect(err).to.not.exist();

const custom = Bell.providers.okta({ uri: 'http://example.com' });

expect(custom.auth).to.equal('http://example.com/oauth2/v1/authorize');
expect(custom.token).to.equal('http://example.com/oauth2/v1/token');

Hoek.merge(custom, provider);

const profile = {
sub: '1234567890',
nickname: 'steve_smith',
given_name: 'steve',
middle_name: 'jared',
family_name: 'smith',
email: 'steve@example.com'
};

Mock.override('http://example.com/oauth2/v1/userinfo', profile);

server.auth.strategy('custom', 'bell', {
password: 'cookie_encryption_password_secure',
isSecure: false,
clientId: 'okta',
clientSecret: 'secret',
provider: custom
});

server.route({
method: '*',
path: '/login',
config: {
auth: 'custom',
handler: function (request, reply) {

reply(request.auth.credentials);
}
}
});

server.inject('/login', (res) => {

const cookie = res.headers['set-cookie'][0].split(';')[0] + ';';
mock.server.inject(res.headers.location, (mockRes) => {

server.inject({ url: mockRes.headers.location, headers: { cookie } }, (response) => {

Mock.clear();
expect(response.result).to.equal({
provider: 'custom',
token: '456',
expiresIn: 3600,
refreshToken: undefined,
query: {},
profile: {
id: '1234567890',
username: 'steve@example.com',
displayName: 'steve_smith',
firstName: 'steve',
lastName: 'smith',
email: 'steve@example.com',
raw: profile
}
});
mock.stop(done);
});
});
});
});
});
});
});