diff --git a/docs/config-oauth2.md b/docs/config-oauth2.md new file mode 100644 index 0000000..9bb94c8 --- /dev/null +++ b/docs/config-oauth2.md @@ -0,0 +1,59 @@ +## 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_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. + +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 +{ + "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", +} +``` + +#### 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_UsernameField": "profile.preferred_username" +} +``` 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/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", diff --git a/server/social.coffee b/server/social.coffee index 1cd9fcb..0ce6eb4 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,75 @@ 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' + + 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, + # not all providers have a way of specifying the callback URL + callbackURL: callbackProtocol + '//' + callbackHost + '/auth/oauth2/callback', + 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) + console.log("profile", profile) + if argv.oauth2_UsernameField? + username_query = argv.oauth2_UsernameField + else + username_query = 'params.user_id' + + 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))) + # Github Strategy if argv.github_clientID? and argv.github_clientSecret? ids.push('github') @@ -275,6 +344,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 +391,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 +579,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 +662,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: {