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

Tuesday, December 21, 2021

Python FastAPI microservice with Okta and OPA

 December 21, 2021     FastAPI, Microservices, Python     1 comment   

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. Python FastAPI with Okta and OPA
  9. Conclusion

Problem Statement

We will develop a FastAPI 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.


Python FastAPI with Okta and OPA 

Now that we have working Okta and OPA services, let us move on to develop the Python FastAPI microservice. In Python FastAPI is a modern, high performance framework to build microservices. Example below provides a simple microservice built with FastAPI which supports two APIs  "/sayhello"  and "/saysecret" and returns a JSON response. 

To run this example need to install these modules.
pip install fastapi
pip install uvicorn
pip install httpx
pip install okta-jwt
Here uvicorn is an implementation of ASGI (Asynchronous Service Gateway Interface) specifications and provides a standard interface between async-capable Python web servers, frameworks and applications.


Application settings

First step is to define an application settings (app.env) file to hold the Okta and OPA settings. This includes the Okta issuer, Okta client id and Okta client secret received during the application registration. Also the OPA authorization URL is configured in this settings file. Also note that we have used the Rego package name created earlier on OPA services in the Auth URL. 
[Okta]
OKTA_CLIENT_ID=****
OKTA_CLIENT_SECRET=****
OKTA_ISSUER=https://dev-33448303.okta.com/oauth2/default
OKTA_AUDIENCE=api://default

[Opa]
OPA_AUTHZ_URL=http://localhost:8181/v1/data/demoapi/authz/allow

Application with local validation of JWT

Next step is to define the FastAPI microservices (app.py). There are 2 APIs with a dependency to validate method. In validate, we check the JWT for authentication then make an API call to OPA service. Based on the allow/ deny decision from OPA service a decision is made to serve the client request.
from fastapi import Depends, FastAPI, HTTPException, Request
from okta_jwt.jwt import validate_token as validate_locally
from fastapi.security import OAuth2PasswordBearer
import uvicorn
import configparser
import httpx
import json

app = FastAPI()

# Define the auth scheme and access token URL
oauth2_scheme = OAuth2PasswordBearer(tokenUrl='token')

# Load environment variables
config = configparser.ConfigParser()
config.read('app.env')


def validate(request: Request, token: str = Depends(oauth2_scheme)):
    try:
        # AuthN: Validate JWT token locally
        auth_res = validate_locally(
            token,
            config.get('Okta', 'OKTA_ISSUER'),
            config.get('Okta', 'OKTA_AUDIENCE'),
            config.get('Okta', 'OKTA_CLIENT_ID')
        )

        if bool(auth_res) is False:
            return False

        # AuthZ: Validate with defined policies
        data = {
            "input": {
                "method": request.method,
                "api": request.url.path,
                "jwt": {
                    "tokenValue": token
                }
            }
        }

        opa_url = config.get('Opa', 'OPA_AUTHZ_URL')
        headers = {
            'accept': 'application/json'
        }

        authz_response = httpx.post(opa_url, headers=headers, data=json.dumps(data))

        if authz_response:
            authz_json  = json.loads(authz_response.text)
            return bool(authz_json["result"])
        else:
            return False

    except Exception as e:
        print("Error: " + str(e))
        raise HTTPException(status_code=403)


@app.get("/sayhello")
async def sayhello(valid: bool = Depends(validate)):
    if valid:
        return {"message": "Hello there!!"}
    else:
        raise HTTPException(status_code=403)


@app.get("/saysecret")
async def saysecret(valid: bool = Depends(validate)):
    if valid:
        return {"message": "This is a secret"}
    else:
        raise HTTPException(status_code=403)


if __name__ == '__main__':
    uvicorn.run('app:app',
                host='127.0.0.1',
                port=8086,
                reload=True)

Application with remote validation of JWT

Only change in the code flow below is to validate the JWT token with Okta using /introspect API call. This endpoint takes an access token and returns whether the. token is active or not. Every API call will require a validation with Okta using this approach.

from fastapi import Depends, FastAPI, HTTPException, Request
from fastapi.security import OAuth2PasswordBearer
import uvicorn
import configparser
import httpx
import json

app = FastAPI()

# Define the auth scheme and access token URL
oauth2_scheme = OAuth2PasswordBearer(tokenUrl='token')

# Load environment variables
config = configparser.ConfigParser()
config.read('app.env')


def validate_remotely(token, issuer, clientId, clientSecret):
    headers = {
        'accept': 'application/json',
        'cache-control': 'no-cache',
        'content-type': 'application/x-www-form-urlencoded',
    }
    data = {
        'client_id': clientId,
        'client_secret': clientSecret,
        'token': token,
    }
    url = issuer + '/v1/introspect'

    response = httpx.post(url, headers=headers, data=data)

    return response.status_code == httpx.codes.OK and response.json()['active']


def validate(request: Request, token: str = Depends(oauth2_scheme)):

    try:
        # AuthN: Validate JWT token locally
        auth_res = validate_remotely(
            token,
            config.get('Okta', 'OKTA_ISSUER'),
            config.get('Okta', 'OKTA_CLIENT_ID'),
            config.get('Okta', 'OKTA_CLIENT_SECRET')
        )

        if auth_res is False:
            return False

        # AuthZ: Validate with defined policies
        data = {
            "input": {
                "method": request.method,
                "api": request.url.path,
                "jwt": {
                    "tokenValue": token
                }
            }
        }

        opa_url = config.get('Opa', 'OPA_AUTHZ_URL')
        headers = {
            'accept': 'application/json'
        }

        authz_response = httpx.post(opa_url, headers=headers, data=json.dumps(data))

        if authz_response:
            authz_json  = json.loads(authz_response.text)
            return bool(authz_json["result"])
        else:
            return False

    except Exception as e:
        print("Error: " + str(e))
        raise HTTPException(status_code=403)


@app.get("/sayhello")
async def sayhello(valid: bool = Depends(validate)):
    if valid:
        return {"message": "Hello there!!"}
    else:
        raise HTTPException(status_code=403)


@app.get("/saysecret")
async def saysecret(valid: bool = Depends(validate)):
    if valid:
        return {"message": "This is a secret"}
    else:
        raise HTTPException(status_code=403)


if __name__ == '__main__':
    uvicorn.run('app:app',
                host='127.0.0.1',
                port=8086,
                reload=True)

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:8086/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:8086/saysecret' --header 'Authorization: Bearer add_access_token_here' 

No output. Status code 403.
Get access to the source code of this example in GitHub.

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. 
  • Share This:  
Newer Post Older Post Home

1 comment:

  1. UnknownJanuary 3, 2022 at 2:09 PM

    Great blog! Thanks for sharing :)

    ReplyDelete
    Replies
      Reply
Add comment
Load more...

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