From 133a0b42ab14ce22f9ab3614671e95e05bc904b2 Mon Sep 17 00:00:00 2001 From: Grzegorz Grasza Date: Tue, 23 Dec 2025 16:28:38 +0100 Subject: [PATCH] Add OIDC authentication flows tests for federation This adds support for testing all OIDC authentication methods: - v3oidcpassword (Resource Owner Password Credentials) - v3oidcclientcredentials (Client Credentials) - v3oidcaccesstoken (Access Token Reuse) - v3oidcauthcode (Authorization Code) Note: v3oidcdeviceauthz requires Python 3.10+ and is not available in OSP18 which ships with Python 3.9. --- galaxy.yml | 2 +- roles/federation/README.md | 152 ++++++++++- roles/federation/defaults/main.yml | 17 ++ roles/federation/tasks/hook_post_deploy.yml | 23 ++ .../tasks/run_keycloak_client_setup.yml | 116 +++++++++ .../tasks/run_openstack_auth_setup.yml | 60 +++++ .../tasks/run_openstack_oidc_auth_tests.yml | 245 ++++++++++++++++++ .../templates/get-keycloak-token.sh.j2 | 107 ++++++++ .../templates/oidc-accesstoken.sh.j2 | 22 ++ .../federation/templates/oidc-authcode.sh.j2 | 28 ++ .../templates/oidc-clientcredentials.sh.j2 | 18 ++ 11 files changed, 786 insertions(+), 4 deletions(-) create mode 100644 roles/federation/tasks/run_keycloak_client_setup.yml create mode 100644 roles/federation/tasks/run_openstack_oidc_auth_tests.yml create mode 100644 roles/federation/templates/get-keycloak-token.sh.j2 create mode 100644 roles/federation/templates/oidc-accesstoken.sh.j2 create mode 100644 roles/federation/templates/oidc-authcode.sh.j2 create mode 100644 roles/federation/templates/oidc-clientcredentials.sh.j2 diff --git a/galaxy.yml b/galaxy.yml index e7b205fb48..66745af696 100644 --- a/galaxy.yml +++ b/galaxy.yml @@ -8,7 +8,7 @@ namespace: cifmw name: general # The version of the collection. Must be compatible with semantic versioning -version: 1.0.0 +version: 1.0.0+e5fa7b63 # The path to the Markdown (.md) readme file. This path is relative to the root of the collection readme: README.md diff --git a/roles/federation/README.md b/roles/federation/README.md index 21ce43cec7..cfa0f3e251 100644 --- a/roles/federation/README.md +++ b/roles/federation/README.md @@ -1,4 +1,150 @@ -federation -========= +# federation -This role will setup Openstack for user federation. The keycloak system will be used for the IdP provider. +This role sets up OpenStack Keystone federation with Keycloak (Red Hat SSO) as the Identity Provider. + +## Overview + +The federation role configures: +- Keycloak realm(s) with test users and groups +- Keystone Identity Provider and protocol configuration +- OIDC authentication for OpenStack CLI +- Comprehensive authentication testing + +## Supported OIDC Authentication Methods + +This role supports testing all OIDC authentication methods available in keystoneauth1: + +| Plugin Name | Description | Status | +|-------------|-------------|--------| +| `v3oidcpassword` | Resource Owner Password Credentials flow | ✅ Supported | +| `v3oidcclientcredentials` | Client Credentials flow | ✅ Supported | +| `v3oidcaccesstoken` | Reuse existing access token | ✅ Supported | +| `v3oidcauthcode` | Authorization Code flow | ✅ Supported | +| `v3oidcdeviceauthz` | Device Authorization flow (RFC 8628) | ⚠️ Requires Python 3.10+ | + +## Variables + +### Infrastructure Configuration + +| Variable | Default | Description | +|----------|---------|-------------| +| `cifmw_federation_keycloak_namespace` | `openstack` | Kubernetes namespace for Keycloak | +| `cifmw_federation_run_osp_cmd_namespace` | `openstack` | Kubernetes namespace for openstackclient | +| `cifmw_federation_domain` | - | Base domain for service URLs | + +### Keycloak Configuration + +| Variable | Default | Description | +|----------|---------|-------------| +| `cifmw_federation_keycloak_realm` | `openstack` | Primary Keycloak realm name | +| `cifmw_federation_keycloak_realm2` | `openstack2` | Secondary realm (multirealm mode) | +| `cifmw_federation_keycloak_admin_username` | `admin` | Keycloak admin username | +| `cifmw_federation_keycloak_admin_password` | `nomoresecrets` | Keycloak admin password | +| `cifmw_federation_deploy_multirealm` | `false` | Deploy multiple realms | + +### Test Users + +| Variable | Default | Description | +|----------|---------|-------------| +| `cifmw_federation_keycloak_testuser1_username` | `kctestuser1` | Test user 1 username | +| `cifmw_federation_keycloak_testuser1_password` | `nomoresecrets1` | Test user 1 password | +| `cifmw_federation_keycloak_testuser2_username` | `kctestuser2` | Test user 2 username | +| `cifmw_federation_keycloak_testuser2_password` | `nomoresecrets2` | Test user 2 password | + +### Keystone Integration + +| Variable | Default | Description | +|----------|---------|-------------| +| `cifmw_federation_IdpName` | `kcIDP` | Identity Provider name in Keystone | +| `cifmw_federation_keystone_domain` | `SSO` | Keystone domain for federated users | +| `cifmw_federation_mapping_name` | `SSOmap` | Keystone mapping name | +| `cifmw_federation_project_name` | `SSOproject` | Project for federated users | +| `cifmw_federation_group_name` | `SSOgroup` | Group for federated users | + +### OIDC Client Configuration + +| Variable | Default | Description | +|----------|---------|-------------| +| `cifmw_federation_keystone_OIDC_ClientID` | `rhoso` | OIDC client ID | +| `cifmw_federation_keystone_OIDC_ClientSecret` | `COX8bmlKAWn56XCGMrKQJj7dgHNAOl6f` | OIDC client secret | +| `cifmw_federation_keystone_OIDC_Scope` | `openid email profile` | OIDC scopes | + +### Testing Configuration + +| Variable | Default | Description | +|----------|---------|-------------| +| `cifmw_federation_run_oidc_auth_tests` | `true` | Run comprehensive OIDC auth tests | + +## Task Files + +### Main Tasks + +- `hook_pre_deploy.yml` - Deploys Keycloak before OpenStack +- `hook_post_deploy.yml` - Configures federation after OpenStack deployment +- `hook_controlplane_config.yml` - Adds federation config to control plane + +### Setup Tasks + +- `run_keycloak_setup.yml` - Deploy Keycloak operator and instance +- `run_keycloak_realm_setup.yml` - Configure Keycloak realm, users, and client +- `run_keycloak_client_setup.yml` - Enable advanced client features (Service Accounts, Device Auth) +- `run_openstack_setup.yml` - Configure Keystone IdP and mappings +- `run_openstack_auth_setup.yml` - Deploy authentication scripts to openstackclient pod + +### Test Tasks + +- `run_openstack_auth_test.yml` - Basic v3oidcpassword authentication test +- `run_openstack_oidc_auth_tests.yml` - Comprehensive OIDC authentication test suite + +## Authentication Scripts + +The following scripts are deployed to `/home/cloud-admin/` in the openstackclient pod: + +| Script | Description | +|--------|-------------| +| `get-token.sh ` | Get token using v3oidcpassword | +| `oidc-clientcredentials.sh` | Configure v3oidcclientcredentials auth | +| `oidc-accesstoken.sh ` | Configure v3oidcaccesstoken auth | +| `oidc-authcode.sh ` | Configure v3oidcauthcode auth | +| `get-keycloak-token.sh` | Helper to obtain tokens from Keycloak | + +### Example Usage + +```bash +# v3oidcpassword - Password flow +kubectl exec -n openstack openstackclient -- bash -c \ + 'source /home/cloud-admin/kctestuser1 && openstack token issue' + +# v3oidcclientcredentials - Client Credentials flow +kubectl exec -n openstack openstackclient -- bash -c \ + 'source /home/cloud-admin/oidc-clientcredentials.sh && openstack token issue' + +# v3oidcaccesstoken - Access Token flow +ACCESS_TOKEN=$(/home/cloud-admin/get-keycloak-token.sh access_token kctestuser1 nomoresecrets1) +kubectl exec -n openstack openstackclient -- bash -c \ + "source /home/cloud-admin/oidc-accesstoken.sh '$ACCESS_TOKEN' && openstack token issue" + +# v3oidcauthcode - Authorization Code flow +AUTH_CODE=$(/home/cloud-admin/get-keycloak-token.sh auth_code kctestuser1 nomoresecrets1) +kubectl exec -n openstack openstackclient -- bash -c \ + "source /home/cloud-admin/oidc-authcode.sh '$AUTH_CODE' && openstack token issue" +``` + +## Test Execution + +The comprehensive OIDC authentication tests are automatically run during the `hook_post_deploy.yml` phase when `cifmw_federation_run_oidc_auth_tests` is `true` (default). + +To run the tests manually: + +```yaml +- name: Run OIDC authentication tests + ansible.builtin.include_role: + name: federation + tasks_from: run_openstack_oidc_auth_tests.yml +``` + +## Notes + +- **Device Authorization Flow**: The `v3oidcdeviceauthz` plugin requires keystoneauth1 with Python 3.10+ support. OSP18 ships with Python 3.9 and does not include this plugin. +- **Multirealm**: CLI-based OIDC authentication testing only works in single realm mode. Multirealm federation is supported for Horizon-based authentication. +- **Keycloak Client**: The role automatically enables Service Accounts and Device Authorization on the Keycloak client to support all authentication methods. diff --git a/roles/federation/defaults/main.yml b/roles/federation/defaults/main.yml index acab89258d..a36d548281 100644 --- a/roles/federation/defaults/main.yml +++ b/roles/federation/defaults/main.yml @@ -147,3 +147,20 @@ cifmw_federation_keystone_idp1_provider_filename: "keycloak-{{ cifmw_federation_ cifmw_federation_keystone_idp2_conf_filename: "keycloak-{{ cifmw_federation_keycloak_namespace }}.{{ cifmw_federation_domain }}%2Fauth%2Frealms%2F{{ cifmw_federation_keycloak_realm2 }}.conf" cifmw_federation_keystone_idp2_client_filename: "keycloak-{{ cifmw_federation_keycloak_namespace }}.{{ cifmw_federation_domain }}%2Fauth%2Frealms%2F{{ cifmw_federation_keycloak_realm2 }}.client" cifmw_federation_keystone_idp2_provider_filename: "keycloak-{{ cifmw_federation_keycloak_namespace }}.{{ cifmw_federation_domain }}%2Fauth%2Frealms%2F{{ cifmw_federation_keycloak_realm2 }}.provider" + +# ============================================================================= +# OIDC AUTHENTICATION TESTING +# ============================================================================= +# Configuration for comprehensive OIDC authentication method testing +# +# When enabled, tests all supported OIDC authentication methods: +# - v3oidcpassword: Resource Owner Password Credentials flow +# - v3oidcclientcredentials: Client Credentials flow +# - v3oidcaccesstoken: Access Token Reuse flow +# - v3oidcauthcode: Authorization Code flow +# +# Note: v3oidcdeviceauthz (Device Authorization flow) requires Python 3.10+ +# and is not available in OSP18. + +# Enable/disable comprehensive OIDC authentication method tests +cifmw_federation_run_oidc_auth_tests: true diff --git a/roles/federation/tasks/hook_post_deploy.yml b/roles/federation/tasks/hook_post_deploy.yml index 7b49c46330..d1b59c88e7 100644 --- a/roles/federation/tasks/hook_post_deploy.yml +++ b/roles/federation/tasks/hook_post_deploy.yml @@ -78,3 +78,26 @@ - "{{ cifmw_federation_keycloak_testuser1_username }}" - "{{ cifmw_federation_keycloak_testuser2_username }}" when: not cifmw_federation_deploy_multirealm|bool + +# ============================================================================= +# Comprehensive OIDC Authentication Methods Testing +# ============================================================================= +# Tests all supported OIDC authentication methods when enabled. +# This requires Keycloak client to be configured for Service Accounts +# and Device Authorization. + +- name: Configure Keycloak client for all OIDC auth methods + ansible.builtin.include_role: + name: federation + tasks_from: run_keycloak_client_setup.yml + when: + - not cifmw_federation_deploy_multirealm | bool + - cifmw_federation_run_oidc_auth_tests | default(true) | bool + +- name: Run comprehensive OIDC authentication method tests + ansible.builtin.include_role: + name: federation + tasks_from: run_openstack_oidc_auth_tests.yml + when: + - not cifmw_federation_deploy_multirealm | bool + - cifmw_federation_run_oidc_auth_tests | default(true) | bool diff --git a/roles/federation/tasks/run_keycloak_client_setup.yml b/roles/federation/tasks/run_keycloak_client_setup.yml new file mode 100644 index 0000000000..8f5fb8e04d --- /dev/null +++ b/roles/federation/tasks/run_keycloak_client_setup.yml @@ -0,0 +1,116 @@ +--- +# Copyright Red Hat, Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +# ============================================================================= +# Keycloak Client Configuration for OIDC Authentication Methods +# ============================================================================= +# This task configures the Keycloak client to support all OIDC authentication +# methods including: +# - Service Accounts (for Client Credentials flow) +# - Device Authorization (for Device Authorization flow) +# +# Prerequisites: +# - Keycloak must be deployed and accessible +# - The OIDC client (rhoso) must already exist +# ============================================================================= + +- name: Get Keycloak admin token + ansible.builtin.uri: + url: "{{ cifmw_federation_keycloak_url }}/auth/realms/master/protocol/openid-connect/token" + method: POST + body_format: form-urlencoded + body: + grant_type: password + client_id: admin-cli + username: "{{ cifmw_federation_keycloak_admin_username }}" + password: "{{ cifmw_federation_keycloak_admin_password }}" + validate_certs: "{{ cifmw_federation_keycloak_url_validate_certs }}" + status_code: 200 + register: federation_keycloak_admin_token_response + +- name: Set admin token fact + ansible.builtin.set_fact: + federation_keycloak_admin_token: "{{ federation_keycloak_admin_token_response.json.access_token }}" + +- name: Get current client configuration + ansible.builtin.uri: + url: "{{ cifmw_federation_keycloak_url }}/auth/admin/realms/{{ cifmw_federation_keycloak_realm }}/clients?clientId={{ cifmw_federation_keystone_OIDC_ClientID }}" + method: GET + headers: + Authorization: "Bearer {{ federation_keycloak_admin_token }}" + validate_certs: "{{ cifmw_federation_keycloak_url_validate_certs }}" + status_code: 200 + register: federation_keycloak_client_response + +- name: Extract client ID + ansible.builtin.set_fact: + federation_keycloak_client_uuid: "{{ federation_keycloak_client_response.json[0].id }}" + when: federation_keycloak_client_response.json | length > 0 + +- name: Display current client configuration + ansible.builtin.debug: + msg: + - "Client ID: {{ cifmw_federation_keystone_OIDC_ClientID }}" + - "Client UUID: {{ federation_keycloak_client_uuid }}" + - "Service Accounts Enabled: {{ federation_keycloak_client_response.json[0].serviceAccountsEnabled | default(false) }}" + - "Device Authorization: {{ federation_keycloak_client_response.json[0].attributes.get('oauth2.device.authorization.grant.enabled', 'false') | default('false') }}" + when: federation_keycloak_client_response.json | length > 0 + +- name: Update client to enable Service Accounts and Device Authorization + ansible.builtin.uri: + url: "{{ cifmw_federation_keycloak_url }}/auth/admin/realms/{{ cifmw_federation_keycloak_realm }}/clients/{{ federation_keycloak_client_uuid }}" + method: PUT + headers: + Authorization: "Bearer {{ federation_keycloak_admin_token }}" + Content-Type: application/json + body_format: json + body: + clientId: "{{ cifmw_federation_keystone_OIDC_ClientID }}" + serviceAccountsEnabled: true + directAccessGrantsEnabled: true + standardFlowEnabled: true + implicitFlowEnabled: true + publicClient: false + attributes: + oauth2.device.authorization.grant.enabled: "true" + validate_certs: "{{ cifmw_federation_keycloak_url_validate_certs }}" + status_code: 204 + when: federation_keycloak_client_response.json | length > 0 + register: federation_keycloak_client_update + +- name: Verify updated client configuration + ansible.builtin.uri: + url: "{{ cifmw_federation_keycloak_url }}/auth/admin/realms/{{ cifmw_federation_keycloak_realm }}/clients/{{ federation_keycloak_client_uuid }}" + method: GET + headers: + Authorization: "Bearer {{ federation_keycloak_admin_token }}" + validate_certs: "{{ cifmw_federation_keycloak_url_validate_certs }}" + status_code: 200 + register: federation_keycloak_client_updated + when: federation_keycloak_client_response.json | length > 0 + +- name: Display updated client configuration + ansible.builtin.debug: + msg: + - "Client updated successfully" + - "Service Accounts Enabled: {{ federation_keycloak_client_updated.json.serviceAccountsEnabled }}" + - "Direct Access Grants: {{ federation_keycloak_client_updated.json.directAccessGrantsEnabled }}" + - "Standard Flow: {{ federation_keycloak_client_updated.json.standardFlowEnabled }}" + - "Device Authorization: {{ federation_keycloak_client_updated.json.attributes.get('oauth2.device.authorization.grant.enabled', 'false') }}" + when: + - federation_keycloak_client_response.json | length > 0 + - federation_keycloak_client_updated is defined + diff --git a/roles/federation/tasks/run_openstack_auth_setup.yml b/roles/federation/tasks/run_openstack_auth_setup.yml index 55c2a30ce1..080eabdbf7 100644 --- a/roles/federation/tasks/run_openstack_auth_setup.yml +++ b/roles/federation/tasks/run_openstack_auth_setup.yml @@ -75,3 +75,63 @@ pod: openstackclient remote_path: "/home/cloud-admin/full-ca-list.crt" local_path: "{{ [ ansible_user_dir, 'ci-framework-data', 'tmp', 'full-ca-list.crt' ] | path_join }}" + +# ============================================================================= +# OIDC Authentication Method Scripts +# ============================================================================= +# These scripts support testing different OIDC authentication methods: +# - v3oidcclientcredentials: Client Credentials flow +# - v3oidcaccesstoken: Access Token Reuse flow +# - v3oidcauthcode: Authorization Code flow + +- name: Render OIDC client credentials script + ansible.builtin.template: + src: oidc-clientcredentials.sh.j2 + dest: "{{ [ansible_user_dir, 'ci-framework-data', 'tmp', 'oidc-clientcredentials.sh'] | path_join }}" + mode: '0755' + +- name: Copy OIDC client credentials script to pod + kubernetes.core.k8s_cp: + namespace: "{{ cifmw_federation_run_osp_cmd_namespace }}" + pod: openstackclient + remote_path: "/home/cloud-admin/oidc-clientcredentials.sh" + local_path: "{{ [ansible_user_dir, 'ci-framework-data', 'tmp', 'oidc-clientcredentials.sh'] | path_join }}" + +- name: Render OIDC access token script + ansible.builtin.template: + src: oidc-accesstoken.sh.j2 + dest: "{{ [ansible_user_dir, 'ci-framework-data', 'tmp', 'oidc-accesstoken.sh'] | path_join }}" + mode: '0755' + +- name: Copy OIDC access token script to pod + kubernetes.core.k8s_cp: + namespace: "{{ cifmw_federation_run_osp_cmd_namespace }}" + pod: openstackclient + remote_path: "/home/cloud-admin/oidc-accesstoken.sh" + local_path: "{{ [ansible_user_dir, 'ci-framework-data', 'tmp', 'oidc-accesstoken.sh'] | path_join }}" + +- name: Render OIDC authorization code script + ansible.builtin.template: + src: oidc-authcode.sh.j2 + dest: "{{ [ansible_user_dir, 'ci-framework-data', 'tmp', 'oidc-authcode.sh'] | path_join }}" + mode: '0755' + +- name: Copy OIDC authorization code script to pod + kubernetes.core.k8s_cp: + namespace: "{{ cifmw_federation_run_osp_cmd_namespace }}" + pod: openstackclient + remote_path: "/home/cloud-admin/oidc-authcode.sh" + local_path: "{{ [ansible_user_dir, 'ci-framework-data', 'tmp', 'oidc-authcode.sh'] | path_join }}" + +- name: Render Keycloak token helper script + ansible.builtin.template: + src: get-keycloak-token.sh.j2 + dest: "{{ [ansible_user_dir, 'ci-framework-data', 'tmp', 'get-keycloak-token.sh'] | path_join }}" + mode: '0755' + +- name: Copy Keycloak token helper script to pod + kubernetes.core.k8s_cp: + namespace: "{{ cifmw_federation_run_osp_cmd_namespace }}" + pod: openstackclient + remote_path: "/home/cloud-admin/get-keycloak-token.sh" + local_path: "{{ [ansible_user_dir, 'ci-framework-data', 'tmp', 'get-keycloak-token.sh'] | path_join }}" diff --git a/roles/federation/tasks/run_openstack_oidc_auth_tests.yml b/roles/federation/tasks/run_openstack_oidc_auth_tests.yml new file mode 100644 index 0000000000..f49e6ec884 --- /dev/null +++ b/roles/federation/tasks/run_openstack_oidc_auth_tests.yml @@ -0,0 +1,245 @@ +--- +# Copyright Red Hat, Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +# ============================================================================= +# OpenStack OIDC Authentication Methods Test Suite +# ============================================================================= +# This task file tests all supported OIDC authentication methods: +# - v3oidcpassword: Resource Owner Password Credentials flow +# - v3oidcclientcredentials: Client Credentials flow +# - v3oidcaccesstoken: Reuse existing access token +# - v3oidcauthcode: Authorization Code flow +# +# Note: v3oidcdeviceauthz (Device Authorization flow) requires keystoneauth1 +# with Python 3.10+ support and is not available in OSP18. +# ============================================================================= + +- name: Display OIDC authentication test suite header + ansible.builtin.debug: + msg: | + ======================================== + OpenStack OIDC Authentication Test Suite + ======================================== + Testing the following authentication methods: + 1. v3oidcpassword (Resource Owner Password Credentials) + 2. v3oidcclientcredentials (Client Credentials) + 3. v3oidcaccesstoken (Access Token Reuse) + 4. v3oidcauthcode (Authorization Code) + ======================================== + +# ============================================================================= +# TEST 1: v3oidcpassword - Resource Owner Password Credentials Flow +# ============================================================================= +- name: "TEST 1: v3oidcpassword - Resource Owner Password Credentials" + block: + - name: "[v3oidcpassword] Get token for test user 1" + vars: + _osp_cmd: "/home/cloud-admin/get-token.sh {{ cifmw_federation_keycloak_testuser1_username }}" + ansible.builtin.include_tasks: run_osp_cmd.yml + + - name: "[v3oidcpassword] Parse token response" + ansible.builtin.set_fact: + federation_oidcpassword_token: "{{ federation_run_ocp_cmd.stdout | from_json }}" + + - name: "[v3oidcpassword] Validate token" + ansible.builtin.assert: + that: + - federation_oidcpassword_token.id is defined + - federation_oidcpassword_token.id | length >= 180 + - federation_oidcpassword_token.user_id is defined + - federation_oidcpassword_token.expires is defined + fail_msg: "v3oidcpassword authentication failed" + success_msg: "v3oidcpassword authentication successful" + + - name: "[v3oidcpassword] Display token info" + ansible.builtin.debug: + msg: + - "Auth Type: v3oidcpassword" + - "User ID: {{ federation_oidcpassword_token.user_id }}" + - "Expires: {{ federation_oidcpassword_token.expires }}" + - "Token ID (truncated): {{ federation_oidcpassword_token.id[:50] }}..." + +# ============================================================================= +# TEST 2: v3oidcclientcredentials - Client Credentials Flow +# ============================================================================= +- name: "TEST 2: v3oidcclientcredentials - Client Credentials Flow" + block: + - name: "[v3oidcclientcredentials] Run token issue command" + vars: + _osp_cmd: >- + bash -c 'source /home/cloud-admin/oidc-clientcredentials.sh && + openstack token issue -f json' + ansible.builtin.include_tasks: run_osp_cmd.yml + + - name: "[v3oidcclientcredentials] Parse token response" + ansible.builtin.set_fact: + federation_oidcclientcreds_token: "{{ federation_run_ocp_cmd.stdout | from_json }}" + + - name: "[v3oidcclientcredentials] Validate token" + ansible.builtin.assert: + that: + - federation_oidcclientcreds_token.id is defined + - federation_oidcclientcreds_token.id | length >= 180 + - federation_oidcclientcreds_token.user_id is defined + - federation_oidcclientcreds_token.expires is defined + fail_msg: "v3oidcclientcredentials authentication failed" + success_msg: "v3oidcclientcredentials authentication successful" + + - name: "[v3oidcclientcredentials] Display token info" + ansible.builtin.debug: + msg: + - "Auth Type: v3oidcclientcredentials" + - "User ID: {{ federation_oidcclientcreds_token.user_id }}" + - "Expires: {{ federation_oidcclientcreds_token.expires }}" + - "Token ID (truncated): {{ federation_oidcclientcreds_token.id[:50] }}..." + +# ============================================================================= +# TEST 3: v3oidcaccesstoken - Access Token Reuse Flow +# ============================================================================= +- name: "TEST 3: v3oidcaccesstoken - Access Token Reuse Flow" + block: + - name: "[v3oidcaccesstoken] Get access token from Keycloak" + vars: + _osp_cmd: >- + /home/cloud-admin/get-keycloak-token.sh access_token + {{ cifmw_federation_keycloak_testuser1_username }} + {{ cifmw_federation_keycloak_testuser1_password }} + ansible.builtin.include_tasks: run_osp_cmd.yml + + - name: "[v3oidcaccesstoken] Store access token" + ansible.builtin.set_fact: + federation_keycloak_access_token: "{{ federation_run_ocp_cmd.stdout | trim }}" + + - name: "[v3oidcaccesstoken] Validate access token obtained" + ansible.builtin.assert: + that: + - federation_keycloak_access_token | length > 100 + fail_msg: "Failed to obtain access token from Keycloak" + success_msg: "Successfully obtained access token from Keycloak" + + - name: "[v3oidcaccesstoken] Run token issue with access token" + vars: + _osp_cmd: >- + bash -c 'source /home/cloud-admin/oidc-accesstoken.sh "{{ federation_keycloak_access_token }}" && + openstack token issue -f json' + ansible.builtin.include_tasks: run_osp_cmd.yml + + - name: "[v3oidcaccesstoken] Parse token response" + ansible.builtin.set_fact: + federation_oidcaccesstoken_token: "{{ federation_run_ocp_cmd.stdout | from_json }}" + + - name: "[v3oidcaccesstoken] Validate token" + ansible.builtin.assert: + that: + - federation_oidcaccesstoken_token.id is defined + - federation_oidcaccesstoken_token.id | length >= 180 + - federation_oidcaccesstoken_token.user_id is defined + - federation_oidcaccesstoken_token.expires is defined + fail_msg: "v3oidcaccesstoken authentication failed" + success_msg: "v3oidcaccesstoken authentication successful" + + - name: "[v3oidcaccesstoken] Display token info" + ansible.builtin.debug: + msg: + - "Auth Type: v3oidcaccesstoken" + - "User ID: {{ federation_oidcaccesstoken_token.user_id }}" + - "Expires: {{ federation_oidcaccesstoken_token.expires }}" + - "Token ID (truncated): {{ federation_oidcaccesstoken_token.id[:50] }}..." + +# ============================================================================= +# TEST 4: v3oidcauthcode - Authorization Code Flow +# ============================================================================= +- name: "TEST 4: v3oidcauthcode - Authorization Code Flow" + block: + - name: "[v3oidcauthcode] Get authorization code from Keycloak" + vars: + _osp_cmd: >- + /home/cloud-admin/get-keycloak-token.sh auth_code + {{ cifmw_federation_keycloak_testuser1_username }} + {{ cifmw_federation_keycloak_testuser1_password }} + ansible.builtin.include_tasks: run_osp_cmd.yml + + - name: "[v3oidcauthcode] Store authorization code" + ansible.builtin.set_fact: + federation_auth_code: "{{ federation_run_ocp_cmd.stdout | trim }}" + + - name: "[v3oidcauthcode] Validate authorization code obtained" + ansible.builtin.assert: + that: + - federation_auth_code | length > 10 + - "'ERROR' not in federation_auth_code" + fail_msg: "Failed to obtain authorization code from Keycloak" + success_msg: "Successfully obtained authorization code from Keycloak" + + - name: "[v3oidcauthcode] Run token issue with authorization code" + vars: + _osp_cmd: >- + bash -c 'source /home/cloud-admin/oidc-authcode.sh "{{ federation_auth_code }}" && + openstack token issue -f json' + ansible.builtin.include_tasks: run_osp_cmd.yml + + - name: "[v3oidcauthcode] Parse token response" + ansible.builtin.set_fact: + federation_oidcauthcode_token: "{{ federation_run_ocp_cmd.stdout | from_json }}" + + - name: "[v3oidcauthcode] Validate token" + ansible.builtin.assert: + that: + - federation_oidcauthcode_token.id is defined + - federation_oidcauthcode_token.id | length >= 180 + - federation_oidcauthcode_token.user_id is defined + - federation_oidcauthcode_token.expires is defined + fail_msg: "v3oidcauthcode authentication failed" + success_msg: "v3oidcauthcode authentication successful" + + - name: "[v3oidcauthcode] Display token info" + ansible.builtin.debug: + msg: + - "Auth Type: v3oidcauthcode" + - "User ID: {{ federation_oidcauthcode_token.user_id }}" + - "Expires: {{ federation_oidcauthcode_token.expires }}" + - "Token ID (truncated): {{ federation_oidcauthcode_token.id[:50] }}..." + +# ============================================================================= +# TEST SUMMARY +# ============================================================================= +- name: "Display OIDC authentication test summary" + ansible.builtin.debug: + msg: | + ======================================== + OIDC Authentication Test Summary + ======================================== + ✓ v3oidcpassword: PASSED + - User: {{ cifmw_federation_keycloak_testuser1_username }} + - Token User ID: {{ federation_oidcpassword_token.user_id }} + + ✓ v3oidcclientcredentials: PASSED + - Client: {{ cifmw_federation_keystone_OIDC_ClientID }} + - Token User ID: {{ federation_oidcclientcreds_token.user_id }} + + ✓ v3oidcaccesstoken: PASSED + - User: {{ cifmw_federation_keycloak_testuser1_username }} + - Token User ID: {{ federation_oidcaccesstoken_token.user_id }} + + ✓ v3oidcauthcode: PASSED + - User: {{ cifmw_federation_keycloak_testuser1_username }} + - Token User ID: {{ federation_oidcauthcode_token.user_id }} + + ---------------------------------------- + Note: v3oidcdeviceauthz requires Python 3.10+ + and is not available in OSP18. + ======================================== + diff --git a/roles/federation/templates/get-keycloak-token.sh.j2 b/roles/federation/templates/get-keycloak-token.sh.j2 new file mode 100644 index 0000000000..5aab458586 --- /dev/null +++ b/roles/federation/templates/get-keycloak-token.sh.j2 @@ -0,0 +1,107 @@ +#!/bin/bash +# Helper script to obtain tokens from Keycloak for OIDC authentication testing +# Usage: +# get-keycloak-token.sh access_token - Get access token +# get-keycloak-token.sh auth_code - Get authorization code +# get-keycloak-token.sh admin_token - Get Keycloak admin token + +set -e + +KEYCLOAK_URL="{{ cifmw_federation_keycloak_url }}" +REALM="{{ cifmw_federation_keycloak_realm }}" +CLIENT_ID="{{ cifmw_federation_keystone_OIDC_ClientID }}" +CLIENT_SECRET="{{ cifmw_federation_keystone_OIDC_ClientSecret }}" +REDIRECT_URI="{{ cifmw_federation_keystone_url }}/v3/redirect_uri" + +TOKEN_ENDPOINT="${KEYCLOAK_URL}/auth/realms/${REALM}/protocol/openid-connect/token" +AUTH_ENDPOINT="${KEYCLOAK_URL}/auth/realms/${REALM}/protocol/openid-connect/auth" +ADMIN_TOKEN_ENDPOINT="${KEYCLOAK_URL}/auth/realms/master/protocol/openid-connect/token" + +get_access_token() { + local username="$1" + local password="$2" + + curl -s -k -X POST "${TOKEN_ENDPOINT}" \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "grant_type=password" \ + -d "client_id=${CLIENT_ID}" \ + -d "client_secret=${CLIENT_SECRET}" \ + -d "username=${username}" \ + -d "password=${password}" \ + -d "scope=openid profile email" | python3 -c "import sys,json; print(json.load(sys.stdin).get('access_token',''))" +} + +get_auth_code() { + local username="$1" + local password="$2" + + # Step 1: Get login page and extract form action + local cookies_file=$(mktemp) + local login_page=$(curl -s -k -c "${cookies_file}" -b "${cookies_file}" \ + "${AUTH_ENDPOINT}?client_id=${CLIENT_ID}&redirect_uri=${REDIRECT_URI}&response_type=code&scope=openid%20profile%20email&state=test_state") + + local action_url=$(echo "${login_page}" | grep -oE 'action="[^"]+"' | head -1 | sed 's/action="//;s/"$//' | sed 's/&/\&/g') + + if [ -z "${action_url}" ]; then + echo "ERROR: Could not find login form action URL" >&2 + rm -f "${cookies_file}" + return 1 + fi + + # Step 2: Submit login and get redirect URL with auth code + local final_url=$(curl -s -k -c "${cookies_file}" -b "${cookies_file}" -L -w "%{url_effective}" \ + -X POST "${action_url}" \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "username=${username}" \ + -d "password=${password}" \ + -o /dev/null 2>&1) + + rm -f "${cookies_file}" + + # Extract authorization code from URL + local auth_code=$(echo "${final_url}" | grep -oE 'code=[^&]+' | sed 's/code=//') + + if [ -z "${auth_code}" ]; then + echo "ERROR: Could not extract authorization code" >&2 + return 1 + fi + + echo "${auth_code}" +} + +get_admin_token() { + curl -s -k -X POST "${ADMIN_TOKEN_ENDPOINT}" \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "grant_type=password" \ + -d "client_id=admin-cli" \ + -d "username={{ cifmw_federation_keycloak_admin_username }}" \ + -d "password={{ cifmw_federation_keycloak_admin_password }}" | python3 -c "import sys,json; print(json.load(sys.stdin).get('access_token',''))" +} + +case "$1" in + access_token) + if [ -z "$2" ] || [ -z "$3" ]; then + echo "Usage: $0 access_token " >&2 + exit 1 + fi + get_access_token "$2" "$3" + ;; + auth_code) + if [ -z "$2" ] || [ -z "$3" ]; then + echo "Usage: $0 auth_code " >&2 + exit 1 + fi + get_auth_code "$2" "$3" + ;; + admin_token) + get_admin_token + ;; + *) + echo "Usage: $0 {access_token|auth_code|admin_token} [args...]" >&2 + echo " access_token - Get user access token from Keycloak" + echo " auth_code - Get authorization code from Keycloak" + echo " admin_token - Get Keycloak admin token" + exit 1 + ;; +esac + diff --git a/roles/federation/templates/oidc-accesstoken.sh.j2 b/roles/federation/templates/oidc-accesstoken.sh.j2 new file mode 100644 index 0000000000..be5a5f5d57 --- /dev/null +++ b/roles/federation/templates/oidc-accesstoken.sh.j2 @@ -0,0 +1,22 @@ +#!/bin/bash +# OIDC Access Token flow configuration +# This script configures authentication using a pre-obtained access token +# The access token is passed as an argument: source oidc-accesstoken.sh + +ACCESS_TOKEN="$1" + +if [ -z "$ACCESS_TOKEN" ]; then + echo "Usage: source oidc-accesstoken.sh " + echo "Error: Access token is required" + return 1 +fi + +unset OS_CLOUD +export OS_CACERT=/home/cloud-admin/full-ca-list.crt +export OS_AUTH_URL="{{ cifmw_federation_keystone_url }}/v3" +export OS_IDENTITY_API_VERSION=3 +export OS_AUTH_TYPE=v3oidcaccesstoken +export OS_IDENTITY_PROVIDER="{{ cifmw_federation_IdpName }}" +export OS_PROTOCOL=openid +export OS_ACCESS_TOKEN="$ACCESS_TOKEN" + diff --git a/roles/federation/templates/oidc-authcode.sh.j2 b/roles/federation/templates/oidc-authcode.sh.j2 new file mode 100644 index 0000000000..7d2e2b9d39 --- /dev/null +++ b/roles/federation/templates/oidc-authcode.sh.j2 @@ -0,0 +1,28 @@ +#!/bin/bash +# OIDC Authorization Code flow configuration +# This script configures authentication using OAuth 2.0 Authorization Code grant type +# The authorization code is passed as an argument: source oidc-authcode.sh + +AUTH_CODE="$1" + +if [ -z "$AUTH_CODE" ]; then + echo "Usage: source oidc-authcode.sh " + echo "Error: Authorization code is required" + return 1 +fi + +unset OS_CLOUD +export OS_CACERT=/home/cloud-admin/full-ca-list.crt +export OS_AUTH_URL="{{ cifmw_federation_keystone_url }}/v3" +export OS_IDENTITY_API_VERSION=3 +export OS_AUTH_TYPE=v3oidcauthcode +export OS_IDENTITY_PROVIDER="{{ cifmw_federation_IdpName }}" +export OS_CLIENT_ID="{{ cifmw_federation_keystone_OIDC_ClientID }}" +export OS_CLIENT_SECRET="{{ cifmw_federation_keystone_OIDC_ClientSecret }}" +export OS_OPENID_SCOPE="openid profile email" +export OS_PROTOCOL=openid +export OS_ACCESS_TOKEN_TYPE=access_token +export OS_DISCOVERY_ENDPOINT="{{ cifmw_federation_keycloak_url }}/auth/realms/{{ cifmw_federation_keycloak_realm }}/.well-known/openid-configuration" +export OS_REDIRECT_URI="{{ cifmw_federation_keystone_url }}/v3/redirect_uri" +export OS_CODE="$AUTH_CODE" + diff --git a/roles/federation/templates/oidc-clientcredentials.sh.j2 b/roles/federation/templates/oidc-clientcredentials.sh.j2 new file mode 100644 index 0000000000..0e6de8e4ce --- /dev/null +++ b/roles/federation/templates/oidc-clientcredentials.sh.j2 @@ -0,0 +1,18 @@ +#!/bin/bash +# OIDC Client Credentials flow configuration +# This script configures authentication using OAuth 2.0 Client Credentials grant type +# The client authenticates directly with Keycloak using client_id and client_secret + +unset OS_CLOUD +export OS_CACERT=/home/cloud-admin/full-ca-list.crt +export OS_AUTH_URL="{{ cifmw_federation_keystone_url }}/v3" +export OS_IDENTITY_API_VERSION=3 +export OS_AUTH_TYPE=v3oidcclientcredentials +export OS_IDENTITY_PROVIDER="{{ cifmw_federation_IdpName }}" +export OS_CLIENT_ID="{{ cifmw_federation_keystone_OIDC_ClientID }}" +export OS_CLIENT_SECRET="{{ cifmw_federation_keystone_OIDC_ClientSecret }}" +export OS_OPENID_SCOPE="openid profile email" +export OS_PROTOCOL=openid +export OS_ACCESS_TOKEN_TYPE=access_token +export OS_DISCOVERY_ENDPOINT="{{ cifmw_federation_keycloak_url }}/auth/realms/{{ cifmw_federation_keycloak_realm }}/.well-known/openid-configuration" +