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, "<", '<') 1.20 + str = string.gsub(value, ">", '>') 1.21 + str = string.gsub(value, """, '"') 1.22 + str = string.gsub(value, "&", '&') 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