azure_jwt_auth 0.1.1

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