Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,6 @@ spec/examples.txt
public/commit_id.txt
vendor/cache/
vendor/bundle/

# Local docker-compose overrides (port mappings, volume mounts)
docker-compose.override.yml
5 changes: 3 additions & 2 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ gem 'dalli'
gem 'deep_cloneable', '~> 3.2.0'
gem 'devise', '~> 4.9'
gem 'doorkeeper', '~> 5.8'
gem 'doorkeeper-jwt', '~> 0.2.1'
gem 'doorkeeper-jwt', '~> 0.4'
gem 'doorkeeper-openid_connect', '~> 1.9'
gem 'faraday', '~> 1.10'
gem 'faraday-http-cache', '~> 2.4'
gem 'faraday_middleware', '~> 1.2'
Expand All @@ -33,7 +34,7 @@ gem 'librato-metrics', '~> 2.1.2'
gem 'lograge'
gem 'mime-types'
gem 'p3p', '~> 2.0'
gem 'panoptes-client'
gem 'panoptes-client', '~> 1.3'
gem 'pg', '~> 1.4'
gem 'pg_search'
gem 'puma', '~> 6.4.3'
Expand Down
5 changes: 3 additions & 2 deletions config/initializers/doorkeeper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,12 @@ def self.force_secure_scheme?(host)
orm :active_record

use_refresh_token
use_pkce

enable_application_owner :confirmation => true

default_scopes :public
optional_scopes *Doorkeeper::Panoptes::Scopes.optional
optional_scopes :openid, *Doorkeeper::Panoptes::Scopes.optional

realm "Panoptes"

Expand Down Expand Up @@ -99,5 +100,5 @@ def self.force_secure_scheme?(host)
secret_key_path Rails.root.join("config", "keys", "doorkeeper-jwt-#{Rails.env}.pem")

# Sign using RSA SHA-512
encryption_method :rs512
signing_method :rs512
end
80 changes: 80 additions & 0 deletions config/initializers/doorkeeper_openid_connect.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# frozen_string_literal: true

# rubocop:disable Metrics/BlockLength
Doorkeeper::OpenidConnect.configure do
issuer do |_resource_owner, _application|
if Rails.env.production?
'https://panoptes.zooniverse.org'
elsif Rails.env.staging? # rubocop:disable Rails/UnknownEnv
'https://panoptes-staging.zooniverse.org'
else
'http://localhost:3000'
end
end

# Use the same RSA key that Doorkeeper::JWT uses for signing access tokens.
signing_key lambda {
key_path = Rails.root.join('config', 'keys', "doorkeeper-jwt-#{Rails.env}.pem")
File.read(key_path)
}

signing_algorithm :rs512

# Resolve the resource owner (User) from a Doorkeeper access token.
resource_owner_from_access_token do |access_token|
User.find_by(id: access_token.resource_owner_id)
end

# Return the time the user last authenticated. Devise's current_sign_in_at tracks this.
auth_time_from_resource_owner do |resource_owner|
resource_owner.current_sign_in_at&.to_i
end

# Re-authentication handler. For the password grant flow used in development,
# the user is always freshly authenticated so we return them directly.
reauthenticate_resource_owner do |resource_owner, _return_to|
resource_owner
end

# OIDC subject identifier - unique, stable user ID.
subject do |resource_owner, _application|
resource_owner.id
end

subject_types_supported %i[public]

claims do
claim :login, scope: :openid do |resource_owner, _scopes, _access_token|
resource_owner.login
end

claim :name, scope: :openid do |resource_owner, _scopes, _access_token|
resource_owner.display_name
end

claim :credited_name, scope: :openid do |resource_owner, _scopes, _access_token|
resource_owner.credited_name
end

claim :email, scope: :openid, response: %i[id_token user_info] do |resource_owner, _scopes, _access_token|
resource_owner.email
end

claim :email_verified, scope: :openid, response: %i[id_token user_info] do |resource_owner, _scopes, _access_token|
resource_owner.confirmed_at.present?
end

claim :admin, scope: :openid do |resource_owner, _scopes, _access_token|
resource_owner.admin?
end

claim :created_at, scope: :openid do |resource_owner, _scopes, _access_token|
resource_owner.created_at.to_i
end

claim :zooniverse_id, scope: :openid do |resource_owner, _scopes, _access_token|
resource_owner.zooniverse_id
end
end
end
# rubocop:enable Metrics/BlockLength
2 changes: 2 additions & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
applications: 'applications'
end

use_doorkeeper_openid_connect

devise_for :users,
controllers: { confirmations: 'confirmations', passwords: 'passwords' },
skip: [ :sessions, :registrations ]
Expand Down
49 changes: 49 additions & 0 deletions db/dev_seed_data/dev_seed_noninteractive.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# frozen_string_literal: true

exit unless Rails.env.development?

# Non-interactive version of dev_seed_data.rb for automated setup.
# Uses a default password for the local admin user.

DEFAULT_PASSWORD = 'password1234'

Rails.logger.info "\n=== Panoptes Local Dev Setup (non-interactive) ==="

# Setup admin user
attrs = {
admin: true,
password: DEFAULT_PASSWORD,
login: 'zooniverse_admin',
email: 'no-reply@zooniverse.org'
}
admin = User.where(login: 'zooniverse_admin').first_or_create(attrs, &:build_identity_group)

if admin.persisted?
Rails.logger.info "Admin user: #{admin.login} (email: #{admin.email})"
Rails.logger.info "Password: #{DEFAULT_PASSWORD}"
else
Rails.logger.info "ERROR: Failed to create admin user: #{admin.errors.full_messages.join(', ')}"
exit 1
end

# Setup Doorkeeper first-party OAuth application with openid scope
app = Doorkeeper::Application.where(name: 'DevAppClient').first_or_create do |da|
da.owner = admin
da.name = 'DevAppClient'
da.redirect_uri = 'urn:ietf:wg:oauth:2.0:oob'
da.trust_level = 2
da.confidential = false
scopes = %i[public openid] | Doorkeeper::Panoptes::Scopes.optional
da.default_scope = scopes.map(&:to_s)
end

if app.persisted?
Rails.logger.info "OAuth app: #{app.name}"
Rails.logger.info "Client ID: #{app.uid}"
Rails.logger.info "\nAccess at: http://localhost:3000/oauth/applications"
else
Rails.logger.info "ERROR: Failed to create OAuth app: #{app.errors.full_messages.join(', ')}"
exit 1
end

Rails.logger.info "\n=== Setup complete ==="
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# frozen_string_literal: true

class CreateDoorkeeperOpenidConnectTables < ActiveRecord::Migration[7.2]
disable_ddl_transaction!

def change
safety_assured do
# Schema defined by doorkeeper-openid_connect; timestamps not applicable.
create_table :oauth_openid_requests do |t| # rubocop:disable Rails/CreateTableWithTimestamps
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
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
class AddPkceToDoorkeeperAccessGrants < ActiveRecord::Migration[6.1]
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Style/FrozenStringLiteralComment: Missing frozen string literal comment.

def change
add_column :oauth_access_grants, :code_challenge, :string, null: true
add_column :oauth_access_grants, :code_challenge_method, :string, null: true
end
end
4 changes: 2 additions & 2 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ services:
- "POSTGRES_USER=panoptes"
- "POSTGRES_PASSWORD=panoptes"
ports:
- "5432:5432"
- "${PG_PORT:-5432}:5432"

redis:
image: redis
Expand All @@ -21,7 +21,7 @@ services:
- ./:/rails_app
- gem_cache:/usr/local/bundle
ports:
- "3000:3000"
- "${PANOPTES_PORT:-3000}:3000"
environment:
- "RAILS_ENV=development"
- "DATABASE_URL=postgresql://panoptes:panoptes@pg"
Expand Down
Loading