publishing_platform_sso 0.2.0

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 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: []