etherpad-lite/src/node/security/OAuth2Provider.ts

264 lines
9.3 KiB
TypeScript
Raw Normal View History

2024-03-23 07:38:23 +01:00
import {ArgsExpressType} from "../types/ArgsExpressType";
2024-03-24 20:53:44 +01:00
import Provider, {Account, Configuration, InteractionResults} from 'oidc-provider';
2024-03-24 14:18:58 +01:00
import {generateKeyPair, exportJWK} from 'jose'
2024-03-24 20:53:44 +01:00
import MemoryAdapter from "./OIDCAdapter";
import path from "path";
const settings = require('../utils/Settings');
import {IncomingForm} from 'formidable'
2024-03-25 15:34:37 +01:00
import express, {Request, Response} from 'express';
2024-03-24 20:53:44 +01:00
import {format} from 'url'
import {ParsedUrlQuery} from "node:querystring";
2024-03-25 15:34:37 +01:00
import cors from 'cors'
import {Http2ServerRequest, Http2ServerResponse} from "node:http2";
2024-03-23 07:38:23 +01:00
const configuration: Configuration = {
2024-03-24 14:18:58 +01:00
scopes: ['openid', 'profile', 'email'],
2024-03-24 20:53:44 +01:00
findAccount: async (ctx, id) => {
2024-03-25 15:34:37 +01:00
const users = settings.users as {
[username: string]: {
password: string;
is_admin: boolean;
}
}
const usersArray1 = Object.keys(users).map((username) => ({
username,
...users[username]
}));
const account = usersArray1.find((user) => user.username === id);
2024-03-24 14:18:58 +01:00
return {
accountId: id,
claims: () => ({
sub: id,
2024-03-25 15:34:37 +01:00
test: "test",
admin: account?.is_admin
2024-03-24 14:18:58 +01:00
})
2024-03-24 20:53:44 +01:00
} as Account
},
ttl:{
AccessToken: 1 * 60 * 60, // 1 hour in seconds
AuthorizationCode: 10 * 60, // 10 minutes in seconds
ClientCredentials: 1 * 60 * 60, // 1 hour in seconds
IdToken: 1 * 60 * 60, // 1 hour in seconds
RefreshToken: 1 * 24 * 60 * 60, // 1 day in seconds
},
2024-03-25 15:34:37 +01:00
claims: {
openid: ['sub'],
email: ['email'],
profile: ['name'],
admin: ['admin']
},
2024-03-24 20:53:44 +01:00
cookies: {
keys: ['oidc'],
},
features:{
devInteractions: {enabled: false},
},
adapter: MemoryAdapter
2024-03-23 07:38:23 +01:00
};
2024-03-24 20:53:44 +01:00
2024-03-25 15:34:37 +01:00
/*
This function is used to initialize the OAuth2 provider
*/
2024-03-24 14:18:58 +01:00
export const expressCreateServer = async (hookName: string, args: ArgsExpressType, cb: Function) => {
const {privateKey} = await generateKeyPair('RS256');
const privateKeyJWK = await exportJWK(privateKey);
2024-03-25 15:34:37 +01:00
// Use cors middleware
args.app.use(cors({
origin: ['http://localhost:3001', 'https://oauth.pstmn.io'], // replace with your allowed origins
}));
2024-03-24 14:18:58 +01:00
const oidc = new Provider('http://localhost:9001', {
...configuration, jwks: {
keys: [
privateKeyJWK
],
},
2024-03-25 15:34:37 +01:00
conformIdTokenClaims: false,
claims: {
address: ['address'],
email: ['email', 'email_verified'],
phone: ['phone_number', 'phone_number_verified'],
profile: ['birthdate', 'family_name', 'gender', 'given_name', 'locale', 'middle_name', 'name',
'nickname', 'picture', 'preferred_username', 'profile', 'updated_at', 'website', 'zoneinfo'],
},
features:{
userinfo: {enabled: true},
claimsParameter: {enabled: true},
devInteractions: {enabled: false},
resourceIndicators: {enabled: true, defaultResource(ctx) {
return ctx.origin;
},
getResourceServerInfo(ctx, resourceIndicator, client) {
return {
scope: client.scope as string,
audience: 'account',
accessTokenFormat: 'jwt',
};
},
useGrantedResource(ctx, model) {
return true;
},},
jwtResponseModes: {enabled: true},
},
clientBasedCORS: (ctx, origin, client) => {
return true
},
extraTokenClaims: async (ctx, token) => {
if(token.kind === 'AccessToken') {
// Add your custom claims here. For example:
const users = settings.users as {
[username: string]: {
password: string;
is_admin: boolean;
}
}
const usersArray1 = Object.keys(users).map((username) => ({
username,
...users[username]
}));
const account = usersArray1.find((user) => user.username === token.accountId);
return {
admin: account?.is_admin
};
}
},
clients: settings.sso.clients
2024-03-24 14:18:58 +01:00
});
2024-03-24 20:53:44 +01:00
2024-03-25 15:34:37 +01:00
args.app.post('/interaction/:uid', async (req: Http2ServerRequest, res: Http2ServerResponse, next:Function) => {
2024-03-24 20:53:44 +01:00
const formid = new IncomingForm();
try {
2024-03-25 15:34:37 +01:00
// @ts-ignore
2024-03-24 20:53:44 +01:00
const {login, password} = (await formid.parse(req))[0]
2024-03-25 15:34:37 +01:00
const {prompt, jti, session,cid, params, grantId} = await oidc.interactionDetails(req, res);
2024-03-24 20:53:44 +01:00
2024-03-25 15:34:37 +01:00
const client = await oidc.Client.find(params.client_id as string);
2024-03-24 20:53:44 +01:00
switch (prompt.name) {
case 'login': {
const users = settings.users as {
[username: string]: {
password: string;
admin: boolean;
}
}
const usersArray1 = Object.keys(users).map((username) => ({
username,
...users[username]
}));
const account = usersArray1.find((user) => user.username === login as unknown as string && user.password === password as unknown as string);
if (!account) {
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({error: "Invalid login"}));
}
if (account) {
await oidc.interactionFinished(req, res, {
login: {accountId: account.username}
}, {mergeWithLastSubmission: false});
}
break;
}
case 'consent': {
let grant;
if (grantId) {
// we'll be modifying existing grant in existing session
grant = await oidc.Grant.find(grantId);
} else {
// we're establishing a new grant
grant = new oidc.Grant({
accountId: session!.accountId,
clientId: params.client_id as string,
});
}
if (prompt.details.missingOIDCScope) {
2024-03-25 15:34:37 +01:00
// @ts-ignore
2024-03-24 20:53:44 +01:00
grant!.addOIDCScope(prompt.details.missingOIDCScope.join(' '));
}
if (prompt.details.missingOIDCClaims) {
grant!.addOIDCClaims(prompt.details.missingOIDCClaims as string[]);
}
if (prompt.details.missingResourceScopes) {
for (const [indicator, scope] of Object.entries(prompt.details.missingResourceScopes)) {
grant!.addResourceScope(indicator, scope.join(' '));
}
}
const result = {consent: {grantId: await grant!.save()}};
await oidc.interactionFinished(req, res, result, {
mergeWithLastSubmission: true,
});
break;
}
}
await next();
2024-03-25 15:34:37 +01:00
} catch (err:any) {
return res.writeHead(500).end(err.message);
2024-03-24 20:53:44 +01:00
}
})
2024-03-25 15:34:37 +01:00
args.app.get('/interaction/:uid', async (req: Request, res: Response, next: Function) => {
2024-03-24 20:53:44 +01:00
try {
const {
uid, prompt, params, session,
} = await oidc.interactionDetails(req, res);
params["state"] = uid
switch (prompt.name) {
case 'login': {
res.redirect(format({
2024-03-25 15:34:37 +01:00
pathname: '/views/login.html',
2024-03-24 20:53:44 +01:00
query: params as ParsedUrlQuery
}))
break
}
case 'consent': {
res.redirect(format({
2024-03-25 15:34:37 +01:00
pathname: '/views/consent.html',
2024-03-24 20:53:44 +01:00
query: params as ParsedUrlQuery
}))
break
}
default:
return res.sendFile(path.join(settings.root,'src','static', 'oidc','login.html'));
}
} catch (err) {
return next(err);
}
});
2024-03-25 15:34:37 +01:00
args.app.use('/views/', express.static(path.join(settings.root,'src','static', 'oidc'), {maxAge: 1000 * 60 * 60 * 24}));
2024-03-24 20:53:44 +01:00
/*
2024-03-24 14:18:58 +01:00
oidc.on('authorization.error', (ctx, error) => {
console.log('authorization.error', error);
})
2024-03-24 20:53:44 +01:00
oidc.on('server_error', (ctx, error) => {
console.log('server_error', error);
})
oidc.on('grant.error', (ctx, error) => {
console.log('grant.error', error);
})
oidc.on('introspection.error', (ctx, error) => {
console.log('introspection.error', error);
})
oidc.on('revocation.error', (ctx, error) => {
console.log('revocation.error', error);
})*/
2024-03-24 14:18:58 +01:00
args.app.use("/oidc", oidc.callback());
2024-03-24 20:53:44 +01:00
//cb();
2024-03-23 07:38:23 +01:00
}