Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add client_secret to oidc requests #7263

Merged
merged 2 commits into from
Aug 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.unreleased.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.released
- Added a new button to the layer settings in view only dataset mode to save the current view configuration as the dataset's default. [#7205](https://github.com/scalableminds/webknossos/pull/7205)
- Added option to select multiple segments in the segment list in order to perform batch actions. [#7242](https://github.com/scalableminds/webknossos/pull/7242)
- If a dataset layer is transformed (using an affine matrix or a thin plate spline), it can be dynamically shown without that transform via the layers sidebar. All other layers will be transformed accordingly. [#7246](https://github.com/scalableminds/webknossos/pull/7246)
- OpenID Connect authorization can now use a client secret for added security. [#7263](https://github.com/scalableminds/webknossos/pull/7263)

### Changed
- Small messages during annotating (e.g. “finished undo”, “applying mapping…”) are now click-through so they do not block users from selecting tools. [7239](https://github.com/scalableminds/webknossos/pull/7239)
Expand Down
32 changes: 20 additions & 12 deletions app/oxalis/security/OpenIdConnectClient.scala
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,13 @@ import scala.concurrent.ExecutionContext

class OpenIdConnectClient @Inject()(rpc: RPC, conf: WkConf)(implicit executionContext: ExecutionContext) {

lazy val oidcConfig: OpenIdConnectConfig =
OpenIdConnectConfig(conf.SingleSignOn.OpenIdConnect.providerUrl, conf.SingleSignOn.OpenIdConnect.clientId)
private lazy val oidcConfig: OpenIdConnectConfig =
OpenIdConnectConfig(
conf.SingleSignOn.OpenIdConnect.providerUrl,
conf.SingleSignOn.OpenIdConnect.clientId,
if (conf.SingleSignOn.OpenIdConnect.clientSecret.nonEmpty) Some(conf.SingleSignOn.OpenIdConnect.clientSecret)
else None
)

/*
Build redirect URL to redirect to OIDC provider for auth request (https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest)
Expand Down Expand Up @@ -49,20 +54,22 @@ class OpenIdConnectClient @Inject()(rpc: RPC, conf: WkConf)(implicit executionCo
_ <- bool2Fox(conf.Features.openIdConnectEnabled) ?~> "oidc.disabled"
_ <- bool2Fox(oidcConfig.isValid) ?~> "oidc.configuration.invalid"
serverInfos <- discover
tokenResponse <- rpc(serverInfos.token_endpoint).postFormParseJson[OpenIdConnectTokenResponse](
Map(
"grant_type" -> "authorization_code",
"client_id" -> oidcConfig.clientId,
"redirect_uri" -> redirectUrl,
"code" -> code
))
tokenResponse <- rpc(serverInfos.token_endpoint)
.withBasicAuthOpt(Some(oidcConfig.clientId), oidcConfig.clientSecret)
.postFormParseJson[OpenIdConnectTokenResponse](
Map(
"grant_type" -> "authorization_code",
"client_id" -> oidcConfig.clientId,
"redirect_uri" -> redirectUrl,
"code" -> code
))
newToken <- validateOpenIdConnectTokenResponse(tokenResponse) ?~> "failed to parse JWT"
} yield newToken

/*
Discover endpoints of the provider (https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig)
*/
def discover: Fox[OpenIdConnectProviderInfo] =
private def discover: Fox[OpenIdConnectProviderInfo] =
for {
response: WSResponse <- rpc(oidcConfig.discoveryUrl).get
serverInfo <- response.json.validate[OpenIdConnectProviderInfo](OpenIdConnectProviderInfo.format)
Expand All @@ -75,7 +82,7 @@ class OpenIdConnectClient @Inject()(rpc: RPC, conf: WkConf)(implicit executionCo
JwtJson.decodeJson(tr.access_token, JwtOptions.DEFAULT.copy(signature = false)).toFox
}

lazy val publicKey: Option[PublicKey] = {
private lazy val publicKey: Option[PublicKey] = {
if (conf.SingleSignOn.OpenIdConnect.publicKey.isEmpty || conf.SingleSignOn.OpenIdConnect.publicKeyAlgorithm.isEmpty) {
None
} else {
Expand Down Expand Up @@ -103,6 +110,7 @@ object OpenIdConnectProviderInfo {
case class OpenIdConnectConfig(
baseUrl: String,
clientId: String,
clientSecret: Option[String],
scope: String = "openid profile"
) {

Expand Down Expand Up @@ -135,5 +143,5 @@ case class OpenIdConnectClaimSet(iss: String,
}

object OpenIdConnectClaimSet {
implicit val format = Json.format[OpenIdConnectClaimSet]
implicit val format: OFormat[OpenIdConnectClaimSet] = Json.format[OpenIdConnectClaimSet]
}
1 change: 1 addition & 0 deletions app/utils/WkConf.scala
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ class WkConf @Inject()(configuration: Configuration) extends ConfigReader with L
object OpenIdConnect {
val providerUrl: String = get[String]("singleSignOn.openIdConnect.providerUrl")
val clientId: String = get[String]("singleSignOn.openIdConnect.clientId")
val clientSecret: String = get[String]("singleSignOn.openIdConnect.clientSecret")
val publicKey: String = get[String]("singleSignOn.openIdConnect.publicKey")
val publicKeyAlgorithm: String = get[String]("singleSignOn.openIdConnect.publicKeyAlgorithm")
}
Expand Down
1 change: 1 addition & 0 deletions conf/application.conf
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ singleSignOn {
openIdConnect {
providerUrl = "http://localhost:8080/auth/realms/master/"
clientId = "myclient"
clientSecret = "myClientSecret"
# Public Key to validate claim, for keycloak see Realm settings > keys
publicKey = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAscUZB3Y5fiOfIdLC/31N1GufZ26bmB21V8D9Crg2bAHPD3g8qofRMg5Uo1+WuKuT5CJrCu+x0hIbA50GYb6E1V78MkYOaCbCT+xE+ec+Jv6zUJAaNJugx71oXI+X5e9kW/O8JSwIicSUYDz7LKvCklwn9/QmgetqGsBrAEOG+4WlwPnrZiKRaQl9V0vBOcwzD946Cbrgg3iLnryJ0pGVKHvWePsXR7Pt8hdA0FeA9V9hVd6gVHR2pHqg46kyPItNMwWTXENqJ4lbhgaoZ9sZpoMXIy1kjh3GXSXGOG+GeOOtOinr1K24I8HG9wsnEefjVSPDB6EvflPrhLKXMfI/JQIDAQAB"
publicKeyAlgorithm = "RSA"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,14 @@ class RPCRequest(val id: Int, val url: String, wsClient: WSClient)(implicit ec:
this
}

def withBasicAuthOpt(usernameOpt: Option[String], passwordOpt: Option[String]): RPCRequest = {
(usernameOpt, passwordOpt) match {
case (Some(username), Some(password)) => request = request.withAuth(username, password, WSAuthScheme.BASIC)
case _ => ()
}
this
}

def withLongTimeout: RPCRequest = {
request = request.withRequestTimeout(2 hours)
this
Expand Down