Skip to content

Commit

Permalink
New Community Module - JWT Headers (#7440)
Browse files Browse the repository at this point in the history
* wip: jwt-headers + documentation start

* wip: access token validation + test cases + documentation

* pom changes from mark's review

* completely remove spring dependencies

* Update doc/en/user/source/community/jwt-headers/configuration.rst

Co-authored-by: Andrea Aime <andrea.aime@gmail.com>

---------

Co-authored-by: david blasby <david.blasby@geocat.net>
Co-authored-by: Andrea Aime <andrea.aime@gmail.com>
  • Loading branch information
3 people authored Feb 26, 2024
1 parent 9bce176 commit 668402c
Show file tree
Hide file tree
Showing 40 changed files with 3,592 additions and 0 deletions.
1 change: 1 addition & 0 deletions doc/en/user/source/community/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ officially part of the GeoServer releases. They are however built along with the
jdbcconfig/index
jdbcstore/index
jms-cluster/index
jwt-headers/index
keycloak/index
libdeflate/index
mbtiles/index
Expand Down
211 changes: 211 additions & 0 deletions doc/en/user/source/community/jwt-headers/configuration.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
.. _community_jwtheaders_config:

JWT Headers configuration
=========================

The JWT Headers module covers three main use cases:

#. Simple Text, JSON, or JWT headers for the username
#. Verification of JWT Access Tokens
#. Getting roles from a JSON header or an attached JWT Access Token claim

Configuration Options
---------------------

User Name Options
^^^^^^^^^^^^^^^^^

.. list-table:: User Name Options
:header-rows: 1

* - Config Option
- Meaning
* - Request header attribute for User Name
- The name of the HTTP header item that contains the user name.
* - Format the Header value is in
- Format that the user name is in:

* Simple String - user name is the header's value.
* JSON - The header is a JSON string. Use "JSON path" for where the user name is in the JSON.
* JWT - The header is a JWT (base64) string. Use "JSON path" for where the user name is in the JWT claims.
* - JSON path for the User Name
- If the user name is in JSON or JWT format, this is the JSON path to the user's name.

If you are using `Apache's mod_auth_openidc <https://github.com/OpenIDC/mod_auth_openidc>`_, then Apache will typically add:

* an `OIDC_id_token_payload` header item (containing a JSON string of the ID token claims)
* an `OIDC_access_token` header item (containing a base64 JWT Access Token)
* optionally, a simple header item with individual claim values (i.e. `OIDC_access_token`)

Here are some example values;

.. code-block::
OIDC_id_token_payload: {"exp":1708555947,"iat":1708555647,"auth_time":1708555288,"jti":"42ee833e-89d3-4779-bd9d-06b979329c9f","iss":"http://localhost:7777/realms/dave-test2","aud":"live-key2","sub":"98cfe060-f980-4a05-8612-6c609219ffe9","typ":"ID","azp":"live-key2","nonce":"4PhqmZSJ355KBtJPbAP_PdwqiLnc7B1lA2SGpB0zXr4","session_state":"7712b364-339a-4053-ae0c-7d3adfca9005","at_hash":"2Tyw8q4ZMewuYrD38alCug","acr":"0","sid":"7712b364-339a-4053-ae0c-7d3adfca9005","upn":"david.blasby@geocat.net","resource_access":{"live-key2":{"roles":["GeonetworkAdministrator","GeoserverAdministrator"]}},"email_verified":false,"address":{},"name":"david blasby","groups":["default-roles-dave-test2","offline_access","uma_authorization"],"preferred_username":"david.blasby@geocat.net","given_name":"david","family_name":"blasby","email":"david.blasby@geocat.net"}
.. code-block::
OIDC_access_token: eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICItb0QyZXphcjF3ZHBUUmZCS0NqMFY4cm5ZVkJGQmxJLW5ldzFEREJCNTJrIn0.eyJleHAiOjE3MDg1NTU5NDcsImlhdCI6MTcwODU1NTY0NywiYXV0aF90aW1lIjoxNzA4NTU1Mjg4LCJqdGkiOiI0M2UyYjUwZS1hYjJkLTQ2OWQtYWJjOC01Nzc1YTY0MTMwNTkiLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0Ojc3NzcvcmVhbG1zL2RhdmUtdGVzdDIiLCJhdWQiOiJhY2NvdW50Iiwic3ViIjoiOThjZmUwNjAtZjk4MC00YTA1LTg2MTItNmM2MDkyMTlmZmU5IiwidHlwIjoiQmVhcmVyIiwiYXpwIjoibGl2ZS1rZXkyIiwibm9uY2UiOiI0UGhxbVpTSjM1NUtCdEpQYkFQX1Bkd3FpTG5jN0IxbEEyU0dwQjB6WHI0Iiwic2Vzc2lvbl9zdGF0ZSI6Ijc3MTJiMzY0LTMzOWEtNDA1My1hZTBjLTdkM2FkZmNhOTAwNSIsImFjciI6IjAiLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOlsiZGVmYXVsdC1yb2xlcy1kYXZlLXRlc3QyIiwib2ZmbGluZV9hY2Nlc3MiLCJ1bWFfYXV0aG9yaXphdGlvbiJdfSwicmVzb3VyY2VfYWNjZXNzIjp7ImxpdmUta2V5MiI6eyJyb2xlcyI6WyJHZW9uZXR3b3JrQWRtaW5pc3RyYXRvciIsIkdlb3NlcnZlckFkbWluaXN0cmF0b3IiXX0sImFjY291bnQiOnsicm9sZXMiOlsibWFuYWdlLWFjY291bnQiLCJtYW5hZ2UtYWNjb3VudC1saW5rcyIsInZpZXctcHJvZmlsZSJdfX0sInNjb3BlIjoib3BlbmlkIHBob25lIG9mZmxpbmVfYWNjZXNzIG1pY3JvcHJvZmlsZS1qd3QgcHJvZmlsZSBhZGRyZXNzIGVtYWlsIiwic2lkIjoiNzcxMmIzNjQtMzM5YS00MDUzLWFlMGMtN2QzYWRmY2E5MDA1IiwidXBuIjoiZGF2aWQuYmxhc2J5QGdlb2NhdC5uZXQiLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsImFkZHJlc3MiOnt9LCJuYW1lIjoiZGF2aWQgYmxhc2J5IiwiZ3JvdXBzIjpbImRlZmF1bHQtcm9sZXMtZGF2ZS10ZXN0MiIsIm9mZmxpbmVfYWNjZXNzIiwidW1hX2F1dGhvcml6YXRpb24iXSwicHJlZmVycmVkX3VzZXJuYW1lIjoiZGF2aWQuYmxhc2J5QGdlb2NhdC5uZXQiLCJnaXZlbl9uYW1lIjoiZGF2aWQiLCJmYW1pbHlfbmFtZSI6ImJsYXNieSIsImVtYWlsIjoiZGF2aWQuYmxhc2J5QGdlb2NhdC5uZXQifQ.Iq8YJ99s_HBd-gU2zaDqGbJadCE--7PlS2kRHaegYTil7WoNKfjfcH-K-59mHGzJm-V_SefE-iWG63z2c6ChddzhvG8I_O5vDNFoGlGOQFunZC379SqhqhCEdwscEUDkNA3iTTXvK9vn0muStDiv9OzpJ1zcpqYqsgxGbolGgLJgeuK8yNDH7kzDtoRzHiHw2rx4seeVpxUYAjyg_cCkEjRt3wzud7H3xlfQWRx75YfpJ0pnVphuXYR7Z8x9p6hCPtrBfDeriudm-wkwXtcV2LNlXrZ2zpKS_6Zdxzza2lN30q_6DQXHGo8EAIr8SiiQrxPQulNiX9r8XmQ917Ep0g
.. code-block::
OIDC_preferred_username: david.blasby@geocat.net
It is recommended to either use the `OIDC_id_token_payload` (JSON) or `OIDC_access_token` (JWT) header.

For `OIDC_id_token_payload`:

* Request header attribute for User Name: `OIDC_id_token_payload`
* Format the Header value is in: `JSON`
* JSON path for the User Name: `preferred_username`

For `OIDC_access_token`:

* Request header attribute for User Name: `OIDC_access_token`
* Format the Header value is in: `JWT`
* JSON path for the User Name: `preferred_username`



New Role Source Options
^^^^^^^^^^^^^^^^^^^^^^^

You can use the standard role source options in GeoServer (`Request Header`, `User Group Service`, or `Role Service`). The JWT Headers module adds two more role sources - `Header Containing JSON String` and `Header containing JWT`.


.. list-table:: New Role Source Options
:header-rows: 1

* - Config Option
- Meaning
* - Role Source
- Which Role Source to use:

* Header containing JSON String - Header contains a JSON claims object
* Header Containing JWT - Header contains a Base64 JWT Access Token
* - Request Header attribute for Roles
- Name of the header item the JSON or JWT is contained in
* - JSON Path
- Path in the JSON object or JWT claims that contains the roles. This should either be a simple string (single role) or a list of strings.


Using the example `OIDC_id_token_payload` (JSON) or `OIDC_access_token` (JWT) shown above, the claims are:


.. code-block:: json
{
"exp": 1708555947,
"iat": 1708555647,
"auth_time": 1708555288,
"jti": "42ee833e-89d3-4779-bd9d-06b979329c9f",
"iss": "http://localhost:7777/realms/dave-test2",
"aud": "live-key2",
"sub": "98cfe060-f980-4a05-8612-6c609219ffe9",
"typ": "ID",
"azp": "live-key2",
"nonce": "4PhqmZSJ355KBtJPbAP_PdwqiLnc7B1lA2SGpB0zXr4",
"session_state": "7712b364-339a-4053-ae0c-7d3adfca9005",
"at_hash": "2Tyw8q4ZMewuYrD38alCug",
"acr": "0",
"sid": "7712b364-339a-4053-ae0c-7d3adfca9005",
"upn": "david.blasby@geocat.net",
"resource_access":
{
"live-key2":
{
"roles":
[
"GeonetworkAdministrator",
"GeoserverAdministrator"
]
}
},
"email_verified": false,
"address": { },
"name": "david blasby",
"groups": ["default-roles-dave-test2", "offline_access", "uma_authorization"],
"preferred_username": "david.blasby@geocat.net",
"given_name": "david",
"family_name": "blasby",
"email": "david.blasby@geocat.net"
}
In this JSON set of claims (mirrored in the JWT claims of the Access Token), and the two roles from the IDP are "GeonetworkAdministrator", and "GeoserverAdministrator". The JSON path to the roles is `resource_access.live-key2.roles`.

Role Conversion
"""""""""""""""

The JWT Headers module also allows for converting roles (from the external IDP) to the GeoServer internal role names.

.. list-table:: Role Converter Options
:header-rows: 1

* - Config Option
- Meaning
* - Role Converter Map from External Roles to Geoserver Roles
- This is a ";" delimited map in the form of `ExternalRole1=GeoServerRole1;ExternalRole2=GeoServerRole2`
* - Only allow External Roles that are explicitly named above
- If checked, external roles that are not mentioned in the conversion map will be ignored. If unchecked, those external roles will be turned into GeoServer roles of the same name.

For example, a conversion map like `GeoserverAdministrator=ROLE_ADMINISTRATOR` will convert our IDP "GeoserverAdministrator" role to GeoServer's "ROLE_ADMINISTRATOR".

In our example, the user has two roles "GeoserverAdministrator" and "GeonetworkAdministrator". If the "Only allow External Roles that are explicitly named above" is checked, then GeoServer will only see the "ROLE_ADMINISTRATOR" role. If unchecked, it will see "ROLE_ADMINISTRATOR" and "GeonetworkAdministrator". In neither case will it see the converted "GeoserverAdministrator" roles.


JWT Validation
^^^^^^^^^^^^^^

If you are using Apache's `mod_auth_openidc` module, then you do *not* have to do JWT validation - Apache will ensure they are valid when it attaches the headers to the request.

However, if you are using robot access to GeoServer, you can attach an Access Token to the request header for access.

.. code-block::
Authentication: Bearer `base64 JWT Access Token`
OR

.. code-block::
Authentication: `base64 JWT Access Token`
You would then setup the user name to come from a JWT token in the `Authentication` header with a JSON path like `preferred_username`.




You can also extract roles from the Access Token in a similar manner - make sure your IDP imbeds roles inside the Access Token.

.. list-table:: JWT Validation Options
:header-rows: 1

* - Config Option
- Meaning
* - Validate JWT (Access Token)
- If unchecked, do not do any validation.
* - Validate Token Expiry
- If checked, validate the `exp` claim in the JWT and ensure it is in the future. This should always be checked so you do not allow expired tokens.
* - Validate JWT (Access Token) Signature
- If checked, validate the Token's Signature
* - JSON Web Key Set URL (jwks_uri)
- URL for a JWK Set. This is typically called `jwks_uri` in the OIDC metadata configuration. This will be downloaded and used to check the JWT's signature. This should always be checked to ensure that the JWT has not been modified.
* - Validate JWT (Access Token) Against Endpoint
- If checked, validate the access token against an IDP's token verification URL.
* - URL (userinfo_endpoint)
- IDP's token validation URL. This URL will be retrieved by adding the Access Token to the `Authentiation: Bearer <access token>` header. It should return a HTTP 200 status code if the token is valid. This is recommened by the OIDC specification.
* - Also validate Subject
- If checked, the `sub` claim of the Access Token and the "userinfo_endpoint" `sub` claim will be checked to ensure they are equal. This is recommened by the OIDC specification.
* - Validate JWT (Access Token) Audience
- If checked, the audience of the Access Token is checked. This is recommened by the OIDC specification since this verifies that the Access Token is meant for us.
* - Claim Name
- The name of the claim the audience is in (`aud`, `azp`, or `appid` claim) the Access Token.
* - Required Claim Value
- The value this claim must be (if the claim is a list of string, then it must contain this value).




38 changes: 38 additions & 0 deletions doc/en/user/source/community/jwt-headers/index.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
.. _community_jwtheaders:

JWT Headers
===========

The JWT Headers module provides a security module for header based security. This provides much more advanced functionality than the HTTP Header Authentication Module (see :ref:`security_tutorials_httpheaderproxy`).

This module allows `JSON-based <https://en.wikipedia.org/wiki/JSON>`_ headers (for username and roles) as well as `JWT-based <https://en.wikipedia.org/wiki/JSON_Web_Token>`_ headers (for username and roles). It also allows for validating JWT-Based AccessTokens (i.e. via `OAUTH2 <https://en.wikipedia.org/wiki/OAuth>`_/`OpenID Connect <https://en.wikipedia.org/wiki/OpenID#OpenID_Connect_(OIDC)>`_).


If you are using something like `Apache's mod_auth_openidc <https://github.com/OpenIDC/mod_auth_openidc>`_, then this module will allow you to;

#. Get the username from an Apache-provided `OIDC_*` header (either as simple-strings or as a component of a JSON object).
#. Get the user's roles from an Apache-provided `OIDC_*` header (as a component of a JSON object).
#. The user's roles can also be from any of the standard GeoServer providers (i.e. User Group Service, Role Service, or Request Header).

If you are using `OAUTH2/OIDC Access Tokens <https://www.oauth.com/oauth2-servers/access-tokens/>`_:

#. Get the username from the attached JWT Access Token (via a path into the `Access Token's JSON Claims <https://auth0.com/docs/authenticate/login/oidc-conformant-authentication/oidc-adoption-access-tokens/>`_).
#. Get the user's roles from the JWT Access Token (via a path into the Token's JSON Claims).
#. Validate the Access Token

* Validate its Signature
* Validate that it hasn't expired
* Validate the token against a token verifier URL ("userinfo_endpoint") and check that subjects match
* Validate components of the Access Token (like `aud (audience) <https://auth0.com/docs/secure/tokens/json-web-tokens/json-web-token-claims>`_)

#. The user's roles can also be from any of the standard GeoServer providers (i.e. User Group Service, Role Service, or Request Header).
#. You can also extract roles from the JWT Access Token (via a JSON path).



.. toctree::
:maxdepth: 2

installing
configuration

40 changes: 40 additions & 0 deletions doc/en/user/source/community/jwt-headers/installing.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
.. _community_jwtheaders_installing:

Installing JWT Headers
======================

To install the JWT Headers module:

#. To obtain the JWT Headers community module:

* If working with a |release| nightly build, download the module: :download_community:`jwt-headers`

Verify that the version number in the filename corresponds to the version of GeoServer you are running (for example |release| above).

* Community modules are not yet ready for distribution with GeoServer release.

To compile the JWT Headers community module yourself download the src bundle for your GeoServer version and compile:

.. code-block:: bash
cd src/community
mvn install -PcommunityRelease -DskipTests
And package:

.. code-block:: bash
cd src/community
mvn assembly:single -N
#. Place the JARs in ``WEB-INF/lib``.

#. Restart GeoServer.


For developers;

.. code-block:: bash
cd src
mvn install -Pjwt-headers -DskipTests
96 changes: 96 additions & 0 deletions src/community/jwt-headers/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ (c) 2024 Open Source Geospatial Foundation - all rights reserved
~ This code is licensed under the GPL 2.0 license, available at the root
~ application directory.
~
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">

<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>org.geoserver</groupId>
<artifactId>community</artifactId>
<version>2.25-SNAPSHOT</version>
</parent>

<groupId>org.geoserver.community</groupId>
<artifactId>jwt-headers</artifactId>
<packaging>jar</packaging>
<name>GeoServer Security Plugin For JWT Headers</name>

<dependencies>
<dependency>
<groupId>org.geoserver</groupId>
<artifactId>gs-main</artifactId>
<version>${project.version}</version>
</dependency>

<dependency>
<groupId>org.geoserver.web</groupId>
<artifactId>gs-web-sec-core</artifactId>
<version>${project.version}</version>
</dependency>

<dependency>
<groupId>org.geoserver</groupId>
<artifactId>gs-main</artifactId>
<version>${project.version}</version>
<classifier>tests</classifier>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.geoserver</groupId>
<artifactId>gs-wms</artifactId>
<version>${project.version}</version>
<classifier>tests</classifier>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.geoserver.web</groupId>
<artifactId>gs-web-core</artifactId>
<version>${project.version}</version>
<classifier>tests</classifier>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.geoserver.web</groupId>
<artifactId>gs-web-sec-core</artifactId>
<version>${project.version}</version>
<classifier>tests</classifier>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.geoserver.security</groupId>
<artifactId>gs-security-tests</artifactId>
<version>${project.version}</version>
<classifier>tests</classifier>
<scope>test</scope>
</dependency>

<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>nimbus-jose-jwt</artifactId>
<version>9.37.3</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.jayway.jsonpath</groupId>
<artifactId>json-path</artifactId>
</dependency>

<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</dependency>

<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>

</dependencies>
</project>
Loading

0 comments on commit 668402c

Please sign in to comment.