There are currently 2 ways to consume Jira Cloud APIs:
-
Other apps
In this tutorial or ultimate guide, we will be implementing a Spring Boot OAuth2 client application for other apps category.
Tip
|
There is a Maven archetype to create boiler plate code for creating a connect app: Atlassian Connect for Spring Boot |
Before we start implementing our client application we need to prepare our Atlassian account and development environment. In this section we will do preparation step by step.
-
Go to https://www.atlassian.com/try/cloud/signup?bundle=jira-software and create an (free trial) account.
-
You can skip every skippable thing.
-
Select Kanban, name your project as
Demo
and go.
Tip
|
At the time of this writing, Atlassian is accepting guerrillamail addresses. So you can use an email address like [email protected] and then validate the email address via https://www.guerrillamail.com .
|
-
Download and install it from https://ngrok.com
-
Then start it as follows:
./ngrok http 8080
We will be working with the https version of the URL.
Warning
|
Don’t close or restart it during your work. ngrok domain is changed in every restart. |
Tip
|
Don’t try to CTRL+C the URL in the console output as it will kill the process. Just CTRL+click on it and it will be open in default browser. |
Go to https://developer.atlassian.com/apps/ and create a new app.
Ensure you have OAuth 2.0 (3LO)
(highlighted in above image).
If you don’t see OAuth 2.0 (3LO)
then enable it with those steps.
-
Use the ngrok URL
-
It should be HTTPS
-
You don’t have to use
/callback
. You can change it if you want.
-
Click
Add APIs
link after saving your callback URL. -
Add Jira platform REST API.
-
Click
configure
on Jira platform REST API. -
Ensure
read:jira-user
scope is added.
Now we have prepared our accounts and environment and we are ready to create a Spring Boot application to act as a client to our Jira Cloud account.
Before creating the application we should check if our environment is actually working.
Otherwise we could easily be lost hours trying to solve a bug which is never in the application but in the account configuration.
1) Replace YOUR_CLIENT_ID
and YOUR_NGROK_DOMAIN
in following URL and call it from your browser.
Tip
|
Remember that YOUR_CLIENT_ID can be found in app details from Atlassian Applications page.
|
https://auth.atlassian.com/authorize?audience=api.atlassian.com&client_id=YOUR_CLIENT_ID&scope=read:jira-user&redirect_uri=https://YOUR_NGROK_DOMAIN/callback&state=KODGEMISI&response_type=code&prompt=consent
2) It should ask you for authorization as follows:
If you see below error then (as the error message says) you need to create a Jira Cloud account. See Create a Jira Cloud account
3) Accept it and you should see an ngrok error page like below:
Your browser’s address bar should be something like:
Note that the state is the same as we provided (KODGEMISI
) and the URL is the redirection URL we entered in the Atlassian app’s OAuth configuration page.
Tip
|
Remember that 4769d10f.ngrok.io points to localhost:8080 hence, https://4769d10f.ngrok.io/callback means localhost:8080/callback .
|
Now it’s clear that we need to create an application which handles /callback
URL path.
Go to start.spring.io or your IDE’s create Spring Boot project wizard and select followings:
-
Using latest stable Spring Boot version, Java 11 and Maven
-
Spring Web
-
Thymeleaf
-
OAuth2 Client
-
Lombok
-
Spring Boot DevTools
We will also need to add following dependency manually:
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
<version>${spring-security-oauth2.version}</version>
</dependency>
Note that only Spring Web, OAuth2 Client and manually added spring-security-oauth2 are essential dependencies. Other dependencies are just for developer’s convenience.
Important
|
You need to choose a compatible version of spring-security-oauth2 with your Spring Security dependency. Check Spring Security dependency version in spring-security-oauth2’s maven page and ensure it’s the same with your Spring Security dependency version which is resolved from spring-boot-starter-oauth2-client . If you can’t find the same version then use the latest version of spring-security-oauth2 .
|
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.8.RELEASE</version>
<relativePath />
</parent>
<groupId>com.kodgemisi.blog</groupId>
<artifactId>jira-cloud-oAuth2-client</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>Jira Cloud OAuth2 Client with Spring Boot</name>
<description>Jira Cloud OAuth2 Client with Spring Boot</description>
<properties>
<java.version>11</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<spring-security-oauth2.version>2.3.6.RELEASE</spring-security-oauth2.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
<version>${spring-security-oauth2.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
</dependency>
<dependency>
<groupId>com.github.kodgemisi</groupId>
<artifactId>better-error-pages-spring-boot-starter</artifactId>
<version>${better-error-pages-spring-boot-starter.version}</version>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
We will define following values in application.yml
:
security:
oauth2:
client:
clientId: ${CLIENT_ID}
clientSecret: ${CLIENT_SECRET}
accessTokenUri: https://auth.atlassian.com/oauth/token
userAuthorizationUri: https://auth.atlassian.com/authorize?audience=api.atlassian.com
scope: 'read:jira-user'
grantType: 'authorization_code'
preEstablishedRedirectUri: ${CALLBACK_URL}
useCurrentUri: false
app:
cloudId: ${CLOUD_ID}
jiraUrl: https://api.atlassian.com/ex/jira/${app.cloudId}/rest/api/3
-
CLIENT_ID: find it from app details page on https://developer.atlassian.com/apps
-
CLIENT_SECRET: secret value on the same page
-
CALLBACK_URL is like https://4769d10f.ngrok.io/callback
-
CLOUD_ID is the id of your Jira Cloud instance. See below section to learn how to obtain it.
Caution
|
You should define those values as environment variables or pass them as JVM arguments so that client id and secret won’t end up in your Git repository. |
CLOUD_ID
-
Click on your site
Look at the address bar: https://admin.atlassian.com/s/1324a887-45db-1bf4-1e99-ef0ff456d421/users
.
1324a887-45db-1bf4-1e99-ef0ff456d421
part is your cloud id.
In configuration we define our beans:
@Bean
@ConfigurationProperties("security.oauth2.client")
OAuth2ProtectedResourceDetails oauth2RemoteResource() {
return new AuthorizationCodeResourceDetails();
}
@Bean
OAuth2RestTemplate oauth2RestTemplate(OAuth2ClientContext oauth2ClientContext,
@Value("${app.jiraUrl}") String baseUrl) {
final OAuth2RestTemplate restTemplate = new OAuth2RestTemplate(oauth2RemoteResource(), oauth2ClientContext);
restTemplate.setUriTemplateHandler(new DefaultUriBuilderFactory(baseUrl));
return restTemplate;
}
Note that OAuth2ProtectedResourceDetails
instance is formed using security.oauth2.client
values from application.yml
.
-
We have configured an
OAuth2RestTemplate
with ourOAuth2ProtectedResourceDetails
. -
We will use this rest template when calling Jira Cloud’s REST APIs.
-
This rest template automatically checks if there is an access token present (cached).
-
If access token not present then rest template checks if there are
code
andstate
request parameters in request.-
If
code
andstate
is found then it uses them to get an access token fromaccessTokenUri
-
If
code
andstate
is NOT found then it redirects us to Atlassian’s authorization page (userAuthorizationUri
).-
Recall from Validating our setup, Atlassian’s authorization page redirects user to configured callback url with
code
andstate
query parameters. -
Now we have
code
andstate
values at hand and manually make theOAuth2RestTemplate
callgetAccessToken()
now. Once access token is retrieved it will be cached internally byOAuth2RestTemplate
. -
At this point we have an access token however not yet make the actual request to Jira Cloud API. Hence, we need to redirect the user to the original URL. After this redirection the flow will hit if access token is present state.
-
-
-
If access token is present then rest template uses the cached access code and puts it in request header before calling Jira Cloud’s REST APIs.
-
-
We happily get our response 🥳
Tip
|
Further descriptions on implementation details can be found in the sample code. |
The /callback
handler has two main responsibility:
-
Getting the access token from
accessTokenUri
usingcode
andstate
values.-
This will be done via
restTemplate.getAccessToken()
method call.getAccessToken
will findcode
andstate
values internally so we don’t have to pass them.
-
-
Redirecting to the original URL after caching the access token.
-
We keep the original URL in
savedRequest
instance. We will see how we create and keep thissavedRequest
later.
-
@Controller
@RequiredArgsConstructor
class CallbackControllerController {
private final OAuth2RestTemplate restTemplate;
@GetMapping("/callback")
String callback(HttpSession httpSession) {
final SavedRequest savedRequest = (SavedRequest) httpSession.getAttribute("SPRING_SECURITY_SAVED_REQUEST");
httpSession.removeAttribute("SPRING_SECURITY_SAVED_REQUEST");
restTemplate.getAccessToken();
return "redirect:" + savedRequest.getRedirectUrl();
}
}
As we mentioned in the 2nd responsibility of callback handler, we need to keep the original URL only when we are being redirected to Atlassian’s authorization page.
The best place to implement this is RedirectStrategy
of OAuth2ClientContextFilter
.
RedirectStrategy
implementation which will save the original URL in session just before redirectingclass OAuth2RedirectStrategy extends DefaultRedirectStrategy {
@Override
public void sendRedirect(HttpServletRequest request, HttpServletResponse response, String url) throws IOException {
final String currentUri = (String) request.getAttribute("currentUri");
final HttpSession session = request.getSession(false);
if (session != null) {
session.setAttribute("SPRING_SECURITY_SAVED_REQUEST", new SimpleSavedRequest(currentUri));
}
super.sendRedirect(request, response, url);
}
}
OAuth2ClientContextFilter
to use our RedirectStrategy
implementation@Bean
RedirectStrategy oAuth2RedirectStrategy() {
return new OAuth2RedirectStrategy();
}
@Bean
OAuth2ClientContextFilter oAuth2ClientContextFilter(OAuth2ClientContextFilter oAuth2ClientContextFilter) {
oAuth2ClientContextFilter.setRedirectStrategy(oAuth2RedirectStrategy());
return oAuth2ClientContextFilter;
}
After that, whenever a callback is made to our /callback
url, we will get the access token and then redirect the user to original request url again. This time the user gets the response because we have an access token now.
Subsequent requests to Jira Cloud’s REST API won’t be redirected because we will be using the cached access token.
So we need some code to actually consumes the Jira Cloud’s REST API.
@Controller
@RequiredArgsConstructor
@RequestMapping("/myself")
class MyselfController {
private final OAuth2RestTemplate restTemplate;
@GetMapping
ResponseEntity<Map<?, ?>> myself(WebRequest request) {
final Map<?, ?> result = restTemplate.getForObject("/myself", Map.class);
return ResponseEntity.ok(result);
}
}
Note that we don’t have to use the restTemplate
in a controller. It’s just convenient in a demo application this way. But in a real application
you can use restTemplate
in service layer, in a batch job, scheduled job etc.
Lastly, to use our client we need to provide a way for the user to be authenticated because Spring Security’s OAuth2 client implementation requires the user be fully authenticated before making access token requests.
@Configuration
@EnableWebSecurity
class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests().anyRequest().fullyAuthenticated()
.and()
.formLogin().and().logout();
}
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication().withUser("kodgemisi").password("{noop}kodgemisi").roles("USER");
}
}
-
Go to link
https://NGROK_HOST_NAME
. -
Login
-
username: kodgemisi
-
password: kodgemisi
-
-
Click the link Show "myself" resource from Atlassian Jira Cloud
-
You will be redirected to Atlassian authorization page, accept the authorization
-
You will be redirected to your callback, hence your application
-
-
Your application makes a request to Jira API behind the scenes and returns you the JSON response.
Something went wrong while executing your request.
Error: Redirect failed because the supplied callback URL is not whitelisted for this application.
ngrok URL may be changed. It changes every time you start ngrok. You should be pro ngrok user to have a static URL.
Reset your callback URL from App Details
page on https://developer.atlassian.com/apps/<yourAppId>/details