Using tokens with Applications and Neo4j Query API for auth

In a previous blog post I discussed a web application obtaining and using a token with Neo4j Query API as a result of a user successfully authenticating. This entry looks at what would be involved for an application to obtain a token and use it with Neo4j Query API.

Plot spoiler - it’s very similar.

Many organisations prefer a token based approach, one reason for this is the limited lifespan and scope of token which helps to reduce risk if it is intercepted. Lets look at how this can be achieved.

We will need

  • A free Okta developer account

  • Neo4j Enterprise running in Docker locally

  • Docker installed

  • A local copy of curl

  • Text editor

Neo4j Docker image

Install & Run the Neo4j Docker image

Note:  If you are not comfortable with the values used for the username & password , change NEO4J_AUTH=neo4j/password to  something that works for you.

The neo4j docker image will use folders in the home directory.  Create those first

mkdir -p ~/neo4j/conf
mkdir -p ~/neo4j/data
mkdir -p ~/neo4j/logs

Tell Docker to download and run Neo4j

docker run -dt \
--name=neo4jDb \
--publish=7474:7474 \
--publish=7687:7687 \
--volume=$HOME/neo4j/data:/data \
--volume=$HOME/neo4j/conf:/conf \
--volume=$HOME/neo4j/logs:/logs \
--env=NEO4J_ACCEPT_LICENSE_AGREEMENT=yes \
--env=NEO4J_AUTH=neo4j/password \
neo4j:enterprise

Test

Check Neo4j is up and running by going to this local URL http://localhost:7474/browser

This should show you the Browser console for Neo4j.  Auth using neo4j for the user and password for the password.


Okta configuration

This requires configuration work in two areas of the Okta console; Applications and  Security and also obtaining the developer account Okta Domain.

Okta developer account domain

Before starting any work , we need the developer account okta domain.  Locate this by going to the top right and selecting your account.  From the drop down menu , you will see your email address and immediately underneath the domain for your developer account.  Make a note of this.

Applications

Applications -> Create App Integration -> API Services

  • Provide a name e.g neo4j m2m query api for API Service

When the new API Service is shown :-

General

Client credentials

  • Client ID: Copy the client id as this will be needed later

  • Client authentication: Client secret

CLIENT SECRETS

  • Client secret:   Copy this as it also will be needed later

General Setting

  • Proof of possession :  Make sure Require Demonstrating Proof of Possession (DPoP) header in token requests is not selected

Security

Security -> API -> Add Authorization Server

  • Name:  Give this a meaningful name e.g neo4j query api

  • Audience: This will form part of the generated token.  Suggest using that is short and descriptive e.g neo4j-query-api.  This will be needed for Neo4j configuration

  • Description: Enter some words that describe what this is for

The newly created authorization server is now displayed.

The next step is to create a Scope which will be used in the Neo4j configuration to map to a Neo4j role.  This determines the access level that will be granted.

Select Scopes

Add Scope

  • Name: Provide a name for the scope e.g Neo4jDba

  • Display phrase:  Enter what this is for e.g DBA access

  • Description: Longer description e.g DBA level access for Neo4j

  • User consent:  Implicit

  • Block services :  Not checked

  • Default scope:  If checked, this will be given to any token request that does not explicitly ask for a scope.  Advise that this is not checked.

  • Metadata:  The scope will be included in the response from the well known API and hence visible.  Advise that this is not checked.

Select Create

The newly created scope will now be shown in the table.  Make a note of the Issuer URI

Takeways from Okta configuration work

Once Okta is configured, we will have

  • Developer account domain

  • Client ID

  • Client Secret

  • Issuer URI

  • Audience

  • Scope


Neo4j configuration

It’s entirely possible for Neo4j to have more than one configured ODIC provider. It’s also possible to hide an entry from users of the Neo4j web Browser console, something that we will need to do as the configuration for an application to use token is not going to work for a user.

This is line that we’ll need to use with our OIDC configuration entry


Edit neo4j.conf and add this in the OIDC section swapping out these values for yours from Okta.

* YOUR_AUDIENCE_ID_FROM_OKTA

* YOUR_CLIENT_ID_FROM_OKTA

* YOUR_CLIENT_SECRET_ID_FROM_OKTA

* YOUR_ISSUER_URI_FROM_OKTA

* YOUR_SCOPE_FROM_OKTA

```Text
# Okta m2m settings
dbms.ecurity.oidc.m2m.visible=false
dbms.security.oidc.m2m.display_name=m2m
dbms.security.oidc.m2m.auth_flow=pkce
dbms.security.oidc.m2m.well_known_discovery_uri=YOUR_ISSUER_URI_FROM_OKTA/.well-known/openid-configuration
dbms.security.oidc.m2m.audience=YOUR_AUDIENCE_ID_FROM_OKTA
dbms.security.oidc.m2m.client_id=YOUR_CLIENT_ID_FROM_OKTA
dbms.security.oidc.m2m.claims.groups=scp
dbms.security.oidc.m2m.claims.username=sub
dbms.security.oidc.m2m.params=client_id=YOUR_CLIENT_ID_FROM_OKTA;response_type=code;scope=openid profile scp
dbms.security.oidc.m2m.authorization.group_to_role_mapping=YOUR_SCOPE_FROM_OKTA=admin

Save the file and then restart Neo4j.

docker restart neo4jDb

Getting a token from Okta to use with Neo4j Query API

A token to use with the Query API is obtained from https://YOUR_DEVELOPER_ACCOUNT_DOMAIN/oauth2/default/v1/token  as illustrated with this example using CURL

Replace

  • YOUR_DEVELOPER_ACCOUNT_DOMAIN

  • YOUR_SCOPE_FROM_OKTA

  • YOUR_CLIENT_ID_FROM_OKTA

  • YOUR_CLIENT_SECRET_ID_FROM_OKTA

with values from the Okta configuration

curl --request POST \

--url https://YOUR_DEVELOPER_ACCOUNT_DOMAIN/oauth2/default/v1/token \
--header 'accept: application/json' \
--header 'cache-control: no-cache' \
--header 'content-type: application/x-www-form-urlencoded' \
--data 'grant_type=client_credentials&scope=YOUR_SCOPE_FROM_OKTA' \
-u CLIENT_ID:CLIENT_SECRET

This should result in a response that looks similar to this

{
"token_type": "Bearer",
"expires_in": 3600,
"access_token": "eyJraWQiOiJfM0lPdk9tUEJGN3hKN2FPbHNmYzVKWmlGWXdua1Q4WHY5ZG9hYk9JOEhFIiwiYWxnIjoiUlMyNTYifQ.eyJ2ZXIiOjEsImp0aSI6IkFULmZkYjJ3eGZPMFJaSmFiUDNIUkxGLVl6VFpHczhkTVFYUnJLWU02aUFlemsiLCJpc3MiOiJodHRwczovL2Rldi04NTI1NzgzOC5va3RhLmNvbS9vYXV0aDIvZGVmYXVsdCIsImF1ZCI6Im5lbzRqLWF1ZCIsImlhdCI6MTcyOTYzMzA2MiwiZXhwIjoxNzI5NjM2NjYyLCJjaWQiOiIwb2FrZ2R4eHJyM3FiVkhFRDVkNyIsInNjcCI6WyJuZW80akRiYSJdLCJzdWIiOiIwb2FrZ2R4eHJyM3FiVkhFRDVkNyJ9.CGHx-dnhKd1d_i_hEroNHOCPUYROh0wqz2EuKCDYuieiIkqx9sG1Z8f1hnb96FL2uyyTL2bpAILiG3-85urVeG-6R5Dazf87opM5IyLhYTxboM5VjF3xsKsUiSjIQBP7jsCqHFxCsBpOB2nUSxzmk3NZpVhV2oZJK5-WBl1wCj7ttyAeuZ7sbm44SdrdIz9pmf6RmTQ30nBexZ6ccNx7YxxZZyo2jJeRvNDOn-yRpydkOOOqe7kR1qk7qhG14cKLQBgmx2RL5DAxG9ZJOHh1dUcOE87duhT3uD476JmcmS8DG589CCO3bMcmORYLkArf_5QFWW-bG8FJy5UGJVffFA",
"scope": "neo4jDba"
}

From this we use the value of the access_token key to use with the Query API.


Using a token with the Query API

Replace

  • YOUR_ACCESS_TOKEN

with the value obtained from Okta


curl -X POST <http://localhost:7474/db/neo4j/query/v2> \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"statement": "MATCH (n) RETURN n LIMIT 1"}'

A response with a single entry from your Neo4j graph will be returned


An example Python application

Needs the request module to be installed before this can be used. Replace YOUR_ with values for your setup.

"""
 * Copyright (c) 2024-Present, Neo4j. and/or its affiliates. All rights reserved.
 *
 * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *
 * See the License for the specific language governing permissions and limitations under the License.
"""

# -*- coding: utf-8 -*-
# Generic/Built-in
import json
from typing import Dict

# Other Libs
from requests import Request, Session
from requests.auth import HTTPBasicAuth, AuthBase

# Owned

# Configuration values
class MyConfiguration():
    CLIENT_ID = "YOUR_CLIENT_ID_FROM_OKTA"
    CLIENT_SECRET = "YOUR_CLIENT_SECRET_FROM_OKTA"
    OKTA_TOKEN_URI = "https://YOUR_DEVELOPER_ACCOUNT_DOMAIN/oauth2/default/v1/token"
    OKTA_SCOPE = "YOUR_SCOPE_FROM_OKTA"
    NEO4J_QUERY_URI = "YOUR_NEO4j_QUERY_API_URL"

# Returns Auth object for use with Python requests 
# that uses our token from Okta
class BearerAuth(AuthBase):
   """
   Returns a request header that uses Bearer token for auth
    
   :return: Header that uses Bearer token
   """
    def __init__(self, token):
        self.token = token

    def __call__(self, r):
        r.headers["authorization"] = "Bearer " + self.token
        return r


def make_request(request_url: str, request_operation: str, request_headers: dict, request_body,
                 request_auth: AuthBase) -> Dict:
    """
   Makes a request to Aura API and returns the JSON from the response
   :param request_url The path to the endpoint to make a request to
   :param request_operation The HTTP operation to perform for the request
   :param request_headers  A dictionary containing headers for the request
   :param request_body  The body of the request
   :param request_auth  Auth to use with the request
   :return: The JSON response back as a Python Dict
   """

    # A session will be used to avoid having to make a new connection with each request
    request_session = Session()

    # Prepare our request
    prepared_request = Request(request_operation, request_url, headers=request_headers, auth=request_auth,
                               data=request_body).prepare()

    try:
        # Send the request
        response = request_session.send(prepared_request)

    except Exception as e:
        print("%s raised an error: \n%s", aura_api_request, e)
        raise

    else:
        return response.json()


def get_token_from_okta(url: str, client_id: str, client_secret: str, token_scope: str):
    """
   Gets a token from Okta
   :param url: URL for Okta token
   :param client_id: Okta client id
   :param client_secret: Okta client secret
   :param token_scope: Okta scope

   """
    # Ask for a bearer token using the client id and client secret from Okta

    headers = {
        "Content-Type": "application/x-www-form-urlencoded",
        "Accept": "application/json",
    }

    body = {"grant_type": "client_credentials", "scope": token_scope}

    okta_response = make_request(url, 'POST', headers, body, HTTPBasicAuth(client_id, client_secret))

    if 'access_token' in okta_response:
        return okta_response['access_token']
    else:
        return None


def get_data_from_neo4j(cypher_statement: str, query_api_url: str, token_from_okta: str) -> Dict:
    """
    Gets a data from Neo4j Query API using a bearer token for auth
    :param cypher_statement: Cypher statement in the form of a dictionary
    :param query_api_url:URL for Neo4j Query API
    :param token_from_okta: token from Okta
    :return A dictionary containing the data from Neo4j
    """
    headers = {
        "Content-Type": "application/json",
        "Accept": "application/json",
    }

    neo4j_response = make_request(query_api_url, 'POST', headers, cypher_statement, BearerAuth(token_from_okta))

    if 'data' in neo4j_response:
        return neo4j_response['data']
    else:
        return {}

    pass


def showcase_token_with_queryapi(config):

    okta_token = get_token_from_okta(config.OKTA_TOKEN_URI, config.CLIENT_ID, config.CLIENT_SECRET, config.OKTA_SCOPE)

    neo4j_query = {"statement": "MATCH (n) RETURN n LIMIT 1"}

    neo4j_data = get_data_from_neo4j(json.dumps(neo4j_query), config.NEO4J_QUERY_URI, okta_token)

    print(f"Data from Neo4j: {neo4j_data['values'][0]}")


if __name__ == '__main__':
    showcase_token_with_queryapi(MyConfiguration)



<
Previous Post
Commentary on the web application code used in SSO post
>
Next Post
Explicit transactions with Neo4j Query API