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