diff --git a/README.md b/README.md index df766a12e1..bb0a425c2d 100644 --- a/README.md +++ b/README.md @@ -513,6 +513,10 @@ Skipping the cluster provisioning and running a single test: `robot -b debug.txt -v SERVER_VERSION:4.1.1 -v SYNC_GATEWAY_VERSION:1.2.0-79 -t "test sync sanity" testsuites/syncgateway/functional/1sg_1ac_1cbs/1sg_1ac_1cbs.robot` +Running a subsuite with parent suite setup + +`robot --loglevel DEBUG -v SERVER_VERSION:4.1.0 -v SYNC_GATEWAY_VERSION:42fc10bbc819fe34940c66abd1fd02a8d51490ca -s 1sg_1cbs-openid-connect testsuites/syncgateway/functional/1sg_1cbs` + **Running Performance Tests** - [Spin up a AWS CloudFormation stack](#Spin=Up-Machines-on-AWS) diff --git a/libraries/testkit/cluster.py b/libraries/testkit/cluster.py index b01dbfd69e..40fcd97472 100644 --- a/libraries/testkit/cluster.py +++ b/libraries/testkit/cluster.py @@ -69,9 +69,7 @@ def reset(self, config_path): # Stop sync_gateways log.info(">>> Stopping sync_gateway") status = ansible_runner.run_ansible_playbook("stop-sync-gateway.yml") - if status != 0: - log.error("Error in provisioning!! Verify your ssh user is correct in 'libraries/provision/playbooks/ansible.cfg'") - raise Exception("Failed to run provisioning") + assert status == 0, "Failed to stop sync gateway" # Stop sync_gateways log.info(">>> Stopping sg_accel") diff --git a/requirements.txt b/requirements.txt index 92a7ac45b1..34e077d2ed 100644 --- a/requirements.txt +++ b/requirements.txt @@ -20,6 +20,7 @@ paramiko==1.16.0 pyasn1==0.1.9 pycparser==2.14 pycrypto==2.6.1 +PyJWT==1.4.0 python-dateutil==2.5.1 PyYAML==3.11 requests==2.9.1 diff --git a/resources/sync_gateway_configs/sync_gateway_openid_connect_cc.json b/resources/sync_gateway_configs/sync_gateway_openid_connect_cc.json new file mode 100644 index 0000000000..f4eb591e2a --- /dev/null +++ b/resources/sync_gateway_configs/sync_gateway_openid_connect_cc.json @@ -0,0 +1,78 @@ +{ + "log":[ + "*" + ], + "compressResponses": false, + "AdminInterface":"0.0.0.0:4985", + "databases":{ + "db":{ + "oidc":{ + "default_provider":"test", + "providers":{ + "testinvalidclientid":{ + "issuer":"http://localhost:4984/db/_oidc_testing", + "client_id":"invalid", + "validation_key":"R75hfd9lasdwertwerutecw8", + "callback_url":"http://localhost:4984/db/_oidc_callback", + "register":true + }, + "test":{ + "issuer":"http://localhost:4984/db/_oidc_testing", + "client_id":"sync_gateway", + "validation_key":"R75hfd9lasdwertwerutecw8", + "callback_url":"http://localhost:4984/db/_oidc_callback", + "register":true + }, + "testnosessions":{ + "issuer":"http://localhost:4984/db/_oidc_testing", + "client_id":"sync_gateway", + "validation_key":"R75hfd9lasdwertwerutecw8", + "callback_url":"http://localhost:4984/db/_oidc_callback", + "register":true, + "disable_session":true + }, + "testinvalidscope":{ + "issuer":"http://localhost:4984/db/_oidc_testing", + "client_id":"sync_gateway", + "validation_key":"R75hfd9lasdwertwerutecw8", + "callback_url":"http://localhost:4984/db/_oidc_callback", + "register":true, + "scope":["invalid_scope"] + }, + "testsmallscope":{ + "issuer":"http://localhost:4984/db/_oidc_testing", + "client_id":"sync_gateway", + "validation_key":"R75hfd9lasdwertwerutecw8", + "callback_url":"http://localhost:4984/db/_oidc_callback", + "register":true, + "scope":["openid"] + }, + "testlargescope":{ + "issuer":"http://localhost:4984/db/_oidc_testing", + "client_id":"sync_gateway", + "validation_key":"R75hfd9lasdwertwerutecw8", + "callback_url":"http://localhost:4984/db/_oidc_callback", + "register":true, + "scope":["openid", "email", "profile"] + } + } + }, + "unsupported":{ + "oidc_test_provider":{ + "enabled":true + } + }, + "server":"http://{{ couchbase_server_primary_node }}:8091", + "bucket":"data-bucket", + "users":{ + "GUEST":{ + "disabled":true, + "admin_channels":[ + "*" + ] + } + } + } + } +} + diff --git a/testsuites/syncgateway/functional/1sg_1cbs/1sg_1cbs-openid-connect.robot b/testsuites/syncgateway/functional/1sg_1cbs/1sg_1cbs-openid-connect.robot new file mode 100644 index 0000000000..c88753bf8f --- /dev/null +++ b/testsuites/syncgateway/functional/1sg_1cbs/1sg_1cbs-openid-connect.robot @@ -0,0 +1,122 @@ +*** Settings *** +Resource resources/common.robot + +Library Process +Library OperatingSystem +Library ../test_openid_connect.py +Library DebugLibrary +Library ${Libraries}/NetworkUtils.py +Library ${KEYWORDS}/Logging.py + +Test Timeout 10 minutes + +Test Setup Setup Test +Test Teardown Teardown Test + +*** Variables *** + + +*** Test Cases *** +# Cluster has been setup + +Test OpenIdConnect Basic Test User Port + [Documentation] + ... Tests the basic OpenIDConnect login flow against the non-admin port + [Tags] sanity + Test OpenIdConnect Basic Test sg_url=${sg_url} sg_db=${sg_db} is_admin_port=${False} + +Test OpenIdConnect Basic Test Admin Port + [Documentation] + ... Tests the basic OpenIDConnect login flow against the admin port + [Tags] sanity + Test OpenIdConnect Basic Test sg_url=${sg_url_admin} sg_db=${sg_db} is_admin_port=${True} + +Test OpenIDConnect Notauthenticated + [Documentation] + ... Simulate a failed authentication and make sure no session is created + [Tags] sanity + Test OpenIDConnect Notauthenticated sg_url=${sg_url} sg_db=${sg_db} + +Test OpenIDConnect No Session + [Documentation] + ... Authenticate with a test openid provider that is configured to NOT add a Set-Cookie header + [Tags] sanity + Test OpenIDConnect No Session sg_url=${sg_url} sg_db=${sg_db} + +Test OpenIDConnect Oidc Challenge Invalid Provider Name + [Documentation] + ... Authenticate with a non-default provider using an invalid provider name and expect an error + [Tags] sanity + Test OpenIDConnect Oidc Challenge Invalid Provider Name sg_url=${sg_url} sg_db=${sg_db} + +Test OpenIDConnect Expired Token + [Documentation] + ... Authenticate and create an ID token that only lasts for 5 seconds, wait 10 seconds + ... and make sure the token is rejected + [Tags] sanity + Test OpenIDConnect Expired Token sg_url=${sg_url} sg_db=${sg_db} + + +Test OpenIDConnect Negative Token Expiry + [Documentation] + ... Create a token with a negative expiry time and expect that authentication + ... is not possible + [Tags] sanity + Test OpenIDConnect Negative Token Expiry sg_url=${sg_url} sg_db=${sg_db} + + +Test OpenIDConnect Garbage Token + [Documentation] + ... Send a garbage/invalid token and make sure it cannot be used + [Tags] sanity + Test OpenIDConnect Garbage Token sg_url=${sg_url} sg_db=${sg_db} + +Test OpenIDConnect Invalid Scope + [Documentation] + ... Try to discover the authenticate endpoint URL with a test provider that has an + ... invalid scope, and expect an error + [Tags] sanity + Test OpenIDConnect Invalid Scope sg_url=${sg_url} sg_db=${sg_db} + +Test OpenIDConnect Small Scope + [Documentation] + ... Use the smallest OpenIDConnect scope possible, and make sure + ... certain claims like "email" are not present in the JWT returned + [Tags] sanity + Test OpenIDConnect Small Scope sg_url=${sg_url} sg_db=${sg_db} + +Test OpenIDConnect Large Scope + [Documentation] + ... Authenticate against a test provider config that only has a larger scope than the default, + ... and make sure things like the nickname are returned in the jwt token returned back + [Tags] sanity + Test OpenIDConnect Large Scope sg_url=${sg_url} sg_db=${sg_db} + +Test OpenIDConnect Public Session Endpoint + [Documentation] + ... Create a new session from the OpenID Connect token returned by hitting + ... the public _session endpoint and make sure the response contains the Set-Cookie header. + [Tags] sanity + Test OpenIDConnect Public Session Endpoint sg_url=${sg_url} sg_db=${sg_db} + +*** Keywords *** +Setup Test + + # Provisioning happens in the __init__.robot file + + Log Using cluster %{CLUSTER_CONFIG} console=True + Set Environment Variable CLUSTER_CONFIG ${CLUSTER_CONFIGS}/1sg_1cbs + + Reset Cluster ${SYNC_GATEWAY_CONFIGS}/sync_gateway_openid_connect_cc.json + + ${cluster_hosts} = Get Cluster Topology %{CLUSTER_CONFIG} + + Set Test Variable ${sg_url} ${cluster_hosts["sync_gateways"][0]["public"]} + Set Test Variable ${sg_url_admin} ${cluster_hosts["sync_gateways"][0]["admin"]} + + Set Test Variable ${sg_db} db + +Teardown Test + Log Tearing down test ... console=True + List Connections + Run Keyword If Test Failed Fetch And Analyze Logs ${TEST_NAME} diff --git a/testsuites/syncgateway/functional/1sg_1cbs/__init__.robot b/testsuites/syncgateway/functional/1sg_1cbs/__init__.robot index 2bdef7c77f..2c589df4d7 100644 --- a/testsuites/syncgateway/functional/1sg_1cbs/__init__.robot +++ b/testsuites/syncgateway/functional/1sg_1cbs/__init__.robot @@ -1,3 +1,7 @@ + +# This will not get run if the test is run with a direct reference to the test robot +# file, unless you define a keyword to explicitly run the Suite Setup in your robot test. + *** Settings *** Resource resources/common.robot @@ -22,6 +26,5 @@ Suite Setup ... sync_gateway_version=${SYNC_GATEWAY_VERSION} ... sync_gateway_config=${SYNC_GATEWAY_CONFIG} - Suite Teardown Log Tearing down suite ... console=True diff --git a/testsuites/syncgateway/functional/test_openid_connect.py b/testsuites/syncgateway/functional/test_openid_connect.py new file mode 100644 index 0000000000..f2755e83b7 --- /dev/null +++ b/testsuites/syncgateway/functional/test_openid_connect.py @@ -0,0 +1,495 @@ +from testkit.cluster import Cluster +import requests +import logging +import time +from urlparse import urlparse +from keywords.utils import log_r +from keywords.utils import log_info +from HTMLParser import HTMLParser +from requests import HTTPError +import jwt +import json + + +DEFAULT_PROVIDER = "test" + + +class FormActionHTMLParser(HTMLParser): + """ + Given some HTML, looks for a
element, and extracts the "action" attribute + and saves it to self.form_action + """ + + def handle_starttag(self, tag, attrs): + if tag == "form": + for attr_tuple in attrs: + if attr_tuple[0] == "action": + self.form_action = attr_tuple[1] + + +def extract_cookie(set_cookie_response): + """ + Given a header string like: + + SyncGatewaySession=0f429ac978005d887131995ef3b1c0459311beff; Path=/db; Expires=Tue, 31 May 2016 21:28:30 GMT + + convert this into a dictionary like: + + {'SyncGatewaySession': '0f429ac978005d887131995ef3b1c0459311beff'} + + """ + name_value_pairs = set_cookie_response.split(";") + sg_session_pair = name_value_pairs[0] # SyncGatewaySession=0f429ac978005d887131995ef3b1c0459311beff + cookie_name = sg_session_pair.split("=")[0] # SyncGatewaySession + cookie_val = sg_session_pair.split("=")[1] # 0f429ac978005d887131995ef3b1c0459311beff + cookies = {cookie_name: cookie_val} # {'SyncGatewaySession': '0f429ac978005d887131995ef3b1c0459311beff'} + return cookies + + +def discover_authenticate_url(sg_url, sg_db, provider): + + """ + Discover the full url to the authenticate endpoint: + + http://:port/db/_oidc_testing/authenticate?client_id=sync_gateway&redirect_uri=http%3A%2F%2F192.168.0.112%3A4984%2Fdb%2F_oidc_callback&response_type=code&scope=openid+email&state= + + """ + + authenticate_endpoint = discover_authenticate_endpoint(sg_url, sg_db, provider) + + # build the full url + url = "{}/{}/_oidc_testing/{}".format( + sg_url, + sg_db, + authenticate_endpoint + ) + + return url + + +def discover_authenticate_endpoint(sg_url, sg_db, provider): + """ + Discover the authenticate endpoint and parameters. + + The result should look something like this: + + authenticate?client_id=sync_gateway&redirect_uri=http%3A%2F%2F192.168.0.112%3A4984%2Fdb%2F_oidc_callback&response_type=code&scope=openid+email&state= + """ + + # make a request to the _oidc_challenge endpoint + oidc_challenge_url = "{}/{}/_oidc_challenge?provider={}".format(sg_url, sg_db, provider) + log_info("Invoking _oidc_challenge against: {}".format(oidc_challenge_url)) + response = requests.get(oidc_challenge_url) + + # the Www-Authenticate header will look something like this: + # 'OIDC login="http://localhost:4984/db/_oidc_testing/authorize?client_id=sync_gateway&redirect_uri=http%3A%2F%2Flocalhost%3A4984%2Fdb%2F_oidc_callback&response_type=code&scope=openid+email&state="' + www_auth_header = response.headers['Www-Authenticate'] + max_split = 1 + + # Split the string on '=' and we should have something like: '"http://localhost:4984/db/_oid ..."' at this point + oidc_login_url = www_auth_header.split("=", max_split)[1] + + # Remove unwanted double quotes (eg, '"stuff"' -> 'stuff') + if oidc_login_url.startswith('"') and oidc_login_url.endswith('"'): + oidc_login_url = oidc_login_url[1:-1] + + # get the sg hostname, since we need to substitute this for any instances of localhost + # needed until https://github.com/couchbase/sync_gateway/issues/1849 is fixed + sg_url_parsed = urlparse(sg_url) + sg_hostname = sg_url_parsed.hostname + oidc_login_url = oidc_login_url.replace("localhost", sg_hostname) + + # Fetch the oidc_login_url + response = requests.get(oidc_login_url) + response.raise_for_status() + parser = FormActionHTMLParser() + parser.feed(response.text) + return parser.form_action + + +def test_openidconnect_basic_test(sg_url, sg_db, is_admin_port): + + # make a request against the db and expect a 401 response since we haven't authenticated yet. + # (but there's no point in doing this on the admin port since we'll never get a 401) + if not is_admin_port: + db_url = "{}/{}".format(sg_url, sg_db) + resp = requests.get(db_url) + assert resp.status_code == 401, "Expected 401 response" + + # get the authenticate endpoint and query params, should look something like: + # authenticate?client_id=sync_gateway&redirect_uri= ... + authenticate_endpoint = discover_authenticate_endpoint(sg_url, sg_db, DEFAULT_PROVIDER) + + # build the full url + authenticate_endpoint_url = "{}/{}/_oidc_testing/{}".format( + sg_url, + sg_db, + authenticate_endpoint + ) + + # Make the request to _oidc_testing + # multipart/form data content + formdata = { + 'username': ('', 'testuser'), + 'authenticated': ('', 'Return a valid authorization code for this user') + } + authenticate_response = requests.post(authenticate_endpoint_url, files=formdata) + set_cookie_response_header = authenticate_response.headers['Set-Cookie'] + log_r(authenticate_response) + + # extract the token from the response + authenticate_response_json = authenticate_response.json() + id_token = authenticate_response_json["id_token"] + refresh_token = authenticate_response_json["refresh_token"] + + # make sure the id token has the email field in it + decoded_id_token = jwt.decode(id_token, verify=False) + assert "email" in decoded_id_token.keys() + + # make a request using the ID token against the db and expect a 200 response + headers = {"Authorization": "Bearer {}".format(id_token)} + db_url = "{}/{}".format(sg_url, sg_db) + resp = requests.get(db_url, headers=headers) + log_r(resp) + assert resp.status_code == 200, "Expected 200 response" + + # make a request using the cookie against the db and expect a 200 response + db_url = "{}/{}".format(sg_url, sg_db) + resp = requests.get(db_url, cookies=extract_cookie(set_cookie_response_header)) + log_r(resp) + assert resp.status_code == 200, "Expected 200 response" + + # make a request using the session_id that's sent in the body + resp = requests.get(db_url, cookies={"SyncGatewaySession": authenticate_response_json["session_id"]}) + assert resp.status_code == 200, "Expected 200 response" + + # try to use the refresh token to get a few new id_tokens + id_tokens = [ id_token ] + for i in xrange(3): + + # This pause is required because according to @ajres: + # The id_token will only be unique if the two calls are more than a second apart. + # It would be easy to add an atomically incrementing nonce claim to each token to ensure that they are always unique + time.sleep(2) + + refresh_token_url = "{}/{}/_oidc_refresh?refresh_token={}&provider={}".format(sg_url, sg_db, refresh_token, "test") + authenticate_response = requests.get(refresh_token_url) + authenticate_response_json = authenticate_response.json() + id_token_refresh = authenticate_response_json["id_token"] + # make sure we get a unique id token each time + assert id_token_refresh not in id_tokens + + # make a request using the ID token against the db and expect a 200 response + headers = {"Authorization": "Bearer {}".format(id_token_refresh)} + resp = requests.get(db_url, headers=headers) + log_r(resp) + assert resp.status_code == 200, "Expected 200 response" + + id_tokens.append(id_token_refresh) + + +def test_openidconnect_notauthenticated(sg_url, sg_db): + + # get the authenticate endpoint and query params, should look something like: + # authenticate?client_id=sync_gateway&redirect_uri= ... + authenticate_endpoint = discover_authenticate_endpoint(sg_url, sg_db, DEFAULT_PROVIDER) + + # build the full url + authenticate_endpoint_url = "{}/{}/_oidc_testing/{}".format( + sg_url, + sg_db, + authenticate_endpoint + ) + + # Make the request to _oidc_testing + formdata = { + 'username': ('', 'testuser'), + 'notauthenticated': ('', 'Return an authorization error for this user') + } + response = requests.post(authenticate_endpoint_url, files=formdata) + assert response.status_code == 401 + + +def test_openidconnect_oidc_challenge_invalid_provider_name(sg_url, sg_db): + """ + If oidc_challenge is called with an invalid provider name, it should not return + an Www-Authenticate header + """ + + # make a request to the _oidc_challenge endpoint + oidc_challenge_url = "{}/{}/_oidc_challenge?provider={}".format(sg_url, sg_db, "bogusprovider") + response = requests.get(oidc_challenge_url) + log_info("response.headers: {}".format(response.headers)) + assert "Www-Authenticate" not in response.headers + assert response.status_code == 400 + + +def test_openidconnect_no_session(sg_url, sg_db): + + # multipart/form data content + formdata = { + 'username': ('', 'testuser'), + 'authenticated': ('', 'Return a valid authorization code for this user') + } + + authenticate_url = discover_authenticate_url(sg_url, sg_db, "testnosessions") + + # Make the request to _oidc_testing + response = requests.post(authenticate_url, files=formdata) + log_r(response) + assert "Set-Cookie" not in response.headers + + +def test_openidconnect_expired_token(sg_url, sg_db): + + token_expiry_seconds = 5 + + # multipart/form data content + formdata = { + 'username': ('', 'testuser'), + 'authenticated': ('', 'Return a valid authorization code for this user'), + 'tokenttl': ('', "{}".format(token_expiry_seconds)), + } + + # get the authenticate endpoint and query params, should look something like: + # authenticate?client_id=sync_gateway&redirect_uri= ... + authenticate_endpoint = discover_authenticate_endpoint(sg_url, sg_db, DEFAULT_PROVIDER) + + # build the full url + url = "{}/{}/_oidc_testing/{}".format( + sg_url, + sg_db, + authenticate_endpoint + ) + + # Make the request to _oidc_testing + response = requests.post(url, files=formdata) + log_r(response) + + # extract the token from the response + response_json = response.json() + id_token = response_json["id_token"] + + # wait until token expires + time.sleep(token_expiry_seconds + 1) + + # make a request using the ID token against the db and expect a 200 response + headers = {"Authorization": "Bearer {}".format(id_token)} + db_url = "{}/{}".format(sg_url, sg_db) + resp = requests.get(db_url, headers=headers) + log_r(resp) + assert resp.status_code != 200, "Expected non-200 response" + + + +def test_openidconnect_negative_token_expiry(sg_url, sg_db): + + token_expiry_seconds = -5 + + # multipart/form data content + formdata = { + 'username': ('', 'testuser'), + 'authenticated': ('', 'Return a valid authorization code for this user'), + 'tokenttl': ('', "{}".format(token_expiry_seconds)), + } + + # get the authenticate endpoint and query params, should look something like: + # authenticate?client_id=sync_gateway&redirect_uri= ... + authenticate_endpoint = discover_authenticate_endpoint(sg_url, sg_db, DEFAULT_PROVIDER) + + # build the full url + url = "{}/{}/_oidc_testing/{}".format( + sg_url, + sg_db, + authenticate_endpoint + ) + + response = requests.post(url, files=formdata) + assert response.status_code == 500 + + + + +def test_openidconnect_garbage_token(sg_url, sg_db): + + token_expiry_seconds = 5 + + # multipart/form data content + formdata = { + 'username': ('', 'testuser'), + 'authenticated': ('', 'Return a valid authorization code for this user'), + 'tokenttl': ('', "{}".format(token_expiry_seconds)), + } + + # get the authenticate endpoint and query params, should look something like: + # authenticate?client_id=sync_gateway&redirect_uri= ... + authenticate_endpoint = discover_authenticate_endpoint(sg_url, sg_db, DEFAULT_PROVIDER) + + # build the full url + url = "{}/{}/_oidc_testing/{}".format( + sg_url, + sg_db, + authenticate_endpoint + ) + + # Make the request to _oidc_testing + response = requests.post(url, files=formdata) + log_r(response) + + # extract the token from the response + response_json = response.json() + id_token = response_json["id_token"] + + # Complete garbage Token + # make a request using the ID token against the db and expect a 200 response + headers = {"Authorization": "Bearer {}".format("garbage")} + db_url = "{}/{}".format(sg_url, sg_db) + resp = requests.get(db_url, headers=headers) + log_r(resp) + assert resp.status_code != 200, "Expected non-200 response" + + # Partial garbage Token + + # get all the components split by "." + token_components = id_token.split(".") + + # get subset of components except for last one + all_components_except_last = token_components[:-1] + + # add a garbage last component + all_components_except_last.append("garbage") + + # create a string out of the components + partial_garbage_token = ".".join(all_components_except_last) + + headers = {"Authorization": "Bearer {}".format(partial_garbage_token)} + db_url = "{}/{}".format(sg_url, sg_db) + resp = requests.get(db_url, headers=headers) + log_r(resp) + assert resp.status_code != 200, "Expected non-200 response" + + +def test_openidconnect_invalid_scope(sg_url, sg_db): + + try: + discover_authenticate_endpoint(sg_url, sg_db, "testinvalidscope") + except HTTPError: + log_info("got expected HTTPError trying to get the authenticate endpoint") + # ok we got an exception, which is expected since we are using an invalid scope + return + + raise Exception("Expected HTTPError since we are using invalid scope") + + +def test_openidconnect_small_scope(sg_url, sg_db): + + # multipart/form data content + formdata = { + 'username': ('', 'testuser'), + 'authenticated': ('', 'Return a valid authorization code for this user') + } + + # get the authenticate endpoint and query params, should look something like: + # authenticate?client_id=sync_gateway&redirect_uri= ... + authenticate_endpoint = discover_authenticate_endpoint(sg_url, sg_db, "testsmallscope") + + # build the full url + url = "{}/{}/_oidc_testing/{}".format( + sg_url, + sg_db, + authenticate_endpoint + ) + + # Make the request to _oidc_testing + response = requests.post(url, files=formdata) + log_r(response) + + # extract the token from the response + response_json = response.json() + id_token = response_json["id_token"] + + # {u'iss': u'http://localhost:4984/db/_oidc_testing', u'iat': 1466050188, u'aud': u'sync_gateway', u'exp': 1466053788, u'sub': u'testuser'} + decoded_id_token = jwt.decode(id_token, verify=False) + + assert "email" not in decoded_id_token.keys() + + +def test_openidconnect_large_scope(sg_url, sg_db): + + # multipart/form data content + formdata = { + 'username': ('', 'testuser'), + 'authenticated': ('', 'Return a valid authorization code for this user') + } + + # get the authenticate endpoint and query params, should look something like: + # authenticate?client_id=sync_gateway&redirect_uri= ... + authenticate_endpoint = discover_authenticate_endpoint(sg_url, sg_db, "testlargescope") + + # build the full url + url = "{}/{}/_oidc_testing/{}".format( + sg_url, + sg_db, + authenticate_endpoint + ) + + # Make the request to _oidc_testing + response = requests.post(url, files=formdata) + log_r(response) + + # extract the token from the response + response_json = response.json() + id_token = response_json["id_token"] + + # {u'iss': u'http://localhost:4984/db/_oidc_testing', u'iat': 1466050188, u'aud': u'sync_gateway', u'exp': 1466053788, u'sub': u'testuser'} + decoded_id_token = jwt.decode(id_token, verify=False) + + log_info("decoded_id_token: {}".format(decoded_id_token)) + + assert "nickname" in decoded_id_token.keys() + + +def test_openidconnect_public_session_endpoint(sg_url, sg_db): + + # multipart/form data content + formdata = { + 'username': ('', 'testuser'), + 'authenticated': ('', 'Return a valid authorization code for this user') + } + + # get the authenticate endpoint and query params, should look something like: + # authenticate?client_id=sync_gateway&redirect_uri= ... + authenticate_endpoint = discover_authenticate_endpoint(sg_url, sg_db, DEFAULT_PROVIDER) + + # build the full url + url = "{}/{}/_oidc_testing/{}".format( + sg_url, + sg_db, + authenticate_endpoint + ) + + # Make the request to _oidc_testing + response = requests.post(url, files=formdata) + log_r(response) + + # extract the token from the response + response_json = response.json() + id_token = response_json["id_token"] + + headers = { + "Authorization": "Bearer {}".format(id_token), + "Content-Type": "application/json" + } + url = "{}/{}/_session".format( + sg_url, + sg_db + ) + + response = requests.post(url, headers=headers) + assert "Set-Cookie" in response.headers.keys() + set_cookie_response = response.headers['Set-Cookie'] + assert "SyncGatewaySession" in set_cookie_response + + + +