Building a Secure REST API with OpenID Connect
In this article, we’ll take a look at building a secured REST API by integrating with Okta as the identity provider via OpenID Connect (OIDC).
Join the DZone community and get the full member experience.
Join For FreeIntroduction
In this article, we’ll take a look at building a secured REST API by integrating with Okta as the identity provider via OpenID Connect (OIDC). This article is based on the DZone article Building a Java REST API with Quarkus, which explains how to create a Java REST API with Quarkus and Okta. We will be implementing a similar scenario here by using Ballerinalang, and show how it’s simpler and more straightforward to implement compared to our Java counterpart.
Prerequisites
- Ballerina Installation(>= v1.2.6)
Verify the installation by typing “ballerina -v” in the command line. This should output the currently installed Ballerina version.
- Okta Developer Account: An Okta developer account can be created by navigating to https://developer.okta.com/.
- CURL or another suitable HTTP client for your respective environment.
Hello World Ballerina Service
Let’s start off by creating a simple hello world service application as our base scenario. Add the following code to a file named hello.bal.
Listing 1: Hello World Service
The above service can be run by using the following command:
$ ballerina run hello.bal
[ballerina/http] started HTTP/WS listener 0.0.0.0:8080
The final source code of our hello world service can be found here.
Let’s invoke the service by sending a request.
xxxxxxxxxx
$ curl http://localhost:8080/secured/hello
Hello Anonymous, authScheme: N/A
Here, the service is invoked through HTTP without any form of user authentication.
A Secured Greeting
Let’s update our hello world service in order to authenticate users who invoke it using a JWT.
Listing 2: Secured Hello World Service with JWT
We have done several new things to our earlier service implementation. First, our HTTP listener that was bound to the service construct HelloService has been changed from new http:Listener(8080) to httpsListener. Earlier we created an anonymous HTTP listener object in place, and now, we have created a separate HTTPS listener object and referred to it from the service. This approach is required when we want to provide additional configurations to the listener compared to the inline creation. A Ballerina service is not limited to having only one listener, but it can have multiple compatible listeners attached to a single service at a time. The listener object simply needs to be given as a comma-separated list.
HTTP listener objects provide the functionality to associate a set of authentication and authorization providers. In our case, we have created a single JWT inbound auth provider and registered as an auth handler in the HTTPS listener configuration. In Ballerina, it is mandated that a secure transport should be used in an authentication scenario such as in our case. This is where a bearer token is sent through the headers and would be susceptible to a man-in-the-middle attack if a secured protocol is not used.
Figure 1 shows a summary of the relationship between the Ballerina services, listeners, and authentication providers.
Figure 1: Ballerina Service-Listener-Authentication Provider Relationship
Let’s try invoking the updated service above.
xxxxxxxxxx
$ curl -k https://localhost:8443/secured/hello
Authentication failure.
As we see above, the invocation to the updated service resource fails with an authentication failure. The service returns an HTTP 401 Unauthorized message. This can be checked by passing in the -v switch to the CURL command above.
The truststore.p12 and the keystore.p12 files are keystore files used in the HTTPS communication. For the purpose of this demo, I’ve copied them from Ballerina default keystore files, which can be found at ${BALLERINA_HOME}/bre/security/. ${BALLERINA_HOME} can be found by executing ballerina home in the command line. For JWT signature verification, the public keys are provided using the JSON Web Key Set (JWKS) format. This is shown at line 9 in Listing 2.
In order to access the service, we have to send a JWT with the service request to authenticate the user. Let’s see how this can be generated using OIDC with Okta.
OIDC Application Integration With Okta
In this section, we will use our Okta developer account to create a new OIDC application, and then generate a JWT in order to invoke our secure service. These are the steps you need to follow:
- Navigate to your domain by clicking on the top-right menu and selecting Your Org
- Click on Applications and then Add Application
- Select the application type Web
- Provide a name, e.g., Ballerina Demo
- Update the Login redirect URIs with “https://oidcdebugger.com/debug”
- Under Grant type allowed set Implicit (Hybrid)
- Leave the rest with the default values
- Click Done
- Note down the Client ID
We will also add some custom scopes to our authorization server in order to use them in our services.
- Click the top menu API -> Authorization Servers
- Select default and click Scopes
- Add the new scopes greet, products_access, products_add, and products_delete
The updated scopes will look similar to Figure 2.
Figure 2: Okta Authorization Server Scopes
Now, we can execute an OIDC request in order to create an access token to authenticate users for our service. Let’s navigate to https://oidcdebugger.com/.
Figure 3: OIDC Request Generation with https://oidcdebugger.com/
As shown in Figure 3, fill in the <your-okta-domain> and <your-client-id> fields with your respective values. Note that we have provided the scopes openid, email, profile, and greet, which is required for our service resource. After the fields are filled, click Send Request and you will be presented with a screen similar to Figure 4.
Figure 4: OIDC Response with the JWT Access Token
Copy the access token that is generated, and set it as a shell variable.
xxxxxxxxxx
$ TOKEN=eyJraWQiOiI3...
Now that we have a JWT token to authenticate the user from Okta, we will be able to use this with our service to do the authentication.
xxxxxxxxxx
$ curl -k -H "Authorization: Bearer $TOKEN" https://localhost:8443/secured/hello
Hello lafernando@gmail.com, authScheme: jwt groups: Everyone
As we can see above, now the service invocation succeeds, since we provided a valid JWT token. Also, in our service implementation, we have stated that we require the greet scope in order to invoke the hello resource in the service. We can create another access token using https://oidcdebugger.com/ without this scope and see that we cannot invoke the service resource anymore.
Data Service With Access Control
Now that we know the generation functionality of using OIDC in creating the JWT for our services, let’s create another general scenario for using access control in our service. We will be creating a simple data service that represents a product catalog, where we will control the reading and writing operations of it based on the authenticated user’s privileges. For this, we will be using the scopes products_add, products_access, and products_delete.
Listing 3 shows the implementation of this data service.
Listing 3: Product Catalog Data Service
Here, we can see we have simply mapped the service resources with the service paths and respective HTTP methods in order to define the functionality. Each resource requires a specific scope in order to invoke the resource. In this manner, we can have a fine-grained access control mechanism when defining the operations in our system.
The service above can be tested by creating tokens with different scope combinations to verify the access control features.
Sample Run
The full source code of our product catalog data service can be found here.
xxxxxxxxxx
$ ballerina run product-catalog-service.bal
[ballerina/http] started HTTPS/WSS listener 0.0.0.0:8443
$ curl -d '{"id":"id1","name":"Pixel 4 XL","price":899.0}' -k -H "Authorization: Bearer $TK_GREET" https://localhost:8443/ProductCatalog/product
Authorization failure.
$ curl -d '{"id":"id1","name":"Pixel 4 XL","price":899.0}' -k -H "Authorization: Bearer $TK_PRODUCT_ACCESS" https://localhost:8443/ProductCatalog/product
Authentication failure.
$ curl -d '{"id":"id1","name":"Pixel 4 XL","price":899.0}' -k -H "Authorization: Bearer $TK_PRODUCTS_ADD" https://localhost:8443/ProductCatalog/product
$ curl -d '{"id":"id2","name":"Pixel 3","price":599.0}' -k -H "Authorization: Bearer $TK_PRODUCTS_ADD" https://localhost:8443/ProductCatalog/product
$ curl -k -H "Authorization: Bearer $TK_GREET" https://localhost:8443/ProductCatalog/product
Authorization failure.
$ curl -k -H "Authorization: Bearer $TK_PRODUCTS_ACCESS" https://localhost:8443/ProductCatalog/product
[{"id":"id1", "name":"Pixel 4 XL", "price":899}, {"id":"id2", "name":"Pixel 3", "price":599}]
$ curl -X DELETE -k -H "Authorization: Bearer $TK_PRODUCTS_ACCESS" https://localhost:8443/ProductCatalog/product/id1
Authorization failure.
$ curl -X DELETE -k -H "Authorization: Bearer $TK_PRODUCTS_DELETE" https://localhost:8443/ProductCatalog/product/id1
$ curl -k -H "Authorization: Bearer $TK_PRODUCTS_ACCESS" https://localhost:8443/ProductCatalog/product
[{"id":"id2", "name":"Pixel 3", "price":599}]
Summary
In this article, we have looked into how we can secure a REST service written in Ballerina using Okta as the identity provider. We have used OIDC when generating a JWT access token to access our service resources.
For more information on writing microservices in Ballerina, check out the following resources:
Opinions expressed by DZone contributors are their own.
Comments