Skip to content

Commit fb93805

Browse files
f3ndottute
authored andcommitted
Properly implement optional authentication for token revocation
Previously, Doorkeeper failed to implement OAuth 2.0 Token Revocation (RFC 7009) in the following ways: 1. Public clients making valid, unauthenticated calls to revoke a token would not have their token revoked 2. Requests were not properly authenticating the *client credentials* but were, instead, looking at the access token in a second location 3. Because of 2, the requests were also not authorizing confidential clients' ability to revoke a given token. It should only revoke tokens that belong to it. This patch assumes that all public clients issue tokens with a NULL application_id, which may or may not be completely correct. CVE-2016-6582 has been assigned due to the security issues raised. An attacker, thanks to 1, can replay a hijacked session after a victim logs out/revokes their token. Additionally, thanks to 2 & 3, an attacker via a compromised confidential client could "grief" other clients by revoking their tokens (albeit this is an exceptionally narrow attack with little value). This patch: 1. Let's public clients revoke their access & refresh tokens anonymously (as per spec) 2. Authenticates OAuth 2.0 client/application credentials before revoking a token if it belongs to a confidential client (as per spec) 3. Verifies that an authenticated client owns the token it wishes to revoke in the request. (as per spec) [fixes #875]
1 parent fffe0ea commit fb93805

File tree

3 files changed

+150
-100
lines changed

3 files changed

+150
-100
lines changed

app/controllers/doorkeeper/tokens_controller.rb

+49-13
Original file line numberDiff line numberDiff line change
@@ -11,29 +11,65 @@ def create
1111

1212
# OAuth 2.0 Token Revocation - http://tools.ietf.org/html/rfc7009
1313
def revoke
14-
# The authorization server first validates the client credentials
15-
if doorkeeper_token && doorkeeper_token.accessible?
16-
# Doorkeeper does not use the token_type_hint logic described in the RFC 7009
17-
# due to the refresh token implementation that is a field in the access token model.
18-
revoke_token(request.POST['token']) if request.POST['token']
14+
# The authorization server, if applicable, first authenticates the client
15+
# and checks its ownership of the provided token.
16+
#
17+
# Doorkeeper does not use the token_type_hint logic described in the
18+
# RFC 7009 due to the refresh token implementation that is a field in
19+
# the access token model.
20+
if authorized?
21+
revoke_token
1922
end
20-
# The authorization server responds with HTTP status code 200 if the
21-
# token has been revoked successfully or if the client submitted an invalid token
23+
24+
# The authorization server responds with HTTP status code 200 if the token
25+
# has been revoked successfully or if the client submitted an invalid
26+
# token
2227
render json: {}, status: 200
2328
end
2429

2530
private
2631

27-
def revoke_token(token)
28-
token = AccessToken.by_token(token) || AccessToken.by_refresh_token(token)
29-
if token && doorkeeper_token.same_credential?(token)
32+
# OAuth 2.0 Section 2.1 defines two client types, "public" & "confidential".
33+
# Public clients (as per RFC 7009) do not require authentication whereas
34+
# confidential clients must be authenticated for their token revocation.
35+
#
36+
# Once a confidential client is authenticated, it must be authorized to
37+
# revoke the provided access or refresh token. This ensures one client
38+
# cannot revoke another's tokens.
39+
#
40+
# Doorkeeper determines the client type implicitly via the presence of the
41+
# OAuth client associated with a given access or refresh token. Since public
42+
# clients authenticate the resource owner via "password" or "implicit" grant
43+
# types, they set the application_id as null (since the claim cannot be
44+
# verified).
45+
#
46+
# https://tools.ietf.org/html/rfc6749#section-2.1
47+
# https://tools.ietf.org/html/rfc7009
48+
def authorized?
49+
if token.present?
50+
# Client is confidential, therefore client authentication & authorization
51+
# is required
52+
if token.application_id?
53+
# We authorize client by checking token's application
54+
server.client && server.client.application == token.application
55+
else
56+
# Client is public, authentication unnecessary
57+
true
58+
end
59+
end
60+
end
61+
62+
def revoke_token
63+
if token.accessible?
3064
token.revoke
31-
true
32-
else
33-
false
3465
end
3566
end
3667

68+
def token
69+
@token ||= AccessToken.by_token(request.POST['token']) ||
70+
AccessToken.by_refresh_token(request.POST['token'])
71+
end
72+
3773
def strategy
3874
@strategy ||= server.token_request params[:grant_type]
3975
end

spec/controllers/tokens_controller_spec.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@
6464
describe 'when revoke authorization has failed' do
6565
# http://tools.ietf.org/html/rfc7009#section-2.2
6666
it 'returns no error response' do
67-
token = double(:token, authorize: false)
67+
token = double(:token, authorize: false, application_id?: true)
6868
allow(controller).to receive(:token) { token }
6969

7070
post :revoke

spec/requests/flows/revoke_token_spec.rb

+100-86
Original file line numberDiff line numberDiff line change
@@ -8,135 +8,149 @@
88
context 'with default parameters' do
99
let(:client_application) { FactoryGirl.create :application }
1010
let(:resource_owner) { User.create!(name: 'John', password: 'sekret') }
11-
let(:authorization_access_token) do
11+
let(:access_token) do
1212
FactoryGirl.create(:access_token,
1313
application: client_application,
1414
resource_owner_id: resource_owner.id,
1515
use_refresh_token: true)
1616
end
17-
let(:headers) { { 'HTTP_AUTHORIZATION' => "Bearer #{authorization_access_token.token}" } }
1817

19-
context 'With invalid token to revoke' do
20-
it 'client wants to revoke the given access token' do
21-
post revocation_token_endpoint_url, { token: 'I_AM_AN_INVALIDE_TOKEN' }, headers
22-
23-
authorization_access_token.reload
24-
# The authorization server responds with HTTP status code 200 if the token
25-
# has been revoked successfully or if the client submitted an invalid token.
26-
expect(response).to be_success
27-
expect(authorization_access_token).to_not be_revoked
18+
context 'with authenticated, confidential OAuth 2.0 client/application' do
19+
let(:headers) do
20+
client_id = client_application.uid
21+
client_secret = client_application.secret
22+
credentials = Base64.encode64("#{client_id}:#{client_secret}")
23+
{ 'HTTP_AUTHORIZATION' => "Basic #{credentials}" }
2824
end
29-
end
30-
31-
context 'The access token to revoke is the same than the authorization access token' do
32-
let(:token_to_revoke) { authorization_access_token }
3325

34-
it 'client wants to revoke the given access token' do
35-
post revocation_token_endpoint_url, { token: token_to_revoke.token }, headers
26+
it 'should revoke the access token provided' do
27+
post revocation_token_endpoint_url, { token: access_token.token }, headers
3628

37-
token_to_revoke.reload
38-
authorization_access_token.reload
29+
access_token.reload
3930

4031
expect(response).to be_success
41-
expect(token_to_revoke.revoked?).to be_truthy
42-
expect(Doorkeeper::AccessToken.by_refresh_token(token_to_revoke.refresh_token).revoked?).to be_truthy
32+
expect(access_token.revoked?).to be_truthy
4333
end
4434

45-
it 'client wants to revoke the given access token using the POST query string' do
46-
url_with_query_string = revocation_token_endpoint_url + '?' + Rack::Utils.build_query(token: token_to_revoke.token)
47-
post url_with_query_string, {}, headers
35+
it 'should revoke the refresh token provided' do
36+
post revocation_token_endpoint_url, { token: access_token.refresh_token }, headers
4837

49-
token_to_revoke.reload
50-
authorization_access_token.reload
38+
access_token.reload
5139

5240
expect(response).to be_success
53-
expect(token_to_revoke.revoked?).to be_falsey
54-
expect(Doorkeeper::AccessToken.by_refresh_token(token_to_revoke.refresh_token).revoked?).to be_falsey
55-
expect(authorization_access_token.revoked?).to be_falsey
41+
expect(access_token.revoked?).to be_truthy
5642
end
57-
end
5843

59-
context 'The access token to revoke app and owners are the same than the authorization access token' do
60-
let(:token_to_revoke) do
61-
FactoryGirl.create(:access_token,
62-
application: client_application,
63-
resource_owner_id: resource_owner.id,
64-
use_refresh_token: true)
44+
context 'with invalid token to revoke' do
45+
it 'should not revoke any tokens and respond successfully' do
46+
num_prev_revoked_tokens = Doorkeeper::AccessToken.where(revoked_at: nil).count
47+
post revocation_token_endpoint_url, { token: 'I_AM_AN_INVALID_TOKEN' }, headers
48+
49+
# The authorization server responds with HTTP status code 200 even if
50+
# token is invalid
51+
expect(response).to be_success
52+
expect(Doorkeeper::AccessToken.where(revoked_at: nil).count).to eq(num_prev_revoked_tokens)
53+
end
6554
end
6655

67-
it 'client wants to revoke the given access token' do
68-
post revocation_token_endpoint_url, { token: token_to_revoke.token }, headers
56+
context 'with bad credentials and a valid token' do
57+
let(:headers) do
58+
client_id = client_application.uid
59+
credentials = Base64.encode64("#{client_id}:poop")
60+
{ 'HTTP_AUTHORIZATION' => "Basic #{credentials}" }
61+
end
62+
it 'should not revoke any tokens and respond successfully' do
63+
post revocation_token_endpoint_url, { token: access_token.token }, headers
6964

70-
token_to_revoke.reload
71-
authorization_access_token.reload
65+
access_token.reload
7266

73-
expect(response).to be_success
74-
expect(token_to_revoke.revoked?).to be_truthy
75-
expect(Doorkeeper::AccessToken.by_refresh_token(token_to_revoke.refresh_token).revoked?).to be_truthy
76-
expect(authorization_access_token.revoked?).to be_falsey
67+
expect(response).to be_success
68+
expect(access_token.revoked?).to be_falsey
69+
end
7770
end
78-
end
7971

80-
context 'The access token to revoke authorization owner is the same than the authorization access token' do
81-
let(:other_client_application) { FactoryGirl.create :application }
82-
let(:token_to_revoke) do
83-
FactoryGirl.create(:access_token,
84-
application: other_client_application,
85-
resource_owner_id: resource_owner.id,
86-
use_refresh_token: true)
72+
context 'with no credentials and a valid token' do
73+
it 'should not revoke any tokens and respond successfully' do
74+
post revocation_token_endpoint_url, { token: access_token.token }
75+
76+
access_token.reload
77+
78+
expect(response).to be_success
79+
expect(access_token.revoked?).to be_falsey
80+
end
8781
end
8882

89-
it 'client wants to revoke the given access token' do
90-
post revocation_token_endpoint_url, { token: token_to_revoke.token }, headers
83+
context 'with valid token for another client application' do
84+
let(:other_client_application) { FactoryGirl.create :application }
85+
let(:headers) do
86+
client_id = other_client_application.uid
87+
client_secret = other_client_application.secret
88+
credentials = Base64.encode64("#{client_id}:#{client_secret}")
89+
{ 'HTTP_AUTHORIZATION' => "Basic #{credentials}" }
90+
end
9191

92-
token_to_revoke.reload
93-
authorization_access_token.reload
92+
it 'should not revoke the token as its unauthorized' do
93+
post revocation_token_endpoint_url, { token: access_token.token }, headers
9494

95-
expect(response).to be_success
96-
expect(token_to_revoke.revoked?).to be_falsey
97-
expect(Doorkeeper::AccessToken.by_refresh_token(token_to_revoke.refresh_token).revoked?).to be_falsey
98-
expect(authorization_access_token.revoked?).to be_falsey
95+
access_token.reload
96+
97+
expect(response).to be_success
98+
expect(access_token.revoked?).to be_falsey
99+
end
99100
end
100101
end
101102

102-
context 'The access token to revoke app is the same than the authorization access token' do
103-
let(:other_resource_owner) { User.create!(name: 'Matheo', password: 'pareto') }
104-
let(:token_to_revoke) do
103+
context 'with public OAuth 2.0 client/application' do
104+
let(:access_token) do
105105
FactoryGirl.create(:access_token,
106-
application: client_application,
107-
resource_owner_id: other_resource_owner.id,
106+
application: nil,
107+
resource_owner_id: resource_owner.id,
108108
use_refresh_token: true)
109109
end
110110

111-
it 'client wants to revoke the given access token' do
112-
post revocation_token_endpoint_url, { token: token_to_revoke.token }, headers
111+
it 'should revoke the access token provided' do
112+
post revocation_token_endpoint_url, { token: access_token.token }
113113

114-
token_to_revoke.reload
115-
authorization_access_token.reload
114+
access_token.reload
116115

117116
expect(response).to be_success
118-
expect(token_to_revoke.revoked?).to be_falsey
119-
expect(Doorkeeper::AccessToken.by_refresh_token(token_to_revoke.refresh_token).revoked?).to be_falsey
120-
expect(authorization_access_token.revoked?).to be_falsey
117+
expect(access_token.revoked?).to be_truthy
121118
end
122-
end
123119

124-
context 'With valid refresh token to revoke' do
125-
let(:token_to_revoke) do
126-
FactoryGirl.create(:access_token,
127-
application: client_application,
128-
resource_owner_id: resource_owner.id,
129-
use_refresh_token: true)
130-
end
120+
it 'should revoke the refresh token provided' do
121+
post revocation_token_endpoint_url, { token: access_token.refresh_token }
131122

132-
it 'client wants to revoke the given refresh token' do
133-
post revocation_token_endpoint_url, { token: token_to_revoke.refresh_token, token_type_hint: 'refresh_token' }, headers
134-
authorization_access_token.reload
135-
token_to_revoke.reload
123+
access_token.reload
136124

137125
expect(response).to be_success
138-
expect(Doorkeeper::AccessToken.by_refresh_token(token_to_revoke.refresh_token).revoked?).to be_truthy
139-
expect(authorization_access_token).to_not be_revoked
126+
expect(access_token.revoked?).to be_truthy
127+
end
128+
129+
context 'with a valid token issued for a confidential client' do
130+
let(:access_token) do
131+
FactoryGirl.create(:access_token,
132+
application: client_application,
133+
resource_owner_id: resource_owner.id,
134+
use_refresh_token: true)
135+
end
136+
137+
it 'should not revoke the access token provided' do
138+
post revocation_token_endpoint_url, { token: access_token.token }
139+
140+
access_token.reload
141+
142+
expect(response).to be_success
143+
expect(access_token.revoked?).to be_falsey
144+
end
145+
146+
it 'should not revoke the refresh token provided' do
147+
post revocation_token_endpoint_url, { token: access_token.token }
148+
149+
access_token.reload
150+
151+
expect(response).to be_success
152+
expect(access_token.revoked?).to be_falsey
153+
end
140154
end
141155
end
142156
end

0 commit comments

Comments
 (0)