Merge pull request #1741 from ONLYOFFICE/bugfix/sso

Bugfix/sso
This commit is contained in:
Alexey Safronov 2023-09-18 13:32:22 +04:00 committed by GitHub
commit fe5de27885
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 183 additions and 53 deletions

View File

@ -86,14 +86,27 @@ export const QuotaBarTypes = Object.freeze({
export const BINDING_POST = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST";
export const BINDING_REDIRECT =
"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect";
export const SSO_NAME_ID_FORMAT =
"urn:oasis:names:tc:SAML:2.0:nameid-format:transient";
export const SSO_NAME_ID_FORMAT = [
"urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified",
"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
"urn:oasis:names:tc:SAML:2.0:nameid-format:entity",
"urn:oasis:names:tc:SAML:2.0:nameid-format:transient",
"urn:oasis:names:tc:SAML:2.0:nameid-format:persistent",
"urn:oasis:names:tc:SAML:2.0:nameid-format:encrypted",
"urn:oasis:names:tc:SAML:2.0:nameid-format:unspecified",
"urn:oasis:names:tc:SAML:1.1:nameid-format:X509SubjectName",
"urn:oasis:names:tc:SAML:1.1:nameid-format:WindowsDomainQualifiedName",
"urn:oasis:names:tc:SAML:2.0:nameid-format:kerberos",
];
export const SSO_GIVEN_NAME = "givenName";
export const SSO_SN = "sn";
export const SSO_EMAIL = "email";
export const SSO_LOCATION = "location";
export const SSO_TITLE = "title";
export const SSO_PHONE = "phone";
export const SSO_SIGNING = "signing";
export const SSO_ENCRYPT = "encrypt";
export const SSO_SIGNING_ENCRYPT = "signing and encrypt";
export const DEFAULT_SELECT_TIMEZONE = {
key: "UTC",

View File

@ -41,6 +41,9 @@ const Certificates = (props) => {
spEncryptAlgorithm,
spDecryptAlgorithm,
isLoadingXml,
isDisabledSpSigning,
isDisabledSpEncrypt,
isDisabledIdpSigning,
} = props;
let prefix = "";
@ -139,7 +142,7 @@ const Certificates = (props) => {
{provider === "IdentityProvider" && (
<>
<SsoComboBox
isDisabled={idpCertificates.length === 0}
isDisabled={isDisabledIdpSigning}
labelText={t("idpSigningAlgorithm")}
name="idpVerifyAlgorithm"
options={verifyAlgorithmsOptions}
@ -152,7 +155,7 @@ const Certificates = (props) => {
{provider === "ServiceProvider" && (
<>
<SsoComboBox
isDisabled={spCertificates.length === 0}
isDisabled={isDisabledSpSigning}
labelText={t("spSigningAlgorithm")}
name="spSigningAlgorithm"
options={verifyAlgorithmsOptions}
@ -161,7 +164,7 @@ const Certificates = (props) => {
/>
<SsoComboBox
isDisabled={spCertificates.length === 0}
isDisabled={isDisabledSpEncrypt}
labelText={t("StandardDecryptionAlgorithm")}
name={"spEncryptAlgorithm"}
options={decryptAlgorithmsOptions}
@ -193,6 +196,9 @@ export default inject(({ ssoStore }) => {
spEncryptAlgorithm,
spDecryptAlgorithm,
isLoadingXml,
isDisabledSpSigning,
isDisabledSpEncrypt,
isDisabledIdpSigning,
} = ssoStore;
return {
@ -207,5 +213,8 @@ export default inject(({ ssoStore }) => {
spEncryptAlgorithm,
spDecryptAlgorithm,
isLoadingXml,
isDisabledSpSigning,
isDisabledSpEncrypt,
isDisabledIdpSigning,
};
})(observer(Certificates));

View File

@ -27,8 +27,8 @@ const CertificatesTable = (props) => {
console.log(prefix, index);
const onEdit = () => {
prefix === "sp"
? setSpCertificate(certificate, index)
: setIdpCertificate(certificate);
? setSpCertificate(certificate, index, true)
: setIdpCertificate(certificate, index, true);
};
const onDelete = () => {

View File

@ -44,11 +44,15 @@ const CheckboxSet = (props) => {
spSignLogoutRequests,
spSignLogoutResponses,
spEncryptAssertions,
enableSso,
setCheckbox,
isLoadingXml,
isDisabledSpSigning,
isDisabledSpEncrypt,
isDisabledIdpSigning,
} = props;
const isDisabled =
prefix === "sp" ? isDisabledSpSigning : isDisabledIdpSigning;
return (
<StyledWrapper>
<Checkbox
@ -58,7 +62,7 @@ const CheckboxSet = (props) => {
: "sp-sign-auth-requests"
}
className="checkbox-input"
isDisabled={!enableSso || isLoadingXml}
isDisabled={isDisabled}
onChange={setCheckbox}
label={prefix === "idp" ? t("idpAuthRequest") : t("spAuthRequest")}
name={checkboxesNames[prefix][0]}
@ -74,7 +78,7 @@ const CheckboxSet = (props) => {
: "sp-sign-logout-requests"
}
className="checkbox-input"
isDisabled={!enableSso || isLoadingXml}
isDisabled={isDisabled}
onChange={setCheckbox}
label={
prefix === "idp" ? t("idpSignExitRequest") : t("spSignExitRequest")
@ -92,7 +96,7 @@ const CheckboxSet = (props) => {
: "sp-sign-logout-responses"
}
className="checkbox-input"
isDisabled={!enableSso || isLoadingXml}
isDisabled={isDisabled}
onChange={setCheckbox}
label={
prefix === "idp"
@ -112,7 +116,7 @@ const CheckboxSet = (props) => {
<Checkbox
id="sp-encrypt-assertions"
className="checkbox-input"
isDisabled={!enableSso || isLoadingXml}
isDisabled={isDisabledSpEncrypt}
onChange={setCheckbox}
label={t("spDecryptStatements")}
name={checkboxesNames[prefix][3]}
@ -133,9 +137,10 @@ export default inject(({ ssoStore }) => {
spSignLogoutRequests,
spSignLogoutResponses,
spEncryptAssertions,
enableSso,
setCheckbox,
isLoadingXml,
isDisabledSpSigning,
isDisabledSpEncrypt,
isDisabledIdpSigning,
} = ssoStore;
return {
@ -146,8 +151,9 @@ export default inject(({ ssoStore }) => {
spSignLogoutRequests,
spSignLogoutResponses,
spEncryptAssertions,
enableSso,
setCheckbox,
isLoadingXml,
isDisabledSpSigning,
isDisabledSpEncrypt,
isDisabledIdpSigning,
};
})(observer(CheckboxSet));

View File

@ -19,6 +19,9 @@ import {
SSO_TITLE,
SSO_PHONE,
SSO_NAME_ID_FORMAT,
SSO_SIGNING,
SSO_ENCRYPT,
SSO_SIGNING_ENCRYPT,
} from "../helpers/constants";
import isEqual from "lodash/isEqual";
@ -41,11 +44,11 @@ class SsoFormStore {
sloUrlPost = "";
sloUrlRedirect = "";
sloBinding = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST";
nameIdFormat = SSO_NAME_ID_FORMAT;
nameIdFormat = SSO_NAME_ID_FORMAT[0];
idpCertificate = "";
idpPrivateKey = null;
idpAction = "signing";
idpAction = SSO_SIGNING;
idpCertificates = [];
// idpCertificateAdvanced
@ -59,7 +62,7 @@ class SsoFormStore {
spCertificate = "";
spPrivateKey = "";
spAction = "signing";
spAction = SSO_SIGNING;
spCertificates = [];
// spCertificateAdvanced
@ -126,6 +129,7 @@ class SsoFormStore {
defaultSettings = null;
editIndex = 0;
isEdit = false;
isInit = false;
@ -191,6 +195,8 @@ class SsoFormStore {
closeIdpModal = () => {
this.idpCertificate = "";
this.idpPrivateKey = "";
this.editIndex = 0;
this.isEdit = false;
this.idpIsModalVisible = false;
};
@ -199,6 +205,7 @@ class SsoFormStore {
this.spPrivateKey = "";
this.spIsModalVisible = false;
this.editIndex = 0;
this.isEdit = false;
};
setComboBoxOption = (option) => {
@ -638,33 +645,77 @@ class SsoFormStore {
return array.filter((item, index, array) => array.indexOf(item) == index);
};
setSpCertificate = (certificate, index) => {
setSpCertificate = (certificate, index, isEdit) => {
this.spCertificate = certificate.crt;
this.spPrivateKey = certificate.key;
this.spAction = certificate.action;
this.editIndex = index;
this.isEdit = isEdit;
this.spIsModalVisible = true;
};
setIdpCertificate = (certificate) => {
setIdpCertificate = (certificate, index, isEdit) => {
this.idpCertificate = certificate.crt;
this.idpPrivateKey = certificate.key;
this.idpAction = certificate.action;
this.editIndex = index;
this.isEdit = isEdit;
this.idpIsModalVisible = true;
};
resetSpCheckboxes = (action) => {
if (action === SSO_SIGNING_ENCRYPT) {
this.spSignAuthRequests = false;
this.spSignLogoutRequests = false;
this.spSignLogoutResponses = false;
this.spEncryptAssertions = false;
}
if (action === SSO_SIGNING) {
this.spSignAuthRequests = false;
this.spSignLogoutRequests = false;
this.spSignLogoutResponses = false;
}
if (action === SSO_ENCRYPT) {
this.spEncryptAssertions = false;
}
};
resetIdpCheckboxes = () => {
this.idpVerifyAuthResponsesSign = false;
this.idpVerifyLogoutRequestsSign = false;
this.idpVerifyLogoutResponsesSign = false;
};
delSpCertificate = (action) => {
this.resetSpCheckboxes(action);
this.spCertificates = this.spCertificates.filter(
(certificate) => certificate.action !== action
);
};
delIdpCertificate = (cert) => {
this.resetIdpCheckboxes();
this.idpCertificates = this.idpCertificates.filter(
(certificate) => certificate.crt !== cert
);
};
checkSpCertificateExist = () => {
if (
this.spAction === SSO_SIGNING_ENCRYPT &&
this.spCertificates.length > 0 &&
!this.isEdit
)
return true;
return this.spCertificates.find(
(item) =>
(item.action === this.spAction ||
item.action === SSO_SIGNING_ENCRYPT) &&
!this.isEdit
);
};
addSpCertificate = async (t) => {
const data = [
{
@ -674,12 +725,7 @@ class SsoFormStore {
},
];
if (
this.spCertificates.find(
(item, index) =>
item.action === this.spAction && this.editIndex !== index
)
) {
if (this.checkSpCertificateExist()) {
toastr.error(t("CertificateExist"));
return;
}
@ -693,10 +739,16 @@ class SsoFormStore {
return;
}
const newCertificates = res.data;
newCertificates.map((cert) => {
this.spCertificates = [...this.spCertificates, cert];
this.checkedSpBoxes(cert);
});
if (this.isEdit) {
this.spCertificates[this.editIndex] = newCertificates[0];
this.checkedSpBoxes(newCertificates[0]);
} else {
newCertificates.map((cert) => {
this.spCertificates = [...this.spCertificates, cert];
this.checkedSpBoxes(cert);
});
}
this.isCertificateLoading = false;
this.closeSpModal();
} catch (err) {
@ -707,14 +759,14 @@ class SsoFormStore {
};
checkedSpBoxes = (cert) => {
if (cert.action === "signing") {
if (cert.action === SSO_SIGNING) {
this.spSignAuthRequests = true;
this.spSignLogoutRequests = true;
}
if (cert.action === "encrypt") {
if (cert.action === SSO_ENCRYPT) {
this.spEncryptAssertions = true;
}
if (cert.action === "signing and encrypt") {
if (cert.action === SSO_SIGNING_ENCRYPT) {
this.spSignAuthRequests = true;
this.spSignLogoutRequests = true;
this.spEncryptAssertions = true;
@ -730,7 +782,11 @@ class SsoFormStore {
},
];
if (this.idpCertificates.find((item) => item.crt === this.idpCertificate)) {
if (
this.idpCertificates.find(
(item) => item.crt === this.idpCertificate && !this.isEdit
)
) {
toastr.error(t("CertificateExist"));
return;
}
@ -744,10 +800,15 @@ class SsoFormStore {
return;
}
const newCertificates = res.data;
newCertificates.map((cert) => {
this.idpCertificates = [...this.idpCertificates, cert];
this.checkedIdpBoxes(cert);
});
if (this.isEdit) {
this.idpCertificates[this.editIndex] = newCertificates[0];
this.checkedIdpBoxes(newCertificates[0]);
} else {
newCertificates.map((cert) => {
this.idpCertificates = [...this.idpCertificates, cert];
this.checkedIdpBoxes(cert);
});
}
this.isCertificateLoading = false;
this.closeIdpModal();
} catch (err) {
@ -824,6 +885,27 @@ class SsoFormStore {
const currentSettings = this.getSettings();
return !isEqual(currentSettings, this.defaultSettings);
}
get isDisabledIdpSigning() {
if (!this.enableSso || this.isLoadingXml) return true;
return this.idpCertificates.length === 0;
}
get isDisabledSpSigning() {
if (!this.enableSso || this.isLoadingXml) return true;
return !this.spCertificates.some(
(cert) =>
cert.action === SSO_SIGNING || cert.action === SSO_SIGNING_ENCRYPT
);
}
get isDisabledSpEncrypt() {
if (!this.enableSso || this.isLoadingXml) return true;
return !this.spCertificates.some(
(cert) =>
cert.action === SSO_ENCRYPT || cert.action === SSO_SIGNING_ENCRYPT
);
}
}
export default SsoFormStore;

View File

@ -1,4 +1,4 @@
import styled from "styled-components";
import styled, { css } from "styled-components";
import commonInputStyles from "./common-input-styles";
import Input from "./input";
import Base from "../themes/base";
@ -24,15 +24,19 @@ const StyledTextInput = styled(Input).attrs((props) => ({
${commonInputStyles}
-webkit-appearance: ${(props) => props.theme.textInput.appearance};
background-color: ${(props) => props.theme.input.backgroundColor};
-webkit-text-fill-color: ${(props) =>
props?.value.length > 0
? props.theme.text.color
: props.theme.textInput.placeholderColor} !important;
caret-color: ${(props) => props.theme.text.color};
-webkit-background-clip: text !important;
box-shadow: inset 0 0 20px 20px
${(props) => props.theme.input.backgroundColor} !important;
${(props) =>
!props.isDisabled &&
css`
background-color: ${(props) => props.theme.input.backgroundColor};
-webkit-text-fill-color: ${(props) =>
props?.value.length > 0
? props.theme.text.color
: props.theme.textInput.placeholderColor} !important;
caret-color: ${(props) => props.theme.text.color};
-webkit-background-clip: text !important;
box-shadow: inset 0 0 20px 20px
${(props) => props.theme.input.backgroundColor} !important;
`}
display: ${(props) => props.theme.textInput.display};
font-family: ${(props) => props.theme.fontFamily};

View File

@ -108,6 +108,10 @@ declare global {
message: string | undefined;
}
interface ISSOSettings {
hideAuthPage: boolean;
}
interface IInitialState {
portalSettings?: IPortalSettings;
buildInfo?: IBuildInfo;
@ -116,6 +120,7 @@ declare global {
match?: MatchType;
currentColorScheme?: ITheme;
isAuth?: boolean;
ssoSettings?: ISSOSettings;
logoUrls: ILogoUrl[];
error?: IError;
}

View File

@ -59,6 +59,13 @@ app.get("*", async (req: ILoginRequest, res: Response, next) => {
try {
initialState = await getInitialState(query);
const hideAuthPage = initialState?.ssoSettings?.hideAuthPage;
const ssoUrl = initialState?.capabilities?.ssoUrl;
if (hideAuthPage && ssoUrl && query.skipssoredirect !== "true") {
res.redirect(ssoUrl);
return next();
}
if (initialState.isAuth && url !== "/login/error") {
res.redirect("/");

View File

@ -8,6 +8,7 @@ import {
getCapabilities,
getAppearanceTheme,
getLogoUrls,
getCurrentSsoSettings
} from "@docspace/common/api/settings";
import { checkIsAuthenticated } from "@docspace/common/api/user";
import { TenantStatus } from "@docspace/common/constants";
@ -51,8 +52,9 @@ export const getInitialState = async (
providers: ProvidersType,
capabilities: ICapabilities,
availableThemes: IThemes,
isAuth: any,
logoUrls: any;
isAuth: boolean,
logoUrls: ILogoUrl[],
ssoSettings: ISSOSettings;
const baseSettings = [
getSettings(),
@ -65,6 +67,7 @@ export const getInitialState = async (
getAuthProviders(),
getCapabilities(),
checkIsAuthenticated(),
getCurrentSsoSettings(),
];
[portalSettings, buildInfo, availableThemes, logoUrls] = await Promise.all(
@ -72,7 +75,7 @@ export const getInitialState = async (
);
if (portalSettings.tenantStatus !== TenantStatus.PortalRestore)
[providers, capabilities, isAuth] = await Promise.all(settings);
[providers, capabilities, isAuth, ssoSettings] = await Promise.all(settings);
const currentColorScheme = availableThemes.themes.find((theme) => {
return availableThemes.selected === theme.id;
@ -87,6 +90,7 @@ export const getInitialState = async (
currentColorScheme,
isAuth,
logoUrls,
ssoSettings
};
return initialState;