feat: initial OIDC support
This commit is contained in:
@@ -3,10 +3,8 @@ import jwt from 'jsonwebtoken';
|
|||||||
|
|
||||||
import { statementSchema } from './access.schemas.ts';
|
import { statementSchema } from './access.schemas.ts';
|
||||||
import type { AccessProvider } from './access.provider.ts';
|
import type { AccessProvider } from './access.provider.ts';
|
||||||
|
import type { Services } from '#root/utils/services.ts';
|
||||||
type AccessTokensOptions = {
|
import { Config } from '#root/config/config.ts';
|
||||||
secret: string | Buffer;
|
|
||||||
};
|
|
||||||
|
|
||||||
const tokenBodySchema = z.object({
|
const tokenBodySchema = z.object({
|
||||||
statements: z.array(statementSchema),
|
statements: z.array(statementSchema),
|
||||||
@@ -15,21 +13,29 @@ const tokenBodySchema = z.object({
|
|||||||
type TokenBody = z.infer<typeof tokenBodySchema>;
|
type TokenBody = z.infer<typeof tokenBodySchema>;
|
||||||
|
|
||||||
class AccessTokens implements AccessProvider {
|
class AccessTokens implements AccessProvider {
|
||||||
#options: AccessTokensOptions;
|
#services: Services;
|
||||||
|
|
||||||
constructor(options: AccessTokensOptions) {
|
constructor(services: Services) {
|
||||||
this.#options = options;
|
this.#services = services;
|
||||||
}
|
}
|
||||||
|
|
||||||
public generate = (options: TokenBody) => {
|
public generate = (options: TokenBody) => {
|
||||||
const { secret } = this.#options;
|
const config = this.#services.get(Config);
|
||||||
const token = jwt.sign(options, secret);
|
const { tokenSecret } = config;
|
||||||
|
if (!tokenSecret) {
|
||||||
|
throw new Error('Token secret does not exist');
|
||||||
|
}
|
||||||
|
const token = jwt.sign(options, tokenSecret);
|
||||||
return token;
|
return token;
|
||||||
};
|
};
|
||||||
|
|
||||||
public getAccess = async (token: string) => {
|
public getAccess = async (token: string) => {
|
||||||
const { secret } = this.#options;
|
const config = this.#services.get(Config);
|
||||||
const data = jwt.verify(token, secret);
|
const { tokenSecret } = config;
|
||||||
|
if (!tokenSecret) {
|
||||||
|
throw new Error('Token secret does not exist');
|
||||||
|
}
|
||||||
|
const data = jwt.verify(token, tokenSecret);
|
||||||
const parsed = tokenBodySchema.parse(data);
|
const parsed = tokenBodySchema.parse(data);
|
||||||
return parsed;
|
return parsed;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { AccessHandler } from './access/access.handler.ts';
|
import { AccessHandler } from './access/access.handler.ts';
|
||||||
|
import { AccessTokens } from './access/access.token.ts';
|
||||||
import { Config } from './config/config.ts';
|
import { Config } from './config/config.ts';
|
||||||
import { K8sService } from './k8s/k8s.ts';
|
import { K8sService } from './k8s/k8s.ts';
|
||||||
|
import { OidcHandler } from './oidc/oidc.handler.ts';
|
||||||
import { MqttServer } from './server/server.ts';
|
import { MqttServer } from './server/server.ts';
|
||||||
import { TopicsHandler } from './topics/topics.handler.ts';
|
import { TopicsHandler } from './topics/topics.handler.ts';
|
||||||
import { Services } from './utils/services.ts';
|
import { Services } from './utils/services.ts';
|
||||||
@@ -38,7 +40,8 @@ class Backbone {
|
|||||||
|
|
||||||
public start = async () => {
|
public start = async () => {
|
||||||
if (this.config.k8s.enabled) {
|
if (this.config.k8s.enabled) {
|
||||||
await this.setupK8sOperator();
|
await this.k8s.setup();
|
||||||
|
this.accessHandler.register('k8s', this.k8s.clients);
|
||||||
}
|
}
|
||||||
if (this.config.http.enabled) {
|
if (this.config.http.enabled) {
|
||||||
console.log('starting http');
|
console.log('starting http');
|
||||||
@@ -49,11 +52,12 @@ class Backbone {
|
|||||||
const tcp = this.server.getTcpServer();
|
const tcp = this.server.getTcpServer();
|
||||||
tcp.listen(this.config.tcp.port);
|
tcp.listen(this.config.tcp.port);
|
||||||
}
|
}
|
||||||
};
|
if (this.config.oidc.enabled) {
|
||||||
|
this.accessHandler.register('oidc', this.#services.get(OidcHandler));
|
||||||
public setupK8sOperator = async () => {
|
}
|
||||||
await this.k8s.setup();
|
if (this.config.tokenSecret) {
|
||||||
this.accessHandler.register('k8s', this.k8s.clients);
|
this.accessHandler.register('token', this.#services.get(AccessTokens));
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
public destroy = async () => {
|
public destroy = async () => {
|
||||||
|
|||||||
@@ -3,6 +3,29 @@ class Config {
|
|||||||
return process.env.TOKEN_SECRET;
|
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() {
|
public get k8s() {
|
||||||
const enabled = process.env.K8S_ENABLED === 'true';
|
const enabled = process.env.K8S_ENABLED === 'true';
|
||||||
return {
|
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