Skip to content

Commit

Permalink
Support End Session Endpoint for OIDC and allow customizable Post-log…
Browse files Browse the repository at this point in the history
…out Redirect URI (#6092)

* Support Logout Endpoint for OIDC and allow customisable Post-logout Redirect URI

---------

Signed-off-by: Haywood Shannon <5781935+haywoodsh@users.noreply.github.com>
  • Loading branch information
haywoodsh authored Aug 7, 2024
1 parent a468f3e commit 06b9507
Show file tree
Hide file tree
Showing 15 changed files with 544 additions and 351 deletions.
4 changes: 4 additions & 0 deletions config/crd/bases/k8s.nginx.org_policies.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -159,8 +159,12 @@ spec:
type: string
clientSecret:
type: string
endSessionEndpoint:
type: string
jwksURI:
type: string
postLogoutRedirectURI:
type: string
redirectURI:
type: string
scope:
Expand Down
4 changes: 4 additions & 0 deletions deploy/crds.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -361,8 +361,12 @@ spec:
type: string
clientSecret:
type: string
endSessionEndpoint:
type: string
jwksURI:
type: string
postLogoutRedirectURI:
type: string
redirectURI:
type: string
scope:
Expand Down
6 changes: 5 additions & 1 deletion docs/content/configuration/policy-resource.md
Original file line number Diff line number Diff line change
Expand Up @@ -583,6 +583,8 @@ spec:
authEndpoint: https://idp.example.com/openid-connect/auth
tokenEndpoint: https://idp.example.com/openid-connect/token
jwksURI: https://idp.example.com/openid-connect/certs
endSessionEndpoint: https://idp.example.com/openid-connect/logout
postLogoutRedirectURI: /
accessTokenEnable: true
```

Expand All @@ -607,7 +609,7 @@ The configuration in the example doesn't enable TLS and the synchronization betw

#### Limitations

The OIDC policy defines a few internal locations that can't be customized: `/_jwks_uri`, `/_token`, `/_refresh`, `/_id_token_validation`, `/logout`, `/_logout`. In addition, as explained below `/_codexch` is the default value for redirect URI, but can be customized. Specifying one of these locations as a route in the VirtualServer or VirtualServerRoute will result in a collision and NGINX Plus will fail to reload.
The OIDC policy defines a few internal locations that can't be customized: `/_jwks_uri`, `/_token`, `/_refresh`, `/_id_token_validation`, `/logout`. In addition, as explained below, `/_codexch` is the default value for redirect URI, and `/_logout` is the default value for post logout redirect URI, both of which can be customized. Specifying one of these locations as a route in the VirtualServer or VirtualServerRoute will result in a collision and NGINX Plus will fail to reload.

{{% table %}}
|Field | Description | Type | Required |
Expand All @@ -617,9 +619,11 @@ The OIDC policy defines a few internal locations that can't be customized: `/_jw
|``authEndpoint`` | URL for the authorization endpoint provided by your OpenID Connect provider. | ``string`` | Yes |
|``authExtraArgs`` | A list of extra URL arguments to pass to the authorization endpoint provided by your OpenID Connect provider. Arguments must be URL encoded, multiple arguments may be included in the list, for example ``[ arg1=value1, arg2=value2 ]`` | ``string[]`` | No |
|``tokenEndpoint`` | URL for the token endpoint provided by your OpenID Connect provider. | ``string`` | Yes |
|``endSessionEndpoint`` | URL provided by your OpenID Connect provider to request the end user be logged out. | ``string`` | No |
|``jwksURI`` | URL for the JSON Web Key Set (JWK) document provided by your OpenID Connect provider. | ``string`` | Yes |
|``scope`` | List of OpenID Connect scopes. The scope ``openid`` always needs to be present and others can be added concatenating them with a ``+`` sign, for example ``openid+profile+email``, ``openid+email+userDefinedScope``. The default is ``openid``. | ``string`` | No |
|``redirectURI`` | Allows overriding the default redirect URI. The default is ``/_codexch``. | ``string`` | No |
|``postLogoutRedirectURI`` | URI to redirect to after the logout has been performed. Requires ``endSessionEndpoint``. The default is ``/_logout``. | ``string`` | No |
|``zoneSyncLeeway`` | Specifies the maximum timeout in milliseconds for synchronizing ID/access tokens and shared values between Ingress Controller pods. The default is ``200``. | ``int`` | No |
|``accessTokenEnable`` | Option of whether Bearer token is used to authorize NGINX to access protected backend. | ``boolean`` | No |
{{% /table %}}
Expand Down
36 changes: 16 additions & 20 deletions examples/custom-resources/oidc/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,15 +84,7 @@ To set up Keycloak:
kubectl apply -f client-secret.yaml
```

## Step 6 - Deploy the OIDC Policy

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

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

## Step 7 - Configure NGINX Plus Zone Synchronization and Resolver
## Step 6 - Configure NGINX Plus Zone Synchronization and Resolver

In this step we configure:

Expand All @@ -110,23 +102,19 @@ Steps:
kubectl apply -f nginx-ingress-headless.yaml
```

1. Get the cluster IP of the KubeDNS service:
1. Apply the ConfigMap `nginx-config.yaml`, which contains a stream snippet that enables zone synchronization and the resolver using the kube-dns service.

```console
kubectl -n kube-system get svc kube-dns
kubectl apply -f nginx-config.yaml
```

```text
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kube-dns ClusterIP 10.4.0.10 <none> 53/UDP,53/TCP 9d
```
## Step 7 - Deploy the OIDC Policy

1. Edit the ConfigMap `nginx-config.yaml`, replacing the `<kube-dns-ip>` with the IP obtained in the previous step.
1. Apply the ConfigMap:
Create a policy with the name `oidc-policy` that references the secret from the previous step:

```console
kubectl apply -f nginx-config.yaml
```
```console
kubectl apply -f oidc.yaml
```

## Step 8 - Configure Load Balancing

Expand All @@ -146,3 +134,11 @@ Note that the VirtualServer references the policy `oidc-policy` created in Step
![keycloak](./keycloak.png)
1. 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.png)

## 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.png)
1. To confirm that you have been logged out, navigate to `https://webapp.example.com`. You will be redirected to
Keycloak to log in again.
13 changes: 8 additions & 5 deletions examples/custom-resources/oidc/keycloak_setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ This guide will help you configure KeyCloak using Keycloak's API:

**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.
- 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/).

Expand All @@ -26,12 +28,13 @@ Steps:
```

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

```console
echo $TOKEN
```

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

1. Create the user `nginx-user`:

Expand All @@ -42,10 +45,10 @@ Steps:
1. Create the client `nginx-plus` and retrieve the secret:

```console
SECRET=`curl -sS -k -X POST -d '{ "clientId": "nginx-plus", "redirectUris": ["https://webapp.example.com:443/_codexch"] }' -H "Content-Type:application/json" -H "Authorization: bearer ${TOKEN}" https://${KEYCLOAK_ADDRESS}/realms/master/clients-registrations/default | jq -r .secret`
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:
If everything went well you should have the secret stored in $SECRET. To double check run:

```console
echo $SECRET
Expand Down
Binary file added examples/custom-resources/oidc/logout.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 1 addition & 2 deletions examples/custom-resources/oidc/nginx-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,11 @@ metadata:
namespace: nginx-ingress
data:
stream-snippets: |
resolver <kube-dns-ip> valid=5s;
server {
listen 12345;
listen [::]:12345;
zone_sync;
zone_sync_server nginx-ingress-headless.nginx-ingress.svc.cluster.local:12345 resolve;
}
resolver-addresses: <kube-dns-ip>
resolver-addresses: kube-dns.kube-system.svc.cluster.local
resolver-valid: 5s
1 change: 1 addition & 0 deletions examples/custom-resources/oidc/oidc.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,6 @@ spec:
authEndpoint: https://keycloak.example.com/realms/master/protocol/openid-connect/auth
tokenEndpoint: http://keycloak.default.svc.cluster.local:8080/realms/master/protocol/openid-connect/token
jwksURI: http://keycloak.default.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
22 changes: 12 additions & 10 deletions internal/configs/version2/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,16 +127,18 @@ type EgressMTLS struct {

// OIDC holds OIDC configuration data.
type OIDC struct {
AuthEndpoint string
ClientID string
ClientSecret string
JwksURI string
Scope string
TokenEndpoint string
RedirectURI string
ZoneSyncLeeway int
AuthExtraArgs string
AccessTokenEnable bool
AuthEndpoint string
ClientID string
ClientSecret string
JwksURI string
Scope string
TokenEndpoint string
EndSessionEndpoint string
RedirectURI string
PostLogoutRedirectURI string
ZoneSyncLeeway int
AuthExtraArgs string
AccessTokenEnable bool
}

// APIKey holds API key configuration.
Expand Down
3 changes: 2 additions & 1 deletion internal/configs/version2/nginx-plus.virtualserver.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -90,13 +90,14 @@ server {
include oidc/oidc.conf;

set $oidc_pkce_enable 0;
set $oidc_logout_redirect "/_logout";
set $oidc_logout_redirect "{{ $oidc.PostLogoutRedirectURI }}";
set $oidc_hmac_key "{{ $s.VSName }}";
set $zone_sync_leeway {{ $oidc.ZoneSyncLeeway }};

set $oidc_authz_endpoint "{{ $oidc.AuthEndpoint }}";
set $oidc_authz_extra_args "{{ $oidc.AuthExtraArgs }}";
set $oidc_token_endpoint "{{ $oidc.TokenEndpoint }}";
set $oidc_end_session_endpoint "{{ $oidc.EndSessionEndpoint }}";
set $oidc_jwt_keyfile "{{ $oidc.JwksURI }}";
set $oidc_scopes "{{ $oidc.Scope }}";
set $oidc_client "{{ $oidc.ClientID }}";
Expand Down
26 changes: 16 additions & 10 deletions internal/configs/virtualserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -1306,6 +1306,10 @@ func (p *policiesCfg) addOIDCConfig(
if redirectURI == "" {
redirectURI = "/_codexch"
}
postLogoutRedirectURI := oidc.PostLogoutRedirectURI
if postLogoutRedirectURI == "" {
postLogoutRedirectURI = "/_logout"
}
scope := oidc.Scope
if scope == "" {
scope = "openid"
Expand All @@ -1316,16 +1320,18 @@ func (p *policiesCfg) addOIDCConfig(
}

oidcPolCfg.oidc = &version2.OIDC{
AuthEndpoint: oidc.AuthEndpoint,
AuthExtraArgs: authExtraArgs,
TokenEndpoint: oidc.TokenEndpoint,
JwksURI: oidc.JWKSURI,
ClientID: oidc.ClientID,
ClientSecret: string(clientSecret),
Scope: scope,
RedirectURI: redirectURI,
ZoneSyncLeeway: generateIntFromPointer(oidc.ZoneSyncLeeway, 200),
AccessTokenEnable: oidc.AccessTokenEnable,
AuthEndpoint: oidc.AuthEndpoint,
AuthExtraArgs: authExtraArgs,
TokenEndpoint: oidc.TokenEndpoint,
JwksURI: oidc.JWKSURI,
EndSessionEndpoint: oidc.EndSessionEndpoint,
ClientID: oidc.ClientID,
ClientSecret: string(clientSecret),
Scope: scope,
RedirectURI: redirectURI,
PostLogoutRedirectURI: postLogoutRedirectURI,
ZoneSyncLeeway: generateIntFromPointer(oidc.ZoneSyncLeeway, 200),
AccessTokenEnable: oidc.AccessTokenEnable,
}
oidcPolCfg.key = polKey
}
Expand Down
Loading

0 comments on commit 06b9507

Please sign in to comment.