Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement pluggable authentication and session support for replicator #1176

Merged
merged 1 commit into from
Mar 5, 2018

Conversation

nickva
Copy link
Contributor

@nickva nickva commented Feb 19, 2018

Overview

Implement pluggable authentication and session support for replicator

Previously replicator used only basic authentication. It was simple and
straightforward. However, with PBKDF2 hashing becoming the default it would be
nice not to do all the password verification work with every single request,
and instead take advantage of session (cookie) based authentication.

Description

This commit implements session based authentication via a plugin mechanism.
The list of available replicator auth modules is configurable:

[replicator]
auth_plugins = couch_replicator_auth_session,couch_replicator_auth_noop

The plugins will be tried in order. The first one to successfully initialize
will end up being used for that endpoint (source or target).

During the initialization callback, a plugin could decide it cannot be used in
the current context. In that case it signals to be "ignored". The plugin
framework will then skip over it and try to initialize the next on in the list.

couch_replicator_auth_basic effectively implements the old behavior. This
plugin should normally be used as a default catch-all at the end of the plugin
list. In some cases, it might be useful to enforce exclusive use of
session-based auth and fail replication jobs if it is not available.

couch_replicator_auth_session does most of the work of handling session based
authentication. On initialization, it strips away basic auth credentials from
headers and url to avoid basic auth being used on the server. Then it is in
charge of periodically issuing POST requests to _session, updating the
headers of each request with the latest cookie value, and possibly picking up
new session cookie if the server can issue them along with reglar responses.

As discussed in #1153 this work also removes OAuth 1.0 support. After
server-side support was removed, it had stopped working anyway since the main
oauth app was removed. However, with the plugin framework in place it would be
possible for someone to implement it as a separate module not entangled with
the rest of the replicator code.

Fixes #1153

Checklist

  • Code is written and works correctly;
  • Changes are covered by tests;
  • Documentation reflects the changes[*];

[*] Will do it after the review stage.

@nickva nickva force-pushed the replicator-pluggable-auth-module branch 2 times, most recently from 380848c to d24e194 Compare February 19, 2018 20:21
handle_call({update_headers, Headers, _Epoch}, _From, State) ->
case maybe_refresh(State) of
{ok, State1} ->
Cookie = "AuthSession=" ++ State1#state.cookie,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am a bit concerned with this. Cookie value is not escaped and can be manipulated. I followed the code we use for maybe_update_cookie and it looks like there is a possibility to inject new headers.

CookieKVs = mochiweb_cookies:parse_cookie("AuthSession=\"see\nInjected=hack\";baz; Version=1").
Cookie = mochiweb_headers:get_value("AuthSession", CaseInsKVs).
40> "AuthSession=" ++ Cookie.
"AuthSession=see\nInjected=hack"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But wondering what the threat model is here. The server somehow injecting extra headers into its own session cookie?

@nickva nickva force-pushed the replicator-pluggable-auth-module branch from d24e194 to bb8e8c7 Compare February 20, 2018 06:21
@nickva
Copy link
Contributor Author

nickva commented Feb 20, 2018

Conducted a simple benchmark using couchdyno https://github.com/cloudant-labs/couchdyno

Timed replicating 100k docs with PBKDF2 iterations set to 10k.

[couch_http_auth]
iterations = 10000
rep.replicate_1_to_n_and_compare(1, normal=True, num=100000)

Master took 1133 seconds and this PR took 574 seconds.

This should allow increasing the work factor to improve security without sacrificing replication performance.

@nickva nickva force-pushed the replicator-pluggable-auth-module branch from bb8e8c7 to 4bf464c Compare February 20, 2018 07:03
@nickva
Copy link
Contributor Author

nickva commented Feb 20, 2018

CouchDyno has a set of extended, long running replication tests. Those pass as well:

$ couchdyno (master) $ ./test.sh
zip_safe flag not set; analyzing archive contents...
=============== test session starts =====================
platform darwin -- Python 2.7.13, pytest-3.0.3, py-1.4.33, pluggy-0.4.0 .../src/couchdyno/venv/bin/python
cachedir: .cache
rootdir: .../src/couchdyno, inifile: pytest.ini
collected 47 items

tests/test_rep_00_basics.py::test_basic_continuous PASSED
tests/test_rep_00_basics.py::test_basic_10_continuous PASSED
tests/test_rep_00_basics.py::test_basic_10_continuous_1000_docs PASSED
tests/test_rep_00_basics.py::test_basic_normal PASSED
tests/test_rep_00_basics.py::test_basic_10_normal PASSED
tests/test_rep_00_basics.py::test_basic_attachment PASSED
tests/test_rep_00_basics.py::test_basic_js_filter PASSED
tests/test_rep_01_patterns.py::test_n_to_n_pattern[False-False] PASSED
tests/test_rep_01_patterns.py::test_n_to_n_pattern[False-True] PASSED
tests/test_rep_01_patterns.py::test_n_to_n_pattern[True-False] PASSED
tests/test_rep_01_patterns.py::test_n_to_n_pattern[True-True] PASSED
tests/test_rep_01_patterns.py::test_1_to_n_pattern PASSED
tests/test_rep_01_patterns.py::test_n_to_1_pattern PASSED
tests/test_rep_01_patterns.py::test_n_chain_pattern PASSED
tests/test_rep_01_patterns.py::test_all_pattern_continuous PASSED
tests/test_rep_02_filter.py::test_n_to_n_view_filter[False-None] PASSED
tests/test_rep_02_filter.py::test_n_to_n_view_filter[False-1] PASSED
tests/test_rep_02_filter.py::test_n_to_n_view_filter[True-None] PASSED
tests/test_rep_02_filter.py::test_n_to_n_view_filter[True-1] PASSED
tests/test_rep_02_filter.py::test_n_to_n_js_filter[False-None] PASSED
tests/test_rep_02_filter.py::test_n_to_n_js_filter[False-1] PASSED
tests/test_rep_02_filter.py::test_n_to_n_js_filter[True-None] PASSED
tests/test_rep_02_filter.py::test_n_to_n_js_filter[True-1] PASSED
tests/test_rep_02_filter.py::test_n_to_n_doc_ids_filter[False-None] PASSED
tests/test_rep_02_filter.py::test_n_to_n_doc_ids_filter[False-1] PASSED
tests/test_rep_02_filter.py::test_n_to_n_doc_ids_filter[True-None] PASSED
tests/test_rep_02_filter.py::test_n_to_n_doc_ids_filter[True-1] PASSED
tests/test_rep_02_filter.py::test_n_to_n_mango_filter[False-None] PASSED
tests/test_rep_02_filter.py::test_n_to_n_mango_filter[False-1] PASSED
tests/test_rep_02_filter.py::test_n_to_n_mango_filter[True-None] PASSED
tests/test_rep_02_filter.py::test_n_to_n_mango_filter[True-1] PASSED
tests/test_rep_03_attachments.py::test_attachments[False-64-1] PASSED
tests/test_rep_03_attachments.py::test_attachments[False-64-100] PASSED
tests/test_rep_03_attachments.py::test_attachments[False-100000-1] PASSED
tests/test_rep_03_attachments.py::test_attachments[False-100000-100] PASSED
tests/test_rep_03_attachments.py::test_attachments[False-attachments4-1] PASSED
tests/test_rep_03_attachments.py::test_attachments[False-attachments5-100] PASSED
tests/test_rep_03_attachments.py::test_attachments[True-64-1] PASSED
tests/test_rep_03_attachments.py::test_attachments[True-64-100] PASSED
tests/test_rep_03_attachments.py::test_attachments[True-100000-1] PASSED
tests/test_rep_03_attachments.py::test_attachments[True-100000-100] PASSED
tests/test_rep_03_attachments.py::test_attachments[True-attachments10-1] PASSED
tests/test_rep_03_attachments.py::test_attachments[True-attachments11-100] PASSED
tests/test_rep_04_cluster.py::test_migration_on_node_failure SKIPPED
tests/test_rep_05_migration.py::test_migration_error PASSED
tests/test_rep_05_migration.py::test_migration_triggered PASSED
tests/test_rep_05_migration.py::test_migration_downgrade_failed SKIPPED

============== 45 passed, 2 skipped in 10065.23 seconds ======================

@nickva nickva force-pushed the replicator-pluggable-auth-module branch 2 times, most recently from 78bf2f4 to fdaa3df Compare February 22, 2018 19:05
@apache apache deleted a comment from nickva Feb 22, 2018
src/couch_replicator/src/couch_replicator_auth_basic.erl Outdated Show resolved Hide resolved
src/couch_replicator/src/couch_replicator_auth_session.erl Outdated Show resolved Hide resolved
src/couch_replicator/src/couch_replicator_auth_session.erl Outdated Show resolved Hide resolved
src/couch_replicator/src/couch_replicator_auth_session.erl Outdated Show resolved Hide resolved
% port and path as they were in the original.
Prefix = lists:concat([Proto, ":https://", User, ":", Pass, "@"]),
Suffix = lists:sublist(Url, length(Prefix) + 1, length(Url) + 1),
NoCreds = lists:concat([Proto, ":https://", Suffix]),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

elaborate, why not an iolist?

Copy link
Contributor Author

@nickva nickva Feb 22, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the url part, that's a string() based on ibrowse's url type:

https://github.com/cmullaparthi/ibrowse/blob/master/src/ibrowse.erl#L154

Also concat automatically does the stringification so don't have to do atom_to_list for Proto for example. Previously also had a Port that was an integer there.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah, bleh. ok.

src/couch_replicator/src/couch_replicator_auth_session.erl Outdated Show resolved Hide resolved
src/couch_replicator/src/couch_replicator_auth_session.erl Outdated Show resolved Hide resolved
@nickva nickva force-pushed the replicator-pluggable-auth-module branch 3 times, most recently from 727af7f to 921609e Compare February 25, 2018 09:10
@nickva
Copy link
Contributor Author

nickva commented Feb 26, 2018

I made two updates to the PR over the weekend:

  1. Added more unit tests covering various cookie update states in _auth_session plugin:

https://github.com/cloudant/couchdb/blob/921609e9ce3b3e535561816b3e435b039ad731df/src/couch_replicator/src/couch_replicator_auth_session.erl#L554-L648

  1. Realized when we POST-ed to _session with content type application/x-www-form-urlencoded we didn't actually urlencode username and password param values. So enabled that:

https://github.com/cloudant/couchdb/blob/921609e9ce3b3e535561816b3e435b039ad731df/src/couch_replicator/src/couch_replicator_auth_session.erl#L554-L648

@nickva
Copy link
Contributor Author

nickva commented Feb 27, 2018

Debating whether to make this an opt-in feature at first and make the default plugin list = couch_replicator_auth_noop. Then after a period of time, presumably if any users have tried this feature out and didn't find issues with it. Make it the default.

@nickva nickva force-pushed the replicator-pluggable-auth-module branch from 921609e to ebc7b6a Compare February 27, 2018 22:14
@janl janl added this to the 2.2.0 milestone Feb 28, 2018
@nickva nickva force-pushed the replicator-pluggable-auth-module branch from ebc7b6a to cbe0e5f Compare February 28, 2018 22:44
@rnewson
Copy link
Member

rnewson commented Mar 1, 2018

I've enabled this with;

+auth_plugins = couch_replicator_auth_session,couch_replicator_auth_noop

and used sys:get_state(Pid) to look at the state of a replicator task.

auth_props remains [].

Until I see this work, it's -1 from me.

@rnewson
Copy link
Member

rnewson commented Mar 2, 2018

ok, figured out my mistake. It does acquire cookies. I'll finish the code review itself today.

@nickva nickva force-pushed the replicator-pluggable-auth-module branch from cbe0e5f to 6a7d69d Compare March 4, 2018 21:37
@nickva
Copy link
Contributor Author

nickva commented Mar 4, 2018

Rebased on latest master

Previously replicator used only basic authentication. It was simple and
straightforward. However, with PBKDF2 hashing becoming the default it would be
nice not to do all the password verification work with every single request,
and instead take advantage of session (cookie) based authentication.

This commit implements session based authentication via a plugin mechanism.
The list of available replicator auth modules is configurable. For example:

```
[replicator]
auth_plugins = couch_replicator_auth_session,couch_replicator_auth_basic
```

The plugins will be tried in order. The first one to successfully initialize
will end up being used for that endpoint (source or target).

During the initialization callback, a plugin could decide it cannot be used in
the current context. In that case it signals to be "ignored". The plugin
framework will then skip over it and try to initialize the next on in the list.

`couch_replicator_auth_basic` effectively implements the old behavior. This
plugin should normally be used as a default catch-all at the end of the plugin
list. In some cases, it might be useful to enforce exclusive use of
session-based auth and fail replication jobs if it is not available.

`couch_replicator_auth_session` does most of the work of handling session based
authentication. On initialization, it strips away basic auth credentials from
headers and url to avoid basic auth being used on the server. Then it is in
charge of periodically issuing POST requests to `_session`, updating the
headers of each request with the latest cookie value, and possibly picking up
new session cookie if the server can issue them along with reglar responses.

Currently session based auth plugin is not enabled by default and is an opt-in
feature. That is, users would have to explicitly add the session module to the
list of auth_plugins. In a future, session might be used by default.

As discussed in apache#1153 this work also removes OAuth 1.0 support. After
server-side support was removed, it had stopped working anyway since the main
oauth app was removed. However, with the plugin framework in place it would be
possible for someone to implement it as a separate module not entangled with
the rest of the replicator code.

Fixes apache#1153
@nickva nickva force-pushed the replicator-pluggable-auth-module branch from 6a7d69d to eedb540 Compare March 5, 2018 16:07
Copy link
Member

@rnewson rnewson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

neatly done.

@nickva nickva merged commit 72b41c4 into apache:master Mar 5, 2018
@nickva nickva deleted the replicator-pluggable-auth-module branch March 5, 2018 18:29
wohali added a commit that referenced this pull request Jul 23, 2018
wohali added a commit that referenced this pull request Jul 24, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

6 participants