Skip to main content

Security Schemes

OpenRPC Security Extension

The OpenRPC spec allows extensions. This framework adds a security scheme extension to enable the built-in security handling detailed below. The Components Object has an added field x_security_schemes ( aliased to x-securitySchemes). This field is used to identify and describe the security schemes used by an API and is largely the same as in OAS 3. Security schemes can be provided to the RPCServer instantiation.

The Method Object has an added field x_security ( aliased to x-security). The security data on a method is a dictionary of the scheme name to a list of scopes.

If a method caller lacks the proper security scheme a permission error will be raised with a code of -32099.

Using Python-OpenRPC Security Schemes

Set Security Scheme and Scopes for a Method

When decorating a function as an RPC method you can specify required security schemes and scopes as such:

@rpc.method(security={"apikey": []})
def require_apikey() -> bool:
"""Method caller needs a valid API Key."""
return True

@rpc.method(security={"apikey": ["scope1", "scope2"]})
def require_apikey_with_permissions() -> bool:
"""Caller needs API Key with permissions `scope1` and `scope2`."""
return True

Setting the Security Function

In order for the framework to know the scheme and scopes of a method caller the RPCServer security_function needs to be set.

When a security scheme is set for a method, any call to that method will raise a permission error unless the same security scheme and scopes are returned from the configured security function.

The security function will be called with each method call and will be passed the caller_details that are provided to the process_request or process_request_async method call.

Example Using FastAPI and Request Headers

This example passes request headers sent to an HTTP endpoint to the process_request_async call, then the security_function is made to get the Authorization header and use that to determine the caller security scheme and scopes.

import uvicorn
from fastapi import FastAPI, Response
from openrpc import APIKeyAuth, RPCServer
from starlette.datastructures import Headers
from starlette.requests import Request


app = FastAPI()

security_scheme = {"apikey": APIKeyAuth()}
rpc = RPCServer(security_schemes=security_scheme)


def security_function(caller_details: Headers) -> dict[str, list[str]]:
"""Determine security scheme of method caller from request headers."""
access_token = caller_details["Authorization"]
# Real app will decode token and find user/permissions.
token_permissions = {"token1": [], "token2": ["scope1", "scope2"]}
return {"apikey": token_permissions[access_token]}


@rpc.method(security={"apikey": []})
def require_apikey() -> bool:
"""Method caller needs a valid API Key."""
return True


@rpc.method(security={"apikey": ["scope1", "scope2"]})
def require_apikey_with_permissions() -> bool:
"""Caller needs API Key with permissions `scope1` and `scope2`."""
return True


@app.post("/api/v1")
async def http_process_rpc(request: Request) -> Response:
"""Process RPC request through HTTP server."""
rpc_response = await rpc.process_request_async(
await request.body(), caller_details=request.headers
)
return Response(content=rpc_response, media_type="application/json")


if __name__ == "__main__":
rpc.security_function = security_function
uvicorn.run(app, host="0.0.0.0", port=8080)

Using the Example

# Requests with `token1` can call `require_apikey` but not `require_apikey_with_permissions`.
curl 'http://localhost:8080/api/v1' -H 'Authorization: token1' --data-raw '{"id": 1, "method": "require_apikey", "jsonrpc": "2.0"}'
{"id":1,"result":true,"jsonrpc":"2.0"}

curl 'http://localhost:8080/api/v1' -H 'Authorization: token1' --data-raw '{"id": 1, "method": "require_apikey_with_permissions", "jsonrpc": "2.0"}'
{"id":1,"error":{"code":-32099,"message":"Permission error"},"jsonrpc":"2.0"}

# Requests with `token2` can call both `require_apikey` and `require_apikey_with_permissions`.
curl 'http://localhost:8080/api/v1' -H 'Authorization: token2' --data-raw '{"id": 1, "method": "require_apikey", "jsonrpc": "2.0"}'
{"id":1,"result":true,"jsonrpc":"2.0"}

curl 'http://localhost:8080/api/v1' -H 'Authorization: token2' --data-raw '{"id": 1, "method": "require_apikey_with_permissions", "jsonrpc": "2.0"}'
{"id":1,"result":true,"jsonrpc":"2.0"}

Depends Arguments

Your methods may need to use caller details to determine the specific user calling a method, not just the security scheme. You can inject values to a method call by writing Depends functions to access caller_details and return those values.

We're going to write a function to get the calling user from caller_details called get_user. Then we use the function by making the default value of a method argument equal to Depends(get_user).

Example Combining Depends and Security Function

import uvicorn
from fastapi import FastAPI, Response
from openrpc import APIKeyAuth, Depends, RPCServer
from starlette.datastructures import Headers
from starlette.requests import Request

app = FastAPI()

security_scheme = {"apikey": APIKeyAuth()}
rpc = RPCServer(security_schemes=security_scheme)
db = {
"token1": {
"username": "user1",
"security_schemes": {"apikey": ["scope1"]},
}
}


def get_user(caller_details: Headers) -> dict[str, list[str]]:
"""Get user calling a method."""
access_token = caller_details["Authorization"]
# Real app will get user from decoding token.
user = db[access_token]
return user


def security_function(user: dict = Depends(get_user)) -> dict[str, list[str]]:
"""Determine security scheme of method caller from request headers."""
return user["security_schemes"]


@rpc.method(security={"apikey": ["scope1"]})
def require_apikey_with_permission() -> bool:
"""Caller needs API Key with permission `scope1`."""
return True


@rpc.method(security={"apikey": ["scope1"]})
def get_caller(user: dict = Depends(get_user)) -> dict:
"""Get the calling user."""
return user


@app.post("/api/v1")
async def http_process_rpc(request: Request) -> Response:
"""Process RPC request through HTTP server."""
rpc_response = await rpc.process_request_async(
await request.body(), caller_details=request.headers
)
return Response(content=rpc_response, media_type="application/json")


if __name__ == "__main__":
rpc.security_function = security_function
uvicorn.run(app, host="0.0.0.0", port=8080)