Skip to content

Conversation

@ThisIsMissEm
Copy link
Contributor

@ThisIsMissEm ThisIsMissEm commented Oct 2, 2024

Summary

This is early work on supporting #1587, it's currently based off an older commit from #1732, #1735, and #1736 since those allow me to work semi-productively on the doorkeeper codebase.

I'm unsure if we'd really need a custom controller for discovery, as I think the initializer for configuration should probably handle the "adding extra stuff to the .well-known/oauth-authorization-server endpoint, instead of actually overriding the controller.

I don't think it makes sense for the scope option with doorkeeper to affect the .well-known/oauth-authorization-server endpoint, however, nesting that within a scope can work (allowing multiple oauth providers on the one domain, similar to how keycloak works)

I'm still to figure out all the necessary stuff for actually building out the data for response. I think I'm probably inclined to create a Doorkeeper::OAuth::DiscoveryResponse class, and a custom_discovery_response configuration block option, then have the controller just apply that class.

There's also the question of "should this be rename-able by other plugins?", e.g., OIDC that uses .well-known/openid-configuration instead of .well-known/oauth-authorization-server — I'd probably guess maybe no, and instead the OIDC plugin could just remove the .well-known/oauth-authorization-server route and add its own which is calling a subclass of Doorkeeper::OAuth::DiscoveryResponse?

@ThisIsMissEm ThisIsMissEm changed the title Feat/implement oauth authorization server metadata Feat: Implement oauth authorization server metadata Oct 2, 2024
@ThisIsMissEm
Copy link
Contributor Author

@nbulaj @ransombriggs would love some feedback on my implementation proposal in the PR description — I think that's the right way forwards, but haven't yet begun to implement.

@ransombriggs
Copy link
Contributor

I would give feedback but I am not familiar with https://www.rfc-editor.org/rfc/rfc8414 and really just a bystander on this project. I got it to do what I needed and then was moved onto other things.

@nbulaj
Copy link
Member

nbulaj commented Oct 30, 2024

I have some feeling that the work should be done inside https://github.com/doorkeeper-gem/doorkeeper-openid_connect 🤔

I remember it handles all these stuff around Discovery and related. Have to recheck deeply

@ThisIsMissEm
Copy link
Contributor Author

I have some feeling that the work should be done inside https://github.com/doorkeeper-gem/doorkeeper-openid_connect 🤔

I remember it handles all these stuff around Discovery and related. Have to recheck deeply

RFC 8414 is part of OAuth and is separate from OIDC's Discovery, but is compatible in some ways.

RFC 8414 uses /.well-known/oauth-authorization-server and OIDC Discovery uses /.well-known/openid_configuration (iirc), but the contents of the documents is largely the same. These differences are noted in RFC 8414

@ThisIsMissEm ThisIsMissEm force-pushed the feat/implement-oauth-authorization-server-metadata branch from a9b73f9 to 9266140 Compare April 19, 2025 21:48
Comment on lines +15 to +16
root_url,
-> (**args) { url_for(**args) }
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm not particularly keen on this, but including UrlFor or similar in the Doorkeeper::OAuth::DiscoveryResponse class didn't work as I'd expect it to, and I need the url builder in order to generate the absolute URLs for the authorization server metadata.

Comment on lines +324 to +325
# Issuer URL for the OAuth Authorization Server
option :issuer, default: nil
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Technically this is maybe OpenID more, but OAuth 2.0 does have a spec for it, see: #1720

Comment on lines +403 to +405
# Allows setting a hash of custom data on the OAuth 2.0 Authorization
# Server Metadata discovery response.
option :custom_discovery_data, default: {}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is intended for applications to use, e.g., Mastodon where we want to set service_documentation and app_registration_url properties in the metadata.


def grant_types_supported
grant_types_supported = config.grant_flows.dup
grant_types_supported << 'refresh_token' if !!config.refresh_token_enabled?
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is technically broken, because use_refresh_token configuration can be a block/proc, which accepts server and context which, well, I don't think we have here, nor would it be possible to have?

Copy link
Contributor Author

@ThisIsMissEm ThisIsMissEm Apr 20, 2025

Choose a reason for hiding this comment

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

Thinking about this more, if we did drastically change how refresh tokens work, as suggested in #1771, then we could have options like:

Doorkeeper.configure do
  orm :active_record

  # Enable using them in general
  use_refresh_tokens true

  # Set the expiry for refresh tokens:
  refresh_token_expires_in 30.days

  # Allow refresh tokens to only be used by specific clients:
  allow_refresh_tokens_for do |client, access_grant|
    access_grant.scopes.exists?('offline_access')
  end
end

Or something.

Comment on lines +16 to +35
expect(json_response).to have_key("issuer")
expect(json_response).to have_key("authorization_endpoint")
expect(json_response).to have_key("token_endpoint")
expect(json_response).to have_key("revocation_endpoint")
expect(json_response).to have_key("userinfo_endpoint")
expect(json_response).to have_key("scopes_supported")
expect(json_response).to have_key("response_types_supported")
expect(json_response).to have_key("response_modes_supported")
expect(json_response).to have_key("grant_types_supported")
expect(json_response).to have_key("token_endpoint_auth_methods_supported")
expect(json_response).to have_key("code_challenge_methods_supported")

expect(json_response["issuer"]).to be_a(String)

expect(json_response["scopes_supported"]).to be_a(Array)
expect(json_response["response_types_supported"]).to be_a(Array)
expect(json_response["response_modes_supported"]).to be_a(Array)
expect(json_response["grant_types_supported"]).to be_a(Array)
expect(json_response["token_endpoint_auth_methods_supported"]).to be_a(Array)
expect(json_response["code_challenge_methods_supported"]).to be_a(Array)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm not sure if I should really test logic of the discovery response here, so instead I'm just asserting properties and types for things that must be arrays.

Copy link
Contributor

Choose a reason for hiding this comment

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

Very similar to what we did for a one-off in our application:

      get oauth_discovery_path

      expect(response).to have_http_status(200)

      expect(response.content_type).to eq("application/json; charset=utf-8")

      object = JSON.parse(response.body)

      expect(object.dig("authorization_endpoint")).to eq("http://www.example.com/oauth/authorize")
      expect(object.dig("token_endpoint")).to eq("http://www.example.com/oauth/token")
      expect(object.dig("revocation_endpoint")).to eq("http://www.example.com/oauth/revoke")
      ...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, similar to the Mastodon one as well: https://github.com/mastodon/mastodon/blob/main/spec/requests/well_known/oauth_metadata_spec.rb

I'm just not sure I want to be validating the values or not here, since the paths can be configured with doorkeeper.

@ThisIsMissEm ThisIsMissEm marked this pull request as ready for review April 19, 2025 21:56
@ThisIsMissEm
Copy link
Contributor Author

@nbulaj @ransombriggs I think this is ready for a look, I think it works okay for now? Not sure if we need to do something else or not, there's a few things I've noted in the comments above.

it "returns json" do
get "/.well-known/oauth-authorization-server"

response_status_should_be(200)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

We should add a content-type assertion here as well.

end

def scopes_supported
config.scopes.to_a
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Might be better as:

Suggested change
config.scopes.to_a
config.scopes.map(&:to_s)

Might depend on how Scopes implements to_a


# FIXME: https://github.com/doorkeeper-gem/doorkeeper/pull/1770
def token_endpoint_auth_methods_supported
%w(none client_secret_basic client_secret_post)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This will be fixed by #1772

@alex-ross
Copy link

Is this kind of forgotten or is it someway to enable a .well-known/oauth-authorization-server trough this gem?

@ThisIsMissEm
Copy link
Contributor Author

@alex-ross this isn't forgotten, work is just progressing more slowly because of time constraints

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.

5 participants