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
1 PATH 1 PATH
2 remote: . 2 remote: .
3 specs: 3 specs:
4 - scim_rails (0.2.1) 4 + scim_rails (0.3.0)
  5 + jwt (~> 2.2)
5 rails (>= 5.0, < 6.1) 6 rails (>= 5.0, < 6.1)
6 7
7 GEM 8 GEM
@@ -77,6 +78,7 @@ GEM @@ -77,6 +78,7 @@ GEM
77 activesupport (>= 4.2.0) 78 activesupport (>= 4.2.0)
78 i18n (1.6.0) 79 i18n (1.6.0)
79 concurrent-ruby (~> 1.0) 80 concurrent-ruby (~> 1.0)
  81 + jwt (2.2.1)
80 loofah (2.3.1) 82 loofah (2.3.1)
81 crass (~> 1.0.2) 83 crass (~> 1.0.2)
82 nokogiri (>= 1.5.9) 84 nokogiri (>= 1.5.9)
@@ -175,4 +177,4 @@ RUBY VERSION @@ -175,4 +177,4 @@ RUBY VERSION
175 ruby 2.4.4p296 177 ruby 2.4.4p296
176 178
177 BUNDLED WITH 179 BUNDLED WITH
178 - 2.0.1 180 + 2.0.2
@@ -78,6 +78,57 @@ When sending requests to the server the `Content-Type` should be set to `applica @@ -78,6 +78,57 @@ When sending requests to the server the `Content-Type` should be set to `applica
78 78
79 All responses will be sent with a `Content-Type` of `application/scim+json`. 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 ### List 132 ### List
82 133
83 ##### All 134 ##### All
app/controllers/scim_rails/application_controller.rb
@@ -9,14 +9,30 @@ module ScimRails @@ -9,14 +9,30 @@ module ScimRails
9 private 9 private
10 10
11 def authorize_request 11 def authorize_request
12 - authenticate_with_http_basic do |username, password| 12 + send(authentication_strategy) do |searchable_attribute, authentication_attribute|
13 authorization = AuthorizeApiRequest.new( 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 @company = authorization.company 17 @company = authorization.company
18 end 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 end 36 end
21 end 37 end
22 end 38 end
lib/generators/scim_rails/templates/initializer.rb
@@ -22,6 +22,18 @@ ScimRails.configure do |config| @@ -22,6 +22,18 @@ ScimRails.configure do |config|
22 # or throws an error (returning 409 Conflict in accordance with SCIM spec) 22 # or throws an error (returning 409 Conflict in accordance with SCIM spec)
23 config.scim_user_prevent_update_on_create = false 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 # Default sort order for pagination is by id. If you 37 # Default sort order for pagination is by id. If you
26 # use non sequential ids for user records, uncomment 38 # use non sequential ids for user records, uncomment
27 # the below line and configure a determinate order. 39 # the below line and configure a determinate order.
lib/scim_rails.rb
1 require "scim_rails/engine" 1 require "scim_rails/engine"
2 require "scim_rails/config" 2 require "scim_rails/config"
  3 +require "scim_rails/encoder"
3 4
4 module ScimRails 5 module ScimRails
5 end 6 end
lib/scim_rails/config.rb
@@ -10,6 +10,8 @@ module ScimRails @@ -10,6 +10,8 @@ module ScimRails
10 end 10 end
11 11
12 class Config 12 class Config
  13 + ALGO_NONE = "none".freeze
  14 +
13 attr_accessor \ 15 attr_accessor \
14 :basic_auth_model, 16 :basic_auth_model,
15 :basic_auth_model_authenticatable_attribute, 17 :basic_auth_model_authenticatable_attribute,
@@ -21,6 +23,8 @@ module ScimRails @@ -21,6 +23,8 @@ module ScimRails
21 :scim_users_model, 23 :scim_users_model,
22 :scim_users_scope, 24 :scim_users_scope,
23 :scim_user_prevent_update_on_create, 25 :scim_user_prevent_update_on_create,
  26 + :signing_secret,
  27 + :signing_algorithm,
24 :user_attributes, 28 :user_attributes,
25 :user_deprovision_method, 29 :user_deprovision_method,
26 :user_reprovision_method, 30 :user_reprovision_method,
@@ -30,6 +34,7 @@ module ScimRails @@ -30,6 +34,7 @@ module ScimRails
30 @basic_auth_model = "Company" 34 @basic_auth_model = "Company"
31 @scim_users_list_order = :id 35 @scim_users_list_order = :id
32 @scim_users_model = "User" 36 @scim_users_model = "User"
  37 + @signing_algorithm = ALGO_NONE
33 @user_schema = {} 38 @user_schema = {}
34 @user_attributes = [] 39 @user_attributes = []
35 end 40 end
lib/scim_rails/encoder.rb 0 → 100644
@@ -0,0 +1,25 @@ @@ -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 module ScimRails 1 module ScimRails
2 - VERSION = '0.2.2' 2 + VERSION = '0.3.0'
3 end 3 end
scim_rails.gemspec
@@ -18,6 +18,7 @@ Gem::Specification.new do |s| @@ -18,6 +18,7 @@ Gem::Specification.new do |s|
18 18
19 s.required_ruby_version = "~> 2.4" 19 s.required_ruby_version = "~> 2.4"
20 s.add_dependency "rails", ">= 5.0", "< 6.1" 20 s.add_dependency "rails", ">= 5.0", "< 6.1"
  21 + s.add_runtime_dependency "jwt", "~> 1.5.1"
21 s.test_files = Dir["spec/**/*"] 22 s.test_files = Dir["spec/**/*"]
22 23
23 s.add_development_dependency "bundler", "~> 2.0" 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,7 +5,7 @@ RSpec.describe ScimRails::ScimUsersController, type: :request do
5 let(:credentials) { Base64::encode64("#{company.subdomain}:#{company.api_token}") } 5 let(:credentials) { Base64::encode64("#{company.subdomain}:#{company.api_token}") }
6 let(:authorization) { "Basic #{credentials}" } 6 let(:authorization) { "Basic #{credentials}" }
7 7
8 - def post_request(content_type) 8 + def post_request(content_type = "application/scim+json")
9 # params need to be transformed into a string to test if they are being parsed by Rack 9 # params need to be transformed into a string to test if they are being parsed by Rack
10 10
11 post "/scim_rails/scim/v2/Users", 11 post "/scim_rails/scim/v2/Users",
@@ -48,4 +48,26 @@ RSpec.describe ScimRails::ScimUsersController, type: :request do @@ -48,4 +48,26 @@ RSpec.describe ScimRails::ScimUsersController, type: :request do
48 expect(company.users.count).to eq 0 48 expect(company.users.count).to eq 0
49 end 49 end
50 end 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 end 73 end
spec/dummy/config/initializers/scim_rails_config.rb
@@ -7,6 +7,9 @@ ScimRails.configure do |config| @@ -7,6 +7,9 @@ ScimRails.configure do |config|
7 config.scim_users_scope = :users 7 config.scim_users_scope = :users
8 config.scim_users_list_order = :id 8 config.scim_users_list_order = :id
9 9
  10 + config.signing_algorithm = "HS256"
  11 + config.signing_secret = "2d6806dd11c2fece2e81b8ca76dcb0062f5b08e28e3264e8ba1c44bbd3578b70"
  12 +
10 config.user_deprovision_method = :archive! 13 config.user_deprovision_method = :archive!
11 config.user_reprovision_method = :unarchive! 14 config.user_reprovision_method = :unarchive!
12 15
spec/factories/company.rb
@@ -2,6 +2,9 @@ FactoryBot.define do @@ -2,6 +2,9 @@ FactoryBot.define do
2 factory :company do 2 factory :company do
3 name { "Test Company" } 3 name { "Test Company" }
4 subdomain { "test" } 4 subdomain { "test" }
5 - api_token { "1" } 5 +
  6 + after(:build) do |company|
  7 + company.api_token = ScimRails::Encoder.encode(company)
  8 + end
6 end 9 end
7 end 10 end
spec/lib/scim_rails/encoder_spec.rb 0 → 100644
@@ -0,0 +1,62 @@ @@ -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,6 +10,9 @@ ScimRails.configure do |config|
10 config.scim_users_scope = :users 10 config.scim_users_scope = :users
11 config.scim_users_list_order = :id 11 config.scim_users_list_order = :id
12 12
  13 + config.signing_algorithm = "HS256"
  14 + config.signing_secret = "2d6806dd11c2fece2e81b8ca76dcb0062f5b08e28e3264e8ba1c44bbd3578b70"
  15 +
13 config.user_deprovision_method = :archive! 16 config.user_deprovision_method = :archive!
14 config.user_reprovision_method = :unarchive! 17 config.user_reprovision_method = :unarchive!
15 18