@backstage/plugin-auth-backend-module-cloudflare-access-provider 0.0.0-nightly-20240420021132

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 ADDED
@@ -0,0 +1,25 @@
1
+ # @backstage/plugin-auth-backend-module-cloudflare-access-provider
2
+
3
+ ## 0.0.0-nightly-20240420021132
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies
8
+ - @backstage/backend-plugin-api@0.0.0-nightly-20240420021132
9
+ - @backstage/config@1.2.0
10
+ - @backstage/errors@1.2.4
11
+ - @backstage/plugin-auth-node@0.0.0-nightly-20240420021132
12
+
13
+ ## 0.1.0
14
+
15
+ ### Minor Changes
16
+
17
+ - c26218d: Created a separate module for the Cloudflare Access auth provider
18
+
19
+ ### Patch Changes
20
+
21
+ - Updated dependencies
22
+ - @backstage/backend-plugin-api@0.6.17
23
+ - @backstage/plugin-auth-node@0.4.12
24
+ - @backstage/config@1.2.0
25
+ - @backstage/errors@1.2.4
package/README.md ADDED
@@ -0,0 +1,3 @@
1
+ # @backstage/plugin-auth-backend-module-cloudflare-access-provider
2
+
3
+ The Cloudflare Access provider backend module for the auth plugin.
package/config.d.ts ADDED
@@ -0,0 +1,37 @@
1
+ /*
2
+ * Copyright 2020 The Backstage Authors
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ import { HumanDuration } from '@backstage/types';
18
+
19
+ export interface Config {
20
+ auth?: {
21
+ providers?: {
22
+ /** @visibility frontend */
23
+ cfaccess?: {
24
+ teamName: string;
25
+ /** @deepVisibility secret */
26
+ serviceTokens?: Array<{
27
+ token: string;
28
+ subject: string;
29
+ }>;
30
+ };
31
+ /**
32
+ * The backstage token expiration.
33
+ */
34
+ backstageTokenExpiration?: HumanDuration;
35
+ };
36
+ };
37
+ }
@@ -0,0 +1,194 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
5
+ var pluginAuthNode = require('@backstage/plugin-auth-node');
6
+ var errors = require('@backstage/errors');
7
+ var jose = require('jose');
8
+ var backendPluginApi = require('@backstage/backend-plugin-api');
9
+
10
+ const CF_JWT_HEADER = "cf-access-jwt-assertion";
11
+ const COOKIE_AUTH_NAME = "CF_Authorization";
12
+ const CACHE_PREFIX = "providers/cloudflare-access/profile-v1";
13
+
14
+ class AuthHelper {
15
+ constructor(teamName, serviceTokens, keySet, cache) {
16
+ this.teamName = teamName;
17
+ this.serviceTokens = serviceTokens;
18
+ this.keySet = keySet;
19
+ this.cache = cache;
20
+ }
21
+ static fromConfig(config, options) {
22
+ var _a, _b;
23
+ const teamName = config.getString("teamName");
24
+ const serviceTokens = (_b = (_a = config.getOptionalConfigArray("serviceTokens")) != null ? _a : []) == null ? void 0 : _b.map((cfg) => {
25
+ return {
26
+ token: cfg.getString("token"),
27
+ subject: cfg.getString("subject")
28
+ };
29
+ });
30
+ const keySet = jose.createRemoteJWKSet(
31
+ new URL(`https://${teamName}.cloudflareaccess.com/cdn-cgi/access/certs`)
32
+ );
33
+ return new AuthHelper(teamName, serviceTokens, keySet, options == null ? void 0 : options.cache);
34
+ }
35
+ async authenticate(req) {
36
+ var _a, _b;
37
+ let jwt = req.header(CF_JWT_HEADER);
38
+ if (!jwt) {
39
+ jwt = req.cookies.CF_Authorization;
40
+ }
41
+ if (!jwt) {
42
+ throw new errors.AuthenticationError(
43
+ `Missing ${CF_JWT_HEADER} from Cloudflare Access`
44
+ );
45
+ }
46
+ const verifyResult = await jose.jwtVerify(jwt, this.keySet, {
47
+ issuer: `https://${this.teamName}.cloudflareaccess.com`
48
+ });
49
+ const isServiceToken = !verifyResult.payload.sub;
50
+ const subject = isServiceToken ? verifyResult.payload.common_name : verifyResult.payload.sub;
51
+ if (!subject) {
52
+ throw new errors.AuthenticationError(
53
+ `Missing both sub and common_name from Cloudflare Access JWT`
54
+ );
55
+ }
56
+ const serviceToken = this.serviceTokens.find((st) => st.token === subject);
57
+ if (isServiceToken && !serviceToken) {
58
+ throw new errors.AuthenticationError(
59
+ `${subject} is not a permitted Service Token.`
60
+ );
61
+ }
62
+ const cacheKey = `${CACHE_PREFIX}/${subject}`;
63
+ const cfAccessResultStr = await ((_a = this.cache) == null ? void 0 : _a.get(cacheKey));
64
+ if (typeof cfAccessResultStr === "string") {
65
+ const result = JSON.parse(cfAccessResultStr);
66
+ return {
67
+ ...result,
68
+ token: jwt
69
+ };
70
+ }
71
+ const claims = verifyResult.payload;
72
+ try {
73
+ let cfIdentity;
74
+ if (serviceToken) {
75
+ cfIdentity = {
76
+ id: subject,
77
+ name: "Bot",
78
+ email: serviceToken.subject,
79
+ groups: []
80
+ };
81
+ } else {
82
+ cfIdentity = await this.getIdentityProfile(jwt);
83
+ }
84
+ const cfAccessResult = {
85
+ claims,
86
+ cfIdentity,
87
+ expiresInSeconds: claims.exp - claims.iat
88
+ };
89
+ (_b = this.cache) == null ? void 0 : _b.set(cacheKey, JSON.stringify(cfAccessResult));
90
+ return {
91
+ ...cfAccessResult,
92
+ token: jwt
93
+ };
94
+ } catch (err) {
95
+ throw new errors.ForwardedError(
96
+ "Failed to populate access identity information",
97
+ err
98
+ );
99
+ }
100
+ }
101
+ async getIdentityProfile(jwt) {
102
+ const headers = new Headers();
103
+ headers.set(CF_JWT_HEADER, jwt);
104
+ headers.set("cookie", `${COOKIE_AUTH_NAME}=${jwt}`);
105
+ try {
106
+ const res = await fetch(
107
+ `https://${this.teamName}.cloudflareaccess.com/cdn-cgi/access/get-identity`,
108
+ { headers }
109
+ );
110
+ if (!res.ok) {
111
+ throw await errors.ResponseError.fromResponse(res);
112
+ }
113
+ const cfIdentity = await res.json();
114
+ return cfIdentity;
115
+ } catch (err) {
116
+ throw new errors.ForwardedError("getIdentityProfile failed", err);
117
+ }
118
+ }
119
+ }
120
+
121
+ function createCloudflareAccessAuthenticator(options) {
122
+ return pluginAuthNode.createProxyAuthenticator({
123
+ async defaultProfileTransform(result) {
124
+ return {
125
+ profile: {
126
+ email: result.claims.email,
127
+ displayName: result.cfIdentity.name
128
+ }
129
+ };
130
+ },
131
+ initialize({ config }) {
132
+ return {
133
+ helper: AuthHelper.fromConfig(config, { cache: options == null ? void 0 : options.cache })
134
+ };
135
+ },
136
+ async authenticate({ req }, { helper }) {
137
+ const result = await helper.authenticate(req);
138
+ return {
139
+ result,
140
+ providerInfo: result
141
+ };
142
+ }
143
+ });
144
+ }
145
+
146
+ exports.cloudflareAccessSignInResolvers = void 0;
147
+ ((cloudflareAccessSignInResolvers2) => {
148
+ cloudflareAccessSignInResolvers2.emailMatchingUserEntityProfileEmail = pluginAuthNode.createSignInResolverFactory({
149
+ create() {
150
+ return async (info, ctx) => {
151
+ const { profile } = info;
152
+ if (!profile.email) {
153
+ throw new Error(
154
+ "Login failed, user profile does not contain an email"
155
+ );
156
+ }
157
+ return ctx.signInWithCatalogUser({
158
+ filter: {
159
+ "spec.profile.email": profile.email
160
+ }
161
+ });
162
+ };
163
+ }
164
+ });
165
+ })(exports.cloudflareAccessSignInResolvers || (exports.cloudflareAccessSignInResolvers = {}));
166
+
167
+ const authModuleCloudflareAccessProvider = backendPluginApi.createBackendModule({
168
+ pluginId: "auth",
169
+ moduleId: "cloudflare-access-provider",
170
+ register(reg) {
171
+ reg.registerInit({
172
+ deps: {
173
+ authProviders: pluginAuthNode.authProvidersExtensionPoint,
174
+ cache: backendPluginApi.coreServices.cache
175
+ },
176
+ async init({ authProviders, cache }) {
177
+ authProviders.registerProvider({
178
+ providerId: "cfaccess",
179
+ factory: pluginAuthNode.createProxyAuthProviderFactory({
180
+ authenticator: createCloudflareAccessAuthenticator({ cache }),
181
+ signInResolverFactories: {
182
+ ...exports.cloudflareAccessSignInResolvers,
183
+ ...pluginAuthNode.commonSignInResolvers
184
+ }
185
+ })
186
+ });
187
+ }
188
+ });
189
+ }
190
+ });
191
+
192
+ exports.createCloudflareAccessAuthenticator = createCloudflareAccessAuthenticator;
193
+ exports.default = authModuleCloudflareAccessProvider;
194
+ //# sourceMappingURL=index.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.cjs.js","sources":["../src/types.ts","../src/helpers.ts","../src/authenticator.ts","../src/resolvers.ts","../src/module.ts"],"sourcesContent":["/*\n * Copyright 2024 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\n// JWT Web Token definitions are in the URL below\n// https://developers.cloudflare.com/cloudflare-one/identity/users/validating-json/\nexport const CF_JWT_HEADER = 'cf-access-jwt-assertion';\nexport const CF_AUTH_IDENTITY = 'cf-access-authenticated-user-email';\nexport const COOKIE_AUTH_NAME = 'CF_Authorization';\nexport const CACHE_PREFIX = 'providers/cloudflare-access/profile-v1';\n\nexport type ServiceToken = {\n token: string;\n subject: string;\n};\n\n/**\n * Can be used in externally provided auth handler or sign in resolver to\n * enrich user profile for sign-in user entity\n *\n * @public\n */\nexport type CloudflareAccessClaims = {\n /**\n * `aud` identifies the application to which the JWT is issued.\n */\n aud: string[];\n /**\n * `email` contains the email address of the authenticated user.\n */\n email: string;\n /**\n * iat and exp are the issuance and expiration timestamps.\n */\n exp: number;\n iat: number;\n /**\n * `nonce` is the session identifier.\n */\n nonce: string;\n /**\n * `identity_nonce` is available in the Application Token and can be used to\n * query all group membership for a given user.\n */\n identity_nonce: string;\n /**\n * `sub` contains the identifier of the authenticated user.\n */\n sub: string;\n /**\n * `iss` the issuer is the application’s Cloudflare Access Domain URL.\n */\n iss: string;\n /**\n * `custom` contains SAML attributes in the Application Token specified by an\n * administrator in the identity provider configuration.\n */\n custom: string;\n};\n\n/**\n * @public\n */\nexport type CloudflareAccessGroup = {\n /**\n * Group id\n */\n id: string;\n /**\n * Name of group as defined in Cloudflare zero trust dashboard\n */\n name: string;\n /**\n * Access group email address\n */\n email: string;\n};\n\n/**\n * @public\n */\nexport type CloudflareAccessIdentityProfile = {\n id: string;\n name: string;\n email: string;\n groups: CloudflareAccessGroup[];\n};\n\n/**\n * @public\n */\nexport type CloudflareAccessResult = {\n claims: CloudflareAccessClaims;\n cfIdentity: CloudflareAccessIdentityProfile;\n expiresInSeconds?: number;\n token: string;\n};\n","/*\n * Copyright 2024 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 { CacheService } from '@backstage/backend-plugin-api';\nimport { Config } from '@backstage/config';\nimport {\n AuthenticationError,\n ForwardedError,\n ResponseError,\n} from '@backstage/errors';\nimport express from 'express';\nimport { createRemoteJWKSet, jwtVerify } from 'jose';\nimport {\n CACHE_PREFIX,\n CF_JWT_HEADER,\n COOKIE_AUTH_NAME,\n CloudflareAccessClaims,\n CloudflareAccessIdentityProfile,\n CloudflareAccessResult,\n ServiceToken,\n} from './types';\n\nexport class AuthHelper {\n static fromConfig(\n config: Config,\n options?: { cache?: CacheService },\n ): AuthHelper {\n const teamName = config.getString('teamName');\n const serviceTokens = (\n config.getOptionalConfigArray('serviceTokens') ?? []\n )?.map(cfg => {\n return {\n token: cfg.getString('token'),\n subject: cfg.getString('subject'),\n } as ServiceToken;\n });\n\n const keySet = createRemoteJWKSet(\n new URL(`https://${teamName}.cloudflareaccess.com/cdn-cgi/access/certs`),\n );\n\n return new AuthHelper(teamName, serviceTokens, keySet, options?.cache);\n }\n\n private constructor(\n private readonly teamName: string,\n private readonly serviceTokens: ServiceToken[],\n private readonly keySet: ReturnType<typeof createRemoteJWKSet>,\n private readonly cache?: CacheService,\n ) {}\n\n async authenticate(req: express.Request): Promise<CloudflareAccessResult> {\n // JWTs generated by Access are available in a request header as\n // Cf-Access-Jwt-Assertion and as cookies as CF_Authorization.\n let jwt = req.header(CF_JWT_HEADER);\n if (!jwt) {\n jwt = req.cookies.CF_Authorization;\n }\n if (!jwt) {\n // Only throw if both are not provided by Cloudflare Access since either\n // can be used.\n throw new AuthenticationError(\n `Missing ${CF_JWT_HEADER} from Cloudflare Access`,\n );\n }\n\n // Cloudflare signs the JWT using the RSA Signature with SHA-256 (RS256).\n // RS256 follows an asymmetric algorithm; a private key signs the JWTs and\n // a separate public key verifies the signature.\n const verifyResult = await jwtVerify(jwt, this.keySet, {\n issuer: `https://${this.teamName}.cloudflareaccess.com`,\n });\n\n const isServiceToken = !verifyResult.payload.sub;\n\n const subject = isServiceToken\n ? (verifyResult.payload.common_name as string)\n : verifyResult.payload.sub;\n if (!subject) {\n throw new AuthenticationError(\n `Missing both sub and common_name from Cloudflare Access JWT`,\n );\n }\n\n const serviceToken = this.serviceTokens.find(st => st.token === subject);\n if (isServiceToken && !serviceToken) {\n throw new AuthenticationError(\n `${subject} is not a permitted Service Token.`,\n );\n }\n\n const cacheKey = `${CACHE_PREFIX}/${subject}`;\n const cfAccessResultStr = await this.cache?.get(cacheKey);\n if (typeof cfAccessResultStr === 'string') {\n const result = JSON.parse(cfAccessResultStr) as CloudflareAccessResult;\n return {\n ...result,\n token: jwt,\n };\n }\n const claims = verifyResult.payload as CloudflareAccessClaims;\n\n // Builds a passport profile from JWT claims first\n try {\n let cfIdentity: CloudflareAccessIdentityProfile;\n if (serviceToken) {\n cfIdentity = {\n id: subject,\n name: 'Bot',\n email: serviceToken.subject,\n groups: [],\n };\n } else {\n // If we successfully fetch the get-identity endpoint,\n // We supplement the passport profile with richer user identity\n // information here.\n cfIdentity = await this.getIdentityProfile(jwt);\n }\n // Stores a stringified JSON object in cfaccess provider cache only when\n // we complete all steps\n const cfAccessResult = {\n claims,\n cfIdentity,\n expiresInSeconds: claims.exp - claims.iat,\n };\n this.cache?.set(cacheKey, JSON.stringify(cfAccessResult));\n return {\n ...cfAccessResult,\n token: jwt,\n };\n } catch (err) {\n throw new ForwardedError(\n 'Failed to populate access identity information',\n err,\n );\n }\n }\n\n private async getIdentityProfile(\n jwt: string,\n ): Promise<CloudflareAccessIdentityProfile> {\n const headers = new Headers();\n // set both headers just the way inbound responses are set\n headers.set(CF_JWT_HEADER, jwt);\n headers.set('cookie', `${COOKIE_AUTH_NAME}=${jwt}`);\n try {\n const res = await fetch(\n `https://${this.teamName}.cloudflareaccess.com/cdn-cgi/access/get-identity`,\n { headers },\n );\n if (!res.ok) {\n throw await ResponseError.fromResponse(res);\n }\n const cfIdentity = await res.json();\n return cfIdentity as unknown as CloudflareAccessIdentityProfile;\n } catch (err) {\n throw new ForwardedError('getIdentityProfile failed', err);\n }\n }\n}\n","/*\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 { CacheService } from '@backstage/backend-plugin-api';\nimport {\n ProxyAuthenticator,\n createProxyAuthenticator,\n} from '@backstage/plugin-auth-node';\nimport { AuthHelper } from './helpers';\nimport { CloudflareAccessResult } from './types';\n\n/**\n * Implements Cloudflare Access authentication.\n *\n * @public\n */\nexport function createCloudflareAccessAuthenticator(options?: {\n cache?: CacheService;\n}): ProxyAuthenticator<\n unknown,\n CloudflareAccessResult,\n CloudflareAccessResult\n> {\n return createProxyAuthenticator({\n async defaultProfileTransform(result: CloudflareAccessResult) {\n return {\n profile: {\n email: result.claims.email,\n displayName: result.cfIdentity.name,\n },\n };\n },\n initialize({ config }) {\n return {\n helper: AuthHelper.fromConfig(config, { cache: options?.cache }),\n };\n },\n async authenticate({ req }, { helper }) {\n const result = await helper.authenticate(req);\n return {\n result,\n providerInfo: result,\n };\n },\n });\n}\n","/*\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 createSignInResolverFactory,\n SignInInfo,\n} from '@backstage/plugin-auth-node';\nimport { CloudflareAccessResult } from './types';\n\n/**\n * Available sign-in resolvers for the Cloudflare Access auth provider.\n *\n * @public\n */\nexport namespace cloudflareAccessSignInResolvers {\n /**\n * Looks up the user by matching their email to the entity email.\n */\n export const emailMatchingUserEntityProfileEmail =\n createSignInResolverFactory({\n create() {\n return async (info: SignInInfo<CloudflareAccessResult>, ctx) => {\n const { profile } = info;\n\n if (!profile.email) {\n throw new Error(\n 'Login failed, user profile does not contain an email',\n );\n }\n\n return ctx.signInWithCatalogUser({\n filter: {\n 'spec.profile.email': profile.email,\n },\n });\n };\n },\n });\n}\n","/*\n * Copyright 2024 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 createBackendModule,\n} from '@backstage/backend-plugin-api';\nimport {\n authProvidersExtensionPoint,\n commonSignInResolvers,\n createProxyAuthProviderFactory,\n} from '@backstage/plugin-auth-node';\nimport { createCloudflareAccessAuthenticator } from './authenticator';\nimport { cloudflareAccessSignInResolvers } from './resolvers';\n\n/**\n * The Cloudflare Access provider backend module for the auth plugin.\n *\n * @public\n */\nexport const authModuleCloudflareAccessProvider = createBackendModule({\n pluginId: 'auth',\n moduleId: 'cloudflare-access-provider',\n register(reg) {\n reg.registerInit({\n deps: {\n authProviders: authProvidersExtensionPoint,\n cache: coreServices.cache,\n },\n async init({ authProviders, cache }) {\n authProviders.registerProvider({\n providerId: 'cfaccess',\n factory: createProxyAuthProviderFactory({\n authenticator: createCloudflareAccessAuthenticator({ cache }),\n signInResolverFactories: {\n ...cloudflareAccessSignInResolvers,\n ...commonSignInResolvers,\n },\n }),\n });\n },\n });\n },\n});\n"],"names":["createRemoteJWKSet","AuthenticationError","jwtVerify","ForwardedError","ResponseError","createProxyAuthenticator","cloudflareAccessSignInResolvers","createSignInResolverFactory","createBackendModule","authProvidersExtensionPoint","coreServices","createProxyAuthProviderFactory","commonSignInResolvers"],"mappings":"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"}
@@ -0,0 +1,113 @@
1
+ import * as _backstage_backend_plugin_api from '@backstage/backend-plugin-api';
2
+ import { CacheService } from '@backstage/backend-plugin-api';
3
+ import * as _backstage_plugin_auth_node from '@backstage/plugin-auth-node';
4
+ import { ProxyAuthenticator } from '@backstage/plugin-auth-node';
5
+
6
+ /**
7
+ * Can be used in externally provided auth handler or sign in resolver to
8
+ * enrich user profile for sign-in user entity
9
+ *
10
+ * @public
11
+ */
12
+ type CloudflareAccessClaims = {
13
+ /**
14
+ * `aud` identifies the application to which the JWT is issued.
15
+ */
16
+ aud: string[];
17
+ /**
18
+ * `email` contains the email address of the authenticated user.
19
+ */
20
+ email: string;
21
+ /**
22
+ * iat and exp are the issuance and expiration timestamps.
23
+ */
24
+ exp: number;
25
+ iat: number;
26
+ /**
27
+ * `nonce` is the session identifier.
28
+ */
29
+ nonce: string;
30
+ /**
31
+ * `identity_nonce` is available in the Application Token and can be used to
32
+ * query all group membership for a given user.
33
+ */
34
+ identity_nonce: string;
35
+ /**
36
+ * `sub` contains the identifier of the authenticated user.
37
+ */
38
+ sub: string;
39
+ /**
40
+ * `iss` the issuer is the application’s Cloudflare Access Domain URL.
41
+ */
42
+ iss: string;
43
+ /**
44
+ * `custom` contains SAML attributes in the Application Token specified by an
45
+ * administrator in the identity provider configuration.
46
+ */
47
+ custom: string;
48
+ };
49
+ /**
50
+ * @public
51
+ */
52
+ type CloudflareAccessGroup = {
53
+ /**
54
+ * Group id
55
+ */
56
+ id: string;
57
+ /**
58
+ * Name of group as defined in Cloudflare zero trust dashboard
59
+ */
60
+ name: string;
61
+ /**
62
+ * Access group email address
63
+ */
64
+ email: string;
65
+ };
66
+ /**
67
+ * @public
68
+ */
69
+ type CloudflareAccessIdentityProfile = {
70
+ id: string;
71
+ name: string;
72
+ email: string;
73
+ groups: CloudflareAccessGroup[];
74
+ };
75
+ /**
76
+ * @public
77
+ */
78
+ type CloudflareAccessResult = {
79
+ claims: CloudflareAccessClaims;
80
+ cfIdentity: CloudflareAccessIdentityProfile;
81
+ expiresInSeconds?: number;
82
+ token: string;
83
+ };
84
+
85
+ /**
86
+ * Implements Cloudflare Access authentication.
87
+ *
88
+ * @public
89
+ */
90
+ declare function createCloudflareAccessAuthenticator(options?: {
91
+ cache?: CacheService;
92
+ }): ProxyAuthenticator<unknown, CloudflareAccessResult, CloudflareAccessResult>;
93
+
94
+ /**
95
+ * The Cloudflare Access provider backend module for the auth plugin.
96
+ *
97
+ * @public
98
+ */
99
+ declare const authModuleCloudflareAccessProvider: () => _backstage_backend_plugin_api.BackendFeature;
100
+
101
+ /**
102
+ * Available sign-in resolvers for the Cloudflare Access auth provider.
103
+ *
104
+ * @public
105
+ */
106
+ declare namespace cloudflareAccessSignInResolvers {
107
+ /**
108
+ * Looks up the user by matching their email to the entity email.
109
+ */
110
+ const emailMatchingUserEntityProfileEmail: _backstage_plugin_auth_node.SignInResolverFactory<CloudflareAccessResult, unknown>;
111
+ }
112
+
113
+ export { type CloudflareAccessClaims, type CloudflareAccessGroup, type CloudflareAccessIdentityProfile, type CloudflareAccessResult, cloudflareAccessSignInResolvers, createCloudflareAccessAuthenticator, authModuleCloudflareAccessProvider as default };
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@backstage/plugin-auth-backend-module-cloudflare-access-provider",
3
+ "version": "0.0.0-nightly-20240420021132",
4
+ "description": "The cloudflare-access-provider backend module for the auth plugin.",
5
+ "backstage": {
6
+ "role": "backend-plugin-module"
7
+ },
8
+ "publishConfig": {
9
+ "access": "public",
10
+ "main": "dist/index.cjs.js",
11
+ "types": "dist/index.d.ts"
12
+ },
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "https://github.com/backstage/backstage",
16
+ "directory": "plugins/auth-backend-module-cloudflare-access-provider"
17
+ },
18
+ "license": "Apache-2.0",
19
+ "main": "dist/index.cjs.js",
20
+ "types": "dist/index.d.ts",
21
+ "files": [
22
+ "config.d.ts",
23
+ "dist"
24
+ ],
25
+ "scripts": {
26
+ "build": "backstage-cli package build",
27
+ "clean": "backstage-cli package clean",
28
+ "lint": "backstage-cli package lint",
29
+ "prepack": "backstage-cli package prepack",
30
+ "postpack": "backstage-cli package postpack",
31
+ "start": "backstage-cli package start",
32
+ "test": "backstage-cli package test"
33
+ },
34
+ "dependencies": {
35
+ "@backstage/backend-plugin-api": "^0.0.0-nightly-20240420021132",
36
+ "@backstage/config": "^1.2.0",
37
+ "@backstage/errors": "^1.2.4",
38
+ "@backstage/plugin-auth-node": "^0.0.0-nightly-20240420021132",
39
+ "express": "^4.18.2",
40
+ "jose": "^5.0.0",
41
+ "node-fetch": "^2.6.7"
42
+ },
43
+ "devDependencies": {
44
+ "@backstage/backend-defaults": "^0.0.0-nightly-20240420021132",
45
+ "@backstage/backend-test-utils": "^0.0.0-nightly-20240420021132",
46
+ "@backstage/cli": "^0.0.0-nightly-20240420021132",
47
+ "@backstage/plugin-auth-backend": "^0.0.0-nightly-20240420021132",
48
+ "@backstage/types": "^1.1.1",
49
+ "msw": "^2.0.0",
50
+ "node-mocks-http": "^1.0.0",
51
+ "uuid": "^9.0.0"
52
+ },
53
+ "configSchema": "config.d.ts"
54
+ }