Added SSO based auth
This commit is contained in:
parent
2e4af394f9
commit
edb2544e60
|
@ -129,3 +129,6 @@ dist
|
|||
.yarn/build-state.yml
|
||||
.yarn/install-state.gz
|
||||
.pnp.*
|
||||
|
||||
**/jwtRS256.key
|
||||
**/jwtRS256.key.pub
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
import { authClient } from "../helpers/auth_client";
|
||||
import { generators } from 'openid-client';
|
||||
import { configKeys } from "..";
|
||||
|
||||
const code_verifier = generators.codeVerifier();
|
||||
const code_challenge = generators.codeChallenge(code_verifier);
|
||||
|
||||
export default () => {
|
||||
const authurl = authClient.authorizationUrl({
|
||||
scope: 'email profile openid',
|
||||
code_challenge,
|
||||
code_challenge_method: 'S256',
|
||||
client_id: configKeys.KEYCLOAK_CLIENT_ID,
|
||||
redirect_uri: configKeys.KEYCLOAK_REDIRECT_URI,
|
||||
});
|
||||
|
||||
return {
|
||||
authurl,
|
||||
};
|
||||
}
|
||||
|
||||
export {
|
||||
code_challenge,
|
||||
code_verifier,
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
import { NextFunction, Response } from "express";
|
||||
import {authClient} from "../helpers/auth_client";
|
||||
import { ModRequest } from "../types";
|
||||
import { CustomError } from "../libs/error";
|
||||
|
||||
const middleware = async (req: ModRequest | any, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const authHeader = req.headers?.authorization?.split(' ');
|
||||
if (!authHeader) {
|
||||
throw new Error("No authorization header");
|
||||
}
|
||||
const token : string = authHeader[1];
|
||||
const user = await authClient.userinfo(token, {
|
||||
method: 'GET',
|
||||
tokenType: 'Bearer',
|
||||
params: {
|
||||
access_token: token
|
||||
},
|
||||
via: 'header'
|
||||
});
|
||||
req.user = user;
|
||||
next();
|
||||
}
|
||||
catch (err) {
|
||||
next(new CustomError({
|
||||
message: "Invalid token",
|
||||
statusCode: 401
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
export default middleware
|
|
@ -0,0 +1,10 @@
|
|||
import {authClient} from "../helpers/auth_client";
|
||||
import { code_verifier } from './check'
|
||||
|
||||
export default async (session_state: string, code: string) => {
|
||||
return authClient.callback(
|
||||
'http://localhost:4038/auth/signin/callback',
|
||||
{ code_verifier, code, session_state, expires_in: "1d" },
|
||||
{ code_verifier }
|
||||
);
|
||||
}
|
|
@ -1,9 +1,13 @@
|
|||
import fs from 'fs'
|
||||
import { parse as parseFile } from 'envfile'
|
||||
import { Issuer } from 'openid-client';
|
||||
|
||||
const keyCloakIssuer : Issuer = await Issuer.discover(process.env.KEYCLOAK_AUTH_SERVER_URL!);
|
||||
console.log('🔐 Connected to Keycloak');
|
||||
|
||||
type IconfigStore = 'development' | 'production'
|
||||
|
||||
interface IConfigKeys {
|
||||
export interface IConfigKeys {
|
||||
PORT: string | number
|
||||
NODE_ENV: string
|
||||
HASURA_GRAPHQL_ADMIN_SECRET: string
|
||||
|
@ -32,6 +36,13 @@ interface IConfigKeys {
|
|||
AWS_ACCESS_KEY_ID: string
|
||||
AWS_SECRET_ACCESS_KEY: string
|
||||
AWS_REGION: string
|
||||
PUBLIC_KEY: string
|
||||
PRIVATE_KEY: string
|
||||
KEYCLOAK_ISSUER: Issuer
|
||||
KEYCLOAK_CLIENT_ID: string
|
||||
KEYCLOAK_CLIENT_SECRET: string
|
||||
KEYCLOAK_REDIRECT_URI: string
|
||||
KEYCLOAK_AUTH_SERVER_URL: string
|
||||
}
|
||||
|
||||
export default class ConfigStoreFactory {
|
||||
|
@ -46,9 +57,14 @@ export default class ConfigStoreFactory {
|
|||
}
|
||||
|
||||
public async getConfigStore() {
|
||||
const publicKEY = fs.readFileSync('./jwtRS256.key', 'utf8');
|
||||
const privateKEY = fs.readFileSync('./jwtRS256.key.pub', 'utf8');
|
||||
if (this.configStoreType === 'development') {
|
||||
const envContent = await fs.readFileSync(`./.env`, 'utf8')
|
||||
const env: Partial<IConfigKeys> = await parseFile(envContent)
|
||||
env.PUBLIC_KEY = publicKEY
|
||||
env.PRIVATE_KEY = privateKEY
|
||||
env.KEYCLOAK_ISSUER = keyCloakIssuer
|
||||
return env
|
||||
} else {
|
||||
let reqEnvContent: any = await fs.readFileSync(
|
||||
|
@ -59,6 +75,9 @@ export default class ConfigStoreFactory {
|
|||
reqEnvContent = reqEnvContent.split('\n')
|
||||
let missingKeys: string[] = []
|
||||
let env: Partial<IConfigKeys> = {}
|
||||
env.PUBLIC_KEY = publicKEY
|
||||
env.PRIVATE_KEY = privateKEY
|
||||
env.KEYCLOAK_ISSUER = keyCloakIssuer
|
||||
for (const line of reqEnvContent) {
|
||||
if (!process.env[line]) {
|
||||
missingKeys.push(line)
|
||||
|
@ -67,6 +86,7 @@ export default class ConfigStoreFactory {
|
|||
if (missingKeys.length > 0) {
|
||||
throw new Error(`Missing keys: ${missingKeys}`)
|
||||
}
|
||||
|
||||
return env
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
import { Request, Response } from 'express'
|
||||
import { makeResponse } from '../libs'
|
||||
import check from '../auth/check'
|
||||
import verify from '../auth/verify'
|
||||
import { ModRequest } from '../types'
|
||||
|
||||
export default class AuthController {
|
||||
public signin = (req: Request, res: Response) => {
|
||||
const { authurl } = check()
|
||||
res.redirect(authurl)
|
||||
}
|
||||
|
||||
public callback = async (req: Request, res: Response) => {
|
||||
const { session_state, code } = req.query as { session_state: string, code: string}
|
||||
res.send(makeResponse(await verify(session_state, code)))
|
||||
}
|
||||
|
||||
public me = (req: ModRequest, res: Response) => {
|
||||
res.send(makeResponse(req.user))
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
actions: []
|
||||
custom_types:
|
||||
enums: []
|
||||
input_objects: []
|
||||
objects: []
|
||||
scalars: []
|
||||
enums: []
|
||||
input_objects: []
|
||||
objects: []
|
||||
scalars: []
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
{}
|
|
@ -1 +1,9 @@
|
|||
[]
|
||||
- name: default
|
||||
kind: postgres
|
||||
configuration:
|
||||
connection_info:
|
||||
database_url:
|
||||
from_env: PG_DATABASE_URL
|
||||
isolation_level: read-committed
|
||||
use_prepared_statements: false
|
||||
tables: "!include default/tables/tables.yaml"
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
[]
|
|
@ -0,0 +1 @@
|
|||
disabled_for_roles: []
|
|
@ -0,0 +1 @@
|
|||
[]
|
|
@ -0,0 +1 @@
|
|||
{}
|
|
@ -0,0 +1 @@
|
|||
[]
|
|
@ -0,0 +1,19 @@
|
|||
import { configKeys } from "..";
|
||||
|
||||
const { KEYCLOAK_ISSUER } = configKeys
|
||||
|
||||
const authConfig = {
|
||||
client_id: configKeys.KEYCLOAK_CLIENT_ID,
|
||||
'auth-server-url': configKeys.KEYCLOAK_AUTH_SERVER_URL,
|
||||
'ssl-required': 'all',
|
||||
resource: configKeys.KEYCLOAK_CLIENT_ID,
|
||||
credentials: { 'secret-jwt': { secret: configKeys.KEYCLOAK_CLIENT_SECRET } },
|
||||
'confidential-port': 0,
|
||||
redirect_uri: configKeys.KEYCLOAK_REDIRECT_URI,
|
||||
client_secret: configKeys.KEYCLOAK_CLIENT_SECRET,
|
||||
default_max_age: 3600000,
|
||||
}
|
||||
|
||||
const authClient = new KEYCLOAK_ISSUER.Client(authConfig)
|
||||
|
||||
export { authClient }
|
|
@ -43,7 +43,7 @@ export default class CacheClient {
|
|||
}
|
||||
|
||||
this._nodeClient = new NodeCache()
|
||||
console.log(`Caching Client initialized in '${env}' environment`)
|
||||
console.log(`🍞 Caching Client initialized in '${env}' environment`)
|
||||
}
|
||||
|
||||
static async set(key: string, value: any) {
|
||||
|
|
|
@ -5,45 +5,40 @@ import morgan from 'morgan'
|
|||
import helmet from 'helmet'
|
||||
|
||||
import { hgqlInit } from './helpers'
|
||||
import cacheClient from './helpers/cache.factory';
|
||||
import routes from './routes'
|
||||
import { errorHandler, notFoundHandler } from './libs'
|
||||
import pkg from './package.json' assert { type: 'json' }
|
||||
import configStore from './configs'
|
||||
import configStore, { IConfigKeys } from './configs';
|
||||
|
||||
export const app: express.Application = express()
|
||||
|
||||
hgqlInit()
|
||||
|
||||
console.log('🚀', '@b68/api', 'v' + pkg.version)
|
||||
|
||||
hgqlInit()
|
||||
cacheClient.init();
|
||||
|
||||
const isDev: boolean = process.env.NODE_ENV == 'production'
|
||||
console.log(isDev ? '🚀 Production Mode' : '🚀 Development Mode')
|
||||
console.log(isDev ? '🚀 Production Mode' : '👷 Development Mode')
|
||||
const configs = new configStore(isDev)
|
||||
const configKeys: any = await configs.getConfigStore()
|
||||
const configKeys: IConfigKeys = await configs.getConfigStore() as IConfigKeys
|
||||
|
||||
app.use(cors())
|
||||
app.use(helmet())
|
||||
app.use(morgan('dev'))
|
||||
app.use(express.json())
|
||||
app.use(express.urlencoded({ extended: true, limit: '50mb' }))
|
||||
app.set('view engine', 'ejs');
|
||||
|
||||
app.use('/health', (req, res) => {
|
||||
return res.status(200).json({
|
||||
app: pkg.name,
|
||||
request_ip: req.ip,
|
||||
uptime: process.uptime(),
|
||||
hrtime: process.hrtime(),
|
||||
})
|
||||
})
|
||||
console.log('☄ ', 'Base Route', '/')
|
||||
|
||||
console.log('☄', 'Base Route', '/')
|
||||
app.use('/', routes)
|
||||
|
||||
app.use(notFoundHandler)
|
||||
app.use(errorHandler)
|
||||
|
||||
app.listen(process.env.PORT, async () => {
|
||||
console.log(`\nServer running on port ${process.env.PORT}`)
|
||||
app.listen(configKeys.PORT, async () => {
|
||||
console.log(`\n🌈 Server running on port ${configKeys.PORT}`)
|
||||
})
|
||||
|
||||
export { configKeys }
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
"axios": "^1.2.1",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.0.3",
|
||||
"ejs": "^3.1.9",
|
||||
"envfile": "^6.18.0",
|
||||
"express": "^4.18.2",
|
||||
"form-data": "^4.0.0",
|
||||
|
@ -27,6 +28,7 @@
|
|||
"napi-nanoid": "^0.0.4",
|
||||
"node-cache": "^5.1.2",
|
||||
"nodemailer": "^6.8.0",
|
||||
"openid-client": "^5.4.0",
|
||||
"osu-api-extended": "^2.5.12",
|
||||
"redis": "^4.5.1",
|
||||
"typescript": "^4.9.3"
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 1.1 KiB |
|
@ -1,18 +1,18 @@
|
|||
import { Router } from 'express'
|
||||
import { makeResponse } from '../libs'
|
||||
import AuthController from '../controllers/auth.controller'
|
||||
import middleware from '../auth/middleware'
|
||||
|
||||
const router = Router()
|
||||
const authController = new AuthController()
|
||||
|
||||
router.get('/', (req, res) => {
|
||||
res.send(makeResponse({ message: 'Hello World auth!' }))
|
||||
})
|
||||
router.get('/signin', authController.signin)
|
||||
|
||||
router.all('/err', async (req, res, next) => {
|
||||
try {
|
||||
throw new Error('This is an error')
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
router.get('/signin/callback', authController.callback)
|
||||
|
||||
router.get('/me', middleware, authController.me as any)
|
||||
|
||||
router.get('/', function(req, res) {
|
||||
res.render('pages/auth');
|
||||
});
|
||||
|
||||
export default router
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
import { Router } from 'express'
|
||||
|
||||
const router = Router()
|
||||
|
||||
router.use('/health',
|
||||
(req, res) => {
|
||||
return res.status(200).json({
|
||||
status: "OK",
|
||||
app: "B68 API",
|
||||
request_ip: req.ip,
|
||||
uptime: process.uptime(),
|
||||
hrtime: process.hrtime(),
|
||||
})
|
||||
})
|
||||
|
||||
export default router
|
|
@ -30,7 +30,7 @@ const loadRoutes = async (dirPath: string, prefix = '/') => {
|
|||
''
|
||||
)
|
||||
const modRoute = path.join(prefix, route)
|
||||
console.log('🛰️', 'Loaded', modRoute)
|
||||
console.log('🛰️ ', 'Loaded', modRoute)
|
||||
|
||||
const mod = await import(path.join(baseDir, prefix + f.name))
|
||||
router.use(modRoute, mod.default)
|
||||
|
@ -48,4 +48,13 @@ let baseDir = path.dirname(__filename)
|
|||
baseDir = path.resolve(baseDir)
|
||||
|
||||
loadRoutes(baseDir)
|
||||
|
||||
router.get('/', function(req, res) {
|
||||
res.render('pages/index');
|
||||
});
|
||||
|
||||
router.get('/favicon.ico', function(req, res) {
|
||||
res.sendFile(path.join(__dirname, '../public', 'favicon.ico'));
|
||||
});
|
||||
|
||||
export default router
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import { Request } from 'express'
|
||||
|
||||
export interface PaginationType {
|
||||
page: number
|
||||
limit: number
|
||||
|
@ -5,3 +7,7 @@ export interface PaginationType {
|
|||
sort_by?: string
|
||||
filters?: { [k: string]: any }
|
||||
}
|
||||
|
||||
export interface ModRequest extends Request {
|
||||
user: any
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<%- include('../partials/head'); %>
|
||||
</head>
|
||||
<body class="container">
|
||||
|
||||
<header>
|
||||
<%- include('../partials/header'); %>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<div class="jumbotron">
|
||||
<a href="/auth/signin">
|
||||
<button type="button" class="btn btn-primary" data-toggle="modal" data-target="#exampleModal">
|
||||
Sign In
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<%- include('../partials/footer'); %>
|
||||
</footer>
|
||||
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,24 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<%- include('../partials/head'); %>
|
||||
</head>
|
||||
<body class="container">
|
||||
|
||||
<header>
|
||||
<%- include('../partials/header'); %>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<div class="jumbotron">
|
||||
<h1>API Landing</h1>
|
||||
<p>Welcome to B68 API Home</p>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<%- include('../partials/footer'); %>
|
||||
</footer>
|
||||
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1 @@
|
|||
<p class="text-center text-muted">© Copyright 2023 <a href="https://itsmebravo.dev">Jyotirmoy Bandyopadhayaya</a> | <a href="https://github.com/bravo68web">Github</a></p>
|
|
@ -0,0 +1,15 @@
|
|||
<meta charset="UTF-8">
|
||||
<title>B68 API</title>
|
||||
|
||||
<!-- CSS (load bootstrap from a CDN) -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.5.2/css/bootstrap.min.css">
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<style>
|
||||
body { padding-top:50px; }
|
||||
.jumbotron {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,8 @@
|
|||
<nav class="navbar navbar-expand-lg navbar-light bg-light">
|
||||
<a class="navbar-brand" href="/">B68 API</a>
|
||||
<ul class="navbar-nav mr-auto">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/auth">Auth</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
32
yarn.lock
32
yarn.lock
|
@ -3754,6 +3754,13 @@ ejs@^3.1.7:
|
|||
dependencies:
|
||||
jake "^10.8.5"
|
||||
|
||||
ejs@^3.1.9:
|
||||
version "3.1.9"
|
||||
resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.9.tgz#03c9e8777fe12686a9effcef22303ca3d8eeb361"
|
||||
integrity sha512-rC+QVNMJWv+MtPgkt0y+0rVEIdbtxVADApW9JXrUVlzHetgcyczP/E7DJmWJ4fJCZF2cPcBk0laWO9ZHMG3DmQ==
|
||||
dependencies:
|
||||
jake "^10.8.5"
|
||||
|
||||
emoji-regex@^8.0.0:
|
||||
version "8.0.0"
|
||||
resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz"
|
||||
|
@ -5332,6 +5339,11 @@ joi@^17.7.0:
|
|||
"@sideway/formula" "^3.0.0"
|
||||
"@sideway/pinpoint" "^2.0.0"
|
||||
|
||||
jose@^4.10.0:
|
||||
version "4.14.0"
|
||||
resolved "https://registry.yarnpkg.com/jose/-/jose-4.14.0.tgz#c8c03579a0ba3598194c92ccca777d96adca8f48"
|
||||
integrity sha512-LSA/XenLPwqk6e2L+PSUNuuY9G4NGsvjRWz6sJcUBmzTLEPJqQh46FHSUxnAQ64AWOkRO6bSXpy3yXuEKZkbIA==
|
||||
|
||||
js-sdsl@^4.1.4:
|
||||
version "4.2.0"
|
||||
resolved "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.2.0.tgz"
|
||||
|
@ -6464,6 +6476,11 @@ object-assign@^4, object-assign@^4.1.1:
|
|||
resolved "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz"
|
||||
integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==
|
||||
|
||||
object-hash@^2.0.1:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-2.2.0.tgz#5ad518581eefc443bd763472b8ff2e9c2c0d54a5"
|
||||
integrity sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==
|
||||
|
||||
object-inspect@^1.12.2, object-inspect@^1.9.0:
|
||||
version "1.12.2"
|
||||
resolved "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz"
|
||||
|
@ -6519,6 +6536,11 @@ object.values@^1.1.5, object.values@^1.1.6:
|
|||
define-properties "^1.1.4"
|
||||
es-abstract "^1.20.4"
|
||||
|
||||
oidc-token-hash@^5.0.1:
|
||||
version "5.0.2"
|
||||
resolved "https://registry.yarnpkg.com/oidc-token-hash/-/oidc-token-hash-5.0.2.tgz#f9ca7f7f1f92d721a2973e66b7430cb52a486648"
|
||||
integrity sha512-U91Ba78GtVBxcExLI7U+hC2AwJQqXQEW/D3fjmJC4hhSVIgdl954KO4Gu95WqAlgDKJdLATxkmuxraWLT0fVRQ==
|
||||
|
||||
on-finished@2.4.1:
|
||||
version "2.4.1"
|
||||
resolved "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz"
|
||||
|
@ -6568,6 +6590,16 @@ open@^8.4.0:
|
|||
is-docker "^2.1.1"
|
||||
is-wsl "^2.2.0"
|
||||
|
||||
openid-client@^5.4.0:
|
||||
version "5.4.0"
|
||||
resolved "https://registry.yarnpkg.com/openid-client/-/openid-client-5.4.0.tgz#77f1cda14e2911446f16ea3f455fc7c405103eac"
|
||||
integrity sha512-hgJa2aQKcM2hn3eyVtN12tEA45ECjTJPXCgUh5YzTzy9qwapCvmDTVPWOcWVL0d34zeQoQ/hbG9lJhl3AYxJlQ==
|
||||
dependencies:
|
||||
jose "^4.10.0"
|
||||
lru-cache "^6.0.0"
|
||||
object-hash "^2.0.1"
|
||||
oidc-token-hash "^5.0.1"
|
||||
|
||||
optionator@^0.9.1:
|
||||
version "0.9.1"
|
||||
resolved "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz"
|
||||
|
|
Loading…
Reference in New Issue