Add support for Ipsilon OpenID Connect
Ipsilon Project (https://ipsilon-project.org/) is a server and a toolkit to configure Apache-based Service Providers. Support sign-in via Ipsilon OpenID Connect Identity Provider. On the Ipsilon side an actual authentication and identity source can be flexibly configured. We expect only three fields to be provided as part of the UserInfo by the Ipsilon: - user name (subject): an account name for an authenticated user - email for the user - _groups: list of groups this user is a member of The Ipsilon authenticator is structured similarly to 'SAML' authenticator but is simpler to configure. Signed-off-by: Alexander Bokovoy <ab@vda.li>
This commit is contained in:
parent
c71361467d
commit
c33864a00e
14 changed files with 272 additions and 8 deletions
|
@ -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 |
|
||||
|
@ -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 |
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
|
24
app.json
24
app.json
|
@ -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": [
|
||||
|
|
|
@ -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"
|
||||
},
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
},
|
||||
|
|
|
@ -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,
|
||||
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
143
lib/web/auth/ipsilon/index.js
Normal file
143
lib/web/auth/ipsilon/index.js
Normal file
|
@ -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 + '/'
|
||||
})
|
||||
)
|
||||
})
|
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
<% if(allowAnonymous) { %>
|
||||
<a type="button" href="<%- url %>/new" class="btn btn-sm btn-primary"><i class="fa fa-plus"></i> <%= __('New guest note') %></a>
|
||||
<% } %>
|
||||
<% if(facebook || twitter || github || gitlab || mattermost || dropbox || google || ldap || saml || email) { %>
|
||||
<% if(facebook || twitter || github || gitlab || mattermost || dropbox || google || ldap || saml || ipsilon || email) { %>
|
||||
<button class="btn btn-sm btn-success ui-signin" data-toggle="modal" data-target=".signin-modal"><%= __('Sign In') %></button>
|
||||
<% } %>
|
||||
</div>
|
||||
|
@ -49,7 +49,7 @@
|
|||
<% if (errorMessage && errorMessage.length > 0) { %>
|
||||
<div class="alert alert-danger" style="max-width: 400px; margin: 0 auto;"><%= errorMessage %></div>
|
||||
<% } %>
|
||||
<% if(facebook || twitter || github || gitlab || mattermost || dropbox || google || ldap || saml || email) { %>
|
||||
<% if(facebook || twitter || github || gitlab || mattermost || dropbox || google || ldap || saml || ipsilon || email) { %>
|
||||
<span class="ui-signin">
|
||||
<br>
|
||||
<a type="button" class="btn btn-lg btn-success ui-signin" data-toggle="modal" data-target=".signin-modal" style="min-width: 200px;"><%= __('Sign In') %></a>
|
||||
|
|
|
@ -48,7 +48,12 @@
|
|||
<i class="fa fa-users"></i> <%= __('Sign in via %s', 'SAML') %>
|
||||
</a>
|
||||
<% } %>
|
||||
<% if((facebook || twitter || github || gitlab || mattermost || dropbox || google || saml) && ldap) { %>
|
||||
<% if(ipsilon) { %>
|
||||
<a href="<%- url %>/auth/ipsilon" class="btn btn-lg btn-block btn-social btn-success">
|
||||
<i class="fa fa-users"></i> <%= __('Sign in via %s', ipsilonIssuerTitle ) %>
|
||||
</a>
|
||||
<% } %>
|
||||
<% if((facebook || twitter || github || gitlab || mattermost || dropbox || google || saml || ipsilon) && ldap) { %>
|
||||
<hr>
|
||||
<% }%>
|
||||
<% if(ldap) { %>
|
||||
|
|
Loading…
Reference in a new issue