Skip to content
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

EXTJWT command for integrating external web services #341

Open
wants to merge 10 commits into
base: master
Choose a base branch
from

Conversation

prawnsalad
Copy link
Contributor

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.


The client MAY send `EXTJWT` or `EXTJWT *` to the server to request a new JWT token. The server MUST then reply with `EXTJWT *` and a JWT token as its jwt_token parameter containing the following claims that are relevant to the client at that time:

* `exp` Number; Unix timestamp for when this token expires. Usually less than 1 minute from the token generation.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • UTC

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unix timestamp is consistent with other parts such as RPL_TOPICWHOTIME so we don't want to go changing that.


### The EXTJWT Command

Syntax: `EXTJWT [channel]`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should be: EXTJWT ( <channel> | * )

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated


* `channel` String; The channel name this token is related to.
* `joined` Boolean; True if the client that requested this token is joined to the channel.
* `time_joined` Number; The time in which the user joined the channel.
Copy link
Contributor

@progval progval Jul 22, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What should its value be if joined is False?

The best solution would probably to merge join and time_joined as a single optional attribute, so the receiver of the JWT does not need to sanity-check this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think originally I preferred to be explicit with the claim names, but being more concise here would save a lot of space.

I can change this by dropping the time_joined claim, changing joined to contain the timestamp of when joined and setting it to 0 when not joined to the channel

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change has now been applied, good shout


#### Handling long responses

In some cases the encoded token may be longer than the maximum line length allowed between the client and server. In this case, the first parameter of the response MUST be `*` to indicate that further data will follow. The final chunk of the response sent to the client MUST NOT include `*` as the first parameter.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should use the same mechanism as AUTHENTICATE, for consistency between IRCv3 specs.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the mechanism that CAP LS 302 uses, so there's consistency between that and EXTJWT at least.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed. As the CAP LS 302 way is much easier to implement, I agree this mechanism should be used.

* `channel` String; The channel name this token is related to.
* `joined` Boolean; True if the client that requested this token is joined to the channel.
* `time_joined` Number; The time in which the user joined the channel.
* `modes` []String; An array of the channel modes the client has in this channel.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe add a way to disclose modes with arguments? eg. +I to tell the mask that allows the user to be in that channel.

* `iss` String; The server name that generated this token.
* `nick` String; The nick of the client that requested this token.
* `account` String; The account name of the user that requested this token. Empty if not available.
* `net_modes` []String; An array of user modes the IRCd may want to disclose. Eg, if the user is an operator.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some generic net modes should probably added to this spec.


Where the replied token is decoded into:
~~~json
{"exp":1529917513,"iss":"irc.example.org","nick":"somenick","account":"somenick","net_modes":["o"]}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"o" looks like a channel op, you should probably rename it. A non-single-letter name would be nice too (let's not repeat the mistakes of the past).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These modes are the user modes the IRC server has set. Ie. o for operator. If an IRCd introduces named modes then the named modes will be placed in here too.


Where the replied token is decoded into:
~~~json
{"exp":1529917513,"iss":"irc.example.org","nick":"somenick","account":"","net_modes":[]}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not allow "account": null, or making that attribute optional?

@progval
Copy link
Contributor

progval commented Jul 22, 2018

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.


Response syntax: `EXTJWT <requested_target> [*] <jwt_token>`

The client MAY send `EXTJWT` or `EXTJWT *` to the server to request a new JWT token. The server MUST then reply with `EXTJWT *` and a JWT token as its jwt_token parameter containing the following claims that are relevant to the client at that time:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any reason to have two command syntax variants for a non-channel JWT (no parameters and *)?

* `iss` String; The server name that generated this token.
* `sub` String; The nick of the client that requested this token.
* `account` String; The account name of the user that requested this token. Empty if not available.
* `net_modes` []String; An array of user modes the IRCd may want to disclose. Eg, if the user is an operator.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bikeshed: net_modes should be referred to as user_modes and modes (below) should be channel_modes.

@SadieCat
Copy link
Contributor

How should a server behave if there was an issue with generating the JWT for some reason?

@SadieCat
Copy link
Contributor

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.

@prawnsalad
Copy link
Contributor Author

@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]

@DanielOaks
Copy link
Member

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.

@prawnsalad
Copy link
Contributor Author

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_modes and modes with umodes and cmodes respectively.
  • Replacing iss with iat (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.

  1. A client receives a JWT token containing "verify":"https://irc.foo.net/extjwtverify/%s".
  2. The client opens a third party service that accepts the token.
  3. The service makes a HTTP call to the verify URL replacing %s with the token
  4. The verify URL replies with either a 200 or 403 HTTP 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.

extensions/extjwt.md Outdated Show resolved Hide resolved
Co-Authored-By: Kyle Fuller <kyle@fuller.li>
@RyanSquared
Copy link
Contributor

RyanSquared commented Apr 15, 2020

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")


Where the replied token is decoded into:
~~~json
{"exp":1529917513,"iss":"irc.example.org",vfy":"https://irc.example.org/extjwt?t=%s","sub":"testnick","account":"testnick","umodes":[],"channel":"#channel","joined":1529917501,"cmodes":["o"]}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

json's a tiny bit mangled here, should be ,"vfy" instead of ,vfy"

@RyanSquared
Copy link
Contributor

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.

extensions/extjwt.md Outdated Show resolved Hide resolved
Co-Authored-By: Sadie Powell <sadie@witchery.services>
@SadieCat
Copy link
Contributor

SadieCat commented Apr 16, 2020

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.

@RyanSquared
Copy link
Contributor

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.

@RyanSquared
Copy link
Contributor

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.

@RyanSquared
Copy link
Contributor

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

@RyanSquared
Copy link
Contributor

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.

@slingamn
Copy link
Contributor

What is the use case for including the channel join time in the token?

@slingamn
Copy link
Contributor

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.

@SadieCat
Copy link
Contributor

SadieCat commented Apr 17, 2020

What is the reason for this change exactly? It's not hard to update an existing implementation to store the join time if necessary.

@slingamn
Copy link
Contributor

I'd prefer not to add it to my implementation, given that there is no use case for it right now.

@slingamn
Copy link
Contributor

Clarification questions that came up during Dan's implementation:

  1. Is the signing algorithm always HS256?
  2. 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?

@prawnsalad
Copy link
Contributor Author

  1. 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.

  1. 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.

@slingamn slingamn mentioned this pull request Apr 19, 2020
2 tasks
@prawnsalad
Copy link
Contributor Author

@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.

@RyanSquared
Copy link
Contributor

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.

@RyanSquared
Copy link
Contributor

RyanSquared commented Apr 21, 2020

Can we add the supported services to the ISUPPORT token in any way? Something like, EXTJWT=V:1&S:nextcloud,jitsi

@k4bek4be
Copy link

EXTJWT=V:1&S:nextcloud,jitsi

Any explanation for this example, please?

@k4bek4be
Copy link

k4bek4be commented Jul 2, 2020

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 *: send ERR_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.

@k4bek4be
Copy link

This draft is now shipped as optional feature with unrealircd 6.0.0.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

8 participants