ircv3-specifications
ircv3-specifications copied to clipboard
EXTJWT command for integrating external web services
This spec provides a way for web services hosted externally to an IRC server to authenticate users that are connected to the IRC server by making use of the standard JWT tokens (https://jwt.io/).
This allows a web service to do things such as:
- Granting admin access to a networks wiki page if a user has +o on the network
- Granting write access to a channels wiki page if the user has +o in the channel
- Automatically creating a user account if the user is logged into the IRC server and has an account name
For a more indepth example we could use the free audio/video conference service - Jitsi Meet. This service has built in JWT verification in that an application can send a user to a URL that contains a JWT token, and if the Jitsi Meet server verifies this token successfully, the user is granted access to that conference room.
When an IRC client wants to join a conference room, it would first call EXTJWT #testchannel to receive a JWT token from the IRCd. The client would then open a browser window navigating to the Jitsi Meet URL while passing that token. It is up to the client to decide how and where to use this token, eg. via a "Jitsi Call" button for example.
Looks like a cool idea! Even if it does not get implemented by IRC daemons (or networks), this could be partially provided by (non-privileged) bots.
How should a server behave if there was an issue with generating the JWT for some reason?
Is it useful to add a context parameter to the command so that JWTs can be generated with different secrets for integrating with multiple services? e.g. EXTJWT #channel servicename.
@SaberUK I could imagine that use case, yes. If I make the * in EXTJWT * required instead of optional as you questioned in another comment, then the service name could be an optional second parameter, EXTJWT * [servicename] / EXTJWT #channel [servicename]
I like this, sounds like a nice way to let nets setup external services without giving db access and similar. From the looks of it, it's not useful for services being setup without the network's prior OK (agreement on the secret, etc), and that's intended?
Will look at writing up an implementation of this in Ora. The servicename stuff being discussed above sounds especially useful, once that change is resolved I'll write up our impl.
I'm about to update this draft spec with some minor tweaks that's come up during testing since this was first implemented.
- Adding the optional
[servicename]argument as mentioned above. - Renaming
net_modesandmodeswithumodesandcmodesrespectively. - Replacing
isswithiat(issued at) so that the external service can decide how long the token should be valid for. Different services may require different lengths of time.
It has been mentioned by a few people that usage would be fairly limited since third parties would need to be configured with JWT secrets from the IRCd. To get around this I could add an optional verify claim in the token that contains a URL. This will allow any external service to make a simple HTTP call to verify a token they received from the network.
eg.
- A client receives a JWT token containing
"verify":"https://irc.foo.net/extjwtverify/%s". - The client opens a third party service that accepts the token.
- The service makes a HTTP call to the verify URL replacing
%swith the token - The verify URL replies with either a
200or403HTTP status if the token is valid.
Advantages:
- The IRCd JWT secrets will never need to be shared to third party services
- No previous agreement needs to be configured between the IRCd and third party services
- The URL is determined by the IRCd and can be configured as fits. eg. it may use a separate host instead of the IRCd directly to verify tokens.
What happens if a user forges a JWT and sets the "vfy" URL to something like, for example, "https://gib_200_always.fusionscript.info/%s"?
~~I'd suggest instead changing the "vfy" format to instead be the following format:~~
~~"https://%s/extjwtverify?t=%s"~~
This would take the "iss" field as the first input, and the token as a whole as the second field.
~~Additionally, from what I can tell, it's possible to downgrade HTTPS to HTTP using the same method?~~ (resolved by just not having the URI passed in, have it "composed")
02:19 <LordRyan> a user-supplied path is bound to have issues.
02:20 <LordRyan> i'd suggest a verify subdomain and a verify path, if you want to go that route.
02:20 <LordRyan> that way it is impossible to spoof the origin.
02:20 <LordRyan> "origin" => "issuer"
02:21 <LordRyan> so { verify_subdomain => "verify", iss => "example.com", verify_path => "/verify_extjwt?t=%s" }
02:21 <LordRyan> would go to https://verify.example.com/verify_extjwt?t=<token>
02:21 <LordRyan> and add in an enforcement that verify_path MUST be preceded by a /, enforced in any implementation that uses this system
02:21 <LordRyan> because otherwise you can just pass ".gib_200_always.example.com"
02:23 <LordRyan> and if you do it this way you can still do a "configuration-less", i.e. this gets added to meet.jit.si and they don't need to know anything about the server, they can just do the request.
As I mentioned on IRC last night, what do people think about sending the token using an Authorisation header instead of the %s thing? A quick search seems to show some precedent for doing this.
I would be fine with that. That means you only need to specify an optional "issuer subdomain" and an optional "issuer verification path" which can both be static strings.
I see some continued comments on IRC (both within IRCv3 and when discussing with some infosec friends) where the reason for this validation URL is misunderstood. I think it would be good to put some emphasis on only using the validation URL IF there is no possible shared secret. Most importantly, if there is a shared secret and it fails, I think that the validation URL MUST NOT attempted, as the shared secret is a more secure method and should take higher priority.
Additionally, some conversations that prawnsalad and I had about this:
02:00 <prawnsalad> LordRyan: if a user tampers with the token then it won’t match the hash any more
02:01 <LordRyan> and so the user makes a new hash
02:01 <LordRyan> i wasn't even thinking "tamper" i was thinking "forge" as in make it themselves
02:02 <LordRyan> if you're using vfy as a backup for not using a signature then there's no integrity in that signature and your ONLY verification is that URL in "vfy"
02:02 <LordRyan> anything including "vfy" can be forged
02:06 <prawnsalad> hm no. a jwt token is "$header.$payload.<hmac($header+"."+$payload . $secret)>"
02:06 <LordRyan> yes
02:06 <prawnsalad> to forge it you would need to know the secret that the ircd has
02:06 <LordRyan> why?
02:06 <LordRyan> this is supposed to be used if the service ALSO doesn't know the secret.
02:07 <LordRyan> you can just make it up. the service doesn't care.
02:10 <LordRyan> btw prawnsalad this issue also came up in a similar context in webauthn domain phish+MITMing so the request for the WebAuthn challenge must include the origin domain, kinda like a "client-side" salt.
02:11 <LordRyan> prawnsalad: let me phrase it this way: how does the party using the JWT for authentication know that you're not spoofing it if they don't have the secret either?
02:13 <prawnsalad> yea i get what you mean now, a rather than a vfy url it would be safer to have that pre set with the third party so there is only ever 1 verify url then
02:14 <LordRyan> or like i suggested on the GitHub, tie the issuer into the vfy url
02:14 <LordRyan> though that has the same issue, now that i think about it
Discussion on IRC brought up usage of an asymmetric key, with the public version stored in a standardized location in the issuer. This resolves both the issue of the external service not having a shared secret and the issue of the client being able to spoof the endpoint used for authentication.
What is the use case for including the channel join time in the token?
From discussion in #kiwiirc: we're tentatively proposing that server implementations that do not store channel join time information can send joined: 1, which indicates that the user is joined to the channel but the join time is unavailable for whatever reason.
What is the reason for this change exactly? It's not hard to update an existing implementation to store the join time if necessary.
I'd prefer not to add it to my implementation, given that there is no use case for it right now.
Clarification questions that came up during Dan's implementation:
- Is the signing algorithm always
HS256? - What should happen when the service name is not specified, or is
*? Should the server be configured with a default secret key for signing these tokens?
- Is the signing algorithm always
HS256?
JWT tokens allow you to use any algo your implementation can support. As long as you can create + verify your own token, you're all good.
- What should happen when the service name is not specified, or is
*? Should the server be configured with a default secret key for signing these tokens?
A default should be used, yes. This default would be the most used as clients making use of EXTJWT wouldn't know what services your IRCd has running or what name to use for them unless the network has a dedicated client for it.
@RyanSquared
Discussion on IRC brought up usage of an asymmetric key, with the public version stored in a standardized location in the issuer. This resolves both the issue of the external service not having a shared secret and the issue of the client being able to spoof the endpoint used for authentication.
Other than the service being able to cache a pub key from the issuer, what other benefit would this have? Both algo types will need a request back to the issuer either for the pub key or verification as far as I understood.
Also to note, the external service is not required to know the shared secret for a token. While services run by the same IRCd network may share the secret which saves the verification request trip, third parties can make the verification request without knowing any secrets.
Both algo types will need a request back to the issuer either for the pub key or verification as far as I understood.
For pubkey based verification you can just put a static key in a directory. For verifying the JWT itself, you'd have to implement something server side. I think a majority of hosts would rather have a static file option.
Can we add the supported services to the ISUPPORT token in any way? Something like, EXTJWT=V:1&S:nextcloud,jitsi
EXTJWT=V:1&S:nextcloud,jitsi
Any explanation for this example, please?
I have created an implementation for the EXTJWT command. I'm supporting HS, ES and RS type tokens, with method and key/password selectable per service. Users on our network are already using the HS384 method.
The specification does not mention any error handling. That's what I've devised:
- No arguments: send
ERR_NEEDMOREPARAMS. - The first parameter is neither an existing channel name nor
*: sendERR_NOSUCHNICK - The second parameter does not match any configured service name: send
:irc.example.com FAIL EXTJWT NO_SUCH_SERVICE :No such service - Token generation failed for some other reason: send
:irc.example.com FAIL EXTJWT UNKNOWN_ERROR :Failed to generate token
The vfy claim can be specified in configuration, separately for each service, and the IRC server is not handling the verification, requiring administrator to set up some separate verification service.
This draft is now shipped as optional feature with unrealircd 6.0.0.