Commit 1485821e authored by Krasimir Angelov's avatar Krasimir Angelov

Use JWTs for Conan API authetication

Instead of reusing the personal access token (and thus having it stored
locally) issue JWT that has the access token id in the payload and when
provided fetch that token and autheticate using it.

Derive JWT secret unique to Conan packages from
attr_encrypted_db_key_base_32.
parent 6381dc98
# frozen_string_literal: true
module API
class ConanPackages < Grape::API
HMAC_KEY = 'gitlab-conan-packages'.freeze
before do
not_found! unless Feature.enabled?(:conan_package_registry)
require_packages_enabled!
......@@ -8,6 +10,16 @@ module API
helpers ::API::Helpers::PackagesHelpers
helpers do
def jwt_secret
OpenSSL::HMAC.hexdigest(
OpenSSL::Digest::SHA256.new,
::Settings.attr_encrypted_db_key_base,
HMAC_KEY
)
end
end
namespace 'packages/conan/v1/users/' do
format :txt
......@@ -21,7 +33,12 @@ module API
authenticate!
token
jwt = JSONWebToken::HMACToken.new(jwt_secret)
jwt['pat'] = access_token.id
jwt['u'] = access_token.user_id
jwt.expire_time = jwt.issued_at + 1.hour
jwt.encoded
end
end
......@@ -32,10 +49,14 @@ module API
helpers do
def require_conan_authentication!
token = headers['Authorization'].to_s.split('Bearer ', 2).second
request.env['HTTP_PRIVATE_TOKEN'] = token
jwt = headers['Authorization'].to_s.split('Bearer ', 2).second
payload = JSONWebToken::HMACToken.decode(jwt, jwt_secret).first
@access_token = PersonalAccessToken.find_by_id_and_user_id(payload['pat'], payload['u'])
authenticate!
rescue JWT::DecodeError
unauthorized!
end
end
......
......@@ -2,8 +2,20 @@
require 'spec_helper'
describe API::ConanPackages do
let(:base_secret) { SecureRandom.base64(32) }
let(:jwt_secret) { OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA256.new, base_secret, API::ConanPackages::HMAC_KEY) }
before do
stub_licensed_features(packages: true)
allow(Settings).to receive(:attr_encrypted_db_key_base).and_return(base_secret)
end
def build_jwt(personal_access_token, secret: jwt_secret, user_id: nil)
JSONWebToken::HMACToken.new(secret).tap do |jwt|
jwt['pat'] = personal_access_token.id
jwt['u'] = user_id || personal_access_token.user_id
end
end
describe 'GET /api/v4/packages/conan/v1/ping' do
......@@ -28,7 +40,8 @@ describe API::ConanPackages do
it 'responds with 200 OK when valid token is provided' do
personal_access_token = create(:personal_access_token)
headers = { 'HTTP_AUTHORIZATION' => "Bearer #{personal_access_token.token}" }
jwt = build_jwt(personal_access_token)
headers = { 'HTTP_AUTHORIZATION' => "Bearer #{jwt.encoded}" }
get api('/packages/conan/v1/ping'), headers: headers
......@@ -36,8 +49,35 @@ describe API::ConanPackages do
expect(response.headers['X-Conan-Server-Capabilities']).to eq("")
end
it 'responds with 401 Unauthorized when invalid token is provided' do
headers = { 'HTTP_AUTHORIZATION' => "Bearer wrong-token" }
it 'responds with 401 Unauthorized when invalid access token ID is provided' do
personal_access_token = create(:personal_access_token)
jwt = build_jwt(double(id: 12345), user_id: personal_access_token.user_id)
headers = { 'HTTP_AUTHORIZATION' => "Bearer #{jwt.encoded}" }
get api('/packages/conan/v1/ping'), headers: headers
expect(response).to have_gitlab_http_status(401)
end
it 'responds with 401 Unauthorized when invalid user is provided' do
personal_access_token = create(:personal_access_token)
jwt = build_jwt(personal_access_token, user_id: 12345)
headers = { 'HTTP_AUTHORIZATION' => "Bearer #{jwt.encoded}" }
get api('/packages/conan/v1/ping'), headers: headers
expect(response).to have_gitlab_http_status(401)
end
it 'responds with 401 Unauthorized when the provided JWT is signed with different secret' do
personal_access_token = create(:personal_access_token)
jwt = build_jwt(personal_access_token, secret: SecureRandom.base64(32))
headers = { 'HTTP_AUTHORIZATION' => "Bearer #{jwt.encoded}" }
get api('/packages/conan/v1/ping'), headers: headers
expect(response).to have_gitlab_http_status(401)
end
it 'responds with 401 Unauthorized when invalid JWT is provided' do
headers = { 'HTTP_AUTHORIZATION' => "Bearer invalid-jwt" }
get api('/packages/conan/v1/ping'), headers: headers
expect(response).to have_gitlab_http_status(401)
......@@ -56,18 +96,25 @@ describe API::ConanPackages do
describe 'GET /api/v4/packages/conan/v1/users/authenticate' do
it 'responds with 401 Unauthorized when invalid token is provided' do
get api("/packages/conan/v1/users/authenticate")
headers = { 'HTTP_AUTHORIZATION' => ActionController::HttpAuthentication::Basic.encode_credentials('foo', 'wrong-token') }
get api('/packages/conan/v1/users/authenticate'), headers: headers
expect(response).to have_gitlab_http_status(401)
end
it 'responds with 200 OK and the token when valid token is provided' do
it 'responds with 200 OK and JWT when valid access token is provided' do
personal_access_token = create(:personal_access_token)
headers = { 'HTTP_AUTHORIZATION' => ActionController::HttpAuthentication::Basic.encode_credentials("foo", personal_access_token.token) }
get api("/packages/conan/v1/users/authenticate"), headers: headers
headers = { 'HTTP_AUTHORIZATION' => ActionController::HttpAuthentication::Basic.encode_credentials('foo', personal_access_token.token) }
get api('/packages/conan/v1/users/authenticate'), headers: headers
expect(response).to have_gitlab_http_status(200)
expect(response.body).to eq(personal_access_token.token)
payload = JSONWebToken::HMACToken.decode(response.body, jwt_secret).first
expect(payload['pat']).to eq(personal_access_token.id)
expect(payload['u']).to eq(personal_access_token.user_id)
duration = payload['exp'] - payload['iat']
expect(duration).to eq(1.hour)
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment