atlassian-connect-spring-boot: call bitbucket Rest Api

Hello! In this article we will talk about developing a Bitbucket Cloud app with atlassian-connect-spring-boot.

We will change the app developed here.

You can find the code for this article here.

Add admin page

We will delete the current module in backend/src/main/resources/atlassian-connect.json and add the following lines:

"modules": {
    "adminPages": [
      {
        "url": "/bbpage",
        "location" : "org.bitbucket.account.admin",
        "name": {
          "value": "Bitbucket app"
        },
        "key": "bb-page"
      }
    ]
  },
  "scopes": ["account", "repository", "pullrequest"],
  "contexts": ["account"]

It means that we will have our own page in the admin settings and this page will call the bbpage endpoint.

Now let’s add the bbpage endpoint.

Add the following lines into backend/src/main/java/ru/matveev/alexey/atlassian/cloud/tutorial/controller/HelloWorldController.java:

@GetMapping("/bbpage")
public String bbPage() {
  return "bbpage";
}

Now let’s create the bbpage template (backend/src/main/resources/templates/bbpage.html):

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <script th:src="@{${atlassianConnectAllJsUrl}}" type="text/javascript"></script>
    <script type="text/javascript" src="/bundled.main.js" charset="utf-8"></script>
</head>
<body>
  <h1>BB App</h1>
  <button type="button" class="ak-button ak-button__appearance-default">Button created using the reduced-ui-pack</button>
</body>

Install and run the app

Run ngrok and create a tunnel. Put the proxy url into backend/src/main/resources/application.yml.

Package the app and run.

Now go to your workspace in Bitbucket.org and choose settings -> Develop Apps:

Screenshot 2021-03-23 at 08.55.44.png

Push the register app button and add the proxy url from ngrok:

Screenshot 2021-03-23 at 08.57.31.png

Push the Register app button:

Screenshot 2021-03-23 at 09.00.47.png

Actually nothing happened yet on your app side. The client is not registered yet in your app. To register the client click on the installation url.

Screenshot 2021-03-23 at 09.02.12.png

Choose the workspace and push the Grant access button:

Screenshot 2021-03-23 at 09.03.37.png

Now your app is correctly installed and your app has registered the client.

Click on the Bitbucket app link:

Screenshot 2021-03-23 at 09.05.20.png

Everyting works.

Call bitbucket rest api

Now let’s call a Bitbucket rest api from our app. Let’s change the bbPage method in backend/src/main/java/ru/matveev/alexey/atlassian/cloud/tutorial/controller/HelloWorldController.java:

@GetMapping("/bbpage")
public String bbPage() {
    String response = atlassianHostRestClients.authenticatedAsAddon().getForObject("/2.0/workspaces", String.class);
    return "bbpage";
}

We use atlassianHostRestClients as in the documentation for atlassian-connect-spring-boot. Run our app and we will have the following error:

java.net.ProtocolException: Server redirected too many  times (20)

Nice! What went wrong?

If we have a look at the docs for atlassian-connect-spring-boot, we will notice the following lines:

Provides a Spring Boot starter for building Atlassian Connect add-ons for JIRA (Software, Service Desk and Core) and Confluence.

Which means that we can not use this framework for Bitbucket Cloud. Obviously, we need to write our own framework!

But it is not true. All we need to do is to call Bitbucket Rest Api with the valid way for Bitbucket.

You can read more about it here. To tell you the truth I spent a way too much time to understand what this doc says. Also information is very misleading in the Bitbucket doc here.

Anyway to translate it to a simple language. First, you need to get a jwt token, then get a token using this jwt token and then use the token in the Authorization header.

I have not found any information on how to generate the jwt token. In the end I found a nodejs app and found in the code how to generate this jwt token.

Let’s do it in our app.

Add the following dependency in the pom.xml file:

<dependency>
            <groupId>com.atlassian.jwt</groupId>
            <artifactId>jwt-api</artifactId>
            <version>3.2.1-SNAPSHOT</version>
            <scope>compile</scope>
</dependency>
<dependency>
            <groupId>com.atlassian.jwt</groupId>
            <artifactId>jwt-core</artifactId>
            <version>3.2.1-SNAPSHOT</version>
            <scope>compile</scope>
</dependency>

Now create backend/src/main/java/ru/matveev/alexey/atlassian/cloud/tutorial/bitbucket/AccessTokenResponse.java:

@Data
public class AccessTokenResponse {
    String access_token;
    String scopes;
    String expires_in;
    String token_type;
}

Then backend/src/main/java/ru/matveev/alexey/atlassian/cloud/tutorial/bitbucket/JWTGenerator.java

@RequiredArgsConstructor
public class JWTGenerator {
    @NonNull
    private final String sharedSecret;
    @NonNull
    private final String clientKey;
    @NonNull
    private final String appKey;
    private Long expireIn = 18000L;
    public String getJWT() {
        long issuedAt = System.currentTimeMillis() / 1000L;
        long expiresAt = issuedAt + expireIn;
        String jwt = new JsonSmartJwtJsonBuilder()
                .issuedAt(issuedAt)
                .expirationTime(expiresAt)
                .issuer(appKey)
                .subject(clientKey)
                .build();

        JwtWriterFactory jwtWriterFactory = new NimbusJwtWriterFactory();
        return  jwtWriterFactory.macSigningWriter(SigningAlgorithm.HS256,
                sharedSecret).jsonToJwt(jwt);

    }

    public JWTGenerator(String sharedSecret,
                        String clientKey,
                        String appKey,
                        Long expireIn) {
        this(sharedSecret, clientKey, appKey);
        this.expireIn = expireIn;
    }
}

That is the correct way to generate a jwt token.

And at last let’s get a token based on the jwt token backend/src/main/java/ru/matveev/alexey/atlassian/cloud/tutorial/bitbucket/AccessToken.java:

@AllArgsConstructor
@RequiredArgsConstructor
public class AccessToken {
    @NonNull
    private final String sharedSecret;
    @NonNull
    private final String clientKey;
    @NonNull
    private final String appKey;
    private Long expireIn;

    public AccessTokenResponse getAccessToken() {
        RestTemplate restTemplate = new RestTemplate();
        JWTGenerator jwtGenerator = expireIn == null ? new JWTGenerator(this.sharedSecret, this.clientKey, this.appKey) : new JWTGenerator(this.sharedSecret, this.clientKey, this.appKey, this.expireIn);
        String jwt =jwtGenerator.getJWT();
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
        headers.set("Authorization", "JWT " + jwt);

        MultiValueMap<String, String> map= new LinkedMultiValueMap<String, String>();
        map.add("grant_type", "urn:bitbucket:oauth2:jwt");

        HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<MultiValueMap<String, String>>(map, headers);

        ResponseEntity<AccessTokenResponse> response
                = restTemplate.postForEntity("https://bitbucket.org/site/oauth2/access_token", request, AccessTokenResponse.class);

        return response.getBody();
    }
}

Done.

Now let’s get this token in our bbPage method and call Bitbucket Rest Api:

@GetMapping("/bbpage")
public String bbPage(@AuthenticationPrincipal AtlassianHostUser hostUser) {
                AccessTokenResponse accessToken = new AccessToken(hostUser.getHost().getSharedSecret(), hostUser.getHost().getClientKey(), "hello-world-app").getAccessToken();

                RestTemplate restTemplate = new RestTemplate();
                HttpHeaders headers = new HttpHeaders();
                headers.add("Authorization", "Bearer " + accessToken.getAccess_token());
                HttpEntity<String> request = new HttpEntity<String>(headers);
                ResponseEntity<String> response = restTemplate.exchange("https://api.bitbucket.org/2.0/workspaces", HttpMethod.GET, request, String.class);
                String account = response.getBody();
                return "bbpage";
}

We get hostUser which contains information about the client who called our bbpage. Then we get the token and use it in the Authorization header.

That is all. The Rest Api call worked this time.

2 comments

Comment

Log in or Sign up to comment
M Amine
Community Leader
Community Leader
Community Leaders are connectors, ambassadors, and mentors. On the online community, they serve as thought leaders, product experts, and moderators.
June 11, 2021

Amazing article

Bipil Raut April 29, 2022

Well Explained!

TAGS
AUG Leaders

Atlassian Community Events