Skip to content
Snippets Groups Projects
Commit 105017c3 authored by Kamil Trzcinski's avatar Kamil Trzcinski
Browse files

Added JWT controller

parent 0a280158
Branches
Tags
No related merge requests found
Loading
Loading
@@ -225,6 +225,7 @@ gem 'request_store', '~> 1.3.0'
gem 'select2-rails', '~> 3.5.9'
gem 'virtus', '~> 1.0.1'
gem 'net-ssh', '~> 3.0.1'
gem 'base32', '~> 0.3.0'
 
# Sentry integration
gem 'sentry-raven', '~> 0.15'
Loading
Loading
Loading
Loading
@@ -74,6 +74,7 @@ GEM
ice_nine (~> 0.11.0)
thread_safe (~> 0.3, >= 0.3.1)
babosa (1.0.2)
base32 (0.3.2)
bcrypt (3.1.10)
benchmark-ips (2.3.0)
better_errors (1.0.1)
Loading
Loading
@@ -897,6 +898,7 @@ DEPENDENCIES
attr_encrypted (~> 1.3.4)
awesome_print (~> 1.2.0)
babosa (~> 1.0.2)
base32 (~> 0.3.0)
benchmark-ips
better_errors (~> 1.0.1)
binding_of_caller (~> 0.7.2)
Loading
Loading
class JwtController < ApplicationController
skip_before_action :authenticate_user!
skip_before_action :verify_authenticity_token
def auth
@authenticated = authenticate_with_http_basic do |login, password|
@ci_project = ci_project(login, password)
@user = authenticate_user(login, password) unless @ci_project
end
unless @authenticated
return render_403 if has_basic_credentials?
end
case params[:service]
when 'docker'
docker_token_auth(params[:scope], params[:offline_token])
else
return render_404
end
end
private
def render_400
head :invalid_request
end
def render_404
head :not_found
end
def render_403
head :forbidden
end
def docker_token_auth(scope, offline_token)
payload = {
aud: params[:service],
sub: @user.try(:username)
}
if offline_token
return render_403 unless @user
elsif scope
access = process_access(scope)
return render_404 unless access
payload[:access] = [access]
end
render json: { token: encode(payload) }
end
def ci_project(login, password)
matched_login = /(?<s>^[a-zA-Z]*-ci)-token$/.match(login)
if matched_login.present?
underscored_service = matched_login['s'].underscore
if underscored_service == 'gitlab_ci'
Project.find_by(builds_enabled: true, runners_token: password)
end
end
end
def authenticate_user(login, password)
user = Gitlab::Auth.new.find(login, password)
# If the user authenticated successfully, we reset the auth failure count
# from Rack::Attack for that IP. A client may attempt to authenticate
# with a username and blank password first, and only after it receives
# a 401 error does it present a password. Resetting the count prevents
# false positives from occurring.
#
# Otherwise, we let Rack::Attack know there was a failed authentication
# attempt from this IP. This information is stored in the Rails cache
# (Redis) and will be used by the Rack::Attack middleware to decide
# whether to block requests from this IP.
config = Gitlab.config.rack_attack.git_basic_auth
if config.enabled
if user
# A successful login will reset the auth failure count from this IP
Rack::Attack::Allow2Ban.reset(request.ip, config)
else
banned = Rack::Attack::Allow2Ban.filter(request.ip, config) do
# Unless the IP is whitelisted, return true so that Allow2Ban
# increments the counter (stored in Rails.cache) for the IP
if config.ip_whitelist.include?(request.ip)
false
else
true
end
end
if banned
Rails.logger.info "IP #{request.ip} failed to login " \
"as #{login} but has been temporarily banned from Git auth"
end
end
end
user
end
def process_access(scope)
type, name, actions = scope.split(':', 3)
actions = actions.split(',')
case type
when 'repository'
process_repository_access(type, name, actions)
end
end
def process_repository_access(type, name, actions)
project = Project.find_with_namespace(name)
return unless project
actions = actions.select do |action|
can_access?(project, action)
end
{ type: 'repository', name: name, actions: actions } if actions
end
def default_payload
{
aud: 'docker',
sub: @user.try(:username),
aud: params[:service],
}
end
def private_key
@private_key ||= OpenSSL::PKey::RSA.new File.read Gitlab.config.registry.key
end
def encode(payload)
issued_at = Time.now
payload = payload.merge(
iss: Gitlab.config.registry.issuer,
iat: issued_at.to_i,
nbf: issued_at.to_i - 5.seconds.to_i,
exp: issued_at.to_i + 60.minutes.to_i,
jti: SecureRandom.uuid,
)
headers = {
kid: kid(private_key)
}
JWT.encode(payload, private_key, 'RS256', headers)
end
def can_access?(project, action)
case action
when 'pull'
project == @ci_project || can?(@user, :download_code, project)
when 'push'
project == @ci_project || can?(@user, :push_code, project)
else
false
end
end
def kid(private_key)
sha256 = Digest::SHA256.new
sha256.update(private_key.public_key.to_der)
payload = StringIO.new(sha256.digest).read(30)
Base32.encode(payload).split('').each_slice(4).each_with_object([]) do |slice, mem|
mem << slice.join
end.join(':')
end
end
Loading
Loading
@@ -63,6 +63,9 @@ Rails.application.routes.draw do
get 'search' => 'search#show'
get 'search/autocomplete' => 'search#autocomplete', as: :search_autocomplete
 
# JSON Web Token
get 'jwt/auth' => 'jwt#auth'
# API
API::API.logger Rails.logger
mount API::API => '/api'
Loading
Loading
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment