easyship-doorkeeper-jwt 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: []