Skip to content

Commit 38ec4aa

Browse files
committed
For #6668: add support for WildFly OIDC in FormRunnerAuthFilter
1 parent 0dd6066 commit 38ec4aa

File tree

5 files changed

+146
-54
lines changed

5 files changed

+146
-54
lines changed

build.sbt

+4-3
Original file line numberDiff line numberDiff line change
@@ -713,9 +713,10 @@ lazy val formRunnerJVM = formRunner.jvm
713713
DebugDatabaseTest / sourceDirectory := (DatabaseTest / sourceDirectory).value,
714714
DebugDatabaseTest / javaOptions += "-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=5005"
715715
).settings(
716-
libraryDependencies += "javax.servlet" % "javax.servlet-api" % JavaxServletApiVersion % Provided,
717-
libraryDependencies += "jakarta.servlet" % "jakarta.servlet-api" % JakartaServletApiVersion % Provided,
718-
libraryDependencies += "javax.portlet" % "portlet-api" % PortletApiVersion % Provided,
716+
libraryDependencies += "javax.servlet" % "javax.servlet-api" % JavaxServletApiVersion % Provided,
717+
libraryDependencies += "jakarta.servlet" % "jakarta.servlet-api" % JakartaServletApiVersion % Provided,
718+
libraryDependencies += "javax.portlet" % "portlet-api" % PortletApiVersion % Provided,
719+
libraryDependencies += "org.wildfly.security" % "wildfly-elytron-http-oidc" % "2.6.0.Final" % Provided,
719720

720721
libraryDependencies += "org.scala-lang.modules" %% "scala-parallel-collections" % ScalaParallelCollectionsVersion,
721722

form-runner/jvm/src/main/scala/org/orbeon/oxf/fr/FormRunnerAuth.scala

+17-9
Original file line numberDiff line numberDiff line change
@@ -39,14 +39,23 @@ object FormRunnerAuth {
3939
Headers.OrbeonCredentials
4040
)
4141

42-
import Private._
42+
import Private.*
4343

4444
def getCredentialsAsHeadersUseSession(
45-
userRoles : UserRolesFacade,
46-
session : ExternalContext.Session,
47-
getHeader : String => List[String]
45+
userRoles: UserRolesFacade,
46+
session : ExternalContext.Session,
47+
getHeader: String => List[String]
4848
): List[(String, NonEmptyList[String])] =
49-
getCredentialsUseSession(userRoles, session, getHeader) match {
49+
getCredentialsAsHeadersUseSession(
50+
findCredentialsFromContainerOrHeaders(userRoles, getHeader),
51+
session
52+
)
53+
54+
def getCredentialsAsHeadersUseSession(
55+
credentialsOpt: => Option[Credentials],
56+
session : ExternalContext.Session
57+
): List[(String, NonEmptyList[String])] =
58+
getCredentialsUseSession(credentialsOpt, session) match {
5059
case Some(credentials) =>
5160
val result = CredentialsSerializer.toHeaders[List](credentials)
5261
Logger.debug(s"setting auth headers to: ${headersAsJSONString(result)}")
@@ -109,17 +118,16 @@ object FormRunnerAuth {
109118
// - https://github.com/orbeon/orbeon-forms/issues/4436
110119
//
111120
def getCredentialsUseSession(
112-
userRoles : UserRolesFacade,
113-
session : ExternalContext.Session,
114-
getHeader : String => List[String]
121+
credentialsOpt: => Option[Credentials],
122+
session : ExternalContext.Session
115123
): Option[Credentials] = {
116124

117125
val sessionCredentialsOpt = ServletPortletRequest.findCredentialsInSession(session)
118126

119127
lazy val stickyHeadersConfigured =
120128
Properties.instance.getPropertySet.getBoolean(HeaderStickyPropertyName, default = false)
121129

122-
lazy val newCredentialsOpt = findCredentialsFromContainerOrHeaders(userRoles, getHeader)
130+
lazy val newCredentialsOpt = credentialsOpt
123131

124132
def storeAndReturnNewCredentials(): Option[Credentials] = {
125133
ServletPortletRequest.storeCredentialsInSession(session, newCredentialsOpt)

form-runner/jvm/src/main/scala/org/orbeon/oxf/servlet/FormRunnerAuthFilter.scala

+78-37
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,15 @@
1313
*/
1414
package org.orbeon.oxf.servlet
1515

16+
import cats.data.NonEmptyList
1617
import org.apache.logging.log4j.ThreadContext
17-
import org.orbeon.oxf.externalcontext.ServletPortletRequest
18+
import org.orbeon.oxf.externalcontext.{Credentials, ServletPortletRequest}
1819
import org.orbeon.oxf.fr.FormRunnerAuth
1920
import org.orbeon.oxf.http.Headers
2021
import org.orbeon.oxf.properties.Properties
2122
import org.orbeon.oxf.util.StringUtils.*
2223
import org.slf4j.LoggerFactory
2324

24-
import scala.jdk.CollectionConverters.*
25-
2625
// For backward compatibility
2726
class FormRunnerAuthFilter extends JavaxFormRunnerAuthFilter
2827

@@ -31,7 +30,7 @@ class JakartaFormRunnerAuthFilter extends JakartaFilter(new FormRunnerAuthFilter
3130

3231
class FormRunnerAuthFilterImpl extends Filter {
3332

34-
import FormRunnerAuthFilterImpl._
33+
import FormRunnerAuthFilterImpl.*
3534

3635
private case class FilterSettings(contentSecurityPolicy: Option[String])
3736

@@ -85,48 +84,90 @@ object FormRunnerAuthFilterImpl {
8584
// The Form Runner service path is hardcoded but that's ok. When we are filtering a service, we don't retrieve the
8685
// credentials, which would be provided by the container or by incoming headers. Instead, credentials are provided
8786
// directly with `Orbeon-*` headers. See https://github.com/orbeon/orbeon-forms/issues/2275
88-
val requestWithAmendedHeaders =
89-
if (servletRequest.getRequestPathInfo.startsWith("/fr/service/")) {
90-
91-
// `ServletPortletRequest` gets credentials from the session, which means we need to store the credentials into
92-
// the session. This is done by `getCredentialsAsHeadersUseSession()` if we are not a service, but here we
93-
// don't use that function so we need to do make sure they are stored.
94-
95-
ServletPortletRequest.findCredentialsInSession(httpSession) match {
96-
case None =>
97-
ServletPortletRequest.storeCredentialsInSession(
98-
httpSession,
99-
FormRunnerAuth.fromHeaderValues(
100-
credentialsOpt = servletRequest.headerFirstValueOpt(Headers.OrbeonCredentials),
101-
usernameOpt = servletRequest.headerFirstValueOpt(Headers.OrbeonUsername),
102-
rolesList = getHttpHeaders(Headers.OrbeonRoles),
103-
groupOpt = servletRequest.headerFirstValueOpt(Headers.OrbeonGroup),
104-
)
105-
)
106-
case Some(_) =>
107-
}
87+
val ServicePath = "/fr/service/"
10888

109-
servletRequest
110-
} else if (servletRequest.getRequestPathInfo.endsWith(".map")) {
89+
val requestWithAmendedHeaders =
90+
if (WildflyOidcAuth.hasWildflyOidcAuth(servletRequest)) {
91+
credentialsFromSessionOrParameter(httpSession, servletRequest, WildflyOidcAuth.credentialsOpt(servletRequest))
92+
} else if (servletRequest.getRequestPathInfo.startsWith(ServicePath)) {
93+
credentialsFromSessionOrHeaders(httpSession, servletRequest, getHttpHeaders)
94+
} else if (servletRequest.isSourceMap) {
11195
// Don't amend headers for `.map` as this would cause the credentials code to clear the credentials
11296
// unnecessarily. https://github.com/orbeon/orbeon-forms/issues/6080
11397
servletRequest
11498
} else {
115-
116-
trait CustomHeaders extends RequestRemoveHeaders with RequestPrependHeaders {
117-
override def headersToRemoveAsSet: Set[String] = FormRunnerAuth.AllAuthHeaderNames
118-
val headersToPrependAsMap = FormRunnerAuth.getCredentialsAsHeadersUseSession(
119-
userRoles = servletRequest,
120-
session = httpSession,
121-
getHeader = getHttpHeaders
122-
).toMap
123-
}
124-
125-
new HttpServletRequestWrapper(servletRequest) with CustomHeaders
99+
credentialsFromSessionHeadersOrContainer(httpSession, servletRequest, getHttpHeaders)
126100
}
127101

128102
logger.debug(s"amended headers:\n${requestWithAmendedHeaders.headersAsString}")
129103

130104
requestWithAmendedHeaders
131105
}
106+
107+
private def credentialsFromSessionOrHeaders(
108+
httpSession : ServletSessionImpl,
109+
servletRequest: HttpServletRequest,
110+
getHttpHeaders: String => List[String]
111+
): HttpServletRequest = {
112+
113+
// `ServletPortletRequest` gets credentials from the session, which means we need to store the credentials into
114+
// the session. This is done by `getCredentialsAsHeadersUseSession()` if we are not a service, but here we
115+
// don't use that function so we need to do make sure they are stored.
116+
117+
ServletPortletRequest.findCredentialsInSession(httpSession) match {
118+
case None =>
119+
ServletPortletRequest.storeCredentialsInSession(
120+
httpSession,
121+
FormRunnerAuth.fromHeaderValues(
122+
credentialsOpt = servletRequest.headerFirstValueOpt(Headers.OrbeonCredentials),
123+
usernameOpt = servletRequest.headerFirstValueOpt(Headers.OrbeonUsername),
124+
rolesList = getHttpHeaders(Headers.OrbeonRoles),
125+
groupOpt = servletRequest.headerFirstValueOpt(Headers.OrbeonGroup),
126+
)
127+
)
128+
case Some(_) =>
129+
}
130+
131+
servletRequest
132+
}
133+
134+
private def credentialsFromSessionOrParameter(
135+
httpSession : ServletSessionImpl,
136+
servletRequest: HttpServletRequest,
137+
credentialsOpt: => Option[Credentials]
138+
): HttpServletRequest =
139+
requestWithCredentialsHeaders(
140+
servletRequest = servletRequest,
141+
credentialHeaders = FormRunnerAuth.getCredentialsAsHeadersUseSession(
142+
credentialsOpt = credentialsOpt,
143+
session = httpSession
144+
).toMap
145+
)
146+
147+
private def credentialsFromSessionHeadersOrContainer(
148+
httpSession : ServletSessionImpl,
149+
servletRequest: HttpServletRequest,
150+
getHttpHeaders: String => List[String]
151+
): HttpServletRequest =
152+
requestWithCredentialsHeaders(
153+
servletRequest = servletRequest,
154+
credentialHeaders = FormRunnerAuth.getCredentialsAsHeadersUseSession(
155+
userRoles = servletRequest,
156+
session = httpSession,
157+
getHeader = getHttpHeaders
158+
).toMap
159+
)
160+
161+
private def requestWithCredentialsHeaders(
162+
servletRequest : HttpServletRequest,
163+
credentialHeaders: Map[String, NonEmptyList[String]]
164+
): HttpServletRequest = {
165+
166+
trait CustomHeaders extends RequestRemoveHeaders with RequestPrependHeaders {
167+
override def headersToRemoveAsSet: Set[String] = FormRunnerAuth.AllAuthHeaderNames
168+
val headersToPrependAsMap: Map[String, NonEmptyList[String]] = credentialHeaders
169+
}
170+
171+
new HttpServletRequestWrapper(servletRequest) with CustomHeaders
172+
}
132173
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/**
2+
* Copyright (C) 2024 Orbeon, Inc.
3+
*
4+
* This program is free software; you can redistribute it and/or modify it under the terms of the
5+
* GNU Lesser General Public License as published by the Free Software Foundation; either version
6+
* 2.1 of the License, or (at your option) any later version.
7+
*
8+
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
9+
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
10+
* See the GNU Lesser General Public License for more details.
11+
*
12+
* The full text of the license is available at http://www.gnu.org/copyleft/lesser.html
13+
*/
14+
package org.orbeon.oxf.servlet
15+
16+
import org.orbeon.oxf.externalcontext.{Credentials, SimpleRole, UserAndGroup}
17+
import org.wildfly.security.http.oidc.OidcSecurityContext
18+
19+
import scala.jdk.CollectionConverters.*
20+
21+
object WildflyOidcAuth {
22+
23+
private val OidcSecurityContextClassName = "org.wildfly.security.http.oidc.OidcSecurityContext"
24+
private val ObjectIDClaimName = "oid"
25+
26+
def hasWildflyOidcAuth(servletRequest: HttpServletRequest): Boolean =
27+
Option(servletRequest.getAttribute(OidcSecurityContextClassName)).isDefined
28+
29+
def credentialsOpt(servletRequest: HttpServletRequest): Option[Credentials] =
30+
Option(servletRequest.getAttribute(OidcSecurityContextClassName)) collect {
31+
case oidcSecurityContext: OidcSecurityContext =>
32+
33+
val objectIdOpt = Option(oidcSecurityContext.getToken.getClaimValue(ObjectIDClaimName)).map(_.toString)
34+
val objectId = objectIdOpt.getOrElse(throw new RuntimeException(s"Claim '$ObjectIDClaimName' not found in OIDC token"))
35+
val roleIds = oidcSecurityContext.getToken.getRolesClaim.asScala.toList
36+
37+
Credentials(
38+
userAndGroup = UserAndGroup(username = objectId, groupname = None),
39+
roles = roleIds.map(SimpleRole.apply),
40+
organizations = Nil
41+
)
42+
}
43+
}

servlet-support/src/main/scala/org/orbeon/oxf/servlet/HttpServletRequest.scala

+4-5
Original file line numberDiff line numberDiff line change
@@ -131,11 +131,10 @@ trait HttpServletRequest extends ServletRequest {
131131
def isFont: Boolean = hasFileExtension(Set("otf", "ttf", "woff", "woff2"))
132132
def isSourceMap: Boolean = hasFileExtension(Set("map"))
133133

134-
private def hasFileExtension(extensions: Set[String]): Boolean =
135-
(for {
136-
url <- Option(getRequestURL)
137-
path <- Option(URI.create(url.toString).getPath)
138-
} yield extensions.exists(ext => path.endsWith(s".$ext"))).getOrElse(false)
134+
private def hasFileExtension(extensions: Set[String]): Boolean = {
135+
val requestPathInfo = getRequestPathInfo
136+
extensions.exists(ext => requestPathInfo.endsWith(s".$ext"))
137+
}
139138
}
140139

141140
class JavaxHttpServletRequest(httpServletRequest: javax.servlet.http.HttpServletRequest) extends JavaxServletRequest(httpServletRequest) with HttpServletRequest {

0 commit comments

Comments
 (0)