OKTA login

This commit is contained in:
Manuel Romero
2024-02-19 15:37:32 +01:00
parent d262735c4e
commit d5d7a128d2
25 changed files with 322 additions and 15529 deletions

View File

@@ -9,9 +9,9 @@
```json
{
"AZURE_TENANT_ID" : "xxxxxxxx",
"AZURE_CLIENT_ID": "yyyyyyyy",
"AZURE_CLIENT_SECRET": "zzzzzzzz"
"IDENTITY_METADATA" : "xxxxxxxx",
"CLIENT_ID": "yyyyyyyy",
"CLIENT_SECRET": "zzzzzzzz"
}
```

View File

@@ -19,9 +19,9 @@
<script crossorigin="anonymous"
type="application/javascript"
src="https://cdn.jsdelivr.net/npm/@qlik/embed-web-components"
data-host="https://gear-presales.eu.qlikcloud.com"
data-host="https://innovation.us.qlikcloud.com"
data-auth-type="Oauth2"
data-client-id="9c6d9154b5299992a4e7343b6ad51f6b"
data-client-id="21be5044bba1072c16a803a3e6e4dca0"
data-redirect-uri="https://qmicloud-dev.qliktech.com/oauth-callback.html"
data-access-token-storage="session"
data-auto-redirect="true"></script>
@@ -29,5 +29,5 @@
<link rel="stylesheet" href="styles.3b2b6672156f20378f8f.css"></head>
<body>
<app-root></app-root>
<script src="runtime.c51bd5b1c616d9ffddc1.js" defer></script><script src="polyfills-es5.6fef7e679f78bcc42760.js" nomodule defer></script><script src="polyfills.51f5cc3d1309de3a873d.js" defer></script><script src="scripts.1af868998801499c8755.js" defer></script><script src="main.9086119c4bc33fe8f8da.js" defer></script></body>
<script src="runtime.c51bd5b1c616d9ffddc1.js" defer></script><script src="polyfills-es5.6fef7e679f78bcc42760.js" nomodule defer></script><script src="polyfills.51f5cc3d1309de3a873d.js" defer></script><script src="scripts.1af868998801499c8755.js" defer></script><script src="main.31a5dbddc5c54964988f.js" defer></script></body>
</html>

File diff suppressed because one or more lines are too long

View File

@@ -5,7 +5,7 @@
<script
crossorigin="anonymous"
type="application/javascript"
data-host="https://gear-presales.eu.qlikcloud.com"
data-host="https://innovation.us.qlikcloud.com"
src="https://cdn.jsdelivr.net/npm/@qlik/embed-web-components@0/dist/oauth-callback.js"
></script>
</head>

15490
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "qmi-cloud-app",
"version": "4.0.0",
"version": "5.0.0",
"scripts": {
"start": "node -r esm server/server.js",
"start:dev": "nodemon -r esm server/server.js",
@@ -51,7 +51,7 @@
"ngx-markdown": "^9.0.0",
"nodemon": "^1.19.1",
"passport": "^0.4.0",
"passport-azure-ad": "^4.1.0",
"passport-openidconnect": "^0.1.2",
"qdt-components": "^2.5.5",
"qmi-cloud-common": "./qmi-cloud-common",
"rxjs": "~6.5.4",

View File

@@ -12,7 +12,10 @@ const userSchema = new mongoose.Schema({
default: Date.now
},
displayName: String,
upn: String,
upn: {
type: String,
index: true
},
sub: String,
oid: {
type: String,

View File

@@ -2,16 +2,14 @@
const secrets = require("/run/secrets/config.json");
const HOSTNAME_URL = process.env.HOSTNAME_URL || "http://localhost:3000"
const TENANT_ID = secrets.AZURE_TENANT_ID;
const CLIENT_ID = secrets.AZURE_CLIENT_ID;
const CLIENT_SECRET = secrets.AZURE_CLIENT_SECRET;
const CLIENT_ID = secrets.CLIENT_ID;
const CLIENT_SECRET = secrets.CLIENT_SECRET;
const LOGOUT_URL = HOSTNAME_URL;
const REDIRECT_URL = `${HOSTNAME_URL}/auth/openid/return`;
exports.creds = {
// Required
identityMetadata: `https://login.microsoftonline.com/${TENANT_ID}/.well-known/openid-configuration`,
// or equivalently: 'https://login.microsoftonline.com/${TENANT_ID}/.well-known/openid-configuration'
//identityMetadata: IDENTITY_METADATA,
//
// or you can use the common endpoint
// 'https://login.microsoftonline.com/common/.well-known/openid-configuration'
@@ -85,8 +83,9 @@ exports.creds = {
exports.resourceURL = 'https://graph.microsoft.com';
// The url you need to go to destroy the session with AAD
exports.destroySessionUrl = `https://login.microsoftonline.com/common/oauth2/logout?post_logout_redirect_uri=${LOGOUT_URL}`;
//exports.destroySessionUrl = `https://login.microsoftonline.com/common/oauth2/logout?post_logout_redirect_uri=${LOGOUT_URL}`;
//exports.destroySessionUrl = `https://qlik.okta.com/logout?id_token_hint=${id_token}&post_logout_redirect_uri=${LOGOUT_URL}&state=${state}`
exports.destroySessionUrl = `https://qlik.okta.com/logout?post_logout_redirect_uri=${LOGOUT_URL}`
// If you want to use the mongoDB session store for session middleware, set to true; otherwise we will use the default
// session store provided by express-session.
// Note that the default session store is designed for development purpose only.

268
server/passport-okta.js Normal file
View File

@@ -0,0 +1,268 @@
const passport = require('passport');
const expressSession = require('express-session');
const config = require('./config');
// set up database for express session
const MongoStore = require('connect-mongo')(expressSession);
const mongoose = require('mongoose');
const db = require("qmi-cloud-common/mongo");
const sessionStore = new MongoStore({
mongooseConnection: mongoose.connection,
url: process.env.MONGO_URI,
autoRemove: 'interval',
autoRemoveInterval: 10
//clear_interval: config.mongoDBSessionMaxAge
});
var OpenIDConnectStrategy = require('passport-openidconnect');
const OKTA_DOMAIN = "qlik.okta.com";
passport.serializeUser(function(user, done) {
done(null, user.upn);
});
passport.deserializeUser(function(upn, done) {
_findByUpn(upn, function (err, user) {
done(err, user);
});
});
var _findByUpn = async function(upn, fn) {
var mongouser = await db.user.getOne({"upn": { $regex: new RegExp(upn, 'i') } });
if (mongouser && mongouser.upn === upn){
return fn(null, mongouser);
} else {
return fn(null, null);
}
};
// set up passport
passport.use('oidc', new OpenIDConnectStrategy({
issuer: "https://qlik.okta.com",
authorizationURL: `https://${OKTA_DOMAIN}/oauth2/v1/authorize`,
tokenURL: `https://${OKTA_DOMAIN}/oauth2/v1/token`,
clientID: config.creds.clientID,
clientSecret: config.creds.clientSecret,
callbackURL: config.creds.redirectUrl,
scope: config.creds.scope,
}, async (issuer, profile, context, idToken, accessToken, refreshToken, done) => {
console.log("OKTA ISSUER ", issuer)
console.log("OKTA PROFILE ", profile)
//console.log("OKTA context ", context)
//console.log("OKTA idToken ", idToken)
//console.log("OKTA accessToken ", accessToken)
//console.log("OKTA refreshToken ", refreshToken)
if ( !profile.id ) {
return done(new Error("No profile id found"), null);
}
// asynchronous verification, for effect...
process.nextTick(function () {
_findByUpn(profile.username, async function(err, user) {
if (err) {
return done(err);
}
if (!user) {
// "Auto-registration"
user = await db.user.add({
"oid": profile.id,
"upn": profile.username.toLowerCase(),
"displayName": profile.displayName,
"lastLogin": new Date(),
"sub": profile.id,
"active": true,
"mail": profile.emails[0].value,
//"jobTitle": jobTitle
});
return done(null, user);
}
db.user.update(user._id, {
"lastLogin": new Date(),
"sub": profile.id,
"active": true,
"mail": profile.emails[0].value,
//"jobTitle": jobTitle
});
return done(null, user);
});
});
//return done(null, profile);
}));
module.exports.init = function(app){
// set up session middleware
if (config.useMongoDBSessionStore) {
//mongoose.connect(config.databaseUri);
app.use(expressSession({
secret: 'secret',
cookie: {maxAge: config.mongoDBSessionMaxAge * 1000},
store: sessionStore,
resave: true,
saveUninitialized: false
}));
} else {
app.use(expressSession({
secret: 'keyboard cat',
resave: true,
saveUninitialized: false
}));
}
// Initialize Passport! Also use passport.session() middleware, to support
// persistent login sessions (recommended).
app.use(passport.initialize());
app.use(passport.session());
app.get('/login',
function(req, res, next) {
passport.authenticate('oidc',
{
response: res, // required
resourceURL: config.resourceURL, // optional. Provide a value if you want to specify the resource.
//customState: 'my_state', // optional. Provide a value if you want to provide custom state value.
failureRedirect: '/',
session: false
}
)(req, res, next);
},
function(req, res) {
res.redirect('/');
}
);
//app.use('/login', passport.authenticate('oidc'));
app.use('/auth/openid/return', passport.authenticate('oidc', { failureRedirect: '/error' }), (req, res) => {
console.log('Passport# We received a return from OKTA ');
res.redirect('/provisions');
});
/*app.get('/login',
function(req, res, next) {
passport.authenticate('azuread-openidconnect',
{
response: res, // required
resourceURL: config.resourceURL, // optional. Provide a value if you want to specify the resource.
//customState: 'my_state', // optional. Provide a value if you want to provide custom state value.
failureRedirect: '/',
session: false
}
)(req, res, next);
},
function(req, res) {
res.redirect('/');
}
);*/
// 'GET returnURL'
// `passport.authenticate` will try to authenticate the content returned in
// query (such as authorization code). If authentication fails, user will be
// redirected to '/' (home page); otherwise, it passes to the next middleware.
/*app.get('/auth/openid/return',
function(req, res, next) {
passport.authenticate('azuread-openidconnect',
{
response: res, // required
failureRedirect: '/'
}
)(req, res, next);
},
function(req, res) {
console.log('Passport# We received a return from AzureAD.');
res.redirect('/provisions');
}
);*/
// 'POST returnURL'
// `passport.authenticate` will try to authenticate the content returned in
// body (such as authorization code). If authentication fails, user will be
// redirected to '/' (home page); otherwise, it passes to the next middleware.
/*app.post('/auth/openid/return',
function(req, res, next) {
passport.authenticate('azuread-openidconnect',
{
response: res, // required
failureRedirect: '/'
}
)(req, res, next);
},
function(req, res) {
console.log('Passport# We received a return from AzureAD.');
res.redirect('/provisions');
}
);*/
// 'logout' route, logout from passport, and destroy the session with AAD.
app.get('/logout', function(req, res){
req.session.destroy(function(err) {
req.logOut();
res.redirect(config.destroySessionUrl);
});
});
};
async function isApiKeyAuthenticated(req) {
let key = req.query.apiKey || req.get('QMI-ApiKey');
if ( key ) {
var result = await db.apiKey.getOne({"apiKey": key});
if ( result ) {
req.user = result.user;
return true;
} else {
return false;
}
} else {
return false;
}
}
module.exports.ensureAuthenticatedDoLogin = async function(req, res, next) {
if ( await isApiKeyAuthenticated(req) || req.isAuthenticated() ) {
return next();
}
res.redirect('/login');
};
module.exports.ensureAuthenticated = async function(req, res, next) {
if ( await isApiKeyAuthenticated(req) || req.isAuthenticated() ) {
return next();
}
res.status(401).send({"error": "Unauthorized"});
};
module.exports.ensureAuthenticatedAndAdmin = async function(req, res, next) {
if ( ( await isApiKeyAuthenticated(req) || req.isAuthenticated()) && (req.user.role === 'admin' || req.user.role === 'superadmin') ) {
return next();
}
res.status(401).send({"error": "Unauthorized"});
};
module.exports.ensureAuthenticatedAndIsMe = async function (req, res, next) {
if ( await isApiKeyAuthenticated(req) || req.isAuthenticated() ) {
var userId = (req.params.userId === 'me')? req.user._id : req.params.userId;
if ( req.user._id == userId || req.user.role === 'admin' || req.user.role === 'superadmin' ) {
return next();
} else {
return res.status(401).send("Error: Unauthorized");
}
}
return res.status(401).send("Error: Unauthorized");
};
module.exports.sessionStore = sessionStore;

View File

@@ -1,7 +1,7 @@
const express = require('express');
const router = express.Router();
const db = require('qmi-cloud-common/mongo');
const passport = require('../passport');
const passport = require('../passport-okta');
/**

View File

@@ -1,7 +1,7 @@
const express = require('express')
const router = express.Router()
const db = require('qmi-cloud-common/mongo');
const passport = require('../passport');
const passport = require('../passport-okta');
/**

View File

@@ -1,7 +1,7 @@
const express = require('express')
const router = express.Router()
const db = require('qmi-cloud-common/mongo');
const passport = require('../passport');
const passport = require('../passport-okta');
/**

View File

@@ -1,7 +1,7 @@
const express = require('express');
const router = express.Router();
const db = require('qmi-cloud-common/mongo');
const passport = require('../passport');
const passport = require('../passport-okta');
import { queues, SYNAPSE_QUEUE } from 'qmi-cloud-common/queues';

View File

@@ -1,7 +1,7 @@
const express = require('express');
const router = express.Router();
const db = require('qmi-cloud-common/mongo');
const passport = require('../passport');
const passport = require('../passport-okta');
const sendEmail = require('qmi-cloud-common/send-email');

View File

@@ -1,7 +1,7 @@
const express = require('express')
const router = express.Router()
const db = require('qmi-cloud-common/mongo');
const passport = require('../passport');
const passport = require('../passport-okta');
const fs = require('fs-extra');

View File

@@ -1,7 +1,7 @@
const express = require('express')
const router = express.Router()
const db = require('qmi-cloud-common/mongo');
const passport = require('../passport');
const passport = require('../passport-okta');
router.get('/all', passport.ensureAuthenticated, async (req, res, next) => {

View File

@@ -1,7 +1,7 @@
const express = require('express');
const router = express.Router();
const db = require('qmi-cloud-common/mongo');
const passport = require('../passport');
const passport = require('../passport-okta');
const cloudshare = require('../training/cloudshare');
const qa = require('../training/automations');

View File

@@ -1,7 +1,7 @@
const express = require('express')
const router = express.Router()
const db = require('qmi-cloud-common/mongo');
const passport = require('../passport');
const passport = require('../passport-okta');
const fs = require('fs-extra');
const cli = require('qmi-cloud-common/cli');
const barracuda = require('qmi-cloud-common/barracuda');

View File

@@ -11,7 +11,7 @@ const { v4: uuidv4 } = require("uuid");
const ws = require("ws");
const WebSocketServer = ws.WebSocketServer;
const WebSocket = ws.WebSocket;
const passport = require('../passport');
const passport = require('../passport-okta');
// This is the frontend application uri used for responding to requests.
@@ -19,7 +19,7 @@ const passport = require('../passport');
const privKey = fs.readFileSync(path.resolve(__dirname, '..', 'certs', 'privateKey.pem'));
const TENANT_DOMAIN = process.env["TENANT_DOMAIN"] || "gear-presales.eu.qlikcloud.com";
const TENANT_DOMAIN = process.env["TENANT_DOMAIN"] || "innovation.us.qlikcloud.com";
const JWT_KEYID = process.env["JWT_KEYID"] || "8889be0d-6cf0-44eb-9fef-24bbc83712c5";
const JWT_ISSUER = process.env["JWT_ISSUER"] || TENANT_DOMAIN;
const JWT_USER_GROUPS = ["AnonJWTGroup"];

View File

@@ -23,7 +23,7 @@ const https = require('https');
const fs = require('fs');
const path = require('path');
const passport = require('./passport');
const passport = require('./passport-okta');
const qsProxy = require("./routes/qsProxy");

View File

@@ -5,7 +5,7 @@
</p>
<p>
Qlik app <a href="https://gear-presales.eu.qlikcloud.com/sense/app/1d2a43cf-8bc7-422c-90bd-d021cb232776/sheet/1ad3eb62-330d-45ac-8456-a6c8a76e044b/state/analysis" target="_blank">here</a>.
Qlik app <a href="https://innovation.us.qlikcloud.com/sense/app/314a70a2-a09c-4871-b74d-2de4f8a350a5/sheet/6251148f-b574-4363-a33f-1f67306e128e/state/analysis7" target="_blank">here</a>.
</p>
<!--
<iframe id="cost-analysis" *ngIf="qcsSheetUrl" class="cost-analysis" [src]="qcsSheetUrl" width="100%" height="1000px"></iframe>
@@ -15,13 +15,13 @@
<div style="width:100%; height: 50px">
<qlik-embed
ui="selections"
app-id="1d2a43cf-8bc7-422c-90bd-d021cb232776"
app-id="477a7c47-3326-427b-9cef-43f5db3787a0"
></qlik-embed>
</div>
<div style="width: 100%; height:1000px;overflow: auto;">
<qlik-embed
ui="analytics/sheet"
app-id="1d2a43cf-8bc7-422c-90bd-d021cb232776"
app-id="477a7c47-3326-427b-9cef-43f5db3787a0"
object-id="6251148f-b574-4363-a33f-1f67306e128e"
>
</qlik-embed>

View File

@@ -19,10 +19,10 @@
<script crossorigin="anonymous"
type="application/javascript"
src="https://cdn.jsdelivr.net/npm/@qlik/embed-web-components"
data-host="https://gear-presales.eu.qlikcloud.com"
data-host="https://innovation.us.qlikcloud.com"
data-auth-type="Oauth2"
data-client-id="9c6d9154b5299992a4e7343b6ad51f6b"
data-redirect-uri="https://qmicloud.qliktech.com/oauth-callback.html"
data-client-id="21be5044bba1072c16a803a3e6e4dca0"
data-redirect-uri="https://qmicloud-dev.qliktech.com/oauth-callback.html"
data-access-token-storage="session"
data-auto-redirect="true"></script>

View File

@@ -5,7 +5,7 @@
<script
crossorigin="anonymous"
type="application/javascript"
data-host="https://gear-presales.eu.qlikcloud.com"
data-host="https://innovation.us.qlikcloud.com"
src="https://cdn.jsdelivr.net/npm/@qlik/embed-web-components@0/dist/oauth-callback.js"
></script>
</head>

View File

@@ -8693,6 +8693,11 @@ oauth-sign@~0.9.0:
resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455"
integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==
oauth@0.10.x:
version "0.10.0"
resolved "https://registry.yarnpkg.com/oauth/-/oauth-0.10.0.tgz#3551c4c9b95c53ea437e1e21e46b649482339c58"
integrity sha512-1orQ9MT1vHFGQxhuy7E/0gECD3fd2fCC+PIX+/jgmU/gI3EpRocXtmtvxCO5x3WZ443FLTLFWNDjl5MPJf9u+Q==
oauth@0.9.15:
version "0.9.15"
resolved "https://registry.yarnpkg.com/oauth/-/oauth-0.9.15.tgz#bd1fefaf686c96b75475aed5196412ff60cfb9c1"
@@ -9108,10 +9113,18 @@ passport-azure-ad@^4.1.0:
request "^2.72.0"
valid-url "^1.0.6"
passport-openidconnect@^0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/passport-openidconnect/-/passport-openidconnect-0.1.2.tgz#86c830a96cfc2cf7e0273d3eba2828ab6abb3166"
integrity sha512-JX3rTyW+KFZ/E9OF/IpXJPbyLO9vGzcmXB5FgSP2jfL3LGKJPdV7zUE8rWeKeeI/iueQggOeFa3onrCmhxXZTg==
dependencies:
oauth "0.10.x"
passport-strategy "1.x.x"
passport-strategy@1.x.x:
version "1.0.0"
resolved "https://registry.yarnpkg.com/passport-strategy/-/passport-strategy-1.0.0.tgz#b5539aa8fc225a3d1ad179476ddf236b440f52e4"
integrity sha1-tVOaqPwiWj0a0XlHbd8ja0QPUuQ=
integrity sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==
passport@^0.3.2:
version "0.3.2"
@@ -9184,7 +9197,7 @@ path-type@^3.0.0:
pause@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/pause/-/pause-0.0.1.tgz#1d408b3fdb76923b9543d96fb4c9dfd535d9cb5d"
integrity sha1-HUCLP9t2kjuVQ9lvtMnf1TXZy10=
integrity sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==
pbf@^3.0.5, pbf@^3.2.1:
version "3.2.1"