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
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,10 @@ There are some config settings you need to change in the files below.
| `HMD_SAML_ATTRIBUTE_ID` | `sAMAccountName` | attribute map for `id` (optional, default: NameID of SAML response) |
| `HMD_SAML_ATTRIBUTE_USERNAME` | `mailNickname` | attribute map for `username` (optional, default: NameID of SAML response) |
| `HMD_SAML_ATTRIBUTE_EMAIL` | `mail` | attribute map for `email` (optional, default: NameID of SAML response if `HMD_SAML_IDENTIFIERFORMAT` is default) |
| `HMD_IPSILON_ISSUERTITLE` | `Ipsilon` | Ipsilon instance name for Sign In dialog |
| `HMD_IPSILON_ISSUERHOST` | `https:/example.com/ipsilon/openidc` | authentication endpoint of IdP. For details, see [guide](docs/guides/auth.md#ipsilon). |
| `HMD_IPSILON_REQUIREDGROUPS` | `Hackmd-users` | group names that allowed (use vertical bar to separate) (optional) |
| `HMD_IPSILON_EXTERNALGROUPS` | `Temporary-staff` | group names that not allowed (use vertical bar to separate) (optional) |
| `HMD_IMGUR_CLIENTID` | no example | Imgur API client id |
| `HMD_EMAIL` | `true` or `false` | set to allow email signin |
| `HMD_ALLOW_PDF_EXPORT` | `true` or `false` | Enable or disable PDF exports |
Expand Down Expand Up @@ -270,7 +274,7 @@ There are some config settings you need to change in the files below.

| service | settings location | description |
| ------- | --------- | ----------- |
| facebook, twitter, github, gitlab, mattermost, dropbox, google, ldap, saml | environment variables or `config.json` | for signin |
| facebook, twitter, github, gitlab, mattermost, dropbox, google, ldap, saml, ipsilon | environment variables or `config.json` | for signin |
| imgur, s3, minio | environment variables or `config.json` | for image upload |
| dropbox(`dropbox/appKey`) | `config.json` | for export and import |

Expand All @@ -286,6 +290,7 @@ There are some config settings you need to change in the files below.
| dropbox | `/auth/dropbox/callback` |
| google | `/auth/google/callback` |
| saml | `/auth/saml/callback` |
| ipsilon | `/auth/ipsilon/callback` |

# Developer Notes

Expand Down
24 changes: 24 additions & 0 deletions app.json
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,30 @@
"HMD_ALLOW_PDF_EXPORT": {
"description": "Enable or disable PDF exports",
"required": false
},
"HMD_IPSILON_CLIENTID": {
"description": "Ipsilon OpenID Connect API client id",
"required": false
},
"HMD_IPSILON_CLIENTSECRET": {
"description": "Ipsilon OpenID Connect API client secret",
"required": false
},
"HMD_IPSILON_ISSUERTITLE": {
"description": "Ipsilon instance name for Sign In dialog",
"required": false
},
"HMD_IPSILON_ISSUERHOST": {
"description": "Ipsilon instance URL",
"required": false
},
"HMD_IPSILON_EXTERNALGROUPS": {
"description": "Ipsilon-reported groups not allowed to access this instance",
"required": false
},
"HMD_IPSILON_REQUIREDGROUPS": {
"description": "Ipsilon-reported groups required to access this instance",
"required": false
}
},
"addons": [
Expand Down
8 changes: 8 additions & 0 deletions config.json.example
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,14 @@
"email": "change or delete this: attribute map for `email` (default: NameID)"
}
},
"ipsilon": {
"clientID": "change this",
"clientSecret": "change this",
"issuerHost": "change this",
"issuerTitle": "Ipsilon or change this",
"requiredGroups": [ "change or delete: group names that allowed" ],
"externalGroups": [ "change or delete: group names that not allowed" ]
},
"imgur": {
"clientID": "change this"
},
Expand Down
47 changes: 47 additions & 0 deletions docs/guides/auth.md
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,53 @@ The basic procedure is the same as the case of OneLogin which is mentioned above
HMD_SAML_EXTERNALGROUPS=temporary-staff
````

### Ipsilon

Ipsilon Project (https://ipsilon-project.org/) is a server and a toolkit to configure Apache-based Service Providers.

Ipsilon has support for multiple authentication protocols, including OpenID Connect.

Once you have installed Ipsilon and configured it to use an identity provider of your choice, login into an administrative interface and add a new OpenID Connect client (a service provider)

Ipsilon generates most of the parameters automatically, only following values should be set:

* ClientID: your client identifier, say, `hackmd.example.com`
* Client name: a free text title to distinguish your client at Ipsilon login page, say, `Hackmd instance`
* Redirect URIs: an HTTPS URL of your Hackmd deployment ending with `/auth/ipsilon/callback`, say, `https://example.com/hackmd/auth/ipsilon/callback`
* Application type: `web`
* Client URI: an HTTPS url of your Hackmd deployment, `https://example.com/hackmd`
* Subject type: `public`
* Response type: `code`, `code id_token` (you can tick all boxes if unsure)
* Grant types: `authorization_code`, `implicit`, `refresh_token`
* Token Andpoint Auth Method: `client_secret_basic`
* Request Object signing Alg: `none`
* Initiate Login URI: an HTTPS url of your Hackmd deployment, `https://example.com/hackmd`

Once saved, copy value of a generated `Client Secret` field to `config.json`
along with the other values. Below is how a typical Ipsilon deployment with
FreeIPA would look like:

* config.json:
````javascript
{
"production": {
"ipsilon": {
"clientID": "hackmd.example.com",
"clientSecret": "<generated Ipsilon client secret>",
"issuerHost": "https://ipsilon.example.com/ipsilon/openidc/",
"issuerTitle": "My FreeIPA",
"requiredGroups": [ "ipausers" ],
"externalGroups": [ ]
}
}
}
````

A setting `issuerTitle` is supposed to be used in the sign-in dialog in Hackmd to provide a user-friendly `Sign in via $issuerTitle` text.

It is possible to limit what users can and cannot sign into a Hackmd instance with `requiredGroups` and `externalGroups` correspondingly.

User email, if provided, is automatically matched against a gravatar server to provide a gravatar.

### GitLab (self-hosted)

Expand Down
8 changes: 8 additions & 0 deletions lib/config/default.js
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,14 @@ module.exports = {
email: undefined
}
},
ipsilon: {
issuerTitle: undefined,
issuerHost: undefined,
clientID: undefined,
clientSecret: undefined,
externalGroups: [],
requiredGroups: []
},
email: true,
allowEmailRegister: true,
allowPDFExport: true
Expand Down
8 changes: 8 additions & 0 deletions lib/config/environment.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,14 @@ module.exports = {
email: process.env.HMD_SAML_ATTRIBUTE_EMAIL
}
},
ipsilon: {
issuerTitle: process.env.HMD_IPSILON_ISSUERTITLE,
issuerHost: process.env.HMD_IPSILON_ISSUERHOST,
clientID: process.env.HMD_IPSILON_CLIENTID,
clientSecret: process.env.HMD_IPSILON_CLIENTSECRET,
externalGroups: toArrayConfig(process.env.HMD_IPSILON_EXTERNALGROUPS, '|', []),
requiredGroups: toArrayConfig(process.env.HMD_IPSILON_REQUIREDGROUPS, '|', [])
},
email: toBooleanConfig(process.env.HMD_EMAIL),
allowEmailRegister: toBooleanConfig(process.env.HMD_ALLOW_EMAIL_REGISTER),
allowPDFExport: toBooleanConfig(process.env.HMD_ALLOW_PDF_EXPORT)
Expand Down
3 changes: 2 additions & 1 deletion lib/config/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,8 @@ config.isGitLabEnable = config.gitlab.clientID && config.gitlab.clientSecret
config.isMattermostEnable = config.mattermost.clientID && config.mattermost.clientSecret
config.isLDAPEnable = config.ldap.url
config.isSAMLEnable = config.saml.idpSsoUrl
config.isPDFExportEnable = config.allowPDFExport
config.isIpsilonEnable = config.ipsilon.clientID && config.ipsilon.clientSecret
config.isPDFExportEnable = config.allowpdfexport

// merge legacy values
let keys = Object.keys(config)
Expand Down
9 changes: 9 additions & 0 deletions lib/models/user.js
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,15 @@ module.exports = function (sequelize, DataTypes) {
photo = generateAvatarURL(profile.username)
}
break
case 'ipsilon':
if (profile.emails[0]) {
photo = 'https://www.gravatar.com/avatar/' + md5(profile.emails[0])
if (bigger) photo += '?s=400'
else photo += '?s=96'
} else {
photo = generateAvatarURL(profile.username)
}
break
}
return photo
},
Expand Down
4 changes: 4 additions & 0 deletions lib/response.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ function showIndex (req, res, next) {
ldap: config.isLDAPEnable,
ldapProviderName: config.ldap.providerName,
saml: config.isSAMLEnable,
ipsilon: config.isIpsilonEnable,
ipsilonIssuerTitle: config.ipsilon.issuerTitle,
email: config.isEmailEnable,
allowEmailRegister: config.allowEmailRegister,
allowPDFExport: config.allowPDFExport,
Expand Down Expand Up @@ -105,6 +107,8 @@ function responseHackMD (res, note) {
ldap: config.isLDAPEnable,
ldapProviderName: config.ldap.providerName,
saml: config.isSAMLEnable,
ipsilon: config.isIpsilonEnable,
ipsilonIssuerTitle: config.ipsilon.issuerTitle,
email: config.isEmailEnable,
allowEmailRegister: config.allowEmailRegister,
allowPDFExport: config.allowPDFExport
Expand Down
1 change: 1 addition & 0 deletions lib/web/auth/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ if (config.isGoogleEnable) authRouter.use(require('./google'))
if (config.isLDAPEnable) authRouter.use(require('./ldap'))
if (config.isSAMLEnable) authRouter.use(require('./saml'))
if (config.isEmailEnable) authRouter.use(require('./email'))
if (config.isIpsilonEnable) authRouter.use(require('./ipsilon'))

// logout
authRouter.get('/logout', function (req, res) {
Expand Down
143 changes: 143 additions & 0 deletions lib/web/auth/ipsilon/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
'use strict'
const Router = require('express').Router
const passport = require('passport')
const Issuer = require('openid-client').Issuer

const Strategy = require('openid-client').Strategy

const config = require('../../../config')
const logger = require('../../../logger')
const models = require('../../../models')
const intersection = function (array1, array2) {
return array1.filter((n) => array2.includes(n))
}

let ipsilonAuth = module.exports = Router()

Issuer.discover(config.ipsilon.issuerHost).then(
function (ipsilonIssuer) {
var client = new ipsilonIssuer.Client({
client_id: config.ipsilon.clientID,
client_secret: config.ipsilon.clientSecret,
userinfo_signed_response_alg: 'RS256',
scope: 'openid profile email phone',
claims: {
userinfo: {
name: {
essential: true
},
email: {
essential: true
},
picture: null,
_groups: null
},
id_token: {
auth_time: {
essential: true
}
}
}
})

var params = {
redirect_uri: config.serverurl + '/auth/ipsilon/callback',
scope: client.scope,
claims: client.claims
}

passport.use('oidc',
new Strategy({
client: client,
params: params,
usePKCE: false
}, (tokenset, done) => {
client.userinfo(tokenset.access_token, {
params: {
scope: params.scope,
claims: params.claims
}
}).then(function (userinfo) {
// check authorization: deny any of the external groups
if (config.ipsilon.externalGroups) {
// lack of external groups in userinfo does not prevent logon
if (userinfo._groups) {
var externalGroups = intersection(config.ipsilon.externalGroups, userinfo._groups)
if (externalGroups.length > 0) {
logger.error('ipsilon permission denied: ' + externalGroups.join(', '))
return done('Permission denied', null)
}
}
}
// check authorization: require any of the required groups
if (config.ipsilon.requiredGroups) {
// lack of required groups in userinfo denies logon
if (!userinfo._groups && config.ipsilon.requiredGroups.length > 0) {
logger.error('ipsilon permission denied')
return done('Permission denied', null)
}
if (intersection(config.ipsilon.requiredGroups, userinfo._groups).length === 0) {
logger.error('ipsilon permission denied')
return done('Permission denied', null)
}
}
// create a user
var uuid = userinfo.sub
var profile = {
provider: 'ipsilon',
id: 'IPSILON-' + uuid,
username: uuid,
emails: userinfo.email ? [userinfo.email] : []
}
var stringifiedProfile = JSON.stringify(profile)

models.User.findOrCreate({
where: {
profileid: profile.id.toString()
},
defaults: {
profile: stringifiedProfile
}
}).spread(function (user, created) {
if (user) {
var needSave = false
if (user.profile !== stringifiedProfile) {
user.profile = stringifiedProfile
needSave = true
}
if (needSave) {
user.save().then(function () {
if (config.debug) {
logger.debug('user login: ' + user.id)
}
return done(null, user)
})
} else {
if (config.debug) {
logger.debug('user login: ' + user.id)
}
return done(null, user)
}
}
}).catch(function (err) {
logger.error('ipsilon auth failed: ' + err)
return done(err, null)
})
})
})
)

ipsilonAuth.get('/auth/ipsilon',
passport.authenticate('oidc', {
successReturnToOrRedirect: config.serverurl + '/',
failureRedirect: config.serverurl + '/'
})
)

ipsilonAuth.get('/auth/ipsilon/callback',
passport.authenticate('oidc', {
successReturnToOrRedirect: config.serverurl + '/',
failureRedirect: config.serverurl + '/'
})
)
})
7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,8 @@
"markdown-it-sup": "^1.0.0",
"markdown-pdf": "^7.0.0",
"mathjax": "~2.7.0",
"mermaid": "~7.1.0",
"mattermost": "^3.4.0",
"mermaid": "~7.1.0",
"meta-marked": "^0.4.2",
"method-override": "^2.3.7",
"minimist": "^1.2.0",
Expand All @@ -87,6 +87,7 @@
"mysql": "^2.12.0",
"node-uuid": "^1.4.7",
"octicons": "~3.5.0",
"openid-client": "^1.19.4",
"passport": "^0.3.2",
"passport-dropbox-oauth2": "^1.1.0",
"passport-facebook": "^2.1.1",
Expand All @@ -96,8 +97,8 @@
"passport-ldapauth": "^0.6.0",
"passport-local": "^1.0.0",
"passport-oauth2": "^1.4.0",
"passport-twitter": "^1.0.4",
"passport-saml": "^0.31.0",
"passport-twitter": "^1.0.4",
"passport.socketio": "^3.7.0",
"pdfobject": "^2.0.201604172",
"pg": "^6.1.2",
Expand All @@ -106,7 +107,7 @@
"randomcolor": "^0.4.4",
"raphael": "git+https://github.com/dmitrybaranovskiy/raphael",
"readline-sync": "^1.4.7",
"request": "^2.79.0",
"request": "^2.83.0",
"reveal.js": "~3.6.0",
"scrypt": "^6.0.3",
"select2": "^3.5.2-browserify",
Expand Down
Loading