Last updated Jun 22, 2021

Rate this page:

Connect app authorization

This overview explains how Connect apps can perform user authorization checks against the permission model defined in Atlassian products.

Requests from Atlassian to Connect apps are authenticated using JSON Web Tokens (JWT) as described in Understanding JWT for Connect apps. These JWTs describe the user, the context (for example, a Jira issue or Confluence page), and the site the request was made from.

After authenticating a request, Connect apps must perform authorization checks to ensure only permitted users can execute actions within the app. These authorization checks are critical because:

  • Your Connect app may have more permissions than that of the user.
  • Module conditions are only intended to determine whether a module is visible. It may still be possible for users to interact directly with your app APIs with a hidden module condition.

Concepts

Performing adequate authorization checks within a Connect app relies on a few concepts:

Approaches for authorization

Your app should implement authorization checks:

  • On every endpoint exposed by your app.
  • When your app interacts with the product APIs to perform an action in response to a user request.
  • In cases where your app leverages product permissions to restrict or grant access to functionality within the app. For example, a product admin may be able to configure settings in your app intended only for admins.

Depending on your app and the interactions with the Atlassian cloud product APIs, your app should do one of the following:

Explicitly authorize requests

After authenticating a request to your app, perform an explicit authorization check using the permission check APIs:

These APIs allow you to check the permissions of the current user, or another user in the case of web triggers, without having to request privileged scopes as only the READ scope is required for permission checks.

If you want to reduce the latency overhead of the permission checks, cache the positive result (for example, user U has access to issue Y) for up to 15 minutes. This 15-minute duration matches the expiry time on context JWTs.

We do not recommend attempting to build an "offline copy" of the product permission model in your app, as it would quickly become out of date from the permissions enforced in the products.

Examples

Below are some examples of explicitly authorizing requests using JavaScript, that are compatible with Atlassian Connect Express. These examples can be adapted to other languages or frameworks your app may use. Some frameworks, such as Atlassian Connect Express 7.1.5 or later, contain request middleware that make checking authorization easier for apps.

Jira

Restrict API to Jira admins

Use the Jira get bulk permissions API to determine the global permissions assigned to a Jira user, including custom global permissions defined by your app. This can be used to restrict an API the app exposes so only Jira admins can call it. To check anonymous user permissions, make the request anonymously (without authentication).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
function userIsJiraAdmin(httpClient, userAccountId) {
  return new Promise((resolve, reject) =>
    httpClient.post(
      {
        url: "/rest/api/3/permissions/check",
        headers: {
          "X-Atlassian-Token": "nocheck",
        },
        anonymous: !userAccountId,
        json: {
          globalPermissions: ["ADMINISTER"],
          accountId: userAccountId,
        },
      },
      function (err, _, body) {
        if (err) {
          reject(err);
          return;
        }

        if (body.hasOwnProperty("errors")) {
          reject(body.errors);
          return;
        }

        const isAdmin = body.globalPermissions.indexOf("ADMINISTER") !== -1;
        resolve(isAdmin);
      }
    )
  );
}

app.get("/my-app-endpoint", [addon.authenticate()], async (req, res) => {
  // The user ID based on the context JWT
  // This could be any user ID if you needed to check if a different user was an admin
  const userId = req.context.userAccountId;
  if (!(await userIsJiraAdmin(addon.httpClient(req), userId))) {
    res.status(401).send("Unauthorized: you must be a Jira admin");
    return;
  }

  // perform admin-only actions

  res.status(200).send("OK");
});

// ACE 7.1.5+ example with context JWTs
app.get("/my-app-context-endpoint",
  [addon.authenticate(true), addon.authorizeJira({ global: ["ADMINISTER"] })],
  (req, res) => {
  // perform admin-only actions

  res.status(200).send("OK");
});
Restrict API to users with Jira project access

Use the Jira get bulk permissions API to determine the permissions that a user has to specific Jira issues or projects, including custom project permissions defined by your app. For example, restricting an endpoint so only Jira project admins can access it.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
function userHasProjectPermission(httpClient, userAccountId, projectPermission, projectId) {
   return new Promise((resolve, reject) =>
    httpClient.post(
      {
        url: "/rest/api/3/permissions/check",
        headers: {
          "X-Atlassian-Token": "nocheck",
        },
        // in the case of no user ID, make the request anonymously (e.g. with no Authorization header)
        anonymous: !userAccountId,
        json: {
          projectPermissions: [{
            permissions: [projectPermission],
            projects: [projectId],
          }],
          accountId: userAccountId,
        },
      },
      function (err, _, body) {
        if (err) {
          reject(err);
          return;
        }

        if (body.hasOwnProperty("errors")) {
          reject(body.errors);
          return;
        }

        const isProjectAdmin = body.projectPermissions.permission === projectPermission && body.projectPermissions[0].projects.indexOf(projectId) !== -1;
        resolve(isProjectAdmin);
      }
    )
  );
}

app.get("/my-app-endpoint/:projectId", [addon.authenticate(true /* allow context JWTs */)], async (req, res) => {
  // The user ID based on the context JWT
  // This could be any user ID if you needed to check if a different user was an admin
  const userAccountId = req.context.userAccountId;

  // The content the user is interacting with
  const { projectId } = req.params;
  const canAdmin = await userHasProjectPermission(addon.httpClient(req), userAccountId, "ADMINISTER_PROJECTS", projectId);
  if (!canAdmin) {
    res.status(401).send("Unauthorized: you don't have access to administer this project");
    return;
  }

  // do something with the project, e.g. with the product APIs or your own database

  res.status(200).send("ok");
});

// ACE 7.1.5+ example with context JWTs
app.get("/my-app-context-endpoint",
  [addon.authenticate(true), addon.authorizeJira({ project: ["ADMINISTER_PROJECTS"] })],
  (req, res) => {
  // do something with the project

  res.status(200).send("OK");
});

Confluence

Restrict API to Confluence admins

Use the Confluence get user with operations API to determine the operations that a user can perform. This can be used to restrict an API that the app exposes, so only Confluence admins can request it.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
function userIsConfluenceAdmin(httpClient, userAccountId) {
  return new Promise((resolve, reject) =>
    httpClient.get(
      {
        url: `/rest/api/user?accountId=${encodeURIComponent(userAccountId)}&expand=operations`,
        json: true
      },
      function (err, httpResponse, body) {
        if (err) {
          reject(err);
          return;
        }

        const isAdmin = body.operations.find(
          (op) =>
            op.operation === "administer" && op.targetType === "application"
        );
        resolve(isAdmin);
      }
    )
  );
}

app.get("/my-app-endpoint", [addon.authenticate()], async (req, res) => {
  // The user ID based on the context JWT
  // This could be any user ID if you needed to check if a different user was an admin
  const userId = req.context.userAccountId;
  if (!(await userIsConfluenceAdmin(addon.httpClient(req), userId))) {
    res.status(401).send("Unauthorized: you must be a Confluence admin");
    return;
  }

  // perform admin-only actions

  res.status(200).send("OK");
});

// ACE 7.1.5+ example with context JWTs
app.get("/my-app-context-endpoint",
  [addon.authenticate(true), addon.authorizeConfluence({ application: ["administer"] })],
  (req, res) => {
  // perform admin-only actions

  res.status(200).send("OK");
});
Restrict API to users with Confluence page access

Use the Confluence check content permissions API to determine the permissions that a user has to Confluence content, such as a page or comment.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
function userCanReadContent(httpClient, userAccountId, contentId) {
  // make sure the content ID is valid to prevent traversal
  if (!/^[A-Z0-9-]+$/i.test(contentId)) {
    throw new Error("Invalid content ID");
  }
  return new Promise((resolve, reject) =>
    httpClient.post(
      {
        url: `/rest/api/content/${encodeURIComponent(contentId)}/permission/check`,
        headers: {
          "X-Atlassian-Token": "no-check",
        },
        json: {
          subject: {
            type: "user",
            identifier: userAccountId,
          },
          operation: "read",
        },
      },
      function (err, httpResponse, body) {
        if (err) {
          reject(err);
          return;
        }

        resolve(body.hasPermission);
      }
    )
  );
}

app.get("/my-app-endpoint/:contentId", [addon.authenticate(true /* allow context JWTs */)], async (req, res) => {
  // The user ID based on the context JWT
  // This could be any user ID if you needed to check if a different user was an admin
  const userAccountId = req.context.userAccountId;

  // The content the user is interacting with
  const { contentId } = req.params;
  const canRead = await userCanReadContent(addon.httpClient(req), userAccountId, contentId);
  if (!canRead) {
    res.status(401).send("Unauthorized: you don't have access to this content");
    return;
  }

  // do something with the content, e.g. with the product APIs or your own database

  res.status(200).send("ok");
});

// ACE 7.1.5+ example with context JWTs
app.get("/my-app-context-endpoint",
  [addon.authenticate(true), addon.authorizeConfluence({ content: "read" })],
  (req, res) => {
  // do something with the conten

  res.status(200).send("OK");
});

Defer authorization to Atlassian products

If your app only interacts with product APIs and not other external systems or data, you may be able to defer authorization checks to the product APIs.

Via JavaScript API

If your app is static, and doesn't have a server component or your app server does not need to interact with the Atlassian product APIs, the JavaScript AP.request can be used to defer authorization to Atlassian.

This works best when:

  1. You only need to interact with the product APIs from within a web browser; and
  2. Users own the data they're interacting with. It's important to note that users are able to modify the requests or responses, as the requests are made via their web browser and not your app server.

Via impersonation

If your app already uses or relies on user impersonation, make requests to Atlassian APIs using the interacting user, rather than as the app. This ensures the app is not providing access to data that the interacting user wouldn't normally be able to access.

There are limited cases where apps need the ACT_AS_USER scope. Its usage is strongly discouraged, as it's highly privileged and requires consideration on how and when the app would opt to impersonate users. Apps should not add the ACT_AS_USER scope for authorization purposes.

This approach works best when:

  1. You already request the ACT_AS_USER scope; and
  2. You don't rely on external systems or storage, such as storing Jira issues in your app database, as it's not possible to defer authorization in this case.

Rate this page: