diff --git a/app/adapters/keycloak_adapter.rb b/app/adapters/keycloak_adapter.rb index 1126a6ed..98f0f238 100644 --- a/app/adapters/keycloak_adapter.rb +++ b/app/adapters/keycloak_adapter.rb @@ -19,6 +19,12 @@ def to_hash }.compact end + def token_exchange_attributes + token_exchange = params[:token_exchange_enabled] + return {} if token_exchange.nil? + { 'standard.token.exchange.enabled' => token_exchange.to_s } + end + protected attr_reader :params @@ -43,18 +49,19 @@ class Client delegate :to_json, to: :to_h alias read to_json - attribute :oidc_configuration, default: {}.freeze + attribute :oidc_configuration, default: -> { OAuthConfiguration.new({}) } def to_h + oidc = oidc_configuration { name: name, description: description, clientId: id, secret: client_secret, redirectUris: [ redirect_url ].compact, - attributes: { '3scale' => true }, + attributes: { '3scale' => true }.merge(oidc.token_exchange_attributes), enabled: enabled?, - **oidc_configuration, + **oidc, **self.class.attributes, } end diff --git a/app/adapters/rest_adapter.rb b/app/adapters/rest_adapter.rb index eacfbdfb..aefbf4dc 100644 --- a/app/adapters/rest_adapter.rb +++ b/app/adapters/rest_adapter.rb @@ -109,6 +109,7 @@ def as_json(*args) implicit_flow_enabled: :implicit, direct_access_grants_enabled: :password, service_accounts_enabled: :client_credentials, + token_exchange_enabled: :"urn:ietf:params:oauth:grant-type:token-exchange", }.freeze private_constant :MAPPING diff --git a/app/services/integration/abstract_service.rb b/app/services/integration/abstract_service.rb index 1ca695ed..cbd10dd7 100644 --- a/app/services/integration/abstract_service.rb +++ b/app/services/integration/abstract_service.rb @@ -95,6 +95,7 @@ def build_client(entry) OIDC_FLOWS = %i[ standard_flow_enabled implicit_flow_enabled service_accounts_enabled direct_access_grants_enabled + token_exchange_enabled ].freeze private_constant :OIDC_FLOWS diff --git a/test/adapters/keycloak_adapter_test.rb b/test/adapters/keycloak_adapter_test.rb index e565b854..5c057a2b 100644 --- a/test/adapters/keycloak_adapter_test.rb +++ b/test/adapters/keycloak_adapter_test.rb @@ -105,12 +105,61 @@ class KeycloakAdapterTest < ActiveSupport::TestCase test 'oauth flows' do keycloak = { clientId: "client_id", implicitFlowEnabled: true, serviceAccountsEnabled: true } - assert_equal keycloak, KeycloakAdapter::Client.new({ - id: 'client_id', - oidc_configuration: { - implicit_flow_enabled: true, - service_accounts_enabled: true, - } - }).to_h.slice(*keycloak.keys) + client = KeycloakAdapter::Client.new({ + id: 'client_id', + oidc_configuration: { + implicit_flow_enabled: true, + service_accounts_enabled: true, + token_exchange_enabled: true, + } + }) + hash = client.to_h + assert_equal keycloak, hash.slice(*keycloak.keys) + assert_equal 'true', hash[:attributes]['standard.token.exchange.enabled'] + assert_equal true, hash[:attributes]['3scale'] + end + + test 'oauth flows without token exchange preserves 3scale attribute' do + client = KeycloakAdapter::Client.new({ + id: 'client_id', + oidc_configuration: { + standard_flow_enabled: true, + } + }) + hash = client.to_h + assert_equal true, hash[:attributes]['3scale'] + assert_nil hash[:attributes]['standard.token.exchange.enabled'] + end + + test 'oauth flows with token exchange disabled' do + client = KeycloakAdapter::Client.new({ + id: 'client_id', + oidc_configuration: { + token_exchange_enabled: false, + } + }) + hash = client.to_h + assert_equal 'false', hash[:attributes]['standard.token.exchange.enabled'] + assert_equal true, hash[:attributes]['3scale'] + end + + test 'create client with token exchange' do + keycloak = KeycloakAdapter.new('http://example.com/auth/realm/name', authentication: 'sometoken') + + client = KeycloakAdapter::Client.new({ + id: 'client_id', + oidc_configuration: { token_exchange_enabled: true } + }) + + create = stub_request(:post, 'http://example.com/auth/realm/name/clients-registrations/default'). + with( + body: client.to_json, + headers: { 'Content-Type' => 'application/json', 'Authorization' => 'Bearer sometoken' }). + to_return(status: 200, body: { clientId: 'client_id' }.to_json, + headers: { 'Content-Type' => 'application/json' }) + + keycloak.create_client(client) + + assert_requested create end end diff --git a/test/adapters/rest_adapter_test.rb b/test/adapters/rest_adapter_test.rb index 55b178a4..03ee7e5c 100644 --- a/test/adapters/rest_adapter_test.rb +++ b/test/adapters/rest_adapter_test.rb @@ -46,6 +46,53 @@ class RESTAdapterTest < ActiveSupport::TestCase assert subject.new('https://example.com').create_client(client) end + test 'oauth flows with token exchange' do + client = RESTAdapter::Client.new( + id: 'foo', + oidc_configuration: { + standard_flow_enabled: true, + token_exchange_enabled: true, + } + ) + grant_types = JSON.parse(client.to_json).fetch('grant_types') + assert_includes grant_types, 'authorization_code' + assert_includes grant_types, 'urn:ietf:params:oauth:grant-type:token-exchange' + end + + test 'oauth flows without token exchange' do + client = RESTAdapter::Client.new( + id: 'foo', + oidc_configuration: { + standard_flow_enabled: true, + token_exchange_enabled: false, + } + ) + grant_types = JSON.parse(client.to_json).fetch('grant_types') + assert_includes grant_types, 'authorization_code' + refute_includes grant_types, 'urn:ietf:params:oauth:grant-type:token-exchange' + end + + test 'create client with token exchange sends grant type' do + client = RESTAdapter::Client.new( + id: 'foo', + oidc_configuration: { + standard_flow_enabled: true, + token_exchange_enabled: true, + } + ) + + stub_request(:get, "https://example.com/.well-known/openid-configuration"). + to_return(status: 404, body: '', headers: {}) + + stub_request(:put, "https://example.com/clients/foo"). + with( + body: client.to_json, + headers: { 'Content-Type'=>'application/json' }). + to_return(status: 200, body: { status: 'ok' }.to_json, headers: { 'Content-Type' => 'application/json' }) + + assert subject.new('https://example.com').create_client(client) + end + test 'create client with basic auth' do client = RESTAdapter::Client.new(id: 'foo') adapter = subject.new('https://user:pass@example.com')