Commit 3a713ef1 authored by Rémy Coutable's avatar Rémy Coutable Committed by Rémy Coutable

Merge branch '18302-use-rails-cookie-in-api' into 'master'

Allow the Rails cookie to be used for API authentication

Makes the Rails cookie into a valid authentication token for the Grape
API, and uses it instead of token authentication in frontend code that
uses the API.

Rendering the private token into client-side javascript is a security
risk; it may be stolen through XSS or other attacks. In general,
re-using API code in the frontend is more desirable than implementing
endless actions that return JSON.

Closes #18302

See merge request !1995
Signed-off-by: default avatarRémy Coutable <remy@rymai.me>
parent 410d7513
Please view this file on the master branch, on stable branches it's out of date. Please view this file on the master branch, on stable branches it's out of date.
v 8.11.7
- Allow the Rails cookie to be used for API authentication.
v 8.11.6 v 8.11.6
- Fix unnecessary horizontal scroll area in pipeline visualizations. !6005 - Fix unnecessary horizontal scroll area in pipeline visualizations. !6005
- Make merge conflict file size limit 200 KB, to match the docs. !6052 - Make merge conflict file size limit 200 KB, to match the docs. !6052
......
...@@ -16,9 +16,6 @@ ...@@ -16,9 +16,6 @@
.replace(':id', group_id); .replace(':id', group_id);
return $.ajax({ return $.ajax({
url: url, url: url,
data: {
private_token: gon.api_token
},
dataType: "json" dataType: "json"
}).done(function(group) { }).done(function(group) {
return callback(group); return callback(group);
...@@ -29,7 +26,6 @@ ...@@ -29,7 +26,6 @@
return $.ajax({ return $.ajax({
url: url, url: url,
data: { data: {
private_token: gon.api_token,
search: query, search: query,
per_page: 20 per_page: 20
}, },
...@@ -43,7 +39,6 @@ ...@@ -43,7 +39,6 @@
return $.ajax({ return $.ajax({
url: url, url: url,
data: { data: {
private_token: gon.api_token,
search: query, search: query,
per_page: 20 per_page: 20
}, },
...@@ -57,7 +52,6 @@ ...@@ -57,7 +52,6 @@
return $.ajax({ return $.ajax({
url: url, url: url,
data: { data: {
private_token: gon.api_token,
search: query, search: query,
order_by: order, order_by: order,
per_page: 20 per_page: 20
...@@ -70,7 +64,6 @@ ...@@ -70,7 +64,6 @@
newLabel: function(project_id, data, callback) { newLabel: function(project_id, data, callback) {
var url = Api.buildUrl(Api.labelsPath) var url = Api.buildUrl(Api.labelsPath)
.replace(':id', project_id); .replace(':id', project_id);
data.private_token = gon.api_token;
return $.ajax({ return $.ajax({
url: url, url: url,
type: "POST", type: "POST",
...@@ -88,7 +81,6 @@ ...@@ -88,7 +81,6 @@
return $.ajax({ return $.ajax({
url: url, url: url,
data: { data: {
private_token: gon.api_token,
search: query, search: query,
per_page: 20 per_page: 20
}, },
......
...@@ -52,11 +52,12 @@ The following documentation is for the [internal CI API](ci/README.md): ...@@ -52,11 +52,12 @@ The following documentation is for the [internal CI API](ci/README.md):
## Authentication ## Authentication
All API requests require authentication via a token. There are three types of tokens All API requests require authentication via a session cookie or token. There are
available: private tokens, OAuth 2 tokens, and personal access tokens. three types of tokens available: private tokens, OAuth 2 tokens, and personal
access tokens.
If a token is invalid or omitted, an error message will be returned with If authentication information is invalid or omitted, an error message will be
status code `401`: returned with status code `401`:
```json ```json
{ {
...@@ -95,6 +96,13 @@ that needs access to the GitLab API. ...@@ -95,6 +96,13 @@ that needs access to the GitLab API.
Once you have your token, pass it to the API using either the `private_token` Once you have your token, pass it to the API using either the `private_token`
parameter or the `PRIVATE-TOKEN` header. parameter or the `PRIVATE-TOKEN` header.
### Session cookie
When signing in to GitLab as an ordinary user, a `_gitlab_session` cookie is
set. The API will use this cookie for authentication if it is present, but using
the API to generate a new session cookie is currently not supported.
## Basic Usage ## Basic Usage
API requests should be prefixed with `api` and the API version. The API version API requests should be prefixed with `api` and the API version. The API version
......
...@@ -33,46 +33,29 @@ module API ...@@ -33,46 +33,29 @@ module API
# #
# If the token is revoked, then it raises RevokedError. # If the token is revoked, then it raises RevokedError.
# #
# If the token is not found (nil), then it raises TokenNotFoundError. # If the token is not found (nil), then it returns nil
# #
# Arguments: # Arguments:
# #
# scopes: (optional) scopes required for this guard. # scopes: (optional) scopes required for this guard.
# Defaults to empty array. # Defaults to empty array.
# #
def doorkeeper_guard!(scopes: [])
if (access_token = find_access_token).nil?
raise TokenNotFoundError
else
case validate_access_token(access_token, scopes)
when Oauth2::AccessTokenValidationService::INSUFFICIENT_SCOPE
raise InsufficientScopeError.new(scopes)
when Oauth2::AccessTokenValidationService::EXPIRED
raise ExpiredError
when Oauth2::AccessTokenValidationService::REVOKED
raise RevokedError
when Oauth2::AccessTokenValidationService::VALID
@current_user = User.find(access_token.resource_owner_id)
end
end
end
def doorkeeper_guard(scopes: []) def doorkeeper_guard(scopes: [])
if access_token = find_access_token access_token = find_access_token
case validate_access_token(access_token, scopes) return nil unless access_token
when Oauth2::AccessTokenValidationService::INSUFFICIENT_SCOPE
raise InsufficientScopeError.new(scopes) case validate_access_token(access_token, scopes)
when Oauth2::AccessTokenValidationService::INSUFFICIENT_SCOPE
raise InsufficientScopeError.new(scopes)
when Oauth2::AccessTokenValidationService::EXPIRED when Oauth2::AccessTokenValidationService::EXPIRED
raise ExpiredError raise ExpiredError
when Oauth2::AccessTokenValidationService::REVOKED when Oauth2::AccessTokenValidationService::REVOKED
raise RevokedError raise RevokedError
when Oauth2::AccessTokenValidationService::VALID when Oauth2::AccessTokenValidationService::VALID
@current_user = User.find(access_token.resource_owner_id) @current_user = User.find(access_token.resource_owner_id)
end
end end
end end
...@@ -96,19 +79,6 @@ module API ...@@ -96,19 +79,6 @@ module API
end end
module ClassMethods module ClassMethods
# Installs the doorkeeper guard on the whole Grape API endpoint.
#
# Arguments:
#
# scopes: (optional) scopes required for this guard.
# Defaults to empty array.
#
def guard_all!(scopes: [])
before do
guard! scopes: scopes
end
end
private private
def install_error_responders(base) def install_error_responders(base)
......
...@@ -12,13 +12,30 @@ module API ...@@ -12,13 +12,30 @@ module API
nil nil
end end
def private_token
params[PRIVATE_TOKEN_PARAM] || env[PRIVATE_TOKEN_HEADER]
end
def warden
env['warden']
end
# Check the Rails session for valid authentication details
def find_user_from_warden
warden ? warden.authenticate : nil
end
def find_user_by_private_token def find_user_by_private_token
token_string = (params[PRIVATE_TOKEN_PARAM] || env[PRIVATE_TOKEN_HEADER]).to_s token = private_token
User.find_by_authentication_token(token_string) || User.find_by_personal_access_token(token_string) return nil unless token.present?
User.find_by_authentication_token(token) || User.find_by_personal_access_token(token)
end end
def current_user def current_user
@current_user ||= (find_user_by_private_token || doorkeeper_guard) @current_user ||= find_user_by_private_token
@current_user ||= doorkeeper_guard
@current_user ||= find_user_from_warden
unless @current_user && Gitlab::UserAccess.new(@current_user).allowed? unless @current_user && Gitlab::UserAccess.new(@current_user).allowed?
return nil return nil
......
...@@ -11,7 +11,6 @@ module Gitlab ...@@ -11,7 +11,6 @@ module Gitlab
if current_user if current_user
gon.current_user_id = current_user.id gon.current_user_id = current_user.id
gon.api_token = current_user.private_token
end end
end end
end end
......
...@@ -35,11 +35,36 @@ describe API::Helpers, api: true do ...@@ -35,11 +35,36 @@ describe API::Helpers, api: true do
params.delete(API::Helpers::SUDO_PARAM) params.delete(API::Helpers::SUDO_PARAM)
end end
def warden_authenticate_returns(value)
warden = double("warden", authenticate: value)
env['warden'] = warden
end
def doorkeeper_guard_returns(value)
allow_any_instance_of(self.class).to receive(:doorkeeper_guard){ value }
end
def error!(message, status) def error!(message, status)
raise Exception raise Exception
end end
describe ".current_user" do describe ".current_user" do
subject { current_user }
describe "when authenticating via Warden" do
before { doorkeeper_guard_returns false }
context "fails" do
it { is_expected.to be_nil }
end
context "succeeds" do
before { warden_authenticate_returns user }
it { is_expected.to eq(user) }
end
end
describe "when authenticating using a user's private token" do describe "when authenticating using a user's private token" do
it "returns nil for an invalid token" do it "returns nil for an invalid token" do
env[API::Helpers::PRIVATE_TOKEN_HEADER] = 'invalid token' env[API::Helpers::PRIVATE_TOKEN_HEADER] = 'invalid token'
......
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