chore: Move remaining auth methods to plugins (#4900)

* Move Google, Email, and Azure to plugins

* Move OIDC provider, remove old loading code

* Move AuthLogo to plugin

* AuthLogo -> PluginIcon

* Lazy load plugin settings
This commit is contained in:
Tom Moor
2023-02-19 22:52:08 -05:00
committed by GitHub
parent 667ffdeaf1
commit 21a1257d06
32 changed files with 302 additions and 314 deletions

View File

@@ -1,26 +0,0 @@
import * as React from "react";
type Props = {
size?: number;
fill?: string;
className?: string;
};
function GoogleLogo({ size = 34, fill = "#FFF", className }: Props) {
return (
<svg
fill={fill}
width={size}
height={size}
viewBox="0 0 34 34"
xmlns="http://www.w3.org/2000/svg"
className={className}
>
<g>
<path d="M32.6162791,13.9090909 L16.8837209,13.9090909 L16.8837209,20.4772727 L25.9395349,20.4772727 C25.0953488,24.65 21.5651163,27.0454545 16.8837209,27.0454545 C11.3581395,27.0454545 6.90697674,22.5636364 6.90697674,17 C6.90697674,11.4363636 11.3581395,6.95454545 16.8837209,6.95454545 C19.2627907,6.95454545 21.4116279,7.80454545 23.1,9.19545455 L28.0116279,4.25 C25.0186047,1.62272727 21.1813953,0 16.8837209,0 C7.52093023,0 0,7.57272727 0,17 C0,26.4272727 7.52093023,34 16.8837209,34 C25.3255814,34 33,27.8181818 33,17 C33,15.9954545 32.8465116,14.9136364 32.6162791,13.9090909 Z" />
</g>
</svg>
);
}
export default GoogleLogo;

View File

@@ -1,43 +0,0 @@
import * as React from "react";
type Props = {
size?: number;
fill?: string;
className?: string;
};
function MicrosoftLogo({ size = 34, fill = "#FFF", className }: Props) {
return (
<svg
fill={fill}
width={size}
height={size}
viewBox="0 0 34 34"
xmlns="http://www.w3.org/2000/svg"
className={className}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M18.0002 1H33.9998C33.9998 5.8172 34.0007 10.6344 33.9988 15.4516C28.6666 15.4508 23.3334 15.4516 18.0012 15.4516C17.9993 10.6344 18.0002 5.8172 18.0002 1Z"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M18.0009 17.5173C23.3333 17.5155 28.6667 17.5164 34 17.5164V33H18C18.0009 27.8388 17.9991 22.6776 18.0009 17.5173V17.5173Z"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M0 1H16L15.9988 15.4516H0V1Z"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M0 17.5161C5.3332 17.5179 10.6664 17.5155 15.9996 17.5179C16.0005 22.6789 15.9996 27.839 15.9996 33H0V17.5161Z"
/>
</svg>
);
}
export default MicrosoftLogo;

View File

@@ -1,49 +0,0 @@
import * as React from "react";
import styled from "styled-components";
import GoogleLogo from "./GoogleLogo";
import MicrosoftLogo from "./MicrosoftLogo";
import SlackLogo from "./SlackLogo";
type Props = {
providerName: string;
size?: number;
color?: string;
};
function AuthLogo({ providerName, color, size = 16 }: Props) {
switch (providerName) {
case "slack":
return (
<Logo>
<SlackLogo size={size} fill={color} />
</Logo>
);
case "google":
return (
<Logo>
<GoogleLogo size={size} fill={color} />
</Logo>
);
case "azure":
return (
<Logo>
<MicrosoftLogo size={size} fill={color} />
</Logo>
);
default:
return null;
}
}
const Logo = styled.div`
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
`;
export default AuthLogo;

View File

@@ -0,0 +1,35 @@
import * as React from "react";
import styled from "styled-components";
import PluginLoader from "~/utils/PluginLoader";
type Props = {
id: string;
size?: number;
color?: string;
};
function PluginIcon({ id, color, size = 24 }: Props) {
const plugin = PluginLoader.plugins[id];
const Icon = plugin?.icon;
if (Icon) {
return (
<Wrapper>
<Icon size={size} fill={color} />
</Wrapper>
);
}
return null;
}
const Wrapper = styled.div`
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
width: 24px;
height: 24px;
`;
export default PluginIcon;

View File

@@ -34,8 +34,8 @@ import Tokens from "~/scenes/Settings/Tokens";
import Zapier from "~/scenes/Settings/Zapier";
import GoogleIcon from "~/components/Icons/GoogleIcon";
import ZapierIcon from "~/components/Icons/ZapierIcon";
import PluginLoader from "~/utils/PluginLoader";
import isCloudHosted from "~/utils/isCloudHosted";
import { loadPlugins } from "~/utils/plugins";
import { accountPreferencesPath } from "~/utils/routeHelpers";
import useCurrentTeam from "./useCurrentTeam";
import usePolicy from "./usePolicy";
@@ -160,7 +160,7 @@ const useSettingsConfig = () => {
icon: ExportIcon,
},
// Integrations
...mapValues(loadPlugins(), (plugin) => {
...mapValues(PluginLoader.plugins, (plugin) => {
return {
name: plugin.config.name,
path: integrationSettingsPath(plugin.id),
@@ -195,14 +195,7 @@ const useSettingsConfig = () => {
icon: ZapierIcon,
},
}),
[
t,
can.createApiKey,
can.update,
can.createImport,
can.createExport,
can.createWebhookSubscription,
]
[t, can.createApiKey, can.update, can.createImport, can.createExport]
);
const enabledConfigs = React.useMemo(

View File

@@ -4,9 +4,9 @@ import { useTranslation } from "react-i18next";
import styled from "styled-components";
import { Client } from "@shared/types";
import { parseDomain } from "@shared/utils/domains";
import AuthLogo from "~/components/AuthLogo";
import ButtonLarge from "~/components/ButtonLarge";
import InputLarge from "~/components/InputLarge";
import PluginIcon from "~/components/PluginIcon";
import env from "~/env";
import { client } from "~/utils/ApiClient";
import Desktop from "~/utils/Desktop";
@@ -117,7 +117,7 @@ function AuthenticationProvider(props: Props) {
<Wrapper>
<ButtonLarge
onClick={() => (window.location.href = href)}
icon={<AuthLogo providerName={id} />}
icon={<PluginIcon id={id} />}
fullwidth
>
{t("Continue with {{ authProviderName }}", {

View File

@@ -6,11 +6,11 @@ import * as React from "react";
import { useTranslation, Trans } from "react-i18next";
import { useTheme } from "styled-components";
import { TeamPreference } from "@shared/types";
import AuthLogo from "~/components/AuthLogo";
import ConfirmationDialog from "~/components/ConfirmationDialog";
import Flex from "~/components/Flex";
import Heading from "~/components/Heading";
import InputSelect from "~/components/InputSelect";
import PluginIcon from "~/components/PluginIcon";
import Scene from "~/components/Scene";
import Switch from "~/components/Switch";
import Text from "~/components/Text";
@@ -155,7 +155,7 @@ function Security() {
key={provider.name}
label={
<Flex gap={8} align="center">
<AuthLogo providerName={provider.name} color="currentColor" />{" "}
<PluginIcon id={provider.name} color="currentColor" />{" "}
{provider.displayName}
</Flex>
}

51
app/utils/PluginLoader.ts Normal file
View File

@@ -0,0 +1,51 @@
import React from "react";
interface Plugin {
id: string;
config: {
name: string;
description: string;
requiredEnvVars?: string[];
};
settings: React.FC;
icon: React.FC<{ size?: number; fill?: string }>;
}
export default class PluginLoader {
private static pluginsCache: { [id: string]: Plugin };
public static get plugins(): { [id: string]: Plugin } {
if (this.pluginsCache) {
return this.pluginsCache;
}
const plugins = {};
function importAll(r: any, property: string) {
Object.keys(r).forEach((key: string) => {
const id = key.split("/")[3];
plugins[id] = plugins[id] || {
id,
};
plugins[id][property] = r[key].default ?? React.lazy(r[key]);
});
}
importAll(
import.meta.glob("../../plugins/*/client/Settings.{ts,js,tsx,jsx}"),
"settings"
);
importAll(
import.meta.glob("../../plugins/*/client/Icon.{ts,js,tsx,jsx}", {
eager: true,
}),
"icon"
);
importAll(
import.meta.glob("../../plugins/*/plugin.json", { eager: true }),
"config"
);
this.pluginsCache = plugins;
return plugins;
}
}

View File

@@ -1,43 +0,0 @@
interface Plugin {
id: string;
config: {
name: string;
description: string;
requiredEnvVars?: string[];
};
settings: React.FC;
icon: React.FC;
}
export function loadPlugins(): { [id: string]: Plugin } {
const plugins = {};
function importAll(r: any, property: string) {
Object.keys(r).forEach((key: string) => {
const id = key.split("/")[3];
plugins[id] = plugins[id] || {
id,
};
plugins[id][property] = r[key].default;
});
}
importAll(
import.meta.glob("../../plugins/*/client/Settings.{ts,js,tsx,jsx}", {
eager: true,
}),
"settings"
);
importAll(
import.meta.glob("../../plugins/*/client/Icon.{ts,js,tsx,jsx}", {
eager: true,
}),
"icon"
);
importAll(
import.meta.glob("../../plugins/*/plugin.json", { eager: true }),
"config"
);
return plugins;
}

View File

@@ -1,13 +1,14 @@
# Authentication Providers
A new auth provider can be added with the addition of a single file in this
folder, and (optionally) a matching logo in `/app/components/AuthLogo/index.js`
that will appear on the signin button.
A new auth provider can be added with the addition of a plugin with a koa router
as the default export in /server/auth/[provider].ts and (optionally) a matching
logo in `/client/Icon.tsx` that will appear on the sign-in button.
Auth providers generally use [Passport](http://www.passportjs.org/) strategies,
although they can use any custom logic if needed. See the `google` auth provider for the cleanest example of what is required some rules:
although they can use any custom logic if needed. See the `google` auth provider
for the cleanest example of what is required some rules:
- The strategy name _must_ be lowercase
- The strategy _must_ call the `accountProvisioner` command in the verify callback
- The auth file _must_ export a `config` object with `name` and `enabled` keys
- The auth file _must_ have a default export with a koa-router
- The auth file _must_ have a default export with a koa-router

View File

@@ -0,0 +1,45 @@
import * as React from "react";
type Props = {
/** The size of the icon, 24px is default to match standard icons */
size?: number;
/** The color of the icon, defaults to the current text color */
fill?: string;
className?: string;
};
function MicrosoftLogo({ size = 24, fill = "#FFF", className }: Props) {
return (
<svg
fill={fill}
width={size}
height={size}
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
className={className}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M12.4707 4.47059H19.9999C19.9999 6.73751 20.0003 9.00442 19.9994 11.2713C17.4902 11.271 14.9804 11.2713 12.4712 11.2713C12.4703 9.00442 12.4707 6.73751 12.4707 4.47059Z"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M12.471 12.2434C14.9804 12.2426 17.4902 12.243 20 12.243V19.5294H12.4706C12.471 17.1006 12.4702 14.6718 12.471 12.2434Z"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M4 4.47059H11.5294L11.5288 11.2713H4V4.47059Z"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M4 12.2429C6.50974 12.2437 9.01948 12.2426 11.5292 12.2437C11.5296 14.6724 11.5292 17.1007 11.5292 19.5294H4V12.2429Z"
/>
</svg>
);
}
export default MicrosoftLogo;

View File

@@ -0,0 +1,5 @@
{
"name": "Microsoft",
"description": "Adds a Microsoft Azure authentication provider.",
"requiredEnvVars": ["AZURE_CLIENT_ID", "AZURE_CLIENT_SECRET"]
}

View File

@@ -0,0 +1,3 @@
{
"extends": "../../../server/.babelrc"
}

View File

@@ -22,11 +22,6 @@ const router = new Router();
const providerName = "azure";
const scopes: string[] = [];
export const config = {
name: "Microsoft",
enabled: !!env.AZURE_CLIENT_ID,
};
if (env.AZURE_CLIENT_ID && env.AZURE_CLIENT_SECRET) {
const strategy = new AzureStrategy(
{

View File

@@ -0,0 +1,4 @@
{
"name": "Email",
"description": "Adds an email magic link authentication provider."
}

View File

@@ -0,0 +1,3 @@
{
"extends": "../../../server/.babelrc"
}

View File

@@ -16,11 +16,6 @@ import { assertEmail, assertPresent } from "@server/validation";
const router = new Router();
export const config = {
name: "Email",
enabled: true,
};
router.post(
"email",
rateLimiter(RateLimiterStrategy.TenPerHour),

View File

@@ -0,0 +1,26 @@
import * as React from "react";
type Props = {
/** The size of the icon, 24px is default to match standard icons */
size?: number;
/** The color of the icon, defaults to the current text color */
fill?: string;
className?: string;
};
function GoogleLogo({ size = 24, fill = "currentColor", className }: Props) {
return (
<svg
fill={fill}
width={size}
height={size}
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
className={className}
>
<path d="M19.2312 10.5455H11.8276V13.6364H16.0892C15.6919 15.6 14.0306 16.7273 11.8276 16.7273C9.22733 16.7273 7.13267 14.6182 7.13267 12C7.13267 9.38182 9.22733 7.27273 11.8276 7.27273C12.9472 7.27273 13.9584 7.67273 14.7529 8.32727L17.0643 6C15.6558 4.76364 13.85 4 11.8276 4C7.42159 4 3.88232 7.56364 3.88232 12C3.88232 16.4364 7.42159 20 11.8276 20C15.8002 20 19.4117 17.0909 19.4117 12C19.4117 11.5273 19.3395 11.0182 19.2312 10.5455Z" />
</svg>
);
}
export default GoogleLogo;

View File

@@ -0,0 +1,5 @@
{
"name": "Google",
"description": "Adds a Google authentication provider.",
"requiredEnvVars": ["GOOGLE_CLIENT_ID", "GOOGLE_CLIENT_SECRET"]
}

View File

@@ -0,0 +1,3 @@
{
"extends": "../../../server/.babelrc"
}

View File

@@ -21,17 +21,13 @@ import {
} from "@server/utils/passport";
const router = new Router();
const GOOGLE = "google";
const providerName = "google";
const scopes = [
"https://www.googleapis.com/auth/userinfo.profile",
"https://www.googleapis.com/auth/userinfo.email",
];
export const config = {
name: "Google",
enabled: !!env.GOOGLE_CLIENT_ID,
};
type GoogleProfile = Profile & {
email: string;
picture: string;
@@ -114,7 +110,7 @@ if (env.GOOGLE_CLIENT_ID && env.GOOGLE_CLIENT_SECRET) {
avatarUrl,
},
authenticationProvider: {
name: GOOGLE,
name: providerName,
providerId: domain ?? "",
},
authentication: {
@@ -136,13 +132,13 @@ if (env.GOOGLE_CLIENT_ID && env.GOOGLE_CLIENT_SECRET) {
router.get(
"google",
passport.authenticate(GOOGLE, {
passport.authenticate(providerName, {
accessType: "offline",
prompt: "select_account consent",
})
);
router.get("google.callback", passportMiddleware(GOOGLE));
router.get("google.callback", passportMiddleware(providerName));
}
export default router;

12
plugins/oidc/plugin.json Normal file
View File

@@ -0,0 +1,12 @@
{
"name": "OIDC",
"description": "Adds an OpenID compatible authentication provider.",
"requiredEnvVars": [
"OIDC_CLIENT_ID",
"OIDC_CLIENT_SECRET",
"OIDC_AUTH_URI",
"OIDC_TOKEN_URI",
"OIDC_USERINFO_URI",
"OIDC_DISPLAY_NAME"
]
}

View File

@@ -0,0 +1,3 @@
{
"extends": "../../../server/.babelrc"
}

View File

@@ -22,32 +22,30 @@ import {
const router = new Router();
const providerName = "oidc";
const OIDC_AUTH_URI = env.OIDC_AUTH_URI || "";
const OIDC_TOKEN_URI = env.OIDC_TOKEN_URI || "";
const OIDC_USERINFO_URI = env.OIDC_USERINFO_URI || "";
export const config = {
name: env.OIDC_DISPLAY_NAME,
enabled: !!env.OIDC_CLIENT_ID,
};
const scopes = env.OIDC_SCOPES.split(" ");
Strategy.prototype.userProfile = async function (accessToken, done) {
try {
const response = await request(OIDC_USERINFO_URI, accessToken);
const response = await request(env.OIDC_USERINFO_URI ?? "", accessToken);
return done(null, response);
} catch (err) {
return done(err);
}
};
if (env.OIDC_CLIENT_ID && env.OIDC_CLIENT_SECRET) {
if (
env.OIDC_CLIENT_ID &&
env.OIDC_CLIENT_SECRET &&
env.OIDC_AUTH_URI &&
env.OIDC_TOKEN_URI &&
env.OIDC_USERINFO_URI
) {
passport.use(
providerName,
new Strategy(
{
authorizationURL: OIDC_AUTH_URI,
tokenURL: OIDC_TOKEN_URI,
authorizationURL: env.OIDC_AUTH_URI,
tokenURL: env.OIDC_TOKEN_URI,
clientID: env.OIDC_CLIENT_ID,
clientSecret: env.OIDC_CLIENT_SECRET,
callbackURL: `${env.URL}/auth/${providerName}.callback`,
@@ -144,4 +142,6 @@ if (env.OIDC_CLIENT_ID && env.OIDC_CLIENT_SECRET) {
router.get(`${providerName}.callback`, passportMiddleware(providerName));
}
export const name = env.OIDC_DISPLAY_NAME;
export default router;

View File

@@ -4,19 +4,19 @@ type Props = {
/** The size of the icon, 24px is default to match standard icons */
size?: number;
/** The color of the icon, defaults to the current text color */
color?: string;
fill?: string;
};
export default function Icon({ size = 24, color = "currentColor" }: Props) {
export default function Icon({ size = 24, fill = "currentColor" }: Props) {
return (
<svg
fill={color}
fill={fill}
width={size}
height={size}
viewBox="0 0 24 24"
version="1.1"
>
<path d="M7.36156352,14.1107492 C7.36156352,15.0358306 6.60586319,15.7915309 5.68078176,15.7915309 C4.75570033,15.7915309 4,15.0358306 4,14.1107492 C4,13.1856678 4.75570033,12.4299674 5.68078176,12.4299674 L7.36156352,12.4299674 L7.36156352,14.1107492 Z M8.20846906,14.1107492 C8.20846906,13.1856678 8.96416938,12.4299674 9.88925081,12.4299674 C10.8143322,12.4299674 11.5700326,13.1856678 11.5700326,14.1107492 L11.5700326,18.3192182 C11.5700326,19.2442997 10.8143322,20 9.88925081,20 C8.96416938,20 8.20846906,19.2442997 8.20846906,18.3192182 C8.20846906,18.3192182 8.20846906,14.1107492 8.20846906,14.1107492 Z M9.88925081,7.36156352 C8.96416938,7.36156352 8.20846906,6.60586319 8.20846906,5.68078176 C8.20846906,4.75570033 8.96416938,4 9.88925081,4 C10.8143322,4 11.5700326,4.75570033 11.5700326,5.68078176 L11.5700326,7.36156352 L9.88925081,7.36156352 Z M9.88925081,8.20846906 C10.8143322,8.20846906 11.5700326,8.96416938 11.5700326,9.88925081 C11.5700326,10.8143322 10.8143322,11.5700326 9.88925081,11.5700326 L5.68078176,11.5700326 C4.75570033,11.5700326 4,10.8143322 4,9.88925081 C4,8.96416938 4.75570033,8.20846906 5.68078176,8.20846906 C5.68078176,8.20846906 9.88925081,8.20846906 9.88925081,8.20846906 Z M16.6384365,9.88925081 C16.6384365,8.96416938 17.3941368,8.20846906 18.3192182,8.20846906 C19.2442997,8.20846906 20,8.96416938 20,9.88925081 C20,10.8143322 19.2442997,11.5700326 18.3192182,11.5700326 L16.6384365,11.5700326 L16.6384365,9.88925081 Z M15.7915309,9.88925081 C15.7915309,10.8143322 15.0358306,11.5700326 14.1107492,11.5700326 C13.1856678,11.5700326 12.4299674,10.8143322 12.4299674,9.88925081 L12.4299674,5.68078176 C12.4299674,4.75570033 13.1856678,4 14.1107492,4 C15.0358306,4 15.7915309,4.75570033 15.7915309,5.68078176 L15.7915309,9.88925081 Z M14.1107492,16.6384365 C15.0358306,16.6384365 15.7915309,17.3941368 15.7915309,18.3192182 C15.7915309,19.2442997 15.0358306,20 14.1107492,20 C13.1856678,20 12.4299674,19.2442997 12.4299674,18.3192182 L12.4299674,16.6384365 L14.1107492,16.6384365 Z M14.1107492,15.7915309 C13.1856678,15.7915309 12.4299674,15.0358306 12.4299674,14.1107492 C12.4299674,13.1856678 13.1856678,12.4299674 14.1107492,12.4299674 L18.3192182,12.4299674 C19.2442997,12.4299674 20,13.1856678 20,14.1107492 C20,15.0358306 19.2442997,15.7915309 18.3192182,15.7915309 L14.1107492,15.7915309 Z" />
<path d="M7.36156 14.1107C7.36156 15.0358 6.60586 15.7915 5.68078 15.7915C4.7557 15.7915 4 15.0358 4 14.1107C4 13.1857 4.7557 12.43 5.68078 12.43H7.36156V14.1107ZM8.20847 14.1107C8.20847 13.1857 8.96417 12.43 9.88925 12.43C10.8143 12.43 11.57 13.1857 11.57 14.1107V18.3192C11.57 19.2443 10.8143 20 9.88925 20C8.96417 20 8.20847 19.2443 8.20847 18.3192V14.1107ZM9.88925 7.36156C8.96417 7.36156 8.20847 6.60586 8.20847 5.68078C8.20847 4.7557 8.96417 4 9.88925 4C10.8143 4 11.57 4.7557 11.57 5.68078V7.36156H9.88925ZM9.88925 8.20847C10.8143 8.20847 11.57 8.96417 11.57 9.88925C11.57 10.8143 10.8143 11.57 9.88925 11.57H5.68078C4.7557 11.57 4 10.8143 4 9.88925C4 8.96417 4.7557 8.20847 5.68078 8.20847H9.88925ZM16.6384 9.88925C16.6384 8.96417 17.3941 8.20847 18.3192 8.20847C19.2443 8.20847 20 8.96417 20 9.88925C20 10.8143 19.2443 11.57 18.3192 11.57H16.6384V9.88925ZM15.7915 9.88925C15.7915 10.8143 15.0358 11.57 14.1107 11.57C13.1857 11.57 12.43 10.8143 12.43 9.88925V5.68078C12.43 4.7557 13.1857 4 14.1107 4C15.0358 4 15.7915 4.7557 15.7915 5.68078V9.88925ZM14.1107 16.6384C15.0358 16.6384 15.7915 17.3941 15.7915 18.3192C15.7915 19.2443 15.0358 20 14.1107 20C13.1857 20 12.43 19.2443 12.43 18.3192V16.6384H14.1107ZM14.1107 15.7915C13.1857 15.7915 12.43 15.0358 12.43 14.1107C12.43 13.1857 13.1857 12.43 14.1107 12.43H18.3192C19.2443 12.43 20 13.1857 20 14.1107C20 15.0358 19.2443 15.7915 18.3192 15.7915H14.1107Z" />
</svg>
);
}

View File

@@ -56,7 +56,7 @@ function Slack() {
const appName = env.APP_NAME;
return (
<Scene title="Slack" icon={<SlackIcon color="currentColor" />}>
<Scene title="Slack" icon={<SlackIcon />}>
<Heading>Slack</Heading>
{error === "access_denied" && (
@@ -106,7 +106,7 @@ function Slack() {
]}
redirectUri={`${env.URL}/auth/slack.commands`}
state={team.id}
icon={<SlackIcon color="currentColor" />}
icon={<SlackIcon />}
/>
)}
</p>

View File

@@ -1,9 +1,70 @@
import { find } from "lodash";
/* eslint-disable @typescript-eslint/no-var-requires */
import path from "path";
import { glob } from "glob";
import Router from "koa-router";
import { find, sortBy } from "lodash";
import env from "@server/env";
import Team from "@server/models/Team";
import providerConfigs from "../../routes/auth/providers";
export type AuthenticationProviderConfig = {
id: string;
name: string;
enabled: boolean;
router: Router;
};
export default class AuthenticationHelper {
private static providersCache: AuthenticationProviderConfig[];
/**
* Returns the enabled authentication provider configurations for the current
* installation.
*
* @returns A list of authentication providers
*/
public static get providers() {
if (this.providersCache) {
return this.providersCache;
}
const authenticationProviderConfigs: AuthenticationProviderConfig[] = [];
const rootDir = env.ENVIRONMENT === "test" ? "" : "build";
glob
.sync(path.join(rootDir, "plugins/*/server/auth/!(*.test).[jt]s"))
.forEach((filePath: string) => {
const { default: authProvider, name } = require(path.join(
process.cwd(),
filePath
));
const id = filePath.replace("build/", "").split("/")[1];
const config = require(path.join(
process.cwd(),
rootDir,
"plugins",
id,
"plugin.json"
));
// Test the all required env vars are set for the auth provider
const enabled = (config.requiredEnvVars ?? []).every(
(name: string) => !!env[name]
);
if (enabled) {
authenticationProviderConfigs.push({
id,
name: name ?? config.name,
enabled,
router: authProvider,
});
}
});
this.providersCache = sortBy(authenticationProviderConfigs, "id");
return this.providersCache;
}
/**
* Returns the enabled authentication provider configurations for a team,
* if given otherwise all enabled providers are returned.
@@ -11,17 +72,12 @@ export default class AuthenticationHelper {
* @param team The team to get enabled providers for
* @returns A list of authentication providers
*/
static providersForTeam(team?: Team) {
public static providersForTeam(team?: Team) {
const isCloudHosted = env.isCloudHosted();
return providerConfigs
return AuthenticationHelper.providers
.sort((config) => (config.id === "email" ? 1 : -1))
.filter((config) => {
// Don't return authentication methods that are not enabled.
if (!config.enabled) {
return false;
}
// Guest sign-in is an exception as it does not have an authentication
// provider using passport, instead it exists as a boolean option.
if (config.id === "email") {

View File

@@ -1,5 +1,5 @@
import { signin } from "@shared/utils/routeHelpers";
import { AuthenticationProviderConfig } from "@server/routes/auth/providers";
import { AuthenticationProviderConfig } from "@server/models/helpers/AuthenticationHelper";
export default function presentProviderConfig(
config: AuthenticationProviderConfig

View File

@@ -3,13 +3,13 @@ import { sequelize } from "@server/database/sequelize";
import auth from "@server/middlewares/authentication";
import validate from "@server/middlewares/validate";
import { AuthenticationProvider, Event } from "@server/models";
import AuthenticationHelper from "@server/models/helpers/AuthenticationHelper";
import { authorize } from "@server/policies";
import {
presentAuthenticationProvider,
presentPolicies,
} from "@server/presenters";
import { APIContext } from "@server/types";
import allAuthenticationProviders from "../../auth/providers";
import * as T from "./schema";
const router = new Router();
@@ -95,7 +95,7 @@ router.post(
"authenticationProviders"
)) as AuthenticationProvider[];
const data = allAuthenticationProviders
const data = AuthenticationHelper.providers
.filter((p) => p.id !== "email")
.map((p) => {
const row = teamAuthenticationProviders.find((t) => t.name === p.id);

View File

@@ -6,8 +6,8 @@ import Router from "koa-router";
import { AuthenticationError } from "@server/errors";
import auth from "@server/middlewares/authentication";
import { Collection, Team, View } from "@server/models";
import AuthenticationHelper from "@server/models/helpers/AuthenticationHelper";
import { AppState, AppContext, APIContext } from "@server/types";
import providers from "./providers";
const app = new Koa<AppState, AppContext>();
const router = new Router();
@@ -15,10 +15,8 @@ const router = new Router();
router.use(passport.initialize());
// dynamically load available authentication provider routes
providers.forEach((provider) => {
if (provider.enabled) {
router.use("/", provider.router.routes());
}
AuthenticationHelper.providers.forEach((provider) => {
router.use("/", provider.router.routes());
});
router.get("/redirect", auth(), async (ctx: APIContext) => {

View File

@@ -1,80 +0,0 @@
/* eslint-disable @typescript-eslint/no-var-requires */
import path from "path";
import { glob } from "glob";
import Router from "koa-router";
import { sortBy } from "lodash";
import env from "@server/env";
import { requireDirectory } from "@server/utils/fs";
export type AuthenticationProviderConfig = {
id: string;
name: string;
enabled: boolean;
router: Router;
};
const authenticationProviderConfigs: AuthenticationProviderConfig[] = [];
requireDirectory<{
default: Router;
config: { name: string; enabled: boolean };
}>(__dirname).forEach(([module, id]) => {
const { config, default: router } = module;
if (id === "index") {
return;
}
if (!config) {
throw new Error(
`Auth providers must export a 'config' object, missing in ${id}`
);
}
if (!router || !router.routes) {
throw new Error(
`Default export of an auth provider must be a koa-router, missing in ${id}`
);
}
if (config && config.enabled) {
authenticationProviderConfigs.push({
id,
name: config.name,
enabled: config.enabled,
router,
});
}
});
// Temporarily also include plugins here until all auth methods are moved over.
glob
.sync(
(env.ENVIRONMENT === "test" ? "" : "build/") +
"plugins/*/server/auth/!(*.test).[jt]s"
)
.forEach((filePath: string) => {
const authProvider = require(path.join(process.cwd(), filePath)).default;
const id = filePath.replace("build/", "").split("/")[1];
const config = require(path.join(
process.cwd(),
env.ENVIRONMENT === "test" ? "" : "build",
"plugins",
id,
"plugin.json"
));
// Test the all required env vars are set for the auth provider
const enabled = (config.requiredEnvVars ?? []).every(
(name: string) => !!env[name]
);
authenticationProviderConfigs.push({
id,
name: config.name,
enabled,
router: authProvider,
});
});
export default sortBy(authenticationProviderConfigs, "id");