parent
b2d703bee4
commit
7a8ccdb229
48
.env.sample
48
.env.sample
|
@ -27,8 +27,29 @@ REDIS_URL=redis://localhost:6479
|
|||
URL=http://localhost:3000
|
||||
PORT=3000
|
||||
|
||||
# Third party signin credentials, at least one of EITHER Google OR Slack is
|
||||
# required for a working installation or you'll have no sign-in options.
|
||||
# To support uploading of images for avatars and document attachments an
|
||||
# s3-compatible storage must be provided. AWS S3 is recommended for redundency
|
||||
# however if you want to keep all file storage local an alternative such as
|
||||
# minio (https://github.com/minio/minio) can be used.
|
||||
|
||||
# A more detailed guide on setting up S3 is available here:
|
||||
# => https://wiki.generaloutline.com/share/125de1cc-9ff6-424b-8415-0d58c809a40f
|
||||
#
|
||||
AWS_ACCESS_KEY_ID=get_a_key_from_aws
|
||||
AWS_SECRET_ACCESS_KEY=get_the_secret_of_above_key
|
||||
AWS_REGION=xx-xxxx-x
|
||||
AWS_S3_UPLOAD_BUCKET_URL=http://s3:4569
|
||||
AWS_S3_UPLOAD_BUCKET_NAME=bucket_name_here
|
||||
AWS_S3_UPLOAD_MAX_SIZE=26214400
|
||||
AWS_S3_FORCE_PATH_STYLE=true
|
||||
AWS_S3_ACL=private
|
||||
|
||||
|
||||
# –––––––––––––– AUTHENTICATION ––––––––––––––
|
||||
|
||||
# Third party signin credentials, at least ONE OF EITHER Google, Slack,
|
||||
# or Microsoft is required for a working installation or you'll have no sign-in
|
||||
# options.
|
||||
|
||||
# To configure Slack auth, you'll need to create an Application at
|
||||
# => https://api.slack.com/apps
|
||||
|
@ -46,6 +67,12 @@ SLACK_SECRET=get_the_secret_of_above_key
|
|||
GOOGLE_CLIENT_ID=
|
||||
GOOGLE_CLIENT_SECRET=
|
||||
|
||||
# To configure Microsoft/Azure auth, you'll need to create an OAuth Client. See
|
||||
# the guide for details on setting up your Azure App:
|
||||
# => https://wiki.generaloutline.com/share/dfa77e56-d4d2-4b51-8ff8-84ea6608faa4
|
||||
AZURE_CLIENT_ID=
|
||||
AZURE_CLIENT_SECRET=
|
||||
AZURE_RESOURCE_APP_ID=
|
||||
|
||||
|
||||
|
||||
|
@ -87,23 +114,6 @@ GOOGLE_ANALYTICS_ID=
|
|||
# Optionally enable Sentry (sentry.io) to track errors and performance
|
||||
SENTRY_DSN=
|
||||
|
||||
# To support uploading of images for avatars and document attachments an
|
||||
# s3-compatible storage must be provided. AWS S3 is recommended for redundency
|
||||
# however if you want to keep all file storage local an alternative such as
|
||||
# minio (https://github.com/minio/minio) can be used.
|
||||
|
||||
# A more detailed guide on setting up S3 is available here:
|
||||
# => https://wiki.generaloutline.com/share/125de1cc-9ff6-424b-8415-0d58c809a40f
|
||||
#
|
||||
AWS_ACCESS_KEY_ID=get_a_key_from_aws
|
||||
AWS_SECRET_ACCESS_KEY=get_the_secret_of_above_key
|
||||
AWS_REGION=xx-xxxx-x
|
||||
AWS_S3_UPLOAD_BUCKET_URL=http://s3:4569
|
||||
AWS_S3_UPLOAD_BUCKET_NAME=bucket_name_here
|
||||
AWS_S3_UPLOAD_MAX_SIZE=26214400
|
||||
AWS_S3_FORCE_PATH_STYLE=true
|
||||
AWS_S3_ACL=private
|
||||
|
||||
# To support sending outgoing transactional emails such as "document updated" or
|
||||
# "you've been invited" you'll need to provide authentication for an SMTP server
|
||||
SMTP_HOST=
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
// @flow
|
||||
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;
|
|
@ -1,19 +1,46 @@
|
|||
// @flow
|
||||
import * as React from "react";
|
||||
import styled from "styled-components";
|
||||
import SlackLogo from "../SlackLogo";
|
||||
import GoogleLogo from "./GoogleLogo";
|
||||
import MicrosoftLogo from "./MicrosoftLogo";
|
||||
|
||||
type Props = {|
|
||||
providerName: string,
|
||||
size?: number,
|
||||
|};
|
||||
|
||||
export default function AuthLogo({ providerName }: Props) {
|
||||
function AuthLogo({ providerName, size = 16 }: Props) {
|
||||
switch (providerName) {
|
||||
case "slack":
|
||||
return <SlackLogo size={16} />;
|
||||
return (
|
||||
<Logo>
|
||||
<SlackLogo size={size} />
|
||||
</Logo>
|
||||
);
|
||||
case "google":
|
||||
return <GoogleLogo size={16} />;
|
||||
return (
|
||||
<Logo>
|
||||
<GoogleLogo size={size} />
|
||||
</Logo>
|
||||
);
|
||||
case "azure":
|
||||
return (
|
||||
<Logo>
|
||||
<MicrosoftLogo size={size} />
|
||||
</Logo>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const Logo = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
`;
|
||||
|
||||
export default AuthLogo;
|
||||
|
|
|
@ -97,13 +97,11 @@ class Provider extends React.Component<Props, State> {
|
|||
);
|
||||
}
|
||||
|
||||
const icon = <AuthLogo providerName={id} />;
|
||||
|
||||
return (
|
||||
<Wrapper key={id}>
|
||||
<ButtonLarge
|
||||
onClick={() => (window.location.href = authUrl)}
|
||||
icon={icon ? <Logo>{icon}</Logo> : null}
|
||||
icon={<AuthLogo providerName={id} />}
|
||||
fullwidth
|
||||
>
|
||||
Continue with {name}
|
||||
|
@ -113,14 +111,6 @@ class Provider extends React.Component<Props, State> {
|
|||
}
|
||||
}
|
||||
|
||||
const Logo = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
`;
|
||||
|
||||
const Wrapper = styled.div`
|
||||
margin-bottom: 1em;
|
||||
width: 100%;
|
||||
|
|
|
@ -73,6 +73,7 @@
|
|||
"@babel/preset-flow": "^7.10.4",
|
||||
"@babel/preset-react": "^7.10.4",
|
||||
"@outlinewiki/koa-passport": "^4.1.4",
|
||||
"@outlinewiki/passport-azure-ad-oauth2": "^0.1.0",
|
||||
"@rehooks/window-scroll-position": "^1.0.1",
|
||||
"@sentry/node": "^6.1.0",
|
||||
"@sentry/react": "^6.1.0",
|
||||
|
|
|
@ -0,0 +1,127 @@
|
|||
// @flow
|
||||
import passport from "@outlinewiki/koa-passport";
|
||||
import { Strategy as AzureStrategy } from "@outlinewiki/passport-azure-ad-oauth2";
|
||||
import jwt from "jsonwebtoken";
|
||||
import Router from "koa-router";
|
||||
import accountProvisioner from "../../commands/accountProvisioner";
|
||||
import env from "../../env";
|
||||
import { MicrosoftGraphError } from "../../errors";
|
||||
import passportMiddleware from "../../middlewares/passport";
|
||||
import { StateStore } from "../../utils/passport";
|
||||
|
||||
const router = new Router();
|
||||
const providerName = "azure";
|
||||
const AZURE_CLIENT_ID = process.env.AZURE_CLIENT_ID;
|
||||
const AZURE_CLIENT_SECRET = process.env.AZURE_CLIENT_SECRET;
|
||||
const AZURE_RESOURCE_APP_ID = process.env.AZURE_RESOURCE_APP_ID;
|
||||
|
||||
const scopes = [];
|
||||
|
||||
export async function request(endpoint: string, accessToken: string) {
|
||||
const response = await fetch(endpoint, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export const config = {
|
||||
name: "Microsoft",
|
||||
enabled: !!AZURE_CLIENT_ID,
|
||||
};
|
||||
|
||||
if (AZURE_CLIENT_ID) {
|
||||
const strategy = new AzureStrategy(
|
||||
{
|
||||
clientID: AZURE_CLIENT_ID,
|
||||
clientSecret: AZURE_CLIENT_SECRET,
|
||||
callbackURL: `${env.URL}/auth/azure.callback`,
|
||||
useCommonEndpoint: true,
|
||||
passReqToCallback: true,
|
||||
resource: AZURE_RESOURCE_APP_ID,
|
||||
store: new StateStore(),
|
||||
scope: scopes,
|
||||
},
|
||||
async function (req, accessToken, refreshToken, params, _, done) {
|
||||
try {
|
||||
// see docs for what the fields in profile represent here:
|
||||
// https://docs.microsoft.com/en-us/azure/active-directory/develop/access-tokens
|
||||
const profile = jwt.decode(params.id_token);
|
||||
|
||||
// Load the users profile from the Microsoft Graph API
|
||||
// https://docs.microsoft.com/en-us/graph/api/resources/users?view=graph-rest-1.0
|
||||
const profileResponse = await request(
|
||||
`https://graph.microsoft.com/v1.0/me`,
|
||||
accessToken
|
||||
);
|
||||
if (!profileResponse) {
|
||||
throw new MicrosoftGraphError(
|
||||
"Unable to load user profile from Microsoft Graph API"
|
||||
);
|
||||
}
|
||||
|
||||
// Load the organization profile from the Microsoft Graph API
|
||||
// https://docs.microsoft.com/en-us/graph/api/organization-get?view=graph-rest-1.0
|
||||
const organizationResponse = await request(
|
||||
`https://graph.microsoft.com/v1.0/organization`,
|
||||
accessToken
|
||||
);
|
||||
if (!organizationResponse) {
|
||||
throw new MicrosoftGraphError(
|
||||
"Unable to load organization info from Microsoft Graph API"
|
||||
);
|
||||
}
|
||||
|
||||
const organization = organizationResponse.value[0];
|
||||
const email = profile.email || profileResponse.mail;
|
||||
if (!email) {
|
||||
throw new MicrosoftGraphError(
|
||||
"'email' property is required but could not be found in user profile."
|
||||
);
|
||||
}
|
||||
|
||||
const domain = email.split("@")[1];
|
||||
const subdomain = domain.split(".")[0];
|
||||
const teamName = organization.displayName;
|
||||
|
||||
const result = await accountProvisioner({
|
||||
ip: req.ip,
|
||||
team: {
|
||||
name: teamName,
|
||||
domain,
|
||||
subdomain,
|
||||
},
|
||||
user: {
|
||||
name: profile.name,
|
||||
email,
|
||||
avatarUrl: profile.picture,
|
||||
},
|
||||
authenticationProvider: {
|
||||
name: providerName,
|
||||
providerId: profile.tid,
|
||||
},
|
||||
authentication: {
|
||||
providerId: profile.oid,
|
||||
accessToken,
|
||||
refreshToken,
|
||||
scopes,
|
||||
},
|
||||
});
|
||||
return done(null, result.user, result);
|
||||
} catch (err) {
|
||||
return done(err, null);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
passport.use(strategy);
|
||||
|
||||
router.get("azure", passport.authenticate(providerName));
|
||||
|
||||
router.get("azure.callback", passportMiddleware(providerName));
|
||||
}
|
||||
|
||||
export default router;
|
|
@ -82,6 +82,12 @@ export function EmailAuthenticationRequiredError(
|
|||
return httpErrors(400, message, { redirectUrl, id: "email_auth_required" });
|
||||
}
|
||||
|
||||
export function MicrosoftGraphError(
|
||||
message: string = "Microsoft Graph API did not return required fields"
|
||||
) {
|
||||
return httpErrors(400, message, { id: "graph_error" });
|
||||
}
|
||||
|
||||
export function GoogleWorkspaceRequiredError(
|
||||
message: string = "Google Workspace is required to authenticate"
|
||||
) {
|
||||
|
|
26
yarn.lock
26
yarn.lock
|
@ -1728,6 +1728,13 @@
|
|||
dependencies:
|
||||
passport "^0.4.0"
|
||||
|
||||
"@outlinewiki/passport-azure-ad-oauth2@^0.1.0":
|
||||
version "0.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@outlinewiki/passport-azure-ad-oauth2/-/passport-azure-ad-oauth2-0.1.0.tgz#29e8dc238c86b7e390997fc3db9accef4118a765"
|
||||
integrity sha512-9tywL/KToBgolno7ZaT4/c4bRromldi/HemPB3BN3KPJyqhJG+dii3lJRsbeRF9UF+FGlm5ifmONMFLVetdZWA==
|
||||
dependencies:
|
||||
passport-oauth "1.0.x"
|
||||
|
||||
"@pmmmwh/react-refresh-webpack-plugin@^0.4.3":
|
||||
version "0.4.3"
|
||||
resolved "https://registry.yarnpkg.com/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.4.3.tgz#1eec460596d200c0236bf195b078a5d1df89b766"
|
||||
|
@ -9930,7 +9937,16 @@ passport-google-oauth2@^0.2.0:
|
|||
dependencies:
|
||||
passport-oauth2 "^1.1.2"
|
||||
|
||||
passport-oauth2@^1.1.2, passport-oauth2@^1.5.0:
|
||||
passport-oauth1@1.x.x:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/passport-oauth1/-/passport-oauth1-1.1.0.tgz#a7de988a211f9cf4687377130ea74df32730c918"
|
||||
integrity sha1-p96YiiEfnPRoc3cTDqdN8ycwyRg=
|
||||
dependencies:
|
||||
oauth "0.9.x"
|
||||
passport-strategy "1.x.x"
|
||||
utils-merge "1.x.x"
|
||||
|
||||
passport-oauth2@1.x.x, passport-oauth2@^1.1.2, passport-oauth2@^1.5.0:
|
||||
version "1.5.0"
|
||||
resolved "https://registry.yarnpkg.com/passport-oauth2/-/passport-oauth2-1.5.0.tgz#64babbb54ac46a4dcab35e7f266ed5294e3c4108"
|
||||
integrity sha512-kqBt6vR/5VlCK8iCx1/KpY42kQ+NEHZwsSyt4Y6STiNjU+wWICG1i8ucc1FapXDGO15C5O5VZz7+7vRzrDPXXQ==
|
||||
|
@ -9941,6 +9957,14 @@ passport-oauth2@^1.1.2, passport-oauth2@^1.5.0:
|
|||
uid2 "0.0.x"
|
||||
utils-merge "1.x.x"
|
||||
|
||||
passport-oauth@1.0.x:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/passport-oauth/-/passport-oauth-1.0.0.tgz#90aff63387540f02089af28cdad39ea7f80d77df"
|
||||
integrity sha1-kK/2M4dUDwIImvKM2tOep/gNd98=
|
||||
dependencies:
|
||||
passport-oauth1 "1.x.x"
|
||||
passport-oauth2 "1.x.x"
|
||||
|
||||
passport-slack-oauth2@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/passport-slack-oauth2/-/passport-slack-oauth2-1.1.0.tgz#4a153b3d0d5a9e1a5041b61599d2b41a4b9486f1"
|
||||
|
|
Reference in New Issue