@backstage/plugin-auth-backend 0.0.0-nightly-202191922036 → 0.0.0-nightly-2021101122259

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 CHANGED
@@ -1,16 +1,47 @@
1
1
  # @backstage/plugin-auth-backend
2
2
 
3
- ## 0.0.0-nightly-202191922036
3
+ ## 0.0.0-nightly-2021101122259
4
4
 
5
5
  ### Patch Changes
6
6
 
7
+ - 5ee31f860b: Only use settings that have a value when creating a new FirestoreKeyStore instance
8
+ - 3e0e2f09d5: Added forwarding of the `audience` option for the SAML provider, making it possible to enable `audience` verification.
9
+ - Updated dependencies
10
+ - @backstage/backend-common@0.0.0-nightly-2021101122259
11
+ - @backstage/test-utils@0.0.0-nightly-2021101122259
12
+ - @backstage/catalog-client@0.0.0-nightly-2021101122259
13
+
14
+ ## 0.4.6
15
+
16
+ ### Patch Changes
17
+
18
+ - 3b767f19c9: Allow OAuth state to be encoded by a stateEncoder.
19
+ - Updated dependencies
20
+ - @backstage/test-utils@0.1.20
21
+ - @backstage/config@0.1.11
22
+ - @backstage/errors@0.1.4
23
+ - @backstage/backend-common@0.9.8
24
+ - @backstage/catalog-model@0.9.6
25
+
26
+ ## 0.4.5
27
+
28
+ ### Patch Changes
29
+
30
+ - 9322e632e9: Require that audience URLs for Okta authentication start with https
7
31
  - de3e26aecc: Fix a bug preventing an access token to be refreshed a second time with the GitHub provider.
32
+ - ab9b4a6ea6: Add Firestore as key-store provider.
33
+ Add `auth.keyStore` section to application config.
34
+ - 202f322927: Atlassian auth provider
35
+
36
+ - AtlassianAuth added to core-app-api
37
+ - Atlassian provider added to plugin-auth-backend
38
+ - Updated user-settings with Atlassian connection
39
+
40
+ - 36e67d2f24: Internal updates to apply more strict checks to throw errors.
8
41
  - Updated dependencies
9
- - @backstage/backend-common@0.0.0-nightly-202191922036
10
- - @backstage/catalog-client@0.0.0-nightly-202191922036
11
- - @backstage/catalog-model@0.0.0-nightly-202191922036
12
- - @backstage/errors@0.0.0-nightly-202191922036
13
- - @backstage/test-utils@0.0.0-nightly-202191922036
42
+ - @backstage/backend-common@0.9.7
43
+ - @backstage/errors@0.1.3
44
+ - @backstage/catalog-model@0.9.5
14
45
 
15
46
  ## 0.4.4
16
47
 
package/config.d.ts CHANGED
@@ -31,6 +31,32 @@ export interface Config {
31
31
  secret?: string;
32
32
  };
33
33
 
34
+ /** To control how to store JWK data in auth-backend */
35
+ keyStore?: {
36
+ provider?: 'database' | 'memory' | 'firestore';
37
+ firestore?: {
38
+ /** The host to connect to */
39
+ host?: string;
40
+ /** The port to connect to */
41
+ port?: number;
42
+ /** Whether to use SSL when connecting. */
43
+ ssl?: boolean;
44
+ /** The Google Cloud Project ID */
45
+ projectId?: string;
46
+ /**
47
+ * Local file containing the Service Account credentials.
48
+ * You can omit this value to automatically read from
49
+ * GOOGLE_APPLICATION_CREDENTIALS env which is useful for local
50
+ * development.
51
+ */
52
+ keyFilename?: string;
53
+ /** The path to use for the collection. Defaults to 'sessions' */
54
+ path?: string;
55
+ /** Timeout used for database operations. Defaults to 10000ms */
56
+ timeout?: number;
57
+ };
58
+ };
59
+
34
60
  /**
35
61
  * The available auth-provider options and attributes
36
62
  */
@@ -49,6 +75,7 @@ export interface Config {
49
75
  logoutUrl?: string;
50
76
  issuer: string;
51
77
  cert: string;
78
+ audience?: string;
52
79
  privateKey?: string;
53
80
  authnContext?: string[];
54
81
  identifierFormat?: string;
package/dist/index.cjs.js CHANGED
@@ -29,6 +29,8 @@ var catalogClient = require('@backstage/catalog-client');
29
29
  var uuid = require('uuid');
30
30
  var luxon = require('luxon');
31
31
  var backendCommon = require('@backstage/backend-common');
32
+ var firestore = require('@google-cloud/firestore');
33
+ var lodash = require('lodash');
32
34
  var session = require('express-session');
33
35
  var passport = require('passport');
34
36
  var minimatch = require('minimatch');
@@ -417,12 +419,10 @@ class OAuthAdapter {
417
419
  response
418
420
  });
419
421
  } catch (error) {
422
+ const {name, message} = errors.isError(error) ? error : new Error("Encountered invalid error");
420
423
  return postMessageResponse(res, appOrigin, {
421
424
  type: "authorization_response",
422
- error: {
423
- name: error.name,
424
- message: error.message
425
- }
425
+ error: {name, message}
426
426
  });
427
427
  }
428
428
  }
@@ -458,7 +458,7 @@ class OAuthAdapter {
458
458
  }
459
459
  res.status(200).json(response);
460
460
  } catch (error) {
461
- res.status(401).send(`${error.message}`);
461
+ res.status(401).send(String(error));
462
462
  }
463
463
  }
464
464
  async populateIdentity(identity) {
@@ -551,6 +551,7 @@ class GithubAuthProvider {
551
551
  constructor(options) {
552
552
  this.signInResolver = options.signInResolver;
553
553
  this.authHandler = options.authHandler;
554
+ this.stateEncoder = options.stateEncoder;
554
555
  this.tokenIssuer = options.tokenIssuer;
555
556
  this.catalogIdentityClient = options.catalogIdentityClient;
556
557
  this.logger = options.logger;
@@ -568,7 +569,7 @@ class GithubAuthProvider {
568
569
  async start(req) {
569
570
  return await executeRedirectStrategy(req, this._strategy, {
570
571
  scope: req.scope,
571
- state: encodeState(req.state)
572
+ state: (await this.stateEncoder(req)).encodedState
572
573
  });
573
574
  }
574
575
  async handler(req) {
@@ -634,7 +635,7 @@ const createGithubProvider = (options) => {
634
635
  catalogApi,
635
636
  logger
636
637
  }) => OAuthEnvironmentHandler.mapConfig(config, (envConfig) => {
637
- var _a, _b;
638
+ var _a, _b, _c;
638
639
  const clientId = envConfig.getString("clientId");
639
640
  const clientSecret = envConfig.getString("clientSecret");
640
641
  const enterpriseInstanceUrl = envConfig.getOptionalString("enterpriseInstanceUrl");
@@ -656,6 +657,9 @@ const createGithubProvider = (options) => {
656
657
  tokenIssuer,
657
658
  logger
658
659
  });
660
+ const stateEncoder = (_c = options == null ? void 0 : options.stateEncoder) != null ? _c : async (req) => {
661
+ return {encodedState: encodeState(req.state)};
662
+ };
659
663
  const provider = new GithubAuthProvider({
660
664
  clientId,
661
665
  clientSecret,
@@ -667,6 +671,7 @@ const createGithubProvider = (options) => {
667
671
  authHandler,
668
672
  tokenIssuer,
669
673
  catalogIdentityClient,
674
+ stateEncoder,
670
675
  logger
671
676
  });
672
677
  return OAuthAdapter.fromConfig(globalConfig, provider, {
@@ -1384,6 +1389,9 @@ const createOktaProvider = (_options) => {
1384
1389
  const clientSecret = envConfig.getString("clientSecret");
1385
1390
  const audience = envConfig.getString("audience");
1386
1391
  const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`;
1392
+ if (!audience.startsWith("https://")) {
1393
+ throw new Error("URL for 'audience' must start with 'https://'.");
1394
+ }
1387
1395
  const catalogIdentityClient = new CatalogIdentityClient({
1388
1396
  catalogApi,
1389
1397
  tokenIssuer
@@ -1555,6 +1563,177 @@ const createBitbucketProvider = (options) => {
1555
1563
  });
1556
1564
  };
1557
1565
 
1566
+ const defaultScopes = ["offline_access", "read:me"];
1567
+ class AtlassianStrategy extends OAuth2Strategy__default['default'] {
1568
+ constructor(options, verify) {
1569
+ if (!options.scope) {
1570
+ throw new TypeError("Atlassian requires a scope option");
1571
+ }
1572
+ const scopes = options.scope.split(" ");
1573
+ const optionsWithURLs = {
1574
+ ...options,
1575
+ authorizationURL: `https://auth.atlassian.com/authorize`,
1576
+ tokenURL: `https://auth.atlassian.com/oauth/token`,
1577
+ scope: Array.from(new Set([...defaultScopes, ...scopes]))
1578
+ };
1579
+ super(optionsWithURLs, verify);
1580
+ this.profileURL = "https://api.atlassian.com/me";
1581
+ this.name = "atlassian";
1582
+ this._oauth2.useAuthorizationHeaderforGET(true);
1583
+ }
1584
+ authorizationParams() {
1585
+ return {
1586
+ audience: "api.atlassian.com",
1587
+ prompt: "consent"
1588
+ };
1589
+ }
1590
+ userProfile(accessToken, done) {
1591
+ this._oauth2.get(this.profileURL, accessToken, (err, body) => {
1592
+ if (err) {
1593
+ return done(new OAuth2Strategy.InternalOAuthError("Failed to fetch user profile", err.statusCode));
1594
+ }
1595
+ if (!body) {
1596
+ return done(new Error("Failed to fetch user profile, body cannot be empty"));
1597
+ }
1598
+ try {
1599
+ const json = typeof body !== "string" ? body.toString() : body;
1600
+ const profile = AtlassianStrategy.parse(json);
1601
+ return done(null, profile);
1602
+ } catch (e) {
1603
+ return done(new Error("Failed to parse user profile"));
1604
+ }
1605
+ });
1606
+ }
1607
+ static parse(json) {
1608
+ const resp = JSON.parse(json);
1609
+ return {
1610
+ id: resp.account_id,
1611
+ provider: "atlassian",
1612
+ username: resp.nickname,
1613
+ displayName: resp.name,
1614
+ emails: [{value: resp.email}],
1615
+ photos: [{value: resp.picture}]
1616
+ };
1617
+ }
1618
+ }
1619
+
1620
+ const atlassianDefaultAuthHandler = async ({
1621
+ fullProfile,
1622
+ params
1623
+ }) => ({
1624
+ profile: makeProfileInfo(fullProfile, params.id_token)
1625
+ });
1626
+ class AtlassianAuthProvider {
1627
+ constructor(options) {
1628
+ this.catalogIdentityClient = options.catalogIdentityClient;
1629
+ this.logger = options.logger;
1630
+ this.tokenIssuer = options.tokenIssuer;
1631
+ this.authHandler = options.authHandler;
1632
+ this.signInResolver = options.signInResolver;
1633
+ this._strategy = new AtlassianStrategy({
1634
+ clientID: options.clientId,
1635
+ clientSecret: options.clientSecret,
1636
+ callbackURL: options.callbackUrl,
1637
+ scope: options.scopes
1638
+ }, (accessToken, refreshToken, params, fullProfile, done) => {
1639
+ done(void 0, {
1640
+ fullProfile,
1641
+ accessToken,
1642
+ refreshToken,
1643
+ params
1644
+ });
1645
+ });
1646
+ }
1647
+ async start(req) {
1648
+ return await executeRedirectStrategy(req, this._strategy, {
1649
+ state: encodeState(req.state)
1650
+ });
1651
+ }
1652
+ async handler(req) {
1653
+ var _a;
1654
+ const {result} = await executeFrameHandlerStrategy(req, this._strategy);
1655
+ return {
1656
+ response: await this.handleResult(result),
1657
+ refreshToken: (_a = result.refreshToken) != null ? _a : ""
1658
+ };
1659
+ }
1660
+ async handleResult(result) {
1661
+ const {profile} = await this.authHandler(result);
1662
+ const response = {
1663
+ providerInfo: {
1664
+ idToken: result.params.id_token,
1665
+ accessToken: result.accessToken,
1666
+ refreshToken: result.refreshToken,
1667
+ scope: result.params.scope,
1668
+ expiresInSeconds: result.params.expires_in
1669
+ },
1670
+ profile
1671
+ };
1672
+ if (this.signInResolver) {
1673
+ response.backstageIdentity = await this.signInResolver({
1674
+ result,
1675
+ profile
1676
+ }, {
1677
+ tokenIssuer: this.tokenIssuer,
1678
+ catalogIdentityClient: this.catalogIdentityClient,
1679
+ logger: this.logger
1680
+ });
1681
+ }
1682
+ return response;
1683
+ }
1684
+ async refresh(req) {
1685
+ const {
1686
+ accessToken,
1687
+ params,
1688
+ refreshToken: newRefreshToken
1689
+ } = await executeRefreshTokenStrategy(this._strategy, req.refreshToken, req.scope);
1690
+ const fullProfile = await executeFetchUserProfileStrategy(this._strategy, accessToken);
1691
+ return this.handleResult({
1692
+ fullProfile,
1693
+ params,
1694
+ accessToken,
1695
+ refreshToken: newRefreshToken
1696
+ });
1697
+ }
1698
+ }
1699
+ const createAtlassianProvider = (options) => {
1700
+ return ({
1701
+ providerId,
1702
+ globalConfig,
1703
+ config,
1704
+ tokenIssuer,
1705
+ catalogApi,
1706
+ logger
1707
+ }) => OAuthEnvironmentHandler.mapConfig(config, (envConfig) => {
1708
+ var _a, _b;
1709
+ const clientId = envConfig.getString("clientId");
1710
+ const clientSecret = envConfig.getString("clientSecret");
1711
+ const scopes = envConfig.getString("scopes");
1712
+ const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`;
1713
+ const catalogIdentityClient = new CatalogIdentityClient({
1714
+ catalogApi,
1715
+ tokenIssuer
1716
+ });
1717
+ const authHandler = (_a = options == null ? void 0 : options.authHandler) != null ? _a : atlassianDefaultAuthHandler;
1718
+ const provider = new AtlassianAuthProvider({
1719
+ clientId,
1720
+ clientSecret,
1721
+ scopes,
1722
+ callbackUrl,
1723
+ authHandler,
1724
+ signInResolver: (_b = options == null ? void 0 : options.signIn) == null ? void 0 : _b.resolver,
1725
+ catalogIdentityClient,
1726
+ logger,
1727
+ tokenIssuer
1728
+ });
1729
+ return OAuthAdapter.fromConfig(globalConfig, provider, {
1730
+ disableRefresh: true,
1731
+ providerId,
1732
+ tokenIssuer
1733
+ });
1734
+ });
1735
+ };
1736
+
1558
1737
  const ALB_JWT_HEADER = "x-amzn-oidc-data";
1559
1738
  const ALB_ACCESSTOKEN_HEADER = "x-amzn-oidc-accesstoken";
1560
1739
  const getJWTHeaders = (input) => {
@@ -1835,12 +2014,10 @@ class SamlAuthProvider {
1835
2014
  }
1836
2015
  });
1837
2016
  } catch (error) {
2017
+ const {name, message} = errors.isError(error) ? error : new Error("Encountered invalid error");
1838
2018
  return postMessageResponse(res, this.appUrl, {
1839
2019
  type: "authorization_response",
1840
- error: {
1841
- name: error.name,
1842
- message: error.message
1843
- }
2020
+ error: {name, message}
1844
2021
  });
1845
2022
  }
1846
2023
  }
@@ -1857,6 +2034,7 @@ const createSamlProvider = (_options) => {
1857
2034
  callbackUrl: `${globalConfig.baseUrl}/${providerId}/handler/frame`,
1858
2035
  entryPoint: config.getString("entryPoint"),
1859
2036
  logoutUrl: config.getOptionalString("logoutUrl"),
2037
+ audience: config.getOptionalString("audience"),
1860
2038
  issuer: config.getString("issuer"),
1861
2039
  cert: config.getString("cert"),
1862
2040
  privateCert: config.getOptionalString("privateKey"),
@@ -2070,7 +2248,8 @@ const factories = {
2070
2248
  oidc: createOidcProvider(),
2071
2249
  onelogin: createOneLoginProvider(),
2072
2250
  awsalb: createAwsAlbProvider(),
2073
- bitbucket: createBitbucketProvider()
2251
+ bitbucket: createBitbucketProvider(),
2252
+ atlassian: createAtlassianProvider()
2074
2253
  };
2075
2254
 
2076
2255
  function createOidcRouter(options) {
@@ -2289,6 +2468,121 @@ class DatabaseKeyStore {
2289
2468
  }
2290
2469
  }
2291
2470
 
2471
+ class MemoryKeyStore {
2472
+ constructor() {
2473
+ this.keys = new Map();
2474
+ }
2475
+ async addKey(key) {
2476
+ this.keys.set(key.kid, {
2477
+ createdAt: luxon.DateTime.utc().toJSDate(),
2478
+ key: JSON.stringify(key)
2479
+ });
2480
+ }
2481
+ async removeKeys(kids) {
2482
+ for (const kid of kids) {
2483
+ this.keys.delete(kid);
2484
+ }
2485
+ }
2486
+ async listKeys() {
2487
+ return {
2488
+ items: Array.from(this.keys).map(([, {createdAt, key: keyStr}]) => ({
2489
+ createdAt,
2490
+ key: JSON.parse(keyStr)
2491
+ }))
2492
+ };
2493
+ }
2494
+ }
2495
+
2496
+ const DEFAULT_TIMEOUT_MS = 1e4;
2497
+ const DEFAULT_DOCUMENT_PATH = "sessions";
2498
+ class FirestoreKeyStore {
2499
+ constructor(database, path, timeout) {
2500
+ this.database = database;
2501
+ this.path = path;
2502
+ this.timeout = timeout;
2503
+ }
2504
+ static async create(settings) {
2505
+ const {path, timeout, ...firestoreSettings} = settings != null ? settings : {};
2506
+ const database = new firestore.Firestore(firestoreSettings);
2507
+ return new FirestoreKeyStore(database, path != null ? path : DEFAULT_DOCUMENT_PATH, timeout != null ? timeout : DEFAULT_TIMEOUT_MS);
2508
+ }
2509
+ static async verifyConnection(keyStore, logger) {
2510
+ try {
2511
+ await keyStore.verify();
2512
+ } catch (error) {
2513
+ if (process.env.NODE_ENV !== "development") {
2514
+ throw new Error(`Failed to connect to database: ${error.message}`);
2515
+ }
2516
+ logger == null ? void 0 : logger.warn(`Failed to connect to database: ${error.message}`);
2517
+ }
2518
+ }
2519
+ async addKey(key) {
2520
+ await this.withTimeout(this.database.collection(this.path).doc(key.kid).set({
2521
+ kid: key.kid,
2522
+ key: JSON.stringify(key)
2523
+ }));
2524
+ }
2525
+ async listKeys() {
2526
+ const keys = await this.withTimeout(this.database.collection(this.path).get());
2527
+ return {
2528
+ items: keys.docs.map((key) => ({
2529
+ key: key.data(),
2530
+ createdAt: key.createTime.toDate()
2531
+ }))
2532
+ };
2533
+ }
2534
+ async removeKeys(kids) {
2535
+ for (const kid of kids) {
2536
+ await this.withTimeout(this.database.collection(this.path).doc(kid).delete());
2537
+ }
2538
+ }
2539
+ async withTimeout(operation) {
2540
+ const timer = new Promise((_, reject) => setTimeout(() => {
2541
+ reject(new Error(`Operation timed out after ${this.timeout}ms`));
2542
+ }, this.timeout));
2543
+ return Promise.race([operation, timer]);
2544
+ }
2545
+ async verify() {
2546
+ await this.withTimeout(this.database.collection(this.path).limit(1).get());
2547
+ }
2548
+ }
2549
+
2550
+ class KeyStores {
2551
+ static async fromConfig(config, options) {
2552
+ var _a;
2553
+ const {logger, database} = options != null ? options : {};
2554
+ const ks = config.getOptionalConfig("auth.keyStore");
2555
+ const provider = (_a = ks == null ? void 0 : ks.getOptionalString("provider")) != null ? _a : "database";
2556
+ logger == null ? void 0 : logger.info(`Configuring "${provider}" as KeyStore provider`);
2557
+ if (provider === "database") {
2558
+ if (!database) {
2559
+ throw new Error("This KeyStore provider requires a database");
2560
+ }
2561
+ return await DatabaseKeyStore.create({
2562
+ database: await database.getClient()
2563
+ });
2564
+ }
2565
+ if (provider === "memory") {
2566
+ return new MemoryKeyStore();
2567
+ }
2568
+ if (provider === "firestore") {
2569
+ const settings = ks == null ? void 0 : ks.getConfig(provider);
2570
+ const keyStore = await FirestoreKeyStore.create(lodash.pickBy({
2571
+ projectId: settings == null ? void 0 : settings.getOptionalString("projectId"),
2572
+ keyFilename: settings == null ? void 0 : settings.getOptionalString("keyFilename"),
2573
+ host: settings == null ? void 0 : settings.getOptionalString("host"),
2574
+ port: settings == null ? void 0 : settings.getOptionalNumber("port"),
2575
+ ssl: settings == null ? void 0 : settings.getOptionalBoolean("ssl"),
2576
+ path: settings == null ? void 0 : settings.getOptionalString("path"),
2577
+ timeout: settings == null ? void 0 : settings.getOptionalNumber("timeout")
2578
+ }, (value) => value !== void 0));
2579
+ await FirestoreKeyStore.verifyConnection(keyStore, logger);
2580
+ return keyStore;
2581
+ }
2582
+ throw new Error(`Unknown KeyStore provider: ${provider}`);
2583
+ }
2584
+ }
2585
+
2292
2586
  async function createRouter({
2293
2587
  logger,
2294
2588
  config,
@@ -2299,10 +2593,8 @@ async function createRouter({
2299
2593
  const router = Router__default['default']();
2300
2594
  const appUrl = config.getString("app.baseUrl");
2301
2595
  const authUrl = await discovery.getExternalBaseUrl("auth");
2596
+ const keyStore = await KeyStores.fromConfig(config, {logger, database});
2302
2597
  const keyDurationSeconds = 3600;
2303
- const keyStore = await DatabaseKeyStore.create({
2304
- database: await database.getClient()
2305
- });
2306
2598
  const tokenIssuer = new TokenFactory({
2307
2599
  issuer: authUrl,
2308
2600
  keyStore,
@@ -2353,6 +2645,7 @@ async function createRouter({
2353
2645
  }
2354
2646
  router.use(`/${providerId}`, r);
2355
2647
  } catch (e) {
2648
+ errors.assertError(e);
2356
2649
  if (process.env.NODE_ENV !== "development") {
2357
2650
  throw new Error(`Failed to initialize ${providerId} auth provider, ${e.message}`);
2358
2651
  }
@@ -2396,6 +2689,7 @@ exports.OAuthAdapter = OAuthAdapter;
2396
2689
  exports.OAuthEnvironmentHandler = OAuthEnvironmentHandler;
2397
2690
  exports.bitbucketUserIdSignInResolver = bitbucketUserIdSignInResolver;
2398
2691
  exports.bitbucketUsernameSignInResolver = bitbucketUsernameSignInResolver;
2692
+ exports.createAtlassianProvider = createAtlassianProvider;
2399
2693
  exports.createAwsAlbProvider = createAwsAlbProvider;
2400
2694
  exports.createBitbucketProvider = createBitbucketProvider;
2401
2695
  exports.createGithubProvider = createGithubProvider;