1
0
Fork 0
mirror of https://github.com/warmcat/libwebsockets.git synced 2025-03-09 00:00:04 +01:00

lws_jwt_token_sanity

This commit is contained in:
Andy Green 2020-07-11 07:16:16 +01:00
parent 1db26f0c64
commit f1f34a7d4b
6 changed files with 527 additions and 3 deletions

176
READMEs/README.jwt.md Normal file
View file

@ -0,0 +1,176 @@
# JWT support in lws
lws supports the common usage scenarios of JWS (signed) JWT generation,
parsing and transferring in and out as http cookies. Care is taken to provide
helpers that implement the current security best practices for cookie handling
and JWT validation. All of the common algorithms like ES512 are supported
along with JWK generation and handling apis.
The build options needed are `-DLWS_WITH_JOSE=1` `-DLWS_WITH_GENCRYPTO=1`.
Underlying JOSE primitives are exposed as apis, some JWT specific primitives
and finally a JWT-via http cookie level creation apis each building on top of
what was underneath.
The higher level APIs are provided additionally because they have the most
opportunity for implementation pitfalls like not validating alg carefully, or
not using the latest cookie security options; the provided APIs handle that
centrally for you. If your needs vary from what the higher level apis are
doing, you can cut-and-paste out those implementations and create your own
using the public lower level apis.
## LWS JWT fields
Lws JWT uses mainly well-known fields
Field|Std|Meaning
---|---|---
iss|yes|Issuer, typically the domain like "warmcat.com"
aud|yes|Audience, typically a url path like "https://warmcat.com/sai"
iat|yes|Unix-time "Issued At"
nbf|yes|Unix-time "Not Before"
exp|yes|Unix-time "Expired"
sub|yes|Subject, eg, a username or user email
csrf|no|A random 16-char hex token generated with the JWT for use in links specific to the JWT bearer
ext|no|Application-specific JSON sub-object with whatever fields you need, eg, `"authorization": 1`
## Approach for JWT as session token
Once JWTs are produced, they are autonomous bearer tokens, if they are not kept
secret between the browser and the site, they will be accepted as evidence for
having rights to the session from anyone.
Requiring https, and various other cookie hardening techniques make it more
difficult for them to leak, but it is still necessary to strictly constrain the
token's validity time, usually to a few tens of minutes or how long it takes a
user to login and get stuff done on the site in one session.
## CSRF mitigation
Cross Site Request Forgery (CSRF) is a hacking scenario where an authorized
user with a valid token is tricked into clicking on an external link that
performs some action with side-effects on the site he has active auth on. For
example, he has a cookie that's logged into his bank, and the link posts a form
to the bank site transferring money to the attacker.
Lws JWT mitigates this possibility by putting a random secret in the generated
JWT; when the authorized user presents his JWT to generate the page, generated
links that require auth to perform their actions include the CSRF string from
that user's current JWT.
When the user clicks those links intentionally, the CSRF string in the link
matches the CSRF string in the currently valid JWT that was also provided to
the server along with the click, and all is well.
An attacker does not know the random, ephemeral JWT CSRF secret to include in
forged links, so the attacker-controlled action gets rejected at the server as
having used an invalid link.
The checking and link manipulation need to be implemented in user code / JS...
lws JWT provides the random CSRF secret in the JWT and makes it visible to the
server when the incoming JWT is processed.
## Need for client tracking of short JWT validity times
Many links or references on pages do not require CSRF strings, only those that
perform actions with side-effects like deletion or money transfer should need
protecting this way.
Due to CSRF mitigation, generated pages containing the protected links
effectively have an expiry time linked to that of the JWT, since only the bearer
of the JWT used to generate the links on the page can use them; once that
expires actually nobody can use them and the page contents, which may anyway
be showing content that only authenticated users can see must be invalidated and
re-fetched. Even if the contents are visible without authentication, additional
UI elements like delete buttons that should only be shown when authenticated
will wrongly still be shown
For that reason, the client should be informed by the server along with the
authentication status, the expiry time of it. The client should then by itself
make arrangements to refresh the page when this time is passed,
either showing an unauthenticated version of the same page if it exists, or by
redirecting to the site homepage if showing any of the contents required
authentication. The user can then log back in using his credientials typically
stored in the browser's password store and receive a new short-term JWT with a
new random csrf token along with a new page using the new csrf token in its
links.
## Considerations for long-lived connections
Once established as authorized, websocket links may be very long-lived and hold
their authorization state at the server. Although the browser monitoring the
JWT reloading the page on auth expiry should mitigate this, an attacker can
choose to just not do that and have an immortally useful websocket link.
At least for actions on the long-lived connection, it should not only confirm
the JWT authorized it but that the current time is still before the "exp" time
in the JWT, this is made available as `expiry_unix_time` in the args struct
after successful validation.
Ideally the server should close long-lived connections according to their auth
expiry time.
## JWT lower level APIs
The related apis are in `./include/libwebsockets/lws-jws.h`
### Validation of JWT
```
int
lws_jwt_signed_validate(struct lws_context *ctx, struct lws_jwk *jwk,
const char *alg_list, const char *com, size_t len,
char *temp, int tl, char *out, size_t *out_len);
```
### Composing and signing JWT
```
int
lws_jwt_sign_compact(struct lws_context *ctx, struct lws_jwk *jwk,
const char *alg, char *out, size_t *out_len, char *temp,
int tl, const char *format, ...);
```
## JWT creation and cookie get / set API
Both the validation and signing apis use the same struct to contain their
aguments.
```
struct lws_jwt_sign_set_cookie {
struct lws_jwk *jwk;
/**< entry: required signing key */
const char *alg;
/**< entry: required signing alg, eg, "ES512" */
const char *iss;
/**< entry: issuer name to use */
const char *aud;
/**< entry: audience */
const char *cookie_name;
/**< entry: the name of the cookie */
char sub[33];
/**< sign-entry, validate-exit: subject */
const char *extra_json;
/**< sign-entry, validate-exit:
* optional "ext" JSON object contents for the JWT */
size_t extra_json_len;
/**< validate-exit:
* length of optional "ext" JSON object contents for the JWT */
const char *csrf_in;
/**< validate-entry:
* NULL, or an external CSRF token to check against what is in the JWT */
unsigned long expiry_unix_time;
/**< sign-entry: seconds the JWT and cookie may live,
* validate-exit: expiry unix time */
};
int
lws_jwt_sign_token_set_http_cookie(struct lws *wsi,
const struct lws_jwt_sign_set_cookie *i,
uint8_t **p, uint8_t *end);
int
lws_jwt_get_http_cookie_validate_jwt(struct lws *wsi,
struct lws_jwt_sign_set_cookie *i,
char *out, size_t *out_len);
```

View file

@ -454,4 +454,106 @@ LWS_VISIBLE LWS_EXTERN int
lws_jwt_sign_compact(struct lws_context *ctx, struct lws_jwk *jwk,
const char *alg, char *out, size_t *out_len, char *temp,
int tl, const char *format, ...) LWS_FORMAT(8);
/**
* lws_jwt_token_sanity() - check a validated jwt payload for sanity
*
* \param in: the JWT payload
* \param in_len: the length of the JWT payload
* \param iss: the expected issuer of the token
* \param aud: the expected audience of the token
* \param csrf_in: NULL, or the csrf token that came in on a URL
* \param sub: a buffer to hold the subject name in the JWT (eg, account name)
* \param sub_len: the max length of the sub buffer
* \param secs_left: set to the number of seconds of valid auth left if valid
*
* This performs some generic sanity tests on validated JWT payload...
*
* - the issuer is as expected
* - the audience is us
* - current time is OK for nbf ("not before") in the token
* - current time is OK for exp ("expiry") in the token
* - if csrf_in is not NULL, that the JWK has a csrf and it matches it
* - if sub is not NULL, that the JWK provides a subject (and copies it to sub)
*
* If the tests pass, *secs_left is set to the number of remaining seconds the
* auth is valid.
*
* Returns 0 if no inconsistency, else nonzero.
*/
LWS_VISIBLE LWS_EXTERN int
lws_jwt_token_sanity(const char *in, size_t in_len,
const char *iss, const char *aud, const char *csrf_in,
char *sub, size_t sub_len, unsigned long *exp_unix_time);
#if defined(LWS_ROLE_H1) || defined(LWS_ROLE_H2)
struct lws_jwt_sign_set_cookie {
struct lws_jwk *jwk;
/**< entry: required signing key */
const char *alg;
/**< entry: required signing alg, eg, "ES512" */
const char *iss;
/**< entry: issuer name to use */
const char *aud;
/**< entry: audience */
const char *cookie_name;
/**< entry: the name of the cookie */
char sub[33];
/**< sign-entry, validate-exit: subject */
const char *extra_json;
/**< sign-entry, validate-exit:
* optional "ext" JSON object contents for the JWT */
size_t extra_json_len;
/**< validate-exit:
* length of optional "ext" JSON object contents for the JWT */
const char *csrf_in;
/**< validate-entry:
* NULL, or an external CSRF token to check against what is in the JWT */
unsigned long expiry_unix_time;
/**< sign-entry: seconds the JWT and cookie may live,
* validate-exit: expiry unix time */
};
/**
* lws_jwt_sign_token_set_cookie() - creates sets a JWT in a wsi cookie
*
* \param wsi: the wsi to create the cookie header on
* \param i: structure describing what should be in the JWT
* \param p: wsi headers area
* \param end: end of wsi headers area
*
* Creates a JWT specified \p i, and attaches it to the outgoing headers on
* wsi. Returns 0 if successful.
*
* Best-practice security restrictions are applied to the cookie set action,
* including forcing httponly, and __Host- prefix. As required by __Host-, the
* cookie Path is set to /. __Host- is applied by the function, the cookie_name
* should just be "xyz" for "__Host-xyz".
*
* \p extra_json should just be the bare JSON, a { } is provided around it by
* the function if it's non-NULL. For example, "\"authorization\": 1".
*
* It's recommended the secs parameter is kept as small as consistent with one
* user session on the site if possible, eg, 10 minutes or 20 minutes. At the
* server, it can determine how much time is left in the auth and inform the
* client; if the JWT validity expires, the page should reload so the UI always
* reflects what's possible to do with the authorization state correctly. If
* the JWT expires, the user can log back in using credentials usually stored in
* the browser and auto-filled-in, so this is not very inconvenient.
*
* This is a helper on top of the other JOSE and JWT apis that somewhat crosses
* over between JWT and HTTP, since it knows about cookies. So it is only built
* if both LWS_WITH_JOSE and one of the http-related roles enabled.
*/
LWS_VISIBLE LWS_EXTERN int
lws_jwt_sign_token_set_http_cookie(struct lws *wsi,
const struct lws_jwt_sign_set_cookie *i,
uint8_t **p, uint8_t *end);
LWS_VISIBLE LWS_EXTERN int
lws_jwt_get_http_cookie_validate_jwt(struct lws *wsi,
struct lws_jwt_sign_set_cookie *i,
char *out, size_t *out_len);
#endif
///@}

View file

@ -261,6 +261,26 @@ lws_json_simple_strcmp(const char *buf, size_t len, const char *name, const char
LWS_VISIBLE LWS_EXTERN int
lws_hex_to_byte_array(const char *h, uint8_t *dest, int max);
/**
* lws_hex_random(): generate len - 1 or - 2 characters of random ascii hex
*
* \param context: the lws_context used to get the random
* \param dest: destination for hex ascii chars
* \param len: the number of bytes the buffer dest points to can hold
*
* This creates random ascii-hex strings up to a given length, with a
* terminating NUL. Hex characters are produced in pairs, if the length of
* the destination buffer is even, after accounting for the NUL there will be
* an unused byte at the end after the NUL. So lengths should be odd to get
* length - 1 characters exactly followed by the NUL.
*
* There will not be any characters produced that are not 0-9, a-f, so it's
* safe to go straight into, eg, JSON.
*/
LWS_VISIBLE LWS_EXTERN int
lws_hex_random(struct lws_context *context, char *dest, size_t len);
/*
* lws_timingsafe_bcmp(): constant time memcmp
*

View file

@ -153,6 +153,27 @@ lws_hex_to_byte_array(const char *h, uint8_t *dest, int max)
return lws_ptr_diff(dest, odest);
}
static char *hexch = "0123456789abcdef";
int
lws_hex_random(struct lws_context *context, char *dest, size_t len)
{
size_t n = (len - 1) / 2;
uint8_t b, *r = (uint8_t *)dest + len - n;
if (lws_get_random(context, r, n) != n)
return 1;
while (n--) {
b = *r++;
*dest++ = hexch[b >> 4];
*dest++ = hexch[b & 0xf];
}
*dest = '\0';
return 0;
}
#if !defined(LWS_PLAT_OPTEE)

View file

@ -957,9 +957,9 @@ lws_jwt_signed_validate(struct lws_context *ctx, struct lws_jwk *jwk,
{
struct lws_tokenize ts;
struct lws_jose jose;
int otl = tl, r = 1;
struct lws_jws jws;
size_t n, r = 1;
int otl = tl;
size_t n;
memset(&jws, 0, sizeof(jws));
lws_jose_init(&jose);
@ -969,7 +969,8 @@ lws_jwt_signed_validate(struct lws_context *ctx, struct lws_jwk *jwk,
* blocks
*/
n = lws_jws_compact_decode(com, len, &jws.map, &jws.map_b64, temp, &tl);
n = lws_jws_compact_decode(com, (int)len, &jws.map, &jws.map_b64,
temp, &tl);
if (n != 3) {
lwsl_err("%s: concat_map failed: %d\n", __func__, (int)n);
goto bail;
@ -1146,3 +1147,90 @@ bail:
return r;
}
int
lws_jwt_token_sanity(const char *in, size_t in_len,
const char *iss, const char *aud,
const char *csrf_in,
char *sub, size_t sub_len, unsigned long *expiry_unix_time)
{
unsigned long now = lws_now_secs(), exp;
const char *cp;
size_t len;
/*
* It has our issuer?
*/
if (lws_json_simple_strcmp(in, in_len, "\"iss\":", iss)) {
lwsl_notice("%s: iss mismatch\n", __func__);
return 1;
}
/*
* ... it is indended for us to consume? (this is set
* to the public base url for this sai instance)
*/
if (lws_json_simple_strcmp(in, in_len, "\"aud\":", aud)) {
lwsl_notice("%s: aud mismatch\n", __func__);
return 1;
}
/*
* ...it's not too early for it?
*/
cp = lws_json_simple_find(in, in_len, "\"nbf\":", &len);
if (!cp || (unsigned long)atol(cp) > now) {
lwsl_notice("%s: nbf fail\n", __func__);
return 1;
}
/*
* ... and not too late for it?
*/
cp = lws_json_simple_find(in, in_len, "\"exp\":", &len);
exp = (unsigned long)atol(cp);
if (!cp || (unsigned long)atol(cp) < now) {
lwsl_notice("%s: exp fail %lu vs %lu\n", __func__,
cp ? (unsigned long)atol(cp) : 0, now);
return 1;
}
/*
* Caller cares about subject? Then we must have it, and it can't be
* empty.
*/
if (sub) {
cp = lws_json_simple_find(in, in_len, "\"sub\":", &len);
if (!cp || !len) {
lwsl_notice("%s: missing subject\n", __func__);
return 1;
}
lws_strnncpy(sub, cp, len, sub_len);
}
/*
* If caller has been told a Cross Site Request Forgery (CSRF) nonce,
* require this JWT to express the same CSRF... this makes generated
* links for dangerous privileged auth'd actions expire with the JWT
* that was accessing the site when the links were generated. And it
* leaves an attacker not knowing what links to synthesize unless he
* can read the token or pages generated with it.
*
* Using this is very good for security, but it implies you must refresh
* generated pages still when the auth token is expiring (and the user
* must log in again).
*/
if (csrf_in &&
lws_json_simple_strcmp(in, in_len, "\"csrf\":", csrf_in)) {
lwsl_notice("%s: csrf mismatch\n", __func__);
return 1;
}
if (expiry_unix_time)
*expiry_unix_time = exp;
return 0;
}

View file

@ -1468,6 +1468,7 @@ lws_http_cookie_get(struct lws *wsi, const char *name, char *buf,
char *p, *bo = buf;
n = lws_hdr_total_length(wsi, WSI_TOKEN_HTTP_COOKIE);
lwsl_notice("%s: cookie hdr len %d\n", __func__, n);
if (n < bl + 1)
return 1;
@ -1475,6 +1476,8 @@ lws_http_cookie_get(struct lws *wsi, const char *name, char *buf,
if (!p)
return 1;
lwsl_hexdump_notice(p, n);
p += bl;
n -= bl;
while (n-- > bl) {
@ -1497,3 +1500,117 @@ lws_http_cookie_get(struct lws *wsi, const char *name, char *buf,
return 1;
}
#if defined(LWS_WITH_JOSE)
#define MAX_JWT_SIZE 1024
int
lws_jwt_get_http_cookie_validate_jwt(struct lws *wsi,
struct lws_jwt_sign_set_cookie *i,
char *out, size_t *out_len)
{
char temp[MAX_JWT_SIZE * 2];
size_t cml = *out_len;
const char *cp;
/* first use out to hold the encoded JWT */
if (lws_http_cookie_get(wsi, i->cookie_name, out, out_len)) {
lwsl_notice("%s: cookie %s not provided\n", __func__,
i->cookie_name);
return 1;
}
/* decode the JWT into temp */
if (lws_jwt_signed_validate(wsi->context, i->jwk, i->alg, out,
*out_len, temp, sizeof(temp), out, &cml)) {
lwsl_notice("%s: jwt validation failed\n", __func__);
return 1;
}
/*
* Copy out the decoded JWT payload into out, overwriting the
* original encoded JWT taken from the cookie (that has long ago been
* translated into allocated buffers in the JOSE object)
*/
if (lws_jwt_token_sanity(out, cml, i->iss, i->aud, i->csrf_in,
i->sub, sizeof(i->sub),
&i->expiry_unix_time)) {
lwsl_notice("%s: jwt sanity failed\n", __func__);
return 1;
}
/*
* If he's interested in his private JSON part, point him to that in
* the args struct (it's pointing to the data in out
*/
cp = lws_json_simple_find(out, cml, "\"ext\":", &i->extra_json_len);
if (cp)
i->extra_json = cp;
if (cp)
lwsl_hexdump_notice(cp, i->extra_json_len);
else
lwsl_notice("%s: no ext JWT payload\n", __func__);
return 0;
}
int
lws_jwt_sign_token_set_http_cookie(struct lws *wsi,
const struct lws_jwt_sign_set_cookie *i,
uint8_t **p, uint8_t *end)
{
char plain[MAX_JWT_SIZE + 1], temp[MAX_JWT_SIZE * 2], csrf[17];
size_t pl = sizeof(plain);
unsigned long long ull;
int n;
/*
* Create a 16-char random csrf token with the same lifetime as the JWT
*/
lws_hex_random(wsi->context, csrf, sizeof(csrf));
ull = lws_now_secs();
if (lws_jwt_sign_compact(wsi->context, i->jwk, i->alg, plain, &pl,
temp, sizeof(temp),
"{\"iss\":\"%s\",\"aud\":\"%s\","
"\"iat\":%llu,\"nbf\":%llu,\"exp\":%llu,"
"\"csrf\":\"%s\",\"sub\":\"%s\"%s%s%s}",
i->iss, i->aud, ull, ull - 60,
ull + i->expiry_unix_time,
csrf, i->sub,
i->extra_json ? ",\"ext\":{" : "",
i->extra_json ? i->extra_json : "",
i->extra_json ? "}" : "")) {
lwsl_err("%s: failed to create JWT\n", __func__);
return 1;
}
/*
* There's no point the browser holding on to a JWT beyond the JWT's
* expiry time, so set it to be the same.
*/
n = lws_snprintf(temp, sizeof(temp), "__Host-%s=%s;"
"HttpOnly;"
"Secure;"
"SameSite=strict;"
"Path=/;"
"Max-Age=%lu",
i->cookie_name, plain, i->expiry_unix_time);
if (lws_add_http_header_by_token(wsi, WSI_TOKEN_HTTP_SET_COOKIE,
(uint8_t *)temp, n, p, end)) {
lwsl_err("%s: failed to add JWT cookie header\n", __func__);
return 1;
}
return 0;
}
#endif