Skip to content

Commit

Permalink
jwt-claims as arguments example (#50)
Browse files Browse the repository at this point in the history
* jwt-claims as arguments example

* cleanup

* cleanup

* cleanup

* cleanup

* wip - JWT claims with explicit SQL predicate

* chore: quick overview

* remove dups

* chore: cleanup

* chore: add testing
  • Loading branch information
ddebrunner authored Dec 5, 2024
1 parent 35ed095 commit c844a26
Show file tree
Hide file tree
Showing 8 changed files with 359 additions and 0 deletions.
37 changes: 37 additions & 0 deletions protection/jwt-claims-dbquery/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Pulling field arguments from JWT claims

Uses a SQL predicate to limit customer rows returned from a database
to those matching the regions defined in a JWT claim.

# Try it Out

Run the [sample operations](operations.graphql):

JWT with `regions: IN`.

```
stepzen request -f operations.graphql --operation-name=Customers \
--header "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1IiwicmVnaW9ucyI6WyJJTiJdfQ.hDi3-qaIOSFKzlFvaXwSh0trXC3vjiOehSKE0OxgOdE"
```

JWT with `regions: IN, UK`.

```
stepzen request -f operations.graphql --operation-name=Customers \
--header "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1IiwicmVnaW9ucyI6WyJJTiIsIlVLIl19.CRD85IIMMwjaFebtQ_p3AjSoUM6KtH4gvjcfLQfdmjw"
```

JWT with `regions: US, UK`.

```
stepzen request -f operations.graphql --operation-name=Customers \
--header "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1IiwicmVnaW9ucyI6WyJVUyIsIlVLIl19.pf0-A6TN_hT-ldCvsZyqYGv4Twjm9s6wO1aatCjK9Aw"
```

JWT with `regions: US, UK` and user supplied filter

```
stepzen request -f operations.graphql --operation-name=Customers \
--var f='{"city": {"eq":"London"}}' \
--header "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1IiwicmVnaW9ucyI6WyJVUyIsIlVLIl19.pf0-A6TN_hT-ldCvsZyqYGv4Twjm9s6wO1aatCjK9Aw"
```
21 changes: 21 additions & 0 deletions protection/jwt-claims-dbquery/config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
deployment:
identity:
keys:
- algorithm: HS256
key: development-only
access:
policies:
- type: Query
policyDefault:
condition: false
rules:
- name: "jwt-control"
fields: [customers]
condition: "?$jwt"
- name: "introspection"
fields: [__schema, __type]
condition: true
configurationset:
- configuration:
name: postgresql_config
uri: postgresql://postgresql.introspection.stepzen.net/introspection?user=testUserIntrospection&password=HurricaneStartingSample1934
14 changes: 14 additions & 0 deletions protection/jwt-claims-dbquery/index.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
schema
@sdl(
files: ["paging.graphql"]
# visibilty controls how fields included through files in this directive
# are visible outside the scope of this directive to GraphQL introspection
# and field references through @materializer etc.
#
# types and fields are regular expressions that match type and field names.
# Like field access rules if aat least one visibility pattern is present then by default
# root operation type (Query, Mutation, Subscription) fields are not exposed.
visibility: [{ expose: true, types: "Query", fields: ".*" }]
) {
query: Query
}
8 changes: 8 additions & 0 deletions protection/jwt-claims-dbquery/operations.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
query Customers($f: CustomerFilter) {
customers(filter: $f) {
id
name
city
region
}
}
101 changes: 101 additions & 0 deletions protection/jwt-claims-dbquery/paging.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
type Customer {
id: ID!
name: String
email: String
street: String
city: String
region: String
}

"""
`CustomerConnection` is the connection type for `Customer` pagination.
"""
type CustomerConnection {
edges: [CustomerEdge]
pageInfo: PageInfo!
}

"""
`CustomerEdge` provides access to the node and its cursor.
"""
type CustomerEdge {
node: Customer
cursor: String
}

input StringFilter {
eq: String
ne: String
}

input CustomerFilter {
name: StringFilter
email: StringFilter
city: StringFilter
}

type _RegionsList {
regions: [String]!
}

extend type Query {
# customers is the exposed field that limits the caller to regions
# based upon the regions claim in the request's JWT.
customers(first: Int! = 10, filter: CustomerFilter): [Customer]
@sequence(
steps: [
{ query: "_regions" }
{
query: "_customers_flatten"
arguments: [
{ name: "first", argument: "first" }
{ name: "filter", argument: "filter" }
]
}
]
)

# extracts the regions visible to the request from the JWT.
_regions: _RegionsList
@value(
script: {
src: """
{"regions": `$jwt`.regions }
"""
language: JSONATA
}
)

# this flattens the customer connection pagination structure
# into a simple list of Customer objects.
# This is needed as @sequence is not supported for connection types.
_customers_flatten(
first: Int! = 10
filter: CustomerFilter
regions: [String]!
): [Customer] @materializer(query: "_customers { edges { node }}")

# Standard paginated field for a customers table in a database.
# Additional regions argument that is used to limit customer
# visibility based upon the 'regions' claim in a JWT.
# The regions allows a list of regions and uses SQL ANY to match rows.
_customers(
first: Int! = 10
after: String! = ""
filter: CustomerFilter
regions: [String]!
): CustomerConnection
@dbquery(
type: "postgresql"
schema: "public"
query: """
SELECT C.id, C.name, C.email, A.street, A.city, A.countryregion AS region
FROM customer C, address A, customeraddress CA
WHERE
CA.customerid = C.id AND
CA.addressid = A.id AND
A.countryregion = ANY(CAST($1 AS VARCHAR ARRAY))
"""
configuration: "postgresql_config"
)
}
3 changes: 3 additions & 0 deletions protection/jwt-claims-dbquery/stepzen.config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"endpoint": "api/miscellaneous"
}
171 changes: 171 additions & 0 deletions protection/jwt-claims-dbquery/tests/Test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
const fs = require("fs");
const path = require("node:path");
const {
deployAndRun,
runtests,
GQLHeaders,
endpoint,
getTestDescription,
} = require("../../../tests/gqltest.js");

testDescription = getTestDescription("snippets", __dirname);

const requestsFile = path.join(path.dirname(__dirname), "operations.graphql");
const requests = fs.readFileSync(requestsFile, "utf8").toString();

describe(testDescription, function () {
// just deploy
deployAndRun(__dirname, [], undefined);

// and then run with various JWTs
runtests(
"regions-in",
endpoint,
new GQLHeaders().withToken(
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1IiwicmVnaW9ucyI6WyJJTiJdfQ.hDi3-qaIOSFKzlFvaXwSh0trXC3vjiOehSKE0OxgOdE"
),
[
{
label: "customers",
query: requests,
operationName: "Customers",
expected: {
customers: [
{
id: "10",
name: "Salma Khan ",
city: "Delhi ",
region: "IN ",
},
],
},
},
]
);
runtests(
"regions-in-uk",
endpoint,
new GQLHeaders().withToken(
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1IiwicmVnaW9ucyI6WyJJTiIsIlVLIl19.CRD85IIMMwjaFebtQ_p3AjSoUM6KtH4gvjcfLQfdmjw"
),
[
{
label: "customers",
query: requests,
operationName: "Customers",
expected: {
customers: [
{
id: "3",
name: "Salim Ali ",
city: "London ",
region: "UK ",
},
{
id: "4",
name: "Jane Xiu ",
city: "Edinburgh ",
region: "UK ",
},
{
id: "10",
name: "Salma Khan ",
city: "Delhi ",
region: "IN ",
},
],
},
},
]
);
runtests(
"regions-us-uk",
endpoint,
new GQLHeaders().withToken(
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1IiwicmVnaW9ucyI6WyJVUyIsIlVLIl19.pf0-A6TN_hT-ldCvsZyqYGv4Twjm9s6wO1aatCjK9Aw"
),
[
{
label: "customers",
query: requests,
operationName: "Customers",
expected: {
customers: [
{
id: "1",
name: "Lucas Bill ",
city: "Boston ",
region: "US ",
},
{
id: "2",
name: "Mandy Jones ",
city: "Round Rock ",
region: "US ",
},
{
id: "3",
name: "Salim Ali ",
city: "London ",
region: "UK ",
},
{
id: "4",
name: "Jane Xiu ",
city: "Edinburgh ",
region: "UK ",
},
{
id: "5",
name: "John Doe ",
city: "Miami ",
region: "US ",
},
{
id: "6",
name: "Jane Smith ",
city: "San Francisco ",
region: "US ",
},
{
id: "7",
name: "Sandeep Bhushan ",
city: "New York ",
region: "US ",
},
{
id: "8",
name: "George Han ",
city: "Seattle ",
region: "US ",
},
{
id: "9",
name: "Asha Kumari ",
city: "Chicago ",
region: "US ",
},
],
},
},
{
label: "customers-filter",
query: requests,
operationName: "Customers",
variables: {
f: { city: { eq: "London" } },
},
expected: {
customers: [
{
id: "3",
name: "Salim Ali ",
city: "London ",
region: "UK ",
},
],
},
},
]
);
});
4 changes: 4 additions & 0 deletions tests/gqltest.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const path = require("node:path");

const {
runtests,
GQLHeaders,
} = require('gqltest/packages/gqltest/gqltest.js');

const stepzen = require("gqltest/packages/gqltest/stepzen.js");
Expand Down Expand Up @@ -59,5 +60,8 @@ function getTestDescription(testRoot, fullDirName) {

exports.deployAndRun = deployAndRun;
exports.getTestDescription = getTestDescription;
exports.endpoint = endpoint;

exports.GQLHeaders = GQLHeaders;
exports.runtests = runtests;
exports.stepzen = stepzen;

0 comments on commit c844a26

Please sign in to comment.