-
-
Notifications
You must be signed in to change notification settings - Fork 751
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
Multiple JWT auth strategies clash #1884
Comments
So I have made some headway on this. The root of the problem is in the core feathers/packages/authentication/src/core.ts Line 234 in 859c601
The method loops over the strategies and calls each strategy's I was able to solve this with class Service extends AuthenticationService {
async handleConnection(event, connection, authResult) {
// The update to the filter function below means that only one
// strategy is being set to the connection.authenticatoin. Previously
// when all strategies (which was really just the one jwt strat),
// the handleConnection basically defaulted to jwt because it was the
// only strat with a handleConnection method. So when authing
// with local, handleConnection just set the one jwt strategy it
// knew about and everything was fine. Since there are multiple
// handleConnections now...we have to manually default local to somehting
if (authResult && authResult.authentication.strategy === 'local') {
authResult.authentication.strategy = 'jwt';
}
const strategies = this.getStrategies(
...Object.keys(this.strategies)
).filter(
current =>
typeof current.handleConnection === 'function' &&
// Only set the connection if this was the strategy that
// was actually used in the authentication
authResult &&
authResult.authentication.strategy === current.name
);
for (const strategy of strategies) {
await strategy.handleConnection(event, connection, authResult);
}
}
} I am not a fan of the part where I reassign the {
strategy: 'local',
username: '...',
password: '...'
} But that is actually correct...because the Instead the local strat's {
strategy: 'jwt', //or how do we specify this default?
accessToken: '...'
} |
Thanks for digging into this! Looks like that part of the code is causing a couple of problems (like the one I've been trying to fix in #1889). What the I think the best way to solve this would be to use the same logic as for header parsing via .parse (where the first |
I don't think that will solve the problem. That still relies on the order in which the strategies are registered. Right now the developer can set I am not sure what the appropriate way for the client to set a "strategy" header would be? I know that is also probably not standard either. But I feel like that would solve a lot of problems. |
With the You're right, that it may also make sense for e.g. the client to indicate what they want to use but that change would at least align connection handling with Authorization header parsing behaviour. |
In my case the I figured this couldn't be the first time this problem has happened. I found this with some quick Googling: hapijs/hapi#3444. That makes sense and kinda what I was thinking anyway. At the end of the day, if you are using the same header scheme for multiple auth strategies you've got to have a switch somewhere that determines what kind of strategy that token is for. That kind of logic probably shouldn't be baked in feathers-auth by default. So on REST I think things work as is. The user can either use a different header scheme per strategy (breaks "standards" but just works), or update the service On to the But, socket connections don't really have a standard in their payload. We are already specifying some DSL in the payload such as On a REST client we already parse the I am thinking you have probably thought of this before and potentially turned away from it? I could see an argument that its "too far from the transport layer". For example because it doesn't happen in socket.io middleware and happens at the service parsing level that it is happening in the wrong place? Not sure if I explained that well. Let me know your thoughts on sending |
I'm facing similar issue for parsing HTTP headers when using multiple JWT strategies. What I did was to check the issuer (JWT strategies should have unique issuer(s)) and return This still relies on the ordering of See this snippet for example implementation. The minor downside is that each |
@DaddyWarbucks is this completed? I was going to see if there is something more that could be done which is why I left it open. |
I suppose not. I was just cleaning up old, open issues I had created. I probably should not have closed this one because it had so many comments. I'll reopen it. Sorry about that! |
Hi everyone, I had a lot of trouble getting multiple JWT strategies working that mixed sockets and REST. The custom service by @DaddyWarbucks helped a lot to understand the main problem, and after spending long hours going deep into the code, I was able to understand the architecture and the problems involved not only with the service but also with the "authenticate" hook. I found the way it was designed to be a bit strange. Well, as I said, I spent a few hours going deep into the code, and I'm going to share with you the strategy I took, which could maybe be considered for inclusion in the future. Let's get started. First, I created my service like @DaddyWarbucks did, with a small difference: I will store the strategy in the token, so that I can use multiple strategies per route in my custom hook. class CustomAuthenticationService extends AuthenticationService {
constructor(app: Application) {
super(app)
}
// Unfortunately, super.getPayload completely ignores _authResult, so we need to rewrite it.
// Otherwise, I could have done this in my own token strategy.
async getPayload(_authResult: AuthenticationResult, params: AuthenticationParams) {
return {
...await super.getPayload(_authResult, params),
// add strategy inside jwttoken, to access after in customAuthenticate
strategy: _authResult.authentication.strategy,
}
}
async handleConnection(event: ConnectionEvent, connection: any, authResult?: AuthenticationResult) {
const strategies = this.getStrategies(...Object.keys(this.strategies)).filter(
(current) =>
typeof current.handleConnection === 'function' &&
authResult?.authentication?.strategy === (current as any).name
)
for (const strategy of strategies) {
await strategy.handleConnection!(event, connection, authResult)
}
}
} Here is an example of my custom strategy for another entity: export class AdminLocalStrategy extends LocalStrategy {
async authenticate(data: any, params: any): Promise<any> {
const result : any = await super.authenticate(data, params);
// this is the authResult, I would like to add the payload that goes to jwt here, but as I said in the comment above, getPayload disregards this guy, although it passes as a parameter
return {
authentication: {
// rewrite, default is always 'jwt' when use LocalStrategy
strategy: 'admin-jwt',
},
user: {
_id: result.user._id,
username: result.user.username
}
}
}
get configuration() {
const config = super.configuration;
return {
...config,
service: adminPath,
usernameField: 'username',
passwordField: 'password',
entityUsernameField: 'username',
entityPasswordField: 'password',
};
}
} And finally my hook customAuthenticate export default (...strategies: string[]) =>
async (context: HookContext, next: NextFunction) => {
const params = context.params
const authService = context.app.service('authentication') as any
// check strategy if socket
if (strategies.includes(params.connection?.authentication?.strategy)) {
const accessToken = params.connection?.authentication?.accessToken
await authService.verifyAccessToken(accessToken, params.jwt)
return next()
}
// check access token from header
const authorization = (params.headers?.authorization || '').split(' ')
if (authorization.length < 2) {
throw new NotAuthenticated()
}
const accessToken = authorization[1]
if (!accessToken) {
throw new NotAuthenticated()
}
// check token strategy (included in CustomAuthenticationService)
const payload = await authService.verifyAccessToken(accessToken, params.jwt)
if (!strategies.includes(payload.strategy)) {
throw new NotAuthenticated()
}
// authenticate to check user
try {
await authService.strategies[payload.strategy].authenticate({ accessToken }, params)
} catch (e) {
throw new NotAuthenticated()
}
return next()
} Now in my services, I use the customAuthenticate import customAuthenticate from "../../hooks/custom-authenticate";
// ...
app.service(zonePath).hooks({
around: {
all: [customAuthenticate('jwt')],
}
})
// or multiple strategy by method
app.service(zonePath).hooks({
around: {
all: [customAuthenticate('jwt', 'admin-jwt')], // jwt or admin-jwt
}
}) It worked great for me, I hope it helps someone |
Steps to reproduce
Create an additional JWT strategy by extending the
JWTStrategy
. Register the new strategy (alongside the default jwt strat) and try to use the new strategy. In this example the new strategy is called "preview"Please see this repo: https://github.com/DaddyWarbucks/test-feathers-auth
for more detailed explanation, comments, and debugging.
I have also noticed that things act differently between a socket provider and rest provider. Checkout the
index.html
to switch betweensetup(socketApp)
andsetup(restApp)
to easily switch between the two.I believe that the problem exists in the
connection
hook for sockets and potentially in the express-auth lib for rest.Expected behavior
When making a request to a service that has the
authenticate('preview')
hook, the request is authenticated with the "preview" strategy.Actual behavior
The request attempts (and fails) to authenticate with the "jwt" strategy.
The text was updated successfully, but these errors were encountered: