publishing_platform_sso 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 9625f245d4f4954072d35c039d5534cfce0e6c518c55346a33f20a05bf53dab7
4
+ data.tar.gz: a05808b7b7fa6a98599a7b30e842f433115d8228e9d6595c6aa8dec872cdb309
5
+ SHA512:
6
+ metadata.gz: e5f0149ff796a41ca14dee9babee288513ba9da2de5ea72c0423a02584a7b2ab76e49e60799d59838229bc9f2b129b8d9f81a22eace9ab5f1f5fb5f9af1aed77
7
+ data.tar.gz: fab81cc20834dec39d953adfe35d525ddd9e497a00ba55cf4896a03edc5dcbc484cdfce05c496439b1793f4115d9d2150bf3db839534160c5d273de9997f5c1b
data/Gemfile ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ # Specify your gem's dependencies in publishing_platform_sso.gemspec
6
+ gemspec
7
+
8
+ gem "rake", "~> 13.0"
9
+
10
+ gem "rspec", "~> 3.0"
data/README.md ADDED
@@ -0,0 +1,156 @@
1
+ # publishing_platform_sso
2
+
3
+ This gem provides everything needed to integrate an application with [Signon](https://github.com/publishing-platform/signon). It's a wrapper around [OmniAuth](https://github.com/intridea/omniauth) that adds a 'strategy' for oAuth2 integration against Signon,
4
+ and the necessary controller to support that request flow.
5
+
6
+
7
+ ## Usage
8
+
9
+ ### Integration with a Rails 4+ app
10
+
11
+ - Include the gem in your Gemfile:
12
+
13
+ ```ruby
14
+ gem 'publishing_platform_sso'
15
+ ```
16
+
17
+ - Create a "users" table in the database. Example migration:
18
+
19
+ ```ruby
20
+ class CreateUsers < ActiveRecord::Migration[7.1]
21
+ def change
22
+ create_table :users do |t|
23
+ t.string :name
24
+ t.string :email
25
+ t.string :uid
26
+ t.string :organisation_slug
27
+ t.string :organisation_content_id
28
+ t.string :app_name # api only
29
+ t.text :permissions
30
+ t.boolean :disabled, default: false
31
+
32
+ t.timestamps
33
+ end
34
+ end
35
+ end
36
+ ```
37
+
38
+ - Create a User model with the following:
39
+
40
+ ```ruby
41
+ serialize :permissions, Array
42
+ ```
43
+
44
+ - Add to your `ApplicationController`:
45
+
46
+ ```ruby
47
+ include PublishingPlatform::SSO::ControllerMethods
48
+ before_action :authenticate_user!
49
+ ```
50
+
51
+ ### Securing your application
52
+
53
+ [PublishingPlatform::SSO::ControllerMethods](/lib/publishing_platform_sso/controller_methods.rb) provides some useful methods for your application controllers.
54
+
55
+ To make sure that only people with a signon account and permission to use your app are allowed in use `authenticate_user!`.
56
+
57
+ ```ruby
58
+ class ApplicationController < ActionController::Base
59
+ include PublishingPlatform::SSO::ControllerMethods
60
+ before_action :authenticate_user!
61
+ # ...
62
+ end
63
+ ```
64
+
65
+ You can refine authorisation to specific controller actions based on permissions using `authorise_user!`. All permissions are assigned via Signon.
66
+
67
+ ```ruby
68
+ class PublicationsController < ActionController::Base
69
+ include PublishingPlatform::SSO::ControllerMethods
70
+ before_action :authorise_for_editing!, except: [:show, :index]
71
+ # ...
72
+ private
73
+ def authorise_for_editing!
74
+ authorise_user!('edit_publications')
75
+ end
76
+ end
77
+ ```
78
+
79
+ `authorise_user!` can be configured to check for multiple permissions:
80
+
81
+ ```ruby
82
+ # fails unless the user has at least one of these permissions
83
+ authorise_user!(any_of: %w(edit create))
84
+
85
+ # fails unless the user has both of these permissions
86
+ authorise_user!(all_of: %w(edit create))
87
+ ```
88
+
89
+ The signon application makes sure that only users who have been granted access to the application can access it (e.g. they have the `signin` permission for your app).
90
+
91
+ ### Authorisation for API Users
92
+
93
+ In addition to the single-sign-on strategy, this gem also allows authorisation
94
+ via a "bearer token". This is used by publishing applications to be authorised
95
+ as an API user.
96
+
97
+ To authorise with a bearer token, a request has to be made with the header:
98
+
99
+ ```
100
+ Authorization: Bearer your-token-here
101
+ ```
102
+
103
+ To avoid making these requests for each incoming request, this gem will [automatically cache a successful response](/lib/publishing_platform_sso/bearer_token.rb), using the [Rails cache](/lib/publishing_platform_sso/railtie.rb).
104
+
105
+ If you are using a Rails app in
106
+ [api_only](http://guides.rubyonrails.org/api_app.html) mode this gem will
107
+ automatically disable the oauth layers which use session persistence. You can
108
+ configure this gem to be in api_only mode (or not) with:
109
+
110
+ ```ruby
111
+ PublishingPlatform::SSO.config do |config|
112
+ # ...
113
+ # Only support bearer token authentication and send responses in JSON
114
+ config.api_only = true
115
+ end
116
+ ```
117
+
118
+ ### Use in production mode
119
+
120
+ To use publishing_platform_sso in production you will need to setup the following environment variables, which we look for in [the config](/lib/publishing_platform_sso/config.rb). You will need to have admin access to Signon to get these.
121
+
122
+ - PUBLISHING_PLATFORM_SSO_OAUTH_ID
123
+ - PUBLISHING_PLATFORM_SSO_OAUTH_SECRET
124
+
125
+ ### Use in development mode
126
+
127
+ In development, you generally want to be able to run an application without needing to run your own SSO server as well. publishing_platform_sso facilitates this by using a 'mock' mode in development. Mock mode loads an arbitrary user from the local application's user tables:
128
+
129
+ ```ruby
130
+ PublishingPlatform::SSO.test_user || PublishingPlatform::SSO::Config.user_klass.first
131
+ ```
132
+
133
+ To make it use a real strategy (e.g. if you're testing an app against the signon server), set the following environment variable when you run your app:
134
+
135
+ ```
136
+ PUBLISHING_PLATFORM_SSO_STRATEGY=real
137
+ ```
138
+
139
+ ### Extra permissions for api users
140
+
141
+ By default the mock strategies will create a user with `signin` permission.
142
+
143
+ If your application needs different or extra permissions for access, you can specify this by adding the following to your config:
144
+
145
+ ```ruby
146
+ PublishingPlatform::SSO.config do |config|
147
+ # other config here
148
+ config.additional_mock_permissions_required = ["array", "of", "permissions"]
149
+ end
150
+ ```
151
+
152
+ The mock bearer token will then ensure that the dummy api user has the required permission.
153
+
154
+ ## Licence
155
+
156
+ [MIT License](LICENSE)
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
@@ -0,0 +1,17 @@
1
+ class AuthenticationsController < ActionController::Base
2
+ include PublishingPlatform::SSO::ControllerMethods
3
+
4
+ before_action :authenticate_user!, only: :callback
5
+ layout false
6
+
7
+ def callback
8
+ redirect_to session["return_to"] || "/"
9
+ end
10
+
11
+ def failure; end
12
+
13
+ def sign_out
14
+ logout
15
+ redirect_to "#{PublishingPlatform::SSO::Config.oauth_root_url}/users/sign_out", allow_other_host: true
16
+ end
17
+ end
@@ -0,0 +1,3 @@
1
+ <h1>Error</h1>
2
+
3
+ <p>There was a problem logging you into the application. Please <%= link_to "try again", "/auth/publishing_platform" %>.</p>
@@ -0,0 +1,5 @@
1
+ <h1><%= message %></h1>
2
+
3
+ <p>Please contact your Delivery Manager or main Publishing Platform contact if you think you should be able to do what you tried to do.</p>
4
+
5
+ <p>If you think something is wrong, try <%= link_to "signing out", publishing_platform_sign_out_path %> and then back in</p>
data/config/routes.rb ADDED
@@ -0,0 +1,7 @@
1
+ Rails.application.routes.draw do
2
+ next if PublishingPlatform::SSO::Config.api_only
3
+
4
+ get "/auth/publishing_platform/callback", to: "authentications#callback", as: :publishing_platform_sign_in
5
+ get "/auth/publishing_platform/sign_out", to: "authentications#sign_out", as: :publishing_platform_sign_out
6
+ get "/auth/failure", to: "authentications#failure", as: :auth_failure
7
+ end
@@ -0,0 +1,28 @@
1
+ require "omniauth-oauth2"
2
+ require "json"
3
+
4
+ class OmniAuth::Strategies::PublishingPlatform < OmniAuth::Strategies::OAuth2
5
+ uid { user["uid"] }
6
+
7
+ option :pkce, true
8
+
9
+ info do
10
+ {
11
+ name: user["name"],
12
+ email: user["email"],
13
+ }
14
+ end
15
+
16
+ extra do
17
+ {
18
+ user:,
19
+ permissions: user["permissions"],
20
+ organisation_slug: user["organisation_slug"],
21
+ organisation_content_id: user["organisation_content_id"],
22
+ }
23
+ end
24
+
25
+ def user
26
+ @user ||= JSON.parse(access_token.get("/user.json?client_id=#{CGI.escape(options.client_id)}").body).fetch("user")
27
+ end
28
+ end
@@ -0,0 +1,9 @@
1
+ module PublishingPlatform
2
+ module SSO
3
+ class ApiAccess
4
+ def self.api_call?(env)
5
+ env["HTTP_AUTHORIZATION"].to_s =~ /\ABearer /
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,49 @@
1
+ module PublishingPlatform
2
+ module SSO
3
+ class AuthoriseUser
4
+ def self.call(...) = new(...).call
5
+
6
+ def initialize(current_user, permissions)
7
+ @current_user = current_user
8
+ @permissions = permissions
9
+ end
10
+
11
+ def call
12
+ case permissions
13
+ when String
14
+ unless current_user.has_permission?(permissions)
15
+ raise PublishingPlatform::SSO::PermissionDeniedError, "Sorry, you don't seem to have the #{permissions} permission for this app."
16
+ end
17
+ when Hash
18
+ raise ArgumentError, "Must be either `any_of` or `all_of`" unless permissions.keys.size == 1
19
+
20
+ if permissions[:any_of]
21
+ authorise_user_with_at_least_one_of_permissions!(permissions[:any_of])
22
+ elsif permissions[:all_of]
23
+ authorise_user_with_all_permissions!(permissions[:all_of])
24
+ else
25
+ raise ArgumentError, "Must be either `any_of` or `all_of`"
26
+ end
27
+ end
28
+ end
29
+
30
+ private
31
+
32
+ attr_reader :current_user, :permissions
33
+
34
+ def authorise_user_with_at_least_one_of_permissions!(permissions)
35
+ if permissions.none? { |permission| current_user.has_permission?(permission) }
36
+ raise PublishingPlatform::SSO::PermissionDeniedError,
37
+ "Sorry, you don't seem to have any of the permissions: #{permissions.to_sentence} for this app."
38
+ end
39
+ end
40
+
41
+ def authorise_user_with_all_permissions!(permissions)
42
+ unless permissions.all? { |permission| current_user.has_permission?(permission) }
43
+ raise PublishingPlatform::SSO::PermissionDeniedError,
44
+ "Sorry, you don't seem to have all of the permissions: #{permissions.to_sentence} for this app."
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,75 @@
1
+ require "json"
2
+ require "oauth2"
3
+
4
+ module PublishingPlatform
5
+ module SSO
6
+ module BearerToken
7
+ def self.locate(token_string)
8
+ user_details = PublishingPlatform::SSO::Config.cache.fetch(["api-user-cache", token_string], expires_in: 5.minutes) do
9
+ access_token = OAuth2::AccessToken.new(oauth_client, token_string)
10
+ response_body = access_token.get("/user.json?client_id=#{CGI.escape(PublishingPlatform::SSO::Config.oauth_id)}").body
11
+ omniauth_style_response(response_body)
12
+ end
13
+
14
+ PublishingPlatform::SSO::Config.user_klass.find_for_oauth(user_details)
15
+ rescue OAuth2::Error
16
+ nil
17
+ end
18
+
19
+ def self.oauth_client
20
+ @oauth_client ||= OAuth2::Client.new(
21
+ PublishingPlatform::SSO::Config.oauth_id,
22
+ PublishingPlatform::SSO::Config.oauth_secret,
23
+ site: PublishingPlatform::SSO::Config.oauth_root_url,
24
+ connection_opts: {
25
+ headers: {
26
+ user_agent: "publishing_platform_sso/#{PublishingPlatform::SSO::VERSION} (#{ENV['PUBLISHING_PLATFORM_APP_NAME']})",
27
+ },
28
+ }.merge(PublishingPlatform::SSO::Config.connection_opts),
29
+ )
30
+ end
31
+
32
+ # Our User code assumes we're getting our user data back
33
+ # via omniauth and so receiving it in omniauth's preferred
34
+ # structure. Here we're addressing signon directly so
35
+ # we need to transform the response ourselves.
36
+ def self.omniauth_style_response(response_body)
37
+ input = JSON.parse(response_body).fetch("user")
38
+
39
+ {
40
+ "uid" => input["uid"],
41
+ "info" => {
42
+ "email" => input["email"],
43
+ "name" => input["name"],
44
+ },
45
+ "extra" => {
46
+ "user" => {
47
+ "permissions" => input["permissions"],
48
+ "organisation_slug" => input["organisation_slug"],
49
+ "organisation_content_id" => input["organisation_content_id"],
50
+ },
51
+ },
52
+ }
53
+ end
54
+ end
55
+
56
+ module MockBearerToken
57
+ def self.locate(_token_string)
58
+ dummy_api_user = PublishingPlatform::SSO.test_user || PublishingPlatform::SSO::Config.user_klass.where(email: "dummyapiuser@domain.com").first
59
+ if dummy_api_user.nil?
60
+ dummy_api_user = PublishingPlatform::SSO::Config.user_klass.new
61
+ dummy_api_user.email = "dummyapiuser@domain.com"
62
+ dummy_api_user.uid = rand(10_000).to_s
63
+ dummy_api_user.name = "Dummy API user created by publishing_platform_sso"
64
+ end
65
+
66
+ unless dummy_api_user.has_all_permissions?(PublishingPlatform::SSO::Config.permissions_for_dummy_api_user)
67
+ dummy_api_user.permissions = PublishingPlatform::SSO::Config.permissions_for_dummy_api_user
68
+ end
69
+
70
+ dummy_api_user.save!
71
+ dummy_api_user
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,64 @@
1
+ require "active_support/cache/null_store"
2
+
3
+ module PublishingPlatform
4
+ module SSO
5
+ module Config
6
+ # rubocop:disable Style/ClassVars
7
+
8
+ # Name of the User class
9
+ mattr_accessor :user_model
10
+ @@user_model = "User"
11
+
12
+ # OAuth ID
13
+ mattr_accessor :oauth_id
14
+ @@oauth_id = ENV.fetch("PUBLISHING_PLATFORM_SSO_OAUTH_ID", "test-oauth-id")
15
+
16
+ # OAuth Secret
17
+ mattr_accessor :oauth_secret
18
+ @@oauth_secret = ENV.fetch("PUBLISHING_PLATFORM_SSO_OAUTH_SECRET", "test-oauth-secret")
19
+
20
+ # Location of the OAuth server
21
+ mattr_accessor :oauth_root_url
22
+ @@oauth_root_url = "http://signon.dev.publishing-platform.co.uk" # Plek.new.external_url_for("signon") # TODO: need to implement this functionality
23
+
24
+ mattr_accessor :auth_valid_for
25
+ @@auth_valid_for = 20 * 3600
26
+
27
+ mattr_accessor :cache
28
+
29
+ mattr_accessor :api_only
30
+
31
+ mattr_accessor :intercept_401_responses
32
+ @@intercept_401_responses = true
33
+
34
+ mattr_accessor :additional_mock_permissions_required
35
+
36
+ mattr_accessor :connection_opts
37
+ @@connection_opts = {
38
+ request: {
39
+ open_timeout: 5,
40
+ },
41
+ }
42
+
43
+ def self.permissions_for_dummy_api_user
44
+ %w[signin].push(*additional_mock_permissions_required)
45
+ end
46
+
47
+ def self.user_klass
48
+ user_model.to_s.constantize
49
+ end
50
+
51
+ def self.use_mock_strategies?
52
+ default_strategy = if %w[development test].include?(Rails.env)
53
+ "mock"
54
+ else
55
+ "real"
56
+ end
57
+
58
+ ENV.fetch("PUBLISHING_PLATFORM_SSO_STRATEGY", default_strategy) == "mock"
59
+ end
60
+
61
+ # rubocop:enable Style/ClassVars
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,48 @@
1
+ module PublishingPlatform
2
+ module SSO
3
+ module ControllerMethods
4
+ def self.included(base)
5
+ base.rescue_from PermissionDeniedError do |e|
6
+ if PublishingPlatform::SSO::Config.api_only
7
+ render json: { message: e.message }, status: :forbidden
8
+ else
9
+ render "authorisations/unauthorised", status: :forbidden, locals: { message: e.message }
10
+ end
11
+ end
12
+
13
+ unless PublishingPlatform::SSO::Config.api_only
14
+ base.helper_method :user_signed_in?
15
+ base.helper_method :current_user
16
+ end
17
+ end
18
+
19
+ def authorise_user!(permissions)
20
+ # Ensure that we're authenticated (and by extension that current_user is set).
21
+ # Otherwise current_user might be nil, and we'd error out
22
+ authenticate_user!
23
+
24
+ PublishingPlatform::SSO::AuthoriseUser.call(current_user, permissions)
25
+ end
26
+
27
+ def authenticate_user!
28
+ warden.authenticate!
29
+ end
30
+
31
+ def user_signed_in?
32
+ warden && warden.authenticated?
33
+ end
34
+
35
+ def current_user
36
+ warden.user if user_signed_in?
37
+ end
38
+
39
+ def logout
40
+ warden.logout
41
+ end
42
+
43
+ def warden
44
+ request.env["warden"]
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,6 @@
1
+ module PublishingPlatform
2
+ module SSO
3
+ class PermissionDeniedError < StandardError
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,59 @@
1
+ require "action_controller/metal"
2
+ require "rails"
3
+
4
+ # Failure application that will be called every time :warden is thrown from
5
+ # any strategy or hook.
6
+ module PublishingPlatform
7
+ module SSO
8
+ class FailureApp < ActionController::Metal
9
+ include ActionController::UrlFor
10
+ include ActionController::Redirecting
11
+ include AbstractController::Rendering
12
+ include ActionController::Rendering
13
+ include ActionController::Renderers
14
+ use_renderers :json
15
+
16
+ include Rails.application.routes.url_helpers
17
+
18
+ def self.call(env)
19
+ if PublishingPlatform::SSO::ApiAccess.api_call?(env)
20
+ action(:api_invalid_token).call(env)
21
+ elsif PublishingPlatform::SSO::Config.api_only
22
+ action(:api_missing_token).call(env)
23
+ else
24
+ action(:redirect).call(env)
25
+ end
26
+ end
27
+
28
+ def redirect
29
+ store_location!
30
+ redirect_to "/auth/publishing_platform"
31
+ end
32
+
33
+ def api_invalid_token
34
+ api_unauthorized("Bearer token does not appear to be valid", "invalid_token")
35
+ end
36
+
37
+ def api_missing_token
38
+ api_unauthorized("No bearer token was provided", "invalid_request")
39
+ end
40
+
41
+ # Stores requested uri to redirect the user after signing in. We cannot use
42
+ # scoped session provided by warden here, since the user is not authenticated
43
+ # yet, but we still need to store the uri based on scope, so different scopes
44
+ # would never use the same uri to redirect.
45
+
46
+ # TOTALLY NOT DOING THE SCOPE THING. PROBABLY SHOULD.
47
+ def store_location!
48
+ session["return_to"] = request.env["warden.options"][:attempted_path] if request.get?
49
+ end
50
+
51
+ private
52
+
53
+ def api_unauthorized(message, bearer_error)
54
+ headers["WWW-Authenticate"] = %(Bearer error="#{bearer_error}")
55
+ render json: { message: }, status: :unauthorized
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,17 @@
1
+ module PublishingPlatform
2
+ module SSO
3
+ class Railtie < Rails::Railtie
4
+ config.action_dispatch.rescue_responses.merge!(
5
+ "PublishingPlatform::SSO::PermissionDeniedError" => :forbidden,
6
+ )
7
+
8
+ initializer "publishing_platform_sso.initializer" do
9
+ PublishingPlatform::SSO.config do |config|
10
+ config.cache = Rails.cache
11
+ config.api_only = Rails.configuration.api_only
12
+ end
13
+ OmniAuth.config.logger = Rails.logger
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,50 @@
1
+ require "active_support/concern"
2
+
3
+ module PublishingPlatform
4
+ module SSO
5
+ module User
6
+ extend ActiveSupport::Concern
7
+
8
+ def has_permission?(permission)
9
+ if permissions
10
+ permissions.include?(permission)
11
+ end
12
+ end
13
+
14
+ def has_all_permissions?(required_permissions)
15
+ if permissions
16
+ required_permissions.all? do |required_permission|
17
+ permissions.include?(required_permission)
18
+ end
19
+ end
20
+ end
21
+
22
+ def self.user_params_from_auth_hash(auth_hash)
23
+ {
24
+ "uid" => auth_hash["uid"],
25
+ "email" => auth_hash["info"]["email"],
26
+ "name" => auth_hash["info"]["name"],
27
+ "permissions" => auth_hash["extra"]["user"]["permissions"],
28
+ "organisation_slug" => auth_hash["extra"]["user"]["organisation_slug"],
29
+ "organisation_content_id" => auth_hash["extra"]["user"]["organisation_content_id"],
30
+ "disabled" => auth_hash["extra"]["user"]["disabled"],
31
+ }
32
+ end
33
+
34
+ module ClassMethods
35
+ def find_for_oauth(auth_hash)
36
+ user_params = PublishingPlatform::SSO::User.user_params_from_auth_hash(auth_hash.to_hash)
37
+ user = where(uid: user_params["uid"]).first ||
38
+ where(email: user_params["email"]).first
39
+
40
+ if user
41
+ user.update!(user_params)
42
+ user
43
+ else # Create a new user.
44
+ create!(user_params)
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PublishingPlatform
4
+ module SSO
5
+ VERSION = "0.2.0"
6
+ end
7
+ end
@@ -0,0 +1,81 @@
1
+ require "warden"
2
+ require "warden-oauth2"
3
+ require "publishing_platform_sso/bearer_token"
4
+
5
+ def logger
6
+ Rails.logger || env["rack.logger"]
7
+ end
8
+
9
+ Warden::Manager.serialize_into_session do |user|
10
+ if user.respond_to?(:uid) && user.uid
11
+ [user.uid, Time.now.utc.iso8601]
12
+ end
13
+ end
14
+
15
+ Warden::Manager.serialize_from_session do |(uid, auth_timestamp)|
16
+ # This will reject old sessions that don't have a previous login timestamp
17
+ if auth_timestamp.is_a?(String)
18
+ begin
19
+ auth_timestamp = Time.parse(auth_timestamp)
20
+ rescue ArgumentError
21
+ auth_timestamp = nil
22
+ end
23
+ end
24
+
25
+ if auth_timestamp && ((auth_timestamp + PublishingPlatform::SSO::Config.auth_valid_for) > Time.now.utc)
26
+ PublishingPlatform::SSO::Config.user_klass.where(uid:).first
27
+ end
28
+ end
29
+
30
+ Warden::Strategies.add(:publishing_platform_sso) do
31
+ def valid?
32
+ !::PublishingPlatform::SSO::ApiAccess.api_call?(env)
33
+ end
34
+
35
+ def authenticate!
36
+ logger.debug("Authenticating with publishing_platform_sso strategy")
37
+
38
+ if request.env["omniauth.auth"].nil?
39
+ fail!("No credentials, bub")
40
+ else
41
+ user = prep_user(request.env["omniauth.auth"])
42
+ success!(user)
43
+ end
44
+ end
45
+
46
+ private
47
+
48
+ def prep_user(auth_hash)
49
+ user = publishing_platform_sso::SSO::Config.user_klass.find_for_oauth(auth_hash)
50
+ fail!("Couldn't process credentials") unless user
51
+ user
52
+ end
53
+ end
54
+
55
+ Warden::OAuth2.configure do |config|
56
+ config.token_model = PublishingPlatform::SSO::Config.use_mock_strategies? ? PublishingPlatform::SSO::MockBearerToken : PublishingPlatform::SSO::BearerToken
57
+ end
58
+ Warden::Strategies.add(:publishing_platform_bearer_token, Warden::OAuth2::Strategies::Bearer)
59
+
60
+ Warden::Strategies.add(:mock_publishing_platform_sso) do
61
+ def valid?
62
+ !::PublishingPlatform::SSO::ApiAccess.api_call?(env)
63
+ end
64
+
65
+ def authenticate!
66
+ logger.warn("Authenticating with mock_publishing_platform_sso strategy")
67
+
68
+ test_user = PublishingPlatform::SSO.test_user
69
+ test_user ||= PublishingPlatform::SSO::Config.user_klass.first
70
+ if test_user
71
+ # Brute force ensure test user has correct perms to signin
72
+ unless test_user.has_permission?("signin")
73
+ permissions = test_user.permissions || []
74
+ test_user.update_attribute(:permissions, permissions << "signin")
75
+ end
76
+ success!(test_user)
77
+ else
78
+ raise "publishing_platform_sso running in mock mode and no test user found. Normally we'd load the first user in the database. Create a user in the database."
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails"
4
+
5
+ require "publishing_platform_sso/config"
6
+ require "publishing_platform_sso/version"
7
+ require "publishing_platform_sso/warden_config"
8
+ require "omniauth"
9
+ require "omniauth/strategies/publishing_platform"
10
+
11
+ require "publishing_platform_sso/railtie" if defined?(Rails)
12
+
13
+ module PublishingPlatform
14
+ module SSO
15
+ autoload :FailureApp, "publishing_platform_sso/failure_app"
16
+ autoload :ControllerMethods, "publishing_platform_sso/controller_methods"
17
+ autoload :User, "publishing_platform_sso/user"
18
+ autoload :ApiAccess, "publishing_platform_sso/api_access"
19
+ autoload :AuthoriseUser, "publishing_platform_sso/authorise_user"
20
+ autoload :PermissionDeniedError, "publishing_platform_sso/errors"
21
+
22
+ # User to return as logged in during tests
23
+ mattr_accessor :test_user
24
+
25
+ def self.config
26
+ yield PublishingPlatform::SSO::Config
27
+ end
28
+
29
+ class Engine < ::Rails::Engine
30
+ # Force routes to be loaded if we are doing any eager load.
31
+ # TODO - check this one - Stolen from Devise because it looked sensible...
32
+ config.before_eager_load(&:reload_routes!)
33
+
34
+ OmniAuth.config.allowed_request_methods = %i[post get]
35
+
36
+ config.app_middleware.use ::OmniAuth::Builder do
37
+ next if PublishingPlatform::SSO::Config.api_only
38
+
39
+ provider :publishing_platform, PublishingPlatform::SSO::Config.oauth_id, PublishingPlatform::SSO::Config.oauth_secret,
40
+ client_options: {
41
+ site: PublishingPlatform::SSO::Config.oauth_root_url,
42
+ authorize_url: "#{PublishingPlatform::SSO::Config.oauth_root_url}/oauth/authorize",
43
+ token_url: "#{PublishingPlatform::SSO::Config.oauth_root_url}/oauth/access_token",
44
+ connection_opts: {
45
+ headers: {
46
+ user_agent: "publishing_platform_sso/#{PublishingPlatform::SSO::VERSION} (#{ENV['PUBLISHING_PLATFORM_APP_NAME']})",
47
+ },
48
+ },
49
+ }
50
+ end
51
+
52
+ def self.default_strategies
53
+ Config.use_mock_strategies? ? %i[mock_publishing_platform_sso publishing_platform_bearer_token] : %i[publishing_platform_sso publishing_platform_bearer_token]
54
+ end
55
+
56
+ config.app_middleware.use Warden::Manager do |config|
57
+ config.default_strategies(*default_strategies)
58
+ config.failure_app = PublishingPlatform::SSO::FailureApp
59
+ config.intercept_401 = PublishingPlatform::SSO::Config.intercept_401_responses
60
+ end
61
+ end
62
+ end
63
+ end
metadata ADDED
@@ -0,0 +1,160 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: publishing_platform_sso
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.0
5
+ platform: ruby
6
+ authors:
7
+ - Publishing Platform
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2024-05-31 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: oauth2
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: omniauth
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '2.1'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '2.1'
41
+ - !ruby/object:Gem::Dependency
42
+ name: omniauth-oauth2
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.8'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.8'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rails
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '7'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '7'
69
+ - !ruby/object:Gem::Dependency
70
+ name: warden
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '1.2'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '1.2'
83
+ - !ruby/object:Gem::Dependency
84
+ name: warden-oauth2
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: 0.0.1
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: 0.0.1
97
+ - !ruby/object:Gem::Dependency
98
+ name: publishing_platform_rubocop
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ description: Client for Publishing Platform's OAuth 2-based SSO.
112
+ email:
113
+ executables: []
114
+ extensions: []
115
+ extra_rdoc_files: []
116
+ files:
117
+ - Gemfile
118
+ - README.md
119
+ - Rakefile
120
+ - app/controllers/authentications_controller.rb
121
+ - app/views/authentications/failure.html.erb
122
+ - app/views/authorisations/unauthorised.html.erb
123
+ - config/routes.rb
124
+ - lib/omniauth/strategies/publishing_platform.rb
125
+ - lib/publishing_platform_sso.rb
126
+ - lib/publishing_platform_sso/api_access.rb
127
+ - lib/publishing_platform_sso/authorise_user.rb
128
+ - lib/publishing_platform_sso/bearer_token.rb
129
+ - lib/publishing_platform_sso/config.rb
130
+ - lib/publishing_platform_sso/controller_methods.rb
131
+ - lib/publishing_platform_sso/errors.rb
132
+ - lib/publishing_platform_sso/failure_app.rb
133
+ - lib/publishing_platform_sso/railtie.rb
134
+ - lib/publishing_platform_sso/user.rb
135
+ - lib/publishing_platform_sso/version.rb
136
+ - lib/publishing_platform_sso/warden_config.rb
137
+ homepage:
138
+ licenses:
139
+ - MIT
140
+ metadata: {}
141
+ post_install_message:
142
+ rdoc_options: []
143
+ require_paths:
144
+ - lib
145
+ required_ruby_version: !ruby/object:Gem::Requirement
146
+ requirements:
147
+ - - ">="
148
+ - !ruby/object:Gem::Version
149
+ version: '3.0'
150
+ required_rubygems_version: !ruby/object:Gem::Requirement
151
+ requirements:
152
+ - - ">="
153
+ - !ruby/object:Gem::Version
154
+ version: '0'
155
+ requirements: []
156
+ rubygems_version: 3.3.7
157
+ signing_key:
158
+ specification_version: 4
159
+ summary: Client for Publishing Platform's OAuth 2-based SSO.
160
+ test_files: []