diff --git a/bin/example-script b/bin/example-script new file mode 100755 index 0000000..68d7423 --- /dev/null +++ b/bin/example-script @@ -0,0 +1,9 @@ +#!/bin/bash + +# Example script for script strategy. Allows any username as long as the password is "$USERNAME-123". + +if [[ $PASSWORD != "$USERNAME-123" ]]; then + echo "{\"message\":\"Wrong password\"}" +else + echo "{\"id\":\"$USERNAME\"}" +fi diff --git a/lib/script-strategy.js b/lib/script-strategy.js new file mode 100644 index 0000000..a94c729 --- /dev/null +++ b/lib/script-strategy.js @@ -0,0 +1,84 @@ +/** + * Script strategy that calls a local script to verify provided credentials. + * + * See: https://github.com/gbv/login-server/issues/117 + * + * Requires a local path to the script. + */ + +/** + * Module dependencies. + */ +import passport from "passport-strategy" +import util from "node:util" + +import { exec as cpexec } from "node:child_process" +/** + * A wrapper around child_process' exec function for async/await. + * + * @param {*} command + * @param {*} options + */ +async function exec(command, options) { + return new Promise((resolve, reject) => { + cpexec(command, options || {}, (error, stdout, stderr) => { + if (error) { + error.stdout = stdout + error.stderr = stderr + return reject(error) + } + resolve(stdout) + }) + }) +} + +/** + * `Strategy` constructor. + * + * @param {Object} options + * @param {Function} callback + * @api public + */ +export function Strategy({ script, passReqToCallback }, callback) { + if (!script) { + throw new TypeError("Script strategy requires a script to run") + } + passport.Strategy.call(this) + this.name = "script" + this._passReqToCallback = passReqToCallback + this._script = script + this._cb = callback +} + +/** + * Inherit from `passport.Strategy`. + */ +util.inherits(Strategy, passport.Strategy) + +/** + * Authenticate using supplied user. + * + * @param {Object} req + * @api protected + */ +Strategy.prototype.authenticate = async function(req) { + let username = (req.body && req.body.username) || req.query.username + let password = (req.body && req.body.password) || req.query.password + if (!username || !password) { + return this.fail({ message: "Missing credentials" }, 400) + } + try { + const result = JSON.parse(await exec(this._script, { env: { ...process.env, USERNAME: username, PASSWORD: password }})) + if (!result?.id) { + throw new Error(result.message || "Wrong credentials") + } + const user = result + let args = this._passReqToCallback ? [req] : [] + args = args.concat([null, null, user, (error, user) => { + this.success(user) + }]) + this._cb(...args) + } catch (error) { + return this.fail({ message: error.message }, 400) + } +} diff --git a/strategies/script.js b/strategies/script.js new file mode 100644 index 0000000..66bb0e3 --- /dev/null +++ b/strategies/script.js @@ -0,0 +1,15 @@ +/** + * Script Stategy. + */ + +import { Strategy } from "../lib/script-strategy.js" + +export default (options, provider, callback) => new Strategy(options, (req, token, tokenSecret, profile, done) => { + callback(req, token, tokenSecret, { + id: profile.id || profile.username, + name: profile.name, + username: profile.username, + uri: profile.uri, + provider: provider.id, + }, done) +})