diff --git a/ci/test_suites/authenticators_oidc/secrets.yml b/ci/test_suites/authenticators_oidc/secrets.yml index 187bd0e538..7507ad7ec3 100644 --- a/ci/test_suites/authenticators_oidc/secrets.yml +++ b/ci/test_suites/authenticators_oidc/secrets.yml @@ -5,9 +5,22 @@ ci: OKTA_USERNAME: !var ci/okta/user/assigned/username OKTA_PASSWORD: !var ci/okta/user/assigned/password + IDENTITY_CLIENT_ID: !var ci/identity/app/client-id + IDENTITY_CLIENT_SECRET: !var ci/identity/app/client-secret + IDENTITY_PROVIDER_URI: !var ci/identity/app/provider-uri + IDENTITY_USERNAME: !var ci/identity/user/assigned/username + IDENTITY_PASSWORD: !var ci/identity/user/assigned/password + development: OKTA_CLIENT_ID: !var dev/okta/app/client-id OKTA_CLIENT_SECRET: !var dev/okta/app/client-secret OKTA_PROVIDER_URI: !var dev/okta/app/provider-uri OKTA_USERNAME: !var dev/okta/user/assigned/username OKTA_PASSWORD: !var dev/okta/user/assigned/password + + IDENTITY_CLIENT_ID: !var dev/identity/app/client-id + IDENTITY_CLIENT_SECRET: !var dev/identity/app/client-secret + IDENTITY_PROVIDER_URI: !var dev/identity/app/provider-uri + # Identity integration tests depend on the following variables. + # IDENTITY_USERNAME: myUsername + # IDENTITY_PASSWORD: myP@ssword! diff --git a/ci/test_suites/authenticators_oidc/test b/ci/test_suites/authenticators_oidc/test index 079b6c8080..59c9a95f17 100755 --- a/ci/test_suites/authenticators_oidc/test +++ b/ci/test_suites/authenticators_oidc/test @@ -33,6 +33,11 @@ function _hydrate_all_env_args() { "OKTA_PROVIDER_URI=${OKTA_PROVIDER_URI}oauth2/default" "OKTA_USERNAME=$OKTA_USERNAME" "OKTA_PASSWORD=$OKTA_PASSWORD" + "IDENTITY_CLIENT_ID=$IDENTITY_CLIENT_ID" + "IDENTITY_CLIENT_SECRET=$IDENTITY_CLIENT_SECRET" + "IDENTITY_PROVIDER_URI=$IDENTITY_PROVIDER_URI" + "IDENTITY_USERNAME=$IDENTITY_USERNAME" + "IDENTITY_PASSWORD=$IDENTITY_PASSWORD" ) } diff --git a/cucumber/authenticators_oidc/features/authn_oidc_identity.feature b/cucumber/authenticators_oidc/features/authn_oidc_identity.feature new file mode 100644 index 0000000000..ffce436c8c --- /dev/null +++ b/cucumber/authenticators_oidc/features/authn_oidc_identity.feature @@ -0,0 +1,51 @@ +@authenticators_oidc +Feature: OIDC Authenticator V2 - Users can authenticate with Identity using OIDC + + Background: + Given the following environment variables are available: + | context_variable | environment_variable | default_value | + | oidc_provider_uri | IDENTITY_PROVIDER_URI | | + | oidc_client_id | IDENTITY_CLIENT_ID | | + | oidc_client_secret | IDENTITY_CLIENT_SECRET | | + | oidc_redirect_url | IDENTITY_REDIRECT | http://localhost:3000/authn-oidc/identity/cucumber/authenticate | + | oidc_username | IDENTITY_USERNAME | | + | oidc_password | IDENTITY_PASSWORD | | + + And I load a policy and enable an oidc user into group "conjur/authn-oidc/identity/users": + """ + - !policy + id: conjur/authn-oidc/identity + body: + - !webservice + annotations: + description: Authentication service for Identity, based on Open ID Connect. + + - !variable provider-uri + - !variable client-id + - !variable client-secret + - !variable claim-mapping + - !variable state + - !variable nonce + - !variable redirect-uri + + - !group users + + - !permit + role: !group users + privilege: [ read, authenticate ] + resource: !webservice + """ + And I set the following conjur variables: + | variable_id | context_variable | default_value | + | conjur/authn-oidc/identity/provider-uri | oidc_provider_uri | | + | conjur/authn-oidc/identity/client-id | oidc_client_id | | + | conjur/authn-oidc/identity/client-secret | oidc_client_secret | | + | conjur/authn-oidc/identity/claim-mapping | | preferred_username | + | conjur/authn-oidc/identity/redirect-uri | oidc_redirect_url | | + + @smoke + Scenario: Authenticating with Conjur using Identity + Given I retrieve OIDC configuration from the provider endpoint for "identity" + And I authenticate and fetch a code from Identity + When I authenticate via OIDC with code and service_id "identity" + Then the OIDC user has been authorized by conjur diff --git a/cucumber/authenticators_oidc/features/authn_oidc_okta.feature b/cucumber/authenticators_oidc/features/authn_oidc_okta.feature index 6491e3f55b..3e895c6492 100644 --- a/cucumber/authenticators_oidc/features/authn_oidc_okta.feature +++ b/cucumber/authenticators_oidc/features/authn_oidc_okta.feature @@ -48,4 +48,4 @@ Feature: OIDC Authenticator V2 - Users can authenticate with Okta using OIDC Given I retrieve OIDC configuration from the provider endpoint for "okta" And I authenticate and fetch a code from Okta When I authenticate via OIDC with code and service_id "okta" - Then the okta user has been authorized by conjur + Then the OIDC user has been authorized by conjur diff --git a/cucumber/authenticators_oidc/features/step_definitions/authn_oidc_steps.rb b/cucumber/authenticators_oidc/features/step_definitions/authn_oidc_steps.rb index dff862dfef..182b1a05ef 100644 --- a/cucumber/authenticators_oidc/features/step_definitions/authn_oidc_steps.rb +++ b/cucumber/authenticators_oidc/features/step_definitions/authn_oidc_steps.rb @@ -95,6 +95,113 @@ @scenario_context.add(:redirect_uri, provider['redirect_uri']) end +Given(/^I authenticate and fetch a code from Identity/) do + # A request to /Security/StartAuthentication begins the login process, + # and returns a list of authentication mechanisms to engage with. + + host = URI(@scenario_context.get(:redirect_uri)).host + resp = start_auth_request(host, @scenario_context.get(:oidc_username)) + resp_h = JSON.parse(resp.body) + + if resp_h["Result"]["PodFqdn"] + resp = start_auth_request(resp_h["Result"]["PodFqdn"], @scenario_context.get(:oidc_username)) + resp_h = JSON.parse(resp.body) + end + + raise "Failed to retrieve OIDC code status: #{resp.code}" unless resp_h["success"] + + session_id = resp_h["Result"]["SessionId"] + challenges = resp_h["Result"]["Challenges"] + + # Usually, we would iterate through MFA challenges sequentially. + # For our purposes, though, we want to make sure to use the Password + # and Mobile Authenticator mechanisms. + + password_mechanism = challenges[0]["Mechanisms"].detect { |m| m["PromptSelectMech"] == "Password" } + mobile_auth_mechanism = challenges[1]["Mechanisms"].detect { |m| m["PromptSelectMech"] == "Mobile Authenticator" } + + # Advance Password-based authentication handshake. + + password_body = JSON.generate({ + "Action": "Answer", + "Answer": @scenario_context.get(:oidc_password), + "MechanismId": password_mechanism["MechanismId"], + "SessionId": session_id + }) + resp = advance_auth_request(host, password_body) + resp_h = JSON.parse(resp.body) + raise "Failed to advance authentication: #{resp_h['Message']}" unless resp_h["success"] + + # Begin temporary block + # + # Engaging with a Mobile Auth and polling for out-of-band authentication + # success is included temporarily, and is required for users that are required + # to perform MFA. Before merging, and service account should be made available + # for running this test in CI, and the account should not be bound by MFA. + + # Advance Mobile Authenticator-based authentication handshake. + + mobile_auth_body = JSON.generate({ + "Action": "StartOOB", + "MechanismId": mobile_auth_mechanism["MechanismId"], + "SessionId": session_id + }) + resp = advance_auth_request(host, mobile_auth_body) + resp_h = JSON.parse(resp.body) + raise "Failed to advance authentication: #{resp_h['Message']}" unless resp_h["success"] + + puts "Dev env users: select #{resp_h['Result']['GeneratedAuthValue']} in your Identity notification" + + # For 30 seconds, Poll for out-of-band authentication success. + + poll_body = JSON.generate({ + "Action": "Poll", + "MechanismId": mobile_auth_mechanism["MechanismId"], + "SessionId": session_id + }) + + token = "" + current = Time.current + while Time.current < current + 30 + resp = advance_auth_request(host, poll_body) + resp_h = JSON.parse(resp.body) + + next unless resp_h["Result"]["Summary"] == "LoginSuccess" + + cookies = resp.get_fields('Set-Cookie') + token_cookie = cookies.detect { |c| c.start_with?(".ASPXAUTH") } + token = token_cookie.split('; ')[0].split('=')[1] + end + if token == "" + raise "Failed to advance authentication: please reattempt" + end + + # End temporary block + # + # Make request to /Authorization endpoint with bearer token. + + target = URI("#{@scenario_context.get(:redirect_uri)}&state=test-state") + resp = nil + until target.to_s.include?("localhost:3000/authn-oidc/identity/cucumber") + http = Net::HTTP.new(target.host, 443) + http.use_ssl = true + req = Net::HTTP::Get.new(target.request_uri) + req['Accept'] = '*/*' + req['Authorization'] = "Bearer #{token}" + resp = http.request(req) + + target = URI(resp['location'].to_s) + end + + if resp.is_a?(Net::HTTPRedirection) + parse_oidc_code(resp['location']).each do |key, value| + @scenario_context.set(key, value) + end + else + raise "Failed to retrieve OIDC code status: #{resp.code}" + end +end + Given(/^I authenticate and fetch a code from Okta/) do uri = URI("https://#{URI(@scenario_context.get(:redirect_uri)).host}/api/v1/authn") body = JSON.generate({ username: @scenario_context.get(:oidc_username), password: @scenario_context.get(:oidc_password) }) @@ -176,7 +283,7 @@ ) end -Then(/^the okta user has been authorized by conjur/) do +Then(/^the OIDC user has been authorized by conjur/) do username = @scenario_context.get(:oidc_username) expect(retrieved_access_token.username).to eq(username) end diff --git a/cucumber/authenticators_oidc/features/support/authn_oidc_helper.rb b/cucumber/authenticators_oidc/features/support/authn_oidc_helper.rb index ffa3dbff45..359cc9b948 100644 --- a/cucumber/authenticators_oidc/features/support/authn_oidc_helper.rb +++ b/cucumber/authenticators_oidc/features/support/authn_oidc_helper.rb @@ -62,6 +62,33 @@ def invalid_id_token "invalididtoken" end + def identity_request(uri, body) + http = Net::HTTP.new(uri.host, 443) + http.use_ssl = true + + req = Net::HTTP::Post.new(uri.request_uri) + req['Accept'] = '*/*' + req['Content-Type'] = 'application/json' + req.body = body + + http.request(req) + end + + def start_auth_request(host, username) + body = JSON.generate({ + "User": username, + "Version": "1.0" + }) + + uri = URI("https://#{host}/Security/StartAuthentication") + identity_request(uri, body) + end + + def advance_auth_request(host, body) + uri = URI("https://#{host}/Security/AdvanceAuthentication") + identity_request(uri, body) + end + private def parse_oidc_id_token diff --git a/dev/docker-compose.yml b/dev/docker-compose.yml index ec2009f3e9..ad3e88ba78 100644 --- a/dev/docker-compose.yml +++ b/dev/docker-compose.yml @@ -49,6 +49,18 @@ services: # See https://github.com/DatabaseCleaner/database_cleaner#safeguards DATABASE_CLEANER_ALLOW_REMOTE_DATABASE_URL: "true" BUNDLE_GEMFILE: /src/conjur-server/Gemfile + # Adding the following envvars allows users to run Cucumber tests for + # AuthnOIDC V2 with Okta and Identity from the dev environment. + OKTA_CLIENT_ID: ${OKTA_CLIENT_ID} + OKTA_CLIENT_SECRET: ${OKTA_CLIENT_SECRET} + OKTA_PROVIDER_URI: ${OKTA_PROVIDER_URI}oauth2/default + OKTA_USERNAME: ${OKTA_USERNAME} + OKTA_PASSWORD: ${OKTA_PASSWORD} + IDENTITY_CLIENT_ID: ${IDENTITY_CLIENT_ID} + IDENTITY_CLIENT_SECRET: ${IDENTITY_CLIENT_SECRET} + IDENTITY_PROVIDER_URI: ${IDENTITY_PROVIDER_URI} + IDENTITY_USERNAME: ${IDENTITY_USERNAME} + IDENTITY_PASSWORD: ${IDENTITY_PASSWORD} cap_add: - SYSLOG ports: