Hello folks,
Does anyone have a working CAS 5.x deployment delegating authentication via OpenID Connect? I'd love to pick your brain.
I
am working on a CAS 5.3.9 deployment, testing CAS delegation to Azure
AD via OpenID Connect. I think I am close, but I'm running into a
problem.
When
I try to access '/cas/clientredirect?client_name=REDACTED' via the link
on the `/cas/login' page, I receive a 500 error with a stacktrace as
follows below. There is no STDOUT/STDERR emitted when this occurs, and I
have tweaked my log levels so that I would expect to see *something*.
I'll share those later.
What
worries me, is if I start nosing around in IDEA, the IDE also throws a
hint that this is an invalid cast. (I used './gradlew idea' in a clone
of the cas repository, and opened the generated idea project in IntelliJ
IDEA Ultimate).
Here's the stack trace:
org.pac4j.core.exception.TechnicalException: java.lang.ClassCastException: java.util.Collections$SingletonList cannot be cast to java.lang.String
at org.pac4j.oidc.redirect.OidcRedirectActionBuilder.buildAuthenticationRequestUrl(OidcRedirectActionBuilder.java:113)
at org.pac4j.oidc.redirect.OidcRedirectActionBuilder.redirect(OidcRedirectActionBuilder.java:78)
at org.pac4j.core.client.IndirectClient.getRedirectAction(IndirectClient.java:109)
at org.apereo.cas.web.DelegatedClientNavigationController.redirectToProvider(DelegatedClientNavigationController.java:83)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:205)
at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:133)
at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:97)
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:849)
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:760)
at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:85)
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:967)
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:901)
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:970)
at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:861)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:687)
at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:846)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:790)
at org.eclipse.jetty.servlet.ServletHolder.handle(ServletHolder.java:867)
at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1623)
at org.eclipse.jetty.websocket.server.WebSocketUpgradeFilter.doFilter(WebSocketUpgradeFilter.java:214)
at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1610)
at org.apereo.cas.web.support.AuthenticationCredentialsThreadLocalBinderClearingFilter.doFilter(AuthenticationCredentialsThreadLocalBinderClearingFilter.java:30)
at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1610)
at org.apereo.cas.security.RequestParameterPolicyEnforcementFilter.doFilter(RequestParameterPolicyEnforcementFilter.java:261)
at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1610)
at org.apereo.cas.security.ResponseHeadersEnforcementFilter.doFilter(ResponseHeadersEnforcementFilter.java:240)
at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1610)
at org.apereo.cas.security.AddResponseHeadersFilter.doFilter(AddResponseHeadersFilter.java:94)
at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1610)
at org.springframework.boot.actuate.trace.WebRequestTraceFilter.doFilterInternal(WebRequestTraceFilter.java:111)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1610)
at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:99)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1610)
at org.springframework.web.filter.HttpPutFormContentFilter.doFilterInternal(HttpPutFormContentFilter.java:109)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1610)
at org.springframework.web.filter.HiddenHttpMethodFilter.doFilterInternal(HiddenHttpMethodFilter.java:93)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1610)
at org.apereo.cas.logging.web.ThreadContextMDCServletFilter.doFilter(ThreadContextMDCServletFilter.java:91)
at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1610)
at org.springframework.boot.actuate.autoconfigure.MetricsFilter.doFilterInternal(MetricsFilter.java:103)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1610)
at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:197)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1610)
at org.springframework.boot.web.support.ErrorPageFilter.doFilter(ErrorPageFilter.java:130)
at org.springframework.boot.web.support.ErrorPageFilter.access$000(ErrorPageFilter.java:66)
at org.springframework.boot.web.support.ErrorPageFilter$1.doFilterInternal(ErrorPageFilter.java:105)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
at org.springframework.boot.web.support.ErrorPageFilter.doFilter(ErrorPageFilter.java:123)
at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1610)
at org.apereo.inspektr.common.web.ClientInfoThreadLocalFilter.doFilter(ClientInfoThreadLocalFilter.java:66)
at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1610)
at org.apache.logging.log4j.web.Log4jServletFilter.doFilter(Log4jServletFilter.java:71)
at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1610)
at org.eclipse.jetty.servlet.ServletHandler.doHandle(ServletHandler.java:540)
at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:146)
at org.eclipse.jetty.security.SecurityHandler.handle(SecurityHandler.java:548)
at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:132)
at org.eclipse.jetty.server.handler.ScopedHandler.nextHandle(ScopedHandler.java:257)
at org.eclipse.jetty.server.session.SessionHandler.doHandle(SessionHandler.java:1588)
at org.eclipse.jetty.server.handler.ScopedHandler.nextHandle(ScopedHandler.java:255)
at org.eclipse.jetty.server.handler.ContextHandler.doHandle(ContextHandler.java:1345)
at org.eclipse.jetty.server.handler.ScopedHandler.nextScope(ScopedHandler.java:203)
at org.eclipse.jetty.servlet.ServletHandler.doScope(ServletHandler.java:480)
at org.eclipse.jetty.server.session.SessionHandler.doScope(SessionHandler.java:1557)
at org.eclipse.jetty.server.handler.ScopedHandler.nextScope(ScopedHandler.java:201)
at org.eclipse.jetty.server.handler.ContextHandler.doScope(ContextHandler.java:1247)
at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:144)
at org.eclipse.jetty.server.handler.ContextHandlerCollection.handle(ContextHandlerCollection.java:220)
at org.eclipse.jetty.server.handler.HandlerCollection.handle(HandlerCollection.java:126)
at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:132)
at org.eclipse.jetty.server.Server.handle(Server.java:502)
at org.eclipse.jetty.server.HttpChannel.handle(HttpChannel.java:364)
at org.eclipse.jetty.server.HttpChannel.run(HttpChannel.java:305)
at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.runTask(EatWhatYouKill.java:333)
at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.doProduce(EatWhatYouKill.java:310)
at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.tryProduce(EatWhatYouKill.java:168)
at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.produce(EatWhatYouKill.java:132)
at org.eclipse.jetty.http2.HTTP2Connection.produce(HTTP2Connection.java:171)
at org.eclipse.jetty.http2.HTTP2Connection.onFillable(HTTP2Connection.java:126)
at org.eclipse.jetty.http2.HTTP2Connection$FillableCallback.succeeded(HTTP2Connection.java:338)
at org.eclipse.jetty.io.FillInterest.fillable(FillInterest.java:103)
at org.eclipse.jetty.io.ssl.SslConnection$DecryptedEndPoint.onFillable(SslConnection.java:411)
at org.eclipse.jetty.io.ssl.SslConnection.onFillable(SslConnection.java:305)
at org.eclipse.jetty.io.ssl.SslConnection$2.succeeded(SslConnection.java:159)
at org.eclipse.jetty.io.FillInterest.fillable(FillInterest.java:103)
at org.eclipse.jetty.io.ChannelEndPoint$2.run(ChannelEndPoint.java:118)
at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.runTask(EatWhatYouKill.java:333)
at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.doProduce(EatWhatYouKill.java:310)
at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.tryProduce(EatWhatYouKill.java:168)
at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.run(EatWhatYouKill.java:126)
at org.eclipse.jetty.util.thread.ReservedThreadExecutor$ReservedThread.run(ReservedThreadExecutor.java:366)
at org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:765)
at org.eclipse.jetty.util.thread.QueuedThreadPool$2.run(QueuedThreadPool.java:683)
at java.lang.Thread.run(Thread.java:748)
Caused by: java.lang.ClassCastException: java.util.Collections$SingletonList cannot be cast to java.lang.String
at com.nimbusds.oauth2.sdk.AuthorizationRequest.parse(AuthorizationRequest.java:972)
at com.nimbusds.openid.connect.sdk.AuthenticationRequest.parse(AuthenticationRequest.java:1374)
at com.nimbusds.openid.connect.sdk.AuthenticationRequest.parse(AuthenticationRequest.java:1340)
at org.pac4j.oidc.redirect.OidcRedirectActionBuilder.buildAuthenticationRequestUrl(OidcRedirectActionBuilder.java:110)
... 103 more
Here's the code from pac4j in question:
protected String buildAuthenticationRequestUrl(final Map<String, String> params) {
// Build authentication request query string
final String queryString;
try {
queryString = AuthenticationRequest.parse(params.entrySet().stream().collect(
Collectors.toMap(Map.Entry::getKey, e -> Collections.singletonList(e.getValue())))).toQueryString();
} catch (Exception e) {
throw new TechnicalException(e);
}
return configuration.getProviderMetadata().getAuthorizationEndpointURI().toString() + "?" + queryString;
}
IDEA
whines about two things, here. I've underlined them both. The first is
"Non-static method cannot be referenced from a static context" and the
second "Bad return type in lambda expression: List<String> cannot
be converted to U." Interestingly this second issue is exactly what
lives at the top of the stacktrace... Is this a bug? In case it's not,
and I've just done something wrong or silly...
Now, for some build/environment stuff. I'm stuffing cas.war into a docker container with jetty (tweaked to work around a bug). I'm building a standalone WAR without tomcat etc. Note that I have my own pom.xml based on cas-overlay. This
deployment is working fine for several other CAS deployments that I
haven't yet pulled forward from CAS 5.2.x, which use JDBC authentication
to an Oracle instance instead.
Here's
what the relevant (I think) section of my cas.properties looks like,
with appropriate fields redacted. The log4j2.xml is unmodified except
for changing the on-disk output paths.
logging.level.org.apereo.cas=INFO
logging.level.org.springframework=FATAL
logging.level.org.apereo.inspektr.audit=INFO
logging.level.org.apereo.cas.support.pac4j=DEBUG
logging.level.org.pac4j=DEBUG
logging.config=file:/etc/cas/config/log4j2.xml
cas.authn.pac4j.cookie.crypto.encryption.key=REDACTED
cas.authn.pac4j.cookie.crypto.signing.key=REDACTED
cas.authn.pac4j.name=DelegatePAC4J
cas.authn.pac4j.oidc[0].autoRedirect=false
cas.authn.pac4j.oidc[0].azureTenantId=REDACTED.onmicrosoft.com
cas.authn.pac4j.oidc[0].clientName=REDACTED
cas.authn.pac4j.oidc[0].discoveryUri=https://login.microsoftonline.com/REDACTED.onmicrosoft.com/v2.0/.well-known/openid-configuration
cas.authn.pac4j.oidc[0].id=REDACTED
cas.authn.pac4j.oidc[0].logoutUrl=https://login.microsoftonline.com/REDACTED.onmicrosoft.com}/oauth2/v2.0/logout
cas.authn.pac4j.oidc[0].maxClockSkew=600
cas.authn.pac4j.oidc[0].preferredJwsAlgorithm=RS256
cas.authn.pac4j.oidc[0].principalAttributeId=oid
cas.authn.pac4j.oidc[0].responseMode=form_post
cas.authn.pac4j.oidc[0].responseType=code
cas.authn.pac4j.oidc[0].scope=openid profile
cas.authn.pac4j.oidc[0].secret=REDACTED
cas.authn.pac4j.oidc[0].type=AZURE
cas.authn.pac4j.oidc[0].useNonce=true
cas.authn.pac4j.oidc[0].usePathBasedCallbackUrl=true
cas.authn.policy.req.enabled=true
cas.authn.policy.req.handlerName=DelegatePAC4J
cas.authn.policy.req.tryAll=false
I have also tried the non-v2.0 URLs but saw no difference.
I
see the Pac4jAuthenticationEventExecutionPlanConfiguration emit the
following log entry while CAS is initializing, the contents look OK to
me (newlines added for clarity)
DEBUG [org.apereo.cas.support.pac4j.config.support.authentication.Pac4jAuthenticationEventExecutionPlanConfiguration] - <
The following clients are built: [
[
#AzureAdClient# |
name: REDACTED |
callbackUrl: null |
callbackUrlResolver: org.pac4j.core.http.callback.PathParameterCallbackUrlResolver@74bfba13 |
ajaxRequestResolver: null |
redirectActionBuilder: null |
credentialsExtractor: null |
authenticator: null |
profileCreator: org.pac4j.core.profile.creator.AuthenticatorProfileCreator@52907b1e |
logoutActionBuilder: org.pac4j.core.logout.NoLogoutActionBuilder@1865ede |
authorizationGenerators: [] |
configuration: #AzureAdOidcConfiguration# |
clientId: REDACTED |
secret: [protected] |
discoveryURI: https://login.microsoftonline.com/REDACTED.onmicrosoft.com/.well-known/openid-configuration |
scope: openid profile |
customParams: {} |
clientAuthenticationMethod: null |
useNonce: true |
preferredJwsAlgorithm: RS256 |
maxAge: null |
maxClockSkew: 600 |
connectTimeout: 500 |
readTimeout: 5000 |
resourceRetriever: null |
responseType: code |
responseMode: form_post |
logoutUrl: https://login.microsoftonline.com/REDACTED.onmicrosoft.com/oauth2/v2.0/logout |
withState: false |
stateGenerator: org.pac4j.core.state.StaticOrRandomStateGenerator@68b2c3aa |
|
]
]
>
I
wouldn't be surprised in the least to find i'm doing something stupid,
here. The docs just throw parameter names at you, with no explanation or
type information - and I had to refer to the source code many times.
For giggles, my pom.xml:
<?xml version="1.0" encoding="UTF-8"?>
<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/xsd/maven-4.0.0.xsd ">
<modelVersion>4.0.0</modelVersion>
<groupId>edu.usf.epi</groupId>
<artifactId>cas-overlay</artifactId>
<packaging>war</packaging>
<version>1.0</version>
<properties>
<cas.version>5.3.9</cas.version>
<springboot.version>1.5.18.RELEASE</springboot.version>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<mainClassName>org.springframework.boot.loader.WarLauncher</mainClassName>
<isExecutable>false</isExecutable>
<manifestFileToUse>${project.build.directory}/war/work/org.apereo.cas/cas-server-webapp/META-INF/MANIFEST.MF</manifestFileToUse>
</properties>
<profiles>
<profile>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<id>default</id>
<dependencies>
<dependency>
<groupId>org.apereo.cas</groupId>
<artifactId>cas-server-webapp</artifactId>
<version>${cas.version}</version>
<type>war</type>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.apereo.cas</groupId>
<artifactId>cas-server-support-pac4j-webflow</artifactId>
<version>${cas.version}</version>
</dependency>
<dependency>
<groupId>org.apereo.cas</groupId>
<artifactId>cas-server-support-json-service-registry</artifactId>
<version>${cas.version}</version>
</dependency>
<dependency>
<groupId>org.apereo.cas</groupId>
<artifactId>cas-server-support-redis-ticket-registry</artifactId>
<version>${cas.version}</version>
</dependency>
<dependency>
<!-- see also Dockerfile and go.sh -->
<groupId>com.oracle.jdbc</groupId>
<artifactId>ojdbc8</artifactId>
<version>12.2.0.1</version>
</dependency>
</dependencies>
</profile>
</profiles>
<repositories>
<repository>
<id>sonatype-releases</id>
<url>http://oss.sonatype.org/content/repositories/releases/</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
<releases>
<enabled>true</enabled>
</releases>
</repository>
<repository>
<id>sonatype-snapshots</id>
<url>https://oss.sonatype.org/content/repositories/snapshots/</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
<releases>
<enabled>false</enabled>
</releases>
</repository>
<repository>
<id>shibboleth-releases</id>
<url>https://build.shibboleth.net/nexus/content/repositories/releases</url>
</repository>
</repositories>
<build>
<plugins> <!-- keep versions in sync with ../cas-redirect/pom.xml for efficiency -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${springboot.version}</version>
<configuration>
<mainClass>${mainClassName}</mainClass>
<addResources>true</addResources>
<executable>${isExecutable}</executable>
<layout>WAR</layout>
<excludeDevtools>true</excludeDevtools>
</configuration>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>2.6</version>
<configuration>
<warName>cas</warName>
<failOnMissingWebXml>false</failOnMissingWebXml>
<recompressZippedFiles>false</recompressZippedFiles>
<archive>
<compress>false</compress>
<manifestFile>${manifestFileToUse}</manifestFile>
</archive>
<overlays>
<overlay>
<groupId>org.apereo.cas</groupId>
<artifactId>cas-server-webapp</artifactId>
</overlay>
</overlays>
<!-- because excludeDevtools is broken in spring-boot-maven-plugin, we use the nuclear option. -->
<!-- also tomcat leaks in and 'mvn dependency:tree' doesn't show why... -->
<packagingExcludes>
WEB-INF/lib/spring-boot-devtools-*.jar,
WEB-INF/lib/spring-boot-starter-tomcat-*.jar,
WEB-INF/lib/tomcat-*.jar
</packagingExcludes>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.3</version>
</plugin>
</plugins>
<finalName>cas</finalName>
</build>
</project>