StackStalk
  • Home
  • Java
    • Java Collection
    • Spring Boot Collection
  • Python
    • Python Collection
  • C++
    • C++ Collection
    • Progamming Problems
    • Algorithms
    • Data Structures
    • Design Patterns
  • General
    • Tips and Tricks

Thursday, December 16, 2021

Spring Boot with Okta and OPA

 December 16, 2021     Java, Microservices, Spring Boot     No comments   

Authentication (AuthN) and Authorization (AuthZ) is a common challenge when developing microservices. In this article, we will explore how to leverage Okta for AuthN and Open Policy Agent (OPA) for AuthZ.

Contents

  1. Problem Statement
  2. Okta Introduction
  3. OPA Introduction
  4. Register application in Okta
  5. OPA as Docker Container
  6. Rego policy creation and deployment
  7. Rego playground
  8. Spring Boot with Okta and OPA
  9. Conclusion

Problem Statement

We will develop a Spring Boot microservice with 2 APIs /sayhello and /saysecret. Clients having a valid access token from the authentication server (using Okta) will be authorized (using OPA) to perform only /sayhello API.



Okta Introduction

  • Okta (https://www.okta.com/) is a secure identity cloud that links all apps logins and devices into a single pane of glass 
  • Okta runs in the cloud and integrates with directories and identity management systems
  • Okta features include Single Sign On (SSO), LDAP integration, Multi factor authentication (MFA) etc.
  • Okta Single Sign-On lets users access any app with a single set of credentials
  • Okta plays the role of authentication server for secure access to API and microservices by authenticating the user and providing access token
Let us say we are writing a microservice which exposes APIs for the clients to consume. Okta is used as a custom OAuth 2.0 authorization server to protect the API endpoints. OAuth 2.0 is a standard that apps use to provide client applications with access. An authorization server is simply an OAuth 2.0 token minting engine. Each authorization server has a unique issuer URI and its own signing key for tokens to keep a proper boundary between security domains.

The usual OAuth 2.0 grant flow looks like this:
  • Client requests authorization from the resource owner (usually the user).
  • If the user gives authorization, the client passes the authorization grant to the authorization server (in this case Okta).
  • If the grant is valid, the authorization server returns an access token, possibly alongside a refresh and/or ID token.
  • The client now uses that access token to access the resource server.

Open Policy Agent (OPA) Introduction

  • The Open Policy Agent (OPA) is an open source, general-purpose policy engine that can be leveraged for policy enforcement on microservices.
  • OPA provides a high-level declarative language (Rego) that lets to specify policy as code and supports simple APIs to offload policy decision-making from the microservices.
  • In a typical scenario, when a microservice needs to make policy decisions (E.g. Is the user authorized to perform this API request) it queries OPA and supplies JSON data as input. OPA evaluates the policy decisions by evaluating the input against configured policies and data. OPA response can be configured to provide yes/ no or allow/ deny decisions.
  • Rego (https://www.openpolicyagent.org/docs/latest/policy-language/) is the declarative query language to define policies. Rego provides support for referencing nested documents and ensures queries are correct and unambiguous.

Let us say we are writing a microservice which exposes APIs for the clients to consume. OPA is used to enforce the authorization policy for the APIs.
  • OPA gets bootstrapped with a Policy document written in Rego which contains the authorization rules. OPA supports Policy API which can be used to add, modify or delete policies.
  • OPA policies make policy decisions based on hierarchical structured data. Data can be loaded from outside world and this is referred as base documents. There are also documents computed by policies which are referred as virtual documents. OPA supports Data API to read, write and update documents.
  • Microservice queries OPA for a decision. Query is typically accompanied with attributes like HTTP method, path, user etc.
  • OPA uses the policies and data documents and responds with an allow or deny decision to the requesting microservices.

Register application in Okta

In this section we will look at the steps required to register an application in Okta. We are building a server side application which would use OAuth 2.0 authentication through API endpoints.

Step 1: Sign in to Okta developer account https://developer.okta.com

Step 2: Create a Web App using "Add Web App".

Step 3: Select the Sign in Method as "OpenID Connect" and application type as "Web Application".


Step 4: Provide the App Integration name, Grant type and Assignments and Save the application


Step 5: Now the app for integration is ready. Note down the Okta domain, Client ID and Client Secret for integration into microservice.



Step 6: Navigate to Security/ API in Okta dashboard and configure the required scopes. We will use default authorization server for this example.


Step 7: Edit the default authorization server and configure the. required scopes. In this example, we have added a new scope "read_sayhello" which we will use in our application.



Step 8: Get access token from Okta

Validate the Okta configuration by requesting for access token.
curl --location --request POST 'https://dev-33448303.okta.com/oauth2/default/v1/token?grant_type=client_credentials&client_id=[client-id]&client_secret=[client_secret]&scope=read_sayhello' \
> --header 'Content-Type: application/x-www-form-urlencoded'
Now we have the Okta setup ready for integration.

OPA as Docker container

It is easy to run OPA as a Docker container. The command below brings up an instance of OPA engine on port 8181.
$ docker run -p 8181:8181 openpolicyagent/opa run --server --log-level debug
{"addrs":[":8181"],"diagnostic-addrs":[],"level":"info","msg":"Initializing server.","time":"2021-12-07T06:04:19Z"}
{"level":"debug","msg":"maxprocs: Leaving GOMAXPROCS=6: CPU quota undefined","time":"2021-12-07T06:04:19Z"}
{"headers":{"Content-Type":["application/json"],"User-Agent":["Open Policy Agent/0.35.0 (linux, amd64)"]},"level":"debug","method":"POST","msg":"Sending request.","time":"2021-12-07T06:04:19Z","url":"https://telemetry.openpolicyagent.org/v1/version"}
{"level":"debug","msg":"Server initialized.","time":"2021-12-07T06:04:19Z"}
{"headers":{"Content-Length":["216"],"Content-Type":["application/json"],"Date":["Tue, 07 Dec 2021 06:04:21 GMT"]},"level":"debug","method":"POST","msg":"Received response.","status":"200 OK","time":"2021-12-07T06:04:21Z","url":"https://telemetry.openpolicyagent.org/v1/version"}
{"current_version":"0.35.0","level":"debug","msg":"OPA is up to date.","time":"2021-12-07T06:04:21Z"}
Validate if OPA service is running by accessing http://localhost:8181/ which should display the OPA page.


Rego Policy creation and deployment

Now that we have OPA service running, next step is to deploy a policy. Let us create a sample Rego policy which denies all requests except the /sayhello GET API.
package demoapi.authz

default allow = false

allow {
   input.method == "GET"
   input.api == "/sayhello"
   token.payload.scp[_] = "read_sayhello"
}

token = {"payload": payload} {
   [header, payload, signature] := io.jwt.decode(input.jwt.tokenValue)
}
Next step is to deploy this policy on to the OPA service. We will use the OPA REST APIs for this https://www.openpolicyagent.org/docs/latest/rest-api/. 

Use the "Create Policy" API to deploy the policy.


Use the "GET Policy" API to validate policy deployment.



Rego Playground

Rego playground can be used to validate the policy against the token from Okta server.  

Copy the policy created earlier into the playground. Specify the input JSON fields and provide the access token received from Okta earlier. If the API is "/sayhello" policy validates to allow as true.


If the API is "/saysecret" policy validates to allow as false.


Spring Boot with Okta and OPA 

Now that we have working Okta and OPA services, let us move on to develop the Spring Boot microservice. Basic understanding of Spring authorization architecture and associated classes is important to work with this example.

Access Decision Manager

  • Access Decision Manager make a final access control authorization decision. 
  • Access Decision Manager contains a list of access decision voter responsible for voting on authorization decisions.
  • There are three concrete AccessDecisionManager provided with Spring Security that tally the votes. 
    • The ConsensusBased implementation will grant or deny access based on the consensus of non-abstain votes.
    • The AffirmativeBased implementation will grant access if one or more ACCESS_GRANTED votes were received (i.e. a deny vote will be ignored, provided there was at least one grant vote). 
    • The UnanimousBased provider expects unanimous ACCESS_GRANTED votes in order to grant access, ignoring abstains. It will deny access if there is any. ACCESS_DENIED vote.

Access Decision Voter

  • Access Decision Voter is responsible for voting on authorization decisions.
  • A voting implementation will return:
    • ACCESS_ABSTAIN if it has no opinion on an authorization decision. 
    • If it does have an opinion, it must return either ACCESS_DENIED or ACCESS_GRANTED.

More details refer Spring Authorization architecture: 
https://docs.spring.io/spring-security/site/docs/4.2.x/reference/html/authz-arch.html

Spring Boot dependencies

Create a Spring starter project and include the Web application starter, Spring Boot Security starter, Okta Spring Boot starter and Spring Boot WebFlux as dependencies to the pom.xml.
<dependencies>
    <dependency>
        <groupId>com.okta.spring</groupId>
<artifactId>okta-spring-boot-starter</artifactId>
</dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency> </dependencies>
    

Configure application.properties

Update the application.properties to point to the Okta issuer. Configure the Okta client id and client secret received during application registration. 
okta.oauth2.issuer=https://dev-33448303.okta.com/oauth2/default
okta.oauth2.client-id=0oa2yn4wxkbbrtwPH5d7
okta.oauth2.client-secret=[client secret]
    

Define the Controller

In the Spring application we have included 2 APIs /sayhello and /saysecret to test the integration with OPA.
package com.stackstalk.security;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@SpringBootApplication
public class MySecureAppApplication {

	public static void main(String[] args) {
	     SpringApplication.run(MySecureAppApplication.class, args);
	}

	@GetMapping(path="/sayhello")
	public String sayhello(JwtAuthenticationToken user) {
	     System.out.println(user.toString());
             return "Hello there!! ";
	}
	
	@GetMapping(path="/saysecret")
	public String saysecret(JwtAuthenticationToken user) {
	     System.out.println(user.toString());
             return "This is a secret!! ";
	}
}
    

Define the Security configuration

Now let us look at the security configuration for this application. To enable HTTP security in Spring we need to extend the WebSecurityConfigurerAdapter and provide the configuration. Here we have used an Access Decision Manager for authorization decisions which contains the OPA based voter. We have indicated in the security config that it uses JWT as the token verification strategy. Also note that we have used the Rego package name created earlier on OPA service in the Auth URL.
package com.stackstalk.security;

import java.util.Arrays;
import java.util.List;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.AccessDecisionVoter;
import org.springframework.security.access.vote.UnanimousBased;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
	
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
        .antMatchers("/").permitAll()
        .anyRequest().authenticated()
        .accessDecisionManager(accessDecisionManager())
        .and()
        .oauth2ResourceServer().jwt();
    }

    public AccessDecisionManager accessDecisionManager() {
	List<AccessDecisionVoter<? extends Object>> decisionVoters = Arrays.asList(
		new OPABasedVoter("http://localhost:8181/v1/data/demoapi/authz/allow"));
	return new UnanimousBased(decisionVoters);
    }
}
    

Define the Access Decision Voter

Now let us review the implementation of OPA based voter. Here we extract the HTTP method, API and JWT token and make a POST call on the OPA Auth URL. Based on the allow/ deny decision from OPA the voter makes a decision to GRANT or DENY access to API.

package com.stackstalk.security;

import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

import org.springframework.security.access.AccessDecisionVoter;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.FilterInvocation;
import org.springframework.web.reactive.function.client.WebClient;

import reactor.core.publisher.Mono;

public class OPABasedVoter implements AccessDecisionVoter<Object> {

	private String opaAuthUrl;
	
	OPABasedVoter(String opaAuthUrl) {
		this.opaAuthUrl = opaAuthUrl;
	}
	
	@Override
	public boolean supports(ConfigAttribute attribute) {
		return true;
	}

	@Override
	public boolean supports(Class clazz) {
		return true;
	}

	@Override
	public int vote(Authentication authentication, Object object, Collection attributes) {
		
		if (!(object instanceof FilterInvocation)) {
			return ACCESS_ABSTAIN;
		}
		
		FilterInvocation filter = (FilterInvocation) object;
		
		Map<String, Object> input = new HashMap<String, Object>();
		input.put("method", filter.getRequest().getMethod());
		input.put("api", filter.getRequest().getRequestURI());
		input.put("jwt", authentication.getPrincipal());
		
		WebClient webClient = WebClient.create();
		OPAResponse response = webClient.post()
				.uri(this.opaAuthUrl)
				.body(Mono.just(new OPARequest(input)), OPARequest.class)
				.retrieve()
				.bodyToMono(OPAResponse.class)
				.block();

                if (response == null || response.getResult() == false) {
                   return ACCESS_DENIED;
                }		
		
                return ACCESS_GRANTED;
	}

}
We have also used request/ response classes.
package com.stackstalk.security;

import java.util.Map;

public class OPARequest {

    Map<String, Object> input;

    public OPARequest(Map<String, Object> input) {
        this.input = input;
    }
    
    public Map<String, Object> getInput() {
        return input;
    }
}
package com.stackstalk.security;

public class OPAResponse {

    private boolean result;

    public OPAResponse() {
    }

    public boolean getResult() {
        return this.result;
    }
}

Putting it all together and testing

Now, let us test the application and examine.

GET Access Token

This API is used to request for an access token from Okta.
$ curl --location --request POST 'https://dev-33448303.okta.com/oauth2/default/v1/token?grant_type=client_credentials&client_id=add_client_id_here&client_secret=add_client_secret_here&scope=read_sayhello' --header 'Content-Type: application/x-www-form-urlencoded'

Returns the access token from Okta

GET request on /sayhello

This API is allowed by the OPA policy and responds with the output.
$ curl --location --request GET 'http://localhost:8080/sayhello' --header 'Authorization: Bearer add_access_token_here' 

Hello there!!

GET request on /saysecret

This API is denied by the OPA policy and responds with the 403 status code.
$ curl --location --request GET 'http://localhost:8080/saysecret' --header 'Authorization: Bearer add_access_token_here' 

No output. Status code 403.

Conclusion

Okta plays the role of authentication server for secure access to API and microservices by authenticating the user and providing access token.  OPA lets to specify policy as code and supports simple APIs to offload policy decision-making from the microservices. Spring authorization architecture classes could be easily leveraged with Spring Boot microservices to implement secure APIs.

Get access to the full source code of this example in GitHub.
  • Share This:  
Newer Post Older Post Home

0 comments:

Post a Comment

Follow @StackStalk
Get new posts by email:
Powered by follow.it

Popular Posts

  • Avro Producer and Consumer with Python using Confluent Kafka
    In this article, we will understand Avro a popular data serialization format in streaming data applications and develop a simple Avro Produc...
  • Monitor Spring Boot App with Micrometer and Prometheus
    Modern distributed applications typically have multiple microservices working together. Ability to monitor and manage aspects like health, m...
  • Server-Sent Events with Spring WebFlux
    In this article we will review the concepts of server-sent events and work on an example using WebFlux. Before getting into this article it ...
  • Implement caching in a Spring Boot microservice using Redis
    In this article we will explore how to use Redis as a data cache for a Spring Boot microservice using PostgreSQL as the database. Idea is to...
  • Python FastAPI microservice with Okta and OPA
    Authentication (AuthN) and Authorization (AuthZ) is a common challenge when developing microservices. In this article, we will explore how t...
  • Spring Boot with Okta and OPA
    Authentication (AuthN) and Authorization (AuthZ) is a common challenge when developing microservices. In this article, we will explore how t...
  • Getting started with Kafka in Python
    This article will provide an overview of Kafka and how to get started with Kafka in Python with a simple example. What is Kafka? ...
  • Getting started in GraphQL with Spring Boot
    In this article we will explore basic concepts on GraphQL and look at how to develop a microservice in Spring Boot with GraphQL support. ...

Copyright © StackStalk