Commit 73da5c4410a8a9dafc25fc0bda3eaf4e6beed2a0

Authored by Kyle Werner
Committed by GitHub
2 parents 2b53a2e1 1d606569
Exists in master

Merge pull request #15 from lessonly/azemoh/ch35118/add-bearer-token-auth-to-scim-rails-gem

Add support for OAuth Bearer Authentication
Gemfile.lock
1 1 PATH
2 2 remote: .
3 3 specs:
4   - scim_rails (0.2.1)
  4 + scim_rails (0.3.0)
  5 + jwt (~> 2.2)
5 6 rails (>= 5.0, < 6.1)
6 7  
7 8 GEM
... ... @@ -77,6 +78,7 @@ GEM
77 78 activesupport (>= 4.2.0)
78 79 i18n (1.6.0)
79 80 concurrent-ruby (~> 1.0)
  81 + jwt (2.2.1)
80 82 loofah (2.3.1)
81 83 crass (~> 1.0.2)
82 84 nokogiri (>= 1.5.9)
... ... @@ -175,4 +177,4 @@ RUBY VERSION
175 177 ruby 2.4.4p296
176 178  
177 179 BUNDLED WITH
178   - 2.0.1
  180 + 2.0.2
... ...
README.md
... ... @@ -78,6 +78,57 @@ When sending requests to the server the `Content-Type` should be set to `applica
78 78  
79 79 All responses will be sent with a `Content-Type` of `application/scim+json`.
80 80  
  81 +#### Authentication
  82 +
  83 +This gem supports both basic and OAuth bearer authentication.
  84 +
  85 +##### Basic Auth
  86 +###### Username
  87 +The config setting `basic_auth_model_searchable_attribute` is the model attribute used to authenticate as the `username`. It defaults to `:subdomain`.
  88 +
  89 +Ensure it is unique to the model records.
  90 +
  91 +###### Password
  92 +The config setting `basic_auth_model_authenticatable_attribute` is the model attribute used to authenticate as `password`. Defaults to `:api_token`.
  93 +
  94 +Assuming the attribute is `:api_token`, generate the password using:
  95 +```ruby
  96 +token = ScimRails::Encoder.encode(company)
  97 +# use the token as password for requests
  98 +company.api_token = token # required
  99 +company.save! # don't forget to persist the company record
  100 +```
  101 +
  102 +This is necessary irrespective of your authentication choice(s) - basic auth, oauth bearer or both.
  103 +
  104 +###### Sample Request
  105 +
  106 +```bash
  107 +$ curl -X GET 'http://username:password@localhost:3000/scim/v2/Users'
  108 +```
  109 +
  110 +##### OAuth Bearer
  111 +
  112 +###### Signing Algorithm
  113 +In the config settings, ensure you set `signing_algorithm` to a valid JWT signing algorithm, e.g "HS256". Defaults to `"none"` when not set.
  114 +
  115 +###### Signing Secret
  116 +In the config settings, ensure you set `signing_secret` to a secret key that will be used to encode and decode tokens. Defaults to `nil` when not set.
  117 +
  118 +If you have already generated the `api_token` in the "Basic Auth" section, then use that as your bearer token and ignore the steps below:
  119 +```ruby
  120 +token = ScimRails::Encoder.encode(company)
  121 +# use the token as bearer token for requests
  122 +company.api_token = token #required
  123 +company.save! # don't forget to persist the company record
  124 +```
  125 +
  126 +##### Sample Request
  127 +
  128 +```bash
  129 +$ curl -H 'Authorization: Bearer xxxxxxx.xxxxxx' -X GET 'http://localhost:3000/scim/v2/Users'
  130 +```
  131 +
81 132 ### List
82 133  
83 134 ##### All
... ...
app/controllers/scim_rails/application_controller.rb
... ... @@ -9,14 +9,30 @@ module ScimRails
9 9 private
10 10  
11 11 def authorize_request
12   - authenticate_with_http_basic do |username, password|
  12 + send(authentication_strategy) do |searchable_attribute, authentication_attribute|
13 13 authorization = AuthorizeApiRequest.new(
14   - searchable_attribute: username,
15   - authentication_attribute: password
  14 + searchable_attribute: searchable_attribute,
  15 + authentication_attribute: authentication_attribute
16 16 )
17 17 @company = authorization.company
18 18 end
19   - raise ScimRails::ExceptionHandler::InvalidCredentials if @company.blank?
  19 + raise ScimRails::ExceptionHandler::InvalidCredentials if @company.blank?
  20 + end
  21 +
  22 + def authentication_strategy
  23 + if request.headers["Authorization"]&.include?("Bearer")
  24 + :authenticate_with_oauth_bearer
  25 + else
  26 + :authenticate_with_http_basic
  27 + end
  28 + end
  29 +
  30 + def authenticate_with_oauth_bearer
  31 + authentication_attribute = request.headers["Authorization"].split(" ").last
  32 + payload = ScimRails::Encoder.decode(authentication_attribute).with_indifferent_access
  33 + searchable_attribute = payload[ScimRails.config.basic_auth_model_searchable_attribute]
  34 +
  35 + yield searchable_attribute, authentication_attribute
20 36 end
21 37 end
22 38 end
... ...
lib/generators/scim_rails/templates/initializer.rb
... ... @@ -22,6 +22,18 @@ ScimRails.configure do |config|
22 22 # or throws an error (returning 409 Conflict in accordance with SCIM spec)
23 23 config.scim_user_prevent_update_on_create = false
24 24  
  25 + # Cryptographic algorithm used for signing the auth tokens.
  26 + # It supports all algorithms supported by the jwt gem.
  27 + # See https://github.com/jwt/ruby-jwt#algorithms-and-usage for supported algorithms
  28 + # It is "none" by default, hence generated tokens are unsigned
  29 + # The tokens do not need to be signed if you only need basic authentication.
  30 + # config.signing_algorithm = "HS256"
  31 +
  32 + # Secret token used to sign authorization tokens
  33 + # It is `nil` by default, hence generated tokens are unsigned
  34 + # The tokens do not need to be signed if you only need basic authentication.
  35 + # config.signing_secret = SECRET_TOKEN
  36 +
25 37 # Default sort order for pagination is by id. If you
26 38 # use non sequential ids for user records, uncomment
27 39 # the below line and configure a determinate order.
... ...
lib/scim_rails.rb
1 1 require "scim_rails/engine"
2 2 require "scim_rails/config"
  3 +require "scim_rails/encoder"
3 4  
4 5 module ScimRails
5 6 end
... ...
lib/scim_rails/config.rb
... ... @@ -10,6 +10,8 @@ module ScimRails
10 10 end
11 11  
12 12 class Config
  13 + ALGO_NONE = "none".freeze
  14 +
13 15 attr_accessor \
14 16 :basic_auth_model,
15 17 :basic_auth_model_authenticatable_attribute,
... ... @@ -21,6 +23,8 @@ module ScimRails
21 23 :scim_users_model,
22 24 :scim_users_scope,
23 25 :scim_user_prevent_update_on_create,
  26 + :signing_secret,
  27 + :signing_algorithm,
24 28 :user_attributes,
25 29 :user_deprovision_method,
26 30 :user_reprovision_method,
... ... @@ -30,6 +34,7 @@ module ScimRails
30 34 @basic_auth_model = "Company"
31 35 @scim_users_list_order = :id
32 36 @scim_users_model = "User"
  37 + @signing_algorithm = ALGO_NONE
33 38 @user_schema = {}
34 39 @user_attributes = []
35 40 end
... ...
lib/scim_rails/encoder.rb 0 โ†’ 100644
... ... @@ -0,0 +1,25 @@
  1 +require "jwt"
  2 +
  3 +module ScimRails
  4 + module Encoder
  5 + extend self
  6 +
  7 + def encode(company)
  8 + payload = {
  9 + iat: Time.current.to_i,
  10 + ScimRails.config.basic_auth_model_searchable_attribute =>
  11 + company.public_send(ScimRails.config.basic_auth_model_searchable_attribute)
  12 + }
  13 +
  14 + JWT.encode(payload, ScimRails.config.signing_secret, ScimRails.config.signing_algorithm)
  15 + end
  16 +
  17 + def decode(token)
  18 + verify = ScimRails.config.signing_algorithm != ScimRails::Config::ALGO_NONE
  19 +
  20 + JWT.decode(token, ScimRails.config.signing_secret, verify, algorithm: ScimRails.config.signing_algorithm).first
  21 + rescue JWT::VerificationError, JWT::DecodeError
  22 + raise ScimRails::ExceptionHandler::InvalidCredentials
  23 + end
  24 + end
  25 +end
... ...
lib/scim_rails/version.rb
1 1 module ScimRails
2   - VERSION = '0.2.2'
  2 + VERSION = '0.3.0'
3 3 end
... ...
scim_rails.gemspec
... ... @@ -18,6 +18,7 @@ Gem::Specification.new do |s|
18 18  
19 19 s.required_ruby_version = "~> 2.4"
20 20 s.add_dependency "rails", ">= 5.0", "< 6.1"
  21 + s.add_runtime_dependency "jwt", "~> 1.5.1"
21 22 s.test_files = Dir["spec/**/*"]
22 23  
23 24 s.add_development_dependency "bundler", "~> 2.0"
... ...
spec/controllers/scim_rails/scim_users_request_spec.rb
... ... @@ -5,7 +5,7 @@ RSpec.describe ScimRails::ScimUsersController, type: :request do
5 5 let(:credentials) { Base64::encode64("#{company.subdomain}:#{company.api_token}") }
6 6 let(:authorization) { "Basic #{credentials}" }
7 7  
8   - def post_request(content_type)
  8 + def post_request(content_type = "application/scim+json")
9 9 # params need to be transformed into a string to test if they are being parsed by Rack
10 10  
11 11 post "/scim_rails/scim/v2/Users",
... ... @@ -48,4 +48,26 @@ RSpec.describe ScimRails::ScimUsersController, type: :request do
48 48 expect(company.users.count).to eq 0
49 49 end
50 50 end
  51 +
  52 + context "OAuth Bearer Authorization" do
  53 + context "with valid token" do
  54 + let(:authorization) { "Bearer #{company.api_token}" }
  55 +
  56 + it "supports OAuth bearer authorization and succeeds" do
  57 + expect { post_request }.to change(company.users, :count).from(0).to(1)
  58 +
  59 + expect(response.status).to eq 201
  60 + end
  61 + end
  62 +
  63 + context "with invalid token" do
  64 + let(:authorization) { "Bearer #{SecureRandom.hex}" }
  65 +
  66 + it "The request fails" do
  67 + expect { post_request }.not_to change(company.users, :count)
  68 +
  69 + expect(response.status).to eq 401
  70 + end
  71 + end
  72 + end
51 73 end
... ...
spec/dummy/config/initializers/scim_rails_config.rb
... ... @@ -7,6 +7,9 @@ ScimRails.configure do |config|
7 7 config.scim_users_scope = :users
8 8 config.scim_users_list_order = :id
9 9  
  10 + config.signing_algorithm = "HS256"
  11 + config.signing_secret = "2d6806dd11c2fece2e81b8ca76dcb0062f5b08e28e3264e8ba1c44bbd3578b70"
  12 +
10 13 config.user_deprovision_method = :archive!
11 14 config.user_reprovision_method = :unarchive!
12 15  
... ...
spec/factories/company.rb
... ... @@ -2,6 +2,9 @@ FactoryBot.define do
2 2 factory :company do
3 3 name { "Test Company" }
4 4 subdomain { "test" }
5   - api_token { "1" }
  5 +
  6 + after(:build) do |company|
  7 + company.api_token = ScimRails::Encoder.encode(company)
  8 + end
6 9 end
7 10 end
... ...
spec/lib/scim_rails/encoder_spec.rb 0 โ†’ 100644
... ... @@ -0,0 +1,62 @@
  1 +require "spec_helper"
  2 +
  3 +describe ScimRails::Encoder do
  4 + let(:company) { Company.new(subdomain: "test") }
  5 +
  6 + describe "::encode" do
  7 + context "with signing configuration" do
  8 + it "generates a signed token with the company attribute" do
  9 + token = ScimRails::Encoder.encode(company)
  10 + payload = ScimRails::Encoder.decode(token)
  11 +
  12 + expect(token).to match /[a-z|A-Z|0-9.]{16,}\.[a-z|A-Z|0-9.]{16,}/
  13 + expect(payload).to contain_exactly(["iat", Integer], ["subdomain", "test"])
  14 + end
  15 + end
  16 +
  17 + context "without signing configuration" do
  18 + before do
  19 + allow(ScimRails.config).to receive(:signing_secret).and_return(nil)
  20 + allow(ScimRails.config).to receive(:signing_algorithm).and_return(ScimRails::Config::ALGO_NONE)
  21 + end
  22 +
  23 + it "generates an unsigned token with the company attribute" do
  24 + token = ScimRails::Encoder.encode(company)
  25 + payload = ScimRails::Encoder.decode(token)
  26 +
  27 + expect(token).to match /[a-z|A-Z|0-9.]{16,}/
  28 + expect(payload).to contain_exactly(["iat", Integer], ["subdomain", "test"])
  29 + end
  30 + end
  31 + end
  32 +
  33 + describe "::decode" do
  34 + let(:token) { ScimRails::Encoder.encode(company) }
  35 +
  36 + it "raises InvalidCredentials error for an invalid token" do
  37 + token = "f487bf84bfub4f74fj4894fnh483f4h4u8f"
  38 + expect { ScimRails::Encoder.decode(token) }.to raise_error ScimRails::ExceptionHandler::InvalidCredentials
  39 + end
  40 +
  41 + context "with signing configuration" do
  42 + it "decodes a signed token, returning the company attributes" do
  43 + payload = ScimRails::Encoder.decode(token)
  44 +
  45 + expect(payload).to contain_exactly(["iat", Integer], ["subdomain", "test"])
  46 + end
  47 + end
  48 +
  49 + context "without signing configuration" do
  50 + before do
  51 + allow(ScimRails.config).to receive(:signing_secret).and_return(nil)
  52 + allow(ScimRails.config).to receive(:signing_algorithm).and_return(ScimRails::Config::ALGO_NONE)
  53 + end
  54 +
  55 + it "decodes an unsigned token, returning the company attributes" do
  56 + payload = ScimRails::Encoder.decode(token)
  57 +
  58 + expect(payload).to contain_exactly(["iat", Integer], ["subdomain", "test"])
  59 + end
  60 + end
  61 + end
  62 +end
... ...
spec/support/scim_rails_config.rb
... ... @@ -10,6 +10,9 @@ ScimRails.configure do |config|
10 10 config.scim_users_scope = :users
11 11 config.scim_users_list_order = :id
12 12  
  13 + config.signing_algorithm = "HS256"
  14 + config.signing_secret = "2d6806dd11c2fece2e81b8ca76dcb0062f5b08e28e3264e8ba1c44bbd3578b70"
  15 +
13 16 config.user_deprovision_method = :archive!
14 17 config.user_reprovision_method = :unarchive!
15 18  
... ...