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
[NEW] Custom authentication via Modules #11256
Comments
@yossigo FYI, as this is a continuation of our older discussion about a better module auth. I'm not sure how I feel about overloading the the RedisModule_BlockClient() API, since the unblock API has a callback that includes the argv/argc of the client. I think we should introduce a new blocking API just for the auth, so that the callback can correctly pass in the username and password. I'm also not convinced about the RedisModule_DenyAuth approach. It does seem like RedisModuleAuthCallback could return whether or not the authentication was successful after setting the user. We could update the API to:
So that you could pass back an error string representing the custom error message. Then have the explicit three value response:
I thin having the explicit 3 responses is likely the most developer friendly to understand. I think the rest of it looks good. |
I agree with @madolson that returning the auth result from the auth callback is the most intuitive for the common case when you don't need to block the client. However, I think we need a 4th response value "auth blocked" (or "no result yet") for the case when we need to block the client before we can return success/fail/skip. I think it's a good idea to reuse the BlockClient mechanism, at least internally. We can introduce a new blocked type "blocked for auth". @KarthikSubbarao What do you suggest to use for I have idea how to encapsulate this: We allow the auth callback to return a value indicating that the client shall be blocked (e.g. |
They would also need to supply the user to be authenticated. The current blocking system assumes that unblocking APIs are called on background threads, so we don't have access to main thread functions such as looking up users, so I'm not sure how this would work.
I think this will actually be a fairly common case. Some people might want to override the auth with a kerberos call or something, but I think it's also likely people will federate to something like active directory or vault, which require offbox API calls. |
I agree that it makes the Module's usage simpler if the blocking callback can provide the username and password as args. But, in the current design, I imagined that the Module will first obtain the username and password through the AuthCallback (registered via the RM_RegisterAuthCallback API) when custom authentication is attempted. If the Module decides to use a blocking implementation, it can use RM_BlockClient. Later, when the background job completes, it can call RM_UnblockClient by providing private data that has everything it needs - example - Authentication result, username and password. With this, when the blocking reply callback is triggered, the Module can just use In this design, I tried to reuse the existing blocking interface as much as possible. Because if we create a new BlockClientOnAuth API, we would need to create and provide a new BlockedAuthReply callback and a TimedOutAuthReply callback both which instead of client args have the username and password as args. If this is done just to obtain the username and password as args, maybe the What do you think about using
However, if we want to have different return codes rather than using the RM_DenyAuth API approach, then I think we should create a new BlockClientOnAuth API and also create the two BlockedAuthReply callback and TimedOutAuthReply callback that will return one of the codes above and also have the username and password as function arguments. If we choose this approach, it might make sense to add a 4th code for "Auth blocked" as @zuiderkwast suggested. The concern I have with adding return codes is that Modules can return "Auth successful" but forget to the AuthenticateUser Module API. Similarly, we have the other case where the Module can return "Auth unsuccessful" and still use the AuthenticateUser Module API. I initially thought it was better to have a single place of deciding whether Auth succeeded - return code or the Module API. |
I'm not very happy with the current blocking API if used with its callbacks. It's not intuitive and involves too many edge cases. So, using a callback return value to block and a dedicated auth-unblock makes sense. Still, we'll need to consider all special cases - who is responsible for timeouts, dropped client handling, cancellation, etc. |
@madolson Can't UnblockClientBlockedForAuth use a pthread mutex (like moduleUnblockClientByHandle already does) and then do the necessary lookup from client to user, etc. We can store the user, client, etc. in the CustomAuthContext that this feature introduces, right? The privdata mechanism can be used for passing stuff like the auth result. What am I missing?
@KarthikSubbarao sounds good
@KarthikSubbarao No, I think the auth mechanism can just make a judgement grant/deny/skip based on username and password. AuthenticateUser is for a different purpose IUCC. |
I didn't follow this, can you provide a concrete flow? How are we handling the edge cases that Yossi mentioned, like timeouts? How do we notify the module that there is pending work to cleanup? How are we supporting the flow where we are returning an ACL user?
The current code supports the username not being the same as the existing ACL username. It seems like you are proposing something else entirely. |
We can add some basic sanity checks here to make sure the APIs were implemented correctly. Generally we expect modules to be implemented correctly, since we have no real defense against them doing things wrong. |
@madolson Sorry for my uninformed ideas. 😳 The flow I had in mind only works for existing ACL users. (In my mind, a 'success' return from the auth callback would correspond to an implicit I guess I agree the module should call |
To help clarify what I initially said, this is sample Module code for the original design proposed. It explains how the module can use the APIs to implement blocking custom authentication. Here, to authenticate, they need to use the Auth Module API (example - RedisModule_AuthenticateClientWithACLUser). To have access to the username and password later on in the blocking reply callback, the Module should use RedisModule_Alloc and later on obtain the data using RedisModule_GetBlockedClientPrivateData. The main advantage in this approach is it reuses a lot of the Module blocking APIs. However, it does need a new API - RM_DenyAuth which has specific use case just for returning an error during custom authentication.
|
Based on the comments added, this is some sample Module code for implementing blocking custom authentication using the "return code" approach. Here, both the custom auth callback and the blocking custom auth callback are provided with the username and password as function parameters. Also, both these callbacks are expected to return one of 4 codes: Auth Succeeded, Auth Denied, Auth Not Handled, Auth Blocked. Timeouts for blocking custom authentications could just be handled in the Module if needed rather than relying on an engine timed out callback. The main advantage in this approach is that Module’s usage of custom auth is clearer with the return codes. Also, the username/password are provided directly to the custom auth callback AND in the blocking custom auth callback, so the Module does not have to provide this to the blocking callback as privdata. The drawback in this approach is I needed to create a new BlockClientOnAuth API so that we can block a client on Auth while providing callbacks which suit custom auth better - having return codes and having username/password/err as args. This is not entirely a drawback, but I felt we could have reused the existing BlockClient API.
Two things that made this hard to implement are (1) We need to unblock the client and this requires a BlockedClientContext (2) To authenticate the client, we need to use the RedisModule_AuthenticateClientWith[ACL]User API which needs the RedisModuleContext and this is better done from a blocking reply callback in the Module. Alternatively, if there are any ideas for blocking the client based on the return code and also being able to unblock the client, I can use them in updating this sample code.
Based on the Module's usage for the two approaches for custom auth, I thought I’d get feedback on what approach makes sense |
The problem/use-case that the feature addresses
In Redis, we have password based authentication for Redis Users. Only supporting this mode of authentication is limiting, as applications might require other variations of authentication.Description of the feature
This issue is about adding new custom auth functionality allowing Redis Modules to implement their own auth logic. And this will be by registering hooks and extending the Authentication flow so thatAUTH
andHELLO
commands are intercepted through these custom hooks. We are introducing two new Module APIs - RM_RegisterAuthCallback and RM_DenyAuth.Some of the main requirements for the feature are:
nopass
flag. (Following the same behavior for the current password based auth)RegisterAuthCallback API
Modules will use this API to register a custom auth callback by providing a function which accepts a RM_Context, username, and password as args. Internally this API will create a new CustomAuthContext that contains the callback and the module that providing the callback - this context is added to a linked list of custom auth contexts. Once registered, when the AUTH or HELLO with the AUTH sub command is used, Module based authentication is attempted.The same Module is allowed to register multiple Auth callbacks. And multiple Modules are allowed to register Auth callbacks at a given time.
Custom Auth
The existing auth flow goes through ACLAuthenticateUser which currently validates using password based authentication. Now, this is extended to also authenticate via module / custom auth callbacks. The username and password are provided to the callbacks and the Module can handle this appropriately - either authenticating, denying auth, or neither (skipping).Inside ACLAuthenticateUser, we will first attempt Module based Authentication to allow the Module to decide whether the client should be authenticated / denied auth. In case that all Modules “skip” by not authenticating / denying auth, we will attempt password based auth.
Custom auth will be attempted for all Redis users (created through the
ACL SETUSER
cmd or through Module APIs) provided that they are not disabled AND they do not have thenopass
flag. When a user has thenopass
flag, custom auth will always succeed, just like in the case of password based auth.For the AUTH command, we will support both variations -
AUTH <username> <password>
andAUTH <password>
. In case of theAUTH <password>
variation, the custom auth callbacks are triggered with “default” as the username and password as what is provided.When module / custom auth is attempted, we iterate through the linked list of all the auth callbacks registered and attempt custom auth serially until one of the callbacks either authenticates (using the Module Auth APIs) OR denies authentication using a new API explained below. If the module wants to allow the engine to attempt other custom auth callbacks, they do should not use either API. If all the modules ignore the callback, password based auth is attempted and this will be used to decide whether AUTH succeeded or failed.
Deny Auth API
Modules can use this API to return an error during authentication. It will cancel the client’s authentication process and break the Auth chain (if one exists) by returning an error to the client with a custom error message. The error message will be immediately added as an error reply to the client. If no custom error message is provided, the standard auth error message is added as an error reply.This API can only be used when custom auth is in progress and will only return an ERR for the AUTH/HELLO command that is in progress. For all future commands, the Module will need to use the DenyAuth API once again to reject the command.
Implementing a blocking behavior
The Module can implement a blocking behavior from the custom auth callbacks. When the callback is triggered, they can use RM_BlockClient and start a background job. And when the background job finishes, they can call the RM_UnblockClient API and utilize the blocked client reply callback to either authenticate (using RM_AuthenticateClient...) or deny auth (using RM_DenyAuth) or skip. Because of this reason, the custom auth callback is a void function. When the callback is triggered, in case of a blocking implementation, we will not know whether auth succeeded or failed as auth is still in progress. The blocked client reply callback is used for this purpose.Expected behavior of Modules
RedisModule_AuthenticateClientWithUser
API or theRedisModule_AuthenticateClientWithACLUser
API.RedisModule_DenyAuth
AND should use theRedisModule_ACLAddLogEntry
API to add to failure logs.auth
)setname
variation, the engine will set the name of the client in the success case. Modules should not use thesetname
API - unless they have a specific use case other than setting this as part of the HELLO command.If this high level design / approach sounds good, I will submit a PR
The text was updated successfully, but these errors were encountered: