Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,11 @@ OIDC_DISPLAY_NAME=OpenID Connect
# Space separated auth scopes.
OIDC_SCOPES=openid profile email

# OAuth tokens issued by Outline’s built-in OAuth provider (seconds).
# FOSS devstack maps SESSION_COOKIE_MAX_AGE_SECONDS / SESSION_REFRESH_TOKEN_MAX_AGE_SECONDS here.
OAUTH_PROVIDER_ACCESS_TOKEN_LIFETIME=604800
OAUTH_PROVIDER_REFRESH_TOKEN_LIFETIME=1209600


# ––––––––––––––––––––––––––––––––––––––
# –––––––––––––– EMAIL –––––––––––––––
Expand Down
4 changes: 3 additions & 1 deletion .env.test
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
NODE_ENV=test
DATABASE_URL=postgres://user:pass@127.0.0.1:5432/outline-test
DATABASE_URL=postgres://postgres:065fec3089ecb7f56bd2d3c35b7eea0e3e777959f7fc36e38ea11b1dd920e5b2@127.0.0.1:5432/outline-test
SECRET_KEY=F0E5AD933D7F6FD8F4DBB3E038C501C052DC0593C686D21ACB30AE205D2F634B

SMTP_HOST=smtp.example.com
Expand Down Expand Up @@ -41,3 +41,5 @@ UTILS_SECRET=test-utils-secret

DEBUG=
LOG_LEVEL=error

DEFAULT_EMAIL_DOMAIN=askii.ai
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ name: CI

on:
push:
branches: [main]
branches: [foss-main]
pull_request:
branches: [main]
branches: [foss-main]

env:
NODE_ENV: test
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/codeql-analysis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ name: "CodeQL"

on:
push:
branches: [main]
branches: [foss-main]
pull_request:
# The branches below must be a subset of the branches above
branches: [main]
branches: [foss-main]
schedule:
- cron: "28 15 * * 2"

Expand Down
10 changes: 3 additions & 7 deletions app/scenes/Logout.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,13 @@
import { Redirect } from "react-router-dom";
import env from "~/env";
import useStores from "~/hooks/useStores";
import { logoutPath } from "~/utils/routeHelpers";

const Logout = () => {
const { auth } = useStores();

void auth.logout({ userInitiated: true });

if (env.OIDC_LOGOUT_URI) {
return null; // user will be redirected to logout URI after logout
}
return <Redirect to={logoutPath()} />;
// AuthStore.logout() always sets logoutRedirectUri to the portal host; the
// unauthenticated branch in Authenticated.tsx performs the actual navigation.
return null;
};

export default Logout;
3 changes: 2 additions & 1 deletion app/stores/AuthStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -352,7 +352,8 @@ export default class AuthStore extends Store<Team> {
}

if (userInitiated) {
this.logoutRedirectUri = env.OIDC_LOGOUT_URI;
const portalHost = window.location.hostname.replace(/^[^.]+\.(?=[^.]*\.[^.]*\.)/, "");
this.logoutRedirectUri = `${window.location.protocol}//${portalHost}`;
}

if (clearCache) {
Expand Down
37 changes: 37 additions & 0 deletions app/utils/ApiClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,16 @@ class ApiClient {
/** Map of in-flight POST requests for deduplication, keyed by path + body. */
private inflightRequests = new Map<string, Promise<any>>();

/**
* Set once we've triggered a full-page reload to recover from an SSO
* stale-session 401 (proxy identity changed; server cleared the JWT
* cookie via err.headers; client must reload to pick up new identity).
* Used to silence subsequent in-flight 401s that would otherwise surface
* as "failed to get docs / access tokens" toasts during the brief window
* before the navigation completes.
*/
private isReauthenticating = false;

constructor(options: Options = {}) {
this.baseUrl = options.baseUrl || "/api";
}
Expand Down Expand Up @@ -180,6 +190,33 @@ class ApiClient {
// Handle 401, log out user
if (response.status === 401) {
if (!this.shareId) {
if (env.AUTH_TYPE === "SSO") {
// In ForwardAuth mode, the stale JWT cookie has been cleared by the
// server. Navigate to the current URL so the browser makes a fresh
// HTTP request — the proxy will inject new identity headers and a new
// session will be issued automatically.
//
// We skip auth.logout() here: clearing MobX state would cause
// <Authenticated> to render <Redirect to="/" /> and land the user on
// the login page instead of their original document.
//
// Multiple parallel requests typically 401 together on the first
// post-switch render (docs, access tokens, etc.). Without the
// isReauthenticating guard, each one would throw an AuthorizationError
// that propagates to a toast handler ("failed to get docs / access
// tokens") before the page finishes navigating — visible flash of
// error UI for what is actually a successful identity-switch flow.
// First 401 triggers the reload; subsequent in-flight 401s stall on
// a never-resolving promise until the navigation completes.
if (!this.isReauthenticating) {
this.isReauthenticating = true;
window.location.replace(window.location.href);
}
return new Promise<T>(() => {
// Intentionally never resolves — the page is navigating away.
// Throwing would surface as a toast on each in-flight request.
});
}
await stores.auth.logout({
savePath: true,
clearCache: false,
Expand Down
34 changes: 31 additions & 3 deletions server/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
} from "class-validator";
import uniq from "lodash/uniq";
import { languages } from "@shared/i18n";
import { Day, Hour } from "@shared/utils/time";
import { Week } from "@shared/utils/time";
import {
CannotUseWith,
CannotUseWithout,
Expand Down Expand Up @@ -518,6 +518,24 @@ export class Environment {
public WORKER_CONCURRENCY_TASKS =
this.toOptionalNumber(environment.WORKER_CONCURRENCY_TASKS) ?? 10;

/**
* The authentication type to use. When set to "SSO", the server will trust
* X-Auth-Request-Email and X-Auth-Request-User headers injected by a reverse
* proxy (e.g. oauth2-proxy, Authelia) for authentication and automatic user
* provisioning. Only enable this when Outline is deployed behind a trusted
* authenticating proxy on a self-hosted instance.
*/
@Public
@IsOptional()
@IsIn(["SSO"])
public AUTH_TYPE = this.toOptionalString(environment.AUTH_TYPE);

/**
* Default email domain for ForwardAuth users.
*/
@IsOptional()
public DEFAULT_EMAIL_DOMAIN = environment.DEFAULT_EMAIL_DOMAIN ?? "askii.ai";

/**
* A boolean switch to toggle the rate limiter at application web server.
*/
Expand Down Expand Up @@ -702,19 +720,21 @@ export class Environment {

/**
* The number of seconds access tokens issue by the OAuth provider are valid.
* Default 7d matches FOSS devstack SESSION_COOKIE_MAX_AGE_SECONDS when unset.
*/
@IsNumber()
public OAUTH_PROVIDER_ACCESS_TOKEN_LIFETIME =
this.toOptionalNumber(environment.OAUTH_PROVIDER_ACCESS_TOKEN_LIFETIME) ??
Hour.seconds;
Week.seconds;

/**
* The number of seconds refresh tokens issue by the OAuth provider are valid.
* Default 14d matches FOSS devstack SESSION_REFRESH_TOKEN_MAX_AGE_SECONDS when unset.
*/
@IsNumber()
public OAUTH_PROVIDER_REFRESH_TOKEN_LIFETIME =
this.toOptionalNumber(environment.OAUTH_PROVIDER_REFRESH_TOKEN_LIFETIME) ??
30 * Day.seconds;
2 * Week.seconds;

/**
* The number of seconds authorization codes issue by the OAuth provider are valid.
Expand Down Expand Up @@ -778,6 +798,14 @@ export class Environment {
@Public
public APP_NAME = "Outline";

/**
* The subdomain name of the portal (e.g. "moneta" for moneta.example.com).
* Used to redirect users to the portal after logout. Defaults to "foss".
*/
@Public
@IsOptional()
public SMB_NAME = environment.SMB_NAME ?? "moneta";

/**
* Gravity constant for time decay in popularity scoring. Higher values cause
* faster decay of older content. Default is 0.7.
Expand Down
Loading