From 64bcf7652bc1053741ab58f3ff54505b3d7820ad Mon Sep 17 00:00:00 2001 From: Milan Cvetkovic Date: Wed, 30 Aug 2023 12:36:55 +0000 Subject: [PATCH 1/1] Add openid connect support using doorkeeper-openid_connect gem ... as discussed in [Issue 507](https://github.com/openstreetmap/operations/issues/507) and described by @mmd-osm. To activate, set the value of `doorkeeper_signing_key` to RSA private key. Allows using openstreetmap as an identity provider. Adds `openid` scope to OAuth2 authorizations, required to login to OSM. Currently, the only claims returned are: - "openid" scope: "sub" and "preferred_username" - "read_email" scope: "email" --- app/views/oauth2_applications/_form.html.erb | 2 +- app/views/oauth2_authorizations/new.html.erb | 2 + config/initializers/doorkeeper.rb | 2 +- .../initializers/doorkeeper_openid_connect.rb | 66 +++++-------------- config/locales/en.yml | 1 + config/routes.rb | 3 +- config/settings.yml | 5 ++ lib/oauth.rb | 4 +- 8 files changed, 31 insertions(+), 54 deletions(-) diff --git a/app/views/oauth2_applications/_form.html.erb b/app/views/oauth2_applications/_form.html.erb index 7fde3e0e7..51267c069 100644 --- a/app/views/oauth2_applications/_form.html.erb +++ b/app/views/oauth2_applications/_form.html.erb @@ -3,5 +3,5 @@ <%= f.form_group :confidential do %> <%= f.check_box :confidential %> <% end %> -<%= f.collection_check_boxes :scopes, Oauth.scopes(:privileged => current_user.administrator?), :name, :description %> +<%= f.collection_check_boxes :scopes, Oauth.scopes(:oauth2 => true, :privileged => current_user.administrator?), :name, :description %> <%= f.primary %> diff --git a/app/views/oauth2_authorizations/new.html.erb b/app/views/oauth2_authorizations/new.html.erb index 971e0e20a..ac9c7c6c5 100644 --- a/app/views/oauth2_authorizations/new.html.erb +++ b/app/views/oauth2_authorizations/new.html.erb @@ -18,6 +18,7 @@ <%= f.hidden_field :state, :value => @pre_auth.state %> <%= f.hidden_field :response_type, :value => @pre_auth.response_type %> <%= f.hidden_field :scope, :value => @pre_auth.scope %> + <%= f.hidden_field :nonce, :value => @pre_auth.nonce %> <%= f.hidden_field :code_challenge, :value => @pre_auth.code_challenge %> <%= f.hidden_field :code_challenge_method, :value => @pre_auth.code_challenge_method %> <%= f.primary t(".authorize") %> @@ -30,6 +31,7 @@ <%= f.hidden_field :state, :value => @pre_auth.state %> <%= f.hidden_field :response_type, :value => @pre_auth.response_type %> <%= f.hidden_field :scope, :value => @pre_auth.scope %> + <%= f.hidden_field :nonce, :value => @pre_auth.nonce %> <%= f.hidden_field :code_challenge, :value => @pre_auth.code_challenge %> <%= f.hidden_field :code_challenge_method, :value => @pre_auth.code_challenge_method %> <%= f.submit t(".deny") %> diff --git a/config/initializers/doorkeeper.rb b/config/initializers/doorkeeper.rb index a2df9167f..c1d4e2f78 100644 --- a/config/initializers/doorkeeper.rb +++ b/config/initializers/doorkeeper.rb @@ -225,7 +225,7 @@ Doorkeeper.configure do # https://doorkeeper.gitbook.io/guides/ruby-on-rails/scopes # default_scopes :public - optional_scopes(*Oauth::SCOPES, *Oauth::PRIVILEGED_SCOPES) + optional_scopes(*Oauth::SCOPES, *Oauth::PRIVILEGED_SCOPES, *Oauth::OAUTH2_SCOPES) # Allows to restrict only certain scopes for grant_type. # By default, all the scopes will be available for all the grant types. diff --git a/config/initializers/doorkeeper_openid_connect.rb b/config/initializers/doorkeeper_openid_connect.rb index e91a907c2..7f409ecbe 100644 --- a/config/initializers/doorkeeper_openid_connect.rb +++ b/config/initializers/doorkeeper_openid_connect.rb @@ -2,71 +2,37 @@ Doorkeeper::OpenidConnect.configure do issuer do |_resource_owner, _application| - "issuer string" + "#{Settings.server_protocol}://#{Settings.server_url}" end - signing_key <<~KEY - -----BEGIN RSA PRIVATE KEY----- - .... - -----END RSA PRIVATE KEY----- - KEY + signing_key Settings.doorkeeper_signing_key subject_types_supported [:public] resource_owner_from_access_token do |access_token| - # Example implementation: - # User.find_by(id: access_token.resource_owner_id) + User.find_by(:id => access_token.resource_owner_id) end auth_time_from_resource_owner do |resource_owner| - # Example implementation: - # resource_owner.current_sign_in_at + # empty block necessary as a workaround to missing configuration + # when no auth_time claim is provided 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 + subject do |resource_owner, _application| + resource_owner.id 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| - # Example implementation: - # store_location_for resource_owner, return_to - # redirect_to account_select_url + protocol do + Settings.server_protocol.to_sym end - subject do |resource_owner, application| - # Example implementation: - # resource_owner.id + claims do + claim :preferred_username, :scope => :openid do |resource_owner, _scopes, _access_token| + resource_owner.display_name + end - # 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'}") + claim :email, :scope => :read_email, :response => [:id_token, :user_info] do |resource_owner, _scopes, _access_token| + resource_owner.email + end 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 - - # Example claims: - # claims do - # normal_claim :_foo_ do |resource_owner| - # resource_owner.foo - # end - - # normal_claim :_bar_ do |resource_owner| - # resource_owner.bar - # end - # end end diff --git a/config/locales/en.yml b/config/locales/en.yml index c9583abce..aca571d53 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -2549,6 +2549,7 @@ en: permissions: missing: "You have not permitted the application access to this facility" scopes: + openid: Sign-in using OpenStreetMap read_prefs: Read user preferences write_prefs: Modify user preferences write_diary: Create diary entries, comments and make friends diff --git a/config/routes.rb b/config/routes.rb index 5b537ea3e..43c43a793 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,11 +1,12 @@ OpenStreetMap::Application.routes.draw do - use_doorkeeper_openid_connect use_doorkeeper :scope => "oauth2" do controllers :authorizations => "oauth2_authorizations", :applications => "oauth2_applications", :authorized_applications => "oauth2_authorized_applications" end + use_doorkeeper_openid_connect :scope => "oauth2" if Settings.key?(:doorkeeper_signing_key) + # API namespace :api do get "capabilities" => "capabilities#show" # Deprecated, remove when 0.6 support is removed diff --git a/config/settings.yml b/config/settings.yml index 3ea298efc..f30331b07 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -150,3 +150,8 @@ smtp_password: null #signup_ip_max_burst: #signup_email_per_day: #signup_email_max_burst: +# Private key for signing id_tokens +#doorkeeper_signing_key: | +# -----BEGIN PRIVATE KEY----- +# ... +# -----END PRIVATE KEY----- diff --git a/lib/oauth.rb b/lib/oauth.rb index 7ff2ba8b4..0456c0873 100644 --- a/lib/oauth.rb +++ b/lib/oauth.rb @@ -1,6 +1,7 @@ module Oauth SCOPES = %w[read_prefs write_prefs write_diary write_api read_gpx write_gpx write_notes].freeze PRIVILEGED_SCOPES = %w[read_email skip_authorization].freeze + OAUTH2_SCOPES = %w[openid].freeze class Scope attr_reader :name @@ -14,9 +15,10 @@ module Oauth end end - def self.scopes(privileged: false) + def self.scopes(oauth2: false, privileged: false) scopes = SCOPES scopes += PRIVILEGED_SCOPES if privileged + scopes += OAUTH2_SCOPES if oauth2 scopes.collect { |s| Scope.new(s) } end end -- 2.39.5