auth.lua 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119
  1. local json = require "json"
  2. local auth_url = "http://app:3001/internal/dovecot/auth"
  3. local secret_file = "/run/secrets/dovecot_auth_secret"
  4. local http_client
  5. local shared_secret
  6. local function read_secret(path)
  7. local file = io.open(path, "r")
  8. if file == nil then
  9. error("Dovecot authentication secret is unavailable")
  10. end
  11. local value = file:read("*a")
  12. file:close()
  13. value = string.gsub(value or "", "^%s+", "")
  14. value = string.gsub(value, "%s+$", "")
  15. if #value < 32 or #value > 512 or string.find(value, "%s") ~= nil then
  16. error("Dovecot authentication secret is invalid")
  17. end
  18. return value
  19. end
  20. function script_init()
  21. shared_secret = read_secret(secret_file)
  22. http_client = dovecot.http.client {
  23. auto_retry = "no",
  24. request_max_attempts = 1,
  25. connect_timeout = "1s",
  26. request_timeout = "30s",
  27. request_absolute_timeout = "30s"
  28. }
  29. return 0
  30. end
  31. local function failure(result)
  32. return result, nil
  33. end
  34. local function valid_user(value)
  35. if type(value) ~= "string" or #value == 0 or #value > 320 then
  36. return false
  37. end
  38. if string.find(value, "/", 1, true) ~= nil
  39. or string.find(value, "\\", 1, true) ~= nil
  40. or string.find(value, "\0", 1, true) ~= nil
  41. or string.find(value, "%s") ~= nil then
  42. return false
  43. end
  44. return string.find(value, "^[^@]+@[^@]+%.[^@]+$") ~= nil
  45. end
  46. local function first_nonempty_string(...)
  47. for index = 1, select("#", ...) do
  48. local value = select(index, ...)
  49. if value ~= nil then
  50. local normalized = tostring(value)
  51. if normalized ~= "" then
  52. return normalized
  53. end
  54. end
  55. end
  56. return ""
  57. end
  58. function auth_password_verify(request, password)
  59. local protocol = string.lower(first_nonempty_string(request.protocol, request.service))
  60. local remote_ip = first_nonempty_string(
  61. request.remote_ip,
  62. request.real_remote_ip,
  63. request.rip,
  64. request.real_rip
  65. )
  66. if protocol ~= "imap" and protocol ~= "pop3" then
  67. return failure(dovecot.auth.PASSDB_RESULT_USER_DISABLED)
  68. end
  69. local http_request = http_client:request {
  70. url = auth_url,
  71. method = "POST"
  72. }
  73. http_request:add_header("content-type", "application/json")
  74. http_request:add_header("authorization", "Bearer " .. shared_secret)
  75. http_request:add_header("connection", "close")
  76. http_request:set_payload(json.encode {
  77. username = request.user,
  78. password = password,
  79. service = protocol,
  80. remoteIp = remote_ip
  81. })
  82. local submitted, response = pcall(function()
  83. return http_request:submit()
  84. end)
  85. if not submitted then
  86. return failure(dovecot.auth.PASSDB_RESULT_INTERNAL_FAILURE)
  87. end
  88. local status = response:status()
  89. if status ~= 200 then
  90. return failure(dovecot.auth.PASSDB_RESULT_INTERNAL_FAILURE)
  91. end
  92. local decoded, payload = pcall(json.decode, response:payload())
  93. if not decoded or type(payload) ~= "table" then
  94. return failure(dovecot.auth.PASSDB_RESULT_INTERNAL_FAILURE)
  95. end
  96. if payload.authenticated == false then
  97. return failure(dovecot.auth.PASSDB_RESULT_PASSWORD_MISMATCH)
  98. end
  99. if payload.authenticated ~= true then
  100. return failure(dovecot.auth.PASSDB_RESULT_INTERNAL_FAILURE)
  101. end
  102. if not valid_user(payload.user) then
  103. return failure(dovecot.auth.PASSDB_RESULT_INTERNAL_FAILURE)
  104. end
  105. return dovecot.auth.PASSDB_RESULT_OK, { user = string.lower(payload.user) }
  106. end