dbalatero-rpx_now 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/README.markdown ADDED
@@ -0,0 +1,95 @@
1
+ Problem
2
+ =======
3
+ - OpenID is complex
4
+ - OpenID is not universally used
5
+ - Facebook / Myspace / MS-LiveId / AOL connections require different libraries and knowledge
6
+ - Multiple heterogenouse providers are hard to map to a single user
7
+
8
+ Solution
9
+ ========
10
+ - Use [RPX](http://rpxnow.com) for universal and usable user login
11
+ - Use view/controller helpers for easy integration
12
+
13
+ Usage
14
+ =====
15
+ - Get an API key @ [RPX](http://rpxnow.com)
16
+ - Build login view
17
+ - Communicate with RPX API in controller to create or login User
18
+ - for more advanced features have a look at the [RPX API Docs](https://rpxnow.com/docs)
19
+
20
+ Install
21
+ =======
22
+ - As Rails plugin: `script/plugin install git://github.com/grosser/rpx_now.git `
23
+ - As gem: `sudo gem install grosser-rpx_now --source http://gems.github.com/`
24
+ - As gem from source: `git clone git://github.com/grosser/rpx_now.git`,`cd rpx_now && rake install`
25
+
26
+ Examples
27
+ ========
28
+
29
+ View
30
+ ----
31
+ #login.erb
32
+ #here 'mywebsite' is your subdomain/realm on RPX
33
+ <%=RPXNow.embed_code('mywebsite',rpx_token_sessions_url)%>
34
+ OR
35
+ <%=RPXNow.popup_code('Login here...','mywebsite',rpx_token_sessions_url,:language=>'de')%>
36
+
37
+ (`popup_code` can also be called with `:unobstrusive=>true`)
38
+
39
+ Controller
40
+ ----------
41
+ # simple: use defaults
42
+ # user_data returns e.g. {:name=>'John Doe',:email=>'john@doe.com',:identifier=>'blug.google.com/openid/dsdfsdfs3f3'}
43
+ # when no user_data was found (invalid token supplied), data is empty, you may want to handle that seperatly...
44
+ # your user model must have an identifier column
45
+ def rpx_token
46
+ data = RPXNow.user_data(params[:token],'YOUR RPX API KEY')
47
+ self.current_user = User.find_by_identifier(data[:identifier]) || User.create!(data)
48
+ redirect_to '/'
49
+ end
50
+
51
+ # process the raw response yourself:
52
+ RPXNow.user_data(params[:token],'YOUR RPX API KEY'){|raw| {:email=>raw['profile']['verifiedEmail']}}
53
+
54
+ # request extended parameters (most users and APIs do not supply them)
55
+ RPXNow.user_data(params[:token],'YOUR RPX API KEY',:extended=>'true'){|raw| ...have a look at the RPX API DOCS...}
56
+
57
+ # you can provide the api key once, and leave it out on all following calls
58
+ RPXNow.api_key = 'YOUR RPX API KEY'
59
+ RPXNow.user_data(params[:token],:extended=>'true')
60
+
61
+ Advanced
62
+ --------
63
+ ###Versions
64
+ The version of RPXNow api can be set globally:
65
+ RPXNow.api_version = 2
66
+ Or local on each call:
67
+ RPXNow.mappings(primary_key, :api_version=>1)
68
+
69
+ ###Mappings
70
+ You can map your primary keys (e.g. user.id) to identifiers, so that
71
+ users can login to the same account with multiple identifiers.
72
+ #add a mapping
73
+ RPXNow.map(identifier,primary_key,'YOUR RPX API KEY')
74
+
75
+ #remove a mapping
76
+ RPXNow.unmap(identifier,primary_key,'YOUR RPX API KEY')
77
+
78
+ #show mappings
79
+ RPXNow.mappings(primary_key,'YOUR RPX API KEY') # [identifier1,identifier2,...]
80
+
81
+ After a primary key is mapped to an identifier, when a user logs in with this identifier,
82
+ `RPXNow.user_data` will contain his `primaryKey` as `:id`.
83
+
84
+ TODO
85
+ ====
86
+ - validate RPXNow.com SSL certificate
87
+
88
+ Author
89
+ ======
90
+ ###Contributors
91
+ - [DBA](http://github.com/DBA)
92
+
93
+ [Michael Grosser](http://pragmatig.wordpress.com)
94
+ grosser.michael@gmail.com
95
+ Hereby placed under public domain, do what you want, just do not hold me accountable...
data/VERSION.yml ADDED
@@ -0,0 +1,4 @@
1
+ ---
2
+ :patch: 1
3
+ :major: 0
4
+ :minor: 4
data/lib/rpx_now.rb ADDED
@@ -0,0 +1,164 @@
1
+ require 'json'
2
+
3
+ module RPXNow
4
+ extend self
5
+
6
+ attr_writer :api_key
7
+ attr_accessor :api_version
8
+ self.api_version = 2
9
+
10
+ # retrieve the users data, or return nil when nothing could be read/token was invalid
11
+ # or data was not found
12
+ def user_data(token, *args)
13
+ api_key, version, options = extract_key_version_and_options!(args)
14
+ options = {:token=>token,:apiKey=>api_key}.merge options
15
+
16
+ begin
17
+ data = secure_json_post("https://rpxnow.com/api/v#{version}/auth_info", options)
18
+ rescue ServerError
19
+ return nil if $!.to_s =~ /token/ or $!.to_s=~/Data not found/
20
+ raise
21
+ end
22
+ if block_given? then yield(data) else read_user_data_from_response(data) end
23
+ end
24
+
25
+ # maps an identifier to an primary-key (e.g. user.id)
26
+ def map(identifier, primary_key, *args)
27
+ api_key, version, options = extract_key_version_and_options!(args)
28
+ options = {:identifier=>identifier,:primaryKey=>primary_key,:apiKey=>api_key}.merge options
29
+ secure_json_post("https://rpxnow.com/api/v#{version}/map", options)
30
+ end
31
+
32
+ # un-maps an identifier to an primary-key (e.g. user.id)
33
+ def unmap(identifier, primary_key, *args)
34
+ api_key, version, options = extract_key_version_and_options!(args)
35
+ options = {:identifier=>identifier,:primaryKey=>primary_key,:apiKey=>api_key}.merge options
36
+ secure_json_post("https://rpxnow.com/api/v#{version}/unmap", options)
37
+ end
38
+
39
+ # returns an array of identifiers which are mapped to one of your primary-keys (e.g. user.id)
40
+ def mappings(primary_key, *args)
41
+ api_key, version, options = extract_key_version_and_options!(args)
42
+ options = {:primaryKey=>primary_key,:apiKey=>api_key}.merge options
43
+ data = secure_json_post("https://rpxnow.com/api/v#{version}/mappings", options)
44
+ data['identifiers']
45
+ end
46
+
47
+ def embed_code(subdomain,url)
48
+ <<EOF
49
+ <iframe src="https://#{subdomain}.rpxnow.com/openid/embed?token_url=#{url}"
50
+ scrolling="no" frameBorder="no" style="width:400px;height:240px;">
51
+ </iframe>
52
+ EOF
53
+ end
54
+
55
+ def popup_code(text, subdomain, url, options = {})
56
+ if options[:unobtrusive]
57
+ unobtrusive_popup_code(text, subdomain, url, options)
58
+ else
59
+ obtrusive_popup_code(text, subdomain, url, options)
60
+ end
61
+ end
62
+
63
+ private
64
+
65
+ def unobtrusive_popup_code(text, subdomain, url, options={})
66
+ version = extract_version! options
67
+ "<a class=\"rpxnow\" href=\"https://#{subdomain}.rpxnow.com/openid/v#{version}/signin?token_url=#{url}\">#{text}</a>"
68
+ end
69
+
70
+ def obtrusive_popup_code(text, subdomain, url, options = {})
71
+ version = extract_version! options
72
+ <<EOF
73
+ <a class="rpxnow" onclick="return false;" href="https://#{subdomain}.rpxnow.com/openid/v#{version}/signin?token_url=#{url}">
74
+ #{text}
75
+ </a>
76
+ <script src="https://rpxnow.com/openid/v#{version}/widget" type="text/javascript"></script>
77
+ <script type="text/javascript">
78
+ //<![CDATA[
79
+ RPXNOW.token_url = "#{url}";
80
+
81
+ RPXNOW.realm = "#{subdomain}";
82
+ RPXNOW.overlay = true;
83
+ RPXNOW.language_preference = '#{options[:language]||'en'}';
84
+ //]]>
85
+ </script>
86
+ EOF
87
+ end
88
+
89
+ def extract_key_version_and_options!(args)
90
+ key, options = extract_key_and_options(args)
91
+ version = extract_version! options
92
+ [key, version, options]
93
+ end
94
+
95
+ # [API_KEY,{options}] or
96
+ # [{options}] or
97
+ # []
98
+ def extract_key_and_options(args)
99
+ if args.length == 2
100
+ [args[0],args[1]]
101
+ elsif args.length==1
102
+ if args[0].is_a? Hash then [@api_key,args[0]] else [args[0],{}] end
103
+ else
104
+ raise unless @api_key
105
+ [@api_key,{}]
106
+ end
107
+ end
108
+
109
+ def extract_version!(options)
110
+ options.delete(:api_version) || api_version
111
+ end
112
+
113
+ def read_user_data_from_response(response)
114
+ user_data = response['profile']
115
+ data = {}
116
+ data[:identifier] = user_data['identifier']
117
+ data[:email] = user_data['verifiedEmail'] || user_data['email']
118
+ data[:name] = user_data['displayName'] || user_data['preferredUsername'] || data[:email].sub(/@.*/,'')
119
+ data[:id] = user_data['primaryKey'] unless user_data['primaryKey'].to_s.empty?
120
+ data
121
+ end
122
+
123
+ def secure_json_post(url,data={})
124
+ data = JSON.parse(post(url,data))
125
+ raise ServerError.new(data['err']) if data['err']
126
+ raise ServerError.new(data.inspect) unless data['stat']=='ok'
127
+ data
128
+ end
129
+
130
+ def post(url,data)
131
+ require 'net/http'
132
+ url = URI.parse(url)
133
+ http = Net::HTTP.new(url.host, url.port)
134
+ if url.scheme == 'https'
135
+ require 'net/https'
136
+ http.use_ssl = true
137
+ #TODO do we really want to verify the certificate? http://notetoself.vrensk.com/2008/09/verified-https-in-ruby/
138
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
139
+ end
140
+ resp, data = http.post(url.path, to_query(data))
141
+ raise "POST FAILED:"+resp.inspect unless resp.is_a? Net::HTTPOK
142
+ data
143
+ end
144
+
145
+ def to_query(data = {})
146
+ return data.to_query if Hash.respond_to? :to_query
147
+ return "" if data.empty?
148
+
149
+ #simpler to_query
150
+ query_data = []
151
+ data.each do |k, v|
152
+ query_data << "#{k}=#{v}"
153
+ end
154
+
155
+ return query_data.join('&')
156
+ end
157
+
158
+ class ServerError < Exception
159
+ #to_s returns message(which is a hash...)
160
+ def to_s
161
+ super.to_s
162
+ end
163
+ end
164
+ end
@@ -0,0 +1,206 @@
1
+ require File.expand_path("spec_helper", File.dirname(__FILE__))
2
+
3
+ API_KEY = '4b339169026742245b754fa338b9b0aebbd0a733'
4
+ API_VERSION = RPXNow.api_version
5
+
6
+ describe RPXNow do
7
+ before do
8
+ RPXNow.api_key = nil
9
+ RPXNow.api_version = API_VERSION
10
+ end
11
+
12
+ describe :api_key= do
13
+ it "stores the api key, so i do not have to supply everytime" do
14
+ RPXNow.api_key='XX'
15
+ RPXNow.expects(:post).with{|x,data|data[:apiKey]=='XX'}.returns %Q({"stat":"ok"})
16
+ RPXNow.mappings(1)
17
+ end
18
+ end
19
+
20
+ describe :api_version= do
21
+ it "can be set to a api_version globally" do
22
+ RPXNow.api_version = 5
23
+ RPXNow.popup_code('x','y','z').should =~ %r(/openid/v5/signin)
24
+ end
25
+ end
26
+
27
+ describe :embed_code do
28
+ it "contains the subdomain" do
29
+ RPXNow.embed_code('xxx','my_url').should =~ /xxx/
30
+ end
31
+
32
+ it "contains the url" do
33
+ RPXNow.embed_code('xxx','my_url').should =~ /token_url=my_url/
34
+ end
35
+ end
36
+
37
+ describe :popup_code do
38
+ it "defaults to obtrusive output" do
39
+ RPXNow.popup_code('sign on', 'subdomain', 'http://fake.domain.com/').should =~ /script src=/
40
+ end
41
+
42
+ it "can build an unobtrusive widget with specific version" do
43
+ expected = %Q(<a class="rpxnow" href="https://subdomain.rpxnow.com/openid/v300/signin?token_url=http://fake.domain.com/">sign on</a>)
44
+ RPXNow.popup_code('sign on', 'subdomain', 'http://fake.domain.com/', { :unobtrusive => true, :api_version => 300 }).should == expected
45
+ end
46
+
47
+ it "allows to specify the version of the widget" do
48
+ RPXNow.popup_code('x','y','z', :api_version => 300).should =~ %r(/openid/v300/signin)
49
+ end
50
+
51
+ it "defaults to widget version 2" do
52
+ RPXNow.popup_code('x','y','z').should =~ %r(/openid/v2/signin)
53
+ end
54
+
55
+ it "defaults to english" do
56
+ RPXNow.popup_code('x','y','z').should =~ /RPXNOW.language_preference = 'en'/
57
+ end
58
+
59
+ it "has a changeable language" do
60
+ RPXNow.popup_code('x','y','z',:language=>'de').should =~ /RPXNOW.language_preference = 'de'/
61
+ end
62
+ end
63
+
64
+ describe :user_data do
65
+ def fake_response
66
+ %Q({"profile":{"verifiedEmail":"grosser.michael@googlemail.com","displayName":"Michael Grosser","preferredUsername":"grosser.michael","identifier":"https:\/\/www.google.com\/accounts\/o8\/id?id=AItOawmaOlyYezg_WfbgP_qjaUyHjmqZD9qNIVM","email":"grosser.michael@gmail.com"},"stat":"ok"})
67
+ end
68
+
69
+ it "is empty when used with an invalid token" do
70
+ RPXNow.user_data('xxxx',API_KEY).should == nil
71
+ end
72
+
73
+ it "is empty when used with an unknown token" do
74
+ RPXNow.user_data('60d8c6374f4e9d290a7b55f39da7cc6435aef3d3',API_KEY).should == nil
75
+ end
76
+
77
+ it "parses JSON response to user data" do
78
+ RPXNow.expects(:post).returns fake_response
79
+ RPXNow.user_data('','x').should == {:name=>'Michael Grosser',:email=>'grosser.michael@googlemail.com',:identifier=>"https://www.google.com/accounts/o8/id?id=AItOawmaOlyYezg_WfbgP_qjaUyHjmqZD9qNIVM"}
80
+ end
81
+
82
+ it "adds a :id when primaryKey was returned" do
83
+ RPXNow.expects(:post).returns fake_response.sub(%Q("verifiedEmail"), %Q("primaryKey":"2","verifiedEmail"))
84
+ RPXNow.user_data('','x')[:id].should == '2'
85
+ end
86
+
87
+ it "handles primaryKeys that are not numeric" do
88
+ RPXNow.expects(:post).returns fake_response.sub(%Q("verifiedEmail"), %Q("primaryKey":"dbalatero","verifiedEmail"))
89
+ RPXNow.user_data('','x')[:id].should == 'dbalatero'
90
+ end
91
+
92
+ it "hands JSON response to supplied block" do
93
+ RPXNow.expects(:post).returns %Q({"x":"1","stat":"ok"})
94
+ response = nil
95
+ RPXNow.user_data('','x'){|data| response = data}
96
+ response.should == {"x" => "1", "stat" => "ok"}
97
+ end
98
+
99
+ it "returns what the supplied block returned" do
100
+ RPXNow.expects(:post).returns %Q({"x":"1","stat":"ok"})
101
+ RPXNow.user_data('','x'){|data| "x"}.should == 'x'
102
+ end
103
+
104
+ it "can send additional parameters" do
105
+ RPXNow.expects(:post).with{|url,data|
106
+ data[:extended].should == 'true'
107
+ }.returns fake_response
108
+ RPXNow.user_data('','x',:extended=>'true')
109
+ end
110
+ end
111
+
112
+ describe :read_user_data_from_response do
113
+ it "reads secondary names" do
114
+ RPXNow.send(:read_user_data_from_response,{'profile'=>{'preferredUsername'=>'1'}})[:name].should == '1'
115
+ end
116
+
117
+ it "parses email when no name is found" do
118
+ RPXNow.send(:read_user_data_from_response,{'profile'=>{'email'=>'1@xxx.com'}})[:name].should == '1'
119
+ end
120
+ end
121
+
122
+ describe :secure_json_post do
123
+ it "parses json when status is ok" do
124
+ RPXNow.expects(:post).returns %Q({"stat":"ok","data":"xx"})
125
+ RPXNow.send(:secure_json_post, %Q("yy"))['data'].should == "xx"
126
+ end
127
+
128
+ it "raises when there is a communication error" do
129
+ RPXNow.expects(:post).returns %Q({"err":"wtf","stat":"ok"})
130
+ lambda{RPXNow.send(:secure_json_post,'xx')}.should raise_error RPXNow::ServerError
131
+ end
132
+
133
+ it "raises when status is not ok" do
134
+ RPXNow.expects(:post).returns %Q({"stat":"err"})
135
+ lambda{RPXNow.send(:secure_json_post,'xx')}.should raise_error RPXNow::ServerError
136
+ end
137
+ end
138
+
139
+ describe :mappings do
140
+ it "parses JSON response to unmap data" do
141
+ RPXNow.expects(:post).returns %Q({"stat":"ok", "identifiers": ["http://test.myopenid.com/"]})
142
+ RPXNow.mappings(1, "x").should == ["http://test.myopenid.com/"]
143
+ end
144
+ end
145
+
146
+ describe :map do
147
+ it "adds a mapping" do
148
+ RPXNow.expects(:post).returns %Q({"stat":"ok"})
149
+ RPXNow.map('http://test.myopenid.com',1, API_KEY)
150
+ end
151
+ end
152
+
153
+ describe :unmap do
154
+ it "unmaps a indentifier" do
155
+ RPXNow.expects(:post).returns %Q({"stat":"ok"})
156
+ RPXNow.unmap('http://test.myopenid.com', 1, "x")
157
+ end
158
+
159
+ it "can be called with a specific version" do
160
+ RPXNow.expects(:secure_json_post).with{|a,b|a == "https://rpxnow.com/api/v300/unmap"}
161
+ RPXNow.unmap('http://test.myopenid.com', 1, :api_key=>'xxx', :api_version=>300)
162
+ end
163
+ end
164
+
165
+ describe :mapping_integration do
166
+ before do
167
+ @k1 = 'http://test.myopenid.com'
168
+ RPXNow.unmap(@k1, 1, API_KEY)
169
+ @k2 = 'http://test-2.myopenid.com'
170
+ RPXNow.unmap(@k2, 1, API_KEY)
171
+ end
172
+
173
+ it "has no mappings when nothing was mapped" do
174
+ RPXNow.mappings(1,API_KEY).should == []
175
+ end
176
+
177
+ it "unmaps mapped keys" do
178
+ RPXNow.map(@k2, 1, API_KEY)
179
+ RPXNow.unmap(@k2, 1, API_KEY)
180
+ RPXNow.mappings(1, API_KEY).should == []
181
+ end
182
+
183
+ it "maps keys to a primary key and then retrieves them" do
184
+ RPXNow.map(@k1, 1, API_KEY)
185
+ RPXNow.map(@k2, 1, API_KEY)
186
+ RPXNow.mappings(1,API_KEY).sort.should == [@k2,@k1]
187
+ end
188
+
189
+ it "does not add duplicate mappings" do
190
+ RPXNow.map(@k1, 1, API_KEY)
191
+ RPXNow.map(@k1, 1, API_KEY)
192
+ RPXNow.mappings(1,API_KEY).should == [@k1]
193
+ end
194
+ end
195
+
196
+ describe :to_query do
197
+ it "should not depend on active support" do
198
+ RPXNow.send('to_query', {:one => " abc"}).should == "one= abc"
199
+ end
200
+
201
+ it "should use ActiveSupport core extensions" do
202
+ require 'activesupport'
203
+ RPXNow.send('to_query', {:one => " abc"}).should == "one=+abc"
204
+ end
205
+ end
206
+ end
@@ -0,0 +1,36 @@
1
+ # ---- requirements
2
+ require 'rubygems'
3
+ require 'spec'
4
+ require 'mocha'
5
+
6
+ $LOAD_PATH << File.expand_path("../lib", File.dirname(__FILE__))
7
+
8
+
9
+ # ---- rspec
10
+ Spec::Runner.configure do |config|
11
+ config.mock_with :mocha
12
+ end
13
+
14
+
15
+ # ---- bugfix
16
+ #`exit?': undefined method `run?' for Test::Unit:Module (NoMethodError)
17
+ #can be solved with require test/unit but this will result in extra test-output
18
+ module Test
19
+ module Unit
20
+ def self.run?
21
+ true
22
+ end
23
+ end
24
+ end
25
+
26
+
27
+ # ---- setup environment/plugin
28
+ require File.expand_path("../init", File.dirname(__FILE__))
29
+
30
+
31
+ # ---- Helpers
32
+ def pending_it(text,&block)
33
+ it text do
34
+ pending(&block)
35
+ end
36
+ end
metadata ADDED
@@ -0,0 +1,67 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: dbalatero-rpx_now
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.4.1
5
+ platform: ruby
6
+ authors:
7
+ - Michael Grosser
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-04-15 00:00:00 -07:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: activesupport
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: "0"
24
+ version:
25
+ description:
26
+ email: grosser.michael@gmail.com
27
+ executables: []
28
+
29
+ extensions: []
30
+
31
+ extra_rdoc_files: []
32
+
33
+ files:
34
+ - VERSION.yml
35
+ - README.markdown
36
+ - lib/rpx_now.rb
37
+ - spec/rpx_now_spec.rb
38
+ - spec/spec_helper.rb
39
+ has_rdoc: true
40
+ homepage: http://github.com/grosser/rpx_now
41
+ post_install_message:
42
+ rdoc_options:
43
+ - --inline-source
44
+ - --charset=UTF-8
45
+ require_paths:
46
+ - lib
47
+ required_ruby_version: !ruby/object:Gem::Requirement
48
+ requirements:
49
+ - - ">="
50
+ - !ruby/object:Gem::Version
51
+ version: "0"
52
+ version:
53
+ required_rubygems_version: !ruby/object:Gem::Requirement
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ version: "0"
58
+ version:
59
+ requirements: []
60
+
61
+ rubyforge_project:
62
+ rubygems_version: 1.2.0
63
+ signing_key:
64
+ specification_version: 2
65
+ summary: Helper to simplify RPX Now user login/creation
66
+ test_files: []
67
+