Commit ba7c3685 authored by Rémy Coutable's avatar 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
parents 187dd50f 5d1b616e
...@@ -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);
...@@ -31,7 +28,6 @@ ...@@ -31,7 +28,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
}, },
...@@ -46,7 +42,6 @@ ...@@ -46,7 +42,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
}, },
...@@ -61,7 +56,6 @@ ...@@ -61,7 +56,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
...@@ -74,7 +68,6 @@ ...@@ -74,7 +68,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",
...@@ -93,7 +86,6 @@ ...@@ -93,7 +86,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
}, },
......
...@@ -55,11 +55,12 @@ The following documentation is for the [internal CI API](ci/README.md): ...@@ -55,11 +55,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
{ {
...@@ -98,6 +99,13 @@ that needs access to the GitLab API. ...@@ -98,6 +99,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
......
...@@ -36,11 +36,36 @@ describe API::Helpers, api: true do ...@@ -36,11 +36,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