hapijs / hapi

The Simple, Secure Framework Developers Trust

Home Page:https://hapi.dev

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Multiple Strategies not working on Hapi v20.xx

ihatelactose opened this issue · comments

Support plan

  • is this issue currently blocking your project? (yes/no): yes
  • is this issue affecting a production system? (yes/no): yes

Context

  • node version: v16.15.1
  • module version: v20.2.1 (hapi version)
  • environment (e.g. node, browser, native): node
  • used with (e.g. hapi application, another framework, standalone, ...): hapi
  • any other relevant information: None

How can we help?

I am trying to use multiple strategies in Hapi v20.2.1, they are properly implemented however I can't seem to get another strategy to trigger if one fails.

This is how I have registered by schemes:

server.auth.scheme('request-signature', requestSignatureScheme);
server.auth.scheme('api-key-scheme', apiKeyScheme);

server.auth.strategy('signature', 'request-signature', {
    payload: 'required'
});
server.auth.strategy('api-key', 'api-key-scheme');

These schemes are self made and if tested like this:

auth: {
    strategy: 'signature'    // or 'api-key'
}

Like above if tested in isolation they work properly however when I do the following:

auth: {
    strategies: ['signature', 'api-key']
},

The moment the signature one throws an unauthorized (I am using Boom) it returns a 400 immediately.

authenticate: async (request, h) => {
    // stuff taken from request

    if (method === 'get') {
        try {
            // Some logic

            return someVariable = someOtherVariable
               ? h.continue
               : unauthorized(null, 'api-key-scheme');
        } catch (error) {
            return unauthorized(null, 'api-key-scheme');
        }
    } else {
        return h.authenticated({
            credentials: {
                result: 'authenticated'
            }
        });
    }
},

I have also tried with h.response(unauthorized(null , 'api-key-scheme')) but that also doesn't seem to work.

What is your scheme doing exactly? hapi only allows chaining when the error message is empty.
See https://hapi.dev/api/?v=21.0.0-beta.1#authentication-scheme and https://github.com/johnbrett/hapi-auth-bearer-token/blob/master/lib/index.js#L83

@AdriVanHoudt alright I figured some stuff out with the documentation. So here is what my scheme tries, the signature scheme validates the payload via a signed signature, think of it as a string, now the thing is that if the user has a payload we use request.payload to re-create the signed string. Since request.payload is null in the authenticate block therefore we use the payload block to make the check.

Now the thing is everything works fine just except, that if I throw an error like throw Boom.unauthorized(null, 'Scheme-name') in the payload block the next strategy (or scheme mentioned in Boom) is never triggered it just shows a 401 Unauthorized straight up.

payload: async (request, h) => {
    // ... other stuff
    const body = request.payload;
    const _signature = await generateStringForValidation(
        // ... other stuff
        body    // this is needed that's why using `payload`
    );

    return headers.signature === _signature
        ? h.continue
        : Boom.unauthorized(null, 'Api-key-scheme');  // never triggers `Api-key-scheme` but shows 401
}

I also tried throwing it but to no avail.

if (headers.signature === _signature) {
    return h.continue;
} else {
    throw Boom.unauthorized(null, 'Api-key-scheme');
}

When you use payload: 'required' on the strategy, that indicates that there's no way to skip over the payload authentication.

I think the key here is that payload authentication needs to be marked as optional on both the strategy and the route. Here's an example of what that looks like in the tests:

hapi/test/auth.js

Lines 1782 to 1801 in 8540cd3

it('skips optional payload', async () => {
const server = Hapi.server();
server.auth.scheme('custom', internals.implementation);
server.auth.strategy('default', 'custom', { users: { optionalPayload: { payload: Boom.unauthorized(null, 'Custom') } } });
server.auth.default('default');
server.route({
method: 'POST',
path: '/',
options: {
handler: (request) => request.auth.credentials.user,
auth: {
payload: 'optional'
}
}
});
const res = await server.inject({ method: 'POST', url: '/', headers: { authorization: 'Custom optionalPayload' } });
expect(res.statusCode).to.equal(204);
});

Hope this helps!

hey @devinivy that indeed is correct, marking the payload as optional in both the strategy and the route itself will work. However that will make the payload validation entirely useless, the idea here is that even if authenticate does succeed the payload validation should happen and if the payload is invalid that should fail the authentication and trigger the next strategy in line.

@ihatelactose - I ran into a similar situation about a year ago, and the best solution that I came up with was to make the payload validation the last in the chain for strategies. Perhaps:

strategies: ['api-key','signature']

Where if the api-key fails, it then moves on to signature. Not sure if that will help your use case, but thought I would throw that out there,

if the payload is invalid that should fail the authentication and trigger the next strategy in line.
...
Where if the api-key fails, it then moves on

Ah, I think I finally caught-on to the issue here. Using a list of strategies isn't about failing and moving on. Rather, hapi is talking about the authentication being deemed missing then moving-on. The unauthenticated(null, scheme) is how you represent a missing auth scheme. By the time you're verifying the payload, we already know that the scheme is not missing, since that would have been determined in authenticate().

I am pretty sure you both have correctly identified that hapi's strategy list doesn't work the way you are hoping to use it. However, hapi does provide the APIs you need to write this logic yourself, particularly with server.auth.test(). You might check out the implementation of the nuisance scheme to see how this can work.

@devinstewart thanks a lot, the thing is, that what you mentioned is what I am doing right now. That's why we are still running on production without much issues. But I want to make sure that this strategy is future-proof just in-case we have something else.

@devinivy oh that's something, lemme look into it first thing tomorrow. Thanks a lot!