Skip to content

Commit

Permalink
feat: allow passing loginHint option to ensureAuthenticated
Browse files Browse the repository at this point in the history
  • Loading branch information
oleksandrpravosudko-okta committed Jan 24, 2022
1 parent 23bba66 commit 51c1c95
Show file tree
Hide file tree
Showing 6 changed files with 99 additions and 6 deletions.
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: 'username@org.org',
chown_base: true
}
};

loginHandler(req, res);
expect(mockAuthenticate).toBeCalledWith('oidc', {
login_hint: 'username@org.org'
});
});
});
});
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: 'username@org.org'
});
let req = jest.mock();
let res = {
redirect: jest.fn()
};
requestHandler(req, res, () => {});
expect(res.redirect).toBeCalledWith('/login?login_hint=username%40org.org');
});
});
})

0 comments on commit 51c1c95

Please sign in to comment.