Commit d14c8b16 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 a73c6c42
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.9.10
- Allow the Rails cookie to be used for API authentication.
v 8.9.9 v 8.9.9
- Exclude some pending or inactivated rows in Member scopes - Exclude some pending or inactivated rows in Member scopes
......
...@@ -15,8 +15,6 @@ ...@@ -15,8 +15,6 @@
$.ajax( $.ajax(
url: url url: url
data:
private_token: gon.api_token
dataType: "json" dataType: "json"
).done (group) -> ).done (group) ->
callback(group) callback(group)
...@@ -29,7 +27,6 @@ ...@@ -29,7 +27,6 @@
$.ajax( $.ajax(
url: url url: url
data: data:
private_token: gon.api_token
search: query search: query
per_page: 20 per_page: 20
dataType: "json" dataType: "json"
...@@ -43,7 +40,6 @@ ...@@ -43,7 +40,6 @@
$.ajax( $.ajax(
url: url url: url
data: data:
private_token: gon.api_token
search: query search: query
per_page: 20 per_page: 20
dataType: "json" dataType: "json"
...@@ -57,7 +53,6 @@ ...@@ -57,7 +53,6 @@
$.ajax( $.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
...@@ -69,7 +64,6 @@ ...@@ -69,7 +64,6 @@
url = Api.buildUrl(Api.labelsPath) url = Api.buildUrl(Api.labelsPath)
url = url.replace(':id', project_id) url = url.replace(':id', project_id)
data.private_token = gon.api_token
$.ajax( $.ajax(
url: url url: url
type: "POST" type: "POST"
...@@ -88,7 +82,6 @@ ...@@ -88,7 +82,6 @@
$.ajax( $.ajax(
url: url url: url
data: data:
private_token: gon.api_token
search: query search: query
per_page: 20 per_page: 20
dataType: "json" dataType: "json"
......
...@@ -46,11 +46,12 @@ The following documentation is for the [internal CI API](ci/README.md): ...@@ -46,11 +46,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
{ {
...@@ -89,6 +90,13 @@ that needs access to the GitLab API. ...@@ -89,6 +90,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)
......
...@@ -9,13 +9,30 @@ module API ...@@ -9,13 +9,30 @@ module API
[ true, 1, '1', 't', 'T', 'true', 'TRUE', 'on', 'ON' ].include?(value) [ true, 1, '1', 't', 'T', 'true', 'TRUE', 'on', 'ON' ].include?(value)
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.allowed?(@current_user) unless @current_user && Gitlab::UserAccess.allowed?(@current_user)
return nil return nil
......
...@@ -12,7 +12,6 @@ module Gitlab ...@@ -12,7 +12,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 "should return nil for an invalid token" do it "should return 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