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 +7 -0
- data/Gemfile +10 -0
- data/README.md +156 -0
- data/Rakefile +8 -0
- data/app/controllers/authentications_controller.rb +17 -0
- data/app/views/authentications/failure.html.erb +3 -0
- data/app/views/authorisations/unauthorised.html.erb +5 -0
- data/config/routes.rb +7 -0
- data/lib/omniauth/strategies/publishing_platform.rb +28 -0
- data/lib/publishing_platform_sso/api_access.rb +9 -0
- data/lib/publishing_platform_sso/authorise_user.rb +49 -0
- data/lib/publishing_platform_sso/bearer_token.rb +75 -0
- data/lib/publishing_platform_sso/config.rb +64 -0
- data/lib/publishing_platform_sso/controller_methods.rb +48 -0
- data/lib/publishing_platform_sso/errors.rb +6 -0
- data/lib/publishing_platform_sso/failure_app.rb +59 -0
- data/lib/publishing_platform_sso/railtie.rb +17 -0
- data/lib/publishing_platform_sso/user.rb +50 -0
- data/lib/publishing_platform_sso/version.rb +7 -0
- data/lib/publishing_platform_sso/warden_config.rb +81 -0
- data/lib/publishing_platform_sso.rb +63 -0
- metadata +160 -0
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
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,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,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,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,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,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: []
|