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
This commit is contained in:
SamTV12345 2024-04-13 10:32:23 +02:00 committed by GitHub
parent cda81ddb7d
commit 8a66b04b68
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 70 additions and 17 deletions

View file

@ -28,7 +28,7 @@ Portal maps the internal userid to an etherpad author.
#### Request #### Request
```http ```http
GET /api/1/createAuthorIfNotExistsFor?apikey=secret&name=Michael&authorMapper=7 GET /api/1/createAuthorIfNotExistsFor?name=Michael&authorMapper=7
``` ```
@ -42,7 +42,7 @@ GET /api/1/createAuthorIfNotExistsFor?apikey=secret&name=Michael&authorMapper=7
> Portal maps the internal userid to an etherpad group: > Portal maps the internal userid to an etherpad group:
```http ```http
GET http://pad.domain/api/1/createGroupIfNotExistsFor?apikey=secret&groupMapper=7 GET http://pad.domain/api/1/createGroupIfNotExistsFor?groupMapper=7
``` ```
### Response ### Response
@ -56,7 +56,7 @@ GET http://pad.domain/api/1/createGroupIfNotExistsFor?apikey=secret&groupMapper=
#### Request #### Request
```http ```http
GET http://pad.domain/api/1/createGroupPad?apikey=secret&groupID=g.s8oes9dhwrvt0zif&padName=samplePad&text=This is the first sentence in the pad GET http://pad.domain/api/1/createGroupPad?groupID=g.s8oes9dhwrvt0zif&padName=samplePad&text=This is the first sentence in the pad
``` ```
#### Response #### Response
@ -70,7 +70,7 @@ GET http://pad.domain/api/1/createGroupPad?apikey=secret&groupID=g.s8oes9dhwrvt0
#### Request #### Request
```http ```http
GET http://pad.domain/api/1/createSession?apikey=secret&groupID=g.s8oes9dhwrvt0zif&authorID=a.s8oes9dhwrvt0zif&validUntil=1312201246 GET http://pad.domain/api/1/createSession?groupID=g.s8oes9dhwrvt0zif&authorID=a.s8oes9dhwrvt0zif&validUntil=1312201246
``` ```
### Response ### Response
@ -87,7 +87,7 @@ A portal (such as WordPress) wants to transform the contents of a pad that multi
Portal retrieves the contents of the pad for entry into the db as a blog post: Portal retrieves the contents of the pad for entry into the db as a blog post:
> Request: `http://pad.domain/api/1/getText?apikey=secret&padID=g.s8oes9dhwrvt0zif$123` > Request: `http://pad.domain/api/1/getText?&padID=g.s8oes9dhwrvt0zif$123`
> >
> Response: `{code: 0, message:"ok", data: {text:"Welcome Text"}}` > Response: `{code: 0, message:"ok", data: {text:"Welcome Text"}}`
@ -108,23 +108,23 @@ The API is accessible via HTTP. Starting from **1.8**, API endpoints can be invo
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. 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.
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. 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.
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. 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.
Example with cURL using GET (toy example, no encoding): Example with cURL using GET (toy example, no encoding):
``` ```
curl "http://pad.domain/api/1/setText?apikey=secret&padID=padname&text=this_text_will_NOT_be_encoded_by_curl_use_next_example" curl "http://pad.domain/api/1/setText?padID=padname&text=this_text_will_NOT_be_encoded_by_curl_use_next_example"
``` ```
Example with cURL using GET (better example, encodes text): Example with cURL using GET (better example, encodes text):
``` ```
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" 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"
``` ```
Example with cURL using POST: Example with cURL using POST:
``` ```
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" 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"
``` ```
### Response Format ### Response Format
@ -161,7 +161,45 @@ Responses are valid JSON in the following format:
### Authentication ### Authentication
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. 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.
#### Example for browser login clients
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.
```json
{
"client_id": "admin_client",
"client_secret": "admin",
"grant_types": ["authorization_code"],
"response_types": ["code"],
"redirect_uris": ["http://my-etherpad-instance.com"],
}
```
#### Example for services
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.
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.
```json
{
"client_id": "client_credentials",
"redirect_uris": [],
"response_types": [],
"grant_types": ["client_credentials"],
"client_secret": "client_credentials",
"extraParams": [
{
"name": "admin",
"value": "true"
}
]
}
```
### Node Interoperability ### Node Interoperability

View file

@ -9,6 +9,7 @@ import express, {Request, Response} from 'express';
import {format} from 'url' import {format} from 'url'
import {ParsedUrlQuery} from "node:querystring"; import {ParsedUrlQuery} from "node:querystring";
import {Http2ServerRequest, Http2ServerResponse} from "node:http2"; import {Http2ServerRequest, Http2ServerResponse} from "node:http2";
import {MapArrayType} from "../types/MapType";
const configuration: Configuration = { const configuration: Configuration = {
scopes: ['openid', 'profile', 'email'], scopes: ['openid', 'profile', 'email'],
@ -19,7 +20,6 @@ const configuration: Configuration = {
is_admin: boolean; is_admin: boolean;
} }
} }
const usersArray1 = Object.keys(users).map((username) => ({ const usersArray1 = Object.keys(users).map((username) => ({
username, username,
...users[username] ...users[username]
@ -99,28 +99,29 @@ export const expressCreateServer = async (hookName: string, args: ArgsExpressTyp
features:{ features:{
userinfo: {enabled: true}, userinfo: {enabled: true},
claimsParameter: {enabled: true}, claimsParameter: {enabled: true},
clientCredentials: {enabled: true},
devInteractions: {enabled: false}, devInteractions: {enabled: false},
resourceIndicators: {enabled: true, defaultResource(ctx) { resourceIndicators: {enabled: true, defaultResource(ctx) {
return ctx.origin; return ctx.origin;
}, },
getResourceServerInfo(ctx, resourceIndicator, client) { getResourceServerInfo(ctx, resourceIndicator, client) {
return { return {
scope: client.scope as string, scope: "openid",
audience: 'account', audience: 'account',
accessTokenFormat: 'jwt', accessTokenFormat: 'jwt',
}; };
}, },
useGrantedResource(ctx, model) { useGrantedResource(ctx, model) {
return true; return true;
},}, },
},
jwtResponseModes: {enabled: true}, jwtResponseModes: {enabled: true},
}, },
clientBasedCORS: (ctx, origin, client) => { clientBasedCORS: (ctx, origin, client) => {
return true return true
}, },
extraParams: [],
extraTokenClaims: async (ctx, token) => { extraTokenClaims: async (ctx, token) => {
if(token.kind === 'AccessToken') { if(token.kind === 'AccessToken') {
// Add your custom claims here. For example: // Add your custom claims here. For example:
const users = settings.users as { const users = settings.users as {
@ -139,6 +140,19 @@ export const expressCreateServer = async (hookName: string, args: ArgsExpressTyp
return { return {
admin: account?.is_admin admin: account?.is_admin
}; };
} else if (token.kind === "ClientCredentials") {
let extraParams: MapArrayType<string> = {}
settings.sso.clients
.filter((client:any) => client.client_id === token.clientId)
.forEach((client:any) => {
if(client.extraParams !== undefined) {
client.extraParams.forEach((param:any) => {
extraParams[param.name] = param.value
})
}
})
return extraParams
} }
}, },
clients: settings.sso.clients clients: settings.sso.clients
@ -252,7 +266,7 @@ export const expressCreateServer = async (hookName: string, args: ArgsExpressTyp
args.app.use('/views/', express.static(path.join(settings.root,'src','static', 'oidc'), {maxAge: 1000 * 60 * 60 * 24})); args.app.use('/views/', express.static(path.join(settings.root,'src','static', 'oidc'), {maxAge: 1000 * 60 * 60 * 24}));
/*
oidc.on('authorization.error', (ctx, error) => { oidc.on('authorization.error', (ctx, error) => {
console.log('authorization.error', error); console.log('authorization.error', error);
}) })
@ -268,7 +282,7 @@ export const expressCreateServer = async (hookName: string, args: ArgsExpressTyp
}) })
oidc.on('revocation.error', (ctx, error) => { oidc.on('revocation.error', (ctx, error) => {
console.log('revocation.error', error); console.log('revocation.error', error);
})*/ })
args.app.use("/oidc", oidc.callback()); args.app.use("/oidc", oidc.callback());
//cb(); //cb();
} }

View file

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