Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create a new access token for the SSO Push API user *before* expiry not *at* expiry #2418

Merged
merged 11 commits into from
Oct 10, 2023
Merged
2 changes: 1 addition & 1 deletion app/controllers/application_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ def current_resource_owner
end

def application_making_request
@_application_making_request ||= ::Doorkeeper::Application.find(doorkeeper_token.application_id) if doorkeeper_token
@_application_making_request ||= Doorkeeper::Application.find(doorkeeper_token.application_id) if doorkeeper_token
end

private
Expand Down
4 changes: 2 additions & 2 deletions app/controllers/root_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ class RootController < ApplicationController
skip_after_action :verify_authorized

def index
applications = ::Doorkeeper::Application.where(show_on_dashboard: true).can_signin(current_user)
applications = Doorkeeper::Application.where(show_on_dashboard: true).can_signin(current_user)

@applications_and_permissions = zip_permissions(applications, current_user)
end

def signin_required
@application = ::Doorkeeper::Application.find_by(id: session.delete(:signin_missing_for_application))
@application = Doorkeeper::Application.find_by(id: session.delete(:signin_missing_for_application))
end

def privacy_notice; end
Expand Down
2 changes: 1 addition & 1 deletion app/jobs/permission_updater.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
class PermissionUpdater < PushUserUpdatesJob
def perform(uid, application_id)
user = User.find_by(uid:)
application = ::Doorkeeper::Application.find_by(id: application_id)
application = Doorkeeper::Application.find_by(id: application_id)
# It's possible they've been deleted between when the job was scheduled and run.
return if user.nil? || application.nil?
return unless application.supports_push_updates?
Expand Down
2 changes: 1 addition & 1 deletion app/jobs/reauth_enforcer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

class ReauthEnforcer < PushUserUpdatesJob
def perform(uid, application_id)
application = ::Doorkeeper::Application.find_by(id: application_id)
application = Doorkeeper::Application.find_by(id: application_id)
# It's possible the application has been deleted between when the job was scheduled and run.
return if application.nil?
return unless application.supports_push_updates?
Expand Down
7 changes: 7 additions & 0 deletions app/models/doorkeeper/access_grant.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module Doorkeeper
class AccessGrant < ::ActiveRecord::Base # rubocop:disable Rails/ApplicationRecord
include Models::ExpirationTimeSqlMath

scope :expired, -> { where.not(expires_in: nil).where("#{sanitize_sql(expiration_time_sql)} < ?", Time.current) }
end
end
9 changes: 9 additions & 0 deletions app/models/doorkeeper/access_token.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
module Doorkeeper
class AccessToken < ::ActiveRecord::Base # rubocop:disable Rails/ApplicationRecord
scope :not_revoked, -> { where(revoked_at: nil) }
scope :expires_after, ->(time) { where.not(expires_in: nil).where("#{sanitize_sql(expiration_time_sql)} > ?", time) }
scope :expired, -> { where.not(expires_in: nil).where("#{sanitize_sql(expiration_time_sql)} < ?", Time.current) }
scope :ordered_by_expires_at, -> { order(expiration_time_sql) }
scope :ordered_by_application_name, -> { includes(:application).merge(Doorkeeper::Application.ordered_by_name) }
end
end
5 changes: 3 additions & 2 deletions app/models/doorkeeper/application.rb
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
require "doorkeeper/orm/active_record/application"

# rubocop:disable Rails/ApplicationRecord
class ::Doorkeeper::Application < ActiveRecord::Base
class Doorkeeper::Application < ActiveRecord::Base
# rubocop:enable Rails/ApplicationRecord
has_many :supported_permissions, dependent: :destroy

default_scope { order("oauth_applications.name") }
default_scope { ordered_by_name }
scope :ordered_by_name, -> { order("oauth_applications.name") }
scope :support_push_updates, -> { where(supports_push_updates: true) }
scope :not_retired, -> { where(retired: false) }
scope :can_signin,
Expand Down
2 changes: 1 addition & 1 deletion app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ def authorised_applications
alias_method :applications_used, :authorised_applications

def revoke_all_authorisations
authorisations.where(revoked_at: nil).find_each(&:revoke)
authorisations.not_revoked.find_each(&:revoke)
end

def grant_application_signin_permission(application)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ def resolve
private

def applications
::Doorkeeper::Application.includes(:supported_permissions)
Doorkeeper::Application.includes(:supported_permissions)
end
end
end
14 changes: 11 additions & 3 deletions app/views/api_users/edit.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -47,19 +47,27 @@
<th>Token (hidden)</th>
<th>Generated</th>
<th>Expires</th>
<th>State</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<% @api_user.authorisations.where(revoked_at: nil).each do |authorisation| %>
<% @api_user.authorisations.not_revoked.ordered_by_application_name.ordered_by_expires_at.each do |authorisation| %>
<tr>
<td><%= authorisation.application.name %></td>
<td><code><%= truncate_access_token(authorisation.token) %></code></td>
<td>
<%= Time.at(authorisation.created_at).to_date.to_fs(:govuk_date) %>
<%= authorisation.created_at.to_date.to_fs(:govuk_date) %>
</td>
<td>
<%= Time.at(authorisation.created_at.to_f + authorisation.expires_in).to_date.to_fs(:govuk_date) %>
<%= authorisation.expires_at.to_date.to_fs(:govuk_date) %>
</td>
<td>
<% if authorisation.expired? %>
<span class="label label-danger">Expired</span>
<% else %>
<span class="label label-success">Valid</span>
<% end %>
</td>
<td>
<div class="btn-group">
Expand Down
6 changes: 3 additions & 3 deletions config/initializers/doorkeeper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@
# Use a custom class for generating the access token.
# See https://doorkeeper.gitbook.io/guides/configuration/other-configurations#custom-access-token-generator
#
# access_token_generator '::Doorkeeper::JWT'
# access_token_generator 'Doorkeeper::JWT'

# The controller +Doorkeeper::ApplicationController+ inherits from.
# Defaults to +ActionController::Base+ unless +api_only+ is set, which changes the default to
Expand Down Expand Up @@ -119,7 +119,7 @@
# If you wish to use another hashing implementation, you can override
# this strategy as follows:
#
# hash_token_secrets using: '::Doorkeeper::Hashing::MyCustomHashImpl'
# hash_token_secrets using: 'Doorkeeper::Hashing::MyCustomHashImpl'
#
# Keep in mind that changing the hashing function will invalidate all existing
# secrets, if there are any.
Expand All @@ -134,7 +134,7 @@
# If you wish to use bcrypt for application secret hashing, uncomment
# this line instead:
#
# hash_application_secrets using: '::Doorkeeper::SecretStoring::BCrypt'
# hash_application_secrets using: 'Doorkeeper::SecretStoring::BCrypt'

# When the above option is enabled, and a hashed token or secret is not found,
# you can allow to fall back to another strategy. For users upgrading
Expand Down
4 changes: 2 additions & 2 deletions lib/collectors/global_prometheus_collector.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ def metrics
def token_expiry_info
# Cache metric to prevent needless expensive calls to the database
Rails.cache.fetch("token_expiry_info", expires_in: 1.hour) do
ApiUser.where.not(email: SSOPushCredential::USER_EMAIL).flat_map do |user|
user.authorisations.where(revoked_at: nil).map do |token|
ApiUser.all.flat_map do |user|
user.authorisations.not_revoked.map do |token|
{
expires_at: token.expires_at.to_i,
api_user: user.email,
Expand Down
4 changes: 1 addition & 3 deletions lib/expired_oauth_access_records_deleter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@ class ExpiredOauthAccessRecordsDeleter
access_token: EventLog::ACCESS_TOKENS_DELETED,
}.freeze

HAS_EXPIRED = "expires_in is not null AND DATE_ADD(created_at, INTERVAL expires_in second) < ?".freeze

def initialize(record_type:)
@record_class = CLASSES.fetch(record_type)
@event = EVENTS.fetch(record_type)
Expand All @@ -19,7 +17,7 @@ def initialize(record_type:)
attr_reader :record_class, :total_deleted

def delete_expired
@record_class.where(HAS_EXPIRED, Time.zone.now).in_batches do |relation|
@record_class.expired.in_batches do |relation|
records_by_user_id = relation.includes(:application).group_by(&:resource_owner_id)
all_users = User.where(id: records_by_user_id.keys)

Expand Down
1 change: 1 addition & 0 deletions lib/sso_push_credential.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ def credentials(application)

user.authorisations
.not_expired
.expires_after(4.weeks.from_now)
.create_with(expires_in: 10.years)
.find_or_create_by!(application_id: application.id).token
end
Expand Down
2 changes: 1 addition & 1 deletion lib/tasks/sync_kubernetes_secrets.rake
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ namespace :kubernetes do
missing_users = emails - api_users.map(&:email)

api_users.each do |api_user|
api_user.authorisations.where(revoked_at: nil).each do |token|
api_user.authorisations.not_revoked.each do |token|
name = "signon-token-#{api_user.name}-#{token.application.name}".parameterize
data = { bearer_token: token.token }

Expand Down
4 changes: 2 additions & 2 deletions lib/user_permissions_controller_methods.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ module UserPermissionsControllerMethods

def visible_applications(user)
if user.api_user?
applications = ::Doorkeeper::Application.includes(:supported_permissions)
applications = Doorkeeper::Application.includes(:supported_permissions)
if current_user.superadmin?
api_user_authorised_apps = user.authorisations.where(revoked_at: nil).pluck(:application_id)
api_user_authorised_apps = user.authorisations.not_revoked.pluck(:application_id)
applications.where(id: api_user_authorised_apps)
else
applications.none
Expand Down
2 changes: 1 addition & 1 deletion script/make_oauth_work_in_dev
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ def deverise_uri(uri)
end

puts "Updating SSO config so that it works in development..."
applications = ::Doorkeeper::Application.where(retired: false)
applications = Doorkeeper::Application.where(retired: false)
applications.each do |application|
local_config = values_for_app(application)
if local_config.present?
Expand Down
2 changes: 1 addition & 1 deletion test/integration/superadmin_application_edit_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class SuperAdminApplicationEditTest < ActionDispatch::IntegrationTest

# normal user who's authorised to use app
@user = create(:user)
::Doorkeeper::AccessToken.create!(resource_owner_id: @user.id, application_id: @application.id, token: "1234")
Doorkeeper::AccessToken.create!(resource_owner_id: @user.id, application_id: @application.id, token: "1234")
end

should "be able to enable push updates to applications" do
Expand Down
2 changes: 1 addition & 1 deletion test/jobs/push_user_updates_job_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ class TestJob < PushUserUpdatesJob
foo_app, _bar_app = *create_list(:application, 2)

# authenticate access
::Doorkeeper::AccessToken.create!(resource_owner_id: user.id, application_id: foo_app.id, token: "1234")
Doorkeeper::AccessToken.create!(resource_owner_id: user.id, application_id: foo_app.id, token: "1234")

assert_enqueued_with(job: TestJob, args: [user.uid, foo_app.id]) do
TestJob.perform_on(user)
Expand Down
16 changes: 15 additions & 1 deletion test/lib/sso_push_credential_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ class SSOPushCredentialTest < ActiveSupport::TestCase

context "given an already authorised application" do
setup do
@authorisation = @user.authorisations.create!(application_id: @application.id)
@authorisation = @user.authorisations.create!(application_id: @application.id, expires_in: 5.weeks)
end

should "return the bearer token for an already-authorized application" do
Expand Down Expand Up @@ -37,6 +37,20 @@ class SSOPushCredentialTest < ActiveSupport::TestCase
end
end

context "given an application with an authorisation close to expiry" do
setup do
@user.authorisations.create!(application_id: @application.id, expires_in: 4.weeks)
end

should "create a new authorisation to replace the expired one" do
bearer_token = SSOPushCredential.credentials(@application)

new_authorisation = @user.authorisations.find_by(token: bearer_token)
assert new_authorisation.expires_at > 4.weeks.from_now
assert_equal @application.id, new_authorisation.application_id
end
end

context "given an application with a revoked authorisation" do
setup do
@user.authorisations.create!(application_id: @application.id, revoked_at: Time.current)
Expand Down
15 changes: 15 additions & 0 deletions test/models/doorkeeper/access_grant_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
require "test_helper"

class Doorkeeper::AccessGrantTest < ActiveSupport::TestCase
context ".expired" do
should "return grants that have expired" do
grant_expiring_1_day_ago = create(:access_grant, expires_in: -1.day)
grant_expiring_in_1_day = create(:access_grant, expires_in: 1.day)

grants = Doorkeeper::AccessGrant.expired

assert_includes grants, grant_expiring_1_day_ago
assert_not_includes grants, grant_expiring_in_1_day
end
end
end
64 changes: 64 additions & 0 deletions test/models/doorkeeper/access_token_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
require "test_helper"

class Doorkeeper::AccessTokenTest < ActiveSupport::TestCase
context ".not_revoked" do
should "return tokens that have not been revoked" do
revoked_token = create(:access_token, revoked_at: Time.current)
non_revoked_token = create(:access_token, revoked_at: nil)

tokens = Doorkeeper::AccessToken.not_revoked

assert_not_includes tokens, revoked_token
assert_includes tokens, non_revoked_token
end
end

context ".expires_after" do
should "return tokens expiring after specified time" do
token_expiring_in_1_week = create(:access_token, expires_in: 1.week)
token_expiring_in_3_weeks = create(:access_token, expires_in: 3.weeks)

tokens = Doorkeeper::AccessToken.expires_after(2.weeks.from_now)

assert_not_includes tokens, token_expiring_in_1_week
assert_includes tokens, token_expiring_in_3_weeks
end
end

context ".expired" do
should "return tokens that have expired" do
token_expiring_1_day_ago = create(:access_token, expires_in: -1.day)
token_expiring_in_1_day = create(:access_token, expires_in: 1.day)

tokens = Doorkeeper::AccessToken.expired

assert_includes tokens, token_expiring_1_day_ago
assert_not_includes tokens, token_expiring_in_1_day
end
end

context ".ordered_by_expires_at" do
should "return tokens ordered by expiry time" do
token_expiring_in_2_weeks = create(:access_token, expires_in: 2.weeks)
token_expiring_in_1_week = create(:access_token, expires_in: 1.week)

tokens = Doorkeeper::AccessToken.ordered_by_expires_at

assert_equal [token_expiring_in_1_week, token_expiring_in_2_weeks], tokens
end
end

context ".ordered_by_application_name" do
should "return tokens ordered by application name" do
application_named_foo = create(:application, name: "Foo")
application_named_bar = create(:application, name: "Bar")

token_for_foo = create(:access_token, application: application_named_foo)
token_for_bar = create(:access_token, application: application_named_bar)

tokens = Doorkeeper::AccessToken.ordered_by_application_name

assert_equal [token_for_bar, token_for_foo], tokens
end
end
end
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
require "test_helper"

class ::Doorkeeper::ApplicationTest < ActiveSupport::TestCase
class Doorkeeper::ApplicationTest < ActiveSupport::TestCase
should "have a signin supported permission on create" do
assert_not_nil create(:application).signin_permission
end
Expand Down Expand Up @@ -242,4 +242,15 @@ class ::Doorkeeper::ApplicationTest < ActiveSupport::TestCase
end
end
end

context ".ordered_by_name" do
should "return applications ordered by name" do
application_named_foo = create(:application, name: "Foo")
application_named_bar = create(:application, name: "Bar")

applications = Doorkeeper::Application.ordered_by_name

assert_equal [application_named_bar, application_named_foo], applications
end
end
end
2 changes: 1 addition & 1 deletion test/models/user_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1110,7 +1110,7 @@ def setup
end

def authenticate_access(user, app)
::Doorkeeper::AccessToken.create!(resource_owner_id: user.id, application_id: app.id)
Doorkeeper::AccessToken.create!(resource_owner_id: user.id, application_id: app.id)
end

def assert_user_has_permissions(expected_permissions, application, user)
Expand Down