Skip to content

Commit 5cfb223

Browse files
committed
Initial implementation for Red Hat SSO authentication provider
Signed-off-by: Denis Golovin [email protected]
1 parent 04b53e6 commit 5cfb223

15 files changed

+1262
-0
lines changed

.gitignore

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
.electron-vendors.cache.json
2+
builtin
3+
dist
4+
node_modules
5+
redhat-authentication.cdix
6+

.nvmrc

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
16

package.json

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
{
2+
"name": "redhat-authentication",
3+
"displayName": "Red Hat Authentication",
4+
"description": "Login to Red Hat Developers",
5+
"version": "0.0.1",
6+
"icon": "icon.png",
7+
"publisher": "dgolvoin",
8+
"license": "Apache-2.0",
9+
"engines": {
10+
"podman-desktop": "^0.0.1"
11+
},
12+
"main": "./dist/extension.js",
13+
"contributes": {
14+
"commands": [
15+
{
16+
"command": "redhat.auth.login",
17+
"title": "Red Hat Authentication: login"
18+
}
19+
]
20+
},
21+
"scripts": {
22+
"build": "rollup --bundleConfigAsCjs --config rollup.config.js --compact --environment BUILD:production && node ./scripts/build.js",
23+
"watch": "rollup --bundleConfigAsCjs --config rollup.config.js -w"
24+
},
25+
"dependencies": {
26+
"@podman-desktop/api": "^0.0.1",
27+
"js-yaml": "^4.1.0",
28+
"openid-client": "5.4.0"
29+
},
30+
"devDependencies": {
31+
"7zip-min": "^1.4.3",
32+
"@types/js-yaml": "^4.0.5",
33+
"mkdirp": "^2.1.3",
34+
"zip-local": "^0.3.5"
35+
}
36+
}

rollup.config.js

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// rollup.config.js
2+
import typescript from '@rollup/plugin-typescript';
3+
import commonjs from '@rollup/plugin-commonjs';
4+
import json from '@rollup/plugin-json';
5+
import { nodeResolve } from '@rollup/plugin-node-resolve';
6+
7+
export default {
8+
input: 'src/extension.ts',
9+
output: {
10+
dir: 'dist',
11+
format: 'cjs',
12+
sourcemap: true,
13+
},
14+
external: [
15+
'@podman-desktop/api',
16+
'electron',
17+
'node:stream',
18+
'node:http',
19+
'node:url',
20+
'node:process',
21+
'node:tls',
22+
'node:util',
23+
'node:buffer',
24+
'node:https',
25+
'node:events',
26+
'node:net',
27+
'node:process',
28+
'node:path',
29+
'node:os',
30+
'node:fs',
31+
'node:child_process',
32+
],
33+
plugins: [
34+
typescript(),
35+
commonjs({ extensions: ['.js', '.ts'] }), // the ".ts" extension is required],
36+
json(),
37+
nodeResolve({preferBuiltins: true}),
38+
],
39+
};

scripts/build.js

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
#!/usr/bin/env node
2+
/**********************************************************************
3+
* Copyright (C) 2022 Red Hat, Inc.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*
17+
* SPDX-License-Identifier: Apache-2.0
18+
***********************************************************************/
19+
20+
const zipper = require('zip-local');
21+
const path = require('path');
22+
const package = require('../package.json');
23+
const { mkdirp } = require('mkdirp');
24+
const fs = require('fs');
25+
26+
const destFile = path.resolve(__dirname, `../${package.name}.cdix`);
27+
const builtinDirectory = path.resolve(__dirname, '../builtin');
28+
const unzippedDirectory = path.resolve(builtinDirectory, `${package.name}.cdix`);
29+
// remove the .cdix file before zipping
30+
if (fs.existsSync(destFile)) {
31+
fs.rmSync(destFile);
32+
}
33+
// remove the builtin folder before zipping
34+
if (fs.existsSync(builtinDirectory)) {
35+
fs.rmSync(builtinDirectory, { recursive: true, force: true });
36+
}
37+
38+
zipper.sync.zip(path.resolve(__dirname, '../')).compress().save(destFile);
39+
40+
// create unzipped built-in
41+
mkdirp(unzippedDirectory).then(() => {
42+
zipper.sync.unzip(destFile).save(unzippedDirectory);
43+
});

src/authentication-server.ts

+127
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
/**********************************************************************
2+
* Copyright (C) 2022 - 2023 Red Hat, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
* SPDX-License-Identifier: Apache-2.0
17+
***********************************************************************/
18+
19+
import * as fs from 'fs';
20+
import * as http from 'http';
21+
import * as path from 'path';
22+
import * as url from 'url';
23+
import { ServerConfig, AuthConfig } from './configuration';
24+
25+
interface Deferred<T> {
26+
resolve: (result: T | Promise<T>) => void;
27+
reject: (reason: any) => void;
28+
}
29+
30+
export function createServer(config: AuthConfig, nonce: string) {
31+
type RedirectResult =
32+
| { req: http.IncomingMessage; res: http.ServerResponse }
33+
| { err: any; res: http.ServerResponse };
34+
let deferredRedirect: Deferred<RedirectResult>;
35+
const redirectPromise = new Promise<RedirectResult>((resolve, reject) => (deferredRedirect = { resolve, reject }));
36+
37+
let deferredCallback: Deferred<RedirectResult>;
38+
const callbackPromise = new Promise<RedirectResult>((resolve, reject) => (deferredCallback = { resolve, reject }));
39+
40+
const server = http.createServer(function (req, res) {
41+
const reqUrl = url.parse(req.url!, /* parseQueryString */ true);
42+
console.log(`Received ${reqUrl.pathname}`);
43+
switch (reqUrl.pathname) {
44+
case '/signin':
45+
// eslint-disable-next-line no-case-declarations
46+
const receivedNonce = ((reqUrl.query.nonce as string) || '').replace(/ /g, '+');
47+
if (receivedNonce === nonce) {
48+
deferredRedirect.resolve({ req, res });
49+
} else {
50+
const err = new Error('Nonce does not match.');
51+
deferredRedirect.resolve({ err, res });
52+
}
53+
break;
54+
case '/':
55+
sendFile(res, path.join(__dirname, '../www/success.html'), 'text/html; charset=utf-8');
56+
break;
57+
case '/auth.css':
58+
sendFile(res, path.join(__dirname, '../www/auth.css'), 'text/css; charset=utf-8');
59+
break;
60+
case '/favicon.ico':
61+
sendFile(res, path.join(__dirname, '../www/favicon.ico'), 'image/vnd.microsoft.icon');
62+
break;
63+
case `/${config.serverConfig.callbackPath}`:
64+
deferredCallback.resolve({ req, res });
65+
break;
66+
default:
67+
res.writeHead(404);
68+
res.end();
69+
break;
70+
}
71+
});
72+
return { server, redirectPromise, callbackPromise };
73+
}
74+
75+
export async function startServer(config: ServerConfig, server: http.Server): Promise<string> {
76+
let portTimer: NodeJS.Timer;
77+
78+
function cancelPortTimer() {
79+
clearTimeout(portTimer);
80+
}
81+
82+
const port = new Promise<string>((resolve, reject) => {
83+
portTimer = setTimeout(() => {
84+
reject(new Error('Timeout waiting for port'));
85+
}, 5000);
86+
87+
server.on('listening', () => {
88+
const address = server.address();
89+
if (typeof address === 'undefined' || address === null) {
90+
reject(new Error('adress is null or undefined'));
91+
} else if (typeof address === 'string') {
92+
resolve(address);
93+
} else {
94+
resolve(address.port.toString());
95+
}
96+
});
97+
98+
server.on('error', _ => {
99+
reject(new Error('Error listening to server'));
100+
});
101+
102+
server.on('close', () => {
103+
reject(new Error('Closed'));
104+
});
105+
106+
server.listen(config.port);
107+
});
108+
109+
port.then(cancelPortTimer, cancelPortTimer);
110+
return port;
111+
}
112+
113+
function sendFile(res: http.ServerResponse, filepath: string, contentType: string) {
114+
fs.readFile(filepath, (err, body) => {
115+
if (err) {
116+
console.error(err);
117+
res.writeHead(404);
118+
res.end();
119+
} else {
120+
res.writeHead(200, {
121+
'Content-Length': body.length,
122+
'Content-Type': contentType,
123+
});
124+
res.end(body);
125+
}
126+
});
127+
}

0 commit comments

Comments
 (0)