| -- |
| -- Licensed to the Apache Software Foundation (ASF) under one or more |
| -- contributor license agreements. See the NOTICE file distributed with |
| -- this work for additional information regarding copyright ownership. |
| -- The ASF licenses this file to You under the Apache License, Version 2.0 |
| -- (the "License"); you may not use this file except in compliance with |
| -- the License. You may obtain a copy of the License at |
| -- |
| -- http://www.apache.org/licenses/LICENSE-2.0 |
| -- |
| -- Unless required by applicable law or agreed to in writing, software |
| -- distributed under the License is distributed on an "AS IS" BASIS, |
| -- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| -- See the License for the specific language governing permissions and |
| -- limitations under the License. |
| -- |
| local core = require("apisix.core") |
| local resty_sha256 = require("resty.sha256") |
| local str = require("resty.string") |
| local ngx = ngx |
| local ngx_encode_base64 = ngx.encode_base64 |
| local ngx_decode_base64 = ngx.decode_base64 |
| local ngx_time = ngx.time |
| local ngx_cookie_time = ngx.cookie_time |
| local math = math |
| local SAFE_METHODS = {"GET", "HEAD", "OPTIONS"} |
| |
| |
| local schema = { |
| type = "object", |
| properties = { |
| key = { |
| description = "use to generate csrf token", |
| type = "string", |
| }, |
| expires = { |
| description = "expires time(s) for csrf token", |
| type = "integer", |
| default = 7200 |
| }, |
| name = { |
| description = "the csrf token name", |
| type = "string", |
| default = "apisix-csrf-token" |
| } |
| }, |
| required = {"key"} |
| } |
| |
| |
| local _M = { |
| version = 0.1, |
| priority = 2980, |
| name = "csrf", |
| schema = schema, |
| } |
| |
| |
| function _M.check_schema(conf) |
| return core.schema.check(schema, conf) |
| end |
| |
| |
| local function gen_sign(random, expires, key) |
| local sha256 = resty_sha256:new() |
| |
| local sign = "{expires:" .. expires .. ",random:" .. random .. ",key:" .. key .. "}" |
| |
| sha256:update(sign) |
| local digest = sha256:final() |
| |
| return str.to_hex(digest) |
| end |
| |
| |
| local function gen_csrf_token(conf) |
| local random = math.random() |
| local timestamp = ngx_time() |
| local sign = gen_sign(random, timestamp, conf.key) |
| |
| local token = { |
| random = random, |
| expires = timestamp, |
| sign = sign, |
| } |
| |
| local cookie = ngx_encode_base64(core.json.encode(token)) |
| return cookie |
| end |
| |
| |
| local function check_csrf_token(conf, ctx, token) |
| local token_str = ngx_decode_base64(token) |
| if not token_str then |
| core.log.error("csrf token base64 decode error") |
| return false |
| end |
| |
| local token_table, err = core.json.decode(token_str) |
| if err then |
| core.log.error("decode token error: ", err) |
| return false |
| end |
| |
| local random = token_table["random"] |
| if not random then |
| core.log.error("no random in token") |
| return false |
| end |
| |
| local expires = token_table["expires"] |
| if not expires then |
| core.log.error("no expires in token") |
| return false |
| end |
| local time_now = ngx_time() |
| if conf.expires > 0 and time_now - expires > conf.expires then |
| core.log.error("token has expired") |
| return false |
| end |
| |
| local sign = gen_sign(random, expires, conf.key) |
| if token_table["sign"] ~= sign then |
| core.log.error("Invalid signatures") |
| return false |
| end |
| |
| return true |
| end |
| |
| |
| function _M.access(conf, ctx) |
| local method = core.request.get_method(ctx) |
| if core.table.array_find(SAFE_METHODS, method) then |
| return |
| end |
| |
| local header_token = core.request.header(ctx, conf.name) |
| if not header_token or header_token == "" then |
| return 401, {error_msg = "no csrf token in headers"} |
| end |
| |
| local cookie_token = ctx.var["cookie_" .. conf.name] |
| if not cookie_token then |
| return 401, {error_msg = "no csrf cookie"} |
| end |
| |
| if header_token ~= cookie_token then |
| return 401, {error_msg = "csrf token mismatch"} |
| end |
| |
| local result = check_csrf_token(conf, ctx, cookie_token) |
| if not result then |
| return 401, {error_msg = "Failed to verify the csrf token signature"} |
| end |
| end |
| |
| |
| function _M.header_filter(conf, ctx) |
| local csrf_token = gen_csrf_token(conf) |
| local cookie = conf.name .. "=" .. csrf_token .. ";path=/;SameSite=Lax;Expires=" |
| .. ngx_cookie_time(ngx_time() + conf.expires) |
| core.response.add_header("Set-Cookie", cookie) |
| end |
| |
| |
| return _M |