-
Notifications
You must be signed in to change notification settings - Fork 251
Description
Ok so this is a shitty bug that I've tried to understand for a while now. Sadly, I have not gotten to the bottom of it, but writing everything down here for others to hopefully help investigate.
Describe the bug
When using JWTs with the ES256 algorithm (that's the only one I tested, might be broken for others as well), Opencast rejects JWTs for some keys, but only when using Java 16 or later. I confirmed it always working for Java 13 or earlier. (I cannot easily test Java 14 or 15.) When rejecting, Opencast prints:
DEBUG | (DynamicLoginHandler:174) - The Token's Signature resulted invalid when verified using the Algorithm: SHA256withECDSA
Testing JWTs rejected by Opencast at jwt.io, they are accepted and the "signature verified". So everything seems to be correct.
Here are some examples of keys & JWTs:
Accepted:
-
Details
JWT
eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6InNhYmluZUBleGFtcGxlLm9yZyIsImV4cCI6MjU4NzQ2NDgzNywibmFtZSI6IlNhYmluZSBSdWRvbGZzIiwidXNlcm5hbWUiOiJzYWJpbmUifQ.k7mRojUsJT8BUu50eauE7gym1yX2PPeFvQC70kUFFEw7NCgtK-hG60Fi3EptOZa2fSkJeHnqo3h59Lyt32MonQ
Key:-----BEGIN RIVATE KEY----- MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg66Zp9DgrYgT6H/dJ p3zkos361IC2DJchnd0ArvCFofihRANCAARdWx7zObah63pPdt6WJPy+A9rIjriA ImQQfWCJRa37KyytlaSAmpUPLEUeW31fDiwD66yphyCeNHgeFfPD/tw6 -----END RIVATE KEY----- -
Details
JWT
eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6InNhYmluZUBleGFtcGxlLm9yZyIsImV4cCI6MjU4NzQ2NDgzNywibmFtZSI6IlNhYmluZSBSdWRvbGZzIiwidXNlcm5hbWUiOiJzYWJpbmUifQ.Q5QsbZrWWDNfzAPCuijzW-k0dttSqp3oXIdN1pCrgtzU0aHs_FdJmnhybnIpaKdoidPOvDUrwUad2OTaVubwRg
Key:-----BEGIN RIVATE KEY----- MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgS1r4pwdDQlIxU7iY kECI940DOYyNP2WYZu+y7nHucQahRANCAAQgfuip+UlGkPGMtggFFVaEFrSll/Qv NZcN91pxkVR18Udu28jcLIVHlHsdUEzamZouJESZXPIaGXMY1W5s9IbV -----END RIVATE KEY----- -
Details
JWT
eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6InNhYmluZUBleGFtcGxlLm9yZyIsImV4cCI6MjU4NzQ2NDgzNiwibmFtZSI6IlNhYmluZSBSdWRvbGZzIiwidXNlcm5hbWUiOiJzYWJpbmUifQ.oky6Nw-s3vja82wSE149mPKS_vinVG7Sx4pd7cPGiik_HLVBTNxkVe9sih94w4-PtVcXrsNEYXSSiwOp6zPorQ
Key:-----BEGIN RIVATE KEY----- MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgnqRelB2vzbYGj4EC X8FAFmGxO/uGOXZNrCzbc7VSRnWhRANCAAQSvQnDO1znp/TLHzoPb2Idq0MMTk0d JbNkdK6ahbYIDUZxxnmq58idIUPPORmK24EHz+yqnTHGvOAXd5IU4ZBZ -----END RIVATE KEY-----
Rejected:
-
Details
JWT
eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6InNhYmluZUBleGFtcGxlLm9yZyIsImV4cCI6MjU4NzQ2NDgzNywibmFtZSI6IlNhYmluZSBSdWRvbGZzIiwidXNlcm5hbWUiOiJzYWJpbmUifQ.sQoHdy6zlhMfBivQz05cV-z4Li-ucM40b4vOXZ3mcSI0hwASGrdEMZJv_2V38bbRCx38F-9u_DFXpaHDJsCZdg
Key:-----BEGIN RIVATE KEY----- MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgVNSBz5wWwrp1RYN9 M6wevPb3IFaRtLmdmD0ygXq1yE+hRANCAARl7zjQYUpf/x8rFy/tXiVJtJ6jldcT GoL+0e4RLBkpRfxA+fxUyC0GsBHd43sGlhUJD1kjFNcqEMzlC1zPGJ73 -----END RIVATE KEY----- -
Details
JWT
eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6InNhYmluZUBleGFtcGxlLm9yZyIsImV4cCI6MjU4NzQ2NDgzOCwibmFtZSI6IlNhYmluZSBSdWRvbGZzIiwidXNlcm5hbWUiOiJzYWJpbmUifQ.10S86oZke93MYBoZ1bq5GmgfQi-fu1KFFByGClgPL32LZIjE2-jzRFFAXmCvdz3svNNc0caEQaMT8-nyNaPF6w
Key:-----BEGIN RIVATE KEY----- MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgFg7Z38zhd8+a0VUd qLu+syxpoNyUHEZICf4bAEEWDrqhRANCAAQeT8Es9YeQPKePDohM2ERv6MYqJIZe dV7U5oIW2X+bOqmQlejJvJe19MzP064eMgcjmFsyw5mRUblfJSSMUVj7 -----END RIVATE KEY----- -
Details
JWT
eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6InNhYmluZUBleGFtcGxlLm9yZyIsImV4cCI6MjU4NzQ2NDc5OSwibmFtZSI6IlNhYmluZSBSdWRvbGZzIiwidXNlcm5hbWUiOiJzYWJpbmUifQ.N0yOjiOwXsVkVSu7AjzjQpGAO9tgt1JUw8lEk1j7YddAexEwWYfz8FW6aix_0t_NmJymZzUhZfCngBVKxQ3wXA
Key:-----BEGIN RIVATE KEY----- MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgKuPkEG9OxUjwjrkz q9ojo8YtcFXK6EvvCGLKaTH8oIShRANCAASDkqnbtxFWn/386gKxhwXn4NIxVPKJ RRUIBmI9AFPFEGRh6SSLXd5Pcp/B3uChHVpV6Ilf8BPUeVgFPIaz2QFg -----END RIVATE KEY-----
I used Tobira to generate the keys and JWTs (JWTs for two keys I generated with openssl were also rejected). I wrote a script to quickly generate a new key, then a JWT for the key, then test it against Opencast. I ran this many many times to get a feeling for the outcome and it's absolutely random. Sometimes it works, sometimes it doesn't. It seems to be completely dependent on the secret key though: for a given key, either all or none of the JWTs are rejected.
The relevant Java release notes are:
But I cannot find anything relevant. First I thought the disabling of secp256k1 in Java 15 would be relevant, but it isn't: they keys in question use the secp256r1 key and it would throw a different exception anyway.
To Reproduce
I tested the most with Opencast 16.1 but I am very sure that all of 16.x and newer is affected, maybe even older Opencasts.
- Change the security config like shown below. This just configures JWT using the key from
tobira.opencast.org. It is just the second half of the security config, the first half is not touched.
Details
<!-- ############################# -->
<!-- # LOGIN / LOGOUT MECHANISMS # -->
<!-- ############################# -->
<!-- Uncomment to enable x509 client certificates for identifying clients -->
<!-- sec:x509 subject-principal-regex="CN=(.*?)," user-service-ref="userDetailsService" / -->
<!-- Enable and configure the failure URL for form-based logins -->
<!-- CAS Auth: Comment this if using CAS authentication -->
<sec:form-login authentication-failure-url="/login.html?error" authentication-success-handler-ref="authSuccessHandler" />
<!-- (Pre-)Authentication filter chain(s) -->
<sec:custom-filter position="BASIC_AUTH_FILTER" ref="authenticationFilters" />
<sec:custom-filter position="PRE_AUTH_FILTER" ref="preAuthenticationFilters" />
<sec:custom-filter ref="asyncTimeoutRedirectFilter" after="EXCEPTION_TRANSLATION_FILTER"/>
<!-- Opencast is shipping its own implementation of the anonymous filter -->
<sec:custom-filter ref="anonymousFilter" position="ANONYMOUS_FILTER" />
<!-- CAS Auth: Uncomment this if using CAS authentication
<sec:custom-filter position="FORM_LOGIN_FILTER" ref="casFilter" />
-->
<!-- Enables "remember me" functionality -->
<sec:remember-me services-ref="rememberMeServices" />
<!-- Set the request cache -->
<sec:request-cache ref="requestCache" />
<!-- If any URLs are to be exposed to anonymous users, the "sec:anonymous" filter must be present -->
<sec:anonymous enabled="false" />
<!-- CAS Auth: Uncomment this if using CAS authentication -->
<!-- Enables CAS Single Sign Out
<sec:logout logout-success-url="/cas-logout.jsp"/>
<sec:custom-filter ref="requestSingleLogoutFilter" before="LOGOUT_FILTER"/>
<sec:custom-filter ref="singleLogoutFilter" before="FORM_LOGIN_FILTER"/>
-->
<!-- Enables log out -->
<sec:logout success-handler-ref="logoutSuccessHandler" />
<!-- JWT single log out
Please specify the URL to return to after logging out. Comment out the logoutSuccessHandler above.
<sec:logout logout-success-url="https://auth.example.org/sign_out?rd=http://www.opencast.org" />
-->
</sec:http>
<bean id="rememberMeServices" class="org.opencastproject.kernel.security.SystemTokenBasedRememberMeService">
<property name="userDetailsService" ref="userDetailsService"/>
<!-- All following settings are optional -->
<property name="tokenValiditySeconds" value="1209600"/>
<property name="cookieName" value="oc-remember-me"/>
<!-- The following key will be augmented by system properties if left at the default value.
Thus, leaving this untouched is okay. This key must be equal to the key passed to
rememberMeAuthenticationProvider (s. below). To generate cookies which are valid for the whole cluster,
set this manually. The key won't be augmented/randomized if you use something different than 'opencast'. -->
<property name="key" value="opencast"/>
</bean>
<bean id="rememberMeAuthenticationProvider"
class="org.opencastproject.kernel.security.SystemTokenBasedRememberMeAuthenticationProvider">
<!-- This key must be equal to the key passed to rememberMeServices (s. above) -->
<property name="key" value="opencast"/>
</bean>
<!-- ############################# -->
<!-- # Authentication Filters # -->
<!-- ############################# -->
<bean id="authenticationFilters" class="org.springframework.web.filter.CompositeFilter">
<property name="filters">
<list>
<!-- Digest auth is used by capture agents and is used to enable transparent clustering of services -->
<!-- ATTENTION! Do not deactivate the digest filter, otherwise the distributed setup would not work -->
<ref bean="digestFilter" />
<!-- Basic authentication -->
<ref bean="basicAuthenticationFilter" />
<!-- 2-legged OAuth is used by trusted 3rd party applications, including LTI. -->
<!-- Uncomment the line below to support LTI or other OAuth clients. -->
<!-- <ref bean="oauthProtectedResourceFilter" /> -->
</list>
</property>
</bean>
<!-- ################################ -->
<!-- # Pre-Aauthentication Filters # -->
<!-- ################################ -->
<bean id="preAuthenticationFilters" class="org.springframework.web.filter.CompositeFilter">
<property name="filters">
<list>
<!-- Uncomment the line below to support Shibboleth. -->
<!-- <ref bean="shibbolethHeaderFilter" /> -->
<!-- Uncomment the line below to support JWT. -->
<ref bean="jwtHeaderFilter" />
<!-- Additionally/alternatively uncomment this to support passing a JWT in a URL parameter. -->
<ref bean="jwtRequestParameterFilter" />
</list>
</property>
</bean>
<!-- ########################################### -->
<!-- # Custom ajax timeout Filter Definition # -->
<!-- ########################################### -->
<bean id="asyncTimeoutRedirectFilter" class="org.opencastproject.kernel.security.AsyncTimeoutRedirectFilter" />
<!-- ######################################## -->
<!-- # Custom Anonymous Filter Definition # -->
<!-- ######################################## -->
<bean id="anonymousFilter" class="org.opencastproject.kernel.security.TrustedAnonymousAuthenticationFilter">
<property name="userAttribute" ref="anonymousUserAttributes" />
<property name="key" value="anonymousKey" />
</bean>
<bean id="anonymousUserAttributes" class="org.springframework.security.core.userdetails.memory.UserAttribute">
<property name="authoritiesAsString" value="ROLE_ANONYMOUS"/>
<property name="password" value="empty"/>
</bean>
<!-- ######################################## -->
<!-- # Authentication Entry and Exit Points # -->
<!-- ######################################## -->
<!-- Differentiates between "normal" user requests and those requesting digest auth -->
<bean id="opencastEntryPoint" class="org.opencastproject.kernel.security.DelegatingAuthenticationEntryPoint">
<!-- CAS Auth: Comment this if using CAS authentication -->
<property name="userEntryPoint" ref="userEntryPoint" />
<!-- CAS Auth: Uncomment this if using CAS authentication -->
<!-- <property name="userEntryPoint" ref="casEntryPoint" /> -->
<property name="digestAuthenticationEntryPoint" ref="digestEntryPoint" />
<property name="basicAuthenticationEntryPoint" ref="basicEntryPoint" />
</bean>
<!-- Redirects unauthenticated requests to the login form -->
<bean id="userEntryPoint" class="org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint">
<property name="loginFormUrl" value="/login.html" />
</bean>
<!-- Redirect unauthenticated requests to custom login url with configurable redirect query parameter
Example: http://localhost/Shibboleth.sso/Login?target=<RELATIVE_REQUEST_URL> -->
<!--bean id="userEntryPoint" class="org.opencastproject.kernel.security.RedirectQueryParamAuthenticationEntryPoint">
<constructor-arg index="0" value="/Shibboleth.sso/Login" />
<constructor-arg index="1" value="target" />
</bean-->
<!-- Returns a 401 request for authentication via digest auth -->
<bean id="digestEntryPoint" class="org.springframework.security.web.authentication.www.DigestAuthenticationEntryPoint">
<property name="realmName" value="Opencast" />
<property name="key" value="opencast" />
<property name="nonceValiditySeconds" value="300" />
</bean>
<bean id="basicEntryPoint" class="org.springframework.security.web.authentication.www.BasicAuthenticationEntryPoint">
<property name="realmName" value="Opencast"/>
</bean>
<bean id="authSuccessHandler" class="org.opencastproject.kernel.security.AuthenticationSuccessHandler">
<property name="securityService" ref="securityService" />
<property name="welcomePages">
<map>
<entry key="ROLE_ADMIN" value="/index.html" />
<entry key="ROLE_ADMIN_UI" value="/index.html" />
<entry key="*" value="/engage/ui/index.html" /> <!-- Any role not listed explicitly will redirect here -->
</map>
</property>
</bean>
<bean id="logoutSuccessHandler" class="org.opencastproject.kernel.security.LogoutSuccessHandler">
<property name="userDirectoryService" ref="userDirectoryService" />
<!-- Shibboleth log out
<property name="defaultTargetUrl" value="/Shibboleth.sso/Logout?return=www.opencast.org"/>
-->
</bean>
<!-- ################# -->
<!-- # Digest Filter # -->
<!-- ################# -->
<!-- Handles the details of the digest authentication dance -->
<bean id="digestFilter" class="org.springframework.security.web.authentication.www.DigestAuthenticationFilter">
<!-- Use only the in-memory users, as these have passwords that are not hashed -->
<property name="userDetailsService" ref="userDetailsService" />
<property name="authenticationEntryPoint" ref="digestEntryPoint" />
<property name="createAuthenticatedToken" value="true" />
<property name="userCache">
<bean class="org.springframework.security.core.userdetails.cache.NullUserCache" />
</property>
</bean>
<!-- ############################### -->
<!-- # Basic Authentication Filter # -->
<!-- ############################### -->
<!-- Handles the details of the basic authentication dance -->
<bean id="basicAuthenticationFilter" class="org.springframework.security.web.authentication.www.BasicAuthenticationFilter">
<property name="authenticationManager" ref="authenticationManager"/>
<property name="authenticationEntryPoint" ref="basicEntryPoint"/>
</bean>
<!-- ####################### -->
<!-- # OAuth (LTI) Support # -->
<!-- ####################### -->
<bean name="oauthProtectedResourceFilter" class="org.opencastproject.kernel.security.LtiProcessingFilter">
<property name="consumerDetailsService" ref="oAuthConsumerDetailsService" />
<property name="tokenServices">
<bean class="org.springframework.security.oauth.provider.token.InMemoryProviderTokenServices" />
</property>
<property name="nonceServices">
<bean class="org.springframework.security.oauth.provider.nonce.InMemoryNonceServices" />
</property>
<property name="authHandler" ref="ltiLaunchAuthenticationHandler" />
</bean>
<!-- ############### -->
<!-- # CAS Support # -->
<!-- ############### -->
<!--
<bean id="casFilter"
class="org.springframework.security.cas.web.CasAuthenticationFilter">
<property name="authenticationManager" ref="authenticationManager"/>
<property name="authenticationSuccessHandler" ref="authSuccessHandler" />
<property name="serviceProperties" ref="serviceProperties" />
<property name="authenticationDetailsSource">
<bean class="org.springframework.security.cas.web.authentication.ServiceAuthenticationDetailsSource"/>
</property>
</bean>
<bean id="casEntryPoint"
class="org.springframework.security.cas.web.CasAuthenticationEntryPoint">
<property name="loginUrl" value="https://auth-test.berkeley.edu/cas/login"/>
<property name="serviceProperties" ref="serviceProperties"/>
</bean>
<bean id="serviceProperties"
class="org.springframework.security.cas.ServiceProperties">
<property name="service" value="https://localhost/j_spring_cas_security_check"/>
<property name="sendRenew" value="false"/>
</bean>
<bean id="casAuthenticationProvider"
class="org.springframework.security.cas.authentication.CasAuthenticationProvider">
<property name="serviceProperties" ref="serviceProperties" />
<property name="authenticationUserDetailsService">
<bean class="org.springframework.security.core.userdetails.UserDetailsByNameServiceWrapper">
<constructor-arg ref="userDetailsService" />
</bean>
</property>
<property name="ticketValidator">
<bean class="org.jasig.cas.client.validation.Cas20ServiceTicketValidator">
<constructor-arg index="0" value="https://auth-test.berkeley.edu/cas" />
</bean>
</property>
<property name="key" value="cas"/>
</bean>
<bean id="singleLogoutFilter" class="org.jasig.cas.client.session.SingleSignOutFilter"/>
<bean id="requestSingleLogoutFilter" class="org.springframework.security.web.authentication.logout.LogoutFilter">
<constructor-arg value="https://auth-test.berkeley.edu/cas/logout"/>
<constructor-arg>
<bean class= "org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler"/>
</constructor-arg>
<property name="filterProcessesUrl" value="https://localhost/j_spring_security_logout"/>
</bean>
-->
<!-- ###################### -->
<!-- # Shibboleth Support # -->
<!-- ###################### -->
<!-- General Shibboleth header extration filter
<bean id="shibbolethHeaderFilter"
class="org.opencastproject.security.shibboleth.ShibbolethRequestHeaderAuthenticationFilter">
<property name="principalRequestHeader" value="<this need to be configured>"/>
<property name="authenticationManager" ref="authenticationManager" />
<property name="userDetailsService" ref="userDetailsService" />
<property name="userDirectoryService" ref="userDirectoryService" />
<property name="shibbolethLoginHandler" ref="aaiLoginHandler" />
<property name="exceptionIfHeaderMissing" value="false" />
</bean>-->
<!-- AAI header extractor and user generator
<bean id="aaiLoginHandler" class="org.opencastproject.security.aai.ConfigurableLoginHandler">
<property name="securityService" ref="securityService" />
<property name="userReferenceProvider" ref="userReferenceProvider" />
</bean>-->
<!-- Dynamic AAI Loginhandler
<bean id="aaiLoginHandler" class="org.opencastproject.security.aai.DynamicLoginHandler">
<property name="securityService" ref="securityService" />
<property name="userReferenceProvider" ref="userReferenceProvider" />
<property name="attributeMapper" ref="attributeMapper" />
</bean>
<bean id="attributeMapper" class="org.opencastproject.security.aai.api.AttributeMapper">
<property name="useHeader" value="true" />
<property name="multiValueDelimiter" value=";" />
<property name="attributeMap" ref="attributeMap" />
<property name="aaiAttributes" ref="aaiAttributes" />
</bean>
<util:list id="aaiAttributes" value-type="java.lang.String">
<value>sn</value>
<value>givenName</value>
<value>mail</value>
<value>homeOrganization</value>
<value>eduPersonEntitlement</value>
<value>eduPersonPrincipalName</value>
<value>homeOrganization</value>
</util:list>
<util:map id="attributeMap" map-class="java.util.HashMap">
<entry key="roles" value-ref="roleMapping" />
<entry key="displayName" value-ref="displayNameMapping" />
<entry key="mail" value-ref="mailMapping" />
</util:map>
<util:list id="displayNameMapping" value-type="java.lang.String">
<value>['givenName'][0] + ' ' + ['sn'][0]</value>
</util:list>
<util:list id="mailMapping" value-type="java.lang.String">
<value>['mail'][0]</value>
</util:list>
<util:list id="roleMapping" value-type="java.lang.String">
<value>'ROLE_AAI_USER'</value>
<value>'ROLE_AAI_USER_' + ['eduPersonPrincipalName']</value>
<value>('ROLE_AAI_OWNER_' + ['eduPersonPrincipalName']).replaceAll("[^a-zA-Z0-9]","_").toUpperCase()</value>
<value>['homeOrganization'] != null ? 'ROLE_AAI_ORG_' + ['homeOrganization'] + '_MEMBER' : null</value>
<value>['eduPersonEntitlement'].contains('urn:mace:opencast.org:permission:shibboleth:opencast_admin') ? 'ROLE_ADMIN' : null</value>
<value>['eduPersonPrincipalName'].contains('[email protected]') ? 'ROLE_ADMIN' : null</value>
<value>['eduPersonScopedAffiliation'].contains('[email protected]') ? 'ROLE_GROUP_AAI_TRAINER' : null</value>
</util:list>
-->
<bean id="preauthAuthProvider"
class="org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationProvider">
<property name="preAuthenticatedUserDetailsService">
<bean id="userDetailsServiceWrapper" class="org.springframework.security.core.userdetails.UserDetailsByNameServiceWrapper">
<property name="userDetailsService" ref="userDetailsService"/>
</bean>
</property>
</bean>
<!-- ################ -->
<!-- # JWT Support # -->
<!-- ################ -->
<!-- General JWT header extraction filter -->
<bean id="jwtHeaderFilter" class="org.opencastproject.security.jwt.JWTRequestHeaderAuthenticationFilter">
<property name="principalRequestHeader" value="Authorization"/>
<property name="principalPrefix" value="Bearer "/>
<property name="authenticationManager" ref="authenticationManager" />
<property name="loginHandler" ref="jwtLoginHandler" />
<property name="exceptionIfHeaderMissing" value="false" />
</bean>
<!-- General JWT request parameter extraction filter -->
<bean id="jwtRequestParameterFilter" class="org.opencastproject.security.jwt.JWTRequestParameterAuthenticationFilter">
<property name="parameterName" value="jwt" />
<property name="authenticationManager" ref="authenticationManager" />
<property name="loginHandler" ref="jwtLoginHandler" />
<property name="exceptionIfParameterMissing" 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" />
<property name="jwksUrl" value="https://tobira.opencast.org/.well-known/jwks.json" />
<property name="jwksCacheExpiresIn" value="1440" />
<property name="expectedAlgorithms" ref="jwtExpectedAlgorithms" />
<property name="claimConstraints" ref="jwtClaimConstraints" />
<property name="usernameMapping" value="['username'].asString()" />
<property name="nameMapping" value="['name'].asString()" />
<property name="emailMapping" value="['username'].asString() + '@tobira.invalid'" />
<property name="roleMappings" ref="jwtRoleMappings" />
<property name="jwtCacheSize" value="500" />
<property name="jwtCacheExpiresIn" value="60" />
</bean>
<!-- The signing algorithms expected for the JWT signature -->
<util:list id="jwtExpectedAlgorithms" value-type="java.lang.String">
<value>ES256</value>
<value>ES384</value>
</util:list>
<!-- The claim constraints that are expected to be fulfilled by the JWT
If you are using JWTs for OpenID Connect, see the specification for claims that must be validated:
https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation -->
<util:list id="jwtClaimConstraints" value-type="java.lang.String">
<value>containsKey('username')</value>
<value>containsKey('name')</value>
<value>containsKey('exp')</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_STUDIO'</value>
<value>'ROLE_ANONYMOUS'</value>
</util:list>
<!-- ################ -->
<!-- # LDAP Support # -->
<!-- ################ -->
<!--
<bean id="contextSource"
class="org.springframework.security.ldap.DefaultSpringSecurityContextSource">
< ! - - URL of the LDAP server - - >
<constructor-arg value="ldap://myldapserver:myport" />
< ! - - "Distinguished name" for the unprivileged user - - >
< ! - - This user is merely to perform searches in the LDAP to find the users to login - - >
<property name="userDn" value="uid=user-id,dc=example,dc=com" />
< ! - - Password of the user above - - >
<property name="password" value="mypassword" />
</bean>
-->
<!--
<bean id="userDetailsMapper" class="org.opencastproject.security.ldap.OpencastUserDetailsMapper">
<constructor-arg ref="userDetailsService" />
</bean>
-->
<!--
<bean id="ldapAuthProvider"
class="org.springframework.security.ldap.authentication.LdapAuthenticationProvider">
<constructor-arg>
<bean
class="org.springframework.security.ldap.authentication.BindAuthenticator">
<constructor-arg ref="contextSource" />
<property name="userDnPatterns">
<list>
< ! - - Dn patterns to search for valid users. Multiple "<value>" tags are allowed - - >
<value>uid={0},dc=example,dc=com</value>
</list>
</property>
< ! - - If your user IDs are not part of the user Dn's, you can use a search filter to find them - - >
< ! - - This property can be used together with the "userDnPatterns" above - - >
< ! - -
<property name="userSearch">
<bean name="filterUserSearch" class="org.springframework.security.ldap.search.FilterBasedLdapUserSearch">
< ! - - Base Dn from where the users will be searched for - - >
<constructor-arg index="0" value="ou=GroupName,dc=my-institution,dc=country" />
< ! - - Filter to located valid users. Use {0} as a placeholder for the login name - - >
<constructor-arg index="1" value="(uid={0})" />
<constructor-arg ref="contextSource" />
</bean>
</property>
- - >
</bean>
</constructor-arg>
< ! - - Retrieve user and attributes from opencasts user details service - - >
<property name="userDetailsContextMapper" ref="userDetailsMapper"/>
< ! - - Defines how the user attributes are converted to authorities (roles) - - >
< ! - - Output is ignored if used together with userDetailsMapper - - >
< ! - - constructor-arg ref="authoritiesPopulator" /- - >
</bean>
-->
<!-- #################### -->
<!-- # OSGI Integration # -->
<!-- #################### -->
<!-- Obtain services from the OSGI service registry -->
<osgi:reference id="userDetailsService" cardinality="1..1"
interface="org.springframework.security.core.userdetails.UserDetailsService" />
<osgi:reference id="securityService" cardinality="1..1"
interface="org.opencastproject.security.api.SecurityService" />
<!-- Uncomment to enable external users e.g. used together with shibboleth and JWT -->
<osgi:reference id="userReferenceProvider" cardinality="1..1"
interface="org.opencastproject.userdirectory.api.UserReferenceProvider" />
<osgi:reference id="userDirectoryService" cardinality="1..1"
interface="org.opencastproject.security.api.UserDirectoryService" />
<!-- Uncomment this as an alternative to the userDetailsMapper -->
<!-- Make sure you provide the same instanceId you used in org.opencastproject.userdirectory.ldap-….cfg -->
<!--
<osgi:reference id="authoritiesPopulator" cardinality="1..1"
interface="org.springframework.security.ldap.userdetails.LdapAuthoritiesPopulator"
filter="(instanceId=theId)"/>
-->
<osgi:reference id="oAuthConsumerDetailsService" cardinality="1..1"
interface="org.springframework.security.oauth.provider.ConsumerDetailsService" />
<osgi:reference id="ltiLaunchAuthenticationHandler" cardinality="1..1"
interface="org.springframework.security.oauth.provider.OAuthAuthenticationHandler" />
<!-- ############################# -->
<!-- # Spring Security Internals # -->
<!-- ############################# -->
<bean id="passwordEncoder" class="org.opencastproject.kernel.security.CustomPasswordEncoder" />
<sec:authentication-manager alias="authenticationManager">
<sec:authentication-provider ref="rememberMeAuthenticationProvider"/>
<!-- CAS Auth: Uncomment this if using CAS authentication -->
<!-- <sec:authentication-provider ref="casAuthenticationProvider" /> -->
<!-- Uncomment this if using Shibboleth or JWT authentication -->
<sec:authentication-provider ref="preauthAuthProvider" />
<!-- Uncomment the following line if using LDAP -->
<!-- <sec:authentication-provider ref="ldapAuthProvider" /> -->
<sec:authentication-provider user-service-ref="userDetailsService">
<!-- The JPA user directory stores bcrypt hashed passwords, but still works with legacy md5 hashes -->
<sec:password-encoder ref="passwordEncoder">
<!-- This salt is used only for decoding legacy MD5 hased passwords -->
<sec:salt-source user-property="username" />
</sec:password-encoder>
</sec:authentication-provider>
</sec:authentication-manager>
<!-- Do not use a request cache -->
<bean id="requestCache" class="org.springframework.security.web.savedrequest.NullRequestCache" />
</beans>-
Start Opencast with Java 16 or newer.
-
Run:
curl localhost:8080/info/me.json?jwt=eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6InNhYmluZUBleGFtcGxlLm9yZyIsImV4cCI6MTcyMzQ2NTgzNCwibmFtZSI6IlNhYmluZSBSdWRvbGZzIiwidXNlcm5hbWUiOiJzYWJpbmUifQ.uU38Ij7e6TStwl71uMCb06ap4CXZq4pEk7QaLpvY4dZv6oNqM2txiz6JEGW6bN11ivyJc_X1z6FEy5JzCL4DxQObserve how you are getting back "anonymous".
-
Paste the JWT form the above
curlcommand to jwt.io and paste the public key{"kty":"EC","crv":"P-256","x":"bB567XHcsKqNCf2tZPjam6zf2tyw12owDuJp21k4p3I","y":"-qwHUvUahcvG47JlxpWnJg27V9-QX8Zgk_2afZEwGpk","use":"sig","alg":"ES256"}from here in the appropriate field. Note the "Signature verified".
If you want to generate your own keys & JWTs, you can run Tobira as described here. In the OC security config, you only have to change the URL from https://tobira.opencast.org to http://localhost:3080.
Expected behavior
The JWTs are accepted by OC.
Server environment:
- OS: Ubuntu 20.04
- Distribution: allinone
- Installation method: local dev build
- Version 16.1