azure_jwt_auth 0.1.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
+ SHA256:
3
+ metadata.gz: f26367508cac71e2138946eca9d96411ada1093c7ca9b6ce88366ca27799c108
4
+ data.tar.gz: edd4ae435720cbc4d8d5438ce447fdbefd18071530afa84c60b21c7d799558fc
5
+ SHA512:
6
+ metadata.gz: dd67132ec694be2951a296f4bfdc3c7b393389e17cb188a0dc738ab2d1da2333346e770ac6c0164f68113ae10f506916236b05e5f1dc53da4f531a65f4b670a7
7
+ data.tar.gz: e47ac0cbe7aaabacb6df6293dfbdc7d7beaf33b150ed3b9f5211dcc0624503541a407699a610b500530ecbea6480d26a6fb066bbe0c3472d91bc7c1ca080c5f5
@@ -0,0 +1,20 @@
1
+ Copyright 2017 rjurado
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.
@@ -0,0 +1,171 @@
1
+ # AzureJwtAuth
2
+ ![Build Status](https://travis-ci.org/nosolosoftware/azure_jwt_auth.svg?branch=master)
3
+
4
+ Easy way for Ruby applications to authenticate to Azure B2C/AD in order to access protected web resources.
5
+
6
+ ## Installation
7
+
8
+ Add this line to your application's Gemfile:
9
+
10
+ ```ruby
11
+ gem 'azure_jwt_auth'
12
+ ```
13
+
14
+ And then execute:
15
+
16
+ ```bash
17
+ $ bundle
18
+ ```
19
+
20
+ Or install it yourself as:
21
+
22
+ ```bash
23
+ $ gem install azure_jwt_auth
24
+ ```
25
+
26
+ ## Usage with Rails
27
+
28
+ First of all, we add our providers into an initializer:
29
+
30
+ ```ruby
31
+ # config/initializers/azure.rb
32
+
33
+ require 'azure_jwt_auth/jwt_manager'
34
+
35
+ AzureJwtAuth::JwtManager.load_provider(
36
+ :b2c,
37
+ 'https://login.microsoftonline.com/.../v2.0/.well-known/openid-configuration')
38
+ )
39
+
40
+ AzureJwtAuth::JwtManager.load_provider(
41
+ :ad,
42
+ 'https://sts.windows.net/.../v2.0/.well-known/openid-configuration'
43
+ )
44
+ ...
45
+ ```
46
+
47
+ Then, we add `Authenticable` module into `ApplicationController` and define `entity_from_token_payload` method.
48
+ This method is used by `Authenticable` module to load `current_user`.
49
+
50
+ ```ruby
51
+ require 'azure_jwt_auth/authenticable'
52
+
53
+ class ApplicationController < ActionController::API
54
+ include AzureJwtAuth::Authenticable
55
+
56
+ rescue_from AzureJwtAuth::NotAuthorized, with: :render_401
57
+
58
+ private
59
+
60
+ def render_401
61
+ render json: {}, status: 401
62
+ end
63
+
64
+ def entity_from_token_payload(payload)
65
+ # Returns a valid entity, `nil` or raise
66
+ # e.g.
67
+ # User.find payload['sub']
68
+ end
69
+ end
70
+ ```
71
+
72
+ Finally, we can use `authenticate!` method into ours controllers:
73
+
74
+ ```ruby
75
+ class ExampleController < ApplicationController
76
+ before_action :authenticate!
77
+
78
+ ...
79
+ end
80
+ ```
81
+
82
+ ## Providers
83
+
84
+ Provider class initializer receives the following parameters:
85
+
86
+ | parameter | description |
87
+ | -- | -- |
88
+ | uid | unique provider identifier |
89
+ | config_url | azure url to get config |
90
+ | validations | payload fields validations which will be checked for each token: `{payload_field: value_expected, ...}` (optional) |
91
+
92
+ We create providers using the `AzureJwtAuth::JwtManager.load_provider` method:
93
+
94
+ ```ruby
95
+ AzureJwtAuth::JwtManager.load_provider(
96
+ :b2c, # uid
97
+ 'https://login.microsoftonline.com/.../v2.0/.well-known/openid-configuration'), # config_url
98
+ {'aud' => 'my_app_id'} # validations
99
+ )
100
+ ```
101
+
102
+ ## Authenticable
103
+
104
+ [This module](lib/azure_jwt_auth/authenticable.rb) provides us with the following methods:
105
+
106
+ * __authenticate!__
107
+
108
+ Check if a token is valid for any provider and loads `current_user`. Otherwise it throws an exception.
109
+
110
+ If you need other behavior you can define your custom authenticate! method like this:
111
+
112
+ ```ruby
113
+ def my_authenticate!
114
+ begin
115
+ token = JwtManager.new(request, :privider_id)
116
+ unauthorize! unless token.valid?
117
+ rescue
118
+ unauthorize!
119
+ end
120
+
121
+ @current_user = User.find(token.payload['sub'])
122
+ end
123
+ ```
124
+
125
+ * __current_user__
126
+
127
+ Returns current_user loaded by `authenticate!` method.
128
+
129
+ * __signed_in?__
130
+
131
+ Check if exists current_user.
132
+
133
+ * __unauthorize!__
134
+
135
+ Throws a `AzureJwtAuth::NotAuthorized` exception.
136
+
137
+ ## Testing (rspec)
138
+
139
+ Require the [AzureJwtAuth::Spec::Helpers](lib/azure_jwt_auth/spec/helpers.rb) helper module in `rails_helper.rb`.
140
+
141
+ ```ruby
142
+ require 'azure_jwt_auth/spec/helpers'
143
+ ...
144
+ RSpec.configure do |config|
145
+ ...
146
+ config.include AzureJwtAuth::Spec::Helpers, :type => :controller
147
+ end
148
+ ```
149
+
150
+ And then we can just call `sign_in(user)`:
151
+
152
+ ```ruby
153
+ describe ExampleController
154
+ let(:user) { MyEntity.create(...) }
155
+
156
+ it "blocks unauthenticated access" do
157
+ get :index
158
+ expect(response).to have_http_status(401)
159
+ end
160
+
161
+ it "allows authenticated access" do
162
+ sign_in user # user will be returned by current_user method
163
+ get :index
164
+ expect(response).to have_http_status(200)
165
+ end
166
+ end
167
+ ```
168
+
169
+ ## License
170
+
171
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -0,0 +1,8 @@
1
+ require 'bcrypt'
2
+
3
+ module AzureJwtAuth
4
+ KidNotFound = Class.new(StandardError)
5
+ InvalidProviderConfig = Class.new(StandardError)
6
+ NotAuthorizationHeader = Class.new(StandardError)
7
+ ProviderNotFound = Class.new(StandardError)
8
+ end
@@ -0,0 +1,36 @@
1
+ require 'azure_jwt_auth/jwt_manager'
2
+
3
+ module AzureJwtAuth
4
+ AzureJwtAuth::NotAuthorized = Class.new(StandardError)
5
+
6
+ module Authenticable
7
+ def current_user
8
+ @current_user
9
+ end
10
+
11
+ def signed_in?
12
+ !current_user.nil?
13
+ end
14
+
15
+ def authenticate!
16
+ unauthorize! unless JwtManager.providers
17
+
18
+ JwtManager.providers.each do |_uid, provider|
19
+ token = JwtManager.new(request, provider.uid)
20
+
21
+ if token.valid?
22
+ @current_user = entity_from_token_payload(token.payload)
23
+ break
24
+ end
25
+ rescue => error
26
+ Rails.logger.info(error) if defined? Rails
27
+ end
28
+
29
+ unauthorize! unless @current_user
30
+ end
31
+
32
+ def unauthorize!
33
+ raise NotAuthorized
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,73 @@
1
+ require 'azure_jwt_auth/provider'
2
+ require 'jwt'
3
+
4
+ module AzureJwtAuth
5
+ class JwtManager
6
+ class << self
7
+ attr_reader :providers
8
+
9
+ def load_provider(uid, config_uri, validations={})
10
+ @providers ||= {}
11
+ @providers[uid] = Provider.new(uid, config_uri, validations)
12
+ end
13
+
14
+ def find_provider(uid)
15
+ return unless @providers
16
+ @providers[uid]
17
+ end
18
+ end
19
+
20
+ def initialize(request, provider_id)
21
+ raise NotAuthorizationHeader unless request.env['HTTP_AUTHORIZATION']
22
+ raise ProviderNotFound unless (@provider = self.class.find_provider(provider_id))
23
+
24
+ @jwt = request.env['HTTP_AUTHORIZATION'].split.last # remove Bearer
25
+ @jwt_info = decode
26
+ end
27
+
28
+ def payload
29
+ @jwt_info ? @jwt_info.first : nil
30
+ end
31
+
32
+ # Validates the payload hash for expiration and meta claims
33
+ def valid?
34
+ payload && iss_valid? && custom_valid?
35
+ end
36
+
37
+ # Check custom validations defined into provider
38
+ def custom_valid?
39
+ @provider.validations.each do |key, value|
40
+ return false unless payload[key] == value
41
+ end
42
+
43
+ true
44
+ end
45
+
46
+ # Validates issuer
47
+ def iss_valid?
48
+ payload['iss'] == @provider.config['issuer']
49
+ end
50
+
51
+ private
52
+
53
+ # Decodes the JWT with the signed secret
54
+ def decode
55
+ dirty_token = JWT.decode(@jwt, nil, false)
56
+ kid = dirty_token.last['kid']
57
+ try = false
58
+
59
+ begin
60
+ rsa = @provider.keys[kid]
61
+ raise KidNotFound, 'kid not found into provider keys' unless rsa
62
+
63
+ JWT.decode(@jwt, rsa.public_key, true, algorithm: 'RS256')
64
+ rescue JWT::VerificationError, KidNotFound
65
+ raise if try
66
+
67
+ @provider.load_keys # maybe keys have been changed
68
+ try = true
69
+ retry
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,36 @@
1
+ require 'net/http'
2
+ require 'rsa_pem'
3
+
4
+ module AzureJwtAuth
5
+ class Provider
6
+ attr_reader :uid, :config_uri, :validations
7
+ attr_reader :config, :keys
8
+
9
+ def initialize(uid, config_uri, validations={})
10
+ @uid = uid
11
+ @config_uri = config_uri
12
+ @validations = validations
13
+
14
+ begin
15
+ @config = JSON.parse(Net::HTTP.get(URI(config_uri)))
16
+ rescue JSON::ParserError
17
+ raise InvalidProviderConfig, "config_uri response is not valid for provider: #{uid}"
18
+ end
19
+
20
+ load_keys
21
+ end
22
+
23
+ def load_keys
24
+ uri = URI(@config['jwks_uri'])
25
+ keys = JSON.parse(Net::HTTP.get(uri))['keys']
26
+
27
+ @keys = {}
28
+ keys.each do |key|
29
+ cert = RsaPem.from(key['n'], key['e'])
30
+ rsa = OpenSSL::PKey::RSA.new(cert)
31
+
32
+ @keys[key['kid']] = rsa
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,12 @@
1
+ module AzureJwtAuth
2
+ module Spec
3
+ module Helpers
4
+ require 'azure_jwt_auth/jwt_manager'
5
+
6
+ def sign_in(user)
7
+ allow(controller).to receive(:authenticate!).and_return(true)
8
+ allow(controller).to receive(:current_user).and_return(user)
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,6 @@
1
+ module AzureJwtAuth
2
+ module Spec
3
+ class NotAuthorized < StandardError
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,3 @@
1
+ module AzureJwtAuth
2
+ VERSION = '0.1.1'
3
+ end
metadata ADDED
@@ -0,0 +1,97 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: azure_jwt_auth
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - rjurado
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2018-04-04 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bcrypt
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
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.5'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rsa-pem-from-mod-exp
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '0.1'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '0.1'
55
+ description: Easy way for Ruby applications to authenticate to Azure B2C/AD in order
56
+ to access protected web resources.
57
+ email:
58
+ - rjurado@nosolosoftware.es
59
+ executables: []
60
+ extensions: []
61
+ extra_rdoc_files: []
62
+ files:
63
+ - MIT-LICENSE
64
+ - README.md
65
+ - Rakefile
66
+ - lib/azure_jwt_auth.rb
67
+ - lib/azure_jwt_auth/authenticable.rb
68
+ - lib/azure_jwt_auth/jwt_manager.rb
69
+ - lib/azure_jwt_auth/provider.rb
70
+ - lib/azure_jwt_auth/spec/helpers.rb
71
+ - lib/azure_jwt_auth/spec/not_authorized.rb
72
+ - lib/azure_jwt_auth/version.rb
73
+ homepage: https://github.com/nosolosoftware/azure_jwt_auth
74
+ licenses:
75
+ - MIT
76
+ metadata: {}
77
+ post_install_message:
78
+ rdoc_options: []
79
+ require_paths:
80
+ - lib
81
+ required_ruby_version: !ruby/object:Gem::Requirement
82
+ requirements:
83
+ - - ">="
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
86
+ required_rubygems_version: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - ">="
89
+ - !ruby/object:Gem::Version
90
+ version: '0'
91
+ requirements: []
92
+ rubyforge_project:
93
+ rubygems_version: 2.7.3
94
+ signing_key:
95
+ specification_version: 4
96
+ summary: Azure B2C/AD authentication using Ruby.
97
+ test_files: []