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:
45
plugins/azure/client/Icon.tsx
Normal file
45
plugins/azure/client/Icon.tsx
Normal 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;
|
||||
5
plugins/azure/plugin.json
Normal file
5
plugins/azure/plugin.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "Microsoft",
|
||||
"description": "Adds a Microsoft Azure authentication provider.",
|
||||
"requiredEnvVars": ["AZURE_CLIENT_ID", "AZURE_CLIENT_SECRET"]
|
||||
}
|
||||
3
plugins/azure/server/.babelrc
Normal file
3
plugins/azure/server/.babelrc
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "../../../server/.babelrc"
|
||||
}
|
||||
139
plugins/azure/server/auth/azure.ts
Normal file
139
plugins/azure/server/auth/azure.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
import passport from "@outlinewiki/koa-passport";
|
||||
import { Strategy as AzureStrategy } from "@outlinewiki/passport-azure-ad-oauth2";
|
||||
import jwt from "jsonwebtoken";
|
||||
import type { Context } from "koa";
|
||||
import Router from "koa-router";
|
||||
import { Profile } from "passport";
|
||||
import { slugifyDomain } from "@shared/utils/domains";
|
||||
import accountProvisioner from "@server/commands/accountProvisioner";
|
||||
import env from "@server/env";
|
||||
import { MicrosoftGraphError } from "@server/errors";
|
||||
import passportMiddleware from "@server/middlewares/passport";
|
||||
import { User } from "@server/models";
|
||||
import { AuthenticationResult } from "@server/types";
|
||||
import {
|
||||
StateStore,
|
||||
request,
|
||||
getTeamFromContext,
|
||||
getClientFromContext,
|
||||
} from "@server/utils/passport";
|
||||
|
||||
const router = new Router();
|
||||
const providerName = "azure";
|
||||
const scopes: string[] = [];
|
||||
|
||||
if (env.AZURE_CLIENT_ID && env.AZURE_CLIENT_SECRET) {
|
||||
const strategy = new AzureStrategy(
|
||||
{
|
||||
clientID: env.AZURE_CLIENT_ID,
|
||||
clientSecret: env.AZURE_CLIENT_SECRET,
|
||||
callbackURL: `${env.URL}/auth/azure.callback`,
|
||||
useCommonEndpoint: true,
|
||||
passReqToCallback: true,
|
||||
resource: env.AZURE_RESOURCE_APP_ID,
|
||||
// @ts-expect-error StateStore
|
||||
store: new StateStore(),
|
||||
scope: scopes,
|
||||
},
|
||||
async function (
|
||||
ctx: Context,
|
||||
accessToken: string,
|
||||
refreshToken: string,
|
||||
params: { expires_in: number; id_token: string },
|
||||
_profile: Profile,
|
||||
done: (
|
||||
err: Error | null,
|
||||
user: User | null,
|
||||
result?: AuthenticationResult
|
||||
) => void
|
||||
) {
|
||||
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) as jwt.JwtPayload;
|
||||
|
||||
const [profileResponse, organizationResponse] = await Promise.all([
|
||||
// Load the users profile from the Microsoft Graph API
|
||||
// https://docs.microsoft.com/en-us/graph/api/resources/users?view=graph-rest-1.0
|
||||
request(`https://graph.microsoft.com/v1.0/me`, accessToken),
|
||||
// Load the organization profile from the Microsoft Graph API
|
||||
// https://docs.microsoft.com/en-us/graph/api/organization-get?view=graph-rest-1.0
|
||||
request(`https://graph.microsoft.com/v1.0/organization`, accessToken),
|
||||
]);
|
||||
|
||||
if (!profileResponse) {
|
||||
throw MicrosoftGraphError(
|
||||
"Unable to load user profile from Microsoft Graph API"
|
||||
);
|
||||
}
|
||||
|
||||
if (!organizationResponse) {
|
||||
throw MicrosoftGraphError(
|
||||
"Unable to load organization info from Microsoft Graph API"
|
||||
);
|
||||
}
|
||||
|
||||
const organization = organizationResponse.value[0];
|
||||
|
||||
// Note: userPrincipalName is last here for backwards compatibility with
|
||||
// previous versions of Outline that did not include it.
|
||||
const email =
|
||||
profile.email ||
|
||||
profileResponse.mail ||
|
||||
profileResponse.userPrincipalName;
|
||||
|
||||
if (!email) {
|
||||
throw MicrosoftGraphError(
|
||||
"'email' property is required but could not be found in user profile."
|
||||
);
|
||||
}
|
||||
|
||||
const team = await getTeamFromContext(ctx);
|
||||
const client = getClientFromContext(ctx);
|
||||
|
||||
const domain = email.split("@")[1];
|
||||
const subdomain = slugifyDomain(domain);
|
||||
|
||||
const teamName = organization.displayName;
|
||||
const result = await accountProvisioner({
|
||||
ip: ctx.ip,
|
||||
team: {
|
||||
teamId: team?.id,
|
||||
name: teamName,
|
||||
domain,
|
||||
subdomain,
|
||||
},
|
||||
user: {
|
||||
name: profile.name,
|
||||
email,
|
||||
avatarUrl: profile.picture,
|
||||
},
|
||||
authenticationProvider: {
|
||||
name: providerName,
|
||||
providerId: profile.tid,
|
||||
},
|
||||
authentication: {
|
||||
providerId: profile.oid,
|
||||
accessToken,
|
||||
refreshToken,
|
||||
expiresIn: params.expires_in,
|
||||
scopes,
|
||||
},
|
||||
});
|
||||
return done(null, result.user, { ...result, client });
|
||||
} catch (err) {
|
||||
return done(err, null);
|
||||
}
|
||||
}
|
||||
);
|
||||
passport.use(strategy);
|
||||
|
||||
router.get(
|
||||
"azure",
|
||||
passport.authenticate(providerName, { prompt: "select_account" })
|
||||
);
|
||||
|
||||
router.get("azure.callback", passportMiddleware(providerName));
|
||||
}
|
||||
|
||||
export default router;
|
||||
Reference in New Issue
Block a user