cypress/integration/oauth2/grant_jwtbearer.js
// Copyright © 2022 Ory Corp
// SPDX-License-Identifier: Apache-2.0
import {
createClient,
createGrant,
deleteClients,
deleteGrants,
prng,
} from "../../helpers"
const dayjs = require("dayjs")
const isBetween = require("dayjs/plugin/isBetween")
const utc = require("dayjs/plugin/utc")
dayjs.extend(utc)
dayjs.extend(isBetween)
const jwt = require("jsonwebtoken")
let testPublicJwk
let testPrivatePem
let invalidtestPrivatePem
const initTestKeyPairs = async () => {
const algorithm = {
name: "RSASSA-PKCS1-v1_5",
modulusLength: 2048,
publicExponent: new Uint8Array([1, 0, 1]),
hash: "SHA-256",
}
const keys = await crypto.subtle.generateKey(algorithm, true, [
"sign",
"verify",
])
// public key to jwk
const publicJwk = await crypto.subtle.exportKey("jwk", keys.publicKey)
publicJwk.kid = "token-service-key"
// private key to pem
const exportedPK = await crypto.subtle.exportKey("pkcs8", keys.privateKey)
const exportedAsBase64 = Buffer.from(exportedPK).toString("base64")
const privatePem = `-----BEGIN PRIVATE KEY-----\n${exportedAsBase64}\n-----END PRIVATE KEY-----`
// create another private key to test invalid signatures
const invalidKeys = await crypto.subtle.generateKey(algorithm, true, [
"sign",
"verify",
])
const invalidPK = await crypto.subtle.exportKey(
"pkcs8",
invalidKeys.privateKey,
)
const invalidAsBase64 = Buffer.from(invalidPK).toString("base64")
const invalidPrivatePem = `-----BEGIN PRIVATE KEY-----\n${invalidAsBase64}\n-----END PRIVATE KEY-----`
testPublicJwk = publicJwk
testPrivatePem = privatePem
invalidtestPrivatePem = invalidPrivatePem
}
const accessTokenStrategies = ["opaque", "jwt"]
accessTokenStrategies.forEach((accessTokenStrategy) => {
describe("access_token_strategy=" + accessTokenStrategy, function () {
describe("The OAuth 2.0 JWT Bearer (RFC 7523) Grant", function () {
beforeEach(() => {
deleteGrants()
deleteClients()
})
before(() => {
return cy.wrap(initTestKeyPairs())
})
const tokenUrl = `${Cypress.env("public_url")}/oauth2/token`
const nc = () => ({
client_secret: prng(),
scope: "foo openid offline_access",
grant_types: ["urn:ietf:params:oauth:grant-type:jwt-bearer"],
token_endpoint_auth_method: "client_secret_post",
response_types: ["token"],
access_token_strategy: accessTokenStrategy,
})
const gr = (subject) => ({
issuer: prng(),
subject: subject,
allow_any_subject: subject === "",
scope: ["foo", "openid", "offline_access"],
jwk: testPublicJwk,
expires_at: dayjs()
.utc()
.add(1, "year")
.set("millisecond", 0)
.toISOString(),
})
const jwtAssertion = (grant, override) => {
const assert = {
jti: prng(),
iss: grant.issuer,
sub: grant.subject,
aud: tokenUrl,
exp: dayjs().utc().add(2, "minute").set("millisecond", 0).unix(),
iat: dayjs().utc().subtract(2, "minute").set("millisecond", 0).unix(),
}
return { ...assert, ...override }
}
it("should return an Access Token when given client credentials and a signed JWT assertion", function () {
createClient(nc()).then((client) => {
const grant = gr(prng())
createGrant(grant)
const assertion = jwt.sign(jwtAssertion(grant), testPrivatePem, {
algorithm: "RS256",
})
cy.request({
method: "POST",
url: tokenUrl,
form: true,
body: {
grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
assertion: assertion,
scope: client.scope,
client_secret: client.client_secret,
client_id: client.client_id,
},
})
.its("body")
.then((body) => {
const { access_token, expires_in, scope, token_type } = body
expect(access_token).to.not.be.empty
expect(expires_in).to.not.be.undefined
expect(scope).to.not.be.empty
expect(token_type).to.not.be.empty
})
})
})
it("should return an Error (400) when not given client credentials", function () {
createClient(nc()).then((client) => {
const grant = gr(prng())
createGrant(grant)
const assertion = jwt.sign(jwtAssertion(grant), testPrivatePem, {
algorithm: "RS256",
})
cy.request({
method: "POST",
url: tokenUrl,
form: true,
body: {
grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
assertion: assertion,
scope: client.scope,
},
failOnStatusCode: false,
})
.its("status")
.then((status) => {
expect(status).to.be.equal(400)
})
})
})
it("should return an Error (400) when given client credentials and a JWT assertion without a jti", function () {
createClient(nc()).then((client) => {
const grant = gr(prng())
createGrant(grant)
var ja = jwtAssertion(grant)
delete ja["jti"]
const assertion = jwt.sign(ja, testPrivatePem, { algorithm: "RS256" })
// first token request should work fine
cy.request({
method: "POST",
url: tokenUrl,
form: true,
body: {
grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
assertion: assertion,
scope: client.scope,
client_secret: client.client_secret,
client_id: client.client_id,
},
failOnStatusCode: false,
})
.its("status")
.then((status) => {
expect(status).to.be.equal(400)
})
})
})
it("should return an Error (400) when given client credentials and a JWT assertion with a duplicated jti", function () {
createClient(nc()).then((client) => {
const grant = gr(prng())
createGrant(grant)
const jwt1 = jwtAssertion(grant)
const assertion1 = jwt.sign(jwt1, testPrivatePem, {
algorithm: "RS256",
})
// first token request should work fine
cy.request({
method: "POST",
url: tokenUrl,
form: true,
body: {
grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
assertion: assertion1,
scope: client.scope,
client_secret: client.client_secret,
client_id: client.client_id,
},
})
.its("body")
.then((body) => {
const { access_token, expires_in, scope, token_type } = body
expect(access_token).to.not.be.empty
expect(expires_in).to.not.be.undefined
expect(scope).to.not.be.empty
expect(token_type).to.not.be.empty
})
const assertion2 = jwt.sign(
jwtAssertion(grant, { jti: jwt1["jti"] }),
testPrivatePem,
{ algorithm: "RS256" },
)
// the second should fail
cy.request({
method: "POST",
url: tokenUrl,
form: true,
body: {
grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
assertion: assertion2,
scope: client.scope,
client_secret: client.client_secret,
client_id: client.client_id,
},
failOnStatusCode: false,
})
.its("status")
.then((status) => {
expect(status).to.be.equal(400)
})
})
})
it("should return an Error (400) when given client credentials and a JWT assertion without an iat", function () {
createClient(nc()).then((client) => {
const grant = gr(prng())
createGrant(grant)
var ja = jwtAssertion(grant)
delete ja["iat"]
const assertion = jwt.sign(ja, testPrivatePem, {
algorithm: "RS256",
noTimestamp: true,
})
// first token request should work fine
cy.request({
method: "POST",
url: tokenUrl,
form: true,
body: {
grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
assertion: assertion,
scope: client.scope,
client_secret: client.client_secret,
client_id: client.client_id,
},
failOnStatusCode: false,
})
.its("status")
.then((status) => {
expect(status).to.be.equal(400)
})
})
})
it("should return an Error (400) when given client credentials and a JWT assertion with an invalid signature", function () {
createClient(nc()).then((client) => {
const grant = gr(prng())
createGrant(grant)
const assertion = jwt.sign(
jwtAssertion(grant),
invalidtestPrivatePem,
{
algorithm: "RS256",
},
)
cy.request({
method: "POST",
url: tokenUrl,
form: true,
body: {
grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
assertion: assertion,
scope: client.scope,
client_secret: client.client_secret,
client_id: client.client_id,
},
failOnStatusCode: false,
})
.its("status")
.then((status) => {
expect(status).to.be.equal(400)
})
})
})
it("should return an Error (400) when given client credentials and a JWT assertion with an invalid subject", function () {
createClient(nc()).then((client) => {
const grant = gr(prng())
createGrant(grant)
const assertion = jwt.sign(
jwtAssertion(grant, { sub: "invalid_subject" }),
testPrivatePem,
{ algorithm: "RS256" },
)
cy.request({
method: "POST",
url: tokenUrl,
form: true,
body: {
grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
assertion: assertion,
scope: client.scope,
client_secret: client.client_secret,
client_id: client.client_id,
},
failOnStatusCode: false,
})
.its("status")
.then((status) => {
expect(status).to.be.equal(400)
})
})
})
it("should return an Access Token when given client credentials and a JWT assertion with any subject", function () {
createClient(nc()).then((client) => {
const grant = gr("") // allow any subject
createGrant(grant)
const assertion = jwt.sign(
jwtAssertion(grant, { sub: "any-subject-is-valid" }),
testPrivatePem,
{
algorithm: "RS256",
},
)
cy.request({
method: "POST",
url: tokenUrl,
form: true,
body: {
grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
assertion: assertion,
scope: client.scope,
client_secret: client.client_secret,
client_id: client.client_id,
},
})
.its("body")
.then((body) => {
const { access_token, expires_in, scope, token_type } = body
expect(access_token).to.not.be.empty
expect(expires_in).to.not.be.undefined
expect(scope).to.not.be.empty
expect(token_type).to.not.be.empty
})
})
})
it("should return an Error (400) when given client credentials and a JWT assertion with an invalid issuer", function () {
createClient(nc()).then((client) => {
const grant = gr(prng())
createGrant(grant)
const assertion = jwt.sign(
jwtAssertion(grant, { iss: "invalid_issuer" }),
testPrivatePem,
{ algorithm: "RS256" },
)
cy.request({
method: "POST",
url: tokenUrl,
form: true,
body: {
grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
assertion: assertion,
scope: client.scope,
client_secret: client.client_secret,
client_id: client.client_id,
},
failOnStatusCode: false,
})
.its("status")
.then((status) => {
expect(status).to.be.equal(400)
})
})
})
it("should return an Error (400) when given client credentials and a JWT assertion with an invalid audience", function () {
createClient(nc()).then((client) => {
const grant = gr(prng())
createGrant(grant)
const assertion = jwt.sign(
jwtAssertion(grant, { aud: "invalid_audience" }),
testPrivatePem,
{ algorithm: "RS256" },
)
cy.request({
method: "POST",
url: tokenUrl,
form: true,
body: {
grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
assertion: assertion,
scope: client.scope,
client_secret: client.client_secret,
client_id: client.client_id,
},
failOnStatusCode: false,
})
.its("status")
.then((status) => {
expect(status).to.be.equal(400)
})
})
})
it("should return an Error (400) when given client credentials and a JWT assertion with an expired date", function () {
createClient(nc()).then((client) => {
const grant = gr(prng())
createGrant(grant)
const assertion = jwt.sign(
jwtAssertion(grant, {
exp: dayjs()
.utc()
.subtract(1, "minute")
.set("millisecond", 0)
.unix(),
}),
testPrivatePem,
{ algorithm: "RS256" },
)
cy.request({
method: "POST",
url: tokenUrl,
form: true,
body: {
grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
assertion: assertion,
scope: client.scope,
client_secret: client.client_secret,
client_id: client.client_id,
},
failOnStatusCode: false,
})
.its("status")
.then((status) => {
expect(status).to.be.equal(400)
})
})
})
it("should return an Error (400) when given client credentials and a JWT assertion with a nbf that is still not valid", function () {
createClient(nc()).then((client) => {
const grant = gr(prng())
createGrant(grant)
const assertion = jwt.sign(
jwtAssertion(grant, {
nbf: dayjs().utc().add(1, "minute").set("millisecond", 0).unix(),
}),
testPrivatePem,
{ algorithm: "RS256" },
)
cy.request({
method: "POST",
url: tokenUrl,
form: true,
body: {
grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
assertion: assertion,
scope: client.scope,
client_secret: client.client_secret,
client_id: client.client_id,
},
failOnStatusCode: false,
})
.its("status")
.then((status) => {
expect(status).to.be.equal(400)
})
})
})
it("should return an Access Token when given client credentials and a JWT assertion with a nbf that is valid", function () {
createClient(nc()).then((client) => {
const grant = gr(prng())
createGrant(grant)
const assertion = jwt.sign(
jwtAssertion(grant, {
nbf: dayjs()
.utc()
.subtract(1, "minute")
.set("millisecond", 0)
.unix(),
}),
testPrivatePem,
{ algorithm: "RS256" },
)
cy.request({
method: "POST",
url: tokenUrl,
form: true,
body: {
grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
assertion: assertion,
scope: client.scope,
client_secret: client.client_secret,
client_id: client.client_id,
},
})
.its("body")
.then((body) => {
const { access_token, expires_in, scope, token_type } = body
expect(access_token).to.not.be.empty
expect(expires_in).to.not.be.undefined
expect(scope).to.not.be.empty
expect(token_type).to.not.be.empty
})
})
})
})
})
})