Configuration for JWT-based Authentication and Authorization

This page describes how to configure Opencast to enable authentication and authorization based on JSON Web Tokens (JWTs). With this feature, a login-mechanism based on the OpenID Connect (OIDC) protocol can be configured, since OIDC uses JWTs.

Prerequisites

This guide assumes that a JWT provider is already setup and Opencast receives the JWT within an HTTP request header. In order to integrate Opencast with an OIDC provider you could use the oauth2-proxy.

Spring Security Configuration

In order to active JWT-based authentication and authorization, you will need to uncomment and adapt the following sections found in etc/security/mh_default_org.xml. Some of the options are configured with the Spring Expression Language (SpEL).

<!-- Uncomment this if using Shibboleth or JWT authentication -->
<sec:authentication-provider ref="preauthAuthProvider" />
<!-- Uncomment to enable external users e.g. used together with shibboleth or JWT -->
<osgi:reference id="userReferenceProvider" cardinality="1..1"
                interface="org.opencastproject.userdirectory.api.UserReferenceProvider" />
<!-- JWT header authentication filter -->
<sec:custom-filter ref="jwtHeaderFilter" position="PRE_AUTH_FILTER"/>
<!-- Uncomment the line below to support JWT. -->
<ref bean="jwtHeaderFilter" />
<!-- General JWT header extraction filter -->
<bean id="jwtHeaderFilter" class="org.opencastproject.security.jwt.JWTRequestHeaderAuthenticationFilter">
  <!-- Name of the HTTP request header that contains the JWT (default: SM_USER) -->
  <property name="principalRequestHeader" value="Authorization"/>
  <!-- Prefix string occurring before the JWT value within the configured header (default: null) -->
  <property name="principalPrefix" value="Bearer "/>
  <property name="authenticationManager" ref="authenticationManager" />
  <property name="loginHandler" ref="jwtLoginHandler" />
  <!-- Throws an exception if a request is missing the configured header (default: true) -->
  <property name="exceptionIfHeaderMissing" value="false" />
  <!-- Logs the header contents for debug purposes (default: false) -->
  <property name="debug" value="false" />
</bean>
<!-- JWT login handler -->
<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" />
  <!-- JWKS URL to use for JWT validation (asymmetric algorithms) (default: null) -->
  <property name="jwksUrl" value="https://auth.example.org/.well-known/jwks.json" />
  <!-- How many minutes to cache a fetched JWKS before re-fetching (default: 1440) -->
  <property name="jwksCacheExpiresIn" value="1440" />
  <!-- Secret to use for JWT validation (symmetric algorithms) (default: null) -->
  <property name="secret" value="***" /> <-- Change this
  <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="['username'].asString()" />
  <!-- Mapping used to extract the name from the JWT (default: null) -->
  <property name="nameMapping" value="['name'].asString()" />
  <!-- Mapping used to extract the email from the JWT (default: null) -->
  <property name="emailMapping" value="['email'].asString()" />
  <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" />
</bean>
<!-- 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('username')</value>
  <value>containsKey('name')</value>
  <value>containsKey('email')</value>
  <value>containsKey('domain')</value>
  <value>containsKey('affiliation')</value>
  <value>['iss'].asString() eq 'https://auth.example.org'</value>
  <value>['aud'].asString() eq 'client-id'</value>
  <value>['username'].asString() matches '.*@example\.org'</value>
  <value>['domain'].asString() eq 'example.org'</value>
  <value>['affiliation'].asList(T(String)).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_' + ['username'].asString()</value>
  <value>('ROLE_JWT_OWNER_' + ['username'].asString()).replaceAll("[^a-zA-Z0-9]","_").toUpperCase()</value>
  <value>['domain'] != null ? 'ROLE_JWT_ORG_' + ['domain'].asString() + '_MEMBER' : null</value>
  <value>['username'].asString() eq ('j_doe01@example.org') ? 'ROLE_ADMIN' : null</value>
  <value>['affiliation'].asList(T(String)).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" />