doorkeeper: Recent Change in Token Revocation Endpoint Behavior Breaks Revocation of Tokens for Public Client

Steps to reproduce

I have a mobile app using a Rails server, which in turn depends on Doorkeeper to generate access tokens for API calls. I used to be able to logout successfully from the client using a call to Doorkeeper::TokensController.revoke. However, after upgrading to version Doorkeeper 5.4.0, I got HTTP response 401 during logout.

Expected behavior

Token revocation using a valid access token from Resource Owner Password Credentials Flow should work.

Actual behavior

Token revocation fails with a valid access token from Resource Owner Password Credentials Flow.

System configuration

In version 5.3.3, before this change https://github.com/doorkeeper-gem/doorkeeper/pull/1370/commits/d194bc40e335464e67bc36dfd7e2e61247547545, a token granted to a public client (https://tools.ietf.org/html/rfc6749#section-2.1) via Resource Owner Password Credentials Flow works fine as the method Doorkeeper::TokensController.authorized? is used for authentication.

In version 5.4.0, the method Doorkeeper::TokensController.revoke is modified to require a server.client, which is a confidential client (https://tools.ietf.org/html/rfc6749#section-2.1).

Ruby version: 2.6.5

About this issue

  • Original URL
  • State: closed
  • Created 4 years ago
  • Comments: 30 (21 by maintainers)

Commits related to this issue

Most upvoted comments

You’re saying that all those years I was using doorkeeper with password grant flow and without creating an application… was a bug. That bug is probably the reason a lot of people love doorkeeper. Simplicity and literally zero setup to have email+password authentication in any project. 😄

I guess you can make this a known issue on the release notes before closing this. That could save other’s time to research on this.

It’s not an issue actually, and it’s a known breaking change:

Known (at least now) issue is that you guys could use ROPC grant without using a client which would be fixed soon, yep.

I think I can close it for now, just need to remember issue number for the same threads in the near future.

Since there is nothing much I can do except to downgrade to version 5.3.3, I guess you can make this a known issue on the release notes before closing this. That could save other’s time to research on this. Thanks.

Yep, this is one of the options I think about. I just don’t like to bloat the configuration with a lot of options, especially when there is no need in them.

So I think I’ll add such option, but with deprecation and it will be removed in some major update.

I don’t sure why it’s so hard to create one record and use it’s credentials in requests…

It’s not exactly about it being hard. It’s pointless when it’s for a front-end app that’s all in a browser. Right now I can have two dockerized projects that require running docker-compose up and everything works out of the box. UI communicates with the API and everything’s great. I don’t have to go into oauth applications endpoint, then create an app, then update env var so the UI knows client_id (or I don’t have to provide client_id at build time so compiled code has it) and so on.

Oh guys, I think I have bad news for you (and everyone else).

I think we must fix this too and break a lot of clients that creates tokens without any client authorization 😦 I foresee much more such issues in a near future…

If the client type is confidential or the client was issued client credentials (or assigned other authentication requirements), the client MUST authenticate with the authorization server as described in Section 3.2.1.

We have confidential column that indicates if application is public/private and always generate an ID (and secret in case of private client). So we must always require authorization for this grant flow

Public client means it doesn’t have s secret, just ID. Public client means that client exists and it could be identified just by ID. Please check also section 5. Security Considerations of RFC7009:

A malicious client may attempt to guess valid tokens on this endpoint by making revocation requests against potential token strings. According to this specification, a client’s request must contain a valid client_id, in the case of a public client, or valid client credentials, in the case of a confidential client. The token being revoked must also belong to the requesting client.

Sorry, but I wouldn’t support this “May. Doesn’t have to” for the security reasons. Nothing actually stops you from creating a public client and using it’s credentials to authorize the request.

Wouldn’t lack of application impact the flow somehow?

Nope. Lack of authentication just open a security hole for brute-forcing (throtling) any tokens you have.

Okay, I’ve taken a look.

RFC7009#2.1 says that token is required and authentication must be included as stated in RFC6749#2.3.

RFC7009#2.1 also provides an example that is considered valid:

     POST /revoke HTTP/1.1
     Host: server.example.com
     Content-Type: application/x-www-form-urlencoded
     Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW

     token=45ghiukldjahdnhzdauz&token_type_hint=refresh_token

So Basic Authorization header would be the client_id + client_secret if I understand correctly.

But there’s RFC6749#2.3. First paragraph - only for confidential clients. Second paragraph - continuation of that. Third paragraph:

The authorization server MAY establish a client authentication method with public clients.

May. Doesn’t have to. So shouldn’t this have worked when testing:

post oauth_revoke_path, 
     params: { token: access_token.token }

It has token which is required. That token has no application (client). Wouldn’t lack of application impact the flow somehow? Before the changes in latest version, token was derived from headers: { 'Authorization': "Bearer #{access_token.token}" } implicitly somehow.

I feel like I’m still getting this wrong or missing some crucial part here.

Hi @edmondchui .

Did you do this from your mobile app?

The client also includes its authentication credentials as described in Section 2.3. of [RFC6749].

server.client which you mentioned just stores client instance that performs a request. It is taken from the credentials used to authorize the request. And if it’s blank - aka no auth passed or wrong credentials used - request is blocked as stated in the the RFC.

You need to call POST /token/revoke with ?token= value of access token you wanna to revoke and include authorization headers (HTTP-Authrozation: Basic Base64(client_id:client_secret)) to inform the server that this request is authorized to perform an action. Alternatively you could send client_id and client_secret via body params, but this method isn’t recommended by the RFC. Don’t forget that if the token you wanna to revoke was issued to some specific client - only this client could revoke the token (it’s credentials must be used to authorize the request).

More details: section 2.1. Revocation Request of the RFC7009 and then section 2.3. Client Authentication of RFC6749