From 7549432c1f9927112ae4c31b58e0e028ee2a93ca Mon Sep 17 00:00:00 2001 From: Stephen von Takach Date: Mon, 3 Nov 2025 17:04:24 +1100 Subject: [PATCH 1/2] feat: remove unused components initial implementation of openid connect --- Gemfile | 1 + Gemfile.lock | 5 + config/application.rb | 14 ++- config/environments/development.rb | 3 - config/environments/production.rb | 3 - config/environments/test.rb | 3 - config/initializers/doorkeeper.rb | 17 ++- .../initializers/doorkeeper_openid_connect.rb | 109 ++++++++++++++++++ .../locales/doorkeeper_openid_connect.en.yml | 23 ++++ config/routes.rb | 1 + ...create_doorkeeper_openid_connect_tables.rb | 15 +++ 11 files changed, 181 insertions(+), 13 deletions(-) create mode 100644 config/initializers/doorkeeper_openid_connect.rb create mode 100644 config/locales/doorkeeper_openid_connect.en.yml create mode 100644 db/migrate/20251103025330_create_doorkeeper_openid_connect_tables.rb diff --git a/Gemfile b/Gemfile index 7cc6832..a16a70f 100644 --- a/Gemfile +++ b/Gemfile @@ -19,6 +19,7 @@ gem "redis" # Authentication gem "doorkeeper", "~> 5.8" +gem "doorkeeper-openid_connect" gem "doorkeeper-jwt" gem "jwt" gem "nkf" # required for omniauth-oauth2 in Ruby 3.4 diff --git a/Gemfile.lock b/Gemfile.lock index b206366..fac8875 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -105,6 +105,10 @@ GEM railties (>= 5) doorkeeper-jwt (0.4.2) jwt (>= 2.1) + doorkeeper-openid_connect (1.8.11) + doorkeeper (>= 5.5, < 5.9) + jwt (>= 2.5) + ostruct (>= 0.5) drb (2.2.3) email_validator (2.2.4) activemodel @@ -322,6 +326,7 @@ DEPENDENCIES debug doorkeeper (~> 5.8) doorkeeper-jwt + doorkeeper-openid_connect email_validator jwt lograge diff --git a/config/application.rb b/config/application.rb index 8bfcebd..aa8a526 100644 --- a/config/application.rb +++ b/config/application.rb @@ -1,6 +1,18 @@ require_relative "boot" -require "rails/all" +require "rails" +# Pick the frameworks you want: +require "active_model/railtie" +require "active_job/railtie" +require "active_record/railtie" +# require "active_storage/engine" +require "action_controller/railtie" +require "action_mailer/railtie" +# require "action_mailbox/engine" +# require "action_text/engine" +require "action_view/railtie" +require "action_cable/engine" +# require "rails/test_unit/railtie" # Require the gems listed in Gemfile, including any gems # you've limited to :test, :development, or :production. diff --git a/config/environments/development.rb b/config/environments/development.rb index 5fef1a2..49adc80 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -28,9 +28,6 @@ # Change to :null_store to avoid any caching. config.cache_store = :null_store - # Store uploaded files on the local file system (see config/storage.yml for options). - config.active_storage.service = :local - # Don't care if the mailer can't send. config.action_mailer.raise_delivery_errors = false diff --git a/config/environments/production.rb b/config/environments/production.rb index 315507a..c1576ad 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -56,9 +56,6 @@ def initialize(*targets) # Enable serving of images, stylesheets, and JavaScripts from an asset server. # config.asset_host = "http://assets.example.com" - # Store uploaded files on the local file system (see config/storage.yml for options). - config.active_storage.service = :local - # Assume all access to the app is happening through a SSL-terminating reverse proxy. # config.assume_ssl = true diff --git a/config/environments/test.rb b/config/environments/test.rb index c2095b1..0424d0f 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -28,9 +28,6 @@ # Disable request forgery protection in test environment. config.action_controller.allow_forgery_protection = false - # Store uploaded files on the local file system in a temporary directory. - config.active_storage.service = :test - # Tell Action Mailer not to deliver emails to the real world. # The :test delivery method accumulates sent emails in the # ActionMailer::Base.deliveries array. diff --git a/config/initializers/doorkeeper.rb b/config/initializers/doorkeeper.rb index 9579a45..81f79e3 100644 --- a/config/initializers/doorkeeper.rb +++ b/config/initializers/doorkeeper.rb @@ -79,13 +79,20 @@ "public", # these can be found via: GET /api/engine/v2/scopes + "alert_dashboards", + "alerts", "api_keys", + "asset_categories", + "asset_purchase_orders", + "asset_types", "asset_instances", "assets", "ldap_authentications", "saml_authentications", "o_auth_authentications", "brokers", + "build_monitor", + "chat_gpt", "cluster", "domains", "drivers", @@ -95,12 +102,16 @@ "guest", "control", "edges", + "edge-control", "metadata", "mqtt", "flux", "o_auth_applications", - "repositories" - ].map { |scope| [scope, "#{scope}.read", "#{scope}.write"] }.flatten + "repositories", + ].map { |scope| [scope, "#{scope}.read", "#{scope}.write"] }.flatten + [ + # this is required for OpenID connect + "openid" + ] default_scopes :public optional_scopes(*all_scopes) @@ -108,7 +119,7 @@ access_token_generator "::Doorkeeper::JWT" force_ssl_in_redirect_uri false - grant_flows %w[authorization_code client_credentials implicit password] + grant_flows %w[authorization_code client_credentials implicit password implicit_oidc] end Doorkeeper::JWT.configure do diff --git a/config/initializers/doorkeeper_openid_connect.rb b/config/initializers/doorkeeper_openid_connect.rb new file mode 100644 index 0000000..cddbcc2 --- /dev/null +++ b/config/initializers/doorkeeper_openid_connect.rb @@ -0,0 +1,109 @@ +# frozen_string_literal: true + +require "uri" + +Doorkeeper::OpenidConnect.configure do + issuer do |resource_owner, application| + "https://#{URI.parse(application.redirect_uri).host}" + end + + # Set the encryption secret. This would be shared with any other applications + # that should be able to read the payload of the token. Defaults to "secret". + key = ENV["JWT_SECRET"] + key = key.try { |k| Base64.decode64(k) } || DEV_KEY + signing_key key + + subject_types_supported [:public] + + resource_owner_from_access_token do |access_token| + User.find_by(id: access_token.resource_owner_id) + end + + auth_time_from_resource_owner do |resource_owner| + resource_owner.last_login + end + + reauthenticate_resource_owner do |resource_owner, return_to| + # Example implementation: + # store_location_for resource_owner, return_to + # sign_out resource_owner + # redirect_to new_user_session_url + domain = "https://#{request.host}" + authority = Authority.find_by_domain(request.host) + url = authority.login_url.gsub("{{url}}", URI.encode_uri_component(return_to)) + + redirect_to "#{domain}#{url}" + end + + # Depending on your configuration, a DoubleRenderError could be raised + # if render/redirect_to is called at some point before this callback is executed. + # To avoid the DoubleRenderError, you could add these two lines at the beginning + # of this callback: (Reference: https://github.com/rails/rails/issues/25106) + # self.response_body = nil + # @_response_body = nil + select_account_for_resource_owner do |resource_owner, return_to| + self.response_body = nil + @_response_body = nil + + # there is no account selection in PlaceOS + redirect_to return_to + end + + subject do |resource_owner, application| + # Example implementation: + # resource_owner.id + + # or if you need pairwise subject identifier, implement like below: + # Digest::SHA256.hexdigest("#{resource_owner.id}#{URI.parse(application.redirect_uri).host}#{'your_secret_salt'}") + + resource_owner.id + end + + end_session_endpoint do + authority = Authority.find_by_domain(request.host) + authority.logout_url + end + + # Protocol to use when generating URIs for the discovery endpoint, + # for example if you also use HTTPS in development + # protocol do + # :https + # end + + # Expiration time on or after which the ID Token MUST NOT be accepted for processing. (default 120 seconds). + # expiration 600 + + claims do + claim :sub do |resource_owner| + resource_owner.id + end + + claim :email do |resource_owner| + resource_owner.email + end + + claim :full_name do |resource_owner| + resource_owner.name + end + + claim :given_name do |resource_owner| + resource_owner.first_name + end + + claim :family_name do |resource_owner| + resource_owner.last_name + end + + claim :nickname do |resource_owner| + resource_owner.nickname + end + + claim :phone_number do |resource_owner| + resource_owner.phone + end + + claim :preferred_username do |resource_owner| + resource_owner.login_name || resource_owner.email + end + end +end diff --git a/config/locales/doorkeeper_openid_connect.en.yml b/config/locales/doorkeeper_openid_connect.en.yml new file mode 100644 index 0000000..1bed506 --- /dev/null +++ b/config/locales/doorkeeper_openid_connect.en.yml @@ -0,0 +1,23 @@ +en: + doorkeeper: + scopes: + openid: 'Authenticate your account' + profile: 'View your profile information' + email: 'View your email address' + address: 'View your physical address' + phone: 'View your phone number' + errors: + messages: + login_required: 'The authorization server requires end-user authentication' + consent_required: 'The authorization server requires end-user consent' + interaction_required: 'The authorization server requires end-user interaction' + account_selection_required: 'The authorization server requires end-user account selection' + openid_connect: + errors: + messages: + # Configuration error messages + resource_owner_from_access_token_not_configured: 'Failure due to Doorkeeper::OpenidConnect.configure.resource_owner_from_access_token missing configuration.' + auth_time_from_resource_owner_not_configured: 'Failure due to Doorkeeper::OpenidConnect.configure.auth_time_from_resource_owner missing configuration.' + reauthenticate_resource_owner_not_configured: 'Failure due to Doorkeeper::OpenidConnect.configure.reauthenticate_resource_owner missing configuration.' + select_account_for_resource_owner_not_configured: 'Failure due to Doorkeeper::OpenidConnect.configure.select_account_for_resource_owner missing configuration.' + subject_not_configured: 'ID Token generation failed due to Doorkeeper::OpenidConnect.configure.subject missing configuration.' diff --git a/config/routes.rb b/config/routes.rb index fe89e50..6b63c82 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -3,6 +3,7 @@ scope :auth do use_doorkeeper + use_doorkeeper_openid_connect get "/login", to: "auth/sessions#new" # for defining continue get "/logout", to: "auth/sessions#destroy" # deletes the session diff --git a/db/migrate/20251103025330_create_doorkeeper_openid_connect_tables.rb b/db/migrate/20251103025330_create_doorkeeper_openid_connect_tables.rb new file mode 100644 index 0000000..b0f8240 --- /dev/null +++ b/db/migrate/20251103025330_create_doorkeeper_openid_connect_tables.rb @@ -0,0 +1,15 @@ +class CreateDoorkeeperOpenidConnectTables < ActiveRecord::Migration[8.1] + def change + create_table :oauth_openid_requests do |t| + t.references :access_grant, null: false, index: true + t.string :nonce, null: false + end + + add_foreign_key( + :oauth_openid_requests, + :oauth_access_grants, + column: :access_grant_id, + on_delete: :cascade + ) + end +end From 5737055f1822ab8410118c24c0804d7371f4192a Mon Sep 17 00:00:00 2001 From: Stephen von Takach Date: Mon, 3 Nov 2025 22:00:32 +1100 Subject: [PATCH 2/2] fix: .well-known routes and issuer --- config/environments/production.rb | 18 ++++++++++++++++++ .../initializers/doorkeeper_openid_connect.rb | 4 ++-- config/routes.rb | 5 ++++- 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/config/environments/production.rb b/config/environments/production.rb index c1576ad..3563b7e 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -154,6 +154,24 @@ def initialize(*targets) config.lograge.formatter = Lograge::Formatters::Logstash.new + # NEW: event-scoped options so we can access the exception object + backtrace + config.lograge.custom_options = lambda do |event| + ex = event.payload[:exception_object] + next {} unless ex + + # Optional: cleaner to strip framework noise from the backtrace + cleaned = Rails.backtrace_cleaner.clean(Array(ex.backtrace)) + + { + error: { + class: ex.class.name, + message: ex.message, + # Keep payloads safe for UDP/ingest by truncating if huge + backtrace: cleaned.first(200) # or fewer if you ship via UDP + } + } + end + # Ensures only our lograge error is logged # standard:disable Lint/ConstantDefinitionInBlock module ActionDispatch diff --git a/config/initializers/doorkeeper_openid_connect.rb b/config/initializers/doorkeeper_openid_connect.rb index cddbcc2..0b4cdfd 100644 --- a/config/initializers/doorkeeper_openid_connect.rb +++ b/config/initializers/doorkeeper_openid_connect.rb @@ -3,8 +3,8 @@ require "uri" Doorkeeper::OpenidConnect.configure do - issuer do |resource_owner, application| - "https://#{URI.parse(application.redirect_uri).host}" + issuer do |request| + "https://#{request.host}" end # Set the encryption secret. This would be shared with any other applications diff --git a/config/routes.rb b/config/routes.rb index 6b63c82..aa1693c 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,6 +1,5 @@ Rails.application.routes.draw do # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html - scope :auth do use_doorkeeper use_doorkeeper_openid_connect @@ -19,4 +18,8 @@ get "/authority", to: "auth/authorities#current" end + + get "/.well-known/openid-configuration(.:format)", to: "doorkeeper/openid_connect/discovery#provider" + get "/.well-known/oauth-authorization-server(.:format)", to: "doorkeeper/openid_connect/discovery#provider" + get "/.well-known/webfinger(.:format)", to: "doorkeeper/openid_connect/discovery#webfinger" end