n8n-nodes-oidc-login 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +108 -0
- package/dist/credentials/OidcBearerApi.credentials.d.ts +22 -0
- package/dist/credentials/OidcBearerApi.credentials.js +43 -0
- package/dist/credentials/OidcProviderApi.credentials.d.ts +12 -0
- package/dist/credentials/OidcProviderApi.credentials.js +47 -0
- package/dist/nodes/OidcLoginTrigger/OidcLoginTrigger.node.d.ts +5 -0
- package/dist/nodes/OidcLoginTrigger/OidcLoginTrigger.node.js +180 -0
- package/package.json +32 -0
package/README.md
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# n8n-nodes-oidc-login
|
|
2
|
+
|
|
3
|
+
Community-Node-Package für einen **OIDC-Login-Flow, der von einem eingehenden Webhook/Web-Formular
|
|
4
|
+
ausgelöst wird** und dessen Access Token danach in nachfolgenden HTTP-Request-Nodes per eigenem
|
|
5
|
+
Credential-Typ genutzt werden kann.
|
|
6
|
+
|
|
7
|
+
## Enthaltene Bausteine
|
|
8
|
+
|
|
9
|
+
| Datei | Zweck |
|
|
10
|
+
|---|---|
|
|
11
|
+
| `credentials/OidcProviderApi.credentials.ts` | Statische Provider-Settings (Client ID/Secret, Authorization-/Token-URL). Wird **vom Trigger-Node** gelesen. |
|
|
12
|
+
| `nodes/OidcLoginTrigger/OidcLoginTrigger.node.ts` | Webhook-Trigger mit zwei Endpunkten: Login-Start (Redirect zum OIDC-Server) und Callback (Code-Exchange). |
|
|
13
|
+
| `credentials/OidcBearerApi.credentials.ts` | Credential mit Feld `Access Token`, **wählbar in jedem HTTP Request Node**. Wert wird per Expression pro Ausführung befüllt. |
|
|
14
|
+
|
|
15
|
+
## Antwortverhalten des Callback-Webhooks
|
|
16
|
+
|
|
17
|
+
Der Callback-Endpunkt (`.../oidc-login/callback`) nutzt `responseMode: 'lastNode'`: Der Browser/Client,
|
|
18
|
+
der nach dem Gitea-Login auf diese URL zurückgeleitet wird, bekommt **keine sofortige "Login erfolgreich"-Seite**,
|
|
19
|
+
sondern n8n lässt die Verbindung offen, bis der komplette Workflow durchgelaufen ist, und schickt dann
|
|
20
|
+
automatisch die Ausgabe der **letzten Node** im Workflow als JSON-Antwort zurück. Baut man z. B. einen
|
|
21
|
+
HTTP-Request-Node (Repos abrufen) + Set-Node (Formatierung) dahinter, landet exakt deren Output als
|
|
22
|
+
JSON-Antwort beim aufrufenden Client.
|
|
23
|
+
|
|
24
|
+
Für Fehlerfälle (ungültiger `state`, Token-Austausch fehlgeschlagen) wird weiterhin sofort und manuell
|
|
25
|
+
geantwortet (HTTP 400/502), da der Workflow dort nicht weiterläuft.
|
|
26
|
+
|
|
27
|
+
## Warum zwei getrennte Credential-Typen?
|
|
28
|
+
|
|
29
|
+
- `OidcProviderApi` ist **statisch** (Client ID/Secret ändern sich nicht) → normale, fest gespeicherte Credential.
|
|
30
|
+
- `OidcBearerApi` ist **dynamisch pro Endnutzer/Ausführung** — n8n löst Expressions in Credential-Feldern
|
|
31
|
+
innerhalb des Kontexts der jeweiligen Workflow-Ausführung auf. Damit bekommt jeder Website-Besucher
|
|
32
|
+
in seiner eigenen Ausführung automatisch seinen eigenen, frisch erhaltenen Token — ohne dass Tokens
|
|
33
|
+
zwischen Nutzern/Ausführungen vermischt werden.
|
|
34
|
+
|
|
35
|
+
## Ablauf
|
|
36
|
+
|
|
37
|
+
1. Endnutzer ruft `https://<n8n>/webhook/oidc-login` auf (z. B. Link/Redirect aus einem Formular).
|
|
38
|
+
2. `OidcLoginTrigger` generiert `state` + PKCE `code_verifier`/`code_challenge`, merkt sich beides
|
|
39
|
+
kurzzeitig in Workflow-Static-Data und leitet (HTTP 302) zur `authorizationUrl` des OIDC-Servers weiter.
|
|
40
|
+
3. Nutzer meldet sich beim Auth-Server an. Dieser leitet zurück auf
|
|
41
|
+
`https://<n8n>/webhook/oidc-login/callback?code=...&state=...`.
|
|
42
|
+
4. `OidcLoginTrigger` verifiziert `state`, tauscht `code` gegen Access Token (`POST` an `tokenUrl`,
|
|
43
|
+
inkl. `code_verifier`) und gibt den Token-Response-JSON (`access_token`, `id_token`, `expires_in`, …)
|
|
44
|
+
als Workflow-Output aus. Der Browser bekommt eine einfache "Anmeldung erfolgreich"-Seite.
|
|
45
|
+
5. Alle nachfolgenden Nodes im Workflow haben Zugriff auf `$json.access_token`.
|
|
46
|
+
|
|
47
|
+
## Einrichtung mit Gitea als OIDC-Server
|
|
48
|
+
|
|
49
|
+
Gitea selbst als Auth-Server nutzt feste, nicht per Discovery konfigurierbare Endpunkte:
|
|
50
|
+
|
|
51
|
+
- Authorization: `https://<gitea>/login/oauth/authorize`
|
|
52
|
+
- Token: `https://<gitea>/login/oauth/access_token`
|
|
53
|
+
- Userinfo (optional, für Profildaten): `https://<gitea>/login/oauth/userinfo`
|
|
54
|
+
|
|
55
|
+
Schritte in Gitea:
|
|
56
|
+
|
|
57
|
+
1. Als der Nutzer, unter dem die App laufen soll (oder als Admin unter *Site Administration →
|
|
58
|
+
Applications* für eine instanzweite OAuth2-App): **Settings → Applications → "Create a new OAuth2 Application"**.
|
|
59
|
+
2. **Application Name** vergeben, als **Redirect URI** exakt die Callback-URL des `OidcLoginTrigger`
|
|
60
|
+
eintragen (z. B. `https://<n8n>/webhook/oidc-login/callback`) — muss zeichengenau übereinstimmen,
|
|
61
|
+
sonst meldet Gitea `redirect_uri_mismatch`.
|
|
62
|
+
3. Gitea zeigt **Client ID** und **Client Secret** an (Secret nur einmal sichtbar!) → in die
|
|
63
|
+
`OIDC Provider`-Credential in n8n eintragen.
|
|
64
|
+
4. **Scope**: `openid profile email` funktioniert bei Gitea für den OIDC-Login. Für Zugriff auf die
|
|
65
|
+
Gitea-API selbst (z. B. Repos lesen) ggf. zusätzlich Gitea-spezifische Scopes wie `read:repository`
|
|
66
|
+
ergänzen (Feld "Scope" im `OidcLoginTrigger`-Node), siehe Gitea-Doku zu "granular scopes".
|
|
67
|
+
5. Optional: ein nachgeschalteter HTTP-Request-Node auf `https://<gitea>/login/oauth/userinfo` (GET,
|
|
68
|
+
Credential = `OIDC Bearer Token`) liefert die Profildaten des eingeloggten Gitea-Nutzers — guter Test,
|
|
69
|
+
ob der Flow funktioniert.
|
|
70
|
+
|
|
71
|
+
## Einrichtung (allgemein)
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
npm install
|
|
75
|
+
npm run build
|
|
76
|
+
npm link
|
|
77
|
+
# im n8n Custom-Extensions-Ordner:
|
|
78
|
+
npm link n8n-nodes-oidc-login
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
n8n neu starten. Danach:
|
|
82
|
+
|
|
83
|
+
1. **Credential "OIDC Provider"** anlegen: Authorization URL, Token URL, Client ID, (optional) Client Secret
|
|
84
|
+
eures OIDC-Servers eintragen.
|
|
85
|
+
2. **Node "OIDC Login Trigger"** in den Workflow ziehen, die Credential auswählen, `Path` setzen
|
|
86
|
+
(z. B. `oidc-login`). n8n zeigt dir automatisch zwei Webhook-URLs an (Start + `.../callback`).
|
|
87
|
+
3. Beim OIDC-Provider den **Callback-Webhook** (`.../oidc-login/callback`) als **Redirect URI** des Clients
|
|
88
|
+
registrieren.
|
|
89
|
+
4. **Credential "OIDC Bearer Token"** anlegen: beim Feld "Access Token" auf **Expression** umschalten
|
|
90
|
+
und z. B. `={{ $json.access_token }}` eintragen (oder `={{ $('OIDC Login Trigger').item.json.access_token }}`,
|
|
91
|
+
falls dazwischen weitere Nodes liegen).
|
|
92
|
+
5. Nachfolgenden **HTTP Request Node** hinzufügen → Authentication → "Predefined Credential Type" →
|
|
93
|
+
"OIDC Bearer Token" auswählen. Der `Authorization: Bearer <token>`-Header wird automatisch gesetzt.
|
|
94
|
+
|
|
95
|
+
## Wichtige Einschränkungen / Hinweise
|
|
96
|
+
|
|
97
|
+
- **Ein Token gilt nur für die Ausführung, in der er erzeugt wurde.** Das ist hier so gewollt
|
|
98
|
+
(jeder Endnutzer meldet sich selbst an), aber es bedeutet auch: Der Token wird nicht dauerhaft
|
|
99
|
+
gespeichert oder automatisch erneuert (kein Refresh-Token-Handling in diesem Minimalbeispiel).
|
|
100
|
+
Für Refresh-Logik müsste `refresh_token` aus `tokenResponse` zusätzlich ausgewertet und ein eigener
|
|
101
|
+
Refresh-Aufruf ergänzt werden.
|
|
102
|
+
- **State-Speicherung** erfolgt über `this.getWorkflowStaticData('node')` (persistiert in der n8n-DB,
|
|
103
|
+
TTL 10 Minuten). Für sehr hohe Login-Frequenzen ggf. auf eine externe Datenbank/Cache umstellen.
|
|
104
|
+
- **Sicherheit**: PKCE (S256) wird immer verwendet; Client Secret ist optional (für "public" Clients).
|
|
105
|
+
Stellt sicher, dass die n8n-Instanz per HTTPS erreichbar ist, da sensible Tokens durchlaufen.
|
|
106
|
+
- Der HTTP Request Node muss in **derselben Workflow-Ausführung** wie der Login-Callback laufen
|
|
107
|
+
(direkt oder indirekt nachgeschaltet) — die Credential kann keinen Token aus einer anderen,
|
|
108
|
+
bereits abgeschlossenen Ausführung "nachladen".
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { ICredentialType, INodeProperties, IAuthenticateGeneric } from 'n8n-workflow';
|
|
2
|
+
/**
|
|
3
|
+
* Credential für nachfolgende HTTP-Request-Nodes.
|
|
4
|
+
*
|
|
5
|
+
* WICHTIG: "Access Token" muss im Credential-UI über den kleinen
|
|
6
|
+
* Expression-Schalter (⚡/"Add Expression") auf einen Ausdruck umgestellt
|
|
7
|
+
* werden, z. B.:
|
|
8
|
+
* ={{ $json.access_token }}
|
|
9
|
+
* oder, falls zwischen Trigger und HTTP-Request-Node weitere Nodes liegen:
|
|
10
|
+
* ={{ $('OIDC Login Trigger').item.json.access_token }}
|
|
11
|
+
*
|
|
12
|
+
* n8n löst Expressions in Credential-Feldern pro Workflow-AUSFÜHRUNG auf
|
|
13
|
+
* (nicht global), d. h. jeder Endnutzer bekommt in seiner eigenen
|
|
14
|
+
* Ausführung automatisch seinen eigenen, gerade erhaltenen Token.
|
|
15
|
+
*/
|
|
16
|
+
export declare class OidcBearerApi implements ICredentialType {
|
|
17
|
+
name: string;
|
|
18
|
+
displayName: string;
|
|
19
|
+
documentationUrl: string;
|
|
20
|
+
properties: INodeProperties[];
|
|
21
|
+
authenticate: IAuthenticateGeneric;
|
|
22
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.OidcBearerApi = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Credential für nachfolgende HTTP-Request-Nodes.
|
|
6
|
+
*
|
|
7
|
+
* WICHTIG: "Access Token" muss im Credential-UI über den kleinen
|
|
8
|
+
* Expression-Schalter (⚡/"Add Expression") auf einen Ausdruck umgestellt
|
|
9
|
+
* werden, z. B.:
|
|
10
|
+
* ={{ $json.access_token }}
|
|
11
|
+
* oder, falls zwischen Trigger und HTTP-Request-Node weitere Nodes liegen:
|
|
12
|
+
* ={{ $('OIDC Login Trigger').item.json.access_token }}
|
|
13
|
+
*
|
|
14
|
+
* n8n löst Expressions in Credential-Feldern pro Workflow-AUSFÜHRUNG auf
|
|
15
|
+
* (nicht global), d. h. jeder Endnutzer bekommt in seiner eigenen
|
|
16
|
+
* Ausführung automatisch seinen eigenen, gerade erhaltenen Token.
|
|
17
|
+
*/
|
|
18
|
+
class OidcBearerApi {
|
|
19
|
+
constructor() {
|
|
20
|
+
this.name = 'oidcBearerApi';
|
|
21
|
+
this.displayName = 'OIDC Bearer Token';
|
|
22
|
+
this.documentationUrl = 'https://docs.n8n.io/code/expressions/';
|
|
23
|
+
this.properties = [
|
|
24
|
+
{
|
|
25
|
+
displayName: 'Access Token',
|
|
26
|
+
name: 'accessToken',
|
|
27
|
+
type: 'string',
|
|
28
|
+
typeOptions: { password: true },
|
|
29
|
+
default: '',
|
|
30
|
+
description: 'In der Regel als Expression setzen, z. B. ={{ $json.access_token }} — verweist auf den vom OIDC Login Trigger in DIESER Ausführung erhaltenen Token.',
|
|
31
|
+
},
|
|
32
|
+
];
|
|
33
|
+
this.authenticate = {
|
|
34
|
+
type: 'generic',
|
|
35
|
+
properties: {
|
|
36
|
+
headers: {
|
|
37
|
+
Authorization: '=Bearer {{$credentials.accessToken}}',
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
exports.OidcBearerApi = OidcBearerApi;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { ICredentialType, INodeProperties } from 'n8n-workflow';
|
|
2
|
+
/**
|
|
3
|
+
* Statische Konfiguration des OIDC-Providers. Wird ausschließlich vom
|
|
4
|
+
* OidcLoginTrigger-Node gelesen (per this.getCredentials), NICHT für
|
|
5
|
+
* "Generic Auth" an HTTP-Request-Nodes gedacht.
|
|
6
|
+
*/
|
|
7
|
+
export declare class OidcProviderApi implements ICredentialType {
|
|
8
|
+
name: string;
|
|
9
|
+
displayName: string;
|
|
10
|
+
documentationUrl: string;
|
|
11
|
+
properties: INodeProperties[];
|
|
12
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.OidcProviderApi = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Statische Konfiguration des OIDC-Providers. Wird ausschließlich vom
|
|
6
|
+
* OidcLoginTrigger-Node gelesen (per this.getCredentials), NICHT für
|
|
7
|
+
* "Generic Auth" an HTTP-Request-Nodes gedacht.
|
|
8
|
+
*/
|
|
9
|
+
class OidcProviderApi {
|
|
10
|
+
constructor() {
|
|
11
|
+
this.name = 'oidcProviderApi';
|
|
12
|
+
this.displayName = 'OIDC Provider';
|
|
13
|
+
this.documentationUrl = 'https://openid.net/specs/openid-connect-core-1_0.html';
|
|
14
|
+
this.properties = [
|
|
15
|
+
{
|
|
16
|
+
displayName: 'Authorization URL',
|
|
17
|
+
name: 'authorizationUrl',
|
|
18
|
+
type: 'string',
|
|
19
|
+
default: 'https://gitea.example.com/login/oauth/authorize',
|
|
20
|
+
description: 'Bei Gitea fest: <Gitea-URL>/login/oauth/authorize',
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
displayName: 'Token URL',
|
|
24
|
+
name: 'tokenUrl',
|
|
25
|
+
type: 'string',
|
|
26
|
+
default: 'https://gitea.example.com/login/oauth/access_token',
|
|
27
|
+
description: 'Bei Gitea fest: <Gitea-URL>/login/oauth/access_token',
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
displayName: 'Client ID',
|
|
31
|
+
name: 'clientId',
|
|
32
|
+
type: 'string',
|
|
33
|
+
default: '',
|
|
34
|
+
required: true,
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
displayName: 'Client Secret',
|
|
38
|
+
name: 'clientSecret',
|
|
39
|
+
type: 'string',
|
|
40
|
+
typeOptions: { password: true },
|
|
41
|
+
default: '',
|
|
42
|
+
description: 'Leer lassen, falls der Client als "public" (nur PKCE, kein Secret) registriert ist',
|
|
43
|
+
},
|
|
44
|
+
];
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
exports.OidcProviderApi = OidcProviderApi;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { IHookFunctions, IWebhookFunctions, INodeType, INodeTypeDescription, IWebhookResponseData } from 'n8n-workflow';
|
|
2
|
+
export declare class OidcLoginTrigger implements INodeType {
|
|
3
|
+
description: INodeTypeDescription;
|
|
4
|
+
webhook(this: IHookFunctions & IWebhookFunctions): Promise<IWebhookResponseData>;
|
|
5
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.OidcLoginTrigger = void 0;
|
|
37
|
+
const crypto = __importStar(require("node:crypto"));
|
|
38
|
+
const n8n_workflow_1 = require("n8n-workflow");
|
|
39
|
+
const PENDING_TTL_MS = 10 * 60 * 1000; // 10 Minuten
|
|
40
|
+
function base64url(input) {
|
|
41
|
+
return input.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
42
|
+
}
|
|
43
|
+
class OidcLoginTrigger {
|
|
44
|
+
constructor() {
|
|
45
|
+
this.description = {
|
|
46
|
+
displayName: 'OIDC Login Trigger',
|
|
47
|
+
name: 'oidcLoginTrigger',
|
|
48
|
+
icon: 'fa:sign-in-alt',
|
|
49
|
+
group: ['trigger'],
|
|
50
|
+
version: 1,
|
|
51
|
+
description: 'Leitet einen eingehenden Webhook-/Formular-Request zum OIDC-Login weiter und stellt danach den Access Token als Workflow-Output bereit',
|
|
52
|
+
defaults: { name: 'OIDC Login Trigger' },
|
|
53
|
+
inputs: [],
|
|
54
|
+
outputs: [n8n_workflow_1.NodeConnectionTypes.Main],
|
|
55
|
+
credentials: [
|
|
56
|
+
{
|
|
57
|
+
name: 'oidcProviderApi',
|
|
58
|
+
required: true,
|
|
59
|
+
},
|
|
60
|
+
],
|
|
61
|
+
webhooks: [
|
|
62
|
+
{
|
|
63
|
+
name: 'default',
|
|
64
|
+
httpMethod: 'GET',
|
|
65
|
+
responseMode: 'onReceived',
|
|
66
|
+
path: '={{$parameter["path"]}}',
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
name: 'callback',
|
|
70
|
+
httpMethod: 'GET',
|
|
71
|
+
// 'lastNode': n8n wartet mit der HTTP-Antwort, bis der gesamte Workflow
|
|
72
|
+
// durchgelaufen ist, und sendet dann automatisch die Ausgabe der letzten
|
|
73
|
+
// Node (hier: die formatierte Repo-Liste) als JSON-Response zurück.
|
|
74
|
+
responseMode: 'lastNode',
|
|
75
|
+
path: '={{$parameter["path"]}}/callback',
|
|
76
|
+
},
|
|
77
|
+
],
|
|
78
|
+
properties: [
|
|
79
|
+
{
|
|
80
|
+
displayName: 'Path',
|
|
81
|
+
name: 'path',
|
|
82
|
+
type: 'string',
|
|
83
|
+
default: 'oidc-login',
|
|
84
|
+
description: 'Basis-Pfad des Login-Webhooks. Der Callback läuft automatisch unter <Pfad>/callback — diese URL muss beim OIDC-Provider als Redirect URI hinterlegt werden.',
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
displayName: 'Scope',
|
|
88
|
+
name: 'scope',
|
|
89
|
+
type: 'string',
|
|
90
|
+
default: 'openid profile email',
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
async webhook() {
|
|
96
|
+
var _a;
|
|
97
|
+
const webhookName = this.getWebhookName();
|
|
98
|
+
const req = this.getRequestObject();
|
|
99
|
+
const res = this.getResponseObject();
|
|
100
|
+
const credentials = (await this.getCredentials('oidcProviderApi'));
|
|
101
|
+
const staticData = this.getWorkflowStaticData('node');
|
|
102
|
+
if (!staticData.pending) {
|
|
103
|
+
staticData.pending = {};
|
|
104
|
+
}
|
|
105
|
+
// Abgelaufene Login-Versuche aufräumen
|
|
106
|
+
const now = Date.now();
|
|
107
|
+
for (const [key, value] of Object.entries(staticData.pending)) {
|
|
108
|
+
if (now - value.createdAt > PENDING_TTL_MS) {
|
|
109
|
+
delete staticData.pending[key];
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (webhookName === 'default') {
|
|
113
|
+
// --- Schritt 1: eingehender Request -> Redirect zum OIDC-Server ---
|
|
114
|
+
const state = crypto.randomBytes(16).toString('hex');
|
|
115
|
+
const codeVerifier = base64url(crypto.randomBytes(32));
|
|
116
|
+
const codeChallenge = base64url(crypto.createHash('sha256').update(codeVerifier).digest());
|
|
117
|
+
staticData.pending[state] = { codeVerifier, createdAt: now };
|
|
118
|
+
const redirectUri = this.getNodeWebhookUrl('callback');
|
|
119
|
+
const scope = this.getNodeParameter('scope') || 'openid profile email';
|
|
120
|
+
const authUrl = new URL(credentials.authorizationUrl);
|
|
121
|
+
authUrl.searchParams.set('response_type', 'code');
|
|
122
|
+
authUrl.searchParams.set('client_id', credentials.clientId);
|
|
123
|
+
authUrl.searchParams.set('redirect_uri', redirectUri);
|
|
124
|
+
authUrl.searchParams.set('scope', scope);
|
|
125
|
+
authUrl.searchParams.set('state', state);
|
|
126
|
+
authUrl.searchParams.set('code_challenge', codeChallenge);
|
|
127
|
+
authUrl.searchParams.set('code_challenge_method', 'S256');
|
|
128
|
+
res.writeHead(302, { Location: authUrl.toString() });
|
|
129
|
+
res.end();
|
|
130
|
+
// Workflow läuft für diesen Request NICHT weiter - erst der Callback tut das
|
|
131
|
+
return { noWebhookResponse: true };
|
|
132
|
+
}
|
|
133
|
+
// --- Schritt 2: Callback vom OIDC-Server ---
|
|
134
|
+
const query = req.query;
|
|
135
|
+
if (query.error) {
|
|
136
|
+
res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
137
|
+
res.end(`<h1>Anmeldung fehlgeschlagen</h1><p>${(_a = query.error_description) !== null && _a !== void 0 ? _a : query.error}</p>`);
|
|
138
|
+
return { noWebhookResponse: true };
|
|
139
|
+
}
|
|
140
|
+
const pending = query.state ? staticData.pending[query.state] : undefined;
|
|
141
|
+
if (!query.code || !query.state || !pending) {
|
|
142
|
+
res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
143
|
+
res.end('<h1>Ungültige oder abgelaufene Login-Anfrage</h1>');
|
|
144
|
+
return { noWebhookResponse: true };
|
|
145
|
+
}
|
|
146
|
+
delete staticData.pending[query.state];
|
|
147
|
+
const redirectUri = this.getNodeWebhookUrl('callback');
|
|
148
|
+
let tokenResponse;
|
|
149
|
+
try {
|
|
150
|
+
tokenResponse = (await this.helpers.httpRequest({
|
|
151
|
+
method: 'POST',
|
|
152
|
+
url: credentials.tokenUrl,
|
|
153
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
154
|
+
body: new URLSearchParams({
|
|
155
|
+
grant_type: 'authorization_code',
|
|
156
|
+
code: query.code,
|
|
157
|
+
redirect_uri: redirectUri,
|
|
158
|
+
client_id: credentials.clientId,
|
|
159
|
+
...(credentials.clientSecret ? { client_secret: credentials.clientSecret } : {}),
|
|
160
|
+
code_verifier: pending.codeVerifier,
|
|
161
|
+
}).toString(),
|
|
162
|
+
json: true,
|
|
163
|
+
}));
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
res.writeHead(502, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
167
|
+
res.end('<h1>Token-Austausch mit dem OIDC-Server fehlgeschlagen</h1>');
|
|
168
|
+
throw error;
|
|
169
|
+
}
|
|
170
|
+
// KEINE manuelle Antwort hier: responseMode 'lastNode' (siehe description.webhooks)
|
|
171
|
+
// sorgt dafür, dass n8n automatisch antwortet, sobald der Workflow durchgelaufen
|
|
172
|
+
// ist - mit der Ausgabe der letzten Node (z. B. die formatierte Repo-Liste) als JSON.
|
|
173
|
+
// tokenResponse (access_token, id_token, ...) steht nachfolgenden Nodes als $json
|
|
174
|
+
// zur Verfügung.
|
|
175
|
+
return {
|
|
176
|
+
workflowData: [[{ json: tokenResponse }]],
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
exports.OidcLoginTrigger = OidcLoginTrigger;
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "n8n-nodes-oidc-login",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Custom credentials + web node for n8n",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"n8n-community-node-package"
|
|
7
|
+
],
|
|
8
|
+
"license": "MIT",
|
|
9
|
+
"main": "index.js",
|
|
10
|
+
"files": [
|
|
11
|
+
"dist"
|
|
12
|
+
],
|
|
13
|
+
"n8n": {
|
|
14
|
+
"n8nNodesApiVersion": 1,
|
|
15
|
+
"credentials": [
|
|
16
|
+
"dist/credentials/OidcProviderApi.credentials.js",
|
|
17
|
+
"dist/credentials/OidcBearerApi.credentials.js"
|
|
18
|
+
],
|
|
19
|
+
"nodes": [
|
|
20
|
+
"dist/nodes/OidcLoginTrigger/OidcLoginTrigger.node.js"
|
|
21
|
+
]
|
|
22
|
+
},
|
|
23
|
+
"scripts": {
|
|
24
|
+
"build": "tsc -p tsconfig.json",
|
|
25
|
+
"dev": "tsc -p tsconfig.json --watch"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@types/node": "^26.4.0",
|
|
29
|
+
"n8n-workflow": "^1.60.0",
|
|
30
|
+
"typescript": "^5.4.0"
|
|
31
|
+
}
|
|
32
|
+
}
|