<commit_before>/**
 * index.js
 * OpenIDConnect provider
 * Based on OAuth 2.0 provider by <NAME>
 *
 * @author <NAME>
 */

var EventEmitter = require('events').EventEmitter,
querystring = require('querystring'),
//serializer = require('serializer'),
//hashlib = require('hashlib2'),
modelling = require('modelling'),
sailsRedis = require('sails-redis'),
crypto = require('crypto'),
_ = require('lodash'),
extend = require('extend'),
url = require('url'),
Q = require('q'),
jwt = require('jwt-simple'),
util = require("util"),
base64url = require('base64url'),
cleanObj = require('clean-obj');


var defaults = {
        login_url: '/login',
        consent_url: '/consent',
        scopes: {
            openid: 'Informs the Authorization Server that the Client is making an OpenID Connect request.',
            profile:'Access to the End-User\'s default profile Claims.',
            email: 'Access to the email and email_verified Claims.',
            address: 'Access to the address Claim.',
            phone: 'Access to the phone_number and phone_number_verified Claims.',
            offline_access: 'Grants access to the End-User\'s UserInfo Endpoint even when the End-User is not present (not logged in).'
        },
        policies:{
            loggedIn: function(req, res, next) {
                if(req.session.user) {
                    next();
                } else {
                    var q = req.parsedParams?req.path+'?'+querystring.stringify(req.parsedParams):req.originalUrl;
                    if(q.indexOf('/v1') == -1){
                        q = '/v1'+q;
                    }
                    res.redirect(this.settings.login_url+'?'+querystring.stringify({return_url: q}));
                }
            },
        },
        adapters: {
            redis: sailsRedis
        },
        connections: {
            def: {
                adapter: 'redis'
            }
        },
        models: {
                user: {
                    identity: 'user',
                    connection: 'def',
                    schema: true,
                    policies: 'loggedIn',
                    attributes: {
                        name: {type: 'string', required: true, unique: true},
                        given_name: {type: 'string', required: true},
                        middle_name: 'string',
                        family_name: {type: 'string', required: true},
                        profile: 'string',
                        email: {type: 'string', email: true, required: true, unique: true},
                        password: '<PASSWORD>',
                        picture: 'binary',
                        birthdate: 'date',
                        gender: 'string',
                        phone_number: 'string',
                        samePassword: function(clearText) {
                            var sha256 = crypto.createHash('sha256');
                            sha256.update(clearText);
                            return this.password == <PASSWORD>256.digest('hex');
                        }
                    },
                    beforeCreate: function(values, next) {
                        if(values.password) {
                            if(values.password != values.passConfirm) {
                                return next("Password and confirmation does not match");
                            }
                            var sha256 = crypto.createHash('sha256');
                            sha256.update(values.password);
                            values.password = <PASSWORD>('hex');
                        }
                        next();
                    },
                    beforeUpdate: function(values, next) {
                        if(values.password) {
                            if(values.password != values.passConfirm) {
                                return next("Password and confirmation does not match");
                            }
                            var sha256 = crypto.createHash('sha256');
                            sha256.update(values.password);
                            values.password = <PASSWORD>('hex');
                        }
                        next();
                    }
                },
                client: {
                    identity: 'client',
                    connection: 'def',
                    schema: true,
                    policies: 'loggedIn',
                    attributes: {
                        key: {type: 'string', required: true, unique: true},
                        secret: {type: 'string', required: true, unique: true},
                        name: {type: 'string', required: true},
                        image: 'binary',
                        user: {model: 'user'},
                        redirect_uris: {type:'array', required: true},
                        credentialsFlow: {type: 'boolean', defaultsTo: false}
                    },
                    beforeCreate: function(values, next) {
                        if(!values.key) {
                            var sha256 = crypto.createHash('sha256');
                            sha256.update(values.name);
                            sha256.update(Math.random()+'');
                            values.key = sha256.digest('hex');
                        }
                        if(!values.secret) {
                            var sha256 = crypto.createHash('sha256');
                            sha256.update(values.key);
                            sha256.update(values.name);
                            sha256.update(Math.random()+'');
                            values.secret = sha256.digest('hex');
                        }
                        next();
                    }
                },
                consent: {
                    identity: 'consent',
                    connection: 'def',
                    policies: 'loggedIn',
                    attributes: {
                        user: {model: 'user', required: true},
                        client: {model: 'client', required: true},
                        scopes: 'array'
                    }
                },
                auth: {
                    identity: 'auth',
                    connection: 'def',
                    policies: 'loggedIn',
                    attributes: {
                        client: {model: 'client',   required: true},
                        scope: {type: 'array', required: true},
                        user: {model: 'user', required: true},
                        sub: {type: 'string', required: true},
                        code: {type: 'string', required: true},
                        redirectUri: {type: 'url', required: true},
                        responseType: {type: 'string', required: true},
                        status: {type: 'string', required: true},
                        accessTokens: {
                            collection: 'access',
                            via: 'auth'
                        },
                        refreshTokens: {
                            collection: 'refresh',
                            via: 'auth'
                        }
                    }
                },
                access: {
                    identity: 'access',
                    connection: 'def',
                    attributes: {
                        token: {type: 'string', required: true},
                        type: {type: 'string', required: true},
                        idToken: 'string',
                        expiresIn: 'integer',
                        scope: {type: 'array', required: true},
                        client: {model: 'client', required: true},
                        user: {model: 'user', required: true},
                        auth: {model: 'auth'}
                    }
                },
                refresh: {
                    identity: 'refresh',
                    connection: 'def',
                    attributes: {
                        token: {type: 'string', required: true},
                        scope: {type: 'array', required: true},
                        auth: {model: 'auth', required: true},
                        status: {type: 'string', required: true}
                    }
                }
        }
};

function parse_authorization(authorization) {
    if(!authorization)
        return null;

    var parts = authorization.split(' ');

    if(parts.length != 2 || parts[0] != 'Basic')
        return null;

    var creds = new Buffer(parts[1], 'base64').toString(),
    i = creds.indexOf(':');

    if(i == -1)
        return null;

    var username = creds.slice(0, i);
    password = creds.slice(i + 1);

    return [username, password];
}

function OpenIDConnect(options) {
    this.settings = extend(true, {}, defaults, options);

    //allow removing attributes, by marking thme as null
    cleanObj(this.settings.models, true);

    for(var i in this.settings.policies) {
        this.settings.policies[i] = this.settings.policies[i].bind(this);
    }

    if(this.settings.alien) {
        for(var i in alien) {
            if(this.settings.models[i]) delete this.settings.models[i];
        }
    }

    if(this.settings.orm) {
        this.orm = this.settings.orm;
        for(var i in this.settings.policies) {
            this.orm.setPolicy(true, i, this.settings.policies[i]);
        }
    } else {

        this.orm = new modelling({
            models: this.settings.models,
            adapters: this.settings.adapters,
            connections: this.settings.connections,
            app: this.settings.app,
            policies: this.settings.policies
        });
    }
}

OpenIDConnect.prototype = new EventEmitter();

OpenIDConnect.prototype.done = function() {
    this.orm.done();
};

OpenIDConnect.prototype.model = function(name) {
    return this.orm.model(name);
}

OpenIDConnect.prototype.use = function(name) {
    var alien = {};
    if(this.settings.alien) {
        var self = this;
        if(!name) {
            alien = this.settings.alien;
        } else {
            var m;
            if(_.isPlainObject(name) && name.models) {
                m = name.models;
            }
            if(util.isArray(m||name)) {
                (m||name).forEach(function(model) {
                    if(self.settings.alien[model]) {
                        alien[model] = self.settings.alien[model];
                    }
                });
            } else if(self.settings.alien[m||name]) {
                alien[m||name] = self.settings.alien[m||name];
            }
        }
    }
    return [this.orm.use(name), function(req, res, next) {
        extend(req.model, alien);
        next();
    }];
};

OpenIDConnect.prototype.getOrm = function() {
    return this.orm;
}
/*OpenIDConnect.prototype.getClientParams = function() {
    return this.orm.client.getParams();
};*/

/*OpenIDConnect.prototype.searchClient = function(parts, callback) {
    return new this.orm.client.reverse(parts, callback);
};

OpenIDConnect.prototype.getUserParams = function() {
    return this.orm.user.getParams();
};

OpenIDConnect.prototype.user = function(params, callback) {
    return new this.orm.user(params, callback);
};

OpenIDConnect.prototype.searchUser = function(parts, callback) {
    return new this.orm.user.reverse(parts, callback);
};*/

OpenIDConnect.prototype.errorHandle = function(res, uri, error, desc) {
    if(uri) {
        var redirect = url.parse(uri,true);
        redirect.query.error = error; //'invalid_request';
        redirect.query.error_description = desc; //'Parameter '+x+' is mandatory.';
        res.redirect(400, url.format(redirect));
    } else {
        res.send(400, error+': '+desc);
    }
};

OpenIDConnect.prototype.endpointParams = function (spec, req, res, next) {
    try {
        req.parsedParams = this.parseParams(req, res, spec);
        next();
    } catch(err) {
        this.errorHandle(res, err.uri, err.error, err.msg);
    }
}

OpenIDConnect.prototype.parseParams = function(req, res, spec) {
    var params = {};
    var r = req.param('redirect_uri');
    for(var i in spec) {
        var x = req.param(i);
        if(x) {
            params[i] = x;
        }
    }

    for(var i in spec) {
        var x = params[i];
        if(!x) {
            var error = false;
            if(typeof spec[i] == 'boolean') {
                error = spec[i];
            } else if (_.isPlainObject(spec[i])) {
                for(var j in spec[i]) {
                    if(!util.isArray(spec[i][j])) {
                        spec[i][j] = [spec[i][j]];
                    }
                    spec[i][j].forEach(function(e) {
                        if(!error) {
                            if(util.isRegExp(e)) {
                                error = e.test(params[j]);
                            } else {
                                error = e == params[j];
                            }
                        }
                    });
                }
            } else if (_.isFunction(spec[i])) {
                error = spec[i](params);
            }

            if(error) {
                throw {type: 'error', uri: r, error: 'invalid_request', msg: 'Parameter '+i+' is mandatory.'};
                //this.errorHandle(res, r, 'invalid_request', 'Parameter '+i+' is mandatory.');
                //return;
            }
        }
    }
    return params;
};

/**
 * login
 *
 * returns a function to be placed as middleware in connect/express routing methods. For example:
 *
 * app.post('/login', oidc.login(),  afterLogin, loginErrorHandler);
 *
 * This calls verification strategy and creates session.
 * Verification strategy must have two parameters: req and callback function with two parameters: error and user
 *
 *
 */

OpenIDConnect.prototype.login = function(validateUser) {
    var self = this;

    return [self.use({policies: {loggedIn: false}, models: ['user', 'client']}),
            function(req, res, next) {
                validateUser(req, /*next:*/function(error,user) {
                    if(!error && !user) {
                        error = new Error('User not validated');
                    }
                    if(!error) {
                        if(user.id) {
                            req.session.user = user.id;
                        } else {
                            delete req.session.user;
                        }
                        if(user.sub) {
                            if(typeof user.sub ==='function') {
                                req.session.sub = user.sub();
                            } else {
                                req.session.sub = user.sub;
                            }
                        } else {
                            delete req.session.sub;
                        }
                        return next();
                    } else {
                        return next(error);
                    }
                });
    }];
};

/**
 * auth
 *
 * returns a function to be placed as middleware in connect/express routing methods. For example:
 *
 * app.get('/authorization', oidc.auth());
 *
 * This is the authorization endpoint, as described in http://tools.ietf.org/html/rfc6749#section-3.1
 *
 */
OpenIDConnect.prototype.auth = function() {
    var self = this;
    var spec = {
            response_type: true,
            client_id: true,
            scope: true,
            redirect_uri: true,
            state: false,
            nonce: function(params){
                return params.response_type.indexOf('id_token')!==-1;
            },
            display: false,
            prompt: false,
            max_age: false,
            ui_locales: false,
            claims_locales: false,
            id_token_hint: false,
            login_hint: false,
            acr_values: false,
            response_mode: false
    };
    return [function(req, res, next) {
                self.endpointParams(spec, req, res, next);
            },
            self.use(['client', 'consent', 'auth', 'access']),
            function(req, res, next) {
                Q(req.parsedParams).then(function(params) {
                    //Step 2: Check if response_type is supported and client_id is valid.

                    var deferred = Q.defer();
                    switch(params.response_type) {
                    case 'none':
                    case 'code':
                    case 'token':
                    case 'id_token':
                        break;
                    default:
                        //var error = false;
                        var sp = params.response_type.split(' ');
                        sp.forEach(function(response_type) {
                            if(['code', 'token', 'id_token'].indexOf(response_type) == -1) {
                                throw {type: 'error', uri: params.redirect_uri, error: 'unsupported_response_type', msg: 'Response type '+response_type+' not supported.'};
                            }
                        });
                    }
                    req.model.client.findOne({key: params.client_id}, function(err, model) {
                        if(err || !model || model === '') {
                            deferred.reject({type: 'error', uri: params.redirect_uri, error: 'invalid_client', msg: 'Client '+params.client_id+' doesn\'t exist.'});
                        } else {
                            req.session.client_id = model.id;
                            req.session.client_secret = model.secret;
                            deferred.resolve(params);
                        }
                    });

                    return deferred.promise;
                }).then(function(params){
                    //Step 3: Check if scopes are valid, and if consent was given.

                    var deferred = Q.defer();
                    var reqsco = params.scope.split(' ');
                    req.session.scopes = {};
                    var promises = [];
                    req.model.consent.findOne({user: req.session.user, client: req.session.client_id}, function(err, consent) {
                            reqsco.forEach(function(scope) {
                                var innerDef = Q.defer();
                                if(!self.settings.scopes[scope]) {
                                    innerDef.reject({type: 'error', uri: params.redirect_uri, error: 'invalid_scope', msg: 'Scope '+scope+' not supported.'});
                                }
                                if(!consent) {
                                    req.session.scopes[scope] = {ismember: false, explain: self.settings.scopes[scope]};
                                    innerDef.resolve(true);
                                } else {
                                    var inScope = consent.scopes.indexOf(scope) !== -1;
                                    req.session.scopes[scope] = {ismember: inScope, explain: self.settings.scopes[scope]};
                                    innerDef.resolve(!inScope);
                                }
                                promises.push(innerDef.promise);
                            });

                            Q.allSettled(promises).then(function(results){
                                var redirect = false;
                                for(var i = 0; i<results.length; i++) {
                                    if(results[i].value) {
                                        redirect = true;
                                        break;
                                    }
                                }
                                if(redirect) {
                                    req.session.client_key = params.client_id;
                                    var q = req.path+'?'+querystring.stringify(params);
                                    if(q.indexOf('/v1') == -1) {
                                        q = '/v1'+q;
                                    }
                                    deferred.reject({type: 'redirect', uri: self.settings.consent_url+'?'+querystring.stringify({return_url: q})});
                                } else {
                                    deferred.resolve(params);
                                }
                            });
                    });

                    return deferred.promise;
                }).then(function(params){
                    //Step 5: create responses
                    if(params.response_type == 'none') {
                        return {params: params, resp: {}};
                    } else {
                        var deferred = Q.defer();
                        var promises = [];

                        var rts = params.response_type.split(' ');

                        rts.forEach(function(rt) {
                            var def = Q.defer();
                            promises.push(def.promise);
                            switch(rt) {
                            case 'code':
                                var createToken = function() {
                                    var token = crypto.createHash('md5').update(params.client_id).update(Math.random()+'').digest('hex');
                                    req.model.auth.findOne({code: token}, function(err, auth){
                                        if(!auth) {
                                            setToken(token);
                                        } else {
                                            createToken();
                                        }
                                    });
                                };
                                var setToken = function(token) {
                                    req.model.auth.create({
                                        client: req.session.client_id,
                                        scope: params.scope.split(' '),
                                        user: req.session.user,
                                        sub: req.session.sub||req.session.user,
                                        code: token,
                                        redirectUri: params.redirect_uri,
                                        responseType: params.response_type,
                                        status: 'created'
                                    }).exec(function(err, auth) {
                                        if(!err && auth) {
                                            setTimeout(function() {
                                                req.model.auth.findOne({code: token}, function(err, auth) {
                                                    if(auth && auth.status == 'created') {
                                                        auth.destroy();
                                                    }
                                                });
                                            }, 1000*60*10); //10 minutes
                                            def.resolve({code: token});
                                        } else {
                                            def.reject(err||'Could not create auth');
                                        }
                                    });

                                };
                                createToken();
                                break;
                            case 'id_token':
                                var d = Math.round(new Date().getTime()/1000);
                                //var id_token = {
                                def.resolve({id_token: {
                                        iss: req.protocol+'://'+req.headers.host,
                                        sub: req.session.sub||req.session.user,
                                        aud: params.client_id,
                                        exp: d+3600,
                                        iat: d,
                                        nonce: params.nonce
                                }});
                                //def.resolve({id_token: jwt.encode(id_token, req.session.client_secret)});
                                break;
                            case 'token':
                                var createToken = function() {
                                    var token = crypto.createHash('md5').update(params.client_id).update(Math.random()+'').digest('hex');
                                    req.model.access.findOne({token: token}, function(err, access) {<commit_msg>adding subscription_id to the JWT output<commit_after>/**
 * index.js
 * OpenIDConnect provider
 * Based on OAuth 2.0 provider by <NAME>
 *
 * @author <NAME>
 */

var EventEmitter = require('events').EventEmitter,
querystring = require('querystring'),
//serializer = require('serializer'),
//hashlib = require('hashlib2'),
modelling = require('modelling'),
sailsRedis = require('sails-redis'),
crypto = require('crypto'),
_ = require('lodash'),
extend = require('extend'),
url = require('url'),
Q = require('q'),
jwt = require('jwt-simple'),
util = require("util"),
base64url = require('base64url'),
cleanObj = require('clean-obj');


var defaults = {
        login_url: '/login',
        consent_url: '/consent',
        scopes: {
            openid: 'Informs the Authorization Server that the Client is making an OpenID Connect request.',
            profile:'Access to the End-User\'s default profile Claims.',
            email: 'Access to the email and email_verified Claims.',
            address: 'Access to the address Claim.',
            phone: 'Access to the phone_number and phone_number_verified Claims.',
            offline_access: 'Grants access to the End-User\'s UserInfo Endpoint even when the End-User is not present (not logged in).'
        },
        policies:{
            loggedIn: function(req, res, next) {
                if(req.session.user) {
                    next();
                } else {
                    var q = req.parsedParams?req.path+'?'+querystring.stringify(req.parsedParams):req.originalUrl;
                    if(q.indexOf('/v1') == -1){
                        q = '/v1'+q;
                    }
                    res.redirect(this.settings.login_url+'?'+querystring.stringify({return_url: q}));
                }
            },
        },
        adapters: {
            redis: sailsRedis
        },
        connections: {
            def: {
                adapter: 'redis'
            }
        },
        models: {
                user: {
                    identity: 'user',
                    connection: 'def',
                    schema: true,
                    policies: 'loggedIn',
                    attributes: {
                        name: {type: 'string', required: true, unique: true},
                        given_name: {type: 'string', required: true},
                        middle_name: 'string',
                        family_name: {type: 'string', required: true},
                        profile: 'string',
                        email: {type: 'string', email: true, required: true, unique: true},
                        password: '<PASSWORD>',
                        picture: 'binary',
                        birthdate: 'date',
                        gender: 'string',
                        phone_number: 'string',
                        samePassword: function(clearText) {
                            var sha256 = crypto.createHash('sha256');
                            sha256.update(clearText);
                            return this.password == <PASSWORD>256.digest('hex');
                        }
                    },
                    beforeCreate: function(values, next) {
                        if(values.password) {
                            if(values.password != values.passConfirm) {
                                return next("Password and confirmation does not match");
                            }
                            var sha256 = crypto.createHash('sha256');
                            sha256.update(values.password);
                            values.password = <PASSWORD>('hex');
                        }
                        next();
                    },
                    beforeUpdate: function(values, next) {
                        if(values.password) {
                            if(values.password != values.passConfirm) {
                                return next("Password and confirmation does not match");
                            }
                            var sha256 = crypto.createHash('sha256');
                            sha256.update(values.password);
                            values.password = <PASSWORD>('hex');
                        }
                        next();
                    }
                },
                client: {
                    identity: 'client',
                    connection: 'def',
                    schema: true,
                    policies: 'loggedIn',
                    attributes: {
                        key: {type: 'string', required: true, unique: true},
                        secret: {type: 'string', required: true, unique: true},
                        name: {type: 'string', required: true},
                        image: 'binary',
                        user: {model: 'user'},
                        redirect_uris: {type:'array', required: true},
                        credentialsFlow: {type: 'boolean', defaultsTo: false}
                    },
                    beforeCreate: function(values, next) {
                        if(!values.key) {
                            var sha256 = crypto.createHash('sha256');
                            sha256.update(values.name);
                            sha256.update(Math.random()+'');
                            values.key = sha256.digest('hex');
                        }
                        if(!values.secret) {
                            var sha256 = crypto.createHash('sha256');
                            sha256.update(values.key);
                            sha256.update(values.name);
                            sha256.update(Math.random()+'');
                            values.secret = sha256.digest('hex');
                        }
                        next();
                    }
                },
                consent: {
                    identity: 'consent',
                    connection: 'def',
                    policies: 'loggedIn',
                    attributes: {
                        user: {model: 'user', required: true},
                        client: {model: 'client', required: true},
                        scopes: 'array'
                    }
                },
                auth: {
                    identity: 'auth',
                    connection: 'def',
                    policies: 'loggedIn',
                    attributes: {
                        client: {model: 'client',   required: true},
                        scope: {type: 'array', required: true},
                        user: {model: 'user', required: true},
                        sub: {type: 'string', required: true},
                        code: {type: 'string', required: true},
                        redirectUri: {type: 'url', required: true},
                        responseType: {type: 'string', required: true},
                        status: {type: 'string', required: true},
                        accessTokens: {
                            collection: 'access',
                            via: 'auth'
                        },
                        refreshTokens: {
                            collection: 'refresh',
                            via: 'auth'
                        }
                    }
                },
                access: {
                    identity: 'access',
                    connection: 'def',
                    attributes: {
                        token: {type: 'string', required: true},
                        type: {type: 'string', required: true},
                        idToken: 'string',
                        expiresIn: 'integer',
                        scope: {type: 'array', required: true},
                        client: {model: 'client', required: true},
                        user: {model: 'user', required: true},
                        auth: {model: 'auth'}
                    }
                },
                refresh: {
                    identity: 'refresh',
                    connection: 'def',
                    attributes: {
                        token: {type: 'string', required: true},
                        scope: {type: 'array', required: true},
                        auth: {model: 'auth', required: true},
                        status: {type: 'string', required: true}
                    }
                }
        }
};

function parse_authorization(authorization) {
    if(!authorization)
        return null;

    var parts = authorization.split(' ');

    if(parts.length != 2 || parts[0] != 'Basic')
        return null;

    var creds = new Buffer(parts[1], 'base64').toString(),
    i = creds.indexOf(':');

    if(i == -1)
        return null;

    var username = creds.slice(0, i);
    password = <PASSWORD>.slice(i + 1);

    return [username, password];
}

function OpenIDConnect(options) {
    this.settings = extend(true, {}, defaults, options);

    //allow removing attributes, by marking thme as null
    cleanObj(this.settings.models, true);

    for(var i in this.settings.policies) {
        this.settings.policies[i] = this.settings.policies[i].bind(this);
    }

    if(this.settings.alien) {
        for(var i in alien) {
            if(this.settings.models[i]) delete this.settings.models[i];
        }
    }

    if(this.settings.orm) {
        this.orm = this.settings.orm;
        for(var i in this.settings.policies) {
            this.orm.setPolicy(true, i, this.settings.policies[i]);
        }
    } else {

        this.orm = new modelling({
            models: this.settings.models,
            adapters: this.settings.adapters,
            connections: this.settings.connections,
            app: this.settings.app,
            policies: this.settings.policies
        });
    }
}

OpenIDConnect.prototype = new EventEmitter();

OpenIDConnect.prototype.done = function() {
    this.orm.done();
};

OpenIDConnect.prototype.model = function(name) {
    return this.orm.model(name);
}

OpenIDConnect.prototype.use = function(name) {
    var alien = {};
    if(this.settings.alien) {
        var self = this;
        if(!name) {
            alien = this.settings.alien;
        } else {
            var m;
            if(_.isPlainObject(name) && name.models) {
                m = name.models;
            }
            if(util.isArray(m||name)) {
                (m||name).forEach(function(model) {
                    if(self.settings.alien[model]) {
                        alien[model] = self.settings.alien[model];
                    }
                });
            } else if(self.settings.alien[m||name]) {
                alien[m||name] = self.settings.alien[m||name];
            }
        }
    }
    return [this.orm.use(name), function(req, res, next) {
        extend(req.model, alien);
        next();
    }];
};

OpenIDConnect.prototype.getOrm = function() {
    return this.orm;
}
/*OpenIDConnect.prototype.getClientParams = function() {
    return this.orm.client.getParams();
};*/

/*OpenIDConnect.prototype.searchClient = function(parts, callback) {
    return new this.orm.client.reverse(parts, callback);
};

OpenIDConnect.prototype.getUserParams = function() {
    return this.orm.user.getParams();
};

OpenIDConnect.prototype.user = function(params, callback) {
    return new this.orm.user(params, callback);
};

OpenIDConnect.prototype.searchUser = function(parts, callback) {
    return new this.orm.user.reverse(parts, callback);
};*/

OpenIDConnect.prototype.errorHandle = function(res, uri, error, desc) {
    if(uri) {
        var redirect = url.parse(uri,true);
        redirect.query.error = error; //'invalid_request';
        redirect.query.error_description = desc; //'Parameter '+x+' is mandatory.';
        res.redirect(400, url.format(redirect));
    } else {
        res.send(400, error+': '+desc);
    }
};

OpenIDConnect.prototype.endpointParams = function (spec, req, res, next) {
    try {
        req.parsedParams = this.parseParams(req, res, spec);
        next();
    } catch(err) {
        this.errorHandle(res, err.uri, err.error, err.msg);
    }
}

OpenIDConnect.prototype.parseParams = function(req, res, spec) {
    var params = {};
    var r = req.param('redirect_uri');
    for(var i in spec) {
        var x = req.param(i);
        if(x) {
            params[i] = x;
        }
    }

    for(var i in spec) {
        var x = params[i];
        if(!x) {
            var error = false;
            if(typeof spec[i] == 'boolean') {
                error = spec[i];
            } else if (_.isPlainObject(spec[i])) {
                for(var j in spec[i]) {
                    if(!util.isArray(spec[i][j])) {
                        spec[i][j] = [spec[i][j]];
                    }
                    spec[i][j].forEach(function(e) {
                        if(!error) {
                            if(util.isRegExp(e)) {
                                error = e.test(params[j]);
                            } else {
                                error = e == params[j];
                            }
                        }
                    });
                }
            } else if (_.isFunction(spec[i])) {
                error = spec[i](params);
            }

            if(error) {
                throw {type: 'error', uri: r, error: 'invalid_request', msg: 'Parameter '+i+' is mandatory.'};
                //this.errorHandle(res, r, 'invalid_request', 'Parameter '+i+' is mandatory.');
                //return;
            }
        }
    }
    return params;
};

/**
 * login
 *
 * returns a function to be placed as middleware in connect/express routing methods. For example:
 *
 * app.post('/login', oidc.login(),  afterLogin, loginErrorHandler);
 *
 * This calls verification strategy and creates session.
 * Verification strategy must have two parameters: req and callback function with two parameters: error and user
 *
 *
 */

OpenIDConnect.prototype.login = function(validateUser) {
    var self = this;

    return [self.use({policies: {loggedIn: false}, models: ['user', 'client']}),
            function(req, res, next) {
                validateUser(req, /*next:*/function(error,user) {
                    if(!error && !user) {
                        error = new Error('User not validated');
                    }
                    if(!error) {
                        if(user.id) {
                            req.session.user = user.id;
                        } else {
                            delete req.session.user;
                        }
                        if(user.sub) {
                            if(typeof user.sub ==='function') {
                                req.session.sub = user.sub();
                            } else {
                                req.session.sub = user.sub;
                            }
                        } else {
                            delete req.session.sub;
                        }
                        return next();
                    } else {
                        return next(error);
                    }
                });
    }];
};

/**
 * auth
 *
 * returns a function to be placed as middleware in connect/express routing methods. For example:
 *
 * app.get('/authorization', oidc.auth());
 *
 * This is the authorization endpoint, as described in http://tools.ietf.org/html/rfc6749#section-3.1
 *
 */
OpenIDConnect.prototype.auth = function() {
    var self = this;
    var spec = {
            response_type: true,
            client_id: true,
            scope: true,
            redirect_uri: true,
            state: false,
            nonce: function(params){
                return params.response_type.indexOf('id_token')!==-1;
            },
            display: false,
            prompt: false,
            max_age: false,
            ui_locales: false,
            claims_locales: false,
            id_token_hint: false,
            login_hint: false,
            acr_values: false,
            response_mode: false
    };
    return [function(req, res, next) {
                self.endpointParams(spec, req, res, next);
            },
            self.use(['client', 'consent', 'auth', 'access']),
            function(req, res, next) {
                Q(req.parsedParams).then(function(params) {
                    //Step 2: Check if response_type is supported and client_id is valid.

                    var deferred = Q.defer();
                    switch(params.response_type) {
                    case 'none':
                    case 'code':
                    case 'token':
                    case 'id_token':
                        break;
                    default:
                        //var error = false;
                        var sp = params.response_type.split(' ');
                        sp.forEach(function(response_type) {
                            if(['code', 'token', 'id_token'].indexOf(response_type) == -1) {
                                throw {type: 'error', uri: params.redirect_uri, error: 'unsupported_response_type', msg: 'Response type '+response_type+' not supported.'};
                            }
                        });
                    }
                    req.model.client.findOne({key: params.client_id}, function(err, model) {
                        if(err || !model || model === '') {
                            deferred.reject({type: 'error', uri: params.redirect_uri, error: 'invalid_client', msg: 'Client '+params.client_id+' doesn\'t exist.'});
                        } else {
                            req.session.client_id = model.id;
                            req.session.client_secret = model.secret;
                            deferred.resolve(params);
                        }
                    });

                    return deferred.promise;
                }).then(function(params){
                    //Step 3: Check if scopes are valid, and if consent was given.

                    var deferred = Q.defer();
                    var reqsco = params.scope.split(' ');
                    req.session.scopes = {};
                    var promises = [];
                    req.model.consent.findOne({user: req.session.user, client: req.session.client_id}, function(err, consent) {
                            reqsco.forEach(function(scope) {
                                var innerDef = Q.defer();
                                if(!self.settings.scopes[scope]) {
                                    innerDef.reject({type: 'error', uri: params.redirect_uri, error: 'invalid_scope', msg: 'Scope '+scope+' not supported.'});
                                }
                                if(!consent) {
                                    req.session.scopes[scope] = {ismember: false, explain: self.settings.scopes[scope]};
                                    innerDef.resolve(true);
                                } else {
                                    var inScope = consent.scopes.indexOf(scope) !== -1;
                                    req.session.scopes[scope] = {ismember: inScope, explain: self.settings.scopes[scope]};
                                    innerDef.resolve(!inScope);
                                }
                                promises.push(innerDef.promise);
                            });

                            Q.allSettled(promises).then(function(results){
                                var redirect = false;
                                for(var i = 0; i<results.length; i++) {
                                    if(results[i].value) {
                                        redirect = true;
                                        break;
                                    }
                                }
                                if(redirect) {
                                    req.session.client_key = params.client_id;
                                    var q = req.path+'?'+querystring.stringify(params);
                                    if(q.indexOf('/v1') == -1) {
                                        q = '/v1'+q;
                                    }
                                    deferred.reject({type: 'redirect', uri: self.settings.consent_url+'?'+querystring.stringify({return_url: q})});
                                } else {
                                    deferred.resolve(params);
                                }
                            });
                    });

                    return deferred.promise;
                }).then(function(params){
                    //Step 5: create responses
                    if(params.response_type == 'none') {
                        return {params: params, resp: {}};
                    } else {
                        var deferred = Q.defer();
                        var promises = [];

                        var rts = params.response_type.split(' ');

                        rts.forEach(function(rt) {
                            var def = Q.defer();
                            promises.push(def.promise);
                            switch(rt) {
                            case 'code':
                                var createToken = function() {
                                    var token = crypto.createHash('md5').update(params.client_id).update(Math.random()+'').digest('hex');
                                    req.model.auth.findOne({code: token}, function(err, auth){
                                        if(!auth) {
                                            setToken(token);
                                        } else {
                                            createToken();
                                        }
                                    });
                                };
                                var setToken = function(token) {
                                    req.model.auth.create({
                                        client: req.session.client_id,
                                        scope: params.scope.split(' '),
                                        user: req.session.user,
                                        sub: req.session.sub||req.session.user,
                                        code: token,
                                        redirectUri: params.redirect_uri,
                                        responseType: params.response_type,
                                        status: 'created'
                                    }).exec(function(err, auth) {
                                        if(!err && auth) {
                                            setTimeout(function() {
                                                req.model.auth.findOne({code: token}, function(err, auth) {
                                                    if(auth && auth.status == 'created') {
                                                        auth.destroy();
                                                    }
                                                });
                                            }, 1000*60*10); //10 minutes
                                            def.resolve({code: token});
                                        } else {
                                            def.reject(err||'Could not create auth');
                                        }
                                    });

                                };
                                createToken();
                                break;
                            case 'id_token':
                                var d = Math.round(new Date().getTime()/1000);
                                //var id_token = {
                                def.resolve({id_token: {
                                        iss: req.protocol+'://'+req.headers.host,
                                        sub: req.session.sub||req.session.user,
                                        subscription_id: req.session.subscription_id,
                                        aud: params.client_id,
                                        exp: d+3600,
                                        iat: d,
                                        nonce: params.nonce
                                }});
                                //def.resolve({id_token: jwt.encode(id_token, req.session.client_secret)});
                                break;
                            case 'token':
                                var createToken = function() {
                                    var token = crypto.createHash('md5').update(params.client_id).update(Math.random()+'').digest('hex');
                                    req.model.access.findOne({token: token}, function(err, access) {
{'fraction_non_alphanumeric': 0.11283320485251755, 'fraction_numerical': 0.004990465371039071, 'mean_word_length': 1.027766351295763, 'pattern_counts': {'":': 0, '<': 18, '<?xml version=': 0, '>': 16, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 169, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '3029635', 'n_tokens_mistral': 11823, 'n_tokens_neox': 11165, 'n_words': 2399}