webmcp

annotate framework/env/auth/openid/discover.lua @ 405:c5f9a1b2f225

Updated year of copyright notice
author jbe
date Wed Jan 06 02:54:45 2016 +0100 (2016-01-06)
parents 47ddf0f86009
children
rev   line source
jbe/bsw@20 1 --[[--
jbe/bsw@20 2 discovery_data, -- table containing "claimed_identifier", "op_endpoint" and "op_local_identifier"
jbe/bsw@20 3 errmsg, -- error message in case of failure
jbe/bsw@20 4 errcode = -- error code in case of failure (TODO: not implemented yet)
jbe/bsw@20 5 auth.openid.discover{
jbe/bsw@20 6 user_supplied_identifier = user_supplied_identifier, -- string given by user
jbe/bsw@20 7 https_as_default = https_as_default, -- default to https
jbe/bsw@20 8 curl_options = curl_options -- options passed to "curl" binary, when performing discovery
jbe/bsw@20 9 }
jbe/bsw@20 10
jbe/bsw@20 11 --]]--
jbe/bsw@20 12
jbe/bsw@20 13 -- helper function
jbe/bsw@20 14 local function decode_entities(str)
jbe/bsw@20 15 local str = str
jbe/bsw@20 16 str = string.gsub(value, "&lt;", '<')
jbe/bsw@20 17 str = string.gsub(value, "&gt;", '>')
jbe/bsw@20 18 str = string.gsub(value, "&quot;", '"')
jbe/bsw@20 19 str = string.gsub(value, "&amp;", '&amp;')
jbe/bsw@20 20 return str
jbe/bsw@20 21 end
jbe/bsw@20 22
jbe/bsw@20 23 -- helper function
jbe/bsw@20 24 local function get_tag_value(
jbe/bsw@20 25 str, -- HTML document or document snippet
jbe/bsw@20 26 match_tag, -- tag name
jbe/bsw@20 27 match_key, -- attribute key to match
jbe/bsw@20 28 match_value, -- attribute value to match
jbe/bsw@20 29 result_key -- attribute key of value to return
jbe/bsw@20 30 )
jbe/bsw@20 31 -- NOTE: The following parameters are case insensitive
jbe/bsw@20 32 local match_tag = string.lower(match_tag)
jbe/bsw@20 33 local match_key = string.lower(match_key)
jbe/bsw@20 34 local match_value = string.lower(match_value)
jbe/bsw@20 35 local result_key = string.lower(result_key)
jbe/bsw@20 36 for tag, attributes in
jbe/bsw@20 37 string.gmatch(str, "<([0-9A-Za-z_-]+) ([^>]*)>")
jbe/bsw@20 38 do
jbe/bsw@20 39 local tag = string.lower(tag)
jbe/bsw@20 40 if tag == match_tag then
jbe/bsw@20 41 local matching = false
jbe/bsw@20 42 for key, value in
jbe/bsw@20 43 string.gmatch(attributes, '([0-9A-Za-z_-]+)="([^"<>]*)"')
jbe/bsw@20 44 do
jbe/bsw@20 45 local key = string.lower(key)
jbe/bsw@20 46 local value = decode_entities(value)
jbe/bsw@20 47 if key == match_key then
jbe/bsw@20 48 -- NOTE: match_key must only match one key of space seperated list
jbe/bsw@20 49 for value in string.gmatch(value, "[^ ]+") do
jbe/bsw@20 50 if string.lower(value) == match_value then
jbe/bsw@20 51 matching = true
jbe/bsw@20 52 break
jbe/bsw@20 53 end
jbe/bsw@20 54 end
jbe/bsw@20 55 end
jbe/bsw@20 56 if key == result_key then
jbe/bsw@20 57 result_value = value
jbe/bsw@20 58 end
jbe/bsw@20 59 end
jbe/bsw@20 60 if matching then
jbe/bsw@20 61 return result_value
jbe/bsw@20 62 end
jbe/bsw@20 63 end
jbe/bsw@20 64 end
jbe/bsw@20 65 return nil
jbe/bsw@20 66 end
jbe/bsw@20 67
jbe/bsw@20 68 -- helper function
jbe/bsw@20 69 local function tag_contents(str, match_tag)
jbe/bsw@20 70 local pos = 0
jbe/bsw@20 71 local tagpos, closing, tag
jbe/bsw@20 72 local function next_tag()
jbe/bsw@20 73 local prefix
jbe/bsw@20 74 tagpos, prefix, tag, pos = string.match(
jbe/bsw@20 75 str,
jbe/bsw@20 76 "()<(/?)([0-9A-Za-z:_-]+)[^>]*>()",
jbe/bsw@20 77 pos
jbe/bsw@20 78 )
jbe/bsw@20 79 closing = (prefix == "/")
jbe/bsw@20 80 end
jbe/bsw@20 81 return function()
jbe/bsw@20 82 repeat
jbe/bsw@20 83 next_tag()
jbe/bsw@20 84 if not tagpos then return nil end
jbe/bsw@20 85 local stripped_tag
jbe/bsw@20 86 if string.find(tag, ":") then
jbe/bsw@20 87 stripped_tag = string.match(tag, ":([^:]*)$")
jbe/bsw@20 88 else
jbe/bsw@20 89 stripped_tag = tag
jbe/bsw@20 90 end
jbe/bsw@20 91 until stripped_tag == match_tag and not closing
jbe/bsw@20 92 local content_start = pos
jbe/bsw@20 93 local used_tag = tag
jbe/bsw@20 94 local counter = 0
jbe/bsw@20 95 while true do
jbe/bsw@20 96 repeat
jbe/bsw@20 97 next_tag()
jbe/bsw@20 98 if not tagpos then return nil end
jbe/bsw@20 99 until tag == used_tag
jbe/bsw@20 100 if closing then
jbe/bsw@20 101 if counter > 0 then
jbe/bsw@20 102 counter = counter - 1
jbe/bsw@20 103 else
jbe/bsw@20 104 return string.sub(str, content_start, tagpos-1)
jbe/bsw@20 105 end
jbe/bsw@20 106 else
jbe/bsw@20 107 counter = counter + 1
jbe/bsw@20 108 end
jbe/bsw@20 109 end
jbe/bsw@20 110 local content = string.sub(rest, 1, startpos-1)
jbe/bsw@20 111 str = string.sub(rest, endpos+1)
jbe/bsw@20 112 return content
jbe/bsw@20 113 end
jbe/bsw@20 114 end
jbe/bsw@20 115
jbe/bsw@20 116 local function strip(str)
jbe/bsw@20 117 local str = str
jbe/bsw@20 118 string.gsub(str, "^[ \t\r\n]+", "")
jbe/bsw@20 119 string.gsub(str, "[ \t\r\n]+$", "")
jbe/bsw@20 120 return str
jbe/bsw@20 121 end
jbe/bsw@20 122
jbe/bsw@20 123 function auth.openid.discover(args)
jbe/bsw@20 124 local url = string.match(args.user_supplied_identifier, "[^#]*")
jbe/bsw@20 125 -- NOTE: XRIs are not supported
jbe/bsw@20 126 if
jbe/bsw@20 127 string.find(url, "^[Xx][Rr][Ii]://") or
jbe/bsw@20 128 string.find(url, "^[=@+$!(]")
jbe/bsw@20 129 then
jbe/bsw@20 130 return nil, "XRI identifiers are not supported."
jbe/bsw@20 131 end
jbe/bsw@20 132 -- Prepend http:// or https://, if neccessary:
jbe/bsw@20 133 if not string.find(url, "://") then
jbe/bsw@20 134 if args.default_to_https then
jbe/bsw@20 135 url = "https://" .. url
jbe/bsw@20 136 else
jbe/bsw@20 137 url = "http://" .. url
jbe/bsw@20 138 end
jbe/bsw@20 139 end
jbe/bsw@20 140 -- Either an xrds_document or an html_document will be fetched
jbe/bsw@20 141 local xrds_document, html_document
jbe/bsw@20 142 -- Repeat max 10 times to avoid endless redirection loops
jbe/bsw@20 143 local redirects = 0
jbe/bsw@20 144 while true do
jbe/bsw@20 145 local status, headers, body = auth.openid._curl(url, args.curl_options)
jbe/bsw@20 146 if not status then
jbe/bsw@20 147 return nil, "Error while locating XRDS or HTML file for discovery."
jbe/bsw@20 148 end
jbe/bsw@20 149 -- Check, if we are redirected:
jbe/bsw@20 150 local location = string.match(
jbe/bsw@20 151 headers,
jbe/bsw@20 152 "\r?\n[Ll][Oo][Cc][Aa][Tt][Ii][Oo][Nn]:[ \t]*([^\r\n]+)"
jbe/bsw@20 153 )
jbe/bsw@20 154 if location then
jbe/bsw@20 155 -- If we are redirected too often, then return an error.
jbe/bsw@20 156 if redirects >= 10 then
jbe/bsw@20 157 return nil, "Too many redirects."
jbe/bsw@20 158 end
jbe/bsw@20 159 -- Otherwise follow the redirection by changing the variable "url"
jbe/bsw@20 160 -- and by incrementing the redirect counter.
jbe/bsw@20 161 url = location
jbe/bsw@20 162 redirects = redirects + 1
jbe/bsw@20 163 else
jbe/bsw@20 164 -- Check, if there is an X-XRDS-Location header
jbe/bsw@20 165 -- pointing to an XRDS document:
jbe/bsw@20 166 local xrds_location = string.match(
jbe/bsw@20 167 headers,
jbe/bsw@20 168 "\r?\n[Xx]%-[Xx][Rr][Dd][Ss]%-[Ll][Oo][Cc][Aa][Tt][Ii][Oo][Nn]:[ \t]*([^\r\n]+)"
jbe/bsw@20 169 )
jbe/bsw@20 170 -- If there is no X-XRDS-Location header, there might be an
jbe/bsw@20 171 -- http-equiv meta tag serving the same purpose:
jbe/bsw@20 172 if not xrds_location and status == 200 then
jbe/bsw@20 173 xrds_location = get_tag_value(body, "meta", "http-equiv", "X-XRDS-Location", "content")
jbe/bsw@20 174 end
jbe/bsw@20 175 if xrds_location then
jbe/bsw@20 176 -- If we know the XRDS-Location, we can fetch the XRDS document
jbe/bsw@20 177 -- from that location:
jbe/bsw@20 178 local status, headers, body = auth.openid._curl(xrds_location, args.curl_options)
jbe/bsw@20 179 if not status then
jbe/bsw@20 180 return nil, "XRDS document could not be loaded."
jbe/bsw@20 181 end
jbe/bsw@20 182 if status ~= 200 then
jbe/bsw@20 183 return nil, "XRDS document not found where expected."
jbe/bsw@20 184 end
jbe/bsw@20 185 xrds_document = body
jbe/bsw@20 186 break
jbe/bsw@20 187 elseif
jbe/bsw@20 188 -- If the Content-Type header is set accordingly, then we already
jbe/bsw@20 189 -- should have received an XRDS document:
jbe/bsw@20 190 string.find(
jbe/bsw@20 191 headers,
jbe/bsw@20 192 "\r?\n[Cc][Oo][Nn][Tt][Ee][Nn][Tt]%-[Tt][Yy][Pp][Ee]:[ \t]*application/xrds%+xml\r?\n"
jbe/bsw@20 193 )
jbe/bsw@20 194 then
jbe/bsw@20 195 if status ~= 200 then
jbe/bsw@20 196 return nil, "XRDS document announced but not found."
jbe/bsw@20 197 end
jbe/bsw@20 198 xrds_document = body
jbe/bsw@20 199 break
jbe/bsw@20 200 else
jbe/bsw@20 201 -- Otherwise we should have received an HTML document:
jbe/bsw@20 202 if status ~= 200 then
jbe/bsw@20 203 return nil, "No XRDS or HTML document found for discovery."
jbe/bsw@20 204 end
jbe/bsw@20 205 html_document = body
jbe/bsw@20 206 break;
jbe/bsw@20 207 end
jbe/bsw@20 208 end
jbe/bsw@20 209 end
jbe/bsw@20 210 local claimed_identifier -- OpenID identifier the user claims to own
jbe/bsw@20 211 local op_endpoint -- OpenID provider endpoint URL
jbe/bsw@20 212 local op_local_identifier -- optional user identifier, local to the OpenID provider
jbe/bsw@20 213 if xrds_document then
jbe/bsw@20 214 -- If we got an XRDS document, we look for a matching <Service> entry:
jbe/bsw@20 215 for content in tag_contents(xrds_document, "Service") do
jbe/bsw@20 216 local service_uri, service_localid
jbe/bsw@20 217 for content in tag_contents(content, "URI") do
jbe/bsw@20 218 if not string.find(content, "[<>]") then
jbe/bsw@20 219 service_uri = strip(content)
jbe/bsw@20 220 break
jbe/bsw@20 221 end
jbe/bsw@20 222 end
jbe/bsw@20 223 for content in tag_contents(content, "LocalID") do
jbe/bsw@20 224 if not string.find(content, "[<>]") then
jbe/bsw@20 225 service_localid = strip(content)
jbe/bsw@20 226 break
jbe/bsw@20 227 end
jbe/bsw@20 228 end
jbe/bsw@20 229 for content in tag_contents(content, "Type") do
jbe/bsw@20 230 if not string.find(content, "[<>]") then
jbe/bsw@20 231 local content = strip(content)
jbe/bsw@20 232 if content == "http://specs.openid.net/auth/2.0/server" then
jbe/bsw@20 233 -- The user entered a provider identifier, thus claimed_identifier
jbe/bsw@20 234 -- and op_local_identifier will be set to nil.
jbe/bsw@20 235 op_endpoint = service_uri
jbe/bsw@20 236 break
jbe/bsw@20 237 elseif content == "http://specs.openid.net/auth/2.0/signon" then
jbe/bsw@20 238 -- The user entered his/her own identifier.
jbe/bsw@20 239 claimed_identifier = url
jbe/bsw@20 240 op_endpoint = service_uri
jbe/bsw@20 241 op_local_identifier = service_localid
jbe/bsw@20 242 break
jbe/bsw@20 243 end
jbe/bsw@20 244 end
jbe/bsw@20 245 end
jbe/bsw@20 246 end
jbe/bsw@20 247 elseif html_document then
jbe/bsw@20 248 -- If we got an HTML document, we look for matching <link .../> tags:
jbe/bsw@20 249 claimed_identifier = url
jbe/bsw@20 250 op_endpoint = get_tag_value(
jbe/bsw@20 251 html_document,
jbe/bsw@20 252 "link", "rel", "openid2.provider", "href"
jbe/bsw@20 253 )
jbe/bsw@20 254 op_local_identifier = get_tag_value(
jbe/bsw@20 255 html_document,
jbe/bsw@20 256 "link", "rel", "openid2.local_id", "href"
jbe/bsw@20 257 )
jbe/bsw@20 258 else
jbe/bsw@20 259 error("Assertion failed") -- should not happen
jbe/bsw@20 260 end
jbe/bsw@20 261 if not op_endpoint then
jbe/bsw@20 262 return nil, "No OpenID endpoint found."
jbe/bsw@20 263 end
jbe/bsw@20 264 if claimed_identifier then
jbe/bsw@20 265 claimed_identifier = auth.openid._normalize_url(claimed_identifier)
jbe/bsw@20 266 if not claimed_identifier then
jbe/bsw@20 267 return nil, "Claimed identifier could not be normalized."
jbe/bsw@20 268 end
jbe/bsw@20 269 end
jbe/bsw@20 270 return {
jbe/bsw@20 271 claimed_identifier = claimed_identifier,
jbe/bsw@20 272 op_endpoint = op_endpoint,
jbe/bsw@20 273 op_local_identifier = op_local_identifier
jbe/bsw@20 274 }
jbe/bsw@20 275 end

Impressum / About Us