Files
outline/server/models/helpers/AuthenticationHelper.ts
T
Tom Moor a06671e8ce OAuth provider (#8884)
This PR contains the necessary work to make Outline an OAuth provider including:

- OAuth app registration
- OAuth app management
- Private / public apps (Public in cloud only)
- Full OAuth 2.0 spec compatible authentication flow
- Granular scopes
- User token management screen in settings
- Associated API endpoints for programatic access
2025-05-03 19:40:18 -04:00

114 lines
3.5 KiB
TypeScript

/* eslint-disable @typescript-eslint/no-var-requires */
import find from "lodash/find";
import { Scope } from "@shared/types";
import env from "@server/env";
import Team from "@server/models/Team";
import { Hook, PluginManager } from "@server/utils/PluginManager";
export default class AuthenticationHelper {
/**
* The mapping of method names to their scopes, anything not listed here
* defaults to `Scope.Write`.
*
* - `documents.create` -> `Scope.Create`
* - `documents.list` -> `Scope.Read`
* - `documents.info` -> `Scope.Read`
*/
private static methodToScope = {
create: Scope.Create,
list: Scope.Read,
info: Scope.Read,
search: Scope.Read,
documents: Scope.Read,
};
/**
* Returns the enabled authentication provider configurations for the current
* installation.
*
* @returns A list of authentication providers
*/
public static get providers() {
return PluginManager.getHooks(Hook.AuthProvider);
}
/**
* Returns the enabled authentication provider configurations for a team,
* if given otherwise all enabled providers are returned.
*
* @param team The team to get enabled providers for
* @returns A list of authentication providers
*/
public static providersForTeam(team?: Team) {
const isCloudHosted = env.isCloudHosted;
return AuthenticationHelper.providers
.sort((hook) => (hook.value.id === "email" ? 1 : -1))
.filter((hook) => {
// Email sign-in is an exception as it does not have an authentication
// provider using passport, instead it exists as a boolean option.
if (hook.value.id === "email") {
return team?.emailSigninEnabled;
}
// If no team return all possible authentication providers except email.
if (!team) {
return true;
}
const authProvider = find(team.authenticationProviders, {
name: hook.value.id,
});
// If cloud hosted then the auth provider must be enabled for the team,
// If self-hosted then it must not be actively disabled, otherwise all
// providers are considered.
return (
(!isCloudHosted && authProvider?.enabled !== false) ||
(isCloudHosted && authProvider?.enabled)
);
});
}
/**
* Returns whether the given path can be accessed with any of the scopes. We
* support scopes in the formats of:
*
* - `/api/namespace.method`
* - `namespace:scope`
* - `scope`
*
* @param path The path to check
* @param scopes The scopes to check
* @returns True if the path can be accessed
*/
public static canAccess = (path: string, scopes: string[]) => {
// strip any query string, this is never used as part of scope matching
path = path.split("?")[0];
const resource = path.split("/").pop() ?? "";
const [namespace, method] = resource.split(".");
return scopes.some((scope) => {
const [scopeNamespace, scopeMethod] = scope.match(/[:\.]/g)
? scope.replace("/api/", "").split(/[:\.]/g)
: ["*", scope];
const isRouteScope = scope.startsWith("/api/");
if (isRouteScope) {
return (
(namespace === scopeNamespace || scopeNamespace === "*") &&
(method === scopeMethod || scopeMethod === "*")
);
}
return (
(namespace === scopeNamespace || scopeNamespace === "*") &&
(scopeMethod === Scope.Write ||
this.methodToScope[method as keyof typeof this.methodToScope] ===
scopeMethod)
);
});
};
}