persephone 1.0.0

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: adbd4c65e1c5b3ead009bcae0cd3a9845bc020d2
4
+ data.tar.gz: 440f0672d16161998c67572f222effe293f214a0
5
+ SHA512:
6
+ metadata.gz: 6eadd80dc2324addc4b816a9c73f719c4e5662b59507476af6489696f733112d3c5b5a808ba19ef6983c75379ae98bbb2fd72c97337b6ef4c036f0b18abe9e55
7
+ data.tar.gz: 6052c750ddad0149b8474b117e76210cfd888c23563d9ef0fda588e93481d146920c83431aaa7d24a07824fab5c85554b3531546e6399e876382adadb0b79cb8
data/.gitignore ADDED
@@ -0,0 +1,28 @@
1
+ *.gem
2
+ *.rbc
3
+ /.config
4
+ /coverage/
5
+ /InstalledFiles
6
+ /pkg/
7
+ /spec/reports/
8
+ /spec/examples.txt
9
+ /test/tmp/
10
+ /test/version_tmp/
11
+ /tmp/
12
+
13
+
14
+ ## Documentation cache and generated files:
15
+ /.yardoc/
16
+ /_yardoc/
17
+ /doc/
18
+ /rdoc/
19
+
20
+ ## Environment normalization:
21
+ /.bundle/
22
+ /vendor/bundle
23
+ /lib/bundler/man/
24
+
25
+ Gemfile.lock
26
+ .ruby-version
27
+ .ruby-gemset
28
+ .rvmrc
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in restful_api_authentication.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2016 David Sloan
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,134 @@
1
+ # persephone
2
+ This gem provides token based OAuth2 authorization within Rails 4 apps that use Mongoid 5 (commonly referred to as client_credentials authorization). It is named after the Greek goddess of the underworld for no good reason other than I liked the name.
3
+
4
+ ## Installation
5
+ In your Rails app gemfile, add the following:
6
+
7
+ ```
8
+ gem 'persephone', '~> 1.0.0'
9
+ ```
10
+
11
+ You will need to create applications that are allowed to authenticate with your API by doing the following in a rails console:
12
+
13
+ ```
14
+ app = Persephone::App.create(name: 'This is a name.', scopes: ['scope1', 'scope2'])
15
+ ```
16
+
17
+ You'll need your client ID and client secret in order to authenticate.
18
+
19
+ ```
20
+ app.client_id
21
+ app.client_secret
22
+ ```
23
+
24
+ ## Authenticating
25
+
26
+ Persephone automatically provides you with the /oauth/token route. You will authenticate by POSTing a JSON payload containing your client_id and client_secret to the /oauth/token location on your API. A payload should look something like this:
27
+
28
+ ```
29
+ {
30
+ "client_id": "dcf155afc48376e40efa6173da6bf16a21aaaa89c888c1e730d001c1e58c816e",
31
+ "client_secret": "da80b20365c221fcc7d0e3e3378ef9c6d6a03c9ea4014f50a94909d3cd395ad5"
32
+ }
33
+ ```
34
+
35
+ If your client ID and secret are correct, you should receive a token as a response. This should look something like:
36
+
37
+ ```
38
+ {
39
+ "token": "63b27b3502241d75abadf72d607c172ec555216b60d3828aff12151f393e8b05",
40
+ "expires": "2016-07-07T20:16:38.369+00:00"
41
+ }
42
+ ```
43
+
44
+ All additional calls to the API will add the Authorization header, which should look something like:
45
+
46
+ ```
47
+ Authorization: Bearer 63b27b3502241d75abadf72d607c172ec555216b60d3828aff12151f393e8b05
48
+ ```
49
+
50
+ ## Protecting API Resources
51
+
52
+ You'll need to create a controller concern with the following code:
53
+
54
+ ```
55
+ module Authentication
56
+ extend ActiveSupport::Concern
57
+
58
+ included do
59
+ before_action :authorize
60
+
61
+ def authorize
62
+ Persephone.authorized? request.headers
63
+ end
64
+ end
65
+ end
66
+ ```
67
+
68
+ Then simply add the Authentication concern to any controller:
69
+
70
+ ```
71
+ class SomeController < ApplicationController
72
+ include Authentication
73
+
74
+ end
75
+ ```
76
+
77
+ You can also skip authentication using the skip_before_action method:
78
+
79
+ ```
80
+ class SomeController < ApplicationController
81
+ include Authentication
82
+ skip_before_action :authorize, only: :some_method
83
+ end
84
+ ```
85
+
86
+ ## Scopes
87
+
88
+ You can add some additional layers of security by locking down various controllers and methods to only allow access to applications with a specific scope. First, you need to set the scope on the application. By default, all applications receive the 'public' scope unless you specifically set the scopes value.
89
+
90
+ ```
91
+ app = Persephone::App.find_by(client_id: 'dcf155afc48376e40efa6173da6bf16a21aaaa89c888c1e730d001c1e58c816e')
92
+ app.scopes = ['public', 'private']
93
+ app.save
94
+ ```
95
+
96
+ Note that scopes must be an array of values.
97
+
98
+ Make a new concern for each scope:
99
+
100
+ ```
101
+ module PrivateAuthentication
102
+ extend ActiveSupport::Concern
103
+
104
+ included do
105
+ before_action :authorize_private_scope
106
+
107
+ def authorize_private_scope
108
+ Persephone.authorized? request.headers, ['private']
109
+ end
110
+ end
111
+ end
112
+
113
+ module PublicAuthentication
114
+ extend ActiveSupport::Concern
115
+
116
+ included do
117
+ before_action :authorize_public_scope
118
+
119
+ def authorize_public_scope
120
+ Persephone.authorized? request.headers
121
+ end
122
+ end
123
+ end
124
+ ```
125
+
126
+ Then simply include whichever concern is appropriate for your controller. You can require BOTH scopes if you include both concerns.
127
+
128
+ ##Contributing
129
+
130
+ * Fork it
131
+ * Create your feature branch (git checkout -b my-new-feature)
132
+ * Commit your changes (git commit -am 'Added some feature')
133
+ * Push to the branch (git push origin my-new-feature)
134
+ * Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
@@ -0,0 +1,16 @@
1
+ module Persephone
2
+ class TokensController < ActionController::Base
3
+
4
+ def create
5
+ app = ::Persephone.authenticate(params['client_id'], params['client_secret'])
6
+ if app
7
+ response = { token: app.auth.token, expires: app.auth.expires }
8
+ code = 201
9
+ else
10
+ response = { error: 'unable to authenticate' }
11
+ code = 401
12
+ end
13
+ render json: response, status: code
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,33 @@
1
+ module Persephone
2
+ class App
3
+ include ::Mongoid::Document
4
+ include ::Mongoid::Timestamps
5
+
6
+ embeds_one :auth, class_name: 'Persephone::Auth'
7
+
8
+ field :name, type: String
9
+ field :scopes, type: Array
10
+ field :client_id, type: String
11
+ field :client_secret, type: String
12
+ field :rate_limit, type: Boolean, default: true
13
+ field :app_id, type: Integer, default: 0
14
+ field :app_slug, type: String
15
+
16
+ validates :name, presence: true, uniqueness: true
17
+ validates :client_id, presence: true, uniqueness: true
18
+ validates :client_secret, presence: true
19
+
20
+ index({ client_id: 1 }, { unique: true })
21
+ index({ client_secret: 1 })
22
+ index({ app_id: 1 })
23
+ index({ app_slug: 1 })
24
+
25
+ default_scope -> { ascending(:name) }
26
+
27
+ before_validation do
28
+ self.client_id ||= Digest::SHA256.hexdigest(UUID.new.generate(:compact))
29
+ self.client_secret ||= Digest::SHA256.hexdigest(UUID.new.generate(:compact))
30
+ self.scopes ||= [Persephone::DEFAULT_SCOPE]
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,29 @@
1
+ module Persephone
2
+ class Auth
3
+ include ::Mongoid::Document
4
+ include ::Mongoid::Timestamps
5
+
6
+ embedded_in :app, class_name: 'Persephone::App'
7
+
8
+ field :expires, type: DateTime
9
+ field :token, type: String
10
+
11
+ validates :expires, presence: true
12
+ validates :token, presence: true, uniqueness: true
13
+
14
+ index({ token: 1 }, { unique: true })
15
+ index({ app_id: 1 })
16
+
17
+ before_validation :generate_token, :generate_expires
18
+
19
+ private
20
+
21
+ def generate_token
22
+ self.token ||= Digest::SHA256.hexdigest UUID.new.generate(:compact)
23
+ end
24
+
25
+ def generate_expires
26
+ self.expires ||= Time.now.utc + 1.hour
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,5 @@
1
+ module Persephone
2
+ class Engine < ::Rails::Engine
3
+ isolate_namespace Persephone
4
+ end
5
+ end
@@ -0,0 +1,10 @@
1
+ require 'action_dispatch/routing'
2
+ require 'active_support/concern'
3
+
4
+ module ActionDispatch::Routing
5
+ class Mapper
6
+ def persephone
7
+ match "oauth/token" => 'persephone/tokens#create', via: :post
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,7 @@
1
+ module Persephone
2
+ class UnauthorizedError < StandardError
3
+ def initialize(message)
4
+ super(message)
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,3 @@
1
+ module Persephone
2
+ VERSION = '1.0.0'
3
+ end
data/lib/persephone.rb ADDED
@@ -0,0 +1,68 @@
1
+ require 'uuid'
2
+ require 'digest'
3
+
4
+ require File.expand_path('../persephone/version.rb', __FILE__)
5
+ require File.expand_path('../persephone/unauthorized_error.rb', __FILE__)
6
+ require File.expand_path('../persephone/routes.rb', __FILE__)
7
+ require File.expand_path('../persephone/engine.rb', __FILE__)
8
+
9
+ module Persephone
10
+ DEFAULT_SCOPE = 'public'.freeze
11
+
12
+ def self.authorized?(headers, scopes = [DEFAULT_SCOPE])
13
+ auth = self.authorization(headers)
14
+ auth && self.in_scope?(auth.app, scopes) && !self.expired?(auth)
15
+ end
16
+
17
+ def self.authorization(headers)
18
+ token = auth_token(headers)
19
+ if token
20
+ app = App.where('auth.token' => token).first
21
+ raise UnauthorizedError.new('token not found') if app.nil?
22
+ app.auth
23
+ else
24
+ raise UnauthorizedError.new('invalid token')
25
+ end
26
+ end
27
+
28
+ def self.current_application(headers)
29
+ token = auth_token(headers)
30
+ if token
31
+ app = App.where('auth.token' => token).first
32
+ else
33
+ nil
34
+ end
35
+ end
36
+
37
+ def self.expired?(auth)
38
+ if auth.expires < Time.now.utc
39
+ raise UnauthorizedError.new('token has expired; please get a new one')
40
+ else
41
+ false
42
+ end
43
+ end
44
+
45
+ def self.authenticate(client_id, client_secret)
46
+ app = App.where(client_id: client_id, client_secret: client_secret).first
47
+ if app
48
+ app.auth&.destroy
49
+ app.auth = Persephone::Auth.create(app: app)
50
+ app.save
51
+ end
52
+ app
53
+ end
54
+
55
+ def self.in_scope?(app, scopes)
56
+ if !(app.scopes & scopes).empty?
57
+ true
58
+ else
59
+ raise UnauthorizedError.new('application does not have access (scope)')
60
+ end
61
+ end
62
+
63
+ def self.auth_token(headers)
64
+ return headers['Authorization'].split[1] unless headers.nil? || headers['Authorization'].nil?
65
+ return false
66
+ end
67
+
68
+ end
@@ -0,0 +1,22 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/persephone/version.rb', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.required_rubygems_version = Gem::Requirement.new(">= 0") if gem.respond_to? :required_rubygems_version=
6
+ gem.authors = ["Dave Sloan"]
7
+ gem.email = ["daveksloan@gmail.com"]
8
+ gem.description = %q{Persephone is a gem which implements simple OAuth2 token based API authentication for Rails 4 and Mongoid 5.}
9
+ gem.summary = %q{This gem implements the OAuth2 client credentials token based authentication for applications. The client requests a token with their ID and secret key and the server responds with a token that can be used on subsequent API requests.}
10
+ gem.homepage = "https://github.com/davesloan/persephone"
11
+
12
+ #gem.files = `git ls-files`.split($\)
13
+ gem.files = `git ls-files`.split("\n")
14
+ gem.test_files = `git ls-files -- spec/*`.split("\n")
15
+ gem.name = "persephone"
16
+ gem.require_paths = ["lib"]
17
+ gem.version = Persephone::VERSION
18
+
19
+ gem.add_runtime_dependency(%q<rails>, ["~> 4.2.5"])
20
+ gem.add_runtime_dependency(%q<uuid>, ["~> 2.3.5"])
21
+ gem.add_runtime_dependency(%q<mongoid>, ["~> 5.1.0"])
22
+ end
metadata ADDED
@@ -0,0 +1,102 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: persephone
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Dave Sloan
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-07-07 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 4.2.5
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 4.2.5
27
+ - !ruby/object:Gem::Dependency
28
+ name: uuid
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 2.3.5
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 2.3.5
41
+ - !ruby/object:Gem::Dependency
42
+ name: mongoid
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: 5.1.0
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: 5.1.0
55
+ description: Persephone is a gem which implements simple OAuth2 token based API authentication
56
+ for Rails 4 and Mongoid 5.
57
+ email:
58
+ - daveksloan@gmail.com
59
+ executables: []
60
+ extensions: []
61
+ extra_rdoc_files: []
62
+ files:
63
+ - ".gitignore"
64
+ - Gemfile
65
+ - LICENSE
66
+ - README.md
67
+ - Rakefile
68
+ - app/controllers/persephone/tokens_controller.rb
69
+ - app/models/persephone/app.rb
70
+ - app/models/persephone/auth.rb
71
+ - lib/persephone.rb
72
+ - lib/persephone/engine.rb
73
+ - lib/persephone/routes.rb
74
+ - lib/persephone/unauthorized_error.rb
75
+ - lib/persephone/version.rb
76
+ - persephone.gemspec
77
+ homepage: https://github.com/davesloan/persephone
78
+ licenses: []
79
+ metadata: {}
80
+ post_install_message:
81
+ rdoc_options: []
82
+ require_paths:
83
+ - lib
84
+ required_ruby_version: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: '0'
89
+ required_rubygems_version: !ruby/object:Gem::Requirement
90
+ requirements:
91
+ - - ">="
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ requirements: []
95
+ rubyforge_project:
96
+ rubygems_version: 2.5.1
97
+ signing_key:
98
+ specification_version: 4
99
+ summary: This gem implements the OAuth2 client credentials token based authentication
100
+ for applications. The client requests a token with their ID and secret key and the
101
+ server responds with a token that can be used on subsequent API requests.
102
+ test_files: []