Skip to content

Commit

Permalink
Merge pull request #2637 from cyberark/ofira_jwt_oidc_header1
Browse files Browse the repository at this point in the history
  • Loading branch information
aloncarmel111 authored Sep 11, 2022
2 parents 18e9f6f + ff46ce5 commit 954d71d
Show file tree
Hide file tree
Showing 6 changed files with 240 additions and 20 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
- Nothing should go in this section, please add to the latest unreleased version
(and update the corresponding date), or add a new version.

## [1.18.4] - 2022-09-11

### Added
- Adds support for authorization token in header in OIDC authenticator.
[cyberark/conjur#2637](https://github.com/cyberark/conjur/pull/2637)

## [1.18.3] - 2022-09-07

### Security
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,14 @@ module AuthnOidc
inputs: %i[authenticator_input]
) do
extend(Forwardable)
# The 'request' field in the 'authenticator_input' object will be used to extract the id token from the request header
def_delegators(:@authenticator_input, :service_id, :authenticator_name,
:account, :username, :webservice, :credentials, :client_ip,
:account, :username, :webservice, :credentials, :client_ip, :request,
:role)

def call
validate_account_exists
validate_service_id_exists
validate_credentials_include_id_token
verify_and_decode_token
validate_conjur_username
input_with_username
Expand All @@ -36,15 +36,6 @@ def validate_service_id_exists
raise Errors::Authentication::AuthnOidc::ServiceIdMissing unless service_id
end

def validate_credentials_include_id_token
id_token_field_name = "id_token"

# check that id token field exists and has some value
if decoded_credentials.fetch(id_token_field_name, "") == ""
raise Errors::Authentication::RequestBody::MissingRequestParam, id_token_field_name
end
end

def verify_and_decode_token
@decoded_token = @verify_and_decode_token.(
provider_uri: oidc_authenticator_secrets["provider-uri"],
Expand All @@ -53,9 +44,45 @@ def verify_and_decode_token
)
end

# The credentials are in a URL encoded form data in the request body
# The credentials are in a URL encoded form data in the request body or in the request header
def decoded_credentials
@decoded_credentials ||= Hash[URI.decode_www_form(credentials)]
@decoded_credentials ||= begin
return token_from_body if token_from_body

return token_from_header if token_from_header

# If the token is not in the header or body, raise a missing param exception
raise Errors::Authentication::RequestBody::MissingRequestParam, 'id_token'
end
end

def token_from_header
@token_from_header ||= begin
return nil unless request&.headers&.key?("HTTP_AUTHORIZATION")

# Extract the token from the authorization header
id_token = request.headers['HTTP_AUTHORIZATION'].split(' ', -1)

# Verify the token is given as a bearer token
return nil unless id_token[0] == "Bearer" && id_token.length == 2

# Return the token formatted as it would be if extracted from the body
# as `application/x-www-form-urlencoded` data.
{
# URL decode the ID token from the header
"id_token" => URI.decode_www_form_component(id_token[1])
}
end
end

def token_from_body
@token_from_body ||= begin
# Parse the request body
credential_token = Hash[URI.decode_www_form(credentials)]

# Return the parsed body if it include the token
credential_token unless credential_token.fetch('id_token', "").empty?
end
end

def oidc_authenticator_secrets
Expand Down
18 changes: 17 additions & 1 deletion cucumber/authenticators_oidc/features/authn_oidc.feature
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,23 @@ Feature: OIDC Authenticator - Hosts can authenticate with OIDC authenticator
And I successfully set OIDC variables

@smoke
Scenario: A valid id token to get Conjur access token
Scenario: A valid id token in header to get Conjur access token
# We want to verify the returned access token is valid for retrieving a secret
Given I have a "variable" resource called "test-variable"
And I permit user "alice" to "execute" it
And I add the secret value "test-secret" to the resource "cucumber:variable:test-variable"
And I fetch an ID Token for username "alice" and password "alice"
And I save my place in the audit log file
When I authenticate via OIDC with id token in header
Then user "alice" has been authorized by Conjur
And I successfully GET "/secrets/cucumber/variable/test-variable" with authorized user
And The following appears in the audit log after my savepoint:
"""
cucumber:user:alice successfully authenticated with authenticator authn-oidc service cucumber:webservice:conjur/authn-oidc/keycloak
"""

@smoke
Scenario: A valid id token in body to get Conjur access token
# We want to verify the returned access token is valid for retrieving a secret
Given I have a "variable" resource called "test-variable"
And I permit user "alice" to "execute" it
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,13 @@
)
end

When(/^I authenticate via OIDC with id token in header$/) do
authenticate_id_token_with_oidc_in_header(
service_id: AuthnOidcHelper::SERVICE_ID,
account: AuthnOidcHelper::ACCOUNT
)
end

Given(/^I successfully set OIDC V2 variables for "([^"]*)"$/) do |service_id|
create_oidc_secret("provider-uri", oidc_provider_uri, service_id)
create_oidc_secret("response-type", oidc_response_type, service_id)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,14 @@ def authenticate_id_token_with_oidc(service_id:, account:, id_token: parsed_id_t
post(path, payload)
end

def authenticate_id_token_with_oidc_in_header(service_id:, account:, id_token: parsed_id_token)
service_id_part = service_id ? "/#{service_id}" : ""
path = "#{conjur_hostname}/authn-oidc#{service_id_part}/#{account}/authenticate"
headers = {}
headers["Authorization"] = "Bearer #{id_token}"
post(path, {}, headers)
end

def authenticate_code_with_oidc(service_id:, account:, code: url_oidc_code, state: url_oidc_state)
path = "#{create_auth_url(service_id: service_id, account: account, user_id: nil)}"
get(url_with_params(path: path,code: code, state: state ))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,14 @@
# request mock
####################################

def mock_authenticate_oidc_request(request_body_data:)
def mock_authenticate_oidc_request(request_body_data: , request_headers:)
double('AuthnOidcRequest').tap do |request|
request_body = StringIO.new
request_body.puts(request_body_data)
request_body.rewind

allow(request).to receive(:body).and_return(request_body)
allow(request).to receive(:headers).and_return(request_headers)
end
end

Expand All @@ -42,23 +43,43 @@ def request_body(request)
end

let(:authenticate_id_token_request) do
mock_authenticate_oidc_request(request_body_data: "id_token={\"id_token_username_field\": \"alice\"}")
mock_authenticate_oidc_request(request_body_data: "id_token={\"id_token_username_field\": \"alice\"}", request_headers: nil)
end

let(:authenticate_id_token_request_missing_id_token_username_field) do
mock_authenticate_oidc_request(request_body_data: "id_token={}")
mock_authenticate_oidc_request(request_body_data: "id_token={}", request_headers: nil)
end

let(:authenticate_id_token_request_empty_id_token_username_field) do
mock_authenticate_oidc_request(request_body_data: "id_token={\"id_token_username_field\": \"\"}")
mock_authenticate_oidc_request(request_body_data: "id_token={\"id_token_username_field\": \"\"}", request_headers: nil)
end

let(:authenticate_id_token_request_missing_id_token_field) do
mock_authenticate_oidc_request(request_body_data: "some_key=some_value")
mock_authenticate_oidc_request(request_body_data: "some_key=some_value", request_headers: nil)
end

let(:authenticate_id_token_request_empty_id_token_field) do
mock_authenticate_oidc_request(request_body_data: "id_token=")
mock_authenticate_oidc_request(request_body_data: "id_token=", request_headers: nil)
end

let(:authenticate_id_token_request_id_token_in_header_field) do
mock_authenticate_oidc_request(request_body_data: "id_token=", request_headers: {"HTTP_AUTHORIZATION" => "Bearer {\"id_token_username_field\":\"alice\"}"})
end

let(:authenticate_id_token_request_invalid_id_token_in_header_field) do
mock_authenticate_oidc_request(request_body_data: "id_token=", request_headers: {"HTTP_AUTHORIZATION" => "{\"id_token_username_field\":\"alice\"}"})
end

let(:authenticate_id_token_request_empty_id_token_in_header_field) do
mock_authenticate_oidc_request(request_body_data: "id_token=", request_headers: {"HTTP_AUTHORIZATION" => ""})
end

let(:authenticate_id_token_request_missing_id_token_in_body_field) do
mock_authenticate_oidc_request(request_body_data: "", request_headers: {"HTTP_AUTHORIZATION" => "Bearer {\"id_token_username_field\":\"alice\"}"})
end

let(:authenticate_id_token_request_contain_only_bearer_in_header_field) do
mock_authenticate_oidc_request(request_body_data: "", request_headers: {"HTTP_AUTHORIZATION" => "Bearer"})
end

# ____ _ _ ____ ____ ____ ___ ____ ___
Expand Down Expand Up @@ -247,6 +268,141 @@ def request_body(request)
expect { subject }.to raise_error(::Errors::Authentication::RequestBody::MissingRequestParam)
end
end

context "with valid id token in header" do
let(:audit_success) { false }

subject do
input_ = Authentication::AuthenticatorInput.new(
authenticator_name: 'authn-oidc',
service_id: 'my-service',
account: 'my-acct',
username: nil,
credentials: request_body(authenticate_id_token_request_id_token_in_header_field),
client_ip: '127.0.0.1',
request: authenticate_id_token_request_id_token_in_header_field
)

::Authentication::AuthnOidc::UpdateInputWithUsernameFromIdToken.new(
verify_and_decode_token: mocked_decode_and_verify_id_token,
validate_account_exists: mock_validate_account_exists(validation_succeeded: true)
).call(
authenticator_input: input_
)
end

it "returns the input with the username inside it" do
expect(subject.username).to eql("alice")
end
end

context "with invalid id token in header and in body" do
let(:audit_success) { false }

subject do
input_ = Authentication::AuthenticatorInput.new(
authenticator_name: 'authn-oidc',
service_id: 'my-service',
account: 'my-acct',
username: nil,
credentials: request_body(authenticate_id_token_request_invalid_id_token_in_header_field),
client_ip: '127.0.0.1',
request: authenticate_id_token_request_invalid_id_token_in_header_field
)

::Authentication::AuthnOidc::UpdateInputWithUsernameFromIdToken.new(
verify_and_decode_token: mocked_decode_and_verify_id_token,
validate_account_exists: mock_validate_account_exists(validation_succeeded: true)
).call(
authenticator_input: input_
)
end

it "raises a MissingRequestParam error" do
expect { subject }.to raise_error(::Errors::Authentication::RequestBody::MissingRequestParam)
end
end

context "with empty id token in header and invalid in body" do
let(:audit_success) { false }

subject do
input_ = Authentication::AuthenticatorInput.new(
authenticator_name: 'authn-oidc',
service_id: 'my-service',
account: 'my-acct',
username: nil,
credentials: request_body(authenticate_id_token_request_empty_id_token_in_header_field),
client_ip: '127.0.0.1',
request: authenticate_id_token_request_empty_id_token_in_header_field
)

::Authentication::AuthnOidc::UpdateInputWithUsernameFromIdToken.new(
verify_and_decode_token: mocked_decode_and_verify_id_token,
validate_account_exists: mock_validate_account_exists(validation_succeeded: true)
).call(
authenticator_input: input_
)
end

it "raises a MissingRequestParam error" do
expect { subject }.to raise_error(::Errors::Authentication::RequestBody::MissingRequestParam)
end
end

context "with valid id token in header and empty body" do
let(:audit_success) { false }

subject do
input_ = Authentication::AuthenticatorInput.new(
authenticator_name: 'authn-oidc',
service_id: 'my-service',
account: 'my-acct',
username: nil,
credentials: request_body(authenticate_id_token_request_missing_id_token_in_body_field),
client_ip: '127.0.0.1',
request: authenticate_id_token_request_missing_id_token_in_body_field
)

::Authentication::AuthnOidc::UpdateInputWithUsernameFromIdToken.new(
verify_and_decode_token: mocked_decode_and_verify_id_token,
validate_account_exists: mock_validate_account_exists(validation_succeeded: true)
).call(
authenticator_input: input_
)
end

it "returns the input with the username inside it" do
expect(subject.username).to eql("alice")
end
end

context "with bearer only in id token in header and empty body" do
let(:audit_success) { false }

subject do
input_ = Authentication::AuthenticatorInput.new(
authenticator_name: 'authn-oidc',
service_id: 'my-service',
account: 'my-acct',
username: nil,
credentials: request_body(authenticate_id_token_request_contain_only_bearer_in_header_field),
client_ip: '127.0.0.1',
request: authenticate_id_token_request_contain_only_bearer_in_header_field
)

::Authentication::AuthnOidc::UpdateInputWithUsernameFromIdToken.new(
verify_and_decode_token: mocked_decode_and_verify_id_token,
validate_account_exists: mock_validate_account_exists(validation_succeeded: true)
).call(
authenticator_input: input_
)
end

it "raises a MissingRequestParam error" do
expect { subject }.to raise_error(::Errors::Authentication::RequestBody::MissingRequestParam)
end
end
end
end
end

0 comments on commit 954d71d

Please sign in to comment.