Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for App ID authentication #117

Merged
merged 2 commits into from
Oct 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 0 additions & 13 deletions Dockerfile

This file was deleted.

1 change: 1 addition & 0 deletions Dockerfile
13 changes: 13 additions & 0 deletions Dockerfile-dev
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
FROM registry.access.redhat.com/ubi9/nodejs-18:1-70.1695740477

WORKDIR /opt/app-root/src

COPY --chown=default:root . .

RUN mkdir -p /opt/app-root/src/node_modules && \
ls -lA && \
npm ci

EXPOSE 5173

CMD ["npm", "run", "dev", "--", "--host"]
26 changes: 26 additions & 0 deletions Dockerfile-express
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
FROM registry.access.redhat.com/ubi9/nodejs-18:1-70.1695740477 AS builder

WORKDIR /opt/app-root/src

COPY --chown=default:root . .

RUN mkdir -p /opt/app-root/src/node_modules && \
ls -lA && \
npm ci && \
npm run build

FROM registry.access.redhat.com/ubi9/nodejs-18:1-70.1695740477

WORKDIR /opt/app-root/src

COPY --from=builder --chown=default:root /opt/app-root/src/dist ./dist

WORKDIR /opt/app-root/src/server

COPY --chown=default:root ./server/* .

RUN npm ci

EXPOSE 8080

CMD ["npm", "start", "--", "--host"]
4 changes: 2 additions & 2 deletions Dockerfile-prod → Dockerfile-nginx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM registry.access.redhat.com/ubi9/nodejs-18:1-62.1692771036 AS builder
FROM registry.access.redhat.com/ubi9/nodejs-18:1-70.1695740477 AS builder

WORKDIR /opt/app-root/src

Expand All @@ -9,7 +9,7 @@ RUN mkdir -p /opt/app-root/src/node_modules && \
npm ci && \
npm run build

FROM registry.access.redhat.com/ubi9/nginx-122:1-22.1692771040
FROM registry.access.redhat.com/ubi9/nginx-122:1-31

WORKDIR /opt/app-root/src

Expand Down
3 changes: 3 additions & 0 deletions config/nginx/nginx-base.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
location / {
try_files $uri /index.html;
}
9 changes: 9 additions & 0 deletions config/nginx/nginx-proxy.conf
Original file line number Diff line number Diff line change
@@ -1,12 +1,21 @@

location /api {
proxy_ssl_name watsonx-demo-svc.16suy1d71dxc.us-south.codeengine.appdomain.cloud;
proxy_ssl_server_name on;
proxy_http_version 1.1;
proxy_pass https://watsonx-demo-svc.16suy1d71dxc.us-south.codeengine.appdomain.cloud/;
}

location /graphql {
proxy_ssl_name watsonx-demo-svc.16suy1d71dxc.us-south.codeengine.appdomain.cloud;
proxy_ssl_server_name on;
proxy_http_version 1.1;
proxy_pass https://watsonx-demo-svc.16suy1d71dxc.us-south.codeengine.appdomain.cloud/graphql;
}

location /subscription {
proxy_ssl_name watsonx-demo-svc.16suy1d71dxc.us-south.codeengine.appdomain.cloud;
proxy_ssl_server_name on;
proxy_pass https://watsonx-demo-svc.16suy1d71dxc.us-south.codeengine.appdomain.cloud/subscription;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
Expand Down
39 changes: 36 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@
"@ibm-watson/assistant-web-chat-react": "^1.0.4",
"dayjs": "^1.11.10",
"graphql-ws": "^5.14.1",
"ibmcloud-appid-js": "^1.0.1",
"jotai": "^2.4.3",
"js-cookie": "^3.0.5",
"optional-js": "^2.3.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
Expand All @@ -29,6 +31,7 @@
},
"devDependencies": {
"@types/carbon__icons-react": "^11.26.1",
"@types/js-cookie": "^3.0.4",
"@types/node": "^20.7.1",
"@types/react": "^18.2.23",
"@types/react-dom": "^18.2.8",
Expand Down
192 changes: 192 additions & 0 deletions server/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@

const express = require('express');
const session = require('express-session');
const proxy = require('express-http-proxy');
const url = require('url');
const path = require('path');
const passport = require('passport');
const WebAppStrategy = require("ibmcloud-appid").WebAppStrategy;

const CALLBACK_URL = "/ibm/cloud/appid/callback";
const LOGOUT_URL = "/ibm/cloud/appid/logout";
const LOGOUT2_URL = "/logout";
const LOGIN_URL = "/ibm/cloud/appid/login";
const LOGIN2_URL = "/login";
const LANDING_PAGE_URL = "/";
const CHANGE_PASSWORD_URL = "/ibm/cloud/appid/change_password";
const CHANGE_PASSWORD2_URL = "/change_password";
const FORGOT_PASSWORD_URL = "/ibm/cloud/appid/forgot_password";
const FORGOT_PASSWORD2_URL = "/forgot_password";

const BACKEND_HOST = process.env.BACKEND_HOST || 'localhost:3000'
const PORT = process.env.PORT || '8080'
const HOST = process.env.HOST || 'http://localhost:8080'

const buildAppIdConfig = () => {
const result = {
clientId: process.env.APPID_CLIENT_ID,
tenantId: process.env.APPID_TENANT_ID,
secret: process.env.APPID_SECRET,
oAuthServerUrl: process.env.OAUTH_SERVER_URL,
redirectUri: `${HOST}${CALLBACK_URL}`
}

if (!result.clientId || !result.tenantId || !result.secret || !result.oAuthServerUrl) {
throw new Error('APPID_CLIENT_ID, APPID_TENANT_ID, APPID_SECRET, or OAUTH_SERVER_URL environment variable missing')
}

return result;
}

const COOKIE_NAME = 'refreshToken'

const createServer = () => {
const app = express();
app.use(session({
secret: "654321",
resave: true,
saveUninitialized: true
}));
app.use(passport.initialize());
app.use(passport.session());

const webAppStrategy = new WebAppStrategy(buildAppIdConfig())

passport.use(webAppStrategy);

passport.serializeUser((user, cb) => {
cb(null, user);
});
passport.deserializeUser((obj, cb) => {
cb(null, obj);
});

app.get(CALLBACK_URL, passport.authenticate(WebAppStrategy.STRATEGY_NAME, { keepSessionInfo: true }));

app.get(LOGIN_URL, passport.authenticate(WebAppStrategy.STRATEGY_NAME, {
successRedirect: LANDING_PAGE_URL,
forceLogin: true
}));
app.get(LOGIN2_URL, passport.authenticate(WebAppStrategy.STRATEGY_NAME, {
successRedirect: LANDING_PAGE_URL,
forceLogin: true
}));

app.get(FORGOT_PASSWORD_URL, passport.authenticate(WebAppStrategy.STRATEGY_NAME, {
successRedirect: LANDING_PAGE_URL,
show: WebAppStrategy.FORGOT_PASSWORD
}));
app.get(FORGOT_PASSWORD2_URL, passport.authenticate(WebAppStrategy.STRATEGY_NAME, {
successRedirect: LANDING_PAGE_URL,
show: WebAppStrategy.FORGOT_PASSWORD
}));

app.get(CHANGE_PASSWORD_URL, passport.authenticate(WebAppStrategy.STRATEGY_NAME, {
successRedirect: LANDING_PAGE_URL,
show: WebAppStrategy.CHANGE_PASSWORD
}));
app.get(CHANGE_PASSWORD2_URL, passport.authenticate(WebAppStrategy.STRATEGY_NAME, {
successRedirect: LANDING_PAGE_URL,
show: WebAppStrategy.CHANGE_PASSWORD
}));

const logout = (req, res) => {
console.log('Logging out')
req._sessionManager = false;
WebAppStrategy.logout(req);
req.logout();
req.session = null;
res.clearCookie(COOKIE_NAME);
res.redirect(LANDING_PAGE_URL);
}
app.get(LOGOUT_URL, logout);
app.get(LOGOUT2_URL, logout);

const apiProxy = proxy(BACKEND_HOST, {
proxyReqPathResolver: req => url.parse(req.baseUrl).path
});
app.use('/api/*', apiProxy);

const graphqlProxy = proxy(`${BACKEND_HOST}/graphql`, {
proxyReqPathResolver: req => url.parse(req.baseUrl).path
});
app.use('/graphql/*', graphqlProxy);
app.use('/graphql', graphqlProxy);

const subscriptionProxy = proxy(`${BACKEND_HOST}/subscription`, {
proxyReqPathResolver: req => url.parse(req.baseUrl).path
});
app.use('/subscription/*', subscriptionProxy);
app.use('/subscription', subscriptionProxy);

function storeRefreshTokenInCookie(req, res, next) {
const refreshToken = req.session[WebAppStrategy.AUTH_CONTEXT][COOKIE_NAME];
console.log('Storing refresh token: ', {refreshToken})
if (refreshToken) {
/* An example of storing user's refresh-token in a cookie with expiration of a month */
res.cookie(COOKIE_NAME, refreshToken, {
maxAge: 1000 * 60 * 60 * 24 * 30 /* 30 days */
});
}
next();
}

function isLoggedIn(req) {
const result = req.session[WebAppStrategy.AUTH_CONTEXT];

console.log('isLoggedIn? ', result)

return result;
}

function tryToRefreshTokenIfNotLoggedIn(req, res, next) {
if (isLoggedIn(req)) {
return next();
}

const refreshToken = (req.cookies || {})[COOKIE_NAME]
if (!refreshToken) {
return next();
}

console.log('Refreshing tokens')
webAppStrategy.refreshTokens(req, refreshToken).then(function () {
console.log('Next')
next();
}).catch(err => console.error('Error refreshing tokens: ', {err}));
}

app.get(
"/secure/*",
tryToRefreshTokenIfNotLoggedIn,
passport.authenticate(WebAppStrategy.STRATEGY_NAME, { keepSessionInfo: true }),
storeRefreshTokenInCookie,
(req, res) => {
res.sendFile(path.resolve(__dirname, '..', 'dist', 'index.html'))
}
);

//
// app.get(
// "/secure/*",
// passport.authenticate(WebAppStrategy.STRATEGY_NAME),
// (req, res) => {
// res.sendFile(path.resolve(__dirname, '..', 'dist', 'index.html'))
// }
// );

// app.get('*', passport.authenticate(WebAppStrategy.STRATEGY_NAME), (req, res) => {
// res.sendFile(path.resolve(__dirname, '..', 'dist', 'index.html'))
// })
// app.get('*', (req, res) => {
// res.sendFile(path.resolve(__dirname, '..', 'dist', 'index.html'))
// })
app.use(express.static(path.join(__dirname, '..', 'dist')))


return app;
}

createServer().listen(PORT, () => {
console.log(`Server started on port ${PORT}`)
})
Loading