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

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

Thijs Feryn
September 26, 2017

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

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); }