oauth20 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ lib/**/*.rb
2
+ bin/*
3
+ -
4
+ features/**/*.feature
5
+ LICENSE.txt
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
data/Gemfile ADDED
@@ -0,0 +1,14 @@
1
+ source "http://rubygems.org"
2
+
3
+ group :development do
4
+ gem "rspec", "~> 2.3.0"
5
+ gem "bundler", "~> 1.0.0"
6
+ gem "jeweler", "~> 1.6.4"
7
+ gem "rcov", ">= 0"
8
+ gem 'shoulda'
9
+ gem 'mocha'
10
+ end
11
+
12
+ gem 'timecop'
13
+ gem 'json'
14
+ gem 'redis'
data/Gemfile.lock ADDED
@@ -0,0 +1,40 @@
1
+ GEM
2
+ remote: http://rubygems.org/
3
+ specs:
4
+ diff-lcs (1.1.2)
5
+ git (1.2.5)
6
+ jeweler (1.6.4)
7
+ bundler (~> 1.0)
8
+ git (>= 1.2.5)
9
+ rake
10
+ json (1.6.1)
11
+ metaclass (0.0.1)
12
+ mocha (0.10.0)
13
+ metaclass (~> 0.0.1)
14
+ rake (0.8.7)
15
+ rcov (0.9.9)
16
+ redis (2.2.2)
17
+ rspec (2.3.0)
18
+ rspec-core (~> 2.3.0)
19
+ rspec-expectations (~> 2.3.0)
20
+ rspec-mocks (~> 2.3.0)
21
+ rspec-core (2.3.1)
22
+ rspec-expectations (2.3.0)
23
+ diff-lcs (~> 1.1.2)
24
+ rspec-mocks (2.3.0)
25
+ shoulda (2.11.3)
26
+ timecop (0.3.5)
27
+
28
+ PLATFORMS
29
+ ruby
30
+
31
+ DEPENDENCIES
32
+ bundler (~> 1.0.0)
33
+ jeweler (~> 1.6.4)
34
+ json
35
+ mocha
36
+ rcov
37
+ redis
38
+ rspec (~> 2.3.0)
39
+ shoulda
40
+ timecop
data/LICENSE.txt ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2011 Petr Janda
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,19 @@
1
+ = oauth20
2
+
3
+ Description goes here.
4
+
5
+ == Contributing to oauth20
6
+
7
+ * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet
8
+ * Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it
9
+ * Fork the project
10
+ * Start a feature/bugfix branch
11
+ * Commit and push until you are happy with your contribution
12
+ * Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
13
+ * Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
14
+
15
+ == Copyright
16
+
17
+ Copyright (c) 2011 Petr Janda. See LICENSE.txt for
18
+ further details.
19
+
data/Rakefile ADDED
@@ -0,0 +1,52 @@
1
+ # encoding: utf-8
2
+
3
+ require 'rubygems'
4
+ require 'bundler'
5
+ begin
6
+ Bundler.setup(:default, :development)
7
+ rescue Bundler::BundlerError => e
8
+ $stderr.puts e.message
9
+ $stderr.puts "Run `bundle install` to install missing gems"
10
+ exit e.status_code
11
+ end
12
+ require 'rake'
13
+
14
+ require 'jeweler'
15
+ Jeweler::Tasks.new do |gem|
16
+ # gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
17
+ gem.name = "oauth20"
18
+ gem.homepage = "http://github.com/petrjanda/oauth20"
19
+ gem.license = "MIT"
20
+ gem.summary = "OAuth 2.0"
21
+ gem.description = "OAuth 2.0"
22
+ gem.email = "petrjanda@me.com"
23
+ gem.authors = ["Petr Janda"]
24
+ gem.add_dependency 'timecop'
25
+ gem.add_dependency 'json'
26
+ gem.add_dependency 'redis'
27
+ # dependencies defined in Gemfile
28
+ end
29
+ Jeweler::RubygemsDotOrgTasks.new
30
+
31
+ require 'rspec/core'
32
+ require 'rspec/core/rake_task'
33
+ RSpec::Core::RakeTask.new(:spec) do |spec|
34
+ spec.pattern = FileList['spec/**/*_spec.rb']
35
+ end
36
+
37
+ RSpec::Core::RakeTask.new(:rcov) do |spec|
38
+ spec.pattern = 'spec/**/*_spec.rb'
39
+ spec.rcov = true
40
+ end
41
+
42
+ task :default => :spec
43
+
44
+ require 'rake/rdoctask'
45
+ Rake::RDocTask.new do |rdoc|
46
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
47
+
48
+ rdoc.rdoc_dir = 'rdoc'
49
+ rdoc.title = "oauth20 #{version}"
50
+ rdoc.rdoc_files.include('README*')
51
+ rdoc.rdoc_files.include('lib/**/*.rb')
52
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.1
@@ -0,0 +1,71 @@
1
+ module OAuth2
2
+
3
+ # Access token represents aceess created on behalf of specific user
4
+ # which first successfully authenticated with authroization server.
5
+ # It can be used to access resource server protected resources
6
+ # until it expires.
7
+ #
8
+ class AccessToken
9
+ attr_reader :client_key, :user_id, :expires_in, :expires_at, :created_at,
10
+ :scope, :key, :token_type
11
+
12
+ # Default timeout in seconds for access token expiration.
13
+ EXPIRES_IN = 3600
14
+
15
+ # Beared token type.
16
+ TYPE_BEARED = 'Beared'
17
+
18
+ # Initialize new access token instance with given attributes.
19
+ #
20
+ # @param [OAuth2::Client] Client for which the token is created.
21
+ # @param [OAuth2::User] User on which behalf token is created.
22
+ # @param [Hash] Hash of additional options.
23
+ #
24
+ def initialize(data)
25
+ @client_key = data[:client_key]
26
+ @user_id = data[:user_id]
27
+ @expires_in = data[:expires_in] || EXPIRES_IN
28
+ @created_at = data[:created_at] || Time.now
29
+ @expires_at = data[:expires_at] || Time.now + EXPIRES_IN
30
+ @scope = data[:scope]
31
+ @key = data[:key] || OAuth2::Utils.generate_key
32
+ @token_type = data[:token_type] || TYPE_BEARED
33
+ end
34
+
35
+ # Check if the access token is valid to be used to access protected
36
+ # resource on the server.
37
+ #
38
+ def expired?
39
+ Time.now >= @expires_at
40
+ end
41
+
42
+ # Revoke the access token. Its no longer valid to be used to access protected
43
+ # resources.
44
+ #
45
+ def revoke!
46
+ @expires_at = Time.now
47
+ save
48
+ end
49
+
50
+ # Return the token description data according to oauth protocol specification.
51
+ # Rest of token attributes is avoided.
52
+ #
53
+ def to_json
54
+ data = {
55
+ 'access_token' => @key,
56
+ 'expires_in' => @expires_in,
57
+ 'token_type' => @token_type
58
+ }
59
+
60
+ data.to_json
61
+ end
62
+
63
+ def self.find_by_key(key)
64
+ Storage.instance.access_token_find_by_key(key)
65
+ end
66
+
67
+ def save
68
+ Storage.instance.access_token_save(self)
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,50 @@
1
+ module OAuth2
2
+
3
+ # Authorization code class represents short-lived authorization for user
4
+ # which should be exchanged for access token until it expires.
5
+ #
6
+ class AuthCode
7
+ attr_reader :client_key, :user_id, :key, :created_at, :expires_at, :access_token
8
+
9
+ # Expires in 10 minutes.
10
+ EXPIRES_IN = 10 * 60
11
+
12
+ # Initialize new authorization code.
13
+ #
14
+ # @param [OAuth2::User] User which signed authorization request.
15
+ # @param [OAuth2::Client] Client used to get the auth code.
16
+ #
17
+ def initialize(data)
18
+ @key = data[:key]
19
+ @created_at = data[:created_at] || Time.now
20
+ @expires_at = data[:expires_at] || Time.now + EXPIRES_IN
21
+ @access_token = data[:access_token]
22
+ @user_id = data[:user_id]
23
+ @client_key = data[:client_key]
24
+ end
25
+
26
+ # Check if the authorization code is still valid to generate new access
27
+ # token.
28
+ #
29
+ def expired?
30
+ Time.now > @expires_at
31
+ end
32
+
33
+ def used?
34
+ not @access_token.nil?
35
+ end
36
+
37
+ def invalidate(access_token)
38
+ @access_token = access_token.key
39
+ save
40
+ end
41
+
42
+ def self.find_by_key(key)
43
+ Storage.instance.auth_code_find_by_key(key)
44
+ end
45
+
46
+ def save
47
+ Storage.instance.auth_code_save(self)
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,28 @@
1
+ module OAuth2
2
+
3
+ ERROR_UNSUPPORTED_GRANT_TYPE = 'unsupported_grant_type'
4
+ ERROR_INVALID_CLIENT = 'invalid_client'
5
+ ERROR_INVALID_GRANT = 'invalid_grant'
6
+
7
+ ERROR_INVALID_REQUEST = 'invalid_request'
8
+ ERROR_UNAUTHORIZED_CLIENT = 'unauthorized_client'
9
+ ERROR_ACCESS_DENIED = 'access_denied'
10
+ ERROR_UNSUPPORTED_RESPONSE_TYPE = 'unsupported_response_type'
11
+ ERROR_INVALID_SCOPE = 'invalid_scope'
12
+ ERROR_SERVER_ERROR = 'server_error'
13
+ ERROR_TEMPORARILY_UNAVAILABLE = 'temporarily_unavailable'
14
+
15
+ class AuthError < RuntimeError
16
+ attr_reader :description
17
+
18
+ def initialize(message, description = nil)
19
+ super(message)
20
+ @description = description
21
+ end
22
+
23
+ def to_error_info
24
+ desc = "&error_description=#{self.description}" unless description.nil?
25
+ "error=#{message}#{desc}"
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,62 @@
1
+ module OAuth2
2
+
3
+ # Class to represent incoming authorization request.
4
+ #
5
+ class AuthRequest
6
+ attr_reader :user, :client, :client_id, :response_type, :redirect_uri, :scope, :state
7
+ attr_writer :user
8
+
9
+ # Initialize OAuth flow request with given attributes.
10
+ #
11
+ # @param [String] Unique client identifier.
12
+ # @param [String] Type of the response expected.
13
+ # @param [Hash] Additional hash of commands (recirect_uri, scope, state).
14
+ #
15
+ def initialize(client_key, response_type, options = {})
16
+ @client_id = client_key
17
+ @response_type = response_type
18
+ @redirect_uri = options[:redirect_uri] || nil
19
+ @scope = options[:scope] || nil
20
+ @state = options[:state] || nil
21
+
22
+ validate!
23
+ end
24
+
25
+ # Get the response object. Its gonna raise error unless user was stored
26
+ # to the request. That should happen after user had used valid credentials
27
+ # to login to authorization server.
28
+ #
29
+ # @throw [AuthError] Exception thrown in case of any error.
30
+ # @return [AuthResponse] AuthResponse object with all necessary attributes.
31
+ #
32
+ def response
33
+ raise AuthError.new(OAuth2::ERROR_ACCESS_DENIED) unless @user
34
+
35
+ AuthResponse.new(self)
36
+ end
37
+
38
+
39
+ # Validate if the request parameters match to the protocol specification.
40
+ # @throw [AuthError] Exception thrown in case of any error.
41
+ #
42
+ def validate!
43
+ unless @response_type && @client_id
44
+ raise AuthError.new(OAuth2::ERROR_INVALID_REQUEST)
45
+ end
46
+
47
+ @client = Client.find_by_key(@client_id)
48
+ raise AuthError.new(OAuth2::ERROR_INVALID_CLIENT) unless @client
49
+
50
+ #if @redirect_uri && @client.redirect_uri
51
+ # raise AuthError.new(OAuth2::ERROR_INVALID_REQUEST) unless @redirect_uri == @client.redirect_uri
52
+ #end
53
+
54
+ @redirect_uri = @client.redirect_uri unless @redirect_uri && @client.redirect_uri
55
+
56
+
57
+ unless @response_type == 'code'
58
+ raise AuthError.new(OAuth2::ERROR_UNSUPPORTED_RESPONSE_TYPE)
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,31 @@
1
+ module OAuth2
2
+ # Authorization code response. It contains all the important data
3
+ # to build response with new authorization code.
4
+ #
5
+ class AuthResponse
6
+
7
+ # Initialize new authorization response.
8
+ #
9
+ # @params [OAuth2::AuthRequest] Request for the auth code.
10
+ #
11
+ def initialize(request)
12
+ @scope = request.scope
13
+ @state = request.state
14
+ @redirect_uri = request.redirect_uri
15
+
16
+ @code = OAuth2::AuthCode.new({
17
+ :user_id => request.user.id,
18
+ :client_key => request.client.key,
19
+ :key => OAuth2::Utils.generate_key
20
+ })
21
+
22
+ @code.save
23
+ end
24
+
25
+ # Get the redirect URI based on response attributes.
26
+ #
27
+ def to_url
28
+ "#{@redirect_uri}?code=#{@code.key}&state=#{@state}&scope=#{@scope}"
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,29 @@
1
+ module OAuth2
2
+ class Client
3
+ attr_reader :name, :key, :secret, :redirect_uri, :client_type
4
+
5
+ def initialize(name, options = {})
6
+ @name = name
7
+ @client_type = 'confidential'
8
+ @key = options[:key] || OAuth2::Utils.generate_key
9
+ @secret = options[:secret] || OAuth2::Utils.generate_key
10
+ @redirect_uri = options[:redirect_uri] || nil
11
+ end
12
+
13
+ def create_access_token(user)
14
+ OAuth2::AccessToken.new({:client_key => key, :user_id => user.id})
15
+ end
16
+
17
+ def self.find_by_key(key)
18
+ OAuth2::Storage.instance.client_find_by_key(key)
19
+ end
20
+
21
+ def save
22
+ OAuth2::Storage.instance.client_save(self)
23
+ end
24
+
25
+ def self.all
26
+ OAuth2::Storage.instance.client_all()
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,30 @@
1
+ require 'forwardable'
2
+ require 'singleton'
3
+
4
+ module OAuth2
5
+ class Storage
6
+ include Singleton
7
+ extend Forwardable
8
+
9
+ def_delegators :@strategy,
10
+ :access_token_save,
11
+ :access_token_find_by_key,
12
+ :auth_code_save,
13
+ :auth_code_find_by_key,
14
+ :client_save,
15
+ :client_find_by_key,
16
+ :client_find_by_secret,
17
+ :client_all,
18
+ :user_find_by_email,
19
+ :user_all,
20
+ :user_save
21
+
22
+ def initialize
23
+ @strategy = OAuth2::Storages::Strategy.new
24
+ end
25
+
26
+ def strategy=(strategy)
27
+ @strategy = strategy
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,22 @@
1
+ #module OAuth2
2
+ # module Storages
3
+ # class MySQLStrategy < Strategy
4
+ # DB = 'oauth20'
5
+ # USER = 'root'
6
+ # HOST = 'localhost'
7
+ #
8
+ # def client_save(client)
9
+ # db = Mysql2::Client.new(:database => 'oauth2', :host => HOST, :username => USER)
10
+ #
11
+ # name = db.escape(client.name)
12
+ # key = db.escape(client.key)
13
+ # secret = db.escape(client.secret)
14
+ # redirect_uri = db.escape(client.redirect_uri) if client.redirect_uri
15
+ # client_type = db.escape(client.client_type) if client.client_type
16
+ #
17
+ # results = db.query("INSERT INTO clients(`name`, `key`, `secret`, `redirect_uri`, `client_type`) VALUES ('#{name}', '#{key}', '#{secret}', '#{redirect_uri}', '#{client_type}')")
18
+ # pp results
19
+ # end
20
+ # end
21
+ # end
22
+ #end
@@ -0,0 +1,137 @@
1
+ require "redis"
2
+ require 'json'
3
+ require 'uri'
4
+
5
+ module OAuth2
6
+ module Storages
7
+ class RedisStrategy < Strategy
8
+ @@redis = nil
9
+
10
+ def initialize(uri)
11
+ @@uri = URI.parse(uri)
12
+ end
13
+
14
+ def redis
15
+ @@redis ||= Redis.new(:host => @@uri.host, :port => @@uri.port, :password => @@uri.password)
16
+ end
17
+
18
+ #
19
+ # ACCESS TOKEN
20
+ #
21
+
22
+ def access_token_save(access_token)
23
+ data = {
24
+ :client_key => access_token.client_key,
25
+ :user_id => access_token.user_id,
26
+ :expires_in => access_token.expires_in,
27
+ :created_at => access_token.created_at.to_i,
28
+ :expires_at => access_token.expires_at.to_i,
29
+ :scope => access_token.scope,
30
+ :key => access_token.key,
31
+ :token_type => access_token.token_type
32
+ }
33
+
34
+ redis.set "access_token:#{access_token.key}", data.to_json
35
+ end
36
+
37
+ def access_token_find_by_key(key)
38
+ data = JSON.parse(redis.get("access_token:#{key}"))
39
+ data['expires_at'] = Time.at(data['expires_at'])
40
+ data['created_at'] = Time.at(data['created_at'])
41
+
42
+ AccessToken.new(symbolize(data))
43
+ end
44
+
45
+ #
46
+ # AUTHORIZATION CODE
47
+ #
48
+
49
+ def auth_code_save(auth_code)
50
+ data = {
51
+ :client_key => auth_code.client_key,
52
+ :user_id => auth_code.user_id,
53
+ :created_at => auth_code.created_at.to_i,
54
+ :expires_at => auth_code.expires_at.to_i,
55
+ :access_token => auth_code.access_token,
56
+ :key => auth_code.key
57
+ }
58
+
59
+ redis.set "auth_code:#{auth_code.key}", data.to_json
60
+ end
61
+
62
+ def auth_code_find_by_key(key)
63
+ data = JSON.parse(redis.get("auth_code:#{key}"))
64
+ data['expires_at'] = Time.at(data['expires_at'])
65
+ data['created_at'] = Time.at(data['created_at'])
66
+ AuthCode.new(symbolize(data))
67
+ end
68
+
69
+ #
70
+ # CLIENT
71
+ #
72
+
73
+ def client_save(client)
74
+ data = {
75
+ :key => client.key,
76
+ :secret => client.secret,
77
+ :redirect_uri => client.redirect_uri,
78
+ :name => client.name
79
+ }
80
+
81
+ redis.set "client:#{client.key}", data.to_json
82
+ redis.set "client_secret:#{client.secret}:key", client.key
83
+ end
84
+
85
+ def client_find_by_key(key)
86
+ begin
87
+ data = JSON.parse(redis.get("client:#{key}"))
88
+ Client.new(data.delete('name'), symbolize(data))
89
+ rescue
90
+ nil
91
+ end
92
+ end
93
+
94
+ def client_find_by_secret(secret)
95
+ key = redis.get("client_secret:#{secret}:key")
96
+ client_find_by_key(key)
97
+ end
98
+
99
+ def client_all
100
+ redis.keys("client:*").map! do |key|
101
+ client_find_by_key(key.split(':').last)
102
+ end
103
+ end
104
+
105
+ def user_save(user)
106
+ data = {
107
+ :email => user.email,
108
+ :password => user.password
109
+ }
110
+
111
+ redis.set "user:#{user.email}", data.to_json
112
+ end
113
+
114
+ def user_find_by_email(email)
115
+ begin
116
+ data = JSON.parse(redis.get("user:#{email}"))
117
+ User.new(symbolize(data))
118
+ rescue
119
+ nil
120
+ end
121
+ end
122
+
123
+ def user_all
124
+ redis.keys("user:*").map! do |email|
125
+ user_find_by_email(email.split(':').last)
126
+ end
127
+ end
128
+
129
+
130
+ private
131
+
132
+ def symbolize(hash)
133
+ Hash[hash.map{|a| [a.first.to_sym, a.last]}]
134
+ end
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,55 @@
1
+ module OAuth2
2
+ module Storages
3
+ class Strategy
4
+
5
+ # Save new access token.
6
+ #
7
+ # @param [Auth2::AccessToken] Access token instance.
8
+ #
9
+ def access_token_save(access_token)
10
+ raise NotImplementedError
11
+ end
12
+
13
+ # Find access token by given key.
14
+ #
15
+ # @param [String] Access token key to search.
16
+ #
17
+ def access_token_find_by_key(key)
18
+ raise NotImplementedError
19
+ end
20
+
21
+ # Save new authorization code.
22
+ #
23
+ # @param [Auth2::AuthCode] Authorization code instance.
24
+ #
25
+ def auth_code_save(auth_code)
26
+ raise NotImplementedError
27
+ end
28
+
29
+ # Find authorization code by the given key.
30
+ #
31
+ # @param [String] Authorization code key.
32
+ #
33
+ def auth_code_find_by_key(key)
34
+ raise NotImplementedError
35
+ end
36
+
37
+ # Save new client.
38
+ #
39
+ # @param [Auth2::Client] Client instance.
40
+ #
41
+ #
42
+ def client_save(client)
43
+ raise NotImplementedError
44
+ end
45
+
46
+ def client_find_by_key(key)
47
+ raise NotImplementedError
48
+ end
49
+
50
+ def client_find_by_secret(secret)
51
+ raise NotImplementedError
52
+ end
53
+ end
54
+ end
55
+ end