From 729b387536d2bcf43a20b2ddead4bffdfd342d2a Mon Sep 17 00:00:00 2001 From: Joachim Mathes Date: Sat, 21 Nov 2020 20:26:12 +0100 Subject: [PATCH] Add oauth2 authorization Signed-off-by: Joachim Mathes --- docs/configuration.md | 3 +++ lib/config/environment.js | 5 ++++- lib/web/auth/oauth2/index.js | 23 ++++++++++++++++++++++- 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 197a2ca07..25df7366c 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -189,12 +189,15 @@ these are rarely used for various reasons. | | `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 | ### SAML Login diff --git a/lib/config/environment.js b/lib/config/environment.js index cf9fb5a11..494e669f3 100644 --- a/lib/config/environment.js +++ b/lib/config/environment.js @@ -87,6 +87,7 @@ module.exports = { providerName: process.env.CMD_OAUTH2_PROVIDERNAME, baseURL: process.env.CMD_OAUTH2_BASEURL, userProfileURL: process.env.CMD_OAUTH2_USER_PROFILE_URL, + userProfileIdAttr: process.env.CMD_OAUTH2_USER_PROFILE_ID_ATTR, userProfileUsernameAttr: process.env.CMD_OAUTH2_USER_PROFILE_USERNAME_ATTR, userProfileDisplayNameAttr: process.env.CMD_OAUTH2_USER_PROFILE_DISPLAY_NAME_ATTR, userProfileEmailAttr: process.env.CMD_OAUTH2_USER_PROFILE_EMAIL_ATTR, @@ -94,7 +95,9 @@ module.exports = { authorizationURL: process.env.CMD_OAUTH2_AUTHORIZATION_URL, clientID: process.env.CMD_OAUTH2_CLIENT_ID, clientSecret: process.env.CMD_OAUTH2_CLIENT_SECRET, - scope: process.env.CMD_OAUTH2_SCOPE + scope: process.env.CMD_OAUTH2_SCOPE, + rolesClaim: process.env.CMD_OAUTH2_ROLES_CLAIM, + accessRole: process.env.CMD_OAUTH2_ACCESS_ROLE }, dropbox: { clientID: process.env.CMD_DROPBOX_CLIENTID, diff --git a/lib/web/auth/oauth2/index.js b/lib/web/auth/oauth2/index.js index 6e3e8373f..b8e62ddab 100644 --- a/lib/web/auth/oauth2/index.js +++ b/lib/web/auth/oauth2/index.js @@ -4,6 +4,7 @@ const Router = require('express').Router const passport = require('passport') const { Strategy, InternalOAuthError } = require('passport-oauth2') const config = require('../../../config') +const logger = require('../../../logger') const { passportGeneralCallback } = require('../utils') let oauth2Auth = module.exports = Router() @@ -31,6 +32,7 @@ class OAuth2CustomStrategy extends Strategy { return done(new Error('Failed to parse user profile')) } + checkAuthorization(json, done) let profile = parseProfile(json) profile.provider = 'oauth2' @@ -50,18 +52,36 @@ function extractProfileAttribute (data, path) { } function parseProfile (data) { + const id = extractProfileAttribute(data, config.oauth2.userProfileIdAttr) const username = extractProfileAttribute(data, config.oauth2.userProfileUsernameAttr) const displayName = extractProfileAttribute(data, config.oauth2.userProfileDisplayNameAttr) const email = extractProfileAttribute(data, config.oauth2.userProfileEmailAttr) return { - id: username, + id: id || username, username: username, displayName: displayName, email: email } } +function checkAuthorization (data, done) { + const roles = extractProfileAttribute(data, config.oauth2.rolesClaim) + const username = extractProfileAttribute(data, config.oauth2.userProfileUsernameAttr) + + if (config.oauth2.accessRole) { + if (!roles) { + logger.error('oauth2: "accessRole" configured, but user profile doesn\'t contain roles attribute. Permission denied') + return done('Permission denied', null) + } + + if (!roles.includes(config.oauth2.accessRole)) { + logger.debug(`oauth2: user "${username}" doesn't have the required role. Permission denied`) + return done('Permission denied', null) + } + } +} + OAuth2CustomStrategy.prototype.userProfile = function (accessToken, done) { this._oauth2.get(this._userProfileURL, accessToken, function (err, body, res) { var json @@ -76,6 +96,7 @@ OAuth2CustomStrategy.prototype.userProfile = function (accessToken, done) { return done(new Error('Failed to parse user profile')) } + checkAuthorization(json, done) let profile = parseProfile(json) profile.provider = 'oauth2'