Configuration for JWT-based Authentication and Authorization

This page describes how to configure Opencast to enable authentication and/or authorization based on JSON Web Tokens (JWTs). JWTs can be used in various ways:

Note: Some of these use cases might not be perfectly supported by Opencast yet, but are possible in principle.

Prerequisites

Opencast does not generate JWTs, but reads them in incoming requests. This guide assumes that you already have a JWT provider, i.e. another services that generates JWTs for consumption in Opencast. This could be Tobira, an LMS, an OIDC provider, or something else. These generally have their own setup guides. The JWT has to be passed to Opencast via HTTP header or via request parameter (i.e. a query parameter for GET-requests, and a form parameter for POST-requests).

In order to integrate Opencast with an OIDC provider you could use the oauth2-proxy.

Standard OC Schema for JWTs

After the signature verification, a JWT is just a bunch of "claims" (JSON fields). The communicating parties can decide fairly freely on how to interpret these claims. Protocols like OIDC define exactly which claims are to be used and how. For other cases where external Opencast apps (like Tobira or an LMS) need to communicate with Opencast, this section defines a standard schema to follow. You can configure Opencast differently, but this aims at standardizing JWT usage across Opencast-related applications and offer an out-of-the-box solution.

This schema is designed to be flexible and support all current and conceivable future use cases, while keeping the JWT size for each use case small.

Quick overview

{
    // Standard claims
    "exp": 1499863217, // required
    "nbf": 1548632170, // optional

    // User info
    "sub": "jose", // username
    "name": "José Carreño Quiñones",
    "email": "jose@example.com",

    // Authorization, i.e. privileges the request/user should have
    "roles": ["ROLE_STUDIO", "ROLE_API_EVENTS_VIEW"],
    "oc": {
        "e:d622b861-4264-4947-8db1-c754c5956433": ["read, "annotate"],
        "s:4ed02421-144c-42a1-b98a-22e84f3ac691": ["write"]
    }
}

Claims

A claim being "required" means that the JWT provider (e.g. Tobira, LMS) has to include it. Opencast will reject all JWTs which lack required claims.

Examples

Static file authorization

An external application wants a user to load a protected static file from Opencast, e.g. a video file. The request needs read permission for the that event. The external app can achieve this with the following JWT (which can simply be attached to the URL as query parameter):

{
    "exp": 1499863217,
    "oc": { "e:d622b861-4264-4947-8db1-c754c5956433": ["read"] }
}

Opencast Studio

An external application wants to let a user use Opencast Studio to record and upload a video. Since this does not happen in a single request, a browser session needs to be created. This can be achieved with the /redirect API. The JWT sent to that API needs to grant permissions to use all required APIs (which can be done by granting ROLE_STUDIO) and also specify user info, since Studio uses some use information.

{
    "exp": 1499863217,
    "sub": "peter",
    "name": "Peter Lustig",
    "email": "peter@example.com",
    "roles": ["ROLE_STUDIO"]
}

Spring Security Configuration

In order to activate JWT-based authentication and authorization, you will need to adjust your security config (e.g. etc/security/mh_default_org.xml).

If you just want to be able to accept JWTs from Tobira or LMS, you only have to fill out a single value: jwksUrl (just search for that string, it's part of the jwtLoginHandler bean configuration). Just set the corresponding Tobira/LMS URL there (check their docs to find the URL). All other JWT configuration can usually be kept as is, as the defaults work fine for most cases. (Note: currently you can only easily connect either Tobira or an LMS. Until Opencast supports multiple URLs out of the box, you have to use an external tool for aggregating JWKS.)

If you want to interface with an OIDC provider or do something else entirely, configuring JWT is a bit more involved.

Advanced configuration

Some of the options are configured with the Spring Expression Language (SpEL). In the following, certain IDs (like jwtHeaderFilter) are mentioned: just search in the security config file for these IDs to see where they can be configured.

A few points of interest:

<bean id="jwtLoginHandler" class="org.opencastproject.security.jwt.DynamicLoginHandler">
  <property name="userDetailsService" ref="userDetailsService" />
  <property name="userDirectoryService" ref="userDirectoryService" />
  <property name="securityService" ref="securityService" />
  <property name="userReferenceProvider" ref="userReferenceProvider" />
  <!-- Set either `jwksUrl` or `secret` to enable this login handler -->
  <!-- <property name="jwksUrl" value="https://replace-me.invalid/.well-known/jwks.json" /> -->
  <!-- <property name="secret" value="***" /> -->
  <property name="expectedAlgorithms" ref="jwtExpectedAlgorithms" />
  <property name="claimConstraints" ref="jwtClaimConstraints" />
  <!-- Mapping used to extract the username from the JWT (default: null) -->
  <property name="usernameMapping" value="['sub']" />
  <!-- Mapping used to extract the name from the JWT (default: null) -->
  <property name="nameMapping" value="['name']" />
  <!-- Mapping used to extract the email from the JWT (default: null) -->
  <property name="emailMapping" value="['email'] ?: (['sub'] + '@jwt.invalid')" />
  <!-- Opencast standard role mapping as defined above in this chapter of the docs. -->
  <!-- I.e. reads `roles` and `oc` claims to specify roles. -->
  <property name="ocStandardRoleMappings" value="true" />
  <!-- Custom role mappings, optional -->
  <property name="roleMappings" ref="jwtRoleMappings" />
  <!-- JWT cache holds already validated JWTs to not always re-validate in subsequent requests -->
  <!-- Maximum number of validated JWTs to cache (default: 500) -->
  <property name="jwtCacheSize" value="500" />
  <!-- How many minutes to cache a JWT before re-validating (default: 60) -->
  <property name="jwtCacheExpiresIn" value="60" />
  <!-- How long to cache a fetched JWKS before re-fetching -->
  <property name="jwksTimeToLive" value="3600000" />
</bean>
  ```

* Configure the `jwtExpectedAlgorithms` list. This list holds the allowed algorithms with which a valid JWT may be
  signed (`alg` claim).
```xml
<!-- The signing algorithms expected for the JWT signature -->
<util:list id="jwtExpectedAlgorithms" value-type="java.lang.String">
  <value>RS256</value>
</util:list>
<!-- The claim constraints that are expected to be fulfilled by the JWT -->
<util:list id="jwtClaimConstraints" value-type="java.lang.String">
  <value>containsKey('iss')</value>
  <value>containsKey('aud')</value>
  <value>containsKey('sub')</value>
  <value>containsKey('name')</value>
  <value>containsKey('email')</value>
  <value>containsKey('domain')</value>
  <value>containsKey('affiliation')</value>
  <value>['iss'] eq 'https://auth.example.org'</value>
  <value>['aud'] eq 'client-id'</value>
  <value>['username'] matches '.*@example\.org'</value>
  <value>['domain'] eq 'example.org'</value>
  <value>['affiliation'].contains('faculty@example.org')</value>
</util:list>
<!-- The mapping from JWT claims to Opencast roles -->
<util:list id="jwtRoleMappings" value-type="java.lang.String">
  <value>'ROLE_JWT_USER'</value>
  <value>'ROLE_JWT_USER_' + ['sub']</value>
  <value>('ROLE_JWT_OWNER_' + ['sub']).replaceAll("[^a-zA-Z0-9]","_").toUpperCase()</value>
  <value>['domain'] != null ? 'ROLE_JWT_ORG_' + ['domain'] + '_MEMBER' : null</value>
  <value>['username'] eq ('j_doe01@example.org') ? 'ROLE_ADMIN' : null</value>
  <value>['affiliation'].contains('faculty@example.org') ? 'ROLE_GROUP_JWT_TRAINER' : null</value>
</util:list>
<!-- Enables log out -->
<!-- <sec:logout success-handler-ref="logoutSuccessHandler" /> -->

<!-- JWT single log out -->
<sec:logout logout-success-url="https://auth.example.org/sign_out?rd=http://www.opencast.org" />