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, "<", '<') 3.20 + str = string.gsub(value, ">", '>') 3.21 + str = string.gsub(value, """, '"') 3.22 + str = string.gsub(value, "&", '&') 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