"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Account = exports.OAuthProvider = void 0;
const openflow_api_1 = require("@openiap/openflow-api");
const Config_1 = require("./Config");
const Crypt_1 = require("./Crypt");
const oidc_provider_1 = require("oidc-provider");
const MongoAdapter_1 = require("./MongoAdapter");
const Logger_1 = require("./Logger");
const Audit_1 = require("./Audit");
// const Request = OAuthServer.Request;
// const Response = OAuthServer.Response;
class OAuthProvider {
    constructor() {
        this.clients = [];
    }
    static async interactionsUrl(ctx, interaction) {
        return "/oidclogin";
    }
    static async logoutSource(ctx, form) {
        if (ctx && ctx.oidc && ctx.oidc.session && ctx.oidc.session.authorizations) {
            for (var i = 0; i < this.instance.clients.length; i++) {
                var cli = this.instance.clients[i];
                var auth = ctx.oidc.session.authorizations[cli.id];
                if (auth) {
                    if (cli.openflowsignout && cli.openflowsignout == true) {
                        ctx.req.logout();
                    }
                    if (cli.signin_url) {
                        ctx.res.cookie("oidcrefere", cli.signin_url, { maxAge: 900000, httpOnly: true });
                    }
                }
            }
        }
        ctx.body = `<!DOCTYPE html>
      <head>
      <title>Logout Request</title>
      <style>/* css and html classes omitted for brevity, see lib/helpers/defaults.js */</style>
      </head>
      <body onload="logout()">
      <div>
        <h1>Do you want to sign-out from ${ctx.hostname}?</h1>
        <script>
          function logout() {
            var form = document.getElementById('op.logoutForm');
            var input = document.createElement('input');
            input.type = 'hidden';
            input.name = 'logout';
            input.value = 'yes';
            form.appendChild(input);
            form.submit();
          }
          function rpLogoutOnly() {
            var form = document.getElementById('op.logoutForm');
            form.submit();
          }
          
        </script>
        ${form}
        <button onclick="logout()">Yes, sign me out</button>
        <button onclick="rpLogoutOnly()">No, stay signed in</button>
      </div>
      </body>
      </html>`;
    }
    static async postLogoutSuccessSource(ctx) {
        // @param ctx - koa request context
        // @param form - form source (id="op.logoutForm") to be embedded in the page and submitted by
        //   the End-User
        var oidcrefere = "";
        if (!openflow_api_1.NoderedUtil.IsNullEmpty(ctx.req.cookies.oidcrefere)) {
            oidcrefere = ctx.req.cookies.oidcrefere;
        }
        ctx.body = `<!DOCTYPE html>
      <head>
      <title>Logout Request</title>
      <style>/* css and html classes omitted for brevity, see lib/helpers/defaults.js */</style>
      </head>
      <body onload="logout()">
      <div>
        <h1>You have successfully signed out from ${ctx.hostname}</h1>`;
        if (!openflow_api_1.NoderedUtil.IsNullEmpty(oidcrefere)) {
            ctx.body += `<a href="${ctx.req.cookies.oidcrefere}">Return to ${ctx.req.cookies.oidcrefere}</a> ?`;
        }
        ctx.body += `</div>
      </body>
      </html>`;
        if (!openflow_api_1.NoderedUtil.IsNullEmpty(ctx.req.cookies.oidcrefere)) {
            // ctx.res.cookie("oidcrefere", "", { expires: new Date(0) });
            // LoginProvider.redirect(ctx.res, ctx.req.cookies.oidcrefere);
        }
    }
    static generatekeys() {
        return new Promise((resolve, reject) => {
            const jose = require('jose');
            const keystore = new jose.JWKS.KeyStore();
            Promise.all([
                keystore.generate('RSA', 2048, { use: 'sig' }),
                keystore.generate('RSA', 2048, { use: 'enc' }),
                keystore.generate('EC', 'P-256', { use: 'sig' }),
                keystore.generate('EC', 'P-256', { use: 'enc' }),
                keystore.generate('OKP', 'Ed25519', { use: 'sig' }),
            ]).then(() => {
                resolve(keystore.toJWKS(true));
            }).catch((err) => {
                reject(err);
            });
        });
    }
    static async LoadClients() {
        const instance = OAuthProvider.instance;
        const span = Logger_1.Logger.otel.startSpan("OAuthProvider.LoadClients");
        try {
            const jwksresults = await Config_1.Config.db.query({ query: { _type: "jwks" }, top: 10, collectionname: "config", jwt: Crypt_1.Crypt.rootToken() }, span);
            let jwks = null;
            if (jwksresults.length == 0) {
                jwks = await this.generatekeys();
                jwks._type = "jwks";
                await Config_1.Config.db.InsertOne(jwks, "config", 1, true, Crypt_1.Crypt.rootToken(), span);
            }
            else {
                jwks = jwksresults[0];
            }
            const result = await Config_1.Config.db.query({ query: { _type: "oauthclient" }, top: 10, collectionname: "config", jwt: Crypt_1.Crypt.rootToken() }, span);
            instance.clients = result;
            instance.clients.forEach(cli => {
                cli.client_id = cli.clientId;
                cli.client_secret = cli.clientSecret;
                cli.redirect_uris = cli.redirectUris;
                // token_endpoint_auth_method can only be none, client_secret_post, client_secret_basic, private_key_jwt or tls_client_auth
                if (openflow_api_1.NoderedUtil.IsNullEmpty(cli.token_endpoint_auth_method))
                    cli.token_endpoint_auth_method = "none";
                // response_types can only contain 'code id_token', 'code', 'id_token', or 'none' 
                // id_token token
                if (openflow_api_1.NoderedUtil.IsNullEmpty(cli.response_types))
                    cli.response_types = ['code', 'id_token', 'code id_token'];
                // https://github.com/panva/node-oidc-provider/blob/64edda69a84e556531f45ac814788c8c92ab6212/test/claim_types/claim_types.test.js
                if (cli.grant_types == null)
                    cli.grant_types = ['implicit', 'authorization_code'];
            });
            const provider = new oidc_provider_1.Provider(Config_1.Config.baseurl() + "oidc", {
                clients: instance.clients,
                adapter: MongoAdapter_1.MongoAdapter,
                formats: {
                    AccessToken: 'jwt',
                },
                ttl: {
                    AccessToken: (Config_1.Config.oidc_access_token_ttl * 60),
                    AuthorizationCode: (Config_1.Config.oidc_authorization_code_ttl * 60),
                    ClientCredentials: (Config_1.Config.oidc_client_credentials_ttl * 60),
                    RefreshToken: (Config_1.Config.oidc_refresh_token_ttl * 60),
                    Session: (Config_1.Config.oidc_session_ttl * 60)
                },
                jwks: jwks,
                features: {
                    encryption: { enabled: true },
                    introspection: { enabled: true },
                    revocation: { enabled: true },
                    devInteractions: { enabled: false },
                    clientCredentials: { enabled: true },
                    userinfo: { enabled: true },
                    jwtUserinfo: { enabled: true },
                    claimsParameter: { enabled: false },
                    rpInitiatedLogout: {
                        enabled: true,
                        logoutSource: this.logoutSource.bind(this),
                        postLogoutSuccessSource: this.postLogoutSuccessSource.bind(this)
                    }
                },
                claims: {
                    acr: null,
                    auth_time: null,
                    iss: null,
                    openid: [
                        'sub', 'name', 'email', 'email_verified', 'role', 'roles'
                    ],
                    sid: null
                },
                conformIdTokenClaims: false,
                interactions: {
                    url: this.interactionsUrl
                },
                findAccount: Account.findAccount,
                cookies: {
                    short: {
                        path: '/',
                    },
                    keys: [Config_1.Config.oidc_cookie_key],
                },
            });
            provider.proxy = true;
            const { invalidate: orig } = provider.Client.Schema.prototype;
            provider.Client.Schema.prototype.invalidate = function invalidate(message, code) {
                if (code === 'implicit-force-https' || code === 'implicit-forbid-localhost') {
                    return;
                }
                if (message === 'redirect_uris must contain members') {
                    return;
                }
                orig.call(this, message);
            };
            const orgpostLogoutRedirectUriAllowed = provider.Client.prototype.postLogoutRedirectUriAllowed;
            provider.Client.prototype.postLogoutRedirectUriAllowed = function (value) {
                if (this.postLogoutRedirectUris == null || this.postLogoutRedirectUris.length == 0)
                    return true;
                return orgpostLogoutRedirectUriAllowed(value);
            };
            const orgredirectUriAllowed = provider.Client.prototype.redirectUriAllowed;
            provider.Client.prototype.redirectUriAllowed = function (value) {
                if (this.redirectUris == null || this.redirectUris.length == 0)
                    return true;
                return orgredirectUriAllowed(value);
            };
            if (instance.oidc != null) {
                instance.oidc = provider;
                return;
            }
            instance.oidc = provider;
            instance.app.use('/oidc', async (req, res, next) => {
                if (req.originalUrl == "/oidc/me/emails") { // Grafana old school hack
                    res.send('[]');
                    return;
                }
                if (req.originalUrl.startsWith("/oidc/session/end")) {
                    if (!openflow_api_1.NoderedUtil.IsNullEmpty(req.headers.referer)) {
                        if (req.headers.referer.indexOf("oidc/session") == -1) {
                            res.cookie("oidcrefere", req.headers.referer, { maxAge: 900000, httpOnly: true });
                        }
                    }
                }
                instance.oidc.callback(req, res);
                // return next();
                // if (req.originalUrl.indexOf('/oidc') > -1) return next();
            });
            instance.app.use('/oidclogin', async (req, res, next) => {
                if (req && req.user) {
                    var validated = true;
                    if (Config_1.Config.validate_user_form != "") {
                        if (!req.user.formvalidated)
                            validated = false;
                    }
                    if (Config_1.Config.validate_emails) {
                        if (!req.user.emailvalidated)
                            validated = false;
                    }
                    if (!validated) {
                        res.cookie("originalUrl", "/oidclogin", { maxAge: 900000, httpOnly: true });
                        res.redirect("/login");
                        return next();
                    }
                    res.cookie("originalUrl", "/oidccb", { maxAge: 900000, httpOnly: true });
                    res.redirect("/oidccb");
                }
                else {
                    res.cookie("originalUrl", "/oidclogin", { maxAge: 900000, httpOnly: true });
                    res.redirect("/login");
                }
            });
            instance.app.use('/oidccb', async (req, res, next) => {
                try {
                    const { uid, prompt, params, session, } = await this.instance.oidc.interactionDetails(req, res);
                    var r = req;
                    var u = req.user;
                    const isAuthenticated = req.isAuthenticated();
                    if (isAuthenticated) {
                    }
                    else {
                        res.cookie("originalUrl", "/oidccb", { maxAge: 900000, httpOnly: true });
                        res.redirect('/login');
                        return;
                    }
                    if (req.user) {
                        const client = await this.instance.clients.filter(x => x.clientId == params.client_id)[0];
                        let _user = req.user;
                        let tuser = openflow_api_1.TokenUser.From(_user);
                        if (!openflow_api_1.NoderedUtil.IsNullEmpty(_user.impersonating)) {
                            const tempjwt = Crypt_1.Crypt.createToken(tuser, Config_1.Config.shorttoken_expires_in);
                            const items = await Config_1.Config.db.query({ query: { _id: _user.impersonating }, top: 1, collectionname: "users", jwt: tempjwt }, span);
                            if (items.length == 1) {
                                const tuserimpostor = tuser;
                                _user = openflow_api_1.User.assign(items[0]);
                                tuser = openflow_api_1.TokenUser.From(_user);
                                Logger_1.Logger.instanse.info("Message", "Signin", tuser.username + " successfully impersonated");
                                await Audit_1.Audit.ImpersonateSuccess(tuser, tuserimpostor, "browser", Config_1.Config.version, span);
                            }
                        }
                        Account.AddAccount(tuser, client);
                        await this.instance.oidc.interactionFinished(req, res, 
                        // result should be an object with some or all the following properties
                        {
                            // authentication/login prompt got resolved, omit if no authentication happened, i.e. the user
                            // cancelled
                            login: {
                                account: tuser._id,
                                acr: "acr",
                                remember: false, // true if provider should use a persistent cookie rather than a session one, defaults to true
                                // ts: number, // unix timestamp of the authentication, defaults to now()
                            },
                            // consent was given by the user to the client for this session
                            consent: {
                                rejectedScopes: [],
                                rejectedClaims: [], // array of strings, claim names the end-user has not granted
                            },
                            // meta is a free object you may store alongside an authorization. It can be useful
                            // during the interaction check to verify information on the ongoing session.
                            meta: {
                                // object structure up-to-you
                                "openflow": "true"
                            },
                            ['custom prompt name resolved']: {},
                        });
                    }
                }
                catch (error) {
                    if (error.name == "SessionNotFound") {
                        res.redirect(`/`);
                        res.end();
                        return;
                    }
                    res.json(error);
                }
            });
        }
        catch (error) {
            span === null || span === void 0 ? void 0 : span.recordException(error);
            Logger_1.Logger.instanse.error("OAuthProvider", "LoadClients", error);
        }
        finally {
            Logger_1.Logger.otel.endSpan(span);
        }
    }
    static configure(app, parent) {
        const span = Logger_1.Logger.otel.startSubSpan("OAuthProvider.configure", parent);
        try {
            const instance = new OAuthProvider();
            try {
                OAuthProvider.instance = instance;
                instance.app = app;
                // @ts-ignore
                this.LoadClients().catch(error => {
                    Logger_1.Logger.instanse.error("OAuthProvider", "configure", error);
                });
            }
            catch (error) {
                Logger_1.Logger.instanse.error("OAuthProvider", "configure", error);
                throw error;
            }
            return instance;
        }
        catch (error) {
            span === null || span === void 0 ? void 0 : span.recordException(error);
            Logger_1.Logger.instanse.error("OAuthProvider", "configure", error);
            return OAuthProvider.instance;
        }
        finally {
            Logger_1.Logger.otel.endSpan(span);
        }
    }
}
exports.OAuthProvider = OAuthProvider;
OAuthProvider.instance = null;
OAuthProvider.store = new Map();
class Account {
    constructor(accountId, user) {
        this.accountId = accountId;
        this.user = user;
        Logger_1.Logger.DBHelper.DeleteKey("users" + accountId);
        if (user == null)
            throw new Error("Cannot create Account from null user for id ${this.accountId}");
        user = Object.assign(user, { accountId: accountId, sub: accountId });
        // node-bb username hack
        if (openflow_api_1.NoderedUtil.IsNullEmpty(user.email))
            user.email = user.username;
        if (user.name == user.email && user.email.indexOf("@") > -1) {
            user.name = user.email.substr(0, user.email.indexOf("@") - 1);
        }
        if (user.name == user.email && user.email.indexOf("@") == -1) {
            user.email = user.email + "@unknown.local";
        }
        if (user.name == user.email) {
            user.name = "user " + user.email;
        }
        if (!openflow_api_1.NoderedUtil.IsNullUndefinded(user.roles) && Array.isArray(user.roles) && user.roles.length > 0) {
            if (!openflow_api_1.NoderedUtil.IsNullEmpty(user.roles[0].name)) {
                user.roles = user.roles.map(x => x.name);
            }
        }
        else {
            user.roles = ["users"];
        }
    }
    claims() {
        return this.user;
    }
    static async findAccount(ctx, id, test) {
        let acc = await Logger_1.Logger.DBHelper.memoryCache.get("oidc" + id);
        if (acc == null) {
            acc = await Logger_1.Logger.DBHelper.FindById(id, undefined, undefined);
        }
        var res = new Account(id, openflow_api_1.TokenUser.From(acc));
        return res;
    }
    static AddAccount(tuser, client) {
        try {
            let role = client.defaultrole;
            const keys = Object.keys(client.rolemappings);
            Logger_1.Logger.instanse.debug("OAuthProvider", "AddAccount", "[" + tuser.username + "] Lookup roles for " + tuser.username);
            for (let i = 0; i < keys.length; i++) {
                if (tuser.HasRoleName(keys[i])) {
                    Logger_1.Logger.instanse.debug("OAuthProvider", "AddAccount", "[" + tuser.username + "] User has role " + keys[i] + " set role " + client.rolemappings[keys[i]]);
                    role = client.rolemappings[keys[i]];
                }
            }
            tuser.role = role;
            Logger_1.Logger.DBHelper.memoryCache.set("oidc" + tuser._id, tuser);
            // DBHelper.DeleteKey("user" + tuser._id);
            var res = new Account(tuser._id, tuser);
            return res;
        }
        catch (error) {
            Logger_1.Logger.instanse.error("OAuthProvider", "AddAccount", error);
        }
        return undefined;
    }
    static async GetTokenRequest(code, parent) {
        let tokens = await Config_1.Config.db.query({ query: { _type: "tokenrequest", "code": code }, top: 1, collectionname: "oauthtokens", jwt: Crypt_1.Crypt.rootToken() }, parent);
        if (tokens.length == 0)
            return null;
        return tokens[0];
    }
    static async AddTokenRequest(code, item, parent) {
        var q = new openflow_api_1.InsertOrUpdateOneMessage();
        q.item = item;
        q.uniqeness = "_type,code";
        q.collectionname = "oauthtokens", q.jwt = Crypt_1.Crypt.rootToken();
        q.w = 1;
        q.j = true;
        let token = await Config_1.Config.db.InsertOrUpdateOne(q, parent);
        return token.item;
    }
    static async RemoveTokenRequest(code, parent) {
        let tokens = await Config_1.Config.db.DeleteMany({ _type: "tokenrequest", "code": code }, null, "oauthtokens", null, false, Crypt_1.Crypt.rootToken(), parent);
        return tokens[0];
    }
}
exports.Account = Account;
//# sourceMappingURL=OAuthProvider.js.map