Skip to content

jwt-claims as arguments example #50

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Dec 5, 2024
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;