Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Using JSON Web Tokens & Varnish to cache conten...

Avatar for Thijs Feryn Thijs Feryn
September 26, 2017

Using JSON Web Tokens & Varnish to cache content for logged-in users - DrupalCon Vienna 2017

Avatar for Thijs Feryn

Thijs Feryn

September 26, 2017
Tweet

More Decks by Thijs Feryn

Other Decks in Technology

Transcript

  1. By Thijs Feryn Using JSON Web Tokens & Varnish to

    cache content for logged-in users
  2. MariaDB [drupal]> select * from main_sessions; +-----+---------------------------------------------+------+-----------------+------------+-------+---------+ | uid |

    sid | ssid | hostname | timestamp | cache | session | +-----+---------------------------------------------+------+-----------------+------------+-------+---------+ | 2 | 26lbuJAm37NrudWYaJ8TcW1u7J7WFs7TjAWOhjv-krI | | 141.135.242.208 | 1505304003 | 0 | | | 2 | O12RKyXsM8_f8vC3xJPE4WtGyGH4eirXhT_AcN5MTGI | | 141.135.242.208 | 1505307466 | 0 | | +-----+---------------------------------------------+------+-----------------+------------+-------+---------+ SESS8776ed48f0b08839e2cd7485bc08b4d0= O12RKyXsM8_f8vC3xJPE4WtGyGH4eirXhT_AcN5MTGI Client-side Server-side
  3. eyJzdWIiOiJhZG1pbiIsIm V4cCI6MTQ5NTUyODc1Niwi bG9naW4iOnRydWV9 { "alg": "HS256", "typ": "JWT" } {

    "sub": "admin", "exp": 1495528756, "login": true } HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret ) eyJhbGciOiJIUzI1NiIsI nR5cCI6IkpXVCJ9 u4Idy- SYnrFdnH1h9_sNc4OasOR BJcrh2fPo1EOTre8
  4. Andreas De Rijcke built the custom Drupal module to handle

    JWT https://twitter.com/andreasderijcke https://gitlab.com/andreasderijcke/jwt-cookie
  5. { "iss": "drupaljwt.feryn.eu", "jti": "mIEBDk2zFP3nqrlDuEScTsc4fVocTmhrkp36fQwJQdA", "iat": 1505313422, "exp": 1507313422, "uid":

    "2", "roles": "authenticated user", "data": "{\"html\":{\"user-name\":\"test1\"},\"attr\":{\"user- url\":{\"href\":\"\\/user\\/2\"}}}" } Issuer
  6. { "iss": "drupaljwt.feryn.eu", "jti": "mIEBDk2zFP3nqrlDuEScTsc4fVocTmhrkp36fQwJQdA", "iat": 1505313422, "exp": 1507313422, "uid":

    "2", "roles": "authenticated user", "data": "{\"html\":{\"user-name\":\"test1\"},\"attr\":{\"user- url\":{\"href\":\"\\/user\\/2\"}}}" } Drupal session ID
  7. { "iss": "drupaljwt.feryn.eu", "jti": "mIEBDk2zFP3nqrlDuEScTsc4fVocTmhrkp36fQwJQdA", "iat": 1505313422, "exp": 1507313422, "uid":

    "2", "roles": "authenticated user", "data": "{\"html\":{\"user-name\":\"test1\"},\"attr\":{\"user- url\":{\"href\":\"\\/user\\/2\"}}}" } Issued at
  8. { "iss": "drupaljwt.feryn.eu", "jti": "mIEBDk2zFP3nqrlDuEScTsc4fVocTmhrkp36fQwJQdA", "iat": 1505313422, "exp": 1507313422, "uid":

    "2", "roles": "authenticated user", "data": "{\"html\":{\"user-name\":\"test1\"},\"attr\":{\"user- url\":{\"href\":\"\\/user\\/2\"}}}" } Expires at
  9. { "iss": "drupaljwt.feryn.eu", "jti": "mIEBDk2zFP3nqrlDuEScTsc4fVocTmhrkp36fQwJQdA", "iat": 1505313422, "exp": 1507313422, "uid":

    "2", "roles": "authenticated user", "data": "{\"html\":{\"user-name\":\"test1\"},\"attr\":{\"user- url\":{\"href\":\"\\/user\\/2\"}}}" } User ID
  10. { "iss": "drupaljwt.feryn.eu", "jti": "mIEBDk2zFP3nqrlDuEScTsc4fVocTmhrkp36fQwJQdA", "iat": 1505313422, "exp": 1507313422, "uid":

    "2", "roles": "authenticated user", "data": "{\"html\":{\"user-name\":\"test1\"},\"attr\":{\"user- url\":{\"href\":\"\\/user\\/2\"}}}" } User roles
  11. { "iss": "drupaljwt.feryn.eu", "jti": "mIEBDk2zFP3nqrlDuEScTsc4fVocTmhrkp36fQwJQdA", "iat": 1505313422, "exp": 1507313422, "uid":

    "2", "roles": "authenticated user", "data": "{\"html\":{\"user-name\":\"test1\"},\"attr\":{\"user- url\":{\"href\":\"\\/user\\/2\"}}}" } HTML content to be renderd by javascript
  12. vcl 4.0;
 
 import std;
 import var;
 import cookie;
 import

    digest;
 
 acl internal {
 "192.168.20.0"/24;
 }
 
 backend default {
 .host = "127.0.0.1";
 .port = "8080";
 }
  13. sub vcl_recv {
 var.set(“key",std.fileread("/home/drupal/jwt.key"));
 
 if ((req.method != "GET" &&

    req.method != "HEAD") || req.http.Authorization) {
 return (pass);
 }
 
 if (req.url ~ "^/(cron|install|update)\.php$" && !client.ip ~ internal) {
 return (synth(404, "Page not found."));
 }
 
 if (req.url ~ "(?i)\.(png|gif|jpeg|jpg|ico|swf|css|js|html|htm|woff)(\?[wd=.-]+)?$") {
 unset req.http.Cookie;
 return(hash);
 }
 
 if (req.http.Cookie) {
 cookie.parse(req.http.cookie);
 
 if(req.http.cookie ~ ".*(SESS[a-z0-9]+)=.*") {
 var.set("sessionCookie",regsub(req.http.cookie,".*(SESS[a-z0-9]+)=.*","\1"));
 }
 
 cookie.filter_except("PHPSESSID,NO_CACHE,ci_session,CI_SESSION,authtoken,jwt_cookie,"+var.get("sessionCookie"));
 
 set req.http.cookie = cookie.get_string();
 
 if (req.http.Cookie ~ "^\s*$") {
 unset req.http.Cookie;
 }
 }
 call jwt; if(var.get("roles") ~ "administrator") {
 std.log("Administrators bypass the cache");
 return(pass);
 }
 
 if(req.url == "/login" && req.http.X-login =="true") {
 return(synth(302,"/user"));
 }

  14. 
 
 if(var.get("roles") ~ "administrator") {
 std.log("Administrators bypass the cache");


    return(pass);
 }
 
 if(req.url == "/login" && req.http.X-login == "true") {
 return(synth(302,"/user"));
 }
 
 if(req.url == "/user" && req.http.X-login != "true") {
 return (hash);
 }
 
 if (req.url == "/user/password" || req.url == "/user/register") {
 return(hash);
 }
 
 if(req.url ~ "^/user/login") {
 return(hash);
 }
 
 if (req.url == "/status.php" ||
 req.url == "/ooyala/ping" ||
 req.url == "/user" ||
 req.url ~ "^/admin/.*$" ||
 req.url ~ "^/info/.*$" ||
 req.url ~ "^/flag/.*$" ||
 req.url ~ "^.*/ajax/.*$" ||
 req.url ~ "^.*/ahah/.*$" ||
 req.url ~ "^/rest/.*$" ||
 req.url ~ "^/users?/.*$"
 ) {
 return (pass);
 }
 
 return(hash);
 }
  15. sub vcl_hash {
 if (req.http.X-Forwarded-Proto) {
 hash_data(req.http.X-Forwarded-Proto);
 }
 }
 


    sub vcl_backend_response {
 set beresp.http.x-url = bereq.url;
 set beresp.http.x-host = bereq.http.host;
 
 if (bereq.url ~ "(?i)\.(png|gif|jpeg|jpg|ico|swf|css|js|html|htm)(\?[wd=.-]+)?$") {
 unset beresp.http.set-cookie;
 }
 }
 sub vcl_synth {
 if (resp.status == 301 || resp.status == 302) {
 set resp.http.location = resp.reason;
 set resp.reason = "Moved";
 return (deliver);
 }
 }
  16. sub jwt {
 if(cookie.isset("jwt_cookie")) {
 var.set("token", cookie.get("jwt_cookie"));
 var.set("header", regsub(var.get("token"),"([^\.]+)\.[^\.]+\.[^\.]+","\1"));
 var.set("type",

    regsub(digest.base64url_decode(var.get("header")),{"^.*?"typ"\s*:\s*"(\w+)".*?$"},"\1"));
 var.set("algorithm", regsub(digest.base64url_decode(var.get("header")),{"^.*?"alg"\s*:\s*"(\w+)".*? $"},"\1"));
 
 if(var.get("type") == "JWT" && var.get("algorithm") == "HS256") {
 var.set("rawPayload",regsub(var.get("token"),"[^\.]+\.([^\.]+)\.[^\.]+$","\1"));
 var.set("signature",regsub(var.get("token"),"^[^\.]+\.[^\.]+\.([^\.]+)$","\1"));
 var.set("currentSignature",digest.base64url_nopad_hex(digest.hmac_sha256(var.get("key"),var.get("header") + "." + var.get("rawPayload"))));
 var.set("payload", digest.base64url_decode(var.get("rawPayload")));
 var.set("exp",regsub(var.get("payload"),{"^.*?"exp"\s*:\s*([0-9]+).*?$"},"\1"));
 var.set("jti",regsub(var.get("payload"),{"^.*?"jti"\s*:\s*"([a-z0-9A-Z_\-]+)".*?$"},"\1"));
 var.set("userId",regsub(var.get("payload"),{"^.*?"uid"\s*:\s*"([0-9]+)".*?$"},"\1"));
 var.set("roles",regsub(var.get("payload"),{"^.*?"roles"\s*:\s*"([a-z0-9A-Z_\-, ]+)".*?$"},"\1"));
 
 if(std.time(var.get("exp"),now) >= now && cookie.get(var.get("sessionCookie")) == var.get(“jti”) && var.get("signature") == var.get("currentSignature")) {
 set req.http.X-login="true";
 }
 } 
 }
 
 if(req.url ~ "/node/2" && req.url !~ "^/user/login") {
 if(req.http.X-login != "true") {
 return(synth(302,"/user/login?destination=" + req.url));
 }
 }
 }
  17. sub jwt {
 if(cookie.isset("jwt_cookie")) {
 var.set("token", cookie.get("jwt_cookie"));
 var.set("header", regsub(var.get("token"),"([^\.]+)\.[^\.]+\.[^\.]+","\1"));
 var.set("type",

    regsub(digest.base64url_decode(var.get("header")),{"^.*?"typ"\s*:\s*"(\w+)".*?$"},"\1"));
 var.set("algorithm", regsub(digest.base64url_decode(var.get("header")),{"^.*?"alg"\s*:\s*"(\w+)".*? $"},"\1"));
 
 if(var.get("type") == "JWT" && var.get("algorithm") == "HS256") {
 var.set("rawPayload",regsub(var.get("token"),"[^\.]+\.([^\.]+)\.[^\.]+$","\1"));
 var.set("signature",regsub(var.get("token"),"^[^\.]+\.[^\.]+\.([^\.]+)$","\1"));
 var.set("currentSignature",digest.base64url_nopad_hex(digest.hmac_sha256(var.get("key"),var.get("header") + "." + var.get("rawPayload"))));
 var.set("payload", digest.base64url_decode(var.get("rawPayload")));
 var.set("exp",regsub(var.get("payload"),{"^.*?"exp"\s*:\s*([0-9]+).*?$"},"\1"));
 var.set("jti",regsub(var.get("payload"),{"^.*?"jti"\s*:\s*"([a-z0-9A-Z_\-]+)".*?$"},"\1"));
 var.set("userId",regsub(var.get("payload"),{"^.*?"uid"\s*:\s*"([0-9]+)".*?$"},"\1"));
 var.set("roles",regsub(var.get("payload"),{"^.*?"roles"\s*:\s*"([a-z0-9A-Z_\-, ]+)".*?$"},"\1"));
 
 if(std.time(var.get("exp"),now) >= now && cookie.get(var.get("sessionCookie")) == var.get(“jti”) && var.get("signature") == var.get("currentSignature")) {
 set req.http.X-login="true";
 }
 } 
 }
 
 if(req.url ~ "/node/2" && req.url !~ "^/user/login") {
 if(req.http.X-login != "true") {
 return(synth(302,"/user/login?destination=" + req.url));
 }
 }
 }
  18. sub jwt {
 if(cookie.isset("jwt_cookie")) {
 var.set("token", cookie.get("jwt_cookie"));
 var.set("header", regsub(var.get("token"),"([^\.]+)\.[^\.]+\.[^\.]+","\1"));
 var.set("type",

    regsub(digest.base64url_decode(var.get("header")),{"^.*?"typ"\s*:\s*"(\w+)".*?$"},"\1"));
 var.set("algorithm", regsub(digest.base64url_decode(var.get("header")),{"^.*?"alg"\s*:\s*"(\w+)".*? $"},"\1"));
 
 if(var.get("type") == "JWT" && var.get("algorithm") == "HS256") {
 var.set("rawPayload",regsub(var.get("token"),"[^\.]+\.([^\.]+)\.[^\.]+$","\1"));
 var.set("signature",regsub(var.get("token"),"^[^\.]+\.[^\.]+\.([^\.]+)$","\1"));
 var.set("currentSignature",digest.base64url_nopad_hex(digest.hmac_sha256(var.get("key"),var.get("header") + "." + var.get("rawPayload"))));
 var.set("payload", digest.base64url_decode(var.get("rawPayload")));
 var.set("exp",regsub(var.get("payload"),{"^.*?"exp"\s*:\s*([0-9]+).*?$"},"\1"));
 var.set("jti",regsub(var.get("payload"),{"^.*?"jti"\s*:\s*"([a-z0-9A-Z_\-]+)".*?$"},"\1"));
 var.set("userId",regsub(var.get("payload"),{"^.*?"uid"\s*:\s*"([0-9]+)".*?$"},"\1"));
 var.set("roles",regsub(var.get("payload"),{"^.*?"roles"\s*:\s*"([a-z0-9A-Z_\-, ]+)".*?$"},"\1"));
 
 if(std.time(var.get("exp"),now) >= now && cookie.get(var.get("sessionCookie")) == var.get(“jti”) && var.get("signature") == var.get("currentSignature")) {
 set req.http.X-login="true";
 }
 } 
 } 
 if(req.url ~ "/node/2" && req.url !~ "^/user/login") {
 if(req.http.X-login != "true") {
 return(synth(302,"/user/login?destination=" + req.url));
 }
 }
 }
  19. sub jwt {
 if(cookie.isset("jwt_cookie")) {
 var.set("token", cookie.get("jwt_cookie"));
 var.set("header", regsub(var.get("token"),"([^\.]+)\.[^\.]+\.[^\.]+","\1"));
 var.set("type",

    regsub(digest.base64url_decode(var.get("header")),{"^.*?"typ"\s*:\s*"(\w+)".*?$"},"\1"));
 var.set("algorithm", regsub(digest.base64url_decode(var.get("header")),{"^.*?"alg"\s*:\s*"(\w+)".*? $"},"\1"));
 
 if(var.get("type") == "JWT" && var.get("algorithm") == "HS256") {
 var.set("rawPayload",regsub(var.get("token"),"[^\.]+\.([^\.]+)\.[^\.]+$","\1"));
 var.set("signature",regsub(var.get("token"),"^[^\.]+\.[^\.]+\.([^\.]+)$","\1"));
 var.set("currentSignature",digest.base64url_nopad_hex(digest.hmac_sha256(var.get("key"),var.get("header") + "." + var.get("rawPayload"))));
 var.set("payload", digest.base64url_decode(var.get("rawPayload")));
 var.set("exp",regsub(var.get("payload"),{"^.*?"exp"\s*:\s*([0-9]+).*?$"},"\1"));
 var.set("jti",regsub(var.get("payload"),{"^.*?"jti"\s*:\s*"([a-z0-9A-Z_\-]+)".*?$"},"\1"));
 var.set("userId",regsub(var.get("payload"),{"^.*?"uid"\s*:\s*"([0-9]+)".*?$"},"\1"));
 var.set("roles",regsub(var.get("payload"),{"^.*?"roles"\s*:\s*"([a-z0-9A-Z_\-, ]+)".*?$"},"\1"));
 
 if(std.time(var.get("exp"),now) >= now && cookie.get(var.get("sessionCookie")) == var.get(“jti”) && var.get("signature") == var.get("currentSignature")) {
 set req.http.X-login="true";
 }
 } 
 }
 
 if(req.url ~ "/node/2" && req.url !~ "^/user/login") {
 if(req.http.X-login != "true") {
 return(synth(302,"/user/login?destination=" + req.url));
 }
 }
 }
  20. sub jwt {
 if(cookie.isset("jwt_cookie")) {
 var.set("token", cookie.get("jwt_cookie"));
 var.set("header", regsub(var.get("token"),"([^\.]+)\.[^\.]+\.[^\.]+","\1"));
 var.set("type",

    regsub(digest.base64url_decode(var.get("header")),{"^.*?"typ"\s*:\s*"(\w+)".*?$"},"\1"));
 var.set("algorithm", regsub(digest.base64url_decode(var.get("header")),{"^.*?"alg"\s*:\s*"(\w+)".*? $"},"\1"));
 
 if(var.get("type") == "JWT" && var.get("algorithm") == "HS256") {
 var.set("rawPayload",regsub(var.get("token"),"[^\.]+\.([^\.]+)\.[^\.]+$","\1"));
 var.set("signature",regsub(var.get("token"),"^[^\.]+\.[^\.]+\.([^\.]+)$","\1"));
 var.set("currentSignature",digest.base64url_nopad_hex(digest.hmac_sha256(var.get("key"),var.get("header") + "." + var.get("rawPayload"))));
 var.set("payload", digest.base64url_decode(var.get("rawPayload")));
 var.set("exp",regsub(var.get("payload"),{"^.*?"exp"\s*:\s*([0-9]+).*?$"},"\1"));
 var.set("jti",regsub(var.get("payload"),{"^.*?"jti"\s*:\s*"([a-z0-9A-Z_\-]+)".*?$"},"\1"));
 var.set("userId",regsub(var.get("payload"),{"^.*?"uid"\s*:\s*"([0-9]+)".*?$"},"\1"));
 var.set("roles",regsub(var.get("payload"),{"^.*?"roles"\s*:\s*"([a-z0-9A-Z_\-, ]+)".*?$"},"\1"));
 
 if(std.time(var.get("exp"),now) >= now && cookie.get(var.get("sessionCookie")) == var.get(“jti”) && var.get("signature") == var.get("currentSignature")) {
 set req.http.X-login="true";
 }
 } 
 }
 
 if(req.url ~ "/node/2" && req.url !~ "^/user/login") {
 if(req.http.X-login != "true") {
 return(synth(302,"/user/login?destination=" + req.url));
 }
 }
 }
  21. sub jwt {
 if(cookie.isset("jwt_cookie")) {
 var.set("token", cookie.get("jwt_cookie"));
 var.set("header", regsub(var.get("token"),"([^\.]+)\.[^\.]+\.[^\.]+","\1"));
 var.set("type",

    regsub(digest.base64url_decode(var.get("header")),{"^.*?"typ"\s*:\s*"(\w+)".*?$"},"\1"));
 var.set("algorithm", regsub(digest.base64url_decode(var.get("header")),{"^.*?"alg"\s*:\s*"(\w+)".*? $"},"\1"));
 
 if(var.get("type") == "JWT" && var.get("algorithm") == "HS256") {
 var.set("rawPayload",regsub(var.get("token"),"[^\.]+\.([^\.]+)\.[^\.]+$","\1"));
 var.set("signature",regsub(var.get("token"),"^[^\.]+\.[^\.]+\.([^\.]+)$","\1"));
 var.set("currentSignature",digest.base64url_nopad_hex(digest.hmac_sha256(var.get("key"),var.get("header") + "." + var.get("rawPayload"))));
 var.set("payload", digest.base64url_decode(var.get("rawPayload")));
 var.set("exp",regsub(var.get("payload"),{"^.*?"exp"\s*:\s*([0-9]+).*?$"},"\1"));
 var.set("jti",regsub(var.get("payload"),{"^.*?"jti"\s*:\s*"([a-z0-9A-Z_\-]+)".*?$"},"\1"));
 var.set("userId",regsub(var.get("payload"),{"^.*?"uid"\s*:\s*"([0-9]+)".*?$"},"\1"));
 var.set("roles",regsub(var.get("payload"),{"^.*?"roles"\s*:\s*"([a-z0-9A-Z_\-, ]+)”.*?$"},"\1")); 
 if(std.time(var.get("exp"),now) >= now && cookie.get(var.get("sessionCookie")) == var.get(“jti”) && var.get("signature") == var.get("currentSignature")) {
 set req.http.X-login="true";
 }
 } 
 }
 
 if(req.url ~ "/node/2" && req.url !~ "^/user/login") {
 if(req.http.X-login != "true") {
 return(synth(302,"/user/login?destination=" + req.url));
 }
 }
 }
  22. Vary: X-login Vary header sent by Drupal X-login header set

    by Varnish Creates cache variations in Varnish
  23. Modules ✓Varnish ✓Key (store JWT key) ✓HTTP Response Headers +

    UI (set cache-control) ✓JWT cookie (set JWT token) ✓JWT cookie Example (replace content & display meta info)
  24. function _jwt_cookie_cookie_create($user, $data, $cached = FALSE) { $cookie_domain = _jwt_cookie_get_domain();

    $builder = new Builder(); $signer = new Sha256(); // Build the token. $expiration = time() + ini_get('session.cookie_lifetime'); $builder ->setIssuer($cookie_domain) ->setId(session_id(), TRUE) ->setIssuedAt(time()) ->setExpiration($expiration) ->set('uid', $user->uid) ->set('roles', implode(',', $user->roles)); // When we use data from cache, we don't need to clean the data again. if (!$cached) { // Get and clean the data. Discard the rest. $attr = isset($data['attr']) ? $data['attr'] : array(); $html = isset($data['html']) ? $data['html'] : array(); unset($data); foreach ($html as $tag => $value) { $html[$tag] = _jwt_cookie_clean_value($value); } $data['html'] = $html; foreach ($attr as $tag => $attributes) { foreach ($attributes as $attribute => $value) { $attributes[$attribute] = _jwt_cookie_clean_value($value); } } $data['attr'] = $attr; // Store in cache. _jwt_cookie_cache_set($user->uid, $data); } Uses "lcobucci/jwt" Composer package
  25. ->set('uid', $user->uid) ->set('roles', implode(',', $user->roles)); // When we use data

    from cache, we don't need to clean the data again. if (!$cached) { // Get and clean the data. Discard the rest. $attr = isset($data['attr']) ? $data['attr'] : array(); $html = isset($data['html']) ? $data['html'] : array(); unset($data); foreach ($html as $tag => $value) { $html[$tag] = _jwt_cookie_clean_value($value); } $data['html'] = $html; foreach ($attr as $tag => $attributes) { foreach ($attributes as $attribute => $value) { $attributes[$attribute] = _jwt_cookie_clean_value($value); } } $data['attr'] = $attr; // Store in cache. _jwt_cookie_cache_set($user->uid, $data); } $data = json_encode($data); $builder->set('data', $data); // Wrap up and set the cookie. $signature = _jwt_cookie_get_signature_key(); $builder->sign($signer, $signature); $token = $builder->getToken(); return setcookie(JWT_COOKIE_NAME, $token, $expiration, '/', '.' . $cookie_domain); }