# Secure GraphRAG: Wiring Microsoft Copilot Studio to Neo4j with per-user Okta SSO

> Source: <https://neo4j.com/blog/graph-database/secure-graphrag-wiring-microsoft-copilot-studio-to-neo4j-with-per-user-okta-sso/>
> Published: 2026-08-20 21:03:12+00:00

# Secure GraphRAG: Wiring Microsoft Copilot Studio to Neo4j with per-user Okta SSO

13 min read

More in this guide:

## Graph access that knows who you are.

A financial advisor asks Copilot,*“Which of my clients have more than 30% of their portfolio concentrated in a single sector and haven’t rebalanced in over 90 days?” *Answering that means traversing clients to accounts to holdings to securities to transaction history, several hops deep, and the advisor should see exactly their own book of business.

Once an LLM is writing the queries, you can’t fully predict or constrain what it will ask for. If the agent connects to Neo4j with a single shared service account, every user’s session inherits whatever that account can reach, and one bad generation can walk straight into a colleague’s clients. Most Copilot-to-database integrations get built this way anyway, because a shared service account is the fastest way to get a demo working. It holds up right until more than one type of user touches it in production.

A more robust solution would be to have Copilot Studio calling a Neo4j MCP server that forwards each user’s own Okta access token straight through to Neo4j, with Okta groups mapped to Neo4j roles so the database enforces who sees what. Your Neo4j instance needs SSO with Okta already configured before you start, since this integration extends that existing trust relationship instead of creating it.

## Why per-user identity matters more for AI agents than for regular apps

In a typical application, your code decides what queries run and enforces authorization above the database. A user can’t ask the app to do something it wasn’t built to do. An AI agent breaks that assumption, because the agent writes the query itself in response to whatever the person types. Ask an LLM to “show me what’s relevant to this deal” and it might traverse into a subgraph the person asking was never supposed to reach. If that agent connects with a shared service account, it becomes what security engineers call a confused deputy. It holds more authority than the person prompting it, and it uses that authority on their behalf whether or not it should.

Pushing identity down to Neo4j closes that gap. The agent can still generate any Cypher it wants, but the signed-in user’s own roles decide what actually comes back. A bad generation can’t return more than that person is already allowed to see.

Identity-scoped retrieval means the agent sometimes returns less than it technically “could,” and two different people can get two different answers to the same question. That’s the access model working as intended. If your team spends an afternoon debugging why the agent gave inconsistent answers, check who was signed in before you check the prompt.

The advisor scenario above is specific, but the same mechanism applies to any graph, whether it holds portfolios, patient records, or supply chain data. Okta groups map to Neo4j roles, Neo4j enforces the boundary, and Copilot only ever surfaces what the signed-in user’s role allows.

## What you’re actually building

A Copilot Studio agent uses OAuth 2.0 to get an Okta access token on behalf of the signed-in user, passes that token as a bearer header to the Neo4j MCP server running on Google Cloud Run, and the MCP server forwards the token unchanged to Neo4j. Neo4j validates the token directly against Okta and maps the user’s Okta groups to database roles. The only credential anywhere on that path is the Okta-issued bearer token.

The key architectural decision: the Neo4j instance validates the Okta-issued access token *directly* — not through Neo4j’s Auth0 broker. By default, Neo4j’s Aura console SSO trusts broker-issued tokens. Copilot sends a raw Okta token, so you configure the database to also accept the Okta issuer directly. That’s the database-side change that makes the whole chain work.

## Before you start

**Your Neo4j database must already have SSO configured with Okta before you start this integration.**

You’ll need:

**Neo4j Enterprise or Aura** at a tier that supports Instance SSO — AuraDB Business Critical or AuraDB Virtual Dedicated Cloud. The instance must have been created*after*SSO was configured (SSO doesn’t retroactively apply to older instances).

Alternatively, you could also implement the same configuration using a Neo4j self-managed instance.**Neo4j instance configured for SSO with Okta** via Organization → Security → Single Sign-On, using the your Neo4j SSO Okta app Okta app with redirect*https://login.neo4j.com/login/callback*and Instance SSO enabled.**Neo4j Aura Organization Owner or Admin access**, plus Okta admin access.** GCP project**with Cloud Run enabled and gcloud CLI access to deploy the MCP server image.** Microsoft Copilot Studio**with permission to add MCP tools using the Streamable HTTP transport.

### Database authentication and role mapping

The Neo4j instance needs to be configured ot validate the Okta authorization server directly for database authentication:

- Issuer:
*https://<your-okta-domain>/oauth2/default* - Audience:
*api://default* - Claims:
*username = email, groups = groups*

This runs alongside the existing Auth0-brokered console login — you’re adding a second trusted issuer, not replacing the first.

Role mapping at the Instance SSO level connects Okta groups to built-in Neo4j roles:

Okta group | Neo4j role |
|---|---|
| neo4j-admin | admin |
| neo4j-architect | architect |
| neo4j-editor | editor |
| neo4j-reader | reader |
| neo4j-custom | custom |

**Three things that will bite you if you skip them:**

- Role-mapping entries are exact string matches. A single typo in the group name or role name silently produces a user who authenticates but has no permissions at all.
- admin, architect, editor, and reader are built-in roles. If you use custom, that role must already exist in the database (CREATE ROLE custom).
- The groups claim must be in the
*access token*, not just the ID token. This is the most common miss in Okta configuration — and it’s covered in detail below.

## Configure Okta for Copilot Studio

You need two Okta app integrations. One already exists if your Neo4j SSO is configured: your Neo4j SSO Okta app. You’re adding a new one for Copilot Studio.

### Create the ‘MS Copilot’ app integration

Create a new OIDC web application in Okta:

**Grant types:** Authorization Code + Refresh Token (add Device Authorization if you want to mint test tokens via script)**Sign-in redirect URI:** Add the Copilot-generated callback URL — you get this from Copilot Studio when you add the MCP tool. It looks like*https://global.consent.azure-apim.net/redirect/….*You can’t fill this in until you’ve started the Copilot Studio setup, so come back and add it once you have it.**Authorization server:** Use the*oauth2/default*custom authorization server**Assignments:** Add the users or groups who should have database access

### Add claims to the access token

Go to Security → API → Authorization Servers → default → Claims and add these two claims to the **access token** (not just the ID token):

Claim | Value | Purpose |
|---|---|---|
| user.email | Maps to the Neo4j username | |
| groups | Groups matching ^neo4j-.* | Drives Neo4j role mapping |

Users must be members of the relevant neo4j-* Okta groups. If the groups claim is missing from the access token, role mapping never fires — your users authenticate successfully and land with no database permissions.

### Custom authorization server settings

The default custom authorization server issues the JWT access tokens Neo4j validates. Confirm its settings:

**Audience:*** api://default***Issuer:*** https://<your-okta-domain>/oauth2/default***Access policy:** grants*openid, profile, email, and offline_access*to both the MS Copilot app and the your Neo4j SSO Okta app

## Deploy the MCP server on Google Cloud Run

The Neo4j MCP server runs as a container on Google Cloud Run. The service is publicly reachable (–allow-unauthenticated), but every request still requires a valid Okta bearer token — –allow-unauthenticated removes the GCP IAM layer only; it doesn’t bypass application-level authentication.

Notice what’s *not* in env.yaml: no NEO4J_USERNAME, no NEO4J_PASSWORD. Identity comes entirely from the bearer token.

```
# env.yaml
NEO4J_TRANSPORT_MODE: "http"
NEO4J_URI: "neo4j+s://<your-aura-db-id>.databases.neo4j.io"
NEO4J_DATABASE: "neo4j"
NEO4J_MCP_HTTP_PORT: "80"
NEO4J_MCP_HTTP_HOST: "0.0.0.0"
gcloud run deploy <INSTANCE_NAME> \
  --service-account=mcp-server-sa@<PROJECT_ID>.iam.gserviceaccount.com \
  --allow-unauthenticated \
  --region=<LOCATION> \
  --image=docker.io/mcp/neo4j:latest \
  --env-vars-file=env.yaml \
  --min-instances=1 \
  --max-instances=1 \
  --port=80
```

Google Cloud Run can also be deployed and managed from the GCP console. For a deeper walkthrough of the deployment options, see the [companion Cloud Run blog post](https://medium.com/neo4j/how-to-deploy-the-neo4j-mcp-server-to-gcp-cloud-run-c2736fdee6ac).

After deployment, your MCP endpoint is: *https://<service>.run.app/mcp*

### Add vector search (optional)

The vector-search tool lets Copilot run semantic queries against a Neo4j vector index. It embeds the user’s query using the GenAI plugin (ai.text.embed, with automatic fallback to genai.vector.encode() on older instances), then runs vector similarity search. The embedding API key lives only in the Cloud Run environment — it’s never exposed to Copilot or any client.

To enable it, you need:

- A Neo4j instance with the GenAI plugin (standard on Aura)
- At least one vector index with stored embeddings
- An embedding provider API key (OpenAI, Azure, Vertex, and Bedrock are all supported)
- The model and dimensions used when you created the stored embeddings — the model you configure here
*must*match what you used at ingest, or similarity scores are meaningless

**Step 1 — Store the API key in Secret Manager.** Never put it in env.yaml.

```
printf '%s' "sk-...your-openai-key..." | \
  gcloud secrets create neo4j-embedding-api-key --data-file=-

gcloud secrets add-iam-policy-binding neo4j-embedding-api-key \
  --member="serviceAccount:mcp-server-sa@<PROJECT_ID>.iam.gserviceaccount.com" \
  --role="roles/secretmanager.secretAccessor"
```

**Step 2 — Add embedding settings to env.yaml.** Model and provider only — not the key.

```
# env.yaml additions
NEO4J_EMBEDDING_PROVIDER: "openai"
NEO4J_EMBEDDING_MODEL: "<MODEL_USED_AT_INGEST>"  # e.g. text-embedding-3-small
# NEO4J_EMBEDDING_DIMENSIONS: "<n>"              # only if stored vectors use a reduced dimension
```

**Step 3 — Redeploy, wiring the key from Secret Manager.**

```
gcloud run deploy <INSTANCE_NAME> \
  --service-account=mcp-server-sa@<PROJECT_ID>.iam.gserviceaccount.com \
  --allow-unauthenticated \
  --region=<LOCATION> \
  --image=docker.io/mcp/neo4j:latest \
  --env-vars-file=env.yaml \
  --set-secrets="NEO4J_EMBEDDING_API_KEY=neo4j-embedding-api-key:latest" \
  --min-instances=1 --max-instances=1 \
  --port=80
```

**Note on the docker image:** If vector search isn’t yet in the official *docker.io/mcp/neo4j:latest* image, build a custom image from the feature branch first, then reference it in the deploy command. Once it ships in the official image, you can drop back to *docker.io/mcp/neo4j:latest*.

## Connect Copilot Studio to the MCP server

In Copilot Studio: Tools → Add a tool → New tool → Model Context Protocol

Set the **Server URL** to *https://<service>.run.app/mcp*, then configure OAuth 2.0 authentication:

Field | Value |
|---|---|
| Authentication type | OAuth 2.0 — Manual |
| Client ID / secret | From the MS Copilot Okta app |
| Authorization URL | https://<your-okta-domain>/oauth2/default/v1/authorize |
| Token URL | https://<your-okta-domain>/oauth2/default/v1/token |
| Refresh URL | https://<your-okta-domain>/oauth2/default/v1/token |
| Scopes | openid profile email offline_access |

Hit **Create**. Copilot Studio generates a redirect URL — copy it and add it to the MS Copilot Okta app’s sign-in redirect URIs. Then create a new connection and sign in through the Okta popup using your Neo4j SSO credentials.

Once connected, confirm get-schema, read-cypher, and write-cypher appear in the tool list and test a query in the Test chat panel.

### Re-syncing after enabling vector search

If you add vector search to an already-running integration, Copilot won’t pick it up automatically. The server registers vector-search on the first initialize of a new connection, and Copilot imports the tool list at configuration time.

After redeploying with the embedding configuration, open the MCP tool connection in Copilot Studio and refresh/re-sync it (or remove and re-add it) to force a fresh tools/list. Confirm vector-search now appears alongside the other three tools.

If it doesn’t appear, the cause is almost always one of two things: the embedding environment variables aren’t set (the tool stays hidden by design when they’re absent), or Copilot is serving a cached tool list.

## Verify the integration

Work through these in order — each step validates a different layer of the chain.

**1. Decode the access token.** Use jwt.io or a device-flow helper script. Confirm:

- iss =
*https://<your-okta-domain>/oauth2/default* - aud =
*api://default* - exp is in the future
- email claim is present
- groups claim is present and contains at least one neo4j-* group

**2. List the tools.** A tools/list call succeeds without touching the database. If this fails, the problem is in your Cloud Run deployment or MCP server configuration.

**3. Run ****get-schema****.** This is the call that proves end-to-end auth into Neo4j. If it succeeds, the full chain works.

```
curl -s "$MCP_URL" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"get-schema"}}'
```

**4. Test vector search (if enabled).** Ask a semantic question in the agent test pane. The agent should call vector-search with a query, optional indexName, topK, and filters. If you have exactly one vector index, indexName can be omitted. Results come back as nodes with similarity scores, ordered by score.

```
{
  "query": "quarterly revenue",
  "topK": 5,
  "filters": [{ "property": "year", "operator": ">=", "value": 2024 }]
}
```

**5. Test agent from Copilot Studio.** You can also test your agent in Copilot Studio to verify the tools are enabled and check the query results. Below, we asked “Is there someone who doesn’t have pets with an account?” The response shows the tools the agent called, the query output, the vector similarity score (if applicable), and a summary of the results.

### Wrapping Up

Passing identity into the graph by presenting each user’s Okta-issued token to Neo4j, changes what the agent can query. Every Cypher query still runs against the full graph, but RBAC scopes the result to the roles that identity carries. The agent effectively queries a different subgraph for every user, defined by their permissions instead of by application code.

That’s what makes this pattern durable as agents get more capable. You don’t have to predict every query an LLM might generate or patch access control into a prompt. You define roles once in Neo4j, map Okta groups to them, and every future query respects that boundary automatically.

*Ready to build it? Start with the **Neo4j MCP server docs** or dig into the **source on GitHub**.*

## References

[Neo4j MCP — Authentication (Bearer Token)](https://neo4j.com/docs/mcp/current/authentication/#_bearer_token_authentication)[Neo4j MCP — Configuration](https://neo4j.com/docs/mcp/current/configuration/)[Neo4j Aura — Single Sign-On](https://neo4j.com/docs/aura/security/single-sign-on/)[Neo4j Operations Manual — SSO integration](https://neo4j.com/docs/operations-manual/current/authentication-authorization/sso-integration/)[Microsoft Copilot Studio — Connect to an existing MCP server](https://learn.microsoft.com/en-us/microsoft-copilot-studio/mcp-add-existing-server-to-agent)[Cloud Run — Allowing public access](https://docs.cloud.google.com/run/docs/authenticating/public)[Neo4j GenAI plugin — ai.text.embed](https://neo4j.com/docs/cypher-manual/current/genai-integrations/)[GCP Secret Manager — Using secrets with Cloud Run](https://docs.cloud.google.com/run/docs/configuring/services/secrets)
