From 5f2164cb12def29f695995a2cc8a648d5ccf2adb Mon Sep 17 00:00:00 2001 From: 3wc <3wc.github@doesthisthing.work> Date: Mon, 18 Oct 2021 21:13:18 +0200 Subject: [PATCH 1/5] Add generic OAuth support --- docs/config-oauth2.md | 15 +++++++++++++ docs/configuration.md | 1 + server/social.coffee | 50 +++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 64 insertions(+), 2 deletions(-) create mode 100644 docs/config-oauth2.md diff --git a/docs/config-oauth2.md b/docs/config-oauth2.md new file mode 100644 index 0000000..8313124 --- /dev/null +++ b/docs/config-oauth2.md @@ -0,0 +1,15 @@ + + +```JSON +{ + "farm": true, + "security_type": "passportjs", + "oauth2_clientID": "CLIENT ID", + "oauth2_clientSecret": "CLIENT SECRET", + "oauth2_AuthorizationURL": "https://auth.example.com/oauth2/authorize", + "oauth2_TokenURL": "https://auth.example.com/oauth2/token", + "wikiDomains": { + "example.wiki": {} + } +} +``` diff --git a/docs/configuration.md b/docs/configuration.md index 455daf8..7cf9c09 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -17,3 +17,4 @@ See, depending on which identity provider you choose to use: * [GitHub](./config-github.md) * [Google](./config-google.md) * [Twitter](./config-twitter.md) +* [Generic OAuth](./config-oauth2.md) diff --git a/server/social.coffee b/server/social.coffee index 1cd9fcb..a4dad83 100644 --- a/server/social.coffee +++ b/server/social.coffee @@ -135,7 +135,7 @@ module.exports = exports = (log, loga, argv) -> idProvider = _.head(_.keys(req.session.passport.user)) console.log 'idProvider: ', idProvider switch idProvider - when 'github', 'google', 'twitter' + when 'github', 'google', 'twitter', 'oauth2' if _.isEqual(owner[idProvider].id, req.session.passport.user[idProvider].id) return true else @@ -165,7 +165,7 @@ module.exports = exports = (log, loga, argv) -> return false switch idProvider - when "github", "google", "twitter" + when "github", "google", "twitter", 'oauth2' if _.isEqual(admin[idProvider], req.session.passport.user[idProvider].id) return true else @@ -194,6 +194,32 @@ module.exports = exports = (log, loga, argv) -> passport.deserializeUser = (obj, req, done) -> done(null, obj) + # OAuth Strategy + if argv.oauth2_clientID? and argv.oauth2_clientSecret? + ids.push('oauth2') + OAuth2Strategy = require('passport-oauth2').Strategy + + oauth2StrategyName = callbackHost + 'OAuth' + + passport.use(oauth2StrategyName, new OAuth2Strategy({ + clientID: argv.oauth2_clientID + clientSecret: argv.oauth2_clientSecret + authorizationURL: argv.oauth2_AuthorizationURL + tokenURL: argv.oauth2_TokenURL + # userURL: argv.oauth2_UserURL + # scope: 'user:emails' + # callbackURL is optional, and if it exists must match that given in + # the OAuth application settings - so we don't specify it. + }, (accessToken, refreshToken, params, profile, cb) -> + console.log("params", params) + console.log("profile", profile) + user.oauth2 = { + id: params.user_id, + username: params.user_id, + displayName: params.user_id, + } + cb(null, user))) + # Github Strategy if argv.github_clientID? and argv.github_clientSecret? ids.push('github') @@ -275,6 +301,11 @@ module.exports = exports = (log, loga, argv) -> app.use(passport.initialize()) app.use(passport.session()) + # OAuth2 + app.get('/auth/oauth2', passport.authenticate(oauth2StrategyName), (req, res) -> ) + app.get('/auth/oauth2/callback', + passport.authenticate(oauth2StrategyName, { successRedirect: '/auth/loginDone', failureRedirect: '/auth/loginDialog'})) + # Github app.get('/auth/github', passport.authenticate(githubStrategyName, {scope: 'user:email'}), (req, res) -> ) app.get('/auth/github/callback', @@ -317,6 +348,7 @@ module.exports = exports = (log, loga, argv) -> schemeButtons = [] _(ids).forEach (scheme) -> switch scheme + when "oauth2" then schemeButtons.push({button: "OAuth2"}) when "twitter" then schemeButtons.push({button: "Twitter"}) when "github" then schemeButtons.push({button: "Github"}) when "google" @@ -504,6 +536,13 @@ module.exports = exports = (log, loga, argv) -> userIds = {} idProviders.forEach (idProvider) -> id = switch idProvider + when "oauth2" then { + name: user.oauth2.displayName + oauth2: { + id: user.oauth2.id + username: user.oauth2.username + } + } when "twitter" then { name: user.twitter.displayName twitter: { @@ -580,6 +619,13 @@ module.exports = exports = (log, loga, argv) -> id = {} idProviders.forEach (idProvider) -> id = switch idProvider + when "oauth2" then { + name: user.oauth2.displayName + oauth2: { + id: user.oauth2.id + username: user.oauth2.username + } + } when "twitter" then { name: user.twitter.displayName twitter: { From f4f44afa35c91bc4325a3fa26c942acb523e4a33 Mon Sep 17 00:00:00 2001 From: 3wc <3wc.github@doesthisthing.work> Date: Fri, 22 Oct 2021 00:29:07 +0200 Subject: [PATCH 2/5] Add passport-oauth2 to package.json --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 8f27f32..3b1e257 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "passport": "^0.3.2", "passport-github": "github:FedWiki/passport-github#70fe99ba7e66422092a19d89f4b1ab1a23eac644", "passport-google-oauth20": "^2.0.0", + "passport-oauth2": "^1.6.1", "passport-twitter": "^1.0.4", "persona-pass": "^0.2.1", "qs": "^6.7.0", From 001def2feaa99199c25a40363952e4b0ff90bfd1 Mon Sep 17 00:00:00 2001 From: 3wc <3wc.github@doesthisthing.work> Date: Sat, 23 Oct 2021 16:56:08 +0200 Subject: [PATCH 3/5] Custom callback and user profile URLs for OAuth2 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit For parsing `oauth2_UsernameField` values like `profile.preferred_username`, this makes use of `eval()` which is generally Evilâ„¢, but I'm assuming that anyone with permission to edit config.json likely has permission to make changes to the fedwiki source code already anyway, so it's fragile rather than increasing a security attack surface. An alternative would be using a small function to look up properties of the `params` / `profile` objects using the same dotted-path notation. --- docs/config-oauth2.md | 51 ++++++++++++++++++++++++++++++++++++++++--- server/social.coffee | 35 ++++++++++++++++++++++------- 2 files changed, 75 insertions(+), 11 deletions(-) diff --git a/docs/config-oauth2.md b/docs/config-oauth2.md index 8313124..b1710fd 100644 --- a/docs/config-oauth2.md +++ b/docs/config-oauth2.md @@ -1,4 +1,36 @@ +## Generic OAuth 2 +### Login provider set-up + +Like the other PassportJS login providers, we'll need a separate "OAuth2 Client" +(others call it an "app", a "product" etc.) for our Federated Wiki instance. + +How to do this varies slightly for each provider. + +### `config.json` + +In general, you will need to specify: +* `oauth2_clientID` -- some systems generate this for you, others allow you to + specify it +* `oauth2_clientSecret` -- secure key (keep this secret!) +* `oauth2_AuthorizationURL` and `oauth2_TokenURL` -- from your login provider's documentation + +You will also need to specify a callback URL. For some providers, you can add +this when making a new "OAuth Client" for your wiki, for others you will need to +specify it with `oauth2_CallbackURL`. + +You might also need to tell Federated Wiki how to look up usernames: +* `oauth2_UserInfoURL` -- from login provider's documentation +* `oauth2_UsernameField` -- starting with + * `params` for information returned in the original token request, or + * `profile` for data returned from `oauth2_UserInfoURL`, if you provided it. + +Sometimes, you'll be able to look up the URLs by visiting your provider's +`/.well-known/openid-configuration` URL in a web browser. + +### Examples + +#### Nextcloud ```JSON { @@ -8,8 +40,21 @@ "oauth2_clientSecret": "CLIENT SECRET", "oauth2_AuthorizationURL": "https://auth.example.com/oauth2/authorize", "oauth2_TokenURL": "https://auth.example.com/oauth2/token", - "wikiDomains": { - "example.wiki": {} - } +} +``` + +#### Keycloak + +```JSON +{ + "farm": true, + "security_type": "passportjs", + "oauth2_clientID": "CLIENT ID", + "oauth2_clientSecret": "CLIENT SECRET", + "oauth2_AuthorizationURL": "https://auth.example.com/auth/realms/Wiki.Cafe/protocol/openid-connect/auth", + "oauth2_TokenURL": "https://auth.example.com/auth/realms/Wiki.Cafe/protocol/openid-connect/token", + "oauth2_UserInfoURL": "https://auth.example.com/auth/realms/Wiki.Cafe/protocol/openid-connect/userinfo", + "oauth2_CallbackURL": "http://localhost:3000/auth/oauth2/callback", + "oauth2_UsernameField": "profile.preferred_username" } ``` diff --git a/server/social.coffee b/server/social.coffee index a4dad83..97ffce0 100644 --- a/server/social.coffee +++ b/server/social.coffee @@ -201,23 +201,42 @@ module.exports = exports = (log, loga, argv) -> oauth2StrategyName = callbackHost + 'OAuth' + if argv.oauth2_UserInfoURL? + OAuth2Strategy::userProfile = (accesstoken, done) -> + console.log "hello" + console.log accesstoken + @_oauth2._request "GET", argv.oauth2_UserInfoURL, null, null, accesstoken, (err, data) -> + if err + return done err + try + data = JSON.parse data + catch e + return done e + done(null, data) + passport.use(oauth2StrategyName, new OAuth2Strategy({ clientID: argv.oauth2_clientID clientSecret: argv.oauth2_clientSecret authorizationURL: argv.oauth2_AuthorizationURL - tokenURL: argv.oauth2_TokenURL - # userURL: argv.oauth2_UserURL - # scope: 'user:emails' - # callbackURL is optional, and if it exists must match that given in - # the OAuth application settings - so we don't specify it. + tokenURL: argv.oauth2_TokenURL, + # not all providers have a way of specifying the callback URL + callbackURL: argv.oauth2_CallbackURL, + userInfoURL: argv.oauth2_UserInfoURL }, (accessToken, refreshToken, params, profile, cb) -> + console.log("accessToken", accessToken) + console.log("refreshToken", refreshToken) console.log("params", params) console.log("profile", profile) + if argv.oauth2_UsernameField? + username_query = argv.oauth2_UsernameField + else + username_query = 'params.user_id' user.oauth2 = { - id: params.user_id, - username: params.user_id, - displayName: params.user_id, + id: eval username_query, + username: eval username_query + displayName: eval username_query } + console.log user.oauth2 cb(null, user))) # Github Strategy From 7eba6ba411a0313855652725dd383774c5c1eee7 Mon Sep 17 00:00:00 2001 From: Paul Rodwell Date: Tue, 2 Nov 2021 18:56:21 +0000 Subject: [PATCH 4/5] replacing eval() with function using property accessors --- docs/config-oauth2.md | 2 +- server/social.coffee | 34 +++++++++++++++++++++++++++++----- 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/docs/config-oauth2.md b/docs/config-oauth2.md index b1710fd..595cd2a 100644 --- a/docs/config-oauth2.md +++ b/docs/config-oauth2.md @@ -21,7 +21,7 @@ specify it with `oauth2_CallbackURL`. You might also need to tell Federated Wiki how to look up usernames: * `oauth2_UserInfoURL` -- from login provider's documentation -* `oauth2_UsernameField` -- starting with +* `oauth2_IdField`, `oauth2_DisplayNameField`, `oauth2_UsernameField` -- starting with * `params` for information returned in the original token request, or * `profile` for data returned from `oauth2_UserInfoURL`, if you provided it. diff --git a/server/social.coffee b/server/social.coffee index 97ffce0..2b56754 100644 --- a/server/social.coffee +++ b/server/social.coffee @@ -223,6 +223,26 @@ module.exports = exports = (log, loga, argv) -> callbackURL: argv.oauth2_CallbackURL, userInfoURL: argv.oauth2_UserInfoURL }, (accessToken, refreshToken, params, profile, cb) -> + + extractUserInfo = (uiParam, uiDef) -> + uiPath = '' + if typeof uiParam == 'undefined' then (uiPath = uiDef) else (uiPath = uiParam) + console.log('extractUI', uiParam, uiDef, uiPath) + sParts = uiPath.split('.') + sFrom = sParts.shift() + switch sFrom + when "params" + obj = params + when "profile" + obj = profile + else + console.error('*** source of user info not recognised', uiPath) + obj = {} + + while (sParts.length) + obj = obj[sParts.shift()] + return obj + console.log("accessToken", accessToken) console.log("refreshToken", refreshToken) console.log("params", params) @@ -231,11 +251,15 @@ module.exports = exports = (log, loga, argv) -> username_query = argv.oauth2_UsernameField else username_query = 'params.user_id' - user.oauth2 = { - id: eval username_query, - username: eval username_query - displayName: eval username_query - } + + try + user.oauth2 = { + id: extractUserInfo(argv.oauth2_IdField, 'params.user_id') + username: extractUserInfo(argv.oauth2_UsernameField, 'params.user_id') + displayName: extractUserInfo(argv.oauth2_DisplayNameField, 'params.user_id') + } + catch e + console.error('*** Error extracting user info:', e) console.log user.oauth2 cb(null, user))) From 88b0e2b825c240c63c9c629b84e9fae947cddf1e Mon Sep 17 00:00:00 2001 From: Paul Rodwell Date: Thu, 4 Nov 2021 10:12:59 +0000 Subject: [PATCH 5/5] callbackURL has fix location, rather than being a parameter. --- docs/config-oauth2.md | 1 - server/social.coffee | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/config-oauth2.md b/docs/config-oauth2.md index 595cd2a..9bb94c8 100644 --- a/docs/config-oauth2.md +++ b/docs/config-oauth2.md @@ -54,7 +54,6 @@ Sometimes, you'll be able to look up the URLs by visiting your provider's "oauth2_AuthorizationURL": "https://auth.example.com/auth/realms/Wiki.Cafe/protocol/openid-connect/auth", "oauth2_TokenURL": "https://auth.example.com/auth/realms/Wiki.Cafe/protocol/openid-connect/token", "oauth2_UserInfoURL": "https://auth.example.com/auth/realms/Wiki.Cafe/protocol/openid-connect/userinfo", - "oauth2_CallbackURL": "http://localhost:3000/auth/oauth2/callback", "oauth2_UsernameField": "profile.preferred_username" } ``` diff --git a/server/social.coffee b/server/social.coffee index 2b56754..0ce6eb4 100644 --- a/server/social.coffee +++ b/server/social.coffee @@ -220,7 +220,7 @@ module.exports = exports = (log, loga, argv) -> authorizationURL: argv.oauth2_AuthorizationURL tokenURL: argv.oauth2_TokenURL, # not all providers have a way of specifying the callback URL - callbackURL: argv.oauth2_CallbackURL, + callbackURL: callbackProtocol + '//' + callbackHost + '/auth/oauth2/callback', userInfoURL: argv.oauth2_UserInfoURL }, (accessToken, refreshToken, params, profile, cb) ->