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

feat: allow passing loginHint option to ensureAuthenticated #40

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
# 4.5.0

### Features

- [#40](https://github.com/okta/okta-oidc-middleware/pull/34) Allows passing `loginHint` to `ensureAuthenticated`

# 4.4.0

### Bug Fixes
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ oidc.on('error', err => {
});
```

#### oidc.ensureAuthenticated({ redirectTo?: '/uri' })
#### oidc.ensureAuthenticated({ redirectTo?: '/uri', loginHint?: 'username' })

Use this to protect your routes. If not authenticated, this will redirect to the login route and trigger the authentication flow. If the request prefers JSON then a 401 error response will be sent.

Expand All @@ -229,6 +229,8 @@ app.get('/protected', oidc.ensureAuthenticated(), (req, res) => {

The `redirectTo` option can be used to redirect the user to a specific URI on your site after a successful authentication callback.

Passing `loginHint` option will append `login_hint` query parameter to URL when redirecting to Okta-hosted sign in page.

#### oidc.forceLogoutAndRevoke()

Use this to define a route that will force a logout of the user from Okta and the local session. Because logout involves redirecting to Okta and then to the logout callback URI, the body of this route will never directly execute. It is recommended to not perform logout on GET queries as it is prone to attacks and/or prefetching misadventures.
Expand Down
3 changes: 3 additions & 0 deletions src/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
{
"env": {
"node": true
},
"parserOptions": {
"ecmaVersion": 2018
}
}
11 changes: 10 additions & 1 deletion src/connectUtil.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@ connectUtil.createOIDCRouter = context => {
};

connectUtil.createLoginHandler = context => {
const passportHandler = passport.authenticate('oidc');
const csrfProtection = csrf();
const ALLOWED_OPTIONS = ['login_hint'];

return function(req, res, next) {
const viewHandler = context.options.routes.login.viewHandler;
Expand Down Expand Up @@ -76,6 +76,15 @@ connectUtil.createLoginHandler = context => {
return res.redirect(authorizationUrl);
});
}
const options = Object.keys(req.query).reduce((opts, option) => {
return ALLOWED_OPTIONS.includes(option) ? {
...opts,
[option]: req.query[option]
} : {
...opts
}
}, {})
const passportHandler = passport.authenticate('oidc', options);
return passportHandler.apply(this, arguments);
}
};
Expand Down
14 changes: 12 additions & 2 deletions src/oidcUtil.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,17 @@ function customizeUserAgent(options) {
return options;
}

function appendOptionsToQuery(url, options) {
if (options.loginHint) {
const urlObject = new URL(url, 'relative:///');
const searchParams = urlObject.searchParams;
searchParams.append('login_hint', options.loginHint);
// extend original query (if any)
return `${url.split('?').shift()}${urlObject.search}`;
}
return url;
}

oidcUtil.createClient = context => {
const {
issuer,
Expand Down Expand Up @@ -133,9 +144,8 @@ oidcUtil.ensureAuthenticated = (context, options = {}) => {
if (req.session) {
req.session.returnTo = req.originalUrl || req.url;
}

const url = options.redirectTo || context.options.routes.login.path;
return res.redirect(url);
return res.redirect(appendOptionsToQuery(url, options));
}

next();
Expand Down
44 changes: 44 additions & 0 deletions test/unit/connectUtil.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
const connectUtil = require('../../src/connectUtil.js');

jest.mock('csurf', function () {
return function () {

}
});

var mockAuthenticate;
jest.mock('passport', function () {
mockAuthenticate = jest.fn().mockReturnValue(() => {})
return {
authenticate: mockAuthenticate
}
})



describe('connectUtil', function () {
describe('createLoginHandler', function () {
it('passes known options to passport handler initializer', function () {
const loginHandler = connectUtil.createLoginHandler({
options: {
routes: {
login: {
}
}
}
});
const res = {};
const req = {
query: {
login_hint: '[email protected]',
chown_base: true
}
};

loginHandler(req, res);
expect(mockAuthenticate).toBeCalledWith('oidc', {
login_hint: '[email protected]'
});
});
});
});
29 changes: 27 additions & 2 deletions test/unit/oidcUtil.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,16 @@ const passport = require('passport');
const OpenIdClient = require('openid-client');
const oidcUtil = require('../../src/oidcUtil.js');

jest.mock('negotiator', function () {
return function () {
return {
mediaType: function () {
return 'text/html';
}
}
}
});

function createMockOpenIdClient(config={}) {
const Issuer = OpenIdClient.Issuer;

Expand Down Expand Up @@ -94,6 +104,21 @@ describe('oidcUtil', function () {
expect(error).toEqual(undefined);
};
passportStrategy.authenticate(createMockRedirectRequest());
})
})
});
});

describe('ensureAuthenticated', () => {
it('appends known options to redirect URL', () => {
const requestHandler = oidcUtil.ensureAuthenticated({}, {
redirectTo: '/login',
loginHint: '[email protected]'
});
let req = jest.mock();
let res = {
redirect: jest.fn()
};
requestHandler(req, res, () => {});
expect(res.redirect).toBeCalledWith('/login?login_hint=username%40org.org');
});
});
})