Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
gitpod-io
GitHub Repository: gitpod-io/gitpod
Path: blob/main/components/dashboard/src/teams/sso/SSOConfigForm.tsx
2501 views
1
/**
2
* Copyright (c) 2023 Gitpod GmbH. All rights reserved.
3
* Licensed under the GNU Affero General Public License (AGPL).
4
* See License.AGPL.txt in the project root for license information.
5
*/
6
7
import { FC, useCallback } from "react";
8
import { InputWithCopy } from "../../components/InputWithCopy";
9
import { InputField } from "../../components/forms/InputField";
10
import { TextInputField } from "../../components/forms/TextInputField";
11
import { gitpodHostUrl } from "../../service/service";
12
import { useOnBlurError } from "../../hooks/use-onblur-error";
13
import isURL from "validator/lib/isURL";
14
import { useCurrentOrg } from "../../data/organizations/orgs-query";
15
import { useUpsertOIDCClientMutation } from "../../data/oidc-clients/upsert-oidc-client-mutation";
16
import { Subheading } from "../../components/typography/headings";
17
import { CheckboxInputField } from "../../components/forms/CheckboxInputField";
18
19
type Props = {
20
config: SSOConfig;
21
readOnly?: boolean;
22
onChange: (config: Partial<SSOConfig>) => void;
23
};
24
25
export const SSOConfigForm: FC<Props> = ({ config, readOnly = false, onChange }) => {
26
const redirectUrl = gitpodHostUrl.with({ pathname: `/iam/oidc/callback` }).toString();
27
28
const issuerError = useOnBlurError(`Please enter a valid URL.`, isValidIssuer(config.issuer));
29
const clientIdError = useOnBlurError("Client ID is missing.", isValidClientID(config.clientId));
30
const clientSecretError = useOnBlurError("Client Secret is missing.", isValidClientSecret(config.clientSecret));
31
32
return (
33
<>
34
<Subheading>
35
<strong>1.</strong> Add the following <strong>redirect URI</strong> to your identity provider's
36
configuration.
37
</Subheading>
38
39
<InputField>
40
<InputWithCopy value={redirectUrl} tip="Copy the redirect URI to clipboard" />
41
</InputField>
42
43
<Subheading className="mt-8">
44
<strong>2.</strong> Find the information below from your identity provider.
45
</Subheading>
46
47
<TextInputField
48
label="Issuer URL"
49
value={config.issuer}
50
placeholder={"e.g. https://accounts.google.com"}
51
error={issuerError.message}
52
disabled={readOnly}
53
onBlur={issuerError.onBlur}
54
onChange={(val) => onChange({ issuer: val })}
55
/>
56
57
<TextInputField
58
label="Client ID"
59
value={config.clientId}
60
error={clientIdError.message}
61
disabled={readOnly}
62
onBlur={clientIdError.onBlur}
63
onChange={(val) => onChange({ clientId: val })}
64
/>
65
66
<TextInputField
67
label="Client Secret"
68
type="password"
69
value={config.clientSecret}
70
error={clientSecretError.message}
71
disabled={readOnly}
72
onBlur={clientSecretError.onBlur}
73
onChange={(val) => onChange({ clientSecret: val })}
74
/>
75
76
<CheckboxInputField
77
label="Use PKCE"
78
checked={config.usePKCE}
79
disabled={readOnly}
80
onChange={(val) => onChange({ usePKCE: val })}
81
/>
82
83
<Subheading className="mt-8">
84
<strong>3.</strong> Restrict available accounts in your Identity Providers.
85
<a
86
href="https://www.gitpod.io/docs/enterprise/setup-gitpod/configure-sso#restrict-available-accounts-in-your-identity-providers"
87
target="_blank"
88
rel="noreferrer noopener"
89
className="gp-link"
90
>
91
Learn more
92
</a>
93
.
94
</Subheading>
95
96
<InputField label="CEL Expression (optional)">
97
<textarea
98
style={{ height: "160px" }}
99
className="w-full resize-none"
100
value={config.celExpression}
101
onChange={(val) => onChange({ celExpression: val.target.value })}
102
/>
103
</InputField>
104
</>
105
);
106
};
107
108
export type SSOConfig = {
109
id?: string;
110
issuer: string;
111
clientId: string;
112
clientSecret: string;
113
celExpression?: string;
114
usePKCE: boolean;
115
};
116
117
export const ssoConfigReducer = (state: SSOConfig, action: Partial<SSOConfig>) => {
118
return { ...state, ...action };
119
};
120
121
export const isValid = (state: SSOConfig) => {
122
return isValidIssuer(state.issuer) && isValidClientID(state.clientId) && isValidClientSecret(state.clientSecret);
123
};
124
125
const isValidIssuer = (issuer: SSOConfig["issuer"]) => {
126
return issuer.trim().length > 0 && isURL(issuer);
127
};
128
129
const isValidClientID = (clientID: SSOConfig["clientId"]) => {
130
return clientID.trim().length > 0;
131
};
132
133
const isValidClientSecret = (clientSecret: SSOConfig["clientSecret"]) => {
134
return clientSecret.trim().length > 0;
135
};
136
137
export const useSaveSSOConfig = () => {
138
const { data: org } = useCurrentOrg();
139
const upsertClientConfig = useUpsertOIDCClientMutation();
140
141
const save = useCallback(
142
async (ssoConfig: SSOConfig) => {
143
if (upsertClientConfig.isLoading) {
144
throw new Error("Already saving");
145
}
146
if (!org) {
147
throw new Error("No current org selected");
148
}
149
150
if (!isValid(ssoConfig)) {
151
throw new Error("Invalid SSO config");
152
}
153
154
const trimmedIssuer = ssoConfig.issuer.trim();
155
const trimmedClientId = ssoConfig.clientId.trim();
156
const trimmedClientSecret = ssoConfig.clientSecret.trim();
157
const trimmedCelExpression = ssoConfig.celExpression?.trim();
158
159
return upsertClientConfig.mutateAsync({
160
config: !ssoConfig.id
161
? {
162
organizationId: org.id,
163
oauth2Config: {
164
clientId: trimmedClientId,
165
clientSecret: trimmedClientSecret,
166
celExpression: trimmedCelExpression,
167
usePkce: ssoConfig.usePKCE,
168
},
169
oidcConfig: {
170
issuer: trimmedIssuer,
171
},
172
}
173
: {
174
id: ssoConfig.id,
175
organizationId: org.id,
176
oauth2Config: {
177
clientId: trimmedClientId,
178
// TODO: determine how we should handle when user doesn't change their secret
179
clientSecret: trimmedClientSecret.toLowerCase() === "redacted" ? "" : trimmedClientSecret,
180
celExpression: trimmedCelExpression,
181
usePkce: ssoConfig.usePKCE,
182
},
183
oidcConfig: {
184
issuer: trimmedIssuer,
185
},
186
},
187
});
188
},
189
[org, upsertClientConfig],
190
);
191
192
return {
193
save,
194
isLoading: upsertClientConfig.isLoading,
195
isError: upsertClientConfig.isError,
196
error: upsertClientConfig.error,
197
};
198
};
199
200