webmcp

diff framework/env/auth/openid/discover.lua @ 20:47ddf0f86009

OpenID 2.0 Relying Party support
author jbe/bsw
date Fri Apr 02 02:11:32 2010 +0200 (2010-04-02)
parents
children
line diff
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/framework/env/auth/openid/discover.lua	Fri Apr 02 02:11:32 2010 +0200
     1.3 @@ -0,0 +1,275 @@
     1.4 +--[[--
     1.5 +discovery_data,                                         -- table containing "claimed_identifier", "op_endpoint" and "op_local_identifier"
     1.6 +errmsg,                                                 -- error message in case of failure
     1.7 +errcode =                                               -- error code in case of failure (TODO: not implemented yet)
     1.8 +auth.openid.discover{
     1.9 +  user_supplied_identifier = user_supplied_identifier,  -- string given by user
    1.10 +  https_as_default         = https_as_default,          -- default to https
    1.11 +  curl_options             = curl_options               -- options passed to "curl" binary, when performing discovery
    1.12 +}
    1.13 +
    1.14 +--]]--
    1.15 +
    1.16 +-- helper function
    1.17 +local function decode_entities(str)
    1.18 +  local str = str
    1.19 +  str = string.gsub(value, "&lt;", '<')
    1.20 +  str = string.gsub(value, "&gt;", '>')
    1.21 +  str = string.gsub(value, "&quot;", '"')
    1.22 +  str = string.gsub(value, "&amp;", '&amp;')
    1.23 +  return str
    1.24 +end
    1.25 +
    1.26 +-- helper function
    1.27 +local function get_tag_value(
    1.28 +  str,          -- HTML document or document snippet
    1.29 +  match_tag,    -- tag name
    1.30 +  match_key,    -- attribute key to match
    1.31 +  match_value,  -- attribute value to match
    1.32 +  result_key    -- attribute key of value to return
    1.33 +)
    1.34 +  -- NOTE: The following parameters are case insensitive
    1.35 +  local match_tag   = string.lower(match_tag)
    1.36 +  local match_key   = string.lower(match_key)
    1.37 +  local match_value = string.lower(match_value)
    1.38 +  local result_key  = string.lower(result_key)
    1.39 +  for tag, attributes in
    1.40 +    string.gmatch(str, "<([0-9A-Za-z_-]+) ([^>]*)>")
    1.41 +  do
    1.42 +    local tag = string.lower(tag)
    1.43 +    if tag == match_tag then
    1.44 +      local matching = false
    1.45 +      for key, value in
    1.46 +        string.gmatch(attributes, '([0-9A-Za-z_-]+)="([^"<>]*)"')
    1.47 +      do
    1.48 +        local key = string.lower(key)
    1.49 +        local value = decode_entities(value)
    1.50 +        if key == match_key then
    1.51 +          -- NOTE: match_key must only match one key of space seperated list
    1.52 +          for value in string.gmatch(value, "[^ ]+") do
    1.53 +            if string.lower(value) == match_value then
    1.54 +              matching = true
    1.55 +              break
    1.56 +            end
    1.57 +          end
    1.58 +        end
    1.59 +        if key == result_key then
    1.60 +          result_value = value
    1.61 +        end
    1.62 +      end
    1.63 +      if matching then
    1.64 +        return result_value
    1.65 +      end
    1.66 +    end
    1.67 +  end
    1.68 +  return nil
    1.69 +end
    1.70 +
    1.71 +-- helper function
    1.72 +local function tag_contents(str, match_tag)
    1.73 +  local pos = 0
    1.74 +  local tagpos, closing, tag
    1.75 +  local function next_tag()
    1.76 +    local prefix
    1.77 +    tagpos, prefix, tag, pos = string.match(
    1.78 +      str,
    1.79 +      "()<(/?)([0-9A-Za-z:_-]+)[^>]*>()",
    1.80 +      pos
    1.81 +    )
    1.82 +    closing = (prefix == "/")
    1.83 +  end
    1.84 +  return function()
    1.85 +    repeat
    1.86 +      next_tag()
    1.87 +      if not tagpos then return nil end
    1.88 +      local stripped_tag
    1.89 +      if string.find(tag, ":") then
    1.90 +        stripped_tag = string.match(tag, ":([^:]*)$")
    1.91 +      else
    1.92 +        stripped_tag = tag
    1.93 +      end
    1.94 +    until stripped_tag == match_tag and not closing
    1.95 +    local content_start = pos
    1.96 +    local used_tag = tag
    1.97 +    local counter = 0
    1.98 +    while true do
    1.99 +      repeat
   1.100 +        next_tag()
   1.101 +        if not tagpos then return nil end
   1.102 +      until tag == used_tag
   1.103 +      if closing then
   1.104 +        if counter > 0 then
   1.105 +          counter = counter - 1
   1.106 +        else
   1.107 +          return string.sub(str, content_start, tagpos-1)
   1.108 +        end
   1.109 +      else
   1.110 +        counter = counter + 1
   1.111 +      end
   1.112 +    end
   1.113 +    local content = string.sub(rest, 1, startpos-1)
   1.114 +    str = string.sub(rest, endpos+1)
   1.115 +    return content
   1.116 +  end
   1.117 +end
   1.118 +
   1.119 +local function strip(str)
   1.120 +  local str = str
   1.121 +  string.gsub(str, "^[ \t\r\n]+", "")
   1.122 +  string.gsub(str, "[ \t\r\n]+$", "")
   1.123 +  return str
   1.124 +end
   1.125 +
   1.126 +function auth.openid.discover(args)
   1.127 +  local url = string.match(args.user_supplied_identifier, "[^#]*")
   1.128 +  -- NOTE: XRIs are not supported
   1.129 +  if
   1.130 +    string.find(url, "^[Xx][Rr][Ii]://") or
   1.131 +    string.find(url, "^[=@+$!(]")
   1.132 +  then
   1.133 +    return nil, "XRI identifiers are not supported."
   1.134 +  end
   1.135 +  -- Prepend http:// or https://, if neccessary:
   1.136 +  if not string.find(url, "://") then
   1.137 +    if args.default_to_https then
   1.138 +      url = "https://" .. url
   1.139 +    else
   1.140 +      url = "http://" .. url
   1.141 +    end
   1.142 +  end
   1.143 +  -- Either an xrds_document or an html_document will be fetched
   1.144 +  local xrds_document, html_document
   1.145 +  -- Repeat max 10 times to avoid endless redirection loops
   1.146 +  local redirects = 0
   1.147 +  while true do
   1.148 +    local status, headers, body = auth.openid._curl(url, args.curl_options)
   1.149 +    if not status then
   1.150 +      return nil, "Error while locating XRDS or HTML file for discovery."
   1.151 +    end
   1.152 +    -- Check, if we are redirected:
   1.153 +    local location = string.match(
   1.154 +      headers,
   1.155 +      "\r?\n[Ll][Oo][Cc][Aa][Tt][Ii][Oo][Nn]:[ \t]*([^\r\n]+)"
   1.156 +    )
   1.157 +    if location then
   1.158 +      -- If we are redirected too often, then return an error.
   1.159 +      if redirects >= 10 then
   1.160 +        return nil, "Too many redirects."
   1.161 +      end
   1.162 +      -- Otherwise follow the redirection by changing the variable "url"
   1.163 +      -- and by incrementing the redirect counter.
   1.164 +      url = location
   1.165 +      redirects = redirects + 1
   1.166 +    else
   1.167 +      -- Check, if there is an X-XRDS-Location header
   1.168 +      -- pointing to an XRDS document:
   1.169 +      local xrds_location = string.match(
   1.170 +        headers,
   1.171 +        "\r?\n[Xx]%-[Xx][Rr][Dd][Ss]%-[Ll][Oo][Cc][Aa][Tt][Ii][Oo][Nn]:[ \t]*([^\r\n]+)"
   1.172 +      )
   1.173 +      -- If there is no X-XRDS-Location header, there might be an
   1.174 +      -- http-equiv meta tag serving the same purpose:
   1.175 +      if not xrds_location and status == 200 then
   1.176 +        xrds_location = get_tag_value(body, "meta", "http-equiv", "X-XRDS-Location", "content")
   1.177 +      end
   1.178 +      if xrds_location then
   1.179 +        -- If we know the XRDS-Location, we can fetch the XRDS document
   1.180 +        -- from that location:
   1.181 +        local status, headers, body = auth.openid._curl(xrds_location, args.curl_options)
   1.182 +        if not status then
   1.183 +          return nil, "XRDS document could not be loaded."
   1.184 +        end
   1.185 +        if status ~= 200 then
   1.186 +          return nil, "XRDS document not found where expected."
   1.187 +        end
   1.188 +        xrds_document = body
   1.189 +        break
   1.190 +      elseif
   1.191 +        -- If the Content-Type header is set accordingly, then we already
   1.192 +        -- should have received an XRDS document:
   1.193 +        string.find(
   1.194 +          headers,
   1.195 +          "\r?\n[Cc][Oo][Nn][Tt][Ee][Nn][Tt]%-[Tt][Yy][Pp][Ee]:[ \t]*application/xrds%+xml\r?\n"
   1.196 +        )
   1.197 +      then
   1.198 +        if status ~= 200 then
   1.199 +          return nil, "XRDS document announced but not found."
   1.200 +        end
   1.201 +        xrds_document = body
   1.202 +        break
   1.203 +      else
   1.204 +        -- Otherwise we should have received an HTML document:
   1.205 +        if status ~= 200 then
   1.206 +          return nil, "No XRDS or HTML document found for discovery."
   1.207 +        end
   1.208 +        html_document = body
   1.209 +        break;
   1.210 +      end
   1.211 +    end
   1.212 +  end
   1.213 +  local claimed_identifier   -- OpenID identifier the user claims to own
   1.214 +  local op_endpoint          -- OpenID provider endpoint URL
   1.215 +  local op_local_identifier  -- optional user identifier, local to the OpenID provider
   1.216 +  if xrds_document then
   1.217 +    -- If we got an XRDS document, we look for a matching <Service> entry:
   1.218 +    for content in tag_contents(xrds_document, "Service") do
   1.219 +      local service_uri, service_localid
   1.220 +      for content in tag_contents(content, "URI") do
   1.221 +        if not string.find(content, "[<>]") then
   1.222 +          service_uri = strip(content)
   1.223 +          break
   1.224 +        end
   1.225 +      end
   1.226 +      for content in tag_contents(content, "LocalID") do
   1.227 +        if not string.find(content, "[<>]") then
   1.228 +          service_localid = strip(content)
   1.229 +          break
   1.230 +        end
   1.231 +      end
   1.232 +      for content in tag_contents(content, "Type") do
   1.233 +        if not string.find(content, "[<>]") then
   1.234 +          local content = strip(content)
   1.235 +          if content == "http://specs.openid.net/auth/2.0/server" then
   1.236 +            -- The user entered a provider identifier, thus claimed_identifier
   1.237 +            -- and op_local_identifier will be set to nil.
   1.238 +            op_endpoint = service_uri
   1.239 +            break
   1.240 +          elseif content == "http://specs.openid.net/auth/2.0/signon" then
   1.241 +            -- The user entered his/her own identifier.
   1.242 +            claimed_identifier  = url
   1.243 +            op_endpoint         = service_uri
   1.244 +            op_local_identifier = service_localid
   1.245 +            break
   1.246 +          end
   1.247 +        end
   1.248 +      end
   1.249 +    end
   1.250 +  elseif html_document then
   1.251 +    -- If we got an HTML document, we look for matching <link .../> tags:
   1.252 +    claimed_identifier = url
   1.253 +    op_endpoint = get_tag_value(
   1.254 +      html_document,
   1.255 +      "link", "rel", "openid2.provider", "href"
   1.256 +    )
   1.257 +    op_local_identifier = get_tag_value(
   1.258 +      html_document,
   1.259 +      "link", "rel", "openid2.local_id", "href"
   1.260 +    )
   1.261 +  else
   1.262 +    error("Assertion failed")  -- should not happen
   1.263 +  end
   1.264 +  if not op_endpoint then
   1.265 +    return nil, "No OpenID endpoint found."
   1.266 +  end
   1.267 +  if claimed_identifier then
   1.268 +    claimed_identifier = auth.openid._normalize_url(claimed_identifier)
   1.269 +    if not claimed_identifier then
   1.270 +      return nil, "Claimed identifier could not be normalized."
   1.271 +    end
   1.272 +  end
   1.273 +  return {
   1.274 +    claimed_identifier  = claimed_identifier,
   1.275 +    op_endpoint         = op_endpoint,
   1.276 +    op_local_identifier = op_local_identifier
   1.277 +  }
   1.278 +end

Impressum / About Us