Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: oidc-integration #385

Open
wants to merge 14 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -95,3 +95,17 @@ PLAID_LINK_WEBHOOK=

PLAID_SANDBOX_REDIRECT_URI=
PLAID_DEVELOPMENT_REDIRECT_URI=

# OIDC
OIDC_LOGIN_DISABLED=false
OIDC_ISSUER=
OIDC_AUTHORIZATION_ENDPOINT=
OIDC_TOKEN_ENDPOINT=
OIDC_USERINFO_ENDPOINT=
OIDC_ENDSESSION_ENDPOINT=
OIDC_REVOCATION_ENDPOINT=
OIDC_CLIENT_ID=
OIDC_CLIENT_SECRET=
OIDC_REDIRECT_URI=
OIDC_SCOPE=
OIDC_JWK_URI=
1 change: 1 addition & 0 deletions packages/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@
"objection-filter": "^4.0.1",
"objection-soft-delete": "^1.0.7",
"objection-unique": "^1.2.2",
"openid-client": "^5.6.5",
"pluralize": "^8.0.0",
"pug": "^3.0.2",
"puppeteer": "^10.2.0",
Expand Down
101 changes: 101 additions & 0 deletions packages/server/src/api/controllers/Oidc/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import BaseController from '@/api/controllers/BaseController';
import AttachCurrentTenantUser from '@/api/middleware/AttachCurrentTenantUser';
import TenancyMiddleware from '@/api/middleware/TenancyMiddleware';
import JWTAuth from '@/api/middleware/jwtAuth';
import { OidcService } from '@/services/Oidc';
import { NextFunction, Request, Response, Router } from 'express';
import { Inject, Service } from 'typedi';
@Service()
export default class OidcController extends BaseController {
@Inject()
private oidcService: OidcService;

/**
* Router constructor method.
*/
public router() {
const router = Router();

router.post('/authorize', this.authorize);

router.post('/login', this.oidcLogin);

router.use(JWTAuth);
router.use(AttachCurrentTenantUser);
router.use(TenancyMiddleware);

router.post('/logout', this.oidcLogout);

return router;
}

/**
* Authentication Oidc authorize.
* @param {Request} req -
* @param {Response} res -
* @param {NextFunction} next -
*/
private authorize = async (
req: Request,
res: Response,
next: NextFunction
) => {
try {
const authorizationUrl = this.oidcService.generateAuthorizationUrl();

res.json({ authorizationUrl });
} catch (error) {
next(error);
}
};

/**
* Authentication oidc login.
* @param {Request} req -
* @param {Response} res -
* @param {NextFunction} next -
*/
private oidcLogin = async (
req: Request,
res: Response,
next: NextFunction
) => {
try {
const code = req.body.code;

const loginResponse = await this.oidcService.loginUser(code);

return res.status(200).send(loginResponse);
} catch (error) {
next(error);
}
};

/**
* Authentication oidc logout.
* @param {Request} req -
* @param {Response} res -
* @param {NextFunction} next -
*/
private oidcLogout = async (
req: Request,
res: Response,
next: NextFunction
) => {
try {
const { token } = req;

const oidcIdToken = token.oidc_id_token;
const oidcAccessToken = token.oidc_access_token;

const logoutUrl = await this.oidcService.generateEndSessionUrl({
oidcIdToken,
oidcAccessToken,
});

return res.status(200).send({ logoutUrl });
} catch (error) {
next(error);
}
};
}
6 changes: 5 additions & 1 deletion packages/server/src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ import { TaxRatesController } from './controllers/TaxRates/TaxRates';
import { ImportController } from './controllers/Import/ImportController';
import { BankingController } from './controllers/Banking/BankingController';
import { Webhooks } from './controllers/Webhooks/Webhooks';
import OidcController from '@/api/controllers/Oidc'
import oidcSessionMiddleware from '@/api/middleware/oidcSession'

export default () => {
const app = Router();
Expand All @@ -67,7 +69,8 @@ export default () => {
// ---------------------------
app.use(asyncRenderMiddleware);
app.use(I18nMiddleware);


app.use('/oidc', Container.get(OidcController).router());
app.use('/auth', Container.get(Authentication).router());
app.use('/invite', Container.get(InviteUsers).nonAuthRouter());
app.use('/organization', Container.get(Organization).router());
Expand All @@ -82,6 +85,7 @@ export default () => {

dashboard.use(JWTAuth);
dashboard.use(AttachCurrentTenantUser);
dashboard.use(oidcSessionMiddleware);
dashboard.use(TenancyMiddleware);
dashboard.use(EnsureTenantIsInitialized);
dashboard.use(SettingsMiddleware);
Expand Down
27 changes: 27 additions & 0 deletions packages/server/src/api/middleware/oidcSession.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { oidcClient } from '@/lib/Oidc/OidcClient';
import { NextFunction, Request, Response } from 'express';

const oidcSessionMiddleware = async (
req: Request,
res: Response,
next: NextFunction
) => {
const token = req.token;

try {
const oidcAccessToken = token.oidc_access_token;

if (oidcAccessToken) {
const oidcUser = await oidcClient.userinfo(oidcAccessToken);

if (!oidcUser) {
return res.boom.unauthorized();
}
}

return next();
} catch (error) {
return next(error);
}
};
export default oidcSessionMiddleware;
5 changes: 4 additions & 1 deletion packages/server/src/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,9 @@ module.exports = {
secretSandbox: process.env.PLAID_SECRET_SANDBOX,
redirectSandBox: process.env.PLAID_SANDBOX_REDIRECT_URI,
redirectDevelopment: process.env.PLAID_DEVELOPMENT_REDIRECT_URI,
linkWebhook: process.env.PLAID_LINK_WEBHOOK
linkWebhook: process.env.PLAID_LINK_WEBHOOK,
},
oidcLogin: {
disabled: parseBoolean<boolean>(process.env.OIDC_LOGIN_DISABLED, false),
},
};
50 changes: 50 additions & 0 deletions packages/server/src/config/oidcConfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import {
AuthorizationParameters,
ClientMetadata,
GrantBody,
IssuerMetadata,
} from 'openid-client';

export const oidcConfig = {
OIDC_CLIENT_ID: process.env.OIDC_CLIENT_ID,
OIDC_CLIENT_SECRET: process.env.OIDC_CLIENT_SECRET,
OIDC_ISSUER: process.env.OIDC_ISSUER,
OIDC_AUTHORIZATION_ENDPOINT: process.env.OIDC_AUTHORIZATION_ENDPOINT,
OIDC_TOKEN_ENDPOINT: process.env.OIDC_TOKEN_ENDPOINT,
OIDC_USERINFO_ENDPOINT: process.env.OIDC_USERINFO_ENDPOINT,
OIDC_ENDSESSION_ENDPOINT: process.env.OIDC_ENDSESSION_ENDPOINT,
OIDC_REVOCATION_ENDPOINT: process.env.OIDC_REVOCATION_ENDPOINT,
OIDC_REDIRECT_URI: process.env.OIDC_REDIRECT_URI,
OIDC_SCOPE: process.env.OIDC_SCOPE,
OIDC_JWK_URI: process.env.OIDC_JWK_URI,
};

export const issuerMetadata: IssuerMetadata = {
issuer: oidcConfig.OIDC_ISSUER,
authorization_endpoint: oidcConfig.OIDC_AUTHORIZATION_ENDPOINT,
token_endpoint: oidcConfig.OIDC_TOKEN_ENDPOINT,
userinfo_endpoint: oidcConfig.OIDC_USERINFO_ENDPOINT,
end_session_endpoint: oidcConfig.OIDC_ENDSESSION_ENDPOINT,
revocation_endpoint: oidcConfig.OIDC_REVOCATION_ENDPOINT,
jwks_uri: oidcConfig.OIDC_JWK_URI,
};

export const clientMetadata: ClientMetadata = {
client_id: oidcConfig.OIDC_CLIENT_ID,
client_secret: oidcConfig.OIDC_CLIENT_SECRET,
redirect_uris: [oidcConfig.OIDC_REDIRECT_URI],
token_endpoint_auth_method: 'client_secret_basic',
id_token_signed_response_alg: 'RS256',
post_logout_redirect_uris: [oidcConfig.OIDC_REDIRECT_URI],
response_types: ['code', 'token', 'id_token'],
};

export const authorizationUrlParameters: AuthorizationParameters = {
scope: oidcConfig.OIDC_SCOPE,
response_type: 'code',
};

export const tokenGrantBody: GrantBody = {
grant_type: 'authorization_code',
redirect_uri: oidcConfig.OIDC_REDIRECT_URI,
};
3 changes: 2 additions & 1 deletion packages/server/src/interfaces/Authentication.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ export interface IAuthSendedResetPassword {
token: string;
}

export interface IAuthGetMetaPOJO {
export interface IAuthGetMetaPOJO {
signupDisabled: boolean;
oidcLoginDisabled: boolean;
}
6 changes: 6 additions & 0 deletions packages/server/src/lib/Oidc/OidcClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { clientMetadata, issuerMetadata } from '@/config/oidcConfig';
import { Issuer } from 'openid-client';

const issuer = new Issuer(issuerMetadata);

export const oidcClient = new issuer.Client(clientMetadata);
2 changes: 1 addition & 1 deletion packages/server/src/loaders/eventEmitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ import { InvoiceChangeStatusOnMailSentSubscriber } from '@/services/Sales/Invoic
import { SaleReceiptMarkClosedOnMailSentSubcriber } from '@/services/Sales/Receipts/subscribers/SaleReceiptMarkClosedOnMailSentSubcriber';
import { SaleEstimateMarkApprovedOnMailSent } from '@/services/Sales/Estimates/subscribers/SaleEstimateMarkApprovedOnMailSent';
import { DeleteCashflowTransactionOnUncategorize } from '@/services/Cashflow/subscribers/DeleteCashflowTransactionOnUncategorize';
import { PreventDeleteTransactionOnDelete } from '@/services/Cashflow/subscribers/PreventDeleteTransactionsOnDelete'; }
import { PreventDeleteTransactionOnDelete } from '@/services/Cashflow/subscribers/PreventDeleteTransactionsOnDelete';
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@abouolia I couldnot build server due to this syntax error here. So i removed it to get it working.


export default () => {
return new EventPublisher();
Expand Down
1 change: 1 addition & 0 deletions packages/server/src/services/Authentication/GetAuthMeta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export class GetAuthMeta {
public async getAuthMeta(): Promise<IAuthGetMetaPOJO> {
return {
signupDisabled: config.signupRestrictions.disabled,
oidcLoginDisabled: config.oidcLogin.disabled,
};
}
}
13 changes: 11 additions & 2 deletions packages/server/src/services/Authentication/_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,18 @@ import JWT from 'jsonwebtoken';
import { ISystemUser } from '@/interfaces';
import config from '@/config';

interface IExtendedSystemUser extends ISystemUser {
oidc_access_token?: string;
oidc_id_token?: string;
oidc_refresh_token?: string;
}

/**
* Generates JWT token for the given user.
* @param {ISystemUser} user
* @param {IExtendedSystemUser} user
* @return {string} token
*/
export const generateToken = (user: ISystemUser): string => {
export const generateToken = (user: IExtendedSystemUser): string => {
const today = new Date();
const exp = new Date(today);
exp.setDate(today.getDate() + 60);
Expand All @@ -16,6 +22,9 @@ export const generateToken = (user: ISystemUser): string => {
{
id: user.id, // We are gonna use this in the middleware 'isAuth'
exp: exp.getTime() / 1000,
oidc_access_token: user.oidc_access_token,
oidc_id_token: user.oidc_id_token,
oidc_refresh_token: user.oidc_refresh_token,
},
config.jwtSecret
);
Expand Down
Loading