Skip to content

Commit

Permalink
Validate tokens in Token Introspection
Browse files Browse the repository at this point in the history
Don't allow to introspect token using the same token for
authorization.
  • Loading branch information
nbulaj committed Mar 7, 2019
1 parent 3295d62 commit 8777f67
Show file tree
Hide file tree
Showing 3 changed files with 62 additions and 38 deletions.
3 changes: 2 additions & 1 deletion NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ User-visible changes worth mentioning.

## master

- [#1202] Use correct HTTP status codes for error responses
- [#1209] Fix tokes validation for Token Introspection request.
- [#1202] Use correct HTTP status codes for error responses.

**[IMPORTANT]**: this change might break your application if you were relying on the previous
401 status codes, this is now a 400 by default, or a 401 for `invalid_client` and `invalid_token` errors.
Expand Down
21 changes: 20 additions & 1 deletion lib/doorkeeper/oauth/token_introspection.rb
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,27 @@ def to_json
#
# @see https://www.oauth.com/oauth2-servers/token-introspection-endpoint/
#
# To prevent token scanning attacks, the endpoint MUST also require
# some form of authorization to access this endpoint, such as client
# authentication as described in OAuth 2.0 [RFC6749] or a separate
# OAuth 2.0 access token such as the bearer token described in OAuth
# 2.0 Bearer Token Usage [RFC6750].
#
def authorize!
# Requested client authorization
if server.credentials
@error = :invalid_client unless authorized_client
elsif authorized_token
# Requested bearer token authorization
@error = :invalid_token unless authorized_token.accessible?
#
# If the protected resource uses an OAuth 2.0 bearer token to authorize
# its call to the introspection endpoint and the token used for
# authorization does not contain sufficient privileges or is otherwise
# invalid for this request, the authorization server responds with an
# HTTP 401 code as described in Section 3 of OAuth 2.0 Bearer Token
# Usage [RFC6750].
#
@error = :invalid_token if authorized_token_matches_introspected? || !authorized_token.accessible?
else
@error = :invalid_request
end
Expand Down Expand Up @@ -147,6 +161,11 @@ def valid_token?
@token && @token.accessible?
end

# RFC7662 Section 2.1
def authorized_token_matches_introspected?
authorized_token.token == @token.token
end

# If token doesn't belong to some client, then it is public.
# Otherwise in it required for token to be connected to the same client.
def authorized_for_client?
Expand Down
76 changes: 40 additions & 36 deletions spec/controllers/tokens_controller_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -130,28 +130,26 @@
end

describe 'when requested token introspection' do
context 'authorized using Bearer token' do
let(:client) { FactoryBot.create(:application) }
let(:access_token) { FactoryBot.create(:access_token, application: client) }
let(:client) { FactoryBot.create(:application) }
let(:access_token) { FactoryBot.create(:access_token, application: client) }
let(:token_for_introspection) { FactoryBot.create(:access_token, application: client) }

context 'authorized using valid Bearer token' do
it 'responds with full token introspection' do
request.headers['Authorization'] = "Bearer #{access_token.token}"

post :introspect, params: { token: access_token.token }
post :introspect, params: { token: token_for_introspection.token }

should_have_json 'active', true
expect(json_response).to include('client_id', 'token_type', 'exp', 'iat')
end
end

context 'authorized using Client Authentication' do
let(:client) { FactoryBot.create(:application) }
let(:access_token) { FactoryBot.create(:access_token, application: client) }

context 'authorized using valid Client Authentication' do
it 'responds with full token introspection' do
request.headers['Authorization'] = basic_auth_header_for_client(client)

post :introspect, params: { token: access_token.token }
post :introspect, params: { token: token_for_introspection.token }

should_have_json 'active', true
expect(json_response).to include('client_id', 'token_type', 'exp', 'iat')
Expand All @@ -160,9 +158,6 @@
end

context 'using custom introspection response' do
let(:client) { FactoryBot.create(:application) }
let(:access_token) { FactoryBot.create(:access_token, application: client) }

before do
Doorkeeper.configure do
orm DOORKEEPER_ORM
Expand All @@ -178,7 +173,7 @@
it 'responds with full token introspection' do
request.headers['Authorization'] = "Bearer #{access_token.token}"

post :introspect, params: { token: access_token.token }
post :introspect, params: { token: token_for_introspection.token }

expect(json_response).to include('client_id', 'token_type', 'exp', 'iat', 'sub', 'aud')
should_have_json 'sub', 'Z5O3upPC88QrAjx00dis'
Expand All @@ -187,13 +182,12 @@
end

context 'public access token' do
let(:client) { FactoryBot.create(:application) }
let(:access_token) { FactoryBot.create(:access_token, application: nil) }
let(:token_for_introspection) { FactoryBot.create(:access_token, application: nil) }

it 'responds with full token introspection' do
request.headers['Authorization'] = basic_auth_header_for_client(client)

post :introspect, params: { token: access_token.token }
post :introspect, params: { token: token_for_introspection.token }

should_have_json 'active', true
expect(json_response).to include('client_id', 'token_type', 'exp', 'iat')
Expand All @@ -202,14 +196,12 @@
end

context 'token was issued to a different client than is making this request' do
let(:client) { FactoryBot.create(:application) }
let(:different_client) { FactoryBot.create(:application) }
let(:access_token) { FactoryBot.create(:access_token, application: client) }

it 'responds with only active state' do
request.headers['Authorization'] = basic_auth_header_for_client(different_client)

post :introspect, params: { token: access_token.token }
post :introspect, params: { token: token_for_introspection.token }

expect(response).to be_successful

Expand All @@ -219,12 +211,25 @@
end

context 'authorized using invalid Bearer token' do
let(:client) { FactoryBot.create(:application) }
let(:access_token) { FactoryBot.create(:access_token, application: client) }
let(:intro_access_token) { FactoryBot.create(:access_token, application: client, revoked_at: 1.day.ago) }
let(:token_for_introspection) do
FactoryBot.create(:access_token, application: client, revoked_at: 1.day.ago)
end

it 'responds with invalid token error' do
request.headers['Authorization'] = "Bearer #{token_for_introspection.token}"

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

response_status_should_be 401

it 'responds with invalid client error' do
request.headers['Authorization'] = "Bearer #{intro_access_token.token}"
should_not_have_json 'active'
should_have_json 'error', 'invalid_token'
end
end

context 'authorized using the Bearer token that need to be introspected' do
it 'responds with invalid token error' do
request.headers['Authorization'] = "Bearer #{access_token.token}"

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

Expand Down Expand Up @@ -253,9 +258,6 @@
end

context 'using wrong token value' do
let(:client) { FactoryBot.create(:application) }
let(:access_token) { FactoryBot.create(:access_token, application: client) }

it 'responds with only active state' do
request.headers['Authorization'] = basic_auth_header_for_client(client)

Expand All @@ -266,39 +268,41 @@
end
end

context 'when requested Access Token expired' do
let(:client) { FactoryBot.create(:application) }
let(:access_token) { FactoryBot.create(:access_token, application: client, created_at: 1.year.ago) }
context 'when requested access token expired' do
let(:token_for_introspection) do
FactoryBot.create(:access_token, application: client, created_at: 1.year.ago)
end

it 'responds with only active state' do
request.headers['Authorization'] = basic_auth_header_for_client(client)

post :introspect, params: { token: access_token.token }
post :introspect, params: { token: token_for_introspection.token }

should_have_json 'active', false
expect(json_response).not_to include('client_id', 'token_type', 'exp', 'iat')
end
end

context 'when requested Access Token revoked' do
let(:client) { FactoryBot.create(:application) }
let(:access_token) { FactoryBot.create(:access_token, application: client, revoked_at: 1.year.ago) }
let(:token_for_introspection) do
FactoryBot.create(:access_token, application: client, revoked_at: 1.year.ago)
end

it 'responds with only active state' do
request.headers['Authorization'] = basic_auth_header_for_client(client)

post :introspect, params: { token: access_token.token }
post :introspect, params: { token: token_for_introspection.token }

should_have_json 'active', false
expect(json_response).not_to include('client_id', 'token_type', 'exp', 'iat')
end
end

context 'unauthorized (no bearer token or client credentials)' do
let(:access_token) { FactoryBot.create(:access_token) }
let(:token_for_introspection) { FactoryBot.create(:access_token) }

it 'responds with invalid_request error' do
post :introspect, params: { token: access_token.token }
post :introspect, params: { token: token_for_introspection.token }

expect(response).not_to be_successful
response_status_should_be 400
Expand Down

0 comments on commit 8777f67

Please sign in to comment.