diff --git a/config.json.example b/config.json.example index 69bdb858..e9105eb2 100644 --- a/config.json.example +++ b/config.json.example @@ -106,6 +106,19 @@ "email": "change or delete this: attribute map for `email` (default: NameID)" } }, + "oauth2": { + "baseURL": "https://auth.example.com/", + "userProfileURL": "https://auth.example.com/oauth2/userinfo/", + "tokenURL": "https://auth.example.com/oauth2/token/", + "authorizationURL": "https://auth.example.com/oauth2/authorize/", + "clientID": "change-this-id", + "clientSecret": "change-this-secret", + "scope": "openid profile user", + "userProfileUsernameAttr": "preferred_username", + "userProfileEmailAttr": "email", + "userProfileDisplayNameAttr": "name", + "pkce": true + }, "imgur": { "clientID": "change this" }, diff --git a/docs/content/configuration.md b/docs/content/configuration.md index 43c9d607..adcb724d 100644 --- a/docs/content/configuration.md +++ b/docs/content/configuration.md @@ -209,22 +209,23 @@ these are rarely used for various reasons. ### OAuth2 Login -| config file | environment | **default** and example value | description | -|-------------|---------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `oauth2` | | `{baseURL: ..., userProfileURL: ..., userProfileUsernameAttr: ..., userProfileDisplayNameAttr: ..., userProfileEmailAttr: ..., tokenURL: ..., authorizationURL: ..., clientID: ..., clientSecret: ..., scope: ...}` | An object detailing your OAuth2 provider. Refer to the [Mattermost](guides/auth/mattermost-self-hosted.md) or [Nextcloud](guides/auth/nextcloud.md) examples for more details! | -| | `CMD_OAUTH2_USER_PROFILE_URL` | **no default**, `https://example.com` | Where to retrieve information about a user after successful login. Needs to output JSON. (no default value) Refer to the [Mattermost](guides/auth/mattermost-self-hosted.md) or [Nextcloud](guides/auth/nextcloud.md) examples for more details on all of the `CMD_OAUTH2...` options. | -| | `CMD_OAUTH2_USER_PROFILE_USERNAME_ATTR` | **no default**, `name` | where to find the username in the JSON from the user profile URL. (no default value) | -| | `CMD_OAUTH2_USER_PROFILE_DISPLAY_NAME_ATTR` | **no default**, `display-name` | where to find the display-name in the JSON from the user profile URL. (no default value) | -| | `CMD_OAUTH2_USER_PROFILE_EMAIL_ATTR` | **no default**, `email` | where to find the email address in the JSON from the user profile URL. (no default value) | -| | `CMD_OAUTH2_USER_PROFILE_ID_ATTR` | **no default**, `user_uuid` | where to find the dedicated user ID (optional, overrides `CMD_OAUTH2_USER_PROFILE_USERNAME_ATTR`) | -| | `CMD_OAUTH2_TOKEN_URL` | **no default**, `https://example.com` | sometimes called token endpoint, please refer to the documentation of your OAuth2 provider (no default value) | -| | `CMD_OAUTH2_AUTHORIZATION_URL` | **no default**, `https://example.com` | authorization URL of your provider, please refer to the documentation of your OAuth2 provider (no default value) | -| | `CMD_OAUTH2_CLIENT_ID` | **no default**, `afae02fckafd...` | you will get this from your OAuth2 provider when you register HedgeDoc as OAuth2-client, (no default value) | -| | `CMD_OAUTH2_CLIENT_SECRET` | **no default**, `afae02fckafd...` | you will get this from your OAuth2 provider when you register HedgeDoc as OAuth2-client, (no default value) | -| | `CMD_OAUTH2_PROVIDERNAME` | **no default**, `My institution` | Optional name to be displayed at login form indicating the oAuth2 provider | -| | `CMD_OAUTH2_SCOPE` | **no default**, `openid email profile` | Scope to request for OIDC (OpenID Connect) providers. | -| | `CMD_OAUTH2_ROLES_CLAIM` | **no default**, `roles` | ID token claim, which is supposed to provide an array of strings of roles | -| | `CMD_OAUTH2_ACCESS_ROLE` | **no default**, `role/hedgedoc` | The role which should be included in the ID token roles claim to grant access | +| config file | environment | **default** and example value | description | +|-------------|---------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `oauth2` | | `{baseURL: ..., userProfileURL: ..., userProfileUsernameAttr: ..., userProfileDisplayNameAttr: ..., userProfileEmailAttr: ..., tokenURL: ..., authorizationURL: ..., clientID: ..., clientSecret: ..., scope: ..., pkce: ...}` | An object detailing your OAuth2 provider. Refer to the [Mattermost](guides/auth/mattermost-self-hosted.md) or [Nextcloud](guides/auth/nextcloud.md) examples for more details! | +| | `CMD_OAUTH2_USER_PROFILE_URL` | **no default**, `https://example.com` | Where to retrieve information about a user after successful login. Needs to output JSON. (no default value) Refer to the [Mattermost](guides/auth/mattermost-self-hosted.md) or [Nextcloud](guides/auth/nextcloud.md) examples for more details on all of the `CMD_OAUTH2...` options. | +| | `CMD_OAUTH2_USER_PROFILE_USERNAME_ATTR` | **no default**, `name` | where to find the username in the JSON from the user profile URL. (no default value) | +| | `CMD_OAUTH2_USER_PROFILE_DISPLAY_NAME_ATTR` | **no default**, `display-name` | where to find the display-name in the JSON from the user profile URL. (no default value) | +| | `CMD_OAUTH2_USER_PROFILE_EMAIL_ATTR` | **no default**, `email` | where to find the email address in the JSON from the user profile URL. (no default value) | +| | `CMD_OAUTH2_USER_PROFILE_ID_ATTR` | **no default**, `user_uuid` | where to find the dedicated user ID (optional, overrides `CMD_OAUTH2_USER_PROFILE_USERNAME_ATTR`) | +| | `CMD_OAUTH2_TOKEN_URL` | **no default**, `https://example.com` | sometimes called token endpoint, please refer to the documentation of your OAuth2 provider (no default value) | +| | `CMD_OAUTH2_AUTHORIZATION_URL` | **no default**, `https://example.com` | authorization URL of your provider, please refer to the documentation of your OAuth2 provider (no default value) | +| | `CMD_OAUTH2_CLIENT_ID` | **no default**, `afae02fckafd...` | you will get this from your OAuth2 provider when you register HedgeDoc as OAuth2-client, (no default value) | +| | `CMD_OAUTH2_CLIENT_SECRET` | **no default**, `afae02fckafd...` | you will get this from your OAuth2 provider when you register HedgeDoc as OAuth2-client, (no default value) | +| | `CMD_OAUTH2_PROVIDERNAME` | **no default**, `My institution` | Optional name to be displayed at login form indicating the oAuth2 provider | +| | `CMD_OAUTH2_SCOPE` | **no default**, `openid email profile` | Scope to request for OIDC (OpenID Connect) providers. | +| | `CMD_OAUTH2_ROLES_CLAIM` | **no default**, `roles` | ID token claim, which is supposed to provide an array of strings of roles | +| | `CMD_OAUTH2_ACCESS_ROLE` | **no default**, `role/hedgedoc` | The role which should be included in the ID token roles claim to grant access | +| | `CMD_OAUTH2_PKCE` | **`false`**, `true` | Whether to use PKCE auth. Defaults to false since not every OAuth2 provider supports it. | !!! info If you are using a [CA not trusted by Node.js](https://github.com/nodejs/node/issues/4175) (like Let's Encrypt e.g) for diff --git a/lib/config/default.js b/lib/config/default.js index 3110119b..19346db4 100644 --- a/lib/config/default.js +++ b/lib/config/default.js @@ -104,7 +104,8 @@ module.exports = { tokenURL: undefined, clientID: undefined, clientSecret: undefined, - scope: undefined + scope: undefined, + pkce: false }, facebook: { clientID: undefined, diff --git a/lib/config/environment.js b/lib/config/environment.js index 6b9f8312..393928b5 100644 --- a/lib/config/environment.js +++ b/lib/config/environment.js @@ -115,7 +115,8 @@ module.exports = { clientSecret: process.env.CMD_OAUTH2_CLIENT_SECRET, scope: process.env.CMD_OAUTH2_SCOPE, rolesClaim: process.env.CMD_OAUTH2_ROLES_CLAIM, - accessRole: process.env.CMD_OAUTH2_ACCESS_ROLE + accessRole: process.env.CMD_OAUTH2_ACCESS_ROLE, + pkce: toBooleanConfig(process.env.CMD_OAUTH2_PKCE) }, dropbox: { clientID: process.env.CMD_DROPBOX_CLIENTID, diff --git a/lib/web/auth/dropbox/index.js b/lib/web/auth/dropbox/index.js index c35f04e3..569887df 100644 --- a/lib/web/auth/dropbox/index.js +++ b/lib/web/auth/dropbox/index.js @@ -12,7 +12,9 @@ passport.use(new DropboxStrategy({ apiVersion: '2', clientID: config.dropbox.clientID, clientSecret: config.dropbox.clientSecret, - callbackURL: config.serverURL + '/auth/dropbox/callback' + callbackURL: config.serverURL + '/auth/dropbox/callback', + state: true, + pkce: true }, passportGeneralCallback)) dropboxAuth.get('/auth/dropbox', function (req, res, next) { diff --git a/lib/web/auth/facebook/index.js b/lib/web/auth/facebook/index.js index acf566eb..162cf6a8 100644 --- a/lib/web/auth/facebook/index.js +++ b/lib/web/auth/facebook/index.js @@ -12,7 +12,9 @@ const facebookAuth = module.exports = Router() passport.use(new FacebookStrategy({ clientID: config.facebook.clientID, clientSecret: config.facebook.clientSecret, - callbackURL: config.serverURL + '/auth/facebook/callback' + callbackURL: config.serverURL + '/auth/facebook/callback', + state: true, + pkce: true }, passportGeneralCallback)) facebookAuth.get('/auth/facebook', function (req, res, next) { diff --git a/lib/web/auth/github/index.js b/lib/web/auth/github/index.js index c7f7e5d1..81ad77bd 100644 --- a/lib/web/auth/github/index.js +++ b/lib/web/auth/github/index.js @@ -12,7 +12,9 @@ const githubAuth = module.exports = Router() passport.use(new GithubStrategy({ clientID: config.github.clientID, clientSecret: config.github.clientSecret, - callbackURL: config.serverURL + '/auth/github/callback' + callbackURL: config.serverURL + '/auth/github/callback', + pkce: true, + state: true }, passportGeneralCallback)) githubAuth.get('/auth/github', function (req, res, next) { diff --git a/lib/web/auth/gitlab/index.js b/lib/web/auth/gitlab/index.js index 11579bd1..b9445da6 100644 --- a/lib/web/auth/gitlab/index.js +++ b/lib/web/auth/gitlab/index.js @@ -14,7 +14,9 @@ passport.use(new GitlabStrategy({ clientID: config.gitlab.clientID, clientSecret: config.gitlab.clientSecret, scope: config.gitlab.scope, - callbackURL: config.serverURL + '/auth/gitlab/callback' + callbackURL: config.serverURL + '/auth/gitlab/callback', + pkce: true, + state: true }, passportGeneralCallback)) gitlabAuth.get('/auth/gitlab', function (req, res, next) { diff --git a/lib/web/auth/google/index.js b/lib/web/auth/google/index.js index 0262dedf..51ffaee0 100644 --- a/lib/web/auth/google/index.js +++ b/lib/web/auth/google/index.js @@ -12,7 +12,9 @@ passport.use(new GoogleStrategy({ clientID: config.google.clientID, clientSecret: config.google.clientSecret, callbackURL: config.serverURL + '/auth/google/callback', - userProfileURL: 'https://www.googleapis.com/oauth2/v3/userinfo' + userProfileURL: 'https://www.googleapis.com/oauth2/v3/userinfo', + pkce: true, + state: true }, passportGeneralCallback)) googleAuth.get('/auth/google', function (req, res, next) { diff --git a/lib/web/auth/mattermost/index.js b/lib/web/auth/mattermost/index.js index 2f15c812..071fd57e 100644 --- a/lib/web/auth/mattermost/index.js +++ b/lib/web/auth/mattermost/index.js @@ -16,7 +16,8 @@ const mattermostStrategy = new OAuthStrategy({ tokenURL: config.mattermost.baseURL + '/oauth/access_token', clientID: config.mattermost.clientID, clientSecret: config.mattermost.clientSecret, - callbackURL: config.serverURL + '/auth/mattermost/callback' + callbackURL: config.serverURL + '/auth/mattermost/callback', + state: true }, passportGeneralCallback) mattermostStrategy.userProfile = (accessToken, done) => { diff --git a/lib/web/auth/oauth2/index.js b/lib/web/auth/oauth2/index.js index b0ffa5e8..0ce38dde 100644 --- a/lib/web/auth/oauth2/index.js +++ b/lib/web/auth/oauth2/index.js @@ -138,6 +138,7 @@ passport.use(new OAuth2CustomStrategy({ callbackURL: config.serverURL + '/auth/oauth2/callback', userProfileURL: config.oauth2.userProfileURL, scope: config.oauth2.scope, + pkce: config.oauth2.pkce, state: true }, passportGeneralCallback)) diff --git a/public/docs/release-notes.md b/public/docs/release-notes.md index 9f7b6f78..dcca9b55 100644 --- a/public/docs/release-notes.md +++ b/public/docs/release-notes.md @@ -16,6 +16,7 @@ - Force kill the server after a timeout when waiting for the realtime server to close connections on shutdown - Secure iframes with `credentialless` and `sandbox` attributes - Fix regexes for `[time=...]`, `[name=...]` and `[color=...]` shortcodes in lists +- Use `state` parameter for OAuth2 flows and PKCE where applicable ## 1.10.3 2025-04-09