@backstage/plugin-auth-backend 0.26.0 → 0.27.0-next.1

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.
@@ -0,0 +1,57 @@
1
+ 'use strict';
2
+
3
+ var errors = require('@backstage/errors');
4
+
5
+ class OidcError extends errors.CustomErrorBase {
6
+ name = "OidcError";
7
+ body;
8
+ statusCode;
9
+ constructor(errorCode, errorDescription, statusCode, cause) {
10
+ super(`${errorCode}, ${errorDescription}`, cause);
11
+ this.statusCode = statusCode;
12
+ this.body = {
13
+ error: errorCode,
14
+ error_description: errorDescription
15
+ };
16
+ }
17
+ static isOidcError(error) {
18
+ return errors.isError(error) && error.name === "OidcError";
19
+ }
20
+ static fromError(error) {
21
+ if (OidcError.isOidcError(error)) {
22
+ return error;
23
+ }
24
+ if (!errors.isError(error)) {
25
+ return new OidcError("server_error", "Unknown error", 500, error);
26
+ }
27
+ const errorMessage = error.message || "Unknown error";
28
+ switch (error.name) {
29
+ case "InputError":
30
+ return new OidcError("invalid_request", errorMessage, 400, error);
31
+ case "AuthenticationError":
32
+ return new OidcError("invalid_client", errorMessage, 401, error);
33
+ case "NotAllowedError":
34
+ return new OidcError("access_denied", errorMessage, 403, error);
35
+ case "NotFoundError":
36
+ return new OidcError("invalid_request", errorMessage, 400, error);
37
+ default:
38
+ return new OidcError("server_error", errorMessage, 500, error);
39
+ }
40
+ }
41
+ static middleware(logger) {
42
+ return (err, _req, res, next) => {
43
+ if (OidcError.isOidcError(err)) {
44
+ logger[err.statusCode >= 500 ? "error" : "info"](
45
+ `OIDC Request failed with status ${err.statusCode}: ${err.body.error} - ${err.body.error_description}`,
46
+ err.cause
47
+ );
48
+ res.status(err.statusCode).json(err.body);
49
+ return;
50
+ }
51
+ next(err);
52
+ };
53
+ }
54
+ }
55
+
56
+ exports.OidcError = OidcError;
57
+ //# sourceMappingURL=OidcError.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"OidcError.cjs.js","sources":["../../src/service/OidcError.ts"],"sourcesContent":["/*\n * Copyright 2025 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport { CustomErrorBase, isError } from '@backstage/errors';\nimport { Request, Response, NextFunction } from 'express';\nimport { LoggerService } from '@backstage/backend-plugin-api';\n\nexport class OidcError extends CustomErrorBase {\n name = 'OidcError';\n\n readonly body: { error: string; error_description: string };\n readonly statusCode: number;\n\n constructor(\n errorCode: string,\n errorDescription: string,\n statusCode: number,\n cause?: Error | unknown,\n ) {\n super(`${errorCode}, ${errorDescription}`, cause);\n this.statusCode = statusCode;\n this.body = {\n error: errorCode,\n error_description: errorDescription,\n };\n }\n\n static isOidcError(error: unknown): error is OidcError {\n return isError(error) && error.name === 'OidcError';\n }\n\n static fromError(error: unknown): OidcError {\n if (OidcError.isOidcError(error)) {\n return error;\n }\n\n if (!isError(error)) {\n return new OidcError('server_error', 'Unknown error', 500, error);\n }\n\n const errorMessage = error.message || 'Unknown error';\n\n switch (error.name) {\n case 'InputError':\n return new OidcError('invalid_request', errorMessage, 400, error);\n case 'AuthenticationError':\n return new OidcError('invalid_client', errorMessage, 401, error);\n case 'NotAllowedError':\n return new OidcError('access_denied', errorMessage, 403, error);\n case 'NotFoundError':\n return new OidcError('invalid_request', errorMessage, 400, error);\n default:\n return new OidcError('server_error', errorMessage, 500, error);\n }\n }\n\n static middleware(\n logger: LoggerService,\n ): (err: unknown, _req: Request, res: Response, next: NextFunction) => void {\n return (\n err: unknown,\n _req: Request,\n res: Response,\n next: NextFunction,\n ): void => {\n if (OidcError.isOidcError(err)) {\n logger[err.statusCode >= 500 ? 'error' : 'info'](\n `OIDC Request failed with status ${err.statusCode}: ${err.body.error} - ${err.body.error_description}`,\n err.cause,\n );\n res.status(err.statusCode).json(err.body);\n return;\n }\n next(err);\n };\n }\n}\n"],"names":["CustomErrorBase","isError"],"mappings":";;;;AAmBO,MAAM,kBAAkBA,sBAAA,CAAgB;AAAA,EAC7C,IAAA,GAAO,WAAA;AAAA,EAEE,IAAA;AAAA,EACA,UAAA;AAAA,EAET,WAAA,CACE,SAAA,EACA,gBAAA,EACA,UAAA,EACA,KAAA,EACA;AACA,IAAA,KAAA,CAAM,CAAA,EAAG,SAAS,CAAA,EAAA,EAAK,gBAAgB,IAAI,KAAK,CAAA;AAChD,IAAA,IAAA,CAAK,UAAA,GAAa,UAAA;AAClB,IAAA,IAAA,CAAK,IAAA,GAAO;AAAA,MACV,KAAA,EAAO,SAAA;AAAA,MACP,iBAAA,EAAmB;AAAA,KACrB;AAAA,EACF;AAAA,EAEA,OAAO,YAAY,KAAA,EAAoC;AACrD,IAAA,OAAOC,cAAA,CAAQ,KAAK,CAAA,IAAK,KAAA,CAAM,IAAA,KAAS,WAAA;AAAA,EAC1C;AAAA,EAEA,OAAO,UAAU,KAAA,EAA2B;AAC1C,IAAA,IAAI,SAAA,CAAU,WAAA,CAAY,KAAK,CAAA,EAAG;AAChC,MAAA,OAAO,KAAA;AAAA,IACT;AAEA,IAAA,IAAI,CAACA,cAAA,CAAQ,KAAK,CAAA,EAAG;AACnB,MAAA,OAAO,IAAI,SAAA,CAAU,cAAA,EAAgB,eAAA,EAAiB,KAAK,KAAK,CAAA;AAAA,IAClE;AAEA,IAAA,MAAM,YAAA,GAAe,MAAM,OAAA,IAAW,eAAA;AAEtC,IAAA,QAAQ,MAAM,IAAA;AAAM,MAClB,KAAK,YAAA;AACH,QAAA,OAAO,IAAI,SAAA,CAAU,iBAAA,EAAmB,YAAA,EAAc,KAAK,KAAK,CAAA;AAAA,MAClE,KAAK,qBAAA;AACH,QAAA,OAAO,IAAI,SAAA,CAAU,gBAAA,EAAkB,YAAA,EAAc,KAAK,KAAK,CAAA;AAAA,MACjE,KAAK,iBAAA;AACH,QAAA,OAAO,IAAI,SAAA,CAAU,eAAA,EAAiB,YAAA,EAAc,KAAK,KAAK,CAAA;AAAA,MAChE,KAAK,eAAA;AACH,QAAA,OAAO,IAAI,SAAA,CAAU,iBAAA,EAAmB,YAAA,EAAc,KAAK,KAAK,CAAA;AAAA,MAClE;AACE,QAAA,OAAO,IAAI,SAAA,CAAU,cAAA,EAAgB,YAAA,EAAc,KAAK,KAAK,CAAA;AAAA;AACjE,EACF;AAAA,EAEA,OAAO,WACL,MAAA,EAC0E;AAC1E,IAAA,OAAO,CACL,GAAA,EACA,IAAA,EACA,GAAA,EACA,IAAA,KACS;AACT,MAAA,IAAI,SAAA,CAAU,WAAA,CAAY,GAAG,CAAA,EAAG;AAC9B,QAAA,MAAA,CAAO,GAAA,CAAI,UAAA,IAAc,GAAA,GAAM,OAAA,GAAU,MAAM,CAAA;AAAA,UAC7C,CAAA,gCAAA,EAAmC,GAAA,CAAI,UAAU,CAAA,EAAA,EAAK,GAAA,CAAI,KAAK,KAAK,CAAA,GAAA,EAAM,GAAA,CAAI,IAAA,CAAK,iBAAiB,CAAA,CAAA;AAAA,UACpG,GAAA,CAAI;AAAA,SACN;AACA,QAAA,GAAA,CAAI,OAAO,GAAA,CAAI,UAAU,CAAA,CAAE,IAAA,CAAK,IAAI,IAAI,CAAA;AACxC,QAAA;AAAA,MACF;AACA,MAAA,IAAA,CAAK,GAAG,CAAA;AAAA,IACV,CAAA;AAAA,EACF;AACF;;;;"}
@@ -5,11 +5,98 @@ var OidcService = require('./OidcService.cjs.js');
5
5
  var errors = require('@backstage/errors');
6
6
  var express = require('express');
7
7
  var readTokenExpiration = require('./readTokenExpiration.cjs.js');
8
+ var zod = require('zod');
9
+ var zodValidationError = require('zod-validation-error');
10
+ var OidcError = require('./OidcError.cjs.js');
8
11
 
9
12
  function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
10
13
 
11
14
  var Router__default = /*#__PURE__*/_interopDefaultCompat(Router);
12
15
 
16
+ const authorizeQuerySchema = zod.z.object({
17
+ client_id: zod.z.string().min(1),
18
+ redirect_uri: zod.z.string().url(),
19
+ response_type: zod.z.string().min(1),
20
+ scope: zod.z.string().optional(),
21
+ state: zod.z.string().optional(),
22
+ nonce: zod.z.string().optional(),
23
+ code_challenge: zod.z.string().optional(),
24
+ code_challenge_method: zod.z.string().optional()
25
+ });
26
+ const sessionIdParamSchema = zod.z.object({
27
+ sessionId: zod.z.string().min(1)
28
+ });
29
+ const tokenRequestBodySchema = zod.z.object({
30
+ grant_type: zod.z.string().min(1),
31
+ code: zod.z.string().optional(),
32
+ redirect_uri: zod.z.string().url().optional(),
33
+ code_verifier: zod.z.string().optional(),
34
+ refresh_token: zod.z.string().optional(),
35
+ client_id: zod.z.string().optional(),
36
+ client_secret: zod.z.string().optional()
37
+ });
38
+ const registerRequestBodySchema = zod.z.object({
39
+ client_name: zod.z.string().optional(),
40
+ redirect_uris: zod.z.array(zod.z.string().url()).min(1),
41
+ response_types: zod.z.array(zod.z.string()).optional(),
42
+ grant_types: zod.z.array(zod.z.string()).optional(),
43
+ scope: zod.z.string().optional()
44
+ });
45
+ const revokeRequestBodySchema = zod.z.object({
46
+ token: zod.z.string().min(1),
47
+ token_type_hint: zod.z.string().optional(),
48
+ client_id: zod.z.string().optional(),
49
+ client_secret: zod.z.string().optional()
50
+ });
51
+ function validateRequest(schema, data) {
52
+ const parseResult = schema.safeParse(data);
53
+ if (!parseResult.success) {
54
+ const errorMessage = zodValidationError.fromZodError(parseResult.error).message;
55
+ throw new OidcError.OidcError("invalid_request", errorMessage, 400);
56
+ }
57
+ return parseResult.data;
58
+ }
59
+ async function authenticateClient(req, oidc, bodyClientId, bodyClientSecret) {
60
+ let clientId;
61
+ let clientSecret;
62
+ const basicAuth = req.headers.authorization?.match(/^Basic[ ]+([^\s]+)$/i);
63
+ if (basicAuth) {
64
+ try {
65
+ const decoded = Buffer.from(basicAuth[1], "base64").toString("utf8");
66
+ const idx = decoded.indexOf(":");
67
+ if (idx >= 0) {
68
+ clientId = decoded.slice(0, idx);
69
+ clientSecret = decoded.slice(idx + 1);
70
+ }
71
+ } catch {
72
+ }
73
+ }
74
+ if (!clientId || !clientSecret) {
75
+ if (bodyClientId && bodyClientSecret) {
76
+ clientId = bodyClientId;
77
+ clientSecret = bodyClientSecret;
78
+ }
79
+ }
80
+ if (!clientId || !clientSecret) {
81
+ throw new OidcError.OidcError(
82
+ "invalid_client",
83
+ "Client authentication required",
84
+ 401
85
+ );
86
+ }
87
+ try {
88
+ const ok = await oidc.verifyClientCredentials({
89
+ clientId,
90
+ clientSecret
91
+ });
92
+ if (!ok) {
93
+ throw new OidcError.OidcError("invalid_client", "Invalid client credentials", 401);
94
+ }
95
+ } catch (e) {
96
+ throw OidcError.OidcError.fromError(e);
97
+ }
98
+ return { clientId, clientSecret };
99
+ }
13
100
  class OidcRouter {
14
101
  oidc;
15
102
  logger;
@@ -58,9 +145,10 @@ class OidcRouter {
58
145
  }
59
146
  res.json(userInfo);
60
147
  });
61
- if (this.config.getOptionalBoolean(
148
+ const dcrEnabled = this.config.getOptionalBoolean(
62
149
  "auth.experimentalDynamicClientRegistration.enabled"
63
- )) {
150
+ );
151
+ if (dcrEnabled) {
64
152
  router.get("/v1/authorize", async (req, res) => {
65
153
  const {
66
154
  client_id: clientId,
@@ -71,14 +159,7 @@ class OidcRouter {
71
159
  nonce,
72
160
  code_challenge: codeChallenge,
73
161
  code_challenge_method: codeChallengeMethod
74
- } = req.query;
75
- if (!clientId || !redirectUri || !responseType) {
76
- this.logger.error(`Failed to authorize: Missing required parameters`);
77
- return res.status(400).json({
78
- error: "invalid_request",
79
- error_description: "Missing required parameters: client_id, redirect_uri, response_type"
80
- });
81
- }
162
+ } = validateRequest(authorizeQuerySchema, req.query);
82
163
  try {
83
164
  const result = await this.oidc.createAuthorizationSession({
84
165
  clientId,
@@ -96,31 +177,25 @@ class OidcRouter {
96
177
  );
97
178
  return res.redirect(authSessionRedirectUrl.toString());
98
179
  } catch (error) {
99
- const errorParams = new URLSearchParams();
100
- errorParams.append(
101
- "error",
102
- errors.isError(error) ? error.name : "server_error"
103
- );
104
- errorParams.append(
105
- "error_description",
106
- errors.isError(error) ? error.message : "Unknown error"
107
- );
108
- if (state) {
109
- errorParams.append("state", state);
180
+ if (OidcError.OidcError.isOidcError(error)) {
181
+ const errorParams = new URLSearchParams();
182
+ errorParams.append("error", error.body.error);
183
+ errorParams.append(
184
+ "error_description",
185
+ error.body.error_description
186
+ );
187
+ if (state) {
188
+ errorParams.append("state", state);
189
+ }
190
+ const redirectUrl = new URL(redirectUri);
191
+ redirectUrl.search = errorParams.toString();
192
+ return res.redirect(redirectUrl.toString());
110
193
  }
111
- const redirectUrl = new URL(redirectUri);
112
- redirectUrl.search = errorParams.toString();
113
- return res.redirect(redirectUrl.toString());
194
+ throw error;
114
195
  }
115
196
  });
116
197
  router.get("/v1/sessions/:sessionId", async (req, res) => {
117
- const { sessionId } = req.params;
118
- if (!sessionId) {
119
- return res.status(400).json({
120
- error: "invalid_request",
121
- error_description: "Missing Authorization Session ID"
122
- });
123
- }
198
+ const { sessionId } = validateRequest(sessionIdParamSchema, req.params);
124
199
  try {
125
200
  const session = await this.oidc.getAuthorizationSession({
126
201
  sessionId
@@ -132,32 +207,19 @@ class OidcRouter {
132
207
  redirectUri: session.redirectUri
133
208
  });
134
209
  } catch (error) {
135
- const description = errors.isError(error) ? error.message : "Unknown error";
136
- this.logger.error(
137
- `Failed to get authorization session: ${description}`,
138
- error
139
- );
140
- return res.status(404).json({
141
- error: "not_found",
142
- error_description: description
143
- });
210
+ throw OidcError.OidcError.fromError(error);
144
211
  }
145
212
  });
146
213
  router.post("/v1/sessions/:sessionId/approve", async (req, res) => {
147
- const { sessionId } = req.params;
148
- if (!sessionId) {
149
- return res.status(400).json({
150
- error: "invalid_request",
151
- error_description: "Missing authorization session ID"
152
- });
153
- }
214
+ const { sessionId } = validateRequest(sessionIdParamSchema, req.params);
154
215
  try {
155
216
  const httpCredentials = await this.httpAuth.credentials(req);
156
217
  if (!this.auth.isPrincipal(httpCredentials, "user")) {
157
- return res.status(401).json({
158
- error: "unauthorized",
159
- error_description: "Authentication required"
160
- });
218
+ throw new OidcError.OidcError(
219
+ "access_denied",
220
+ "Authentication required",
221
+ 403
222
+ );
161
223
  }
162
224
  const { userEntityRef } = httpCredentials.principal;
163
225
  const result = await this.oidc.approveAuthorizationSession({
@@ -168,31 +230,14 @@ class OidcRouter {
168
230
  redirectUrl: result.redirectUrl
169
231
  });
170
232
  } catch (error) {
171
- const description = errors.isError(error) ? error.message : "Unknown error";
172
- this.logger.error(
173
- `Failed to approve authorization session: ${description}`,
174
- error
175
- );
176
- return res.status(400).json({
177
- error: "invalid_request",
178
- error_description: description
179
- });
233
+ throw OidcError.OidcError.fromError(error);
180
234
  }
181
235
  });
182
236
  router.post("/v1/sessions/:sessionId/reject", async (req, res) => {
183
- const { sessionId } = req.params;
184
- if (!sessionId) {
185
- return res.status(400).json({
186
- error: "invalid_request",
187
- error_description: "Missing authorization session ID"
188
- });
189
- }
237
+ const { sessionId } = validateRequest(sessionIdParamSchema, req.params);
190
238
  const httpCredentials = await this.httpAuth.credentials(req);
191
239
  if (!this.auth.isPrincipal(httpCredentials, "user")) {
192
- return res.status(401).json({
193
- error: "unauthorized",
194
- error_description: "Authentication required"
195
- });
240
+ throw new OidcError.OidcError("access_denied", "Authentication required", 403);
196
241
  }
197
242
  const { userEntityRef } = httpCredentials.principal;
198
243
  try {
@@ -215,15 +260,7 @@ class OidcRouter {
215
260
  redirectUrl: redirectUrl.toString()
216
261
  });
217
262
  } catch (error) {
218
- const description = errors.isError(error) ? error.message : "Unknown error";
219
- this.logger.error(
220
- `Failed to reject authorization session: ${description}`,
221
- error
222
- );
223
- return res.status(400).json({
224
- error: "invalid_request",
225
- error_description: description
226
- });
263
+ throw OidcError.OidcError.fromError(error);
227
264
  }
228
265
  });
229
266
  router.post("/v1/token", async (req, res) => {
@@ -231,59 +268,83 @@ class OidcRouter {
231
268
  grant_type: grantType,
232
269
  code,
233
270
  redirect_uri: redirectUri,
234
- code_verifier: codeVerifier
235
- } = req.body;
236
- if (!grantType || !code || !redirectUri) {
237
- this.logger.error(
238
- `Failed to exchange code for token: Missing required parameters`
239
- );
240
- return res.status(400).json({
241
- error: "invalid_request",
242
- error_description: "Missing required parameters"
243
- });
244
- }
271
+ code_verifier: codeVerifier,
272
+ refresh_token: refreshToken,
273
+ client_id: bodyClientId,
274
+ client_secret: bodyClientSecret
275
+ } = validateRequest(tokenRequestBodySchema, req.body);
245
276
  const expiresIn = readTokenExpiration.readDcrTokenExpiration(this.config);
246
277
  try {
247
- const result = await this.oidc.exchangeCodeForToken({
248
- code,
249
- redirectUri,
250
- codeVerifier,
251
- grantType,
252
- expiresIn
253
- });
254
- return res.json({
255
- access_token: result.accessToken,
256
- token_type: result.tokenType,
257
- expires_in: result.expiresIn,
258
- id_token: result.idToken,
259
- scope: result.scope
260
- });
261
- } catch (error) {
262
- const description = errors.isError(error) ? error.message : "Unknown error";
263
- this.logger.error(
264
- `Failed to exchange code for token: ${description}`,
265
- error
266
- );
267
- if (errors.isError(error)) {
268
- if (error.name === "AuthenticationError") {
269
- return res.status(401).json({
270
- error: "invalid_client",
271
- error_description: error.message
272
- });
278
+ if (grantType === "authorization_code") {
279
+ if (!code || !redirectUri) {
280
+ throw new OidcError.OidcError(
281
+ "invalid_request",
282
+ "Missing code or redirect_uri parameters for authorization_code grant",
283
+ 400
284
+ );
285
+ }
286
+ const result = await this.oidc.exchangeCodeForToken({
287
+ code,
288
+ redirectUri,
289
+ codeVerifier,
290
+ grantType,
291
+ expiresIn
292
+ });
293
+ return res.json({
294
+ access_token: result.accessToken,
295
+ token_type: result.tokenType,
296
+ expires_in: result.expiresIn,
297
+ id_token: result.idToken,
298
+ scope: result.scope,
299
+ ...result.refreshToken && {
300
+ refresh_token: result.refreshToken
301
+ }
302
+ });
303
+ }
304
+ if (grantType === "refresh_token") {
305
+ if (!refreshToken) {
306
+ throw new OidcError.OidcError(
307
+ "invalid_request",
308
+ "Missing refresh_token parameter for refresh_token grant",
309
+ 400
310
+ );
273
311
  }
274
- if (error.name === "InputError") {
275
- return res.status(400).json({
276
- error: "invalid_request",
277
- error_description: error.message
278
- });
312
+ const hasCredentials = req.headers.authorization?.match(/^Basic[ ]+([^\s]+)$/i) || bodyClientId && bodyClientSecret;
313
+ let authenticatedClientId;
314
+ if (hasCredentials) {
315
+ const { clientId: authedId } = await authenticateClient(
316
+ req,
317
+ this.oidc,
318
+ bodyClientId,
319
+ bodyClientSecret
320
+ );
321
+ authenticatedClientId = authedId;
279
322
  }
323
+ const result = await this.oidc.refreshAccessToken({
324
+ refreshToken,
325
+ clientId: authenticatedClientId
326
+ });
327
+ return res.json({
328
+ access_token: result.accessToken,
329
+ token_type: result.tokenType,
330
+ expires_in: result.expiresIn,
331
+ refresh_token: result.refreshToken
332
+ });
280
333
  }
281
- return res.status(500).json({
282
- error: "server_error",
283
- error_description: description
284
- });
334
+ throw new OidcError.OidcError(
335
+ "unsupported_grant_type",
336
+ `Grant type ${grantType} is not supported`,
337
+ 400
338
+ );
339
+ } catch (error) {
340
+ if (errors.isError(error) && error.name === "AuthenticationError") {
341
+ throw new OidcError.OidcError("invalid_grant", error.message, 400, error);
342
+ }
343
+ throw OidcError.OidcError.fromError(error);
285
344
  }
286
345
  });
346
+ }
347
+ if (dcrEnabled) {
287
348
  router.post("/v1/register", async (req, res) => {
288
349
  const {
289
350
  client_name: clientName,
@@ -291,37 +352,49 @@ class OidcRouter {
291
352
  response_types: responseTypes,
292
353
  grant_types: grantTypes,
293
354
  scope
294
- } = req.body;
295
- if (!redirectUris?.length) {
296
- res.status(400).json({
297
- error: "invalid_request",
298
- error_description: "redirect_uris is required"
299
- });
300
- return;
301
- }
355
+ } = validateRequest(registerRequestBodySchema, req.body);
302
356
  try {
303
357
  const client = await this.oidc.registerClient({
304
- clientName,
358
+ clientName: clientName ?? "Backstage CLI",
305
359
  redirectUris,
306
360
  responseTypes,
307
361
  grantTypes,
308
362
  scope
309
363
  });
310
- res.status(201).json({
364
+ return res.status(201).json({
311
365
  client_id: client.clientId,
312
366
  redirect_uris: client.redirectUris,
313
367
  client_secret: client.clientSecret
314
368
  });
315
369
  } catch (e) {
316
- const description = errors.isError(e) ? e.message : "Unknown error";
317
- this.logger.error(`Failed to register client: ${description}`, e);
318
- res.status(500).json({
319
- error: "server_error",
320
- error_description: `Failed to register client: ${description}`
321
- });
370
+ throw OidcError.OidcError.fromError(e);
371
+ }
372
+ });
373
+ router.post("/v1/revoke", async (req, res) => {
374
+ try {
375
+ const {
376
+ token,
377
+ client_id: bodyClientId,
378
+ client_secret: bodyClientSecret
379
+ } = validateRequest(revokeRequestBodySchema, req.body ?? {});
380
+ await authenticateClient(
381
+ req,
382
+ this.oidc,
383
+ bodyClientId,
384
+ bodyClientSecret
385
+ );
386
+ try {
387
+ await this.oidc.revokeRefreshToken(token);
388
+ } catch (e) {
389
+ this.logger.debug("Failed to revoke token", e);
390
+ }
391
+ return res.status(200).send("");
392
+ } catch (e) {
393
+ throw OidcError.OidcError.fromError(e);
322
394
  }
323
395
  });
324
396
  }
397
+ router.use(OidcError.OidcError.middleware(this.logger));
325
398
  return router;
326
399
  }
327
400
  }