feat: initial OIDC support
This commit is contained in:
@@ -3,10 +3,8 @@ import jwt from 'jsonwebtoken';
|
||||
|
||||
import { statementSchema } from './access.schemas.ts';
|
||||
import type { AccessProvider } from './access.provider.ts';
|
||||
|
||||
type AccessTokensOptions = {
|
||||
secret: string | Buffer;
|
||||
};
|
||||
import type { Services } from '#root/utils/services.ts';
|
||||
import { Config } from '#root/config/config.ts';
|
||||
|
||||
const tokenBodySchema = z.object({
|
||||
statements: z.array(statementSchema),
|
||||
@@ -15,21 +13,29 @@ const tokenBodySchema = z.object({
|
||||
type TokenBody = z.infer<typeof tokenBodySchema>;
|
||||
|
||||
class AccessTokens implements AccessProvider {
|
||||
#options: AccessTokensOptions;
|
||||
#services: Services;
|
||||
|
||||
constructor(options: AccessTokensOptions) {
|
||||
this.#options = options;
|
||||
constructor(services: Services) {
|
||||
this.#services = services;
|
||||
}
|
||||
|
||||
public generate = (options: TokenBody) => {
|
||||
const { secret } = this.#options;
|
||||
const token = jwt.sign(options, secret);
|
||||
const config = this.#services.get(Config);
|
||||
const { tokenSecret } = config;
|
||||
if (!tokenSecret) {
|
||||
throw new Error('Token secret does not exist');
|
||||
}
|
||||
const token = jwt.sign(options, tokenSecret);
|
||||
return token;
|
||||
};
|
||||
|
||||
public getAccess = async (token: string) => {
|
||||
const { secret } = this.#options;
|
||||
const data = jwt.verify(token, secret);
|
||||
const config = this.#services.get(Config);
|
||||
const { tokenSecret } = config;
|
||||
if (!tokenSecret) {
|
||||
throw new Error('Token secret does not exist');
|
||||
}
|
||||
const data = jwt.verify(token, tokenSecret);
|
||||
const parsed = tokenBodySchema.parse(data);
|
||||
return parsed;
|
||||
};
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { AccessHandler } from './access/access.handler.ts';
|
||||
import { AccessTokens } from './access/access.token.ts';
|
||||
import { Config } from './config/config.ts';
|
||||
import { K8sService } from './k8s/k8s.ts';
|
||||
import { OidcHandler } from './oidc/oidc.handler.ts';
|
||||
import { MqttServer } from './server/server.ts';
|
||||
import { TopicsHandler } from './topics/topics.handler.ts';
|
||||
import { Services } from './utils/services.ts';
|
||||
@@ -38,7 +40,8 @@ class Backbone {
|
||||
|
||||
public start = async () => {
|
||||
if (this.config.k8s.enabled) {
|
||||
await this.setupK8sOperator();
|
||||
await this.k8s.setup();
|
||||
this.accessHandler.register('k8s', this.k8s.clients);
|
||||
}
|
||||
if (this.config.http.enabled) {
|
||||
console.log('starting http');
|
||||
@@ -49,11 +52,12 @@ class Backbone {
|
||||
const tcp = this.server.getTcpServer();
|
||||
tcp.listen(this.config.tcp.port);
|
||||
}
|
||||
};
|
||||
|
||||
public setupK8sOperator = async () => {
|
||||
await this.k8s.setup();
|
||||
this.accessHandler.register('k8s', this.k8s.clients);
|
||||
if (this.config.oidc.enabled) {
|
||||
this.accessHandler.register('oidc', this.#services.get(OidcHandler));
|
||||
}
|
||||
if (this.config.tokenSecret) {
|
||||
this.accessHandler.register('token', this.#services.get(AccessTokens));
|
||||
}
|
||||
};
|
||||
|
||||
public destroy = async () => {
|
||||
|
||||
@@ -3,6 +3,29 @@ class Config {
|
||||
return process.env.TOKEN_SECRET;
|
||||
}
|
||||
|
||||
public get oidc() {
|
||||
const enabled = process.env.OIDC_ENABLED === 'true';
|
||||
const discoveryUrl = process.env.OIDC_DISCOVERY_URL;
|
||||
const clientId = process.env.OIDC_CLIENT_ID;
|
||||
const clientSecret = process.env.OIDC_CLIENT_SECRET;
|
||||
const groupField = process.env.OIDC_GROUP_FIELD || 'groups';
|
||||
const adminGroup = process.env.OIDC_ADMIN_GROUP;
|
||||
const writerGroup = process.env.OIDC_WRITER_GROUP;
|
||||
const readerGroup = process.env.OIDC_READER_GROUP;
|
||||
return {
|
||||
enabled,
|
||||
discoveryUrl,
|
||||
clientId,
|
||||
clientSecret,
|
||||
groupField,
|
||||
groups: {
|
||||
admin: adminGroup,
|
||||
writer: writerGroup,
|
||||
reader: readerGroup,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public get k8s() {
|
||||
const enabled = process.env.K8S_ENABLED === 'true';
|
||||
return {
|
||||
|
||||
66
src/oidc/oidc.handler.ts
Normal file
66
src/oidc/oidc.handler.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import jwt from 'jsonwebtoken';
|
||||
|
||||
import type { AccessProvider } from '#root/access/access.provider.ts';
|
||||
import type { Services } from '#root/utils/services.ts';
|
||||
import type { Statement } from '#root/access/access.schemas.ts';
|
||||
import { Config } from '#root/config/config.ts';
|
||||
|
||||
const adminStatements: Statement[] = [
|
||||
{
|
||||
effect: 'allow',
|
||||
resources: ['**'],
|
||||
actions: ['**'],
|
||||
},
|
||||
];
|
||||
const writerStatements: Statement[] = [
|
||||
{
|
||||
effect: 'allow',
|
||||
resources: ['**'],
|
||||
actions: ['mqtt:**'],
|
||||
},
|
||||
];
|
||||
const readerStatements: Statement[] = [
|
||||
{
|
||||
effect: 'allow',
|
||||
resources: ['**'],
|
||||
actions: ['mqtt:read', 'mqtt:subscribe'],
|
||||
},
|
||||
];
|
||||
|
||||
class OidcHandler implements AccessProvider {
|
||||
#services: Services;
|
||||
|
||||
constructor(services: Services) {
|
||||
this.#services = services;
|
||||
}
|
||||
|
||||
public getAccess = async (token: string) => {
|
||||
const data = jwt.decode(token);
|
||||
const config = this.#services.get(Config);
|
||||
if (!data || typeof data !== 'object') {
|
||||
throw new Error('JWT body malformed');
|
||||
}
|
||||
// TODO: Validate signature against issuer!!!
|
||||
if (data.exp && data.exp > Date.now() / 1000) {
|
||||
throw new Error('JWT token is expired');
|
||||
}
|
||||
let statements: Statement[] = [];
|
||||
const groups = data[config.oidc.groupField];
|
||||
if (Array.isArray(groups)) {
|
||||
if (config.oidc.groups.admin && groups.includes(config.oidc.groups.admin)) {
|
||||
statements = adminStatements;
|
||||
}
|
||||
if (config.oidc.groups.writer && groups.includes(config.oidc.groups.writer)) {
|
||||
statements = writerStatements;
|
||||
}
|
||||
if (config.oidc.groups.reader && groups.includes(config.oidc.groups.reader)) {
|
||||
statements = readerStatements;
|
||||
}
|
||||
}
|
||||
return {
|
||||
statements,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export { OidcHandler };
|
||||
Reference in New Issue
Block a user