Skip to content

Commit

Permalink
Support authentication credentials verification. Closes hapijs#3885
Browse files Browse the repository at this point in the history
  • Loading branch information
hueniverse committed Nov 22, 2018
1 parent 82f2870 commit 4dbd0d2
Show file tree
Hide file tree
Showing 3 changed files with 160 additions and 2 deletions.
52 changes: 50 additions & 2 deletions API.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
- [`server.options.plugins`](#server.options.plugins)
- [`server.options.port`](#server.options.port)
- [`server.options.query`](#server.options.query)
- [`server.options.query.parser`](#server.options.query.parser)
- [`server.options.router`](#server.options.router)
- [`server.options.routes`](#server.options.routes)
- [`server.options.state`](#server.options.state)
Expand Down Expand Up @@ -56,6 +57,7 @@
- [Authentication scheme](#authentication-scheme)
- [`server.auth.strategy(name, scheme, [options])`](#server.auth.strategy())
- [`await server.auth.test(strategy, request)`](#server.auth.test())
- [`await server.auth.verify(request)`](#server.auth.verify())
- [`server.bind(context)`](#server.bind())
- [`server.cache(options)`](#server.cache())
- [`await server.cache.provision(options)`](#server.cache.provision())
Expand Down Expand Up @@ -239,8 +241,8 @@
- [`request.server`](#request.server)
- [`request.state`](#request.state)
- [`request.url`](#request.url)
- [`request.active()`](#request.active())
- [`request.generateResponse(source, [options])`](#request.generateResponse())
- [`request.active()`](#request.active())
- [`request.log(tags, [data])`](#request.log())
- [`request.route.auth.access(request)`](#request.route.auth.access())
- [`request.setMethod(method)`](#request.setMethod())
Expand Down Expand Up @@ -1218,6 +1220,14 @@ An authentication scheme is an object with the following properties:
- `async response(request, h)` - (optional) a [lifecycle method](#lifecycle-methods) to decorate
the response with authentication headers before the response headers or payload is written.

- `async verify(auth)` - (optional) a method used to verify the authentication credentials provided
are still valid (e.g. not expired or revoked after the initial authentication) where:
- `auth` - the [`request.auth`](#request.auth) object containing the `credentials` and
`artifacts` objects returned by the scheme's `authenticate()` method.
- the method throws an `Error` when the credentials passed are no longer valid (e.g. expired or
revoked). Note that the method does not have access to the original request, only to the
credentials and artifacts produced by the `authenticate()` method.

- `options` - (optional) an object with the following keys:
- `payload` - if `true`, requires payload validation as part of the scheme and forbids routes
from disabling payload auth validation. Defaults to `false`.
Expand Down Expand Up @@ -1326,6 +1336,44 @@ server.route({
});
```


### <a name="server.auth.verify()" /> `await server.auth.verify(request)`

Verify a request's authentication credentials against an authentication strategy where:

- `request` - the [request object](#request).

Return value: nothing if verification was successful, otherwise throws an error.

Note that the `verify()` method does not take into account the route authentication configuration
or any other information from the request other than the `request.auth` object. It also does not
perform payload authentication. It is limited to verifying that the previously valid credentials
are still valid (e.g. have not been revoked or expired). It does not include verifying scope,
entity, or other route properties.

```js
const Hapi = require('hapi');
const server = Hapi.server({ port: 80 });

server.auth.scheme('custom', scheme);
server.auth.strategy('default', 'custom');

server.route({
method: 'GET',
path: '/',
handler: async function (request, h) {

try {
const credentials = await request.server.auth.verify(request);
return { status: true, user: credentials.name };
}
catch (err) {
return { status: false };
}
}
});
```

### <a name="server.bind()" /> `server.bind(context)`

Sets a global context used as the default bind object when adding a route or an extension where:
Expand Down Expand Up @@ -4590,7 +4638,7 @@ Authentication information:
- `credentials` - the `credential` object received during the authentication process. The
presence of an object does not mean successful authentication.

- `error` - the authentication error is failed and mode set to `'try'`.
- `error` - the authentication error if failed and mode set to `'try'`.

- `isAuthenticated` - `true` if the request has been successfully authenticated, otherwise `false`.

Expand Down
25 changes: 25 additions & 0 deletions lib/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,31 @@ exports = module.exports = internals.Auth = class {
return response.data.credentials;
}

async verify(request) {

const auth = request.auth;

if (auth.error) {
throw auth.error;
}

if (!auth.isAuthenticated) {
return;
}

if (auth.strategy === 'bypass') {
return;
}

const strategy = this._strategies[auth.strategy];
if (!strategy.methods.verify) {
return;
}

const bind = strategy.methods;
await strategy.methods.verify.call(bind, auth);
}

static testAccess(request, route) {

const auth = request._core.auth;
Expand Down
85 changes: 85 additions & 0 deletions test/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -1323,6 +1323,91 @@ describe('authentication', () => {
});
});

describe('verify()', () => {

it('verifies an authenticated request', async () => {

const implementation = (...args) => {

const imp = internals.implementation(...args);
imp.verify = async (auth) => {

await Hoek.wait(1);
if (auth.credentials.user !== 'steve') {
throw Boom.unauthorized('Invalid');
}
};

return imp;
};

const server = Hapi.server();
server.auth.scheme('custom', implementation);
server.auth.strategy('default', 'custom', { users: { steve: { user: 'steve' }, john: { user: 'john' } } });

server.route({
method: 'GET',
path: '/',
options: {
auth: {
mode: 'try',
strategy: 'default'
},
handler: async (request) => {

if (request.auth.error &&
request.auth.error.message === 'Missing authentication') {

request.auth.error = null;
}

return await server.auth.verify(request) || 'ok';
}
}
});

const res1 = await server.inject('/');
expect(res1.result).to.equal('ok');

const res2 = await server.inject({ url: '/', headers: { authorization: 'Custom steve' } });
expect(res2.result).to.equal('ok');

const res3 = await server.inject({ url: '/', headers: { authorization: 'Custom unknown' } });
expect(res3.result.message).to.equal('Missing credentials');

const res4 = await server.inject({ url: '/', credentials: {} });
expect(res4.result).to.equal('ok');

const res5 = await server.inject({ url: '/', headers: { authorization: 'Custom john' } });
expect(res5.result.message).to.equal('Invalid');
});

it('skips when verify unsupported', async () => {

const server = Hapi.server();
server.auth.scheme('custom', internals.implementation);
server.auth.strategy('default', 'custom', { users: { steve: { user: 'steve' } } });

server.route({
method: 'GET',
path: '/',
options: {
auth: {
mode: 'try',
strategy: 'default'
},
handler: async (request) => {

return await server.auth.verify(request) || 'ok';
}
}
});

const res = await server.inject({ url: '/', headers: { authorization: 'Custom steve' } });
expect(res.result).to.equal('ok');
});
});

describe('access()', () => {

it('skips access when unauthenticated and mode is not required', async () => {
Expand Down

0 comments on commit 4dbd0d2

Please sign in to comment.