-
Notifications
You must be signed in to change notification settings - Fork 26
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* adds certificate public key * add certificate validation * add certificate validation * fix proofreading message * format backend * fix frontend * fix wording * fix snapshots * fix typos and improve verification * styling * rename file * include expiration date * use formatted date * unify with application conf naming * format backend * fix typecheck tests * improve comments * do not block if server is down * apply frontend feedback * increase certificate cache time * apply backend feedback * rename proofreading enabling var * add new image * format backend * fix single sign on * make certificate route accessible to unauthorized users * fix image sizing * Update app/security/CertificateValidationService.scala Co-authored-by: frcroth <[email protected]> --------- Co-authored-by: Michael Büßemeyer <[email protected]> Co-authored-by: MichaelBuessemeyer <[email protected]> Co-authored-by: frcroth <[email protected]>
- Loading branch information
1 parent
7f49dda
commit 4e33465
Showing
20 changed files
with
668 additions
and
30 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
package security | ||
|
||
import com.scalableminds.util.cache.AlfuCache | ||
import com.scalableminds.util.tools.Fox | ||
import com.typesafe.scalalogging.LazyLogging | ||
import net.liftweb.common.{Box, Empty, Failure, Full} | ||
|
||
import java.security.{KeyFactory, PublicKey} | ||
import pdi.jwt.{JwtJson, JwtOptions} | ||
|
||
import java.security.spec.X509EncodedKeySpec | ||
import java.util.Base64 | ||
import javax.inject.Inject | ||
import scala.concurrent.ExecutionContext | ||
import scala.concurrent.duration.DurationInt | ||
import scala.util.Properties | ||
|
||
class CertificateValidationService @Inject()(implicit ec: ExecutionContext) extends LazyLogging { | ||
|
||
// The publicKeyBox is empty if no public key is provided, Failure if decoding the public key failed or Full if there is a valid public key. | ||
private lazy val publicKeyBox: Box[PublicKey] = webknossos.BuildInfo.toMap.get("certificatePublicKey").flatMap { | ||
case Some(value: String) => deserializePublicKey(value) | ||
case None => Empty | ||
} | ||
|
||
private lazy val cache: AlfuCache[String, (Boolean, Long)] = AlfuCache(timeToLive = 1 hour) | ||
|
||
private def deserializePublicKey(pem: String): Box[PublicKey] = | ||
try { | ||
val base64Key = pem.replaceAll("\\s", "") | ||
val decodedKey = Base64.getDecoder.decode(base64Key) | ||
val keySpec = new X509EncodedKeySpec(decodedKey) | ||
Some(KeyFactory.getInstance("EC").generatePublic(keySpec)) | ||
} catch { | ||
case _: Throwable => | ||
val message = s"Could not deserialize public key from PEM string: $pem" | ||
logger.error(message) | ||
Failure(message) | ||
} | ||
|
||
private def checkCertificate: (Boolean, Long) = publicKeyBox match { | ||
case Full(publicKey) => | ||
(for { | ||
certificate <- Properties.envOrNone("CERTIFICATE") | ||
// JwtJson would throw an error in case the exp time of the token is expired. As we want to check the expiration | ||
// date yourself, we don't want to throw an error. | ||
token <- JwtJson.decodeJson(certificate, publicKey, JwtOptions(expiration = false)).toOption | ||
expirationInSeconds <- (token \ "exp").asOpt[Long] | ||
currentTimeInSeconds = System.currentTimeMillis() / 1000 | ||
isExpired = currentTimeInSeconds < expirationInSeconds | ||
} yield (isExpired, expirationInSeconds)).getOrElse((false, 0L)) | ||
case Empty => (true, 0L) // No public key provided, so certificate is always valid. | ||
case _ => (false, 0L) // Invalid public key provided, so certificate is always invalid. | ||
} | ||
|
||
def checkCertificateCached(): Fox[(Boolean, Long)] = cache.getOrLoad("c", _ => Fox.successful(checkCertificate)) | ||
|
||
private def defaultConfigOverridesMap: Map[String, Boolean] = | ||
Map("openIdConnectEnabled" -> false, "segmentAnythingEnabled" -> false, "editableMappingsEnabled" -> false) | ||
|
||
lazy val getFeatureOverrides: Map[String, Boolean] = publicKeyBox match { | ||
case Full(publicKey) => | ||
(for { | ||
certificate <- Properties.envOrNone("CERTIFICATE") | ||
// JwtJson already throws an error which is transformed to an empty option when the certificate is expired. | ||
// In case the token is expired, tge default map will be used. | ||
token <- JwtJson.decodeJson(certificate, publicKey, JwtOptions(expiration = false)).toOption | ||
featureOverrides <- Some( | ||
(token \ "webknossos").asOpt[Map[String, Boolean]].getOrElse(defaultConfigOverridesMap)) | ||
featureOverridesWithDefaults = featureOverrides ++ defaultConfigOverridesMap.view.filterKeys( | ||
!featureOverrides.contains(_)) | ||
} yield featureOverridesWithDefaults).getOrElse(defaultConfigOverridesMap) | ||
case Empty => Map.empty | ||
case _ => defaultConfigOverridesMap | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
import Request from "libs/request"; | ||
|
||
type CertificateValidationResult = { | ||
isValid: boolean; | ||
expiresAt: number; | ||
}; | ||
|
||
export async function isCertificateValid(): Promise<CertificateValidationResult> { | ||
try { | ||
return await Request.receiveJSON("/api/checkCertificate", { showErrorToast: false }); | ||
} catch (errorResponse: any) { | ||
if (errorResponse.status !== 400) { | ||
// In case the server is not available or some other kind of error occurred, we assume the certificate is valid. | ||
return { isValid: true, expiresAt: 0 }; | ||
} | ||
try { | ||
const { isValid, expiresAt } = JSON.parse(errorResponse.errors[0]); | ||
return { isValid, expiresAt }; | ||
} catch (_e) { | ||
// If parsing the error message fails, we assume the certificate is valid. | ||
return { isValid: true, expiresAt: 0 }; | ||
} | ||
} | ||
} |
66 changes: 66 additions & 0 deletions
66
frontend/javascripts/components/check_certificate_modal.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
import { isCertificateValid } from "admin/api/certificate_validation"; | ||
import { Col, Modal, Result, Row } from "antd"; | ||
import { useInterval } from "libs/react_helpers"; | ||
import _ from "lodash"; | ||
import { useEffect, useState } from "react"; | ||
import FormattedDate from "./formatted_date"; | ||
|
||
export function CheckCertificateModal() { | ||
const [isValid, setIsValid] = useState(true); | ||
const [expiresAt, setExpiresAt] = useState(Number.POSITIVE_INFINITY); | ||
useEffect(() => { | ||
isCertificateValid().then(({ isValid, expiresAt }) => { | ||
setIsValid(isValid); | ||
setExpiresAt(expiresAt); | ||
}); | ||
}, []); | ||
useInterval( | ||
async () => { | ||
const { isValid, expiresAt } = await isCertificateValid(); | ||
setIsValid(isValid); | ||
setExpiresAt(expiresAt); | ||
}, | ||
5 * 60 * 1000, // 5 minutes | ||
); | ||
if (isValid) { | ||
return null; | ||
} | ||
return ( | ||
<Modal | ||
open={true} | ||
closable={false} | ||
footer={null} | ||
onCancel={_.noop} | ||
width={"max(70%, 600px)"} | ||
keyboard={false} | ||
maskClosable={false} | ||
> | ||
<Row justify="center" align="middle" style={{ maxHeight: "50%", width: "auto" }}> | ||
<Col> | ||
<Result | ||
icon={<i className="drawing drawing-license-expired" />} | ||
status="warning" | ||
title={ | ||
<span> | ||
Sorry, your WEBKNOSSOS license expired on{" "} | ||
<FormattedDate timestamp={expiresAt * 1000} format="YYYY-MM-DD" />. | ||
<br /> | ||
Please{" "} | ||
<a | ||
target="_blank" | ||
rel="noreferrer" | ||
href="mailto:[email protected]" | ||
style={{ color: "inherit", textDecoration: "underline" }} | ||
> | ||
contact us | ||
</a>{" "} | ||
to renew your license. | ||
</span> | ||
} | ||
style={{ height: "100%" }} | ||
/> | ||
</Col> | ||
</Row> | ||
</Modal> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.