521 lines
13 KiB
Plaintext
521 lines
13 KiB
Plaintext
note
|
|
description: "[
|
|
Light implementation of {OPENID} consumer.
|
|
|
|
Sign-on with OpenID is a two step process:
|
|
]"
|
|
author: ""
|
|
date: "$Date$"
|
|
revision: "$Revision$"
|
|
|
|
class
|
|
OPENID_CONSUMER
|
|
|
|
create
|
|
make
|
|
|
|
feature {NONE} -- Initialization
|
|
|
|
make (a_server: READABLE_STRING_8)
|
|
do
|
|
trusted_root := a_server
|
|
return_url := a_server
|
|
create required_info.make (0)
|
|
create optional_info.make (0)
|
|
end
|
|
|
|
feature -- Access
|
|
|
|
trusted_root: READABLE_STRING_8
|
|
|
|
return_url: READABLE_STRING_8
|
|
|
|
required_info: ARRAYED_LIST [READABLE_STRING_8]
|
|
optional_info: ARRAYED_LIST [READABLE_STRING_8]
|
|
|
|
error: detachable READABLE_STRING_8
|
|
|
|
has_error: BOOLEAN
|
|
do
|
|
Result := error /= Void
|
|
end
|
|
|
|
feature -- Change
|
|
|
|
ask_all_info (is_required: BOOLEAN)
|
|
do
|
|
across
|
|
ax_to_sreg_map as c
|
|
loop
|
|
ask_info (c.key, is_required)
|
|
end
|
|
end
|
|
|
|
ask_required_info (s: READABLE_STRING_8)
|
|
do
|
|
required_info.force (s)
|
|
end
|
|
|
|
ask_optional_info (s: READABLE_STRING_8)
|
|
do
|
|
optional_info.force (s)
|
|
end
|
|
|
|
ask_info (s: READABLE_STRING_8; is_required: BOOLEAN)
|
|
do
|
|
if is_required then
|
|
ask_required_info (s)
|
|
else
|
|
ask_optional_info (s)
|
|
end
|
|
end
|
|
|
|
ask_nickname (is_required: BOOLEAN)
|
|
do
|
|
ask_info ("namePerson/friendly", is_required)
|
|
end
|
|
|
|
ask_email (is_required: BOOLEAN)
|
|
do
|
|
ask_info ("contact/email", is_required)
|
|
end
|
|
|
|
ask_fullname (is_required: BOOLEAN)
|
|
do
|
|
ask_info ("namePerson", is_required)
|
|
end
|
|
|
|
ask_birthdate (is_required: BOOLEAN)
|
|
do
|
|
ask_info ("birthDate", is_required)
|
|
end
|
|
|
|
ask_gender (is_required: BOOLEAN)
|
|
do
|
|
ask_info ("person/gender", is_required)
|
|
end
|
|
|
|
ask_postcode (is_required: BOOLEAN)
|
|
do
|
|
ask_info ("contact/postalCode/home", is_required)
|
|
end
|
|
|
|
ask_country (is_required: BOOLEAN)
|
|
do
|
|
ask_info ("contact/country/home", is_required)
|
|
end
|
|
|
|
ask_language (is_required: BOOLEAN)
|
|
do
|
|
ask_info ("pref/language", is_required)
|
|
end
|
|
|
|
ask_timezone (is_required: BOOLEAN)
|
|
do
|
|
ask_info ("pref/timezone", is_required)
|
|
end
|
|
|
|
feature -- Query
|
|
|
|
auth_url (a_identity: READABLE_STRING_8): detachable READABLE_STRING_8
|
|
do
|
|
error := Void
|
|
if attached discovering_info (a_identity) as d_info then
|
|
if d_info.version = 2 then
|
|
Result := auth_url_v2 (a_identity, d_info)
|
|
else
|
|
Result := auth_url_v1 (a_identity, d_info) -- FIXME for claimed_id
|
|
end
|
|
end
|
|
end
|
|
|
|
feature {OPENID_CONSUMER_VALIDATION} -- Implementation
|
|
|
|
discovering_info (id: READABLE_STRING_8): detachable OPENID_DISCOVER
|
|
local
|
|
|
|
sess: HTTP_CLIENT_SESSION
|
|
ctx: detachable HTTP_CLIENT_REQUEST_CONTEXT
|
|
xrds_location: detachable READABLE_STRING_8
|
|
xml: XML_STANDARD_PARSER
|
|
tree: XML_CALLBACKS_DOCUMENT
|
|
xelt: detachable XML_ELEMENT
|
|
s: READABLE_STRING_32
|
|
r_uri: detachable READABLE_STRING_8
|
|
r_err: BOOLEAN
|
|
r_delegate: detachable READABLE_STRING_8
|
|
r_sreg_supported, r_ax_supported, r_identifier_select: BOOLEAN
|
|
r_version: INTEGER
|
|
l_xrds_content: detachable READABLE_STRING_8
|
|
do
|
|
sess := new_session (id)
|
|
if attached sess.head ("", ctx) as rep then
|
|
if rep.error_occurred then
|
|
report_error ("Unable get answer from openid provider at " + rep.url)
|
|
else
|
|
if
|
|
attached rep.header ("Content-Type") as l_content_type and then
|
|
l_content_type.has_substring ("application/xrds+xml") and then
|
|
attached sess.get ("", ctx) as l_getres
|
|
then
|
|
l_xrds_content := l_getres.body
|
|
elseif attached rep.header ("X-XRDS-Location") as loc then
|
|
xrds_location := loc
|
|
else
|
|
report_error ("Failed (probably %""+ id +"%" is an invalid openid identifier).")
|
|
end
|
|
end
|
|
end
|
|
if l_xrds_content = Void and xrds_location /= Void then
|
|
sess := new_session (xrds_location)
|
|
if attached sess.get ("", ctx) as rep then
|
|
if rep.error_occurred then
|
|
r_err := True
|
|
report_error ("Can not get " + rep.url)
|
|
elseif attached rep.body as l_content then
|
|
l_xrds_content := l_content
|
|
else
|
|
r_err := True
|
|
report_error ("No content: " + rep.url)
|
|
end
|
|
end
|
|
end
|
|
if l_xrds_content = Void then
|
|
r_err := True
|
|
report_error ("Unable to get the XRDS message.")
|
|
else
|
|
create xml.make
|
|
create tree.make_null
|
|
xml.set_callbacks (tree)
|
|
xml.parse_from_string (l_xrds_content)
|
|
if attached tree.document as xrds then
|
|
xelt := Void
|
|
xelt := xrds.elements.first
|
|
xelt := xelt.elements.first
|
|
if attached xelt as l_xrd then
|
|
if attached l_xrd.elements_by_name ("Service") as l_services then
|
|
across
|
|
l_services as c
|
|
until
|
|
r_uri /= Void
|
|
loop
|
|
if attached c.item.elements_by_name ("Type") as l_types then
|
|
across
|
|
l_types as t
|
|
loop
|
|
s := xml_content (t.item)
|
|
if s.same_string_general ("http://openid.net/sreg/1.0") then
|
|
r_sreg_supported := True
|
|
elseif s.same_string_general ("http://openid.net/extensions/sreg/1.1") then
|
|
r_sreg_supported := True
|
|
elseif s.same_string_general ("http://openid.net/srv/ax/1.0") then
|
|
r_ax_supported := True
|
|
elseif s.same_string_general ("http://specs.openid.net/auth/2.0/signon") then
|
|
r_version := 2
|
|
elseif s.same_string_general ("http://specs.openid.net/auth/2.0/server") then
|
|
r_version := 2
|
|
r_identifier_select := True
|
|
elseif s.same_string_general ("http://openid.net/signon/1.1") then
|
|
r_version := 1
|
|
end
|
|
end
|
|
end
|
|
if attached c.item.element_by_name ("URI") as l_uri then
|
|
r_uri := xml_content (l_uri)
|
|
end
|
|
if r_version = 1 then
|
|
if attached c.item.element_by_name ("openid:Delegate") as l_id then
|
|
r_delegate := xml_content (l_id)
|
|
end
|
|
if attached c.item.element_by_name ("LocalID") as l_id then
|
|
r_delegate := xml_content (l_id)
|
|
end
|
|
else
|
|
if attached c.item.element_by_name ("CanonicalID") as l_id then
|
|
r_delegate := xml_content (l_id)
|
|
end
|
|
if attached c.item.element_by_name ("LocalID") as l_id then
|
|
r_delegate := xml_content (l_id)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
end
|
|
if r_uri /= Void then
|
|
create Result.make (r_uri, r_version)
|
|
if r_delegate = Void then
|
|
r_delegate := id
|
|
end
|
|
Result.delegate := r_delegate
|
|
Result.ax_supported := r_ax_supported
|
|
Result.sreg_supported := r_sreg_supported
|
|
Result.identifier_select := r_identifier_select
|
|
Result.has_error := r_err
|
|
end
|
|
end
|
|
|
|
feature {NONE} -- Implementation
|
|
|
|
auth_url_v1 (a_id: READABLE_STRING_8; a_info: OPENID_DISCOVER): READABLE_STRING_8
|
|
local
|
|
u: URI
|
|
ret: URI
|
|
do
|
|
create u.make_from_string (a_info.server_uri)
|
|
create ret.make_from_string (return_url)
|
|
if
|
|
attached a_info.delegate as l_claimed_id and then
|
|
not a_id.same_string (l_claimed_id)
|
|
then
|
|
ret.add_query_parameter ("openid.claimed_id", l_claimed_id)
|
|
end
|
|
|
|
u.add_query_parameter ("openid.return_to", ret.string)
|
|
u.add_query_parameter ("openid.mode", "checkid_setup") -- or "checkid_immediate"
|
|
u.add_query_parameter ("openid.identity", a_id)
|
|
u.add_query_parameter ("openid.trust_root", trusted_root)
|
|
|
|
if a_info.sreg_supported then
|
|
add_sreg_parameters_to (u)
|
|
end
|
|
|
|
Result := u.string
|
|
end
|
|
|
|
auth_url_v2 (a_id: READABLE_STRING_8; a_info: OPENID_DISCOVER): READABLE_STRING_8
|
|
local
|
|
u: URI
|
|
do
|
|
create u.make_from_string (a_info.server_uri)
|
|
u.add_query_parameter ("openid.ns", "http://specs.openid.net/auth/2.0")
|
|
u.add_query_parameter ("openid.mode", "checkid_setup") -- or "checkid_immediate"
|
|
u.add_query_parameter ("openid.return_to", return_url)
|
|
u.add_query_parameter ("openid.realm", trusted_root)
|
|
|
|
if a_info.ax_supported then
|
|
add_ax_parameters_to (u)
|
|
end
|
|
if a_info.sreg_supported then
|
|
add_sreg_parameters_to (u)
|
|
end
|
|
if a_info.identifier_select then
|
|
u.add_query_parameter ("openid.identity", "http://specs.openid.net/auth/2.0/identifier_select")
|
|
u.add_query_parameter ("openid.claimed_id", "http://specs.openid.net/auth/2.0/identifier_select")
|
|
else
|
|
u.add_query_parameter ("openid.identity", a_id)
|
|
u.add_query_parameter ("openid.claimed_id", a_id) -- Fixme
|
|
end
|
|
|
|
Result := u.string
|
|
end
|
|
|
|
add_ax_parameters_to (a_uri: URI)
|
|
local
|
|
lst: ARRAYED_LIST [READABLE_STRING_8]
|
|
l_aliases: HASH_TABLE [READABLE_STRING_8, STRING_8]
|
|
l_counts: HASH_TABLE [INTEGER, STRING_8]
|
|
l_alias: READABLE_STRING_8
|
|
s: STRING
|
|
do
|
|
create lst.make (required_info.count + optional_info.count)
|
|
lst.append (required_info)
|
|
lst.append (optional_info)
|
|
if lst.count > 0 then
|
|
a_uri.add_query_parameter ("openid.ns.ax", "http://openid.net/srv/ax/1.0")
|
|
a_uri.add_query_parameter ("openid.ax.mode", "fetch_request");
|
|
|
|
create l_aliases.make (lst.count)
|
|
create l_counts.make (lst.count)
|
|
across
|
|
lst as c
|
|
loop
|
|
l_alias := ax_to_alias (c.item)
|
|
if l_aliases.has (l_alias) then
|
|
if attached l_counts.item (l_alias) as l_count then
|
|
l_counts.replace (l_count + 1, l_alias)
|
|
else
|
|
check has_alias: False end
|
|
l_counts.force (1, l_alias)
|
|
end
|
|
else
|
|
l_aliases.force ("http://axschema.org/" + c.item, l_alias)
|
|
l_counts.force (1, l_alias)
|
|
end
|
|
end
|
|
across
|
|
l_aliases as c
|
|
loop
|
|
a_uri.add_query_parameter ("openid.ax.type." + c.key, c.item)
|
|
end
|
|
across
|
|
l_counts as c
|
|
loop
|
|
if c.item > 1 then
|
|
a_uri.add_query_parameter ("openid.ax.count." + c.key, c.item.out)
|
|
end
|
|
end
|
|
-- required
|
|
create s.make_empty
|
|
across
|
|
required_info as c
|
|
loop
|
|
if not s.is_empty then
|
|
s.append_character (',')
|
|
end
|
|
s.append (ax_to_alias (c.item))
|
|
end
|
|
if not s.is_empty then
|
|
a_uri.add_query_parameter ("openid.ax.required", s)
|
|
end
|
|
-- optional
|
|
create s.make_empty
|
|
across
|
|
optional_info as c
|
|
loop
|
|
if not s.is_empty then
|
|
s.append_character (',')
|
|
end
|
|
s.append (ax_to_alias (c.item))
|
|
end
|
|
if not s.is_empty then
|
|
a_uri.add_query_parameter ("openid.ax.if_available", s)
|
|
end
|
|
end
|
|
end
|
|
|
|
ax_to_alias (n: READABLE_STRING_8): STRING_8
|
|
do
|
|
if attached ax_to_sreg (n) as s then
|
|
Result := s
|
|
else
|
|
Result := n.string
|
|
Result.replace_substring_all ("/", "_")
|
|
Result.replace_substring_all (".", "_")
|
|
end
|
|
end
|
|
|
|
add_sreg_parameters_to (a_uri: URI)
|
|
local
|
|
s: STRING
|
|
do
|
|
-- We always use SREG 1.1, even if the server is advertising only support for 1.0.
|
|
-- That's because it's fully backwards compatibile with 1.0, and some providers
|
|
-- advertise 1.0 even if they accept only 1.1. One such provider is myopenid.com
|
|
a_uri.add_query_parameter ("openid.ns.sreg", "http://openid.net/extensions/sreg/1.1")
|
|
if not required_info.is_empty then
|
|
create s.make_empty
|
|
across
|
|
required_info as c
|
|
loop
|
|
if attached ax_to_sreg (c.item) as sreg then
|
|
if not s.is_empty then
|
|
s.append_character (',')
|
|
end
|
|
s.append (sreg)
|
|
end
|
|
end
|
|
if not s.is_empty then
|
|
a_uri.add_query_parameter ("openid.sreg.required", s)
|
|
end
|
|
end
|
|
|
|
if not optional_info.is_empty then
|
|
create s.make_empty
|
|
across
|
|
optional_info as c
|
|
loop
|
|
if attached ax_to_sreg (c.item) as sreg then
|
|
if not s.is_empty then
|
|
s.append_character (',')
|
|
end
|
|
s.append (sreg)
|
|
end
|
|
end
|
|
if not s.is_empty then
|
|
a_uri.add_query_parameter ("openid.sreg.optional", s)
|
|
end
|
|
end
|
|
end
|
|
|
|
ax_to_sreg_map: HASH_TABLE [READABLE_STRING_8, STRING_8]
|
|
once
|
|
create Result.make (7)
|
|
Result.compare_objects
|
|
Result.force ("nickname", "namePerson/friendly")
|
|
Result.force ("email", "contact/email")
|
|
Result.force ("fullname", "namePerson")
|
|
Result.force ("dob", "birthDate")
|
|
Result.force ("gender", "person/gender")
|
|
Result.force ("postcode", "contact/postalCode/home")
|
|
Result.force ("country", "contact/country/home")
|
|
Result.force ("language", "pref/language")
|
|
Result.force ("timezone", "pref/timezone")
|
|
|
|
-- -- extension
|
|
-- Result.force ("firstname", "namePerson/first")
|
|
-- Result.force ("lastname", "namePerson/last")
|
|
end
|
|
|
|
ax_to_sreg (n: READABLE_STRING_8): detachable READABLE_STRING_8
|
|
do
|
|
if attached ax_to_sreg_map.item (n) as v then
|
|
Result := v
|
|
end
|
|
end
|
|
|
|
sreg_to_ax (n: READABLE_STRING_8): detachable READABLE_STRING_8
|
|
do
|
|
if ax_to_sreg_map.has_item (n) and then
|
|
attached ax_to_sreg_map.found_item as v
|
|
then
|
|
Result := v
|
|
end
|
|
end
|
|
|
|
report_error (m: READABLE_STRING_8)
|
|
local
|
|
err: like error
|
|
do
|
|
err := error
|
|
if err = Void then
|
|
error := m
|
|
else
|
|
error := err + "%N" + m
|
|
end
|
|
debug
|
|
print (m)
|
|
end
|
|
ensure
|
|
has_error
|
|
end
|
|
|
|
feature -- Helper
|
|
|
|
xml_content (e: XML_ELEMENT): STRING_32
|
|
do
|
|
create Result.make_empty
|
|
if attached e.contents as lst then
|
|
across
|
|
lst as c
|
|
loop
|
|
Result.append (c.item.content)
|
|
end
|
|
end
|
|
end
|
|
|
|
new_session (a_uri: READABLE_STRING_8): HTTP_CLIENT_SESSION
|
|
local
|
|
cl: DEFAULT_HTTP_CLIENT
|
|
do
|
|
create cl
|
|
Result := cl.new_session (a_uri)
|
|
Result.set_is_insecure (True)
|
|
Result.set_max_redirects (5)
|
|
Result.add_header ("Accept", "application/xrds+xml, */*")
|
|
end
|
|
|
|
end
|