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:
- With static file authorization (particularly useful with external tools).
- External apps can send users to Opencast (e.g. for Studio or Editor) with a session that has certain roles.
- A login-mechanism based on the OpenID Connect (OIDC) protocol can be configured.
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.
-
exp(expiration time): As defined in the JWT RFC, a number timestamp in seconds since the unix epoch. JWTs withexp < now()are rejected by Opencast. This claim is required. -
nbf(not before): As defined in the JWT RFC, a number timestamp in seconds since the unix epoch. JWTs withnbf > now()are rejected by Opencast. This claim is optional. -
User Info: information about the user that's possibly used by parts of Opencast. Some JWT use cases don't require user information at all (e.g. static file auth), while others do (e.g. Studio). Therefore, these claims are optional. (TODO: specify when this is stored as user reference)
sub: Opencast username, string. (TODO: explain how this is used to lookup existing users, if at all)name: display name, string.email: string.
-
Authorization: at least one of these claims has to be set, as otherwise no privileges are granted (which is the same as not sending a JWT at all).
roles: array of strings, specifying roles to grant to the request/user directly.oc: JSON map that grants access to single items in Opencast, as identified by the map key.- The map key consists of a one-letter prefix (the item type), a colon, and finally the actual ID of the Opencast item. The following item types are supported. Note: giving access to a series or playlist only gives access to that item directly, and not to its videos (e.g. give write access for a series to be able to edit its metadata or add videos to it).
e: events: seriesp: playlist
- The map value is just an array of actions, corresponding to actions in the OC ACLs, e.g.
readandwrite.
- The map key consists of a one-letter prefix (the item type), a colon, and finally the actual ID of the Opencast item. The following item types are supported. Note: giving access to a series or playlist only gives access to that item directly, and not to its videos (e.g. give write access for a series to be able to edit its metadata or add videos to it).
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:
-
jwtHeaderFilterandjwtRequestParameterFilterdefine how JWTs are extract from incoming requests. By default,jwtquery parameter and theAuthorizationheader (with prefixBearer) are checked. -
The main configuration is in
jwtLoginHandler. Most importantly, you have to set one ofsecretorjwksUrlto enable the login handler, depending on whether you are using a symmetric or asymmetric algorithm. ThejwksUrlshould provide the JSON Web Key Set (JWKS)
<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>
- Configure the
jwtClaimConstraintslist. This list contains claim constraints that are expected to be fulfilled by a valid JWT. If you are using JWTs for OpenID Connect, see the specification for claims that must be validated.
<!-- 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>
- Optionally configure the
jwtRoleMappingslist. This list contains expressions used to construct Opencast roles from JWT claims. This can be used instead of or in addition to the OC standard role mapping. Each entry in this list contains a SPeL expression that needs to returnnull, or a string, or aList<String>, orString[].
<!-- 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>
- Enable single log out (optional). Make sure to comment out the standard
logoutSuccessHandler(otherwise the standard logout mechanism will still be active and the configured logout URL will not be used). Some authentication providers allow to specify a redirection URL to which the user is redirected after a successful logout (e.g. using an URL parameter likerd). Make sure to change the URL in the example below.
<!-- 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" />