Skip to content
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
4c5ce49
Update the openid-connect.js to current latest version
javorszky Sep 19, 2025
d4f3405
Update pytest keycloak from 25.0.2 to 26.3
javorszky Sep 26, 2025
f3a222e
Remove unused variable from test_oidc.py
javorszky Sep 26, 2025
d024309
Add front channel logout configs to oidc conf
javorszky Sep 26, 2025
23dd484
Move the idp sid keyval into virtualserver template
javorszky Sep 30, 2025
2fe9b3e
Update keycloak image version and env vars
javorszky Oct 2, 2025
16d4925
Fix virtualserver template and snaps
javorszky Oct 2, 2025
b88074e
Keycloak 26.4 uses button type submit
javorszky Oct 3, 2025
958a0e8
Add fclo example
javorszky Oct 13, 2025
ffcbeea
Turn on oidc debug flag if main error level is debug
javorszky Oct 15, 2025
8fdd238
Add logic to enable oidc debug variable
javorszky Oct 15, 2025
af5e2ce
Add oidc fclo test files
javorszky Oct 21, 2025
d05e340
Fix the create keycloak and test setup functionality
javorszky Oct 21, 2025
14e28d6
Add fclo page tests
javorszky Oct 21, 2025
588fe40
Finish pytest for oidc fclo
javorszky Oct 22, 2025
ee28fee
Add configmap and snap tests
javorszky Oct 22, 2025
6bb25ea
Rewrite readme for examples/oidc-fclo
javorszky Oct 22, 2025
eb73a3f
Update python:3.14-bookworm Docker digest to 0cc5dcf (main) (#8450)
renovate[bot] Oct 22, 2025
b6e7281
Use renovate to monitor test data yaml files (#8445)
pdabelf5 Oct 23, 2025
ecf361d
Factor out creating oidc policy in fclo pytest
javorszky Oct 23, 2025
e8bee1a
Merge branch 'main' into feat/7781-oidc-front-channel-logout
javorszky Oct 23, 2025
3518e69
Reword OIDC Policies in fclo example readme
javorszky Oct 23, 2025
4716226
Add more detail to fclo readme
javorszky Oct 23, 2025
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
463 changes: 0 additions & 463 deletions charts/tests/__snapshots__/helmunit_test.snap

Large diffs are not rendered by default.

151 changes: 151 additions & 0 deletions examples/custom-resources/oidc-fclo/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
# OIDC with Front Channel Logout

In this example, we deploy a web application, configure load balancing for it via a VirtualServer, and protect the
application using an OpenID Connect policy and [Keycloak](https://www.keycloak.org/), and ensure behaviour is consistent across multiple replicas by enabling [Zone Synchronization](https://docs.nginx.com/nginx/admin-guide/high-availability/zone_sync/).

**Note**: The KeyCloak container does not support IPv6 environments.

**Note**: This example assumes that your default namespace is set to `default`. You can check this with

```shell
kubectl config view --minify | grep namespace
```

If it's not empty, and anything other than `default`, you can set to `default` with the following command:

```shell
kubectl config set-context --namespace default --current
```

## Prerequisites

1. Follow the [installation](https://docs.nginx.com/nginx-ingress-controller/installation/installation-with-manifests/)
instructions to deploy NGINX Ingress Controller. This example requires that the HTTPS port of the Ingress
Controller is `443`.
2. Save the public IP address of the Ingress Controller into `/etc/hosts` of your machine:

```text
...

XXX.YYY.ZZZ.III webapp.example.com
XXX.YYY.ZZZ.III keycloak.example.com
```

Here `webapp.example.com` is the domain for the web application and `keycloak.example.com` is the domain for
Keycloak.

## Step 1 - Deploy a TLS Secret

Create a secret with the TLS certificate and key that will be used for TLS termination of the web application and
Keycloak:

```shell
kubectl apply -f tls-secret.yaml
```

## Step 2 - Deploy a Web Application

Create the application deployment and service:

```shell
kubectl apply -f webapp.yaml
```

## Step 3 - Deploy Keycloak

1. Create the Keycloak deployment and service:

```shell
kubectl apply -f keycloak.yaml
```

2. Create a VirtualServer resource for Keycloak:

```shell
kubectl apply -f virtual-server-idp.yaml
```

## Step 4 - Configure Keycloak

To set up Keycloak:

1. Follow the steps in the "Configuring Keycloak" [section of the documentation](https://docs.nginx.com/nginx/deployment-guides/single-sign-on/keycloak/#configuring-keycloak):
1. To connect to Keycloak, use `https://keycloak.example.com`.
2. Make sure to save the client secret for NGINX-Plus client to the `SECRET` shell variable:

```shell
SECRET=value
```

2. Alternatively, [execute the commands](./keycloak_setup.md).

## Step 5 - Deploy the Client Secret

**Note**: If you're using PKCE, skip this step. PKCE clients do not have client secrets. Applying this will result
in a broken deployment.

1. Encode the secret, obtained in the previous step:

```shell
echo -n $SECRET | base64
```

2. Edit `client-secret.yaml`, replacing `<insert-secret-here>` with the encoded secret.

3. Create a secret with the name `oidc-secret` that will be used by the OIDC policy:

```shell
kubectl apply -f client-secret.yaml
```

## Step 6 - Configure Zone Synchronization and Resolver

In this step we configure:

- [Zone Synchronization](https://docs.nginx.com/nginx/admin-guide/high-availability/zone_sync/). For the OIDC feature to
work when you have two or more replicas of the Ingress Controller, it is necessary to enable zone synchronization
among the replicas. This is to ensure that each replica has access to the required session information when authenticating via IDP such as Keycloak.
- The resolver can resolve the host names.

Steps:

1. Apply the ConfigMap `nginx-config.yaml`, which contains `zone-sync` configuration parameter that enable zone synchronization and the resolver using the kube-dns service.

```shell
kubectl apply -f nginx-config.yaml
```

## Step 7 - Deploy the OIDC Policy

Create a policy with the name `oidc-policy` that references the secret from the previous step:

```shell
kubectl apply -f oidc.yaml
```

## Step 8 - Configure Load Balancing

Create a VirtualServer resource for the web application:

```shell
kubectl apply -f virtual-server.yaml
```

Note that the VirtualServer references the policy `oidc-policy` created in Step 6.

## Step 9 - Test the Configuration

1. Open a web browser and navigate to the URL of the web application: `https://webapp.example.com`. You will be
redirected to Keycloak.
2. Log in with the username and password for the user you created in Keycloak, `nginx-user` and `test`.
![keycloak](./keycloak.webp)
3. Once logged in, you will be redirected to the web application and get a response from it. Notice the field `User ID`
in the response, this will match the ID for your user in Keycloak. ![webapp](./webapp.webp)

## Step 10 - Log Out

1. To log out, navigate to `https://webapp.example.com/logout`. Your session will be terminated, and you will be
redirected to the default post logout URI `https://webapp.example.com/_logout`.
![logout](./logout.webp)
2. To confirm that you have been logged out, navigate to `https://webapp.example.com`. You will be redirected to
Keycloak to log in again.
7 changes: 7 additions & 0 deletions examples/custom-resources/oidc-fclo/client-secret-one.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
apiVersion: v1
kind: Secret
metadata:
name: fclo-secret-one
type: nginx.org/oidc
data:
client-secret: <client secret here>
7 changes: 7 additions & 0 deletions examples/custom-resources/oidc-fclo/client-secret-two.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
apiVersion: v1
kind: Secret
metadata:
name: fclo-secret-two
type: nginx.org/oidc
data:
client-secret: <client secret here>
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
50 changes: 50 additions & 0 deletions examples/custom-resources/oidc-fclo/keycloak.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
apiVersion: v1
kind: Service
metadata:
name: keycloak
labels:
app: keycloak
spec:
ports:
- name: http
port: 8080
targetPort: 8080
selector:
app: keycloak
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: keycloak
labels:
app: keycloak
spec:
replicas: 1
selector:
matchLabels:
app: keycloak
template:
metadata:
labels:
app: keycloak
spec:
containers:
- name: keycloak
image: quay.io/keycloak/keycloak:26.4
args: ["start-dev"]
env:
- name: KC_BOOTSTRAP_ADMIN_USERNAME
value: "admin"
- name: KC_BOOTSTRAP_ADMIN_PASSWORD
value: "admin"
- name: KC_PROXY_HEADERS
value: "xforwarded"
ports:
- name: http
containerPort: 8080
- name: https
containerPort: 8443
readinessProbe:
httpGet:
path: /realms/master
port: 8080
79 changes: 79 additions & 0 deletions examples/custom-resources/oidc-fclo/keycloak_setup.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# Keycloak Setup

This guide will help you configure KeyCloak using Keycloak's API:

- Create a `client` with the name `nginx-plus`.
- Add a user `nginx-user` with the password `test`.

**Notes**:

- This guide has been tested with keycloak 19.0.2 and later. If you modify `keycloak.yaml` to use an older version,
Keycloak may not start correctly or the commands in this guide may not work as expected. The Keycloak OpenID
endpoints `oidc.yaml` might also be different in older versions of Keycloak.
- if you changed the admin username and password for Keycloak in `keycloak.yaml`, modify the commands accordingly.
- The instructions use [`jq`](https://stedolan.github.io/jq/).

Steps:

1. Save the address of Keycloak into a shell variable:

```shell
KEYCLOAK_ADDRESS=keycloak.example.com
```

2. Retrieve the access token and store it into a shell variable:

```shell
TOKEN=`curl -sS -k --data "username=admin&password=admin&grant_type=password&client_id=admin-cli" "https://${KEYCLOAK_ADDRESS}/realms/master/protocol/openid-connect/token" | jq -r .access_token`
```

Ensure the request was successful and the token is stored in the shell variable by running:

```shell
echo $TOKEN
```

***Note***: The access token lifespan is very short. If it expires between commands, retrieve it again with the
command above.

3. Create the user `nginx-user`:

```shell
curl -sS -k -X POST -d '{ "username": "nginx-user", "enabled": true, "credentials":[{"type": "password", "value": "test", "temporary": false}]}' -H "Content-Type:application/json" -H "Authorization: bearer ${TOKEN}" https://${KEYCLOAK_ADDRESS}/admin/realms/master/users
```

4. Create the client `nginx-plus`:

- If you are not using PKCE, use the following command to create an OIDC client that does not use PKCE:

```shell
SECRET=`curl -sS -k -X POST -d '{ "clientId": "nginx-plus", "redirectUris": ["https://webapp.example.com:443/_codexch"], "attributes": {"post.logout.redirect.uris": "https://webapp.example.com:443/*"}}' -H "Content-Type:application/json" -H "Authorization: bearer ${TOKEN}" https://${KEYCLOAK_ADDRESS}/realms/master/clients-registrations/default | jq -r .secret`
```

If everything went well, you should have the secret stored in $SECRET. To double-check, run:

```shell
echo $SECRET
```

- Or if you are using PKCE with OIDC, use the following command to create the client:

```shell
curl -sS -k -H "Content-Type: application/json" -H "Authorization: Bearer ${TOKEN}" \
--data '{
"clientId": "nginx-plus",
"enabled": true,
"standardFlowEnabled": true,
"directAccessGrantsEnabled": false,
"publicClient": true,
"redirectUris": [
"https://webapp.example.com:443/_codexch"
],
"attributes": {
"pkce.code.challenge.method":"S256",
"post.logout.redirect.uris": "https://webapp.example.com:443/*"
},
"protocol": "openid-connect"
}' \
https://${KEYCLOAK_ADDRESS}/admin/realms/master/clients
```
Binary file added examples/custom-resources/oidc-fclo/logout.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 9 additions & 0 deletions examples/custom-resources/oidc-fclo/nginx-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
kind: ConfigMap
apiVersion: v1
metadata:
name: nginx-config
namespace: nginx-ingress
data:
zone-sync: "true"
resolver-addresses: kube-dns.kube-system.svc.cluster.local
resolver-valid: 5s
14 changes: 14 additions & 0 deletions examples/custom-resources/oidc-fclo/oidc-one.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
apiVersion: k8s.nginx.org/v1
kind: Policy
metadata:
name: oidc-one-policy
spec:
oidc:
clientID: fclo-one
clientSecret: fclo-secret-one
authEndpoint: https://keycloak.example.com/realms/master/protocol/openid-connect/auth
tokenEndpoint: http://keycloak.nginx-ingress.svc.cluster.local:8080/realms/master/protocol/openid-connect/token
jwksURI: http://keycloak.nginx-ingress.svc.cluster.local:8080/realms/master/protocol/openid-connect/certs
endSessionEndpoint: https://keycloak.example.com/realms/master/protocol/openid-connect/logout
scope: openid+profile+email
accessTokenEnable: true
14 changes: 14 additions & 0 deletions examples/custom-resources/oidc-fclo/oidc-two.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
apiVersion: k8s.nginx.org/v1
kind: Policy
metadata:
name: oidc-two-policy
spec:
oidc:
clientID: fclo-two
clientSecret: fclo-secret-two
authEndpoint: https://keycloak.example.com/realms/master/protocol/openid-connect/auth
tokenEndpoint: http://keycloak.nginx-ingress.svc.cluster.local:8080/realms/master/protocol/openid-connect/token
jwksURI: http://keycloak.nginx-ingress.svc.cluster.local:8080/realms/master/protocol/openid-connect/certs
endSessionEndpoint: https://keycloak.example.com/realms/master/protocol/openid-connect/logout
scope: openid+profile+email
accessTokenEnable: true
8 changes: 8 additions & 0 deletions examples/custom-resources/oidc-fclo/tls-secret.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
apiVersion: v1
kind: Secret
metadata:
name: tls-secret
type: kubernetes.io/tls
data:
tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUZyRENDQTVTZ0F3SUJBZ0lVTE1iL3hUWmVQNFZlQjRYMDM5OTlCc2FubzA0d0RRWUpLb1pJaHZjTkFRRUwKQlFBd1dURUxNQWtHQTFVRUJoTUNTVVV4RFRBTEJnTlZCQWdNQkVOdmNtc3hEakFNQmdOVkJBb01CVTVIU1U1WQpNUTR3REFZRFZRUUxEQVZPUjBsT1dERWJNQmtHQTFVRUF3d1NkMlZpWVhCd0xtVjRZVzF3YkdVdVkyOXRNQjRYCkRUSTBNVEF6TURFek5EYzFPVm9YRFRNME1UQXlPREV6TkRjMU9Wb3dXVEVMTUFrR0ExVUVCaE1DU1VVeERUQUwKQmdOVkJBZ01CRU52Y21zeERqQU1CZ05WQkFvTUJVNUhTVTVZTVE0d0RBWURWUVFMREFWT1IwbE9XREViTUJrRwpBMVVFQXd3U2QyVmlZWEJ3TG1WNFlXMXdiR1V1WTI5dE1JSUNJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBZzhBCk1JSUNDZ0tDQWdFQXlVaEFtaVpOY1FOWU1sR21LenhEWmJZWlVQdll2U3ZweXBaYzgwalhqMjExZGZRNTcySHkKcTlTR1d1NnNKM2pwMVB0TU5Dd2M0Yko0Rm5qcS82OFZCZlJHVkNuQzRsNVJHUG14SzQycjdNWHMyYldvM1ZvQQpEZ3VGYnlpV2xhRmxDZTNVSmM4aHJpblJ1SHFkRXM5SXJaKzlzQkx0eVBuRzAxNjIydzM0YXVzblc3aFpWeUNNCmFaWnZMb1VEYVFmRWNhQ2VZcUJpcTAzYnR4WmFGOERKeXpTc1A0cTJBRlR5a2kxb29FVHoxSWdIL3g2WkRra0sKclhqb0hRS2xndEJuaU9yRWM2bk9JMzlCTkxFNi9HOTliQjZ4VjExa0VXZ2NHWk5nRXNxTEl1MFE0OTZNUzl3ZQovT24zWFgwdnF3WG1weDJSajRtdTVkcC8velJIc1NjWTgxMldWZGJvTzVGL3hNK2k2ZTR1Qis5bnBydGpZSXZaCmpMOGh2RDhQVVVZa0k3ZDVqR2ZBQlFXclRFUGhCZ0VtaHFkTCtrQjV5anQvd21acENINDFYUjAxdHlCWUQzVzkKc2dIaDVtWjdRQXlYbjRxdW5GMzdoOUo3cUh0UmtuNy9EMEFGYzFXaGY3bHlsTFJOUm1ub0ZLTnNoMXVwa01WaApTZldVNkFXNFVMNGZ5UGVpdDFzdDN5aC96aTgvVGtCVUR4NVJPbWxIN05XVHJBTW55Yk10RGVQRno3ZU85L1RBCmQ4VnZrRjd2T3QwWFNKK0d1OWszMHcrckVleXBQbTFEcjZ2R0lqT3JmN2x4bEhVTjh6d0VYeGlMSHZQdjhyMksKNFBpK2tHVy9reFRjRS81L0lpQm4wNEtybTZjRk9pU3Q1WmNMM29OMWpacnhBb2YyN1NDYi9aMENBd0VBQWFOcwpNR293SFFZRFZSME9CQllFRkNlMWkrMGVFRjNFMEYxVkt2MThjRVNEWCtpOU1COEdBMVVkSXdRWU1CYUFGQ2UxCmkrMGVFRjNFMEYxVkt2MThjRVNEWCtpOU1CMEdBMVVkRVFRV01CU0NFbmRsWW1Gd2NDNWxlR0Z0Y0d4bExtTnYKYlRBSkJnTlZIUk1FQWpBQU1BMEdDU3FHU0liM0RRRUJDd1VBQTRJQ0FRQ0hXV3gvNVk4RlE5czU2Y25RNVg5VwpXN1N1bTlmTVQxNTBnUmlTcHlDcVh4WFd4d2JNWERsRUdVcTdaL3lFNXNzZkF2c3ZZdTlVc05IM3pTUFpVb2RNClFZdnJzVmpFTHAySGhHWnV1MlJuaVg3NDRKUm5ucXVDays4NitmcnEva3Y2RHlkUTRNT2sxaG1jR2dsK0ZBVWQKWGpvUmdvaXpzeTNUWkFIK2NHV3MrUEdvSFJRaFoyT1ZJSlAvOUtBM29XZFh3aFU5T2ROWk15RG0zRWk4cXFLMAp4TklqVU9nTG8zdm9WS3hpV1FUM0d2VFQ5Q0RoQlJSK1VseWJMQXI2TlQxeGsvVFd1OFZYNDIvWXNvVmE0cTFhCm1MRkhGcWlDbytsSlJ0UERweUoyempab3lzNHFaa1o1ZGZVcFNGUzNjd1B5bjY4REVlMjNPRW10MW0xNDR1UVoKYTlQY2dyZnhjNnZCUGVUMnozUzYxbHZuZ2hhWXBYUnltK3hEOUhUSzhqdUFDL0ZJOXZWQlRmaHRxT2o2TkYyawp3d3NQNUt0NW03T1pjMk9QU1I4OGFxY2p3cnE4UUpaNUw5cHpsOTlrSGE0UDhxR1pnQ0pNYi9ld0kwZTRrR2ZQCjQ2TVNUM2wzNFdaWHZ0alB5T0gyOWpScXZya1hJMWliSHZDQXFjLzBkZUtVcW5qQ1dMVVh1RmVyWG5Ud1lOZDAKSUovc2pSVExzaGRKclptbjA4Kzc2WUY1aEtLZS8vcERVL2d2WWdhSEVlV3dFNmx4WWppc0FFL0h0eXRyR2ZKcAorY2JSVUJVZUZoYnlLdm52WDNIVjZ2elRPZHlRcS8rUC9lVTNwYmFudm9ydCtiTzRYQzNDUWZJeXdJbWZjUFd2CitTZXNrVXl2KzJXNm8ySUgxN0pzdGc9PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==
tls.key: LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUpRd0lCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQ1Mwd2dna3BBZ0VBQW9JQ0FRREpTRUNhSmsxeEExZ3kKVWFZclBFTmx0aGxRKzlpOUsrbktsbHp6U05lUGJYVjE5RG52WWZLcjFJWmE3cXduZU9uVSswdzBMQnpoc25nVwplT3IvcnhVRjlFWlVLY0xpWGxFWStiRXJqYXZzeGV6WnRhamRXZ0FPQzRWdktKYVZvV1VKN2RRbHp5R3VLZEc0CmVwMFN6MGl0bjcyd0V1M0krY2JUWHJiYkRmaHE2eWRidUZsWElJeHBsbTh1aFFOcEI4UnhvSjVpb0dLclRkdTMKRmxvWHdNbkxOS3cvaXJZQVZQS1NMV2lnUlBQVWlBZi9IcGtPU1FxdGVPZ2RBcVdDMEdlSTZzUnpxYzRqZjBFMApzVHI4YjMxc0hyRlhYV1FSYUJ3WmsyQVN5b3NpN1JEajNveEwzQjc4NmZkZGZTK3JCZWFuSFpHUGlhN2wybi8vCk5FZXhKeGp6WFpaVjF1ZzdrWC9FejZMcDdpNEg3MmVtdTJOZ2k5bU12eUc4UHc5UlJpUWp0M21NWjhBRkJhdE0KUStFR0FTYUdwMHY2UUhuS08zL0NabWtJZmpWZEhUVzNJRmdQZGIyeUFlSG1abnRBREplZmlxNmNYZnVIMG51bwplMUdTZnY4UFFBVnpWYUYvdVhLVXRFMUdhZWdVbzJ5SFc2bVF4V0ZKOVpUb0JiaFF2aC9JOTZLM1d5M2ZLSC9PCkx6OU9RRlFQSGxFNmFVZnMxWk9zQXlmSnN5ME40OFhQdDQ3MzlNQjN4VytRWHU4NjNSZEluNGE3MlRmVEQ2c1IKN0trK2JVT3ZxOFlpTTZ0L3VYR1VkUTN6UEFSZkdJc2U4Ky95dllyZytMNlFaYitURk53VC9uOGlJR2ZUZ3F1Ygpwd1U2SkszbGx3dmVnM1dObXZFQ2gvYnRJSnY5blFJREFRQUJBb0lDQUFqbWl3VEdBTktvaFRQa2JHYXBDWW5yCjNYNjVSRUpKT05OZWhzbXpST1R3d0NyeEc4YThIQkVCR3RmV2lnSk0xSG80aW93Y0QwTGpzMis2OVJsTlVxNnEKdUpsc0oxUC9PN0xSQjhhWFF5ejdLNWdNOG1TbllDMCswUzJ6SzhWK0Y0dXkycGk1YWhIYmc0eVd6MjlQZnpVKwpSUk1PSXptcDRlTGk0MDhZZFEyMVRFNC8vcU5kcXhmWU1SNXJmMVVicE5JcGVoZCtaQjZUR000bHpPSVVBbGhjCkFlbmxabHJwWnJpVURYWlkxamRsdElUUmc0OGdKN3E0Qi91UWJHVTJkZjJWOUEzeFNrNURpRXllTWErTDlvM2IKWlpncFp3MWwveWdhWGpzMmZhU1R1eFY0ZDllNjZodmc5TEZMb2RuOEx0RGcrOWpQQzg3YU5LTENXeEU0VzJBRAplMnhBUWR2TWVSU3REVnMyV29XSGxZUWllQ0Q3REZoYmZHc29vaktULzhocUp2VEhvRzJmckVYYk1CT2RoQnI4Cm5MdkVXWEpNUXNMQ0hVcVBET1pRQk5BalVVRzl4SVZqb2dkVmdsUmtTekNBM1U4aUU4bG05NEh3Z0VXQjEreGMKYmxMVGhNQXlmbTVuY2lBVlhTc2VadUZTR3RreG15ZElrVmU1ay9VV1VxUXczNTFUcmVMMU10RTl5eXdsV0w4cgpJdWJ5dWFFWmxSWFNwaHk2N0FiYTlqazY2K3hNcUNwb3pvQXgrSDFnV25PaFdTc3FkQ2VUeE9QL084b1ZPa2IvCnR5czloMkZIZ3JCeVpHV254WHFjQnc1WXljYWlSNS9NZ1JwQVpMUk1xbDI3dTFETnArU2hWVmQ2aXA2enhVNXcKTmg2YVFVdGZBR1JYUGY0U2EvUEJBb0lCQVFEeUVKMk5XWk14d202S29xY25iN3ZWcnJHWXBaL2huRUpySFJCagpLcDVWVFdNZXN4OHlTcWtsYnV3TUhQbE83bFo0Q2ZtWllmU3lJTU5GWHlWM1R5bVVocFMxUTE0VjRsQ1o5bGZQCjhhTTJrZnI3ZCs3RlF4WEszTkYvbEJrZlVPNUFqR3crWDhsWFFFVW1hTmlxWFRqVFUvcmZiTktYLzRyQ05YQ3kKdnltejdNU1cwMS9WWnhaWnVpTkdOLzRJVXN4TFFNQy9qQXIzdGdrMzNEN0xSTkx0QWRETXgwY1NTNmY1SE1xQgpUck51dER6bWppdk1vZlZMa01RSGplVmh6WE9YRFFXK0VHVmNhckpuV0FXSWpKMTZBa1BTTXQxY3ZMbitJMVlNCnVTcmsrTVhtdkhSeUVjM1Bwdno3K1hFOHZZeGFoZ3o4VjlIeG04dUtOT2RlcHZzRkFvSUJBUURVM3B3NWVHUlQKUzNVcytIYStkcFAvVGtXR0FpeUNNYTZ4bGpHQ0JmdTJnVFN0VXljeDBYalhhVXJqVW1DbTcrSmloSk15TkdYbgpud29CYU9Dek5DdXQzT0VKbkVqZXIzYVgyYUNtU295RzZNWk1uOHlSa29HdjZQWnVOTHBoSk5YaFlMWmRidmRLCkhRZlY3YWhGdENoWnVZWGNnbW9LWHFvemZvam8vMGIrTlo4djQzY0lhR0pxK0svNjFnYmFkenZFa2lWc00xUSsKZldhL28rOHFSUUR0ZTdnbGdxNHU2alNYeFZoMmtOemQ0aE5XVVFmZUFPMS9LS0t2OG11QmNxNWpybTBqT0ovWgpsYU9YbmtBRUczL0RmU21XcjRmYmtoT0lrb0VQekRoZE8vVy9WTDErN1EyS2ovR0owcHYvRlJRRjRaM244VDZNCjZNdWJKUVV3NSt1NUFvSUJBQWFhcFhIQnk5NURxN2hrajZMbnpYd2E5QVZ5SDFhTWFOTjdTNE1wR29EQlI3OEMKckFzM05qNHJOSTF3RE8wMlcyMlMrQmhUTDlYY1J2ZVJqUGdnVk1ZVWxlSU1JSGtBWDZxVHFmbW1ZZ21QR2dYYQpVODFWOHpaQnFBV1BDTkJ0Nk5JaUFxSUJBd0U2WTZpVW03U3FMbTYxajlhZ3BXNDRMcFQxMkVsSUpkOGV5bzVDCjNnNTRiWWV0S0dFMkRkdzBSaGFYZ2FxNEsyUnV0dm1yTEp0bkdVb1dEcGhIcDR5OE82ejBPQ0ltLzRZNXJKK3QKcVV3Lzd1MU0yY3hLOXNNZ1U5TC9LL1R2aFpScjVNb2xBS0dsRkhiTHNRWC9GVUwrY3lTWDJqVW1xQ0R2R0pjZQo5UjVYbGdIZ1VHNmZjNU53cUcrZjBLTGgwbnlBLzZDWnFPWlFML0VDZ2dFQkFNWGRDYyt0cFd0N0p6YWUyUmtlCjlXQUpiRHd0Qnh3WmZDMGIwM2J0Z3RSWWN4TnN5SERaS1g3cEl5LzdvVlZxZ3I1YVJzd1N3bW95ZlVWa0xBREcKekpiMlNjTDZIdzNHZ1BDUzNHM1Z1NXVuQUxPMmtacjZXRXVmdW5najBONTlNOFVqZFQrUjVwQmdQYWxQRit0NgprMHNiVkY3c1pnNnZnWHNOOGNySmhqN0NydTMvZStRM3lzdHR6MzNUdFZrYUhWY1JGWEhtb0RiWnIwa1E0ejBpCkdNT21EVHZvcFdsOFQxaUhtanZUV1VseFc5SU96Y1pBaklGMnp3bkd4c0R6VFQvZ29SZHRDY0JoQkVmcFU4MjkKbGR1ckdwNHpHSkF5enE5U3BsNTkwQ0p4bW5LM0hOQy9IYWdmTmorS29XL1FNdVZvbXJNK25ZcXkxSmFvS1pRVQo3eGtDZ2dFQkFNbTRZamZKL0RlRFlrQVhiYTQ4QnZuSVo4WlFWSm1GWE9zWFU5Q01uOUg0YW4vT1Y0Q0RLLy85ClZOWGVvUW55N3hlUlJBcFR0cGFWOWVwVDVCc2VOdFMyaG1IOFFtc3Fvd3dGSjMvTmIvZHc0dHZiVFJUSzVlWmMKZ3NaYlZhcVBKVlR0ZVUzUlAwYzUyMDhFRnRrWXY2QytDdllJUE43dlJiZUVzNmFBelQ4UXdTWFB3VCtTMDFUNQoxRGNTc2dFMmpZb3V0OXhHUzFiWTZFemMvK3FnSDdTN2RscXZVQ1lzRkUvbGx0SjQzT0hnNGxMekRIaWpJZkRJCjJsNGdCWDdaZTEzbWZzV05VeEdCU2hySXFKOTV3QkE0UGFTRElSZkNaR0k1T1lBR2EyYnhuWFpBeG13eDNFamYKaTA3V1ozTHdidkpGclZzcHVxb2lhYXQ0WHJLQ2NoWT0KLS0tLS1FTkQgUFJJVkFURSBLRVktLS0tLQo=
Loading
Loading