first commit
This commit is contained in:
2
services/web/scripts/ukamf/.gitignore
vendored
Normal file
2
services/web/scripts/ukamf/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
node_modules
|
||||
ukfederation-metadata.xml
|
98
services/web/scripts/ukamf/check-certs.js
Normal file
98
services/web/scripts/ukamf/check-certs.js
Normal file
@@ -0,0 +1,98 @@
|
||||
/**
|
||||
* Checks that all institutional sso provider certs are still current with the
|
||||
* data provided by the ukamf export file.
|
||||
*
|
||||
* Run with: node check-certs /path/ukamf.xml
|
||||
*
|
||||
* The ukamf metadata xml file can be downloaded from:
|
||||
* http://metadata.ukfederation.org.uk/
|
||||
*/
|
||||
|
||||
import { Certificate } from '@fidm/x509'
|
||||
import UKAMFDB from './ukamf-db.js'
|
||||
import V1ApiModule from '../../app/src/Features/V1/V1Api.js'
|
||||
import { db } from '../../app/src/infrastructure/mongodb.js'
|
||||
import moment from 'moment'
|
||||
|
||||
const { promises: V1Api } = V1ApiModule
|
||||
|
||||
async function main() {
|
||||
const [, , file] = process.argv
|
||||
|
||||
console.log(`loading file ${file}`)
|
||||
|
||||
const ukamfDB = new UKAMFDB(file)
|
||||
await ukamfDB.init()
|
||||
|
||||
const activeProviderIds = await getActiveProviderIds()
|
||||
|
||||
for (const providerId of activeProviderIds) {
|
||||
await checkCert(ukamfDB, providerId)
|
||||
}
|
||||
}
|
||||
|
||||
async function checkCert(ukamfDB, providerId) {
|
||||
console.log(`Checking certificates for providerId: ${providerId}`)
|
||||
try {
|
||||
const { body } = await V1Api.request({
|
||||
json: true,
|
||||
qs: { university_id: providerId },
|
||||
uri: '/api/v1/overleaf/university_saml',
|
||||
})
|
||||
// show notice if sso not currently enabled
|
||||
if (body.sso_enabled === true) {
|
||||
console.log(` * SSO enabled`)
|
||||
} else {
|
||||
console.log(` ! SSO NOT enabled`)
|
||||
}
|
||||
// lookup entity id in ukamf database
|
||||
const entity = ukamfDB.findByEntityID(body.sso_entity_id)
|
||||
// if entity found then compare certs
|
||||
if (entity) {
|
||||
const samlConfig = entity.getSamlConfig()
|
||||
// check if certificates match
|
||||
if (samlConfig.cert === body.sso_cert) {
|
||||
console.log(' * UKAMF certificate matches configuration')
|
||||
} else {
|
||||
console.log(' ! UKAMF certificate DOES NOT match configuration')
|
||||
}
|
||||
} else {
|
||||
console.log(` ! No UKAMF entity found for ${body.sso_entity_id}`)
|
||||
}
|
||||
// check expiration on configured certificate
|
||||
const certificate = Certificate.fromPEM(
|
||||
Buffer.from(
|
||||
`-----BEGIN CERTIFICATE-----\n${body.sso_cert}\n-----END CERTIFICATE-----`,
|
||||
'utf8'
|
||||
)
|
||||
)
|
||||
|
||||
const validFrom = moment(certificate.validFrom)
|
||||
const validTo = moment(certificate.validTo)
|
||||
|
||||
if (validFrom.isAfter(moment())) {
|
||||
console.log(` ! Certificate not valid till: ${validFrom.format('LLL')}`)
|
||||
} else if (validTo.isBefore(moment())) {
|
||||
console.log(` ! Certificate expired: ${validTo.format('LLL')}`)
|
||||
} else if (validTo.isBefore(moment().add(60, 'days'))) {
|
||||
console.log(` ! Certificate expires: ${validTo.format('LLL')}`)
|
||||
} else {
|
||||
console.log(` * Certificate expires: ${validTo.format('LLL')}`)
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(` ! ${err.statusCode} Error getting university config from v1`)
|
||||
}
|
||||
}
|
||||
|
||||
async function getActiveProviderIds() {
|
||||
return db.users.distinct('samlIdentifiers.providerId', {
|
||||
'samlIdentifiers.externalUserId': { $exists: true },
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
await main()
|
||||
} catch (error) {
|
||||
console.error(error.stack)
|
||||
}
|
||||
process.exit()
|
67
services/web/scripts/ukamf/check-idp-metadata.js
Normal file
67
services/web/scripts/ukamf/check-idp-metadata.js
Normal file
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
Checks the SAML metadata provided by the IdP.
|
||||
Currently, only checking the valid from and to dates for the certificate
|
||||
Run with: node check-idp-metadata /path/idp-metadata.xml
|
||||
*/
|
||||
|
||||
import { Certificate } from '@fidm/x509'
|
||||
import _ from 'lodash'
|
||||
import moment from 'moment'
|
||||
import fs from 'fs'
|
||||
import xml2js from 'xml2js'
|
||||
|
||||
function checkCertDates(signingKey) {
|
||||
let cert = _.get(signingKey, [
|
||||
'ds:KeyInfo',
|
||||
0,
|
||||
'ds:X509Data',
|
||||
0,
|
||||
'ds:X509Certificate',
|
||||
0,
|
||||
])
|
||||
if (!cert) {
|
||||
throw new Error('no cert')
|
||||
}
|
||||
cert = cert.replace(/\s/g, '')
|
||||
|
||||
const certificate = Certificate.fromPEM(
|
||||
Buffer.from(
|
||||
`-----BEGIN CERTIFICATE-----\n${cert}\n-----END CERTIFICATE-----`,
|
||||
'utf8'
|
||||
)
|
||||
)
|
||||
|
||||
const validFrom = moment(certificate.validFrom)
|
||||
const validTo = moment(certificate.validTo)
|
||||
|
||||
return {
|
||||
validFrom,
|
||||
validTo,
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const [, , file] = process.argv
|
||||
|
||||
console.log('Checking SAML metadata')
|
||||
|
||||
const data = await fs.promises.readFile(file, 'utf8')
|
||||
const parser = new xml2js.Parser()
|
||||
const xml = await parser.parseStringPromise(data)
|
||||
|
||||
const idp = xml.EntityDescriptor.IDPSSODescriptor
|
||||
const keys = idp[0].KeyDescriptor
|
||||
|
||||
const signingKey =
|
||||
keys.length === 1
|
||||
? keys[0]
|
||||
: keys.find(key => _.get(key, ['$', 'use']) === 'signing')
|
||||
|
||||
const certDates = checkCertDates(signingKey)
|
||||
|
||||
console.log(
|
||||
`SSO certificate is valid from ${certDates.validFrom} to ${certDates.validTo}`
|
||||
)
|
||||
}
|
||||
|
||||
main()
|
72
services/web/scripts/ukamf/metadata-processor.js
Normal file
72
services/web/scripts/ukamf/metadata-processor.js
Normal file
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* Run with: node metadata-processor /path/ukamf.xml http://idp/entity/id
|
||||
*
|
||||
* `npm install` must be run for scripts/ukamf first.
|
||||
*
|
||||
* The ukamf metadata xml file can be downloaded from:
|
||||
* http://metadata.ukfederation.org.uk/
|
||||
*
|
||||
* The entity id should be provided by the university.
|
||||
*/
|
||||
|
||||
import { Certificate } from '@fidm/x509'
|
||||
import moment from 'moment'
|
||||
import UKAMFDB from './ukamf-db.js'
|
||||
|
||||
async function main() {
|
||||
const [, , file, entityId] = process.argv
|
||||
|
||||
console.log(`loading file ${file}...\n`)
|
||||
|
||||
const ukamfDB = new UKAMFDB(file)
|
||||
await ukamfDB.init()
|
||||
|
||||
const entity = ukamfDB.findByEntityID(entityId)
|
||||
if (!entity) {
|
||||
throw new Error(`could not find entity for ${entityId}`)
|
||||
}
|
||||
const samlConfig = entity.getSamlConfig()
|
||||
|
||||
const certificate = Certificate.fromPEM(
|
||||
Buffer.from(
|
||||
`-----BEGIN CERTIFICATE-----\n${samlConfig.cert}\n-----END CERTIFICATE-----`,
|
||||
'utf8'
|
||||
)
|
||||
)
|
||||
|
||||
const validFrom = moment(certificate.validFrom)
|
||||
const validTo = moment(certificate.validTo)
|
||||
|
||||
if (validFrom.isAfter(moment())) {
|
||||
throw new Error(`certificate not valid till: ${validFrom.format('LLL')}`)
|
||||
}
|
||||
|
||||
if (validTo.isBefore(moment())) {
|
||||
throw new Error(`certificate expired: ${validTo.format('LLL')}`)
|
||||
}
|
||||
|
||||
console.log(
|
||||
`!!!!!!!!!!!!!\nCERTIFICATE EXPIRES: ${validTo.format(
|
||||
'LLL'
|
||||
)}\n!!!!!!!!!!!!!\n`
|
||||
)
|
||||
|
||||
console.log(`SSO Entity ID: ${samlConfig.entityId}\n`)
|
||||
console.log(`SSO Entry Point: ${samlConfig.entryPoint}\n`)
|
||||
console.log(`SSO Certificate: ${samlConfig.cert}\n`)
|
||||
if (samlConfig.hiddenIdP) {
|
||||
console.log('!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!')
|
||||
console.log('!!!!!!!!!!!!!!! WARNING !!!!!!!!!!!!!!!')
|
||||
console.log(
|
||||
`The IdP metadata indicates it should be\nhidden from discovery. Check this is\nthe correct entity ID before using.`
|
||||
)
|
||||
console.log('!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!')
|
||||
console.log('!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!')
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await main()
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
40
services/web/scripts/ukamf/package-lock.json
generated
Normal file
40
services/web/scripts/ukamf/package-lock.json
generated
Normal file
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"name": "ukamf",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"devDependencies": {
|
||||
"@fidm/x509": "^1.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@fidm/asn1": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@fidm/asn1/-/asn1-1.0.4.tgz",
|
||||
"integrity": "sha512-esd1jyNvRb2HVaQGq2Gg8Z0kbQPXzV9Tq5Z14KNIov6KfFD6PTaRIO8UpcsYiTNzOqJpmyzWgVTrUwFV3UF4TQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/@fidm/x509": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@fidm/x509/-/x509-1.2.1.tgz",
|
||||
"integrity": "sha512-nwc2iesjyc9hkuzcrMCBXQRn653XuAUKorfWM8PZyJawiy1QzLj4vahwzaI25+pfpwOLvMzbJ0uKpWLDNmo16w==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@fidm/asn1": "^1.0.4",
|
||||
"tweetnacl": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/tweetnacl": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz",
|
||||
"integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
}
|
6
services/web/scripts/ukamf/package.json
Normal file
6
services/web/scripts/ukamf/package.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"devDependencies": {
|
||||
"@fidm/x509": "^1.2.1"
|
||||
},
|
||||
"type": "module"
|
||||
}
|
28
services/web/scripts/ukamf/ukamf-db.js
Normal file
28
services/web/scripts/ukamf/ukamf-db.js
Normal file
@@ -0,0 +1,28 @@
|
||||
import fs from 'fs'
|
||||
import xml2js from 'xml2js'
|
||||
import UKAMFEntity from './ukamf-entity.js'
|
||||
|
||||
class UKAMFDB {
|
||||
constructor(file) {
|
||||
this.file = file
|
||||
}
|
||||
|
||||
async init() {
|
||||
const data = await fs.promises.readFile(this.file, 'utf8')
|
||||
const parser = new xml2js.Parser()
|
||||
const xml = await parser.parseStringPromise(data)
|
||||
|
||||
this.entities = xml.EntitiesDescriptor.EntityDescriptor
|
||||
}
|
||||
|
||||
findByEntityID(matcher) {
|
||||
const entity = this.entities.find(
|
||||
matcher instanceof RegExp
|
||||
? e => e.$.entityID.match(matcher)
|
||||
: e => e.$.entityID.includes(matcher)
|
||||
)
|
||||
return entity ? new UKAMFEntity(entity) : null
|
||||
}
|
||||
}
|
||||
|
||||
export default UKAMFDB
|
69
services/web/scripts/ukamf/ukamf-entity.js
Normal file
69
services/web/scripts/ukamf/ukamf-entity.js
Normal file
@@ -0,0 +1,69 @@
|
||||
import _ from 'lodash'
|
||||
|
||||
class UKAMFEntity {
|
||||
constructor(data) {
|
||||
this.data = data
|
||||
}
|
||||
|
||||
getSamlConfig() {
|
||||
let hiddenIdP = false
|
||||
const idp = this.data.IDPSSODescriptor[0]
|
||||
const idpMetaData =
|
||||
_.get(this.data, [
|
||||
'Extensions',
|
||||
0,
|
||||
'mdattr:EntityAttributes',
|
||||
0,
|
||||
'saml:Attribute',
|
||||
]) || []
|
||||
idpMetaData.forEach(data => {
|
||||
const value = _.get(data, ['saml:AttributeValue', 0])
|
||||
if (
|
||||
value === 'http://refeds.org/category/hide-from-discovery' ||
|
||||
value === 'https://refeds.org/category/hide-from-discovery'
|
||||
) {
|
||||
hiddenIdP = true
|
||||
}
|
||||
})
|
||||
|
||||
const keys = idp.KeyDescriptor
|
||||
|
||||
const signingKey =
|
||||
keys.length === 1
|
||||
? keys[0]
|
||||
: keys.find(key => _.get(key, ['$', 'use']) === 'signing')
|
||||
const entityId = this.data.$.entityID
|
||||
|
||||
let cert = _.get(signingKey, [
|
||||
'ds:KeyInfo',
|
||||
0,
|
||||
'ds:X509Data',
|
||||
0,
|
||||
'ds:X509Certificate',
|
||||
0,
|
||||
])
|
||||
if (!cert) {
|
||||
throw new Error('no cert')
|
||||
}
|
||||
cert = cert.replace(/\s/g, '')
|
||||
|
||||
let entryPoint = idp.SingleSignOnService.find(
|
||||
sso =>
|
||||
_.get(sso, ['$', 'Binding']) ===
|
||||
'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect'
|
||||
)
|
||||
entryPoint = _.get(entryPoint, ['$', 'Location'])
|
||||
if (!entryPoint) {
|
||||
throw new Error('no entryPoint')
|
||||
}
|
||||
|
||||
return {
|
||||
cert,
|
||||
entityId,
|
||||
entryPoint,
|
||||
hiddenIdP,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default UKAMFEntity
|
Reference in New Issue
Block a user