@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.
- package/CHANGELOG.md +27 -0
- package/config.d.ts +31 -0
- package/dist/authPlugin.cjs.js +14 -1
- package/dist/authPlugin.cjs.js.map +1 -1
- package/dist/database/OfflineSessionDatabase.cjs.js +136 -0
- package/dist/database/OfflineSessionDatabase.cjs.js.map +1 -0
- package/dist/identity/StaticKeyStore.cjs.js +2 -2
- package/dist/identity/StaticKeyStore.cjs.js.map +1 -1
- package/dist/lib/refreshToken.cjs.js +60 -0
- package/dist/lib/refreshToken.cjs.js.map +1 -0
- package/dist/service/OfflineAccessService.cjs.js +177 -0
- package/dist/service/OfflineAccessService.cjs.js.map +1 -0
- package/dist/service/OidcError.cjs.js +57 -0
- package/dist/service/OidcError.cjs.js.map +1 -0
- package/dist/service/OidcRouter.cjs.js +215 -142
- package/dist/service/OidcRouter.cjs.js.map +1 -1
- package/dist/service/OidcService.cjs.js +98 -20
- package/dist/service/OidcService.cjs.js.map +1 -1
- package/dist/service/router.cjs.js +2 -1
- package/dist/service/router.cjs.js.map +1 -1
- package/migrations/20251020000000_offline_sessions.js +78 -0
- package/package.json +16 -14
|
@@ -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
|
-
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
"error",
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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
|
-
|
|
248
|
-
code
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
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
|
}
|