862 lines
32 KiB
Ruby
862 lines
32 KiB
Ruby
require 'spec_helper_min'
|
|
require 'carto/oauth_provider_controller'
|
|
require 'support/helpers'
|
|
require 'helpers/subdomainless_helper'
|
|
|
|
describe Carto::OauthProviderController do
|
|
include HelperMethods
|
|
include_context 'organization with users helper'
|
|
|
|
before(:all) do
|
|
@sequel_developer = FactoryGirl.create(:valid_user)
|
|
@developer = Carto::User.find(@sequel_developer.id)
|
|
@user = FactoryGirl.create(:valid_user)
|
|
end
|
|
|
|
before(:each) do
|
|
@oauth_app = FactoryGirl.create(:oauth_app, user: @developer)
|
|
end
|
|
|
|
after(:each) do
|
|
@oauth_app.reload.destroy
|
|
end
|
|
|
|
after(:all) do
|
|
@developer.destroy
|
|
@user.destroy
|
|
end
|
|
|
|
after(:each) do
|
|
Carto::User.find(@user.id).oauth_app_users.each(&:destroy)
|
|
end
|
|
|
|
let(:valid_payload) do
|
|
{
|
|
client_id: @oauth_app.client_id,
|
|
response_type: 'code',
|
|
state: 'random_state_thingy',
|
|
accept: true
|
|
}
|
|
end
|
|
|
|
def parse_fragment_parameters(uri)
|
|
URI.decode_www_form(Addressable::URI.parse(uri).fragment).to_h
|
|
end
|
|
|
|
def parse_query_parameters(uri)
|
|
Addressable::URI.parse(uri).query_values
|
|
end
|
|
|
|
def validate_token_response(parameters, access_token, refresh_token = nil)
|
|
expect(parameters[:access_token]).to(eq(access_token.api_key.token))
|
|
expect(parameters[:token_type]).to(eq('Bearer'))
|
|
expect(parameters[:expires_in].to_i).to(be_between(3595, 3600)) # Little margin for slowness
|
|
if refresh_token
|
|
expect(parameters[:refresh_token]).to(eq(refresh_token.token))
|
|
else
|
|
expect(parameters[:refresh_token]).to(be_nil)
|
|
end
|
|
expect(parameters[:user_info_url]).to(include(api_v4_users_me_path, access_token.oauth_app_user.user.username))
|
|
end
|
|
|
|
shared_examples_for 'authorization parameter validation' do
|
|
it 'returns a 400 error if application cannot be found' do
|
|
request_endpoint(valid_payload.merge(client_id: 'e'))
|
|
|
|
expect(response.status).to(eq(400))
|
|
expect(response.body).to(include('invalid_client'))
|
|
end
|
|
|
|
it 'shows an error if invalid response_type' do
|
|
request_endpoint(valid_payload.merge(response_type: 'err'))
|
|
|
|
expect(response.status).to(eq(400))
|
|
expect(response.body).to(include('unsupported_response_type'))
|
|
end
|
|
|
|
shared_examples_for 'invalid parameter redirections' do
|
|
it 'redirects with an error if missing state' do
|
|
request_endpoint(valid_payload.merge(state: ''))
|
|
|
|
expect(response.status).to(eq(302))
|
|
expect(response.location).to(start_with(@oauth_app.redirect_uris.first))
|
|
qs = parse_uri_parameters(response.location)
|
|
expect(qs['error']).to(eq('invalid_request'))
|
|
expect(qs['error_description']).to(eq('The following required params are missing: state'))
|
|
end
|
|
|
|
it 'redirects with an error if requesting unknown scopes' do
|
|
request_endpoint(valid_payload.merge(scope: 'invalid wadus'))
|
|
|
|
expect(response.status).to(eq(302))
|
|
expect(response.location).to(start_with(@oauth_app.redirect_uris.first))
|
|
qs = parse_uri_parameters(response.location)
|
|
expect(qs['error']).to(eq('invalid_scope'))
|
|
end
|
|
|
|
it 'redirects with an error if requesting non-existent datasets' do
|
|
request_endpoint(valid_payload.merge(scope: 'datasets:r:blabla'))
|
|
|
|
expect(response.status).to(eq(302))
|
|
expect(response.location).to(start_with(@oauth_app.redirect_uris.first))
|
|
qs = parse_uri_parameters(response.location)
|
|
expect(qs['error']).to(eq('invalid_scope'))
|
|
end
|
|
|
|
it 'redirects with an error if requesting non-existent schemas' do
|
|
request_endpoint(valid_payload.merge(scope: 'schemas:c:blabla'))
|
|
|
|
expect(response.status).to(eq(302))
|
|
expect(response.location).to(start_with(@oauth_app.redirect_uris.first))
|
|
qs = parse_uri_parameters(response.location)
|
|
expect(qs['error']).to(eq('invalid_scope'))
|
|
end
|
|
|
|
it 'redirects with an error if requesting invalid dataset scopes' do
|
|
user_table = FactoryGirl.create(:carto_user_table, :with_db_table, user_id: @developer.id)
|
|
request_endpoint(valid_payload.merge(scope: "datasets:wtf:#{user_table.name}"))
|
|
|
|
expect(response.status).to(eq(302))
|
|
expect(response.location).to(start_with(@oauth_app.redirect_uris.first))
|
|
qs = parse_uri_parameters(response.location)
|
|
expect(qs['error']).to(eq('invalid_scope'))
|
|
end
|
|
|
|
it 'redirects with an error if requesting invalid schema scopes' do
|
|
request_endpoint(valid_payload.merge(scope: "schemas:wtf:#{@developer.database_schema}"))
|
|
|
|
expect(response.status).to(eq(302))
|
|
expect(response.location).to(start_with(@oauth_app.redirect_uris.first))
|
|
qs = parse_uri_parameters(response.location)
|
|
expect(qs['error']).to(eq('invalid_scope'))
|
|
end
|
|
|
|
it 'redirects with an error if requesting with an invalid redirect_uri' do
|
|
request_endpoint(valid_payload.merge(redirect_uri: 'invalid'))
|
|
|
|
expect(response.status).to(eq(302))
|
|
expect(response.location).to(start_with(@oauth_app.redirect_uris.first))
|
|
qs = parse_uri_parameters(response.location)
|
|
expect(qs['error']).to(eq('invalid_request'))
|
|
expect(qs['error_description']).to(eq('The redirect_uri must match the redirect_uri param used in the authorization request'))
|
|
end
|
|
|
|
describe 'with restricted app' do
|
|
before(:each) do
|
|
@oauth_app.update!(restricted: true)
|
|
@org_authorization = @oauth_app.oauth_app_organizations.create!(organization: @carto_organization, seats: 1)
|
|
end
|
|
|
|
it 'redirects with an error if the user is not an organization member' do
|
|
request_endpoint(valid_payload)
|
|
|
|
expect(response.status).to(eq(302))
|
|
expect(response.location).to(start_with(@oauth_app.redirect_uris.first))
|
|
qs = parse_uri_parameters(response.location)
|
|
expect(qs['error']).to(eq('access_denied'))
|
|
expect(qs['error_description']).to(eq('User is not part of an organization'))
|
|
end
|
|
|
|
it 'succeeds if logged in as a member of an allowed organization' do
|
|
logout
|
|
login_as(@org_user_1, scope: @org_user_1.username)
|
|
request_endpoint(valid_payload)
|
|
|
|
expect_success(response)
|
|
end
|
|
|
|
it 'redirects with an error if the organization is out of seats for the application' do
|
|
@oauth_app.oauth_app_users.create!(user: @carto_org_user_2)
|
|
logout
|
|
login_as(@org_user_1, scope: @org_user_1.username)
|
|
request_endpoint(valid_payload)
|
|
|
|
expect(response.status).to(eq(302))
|
|
expect(response.location).to(start_with(@oauth_app.redirect_uris.first))
|
|
qs = parse_uri_parameters(response.location)
|
|
expect(qs['error']).to(eq('access_denied'))
|
|
expect(qs['error_description']).to(eq('User does not have an available seat to use this application'))
|
|
end
|
|
end
|
|
end
|
|
|
|
describe 'with code response' do
|
|
it_behaves_like 'invalid parameter redirections' do
|
|
def parse_uri_parameters(uri)
|
|
parse_query_parameters(uri)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe 'with token response' do
|
|
it_behaves_like 'invalid parameter redirections' do
|
|
before(:each) do
|
|
valid_payload[:response_type] = 'token'
|
|
end
|
|
|
|
def parse_uri_parameters(uri)
|
|
parse_fragment_parameters(uri)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#consent' do
|
|
before(:each) do
|
|
login_as(@user, scope: @user.username)
|
|
host!("#{@user.username}.localhost.lan")
|
|
end
|
|
|
|
it_behaves_like 'authorization parameter validation' do
|
|
def request_endpoint(parameters)
|
|
get oauth_provider_authorize_url(parameters)
|
|
end
|
|
|
|
def expect_success(response)
|
|
expect(response.status).to(eq(200))
|
|
end
|
|
end
|
|
|
|
shared_examples_for 'success with response pre-authorized' do
|
|
it 'with valid payload and code response, pre-authorized, redirects back to the application' do
|
|
oau = @oauth_app.oauth_app_users.create!(user_id: @user.id)
|
|
get oauth_provider_authorize_url(valid_payload.merge(response_type: 'code'))
|
|
|
|
authorization_code = oau.oauth_authorization_codes.first
|
|
expect(authorization_code).to(be)
|
|
expect(authorization_code.code).to(be_present)
|
|
|
|
expect(response.status).to(eq(302))
|
|
expect(parse_query_parameters(response.location)['code']).to(eq(authorization_code.code))
|
|
end
|
|
|
|
it 'with valid payload and token response, pre-authorized, redirects back to the application' do
|
|
oau = @oauth_app.oauth_app_users.create!(user_id: @user.id)
|
|
get oauth_provider_authorize_url(valid_payload.merge(response_type: 'token'))
|
|
|
|
access_token = oau.oauth_access_tokens.first
|
|
expect(access_token).to(be)
|
|
expect(access_token.api_key).to(be_present)
|
|
|
|
expect(response.status).to(eq(302))
|
|
response_parameters = parse_fragment_parameters(response.location)
|
|
validate_token_response(response_parameters.symbolize_keys, access_token)
|
|
end
|
|
end
|
|
|
|
describe 'domains and authentication' do
|
|
it 'works with a URL for another username/org' do
|
|
# e.g: org.carto.com/oauth2 should work, even if the correct one is org.carto.com/u/username/oauth2
|
|
stub_domainful('wadus')
|
|
expect(oauth_provider_authorize_url).not_to(include(@user.username))
|
|
get oauth_provider_authorize_url(valid_payload)
|
|
expect(response.status).to(eq(200))
|
|
end
|
|
|
|
it 'in subdomainless, should not require username at all' do
|
|
stub_subdomainless
|
|
expect(oauth_provider_authorize_url).not_to(include(@user.username))
|
|
get oauth_provider_authorize_url(valid_payload)
|
|
expect(response.status).to(eq(200))
|
|
end
|
|
end
|
|
|
|
it 'logged out, redirects to login' do
|
|
logout
|
|
get oauth_provider_authorize_url(valid_payload)
|
|
|
|
expect(response.status).to(eq(302))
|
|
expect(response.location).to(include('/login'))
|
|
end
|
|
|
|
it 'with valid payload, shows the consent form' do
|
|
get oauth_provider_authorize_url(valid_payload)
|
|
|
|
expect(response.status).to(eq(200))
|
|
expect(response.body).to(include(valid_payload[:client_id]))
|
|
expect(response.body).to(include(valid_payload[:state]))
|
|
end
|
|
|
|
it 'with valid payload and datasets scopes shows the consent form' do
|
|
user_table = FactoryGirl.create(:carto_user_table, :with_db_table, user_id: @developer.id)
|
|
scopes = ["datasets:r:#{user_table.name}", "datasets:metadata"]
|
|
get oauth_provider_authorize_url(valid_payload.merge(scopes: scopes))
|
|
|
|
expect(response.status).to(eq(200))
|
|
expect(response.body).to(include(valid_payload[:client_id]))
|
|
expect(response.body).to(include(valid_payload[:state]))
|
|
end
|
|
|
|
it 'with valid payload and schemas scopes shows the consent form' do
|
|
user_table = FactoryGirl.create(:carto_user_table, :with_db_table, user_id: @developer.id)
|
|
get oauth_provider_authorize_url(valid_payload.merge(scopes: "schemas:r:#{user_table.name}"))
|
|
|
|
expect(response.status).to(eq(200))
|
|
expect(response.body).to(include(valid_payload[:client_id]))
|
|
expect(response.body).to(include(valid_payload[:state]))
|
|
end
|
|
|
|
it 'with valid payload, pre-authorized and requesting more scopes, shows the consent screen' do
|
|
@oauth_app.oauth_app_users.create!(user_id: @user.id)
|
|
get oauth_provider_authorize_url(valid_payload.merge(scope: 'offline'))
|
|
|
|
expect(response.status).to(eq(200))
|
|
expect(response.body).to(include(valid_payload[:client_id]))
|
|
expect(response.body).to(include(valid_payload[:state]))
|
|
end
|
|
|
|
describe 'with code or token' do
|
|
it_behaves_like 'success with response pre-authorized'
|
|
end
|
|
|
|
describe 'with silent flow' do
|
|
before(:each) do
|
|
valid_payload[:response_type] = 'token'
|
|
valid_payload[:prompt] = 'none'
|
|
end
|
|
|
|
it_behaves_like 'success with response pre-authorized'
|
|
|
|
it 'redirects with invalid request if prompt is not none' do
|
|
get oauth_provider_authorize_url(valid_payload.merge(response_type: 'token', prompt: "wat"))
|
|
|
|
expect(response.status).to(eq(302))
|
|
expect(response.body).to(include('invalid_request'))
|
|
end
|
|
|
|
it 'redirects with login_required if not logged in and client_id exists' do
|
|
logout
|
|
get oauth_provider_authorize_url(valid_payload)
|
|
|
|
expect(response.status).to(eq(302))
|
|
expect(response.body).to(include('login_required'))
|
|
end
|
|
|
|
it 'shows consent screen if not logged in and wrong client_id' do
|
|
logout
|
|
get oauth_provider_authorize_url(valid_payload.merge(client_id: 'wrong'))
|
|
|
|
expect(response.status).to(eq(400))
|
|
expect(response.body).to(include('Invalid client ID or secret'))
|
|
end
|
|
|
|
it 'redirects with access_denied if not authorized' do
|
|
get oauth_provider_authorize_url(valid_payload)
|
|
|
|
expect(response.status).to(eq(302))
|
|
expect(response.body).to(include('access_denied'))
|
|
end
|
|
|
|
it 'with valid payload, pre-authorized and requesting more scopes redirects with access_denied' do
|
|
@oauth_app.oauth_app_users.create!(user_id: @user.id)
|
|
get oauth_provider_authorize_url(valid_payload.merge(scope: 'offline'))
|
|
|
|
expect(response.status).to(eq(302))
|
|
expect(response.body).to(include('access_denied'))
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#authorize' do
|
|
before(:each) do
|
|
login_as(@user, scope: @user.username)
|
|
host!("#{@user.username}.localhost.lan")
|
|
end
|
|
|
|
it_behaves_like 'authorization parameter validation' do
|
|
def request_endpoint(parameters)
|
|
post oauth_provider_authorize_url(parameters)
|
|
end
|
|
|
|
def expect_success(response)
|
|
expect(response.status).to(eq(302))
|
|
expect(response.location).not_to(include('error'))
|
|
end
|
|
end
|
|
|
|
context 'without session' do
|
|
before(:each) do
|
|
logout
|
|
end
|
|
|
|
context 'with subdomainless' do
|
|
before(:each) do
|
|
stub_subdomainless
|
|
end
|
|
|
|
it 'redirects to login with username' do
|
|
endpoint = "http://localhost.lan:53716/user/#{@user.username}/oauth2/authorize"
|
|
expected_url = "http://localhost.lan:53716/user/#{@user.username}/login"
|
|
|
|
post endpoint, valid_payload
|
|
|
|
expect(response.status).to(eq(302))
|
|
expect(response.location).to eql expected_url
|
|
end
|
|
|
|
it 'redirects to login without username' do
|
|
endpoint = "http://localhost.lan:53716/oauth2/authorize"
|
|
expected_url = "http://localhost.lan:53716/login"
|
|
|
|
post endpoint, valid_payload
|
|
|
|
expect(response.status).to(eq(302))
|
|
expect(response.location).to eql expected_url
|
|
end
|
|
end
|
|
|
|
context 'with subdomainful' do
|
|
before(:each) do
|
|
stub_domainful('wadus')
|
|
end
|
|
|
|
it 'redirects to login with username' do
|
|
endpoint = "http://wadus.localhost.lan:53716/user/#{@user.username}/oauth2/authorize"
|
|
expected_url = "http://wadus.localhost.lan:53716/login"
|
|
|
|
post endpoint, valid_payload
|
|
|
|
expect(response.status).to(eq(302))
|
|
expect(response.location).to eql expected_url
|
|
end
|
|
|
|
it 'redirects to login without username' do
|
|
endpoint = "http://wadus.localhost.lan:53716/oauth2/authorize"
|
|
expected_url = "http://wadus.localhost.lan:53716/login"
|
|
|
|
post endpoint, valid_payload
|
|
|
|
expect(response.status).to(eq(302))
|
|
expect(response.location).to eql expected_url
|
|
end
|
|
end
|
|
end
|
|
|
|
shared_examples_for 'successfully authorizes' do
|
|
it 'with valid payload, creates an authorization and redirects back to the application with a code' do
|
|
post oauth_provider_authorize_url(valid_payload)
|
|
|
|
validate_response(response)
|
|
end
|
|
|
|
it 'with valid payload and redirect URIs, creates an authorization and redirects back to the requested URI' do
|
|
@oauth_app.update!(redirect_uris: ['https://domain1', 'https://domain2', 'https://domain3'])
|
|
|
|
post oauth_provider_authorize_url(valid_payload.merge(redirect_uri: 'https://domain3'))
|
|
|
|
validate_response(response)
|
|
expect(response.location).to(start_with('https://domain3'))
|
|
end
|
|
|
|
it 'with valid payload, and a pre-existing grant, upgrades it adding more scopes' do
|
|
oau = @oauth_app.oauth_app_users.create!(user_id: @user.id)
|
|
post oauth_provider_authorize_url(valid_payload.merge(scope: 'dataservices:geocoding'))
|
|
|
|
expect(oau.scopes).to(eq([]))
|
|
oau.reload
|
|
expect(oau.scopes).to(eq(['dataservices:geocoding']))
|
|
|
|
validate_response(response)
|
|
end
|
|
|
|
it 'with client_secret in the payload throws an error' do
|
|
payload = valid_payload.merge(client_secret: 'abcdefgh')
|
|
post oauth_provider_authorize_url(payload)
|
|
|
|
expect(response.status).to(eq(302))
|
|
expect(response.body).to(include('invalid_request'))
|
|
expect(response.body).to(include('client_secret'))
|
|
end
|
|
end
|
|
|
|
describe 'with code response' do
|
|
it_behaves_like 'successfully authorizes' do
|
|
def validate_response(response)
|
|
authorization_code = @oauth_app.oauth_app_users.find_by_user_id!(@user.id).oauth_authorization_codes.first
|
|
expect(authorization_code).to(be)
|
|
expect(authorization_code.code).to(be_present)
|
|
|
|
expect(response.status).to(eq(302))
|
|
expect(parse_query_parameters(response.location)['code']).to(eq(authorization_code.code))
|
|
end
|
|
end
|
|
end
|
|
|
|
describe 'with token response' do
|
|
before(:each) do
|
|
valid_payload[:response_type] = 'token'
|
|
end
|
|
|
|
it_behaves_like 'successfully authorizes' do
|
|
def validate_response(response)
|
|
access_token = @oauth_app.oauth_app_users.find_by_user_id!(@user.id).oauth_access_tokens.first
|
|
expect(access_token).to(be)
|
|
expect(access_token.api_key).to(be_present)
|
|
|
|
expect(response.status).to(eq(302))
|
|
validate_token_response(parse_fragment_parameters(response.location).symbolize_keys, access_token)
|
|
end
|
|
end
|
|
|
|
it 'redirects with an error if requesting offline scope' do
|
|
post oauth_provider_authorize_url(valid_payload.merge(scope: 'offline'))
|
|
|
|
expect(response.status).to(eq(302))
|
|
expect(response.location).to(start_with(@oauth_app.redirect_uris.first))
|
|
qs = parse_fragment_parameters(response.location)
|
|
expect(qs['error']).to(eq('invalid_scope'))
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#token' do
|
|
before(:each) do
|
|
@oauth_app_user = @oauth_app.oauth_app_users.create!(user_id: @user.id)
|
|
@authorization_code = @oauth_app_user.oauth_authorization_codes.create!
|
|
end
|
|
|
|
let (:auth_code_token_payload) do
|
|
{
|
|
client_id: @oauth_app.client_id,
|
|
client_secret: @oauth_app.client_secret,
|
|
grant_type: 'authorization_code',
|
|
code: @authorization_code.code
|
|
}
|
|
end
|
|
|
|
describe 'with authorization code' do
|
|
it 'with valid code returns an api key' do
|
|
post_json oauth_provider_token_url(auth_code_token_payload) do |response|
|
|
expect(Carto::OauthAuthorizationCode.exists?(@authorization_code.id)).to(be_false)
|
|
access_token = @oauth_app_user.oauth_access_tokens.reload.first
|
|
expect(access_token).to(be)
|
|
|
|
expect(response.status).to(eq(200))
|
|
validate_token_response(response.body, access_token)
|
|
end
|
|
end
|
|
|
|
it 'with valid code and offline scope returns an api key and refresh token' do
|
|
@authorization_code.update!(scopes: ['offline'])
|
|
|
|
post_json oauth_provider_token_url(auth_code_token_payload) do |response|
|
|
expect(Carto::OauthAuthorizationCode.exists?(@authorization_code.id)).to(be_false)
|
|
access_token = @oauth_app_user.oauth_access_tokens.reload.first
|
|
expect(access_token).to(be)
|
|
refresh_token = @oauth_app_user.oauth_refresh_tokens.find_by_token(response.body[:refresh_token])
|
|
expect(refresh_token).to(be)
|
|
|
|
expect(response.status).to(eq(200))
|
|
validate_token_response(response.body, access_token, refresh_token)
|
|
end
|
|
end
|
|
|
|
it 'with valid code and redirect uri returns an api key' do
|
|
@oauth_app.update!(redirect_uris: ['https://domain1', 'https://domain2', 'https://domain3'])
|
|
@authorization_code.update!(redirect_uri: 'https://domain3')
|
|
|
|
post_json oauth_provider_token_url(auth_code_token_payload.merge(redirect_uri: 'https://domain3')) do |response|
|
|
expect(Carto::OauthAuthorizationCode.exists?(@authorization_code.id)).to(be_false)
|
|
access_token = @oauth_app_user.oauth_access_tokens.reload.first
|
|
expect(access_token).to(be)
|
|
|
|
expect(response.status).to(eq(200))
|
|
validate_token_response(response.body, access_token)
|
|
end
|
|
end
|
|
|
|
it 'with expired code, returns code not valid' do
|
|
Delorean.jump(2.minutes)
|
|
|
|
post_json oauth_provider_token_url(auth_code_token_payload) do |response|
|
|
expect(Carto::OauthAuthorizationCode.exists?(@authorization_code.id)).to(be_true)
|
|
access_token = @oauth_app_user.oauth_access_tokens.reload.first
|
|
expect(access_token).to(be_nil)
|
|
|
|
expect(response.status).to(eq(400))
|
|
expect(response.body[:error]).to(eq('invalid_grant'))
|
|
end
|
|
end
|
|
|
|
it 'with invalid code, returns error without creating the api key' do
|
|
post_json oauth_provider_token_url(auth_code_token_payload.merge(code: 'invalid')) do |response|
|
|
expect(Carto::OauthAuthorizationCode.exists?(@authorization_code.id)).to(be_true)
|
|
access_token = @oauth_app_user.oauth_access_tokens.reload.first
|
|
expect(access_token).to(be_nil)
|
|
|
|
expect(response.status).to(eq(400))
|
|
expect(response.body[:error]).to(eq('invalid_grant'))
|
|
end
|
|
end
|
|
|
|
it 'with invalid redirect_uri, returns error without creating the api key' do
|
|
post_json oauth_provider_token_url(auth_code_token_payload.merge(redirect_uri: 'invalid')) do |response|
|
|
expect(Carto::OauthAuthorizationCode.exists?(@authorization_code.id)).to(be_true)
|
|
access_token = @oauth_app_user.oauth_access_tokens.reload.first
|
|
expect(access_token).to(be_nil)
|
|
|
|
expect(response.status).to(eq(400))
|
|
expect(response.body[:error]).to(eq('invalid_request'))
|
|
end
|
|
end
|
|
|
|
it 'without redirect_uri, returns error without creating the api key' do
|
|
@authorization_code.update!(redirect_uri: @oauth_app.redirect_uris.first)
|
|
|
|
post_json oauth_provider_token_url(auth_code_token_payload) do |response|
|
|
expect(Carto::OauthAuthorizationCode.exists?(@authorization_code.id)).to(be_true)
|
|
access_token = @oauth_app_user.oauth_access_tokens.reload.first
|
|
expect(access_token).to(be_nil)
|
|
|
|
expect(response.status).to(eq(400))
|
|
expect(response.body[:error]).to(eq('invalid_request'))
|
|
end
|
|
end
|
|
end
|
|
|
|
describe 'with refresh token' do
|
|
before(:each) do
|
|
@refresh_token = @oauth_app_user.oauth_refresh_tokens.create!(scopes: ['offline'])
|
|
end
|
|
|
|
let (:refresh_token_payload) do
|
|
{
|
|
client_id: @oauth_app.client_id,
|
|
client_secret: @oauth_app.client_secret,
|
|
grant_type: 'refresh_token',
|
|
refresh_token: @refresh_token.token
|
|
}
|
|
end
|
|
|
|
it 'with valid token returns an api key' do
|
|
post_json oauth_provider_token_url(refresh_token_payload) do |response|
|
|
access_token = @oauth_app_user.oauth_access_tokens.reload.first
|
|
expect(access_token).to(be)
|
|
expect { @refresh_token.reload }.to(change { @refresh_token.token })
|
|
expect(access_token.scopes).to(eq(@refresh_token.scopes))
|
|
|
|
expect(response.status).to(eq(200))
|
|
validate_token_response(response.body, access_token, @refresh_token)
|
|
end
|
|
end
|
|
|
|
it 'with valid token and explicit scopes returns a restricted api key' do
|
|
post_json oauth_provider_token_url(refresh_token_payload.merge(scope: '')) do |response|
|
|
access_token = @oauth_app_user.oauth_access_tokens.reload.first
|
|
expect(access_token).to(be)
|
|
expect { @refresh_token.reload }.to(change { @refresh_token.token })
|
|
expect(access_token.scopes).to(eq([]))
|
|
|
|
expect(response.status).to(eq(200))
|
|
validate_token_response(response.body, access_token, @refresh_token)
|
|
end
|
|
end
|
|
|
|
it 'with expired token, returns error without creating the api key' do
|
|
Delorean.jump 1.year
|
|
|
|
post_json oauth_provider_token_url(refresh_token_payload) do |response|
|
|
access_token = @oauth_app_user.oauth_access_tokens.reload.first
|
|
expect(access_token).to(be_nil)
|
|
|
|
expect(response.status).to(eq(400))
|
|
expect(response.body[:error]).to(eq('invalid_grant'))
|
|
end
|
|
end
|
|
|
|
it 'with invalid code, returns error without creating the api key' do
|
|
post_json oauth_provider_token_url(refresh_token_payload.merge(refresh_token: 'invalid')) do |response|
|
|
access_token = @oauth_app_user.oauth_access_tokens.reload.first
|
|
expect(access_token).to(be_nil)
|
|
|
|
expect(response.status).to(eq(400))
|
|
expect(response.body[:error]).to(eq('invalid_grant'))
|
|
end
|
|
end
|
|
|
|
it 'with missing refresh_token parameter returns an informative error' do
|
|
payload = refresh_token_payload.except(:refresh_token)
|
|
post_json oauth_provider_token_url(payload) do |response|
|
|
expect(response.status).to(eq(400))
|
|
expect(response.body[:error]).to(eq('invalid_request'))
|
|
expect(response.body[:error_description]).to(include('refresh_token'))
|
|
end
|
|
end
|
|
end
|
|
|
|
it 'with missing required parameters returns an informative error' do
|
|
payload = auth_code_token_payload.except(:client_secret).except(:client_id).except(:code)
|
|
post_json oauth_provider_token_url(payload) do |response|
|
|
expect(response.status).to(eq(400))
|
|
expect(response.body[:error]).to(eq('invalid_request'))
|
|
expect(response.body[:error_description]).to(include('client_secret'))
|
|
expect(response.body[:error_description]).to(include('client_id'))
|
|
expect(response.body[:error_description]).to(include('code'))
|
|
end
|
|
end
|
|
|
|
it 'with invalid client_id, returns error without creating the api key' do
|
|
post_json oauth_provider_token_url(auth_code_token_payload.merge(client_id: 'invalid')) do |response|
|
|
expect(Carto::OauthAuthorizationCode.exists?(@authorization_code.id)).to(be_true)
|
|
access_token = @oauth_app_user.oauth_access_tokens.reload.first
|
|
expect(access_token).to(be_nil)
|
|
|
|
expect(response.status).to(eq(400))
|
|
expect(response.body[:error]).to(eq('invalid_client'))
|
|
end
|
|
end
|
|
|
|
it 'with invalid client_secret, returns error without creating the api key' do
|
|
post_json oauth_provider_token_url(auth_code_token_payload.merge(client_secret: 'invalid')) do |response|
|
|
expect(Carto::OauthAuthorizationCode.exists?(@authorization_code.id)).to(be_true)
|
|
access_token = @oauth_app_user.oauth_access_tokens.reload.first
|
|
expect(access_token).to(be_nil)
|
|
|
|
expect(response.status).to(eq(400))
|
|
expect(response.body[:error]).to(eq('invalid_client'))
|
|
end
|
|
end
|
|
|
|
it 'with invalid grant_type, returns error without creating the api key' do
|
|
post_json oauth_provider_token_url(auth_code_token_payload.merge(grant_type: 'invalid')) do |response|
|
|
expect(Carto::OauthAuthorizationCode.exists?(@authorization_code.id)).to(be_true)
|
|
access_token = @oauth_app_user.oauth_access_tokens.reload.first
|
|
expect(access_token).to(be_nil)
|
|
|
|
expect(response.status).to(eq(400))
|
|
expect(response.body[:error]).to(eq('unsupported_grant_type'))
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#acceptance' do
|
|
include Capybara::DSL
|
|
|
|
# Since Capybara+rack passes all requests to the local server, we set a redirect URI inside localhost
|
|
let(:redirect_uri) { "https://#{@user.username}.localhost.lan/redirect" }
|
|
|
|
let(:state) { SecureRandom.hex(16) }
|
|
|
|
let(:base_uri) { "http://#{@user.username}.localhost.lan" }
|
|
|
|
before(:each) do
|
|
@oauth_app.update!(redirect_uris: ['https://fake_uri', redirect_uri])
|
|
end
|
|
|
|
def login
|
|
login_as(@user, scope: @user.username)
|
|
begin
|
|
visit "#{base_uri}/login"
|
|
rescue ActionView::MissingTemplate
|
|
# Expected error trying to load dashboard statics
|
|
end
|
|
end
|
|
|
|
def request_authorization(response_type, scope)
|
|
visit "#{base_uri}/oauth2/authorize?client_id=#{@oauth_app.client_id}&state=#{state}" \
|
|
"&response_type=#{response_type}&scope=#{scope}&redirect_uri=#{redirect_uri}"
|
|
|
|
begin
|
|
click_on 'Accept'
|
|
rescue ActionController::RoutingError
|
|
# Expected error since /redirect is a made up URL
|
|
end
|
|
end
|
|
|
|
def test_access_token(token_response, expect_success:)
|
|
# TODO: use bearer auth
|
|
get_json "#{token_response[:user_info_url]}?api_key=#{token_response[:access_token]}" do |response|
|
|
if expect_success
|
|
expect(response.status).to(eq(200))
|
|
expect(response.body[:username]).to(eq(@user.username))
|
|
else
|
|
expect(response.status).to(eq(401))
|
|
end
|
|
end
|
|
end
|
|
|
|
it 'following the code flow produces a valid API Key and refresh token to renew it' do
|
|
login
|
|
|
|
request_authorization('code', 'offline')
|
|
|
|
expect(current_url).to(start_with(redirect_uri))
|
|
response_parameters = parse_query_parameters(current_url)
|
|
expect(response_parameters['state']).to(eq(state))
|
|
code = response_parameters['code']
|
|
|
|
# Exchange token for API Key
|
|
logout
|
|
payload = {
|
|
client_id: @oauth_app.client_id,
|
|
client_secret: @oauth_app.client_secret,
|
|
grant_type: 'authorization_code',
|
|
code: code,
|
|
redirect_uri: redirect_uri
|
|
}
|
|
token_response = post_json oauth_provider_token_url(payload) do |response|
|
|
expect(response.status).to(eq(200))
|
|
|
|
response.body
|
|
end
|
|
|
|
refresh_token = token_response[:refresh_token]
|
|
expect(refresh_token).to(be)
|
|
|
|
# Try to use the access token
|
|
test_access_token(token_response, expect_success: true)
|
|
|
|
# Access token expiration, should no longer work
|
|
Delorean.jump(2.hours)
|
|
Rake.application.rake_require('tasks/oauth')
|
|
Rake::Task.define_task(:environment)
|
|
Rake::Task['cartodb:oauth:destroy_expired_access_tokens'].reenable
|
|
Rake::Task['cartodb:oauth:destroy_expired_access_tokens'].invoke
|
|
|
|
test_access_token(token_response, expect_success: false)
|
|
|
|
# Get a new acess token using refresh token
|
|
payload = {
|
|
client_id: @oauth_app.client_id,
|
|
client_secret: @oauth_app.client_secret,
|
|
grant_type: 'refresh_token',
|
|
refresh_token: refresh_token
|
|
}
|
|
token_response = post_json oauth_provider_token_url(payload) do |response|
|
|
expect(response.status).to(eq(200))
|
|
|
|
response.body
|
|
end
|
|
|
|
test_access_token(token_response, expect_success: true)
|
|
end
|
|
|
|
it 'following the implicit flow produces a valid API Key' do
|
|
login
|
|
|
|
request_authorization('token', '')
|
|
|
|
# Capybara driver eats the fragment part of the URL to emulate browsers but we can recover it with some trickery
|
|
redirected_url = Capybara.current_session.driver.response.location
|
|
expect(redirected_url).to(start_with(redirect_uri))
|
|
response_parameters = parse_fragment_parameters(redirected_url)
|
|
expect(response_parameters['state']).to(eq(state))
|
|
expect(response_parameters['refresh_token']).to(be_nil)
|
|
|
|
test_access_token(response_parameters.symbolize_keys, expect_success: true)
|
|
end
|
|
|
|
it 'will return to oauth flow after login' do
|
|
base_uri = "http://#{@organization.name}.localhost.lan"
|
|
oauth_url = "#{base_uri}/oauth2/authorize?client_id=#{@oauth_app.client_id}&state=123&response_type=code"
|
|
|
|
visit oauth_url
|
|
fill_in 'email', with: @org_user_1.email
|
|
fill_in 'password', with: @org_user_1.password
|
|
click_on 'Log in'
|
|
|
|
expect(current_url).to(eq(oauth_url))
|
|
end
|
|
end
|
|
end
|