Create a CRUD App With React, Kotlin, and Spring Boot

Buckle up—we’re going to use a lot of tools in this tutorial. We don’t have enough time to dive too deeply into any of them, so it will be helpful if you are familiar with React, Kotlin, Spring Boot, and REST APIs before we get started.

Our goal today is to use React for the frontend and Kotlin with Spring Boot for the backend to build a client and server application. First, you’ll build the app unsecured, and then you’ll use Okta to secure it. You’ll secure the frontend with OAuth 2.0 login, and you’ll secure the backend with a JSON Web Token and Spring Boot’s resource server OAuth implementation.

Install Kotlin and React Project Dependencies

You’ll need to install a few things before you get started.

Java 11: If you don’t have Java 11, you can install OpenJDK. The OpenJDK website has instructions for installation. OpenJDK can also be installed using Homebrew. SDKMAN is another excellent option for installing and managing Java versions.

Node 12: You’ll need Node to create and run your React application. You can install it with Homebrew or download it from nodejs.org.

Yarn: Yarn is a JavaScript package manager. You’ll use it for the React UI application. To install Yarn, head to their website for instructions.

HTTPie: This is a simple command-line utility for making HTTP requests. You’ll use this to test the REST application. Check out the installation instructions on their website.

Okta Developer Account: You’ll be using Okta as an OAuth/OIDC provider to add JWT authentication and authorization to the application. Go to developer.okta.com/signup and sign up for a free developer account, if you haven’t already.

Download a Kotlin Starter Project With Spring Initializr

To get the party started, you’re going to use the Spring Initializr. It’s a great resource that makes getting started with Spring Boot projects super simple. If you want to dig into the options, take a look at the Spring Initializr GitHub page.

Open this link to a pre-configured project for this tutorial.

Take a moment to peruse the pre-selected options. Note a feature that I really like: you can explore the project online using the Explore button at the bottom of the page.

To highlight some of the important settings:

Once you’re ready, click the green Generate button at the bottom of the Spring Initializr page. Download the project and move it to a suitable parent directory before unzipping it. You’re also going to create a React frontend, so you might want to make a kotlin-react-app parent directory for both projects.

Test the Kotlin Starter App

You can test the starter project by opening a shell and running the following command from the resource server project’s base directory.

Shell
 




xxxxxxxxxx
1


 
1
./gradlew bootRun


Give that a few seconds to run. You should see something like this:

Plain Text
 




xxxxxxxxxx
1


 
1
2020-01-10 13:54:05.692  INFO 41512 --- [    main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
2
2020-01-10 13:54:05.696  INFO 41512 --- [    main] c.o.k.ResourceServerApplicationKt : Started ResourceServerApplicationKt in 3.276 seconds (JVM running for 3.624)
3
<==========---> 80% EXECUTING [29s]
4
> :bootRun


Open a separate shell and use HTTPie to make a simple request.

HTTP
 




xxxxxxxxxx
1
14


1
$ http :8080
2
 
          
3
HTTP/1.1 200
4
Connection: keep-alive
5
Content-Type: application/hal+json
6
...
7
 
          
8
{
9
  "_links": {
10
    "profile": {
11
      "href": "http://localhost:8080/profile"
12
    }
13
  }
14
}


You may be wondering why, without even defining a controller, you have any response at all. This is because by including the spring-boot-starter-data-rest starter dependency, you have included Spring’s auto-magic “hypermedia-based RESTful front end” (as Spring describes it in their docs).

A “hypermedia-based RESTful front end” is a REST API that uses Hypertext Application Language (HAL) format to output descriptive JSON. It’s essentially a systematic way for a REST API to describe itself to client applications and for the client applications to easily navigate between the various endpoints.

The /profile endpoint that you see is an endpoint automatically added by the spring-boot-starter-data-rest starter dependency.

Otherwise, there isn’t much going on yet. That’s about to change!

Create a Kotlin REST API

The next step is to create a REST API using Kotlin and Spring Boot. For this example application, you’re going to model a list of coffee shops (‘cause I spend a lot of time in coffee shops, and not all coffee shops are created equal).

One of the nice things about Spring Boot is that if you define a data model and a repository, it can automatically expose that data as a REST API using the aforementioned HAL. In practice, in production, you may find yourself needing to create custom controllers for your REST APIs, since you may need to inject business logic between the data and the client apps. However, this auto-generated REST API is a great start and may be enough for simple applications.

So, step 1. Define the data model.

Create a new Kotlin class called CoffeeShopModel.kt in the src/main/kotlin/com/okta/kotlin directory.

Kotlin
 




xxxxxxxxxx
1
19


1
package com.okta.kotlin
2
 
          
3
import javax.persistence.Entity
4
import javax.persistence.GeneratedValue
5
import javax.persistence.GenerationType
6
import javax.persistence.Id
7
 
          
8
@Entity
9
data class CoffeeShopModel(
10
    @Id
11
    @GeneratedValue(strategy = GenerationType.AUTO)
12
    var id: Long = -1,
13
    var name: String = "",
14
    var address: String = "",
15
    var phone: String = "",
16
    var priceOfCoffee: Double = 0.0,
17
    var powerAccessible: Boolean = true,
18
    var internetReliability: Short = 3 // 1-5
19
) {}


Step 2. Define the repository. Create a Kotlin class called CoffeeShopRepository.kt in the same package.

Kotlin
 




xxxxxxxxxx
1


 
1
package com.okta.kotlin  
2
  
3
import org.springframework.data.repository.CrudRepository  
4
import org.springframework.data.rest.core.annotation.RepositoryRestResource  
5
  
6
@RepositoryRestResource(collectionResourceRel = "coffeeshops", path = "coffeeshops")  
7
interface CoffeeShopRepository : CrudRepository <CoffeeShopModel, Long >{  
8
}


That’s all you have to do to turn a data model into a REST API with full CRUD (Create, Read, Update, and Delete) capabilities. The magic happens primarily with two technologies: Spring Data JPA and the Spring Data @RepositoryRestResource annotation. Spring Data JPA is what turns the data model into a persisted entity, using our H2 database. In our case, the database is in-memory by default, so nothing will be persisted across database restarts. Obviously, in a production app, you’d need to connect the app to an actual SQL database instance.

The @RepositoryRestResource and the CrudRepository are what take the persisted model and turn it into a REST API. You can inspect the CrudRepository to see what methods it exposes to get an idea of its capabilities (see its API docs).

Before you test the REST API, you need to make a change to the ResourceServerApplication class. This is not a functional change. It adds a couple of sample coffees shop to your in-memory database so that you have something to work with.

Kotlin
 




xxxxxxxxxx
1
35


1
package com.okta.kotlin
2
 
          
3
import org.springframework.boot.ApplicationRunner
4
import org.springframework.boot.autoconfigure.SpringBootApplication
5
import org.springframework.boot.runApplication
6
import org.springframework.context.annotation.Bean
7
 
          
8
@SpringBootApplication
9
class ResourceServerApplication  {
10
 
          
11
  @Bean
12
  fun run(repository: CoffeeShopRepository) = ApplicationRunner {
13
    repository.save(CoffeeShopModel(
14
      name = "Oblique",
15
      address = "3039 SE Stark St, Portland, OR 97214",
16
      phone = "555-111-4444",
17
      priceOfCoffee = 1.50,
18
      powerAccessible = true,
19
      internetReliability = 5
20
    ))
21
    repository.save(CoffeeShopModel(
22
      name = "Epoch Coffee",
23
      address = "221 W N Loop Blvd, Austin, TX 78751",
24
      phone = "555-111-2424",
25
      priceOfCoffee = 2.50,
26
      powerAccessible = true,
27
      internetReliability = 3
28
    ))
29
  }
30
 
          
31
}
32
 
          
33
fun main(args: Array<String>) {
34
    runApplication<ResourceServerApplication>(*args)
35
}


Restart everything and test your new REST API.

Java
 




xxxxxxxxxx
1
51


 
1
$ http :8080/coffeeshops
2
 
          
3
HTTP/1.1 200
4
...
5
 
          
6
{
7
  "_embedded": {
8
    "coffeeshops": [
9
      {
10
        "_links": {
11
          "coffeeShopModel": {
12
            "href": "http://localhost:8080/coffeeshops/1"
13
          },
14
          "self": {
15
            "href": "http://localhost:8080/coffeeshops/1"
16
          }
17
        },
18
        "address": "3039 SE Stark St, Portland, OR 97214",
19
        "internetReliability": 5,
20
        "name": "Oblique",
21
        "phone": "555-111-4444",
22
        "powerAccessible": true,
23
        "priceOfCoffee": 1.5
24
      },
25
      {
26
        "_links": {
27
          "coffeeShopModel": {
28
            "href": "http://localhost:8080/coffeeshops/2"
29
          },
30
          "self": {
31
            "href": "http://localhost:8080/coffeeshops/2"
32
          }
33
        },
34
        "address": "221 W N Loop Blvd, Austin, TX 78751",
35
        "internetReliability": 3,
36
        "name": "Epoch",
37
        "phone": "555-111-2424",
38
        "powerAccessible": true,
39
        "priceOfCoffee": 2.5
40
      }
41
    ]
42
  },
43
  "_links": {
44
    "profile": {
45
      "href": "http://localhost:8080/profile/coffeeshops"
46
    },
47
    "self": {
48
      "href": "http://localhost:8080/coffeeshops"
49
    }
50
  }
51
}


Before you move on to the React UI, make two more changes to the REST API. You’ll notice above that the id field is not being returned as part of the JSON. This is inconvenient. Also, it’d be better if the REST API was set to have a base context path of /api, making the full path /api/coffeeshops.

To make these changes, you need to add a RepositoryRestConfigurer configuration class.

Add RestConfiguration.kt at src/main/kotlin/com/okta/kotlin/RestConfiguration.kt.

Kotlin
 




xxxxxxxxxx
1
13


1
package com.okta.kotlin
2
  
3
import org.springframework.context.annotation.Configuration  
4
import org.springframework.data.rest.core.config.RepositoryRestConfiguration  
5
import org.springframework.data.rest.webmvc.config.RepositoryRestConfigurer  
6
  
7
@Configuration  
8
open class RestConfiguration : RepositoryRestConfigurer {  
9
  override fun configureRepositoryRestConfiguration(config: RepositoryRestConfiguration?) {  
10
    config?.exposeIdsFor(CoffeeShopModel::class.java) 
11
    config?.setBasePath("/api"); 
12
  }  
13
}


If you re-start the resource server and execute an http :8080/api/coffeeshops request, you’ll find that the id field is now included in the JSON response.

Create the React Frontend

You will use Create React App to create the starter React application. The project is well documented on the Create React App homepage. It does a lot of routine work for us, setting up a React application.

From the root directory of the project (the parent directory of the Spring Boot resource server project), run the following command:

Shell
 




xxxxxxxxxx
1


 
1
yarn create react-app client


If that doesn’t work, as it didn’t for me, failing with a 404 error, you can use npx instead (requires that Node be installed):

Shell
 




xxxxxxxxxx
1


 
1
npx create-react-app client


Once the client application is created, navigate into the client directory and add a few dependencies:

Shell
 




xxxxxxxxxx
1


 
1
yarn add bootstrap react-router-dom reactstrap


This installs Bootstrap, React Router, and Reactstrap. It’s unlikely I have to tell you what Bootstrap is, but if you want to dig deeper, take a look at the project page. react-router-dom provides DOM bindings for React Router (their docs). Reactstrap is a library of React components that leverage Bootstrap to provide a set of mobile-friendly UI components (their docs).

Add Bootstrap’s CSS file as an import in client/src/index.js.

JavaScript
 




xxxxxxxxxx
1


 
1
import 'bootstrap/dist/css/bootstrap.min.css';


Update the client/src/App.js file:

JavaScript
 




xxxxxxxxxx
1
40


1
import React, { Component } from 'react';
2
import './App.css';
3
 
          
4
class App extends Component {
5
  state = {
6
    isLoading: true,
7
    coffeeShops: []
8
  };
9
 
          
10
  async componentDidMount() {
11
    const response = await fetch('/api/coffeeshops');
12
    const body = await response.json();
13
    this.setState({coffeeShops: body._embedded.coffeeshops, isLoading: false});
14
  }
15
 
          
16
  render() {
17
    const {coffeeShops, isLoading} = this.state;
18
 
          
19
    if (isLoading) {
20
      return <p>Loading...</p>;
21
    }
22
 
          
23
    return (
24
      <div className="App">
25
        <header className="App-header">
26
          <div className="App-intro">
27
            <h2>Coffee Shop List</h2>
28
            {coffeeShops.map(coffeeShop =>
29
              <div key={coffeeShop.id}>
30
                {coffeeShop.name} - {coffeeShop.address}
31
              </div>
32
            )}
33
          </div>
34
        </header>
35
      </div>
36
    );
37
  }
38
}
39
 
          
40
export default App;


Instead of using config values to set the resource server’s URL, you can use a proxy. Do this by adding the following to the package.json file.

JSON
 




xxxxxxxxxx
1


1
"proxy": "http://localhost:8080",


Take a look at the Create React App docs on this feature to learn more about proxying requests in development.

Run the React app using yarn start (ensure your Spring Boot resource server is running). You should see a very simple list of the coffee shop names and addresses.

Build a Full-Featured React UI

The UI as it is doesn’t do very much except demonstrate successful communication between the client and the server. The next step is to add some components, such as a navigation bar, a page for editing and adding coffee shops, and a nicer way to display the data. You’re also going to use routing to map URLs to app pages.

Change src/App.js to match the following. This adds three routes: a home route, a coffee shop list route, and a route for editing and creating new coffee shop entries. You’ll notice that the module uses composition (via the render() method) to pass the Api class and the NavBar module to the route components. In our current app state, without authentication and authorization, this is unnecessary. However, once you add in OAuth, you’ll see how this allows you to keep all of the auth logic in one place, avoiding a bunch of repeated code (or having to use something like MobX or Redux to manage a global state).

src/App.s

JavaScript
 




xxxxxxxxxx
1
40


1
import React, { Component } from 'react';
2
import './App.css';
3
import Home from './Home';
4
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
5
import CoffeeShopsList from './CoffeeShopsList';
6
import CoffeeShopEdit from './CoffeeShopEdit';
7
import Api from './Api';
8
import NavBar from './NavBar';
9
 
          
10
const api = new Api();
11
 
          
12
class App extends Component {
13
 
          
14
  render() {
15
    const navbar = <NavBar/>;
16
 
          
17
    return (
18
      <Router>
19
        <Switch>
20
          <Route
21
            path='/'
22
            exact={true}
23
            render={(props) => <Home {...props} api={api} navbar={navbar}/>}
24
          />
25
          <Route
26
            path='/coffee-shops'
27
            exact={true}
28
            render={(props) => <CoffeeShopsList {...props} api={api} navbar={navbar}/>}
29
          />
30
          <Route
31
            path='/coffee-shops/:id'
32
            render={(props) => <CoffeeShopEdit {...props} api={api} navbar={navbar}/>}
33
          />
34
        </Switch>
35
      </Router>
36
    )
37
  }
38
}
39
 
          
40
export default App;


Create a new JavaScript file, src/Home.js, with the following contents. This will be a simple home page. All it does is display the navigation bar and a button to open the list of coffee shops.

src/Home.js

JavaScript
 




xxxxxxxxxx
1
24


1
import React, { Component } from 'react';
2
import './App.css';
3
import { Link } from 'react-router-dom';
4
import { Button, Container } from 'reactstrap';
5
 
          
6
class Home extends Component {
7
 
          
8
  render() {
9
    return (
10
      <div className="app">
11
        {this.props.navbar}
12
        <Container fluid>
13
          <div>
14
            <Button color="secondary">
15
              <Link className="app-link" to="/coffee-shops">Manage Coffee Shops</Link>
16
            </Button>
17
          </div>
18
        </Container>
19
      </div>
20
    );
21
  }
22
}
23
 
          
24
export default Home;


Create the navigation bar JavaScript file, src/NavBar.js. The navbar displays a “Home” link, as well as a link to Okta’s Twitter channel and a link to the project repository.

src/NavBar.js

JavaScript
 




xxxxxxxxxx
1
38


1
import React, { Component } from 'react';
2
import { Collapse, Nav, Navbar, NavbarBrand, NavbarToggler, NavItem, NavLink } from 'reactstrap';
3
import { Link } from 'react-router-dom';
4
 
          
5
class NavBar extends Component {
6
 
          
7
  constructor(props) {
8
    super(props);
9
    this.state = {isOpen: false};
10
    this.toggle = this.toggle.bind(this);
11
  }
12
 
          
13
  toggle() {
14
    this.setState({
15
      isOpen: !this.state.isOpen
16
    });
17
  }
18
 
          
19
  render() {
20
    return <Navbar color="light" light expand="md">
21
      <NavbarBrand tag={Link} to="/">Home</NavbarBrand>
22
      <NavbarToggler onClick={this.toggle}/>
23
      <Collapse isOpen={this.state.isOpen} navbar>
24
        <Nav className="ml-auto" navbar>
25
          <NavItem>
26
            <NavLink
27
              href="https://twitter.com/oktadev">@oktadev</NavLink>
28
          </NavItem>
29
          <NavItem>
30
            <NavLink href="https://github.com/oktadeveloper/okta-kotlin-react-crud-example">GitHub</NavLink>
31
          </NavItem>
32
        </Nav>
33
      </Collapse>
34
    </Navbar>;
35
  }
36
}
37
 
          
38
export default NavBar;


The next file holds the components that display the coffee shops in a responsive, card-style grid layout. Create a new file, src/CoffeeShopsList.js, and add the following contents. There are two components in this file. CoffeeShop is a simple, functional component that encapsulates the display of each coffee shop item. CoffeeShopsList manages the overall display logic as well as the asynchronous calls to the server for loading and updating data.

src/CoffeeShopsList.js

JavaScript
 




xxxxxxxxxx
1
103


1
import React, { Component } from 'react';
2
import {
3
  Alert,
4
  Button
5
} from 'reactstrap';
6
import { Link } from 'react-router-dom';
7
 
          
8
const CoffeeShop = (props) => (
9
  <div className="coffeeshop-container p-2 m-2 d-flex flex-column">
10
    <h3>{props.name}</h3>
11
    <div className="coffeeshop-body">
12
      <div className="subtitle-container">
13
        <div>Cost: ${props.priceOfCoffee} / cup</div>
14
        <div>Internet Reliability: {props.internetReliability} / 5 </div>
15
        <div>{ props.powerAccessible ? "Power Accessible" : "Power NOT Accessible"} </div>
16
      </div>
17
      <div>{props.address}</div>
18
      <div>{props.phone}</div>
19
    </div>
20
    <div className="coffeeshop-footer">
21
      <Button color="secondary" tag={Link} to={"/coffee-shops/" + props.id}>Edit</Button>
22
      <Button color="danger" onClick={() => props.remove(props.id)}>Delete</Button>
23
    </div>
24
  </div>
25
);
26
 
          
27
class CoffeeShopsList extends Component {
28
 
          
29
  constructor(props) {
30
    super(props);
31
    this.state = {
32
      coffeeShops: [],
33
      isLoading: true,
34
      errorMessage: null
35
    };
36
    this.remove = this.remove.bind(this);
37
  }
38
 
          
39
  async componentDidMount() {
40
    this.setState({isLoading: true});
41
    const response = await this.props.api.getAll();
42
    if (!response.ok) {
43
      this.setState({
44
          errorMessage: `Failed to load coffee shops: ${response.status} ${response.statusText}`,
45
          isLoading: false
46
        }
47
      )
48
    }
49
    else {
50
      const body = await response.json();
51
      const coffeeShops = body._embedded.coffeeshops;
52
      this.setState({
53
        coffeeShops: coffeeShops,
54
        isLoading: false,
55
        errorMessage: null
56
      });
57
    }
58
  }
59
 
          
60
  async remove(id) {
61
    let response = await this.props.api.delete(id);
62
    if (!response.ok) {
63
      this.setState({errorMessage: `Failed to delete coffee shop: ${response.status} ${response.statusText}`})
64
    }
65
    else {
66
      let updatedCoffeeShops = [...this.state.coffeeShops].filter(i => i.id !== id);
67
      this.setState({coffeeShops: updatedCoffeeShops, errorMessage: null});
68
    }
69
  }
70
 
          
71
  render() {
72
    const {coffeeShops, isLoading, errorMessage} = this.state;
73
 
          
74
    if (isLoading) {
75
      return <p>Loading...</p>;
76
    }
77
 
          
78
    return (
79
      <div>
80
        {this.props.navbar}
81
        <div className="d-flex flex-row justify-content-between p-3">
82
          <h3 className="coffee-shops-title">Coffee Shops</h3>
83
          <Button color="success" tag={Link} to="/coffee-shops/new">Add New</Button>
84
        </div>
85
        { errorMessage ?
86
          <div className="d-flex flex-row justify-content-center">
87
            <Alert color="warning" style={{flex:1, maxWidth:'80%'}}>
88
              {errorMessage}
89
            </Alert>
90
          </div> : null
91
        }
92
        <div className="d-flex flex-row flex-container flex-wrap justify-content-center">
93
          { coffeeShops.map( coffeeShop =>
94
            <CoffeeShop {...coffeeShop} remove={this.remove.bind(this)} key={coffeeShop.id}/>
95
          )}
96
          { !coffeeShops || coffeeShops.length === 0 ? <p>No coffee shops!</p> : null}
97
        </div>
98
      </div>
99
    );
100
  }
101
}
102
 
          
103
export default CoffeeShopsList;


Add a React Component to Edit with Reactstrap Form Elements

The next new file, src/CoffeeShopEdit.js, is the component that is responsible for editing existing coffee shop entries and creating new ones. It demonstrates the use of Reactstrap form elements, as well as making some asynchronous calls to the server.

src/CoffeeShopEdit.js

JavaScript
 




xxxxxxxxxx
1
132


1
import React, { Component } from 'react';
2
import { Link, withRouter } from 'react-router-dom';
3
import { Alert, Button, Container, Form, FormGroup, Input, Label } from 'reactstrap';
4
 
          
5
class CoffeeShopEdit extends Component {
6
 
          
7
  emptyItem = {
8
    name: '',
9
    address: '',
10
    phone: '',
11
    priceOfCoffee: '',
12
    powerAccessible: '',
13
    internetReliability: ''
14
  };
15
 
          
16
  constructor(props) {
17
    super(props);
18
    this.state = {
19
      item: this.emptyItem,
20
      errorMessage: null,
21
      isCreate: false
22
    };
23
    this.handleChange = this.handleChange.bind(this);
24
    this.handleSubmit = this.handleSubmit.bind(this);
25
  }
26
 
          
27
  async componentDidMount() {
28
    this.state.isCreate = this.props.match.params.id === 'new'; // are we editing or creating?
29
    if (!this.state.isCreate) {
30
      const response = await this.props.api.getById(this.props.match.params.id);
31
      const coffeeShop = await response.json();
32
      this.setState({item: coffeeShop});
33
    }
34
  }
35
 
          
36
  handleChange(event) {
37
    const target = event.target;
38
    const value = target.value;
39
    const name = target.name;
40
    let item = {...this.state.item};
41
    item[name] = value;
42
    this.setState({item});
43
  }
44
 
          
45
  async handleSubmit(event) {
46
    event.preventDefault();
47
    const {item, isCreate} = this.state;
48
 
          
49
    let result = isCreate ? await this.props.api.create(item) : await this.props.api.update(item);
50
 
          
51
    if (!result.ok) {
52
      this.setState({errorMessage: `Failed to ${isCreate ? 'create' : 'update'} record: ${result.status} ${result.statusText}`})
53
    } else {
54
      this.setState({errorMessage: null});
55
      this.props.history.push('/coffee-shops');
56
    }
57
 
          
58
  }
59
 
          
60
  render() {
61
    const {item, errorMessage, isCreate} = this.state;
62
    const title = <h2>{isCreate ? 'Add Coffee Shop' : 'Edit Coffee Shop'}</h2>;
63
 
          
64
    return (
65
      <div>
66
        {this.props.navbar}
67
        <Container style={{textAlign: 'left'}}>
68
          {title}
69
          {errorMessage ?
70
            <Alert color="warning">
71
              {errorMessage}
72
            </Alert> : null
73
          }
74
          <Form onSubmit={this.handleSubmit}>
75
            <div className="row">
76
              <FormGroup className="col-md-8 mb-3">
77
                <Label for="name">Name</Label>
78
                <Input type="text" name="name" id="name" value={item.name || ''}
79
                       onChange={this.handleChange} autoComplete="name"/>
80
              </FormGroup>
81
              <FormGroup className="col-md-4 mb-3">
82
                <Label for="phone">Phone</Label>
83
                <Input type="text" name="phone" id="phone" value={item.phone || ''}
84
                       onChange={this.handleChange} autoComplete="phone"/>
85
              </FormGroup>
86
            </div>
87
            <FormGroup>
88
              <Label for="address">Address</Label>
89
              <Input type="text" name="address" id="address" value={item.address || ''}
90
                     onChange={this.handleChange} autoComplete="address-level1"/>
91
            </FormGroup>
92
            <div className="row">
93
              <FormGroup className="col-md-4 mb-3">
94
                <Label for="priceOfCoffee">Price of Coffee</Label>
95
                <Input type="text" name="priceOfCoffee" id="priceOfCoffee" value={item.priceOfCoffee || ''}
96
                       onChange={this.handleChange}/>
97
              </FormGroup>
98
              <FormGroup className="col-md-4 mb-3">
99
                <Label for="powerAccessible">Power Accessible?</Label>
100
                <Input type="select" name="powerAccessible" id="powerAccessible"
101
                       value={item.powerAccessible ? 'true' : 'false'}
102
                       onChange={this.handleChange}>
103
                  <option value="true">Yes</option>
104
                  <option value="false">No</option>
105
                </Input>
106
              </FormGroup>
107
              <FormGroup className="col-md-4 mb-3">
108
                <Label for="internetReliability">Internet Reliability</Label>
109
                <Input type="select" name="internetReliability" id="internetReliability"
110
                       value={item.internetReliability || '-'}
111
                       onChange={this.handleChange}>
112
                  <option>1</option>
113
                  <option>2</option>
114
                  <option>3</option>
115
                  <option>4</option>
116
                  <option>5</option>
117
                  <option value="-">-</option>
118
                </Input>
119
              </FormGroup>
120
            </div>
121
            <FormGroup>
122
              <Button color="primary" type="submit">Save</Button>{' '}
123
              <Button color="secondary" tag={Link} to="/coffee-shops">Cancel</Button>
124
            </FormGroup>
125
          </Form>
126
        </Container>
127
      </div>
128
    );
129
  }
130
}
131
 
          
132
export default withRouter(CoffeeShopEdit);


Add an Authentication-Aware Service for Server Requests

Create one more new file, src/Api.js. This module serves to centralize all of the server request logic. It is written so that it allows you to pass an authorization token to its constructor (which you’ll get to in the next section), and it will set the appropriate header. Without an auth token, it makes an unauthenticated request.

src/Api.js

JavaScript
 




xxxxxxxxxx
1
59


 
1
class Api {
2
 
          
3
  constructor(authToken) {
4
    this.authToken = authToken;
5
  }
6
 
          
7
  headers = {
8
    'Accept': 'application/json',
9
    'Content-Type': 'application/json'
10
  };
11
 
          
12
  BASE_URL = '/api/coffeeshops';
13
 
          
14
  createHeaders() {
15
    return this.authToken ? {
16
      ...this.headers,
17
      'Authorization': 'Bearer ' + this.authToken
18
    } : this.headers;
19
  }
20
 
          
21
  async getAll() {
22
    return await fetch(this.BASE_URL, {
23
      method: 'GET',
24
      headers: this.createHeaders()
25
    });
26
  }
27
 
          
28
  async getById(id) {
29
    return await fetch(`${this.BASE_URL}/${id}`, {
30
      method: 'GET',
31
      headers: this.createHeaders()
32
    });
33
  }
34
 
          
35
  async delete(id) {
36
    return await fetch(`${this.BASE_URL}/${id}`, {
37
      method: 'DELETE',
38
      headers: this.createHeaders()
39
    });
40
  }
41
 
          
42
  async update(item) {
43
    return await fetch(`${this.BASE_URL}/${item.id}`, {
44
      method:'PUT',
45
      headers: this.createHeaders(),
46
      body: JSON.stringify(item),
47
    });
48
  }
49
 
          
50
  async create(item) {
51
    return await fetch(this.BASE_URL, {
52
      method:'POST',
53
      headers: this.createHeaders(),
54
      body: JSON.stringify(item),
55
    });
56
  }
57
}
58
 
          
59
export default Api;


Make Your React App Look Good

Finally, add some styling to make things look good. Modify src/App.css to have the CSS below.

CSS
 




xxxxxxxxxx
1
84


 
1
html, #root {
2
  background-color: #282c34;
3
}
4
 
          
5
.row {
6
  margin-bottom: 10px;
7
}
8
 
          
9
a.app-link {
10
   color: #d3d8e3;
11
}
12
 
          
13
a.app-link:hover {
14
  color: #a2a9b8;
15
  text-decoration: none;
16
}
17
 
          
18
.container-fluid {
19
  color: white;
20
  text-align: center;
21
  padding-top: 40px;
22
}
23
 
          
24
.flex-container {
25
  color: white;
26
  text-align: center;
27
  padding-top: 40px;
28
}
29
 
          
30
.container {
31
  color: white;
32
  text-align: left;
33
  padding-top: 40px;
34
}
35
 
          
36
.coffee-shops-title {
37
  color: white;
38
}
39
 
          
40
.coffeeshop-container {
41
  width: 400px;
42
  min-width: 300px;
43
  background-color: #e9edf7;
44
  border-radius: 10px;
45
  color: #282c34;
46
  font-size: calc(10px + 1.0vmin);
47
}
48
 
          
49
.coffeeshop-container h3 {
50
  font-size: calc(10px + 2vmin);
51
}
52
 
          
53
.subtitle-container {
54
  font-size: calc(10px + 0.8vmin);
55
  color: #596273;
56
  margin-bottom: 10px;
57
}
58
 
          
59
.coffeeshop-body {
60
  flex: 1;
61
  margin-bottom: 10px;
62
}
63
 
          
64
.coffeeshop-footer {
65
  padding-top:8px;
66
  margin-top:8px;
67
  border-top: 1px solid #282c34;
68
}
69
 
          
70
.coffeeshop-footer .btn {
71
  margin: 5px 5px;
72
}
73
 
          
74
@media only screen and (max-width: 992px) {
75
  .coffeeshop-container {
76
    width: 300px;
77
  }
78
}
79
 
          
80
@media only screen and (max-width: 576px) {
81
  .coffeeshop-container {
82
    width: 80%;
83
  }
84
}


Run yarn start again if you need to (you may not need to; if you left it running, it should update automatically as you make changes). Make sure your resource server is running as well.

Take a look at the updated app at http://localhost:3000/.

You’ll see the new home page with the navbar.

Press the Manage Coffee Shops button.

You can now view, edit, create, and delete coffee shops.

Secure Your Kotlin + React App

You’ve got a nice, functional client and server application going. Don’t rest on your laurels yet! It’s unsecured, and if you leave that thing unattended, hackers will have it spewing spam and twist it into cheating little old ladies out of their retirement funds quicker than you can figure out what the heck a ‘hook’ is.

The next step is to secure it. You’re going to implement OAuth 2.0 login on the frontend. On the back end, you’re going to use a JSON Web Token (JWT) to secure the resource server. To make life much easier, you’re going to use Okta as your OAuth provider. Using Okta means you won’t have to write or maintain any login code or handle user passwords; nor will you have to waste any time mucking around writing code to verify tokens.

Okta has two projects that are going to make this painless: the Okta React SDK and the Okta Spring Boot Starter.

Before you make any code changes, though, you need to log into your Okta developer account and create an OpenID Connect (OIDC) application.

Create an OpenID Connect Application

OpenID Connect (or, OIDC) is an authentication protocol built on top of OAuth 2.0, which is an authorization protocol. Very briefly, OIDC allows you to know who a client is, and OAuth 2.0 allows you to determine what they’re allowed to do. Together they specify a complete authentication and authorization system. You’re going to use Okta’s implementation of these protocols as your OAuth 2.0 / OIDC provider.

To do this, you need to create an OIDC application in your Okta account. This configures and activates the OIDC application that your frontend application and backend resources server will interact with when verifying authentication and authorization.

If you haven’t already, head over to developer.okta.com to sign up for a free account. Once you have an account, open the developer dashboard and create an OpenID Connect (OIDC) application by clicking on the Applications top-menu item, and then on the Add Application button.

You’ll need your Client ID in a moment.

Configure the Resource Server for JWT Authentication

Securing the Spring Boot REST API is super easy. First, you need to add the Okta Spring Boot Starter to your Gradle project. This is a library we’ve created that streamlines adding OAuth 2.0/OIDC to your Spring Boot app with Okta.

Add the dependency in build.gradle.kts:

Java
 




xxxxxxxxxx
1


1
dependencies {
2
    ...
3
    implementation("com.okta.spring:okta-spring-boot-starter:1.3.0")
4
    ...
5
}


Next, create a SecurityConfiguration class to configure Spring Boot as an OAuth 2.0 resource server.

src/main/kotlin/com/okta/kotlin/SecurityConfiguration.kt

Kotlin
 




xxxxxxxxxx
1
20


1
package com.okta.kotlin
2
 
          
3
import com.okta.spring.boot.oauth.Okta
4
import org.springframework.context.annotation.Configuration
5
import org.springframework.security.config.annotation.web.builders.HttpSecurity
6
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter
7
 
          
8
@Configuration
9
class SecurityConfiguration : WebSecurityConfigurerAdapter() {
10
  override fun configure(http: HttpSecurity) {
11
    http
12
      .csrf().disable()
13
      .authorizeRequests().anyRequest().authenticated()
14
      .and()
15
      .oauth2ResourceServer().jwt();
16
 
          
17
    // Send a 401 message to the browser (w/o this, you'll see a blank page)
18
    Okta.configureResourceServer401ResponseBody(http);
19
  }
20
}


NOTE: This file also does a couple of other things: 1) it disables CSRF (Cross-site forgery protection), and 2) it configures the resource server to send a 401 instead of a blank page if a request is not authorized. Disabling CSRF in production is definitely NOT recommended, and you’re doing it here to simplify things so you can focus on OAuth 2.0 login and JWT authentication. To see how to implement CSRF protection, take a look at some of the other blog posts at the end of this tutorial.

Finally, add some configuration to src/main/resources/application.properties. Here you need the Okta Issuer URL (from the Okta developer dashboard, go to API > Authorization Servers and look in the table under the default server). You also need the Client ID from the OIDC application you just created.

Properties files
 




xxxxxxxxxx
1


 
1
okta.oauth2.issuer=https://{yourOktaUrl}/oauth2/default
2
okta.oauth2.clientId={yourClientID}


That’s it. The resource server now requires a valid JWT for all requests.

Stop and restart the resource server: ./gradlew bootRun.

You can test that it requires a JWT by opening a shell and running a simple request using HTTPie:

Shell
 




xxxxxxxxxx
1


 
1
http :8080/coffee-shops


You’ll get a 401 / Unauthorized:

Plain Text
 




xxxxxxxxxx
1


 
1
HTTP/1.1 401
2
...
3
 
          
4
401 Unauthorized


Add OAuth 2.0 Login to the React Application

The first step to adding Okta OAuth 2.0 login to your frontend React application is to add the Okta React SDK. Take a look at the project’s GitHub page for more info.

Use Yarn to add the project dependency to your React client.

Shell
 




xxxxxxxxxx
1


 
1
yarn add @okta/okta-react@1.3.1


Now update src/App.js to match the following. Fill in your Client ID and your Issuer URL in the Security component properties.

JavaScript
 




xxxxxxxxxx
1
102


 
1
import React, { Component } from 'react';
2
import './App.css';
3
import Home from './Home';
4
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
5
import { Security, SecureRoute, ImplicitCallback } from '@okta/okta-react';
6
import CoffeeShopsList from './CoffeeShopsList';
7
import CoffeeShopEdit from './CoffeeShopEdit';
8
import { withAuth } from '@okta/okta-react';
9
import Api from './Api';
10
import NavBar from "./NavBar";
11
 
          
12
const AuthWrapper = withAuth(class WrappedRoutes extends Component {
13
 
          
14
  constructor(props) {
15
    super(props);
16
    this.state = { authenticated: null, user: null, api: new Api() };
17
    this.checkAuthentication = this.checkAuthentication.bind(this);
18
  }
19
 
          
20
  async checkAuthentication() {
21
    const authenticated = await this.props.auth.isAuthenticated();
22
    if (authenticated !== this.state.authenticated) {
23
      if (authenticated) {
24
        const user = await this.props.auth.getUser();
25
        let accessToken = await this.props.auth.getAccessToken();
26
        this.setState({ authenticated, user, api: new Api(accessToken) });
27
      }
28
      else {
29
        this.setState({ authenticated, user:null, api: new Api() });
30
      }
31
    }
32
  }
33
 
          
34
  async componentDidMount() {
35
    this.checkAuthentication();
36
  }
37
 
          
38
  async componentDidUpdate() {
39
    this.checkAuthentication();
40
  }
41
 
          
42
  async login() {
43
    if (this.state.authenticated === null) return; // do nothing if auth isn't loaded yet
44
    this.props.auth.login('/');
45
  }
46
 
          
47
  async logout() {
48
    this.props.auth.logout('/');
49
  }
50
 
          
51
  render() {
52
    let {authenticated, user, api} = this.state;
53
 
          
54
    if (authenticated === null) {
55
      return null;
56
    }
57
 
          
58
    const navbar = <NavBar
59
      isAuthenticated={authenticated}
60
      login={this.login.bind(this)}
61
      logout={this.logout.bind(this)}
62
    />;
63
 
          
64
    return (
65
      <Switch>
66
        <Route
67
          path='/'
68
          exact={true}
69
          render={(props) => <Home {...props} authenticated={authenticated} user={user} api={api} navbar={navbar} />}
70
        />
71
        <SecureRoute
72
          path='/coffee-shops'
73
          exact={true}
74
          render={(props) => <CoffeeShopsList {...props} authenticated={authenticated} user={user} api={api} navbar={navbar}/>}
75
        />
76
        <SecureRoute
77
          path='/coffee-shops/:id'
78
          render={(props) => <CoffeeShopEdit {...props} authenticated={authenticated} user={user} api={api} navbar={navbar}/>}
79
        />
80
      </Switch>
81
    )
82
  }
83
});
84
 
          
85
class App extends Component {
86
 
          
87
  render() {
88
    return (
89
      <Router>
90
        <Security issuer='https://{yourOktaUrl}/oauth2/default'
91
              clientId='{yourClientId}'
92
              redirectUri={window.location.origin + '/implicit/callback'}
93
              pkce={true}>
94
          <Route path='/implicit/callback' component={ImplicitCallback} />
95
          <AuthWrapper />
96
        </Security>
97
      </Router>
98
    )
99
  }
100
}
101
 
          
102
export default App;


The Security component is where the Okta OAuth configuration happens. You need to fill in your clientId and your issuer in the properties there.

You’ll also notice that I chose to centralize all the security logic in a wrapper component called AuthWrapper. This allows you to handle all authentication-related logic in one place and pass down the authentication state as properties. It also allows you to pass down the Api module with the access token to the child components. This keeps a lot of the security logic out of the route/view components and avoids some repeated code, which I like.

However, the withAuth() function supplied by the Okta React SDK can be applied to any React component (that is a child of the Security component). Thus, it’s also possible not to use a wrapper class like this and inject the auth prop into the various routes directly. Ultimately, in an app in production, the auth state would likely be moved to a global state using something like MobX or Redux.

Two more files need to be updated, the home page and the navigation bar.

Update src/Home.js:

JavaScript
 




xxxxxxxxxx
1
37


1
import React, { Component } from 'react';
2
import './App.css';
3
import { Link } from 'react-router-dom';
4
import { Button, Container } from 'reactstrap';
5
 
          
6
class Home extends Component {
7
 
          
8
  render() {
9
    if (this.props.authenticated === null) {
10
      return <p>Loading...</p>;
11
    }
12
 
          
13
    return (
14
      <div className="app">
15
        {this.props.navbar}
16
        <Container fluid>
17
          { this.props.authenticated ?
18
            <div>
19
              <p>Welcome, {this.props.user.name}</p>
20
              <Button color="secondary">
21
                <Link className="app-link" to="/coffee-shops">Manage Coffee Shops</Link>
22
              </Button>
23
            </div> :
24
            <div>
25
              <p>Please log in to manage coffee shops.</p>
26
              <Button color="secondary" disabled={true}>
27
                Manage Coffee Shops
28
              </Button>
29
            </div>
30
          }
31
        </Container>
32
      </div>
33
    );
34
  }
35
}
36
 
          
37
export default Home;


Update src/NavBar.js to add login and logout buttons:

JavaScript
 




xxxxxxxxxx
1
48


1
import React, { Component } from 'react';
2
import { Button, Collapse, Nav, Navbar, NavbarBrand, NavbarToggler, NavItem, NavLink } from 'reactstrap';
3
import { Link } from 'react-router-dom';
4
 
          
5
class NavBar extends Component {
6
 
          
7
  constructor(props) {
8
    super(props);
9
    this.state = {isOpen: false};
10
    this.toggle = this.toggle.bind(this);
11
  }
12
 
          
13
  toggle() {
14
    this.setState({
15
      isOpen: !this.state.isOpen
16
    });
17
  }
18
 
          
19
  render() {
20
    const {isAuthenticated, login, logout} = this.props;
21
 
          
22
    return <Navbar color="light" light expand="md">
23
      <NavbarBrand tag={Link} to="/">Home</NavbarBrand>
24
      <NavbarToggler onClick={this.toggle}/>
25
      <Collapse isOpen={this.state.isOpen} navbar>
26
        <Nav className="ml-auto" navbar>
27
          <NavItem>
28
            <NavLink
29
              href="https://twitter.com/oktadev">@oktadev</NavLink>
30
          </NavItem>
31
          <NavItem>
32
            <NavLink href="https://github.com/oktadeveloper/okta-kotlin-react-crud-example">GitHub</NavLink>
33
          </NavItem>
34
          { !isAuthenticated ?
35
            <NavItem>
36
              <Button color="secondary" outline onClick={login}>Login</Button>
37
            </NavItem> :
38
            <NavItem>
39
              <Button color="secondary" outline onClick={logout}>Logout</Button>
40
            </NavItem>
41
          }
42
        </Nav>
43
      </Collapse>
44
    </Navbar>;
45
  }
46
}
47
 
          
48
export default NavBar;


Time to test the secured app.

Test Your Secured Kotlin + React Application

Run the resource server (if you need to):

Shell
 




xxxxxxxxxx
1


 
1
./gradlew bootRun


Start the React client:

Shell
 




x


 
1
yarn start


Open a browser: http://localhost:3000.

Click the Login button in the header. You’ll be redirected to the Okta login page. Log in using your Okta credentials.

When you return to your client app’s homepage, click the Manage Coffee Shops button.

From this page, you can edit, delete, and add new coffee shops.

All done! In this tutorial, you created a web-based application using a React frontend with data being served by a Kotlin Spring Boot resource server. You also saw how to use a free developer account from Okta to add OAuth 2.0 login to your application and to secure your resource server.

You can find the source code for this example on GitHub in the okta-kotlin-react-crud-example repository.

Learn More About Kotlin and React

If you want to keep learning, take a look at these related posts:

If you have any questions about this post, please add a comment below. For more awesome content, follow @oktadev on Twitter, like us on Facebook, or subscribe to our YouTube channel.

 

 

 

 

Top