webmcp

changeset 20:47ddf0f86009

OpenID 2.0 Relying Party support
author jbe/bsw
date Fri Apr 02 02:11:32 2010 +0200 (2010-04-02)
parents ee69f20ea0c2
children 2e1dbc97877a
files framework/env/auth/openid/_curl.lua framework/env/auth/openid/_normalize_url.lua framework/env/auth/openid/discover.lua framework/env/auth/openid/initiate.lua framework/env/auth/openid/verify.lua framework/env/auth/openid/xrds_document.lua framework/env/auth/openid/xrds_header.lua
line diff
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/framework/env/auth/openid/_curl.lua	Fri Apr 02 02:11:32 2010 +0200
     1.3 @@ -0,0 +1,20 @@
     1.4 +function auth.openid._curl(url, curl_options)
     1.5 +  -- NOTE: Don't accept URLs starting with file:// or other nasty protocols
     1.6 +  if not string.find(url, "^[Hh][Tt][Tt][Pp][Ss]?://") then
     1.7 +    return nil
     1.8 +  end
     1.9 +  local options = table.new(curl_options)
    1.10 +  options[#options+1] = "-i"
    1.11 +  options[#options+1] = url
    1.12 +  local stdout, errmsg, status = os.pfilter(nil, "curl", unpack(options))
    1.13 +  if not stdout then
    1.14 +    error("Error while executing curl: " .. errmsg)
    1.15 +  end
    1.16 +  if status ~= 0 then
    1.17 +    return nil
    1.18 +  end
    1.19 +  local status  = tonumber(string.match(stdout, "^[^ ]+ *([0-9]*)"))
    1.20 +  local headers = string.match(stdout, "(\r?\n.-\r?\n)\r?\n")
    1.21 +  local body    = string.match(stdout, "\r?\n\r?\n(.*)")
    1.22 +  return status, headers, body
    1.23 +end
     2.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     2.2 +++ b/framework/env/auth/openid/_normalize_url.lua	Fri Apr 02 02:11:32 2010 +0200
     2.3 @@ -0,0 +1,93 @@
     2.4 +--[[--
     2.5 +url,                         -- normalized URL or nil
     2.6 +auth.openid._normalize_url(
     2.7 +  url                        -- unnormalized URL
     2.8 +)
     2.9 +
    2.10 +This function normalizes an URL, and returns nil if the given URL is not a
    2.11 +valid absolute URL. For security reasons only a restricted set of URLs is
    2.12 +valid.
    2.13 +
    2.14 +--]]--
    2.15 +
    2.16 +function auth.openid._normalize_url(url)
    2.17 +  local url = string.match(url, "^(.-)??$")  -- remove "?" at end
    2.18 +  local proto, host, path = string.match(
    2.19 +    url,
    2.20 +    "([A-Za-z]+)://([0-9A-Za-z.:_-]+)/?([0-9A-Za-z%%/._~-]*)$"
    2.21 +  )
    2.22 +  if not proto then
    2.23 +    return nil
    2.24 +  end
    2.25 +  proto = string.lower(proto)
    2.26 +  host  = string.lower(host)
    2.27 +  local port = string.match(host, ":(.*)")
    2.28 +  if port then
    2.29 +    if string.find(port, "^[0-9]+$") then
    2.30 +      port = tonumber(port)
    2.31 +      host = string.match(host, "^(.-):")
    2.32 +      if port < 1 or port > 65535 then
    2.33 +        return nil
    2.34 +      end
    2.35 +    else
    2.36 +      return nil
    2.37 +    end
    2.38 +  end
    2.39 +  if proto == "http" then
    2.40 +    if port == 80 then port = nil end
    2.41 +  elseif proto == "https" then
    2.42 +    if port == 443 then port = nil end
    2.43 +  else
    2.44 +    return nil
    2.45 +  end
    2.46 +  if
    2.47 +    string.find(host, "^%.") or
    2.48 +    string.find(host, "%.$") or
    2.49 +    string.find(host, "%.%.")
    2.50 +  then
    2.51 +    return nil
    2.52 +  end
    2.53 +  for part in string.gmatch(host, "[^.]+") do
    2.54 +    if not string.find(part, "[A-Za-z]") then
    2.55 +      return nil
    2.56 +    end
    2.57 +  end
    2.58 +  local path_parts = {}
    2.59 +  for part in string.gmatch(path, "[^/]+") do
    2.60 +    if part == "." then
    2.61 +      -- do nothing
    2.62 +    elseif part == ".." then
    2.63 +      path_parts[#path_parts] = nil
    2.64 +    else
    2.65 +      local fail = false
    2.66 +      local part = string.gsub(
    2.67 +        part,
    2.68 +        "%%([0-9A-Fa-f]?[0-9A-Fa-f]?)",
    2.69 +        function (hex)
    2.70 +          if #hex ~= 2 then
    2.71 +            fail = true
    2.72 +            return
    2.73 +          end
    2.74 +          local char = string.char(tonumber("0x" .. hex))
    2.75 +          if string.find(char, "[0-9A-Za-z._~-]") then
    2.76 +            return char
    2.77 +          else
    2.78 +            return "%" .. string.upper(hex)
    2.79 +          end
    2.80 +        end
    2.81 +      )
    2.82 +      if fail then
    2.83 +        return nil
    2.84 +      end
    2.85 +      path_parts[#path_parts+1] = part
    2.86 +    end
    2.87 +  end
    2.88 +  if string.find(path, "/$") then
    2.89 +    path_parts[#path_parts+1] = ""
    2.90 +  end
    2.91 +  path = table.concat(path_parts, "/")
    2.92 +  if port then
    2.93 +    host = host .. ":" .. tostring(port)
    2.94 +  end
    2.95 +  return proto .. "://" .. host .. "/" .. path
    2.96 +end
     3.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     3.2 +++ b/framework/env/auth/openid/discover.lua	Fri Apr 02 02:11:32 2010 +0200
     3.3 @@ -0,0 +1,275 @@
     3.4 +--[[--
     3.5 +discovery_data,                                         -- table containing "claimed_identifier", "op_endpoint" and "op_local_identifier"
     3.6 +errmsg,                                                 -- error message in case of failure
     3.7 +errcode =                                               -- error code in case of failure (TODO: not implemented yet)
     3.8 +auth.openid.discover{
     3.9 +  user_supplied_identifier = user_supplied_identifier,  -- string given by user
    3.10 +  https_as_default         = https_as_default,          -- default to https
    3.11 +  curl_options             = curl_options               -- options passed to "curl" binary, when performing discovery
    3.12 +}
    3.13 +
    3.14 +--]]--
    3.15 +
    3.16 +-- helper function
    3.17 +local function decode_entities(str)
    3.18 +  local str = str
    3.19 +  str = string.gsub(value, "&lt;", '<')
    3.20 +  str = string.gsub(value, "&gt;", '>')
    3.21 +  str = string.gsub(value, "&quot;", '"')
    3.22 +  str = string.gsub(value, "&amp;", '&amp;')
    3.23 +  return str
    3.24 +end
    3.25 +
    3.26 +-- helper function
    3.27 +local function get_tag_value(
    3.28 +  str,          -- HTML document or document snippet
    3.29 +  match_tag,    -- tag name
    3.30 +  match_key,    -- attribute key to match
    3.31 +  match_value,  -- attribute value to match
    3.32 +  result_key    -- attribute key of value to return
    3.33 +)
    3.34 +  -- NOTE: The following parameters are case insensitive
    3.35 +  local match_tag   = string.lower(match_tag)
    3.36 +  local match_key   = string.lower(match_key)
    3.37 +  local match_value = string.lower(match_value)
    3.38 +  local result_key  = string.lower(result_key)
    3.39 +  for tag, attributes in
    3.40 +    string.gmatch(str, "<([0-9A-Za-z_-]+) ([^>]*)>")
    3.41 +  do
    3.42 +    local tag = string.lower(tag)
    3.43 +    if tag == match_tag then
    3.44 +      local matching = false
    3.45 +      for key, value in
    3.46 +        string.gmatch(attributes, '([0-9A-Za-z_-]+)="([^"<>]*)"')
    3.47 +      do
    3.48 +        local key = string.lower(key)
    3.49 +        local value = decode_entities(value)
    3.50 +        if key == match_key then
    3.51 +          -- NOTE: match_key must only match one key of space seperated list
    3.52 +          for value in string.gmatch(value, "[^ ]+") do
    3.53 +            if string.lower(value) == match_value then
    3.54 +              matching = true
    3.55 +              break
    3.56 +            end
    3.57 +          end
    3.58 +        end
    3.59 +        if key == result_key then
    3.60 +          result_value = value
    3.61 +        end
    3.62 +      end
    3.63 +      if matching then
    3.64 +        return result_value
    3.65 +      end
    3.66 +    end
    3.67 +  end
    3.68 +  return nil
    3.69 +end
    3.70 +
    3.71 +-- helper function
    3.72 +local function tag_contents(str, match_tag)
    3.73 +  local pos = 0
    3.74 +  local tagpos, closing, tag
    3.75 +  local function next_tag()
    3.76 +    local prefix
    3.77 +    tagpos, prefix, tag, pos = string.match(
    3.78 +      str,
    3.79 +      "()<(/?)([0-9A-Za-z:_-]+)[^>]*>()",
    3.80 +      pos
    3.81 +    )
    3.82 +    closing = (prefix == "/")
    3.83 +  end
    3.84 +  return function()
    3.85 +    repeat
    3.86 +      next_tag()
    3.87 +      if not tagpos then return nil end
    3.88 +      local stripped_tag
    3.89 +      if string.find(tag, ":") then
    3.90 +        stripped_tag = string.match(tag, ":([^:]*)$")
    3.91 +      else
    3.92 +        stripped_tag = tag
    3.93 +      end
    3.94 +    until stripped_tag == match_tag and not closing
    3.95 +    local content_start = pos
    3.96 +    local used_tag = tag
    3.97 +    local counter = 0
    3.98 +    while true do
    3.99 +      repeat
   3.100 +        next_tag()
   3.101 +        if not tagpos then return nil end
   3.102 +      until tag == used_tag
   3.103 +      if closing then
   3.104 +        if counter > 0 then
   3.105 +          counter = counter - 1
   3.106 +        else
   3.107 +          return string.sub(str, content_start, tagpos-1)
   3.108 +        end
   3.109 +      else
   3.110 +        counter = counter + 1
   3.111 +      end
   3.112 +    end
   3.113 +    local content = string.sub(rest, 1, startpos-1)
   3.114 +    str = string.sub(rest, endpos+1)
   3.115 +    return content
   3.116 +  end
   3.117 +end
   3.118 +
   3.119 +local function strip(str)
   3.120 +  local str = str
   3.121 +  string.gsub(str, "^[ \t\r\n]+", "")
   3.122 +  string.gsub(str, "[ \t\r\n]+$", "")
   3.123 +  return str
   3.124 +end
   3.125 +
   3.126 +function auth.openid.discover(args)
   3.127 +  local url = string.match(args.user_supplied_identifier, "[^#]*")
   3.128 +  -- NOTE: XRIs are not supported
   3.129 +  if
   3.130 +    string.find(url, "^[Xx][Rr][Ii]://") or
   3.131 +    string.find(url, "^[=@+$!(]")
   3.132 +  then
   3.133 +    return nil, "XRI identifiers are not supported."
   3.134 +  end
   3.135 +  -- Prepend http:// or https://, if neccessary:
   3.136 +  if not string.find(url, "://") then
   3.137 +    if args.default_to_https then
   3.138 +      url = "https://" .. url
   3.139 +    else
   3.140 +      url = "http://" .. url
   3.141 +    end
   3.142 +  end
   3.143 +  -- Either an xrds_document or an html_document will be fetched
   3.144 +  local xrds_document, html_document
   3.145 +  -- Repeat max 10 times to avoid endless redirection loops
   3.146 +  local redirects = 0
   3.147 +  while true do
   3.148 +    local status, headers, body = auth.openid._curl(url, args.curl_options)
   3.149 +    if not status then
   3.150 +      return nil, "Error while locating XRDS or HTML file for discovery."
   3.151 +    end
   3.152 +    -- Check, if we are redirected:
   3.153 +    local location = string.match(
   3.154 +      headers,
   3.155 +      "\r?\n[Ll][Oo][Cc][Aa][Tt][Ii][Oo][Nn]:[ \t]*([^\r\n]+)"
   3.156 +    )
   3.157 +    if location then
   3.158 +      -- If we are redirected too often, then return an error.
   3.159 +      if redirects >= 10 then
   3.160 +        return nil, "Too many redirects."
   3.161 +      end
   3.162 +      -- Otherwise follow the redirection by changing the variable "url"
   3.163 +      -- and by incrementing the redirect counter.
   3.164 +      url = location
   3.165 +      redirects = redirects + 1
   3.166 +    else
   3.167 +      -- Check, if there is an X-XRDS-Location header
   3.168 +      -- pointing to an XRDS document:
   3.169 +      local xrds_location = string.match(
   3.170 +        headers,
   3.171 +        "\r?\n[Xx]%-[Xx][Rr][Dd][Ss]%-[Ll][Oo][Cc][Aa][Tt][Ii][Oo][Nn]:[ \t]*([^\r\n]+)"
   3.172 +      )
   3.173 +      -- If there is no X-XRDS-Location header, there might be an
   3.174 +      -- http-equiv meta tag serving the same purpose:
   3.175 +      if not xrds_location and status == 200 then
   3.176 +        xrds_location = get_tag_value(body, "meta", "http-equiv", "X-XRDS-Location", "content")
   3.177 +      end
   3.178 +      if xrds_location then
   3.179 +        -- If we know the XRDS-Location, we can fetch the XRDS document
   3.180 +        -- from that location:
   3.181 +        local status, headers, body = auth.openid._curl(xrds_location, args.curl_options)
   3.182 +        if not status then
   3.183 +          return nil, "XRDS document could not be loaded."
   3.184 +        end
   3.185 +        if status ~= 200 then
   3.186 +          return nil, "XRDS document not found where expected."
   3.187 +        end
   3.188 +        xrds_document = body
   3.189 +        break
   3.190 +      elseif
   3.191 +        -- If the Content-Type header is set accordingly, then we already
   3.192 +        -- should have received an XRDS document:
   3.193 +        string.find(
   3.194 +          headers,
   3.195 +          "\r?\n[Cc][Oo][Nn][Tt][Ee][Nn][Tt]%-[Tt][Yy][Pp][Ee]:[ \t]*application/xrds%+xml\r?\n"
   3.196 +        )
   3.197 +      then
   3.198 +        if status ~= 200 then
   3.199 +          return nil, "XRDS document announced but not found."
   3.200 +        end
   3.201 +        xrds_document = body
   3.202 +        break
   3.203 +      else
   3.204 +        -- Otherwise we should have received an HTML document:
   3.205 +        if status ~= 200 then
   3.206 +          return nil, "No XRDS or HTML document found for discovery."
   3.207 +        end
   3.208 +        html_document = body
   3.209 +        break;
   3.210 +      end
   3.211 +    end
   3.212 +  end
   3.213 +  local claimed_identifier   -- OpenID identifier the user claims to own
   3.214 +  local op_endpoint          -- OpenID provider endpoint URL
   3.215 +  local op_local_identifier  -- optional user identifier, local to the OpenID provider
   3.216 +  if xrds_document then
   3.217 +    -- If we got an XRDS document, we look for a matching <Service> entry:
   3.218 +    for content in tag_contents(xrds_document, "Service") do
   3.219 +      local service_uri, service_localid
   3.220 +      for content in tag_contents(content, "URI") do
   3.221 +        if not string.find(content, "[<>]") then
   3.222 +          service_uri = strip(content)
   3.223 +          break
   3.224 +        end
   3.225 +      end
   3.226 +      for content in tag_contents(content, "LocalID") do
   3.227 +        if not string.find(content, "[<>]") then
   3.228 +          service_localid = strip(content)
   3.229 +          break
   3.230 +        end
   3.231 +      end
   3.232 +      for content in tag_contents(content, "Type") do
   3.233 +        if not string.find(content, "[<>]") then
   3.234 +          local content = strip(content)
   3.235 +          if content == "http://specs.openid.net/auth/2.0/server" then
   3.236 +            -- The user entered a provider identifier, thus claimed_identifier
   3.237 +            -- and op_local_identifier will be set to nil.
   3.238 +            op_endpoint = service_uri
   3.239 +            break
   3.240 +          elseif content == "http://specs.openid.net/auth/2.0/signon" then
   3.241 +            -- The user entered his/her own identifier.
   3.242 +            claimed_identifier  = url
   3.243 +            op_endpoint         = service_uri
   3.244 +            op_local_identifier = service_localid
   3.245 +            break
   3.246 +          end
   3.247 +        end
   3.248 +      end
   3.249 +    end
   3.250 +  elseif html_document then
   3.251 +    -- If we got an HTML document, we look for matching <link .../> tags:
   3.252 +    claimed_identifier = url
   3.253 +    op_endpoint = get_tag_value(
   3.254 +      html_document,
   3.255 +      "link", "rel", "openid2.provider", "href"
   3.256 +    )
   3.257 +    op_local_identifier = get_tag_value(
   3.258 +      html_document,
   3.259 +      "link", "rel", "openid2.local_id", "href"
   3.260 +    )
   3.261 +  else
   3.262 +    error("Assertion failed")  -- should not happen
   3.263 +  end
   3.264 +  if not op_endpoint then
   3.265 +    return nil, "No OpenID endpoint found."
   3.266 +  end
   3.267 +  if claimed_identifier then
   3.268 +    claimed_identifier = auth.openid._normalize_url(claimed_identifier)
   3.269 +    if not claimed_identifier then
   3.270 +      return nil, "Claimed identifier could not be normalized."
   3.271 +    end
   3.272 +  end
   3.273 +  return {
   3.274 +    claimed_identifier  = claimed_identifier,
   3.275 +    op_endpoint         = op_endpoint,
   3.276 +    op_local_identifier = op_local_identifier
   3.277 +  }
   3.278 +end
     4.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     4.2 +++ b/framework/env/auth/openid/initiate.lua	Fri Apr 02 02:11:32 2010 +0200
     4.3 @@ -0,0 +1,55 @@
     4.4 +--[[--
     4.5 +success,                                                -- boolean indicating success or failure
     4.6 +errmsg =                                                -- error message in case of failure (TODO: not implemented yet)
     4.7 +auth.openid.initiate{
     4.8 +  user_supplied_identifier = user_supplied_identifier,  -- string given by user
     4.9 +  https_as_default         = https_as_default,          -- default to https
    4.10 +  curl_options             = curl_options,              -- additional options passed to "curl" binary, when performing discovery
    4.11 +  return_to_module         = return_to_module,          -- module of the verifying view, the user shall return to after authentication
    4.12 +  return_to_view           = return_to_view,            -- verifying view, the user shall return to after authentication
    4.13 +  realm                    = realm                      -- URL the user should authenticate for, defaults to application base
    4.14 +}
    4.15 +
    4.16 +In order to authenticate using OpenID the user should enter an identifier.
    4.17 +It is recommended that the form field element for this identifier is named
    4.18 +"openid_identifier", so that User-Agents can automatically determine the
    4.19 +given field should contain an OpenID identifier. The entered identifier is
    4.20 +then passed as "user_supplied_identifier" argument to this function. It
    4.21 +returns false on error and currently never returns on success. However in
    4.22 +future this function shall return true on success. After the user has
    4.23 +authenticated successfully, he/she is forwarded to the URL given by the
    4.24 +"return_to" argument. Under this URL the application has to verify the
    4.25 +result by calling auth.openid.verify{...}.
    4.26 +
    4.27 +--]]--
    4.28 +
    4.29 +function auth.openid.initiate(args)
    4.30 +  local dd, errmsg, errcode = auth.openid.discover(args)
    4.31 +  if not dd then
    4.32 +    return nil, errmsg, errcode
    4.33 +  end
    4.34 +  -- TODO: Use request.redirect once it supports external URLs
    4.35 +  cgi.set_status("303 See Other")
    4.36 +  cgi.add_header(
    4.37 +    "Location: " ..
    4.38 +    encode.url{
    4.39 +      external = dd.op_endpoint,
    4.40 +      params = {
    4.41 +        ["openid.ns"]         = "http://specs.openid.net/auth/2.0",
    4.42 +        ["openid.mode"]       = "checkid_setup",
    4.43 +        ["openid.claimed_id"] = dd.claimed_identifier or
    4.44 +                                "http://specs.openid.net/auth/2.0/identifier_select",
    4.45 +        ["openid.identity"]   = dd.op_local_identifier or dd.claimed_identifier or
    4.46 +                                "http://specs.openid.net/auth/2.0/identifier_select",
    4.47 +        ["openid.return_to"]  = encode.url{
    4.48 +                                  base   = request.get_absolute_baseurl(),
    4.49 +                                  module = args.return_to_module,
    4.50 +                                  view   = args.return_to_view
    4.51 +                                },
    4.52 +        ["openid.realm"]      = args.realm or request.get_absolute_baseurl()
    4.53 +      }
    4.54 +    }
    4.55 +  )
    4.56 +  cgi.send_data()
    4.57 +  exit()
    4.58 +end
     5.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     5.2 +++ b/framework/env/auth/openid/verify.lua	Fri Apr 02 02:11:32 2010 +0200
     5.3 @@ -0,0 +1,113 @@
     5.4 +--[[--
     5.5 +claimed_identifier,                        -- identifier owned by the user
     5.6 +errmsg,                                    -- error message in case of failure
     5.7 +errcode =                                  -- error code in case of failure (TODO: not implemented yet)
     5.8 +auth.openid.verify(
     5.9 +  force_https              = force_https,  -- only allow https
    5.10 +  curl_options             = curl_options  -- options passed to "curl" binary, when performing discovery
    5.11 +)
    5.12 +
    5.13 +--]]--
    5.14 +
    5.15 +function auth.openid.verify(args)
    5.16 +  local args = args or {}
    5.17 +  if cgi.params["openid.ns"] ~= "http://specs.openid.net/auth/2.0" then
    5.18 +    return nil, "No indirect OpenID 2.0 message received."
    5.19 +  end
    5.20 +  local mode = cgi.params["openid.mode"]
    5.21 +  if mode == "id_res" then
    5.22 +    local return_to_url = cgi.params["openid.return_to"]
    5.23 +    if not return_to_url then
    5.24 +      return nil, "No return_to URL received in answer."
    5.25 +    end
    5.26 +    if return_to_url ~= encode.url{
    5.27 +      base   = request.get_absolute_baseurl(),
    5.28 +      module = request.get_module(),
    5.29 +      view   = request.get_view()
    5.30 +    } then
    5.31 +      return nil, "return_to URL not matching."
    5.32 +    end
    5.33 +    local discovery_args = table.new(args)
    5.34 +    local claimed_identifier = cgi.params["openid.claimed_id"]
    5.35 +    if not claimed_identifier then
    5.36 +      return nil, "No claimed identifier received."
    5.37 +    end
    5.38 +    local cropped_identifier = string.match(claimed_identifier, "[^#]*")
    5.39 +    local normalized_identifier = auth.openid._normalize_url(
    5.40 +      cropped_identifier
    5.41 +    )
    5.42 +    if not normalized_identifier then
    5.43 +      return nil, "Claimed identifier could not be normalized."
    5.44 +    end
    5.45 +    if normalized_identifier ~= cropped_identifier then
    5.46 +      return nil, "Claimed identifier was not normalized."
    5.47 +    end
    5.48 +    discovery_args.user_supplied_identifier = cropped_identifier
    5.49 +    local dd, errmsg, errcode = auth.openid.discover(discovery_args)
    5.50 +    if not dd then
    5.51 +      return nil, errmsg, errcode
    5.52 +    end
    5.53 +    if not dd.claimed_identifier then
    5.54 +      return nil, "Identifier is an OpenID Provider."
    5.55 +    end
    5.56 +    if dd.claimed_identifier ~= cropped_identifier then
    5.57 +      return nil, "Claimed identifier does not match."
    5.58 +    end
    5.59 +    local nonce = cgi.params["openid.response_nonce"]
    5.60 +    if not nonce then
    5.61 +      return nil, "Did not receive a response nonce."
    5.62 +    end 
    5.63 +    local year, month, day, hour, minute, second = string.match(
    5.64 +      nonce,
    5.65 +      "^([0-9][0-9][0-9][0-9])%-([0-9][0-9])%-([0-9][0-9])T([0-9][0-9]):([0-9][0-9]):([0-9][0-9])Z"
    5.66 +    )
    5.67 +    if not year then
    5.68 +      return nil, "Response nonce did not contain a parsable date/time."
    5.69 +    end
    5.70 +    local ts = atom.timestamp{
    5.71 +      year   = tonumber(year),
    5.72 +      month  = tonumber(month),
    5.73 +      day    = tonumber(day),
    5.74 +      hour   = tonumber(hour),
    5.75 +      minute = tonumber(minute),
    5.76 +      second = tonumber(second)
    5.77 +    }
    5.78 +    -- NOTE: 50 hours margin allows us to ignore time zone issues here:
    5.79 +    if math.abs(ts - atom.timestamp:get_current()) > 3600 * 50 then
    5.80 +      return nil, "Response nonce contains wrong time or local time is wrong."
    5.81 +    end
    5.82 +    local params = {}
    5.83 +    for key, value in pairs(cgi.params) do
    5.84 +      local trimmed_key = string.match(key, "^openid%.(.+)")
    5.85 +      if trimmed_key then
    5.86 +        params[key] = value
    5.87 +      end
    5.88 +    end
    5.89 +    params["openid.mode"] = "check_authentication"
    5.90 +    local options = table.new(args.curl_options)
    5.91 +    for key, value in pairs(params) do
    5.92 +      options[#options+1] = "--data-urlencode"
    5.93 +      options[#options+1] = key .. "=" .. value
    5.94 +    end
    5.95 +    local status, headers, body = auth.openid._curl(dd.op_endpoint, options)
    5.96 +    if status ~= 200 then
    5.97 +      return nil, "Authorization could not be verified."
    5.98 +    end
    5.99 +    local result = {}
   5.100 +    for key, value in string.gmatch(body, "([^\n:]+):([^\n]*)") do
   5.101 +      result[key] = value
   5.102 +    end
   5.103 +    if result.ns ~= "http://specs.openid.net/auth/2.0" then
   5.104 +      return nil, "No OpenID 2.0 message replied."
   5.105 +    end
   5.106 +    if result.is_valid == "true" then
   5.107 +      return claimed_identifier
   5.108 +    else
   5.109 +      return nil, "Signature invalid."
   5.110 +    end
   5.111 +  elseif mode == "cancel" then
   5.112 +    return nil, "Authorization failed according to OpenID provider."
   5.113 +  else
   5.114 +    return nil, "Unexpected OpenID mode."
   5.115 +  end
   5.116 +end
     6.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     6.2 +++ b/framework/env/auth/openid/xrds_document.lua	Fri Apr 02 02:11:32 2010 +0200
     6.3 @@ -0,0 +1,32 @@
     6.4 +--[[--
     6.5 +auth.openid.xrds_document{
     6.6 +  return_to_module = return_to_module,
     6.7 +  return_to_view   = return_to_view
     6.8 +}
     6.9 +
    6.10 +This function returns an XRDS document with Content-Type
    6.11 +application/xrds+xml. For more information see documentation on
    6.12 +auth.openid.xrds_document{...}.
    6.13 +
    6.14 +--]]--
    6.15 +
    6.16 +function auth.openid.xrds_document(args)
    6.17 +  slot.set_layout(nil, "application/xrds+xml")
    6.18 +  slot.put_into("data",
    6.19 +    "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n",
    6.20 +    "<xrds:XRDS xmlns:xrds=\"xri://$xrds\" xmlns=\"xri://$xrd*($v*2.0)\">\n",
    6.21 +    "  <XRD>\n",
    6.22 +    "    <Service>\n",                                                   
    6.23 +    "      <Type>http://specs.openid.net/auth/2.0/return_to</Type>\n",
    6.24 +    "      <URI>",
    6.25 +    encode.url{
    6.26 +      base   = request.get_absolute_baseurl(),
    6.27 +      module = args.return_to_module,
    6.28 +      view   = args.return_to_view
    6.29 +    },
    6.30 +    "</URI>\n",
    6.31 +    "    </Service>\n",
    6.32 +    "  </XRD>\n",
    6.33 +    "</xrds:XRDS>\n"
    6.34 +  )
    6.35 +end
     7.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     7.2 +++ b/framework/env/auth/openid/xrds_header.lua	Fri Apr 02 02:11:32 2010 +0200
     7.3 @@ -0,0 +1,54 @@
     7.4 +--[[--
     7.5 +auth.openid.xrds_header{
     7.6 +  ...                     -- arguments as used for encode.url{...}, pointing to an XRDS document as explained below
     7.7 +}
     7.8 +
     7.9 +According to the OpenID specification, providers should verify, that
    7.10 +return_to URLs are an OpenID relying party endpoint. To use OpenID
    7.11 +providers following this recommendation, the relying parties can send a
    7.12 +X-XRDS-Location header by calling this function. Its arguments must refer
    7.13 +to an URL returning a document as follows:
    7.14 +
    7.15 +<?xml version="1.0" encoding="UTF-8"?>
    7.16 +<xrds:XRDS xmlns:xrds="xri://$xrds" xmlns="xri://$xrd*($v*2.0)">
    7.17 +  <XRD>                                                         
    7.18 +    <Service>                                                   
    7.19 +      <Type>http://specs.openid.net/auth/2.0/return_to</Type>   
    7.20 +      <URI>RETURN_TO_URL</URI>                                  
    7.21 +    </Service>                                                  
    7.22 +  </XRD>                                                        
    7.23 +</xrds:XRDS>
    7.24 +
    7.25 +The placeholder RETURN_TO_URL has to be replaced by the absolute URL of the
    7.26 +given return_to_module and return_to_view.
    7.27 +
    7.28 +
    7.29 +Example application-wide filter, assuming the document above is saved in
    7.30 +"static/openid.xrds":
    7.31 +
    7.32 +auth.openid.xrds_header{ static = "openid.xrds" }
    7.33 +execute.inner()
    7.34 +
    7.35 +
    7.36 +Example applications-wide filter, assuming
    7.37 +- the return_to_module is "openid"
    7.38 +- the return_to_view is "return"
    7.39 +- the module for returning the xrds document is "openid"
    7.40 +- the view for returning the xrds document is "xrds"
    7.41 +
    7.42 +auth.openid.xrds_header{ module = "openid", view = "xrds" }
    7.43 +execute.inner()
    7.44 +
    7.45 +
    7.46 +In the last example the "xrds" view in module "openid" has to make the
    7.47 +following call:
    7.48 +
    7.49 +auth.openid.xrds_document{
    7.50 +  return_to_module = "openid",
    7.51 +  return_to_view   = "return"
    7.52 +}
    7.53 +
    7.54 +--]]--
    7.55 +function auth.openid.xrds_header(args)
    7.56 +  cgi.add_header("X-XRDS-Location: " .. encode.url(args))
    7.57 +end

Impressum / About Us