Skip to content

Fix hash_application_secrets fallback not upgrading secret on access#1801

Open
Copilot wants to merge 4 commits intomainfrom
copilot/fix-fallback-on-hash-application-secrets
Open

Fix hash_application_secrets fallback not upgrading secret on access#1801
Copilot wants to merge 4 commits intomainfrom
copilot/fix-fallback-on-hash-application-secrets

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Mar 17, 2026

hash_application_secrets fallback: :plain could find applications stored with plain secrets, but unlike access tokens and grants, never upgraded those secrets to the active hashing strategy. The upgrade path through upgrade_fallback_value was only wired up in AccessGrantMixin and AccessTokenMixin (via find_by_plaintext_token), not for applications.

Changes

  • application_mixin.rbby_uid_and_secret now explicitly tries the primary strategy first, then the fallback strategy with the upgrade embedded directly in the fallback path — mirroring the find_by_plaintext_tokenfind_by_fallback_tokenupgrade_fallback_value pattern used by tokens and grants. secret_matches? remains a pure comparison predicate with no side effects:
# Try the primary (active) secret strategy first.
if !secret.nil? && !app.secret.nil? &&
    secret_strategy.secret_matches?(secret.to_s, app.secret.to_s)
  return app
end

# Fall back to the previous strategy if configured, upgrading on success
# to migrate the stored secret to the active strategy.
if fallback_secret_strategy && !secret.nil? && !app.secret.nil? &&
    fallback_secret_strategy.secret_matches?(secret.to_s, app.secret.to_s)
  upgrade_fallback_value(app, :secret, secret)
  return app
end
  • hashing_shared_context.rb — Added "with application hashing and fallback lookup enabled" shared context (configures hash_application_secrets fallback: :plain).

  • application_spec.rb — Added test verifying that a plain secret is upgraded to SHA256 on first access via by_uid_and_secret, and that subsequent lookups succeed via the hashed value.

Original prompt

This section details on the original issue you should resolve

<issue_title>fallback on hash_application_secrets does not upgrade secret when accessed</issue_title>
<issue_description>The Doorkeeper docs outline that, similar to access grants and access tokens, you can choose to encrypt application secrets and if you do this with existing applications, it will "upgrade" your secrets to be encrypted when they are accessed.

To enable plain values to be found and upgraded (to your active strategy, SHA256 by default) when it is accessed, you may use the following statement: hash_application_secrets fallback: :plain

I implemented this in my service and realized it does not use the upgrade path like access tokens and grants does. The method find_by_plaintext_token that eventually calls upgrade_fallback_value in SecretsStorable is only called in the AccessGrantMixin and AccessTokenMixin, not the ApplicationMixin. This difference in upgrading is also shown in tests between the types.

It's possible this could be checked in secret_matches? for an application in ApplicationMixin here.</issue_description>

<agent_instructions>Check it and if changes required - prepare a PR</agent_instructions>

Comments on the Issue (you are @copilot in this section)


📍 Connect Copilot coding agent with Jira, Azure Boards or Linear to delegate work to Copilot in one click without leaving your project management tool.

Copilot AI changed the title [WIP] Fix fallback on hash_application_secrets upgrade issue Fix hash_application_secrets fallback not upgrading secret on access Mar 17, 2026
Copilot AI requested a review from nbulaj March 17, 2026 16:28
@nbulaj
Copy link
Copy Markdown
Member

nbulaj commented Mar 19, 2026

@copilot should it be implemented using secret storable pattern? Like in find_by_plaintext_token which uses find_by and then fallbacks to find_by_fallback_token in case not found. I don't think your solution works when we lookup for Application using by_uid_and_secret (which is used internally by Doorkeeper gem)

Copy link
Copy Markdown
Contributor Author

Copilot AI commented Mar 19, 2026

@copilot should it be implemented using secret storable pattern? Like in find_by_plaintext_token which uses find_by and then fallbacks to find_by_fallback_token in case not foun...

Good point. Moved the upgrade logic out of secret_matches? (which should stay a pure predicate) and into by_uid_and_secret, following the same pattern as find_by_plaintext_tokenfind_by_fallback_tokenupgrade_fallback_value. After confirming a match via secret_matches?, by_uid_and_secret calls upgrade_fallback_value when the primary strategy didn't match (meaning fallback matched). Commit: c02a6fd.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • coveralls.io
    • Triggering command: /usr/bin/ruby3.2 ruby3.2 /home/REDACTED/work/doorkeeper/doorkeeper/vendor/bundle/ruby/3.2.0/bin/rspec spec/models/doorkeeper/application_spec.rb --format documentation -I /home/REDACTED/work/doorkeeper/doorkeeper/vendor/bundle/ruby/3.2.0/gems/prism-1.9.-ffile-prefix-map=BUILDDIR=. --gdwarf-5 --64 -o regexp.o /tmp/cc9ToBQe.s -I by-3.2.0 -I 0/include -I 0/ext -I as (dns block)
    • Triggering command: /usr/bin/ruby3.2 ruby3.2 /home/REDACTED/work/doorkeeper/doorkeeper/vendor/bundle/ruby/3.2.0/bin/rspec spec/models/doorkeeper/ --format progress -I /home/REDACTED/work/doorkeeper/doorkeeper/vendor/bundle/ruby/3.2.0/gems/prism-1.9.0/ext --gdwarf-5 --64 -o token_type.o /tmp/ccA7wHcW.s -I ruby-3.2.0 ackward undle/ruby/3.2.0-I -I undle/ruby/3.2.0-I oorkeeper/vendor. -gcc (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

@nbulaj
Copy link
Copy Markdown
Member

nbulaj commented Apr 24, 2026

@copilot shouldn't fallback happen before return unless app.secret_matches?(secret) is called? Since secret is hashed

Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 24, 2026

@copilot shouldn't fallback happen before return unless app.secret_matches?(secret) is called? Since secret is hashed

Yes, fixed in 921428f. by_uid_and_secret now tries the primary strategy first and returns immediately on match, then explicitly handles the fallback path with the upgrade integrated — so the fallback check (with upgrade) happens as its own step, not as a post-check after a single secret_matches? gate.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • coveralls.io
    • Triggering command: /usr/bin/ruby3.2 ruby3.2 /home/REDACTED/work/doorkeeper/doorkeeper/vendor/bundle/ruby/3.2.0/bin/rspec spec/models/doorkeeper/application_spec.rb --format documentation -I /home/REDACTED/work/doorkeeper/doorkeeper/vendor/bundle/ruby/3.2.0/gems/prism-1.9.0/ext --gdwarf-5 --64 -o regexp.o /tmp/cc0qFKr2.s -I by-3.2.0 -I undle/ruby/3.2.0-Wdate-time -I undle/ruby/3.2.0compiling -I as (dns block)
    • Triggering command: /usr/bin/ruby3.2 ruby3.2 /home/REDACTED/work/doorkeeper/doorkeeper/vendor/bundle/ruby/3.2.0/bin/rspec spec/models/doorkeeper/application_spec.rb --format documentation -I /home/REDACTED/work/doorkeeper/doorkeeper/vendor/bundle/ruby/3.2.0/gems/prism-1.9.0/ext --gdwarf-5 --64 -o token_type.o /tmp/ccWVtk0d.s -I _64-linux-gnu/ru-I pointer undle/ruby/3.2.0-I p=BUILDDIR=. undle/ruby/3.2.0-I otection as (dns block)
    • Triggering command: /usr/bin/ruby3.2 ruby3.2 /home/REDACTED/work/doorkeeper/doorkeeper/vendor/bundle/ruby/3.2.0/bin/rspec spec/models/doorkeeper/application_spec.rb --gdwarf-5 --64 -o iseq_collector.o /tmp/ccz4BQjx.s 3 0/ext /usr/include/rub. -gcc -I 86_64-linux-gnu/-I uby-3.2.0/ruby/b/usr/include/ruby-3.2.0/ruby/backward by-3.2.0 -I kward ork/doorkeeper/d. x86_64-linux-gnu-I (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

app
# Try the primary (active) secret strategy first.
if !secret.nil? && !app.secret.nil? &&
secret_strategy.secret_matches?(secret.to_s, app.secret.to_s)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📝 [rubocop] <Layout/MultilineOperationIndentation> reported by reviewdog 🐶
Align the operands of a condition in an if statement spanning multiple lines.

# to migrate the stored secret to the active strategy (mirrors the
# find_by_plaintext_token -> find_by_fallback_token pattern).
if fallback_secret_strategy && !secret.nil? && !app.secret.nil? &&
fallback_secret_strategy.secret_matches?(secret.to_s, app.secret.to_s)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📝 [rubocop] <Layout/MultilineOperationIndentation> reported by reviewdog 🐶
Align the operands of a condition in an if statement spanning multiple lines.

Comment thread spec/models/doorkeeper/application_spec.rb
Comment thread lib/doorkeeper/models/application_mixin.rb
Comment thread spec/models/doorkeeper/application_spec.rb
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fallback on hash_application_secrets does not upgrade secret when accessed

2 participants