Add draft authtoken spec#602
Conversation
This spec aims to be a replacement for the EXTJWT pull request by specifying a more general mechanism for generating and validating authentication tokens. Unlike EXTJWT, this does not limit the tokens to the JWT format, instead strongly preferring single-use OTPs (although JWTs are still permissible and supported by the spec). Validation occurs through the IRC protocol as well, to reduce implementation burden on servers (it's just another command, rather than needing to spin up an httpd or other external service). Co-authored-by: whitequark <whitequark@whitequark.org>
|
Ergochat has a PR implementing |
slingamn
left a comment
There was a problem hiding this comment.
I updated my PR implementation. Overall I really like this spec!
|
|
||
| ``` | ||
| Client: | ||
| TOKEN VALIDATE FILEHOST https://upload.example.com :54a333f6e0c218382ab5f64c1135f07798d0255e87115c451ba67cc440f5ea84 |
There was a problem hiding this comment.
Do we really need the URL as a parameter here? The client can validate it from the batch parameter in the reply batch. It may also slightly constrain the lengths of URLs that can be used (since a single-line token can be up to 200 bytes).
There was a problem hiding this comment.
Reason for the URL here was to prevent token confusion attacks. Say an ircd is migrating from an old filehost to a new one; once the migration happens, we don't want users to be able to upload to the old filehost anymore. If all that was passed was the service key, we have no real means of preventing this without trusting the client. If the service also passes its own URL, then the server can recognize that it's not meant to be interacting with that service and reject the validate attempt. I think it's important to keep, which is partly why the byte limit for single-line tokens is 200 in order to give more space for the URL.
Edit: your suggestion to recommend a different key per service is another way to avoid the above scenario while not requiring a URL parameter.
There was a problem hiding this comment.
I'm not totally clear on whether the threat model here is accident or fraud, so to speak. If it's fraud, the new service's URL is not actually a secret, so nothing prevents the old service from passing it instead of its own URL. If it's accident, the service can guard against this itself by validating the URL returned in the batch parameters (the same way JWT consumers normally validate the aud claim).
Rotating the key would work too if the tokens are JWTs signed with a unique per-service key, or MAC'ed UUIDs with a key held only by the ircd. But I'm not sure that's even relevant given what we're trying to prevent.
It seems like maybe the real concern here is usability, not security, in that we could use some way to push updated service lists to clients?
There was a problem hiding this comment.
I wonder if a CAP to subscribe to updates like authtoken-notify would be useful to combat the usability problem. If negotiated, the ircd could output the TOKEN SERVICELIST on changes or any other relevant notify to tell the client that things have changed.
Reconnection being the only way to receive external service updates seems undesirable. That encourages polling, which is just inefficient overall. Your client could track the URL parameter, but I don't like that as much as having an implied mechanism for it.
There was a problem hiding this comment.
If the threat model is "the old filehost has turned evil, and is trying to impersonate the user to the new filehost", that seems generally impossible to prevent in this design (with or without interactive validation), since it's equivalent to MITM of the bearer token.
|
|
||
| - The service must validate that the token is intended for that service, to avoid token confusion attacks. Using URL-based validation (i.e. checking that one of the claims of the token matches the URL of the service) is a good way to do this. For JWT, this would be the "aud" claim. | ||
| - The service must validate that the token came from the IRC server it is expecting tokens to come from. For JWT, this would be the "iss" claim. | ||
| - The service must validate that the token is signed by the IRC server with a secure algorithm and that the signature is valid. The signing keys (recommended) or secrets (not recommended) should be pre-shared out of band. |
There was a problem hiding this comment.
Is "secrets" intended to refer to symmetric signing keys used by, e.g. HS256? Discouraging this seems unwarranted to me.
There was a problem hiding this comment.
Yes, this section was going over what claims should be present in tokens that are non-interactively validated (i.e. the service is not using TOKEN VALIDATE). In such an event, HS256 or similar symmetric algorithms means that a secret key is being shared between multiple parties, since the specification makes no assumption that the ircd and external service are being run by the same operators. This in turn means that the external service could spoof its own properly-signed tokens without the ircd's input, for whatever that might allow it to do (in theory not much, but the risk exists that there are now multiple parties able to issue valid signed tokens).
If the ircd is intending for interactive validation only (i.e. external services must use TOKEN VALIDATE to get the claims list), then HS256 is perfectly appropriate because the secret key only lives in one place: on the ircd. Doing public key in such scenarios would be overkill and unnecessary.
There was a problem hiding this comment.
Makes sense. I was thinking that if you're doing one key per service, there would be no real disadvantage to using a symmetric secret key. But actually, with a symmetric key, the service would be able to sign its own tokens and then submit them for online validation, possibly learning some user data via TOKEN CLAIM that it might not normally be able to access. (If alice uses QDB and but bob never does, QDB would have normally have access to alice's TOKEN CLAIM outputs, but not bob's.) Probably not a big deal, but a real consideration.
There was a problem hiding this comment.
This suggests another hardening recommendation for JWTs: if interactive validation is not needed, it can be administratively disabled. And then there's really no problem with HS256 with unique keys: the service can forge whatever token it wants, but it's the only system that will accept the token.
If interactive validation is required, I think a unique asymmetric keypair per service should work (the ircd has the private key, the service has the public key, the general public has nothing).
There was a problem hiding this comment.
This in turn means that the external service could spoof its own properly-signed tokens without the ircd's input
Is it realistic to expect there would be services that are hosted by a potentially unstrusted 3rd-party and yet require the IRCd as authorisation provider? That's a weird combo if the IRCd isn't a generic auth provider, and external services instead have to be manually added to the SERVICELIST by network operators.
What is the intent of this spec? Allowing networks to host additional services with SSO but without HTTP-dependent standards like OIDC, or is it turning networks into generic decentralised authentication providers, like a IRC-based Open-ID clone?
There was a problem hiding this comment.
The intent is for federated authentication and authorization with allowlisted services, which may or may not be run by 3rd parties. Partnering with a 3rd party for some service doesn't imply a full-trust relationship, and it makes sense to limit the blast radius of potential security issues as much as possible in the event that the 3rd party is compromised. HTTP-based mechanisms are not specified because IRC is not HTTP, and there are likely going to be non-HTTP-based services that wish to integrate with IRC for authn/authz decisions in this manner.
By specifying both the token generation and token validation layers, these services do not need to ship a validation implementation on a per-ircd basis. This increases interoperability by allowing network operators to choose services based on criteria other than "do they support the specific magic incantations needed to talk to the ircd I'm running"
As a note, while JWTs are popular (which is why there is a lot of space devoted to talking about them), the spec recommends single-use randomly generated OTPs and interactive validation. These are significantly more secure, more compact, and much easier to implement. The JWT discussion is primarily around how to avoid the numerous footguns that the JWT specification comes with and have resulted in security vulnerabilities in other services, without the implementer needing to be an expert in JWT security and/or having done all of that prior research. There are a lot of really bad JWT implementations out there and I don't want this spec to be another one in that pile.
| BATCH -12345 | ||
| ``` | ||
|
|
||
| ### VALIDATE subcommand (batched) |
There was a problem hiding this comment.
Ingesting C2S-batched VALIDATE was the hardest implementation step for me. I'm not sure there's a better option overall, but I wanted to air this issue: C2S batches might seem like they simplify server ingestion of multiline requests, but I don't think they do, because each type of C2S batch will have its own application-specific constraints, length limits, and abuse risks, so you can't really write a generic "ingest C2S batch" handler. (For example, in the Ergo implementation, we turn off fakelag when a draft/multiline batch is in progress, so we need to make sure we turn it back on on every fail path that ends the batch.)
There was a problem hiding this comment.
Another pain point for me was splitting input validation and permissions checking logic between the BATCH and VALIDATE handlers.
This would actually be cleaner for me if it were just:
VALIDATE TOKEN <servicename> <single-line token under 300 bytes>
or else
VALIDATE TOKEN <servicename> <multi-line token chunk exactly 300 bytes>
[repeat zero or more times]
VALIDATE TOKEN <servicename> <final token chunk under 300 bytes, possibly 0 bytes>
I invite comment as to whether this is cleaner in general, or just an idiosyncratic constraint imposed by my implementation :-) I would note in passing that VALIDATE TOKEN is not intended for use in "normal" IRC traffic, so the full extensibility of batch (e.g. allowing nested batches, allowing interspersed messages that are not part of the batch) is not directly relevant.
There was a problem hiding this comment.
My solanum C2S batch implementation involves a generic handler that works across every registered batch type to collect the batched lines together and fire off whatever function is supposed to handle the batch once the entire thing is received, so your inability to do that is certainly an ergo limitation rather than an inherent limitation of C2S batches (this is just an observation; I'm not saying that your argument lacks merit). For reference/comparison, solanum doesn't turn off fakelag for C2S batches. It might take a while to send it but once fully sent it gets burst out all at once.
I originally had it worded to be like AUTHENTICATE where each intermediate line was a fixed byte count and the final line was the leftovers (or blank), however I felt that C2S batches were a more elegant solution that requires fewer bespoke per-command multiline handlers on the client and server side -- AUTHENTICATE is 400 bytes, this could be 400 or might be 300, something else may choose some other limit, etc. I'd also like to drive more adoption of C2S batches in general as a useful framing tool for things that take up more than a line.
There was a problem hiding this comment.
Hmm, from the n=2 sample I have, I'm not convinced that C2S batching is a savings on implementation complexity. From my point of view, the server implementation still needs (1) custom validation rules, per batch type, about what can or can't be in the batch (2) custom length limits (which also have to be batch-type-aware). And then there generally seems to be a conservation effect, where any validation or limiting logic you try to make generic at the ingestion stage has to be made up later. (For example, you could try to implement generic length limits based on total byte size of the messages being queued, but you have to apply the real length limit later after doing whatever application-specific aggregation is required.)
One of the things that makes C2S batching compelling for draft/multiline is the interaction with labeled-response and echo-message (the client labels the initial BATCH line, then gets a labeled echo message after the whole batch is complete). What are some of the other use cases you're thinking about for C2S batching?
There was a problem hiding this comment.
What are some of the other use cases you're thinking about for C2S batching?
Rich content (aka "embeds" in discord or "blocks" in slack) is the main one I have in mind. Basically, a message that contains some title, content, optional image, optional buttons along the bottom, etc. I don't have anything written down for that yet but C2S batch would be the natural way of sending that and collecting all of those disparate things together as a single message.
There was a problem hiding this comment.
The other way of doing that is just a client tag. e.g. ircv3/ircv3-ideas#22 although the fallback mentioned there could potentially be batched along with it.
There was a problem hiding this comment.
I guess also if we wanted to go over 4 KB we would have to batch it?
- Reword a couple of sections to make motivations clearer. - Make SERVICELIST the canonical place for services to live, avoiding duplication in ISUPPORT or CAP. The draft/authtoken cap was made a requirement to support this feature, although the C2S batch that it enables remains optional. - Make SERVICELIST replies use a batch instead of NOTE for the standard case.
| A user sends `TOKEN SERVICELIST` on a network where no services are configured. As such, they receive a note that no services are defined. | ||
|
|
||
| ``` | ||
| NOTE TOKEN NO_SERVICES :No services are defined for this network. |
There was a problem hiding this comment.
Would TOKEN NO_SERVICES :No services are defined (i.e. NO_SERVICES as a new subcommand of TOKEN) make it easier for clients to route this message to a handler that clears the service list?
There was a problem hiding this comment.
It wouldn't be easier necessarily, but I agree that a NOTE might not be the right reply, since NOTE seems to generally be informational, but I do feel that using a Standard Reply here is right, perhaps WARN instead? Slightly more actionable than a NOTE in terms of client handling
| TOKEN SERVICELIST | ||
| ``` | ||
|
|
||
| The `TOKEN SERVICELIST` subcommand provides a list of all recognized service keys as well as their corresponding URLs. The description parameter provides some sort of description about the service. |
There was a problem hiding this comment.
Is it really the right spec to list services?
Wouldn't it be cleaner to have a separate spec for discovering external services? It could also list services that do not require a token.
There was a problem hiding this comment.
This is necessary for a client to discover service capabilities.
There was a problem hiding this comment.
AIUI this is intended as the unique discovery mechanism for services that use tokens. Services that do not use tokens can be discovered via 005 (as with draft/filehost for example).
This spec aims to be a replacement for the EXTJWT pull request by specifying a more general mechanism for generating and validating authentication tokens. Unlike EXTJWT, this does not limit the tokens to the JWT format, instead strongly preferring single-use OTPs (although JWTs are still permissible and supported by the spec).
Validation occurs through the IRC protocol as well, to reduce implementation burden on servers (it's just another command, rather than needing to spin up an httpd or other external service).