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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +171 -0
- data/Rakefile +6 -0
- data/lib/azure_jwt_auth.rb +8 -0
- data/lib/azure_jwt_auth/authenticable.rb +36 -0
- data/lib/azure_jwt_auth/jwt_manager.rb +73 -0
- data/lib/azure_jwt_auth/provider.rb +36 -0
- data/lib/azure_jwt_auth/spec/helpers.rb +12 -0
- data/lib/azure_jwt_auth/spec/not_authorized.rb +6 -0
- data/lib/azure_jwt_auth/version.rb +3 -0
- metadata +97 -0
checksums.yaml
ADDED
@@ -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
|
data/MIT-LICENSE
ADDED
@@ -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.
|
data/README.md
ADDED
@@ -0,0 +1,171 @@
|
|
1
|
+
# AzureJwtAuth
|
2
|
+

|
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).
|
data/Rakefile
ADDED
@@ -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
|
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: []
|