@backstage/plugin-auth-backend 0.25.4-next.0 → 0.25.4-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 +10 -0
- package/dist/authPlugin.cjs.js +4 -1
- package/dist/authPlugin.cjs.js.map +1 -1
- package/dist/database/OidcDatabase.cjs.js +214 -0
- package/dist/database/OidcDatabase.cjs.js.map +1 -0
- package/dist/service/OidcRouter.cjs.js +284 -5
- package/dist/service/OidcRouter.cjs.js.map +1 -1
- package/dist/service/OidcService.cjs.js +250 -5
- package/dist/service/OidcService.cjs.js.map +1 -1
- package/dist/service/router.cjs.js +10 -2
- package/dist/service/router.cjs.js.map +1 -1
- package/migrations/20250909120000_oidc_client_registration.js +159 -0
- package/package.json +7 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# @backstage/plugin-auth-backend
|
|
2
2
|
|
|
3
|
+
## 0.25.4-next.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 1d47bf3: Implementing Dynamic Client Registration with the OIDC server. You can enable this by setting `auth.experimentalDynamicClientRegistration.enabled` in `app-config.yaml`. This is highly experimental, but feedback welcome.
|
|
8
|
+
- 54ddfef: Updating plugin metadata
|
|
9
|
+
- Updated dependencies
|
|
10
|
+
- @backstage/plugin-auth-node@0.6.7-next.1
|
|
11
|
+
- @backstage/plugin-catalog-node@1.19.0-next.1
|
|
12
|
+
|
|
3
13
|
## 0.25.4-next.0
|
|
4
14
|
|
|
5
15
|
### Patch Changes
|
package/dist/authPlugin.cjs.js
CHANGED
|
@@ -36,6 +36,7 @@ const authPlugin = backendPluginApi.createBackendPlugin({
|
|
|
36
36
|
database: backendPluginApi.coreServices.database,
|
|
37
37
|
discovery: backendPluginApi.coreServices.discovery,
|
|
38
38
|
auth: backendPluginApi.coreServices.auth,
|
|
39
|
+
httpAuth: backendPluginApi.coreServices.httpAuth,
|
|
39
40
|
catalog: pluginCatalogNode.catalogServiceRef
|
|
40
41
|
},
|
|
41
42
|
async init({
|
|
@@ -45,6 +46,7 @@ const authPlugin = backendPluginApi.createBackendPlugin({
|
|
|
45
46
|
database,
|
|
46
47
|
discovery,
|
|
47
48
|
auth,
|
|
49
|
+
httpAuth,
|
|
48
50
|
catalog
|
|
49
51
|
}) {
|
|
50
52
|
const router$1 = await router.createRouter({
|
|
@@ -55,7 +57,8 @@ const authPlugin = backendPluginApi.createBackendPlugin({
|
|
|
55
57
|
auth,
|
|
56
58
|
catalog,
|
|
57
59
|
providerFactories: Object.fromEntries(providers),
|
|
58
|
-
ownershipResolver
|
|
60
|
+
ownershipResolver,
|
|
61
|
+
httpAuth
|
|
59
62
|
});
|
|
60
63
|
httpRouter.addAuthPolicy({
|
|
61
64
|
path: "/",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"authPlugin.cjs.js","sources":["../src/authPlugin.ts"],"sourcesContent":["/*\n * Copyright 2023 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 */\n\nimport {\n coreServices,\n createBackendPlugin,\n} from '@backstage/backend-plugin-api';\nimport {\n authOwnershipResolutionExtensionPoint,\n AuthOwnershipResolver,\n AuthProviderFactory,\n authProvidersExtensionPoint,\n} from '@backstage/plugin-auth-node';\nimport { catalogServiceRef } from '@backstage/plugin-catalog-node';\nimport { createRouter } from './service/router';\n\n/**\n * Auth plugin\n *\n * @public\n */\nexport const authPlugin = createBackendPlugin({\n pluginId: 'auth',\n register(reg) {\n const providers = new Map<string, AuthProviderFactory>();\n let ownershipResolver: AuthOwnershipResolver | undefined = undefined;\n\n reg.registerExtensionPoint(authProvidersExtensionPoint, {\n registerProvider({ providerId, factory }) {\n if (providers.has(providerId)) {\n throw new Error(\n `Auth provider '${providerId}' was already registered`,\n );\n }\n providers.set(providerId, factory);\n },\n });\n\n reg.registerExtensionPoint(authOwnershipResolutionExtensionPoint, {\n setAuthOwnershipResolver(resolver) {\n if (ownershipResolver) {\n throw new Error('Auth ownership resolver is already set');\n }\n ownershipResolver = resolver;\n },\n });\n\n reg.registerInit({\n deps: {\n httpRouter: coreServices.httpRouter,\n logger: coreServices.logger,\n config: coreServices.rootConfig,\n database: coreServices.database,\n discovery: coreServices.discovery,\n auth: coreServices.auth,\n catalog: catalogServiceRef,\n },\n async init({\n httpRouter,\n logger,\n config,\n database,\n discovery,\n auth,\n catalog,\n }) {\n const router = await createRouter({\n logger,\n config,\n database,\n discovery,\n auth,\n catalog,\n providerFactories: Object.fromEntries(providers),\n ownershipResolver,\n });\n httpRouter.addAuthPolicy({\n path: '/',\n allow: 'unauthenticated',\n });\n httpRouter.use(router);\n },\n });\n },\n});\n"],"names":["createBackendPlugin","authProvidersExtensionPoint","authOwnershipResolutionExtensionPoint","coreServices","catalogServiceRef","router","createRouter"],"mappings":";;;;;;;AAkCO,MAAM,aAAaA,oCAAA,CAAoB;AAAA,EAC5C,QAAA,EAAU,MAAA;AAAA,EACV,SAAS,GAAA,EAAK;AACZ,IAAA,MAAM,SAAA,uBAAgB,GAAA,EAAiC;AACvD,IAAA,IAAI,iBAAA,GAAuD,MAAA;AAE3D,IAAA,GAAA,CAAI,uBAAuBC,0CAAA,EAA6B;AAAA,MACtD,gBAAA,CAAiB,EAAE,UAAA,EAAY,OAAA,EAAQ,EAAG;AACxC,QAAA,IAAI,SAAA,CAAU,GAAA,CAAI,UAAU,CAAA,EAAG;AAC7B,UAAA,MAAM,IAAI,KAAA;AAAA,YACR,kBAAkB,UAAU,CAAA,wBAAA;AAAA,WAC9B;AAAA,QACF;AACA,QAAA,SAAA,CAAU,GAAA,CAAI,YAAY,OAAO,CAAA;AAAA,MACnC;AAAA,KACD,CAAA;AAED,IAAA,GAAA,CAAI,uBAAuBC,oDAAA,EAAuC;AAAA,MAChE,yBAAyB,QAAA,EAAU;AACjC,QAAA,IAAI,iBAAA,EAAmB;AACrB,UAAA,MAAM,IAAI,MAAM,wCAAwC,CAAA;AAAA,QAC1D;AACA,QAAA,iBAAA,GAAoB,QAAA;AAAA,MACtB;AAAA,KACD,CAAA;AAED,IAAA,GAAA,CAAI,YAAA,CAAa;AAAA,MACf,IAAA,EAAM;AAAA,QACJ,YAAYC,6BAAA,CAAa,UAAA;AAAA,QACzB,QAAQA,6BAAA,CAAa,MAAA;AAAA,QACrB,QAAQA,6BAAA,CAAa,UAAA;AAAA,QACrB,UAAUA,6BAAA,CAAa,QAAA;AAAA,QACvB,WAAWA,6BAAA,CAAa,SAAA;AAAA,QACxB,MAAMA,6BAAA,CAAa,IAAA;AAAA,QACnB,OAAA,EAASC;AAAA,OACX;AAAA,MACA,MAAM,IAAA,CAAK;AAAA,QACT,UAAA;AAAA,QACA,MAAA;AAAA,QACA,MAAA;AAAA,QACA,QAAA;AAAA,QACA,SAAA;AAAA,QACA,IAAA;AAAA,QACA;AAAA,OACF,EAAG;AACD,QAAA,MAAMC,QAAA,GAAS,MAAMC,mBAAA,CAAa;AAAA,UAChC,MAAA;AAAA,UACA,MAAA;AAAA,UACA,QAAA;AAAA,UACA,SAAA;AAAA,UACA,IAAA;AAAA,UACA,OAAA;AAAA,UACA,iBAAA,EAAmB,MAAA,CAAO,WAAA,CAAY,SAAS,CAAA;AAAA,UAC/C;AAAA,SACD,CAAA;AACD,QAAA,UAAA,CAAW,aAAA,CAAc;AAAA,UACvB,IAAA,EAAM,GAAA;AAAA,UACN,KAAA,EAAO;AAAA,SACR,CAAA;AACD,QAAA,UAAA,CAAW,IAAID,QAAM,CAAA;AAAA,MACvB;AAAA,KACD,CAAA;AAAA,EACH;AACF,CAAC;;;;"}
|
|
1
|
+
{"version":3,"file":"authPlugin.cjs.js","sources":["../src/authPlugin.ts"],"sourcesContent":["/*\n * Copyright 2023 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 */\n\nimport {\n coreServices,\n createBackendPlugin,\n} from '@backstage/backend-plugin-api';\nimport {\n authOwnershipResolutionExtensionPoint,\n AuthOwnershipResolver,\n AuthProviderFactory,\n authProvidersExtensionPoint,\n} from '@backstage/plugin-auth-node';\nimport { catalogServiceRef } from '@backstage/plugin-catalog-node';\nimport { createRouter } from './service/router';\n\n/**\n * Auth plugin\n *\n * @public\n */\nexport const authPlugin = createBackendPlugin({\n pluginId: 'auth',\n register(reg) {\n const providers = new Map<string, AuthProviderFactory>();\n let ownershipResolver: AuthOwnershipResolver | undefined = undefined;\n\n reg.registerExtensionPoint(authProvidersExtensionPoint, {\n registerProvider({ providerId, factory }) {\n if (providers.has(providerId)) {\n throw new Error(\n `Auth provider '${providerId}' was already registered`,\n );\n }\n providers.set(providerId, factory);\n },\n });\n\n reg.registerExtensionPoint(authOwnershipResolutionExtensionPoint, {\n setAuthOwnershipResolver(resolver) {\n if (ownershipResolver) {\n throw new Error('Auth ownership resolver is already set');\n }\n ownershipResolver = resolver;\n },\n });\n\n reg.registerInit({\n deps: {\n httpRouter: coreServices.httpRouter,\n logger: coreServices.logger,\n config: coreServices.rootConfig,\n database: coreServices.database,\n discovery: coreServices.discovery,\n auth: coreServices.auth,\n httpAuth: coreServices.httpAuth,\n catalog: catalogServiceRef,\n },\n async init({\n httpRouter,\n logger,\n config,\n database,\n discovery,\n auth,\n httpAuth,\n catalog,\n }) {\n const router = await createRouter({\n logger,\n config,\n database,\n discovery,\n auth,\n catalog,\n providerFactories: Object.fromEntries(providers),\n ownershipResolver,\n httpAuth,\n });\n httpRouter.addAuthPolicy({\n path: '/',\n allow: 'unauthenticated',\n });\n httpRouter.use(router);\n },\n });\n },\n});\n"],"names":["createBackendPlugin","authProvidersExtensionPoint","authOwnershipResolutionExtensionPoint","coreServices","catalogServiceRef","router","createRouter"],"mappings":";;;;;;;AAkCO,MAAM,aAAaA,oCAAA,CAAoB;AAAA,EAC5C,QAAA,EAAU,MAAA;AAAA,EACV,SAAS,GAAA,EAAK;AACZ,IAAA,MAAM,SAAA,uBAAgB,GAAA,EAAiC;AACvD,IAAA,IAAI,iBAAA,GAAuD,MAAA;AAE3D,IAAA,GAAA,CAAI,uBAAuBC,0CAAA,EAA6B;AAAA,MACtD,gBAAA,CAAiB,EAAE,UAAA,EAAY,OAAA,EAAQ,EAAG;AACxC,QAAA,IAAI,SAAA,CAAU,GAAA,CAAI,UAAU,CAAA,EAAG;AAC7B,UAAA,MAAM,IAAI,KAAA;AAAA,YACR,kBAAkB,UAAU,CAAA,wBAAA;AAAA,WAC9B;AAAA,QACF;AACA,QAAA,SAAA,CAAU,GAAA,CAAI,YAAY,OAAO,CAAA;AAAA,MACnC;AAAA,KACD,CAAA;AAED,IAAA,GAAA,CAAI,uBAAuBC,oDAAA,EAAuC;AAAA,MAChE,yBAAyB,QAAA,EAAU;AACjC,QAAA,IAAI,iBAAA,EAAmB;AACrB,UAAA,MAAM,IAAI,MAAM,wCAAwC,CAAA;AAAA,QAC1D;AACA,QAAA,iBAAA,GAAoB,QAAA;AAAA,MACtB;AAAA,KACD,CAAA;AAED,IAAA,GAAA,CAAI,YAAA,CAAa;AAAA,MACf,IAAA,EAAM;AAAA,QACJ,YAAYC,6BAAA,CAAa,UAAA;AAAA,QACzB,QAAQA,6BAAA,CAAa,MAAA;AAAA,QACrB,QAAQA,6BAAA,CAAa,UAAA;AAAA,QACrB,UAAUA,6BAAA,CAAa,QAAA;AAAA,QACvB,WAAWA,6BAAA,CAAa,SAAA;AAAA,QACxB,MAAMA,6BAAA,CAAa,IAAA;AAAA,QACnB,UAAUA,6BAAA,CAAa,QAAA;AAAA,QACvB,OAAA,EAASC;AAAA,OACX;AAAA,MACA,MAAM,IAAA,CAAK;AAAA,QACT,UAAA;AAAA,QACA,MAAA;AAAA,QACA,MAAA;AAAA,QACA,QAAA;AAAA,QACA,SAAA;AAAA,QACA,IAAA;AAAA,QACA,QAAA;AAAA,QACA;AAAA,OACF,EAAG;AACD,QAAA,MAAMC,QAAA,GAAS,MAAMC,mBAAA,CAAa;AAAA,UAChC,MAAA;AAAA,UACA,MAAA;AAAA,UACA,QAAA;AAAA,UACA,SAAA;AAAA,UACA,IAAA;AAAA,UACA,OAAA;AAAA,UACA,iBAAA,EAAmB,MAAA,CAAO,WAAA,CAAY,SAAS,CAAA;AAAA,UAC/C,iBAAA;AAAA,UACA;AAAA,SACD,CAAA;AACD,QAAA,UAAA,CAAW,aAAA,CAAc;AAAA,UACvB,IAAA,EAAM,GAAA;AAAA,UACN,KAAA,EAAO;AAAA,SACR,CAAA;AACD,QAAA,UAAA,CAAW,IAAID,QAAM,CAAA;AAAA,MACvB;AAAA,KACD,CAAA;AAAA,EACH;AACF,CAAC;;;;"}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function toDate(value) {
|
|
4
|
+
if (!value) {
|
|
5
|
+
return void 0;
|
|
6
|
+
}
|
|
7
|
+
return typeof value === "string" || typeof value === "number" ? new Date(value) : value;
|
|
8
|
+
}
|
|
9
|
+
class OidcDatabase {
|
|
10
|
+
constructor(db) {
|
|
11
|
+
this.db = db;
|
|
12
|
+
}
|
|
13
|
+
static async create(options) {
|
|
14
|
+
const client = await options.database.get();
|
|
15
|
+
return new OidcDatabase(client);
|
|
16
|
+
}
|
|
17
|
+
async createClient(client) {
|
|
18
|
+
await this.db("oidc_clients").insert({
|
|
19
|
+
client_id: client.clientId,
|
|
20
|
+
client_secret: client.clientSecret,
|
|
21
|
+
client_name: client.clientName,
|
|
22
|
+
response_types: JSON.stringify(client.responseTypes),
|
|
23
|
+
grant_types: JSON.stringify(client.grantTypes),
|
|
24
|
+
redirect_uris: JSON.stringify(client.redirectUris),
|
|
25
|
+
scope: client.scope,
|
|
26
|
+
metadata: JSON.stringify(client.metadata)
|
|
27
|
+
});
|
|
28
|
+
return client;
|
|
29
|
+
}
|
|
30
|
+
async getClient({ clientId }) {
|
|
31
|
+
const client = await this.db("oidc_clients").where("client_id", clientId).first();
|
|
32
|
+
if (!client) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
return this.rowToClient(client);
|
|
36
|
+
}
|
|
37
|
+
async createAuthorizationSession(session) {
|
|
38
|
+
await this.db(
|
|
39
|
+
"oauth_authorization_sessions"
|
|
40
|
+
).insert({
|
|
41
|
+
id: session.id,
|
|
42
|
+
client_id: session.clientId,
|
|
43
|
+
user_entity_ref: session.userEntityRef,
|
|
44
|
+
redirect_uri: session.redirectUri,
|
|
45
|
+
scope: session.scope,
|
|
46
|
+
state: session.state,
|
|
47
|
+
response_type: session.responseType,
|
|
48
|
+
code_challenge: session.codeChallenge,
|
|
49
|
+
code_challenge_method: session.codeChallengeMethod,
|
|
50
|
+
nonce: session.nonce,
|
|
51
|
+
status: "pending",
|
|
52
|
+
expires_at: session.expiresAt
|
|
53
|
+
});
|
|
54
|
+
return {
|
|
55
|
+
...session,
|
|
56
|
+
status: "pending"
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
async updateAuthorizationSession(session) {
|
|
60
|
+
const row = this.authorizationSessionToRow(session);
|
|
61
|
+
const updatedFields = Object.fromEntries(
|
|
62
|
+
Object.entries(row).filter(([_, value]) => value !== void 0)
|
|
63
|
+
);
|
|
64
|
+
if (this.db.client.config.client.includes("sqlite3") || this.db.client.config.client.includes("mysql")) {
|
|
65
|
+
return await this.db.transaction(async (trx) => {
|
|
66
|
+
await trx("oauth_authorization_sessions").where("id", session.id).update(updatedFields);
|
|
67
|
+
const updated = await trx(
|
|
68
|
+
"oauth_authorization_sessions"
|
|
69
|
+
).where("id", session.id).first();
|
|
70
|
+
if (!updated) {
|
|
71
|
+
throw new Error(
|
|
72
|
+
`Failed to retrieve updated authorization session with id ${session.id}`
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
return this.rowToAuthorizationSession(updated);
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
const returnedRows = await this.db(
|
|
79
|
+
"oauth_authorization_sessions"
|
|
80
|
+
).where("id", session.id).update(updatedFields).returning("*");
|
|
81
|
+
if (returnedRows.length !== 1) {
|
|
82
|
+
throw new Error(
|
|
83
|
+
`Failed to retrieve updated authorization session with id ${session.id}`
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
const [returnedSession] = returnedRows;
|
|
87
|
+
return this.rowToAuthorizationSession(
|
|
88
|
+
returnedSession
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
async getAuthorizationSession({ id }) {
|
|
92
|
+
const session = await this.db(
|
|
93
|
+
"oauth_authorization_sessions"
|
|
94
|
+
).where("id", id).first();
|
|
95
|
+
if (!session) {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
return this.rowToAuthorizationSession(session);
|
|
99
|
+
}
|
|
100
|
+
async createAuthorizationCode(authorizationCode) {
|
|
101
|
+
await this.db("oidc_authorization_codes").insert({
|
|
102
|
+
code: authorizationCode.code,
|
|
103
|
+
session_id: authorizationCode.sessionId,
|
|
104
|
+
expires_at: authorizationCode.expiresAt,
|
|
105
|
+
used: false
|
|
106
|
+
});
|
|
107
|
+
return {
|
|
108
|
+
...authorizationCode,
|
|
109
|
+
used: false
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
async getAuthorizationCode({ code }) {
|
|
113
|
+
const authCode = await this.db(
|
|
114
|
+
"oidc_authorization_codes"
|
|
115
|
+
).where("code", code).first();
|
|
116
|
+
if (!authCode) {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
return this.rowToAuthorizationCode(authCode);
|
|
120
|
+
}
|
|
121
|
+
async updateAuthorizationCode(authorizationCode) {
|
|
122
|
+
const row = this.authorizationCodeToRow(authorizationCode);
|
|
123
|
+
const updatedFields = Object.fromEntries(
|
|
124
|
+
Object.entries(row).filter(([_, value]) => value !== void 0)
|
|
125
|
+
);
|
|
126
|
+
if (this.db.client.config.client.includes("sqlite3") || this.db.client.config.client.includes("mysql")) {
|
|
127
|
+
return await this.db.transaction(async (trx) => {
|
|
128
|
+
await trx("oidc_authorization_codes").where("code", authorizationCode.code).update(updatedFields);
|
|
129
|
+
const updated = await trx(
|
|
130
|
+
"oidc_authorization_codes"
|
|
131
|
+
).where("code", authorizationCode.code).first();
|
|
132
|
+
if (!updated) {
|
|
133
|
+
throw new Error(
|
|
134
|
+
`Failed to retrieve updated authorization code with code ${authorizationCode.code}`
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
return this.rowToAuthorizationCode(updated);
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
const returnedRows = await this.db(
|
|
141
|
+
"oidc_authorization_codes"
|
|
142
|
+
).where("code", authorizationCode.code).update(updatedFields).returning("*");
|
|
143
|
+
if (returnedRows.length !== 1) {
|
|
144
|
+
throw new Error(
|
|
145
|
+
`Failed to retrieve updated authorization code with code ${authorizationCode.code}`
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
const [returnedCode] = returnedRows;
|
|
149
|
+
return this.rowToAuthorizationCode(returnedCode);
|
|
150
|
+
}
|
|
151
|
+
rowToClient(row) {
|
|
152
|
+
return {
|
|
153
|
+
clientId: row.client_id,
|
|
154
|
+
clientName: row.client_name,
|
|
155
|
+
clientSecret: row.client_secret,
|
|
156
|
+
redirectUris: row.redirect_uris ? JSON.parse(row.redirect_uris) : void 0,
|
|
157
|
+
responseTypes: row.response_types ? JSON.parse(row.response_types) : void 0,
|
|
158
|
+
grantTypes: row.grant_types ? JSON.parse(row.grant_types) : void 0,
|
|
159
|
+
scope: row.scope ?? void 0,
|
|
160
|
+
metadata: row.metadata ? JSON.parse(row.metadata) : void 0
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
authorizationSessionToRow(session) {
|
|
164
|
+
return {
|
|
165
|
+
id: session.id,
|
|
166
|
+
client_id: session.clientId,
|
|
167
|
+
user_entity_ref: session.userEntityRef,
|
|
168
|
+
redirect_uri: session.redirectUri,
|
|
169
|
+
scope: session.scope,
|
|
170
|
+
state: session.state,
|
|
171
|
+
response_type: session.responseType,
|
|
172
|
+
code_challenge: session.codeChallenge,
|
|
173
|
+
code_challenge_method: session.codeChallengeMethod,
|
|
174
|
+
nonce: session.nonce,
|
|
175
|
+
status: session.status,
|
|
176
|
+
expires_at: toDate(session.expiresAt)
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
rowToAuthorizationSession(row) {
|
|
180
|
+
return {
|
|
181
|
+
id: row.id,
|
|
182
|
+
clientId: row.client_id,
|
|
183
|
+
userEntityRef: row.user_entity_ref ?? void 0,
|
|
184
|
+
redirectUri: row.redirect_uri,
|
|
185
|
+
scope: row.scope ?? void 0,
|
|
186
|
+
state: row.state ?? void 0,
|
|
187
|
+
responseType: row.response_type,
|
|
188
|
+
codeChallenge: row.code_challenge ?? void 0,
|
|
189
|
+
codeChallengeMethod: row.code_challenge_method ?? void 0,
|
|
190
|
+
nonce: row.nonce ?? void 0,
|
|
191
|
+
status: row.status,
|
|
192
|
+
expiresAt: toDate(row.expires_at)
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
authorizationCodeToRow(authorizationCode) {
|
|
196
|
+
return {
|
|
197
|
+
code: authorizationCode.code,
|
|
198
|
+
session_id: authorizationCode.sessionId,
|
|
199
|
+
expires_at: toDate(authorizationCode.expiresAt),
|
|
200
|
+
used: authorizationCode.used
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
rowToAuthorizationCode(row) {
|
|
204
|
+
return {
|
|
205
|
+
code: row.code,
|
|
206
|
+
sessionId: row.session_id,
|
|
207
|
+
expiresAt: toDate(row.expires_at),
|
|
208
|
+
used: Boolean(row.used)
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
exports.OidcDatabase = OidcDatabase;
|
|
214
|
+
//# sourceMappingURL=OidcDatabase.cjs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"OidcDatabase.cjs.js","sources":["../../src/database/OidcDatabase.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 { Knex } from 'knex';\nimport { AuthDatabase } from './AuthDatabase';\n\nfunction toDate(value?: Date | string | number): Date | undefined {\n if (!value) {\n return undefined;\n }\n\n return typeof value === 'string' || typeof value === 'number'\n ? new Date(value)\n : value;\n}\ntype OidcClientRow = {\n client_id: string;\n client_secret: string;\n client_name: string;\n response_types: string;\n grant_types: string;\n redirect_uris: string;\n scope: string | null;\n metadata: string | null;\n};\n\ntype OAuthAuthorizationSessionRow = {\n id: string;\n client_id: string;\n user_entity_ref: string | null;\n redirect_uri: string;\n scope: string | null;\n state: string | null;\n response_type: string;\n code_challenge: string | null;\n code_challenge_method: string | null;\n nonce: string | null;\n status: 'pending' | 'approved' | 'rejected' | 'expired';\n expires_at: Date | string;\n};\n\ntype OidcAuthorizationCodeRow = {\n code: string;\n session_id: string;\n expires_at: Date | string;\n used: boolean;\n};\n\nexport type Client = {\n clientId: string;\n clientName: string;\n clientSecret: string;\n redirectUris: string[];\n responseTypes: string[];\n grantTypes: string[];\n scope?: string;\n metadata?: Record<string, unknown>;\n};\n\nexport type AuthorizationSession = {\n id: string;\n clientId: string;\n userEntityRef?: string;\n redirectUri: string;\n scope?: string;\n state?: string;\n responseType: string;\n codeChallenge?: string;\n codeChallengeMethod?: string;\n nonce?: string;\n status: 'pending' | 'approved' | 'rejected' | 'expired';\n expiresAt: Date;\n};\n\nexport type ConsentRequest = {\n id: string;\n sessionId: string;\n expiresAt: Date;\n};\n\nexport type AuthorizationCode = {\n code: string;\n sessionId: string;\n expiresAt: Date;\n used: boolean;\n};\n\nexport type AccessToken = {\n tokenId: string;\n sessionId: string;\n expiresAt: Date;\n};\n\n/**\n * This class provides database operations for OpenID Connect (OIDC) authentication flows.\n * It manages OIDC clients, authorization codes, and access tokens in the database.\n */\nexport class OidcDatabase {\n private constructor(private readonly db: Knex) {}\n\n static async create(options: { database: AuthDatabase }) {\n const client = await options.database.get();\n return new OidcDatabase(client);\n }\n\n async createClient(client: Client) {\n await this.db<OidcClientRow>('oidc_clients').insert({\n client_id: client.clientId,\n client_secret: client.clientSecret,\n client_name: client.clientName,\n response_types: JSON.stringify(client.responseTypes),\n grant_types: JSON.stringify(client.grantTypes),\n redirect_uris: JSON.stringify(client.redirectUris),\n scope: client.scope,\n metadata: JSON.stringify(client.metadata),\n });\n\n return client;\n }\n\n async getClient({ clientId }: { clientId: string }) {\n const client = await this.db<OidcClientRow>('oidc_clients')\n .where('client_id', clientId)\n .first();\n\n if (!client) {\n return null;\n }\n\n return this.rowToClient(client) as Client;\n }\n\n async createAuthorizationSession(\n session: Omit<AuthorizationSession, 'status'>,\n ) {\n await this.db<OAuthAuthorizationSessionRow>(\n 'oauth_authorization_sessions',\n ).insert({\n id: session.id,\n client_id: session.clientId,\n user_entity_ref: session.userEntityRef,\n redirect_uri: session.redirectUri,\n scope: session.scope,\n state: session.state,\n response_type: session.responseType,\n code_challenge: session.codeChallenge,\n code_challenge_method: session.codeChallengeMethod,\n nonce: session.nonce,\n status: 'pending',\n expires_at: session.expiresAt,\n });\n\n return {\n ...session,\n status: 'pending',\n };\n }\n\n async updateAuthorizationSession(\n session: Partial<AuthorizationSession> & { id: string },\n ) {\n const row = this.authorizationSessionToRow(session);\n const updatedFields = Object.fromEntries(\n Object.entries(row).filter(([_, value]) => value !== undefined),\n );\n\n // MySQL and SQLite3 don't support RETURNING\n if (\n this.db.client.config.client.includes('sqlite3') ||\n this.db.client.config.client.includes('mysql')\n ) {\n return await this.db.transaction(async trx => {\n await trx<OAuthAuthorizationSessionRow>('oauth_authorization_sessions')\n .where('id', session.id)\n .update(updatedFields);\n\n const updated = await trx<OAuthAuthorizationSessionRow>(\n 'oauth_authorization_sessions',\n )\n .where('id', session.id)\n .first();\n\n if (!updated) {\n throw new Error(\n `Failed to retrieve updated authorization session with id ${session.id}`,\n );\n }\n\n return this.rowToAuthorizationSession(updated) as AuthorizationSession;\n });\n }\n\n const returnedRows = await this.db<OAuthAuthorizationSessionRow>(\n 'oauth_authorization_sessions',\n )\n .where('id', session.id)\n .update(updatedFields)\n .returning('*');\n\n if (returnedRows.length !== 1) {\n throw new Error(\n `Failed to retrieve updated authorization session with id ${session.id}`,\n );\n }\n\n const [returnedSession] = returnedRows;\n\n return this.rowToAuthorizationSession(\n returnedSession,\n ) as AuthorizationSession;\n }\n\n async getAuthorizationSession({ id }: { id: string }) {\n const session = await this.db<OAuthAuthorizationSessionRow>(\n 'oauth_authorization_sessions',\n )\n .where('id', id)\n .first();\n\n if (!session) {\n return null;\n }\n\n return this.rowToAuthorizationSession(session) as AuthorizationSession;\n }\n\n async createAuthorizationCode(\n authorizationCode: Omit<AuthorizationCode, 'used'>,\n ) {\n await this.db<OidcAuthorizationCodeRow>('oidc_authorization_codes').insert({\n code: authorizationCode.code,\n session_id: authorizationCode.sessionId,\n expires_at: authorizationCode.expiresAt,\n used: false,\n });\n\n return {\n ...authorizationCode,\n used: false,\n };\n }\n\n async getAuthorizationCode({ code }: { code: string }) {\n const authCode = await this.db<OidcAuthorizationCodeRow>(\n 'oidc_authorization_codes',\n )\n .where('code', code)\n .first();\n\n if (!authCode) {\n return null;\n }\n\n return this.rowToAuthorizationCode(authCode) as AuthorizationCode;\n }\n\n async updateAuthorizationCode(\n authorizationCode: Partial<AuthorizationCode> & { code: string },\n ) {\n const row = this.authorizationCodeToRow(authorizationCode);\n const updatedFields = Object.fromEntries(\n Object.entries(row).filter(([_, value]) => value !== undefined),\n );\n\n // MySQL and SQLite3 don't support RETURNING\n if (\n this.db.client.config.client.includes('sqlite3') ||\n this.db.client.config.client.includes('mysql')\n ) {\n return await this.db.transaction(async trx => {\n await trx<OidcAuthorizationCodeRow>('oidc_authorization_codes')\n .where('code', authorizationCode.code)\n .update(updatedFields);\n\n const updated = await trx<OidcAuthorizationCodeRow>(\n 'oidc_authorization_codes',\n )\n .where('code', authorizationCode.code)\n .first();\n\n if (!updated) {\n throw new Error(\n `Failed to retrieve updated authorization code with code ${authorizationCode.code}`,\n );\n }\n\n return this.rowToAuthorizationCode(updated) as AuthorizationCode;\n });\n }\n\n const returnedRows = await this.db<OidcAuthorizationCodeRow>(\n 'oidc_authorization_codes',\n )\n .where('code', authorizationCode.code)\n .update(updatedFields)\n .returning('*');\n\n if (returnedRows.length !== 1) {\n throw new Error(\n `Failed to retrieve updated authorization code with code ${authorizationCode.code}`,\n );\n }\n\n const [returnedCode] = returnedRows;\n\n return this.rowToAuthorizationCode(returnedCode) as AuthorizationCode;\n }\n\n private rowToClient(row: OidcClientRow): Client {\n return {\n clientId: row.client_id,\n clientName: row.client_name,\n clientSecret: row.client_secret,\n redirectUris: row.redirect_uris\n ? JSON.parse(row.redirect_uris)\n : undefined,\n responseTypes: row.response_types\n ? JSON.parse(row.response_types)\n : undefined,\n grantTypes: row.grant_types ? JSON.parse(row.grant_types) : undefined,\n scope: row.scope ?? undefined,\n metadata: row.metadata ? JSON.parse(row.metadata) : undefined,\n };\n }\n\n private authorizationSessionToRow(\n session: Partial<AuthorizationSession>,\n ): Partial<OAuthAuthorizationSessionRow> {\n return {\n id: session.id,\n client_id: session.clientId,\n user_entity_ref: session.userEntityRef,\n redirect_uri: session.redirectUri,\n scope: session.scope,\n state: session.state,\n response_type: session.responseType,\n code_challenge: session.codeChallenge,\n code_challenge_method: session.codeChallengeMethod,\n nonce: session.nonce,\n status: session.status,\n expires_at: toDate(session.expiresAt),\n };\n }\n\n private rowToAuthorizationSession(\n row: OAuthAuthorizationSessionRow,\n ): Partial<AuthorizationSession> {\n return {\n id: row.id,\n clientId: row.client_id,\n userEntityRef: row.user_entity_ref ?? undefined,\n redirectUri: row.redirect_uri,\n scope: row.scope ?? undefined,\n state: row.state ?? undefined,\n responseType: row.response_type,\n codeChallenge: row.code_challenge ?? undefined,\n codeChallengeMethod: row.code_challenge_method ?? undefined,\n nonce: row.nonce ?? undefined,\n status: row.status,\n expiresAt: toDate(row.expires_at),\n };\n }\n\n private authorizationCodeToRow(\n authorizationCode: Partial<AuthorizationCode>,\n ): Partial<OidcAuthorizationCodeRow> {\n return {\n code: authorizationCode.code,\n session_id: authorizationCode.sessionId,\n expires_at: toDate(authorizationCode.expiresAt),\n used: authorizationCode.used,\n };\n }\n\n private rowToAuthorizationCode(\n row: OidcAuthorizationCodeRow,\n ): Partial<AuthorizationCode> {\n return {\n code: row.code,\n sessionId: row.session_id,\n expiresAt: toDate(row.expires_at),\n used: Boolean(row.used),\n };\n }\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -3,20 +3,34 @@
|
|
|
3
3
|
var Router = require('express-promise-router');
|
|
4
4
|
var OidcService = require('./OidcService.cjs.js');
|
|
5
5
|
var errors = require('@backstage/errors');
|
|
6
|
+
var express = require('express');
|
|
6
7
|
|
|
7
8
|
function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
|
|
8
9
|
|
|
9
10
|
var Router__default = /*#__PURE__*/_interopDefaultCompat(Router);
|
|
10
11
|
|
|
11
12
|
class OidcRouter {
|
|
12
|
-
constructor(oidc) {
|
|
13
|
+
constructor(oidc, logger, auth, appUrl, httpAuth, config) {
|
|
13
14
|
this.oidc = oidc;
|
|
15
|
+
this.logger = logger;
|
|
16
|
+
this.auth = auth;
|
|
17
|
+
this.appUrl = appUrl;
|
|
18
|
+
this.httpAuth = httpAuth;
|
|
19
|
+
this.config = config;
|
|
14
20
|
}
|
|
15
21
|
static create(options) {
|
|
16
|
-
return new OidcRouter(
|
|
22
|
+
return new OidcRouter(
|
|
23
|
+
OidcService.OidcService.create(options),
|
|
24
|
+
options.logger,
|
|
25
|
+
options.auth,
|
|
26
|
+
options.appUrl,
|
|
27
|
+
options.httpAuth,
|
|
28
|
+
options.config
|
|
29
|
+
);
|
|
17
30
|
}
|
|
18
31
|
getRouter() {
|
|
19
32
|
const router = Router__default.default();
|
|
33
|
+
router.use(express.json());
|
|
20
34
|
router.get("/.well-known/openid-configuration", (_req, res) => {
|
|
21
35
|
res.json(this.oidc.getConfiguration());
|
|
22
36
|
});
|
|
@@ -24,9 +38,6 @@ class OidcRouter {
|
|
|
24
38
|
const { keys } = await this.oidc.listPublicKeys();
|
|
25
39
|
res.json({ keys });
|
|
26
40
|
});
|
|
27
|
-
router.get("/v1/token", (_req, res) => {
|
|
28
|
-
res.status(501).send("Not Implemented");
|
|
29
|
-
});
|
|
30
41
|
router.get("/v1/userinfo", async (req, res) => {
|
|
31
42
|
const matches = req.headers.authorization?.match(/^Bearer[ ]+(\S+)$/i);
|
|
32
43
|
const token = matches?.[1];
|
|
@@ -40,9 +51,277 @@ class OidcRouter {
|
|
|
40
51
|
}
|
|
41
52
|
res.json(userInfo);
|
|
42
53
|
});
|
|
54
|
+
if (this.config.getOptionalBoolean(
|
|
55
|
+
"auth.experimentalDynamicClientRegistration.enabled"
|
|
56
|
+
)) {
|
|
57
|
+
router.get("/v1/authorize", async (req, res) => {
|
|
58
|
+
const {
|
|
59
|
+
client_id: clientId,
|
|
60
|
+
redirect_uri: redirectUri,
|
|
61
|
+
response_type: responseType,
|
|
62
|
+
scope,
|
|
63
|
+
state,
|
|
64
|
+
nonce,
|
|
65
|
+
code_challenge: codeChallenge,
|
|
66
|
+
code_challenge_method: codeChallengeMethod
|
|
67
|
+
} = req.query;
|
|
68
|
+
if (!clientId || !redirectUri || !responseType) {
|
|
69
|
+
this.logger.error(`Failed to authorize: Missing required parameters`);
|
|
70
|
+
return res.status(400).json({
|
|
71
|
+
error: "invalid_request",
|
|
72
|
+
error_description: "Missing required parameters: client_id, redirect_uri, response_type"
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
try {
|
|
76
|
+
const result = await this.oidc.createAuthorizationSession({
|
|
77
|
+
clientId,
|
|
78
|
+
redirectUri,
|
|
79
|
+
responseType,
|
|
80
|
+
scope,
|
|
81
|
+
state,
|
|
82
|
+
nonce,
|
|
83
|
+
codeChallenge,
|
|
84
|
+
codeChallengeMethod
|
|
85
|
+
});
|
|
86
|
+
const authSessionRedirectUrl = new URL(
|
|
87
|
+
`./oauth2/authorize/${result.id}`,
|
|
88
|
+
ensureTrailingSlash(this.appUrl)
|
|
89
|
+
);
|
|
90
|
+
return res.redirect(authSessionRedirectUrl.toString());
|
|
91
|
+
} catch (error) {
|
|
92
|
+
const errorParams = new URLSearchParams();
|
|
93
|
+
errorParams.append(
|
|
94
|
+
"error",
|
|
95
|
+
errors.isError(error) ? error.name : "server_error"
|
|
96
|
+
);
|
|
97
|
+
errorParams.append(
|
|
98
|
+
"error_description",
|
|
99
|
+
errors.isError(error) ? error.message : "Unknown error"
|
|
100
|
+
);
|
|
101
|
+
if (state) {
|
|
102
|
+
errorParams.append("state", state);
|
|
103
|
+
}
|
|
104
|
+
const redirectUrl = new URL(redirectUri);
|
|
105
|
+
redirectUrl.search = errorParams.toString();
|
|
106
|
+
return res.redirect(redirectUrl.toString());
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
router.get("/v1/sessions/:sessionId", async (req, res) => {
|
|
110
|
+
const { sessionId } = req.params;
|
|
111
|
+
if (!sessionId) {
|
|
112
|
+
return res.status(400).json({
|
|
113
|
+
error: "invalid_request",
|
|
114
|
+
error_description: "Missing Authorization Session ID"
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
try {
|
|
118
|
+
const session = await this.oidc.getAuthorizationSession({
|
|
119
|
+
sessionId
|
|
120
|
+
});
|
|
121
|
+
return res.json({
|
|
122
|
+
id: session.id,
|
|
123
|
+
clientName: session.clientName,
|
|
124
|
+
scope: session.scope,
|
|
125
|
+
redirectUri: session.redirectUri
|
|
126
|
+
});
|
|
127
|
+
} catch (error) {
|
|
128
|
+
const description = errors.isError(error) ? error.message : "Unknown error";
|
|
129
|
+
this.logger.error(
|
|
130
|
+
`Failed to get authorization session: ${description}`,
|
|
131
|
+
error
|
|
132
|
+
);
|
|
133
|
+
return res.status(404).json({
|
|
134
|
+
error: "not_found",
|
|
135
|
+
error_description: description
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
router.post("/v1/sessions/:sessionId/approve", async (req, res) => {
|
|
140
|
+
const { sessionId } = req.params;
|
|
141
|
+
if (!sessionId) {
|
|
142
|
+
return res.status(400).json({
|
|
143
|
+
error: "invalid_request",
|
|
144
|
+
error_description: "Missing authorization session ID"
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
try {
|
|
148
|
+
const httpCredentials = await this.httpAuth.credentials(req);
|
|
149
|
+
if (!this.auth.isPrincipal(httpCredentials, "user")) {
|
|
150
|
+
return res.status(401).json({
|
|
151
|
+
error: "unauthorized",
|
|
152
|
+
error_description: "Authentication required"
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
const { userEntityRef } = httpCredentials.principal;
|
|
156
|
+
const result = await this.oidc.approveAuthorizationSession({
|
|
157
|
+
sessionId,
|
|
158
|
+
userEntityRef
|
|
159
|
+
});
|
|
160
|
+
return res.json({
|
|
161
|
+
redirectUrl: result.redirectUrl
|
|
162
|
+
});
|
|
163
|
+
} catch (error) {
|
|
164
|
+
const description = errors.isError(error) ? error.message : "Unknown error";
|
|
165
|
+
this.logger.error(
|
|
166
|
+
`Failed to approve authorization session: ${description}`,
|
|
167
|
+
error
|
|
168
|
+
);
|
|
169
|
+
return res.status(400).json({
|
|
170
|
+
error: "invalid_request",
|
|
171
|
+
error_description: description
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
router.post("/v1/sessions/:sessionId/reject", async (req, res) => {
|
|
176
|
+
const { sessionId } = req.params;
|
|
177
|
+
if (!sessionId) {
|
|
178
|
+
return res.status(400).json({
|
|
179
|
+
error: "invalid_request",
|
|
180
|
+
error_description: "Missing authorization session ID"
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
const httpCredentials = await this.httpAuth.credentials(req);
|
|
184
|
+
if (!this.auth.isPrincipal(httpCredentials, "user")) {
|
|
185
|
+
return res.status(401).json({
|
|
186
|
+
error: "unauthorized",
|
|
187
|
+
error_description: "Authentication required"
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
const { userEntityRef } = httpCredentials.principal;
|
|
191
|
+
try {
|
|
192
|
+
const session = await this.oidc.getAuthorizationSession({
|
|
193
|
+
sessionId
|
|
194
|
+
});
|
|
195
|
+
await this.oidc.rejectAuthorizationSession({
|
|
196
|
+
sessionId,
|
|
197
|
+
userEntityRef
|
|
198
|
+
});
|
|
199
|
+
const errorParams = new URLSearchParams();
|
|
200
|
+
errorParams.append("error", "access_denied");
|
|
201
|
+
errorParams.append("error_description", "User denied the request");
|
|
202
|
+
if (session.state) {
|
|
203
|
+
errorParams.append("state", session.state);
|
|
204
|
+
}
|
|
205
|
+
const redirectUrl = new URL(session.redirectUri);
|
|
206
|
+
redirectUrl.search = errorParams.toString();
|
|
207
|
+
return res.json({
|
|
208
|
+
redirectUrl: redirectUrl.toString()
|
|
209
|
+
});
|
|
210
|
+
} catch (error) {
|
|
211
|
+
const description = errors.isError(error) ? error.message : "Unknown error";
|
|
212
|
+
this.logger.error(
|
|
213
|
+
`Failed to reject authorization session: ${description}`,
|
|
214
|
+
error
|
|
215
|
+
);
|
|
216
|
+
return res.status(400).json({
|
|
217
|
+
error: "invalid_request",
|
|
218
|
+
error_description: description
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
router.post("/v1/token", async (req, res) => {
|
|
223
|
+
const {
|
|
224
|
+
grant_type: grantType,
|
|
225
|
+
code,
|
|
226
|
+
redirect_uri: redirectUri,
|
|
227
|
+
code_verifier: codeVerifier
|
|
228
|
+
} = req.body;
|
|
229
|
+
if (!grantType || !code || !redirectUri) {
|
|
230
|
+
this.logger.error(
|
|
231
|
+
`Failed to exchange code for token: Missing required parameters`
|
|
232
|
+
);
|
|
233
|
+
return res.status(400).json({
|
|
234
|
+
error: "invalid_request",
|
|
235
|
+
error_description: "Missing required parameters"
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
try {
|
|
239
|
+
const result = await this.oidc.exchangeCodeForToken({
|
|
240
|
+
code,
|
|
241
|
+
redirectUri,
|
|
242
|
+
codeVerifier,
|
|
243
|
+
grantType
|
|
244
|
+
});
|
|
245
|
+
return res.json({
|
|
246
|
+
access_token: result.accessToken,
|
|
247
|
+
token_type: result.tokenType,
|
|
248
|
+
expires_in: result.expiresIn,
|
|
249
|
+
id_token: result.idToken,
|
|
250
|
+
scope: result.scope
|
|
251
|
+
});
|
|
252
|
+
} catch (error) {
|
|
253
|
+
const description = errors.isError(error) ? error.message : "Unknown error";
|
|
254
|
+
this.logger.error(
|
|
255
|
+
`Failed to exchange code for token: ${description}`,
|
|
256
|
+
error
|
|
257
|
+
);
|
|
258
|
+
if (errors.isError(error)) {
|
|
259
|
+
if (error.name === "AuthenticationError") {
|
|
260
|
+
return res.status(401).json({
|
|
261
|
+
error: "invalid_client",
|
|
262
|
+
error_description: error.message
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
if (error.name === "InputError") {
|
|
266
|
+
return res.status(400).json({
|
|
267
|
+
error: "invalid_request",
|
|
268
|
+
error_description: error.message
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return res.status(500).json({
|
|
273
|
+
error: "server_error",
|
|
274
|
+
error_description: description
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
router.post("/v1/register", async (req, res) => {
|
|
279
|
+
const {
|
|
280
|
+
client_name: clientName,
|
|
281
|
+
redirect_uris: redirectUris,
|
|
282
|
+
response_types: responseTypes,
|
|
283
|
+
grant_types: grantTypes,
|
|
284
|
+
scope
|
|
285
|
+
} = req.body;
|
|
286
|
+
if (!redirectUris?.length) {
|
|
287
|
+
res.status(400).json({
|
|
288
|
+
error: "invalid_request",
|
|
289
|
+
error_description: "redirect_uris is required"
|
|
290
|
+
});
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
try {
|
|
294
|
+
const client = await this.oidc.registerClient({
|
|
295
|
+
clientName,
|
|
296
|
+
redirectUris,
|
|
297
|
+
responseTypes,
|
|
298
|
+
grantTypes,
|
|
299
|
+
scope
|
|
300
|
+
});
|
|
301
|
+
res.status(201).json({
|
|
302
|
+
client_id: client.clientId,
|
|
303
|
+
redirect_uris: client.redirectUris,
|
|
304
|
+
client_secret: client.clientSecret
|
|
305
|
+
});
|
|
306
|
+
} catch (e) {
|
|
307
|
+
const description = errors.isError(e) ? e.message : "Unknown error";
|
|
308
|
+
this.logger.error(`Failed to register client: ${description}`, e);
|
|
309
|
+
res.status(500).json({
|
|
310
|
+
error: "server_error",
|
|
311
|
+
error_description: `Failed to register client: ${description}`
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
}
|
|
43
316
|
return router;
|
|
44
317
|
}
|
|
45
318
|
}
|
|
319
|
+
function ensureTrailingSlash(appUrl) {
|
|
320
|
+
if (appUrl.endsWith("/")) {
|
|
321
|
+
return appUrl;
|
|
322
|
+
}
|
|
323
|
+
return `${appUrl}/`;
|
|
324
|
+
}
|
|
46
325
|
|
|
47
326
|
exports.OidcRouter = OidcRouter;
|
|
48
327
|
//# sourceMappingURL=OidcRouter.cjs.js.map
|