feat: demo application
This commit is contained in:
@@ -1,3 +1,15 @@
|
|||||||
# auth
|
# Demo app
|
||||||
|
Create a `.env` file containing
|
||||||
|
```env
|
||||||
|
OIDC_CLIENT_ID=<CLIENT ID>
|
||||||
|
OIDC_CLIENT_SECRET=<CLIENT SECRET>
|
||||||
|
OIDC_ISSUER=https://auth.example.org
|
||||||
|
SIGNING_SECRET=<LONG RANDOM STRING>
|
||||||
|
APP_BASE_URL=https://app.example.org
|
||||||
|
```
|
||||||
|
|
||||||
Wrapper on top of `openid-client`, with integration into `express.js`
|
You can then run the app using
|
||||||
|
```bash
|
||||||
|
tsc && node --env-file=.env dist/index.js
|
||||||
|
```
|
||||||
|
The app should be served via HTTPS on a reverse proxy.
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
tsc --declaration
|
|
||||||
cp ./package.json ./dist
|
|
||||||
cp ./README.md ./dist
|
|
||||||
mkdir dist/public || true
|
|
||||||
cp ./public/auto-redirect.html ./dist/public/auto-redirect.html
|
|
||||||
cp ./public/error-page.html ./dist/public/error-page.html
|
|
||||||
if [[ -z $1 ]]; then
|
|
||||||
rm dist/testing.js
|
|
||||||
rm dist/testing.d.ts
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Build complete"
|
|
||||||
Generated
+1389
-278
File diff suppressed because it is too large
Load Diff
+5
-10
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@janishutz/oidc-login-sdk",
|
"name": "oidc-express-demo",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "Simple, limited wrapper on top of passport for oidc authentication, used by my FOSS projects",
|
"description": "Simple example of OIDC using express",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"oidc",
|
"oidc",
|
||||||
"auth"
|
"auth"
|
||||||
@@ -18,21 +18,16 @@
|
|||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@types/cors": "^2.8.19",
|
||||||
|
"cors": "^2.8.6",
|
||||||
"express": "^5.2.1",
|
"express": "^5.2.1",
|
||||||
"express-session": "^1.19.0",
|
"express-openid-connect": "^3.4.0"
|
||||||
"openid-client": "^6.8.8",
|
|
||||||
"passport": "^0.7.0",
|
|
||||||
"passport-openidconnect": "^0.1.2"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^10.0.1",
|
"@eslint/js": "^10.0.1",
|
||||||
"@stylistic/eslint-plugin": "^5.10.0",
|
"@stylistic/eslint-plugin": "^5.10.0",
|
||||||
"@types/express": "^5.0.6",
|
"@types/express": "^5.0.6",
|
||||||
"@types/express-session": "^1.19.0",
|
|
||||||
"@types/node": "^22.20.2",
|
"@types/node": "^22.20.2",
|
||||||
"@types/openid-client": "^3.1.6",
|
|
||||||
"@types/passport": "^1.0.17",
|
|
||||||
"@types/passport-openidconnect": "^0.1.3",
|
|
||||||
"eslint": "^10.10.0",
|
"eslint": "^10.10.0",
|
||||||
"eslint-plugin-vue": "^10.11.0",
|
"eslint-plugin-vue": "^10.11.0",
|
||||||
"globals": "^17.12.0",
|
"globals": "^17.12.0",
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
||||||
<title>Login - janishutz.com Account SDK</title>
|
|
||||||
<style>
|
|
||||||
html, body {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: sans-serif;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
flex-direction: column;
|
|
||||||
color: white;
|
|
||||||
background-color: #202020;
|
|
||||||
font-size: 200%;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<h1>Redirecting to login</h1>
|
|
||||||
<p>Please be patient...</p>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
location.href = '[[ redirect ]]'
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
||||||
<title>Login - janishutz.com Account SDK</title>
|
|
||||||
<style>
|
|
||||||
html,
|
|
||||||
body {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: sans-serif;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
flex-direction: column;
|
|
||||||
color: white;
|
|
||||||
background-color: #202020;
|
|
||||||
font-size: 200%;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<h1>Failed to log in</h1>
|
|
||||||
<p>Click <a href="/auth/v2/login">here</a> to try again</p>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
-15
@@ -1,15 +0,0 @@
|
|||||||
import express from 'express';
|
|
||||||
|
|
||||||
export const ensureAuth = ( redirectOnError?: string ) => {
|
|
||||||
return ( request: express.Request, response: express.Response, next: express.NextFunction ) => {
|
|
||||||
if ( request.isAuthenticated() ) {
|
|
||||||
next();
|
|
||||||
} else {
|
|
||||||
if ( redirectOnError ) {
|
|
||||||
response.redirect( redirectOnError );
|
|
||||||
} else {
|
|
||||||
response.sendStatus( 401 );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
+64
@@ -0,0 +1,64 @@
|
|||||||
|
import {
|
||||||
|
CorsOptions
|
||||||
|
} from 'cors';
|
||||||
|
import cors from 'cors';
|
||||||
|
|
||||||
|
let corsWhitelist: string[] = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the URLs that are allowed to send CORS requests
|
||||||
|
* @param whitelist - An array of URLs that are allowed to send CORS requests
|
||||||
|
*/
|
||||||
|
const setWhitelist = ( whitelist: string[] ) => {
|
||||||
|
corsWhitelist = whitelist;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Express.js middleware for cors verification
|
||||||
|
* @param rest - Whether or not to allow REST requests or not (default = false)
|
||||||
|
*/
|
||||||
|
const middleware = ( rest: boolean = false ) => {
|
||||||
|
return cors( generateOpts( rest ) );
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate the options for the CORS library
|
||||||
|
* @param rest - Whether to allow REST requests or not (default = false)
|
||||||
|
* @returns CORS options object that can be used for the cors library
|
||||||
|
*/
|
||||||
|
const generateOpts = ( rest: boolean = false ): CorsOptions => {
|
||||||
|
const corsOpts: CorsOptions = {
|
||||||
|
'credentials': true,
|
||||||
|
'optionsSuccessStatus': 200,
|
||||||
|
'origin': ( origin, cb ) => {
|
||||||
|
const status = blockREST( origin! );
|
||||||
|
|
||||||
|
cb( null, status );
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if ( rest ) {
|
||||||
|
corsOpts[ 'origin' ] = ( origin, cb ) => {
|
||||||
|
const status = allowREST( origin! );
|
||||||
|
|
||||||
|
cb( null, status );
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return corsOpts;
|
||||||
|
};
|
||||||
|
|
||||||
|
const blockREST = ( origin: string ): boolean => {
|
||||||
|
return corsWhitelist.includes( origin );
|
||||||
|
};
|
||||||
|
|
||||||
|
const allowREST = ( origin: string ): boolean => {
|
||||||
|
return blockREST( origin ) || !origin;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export default {
|
||||||
|
generateOpts,
|
||||||
|
setWhitelist,
|
||||||
|
middleware
|
||||||
|
};
|
||||||
Vendored
-36
@@ -1,36 +0,0 @@
|
|||||||
declare global {
|
|
||||||
namespace Express {
|
|
||||||
interface User {
|
|
||||||
'id': string;
|
|
||||||
'username'?: string;
|
|
||||||
'displayName'?: string;
|
|
||||||
'emails'?: {
|
|
||||||
'value': string,
|
|
||||||
'type'?: string
|
|
||||||
}[];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface OIDCConfig {
|
|
||||||
'clientID': string;
|
|
||||||
'clientSecret': string;
|
|
||||||
'issuer': URL;
|
|
||||||
'scopes'?: ( 'email' | 'profile' )[],
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AppConfig {
|
|
||||||
'failRedirect'?: string;
|
|
||||||
'successRedirect'?: string;
|
|
||||||
'logoutRedirect'?: string;
|
|
||||||
'url': URL;
|
|
||||||
'sessionSecret': string;
|
|
||||||
'cookieName'?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Config {
|
|
||||||
'oidc': OIDCConfig;
|
|
||||||
'prod': boolean;
|
|
||||||
'app': AppConfig
|
|
||||||
}
|
|
||||||
|
|
||||||
+21
-6
@@ -1,12 +1,27 @@
|
|||||||
|
import connect from 'express-openid-connect';
|
||||||
import express from 'express';
|
import express from 'express';
|
||||||
|
|
||||||
export * from './routes.js';
|
const app = express();
|
||||||
|
|
||||||
export * from './auth.js';
|
app.use( connect.auth( {
|
||||||
|
'authRequired': false,
|
||||||
|
'issuerBaseURL': process.env.OIDC_ISSUER,
|
||||||
|
'clientID': process.env.OIDC_CLIENT_ID,
|
||||||
|
'clientSecret': process.env.OIDC_CLIENT_SECRET,
|
||||||
|
'secret': process.env.SIGNING_SECRET,
|
||||||
|
'baseURL': process.env.APP_BASE_URL,
|
||||||
|
'authorizationParams': {
|
||||||
|
'scope': 'openid profile email',
|
||||||
|
'response_type': 'code'
|
||||||
|
}
|
||||||
|
} ) );
|
||||||
|
|
||||||
export type * from './dtype.d.ts';
|
app.get( '/', ( _request, response ) => {
|
||||||
|
response.send( 'Hello World' );
|
||||||
|
} );
|
||||||
|
|
||||||
|
app.get( '/account', connect.requiresAuth(), ( request, response ) => {
|
||||||
|
response.send( 'Account' );
|
||||||
|
} );
|
||||||
|
|
||||||
export const getUserId = ( request: express.Request ): string | undefined => {
|
app.listen( 8080 );
|
||||||
return request.user?.id;
|
|
||||||
};
|
|
||||||
|
|||||||
-103
@@ -1,103 +0,0 @@
|
|||||||
import {
|
|
||||||
Config
|
|
||||||
} from './dtype.js';
|
|
||||||
import OIDCStrategy from 'passport-openidconnect';
|
|
||||||
import {
|
|
||||||
discovery
|
|
||||||
} from 'openid-client';
|
|
||||||
import {
|
|
||||||
ensureAuth
|
|
||||||
} from './auth.js';
|
|
||||||
import express from 'express';
|
|
||||||
import passport from 'passport';
|
|
||||||
import session from 'express-session';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add the necessary routes for login
|
|
||||||
* @param app - Express application to register routes to
|
|
||||||
* @param config - Configuration for the Auth system
|
|
||||||
* @param verify - A verification function. Should create user if user doesn't exist
|
|
||||||
* @param getUser - Function to get a user by user ID. Optional, if not provided, only UID will be recovered (should be good enough in most cases)
|
|
||||||
*/
|
|
||||||
export const addRoutes = async (
|
|
||||||
app: express.Application,
|
|
||||||
config: Config,
|
|
||||||
verify: ( profile: OIDCStrategy.Profile ) => Promise<boolean>,
|
|
||||||
getUser?: ( id: string ) => Promise<Express.User>
|
|
||||||
) => {
|
|
||||||
app.use( session( {
|
|
||||||
'secret': config.app.sessionSecret,
|
|
||||||
'cookie': {
|
|
||||||
'httpOnly': true,
|
|
||||||
'secure': config.prod
|
|
||||||
},
|
|
||||||
'resave': false,
|
|
||||||
'saveUninitialized': false
|
|
||||||
} ) );
|
|
||||||
app.use( passport.session() );
|
|
||||||
app.use( passport.initialize() );
|
|
||||||
|
|
||||||
passport.serializeUser( ( user, cb ) => {
|
|
||||||
cb( null, user.id );
|
|
||||||
} );
|
|
||||||
|
|
||||||
if ( getUser )
|
|
||||||
passport.deserializeUser<string>( async ( user, cb ) => {
|
|
||||||
return cb( null, await getUser( user ) );
|
|
||||||
} );
|
|
||||||
else
|
|
||||||
passport.deserializeUser<string>( ( user, cb ) => {
|
|
||||||
return cb( null, {
|
|
||||||
'id': user
|
|
||||||
} );
|
|
||||||
} );
|
|
||||||
|
|
||||||
const oidcServerConfig = ( await discovery( config.oidc.issuer, config.oidc.clientID, config.oidc.clientSecret ) ).serverMetadata();
|
|
||||||
const iss = config.oidc.issuer.toString();
|
|
||||||
|
|
||||||
passport.use( new OIDCStrategy(
|
|
||||||
{
|
|
||||||
'clientID': config.oidc.clientID,
|
|
||||||
'clientSecret': config.oidc.clientSecret,
|
|
||||||
'callbackURL': config.app.url.toString() + 'auth/v2/verify',
|
|
||||||
'authorizationURL': oidcServerConfig.authorization_endpoint ?? oidcServerConfig.issuer + '/auth',
|
|
||||||
'issuer': iss,
|
|
||||||
'userInfoURL': oidcServerConfig.userinfo_endpoint ?? oidcServerConfig.issuer + '/me',
|
|
||||||
'tokenURL': oidcServerConfig.token_endpoint ?? oidcServerConfig.issuer + '/token',
|
|
||||||
'scope': config.oidc.scopes
|
|
||||||
},
|
|
||||||
async ( issuer: string, profile: OIDCStrategy.Profile, cb: OIDCStrategy.VerifyCallback ) => {
|
|
||||||
if ( issuer !== iss ) cb( new Error( 'ERR_FORBIDDEN' ) );
|
|
||||||
|
|
||||||
cb( null, profile, await verify( profile ) );
|
|
||||||
}
|
|
||||||
) );
|
|
||||||
|
|
||||||
// TODO: Allow CORS here
|
|
||||||
app.get( '/auth/v2/login', passport.authenticate( 'openidconnect' ) );
|
|
||||||
|
|
||||||
app.get( '/auth/v2/fail', ( _request, response ) => {
|
|
||||||
response.sendFile( './public/error-page.html' );
|
|
||||||
} );
|
|
||||||
|
|
||||||
app.get( '/auth/v2/verify', passport.authenticate( 'openidconnect', {
|
|
||||||
'failureRedirect': config.app.failRedirect ?? '/auth/v2/fail',
|
|
||||||
'failureMessage': true
|
|
||||||
} ), ( request: express.Request, response: express.Response ) => {
|
|
||||||
request.session.save();
|
|
||||||
response.redirect( config.app.successRedirect ?? '/' );
|
|
||||||
} );
|
|
||||||
|
|
||||||
// TODO: Allow CORS here
|
|
||||||
app.get( '/auth/v2/logout', ensureAuth( config.app.failRedirect ), ( request: express.Request, response: express.Response ) => {
|
|
||||||
request.logout( {
|
|
||||||
'keepSessionInfo': false
|
|
||||||
}, err => {
|
|
||||||
console.error( err );
|
|
||||||
} );
|
|
||||||
|
|
||||||
// Ensure session is fully destroyed
|
|
||||||
request.session.destroy( () => {} );
|
|
||||||
response.redirect( config.app.logoutRedirect ?? '/' );
|
|
||||||
} );
|
|
||||||
};
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
import {
|
|
||||||
addRoutes,
|
|
||||||
ensureAuth
|
|
||||||
} from './index.js';
|
|
||||||
import express from 'express';
|
|
||||||
|
|
||||||
const app = express();
|
|
||||||
|
|
||||||
addRoutes( app, {
|
|
||||||
'oidc': {
|
|
||||||
'clientID': process.env.CLIENT_ID ?? '',
|
|
||||||
'clientSecret': process.env.CLIENT_SECRET ?? '',
|
|
||||||
'issuer': new URL( 'https://home.janishutz.com/oidc' )
|
|
||||||
},
|
|
||||||
'app': {
|
|
||||||
'sessionSecret': 'secret',
|
|
||||||
'url': new URL( 'https://home2.janishutz.com' ),
|
|
||||||
'successRedirect': '/account'
|
|
||||||
},
|
|
||||||
'prod': false
|
|
||||||
}, async uid => {
|
|
||||||
console.log( uid );
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} );
|
|
||||||
|
|
||||||
app.get( '/', ( _request, response ) => {
|
|
||||||
response.send( 'Hello World' );
|
|
||||||
} );
|
|
||||||
|
|
||||||
app.get( '/account', ensureAuth( '/' ), ( _request, response ) => {
|
|
||||||
response.send( 'Account' );
|
|
||||||
} );
|
|
||||||
|
|
||||||
app.listen( 8080 );
|
|
||||||
Reference in New Issue
Block a user