easyship-doorkeeper-jwt 0.0.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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: f2b423dfa40ffd4acc4694de503b46886c2d4f04
4
+ data.tar.gz: 57c6b431d288f663f06da460309e3cfff31e3bf9
5
+ SHA512:
6
+ metadata.gz: 62f5518b923d5f375c547e4d92b693f9db166251591d204f1f7c8a49bddd1863f42d4e1024cf25460cf5c6c8b5c5c6d809d97329981450c151551fd5d9d82dc4
7
+ data.tar.gz: 584d82408f354d40e302b3adabe9fcceb3b67b95207ac82957814e54b23712f2149fb98d7707ef8990b1e8141851c891f93c2b71a483e066dec803dec5a8949f
@@ -0,0 +1,15 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.bundle
11
+ *.so
12
+ *.o
13
+ *.a
14
+ mkmf.log
15
+ *.gem
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in doorkeeper-doorkeeper-jwt.gemspec
4
+ gemspec
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2016 Omac, Aloha
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.
@@ -0,0 +1,90 @@
1
+ # Doorkeeper JWT Assertion
2
+
3
+ Extending [Doorkeeper](https://github.com/doorkeeper-gem/doorkeeper) to support JWT Assertion grant type using a **secret** or a **private key file** or **application's public key**.
4
+
5
+ **This library is in alpha. Future incompatible changes may be necessary.**
6
+
7
+ ## Install
8
+
9
+ Add the gem to the Gemfile
10
+
11
+ ```ruby
12
+ gem 'easyship-doorkeeper-jwt', require: 'doorkeeper/jwt_assertion'
13
+ ```
14
+
15
+ ## Configuration
16
+
17
+ Inside your doorkeeper configuration file add the one of the following:
18
+
19
+ ``` ruby
20
+ Doorkeeper.configure do
21
+
22
+ # enable jwt handler
23
+ jwt_enable true
24
+
25
+ jwt_private_key Rails.root.join('config', 'keys', 'private.key')
26
+
27
+ jwt_secret 'notasecret'
28
+
29
+ # Optional
30
+ jwt_use_issuer_as_client_id true
31
+
32
+ # using public key as decode key
33
+ jwt_use_application_public_key_as_key true
34
+
35
+ end
36
+ ```
37
+
38
+ This will automatically push `assertion` into the Doorkeeper's grant_types configuration attribute.
39
+
40
+ When `jwt_use_issuer_as_client_id` is set to false then the `client_id` MUST be available from the parameters. By default it will extract the 'iss' and use it as the client_id to retrieve the oauth application.
41
+
42
+ Use the `resource_owner_authenticator` in the configuration to identify the owner based on the JWT claim values. This values can be accessible from `jwt`.
43
+
44
+ **by default it setup application's owner as owner.**
45
+
46
+ If the client request a token with an invalid assertion, or an expired JWT claim, an :invalid_grant error response will be generated before retrieving the resource_owner.
47
+
48
+ ``` ruby
49
+ Doorkeeper.configure do
50
+
51
+ resource_owner_authenticator do
52
+
53
+ if jwt
54
+ jwt['sub'].present? and User.find_by_email(jwt['sub'])
55
+ end
56
+
57
+ end
58
+
59
+ end
60
+
61
+ ```
62
+
63
+ ## Client Usage
64
+
65
+ Generate an assertion request token using a private key file or a secret:
66
+
67
+ ``` ruby
68
+ client = OAuth2::Client.new('client_id', 'client_secret', :site => 'http://my-site.com')
69
+
70
+ p12 = OpenSSL::PKCS12.new( Rails.root.join('config', 'keys', 'private.p12').open )
71
+
72
+ params = { :aud => 'audience',
73
+ :sub => 'client_id',
74
+ :iss => 'client_id',
75
+ :scope => 'scope',
76
+ :exp => Time.now.utc.to_i + 5.minutes }
77
+
78
+ token = client.assertion.get_token(params)
79
+ ```
80
+
81
+ > "[...] refresh tokens are not issued
82
+ > in response to assertion grant requests and access tokens will be
83
+ > issued with a reasonably short lifetime."
84
+ > - [draft-ietf-oauth-assertions-18](https://tools.ietf.org/html/draft-ietf-oauth-assertions-18#section-4.1)
85
+
86
+ ## TO DO
87
+
88
+ * Better error handling
89
+ * JWT Client Authentication Flow
90
+ * Testing
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+
@@ -0,0 +1,26 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'doorkeeper/jwt_assertion/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'easyship-doorkeeper-jwt'
8
+ spec.version = Doorkeeper::JwtAssertion::VERSION
9
+ spec.authors = ['AlohaCC']
10
+ spec.email = ['y.alohac@gmail.com']
11
+ spec.summary = 'Easyship OAuth JWT assertion extension for Doorkeeper'
12
+ spec.description = 'Extend your Doorkeeper implementation adding a new grant type: assertion. And decoding JWT claim messages to generate access tokens.'
13
+ spec.homepage = 'https://github.com/easyship/easyship-doorkeeper-jwt'
14
+ spec.license = 'MIT'
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ['lib']
20
+
21
+ spec.add_dependency 'doorkeeper', '~> 3.1'
22
+ spec.add_dependency 'jwt', '~> 1.5', '>= 1.5.4'
23
+
24
+ spec.add_development_dependency 'bundler', '~> 1.7'
25
+ spec.add_development_dependency 'rake', '~> 11.0'
26
+ end
@@ -0,0 +1,82 @@
1
+ require 'doorkeeper/jwt_assertion/version'
2
+ require 'doorkeeper/request/assertion'
3
+ require 'doorkeeper/jwt_assertion/railtie'
4
+
5
+ require 'jwt'
6
+
7
+ module Doorkeeper
8
+ module JWTAssertion
9
+ attr_reader :jwt, :jwt_header
10
+ end
11
+ end
12
+
13
+ module Doorkeeper
14
+ class Server
15
+ attr_reader :jwt
16
+
17
+ def jwt=(jwt)
18
+ @jwt = jwt
19
+ context.instance_variable_set('@jwt', jwt)
20
+ end
21
+
22
+ def jwt_header=(jwt_header)
23
+ @jwt_header = jwt_header
24
+ context.instance_variable_set('@jwt_header', jwt_header)
25
+ end
26
+ end
27
+ end
28
+
29
+ module Doorkeeper
30
+ class Config
31
+ option :jwt_key
32
+ option :jwt_use_issuer_as_client_id, default: true
33
+ option :jwt_use_application_public_key_as_key, default: true
34
+
35
+ class Builder
36
+ def jwt_secret(key)
37
+ set_jwt(key)
38
+ end
39
+
40
+ def jwt_private_key(key_file, passphrase = nil)
41
+ key = OpenSSL::PKey::RSA.new(File.open(key_file), passphrase)
42
+ set_jwt(key)
43
+ end
44
+
45
+ def jwt_enable(flag)
46
+ enable_jwt if flag
47
+ end
48
+
49
+ private
50
+
51
+ def set_jwt(key)
52
+ jwt_key key
53
+ end
54
+
55
+ def enable_jwt
56
+ Config.class_eval do
57
+ alias_method :remember_calculate_token_grant_types, :calculate_token_grant_types
58
+ define_method :calculate_token_grant_types do
59
+ remember_calculate_token_grant_types << 'assertion' << 'urn:ietf:params:oauth:grant-type:jwt-bearer'
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
66
+
67
+ module Doorkeeper
68
+ module Errors
69
+ class ExpiredSignature < DoorkeeperError
70
+ end
71
+ end
72
+ end
73
+
74
+ module Doorkeeper
75
+ module Easyship
76
+ class TokenGenerator
77
+ def self.generate(options = {})
78
+ "#{::Rails.env[0..3]}_" + Base64.strict_encode64(OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha256'), SecureRandom.hex, options.to_s))
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,9 @@
1
+ module Doorkeeper
2
+ module JWTAssertion
3
+ class Railtie < ::Rails::Railtie
4
+ initializer 'doorkeeper.jwt_assertion' do
5
+ Doorkeeper::Helpers::Controller.send :include, Doorkeeper::JWTAssertion
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,5 @@
1
+ module Doorkeeper
2
+ module JwtAssertion
3
+ VERSION = '0.0.1'
4
+ end
5
+ end
@@ -0,0 +1,151 @@
1
+ require 'doorkeeper/oauth/describable_error_response'
2
+
3
+ module Doorkeeper
4
+ module OAuth
5
+ class AssertionAccessTokenRequest
6
+ include Validations
7
+ include OAuth::RequestConcern
8
+ include OAuth::Helpers
9
+
10
+ attr_accessor :server, :original_scopes
11
+ attr_reader :resource_owner, :client, :configuration, :access_token, :response, :error_description
12
+
13
+ validate :assertion, error: :invalid_grant
14
+ validate :client, error: :invalid_client
15
+ validate :scopes, error: :invalid_scope
16
+ validate :resource_owner, error: :invalid_grant
17
+ validate :access_token, error: :invalid_grant
18
+
19
+ ##
20
+ # @see https://tools.ietf.org/html/draft-ietf-oauth-jwt-bearer-12#section-2.1
21
+ #
22
+ def initialize(server, configuration)
23
+ @server = server
24
+ @configuration = configuration
25
+ @response = nil
26
+ @original_scopes = server.parameters[:scope]
27
+ end
28
+
29
+ def authorize
30
+ validate
31
+ if valid?
32
+ @response = TokenResponse.new(access_token)
33
+ else
34
+ @response = DescribableErrorResponse.from_request(self)
35
+ @response.description = error_description
36
+ @response
37
+ end
38
+ end
39
+
40
+ private
41
+
42
+ ##
43
+ # > The value of the "grant_type" is "urn:ietf:params:oauth:grant-
44
+ # > type:jwt-bearer".
45
+ # > The value of the "assertion" parameter MUST contain a single JWT.
46
+ # > - draft-ietf-oauth-jwt-bearer-12
47
+ #
48
+ # @see https://tools.ietf.org/html/draft-ietf-oauth-jwt-bearer-12#section-2.1
49
+ #
50
+ # > assertion_type
51
+ # > REQUIRED. The format of the assertion as defined by the
52
+ # > authorization server. The value MUST be an absolute URI.
53
+ # >
54
+ # > assertion
55
+ # > REQUIRED. The assertion.
56
+ # >
57
+ # > - draft-ietf-oauth-v2-10
58
+ # @see http://tools.ietf.org/html/draft-ietf-oauth-v2-10#section-4.1.3
59
+ #
60
+ # Newer versions of ietf-oauth-v2 don't need assertion_type. So it's still optional.
61
+ def validate_assertion
62
+ assertion, assertion_type = server.parameters.values_at(:assertion, :assertion_type)
63
+
64
+ if assertion_type and assertion_type != 'urn:ietf:params:oauth:grant-type:jwt-bearer'
65
+ raise StandardError.new('Assertion type not valid. Expected urn:ietf:params:oauth:grant-type:jwt-bearer')
66
+ end
67
+
68
+ if configuration.jwt_use_application_public_key_as_key
69
+ payload, header = JWT.decode(assertion, nil, false)
70
+ raise StandardError.new('App ID is not correct') unless OAuth::Client.find(payload['iss']).present?
71
+ public_key = OAuth::Client.find(payload['iss']).application.public_key
72
+ payload, header = JWT.decode(assertion, OpenSSL::PKey::RSA.new(public_key), true, { algorithm: header['alg'] })
73
+ else
74
+ payload, header = JWT.decode(assertion, configuration.jwt_key)
75
+ end
76
+ server.jwt = payload
77
+ server.jwt_header = header
78
+
79
+ rescue => error
80
+ @error_description = error.message
81
+ false
82
+ end
83
+
84
+ ##
85
+ # If `jwt_use_issuer_as_client_id` is `true` then validate the client using the issuer as the client_id.
86
+ # Otherwise, use the client_id directly from the parameters.
87
+ #
88
+ # > Authentication of the client is optional, as described in
89
+ # > Section 3.2.1 of OAuth 2.0 [RFC6749] and consequently, the
90
+ # > "client_id" is only needed when a form of client authentication that
91
+ # > relies on the parameter is used.
92
+ # > - draft-ietf-oauth-assertions-18
93
+ #
94
+ # @see https://tools.ietf.org/html/draft-ietf-oauth-assertions-18#section-4.1
95
+ #
96
+ # > The JWT MUST contain an "iss" (issuer) claim that contains a
97
+ # > unique identifier for the entity that issued the JWT.
98
+ # > - draft-ietf-oauth-jwt-bearer-12
99
+ #
100
+ # @see https://tools.ietf.org/html/draft-ietf-oauth-jwt-bearer-12#section-3
101
+ #
102
+ def validate_client
103
+ @client ||= if configuration.jwt_use_issuer_as_client_id
104
+ OAuth::Client.find(server.jwt['iss']) if server.jwt['iss'].present?
105
+ elsif sever.parameters[:client_id].present?
106
+ OAuth::Client.find(sever.parameters[:client_id])
107
+ end
108
+ end
109
+
110
+ ##
111
+ # > The "scope" parameter may be used, as defined in the Assertion
112
+ # > Framework for OAuth 2.0 Client Authentication and Authorization
113
+ # > Grants [I-D.ietf-oauth-assertions] specification, to indicate the
114
+ # > requested scope.
115
+ #
116
+ # @see https://tools.ietf.org/html/draft-ietf-oauth-jwt-bearer-12#section-2.1
117
+ #
118
+ def validate_scopes
119
+ @original_scopes = @original_scopes || server.jwt['scope']
120
+ return true unless @original_scopes
121
+ ScopeChecker.valid? @original_scopes, configuration.scopes, client.try(:scopes)
122
+ end
123
+
124
+ def validate_resource_owner
125
+ resource_owner || (@resource_owner = client.try(:application).try(:owner) || server.current_resource_owner)
126
+ end
127
+
128
+ def validate_access_token
129
+ access_token or create_token
130
+ end
131
+
132
+ ##
133
+ # > [...] refresh tokens are not issued
134
+ # > in response to assertion grant requests and access tokens will be
135
+ # > issued with a reasonably short lifetime.
136
+ #
137
+ # @see https://tools.ietf.org/html/draft-ietf-oauth-assertions-18#section-4.1
138
+ #
139
+ def create_token
140
+ client_requested_expires_in = server.jwt['exp'].to_i - server.jwt['iat'].to_i
141
+ server_expires_in = Authorization::Token.access_token_expires_in(configuration, client)
142
+ if server_expires_in
143
+ expires_in = (client_requested_expires_in > 0 && client_requested_expires_in <= server_expires_in) ? client_requested_expires_in : server_expires_in
144
+ else
145
+ expires_in = nil
146
+ end
147
+ @access_token = AccessToken.find_or_create_for(client, resource_owner.id, scopes, expires_in, configuration.refresh_token_enabled?)
148
+ end
149
+ end
150
+ end
151
+ end
@@ -0,0 +1,7 @@
1
+ module Doorkeeper
2
+ module OAuth
3
+ class DescribableErrorResponse < ErrorResponse
4
+ attr_accessor :description
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,25 @@
1
+ require 'doorkeeper/oauth/assertion_access_token_request'
2
+
3
+ module Doorkeeper
4
+ module Request
5
+ class Assertion
6
+ def self.build(server)
7
+ new(server)
8
+ end
9
+
10
+ attr_reader :server
11
+
12
+ def initialize(server)
13
+ @server = server
14
+ end
15
+
16
+ def request
17
+ @request ||= OAuth::AssertionAccessTokenRequest.new(server, Doorkeeper.configuration)
18
+ end
19
+
20
+ def authorize
21
+ request.authorize
22
+ end
23
+ end
24
+ end
25
+ end
metadata ADDED
@@ -0,0 +1,119 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: easyship-doorkeeper-jwt
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - AlohaCC
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2017-08-03 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: doorkeeper
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '3.1'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '3.1'
27
+ - !ruby/object:Gem::Dependency
28
+ name: jwt
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.5'
34
+ - - ">="
35
+ - !ruby/object:Gem::Version
36
+ version: 1.5.4
37
+ type: :runtime
38
+ prerelease: false
39
+ version_requirements: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - "~>"
42
+ - !ruby/object:Gem::Version
43
+ version: '1.5'
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: 1.5.4
47
+ - !ruby/object:Gem::Dependency
48
+ name: bundler
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '1.7'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '1.7'
61
+ - !ruby/object:Gem::Dependency
62
+ name: rake
63
+ requirement: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '11.0'
68
+ type: :development
69
+ prerelease: false
70
+ version_requirements: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '11.0'
75
+ description: 'Extend your Doorkeeper implementation adding a new grant type: assertion.
76
+ And decoding JWT claim messages to generate access tokens.'
77
+ email:
78
+ - y.alohac@gmail.com
79
+ executables: []
80
+ extensions: []
81
+ extra_rdoc_files: []
82
+ files:
83
+ - ".gitignore"
84
+ - Gemfile
85
+ - LICENSE.txt
86
+ - README.md
87
+ - Rakefile
88
+ - easyship-doorkeeper-jwt.gemspec
89
+ - lib/doorkeeper/jwt_assertion.rb
90
+ - lib/doorkeeper/jwt_assertion/railtie.rb
91
+ - lib/doorkeeper/jwt_assertion/version.rb
92
+ - lib/doorkeeper/oauth/assertion_access_token_request.rb
93
+ - lib/doorkeeper/oauth/describable_error_response.rb
94
+ - lib/doorkeeper/request/assertion.rb
95
+ homepage: https://github.com/easyship/easyship-doorkeeper-jwt
96
+ licenses:
97
+ - MIT
98
+ metadata: {}
99
+ post_install_message:
100
+ rdoc_options: []
101
+ require_paths:
102
+ - lib
103
+ required_ruby_version: !ruby/object:Gem::Requirement
104
+ requirements:
105
+ - - ">="
106
+ - !ruby/object:Gem::Version
107
+ version: '0'
108
+ required_rubygems_version: !ruby/object:Gem::Requirement
109
+ requirements:
110
+ - - ">="
111
+ - !ruby/object:Gem::Version
112
+ version: '0'
113
+ requirements: []
114
+ rubyforge_project:
115
+ rubygems_version: 2.6.11
116
+ signing_key:
117
+ specification_version: 4
118
+ summary: Easyship OAuth JWT assertion extension for Doorkeeper
119
+ test_files: []