mirror of
https://github.com/ether/etherpad-lite.git
synced 2025-05-05 06:37:10 -04:00
Added auth flow.
This commit is contained in:
parent
aa627761e8
commit
66fc735253
6 changed files with 294 additions and 32 deletions
9
pnpm-lock.yaml
generated
9
pnpm-lock.yaml
generated
|
@ -278,6 +278,9 @@ importers:
|
||||||
'@types/express':
|
'@types/express':
|
||||||
specifier: ^4.17.21
|
specifier: ^4.17.21
|
||||||
version: 4.17.21
|
version: 4.17.21
|
||||||
|
'@types/formidable':
|
||||||
|
specifier: ^3.4.5
|
||||||
|
version: 3.4.5
|
||||||
'@types/http-errors':
|
'@types/http-errors':
|
||||||
specifier: ^2.0.4
|
specifier: ^2.0.4
|
||||||
version: 2.0.4
|
version: 2.0.4
|
||||||
|
@ -2203,6 +2206,12 @@ packages:
|
||||||
'@types/serve-static': 1.15.5
|
'@types/serve-static': 1.15.5
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@types/formidable@3.4.5:
|
||||||
|
resolution: {integrity: sha512-s7YPsNVfnsng5L8sKnG/Gbb2tiwwJTY1conOkJzTMRvJAlLFW1nEua+ADsJQu8N1c0oTHx9+d5nqg10WuT9gHQ==}
|
||||||
|
dependencies:
|
||||||
|
'@types/node': 20.11.30
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@types/fs-extra@9.0.13:
|
/@types/fs-extra@9.0.13:
|
||||||
resolution: {integrity: sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==}
|
resolution: {integrity: sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|
|
@ -1,7 +1,13 @@
|
||||||
import {ArgsExpressType} from "../types/ArgsExpressType";
|
import {ArgsExpressType} from "../types/ArgsExpressType";
|
||||||
import Provider, {Account, Configuration} from 'oidc-provider';
|
import Provider, {Account, Configuration, InteractionResults} from 'oidc-provider';
|
||||||
import {generateKeyPair, exportJWK} from 'jose'
|
import {generateKeyPair, exportJWK} from 'jose'
|
||||||
|
import MemoryAdapter from "./OIDCAdapter";
|
||||||
|
import path from "path";
|
||||||
|
const settings = require('../utils/Settings');
|
||||||
|
import {IncomingForm} from 'formidable'
|
||||||
|
import {Request, Response} from 'express';
|
||||||
|
import {format} from 'url'
|
||||||
|
import {ParsedUrlQuery} from "node:querystring";
|
||||||
const configuration: Configuration = {
|
const configuration: Configuration = {
|
||||||
// refer to the documentation for other available configuration
|
// refer to the documentation for other available configuration
|
||||||
clients: [ {
|
clients: [ {
|
||||||
|
@ -9,7 +15,7 @@ const configuration: Configuration = {
|
||||||
client_secret: 'a_different_secret',
|
client_secret: 'a_different_secret',
|
||||||
grant_types: ['authorization_code'],
|
grant_types: ['authorization_code'],
|
||||||
response_types: ['code'],
|
response_types: ['code'],
|
||||||
redirect_uris: ['http://localhost:3001/cb']
|
redirect_uris: ['http://localhost:3001/cb', 'https://oauth.pstmn.io/v1/callback']
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
client_id: 'app',
|
client_id: 'app',
|
||||||
|
@ -20,18 +26,33 @@ const configuration: Configuration = {
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
scopes: ['openid', 'profile', 'email'],
|
scopes: ['openid', 'profile', 'email'],
|
||||||
//adapter: MemoryAdapter,
|
findAccount: async (ctx, id) => {
|
||||||
/*findAccount: async (ctx, id) => {
|
console.log("Finding account", id)
|
||||||
console.log(ctx, id)
|
|
||||||
return {
|
return {
|
||||||
accountId: id,
|
accountId: id,
|
||||||
claims: () => ({
|
claims: () => ({
|
||||||
sub: id,
|
sub: id,
|
||||||
})
|
})
|
||||||
} satisfies Account
|
} 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
|
||||||
|
},
|
||||||
|
cookies: {
|
||||||
|
keys: ['oidc'],
|
||||||
|
},
|
||||||
|
features:{
|
||||||
|
devInteractions: {enabled: false},
|
||||||
|
},
|
||||||
|
adapter: MemoryAdapter
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export const expressCreateServer = async (hookName: string, args: ArgsExpressType, cb: Function) => {
|
export const expressCreateServer = async (hookName: string, args: ArgsExpressType, cb: Function) => {
|
||||||
const {privateKey} = await generateKeyPair('RS256');
|
const {privateKey} = await generateKeyPair('RS256');
|
||||||
const privateKeyJWK = await exportJWK(privateKey);
|
const privateKeyJWK = await exportJWK(privateKey);
|
||||||
|
@ -43,9 +64,154 @@ export const expressCreateServer = async (hookName: string, args: ArgsExpressTyp
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
args.app.post('/interaction/:uid', async (req, res, next) => {
|
||||||
|
const formid = new IncomingForm();
|
||||||
|
try {
|
||||||
|
const {login, password} = (await formid.parse(req))[0]
|
||||||
|
const {prompt, jti, session, params, grantId} = await oidc.interactionDetails(req, res);
|
||||||
|
|
||||||
|
console.log("Session is", session)
|
||||||
|
|
||||||
|
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) {
|
||||||
|
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();
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err)
|
||||||
|
return next(err);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
args.app.get('/interaction/:uid', async (req: Request, res: Response, next) => {
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
uid, prompt, params, session,
|
||||||
|
} = await oidc.interactionDetails(req, res);
|
||||||
|
|
||||||
|
console.log("Params are", params)
|
||||||
|
params["state"] = uid
|
||||||
|
|
||||||
|
console.log("Prompt is", prompt)
|
||||||
|
switch (prompt.name) {
|
||||||
|
case 'login': {
|
||||||
|
res.redirect(format({
|
||||||
|
pathname: '/views/login',
|
||||||
|
query: params as ParsedUrlQuery
|
||||||
|
}))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'consent': {
|
||||||
|
console.log("Consent")
|
||||||
|
res.redirect(format({
|
||||||
|
pathname: '/views/consent',
|
||||||
|
query: params as ParsedUrlQuery
|
||||||
|
}))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return res.sendFile(path.join(settings.root,'src','static', 'oidc','login.html'));
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
return next(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
args.app.get('/views/login', async (req, res) => {
|
||||||
|
res.sendFile(path.join(settings.root,'src','static', 'oidc','login.html'));
|
||||||
|
})
|
||||||
|
|
||||||
|
args.app.get('/views/consent', async (req, res) => {
|
||||||
|
res.sendFile(path.join(settings.root,'src','static', 'oidc','consent.html'));
|
||||||
|
})
|
||||||
|
|
||||||
|
args.app.get('/interaction/:uid/confirm', async (req, res) => {
|
||||||
|
const {uid, prompt, params} = await oidc.interactionDetails(req, res);
|
||||||
|
console.log('interaction', uid, prompt, params);
|
||||||
|
res.render('interaction', {
|
||||||
|
uid,
|
||||||
|
prompt,
|
||||||
|
params,
|
||||||
|
title: 'Authorize',
|
||||||
|
client: await oidc.Client.find(params.client_id!),
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
||||||
|
args.app.get('/interaction/:uid', async (req, res) => {
|
||||||
|
return res.sendFile(path.join(settings.root,'src','static', 'oidc','login.html'));
|
||||||
|
})
|
||||||
|
|
||||||
oidc.on('authorization.error', (ctx, error) => {
|
oidc.on('authorization.error', (ctx, error) => {
|
||||||
console.log('authorization.error', error);
|
console.log('authorization.error', error);
|
||||||
})
|
})
|
||||||
|
|
||||||
|
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);
|
||||||
|
})
|
||||||
args.app.use("/oidc", oidc.callback());
|
args.app.use("/oidc", oidc.callback());
|
||||||
cb();
|
//cb();
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
import {LRUCache} from 'lru-cache';
|
import {LRUCache} from 'lru-cache';
|
||||||
import {Adapter, AdapterPayload} from "oidc-provider";
|
import type {Adapter, AdapterPayload} from "oidc-provider";
|
||||||
|
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
max: 500,
|
max: 500,
|
||||||
|
sizeCalculation: (item, key) => {
|
||||||
|
return 1
|
||||||
|
},
|
||||||
// for use with tracking overall storage size
|
// for use with tracking overall storage size
|
||||||
maxSize: 5000,
|
maxSize: 5000,
|
||||||
|
|
||||||
|
@ -41,6 +43,7 @@ class MemoryAdapter implements Adapter{
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy(id:string) {
|
destroy(id:string) {
|
||||||
|
console.log("destroy", id);
|
||||||
const key = this.key(id);
|
const key = this.key(id);
|
||||||
|
|
||||||
const found = storage.get(key) as AdapterPayload;
|
const found = storage.get(key) as AdapterPayload;
|
||||||
|
@ -58,51 +61,63 @@ class MemoryAdapter implements Adapter{
|
||||||
}
|
}
|
||||||
|
|
||||||
consume(id: string) {
|
consume(id: string) {
|
||||||
|
console.log("consume", id);
|
||||||
(storage.get(this.key(id)) as AdapterPayload)!.consumed = epochTime();
|
(storage.get(this.key(id)) as AdapterPayload)!.consumed = epochTime();
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
find(id: string): Promise<AdapterPayload | void | undefined> {
|
find(id: string): Promise<AdapterPayload | void | undefined> {
|
||||||
|
const foundSession = storage.get(this.key(id)) as AdapterPayload;
|
||||||
|
console.log("find", id, foundSession);
|
||||||
|
if (storage.has(this.key(id))){
|
||||||
return Promise.resolve<AdapterPayload>(storage.get(this.key(id)) as AdapterPayload);
|
return Promise.resolve<AdapterPayload>(storage.get(this.key(id)) as AdapterPayload);
|
||||||
}
|
}
|
||||||
|
return Promise.resolve<undefined>(undefined)
|
||||||
|
}
|
||||||
|
|
||||||
findByUserCode(userCode: string) {
|
findByUserCode(userCode: string) {
|
||||||
|
console.log("findByUserCode", userCode);
|
||||||
const id = storage.get(userCodeKeyFor(userCode)) as string;
|
const id = storage.get(userCodeKeyFor(userCode)) as string;
|
||||||
return this.find(id);
|
return this.find(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
upsert(id: string, payload: any, expiresIn: number) {
|
upsert(id: string, payload: {
|
||||||
console.log('upsert');
|
iat: number;
|
||||||
|
exp: number;
|
||||||
|
uid: string;
|
||||||
|
kind: string;
|
||||||
|
jti: string;
|
||||||
|
accountId: string;
|
||||||
|
loginTs: number;
|
||||||
|
}, expiresIn: number) {
|
||||||
|
console.log("upsert", payload);
|
||||||
const key = this.key(id);
|
const key = this.key(id);
|
||||||
|
|
||||||
const { grantId, userCode } = payload;
|
|
||||||
if (grantId) {
|
|
||||||
const grantKey = grantKeyFor(grantId);
|
|
||||||
const grant = storage.get(grantKey) as unknown as string[];
|
|
||||||
if (!grant) {
|
|
||||||
storage.set(grantKey, [key]);
|
|
||||||
} else {
|
|
||||||
grant.push(key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (userCode) {
|
|
||||||
storage.set(userCodeKeyFor(userCode), id, {ttl:expiresIn * 1000});
|
|
||||||
}
|
|
||||||
|
|
||||||
storage.set(key, payload, {ttl: expiresIn * 1000});
|
storage.set(key, payload, {ttl: expiresIn * 1000});
|
||||||
|
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
findByUid(uid: string): Promise<AdapterPayload | void | undefined> {
|
findByUid(uid: string): Promise<AdapterPayload | void | undefined> {
|
||||||
console.log('findByUid', uid);
|
console.log("findByUid", uid);
|
||||||
|
for(const [_, value] of storage.entries()){
|
||||||
|
if(typeof value ==="object" && "uid" in value && value.uid === uid){
|
||||||
|
console.log("found", value);
|
||||||
|
return Promise.resolve(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log("not found");
|
||||||
return Promise.resolve(undefined);
|
return Promise.resolve(undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
revokeByGrantId(grantId: string): Promise<void | undefined> {
|
revokeByGrantId(grantId: string): Promise<void | undefined> {
|
||||||
console.log('findByUid', grantId);
|
const grantKey = grantKeyFor(grantId);
|
||||||
return Promise.resolve(undefined);
|
const grant = storage.get(grantKey) as string[];
|
||||||
|
if (grant) {
|
||||||
|
grant.forEach((token) => storage.delete(token));
|
||||||
|
storage.delete(grantKey);
|
||||||
|
}
|
||||||
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -84,6 +84,7 @@
|
||||||
"@playwright/test": "^1.42.1",
|
"@playwright/test": "^1.42.1",
|
||||||
"@types/async": "^3.2.24",
|
"@types/async": "^3.2.24",
|
||||||
"@types/express": "^4.17.21",
|
"@types/express": "^4.17.21",
|
||||||
|
"@types/formidable": "^3.4.5",
|
||||||
"@types/http-errors": "^2.0.4",
|
"@types/http-errors": "^2.0.4",
|
||||||
"@types/jsdom": "^21.1.6",
|
"@types/jsdom": "^21.1.6",
|
||||||
"@types/mocha": "^10.0.6",
|
"@types/mocha": "^10.0.6",
|
||||||
|
|
18
src/static/oidc/consent.html
Normal file
18
src/static/oidc/consent.html
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Consent</title>
|
||||||
|
<script>
|
||||||
|
window.onload = ()=>{
|
||||||
|
const form = document.getElementsByTagName('form')[0]
|
||||||
|
form.action = '/interaction/' + new URLSearchParams(window.location.search).get('state')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<form action="" autocomplete="off" method="post">
|
||||||
|
<input type="hidden" name="prompt" value="consent"/>
|
||||||
|
<button autofocus type="submit" class="login login-submit">Continue</button>
|
||||||
|
</form>
|
||||||
|
</body>
|
53
src/static/oidc/login.html
Normal file
53
src/static/oidc/login.html
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Title</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script>
|
||||||
|
function login() {
|
||||||
|
const form = document.querySelector('form');
|
||||||
|
form.addEventListener('submit', function (event) {
|
||||||
|
event.preventDefault();
|
||||||
|
const formData = new FormData(form);
|
||||||
|
const data = {};
|
||||||
|
formData.forEach((value, key) => {
|
||||||
|
data[key] = value;
|
||||||
|
});
|
||||||
|
const sessionId = new URLSearchParams(window.location.search).get('state');
|
||||||
|
|
||||||
|
fetch('/interaction/'+sessionId, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
redirect: 'follow',
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
}).then(response => {
|
||||||
|
if (response.ok) {
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
|
throw new Error('Network response was not ok.');
|
||||||
|
}).then(data => {
|
||||||
|
if (data.status === 'success') {
|
||||||
|
window.location.href = data.redirect;
|
||||||
|
} else {
|
||||||
|
document.getElementById('error').innerText = "Error signing in";
|
||||||
|
}
|
||||||
|
}).catch(error => {
|
||||||
|
console.error('There has been a problem with your fetch operation:', error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
window.onload = login;
|
||||||
|
</script>
|
||||||
|
<form autocomplete="off" method="post">
|
||||||
|
<input type="hidden" name="prompt" value="login"/>
|
||||||
|
<input required type="text" name="login" placeholder="Enter any login"/>
|
||||||
|
<input required type="password" name="password" placeholder="and password"/>
|
||||||
|
<button type="submit" class="login login-submit">Sign-in</button>
|
||||||
|
<div id="error"></div>
|
||||||
|
</form>
|
||||||
|
</body>
|
||||||
|
</html>
|
Loading…
Add table
Add a link
Reference in a new issue