Skip to content

Commit 8a66b04

Browse files
authored
chore: Added client credentials grant for API calling from services. (#6325)
* chore: Added client credentials grant for API calling from services. * chore: Added authentication documentation
1 parent cda81dd commit 8a66b04

File tree

3 files changed

+70
-17
lines changed

3 files changed

+70
-17
lines changed

doc/api/http_api.md

+48-10
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ Portal maps the internal userid to an etherpad author.
2828
#### Request
2929

3030
```http
31-
GET /api/1/createAuthorIfNotExistsFor?apikey=secret&name=Michael&authorMapper=7
31+
GET /api/1/createAuthorIfNotExistsFor?name=Michael&authorMapper=7
3232
```
3333

3434

@@ -42,7 +42,7 @@ GET /api/1/createAuthorIfNotExistsFor?apikey=secret&name=Michael&authorMapper=7
4242
> Portal maps the internal userid to an etherpad group:
4343
4444
```http
45-
GET http://pad.domain/api/1/createGroupIfNotExistsFor?apikey=secret&groupMapper=7
45+
GET http://pad.domain/api/1/createGroupIfNotExistsFor?groupMapper=7
4646
```
4747

4848
### Response
@@ -56,7 +56,7 @@ GET http://pad.domain/api/1/createGroupIfNotExistsFor?apikey=secret&groupMapper=
5656
#### Request
5757

5858
```http
59-
GET http://pad.domain/api/1/createGroupPad?apikey=secret&groupID=g.s8oes9dhwrvt0zif&padName=samplePad&text=This is the first sentence in the pad
59+
GET http://pad.domain/api/1/createGroupPad?groupID=g.s8oes9dhwrvt0zif&padName=samplePad&text=This is the first sentence in the pad
6060
```
6161

6262
#### Response
@@ -70,7 +70,7 @@ GET http://pad.domain/api/1/createGroupPad?apikey=secret&groupID=g.s8oes9dhwrvt0
7070
#### Request
7171

7272
```http
73-
GET http://pad.domain/api/1/createSession?apikey=secret&groupID=g.s8oes9dhwrvt0zif&authorID=a.s8oes9dhwrvt0zif&validUntil=1312201246
73+
GET http://pad.domain/api/1/createSession?groupID=g.s8oes9dhwrvt0zif&authorID=a.s8oes9dhwrvt0zif&validUntil=1312201246
7474
```
7575

7676
### Response
@@ -87,7 +87,7 @@ A portal (such as WordPress) wants to transform the contents of a pad that multi
8787

8888
Portal retrieves the contents of the pad for entry into the db as a blog post:
8989

90-
> Request: `http://pad.domain/api/1/getText?apikey=secret&padID=g.s8oes9dhwrvt0zif$123`
90+
> Request: `http://pad.domain/api/1/getText?&padID=g.s8oes9dhwrvt0zif$123`
9191
>
9292
> Response: `{code: 0, message:"ok", data: {text:"Welcome Text"}}`
9393
@@ -108,23 +108,23 @@ The API is accessible via HTTP. Starting from **1.8**, API endpoints can be invo
108108

109109
The URL of the HTTP request is of the form: `/api/$APIVERSION/$FUNCTIONNAME`. $APIVERSION depends on the endpoint you want to use. Depending on the verb you use (GET or POST) **parameters** can be passed differently.
110110

111-
When invoking via GET (mandatory until **1.7.5** included), parameters must be included in the query string (example: `/api/$APIVERSION/$FUNCTIONNAME?apikey=<APIKEY>&param1=value1`). Please note that starting with nodejs 8.14+ the total size of HTTP request headers has been capped to 8192 bytes. This limits the quantity of data that can be sent in an API request.
111+
When invoking via GET (mandatory until **1.7.5** included), parameters must be included in the query string (example: `/api/$APIVERSION/$FUNCTIONNAME?param1=value1`). Please note that starting with nodejs 8.14+ the total size of HTTP request headers has been capped to 8192 bytes. This limits the quantity of data that can be sent in an API request.
112112

113113
Starting from Etherpad **1.8** it is also possible to invoke the HTTP API via POST. In this case, querystring parameters will still be accepted, but **any parameter with the same name sent via POST will take precedence**. If you need to send large chunks of text (for example, for `setText()`) it is advisable to invoke via POST.
114114

115115
Example with cURL using GET (toy example, no encoding):
116116
```
117-
curl "http://pad.domain/api/1/setText?apikey=secret&padID=padname&text=this_text_will_NOT_be_encoded_by_curl_use_next_example"
117+
curl "http://pad.domain/api/1/setText?padID=padname&text=this_text_will_NOT_be_encoded_by_curl_use_next_example"
118118
```
119119

120120
Example with cURL using GET (better example, encodes text):
121121
```
122-
curl "http://pad.domain/api/1/setText?apikey=secret&padID=padname" --get --data-urlencode "text=Text sent via GET with proper encoding. For big documents, please use POST"
122+
curl "http://pad.domain/api/1/setText?padID=padname" --get --data-urlencode "text=Text sent via GET with proper encoding. For big documents, please use POST"
123123
```
124124

125125
Example with cURL using POST:
126126
```
127-
curl "http://pad.domain/api/1/setText?apikey=secret&padID=padname" --data-urlencode "text=Text sent via POST with proper encoding. For big texts (>8 KB), use this method"
127+
curl "http://pad.domain/api/1/setText?padID=padname" --data-urlencode "text=Text sent via POST with proper encoding. For big texts (>8 KB), use this method"
128128
```
129129

130130
### Response Format
@@ -161,7 +161,45 @@ Responses are valid JSON in the following format:
161161

162162
### Authentication
163163

164-
Authentication works via a token that is sent with each request as a post parameter. There is a single token per Etherpad deployment. This token will be random string, generated by Etherpad at the first start. It will be saved in APIKEY.txt in the root folder of Etherpad. Only Etherpad and the requesting application knows this key. Token management will not be exposed through this API.
164+
Authentication works via an OAuth token that is sent with each request as a post parameter. You can add new clients that can sign in via the API by adding new entries to the sso section in the settings.json.
165+
166+
167+
#### Example for browser login clients
168+
169+
This example illustrates how to add a new client that can sign in via the API using the browser login method. This method is used for users trying to sign in to the API via the browser. You can log in with the users in the settings.json file. The redirect URI is the URL where the user is redirected after the login. This is normally your etherpad instance url.
170+
171+
```json
172+
{
173+
"client_id": "admin_client",
174+
"client_secret": "admin",
175+
"grant_types": ["authorization_code"],
176+
"response_types": ["code"],
177+
"redirect_uris": ["http://my-etherpad-instance.com"],
178+
}
179+
```
180+
181+
182+
#### Example for services
183+
184+
This example illustrates how to add a new client that can sign in via the API using the client credentials method. This method is used for services trying to sign in to the API where there is no browser.
185+
E.g. a service that creates a pad for a user or a service that inserts a text into a pad. Just make sure that the secret is complex enough as anybody who knows the secret can access the API.
186+
187+
```json
188+
{
189+
"client_id": "client_credentials",
190+
"redirect_uris": [],
191+
"response_types": [],
192+
"grant_types": ["client_credentials"],
193+
"client_secret": "client_credentials",
194+
"extraParams": [
195+
{
196+
"name": "admin",
197+
"value": "true"
198+
}
199+
]
200+
}
201+
```
202+
165203

166204
### Node Interoperability
167205

src/node/security/OAuth2Provider.ts

+21-7
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import express, {Request, Response} from 'express';
99
import {format} from 'url'
1010
import {ParsedUrlQuery} from "node:querystring";
1111
import {Http2ServerRequest, Http2ServerResponse} from "node:http2";
12+
import {MapArrayType} from "../types/MapType";
1213

1314
const configuration: Configuration = {
1415
scopes: ['openid', 'profile', 'email'],
@@ -19,7 +20,6 @@ const configuration: Configuration = {
1920
is_admin: boolean;
2021
}
2122
}
22-
2323
const usersArray1 = Object.keys(users).map((username) => ({
2424
username,
2525
...users[username]
@@ -99,28 +99,29 @@ export const expressCreateServer = async (hookName: string, args: ArgsExpressTyp
9999
features:{
100100
userinfo: {enabled: true},
101101
claimsParameter: {enabled: true},
102+
clientCredentials: {enabled: true},
102103
devInteractions: {enabled: false},
103104
resourceIndicators: {enabled: true, defaultResource(ctx) {
104105
return ctx.origin;
105106
},
106107
getResourceServerInfo(ctx, resourceIndicator, client) {
107108
return {
108-
scope: client.scope as string,
109+
scope: "openid",
109110
audience: 'account',
110111
accessTokenFormat: 'jwt',
111112
};
112113
},
113114
useGrantedResource(ctx, model) {
114115
return true;
115-
},},
116+
},
117+
},
116118
jwtResponseModes: {enabled: true},
117119
},
118120
clientBasedCORS: (ctx, origin, client) => {
119121
return true
120122
},
123+
extraParams: [],
121124
extraTokenClaims: async (ctx, token) => {
122-
123-
124125
if(token.kind === 'AccessToken') {
125126
// Add your custom claims here. For example:
126127
const users = settings.users as {
@@ -139,6 +140,19 @@ export const expressCreateServer = async (hookName: string, args: ArgsExpressTyp
139140
return {
140141
admin: account?.is_admin
141142
};
143+
} else if (token.kind === "ClientCredentials") {
144+
let extraParams: MapArrayType<string> = {}
145+
146+
settings.sso.clients
147+
.filter((client:any) => client.client_id === token.clientId)
148+
.forEach((client:any) => {
149+
if(client.extraParams !== undefined) {
150+
client.extraParams.forEach((param:any) => {
151+
extraParams[param.name] = param.value
152+
})
153+
}
154+
})
155+
return extraParams
142156
}
143157
},
144158
clients: settings.sso.clients
@@ -252,7 +266,7 @@ export const expressCreateServer = async (hookName: string, args: ArgsExpressTyp
252266

253267
args.app.use('/views/', express.static(path.join(settings.root,'src','static', 'oidc'), {maxAge: 1000 * 60 * 60 * 24}));
254268

255-
/*
269+
256270
oidc.on('authorization.error', (ctx, error) => {
257271
console.log('authorization.error', error);
258272
})
@@ -268,7 +282,7 @@ export const expressCreateServer = async (hookName: string, args: ArgsExpressTyp
268282
})
269283
oidc.on('revocation.error', (ctx, error) => {
270284
console.log('revocation.error', error);
271-
})*/
285+
})
272286
args.app.use("/oidc", oidc.callback());
273287
//cb();
274288
}

src/static/js/pluginfw/installer.ts

+1
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ const migratePluginsFromNodeModules = async () => {
6060
const cmd = ['pnpm', 'ls', '--long', '--json', '--depth=0', '--no-production'];
6161
const [{dependencies = {}}] = JSON.parse(await runCmd(cmd,
6262
{stdio: [null, 'string']}));
63+
6364
await Promise.all(Object.entries(dependencies)
6465
.filter(([pkg, info]) => pkg.startsWith(plugins.prefix) && pkg !== 'ep_etherpad-lite')
6566
.map(async ([pkg, info]) => {

0 commit comments

Comments
 (0)